Skip to content

fix(bin): resolve decision keys written anywhere on the status line - #4

Merged
Aviator-Coding merged 5 commits into
mainfrom
fm/fm-send-resolve-key-false-negative
Aug 14, 2026
Merged

Aviator-Coding merged 5 commits into
mainfrom
fm/fm-send-resolve-key-false-negative

Conversation

@Aviator-Coding

Copy link
Copy Markdown
Owner

Intent

fm-send.sh --resolve-key refuses a key that the wake drain has just displayed as open. Implement the already-diagnosed fix; do not re-derive the root cause.

The defect: bin/fm-classify-lib.sh's _fm_decision_key reads only the prefix before the first colon, so needs-decision: [key=my-slug] text... folds as default (key sits AFTER the colon) while resolved [key=my-slug]: text... folds as my-slug (key sits BEFORE the colon). Generated crewmate briefs tell workers a decision stays open until a resolved line carrying its exact key lands, so workers naturally write needs-decision: [key=slug] .... That records the decision under default, and fm-send.sh --resolve-key then correctly refuses because the ledger genuinely has no such key. The wake drain's OPEN DECISIONS section prints the raw status text, which still contains the literal [key=slug], so the operator sees the slug, uses it, and is told it does not exist. The two readers never actually disagreed - the display is misleading.

Any fix must handle a [key=...] token anywhere in the line, not just adjacent to the colon. The 2026-08-14 reproduction puts the key at the end of the line: needs-decision: review gate 3 ask-user findings [key=review-ask-user]. A status line's text can itself contain a colon before the key. Do not change the meaning of an unkeyed resolved: line: it closes default and only default.

Deliver:

  1. Accept the key in either position in _fm_decision_key - scan the whole line for the [key=...] token rather than only the prefix. This is the compatibility path: every existing brief and every already-written status log keeps working. Keep default as the fold for a line carrying no token at all.
  2. Make OPEN DECISIONS print the folded key, not just the raw text, so the operator is never shown a key the ledger does not hold. Keep the raw text too - it is what makes the entry readable - but the key the operator is told to use must be the key that will actually work.
  3. Regression tests covering both positions, plus the key-at-end-of-line shape and the no-key default case, in the existing colocated test for that library.
  4. Check whether bin/fm-send.sh and the drain genuinely share one open-key predicate. If they do not, make them.

The generated brief's status protocol wording may be worth tightening so new workers write the unambiguous form, but the parser fix must stand on its own - every status log already on disk uses the loose form and must keep working.

This is firstmate shared tracked material; firstmate-coding-guidelines apply. Delivery is no-mistakes through a PR. Do not merge.

What Changed

  • _fm_decision_key in bin/fm-classify-lib.sh now scans the whole status line for the first [key=<slug>] token instead of only the prefix before the first colon, so needs-decision: [key=slug] ... and a key at end of line fold under that slug rather than default; a line with no token still folds to default, and a malformed token on an opening verb (needs-decision/blocked) falls back to default so the escalation still surfaces instead of being dropped. The routed-work activity fold keeps its prefix-only reading via a new _fm_activity_key, and FM_OPEN_DECISIONS_FOLD_VERSION is bumped to 4 to force existing cursors to re-fold.
  • bin/fm-wake-drain.sh prints the folded ledger key (including default) on every OPEN DECISIONS item alongside the raw note, so the key the operator is shown is the key --resolve-key will accept; bin/fm-send.sh now validates through a new shared status_decision_key_is_open predicate over the same fold instead of inlining its own open-set match. Generated crewmate briefs (bin/fm-brief.sh) template the unambiguous needs-decision [key=your-slug]: form and spell out the allowed slug characters.
  • Added regression coverage for both key positions, the key-at-end-of-line shape, and the no-key default in tests/fm-watch-triage.test.sh, plus a loose-form drain-then-resolve case in tests/fm-send-resolve-key.test.sh and a folded-key printing case in tests/fm-wake-drain-open-decisions.test.sh; tests/fm-brief.test.sh follows the new brief wording.

The pipeline's Review phase left one informational note open: fm-pending-reply-lib.sh still calls _fm_decision_key directly, so for a legacy unkeyed escalation whose payload embeds a malformed token, the fold's new default fallback and that call site can disagree about which key was opened.

Risk Assessment

✅ Low: Both requested fixes are implemented exactly as specified, pinned by new colocated tests, and correctly versioned (fold version 4), leaving only one narrow legacy-compat inconsistency between the fold's malformed fallback and the pending-reply closer.

Testing

I reproduced the reported defect end-to-end against the base commit and confirmed it gone at HEAD, driving the real fm-wake-drain.sh and fm-send.sh over a stubbed tmux transport against a throwaway FM_HOME - the same harness shape the existing suite uses. The captured CLI transcripts show the operator's actual experience flipping from "drain shows the slug, fm-send refuses it" to "drain shows the folded key, fm-send accepts it and the decision closes". I also verified the on-disk upgrade path (a stale v2 cursor holding the wrong key is discarded by the fold-version bump), that an unkeyed resolved: still closes default only, and that a freshly generated brief now teaches the unambiguous keyed form. On the automated side I ran the colocated classifier-library test plus the drain, resolve-key, cursor, decision-hold, pending-reply and brief tests - every direct consumer of the changed fold - all passing with no failures or flakes. This change has no rendered UI surface; it is a shell CLI, so the CLI transcripts are the end-user surface and stand in for screenshots. The worktree is clean and all transient scratch outside the evidence directory was removed.

Evidence: BEFORE (base 48ea5d1): drain shows the key, fm-send refuses it

$ cat state/review.status # what the crewmate actually wrote working: gate 3 review under way needs-decision: review gate 3 ask-user findings [key=review-ask-user] working: kept busy on an unrelated stream $ bin/fm-wake-drain.sh # what the operator sees on wake OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line): review needs-decision: review gate 3 ask-user findings [key=review-ask-user] OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>' $ bin/fm-send.sh review --resolve-key review-ask-user 'accept the findings' error: --resolve-key 'review-ask-user': no open decision or blocker with that key in .../review.status (already closed, mistyped, or transferred). Re-check the OPEN DECISIONS listing, then resend without that key or with the right one; nothing was sent. (exit 1) $ bin/fm-wake-drain.sh # is the decision still open? OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line): review needs-decision: review gate 3 ask-user findings [key=review-ask-user]

############################################################
# BEFORE (base commit 48ea5d1) - the reported defect
############################################################

$ cat state/review.status      # what the crewmate actually wrote
working: gate 3 review under way
needs-decision: review gate 3 ask-user findings [key=review-ask-user]
working: kept busy on an unrelated stream

$ bin/fm-wake-drain.sh         # what the operator sees on wake
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
review needs-decision: review gate 3 ask-user findings [key=review-ask-user]
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

$ bin/fm-send.sh review --resolve-key review-ask-user 'accept the findings'
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
error: --resolve-key 'review-ask-user': no open decision or blocker with that key in /var/folders/yr/h20mxtv56yj1c1pt9tr1_kbc0000gn/T/tmp.PanpcMHdNN/home/state/review.status (already closed, mistyped, or transferred). Re-check the OPEN DECISIONS listing, then resend without that key or with the right one; nothing was sent.
(exit 1)

$ cat state/review.status      # ledger after the answer
working: gate 3 review under way
needs-decision: review gate 3 ask-user findings [key=review-ask-user]
working: kept busy on an unrelated stream

$ bin/fm-wake-drain.sh         # is the decision still open?
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
review needs-decision: review gate 3 ask-user findings [key=review-ask-user]
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'
Evidence: AFTER (HEAD 557c94e): drain shows the folded key, fm-send resolves it

$ cat state/review.status # what the crewmate actually wrote working: gate 3 review under way needs-decision: review gate 3 ask-user findings [key=review-ask-user] working: kept busy on an unrelated stream $ bin/fm-wake-drain.sh # what the operator sees on wake OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line): review [key=review-ask-user] needs-decision: review gate 3 ask-user findings [key=review-ask-user] OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>' $ bin/fm-send.sh review --resolve-key review-ask-user 'accept the findings' (exit 0) $ cat state/review.status # ledger after the answer working: gate 3 review under way needs-decision: review gate 3 ask-user findings [key=review-ask-user] working: kept busy on an unrelated stream resolved [key=review-ask-user]: answered: accept the findings $ bin/fm-wake-drain.sh # is the decision still open? (no output - nothing open)

############################################################
# AFTER (HEAD 557c94e) - the fix
############################################################

$ cat state/review.status      # what the crewmate actually wrote
working: gate 3 review under way
needs-decision: review gate 3 ask-user findings [key=review-ask-user]
working: kept busy on an unrelated stream

$ bin/fm-wake-drain.sh         # what the operator sees on wake
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
review [key=review-ask-user] needs-decision: review gate 3 ask-user findings [key=review-ask-user]
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

$ bin/fm-send.sh review --resolve-key review-ask-user 'accept the findings'
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
(exit 0)

$ cat state/review.status      # ledger after the answer
working: gate 3 review under way
needs-decision: review gate 3 ask-user findings [key=review-ask-user]
working: kept busy on an unrelated stream
resolved [key=review-ask-user]: answered: accept the findings

$ bin/fm-wake-drain.sh         # is the decision still open?
(no output - nothing open)
Evidence: Decision-key fold across all four key positions, base vs HEAD

### BEFORE (base 48ea5d1): only the before-colon key survives; the rest collapse into one 'default' status log: needs-decision [key=before-colon]: pick REST or RPC needs-decision: [key=after-colon] pick REST or RPC needs-decision: review gate 3 ask-user findings [key=end-of-line] needs-decision: option A: ship now vs later [key=after-second-colon] needs-decision: no token at all folded open-decision keys (what --resolve-key will accept): before-colon default ### AFTER (HEAD 557c94e): every position folds to its own slug; the tokenless line still folds to default folded open-decision keys (what --resolve-key will accept): before-colon after-colon end-of-line after-second-colon default ### An unkeyed 'resolved:' must close default and ONLY default (no meaning change) status log: needs-decision: unkeyed escalation needs-decision [key=keep-me]: keyed escalation needs-decision: end-of-line one [key=also-keep] resolved: cleared the unkeyed one still open after the unkeyed resolved: keep-me also-keep

### BEFORE (base 48ea5d1): only the before-colon key survives; the rest collapse into one 'default'
status log:
    needs-decision [key=before-colon]: pick REST or RPC
    needs-decision: [key=after-colon] pick REST or RPC
    needs-decision: review gate 3 ask-user findings [key=end-of-line]
    needs-decision: option A: ship now vs later [key=after-second-colon]
    needs-decision: no token at all
folded open-decision keys (what --resolve-key will accept):
    before-colon
    default

### AFTER (HEAD 557c94e): every position folds to its own slug; the tokenless line still folds to default
status log:
    needs-decision [key=before-colon]: pick REST or RPC
    needs-decision: [key=after-colon] pick REST or RPC
    needs-decision: review gate 3 ask-user findings [key=end-of-line]
    needs-decision: option A: ship now vs later [key=after-second-colon]
    needs-decision: no token at all
folded open-decision keys (what --resolve-key will accept):
    before-colon
    after-colon
    end-of-line
    after-second-colon
    default

### An unkeyed 'resolved:' must close default and ONLY default (no meaning change)
status log:
    needs-decision: unkeyed escalation
    needs-decision [key=keep-me]: keyed escalation
    needs-decision: end-of-line one [key=also-keep]
    resolved: cleared the unkeyed one
still open after the unkeyed resolved:
    keep-me
    also-keep
Evidence: Upgrade path: a stale v2 cursor holding the wrong key is re-folded

$ <old bin>/fm-wake-drain.sh # operator on the old version - writes a v2 cursor review needs-decision: review gate 3 ask-user findings [key=review-ask-user] $ cat state/.review.open-decisions-cursor # stale cursor recorded the wrong key version=2 offset=103 default needs-decision review gate 3 ask-user findings [key=review-ask-user] $ bin/fm-wake-drain.sh # after the upgrade, same state dir, stale cursor present review [key=review-ask-user] needs-decision: review gate 3 ask-user findings [key=review-ask-user] $ cat state/.review.open-decisions-cursor # cursor re-folded at the new version version=4 offset=103 review-ask-user needs-decision review gate 3 ask-user findings [key=review-ask-user]

Upgrade path: a cursor file already written by the OLD buggy fold, then the fixed drain runs.

$ <old bin>/fm-wake-drain.sh    # operator on the old version - writes a v2 cursor
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
review needs-decision: review gate 3 ask-user findings [key=review-ask-user]
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

$ cat state/.review.open-decisions-cursor   # stale cursor recorded the wrong key
version=2
offset=103
ident=16777230:4175277
default	needs-decision	review gate 3 ask-user findings [key=review-ask-user]
$ bin/fm-wake-drain.sh          # after the upgrade, same state dir, stale cursor present
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
review [key=review-ask-user] needs-decision: review gate 3 ask-user findings [key=review-ask-user]
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

$ cat state/.review.open-decisions-cursor   # cursor re-folded at the new version
version=4
offset=103
ident=16777230:4175277
review-ask-user	needs-decision	review gate 3 ask-user findings [key=review-ask-user]
Evidence: Generated crewmate brief now teaches the unambiguous keyed form

40: append needs-decision [key=your-slug]: {summary of options} and stop, replacing your-slug with a short name for this decision (letters, digits, ., _, and - only). 41: A decision or blocker you opened stays open until a resolved [key=your-slug]: line carrying that same key lands; a later done: or working: line never closes it. 42: Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append resolved: {how it cleared} yourself (same [key=...] if you opened it with one) as you resume.

40:   append `needs-decision [key=your-slug]: {summary of options}` and stop, replacing `your-slug` with a short name for this decision (letters, digits, `.`, `_`, and `-` only). Firstmate will apply the configured authority and reply with the decision.
41:   A decision or blocker you opened stays open until a `resolved [key=your-slug]:` line carrying that same key lands; a later `done:` or `working:` line never closes it, even when the answer is what started that work.
42:   Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append `resolved: {how it cleared}` yourself (same `[key=...]` if you opened it with one) as you resume.
Evidence: E2E reproduction harness (drives the real drain + fm-send against any bin/ tree)
#!/usr/bin/env bash
# End-to-end reproduction of the fm-send --resolve-key false negative, run
# against a given bin/ tree (base = broken, HEAD = fixed).
#
# Usage: e2e-repro.sh <bin-dir> <label>
# Drives the REAL bin/fm-wake-drain.sh and bin/fm-send.sh over a stubbed tmux
# transport, exactly as an operator would: read OPEN DECISIONS, copy the key it
# shows, answer with --resolve-key <key>.
set -u

BIN=$1
LABEL=$2
WORK=$(mktemp -d)
HOME_DIR="$WORK/home"
FAKEBIN="$WORK/fakebin"
mkdir -p "$HOME_DIR/state" "$FAKEBIN"

# Stub tmux so the send path can "type" into a window and reach a clean verdict.
cat > "$FAKEBIN/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "${1:-}" in
  send-keys)
    shift; literal=0
    while [ $# -gt 0 ]; do
      case "$1" in
        -t) shift 2 ;;
        -l) literal=1; shift ;;
        *) break ;;
      esac
    done
    [ "$literal" = 1 ] && printf '%s' "${1:-}" >> "$FM_SEND_LOG"
    exit 0 ;;
  display-message)
    for a in "$@"; do case "$a" in *cursor_y*) printf '1\n'; exit 0 ;; esac; done
    printf 'fakepane\n'; exit 0 ;;
  capture-pane) printf '╭────╮\n│    │\n╰────╯\n'; exit 0 ;;
  list-windows) exit 0 ;;
esac
exit 0
SH
chmod +x "$FAKEBIN/tmux"
printf '#!/usr/bin/env bash\nexit 0\n' > "$FAKEBIN/sleep"; chmod +x "$FAKEBIN/sleep"

# A real crewmate task ledger. The worker wrote the exact 2026-08-14 shape the
# generated brief leads them to write: the [key=...] token sits at END of line,
# after another colon in the note text.
printf 'window=sess:fm-review\nkind=ship\n' > "$HOME_DIR/state/review.meta"
{
  printf 'working: gate 3 review under way\n'
  printf 'needs-decision: review gate 3 ask-user findings [key=review-ask-user]\n'
  printf 'working: kept busy on an unrelated stream\n'
} > "$HOME_DIR/state/review.status"

echo "############################################################"
echo "# $LABEL"
echo "############################################################"
echo
echo "\$ cat state/review.status      # what the crewmate actually wrote"
cat "$HOME_DIR/state/review.status"
echo
echo "\$ bin/fm-wake-drain.sh         # what the operator sees on wake"
FM_STATE_OVERRIDE="$HOME_DIR/state" "$BIN/fm-wake-drain.sh" 2>/dev/null
echo
echo "\$ bin/fm-send.sh review --resolve-key review-ask-user 'accept the findings'"
set +e
env PATH="$FAKEBIN:$PATH" \
  FM_GATE_REFUSE_BYPASS=1 \
  FM_ROOT_OVERRIDE="$HOME_DIR" FM_HOME="$HOME_DIR" \
  FM_SEND_LOG="$WORK/send.log" FM_SEND_SETTLE=0 \
  "$BIN/fm-send.sh" review --resolve-key review-ask-user "accept the findings" 2>&1 \
  | grep -vE '^(guard|note|warn):'
rc=${PIPESTATUS[0]}
set -e
echo "(exit $rc)"
echo
echo "\$ cat state/review.status      # ledger after the answer"
cat "$HOME_DIR/state/review.status"
echo
echo "\$ bin/fm-wake-drain.sh         # is the decision still open?"
out=$(FM_STATE_OVERRIDE="$HOME_DIR/state" "$BIN/fm-wake-drain.sh" 2>/dev/null)
if [ -n "$out" ]; then printf '%s\n' "$out"; else echo "(no output - nothing open)"; fi
echo
rm -rf "$WORK"

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-classify-lib.sh:196 - An invalid slug inside a [key=...] token now discards the entire status line from the fold, and the whole-line scan makes that newly reachable from note prose. fm_decision_key returns 1 for any first token containing a character outside [A-Za-z0-9.-], and _fm_decision_fold_line (line 266) treats that failure as 'not a decision transition', so the line opens nothing. Concrete sequence: a worker appends needs-decision: should the brief still require [key=&lt;slug&gt;]? - before this change the prefix held no token, the line folded as default, and the drain listed it; now the token &lt;slug&gt; is invalid, the line is dropped, the decision never appears in OPEN DECISIONS, and the worker waits on an answer nobody can see. The brief change compounds this: bin/fm-brief.sh rules 6 now template needs-decision [key=&lt;slug&gt;]: {summary of options}, so a worker copying the placeholder literally also loses its escalation silently, where the previous needs-decision: {summary} template always folded to default. Consider making an opening verb (needs-decision/blocked) fall back to default when the token is malformed - so the escalation still surfaces - while keeping the drop for closing verbs (resolved/captain-held) so a malformed key cannot close anything. That choice changes fold semantics, so it needs your call.
  • ⚠️ bin/fm-classify-lib.sh:186 - The widened scan also changes the routed-work activity fold, which is not part of the stated intent and has no test coverage. _fm_status_open_activities_stream (line 563) calls the same _fm_decision_key, so a working/done/paused line whose note merely mentions a key now folds under that key instead of default. Concrete sequence: working: implementing the [key=api-shape] decision followed by done: shipped - previously both folded as default and the done closed the phase; now the phase opens under api-shape and the unkeyed done closes default instead, so api-shape\tworking\t... stays open forever in bin/fm-fleet-snapshot.sh:956's activity records, where an open phase is the evidence used to judge whether a parent event was superseded. The new tests in tests/fm-watch-triage.test.sh only exercise the decision fold (keys.status); activity.status at line 230 still uses prefix-only keys, so this path is unpinned either way. Decide whether the whole-line scan should apply to activities as well, and pin whichever behavior you intend.
  • ℹ️ bin/fm-classify-lib.sh:193 - Informational, and an accepted consequence of the intent's required 'token anywhere, first token wins' rule: a needs-decision/blocked line that names another decision's key in prose now takes that key over in the fold. needs-decision [key=api-shape]: pick REST or RPC followed by blocked: cannot proceed until the [key=api-shape] decision is answered drops the original entry and re-adds api-shape with the blocker's note, so OPEN DECISIONS shows the blocker text rather than the original question and --resolve-key api-shape closes the merged entry. This is exactly the invariant the removed assertions in tests/fm-watch-triage.test.sh ('a key token in note prose changed the decision key') used to pin, and the intent explicitly authorizes trading it away for the compatibility path. Noting it so the tradeoff is recorded, not asking for a change.
  • ℹ️ bin/fm-send.sh:369 - Informational efficiency note on the shared-predicate refactor: status_decision_key_is_open re-runs the whole-file fold per key, so a send with N repeated --resolve-key flags now performs N full folds of the status log where the previous code computed the open set once. status_open_decisions forks a subshell per status line (bin/fm-classify-lib.sh:302), so on a long-lived log this multiplies an already fork-heavy scan. It runs once per interactive send with a small N, so it is a reasonable price for the single shared predicate the intent asked for; a set-based variant of the predicate would remove it if the cost ever shows up.

🔧 Fix: Keep activity keys prefix-only; surface malformed opening decision keys
1 info still open:

  • ℹ️ bin/fm-classify-lib.sh:299 - The new opening-verb fallback lives inside _fm_decision_fold_line, so the fold and the only other _fm_decision_key caller now disagree about which key a malformed line opened. Reachable path (narrow, legacy-compat only): fm-pending-reply-lib.sh:865 still matches a legacy unkeyed escalation blocked: &lt;payload&gt; where payload embeds request=&lt;summary&gt;; if that summary carries a malformed token (e.g. the request text quoted [key=&lt;slug&gt;]), the fold now opens the escalation under default (line 302), while fm-pending-reply-lib.sh:920 does key=$(_fm_decision_key &#34;$escalation&#34;) || key=&#39;&#39; and gets an empty key, so the open_key comparison at line 925 never matches, no resolved [key=default]: line is appended, and line 939 stamps escalation_closed_epoch unconditionally - the decision stays open in OPEN DECISIONS forever with no retry. Before this commit the same line was dropped by the fold entirely, so the two agreed (nothing opened, nothing to close). The keyed escalation form at line 996 is unaffected because its prefix token always wins and is always valid. Earliest shared boundary: expose the fold's key resolution (whole-line scan plus the opening-verb default fallback) as one helper and have fm-pending-reply-lib.sh:920 ask that instead of _fm_decision_key, so 'which key did this line open under' has a single answer; the equivalent one-line fix is falling back to default at that call site, since the closer already requires an exact note match before it appends anything.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-watch-triage.test.sh - colocated classifier-library test carrying the new key-position regression coverage (47 cases, incl. ok - classifier primitives: keyed decisions and activity phases, ...)
  • bin/fm-test-run.sh tests/fm-send-resolve-key.test.sh - incl. the new test_loose_form_key_matches_drain_and_resolves
  • bin/fm-test-run.sh tests/fm-wake-drain-open-decisions.test.sh - incl. the new test_post_colon_and_end_of_line_keys_print_the_folded_key
  • bin/fm-test-run.sh tests/fm-wake-drain-open-decisions-cursor.test.sh tests/fm-decision-hold-lifecycle.test.sh tests/fm-pending-reply.test.sh tests/fm-brief.test.sh - the other direct consumers of the changed fold and of _fm_decision_key
  • Manual E2E: drove the real bin/fm-wake-drain.sh and bin/fm-send.sh over a stubbed tmux transport against a throwaway FM_HOME, using the 2026-08-14 end-of-line key shape - run once against a git archive of the base commit's bin/ (reproduces the refusal) and once against HEAD (drain prints the folded key, send resolves, decision closes)
  • Manual upgrade check: ran the base drain to write a version=2 cursor recording the wrong default key, then ran the HEAD drain over the same state dir to confirm the FM_OPEN_DECISIONS_FOLD_VERSION=4 bump forces a re-fold to review-ask-user
  • Manual library check: status_open_decisions over a fixture with all four key positions plus a tokenless line, base vs HEAD; plus an unkeyed resolved: against one default and two keyed decisions to confirm it closes default only
  • Manual copy check: generated a real crewmate brief via bin/fm-brief.sh &lt;task&gt; demo --mode no-mistakes and read back the status-protocol lines
⚠️ **Document** - 1 info
  • ℹ️ AGENTS.md:291 - Judgment call, no edit made: the operator-facing rule "pass the key OPEN DECISIONS prints in [key=...]" is now recorded only in bin/fm-send.sh's header and enforced by the drain's output. AGENTS.md sections 8/9 tell firstmate to pass --resolve-key and explicitly delegate the contract to that header, so I did not add a prose copy there - duplicating it would create a second place to drift. If you would rather have it inline for the answering loop, AGENTS.md:291 is the one spot to add it and bin/fm-send.sh's header line should then be reduced to the pointer.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

_fm_decision_key only read the prefix before the first colon, so
needs-decision: [key=slug] ... folded as default. OPEN DECISIONS then
printed the slug from the raw note, and fm-send --resolve-key slug
correctly refused a key the ledger did not hold.

Scan the whole line for the first [key=...] token, keep default when
there is no token, and always print the folded key in OPEN DECISIONS.
fm-send now uses status_decision_key_is_open so the resolve check and
the drain listing share one fold. Bump the incremental fold version so
existing cursors rebuild under the new key grammar.
Pin both token positions, the key-at-end-of-line shape, a colon in the
note before the key, and the unkeyed default close. The drain test
checks that OPEN DECISIONS prints the folded key, and the send test
checks that --resolve-key accepts the same key the drain just listed.
Workers followed needs-decision: {summary} and then put [key=slug] in
the note. Point ship, scout, and secondmate scaffolds at the
key-before-colon form so new status lines match the documented grammar.
The parser still accepts the loose form already on disk.
@Aviator-Coding
Aviator-Coding merged commit 1a7b107 into main Aug 14, 2026
13 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