feat(bin): close answered captain holds with a terminal disposition - #11
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Implement the smallest safe Firstmate lifecycle repair that durably retires captain decisions answered, implemented, superseded, declined, or answered without new work, without creating fake pending implementation tasks. Extend the existing decision owner rather than adding a parallel path. Require an exact decision or evidence file, preserve the original decision and disposition evidence in completed history, stay idempotent, reject ambiguous or malformed inputs, and keep the existing routed-task resolution contract unchanged. Cover approved-but-never-linked tasks, answers after source completion, already-completed implementations, retry safety, Bearings removal of terminalized records, and continued visibility of genuine unresolved decisions. Preserve support for the captain's later terminal answers with no downstream task, update only the authoritative policy and mechanism documentation, and do not mutate private backlog records in this isolated branch.
What Changed
terminalsubcommand tobin/fm-decision-hold.shthat closes an answered captain hold without requiring or creating a downstream task. It accepts onlyimplemented,superseded,declined, oranswered-without-new-work, requires a non-empty decision/evidence file (capped at 8192 bytes), writes the disposition, evidence digest, evidence text, and the original decision title and body into the hold before marking it Done, and rejects duplicate flags, unsupported dispositions, missing files, and changed disposition or evidence on retry. Exact retries are idempotent and can finish a partial close.verify_hold_terminally_resolvedand the terminal branch inverify_hold_durableletcomplete/verifyaccept a terminalized hold, while the existing routedresolvecontract is untouched -terminalrefuses a hold already resolved through routed work, and vice versa..agents/skills/decision-hold-lifecycle/SKILL.md(policy) anddocs/decision-hold-lifecycle.md(mechanism plus regression evidence), and updated thefm-decision-hold.shrow indocs/scripts.md.tests/fm-decision-hold-lifecycle.test.shcovering evidence preservation, no invented task, retry safety, Bearings removal of the terminalized record, and continued visibility of a genuine unresolved decision. The Test gate ran that suite (10 cases) plus the documentation-audience and instruction-owner suites; Review returned 3 informational notes and no blocking findings.Risk Assessment
✅ Low: The change is purely additive - a new
terminalsubcommand plus matching policy/mechanism docs and a regression test - leaving the existingresolverouted-work contract byte-for-byte unchanged, and every intent-required behavior (exact evidence file, allowlisted dispositions, preserved original decision in Done history, idempotent exact retry, rejection of ambiguous input, Bearings removal of terminalized records with genuine unresolved decisions still visible) is present and source-verifiable.Testing
Ran the focused decision-hold lifecycle suite (all 10 cases pass, including the pre-existing routed-resolve cases, so that contract is unchanged) plus the two documentation-ownership suites touched by the prose edits, then drove the CLI by hand in a throwaway FM_HOME to capture an operator-level transcript: Bearings shows two open captain decisions,terminal ... --disposition implementedcloses one and printsterminal: <id> (implemented), an identical rerun is idempotent, and a changed disposition, an unsupported disposition, and a missing decision file are each rejected with exit 1 and a clear message.tasks-axi showon the closed hold proves the disposition, evidence digest, evidence text, and the original decision title and body are all preserved in Done history;tasks-axi listshows no fake pending implementation task appeared; the untouched hold stays queued and held, and the final Bearings JSON lists only that genuine unresolved decision. This is a shell/CLI surface with no rendered UI, so the reviewer-visible artifact is the terminal transcript rather than a screenshot.Evidence: Terminal-disposition CLI transcript (before/after Bearings, preserved history, rejections)
$ fm-decision-hold.sh terminal sample-historical-review already-built --disposition implemented --decision-file implemented.txt terminal: sample-historical-review-decision-already-built (implemented) $ same command again (idempotent retry) terminal: sample-historical-review-decision-already-built (implemented) $ terminal with a different disposition on the same hold (must be rejected) fm-decision-hold: captain hold sample-historical-review-decision-already-built records a different terminal disposition exit=1 $ terminal with an unsupported disposition (must be rejected) fm-decision-hold: unsupported terminal disposition: ambiguous exit=1 $ tasks-axi show <terminalized hold> --full state: done body: "Terminal disposition recorded by fm-decision-hold.\nDisposition: implemented\nEvidence digest: f590fb8e...\n\nDisposition evidence:\nThe selected behavior was already implemented by sample-completed-implementation.\n\nOriginal decision title:\nChoose the implemented sample behavior\n\nOriginal decision record:\n..." $ tasks-axi list # no fake pending implementation task was created sample-historical-review-decision-unresolved,queued,captain,sample,Choose the unresolved sample policy sample-historical-review-decision-already-built,done,captain,sample,Choose the implemented sample behavior $ bearings --json | jq .decisions_open # AFTER {"id":"sample-historical-review-decision-unresolved","verb":"captain-hold"}/var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/no-mistakes-evidence/01KZPFZWRP3BB4P4PSS6PQC9C2/demo-home)Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-decision-hold.sh:509- In command_terminal, a trailing--dispositionor--decision-filewith no value causes the arg loop's finalshiftto fail underset -e, so the command exits 1 with no diagnostic instead of the intended rejection message. Verified:bin/fm-decision-hold.sh terminal sample-o key --disposition implemented --decision-filereturns rc=1 and prints nothing. Guard with[ "$#" -ge 2 ]before the inner shift, or shift only when a value is present.bin/fm-decision-hold.sh:529- command_terminal re-implements logic command_resolve already owns: the decision-file validation block (lines 529-535 duplicate 428-437), verify_terminal_identity (250-268) mirrors verify_resolution_identity (230-248), and verify_hold_terminally_resolved (196-208) mirrors verify_hold_resolved (182-194). A future change to the 8192-byte cap, the digest scheme, or the identity-parse shape must be made twice. Extract a sharedread_decision_evidencehelper and a marker-parameterized identity/state verifier; this is non-functional and leaves the routed-work contract intact.bin/fm-decision-hold.sh:447-terminalexplicitly rejects an already-routed hold ("captain hold X is already resolved through routed work"), but the reverse is not symmetric:resolveon an already-terminalized hold falls through verify_hold_resolved to verify_hold_active and fails with the generic "captain hold X is not queued (state=done)". The operator cannot tell from the error that the hold was deliberately terminalized. Add a verify_hold_terminally_resolved check in command_resolve before verify_hold_active that fails with an explicit message. Message-only; does not alter the routed-task contract.✅ **Test** - passed
✅ No issues found.
bash tests/fm-decision-hold-lifecycle.test.sh(10 cases, includes the newterminal dispositions preserve evidence, need no fake task, remain retry-safe, and leave unresolved decisions visiblecase plus the pre-existing routedresolvecases)bash tests/fm-documentation-audiences.test.sh(docs/skill prose ownership, since docs/decision-hold-lifecycle.md and the SKILL.md changed)bash tests/fm-instruction-owners.test.sh(AGENTS.md/skill owner map)Manual CLI transcript in a synthetic FM_HOME: registered two captain holds,completed the inventory, capturedfm-bearings-snapshot.sh --jsonbefore, ranfm-decision-hold.sh terminal <origin> already-built --disposition implemented --decision-file ..., repeated it verbatim for idempotency, then attempted a changed disposition, an unsupported disposition (ambiguous), and a missing decision filetasks-axi show <hold> --fullon both the terminalized and the untouched hold, andtasks-axi listto prove no downstream task was inventedfm-bearings-snapshot.sh --jsonafter terminalization to confirm the retired decision disappears and the unresolved one remains✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.