Skip to content

fix: prevent silent keyed decision closure failures - #1880

Open
mremond wants to merge 5 commits into
kunchenguid:mainfrom
mremond:fm/status-key-closure-swallowed
Open

fix: prevent silent keyed decision closure failures#1880
mremond wants to merge 5 commits into
kunchenguid:mainfrom
mremond:fm/status-key-closure-swallowed

Conversation

@mremond

@mremond mremond commented Aug 7, 2026

Copy link
Copy Markdown

A malformed keyed status line was classified silently, so decisions could stay open forever — or close when they should not.

The bug

bin/fm-classify-lib.sh derives both the verb and the key from everything before the first colon, rather than from the documented grammar <verb> [key=<slug>]: <note>. Two shapes fail, in opposite directions:

resolved corr=abc123 [key=x]: …     → verb reads "resolved corr=abc123" → key x stays OPEN
needs-decision: [key=x] …           → key token invisible → filed under "default"

The second is the nastier one: the closing line is well-formed, so re-reading your own work reveals nothing — the defect sits in a line written earlier by someone else. And the fold prints the key token from inside the note, visually confirming a match that does not exist.

Both were observed on real tasks hours apart. Nine decisions were affected.

Why it matters more than the noise: the author believes the decision is closed while the fold keeps shouting. An alert that cries wolf on every wake gets skimmed — which is exactly when the one genuinely open decision goes unnoticed. That is the failure this fold exists to prevent.

What changed

  • Verb and key are parsed against the grammar instead of the whole prefix.
  • An out-of-grammar line is refused and reported, never classified silently.
  • A closure matching no open key is reported as an anomaly — previously it produced nothing at all, and reporting it would have exposed the second shape in one day instead of two.
  • The incremental cursor is versioned, so records written before this lands are re-read once rather than staying stuck on the old parse.
  • bin/fm-brief.sh taught the failing shape in three generated briefs (“append resolved: … then add the key”). Corrected — the token is now shown in its required position.

Deliberately unchanged

  • done [key=x]: still does not close a key. A terminal line must never cancel an open decision.
  • A legitimate activity closure (working [key=p]:resolved [key=p]:) is not an anomaly: the unmatched-close check consults both the decision fold and the activity fold before reporting.
  • status_is_captain_relevant() shares this parser; its free-text fallback is unchanged, and covered.

The obvious fix — take the first word — was rejected: prose beginning resolved the conflict by hand: would then close a key it never claimed. A wrongly closed decision disappears with no review, which is worse than the noise.

Verification

Both directions are tested: a legitimate token-bearing closure must close; prose beginning with a verb word must not. Plus post-colon keys, unmatched closures, the done non-regression, and cursor replay bounded to snapshotted bytes.

Suites: fm-wake-drain-open-decisions, fm-wake-drain-open-decisions-cursor, fm-watch-triage, fm-brief. Drain output captured twice over crafted incident logs to confirm each anomaly is reported exactly once.

Pipeline

Updates from git push no-mistakes

intent ✅ · rebase ✅ · review ✅ (3 findings, auto-fixed) · test ✅ · document ✅ · lint ✅ · push ✅

mremond added 5 commits August 7, 2026 10:56
A keyed decision closure was classified in silence whenever its status
line drifted off the documented "<verb> [key=<slug>]: <note>" grammar,
because both parsers read the whole raw prefix before the colon.

Two forms failed, both silently, on a safety surface:

- An extra token before the key made the verb unreadable, so
  "resolved corr=<id> [key=k]: ..." parsed its verb as
  "resolved corr=<id>" and never closed k. This is the exact shape
  fm-brief.sh tells a secondmate to write for a correlated reply, and
  the shape fm-secondmate-report.sh emits as "<verb> [corr=<id>]:".
- A key token written after the colon was invisible, so the OPENING
  line was filed under "default" while a later, perfectly written
  closure closed a key nothing had opened. "default" then stayed open
  indefinitely, and the fallback printed the key token inside the note
  text, visually confirming a match that did not exist.

An alert that cries wolf on every wake stops being read, which is the
failure this fold exists to prevent.

Introduce one grammar owner, _fm_status_parse, and have status_line_verb,
status_line_note, _fm_decision_key, the decision fold, and the activity
fold all read its verdict instead of re-deriving the line's shape. It
classifies each line as strict, misplaced-key, malformed, or freeform,
and never guesses:

- A verb followed only by structured "<name>=<value>" or
  "[<name>=<value>]" tokens is the grammar, whatever the tokens are.
- A key token after the colon is honored rather than filed under
  "default", but only when it LEADS the note; a key mentioned inside
  prose stays prose.
- Prose that merely begins with a lifecycle verb is REFUSED, never
  applied. Taking the first word alone would fix both forms above while
  letting "resolved the conflict by hand: ..." close a key it never
  claimed to close, and a wrongly closed decision disappears with no
  review at all. Two conflicting key tokens are refused for the same
  reason.
- "done [key=x]:" still does not close x: a terminal line never cancels
  an open captain decision.

Recover the signal that was previously lost entirely. The fold now
reports every refused line, every honored misplaced key, and every
closure for a key that was not open on an opt-in side channel, and
fm-wake-drain.sh prints them as a STATUS LINE ANOMALIES section beside
OPEN DECISIONS from the same single pass. An unmatched close is the
direct, same-day symptom of an opening line misfiled under another key.
Callers that only want the open set wire no sink and are unaffected.

status_is_captain_relevant's free-text fallback is unchanged: lines the
grammar refuses keep their previous whole-prefix verb read, verified by
differential over prose, legacy bare lines, and FM_CAPTAIN_RE overrides.
Token-carrying lines do change, and must: "blocked [corr=<id>]: ...",
which fm-secondmate-report.sh emits for a secondmate escalation, was not
captain-relevant at all before this, "done [corr=<id>]" was equally
invisible, and "paused [corr=<id>]" did not register as a declared wait.

Also correct the three places in the brief scaffold that told a worker
to append "resolved:" and then add the key, which is how the misplaced
token gets written in the first place.
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