Skip to content

feat(bin): close answered captain holds with a terminal disposition - #11

Merged
morris2spears merged 2 commits into
mainfrom
fm/firstmate-terminal-decision-dispositions
Aug 10, 2026
Merged

feat(bin): close answered captain holds with a terminal disposition#11
morris2spears merged 2 commits into
mainfrom
fm/firstmate-terminal-decision-dispositions

Conversation

@morris2spears

Copy link
Copy Markdown
Owner

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

  • Added a terminal subcommand to bin/fm-decision-hold.sh that closes an answered captain hold without requiring or creating a downstream task. It accepts only implemented, superseded, declined, or answered-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.
  • Taught the durability checks about the new record: verify_hold_terminally_resolved and the terminal branch in verify_hold_durable let complete/verify accept a terminalized hold, while the existing routed resolve contract is untouched - terminal refuses a hold already resolved through routed work, and vice versa.
  • Documented the two closing paths in .agents/skills/decision-hold-lifecycle/SKILL.md (policy) and docs/decision-hold-lifecycle.md (mechanism plus regression evidence), and updated the fm-decision-hold.sh row in docs/scripts.md.
  • Added a lifecycle regression case in tests/fm-decision-hold-lifecycle.test.sh covering 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 terminal subcommand plus matching policy/mechanism docs and a regression test - leaving the existing resolve routed-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 implemented closes one and prints terminal: <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 show on 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 list shows 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"}


�[1m$ fm-decision-hold.sh hold ... (register two captain decisions)�[0m
sample-historical-review-decision-already-built
sample-historical-review-decision-unresolved
complete: sample-historical-review decision inventory reviewed (already-built,unresolved)

�[1m$ bearings --json | jq .decisions_open   # BEFORE�[0m
{"id":"sample-historical-review-decision-already-built","verb":"captain-hold","title":null}
{"id":"sample-historical-review-decision-unresolved","verb":"captain-hold","title":null}

�[1m$ fm-decision-hold.sh terminal sample-historical-review already-built --disposition implemented --decision-file implemented.txt�[0m
terminal: sample-historical-review-decision-already-built (implemented)

�[1m$ same command again (idempotent retry)�[0m
terminal: sample-historical-review-decision-already-built (implemented)

�[1m$ terminal with a different disposition on the same hold (must be rejected)�[0m
fm-decision-hold: captain hold sample-historical-review-decision-already-built records a different terminal disposition
exit=1

�[1m$ terminal with an unsupported disposition (must be rejected)�[0m
fm-decision-hold: unsupported terminal disposition: ambiguous
exit=1

�[1m$ terminal with no decision file (must be rejected)�[0m
fm-decision-hold: decision file does not exist: /var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/no-mistakes-evidence/01KZPFZWRP3BB4P4PSS6PQC9C2/demo-home/missing.txt
exit=1

�[1m$ tasks-axi show <terminalized hold> --full   # preserved history�[0m
task:
  id: sample-historical-review-decision-already-built
  title: Choose the implemented sample behavior
  state: done
  blocked: no
  blocked_by: none
  held: no
  hold_reason: captain behavior pending
  hold_kind: captain
  hold_until: "-"
  kind: captain
  repo: sample
  priority: "-"
  created: "-"
  closed: 2026-08-10
  deps: none
  links: none
  body: "Terminal disposition recorded by fm-decision-hold.\nDisposition: implemented\nEvidence digest: f590fb8e787b94d5c0a6b16ad5753d35256b962c3120e69f7f15c85aee3100ea\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\"Origin: sample-historical-review\\nDecision key: already-built\\nState: awaiting captain decision.\""

�[1m$ tasks-axi show <untouched hold> --full   # genuine unresolved decision survives�[0m
task:
  id: sample-historical-review-decision-unresolved
  title: Choose the unresolved sample policy
  state: queued
  blocked: no
  blocked_by: none
  held: yes
  hold_reason: captain policy pending
  hold_kind: captain
  hold_until: "-"
  kind: captain
  repo: sample
  priority: "-"
  created: 2026-08-10
  closed: "-"
  deps: none
  links: none
  body: "Origin: sample-historical-review\nDecision key: unresolved\nState: awaiting captain decision."

�[1m$ tasks-axi list  # no fake pending implementation task was created�[0m
count: 3
tasks[3]{id,state,kind,repo,title}:
  sample-historical-review,in_flight,scout,sample,Historical sample review
  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
help[2]:
  - Run `tasks-axi show <id>` for full notes on a task
  - Run `tasks-axi ready` to see unblocked queued work

�[1m$ bearings --json | jq .decisions_open   # AFTER�[0m
{"id":"sample-historical-review-decision-unresolved","verb":"captain-hold","title":null}
- Evidence: Synthetic FM_HOME used for the manual run (backlog state after terminalization) (local file: /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.

⚠️ **Review** - 3 infos
  • ℹ️ bin/fm-decision-hold.sh:509 - In command_terminal, a trailing --disposition or --decision-file with no value causes the arg loop's final shift to fail under set -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-file returns rc=1 and prints nothing. Guard with [ &#34;$#&#34; -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 shared read_decision_evidence helper and a marker-parameterized identity/state verifier; this is non-functional and leaves the routed-work contract intact.
  • ℹ️ bin/fm-decision-hold.sh:447 - terminal explicitly rejects an already-routed hold ("captain hold X is already resolved through routed work"), but the reverse is not symmetric: resolve on 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 new terminal dispositions preserve evidence, need no fake task, remain retry-safe, and leave unresolved decisions visible case plus the pre-existing routed resolve cases)
  • 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, captured fm-bearings-snapshot.sh --json before, ran fm-decision-hold.sh terminal &lt;origin&gt; already-built --disposition implemented --decision-file ..., repeated it verbatim for idempotency, then attempted a changed disposition, an unsupported disposition (ambiguous), and a missing decision file
  • tasks-axi show &lt;hold&gt; --full on both the terminalized and the untouched hold, and tasks-axi list to prove no downstream task was invented
  • fm-bearings-snapshot.sh --json after 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.

@morris2spears
morris2spears merged commit 21088a1 into main Aug 10, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant