fix(bin): support keyed answer resolution - #102
Merged
Merged
Conversation
Port upstream kunchenguid/firstmate kunchenguid#2490 (keyed-answer path), adapted to this fork, together with the two smaller upstream pieces it depends on for the incident it is meant to fix: the kunchenguid#2202 colon-first decision-key parser and the unrouted-close mechanics from kunchenguid#2330 that `answer` shares. On 2026-08-30 a worker wrote `needs-decision: [key=X] ...` with the tag after the verb colon. The status fold collapsed that stated key into the shared `default` bucket, so `fm-send --resolve-key X` was refused three times while the captain's answer sat on disk. bin/fm-classify-lib.sh now honors a complete `[key=<slug>]` token at the head of the note as an equivalent position to the documented before-colon one, strips the consumed token from the note so both positions fold to byte-identical records, keeps a mid-note mention as prose, rejects a malformed stated key instead of rewriting it to `default`, and bumps the persisted fold version so stale cursors are rebuilt from byte 0. bin/fm-decision-hold.sh owns "a keyed answer closes its matching hold" as one channel-agnostic capability: `answer` closes a single actively held hold that routes no work through a shared unrouted close that keeps every guard (captain decision file, active-hold requirement, digest-based retry identity, refusal while routed work is still blocked); `answers` is the stdin intake that maps `<key>\t<answer>\t<label>` lines to `<origin>-decision-<key>` and closes each through `answer`; `bind`, `unbind`, and `binding` record a captured-answer source's origin under state/decision-bindings/. Every path records a `Resolution mode:` and older routed records remain valid. Every channel is an ordinary caller. bin/fm-send.sh --resolve-key still closes a key the status log owns exactly as before and now feeds the intake for a key that names an active captain hold on the target task, which is what lets chat answer a decision `complete` already transferred to its durable hold; a key open in neither ledger is still refused before anything is sent. bin/fm-procevent.sh pipes a bound source's captured result through `fm-procevent-<adapter>.sh answers` into the intake without naming an adapter or parsing a result, never acknowledges the capture, and drops the binding on retire. bin/fm-procevent-lavish.sh gains `answers`, which reports only rows tagged `choice` so freeform prose cannot forge a key. Not pulled in: upstream kunchenguid#2728's captain-hold migration, and kunchenguid#2330's `decline` and `repair` commands; the shared close is parameterized so they slot in later without weakening `answer`. Tests: the four upstream kunchenguid#2490 regressions (fixture adapter through the real runner, unbound source, answer guards, chat channel) join tests/fm-decision-hold-lifecycle.test.sh, the kunchenguid#2202 parser suite is added as tests/fm-classify-decision-key.test.sh, and the colon-first end-to-end case joins tests/fm-send-resolve-key.test.sh. Claude-Session: https://claude.ai/code/session_01SSAeLowy3hRkryXkiKqYvD
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
Port upstream kunchenguid/firstmate PR kunchenguid#2490 (keyed-answer path) faithfully into this fork, adapt it to the fork, and preserve OMP. Motivation: a
needs-decisionwhose[key=...]tag sat AFTER the verb colon folded underdefault, sobin/fm-send.sh --resolve-key <key>was refused three times on 2026-08-30; a keyed decision must resolve correctly via --resolve-key. Port the upstream tests too, adapted for the fork. Do NOT pull in the high-cost upstream kunchenguid#2728 captain-hold migration (explicitly out of scope). This is firstmate shared tracked material (bin/, tests/, docs/, .agents/skills/, AGENTS.md) and follows the firstmate-coding-guidelines skill: one sentence per line in tracked Markdown, plain dash, no agent co-author, bin/*.sh shellcheck-clean via bin/fm-lint.sh, tests colocated in tests/ and behavioral through executables (never asserting implementation-source bytes), one-owner rule for contracts with cross-references patched in place, bin/fm-doc-audience-check.sh passing.Acceptance criteria:
needs-decisionwith the tag after the colon (needs-decision: [key=X] ...) resolves correctly via--resolve-key Xand no longer folds underdefaultor gets refused.Decisions made while implementing, carried as accepted scope: (1) kunchenguid#2490 itself never touches the status-key parser; the tag-after-colon fold in AC2 is fixed by upstream kunchenguid#2202 ("honor a decision key stated after the verb colon"), so kunchenguid#2202 is ported too: bin/fm-classify-lib.sh accepts a complete
[key=<slug>]token at the head of the note as an equivalent position to the documented before-colon one, strips the consumed token so both positions fold to byte-identical records, treats a mid-note mention as prose, rejects a malformed stated key rather than rewriting it to default, and bumps FM_OPEN_DECISIONS_FOLD_VERSION (5 to 6) so stale cursors rebuild from byte 0; tests/fm-classify-decision-key.test.sh is added (registered in bin/fm-test-run.sh's pure-contract-unit family) plus a colon-first end-to-end case in tests/fm-send-resolve-key.test.sh. (2) Upstream kunchenguid#2490'sanswershares an unrouted close with kunchenguid#2330'sdecline, which this fork never received; only the kunchenguid#2330 mechanics thatanswerneeds are ported (load_decision, ROUTED_NONE, resolution_body withResolution mode:, body_has_resolution_record, normalized_blocked_by, tasks_blocked_by, parse_decision_only_flags, and resolve now writing through resolution_body with mode routed while older routed records stay valid);declineandrepairare deliberately NOT ported, and close_unrouted_hold stays parameterized by mode so they can slot in later without weakeninganswer. (3) bin/fm-decision-hold.sh gainsanswer, the channel-agnostic stdinanswersintake mapping<key>\t<answer>\t<label>lines to<origin>-decision-<key>and closing throughanswer, andbind/unbind/bindingstoring a captured-answer source's origin under state/decision-bindings/ (schema fm-decision-binding.v1). (4) bin/fm-send.sh --resolve-key keeps its status-log close unchanged for keys the status log owns and, for a key the status log no longer owns, checks for an active captain hold on the target task and feeds the intake; a key open in neither ledger is still refused before anything is sent; the hold feed runs from the same shared close function used by every delivery verdict (inbox, typed, busy-confirmed OMP, remote) so OMP behavior is unchanged. (5) bin/fm-procevent.sh feeds a bound source's captured result throughfm-procevent-<adapter>.sh answersinto the intake right after capture, independent of publication and acknowledgement (never acknowledges, never suppresses the check wake), and unbinds on retire; bin/fm-procevent-lavish.sh gainsanswers, byte-identical to upstream, reading only rows tagged choice. (6) The upstream kunchenguid#2490 tests are ported into tests/fm-decision-hold-lifecycle.test.sh with one fork adaptation: a local task target in this fork receives the answer as a durable steering-inbox record plus a tmux doorbell, so the chat-channel case asserts the inbox record and doorbell rather than typed text. (7) Docs patched in place, not duplicated: docs/decision-hold-lifecycle.md (mechanism, new Answer-time closure section, verification record dated 2026-09-05 with exact outputs), .agents/skills/decision-hold-lifecycle/SKILL.md, .agents/skills/process-event-sources/SKILL.md, docs/configuration.md process-to-event section, docs/verification/process-event-sources.md, docs/scripts.md, and the state/ inventory line for decision-bindings/ in AGENTS.md. Verified: tests/fm-decision-hold-lifecycle.test.sh 13 ok, tests/fm-classify-decision-key.test.sh 8 ok, tests/fm-send-resolve-key.test.sh 17 ok, tests/fm-procevent.test.sh 41 ok, tests/fm-procevent-when.test.sh 15 ok, fm-send-strict 9 ok, fm-send-secondmate-marker 9 ok, fm-fleet-snapshot-view 16 ok, fm-bearings-snapshot 45 ok, fm-brief 23 ok, fm-wake-drain-open-decisions 9 ok, bin/fm-lint.sh (ShellCheck 0.11.0 pinned) clean, bin/fm-doc-audience-check.sh ok. Two pre-existing failures unrelated to this change reproduce identically on an untouched HEAD copy: one wake-lock timing case in tests/fm-send-turn-start.test.sh and the content-landed case in tests/fm-teardown.test.sh.Firstmate-Validation-Generation: 37c09d4d4d57711ecc4cef455a2e21f5
What Changed
--resolve-keyclose active captain holds after status-ledger transfer.resolveand OMP delivery behavior.Risk Assessment
Testing
Ran the focused behavioral test set for the requested keyed-answer and OMP-preserving change; every targeted test passed, and a reviewer-visible CLI transcript was captured as evidence. No source or transient worktree artifacts were created.
Evidence: Keyed answer resolve transcript
CLI transcript showing keyed--resolve-keybehavior and parser edge cases; all listed cases reportok.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-classify-lib.sh:286-_fm_key_at_note_headrecognizes any text beginning with[key=...as a complete token and extracts up to the first], without requiring a token boundary. For inputneeds-decision: [key=alpha]beta, it incorrectly assigns keyalphaand strips the marker, rather than treating the text as prose or rejecting the malformed stated key. This can place/resolve a decision under the wrong key despite the new contract requiring a complete head token.✅ **Test** - passed
✅ No issues found.
tests/fm-classify-decision-key.test.shtests/fm-send-resolve-key.test.shtests/fm-decision-hold-lifecycle.test.shtests/fm-procevent.test.shtests/fm-procevent-when.test.shtests/fm-send-strict.test.shtests/fm-send-secondmate-marker.test.shtests/fm-fleet-snapshot-view.test.shtests/fm-bearings-snapshot.test.shtests/fm-brief.test.shtests/fm-wake-drain-open-decisions.test.sh✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.