feat(supervision): surface open captain decisions and waits - #4
Merged
Conversation
Firstmate could stop on a decision or an external delay without the captain ever receiving a self-contained explanation of what was needed, why it mattered, or what would happen next. Work sat parked and the fleet read as permanently waiting for no understandable reason. Three separate mechanisms dropped that information. A captain decision was recorded as a one-line title plus a one-line reason, so every renderer could only truncate it - and the backlog reader truncated the reason again at its first comma. A declared external delay was excluded from the captain-relevant verb set, which correctly stops wedge-nagging an idle pane but also removed the delay from every captain-facing surface. Supervision counted state/*.meta, so a home whose only live work was an unanswered decision reported as idle and the whole guard stack went silent. Add one derived, read-only contract over state firstmate already keeps durably - the backlog and the status event logs - rather than another status surface. Every open item is a decision or a wait; a wait re-declared past a threshold without clearing becomes a decision, derived from the keyed event fold rather than from its wording. Identities carry no prose, so a delay re-reported hourly stays one item and surfaces once. Record the plain language durably: fm-decision-hold.sh hold gains briefing fields for the concrete choice, why it matters now, what waiting costs, the options, and a recommendation, stored in the hold body and cleared when the hold resolves. A hold without one renders marked as not yet written rather than dressing up a raw operational note. Surface it in one captain-facing place and in ordinary replies: fm-attention.sh renders it already translated; fm-guard.sh surfaces a changed set before any in-flight test, closing the blind spot where it used to return early; fm-turnend-guard.sh stops a turn that would end on a decision the captain has never been shown, once per distinct set and never on a routine delay; and the session-start digest and supervision block carry it. Rendering is surfacing, so an ordinary read that changes nothing is never an alarm. Every supported primary harness and runtime backend is reviewed in docs/verification/supervision.md. Each passive adapter now selects its follow-up headline from the guard's own banner, so a captain decision is no longer announced as a supervision lapse.
…sion holds Point the maintainer-architecture and decision-hold mechanism surfaces at captain-attention.md rather than restating the contract, and record the durable briefing that fm-decision-hold.sh now stores on a captain hold.
The captain inbox board that landed on main carries evidence this contract missed: selecting decisions on the snapshot's captain_actionable flag also requires the backlog item's own kind to be "captain", but the documented way to gate ordinary work on the captain is tasks-axi hold <id> --reason "<reason>" --kind captain which leaves kind as ship. The flag is therefore false for exactly the threads this contract exists for. Reproduced on the real shape of the reported case: a ship item held for the captain rendered as "0 decisions need you, 1 thing is waiting", filed under waiting on something else with "next check: when the work it is waiting on finishes" - the fleet-is-always-waiting symptom, reproduced through the new surface itself. Select on the captain hold instead, whatever the item's kind says, and keep a hold with an unresolved blocker as a wait so a future-gated hold does not nag now. captain_actionable is deliberately left alone: bearings and the secondmate summary consume it with their own meanings and tests. This is also the rule the inbox board already uses, so the two captain-facing surfaces cannot disagree about what needs the captain. Record the relationship in both directions rather than leaving them reading as rival owners: this contract owns the derived set, its plain language, and the interrupt budget; the board is where the captain answers one in a browser. Correct one rationale the parser fix in the previous commit made stale: fm-inbox-view.sh no longer needs tasks-axi because the snapshot truncates a hold reason at its first comma, but it does still need it for the length cap and for the durable briefing, which the snapshot carries no body for.
tests/fm-backend.test.sh builds a synthetic old bin/ from historical copies of the refactored entrypoints plus CURRENT copies of their siblings. Two of those siblings, fm-guard.sh and fm-decision-hold.sh, now source fm-attention-lib.sh, which was not in the sibling list, so under set -eu the source aborted, the unresolved-decision gate could not run, and the old fm-teardown.sh scout case was refused: fm-guard.sh: line 53: .../fm-attention-lib.sh: No such file or directory fm-guard.sh: line 164: FM_ATT_AVAILABLE: unbound variable REFUSED: scout task teardownconform1 has not passed the unresolved-decision completion gate. Add the dependency to OLD_BIN_UNCHANGED_SIBLINGS. Also close the selection gap that let this reach CI. bin/fm-attention-lib.sh mapped only to watcher-wake-lock, snapshot-bearings, session-bootstrap, and pure-contract-unit, so no local run ever selected the backend-dispatch family that owns this fixture. Map the attention files to backend-dispatch as well, so the suite that consumes them is selected when they change. The same fixture shape already required this dependency in tests/fm-turnend-guard.test.sh; this was the second instance.
…single focused verification across the four touched contracts now
… and readable state
… decision-hold JSON helpers to jq
…rs, knobs, and library
ItsFlow
force-pushed
the
fm/fm-meta-blindness
branch
from
August 4, 2026 21:48
e609cd4 to
5085634
Compare
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
Make every open captain decision and meaningful wait visible in one captain-facing place and in ordinary Firstmate replies, in plain language carrying the concrete choice, why it matters now, what waiting costs, real options, and a recommendation. Waits and decisions must be deduplicated, stay visible until resolved, and clear when resolved; a routine external delay must say what is awaited and when it is next checked. Integrate this with the existing primary-activity blind-spot state so unaccounted primary work appears suspicious rather than idle, keeping one authoritative contract rather than another disconnected status surface.
What Changed
bin/fm-attention.sh+bin/fm-attention-lib.sh) that derives one deduplicated, authoritative set of open captain decisions and meaningful waits and renders them in plain language — each decision carrying the concrete choice, why it matters now, what waiting costs, real options, and a recommendation; each wait naming what is awaited and when it is next checked. Items stay listed until resolved through a receipt check and clear once resolved..captain-attention-unknownand.captain-attention-decisionsmarkers so harnesses that cannot deliver assistant-reply evidence surface once rather than loop.docs/captain-attention.mdand inventory updates plus a newtests/fm-attention.test.shsuite (all supervision tests pass under the pipeline).Risk Assessment
✅ Low: All five round-1 findings are correctly and verifiably fixed — the evidence-less decision stop is now bounded by a surfaced-once marker (terminating the pre-native Grok recursion at one resume), the decision-hold helpers are ported to jq with semantics I verified by executing both filters, docs match the real behavior, and new regression coverage exercises the bounded stop — leaving only a minor AGENTS.md state-listing docs gap.
Testing
Ran the focused 9-suite regression set (all pass, no gate skips), then demonstrated the contract end-to-end with the real CLIs in temp Firstmate homes: the captain view carries the concrete choice, stakes, cost of waiting, options, and recommendation in plain language; waits show what is awaited and when it is next checked; the same set reaches session-start, guard, supervision-instructions, and the turn-end stop; receipts require actual captain-visible delivery; reworded delays deduplicate; items persist until resolved and clear on resolution; and a decision-holding home reads as not idle while a broken derivation reads as unknown. Evidence is CLI transcripts because the end-user surface is a terminal CLI; no rendered UI exists to screenshot.
Evidence: Captain view: briefed decision + external wait with next check
$ bin/fm-attention.sh CAPTAIN'S CALL 1 decision needs you. 1 thing is waiting. NEEDS YOUR DECISION 1. Sync the fork main branch with the author upstream The choice: Sync the fork main from the author upstream now, or keep it frozen and rebase the task board onto the current fork main. Why it matters now: The task board is the last thing before the live view ships and cannot be validated against a stale fork main. If this waits: The task board stays parked and nothing else will move it. Options: - Sync the fork main from upstream now, then rebase and re-validate. - Leave the fork frozen and rebase the board onto the current fork main. Recommended: Sync the fork main from upstream now; it is the smaller change and unblocks everything downstream. WAITING ON SOMETHING ELSE - waiting for the upstream fork synchronization to land before the board can be rebased Waiting for: waiting for the upstream fork synchronization to land before the board can be rebased Next check: in about 60 minutes Everything above stays listed here until it is answered or clears.Evidence: Firstmate brief and status counters
Evidence: Integrated surfaces: supervision-instructions count line, guard banner, session-start digest
Evidence: Receipt lifecycle: only captain-visible delivery spends it; items persist
Evidence: Turn-end stop on an unshown decision, released by the relayed reply
$ ... last_assistant_message:'All quiet, captain.' | bin/fm-turnend-guard.sh ● TURN WOULD END WITHOUT TELLING THE CAPTAIN - 1 DECISION(S) ARE WAITING ON HIM ● CAPTAIN'S CALL ● 1 decision needs you. 0 things are waiting. ... ● Relay each one to the captain in plain language before ending this turn: ● the concrete choice, why it matters now, what waiting costs, and your recommendation. exit=2 $ ... last_assistant_message:<the relayed CAPTAIN'S CALL> | bin/fm-turnend-guard.sh exit=0 (turn may end: the captain has actually seen the decision)Evidence: Dedup: reworded hourly delay stays one quiet item
Evidence: Resolution clears the decision and the wait
Evidence: Blind spot: decision-holding home is NOT idle while watcher-need stays false
Evidence: Broken derivation reads unknown, never all-clear
Evidence: Reproducible demo script that generated all transcripts
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-turnend-guard-grok.sh:77- Pre-native Grok path can recurse without bound: the captain-attention headlines now pierce the GROK_TURNEND_GUARD_ACTIVE one-resume latch, but on this legacy path the Stop payload provably lacks last_assistant_message (a payload carrying stopHookActive/lastAssistantMessage takes the native branch), so fm-attention.sh --record-visible can never validate a receipt and FM_ATT_DECISIONS_NEW never clears. Eachgrok --resumerun ends, fires its own Stop hook with GROK_TURNEND_GUARD_ACTIVE=1, matches the 'TURN WOULD END WITHOUT TELLING THE CAPTAIN' headline again, and spawns another nested resume — an infinite recursive loop while any captain decision is open, even one that was correctly briefed and faithfully relayed. This breaks the adapter's documented bound ('at most one forced continuation per turn - never a wedged, un-endable session'). The unknown headline is bounded by the .captain-attention-unknown marker; the decision headline has no equivalent bound here. Fix by keeping the latch authoritative on this evidence-free path (or bounding attention resumes with a marker), since the pull surfaces still keep the decision visible.bin/fm-turnend-guard.sh:195- The captain-decision turn-end stop is unbounded on any harness that cannot deliver last_assistant_message. attention_gate blocks (exit 2) whenever FM_ATT_DECISIONS_NEW is true, and unlike the watcher stop (BLOCK_BUDGET) and the unknown stop (.captain-attention-unknown marker) it has no budget or surfaced-once marker; the only clearing path is a validated receipt from the assistant message in the hook payload. docs/turnend-guard.md:63 states 'a true value still passes through the attention gate, so a continuation cannot end without a validated captain receipt', but the repo's own verification table claims assistant-reply evidence only for OpenCode, Pi, and native Grok — for Claude and Codex it only says exit 2 blocks. If those harnesses' Stop payloads omit last_assistant_message, a session with an open decision and an absent captain is forced to continue on every stop attempt (Claude bounded per attempt only by its external block override; Codex potentially unbounded), burning tokens until the decision is externally resolved. Confirm each harness actually supplies the field, or add a bounded interrupt (e.g. a decision-surfaced marker written after N blocks) for evidence-less harnesses.bin/fm-decision-hold.sh:185- fm-decision-hold.sh now hard-requires node for every subcommand (require_tasks_axi fails without it), used only by briefing_complete_body and write_updated_body_file to JSON-decode/re-encode the hold body. jq is already the repo's established JSON dependency and can decode a quoted scalar (jq -r .), filter lines, and re-emit raw text, so the same logic could run without adding a second runtime requirement to the core decision lifecycle. A home without node can no longer register, retry, complete, or resolve any captain hold. If node is a deliberate choice (e.g. exact byte-preserving writes), keep it; otherwise port the two helpers to jq.bin/fm-attention-lib.sh:753- fm_attention_status contains two nearly identical ~15-line unknown-fallback blocks (the jq/derivation failure branch at lines 753-766 and the empty-summary branch at lines 794-808), both setting the same nine FM_ATT_* fields and re-reading the unknown marker. Extracting one _fm_attention_mark_unknown helper removes the duplication and keeps the two branches from drifting.bin/fm-attention-lib.sh:836- fm_attention_home_idle is documented (here, in bin/fm-supervision-lib.sh, and in docs/captain-attention.md) as the owner of the idleness predicate, but no production code calls it — only tests/fm-attention.test.sh. The guards integrate the attention set directly instead. This is fine as a stated contract point, but note it is currently API-for-docs/tests; if a future consumer asks "is this home idle" via the old supervision counts, nothing forces it through this predicate.tests/fm-attention.test.sh:1- tests/fm-attention.test.sh is committed mode 100644 while every other tests/*.test.sh is 100755, and docs/verification/supervision.md liststests/fm-attention.test.shas a direct entry point (the runner invokes viabashso selection still works, but the documented direct invocation fails with permission denied). Set the executable bit for consistency.bin/fm-guard.sh:163- Every guarded command (fm-send, fm-spawn, fm-peek, fm-teardown, wake drain, etc.) now runs the full attention derivation (fleet-snapshot --backlog-json plus several jq passes over every status log) via fm_attention_status, and when the banner fires it derives the whole set a second time by shelling out to fm-attention.sh --no-mark. Acceptable for current fleet sizes, but the banner path could render from the already-computed FM_ATT_JSON instead of re-deriving.🔧 Fix: Bound evidence-less captain-decision stops; port decision-hold JSON helpers to jq
1 info still open:
AGENTS.md:111- The branch introduces three volatile state files, but AGENTS.md's state/ contract listing (line 111) documents only.captain-attention. The.captain-attention-unknownmarker (bounds the unknown turn-end stop) and the new.captain-attention-decisionsmarker (bounds the evidence-less decision stop, added in 45c29fd) are absent from that listing even though both are documented in docs/turnend-guard.md and docs/captain-attention.md. Adding them to the same AGENTS.md line keeps the state-file inventory complete and consistent with the repo's practice of enumerating every state file with its owner.✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh tests/fm-attention.test.sh tests/fm-turnend-guard.test.sh tests/fm-decision-hold-lifecycle.test.sh tests/fm-session-start.test.sh tests/fm-supervision-instructions.test.sh tests/fm-inbox-view.test.sh tests/fm-fleet-snapshot-view.test.sh tests/fm-pi-watch-extension.test.sh tests/fm-backend.test.sh— 9/9 pass, 0 failures, 0 gate skips (jq and tasks-axi present)Manual E2E: registered a real captain decision viabin/fm-decision-hold.sh holdwith the full briefing and a parked crewmate delay, then capturedbin/fm-attention.sh(plain-language captain view with choice/why-now/cost/options/recommendation and wait next-check),--brief, and--statusManual E2E: verified the set reaches ordinary replies viabin/fm-session-start.sh(digest section),bin/fm-guard.sh(changed-set banner), andbin/fm-supervision-instructions.sh(one-line captain's-call count)Manual E2E: receipt lifecycle viabin/fm-attention.sh --record-visible— reply without the alert rejected (exit 4), internal render cannot spend the receipt, relayed reply records it, items stay listed until resolvedManual E2E:bin/fm-turnend-guard.shblocks (exit 2) a turn ending with an unshown decision on a zero-metadata home and allows (exit 0) once the relayed alert is in the assistant replyManual E2E: dedup — rewording the hourly external delay keeps one wait andnew=false; resolution viabin/fm-decision-hold.sh resolveplus terminal work outcomes clears the view to 'Nothing needs your decision, and nothing is waiting.'Manual E2E: blind-spot — home with only a captain-gated tasks-axi hold reportsin_flight=0 needed=falsefrom fm-supervision-lib but NOT idle fromfm_attention_home_idle, and the guard still renders the decision; dangling state symlink renders unknown (exit 3), never all-cleardocs/scripts.md:8- Pre-existing inventory gap, out of scope for this change: docs/scripts.md omits about ten older bin/ scripts (fm-lint.sh, fm-doc-audience-check.sh, fm-cd-pretool-check.sh, fm-cd-command-policy.mjs, fm-transition-lib.sh, fm-push-transition-lib.sh, fm-herdr-session-cleanup.sh, fm-install-shellcheck.sh, backends/herdr-eventwait.py, backends/herdr-workspace-move.py) even though comparable policy/lib scripts are listed. A follow-up should either complete the inventory or state its inclusion rule; nothing enforces coverage today.docs/verification/supervision.md:180- Judgment call: the 'Captain's call: decisions and waits' verification section keeps its 2026-07-30 review date while the final commit (2026-08-04) added the bounded evidence-less decision stop. I verified the harness table's statements against the final adapters and guard code and confirmed the extended suite-coverage sentence by running tests/fm-attention.test.sh (all pass), but did not re-run the cross-harness live review the date refers to.🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix: fm-attention: resolve shellcheck SC2034/SC2016 findings
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.