Skip to content

fix(bin): recognize correlation tokens in status transitions - #1967

Open
mremond wants to merge 4 commits into
kunchenguid:mainfrom
mremond:fm/jeton-correlation-decisions
Open

fix(bin): recognize correlation tokens in status transitions#1967
mremond wants to merge 4 commits into
kunchenguid:mainfrom
mremond:fm/jeton-correlation-decisions

Conversation

@mremond

@mremond mremond commented Aug 8, 2026

Copy link
Copy Markdown

Intent

A captain-facing decision can be silently invisible. The durable decision fold in bin/fm-classify-lib.sh does not recognize a status line whose verb is followed by a correlation token, so such a line is folded as ordinary status: it neither OPENS nor CLOSES a decision. Both directions of the failure were live in the operating home.

THE DEFECT. status_line_verb stripped a trailing [key=...] from the prefix but left everything else glued to the verb, so 'resolved corr=7ab3e5dd13c9a993 [key=k]: closed' parsed as verb 'resolved corr=7ab3e5dd13c9a993'. No case arm in _fm_decision_fold_line matches that, so the line is not a transition. _fm_decision_key extracted the key correctly, which is why the two halves disagreed. bin/fm-pending-reply-lib.sh writes corr=<16 hex> by design (fm_pending_reply_corr_token) and secondmates put it on their status lines, so this is a shape firstmate's own tooling produces, not malformed input.

TWO LIVE CONSEQUENCES. Closers that do not close: six answered, shipped and merged decisions stayed on the captain's board for days. Openers that do not open, the worse half: two genuine captain decisions were the last lines of the real state/site-perso.status and had never appeared in any OPEN DECISIONS listing. A decision the captain was owed never reached him.

WHAT WAS ASKED. Make the verb reading tolerate a correlation token between the verb and the rest of the line, in BOTH directions, so an opener opens and a closer closes. Establish first, by reading the code rather than assuming, whether status_line_verb is the right place or whether a shared prefix-normalisation helper is, and whether any other consumer of status_line_verb changes behaviour when the token is stripped. Name every consumer checked and what changed for it. A fix that closes decisions but makes the away-mode daemon misclassify a wake is a worse defect than the one it repairs.

THE TRAP, AND IT IS THE WHOLE RISK OF THIS TASK. Do NOT loosen the verb parse into prose matching. The parse is deliberately strict so free-text prose can never impersonate a transition; _fm_decision_key_transition_allowed exists for exactly that class of takeover. Recognise a token of the exact shape the correlation library produces - an anchored, bounded, hex-shaped corr= token - and nothing else. A line reading 'resolved the corr= issue yesterday [key=x]:' must NOT become a closing transition. It was required to decide explicitly whether an unknown foo=bar token should be skipped too, and to state the trade-off rather than settle it silently.

DECISIONS MADE WHILE DOING THE WORK, all deliberate:

  1. status_line_verb is the right place, established by reading the code. _fm_decision_key already scans the prefix for [key=...] anywhere in it and was never wrong - that asymmetry is exactly why the two halves disagreed. No shared prefix-normalisation helper was introduced because only one parser was broken.

  2. Reading the REAL status log surfaced more shapes than the report named. Four token placements exist in the wild plus one more from a helper: 'verb corr= [key=k]:' (broken), 'verb corr=:' (broken, default key), 'verb corr= corr= [key=k]:' (broken, a recovery turn re-embedding two tokens on one line), 'verb [corr=]:' (broken, written by bin/fm-secondmate-report.sh), and 'verb [key=k] corr=:' (which worked only by accident, because the old [key= strip ate the tail). All are handled now. Including the bracketed helper shape was a deliberate widening of the reported scope: it is equally broken and is written by a firstmate library, so it belongs to the same defect.

  3. STRICT ROAD TAKEN on the foo=bar question, deliberately. Only corr=<16 hex> and its bracketed form are recognised; an arbitrary name=value token is NOT skipped. Rationale stated in the code: skipping unknown tokens would let any free-text word carrying an equals sign ('resolved x=1 [key=k]: ...') reduce to a bare verb and impersonate a transition, which is the takeover the strict parse exists to prevent. The cost is one reviewed line each time a genuinely new token shape appears, and that is the intended trade - a new shape becomes a deliberate edit rather than a silent widening.

  4. Implementation shape. status_line_verb keeps its existing [key= strip and trim, then takes a fast path: a prefix with no 'corr=' substring is returned byte-for-byte as before, which is the no-regression guarantee for every untokened line including odd interior spacing. Only when 'corr=' is present does it walk whole words, dropping ONLY recognised tokens and keeping everything else - so a multi-word prose prefix stays multi-word and matches no verb arm, preserving the strictness.

  5. The 16 hex classes are written out literally rather than built from a variable. A first implementation built the glob from a variable; that only re-globs under some shells' expansion rules (it silently fails under zsh), and a safety parse must not depend on that. This matches the existing precedent in bin/fm-secondmate-report.sh.

  6. FM_OPEN_DECISIONS_FOLD_VERSION bumped 2 -> 3. This was NOT in the original report but is load-bearing: the incremental cursor persists a folded open set, so every cursor written under the previous reading holds decisions computed while correlated lines were invisible. The operating home's live cursor is version=2, offset=, with an EMPTY open set - so without the bump the fix would ship and those two decisions would still never surface there.

EVERY CONSUMER OF status_line_verb, enumerated with its verdict, as required:

  • status_is_captain_relevant: CHANGED, correctly. Correlated done/blocked lines were invisible to it; correlated working/resolved lines leaked through the free-text fallback the nonterminal guard was supposed to stop.
  • status_is_terminal_verb: CHANGED, correctly.
  • status_is_paused and status_is_paused_or_captain_held: CHANGED, correctly - a correlated 'paused:' was being aged as a possible wedge instead of respected as a declared external wait.
  • _fm_decision_fold_line (both the whole-file and the incremental cursor-backed fold): CHANGED - the target defect.
  • _fm_status_open_activities_stream: CHANGED, same defect class; it is evidence-only and never authoritative current state.
  • bin/fm-crew-state.sh map_log_state and LOG_VERB: CHANGED - correlated lines reported 'unknown'.
  • bin/fm-supervise-daemon.sh, both sites (lines ~391 and ~1263): CHANGED TOWARD CORRECT. This is precisely the away-mode-daemon risk the report warned about. Both sites check working|resolved|captain-held to avoid clearing wedge aging on a nonterminal line; the glued verb made that guard miss, so the daemon took the terminal path and cleared wedge aging on nonterminal lines. The fix makes the guard fire as designed.
  • bin/fm-decision-hold.sh: CHANGED - a correlated done now suppresses leftover opens on a finished ordinary task.
  • bin/fm-fleet-snapshot.sh: CHANGED - reports 'working' instead of 'working corr=...' in its JSON last_event state.
  • bin/fm-pending-reply-lib.sh line 871: NO NET CHANGE - the widened verb filter is still followed by an exact whole-line match against that library's own payload.

EVIDENCE GATHERED, not assertion. All four predicate verdicts were diffed old-code-vs-new over the real 383-line state/site-perso.status: 254 lines byte-identical, and all 129 changed lines carry a valid correlation token in their prefix. No untokened line changed any verdict. Of the changed lines, 14 correlated done/blocked/needs-decision lines become captain-visible and 20 correlated working/resolved lines stop being escalated on prose alone. Before the fix the whole-file fold returned NOTHING on that log; after it, eleven genuinely open captain decisions including the two named openers.

TESTS. New tests/fm-classify-corr-token.test.sh with 10 cases: both directions through the REAL bin/fm-wake-drain.sh; every token position (before the key, after it, unkeyed, doubled, bracketed); the untokened pair to prove nothing regressed; ten impostor shapes (prose containing corr=, wrong-length, non-hex, glued prefix, unknown name=value) asserted to neither open nor close; captain-relevance and pause verdicts pinned unchanged for untokened lines and proven correct for tokened ones; the reserved pending-reply- key namespace still protected; the incremental cursor-backed fold and the whole-file fold asserted to agree AND to give the right answer at every step; a version-2 cursor proven to be discarded and refolded; and a drift pin that drives the REAL writers (fm_pending_reply_corr_token over generated ids, and bin/fm-secondmate-report.sh in both its plain and --doc modes) to prove this library's second statement of the token shape cannot drift from the library that owns the grammar. Each test was run individually against the pre-fix library: 7 fail there, and the 3 that pass are the deliberate no-regression walls.

VERIFICATION RUN. bin/fm-lint.sh clean, bin/fm-doc-audience-check.sh clean, and every affected suite green: watch-triage 46, daemon 99, crew-state 49, pending-reply 29, remote-reply 16, fleet-snapshot-view 15, wake-drain-open-decisions 9, wake-drain-open-decisions-cursor 6, decision-hold-lifecycle 9, send-resolve-key 10, afk-return 5.

OUT OF SCOPE, deliberately. Issue #1879 is a different defect in the same family (key position after the colon). It was not fixed here. It lives in _fm_decision_key's handling of a key after the colon, which this change does not touch, so it is neither easier nor harder to fix.

KNOWLEDGE PLACEMENT. Per .agents/skills/firstmate-coding-guidelines, this is library mechanics, so the knowledge went into the fm-classify-lib.sh header comment where the parsers live. No AGENTS.md edit: nothing here is needed by every session, and AGENTS.md's existing cross-reference to the cursor file remains accurate. No docs/ surface documents this grammar. The token grammar's one owner remains bin/fm-pending-reply-lib.sh; this library states only the SHAPE, with the drift pin test holding the two together, because that library sources this one and cannot be sourced back.

The real state/site-perso.status was copied into the task worktree for analysis; nothing under the operating home's state/ was written.

What Changed

  • Normalize exact corr=<16 hex> status tokens, including bracketed and repeated forms, so correlated decision openers and closers are classified correctly while token-first lines, prose, malformed tokens, and unknown name=value words remain non-transitions.
  • Apply normalized verbs across decision folding and crew-state consumers, and bump the persisted fold version so existing cursors are rebuilt with the corrected semantics.
  • Add regression coverage for token placement, strict parsing, cursor refolding, classifier behavior, wake-drain decisions, and correlation-token writer compatibility.

Risk Assessment

✅ Low: Captain, the corrected parser now preserves the leading word, resolves the prior token-first transition path, and keeps the change narrowly bounded to recognized post-verb correlation tokens.

Testing

The author-supplied baseline reported the affected suites and lint checks green; this phase independently passed the focused regression script and product-level wake-drain and away-daemon checks, after discarding and successfully retrying one invalid zsh evidence attempt under Bash.

Evidence: OPEN DECISIONS correlated-token transcript
STEP 1 — correlated opener written to the durable status log
needs-decision corr=c44897ee2db4326b [key=texte-du-mur]: propose the wall text

REAL fm-wake-drain.sh OUTPUT
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
site-perso [key=texte-du-mur] needs-decision: propose the wall text
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

STEP 2 — prose containing corr= is appended; it must not impersonate resolved
resolved the corr= issue yesterday [key=texte-du-mur]: prose must not close

REAL fm-wake-drain.sh OUTPUT
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
site-perso [key=texte-du-mur] needs-decision: propose the wall text
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

STEP 3 — exact 16-hex correlated closer is appended
resolved corr=c44897ee2db4326b [key=texte-du-mur]: captain chose the third wording

REAL fm-wake-drain.sh OUTPUT
(no OPEN DECISIONS output; the key is closed)
Evidence: Away-mode daemon correlated-working transcript
STATUS LINE SEEN BY THE REAL AWAY-MODE DAEMON FUNCTIONS
working corr=c44897ee2db4326b: stage 2 rebased onto merged #76; task dates preserved

classify_stale RESULT
self|transient stale (sess:fm-wishlist-w1): working corr=c44897ee2db4326b: stage 2 rebased onto merged #76; task dates preserved

possible-wedge aging marker: PRESERVED
immediate terminal escalation: ABSENT

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 bin/fm-classify-lib.sh:242 - Intent requires “the verb reading [to] tolerate a correlation token between the verb and the rest of the line” and says free-text prose “must never impersonate a transition,” but _fm_classify_is_corr_token &#34;$word&#34; &amp;&amp; continue removes valid tokens regardless of position. After needs-decision [key=victim]: owed, appending corr=c44897ee2db4326b resolved [key=victim]: prose now parses as resolved and silently closes victim; previously the glued prefix matched no transition. Restrict removal to tokens after the retained leading verb, and pin token-first opener/closer lines as non-transitions.

🔧 Fix: Captain, block token-first decision impersonation
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Inspected git diff 833a9a25bcf2ae522d6f93dbbd9911a6d8e7c409..ba444d8000a9fda5dc83415068f246d3db78eb56 and all status_line_verb consumers.
  • bash tests/fm-classify-corr-token.test.sh
  • Appended a valid correlated opener, prose impostor, and valid closer to an isolated status log; ran FM_STATE_OVERRIDE=… bin/fm-wake-drain.sh after each append and captured its actual output.
  • Sourced bin/fm-supervise-daemon.sh under Bash; exercised classify_stale and handle_wake with correlated working plus merged prose, asserting transient classification, preserved wedge aging, and no terminal escalation.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

mremond added 4 commits August 8, 2026 21:32
status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.
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