Skip to content

CI mirror: fix(supervision): suppress false-positive stale wakes and catch runs parked at a gate - #6

Open
sbracewell64 wants to merge 9 commits into
mainfrom
fm/supervision-increments
Open

CI mirror: fix(supervision): suppress false-positive stale wakes and catch runs parked at a gate#6
sbracewell64 wants to merge 9 commits into
mainfrom
fm/supervision-increments

Conversation

@sbracewell64

Copy link
Copy Markdown
Owner

CI mirror of upstream PR kunchenguid#1091, opened inside this fork solely to run the repository's own workflows on the identical commit. Upstream cross-fork PRs do not run workflows without maintainer approval. Not intended to merge; the upstream PR is the real submission. Body reproduced verbatim below so the compliance check tests the real thing.

Intent

Implement four approved supervision increments from an evidence-gated plan revision (data/supervision-plan-revision/report.md), each shipped as an independently reviewable fix: commit, under four explicit captain decisions.

GOAL: reduce false-positive supervision wakes measured at 61 stale wakes over 22.5 hours, without ever trading a false positive for a false negative. The captain was explicit: 'Preserving supervision correctness is significantly more important than minimizing implementation effort.'

THE FOUR CAPTAIN DECISIONS, which are architecture and not my choices to revisit:

  1. Batch contract Option A: build NO batch abstraction and no generalized multi-worker interface. Leave the code structured so adding it later stays straightforward, but do not build it prematurely. Rationale: task metadata is already an append-and-grow key=value file that has absorbed four independent field additions from four writers with no migration or schema, so later migration cost is ~zero and an abstraction now adds complexity without solving a present problem.
  2. Speculative-parallel execution: dormant, NOT rejected. Do not implement. Document it as a future architectural possibility intentionally disabled under the current cleanup contract, because every losing branch creates unlanded work and cleanup correctly refuses to discard unlanded work, so each round would ADD captain authorization decisions rather than reduce them.
  3. Gate instrumentation: minimum viable only. Collect only measurements an actual gate depends on, integrate with existing telemetry, avoid parallel monitoring systems, prefer reusing an existing metric over new collection.
  4. Wedge fingerprint: the CORRECTED design only.

DELIBERATE DECISIONS A REVIEWER READING ONLY THE DIFF WOULD LIKELY FLAG AS MISTAKES:

  • The progress fingerprint in bin/fm-crew-state.sh --progress deliberately EXCLUDES the most informative-looking fields from 'no-mistakes axi status': active_for and last_activity. Both embed a ticking elapsed counter (verified live on installed v1.40.3: active_for=2h3m, last_activity='quiet 1h25m ago: ...'), so a fingerprint containing either differs on every read, the wedge reset fires unconditionally, and a genuinely frozen worker would never escalate again. That is a silent false negative and is strictly worse than the false positive being removed. The captain explicitly instructed: do NOT implement the 'one grep away' version that uses those fields, and document why so a later reader who notices the simpler version does not reintroduce the defect. agent_pid is likewise excluded because it is empty on a live healthy step; round is compared as an opaque string because its observed value is the non-numeric token 'starting'.
  • With no stored progress baseline, the wedge escalation still fires exactly as before. This looks like a missed optimization; it is intentional. Absence of evidence is not evidence of progress, and suppressing the first escalation of a chain would trade the false positive for a false negative on a frozen worker. Only REPEAT escalations are suppressed, each backed by positive evidence the run advanced. An empty or unreadable fingerprint likewise compares equal and escalates.
  • The wedge fix knowingly narrows coverage: a worker that freezes while its pipeline keeps advancing is now absorbed. This is stated in the commit body as accepted, because the alternative today is escalating every healthy validating worker, and because the new park sweep covers the sub-case where a run reaches a gate and nobody answers.
  • bin/fm-send.sh marks crewmate/scout steers but deliberately does NOT mark messages starting with '/' (any harness) or '$' (codex). This looks like an inconsistency. It is required: any prefix in front of a slash command turns it into plain text and the steer silently fails to dispatch, which would break steering a crewmate with '/no-mistakes'. The identity confusion being fixed is about prose that reads like a person speaking, which a slash command cannot be mistaken for.
  • The park sweep in bin/fm-watch.sh is the ONLY change here that ADDS wakes rather than removing them, and the only wake class that surfaces regardless of pane state. That is deliberate: it closes a silent-failure hole where a run parked at a decision gate emits no wake of any kind (observed: ten minutes of silence). Both bounds are configuration and FM_PARK_SCAN_MAX=0 disables it entirely.
  • The declared-pause live-probe marker is spent where the SURFACE happens (surface_nonterminal_stale), not where the classification decides. That indirection is deliberate: the classification also returns the fail-open verdict on paths that do not surface, and spending the budget there would consume the documented one look without ever taking it. Its release at the loop top is deliberately NOT gated on the sibling .paused- file, because the hash-change path removes that file while the crew is still paused, and gating would strand the marker for the rest of the task's life.

FIXES TO PRE-EXISTING DEFECTS FOUND DURING IMPLEMENTATION, included because they are prerequisites:

  • bin/fm-supervise-daemon.sh extracted a window from a stale wake reason with a bare ${reason#stale: } prefix strip on every away-mode wake. That was ALREADY wrong for the watcher's decorated wedge and pause reasons: it yields ' (idle 300s, ...)', matches no recorded window= line, and falls through window_to_task's suffix heuristic to a garbage task id. Adding a branch tag to every stale reason would have widened that bug, so extraction moved to one owner, stale_reason_window.

STRUCTURAL CHANGES THAT ARE MEANS, NOT ENDS:

  • triage_log moved from bin/fm-push-transition-lib.sh to bin/fm-wake-lib.sh so the watcher and the wake drain share one writer against one size bound, rather than opening a second differently-bounded writer against the same log.
  • bin/fm-watch-arm.sh gained a source guard in the same position and for the same reason as the existing one in bin/fm-watch.sh, so its output classifier is unit-testable without executing the runtime.

TEST ASSERTIONS DELIBERATELY LOOSENED, which will look like weakening tests: fm-send-strict, fm-gate-refuse and fm-pending-reply asserted the exact literal typed text for a crewmate send. Those sends now carry the steer marker, so each was changed to assert the target plus the message BODY, with the marker itself pinned precisely in tests/fm-send-secondmate-marker.test.sh, which is its owner. This keeps one owner for the marker contract instead of duplicating it across four suites.

INSTRUMENTATION CHOICES, per decision 3: three measurements ride logs that already exist. Continuous per-poll duration sampling was REJECTED on measurement rather than taste: it would write ~5,700 lines a day into the absorbed-wake log and evict the history that log exists for, well inside its own size bound, destroying the evidence the instrument was added to produce. So the slow-poll line is written only once a poll reaches FM_SLOW_POLL_SECS, which is the saturation threshold itself, and fleet size rides the rare no-change heartbeat line. Supervision latency and drain depth were added because they express coordinator attention cost directly and nothing recorded them. A human-intervention rate and an average fan-out were deliberately NOT collected.

The new docs/supervision-dormant-designs.md is the required deliverable for captain decisions 1 and 2, plus the rationale for the three larger architectures each increment postpones. It deliberately does NOT restate the fingerprint's field-level contract, which stays owned by bin/fm-crew-state.sh's header; it carries only the architectural why-not and points at the code.

This repo is firstmate's own shared tracked material, so firstmate-coding-guidelines was loaded before editing: one owner per contract, cross-references rather than restatements, one sentence per line in tracked Markdown, plain dashes, no agent co-author, shellcheck-clean bin scripts, and colocated tests extending existing runners.

Validation already run locally: bin/fm-lint.sh clean, bin/fm-doc-audience-check.sh clean, and 15 touched suites green at 315 assertions. Six failures in the wider changed-selection run were each reproduced identically at base commit a5fe1bc and are pre-existing environment issues (three Node/Pi, one tmux smoke readiness, one session-start bootstrap diagnostic, one timing-sensitive watcher-lock restart-attach), not regressions from this branch.

What Changed

  • Made the wedge escalation progress-aware: bin/fm-crew-state.sh --progress emits a short fingerprint (completed-step count, active step name and status, the opaque round token, worktree head — deliberately excluding the ticking active_for/last_activity fields), the watcher stores it in state/.progress-<key>, and wedge_timer_check absorbs a repeat escalation only when the fingerprint proves the run moved; with no baseline, or an empty or degraded read, it escalates as before. A declared pause on a live agent now surfaces once per pause window via state/.paused-liveprobe-<key> instead of once per pane redraw.
  • Added a bounded park sweep to bin/fm-watch.sh that wakes when a run sits unchanged at a decision gate across two sweeps — the one wake class this branch adds — bounded by FM_PARK_SCAN_SECS/FM_PARK_SCAN_MAX with 0 disabling it, and rotated through a persisted state/.park-scan-cursor so every ship task is covered within ceil(N/max) sweeps. Every stale reason now ends with a [branch=<name>] tag that the arm layer records as reason=actionable-stale-<branch>; window extraction moved to the single owner stale_reason_window, fixing bin/fm-supervise-daemon.sh's bare ${reason#stale: } strip, and triage_log moved to bin/fm-wake-lib.sh so the watcher and wake drain share one bounded writer. Three sampled measurements ride existing logs: a slow-poll line at FM_SLOW_POLL_SECS, endpoint count on the heartbeat line, and drain depth plus oldest-wake age from bin/fm-wake-drain.sh.
  • bin/fm-send.sh now marks metadata-routed crewmate and scout steers with the generic firstmate-steer operational-input kind, leaving slash commands, codex $ skill invocations, explicit endpoints and the key-send path bare so harness-dispatched commands still run; generated briefs teach the rule. docs/supervision-dormant-designs.md records the batch abstraction and speculative-parallel execution as decided-dormant with measurable revisit triggers, and the marker contract is pinned in tests/fm-send-secondmate-marker.test.sh while the four sibling suites assert target plus message body.

Risk Assessment

✅ Low: Every finding from the prior round is fixed exactly as directed, both flagged hazards are respected (the liveprobe marker was added only to the daemon's cleanup, and the sha-only fingerprint form is preserved for genuine no-run cases), the new tests provably fail without their fixes, and only a minor non-functional duplication remains.

Testing

Ran the 15 touched suites (all green), then built four before/after harnesses that drive the real watcher, sender, crew-state helper and wake drain against both this branch and base commit a5fe1bc, capturing the coordinator-visible wake stream as CLI transcripts. Those transcripts show the headline goal met without the forbidden trade: a healthy validating worker drops from 6 wakes to 1 over six wedge thresholds while a genuinely frozen worker still escalates all 6; an unanswered decision gate behind a busy pane goes from silent to one wake naming the gate, disabled by FM_PARK_SCAN_MAX=0; a live-agent pause drops from 4 wakes to 1 across four redraws; crewmate and scout steers carry the firstmate-steer marker while slash commands, codex skill invocations and explicit endpoints stay bare; the --progress fingerprint is byte-identical when only the ticking fields move and empty when degraded; and the pre-existing daemon window-extraction defect is reproduced at base and fixed here. Artifacts are CLI transcripts rather than screenshots because this change has no rendered UI surface - the end-user experience is exactly the wake reason lines and pane keystrokes captured. Temp base checkout removed; worktree clean.

Evidence: Wedge escalation before/after: healthy worker 6 wakes -> 1, frozen worker 6 -> 6

SCENARIO 1 wedge escalation on a HEALTHY worker driving a no-mistakes run pane byte-identical throughout - the pipeline owns the branch BEFORE base commit a5fe1bc threshold 1 pipeline=3/ci/running/starting/9f1c2ab WAKE stale: captain:fm-ship-login (idle 501s, possible wedge, escalation 1) threshold 2 pipeline=4/ci/running/starting/9f1c2ab WAKE stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 2) threshold 3 pipeline=5/ci/running/starting/9f1c2ab WAKE stale: ... escalation 3, demand-deep-inspection: ... threshold 4 pipeline=6/ci/running/starting/9f1c2ab WAKE stale: ... escalation 4, demand-deep-inspection: ... threshold 5 pipeline=7/ci/running/starting/9f1c2ab WAKE stale: ... escalation 5, demand-deep-inspection: ... threshold 6 pipeline=8/ci/running/starting/9f1c2ab WAKE stale: ... escalation 6, demand-deep-inspection: ... ==> 6 coordinator wakes over 6 wedge thresholds AFTER this branch threshold 1 pipeline=3/ci/running/starting/9f1c2ab WAKE stale: captain:fm-ship-login (idle 501s, possible wedge, escalation 1) [branch=wedge] threshold 2 pipeline=4/ci/running/starting/9f1c2ab absorbed - no coordinator turn threshold 3 pipeline=5/ci/running/starting/9f1c2ab absorbed - no coordinator turn threshold 4 pipeline=6/ci/running/starting/9f1c2ab absorbed - no coordinator turn threshold 5 pipeline=7/ci/running/starting/9f1c2ab absorbed - no coordinator turn threshold 6 pipeline=8/ci/running/starting/9f1c2ab absorbed - no coordinator turn ==> 1 coordinator wakes over 6 wedge thresholds SCENARIO 2 the same watcher on a GENUINELY FROZEN worker BEFORE base commit a5fe1bc ==> 6 coordinator wakes over 6 wedge thresholds AFTER this branch ==> 6 coordinator wakes over 6 wedge thresholds (every threshold still emits stale: ... possible wedge, escalation N [branch=wedge])

========================================================================
SCENARIO 1  wedge escalation on a HEALTHY worker driving a no-mistakes run
            (the shape behind 28 of the 61 measured stale wakes)
            pane byte-identical throughout - the pipeline owns the branch
========================================================================

BEFORE  base commit a5fe1bc
  threshold 1  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 1)
  threshold 2  pipeline=4/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 2)
  threshold 3  pipeline=5/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone)
  threshold 4  pipeline=6/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 4, demand-deep-inspection: same pane has wedge-escalated 4 times in a row - do not re-absorb on the run-step/pane state alone)
  threshold 5  pipeline=7/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 501s, possible wedge, escalation 5, demand-deep-inspection: same pane has wedge-escalated 5 times in a row - do not re-absorb on the run-step/pane state alone)
  threshold 6  pipeline=8/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 6, demand-deep-inspection: same pane has wedge-escalated 6 times in a row - do not re-absorb on the run-step/pane state alone)
  ==> 6 coordinator wakes over 6 wedge thresholds

AFTER   this branch
  threshold 1  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 501s, possible wedge, escalation 1) [branch=wedge]
  threshold 2  pipeline=4/ci/running/starting/9f1c2ab     absorbed - no coordinator turn
  threshold 3  pipeline=5/ci/running/starting/9f1c2ab     absorbed - no coordinator turn
  threshold 4  pipeline=6/ci/running/starting/9f1c2ab     absorbed - no coordinator turn
  threshold 5  pipeline=7/ci/running/starting/9f1c2ab     absorbed - no coordinator turn
  threshold 6  pipeline=8/ci/running/starting/9f1c2ab     absorbed - no coordinator turn
  ==> 1 coordinator wakes over 6 wedge thresholds

========================================================================
SCENARIO 2  the same watcher on a GENUINELY FROZEN worker
            (the false negative this change must NOT introduce)
========================================================================

BEFORE  base commit a5fe1bc
  threshold 1  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 1)
  threshold 2  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 2)
  threshold 3  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone)
  threshold 4  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 4, demand-deep-inspection: same pane has wedge-escalated 4 times in a row - do not re-absorb on the run-step/pane state alone)
  threshold 5  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 501s, possible wedge, escalation 5, demand-deep-inspection: same pane has wedge-escalated 5 times in a row - do not re-absorb on the run-step/pane state alone)
  threshold 6  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 6, demand-deep-inspection: same pane has wedge-escalated 6 times in a row - do not re-absorb on the run-step/pane state alone)
  ==> 6 coordinator wakes over 6 wedge thresholds

AFTER   this branch
  threshold 1  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 501s, possible wedge, escalation 1) [branch=wedge]
  threshold 2  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 2) [branch=wedge]
  threshold 3  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 501s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone) [branch=wedge]
  threshold 4  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 4, demand-deep-inspection: same pane has wedge-escalated 4 times in a row - do not re-absorb on the run-step/pane state alone) [branch=wedge]
  threshold 5  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 5, demand-deep-inspection: same pane has wedge-escalated 5 times in a row - do not re-absorb on the run-step/pane state alone) [branch=wedge]
  threshold 6  pipeline=3/ci/running/starting/9f1c2ab     WAKE  stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 6, demand-deep-inspection: same pane has wedge-escalated 6 times in a row - do not re-absorb on the run-step/pane state alone) [branch=wedge]
  ==> 6 coordinator wakes over 6 wedge thresholds
Evidence: Park sweep: an unanswered gate behind a busy pane goes from silent to one named wake

SCENARIO 3 a run parked at a decision gate nobody answers pane stays BUSY the whole time, so no stale path can see it BEFORE base commit a5fe1bc t+120 s silent (gate recorded on disk: <none>) t+240 s silent (gate recorded on disk: <none>) t+360 s silent (gate recorded on disk: <none>) gate answered, run advances: silent (this task's park markers left on disk: 0) FM_PARK_SCAN_MAX=0 on a gate due to surface: silent - sweep disabled --- what the coordinator drains (fm-wake-drain.sh) --- AFTER this branch t+120 s silent (gate recorded on disk: parked at review: 2 finding(s)) t+240 s WAKE stale: captain:fm-ship-login (parked at review: 2 finding(s) across two sweeps - the run is waiting on a response the worker has not given) [branch=park] t+360 s silent (gate recorded on disk: parked at review: 2 finding(s)) gate answered, run advances: silent (this task's park markers left on disk: 0) FM_PARK_SCAN_MAX=0 on a gate due to surface: silent - sweep disabled --- what the coordinator drains (fm-wake-drain.sh) --- | 1785107633 1 stale captain:fm-ship-login stale: captain:fm-ship-login (parked at review: 2 finding(s) across two sweeps - the run is waiting on a response the worker has not given) [branch=park]

========================================================================
SCENARIO 3  a run parked at a decision gate nobody answers
            pane stays BUSY the whole time, so no stale path can see it
========================================================================

BEFORE  base commit a5fe1bc
  t+120 s  silent   (gate recorded on disk: <none>)
  t+240 s  silent   (gate recorded on disk: <none>)
  t+360 s  silent   (gate recorded on disk: <none>)
  gate answered, run advances: silent (this task's park markers left on disk: 0)
  FM_PARK_SCAN_MAX=0 on a gate due to surface: silent - sweep disabled
  --- what the coordinator drains (fm-wake-drain.sh) ---

AFTER   this branch
  t+120 s  silent   (gate recorded on disk: parked at review: 2 finding(s))
  t+240 s  WAKE  stale: captain:fm-ship-login (parked at review: 2 finding(s) across two sweeps - the run is waiting on a response the worker has not given) [branch=park]
  t+360 s  silent   (gate recorded on disk: parked at review: 2 finding(s))
  gate answered, run advances: silent (this task's park markers left on disk: 0)
  FM_PARK_SCAN_MAX=0 on a gate due to surface: silent - sweep disabled
  --- what the coordinator drains (fm-wake-drain.sh) ---
  | 1785107633	1	stale	captain:fm-ship-login	stale: captain:fm-ship-login (parked at review: 2 finding(s) across two sweeps - the run is waiting on a response the worker has not given) [branch=park]
Evidence: Declared pause on a live agent: 4 wakes across 4 pane redraws -> 1

SCENARIO 4 a declared pause on a LIVE agent, four pane redraws a pane with a ticking token counter rehashes every poll BEFORE base commit a5fe1bc pane redraw 1 WAKE stale: captain:fm-scout-api pane redraw 2 WAKE stale: captain:fm-scout-api pane redraw 3 WAKE stale: captain:fm-scout-api pane redraw 4 WAKE stale: captain:fm-scout-api ==> 4 coordinator wakes across 4 redraws of ONE pause window crew leaves the pause: live-probe marker released - the next pause gets its own look AFTER this branch pane redraw 1 WAKE stale: captain:fm-scout-api [branch=nonterminal] pane redraw 2 absorbed - this pause window already had its one look pane redraw 3 absorbed - this pause window already had its one look pane redraw 4 absorbed - this pause window already had its one look ==> 1 coordinator wakes across 4 redraws of ONE pause window crew leaves the pause: live-probe marker released - the next pause gets its own look

========================================================================
SCENARIO 4  a declared pause on a LIVE agent, four pane redraws
            a pane with a ticking token counter rehashes every poll
========================================================================

BEFORE  base commit a5fe1bc
  pane redraw 1  WAKE  stale: captain:fm-scout-api
  pane redraw 2  WAKE  stale: captain:fm-scout-api
  pane redraw 3  WAKE  stale: captain:fm-scout-api
  pane redraw 4  WAKE  stale: captain:fm-scout-api
  ==> 4 coordinator wakes across 4 redraws of ONE pause window
  crew leaves the pause: live-probe marker released - the next pause gets its own look

AFTER   this branch
  pane redraw 1  WAKE  stale: captain:fm-scout-api [branch=nonterminal]
  pane redraw 2  absorbed - this pause window already had its one look
  pane redraw 3  absorbed - this pause window already had its one look
  pane redraw 4  absorbed - this pause window already had its one look
  ==> 1 coordinator wakes across 4 redraws of ONE pause window
  crew leaves the pause: live-probe marker released - the next pause gets its own look
Evidence: fm-send: the literal text a crewmate, scout and harness command actually receive

AFTER this branch prose steer to a CREWMATE (kind=ship) typed by the operator : the captain wants the login fix rebased first lands in the pane as : <U+2063>FIRSTMATE_OP: v1 firstmate-steer: the captain wants the login fix rebased first prose steer to a SCOUT (kind=scout) typed by the operator : report what you found so far lands in the pane as : <U+2063>FIRSTMATE_OP: v1 firstmate-steer: report what you found so far SLASH COMMAND to a crewmate - must stay bare or it never dispatches typed by the operator : /no-mistakes lands in the pane as : /no-mistakes CODEX skill invocation - must stay bare on codex typed by the operator : $review the diff lands in the pane as : $review the diff dollar-prefixed PROSE to a non-codex crewmate - marked typed by the operator : $5 says this is worth doing lands in the pane as : <U+2063>FIRSTMATE_OP: v1 firstmate-steer: $5 says this is worth doing explicit backend endpoint - unchanged, stays bare typed by the operator : raw endpoint steer lands in the pane as : raw endpoint steer (BEFORE, at base a5fe1bc: every one of the six lands bare - indistinguishable from a human typing.)

========================================================================
A. what a crewmate actually receives from fm-send
   (a worker that cannot tell firstmate from a human at the keyboard has
    composed a message addressed to the captain into its own pane)
========================================================================

BEFORE  base commit a5fe1bc

  prose steer to a CREWMATE (kind=ship)
    typed by the operator : the captain wants the login fix rebased first
    lands in the pane as  : the captain wants the login fix rebased first

  prose steer to a SCOUT (kind=scout)
    typed by the operator : report what you found so far
    lands in the pane as  : report what you found so far

  SLASH COMMAND to a crewmate - must stay bare or it never dispatches
    typed by the operator : /no-mistakes
    lands in the pane as  : /no-mistakes

  CODEX skill invocation - must stay bare on codex
    typed by the operator : $review the diff
    lands in the pane as  : $review the diff

  dollar-prefixed PROSE to a non-codex crewmate - marked
    typed by the operator : $5 says this is worth doing
    lands in the pane as  : $5 says this is worth doing

  explicit backend endpoint - unchanged, stays bare
    typed by the operator : raw endpoint steer
    lands in the pane as  : raw endpoint steer

AFTER   this branch

  prose steer to a CREWMATE (kind=ship)
    typed by the operator : the captain wants the login fix rebased first
    lands in the pane as  : <U+2063>FIRSTMATE_OP: v1 firstmate-steer: the captain wants the login fix rebased first

  prose steer to a SCOUT (kind=scout)
    typed by the operator : report what you found so far
    lands in the pane as  : <U+2063>FIRSTMATE_OP: v1 firstmate-steer: report what you found so far

  SLASH COMMAND to a crewmate - must stay bare or it never dispatches
    typed by the operator : /no-mistakes
    lands in the pane as  : /no-mistakes

  CODEX skill invocation - must stay bare on codex
    typed by the operator : $review the diff
    lands in the pane as  : $review the diff

  dollar-prefixed PROSE to a non-codex crewmate - marked
    typed by the operator : $5 says this is worth doing
    lands in the pane as  : <U+2063>FIRSTMATE_OP: v1 firstmate-steer: $5 says this is worth doing

  explicit backend endpoint - unchanged, stays bare
    typed by the operator : raw endpoint steer
    lands in the pane as  : raw endpoint steer
Evidence: --progress fingerprint beside the live axi status row it derives from

READ 1 - what no-mistakes axi status reports (the active_steps row): active_steps[1]{step,status,active_for,last_activity,agent_pid,round}: ci,running,1h22m,"quiet 43m56s ago: log: all CI checks passed - still monitoring until merged or closed","",starting fm-crew-state.sh ship-login --progress -> 3/ci/running/starting/a622a96 READ 2 - 41 minutes later, NOTHING about the run has advanced; only the two ticking fields moved (1h22m -> 2h3m, 43m56s -> 1h25m): ci,running,2h3m,"quiet 1h25m ago: log: all CI checks passed - still monitoring until merged or closed","",starting fm-crew-state.sh ship-login --progress -> 3/ci/running/starting/a622a96 => byte-identical. A frozen worker still escalates. READ 3 - the pipeline genuinely advances (ci running -> passed): fm-crew-state.sh ship-login --progress -> 3/ci/passed/starting/a622a96 => moved. The healthy worker's escalation is absorbed. READ 4 - the worktree is torn down / the run read fails (degraded): fm-crew-state.sh ship-login --progress -> [] => empty. An empty fingerprint compares equal, so the escalation still fires.

========================================================================
B. the --progress fingerprint beside the live `axi status` it comes from
   (the deliberately excluded fields are the most informative-looking ones)
========================================================================

READ 1 - what `no-mistakes axi status` reports (the active_steps row):
      active_steps[1]{step,status,active_for,last_activity,agent_pid,round}:
    ci,running,1h22m,"quiet 43m56s ago: log: all CI checks passed - still monitoring until merged or closed","",starting
  fm-crew-state.sh ship-login --progress  ->  3/ci/running/starting/a622a96

READ 2 - 41 minutes later, NOTHING about the run has advanced;
         only the two ticking fields moved (1h22m -> 2h3m, 43m56s -> 1h25m):
      active_steps[1]{step,status,active_for,last_activity,agent_pid,round}:
    ci,running,2h3m,"quiet 1h25m ago: log: all CI checks passed - still monitoring until merged or closed","",starting
  fm-crew-state.sh ship-login --progress  ->  3/ci/running/starting/a622a96
  => byte-identical. A frozen worker still escalates.

READ 3 - the pipeline genuinely advances (ci running -> passed):
  fm-crew-state.sh ship-login --progress  ->  3/ci/passed/starting/a622a96
  => moved. The healthy worker's escalation is absorbed.

READ 4 - the worktree is torn down / the run read fails (degraded):
  fm-crew-state.sh ship-login --progress  ->  []
  => empty. An empty fingerprint compares equal, so the escalation still fires.
Evidence: Pre-existing daemon defect: task id backed out of a decorated stale reason

BEFORE base commit a5fe1bc (bare ${reason#stale: } prefix strip) reason : stale: sess:fm-ship-login (idle 300s, possible wedge, escalation 2) [branch=wedge] window : sess:fm-ship-login (idle 300s, possible wedge, escalation 2) [branch=wedge] task : ship-login (idle 300s, possible wedge, escalation 2) [branch=wedge] reason : stale: sess:fm-ship-login (parked at review: 2 finding(s) across two sweeps) [branch=park] window : sess:fm-ship-login (parked at review: 2 finding(s) across two sweeps) [branch=park] task : 2 finding(s) across two sweeps) [branch=park] <-- garbage task id AFTER this branch (stale_reason_window, one owner) every one of the four forms -> window: sess:fm-ship-login , task: ship-login

========================================================================
C. away-mode daemon backing a task id out of a stale wake reason
   (a garbage task id means the daemon steers the wrong pane, or none)
========================================================================

BEFORE  base commit a5fe1bc (bare ${reason#stale: } prefix strip)

  reason : stale: sess:fm-ship-login
  window : sess:fm-ship-login
  task   : ship-login

  reason : stale: sess:fm-ship-login (idle 300s, possible wedge, escalation 2) [branch=wedge]
  window : sess:fm-ship-login (idle 300s, possible wedge, escalation 2) [branch=wedge]
  task   : ship-login (idle 300s, possible wedge, escalation 2) [branch=wedge]

  reason : stale: sess:fm-ship-login (paused 3600s, awaiting external - declared pause)
  window : sess:fm-ship-login (paused 3600s, awaiting external - declared pause)
  task   : ship-login (paused 3600s, awaiting external - declared pause)

  reason : stale: sess:fm-ship-login (parked at review: 2 finding(s) across two sweeps) [branch=park]
  window : sess:fm-ship-login (parked at review: 2 finding(s) across two sweeps) [branch=park]
  task   :  2 finding(s) across two sweeps) [branch=park]

AFTER   this branch (stale_reason_window, one owner)

  reason : stale: sess:fm-ship-login
  window : sess:fm-ship-login
  task   : ship-login

  reason : stale: sess:fm-ship-login (idle 300s, possible wedge, escalation 2) [branch=wedge]
  window : sess:fm-ship-login
  task   : ship-login

  reason : stale: sess:fm-ship-login (paused 3600s, awaiting external - declared pause)
  window : sess:fm-ship-login
  task   : ship-login

  reason : stale: sess:fm-ship-login (parked at review: 2 finding(s) across two sweeps) [branch=park]
  window : sess:fm-ship-login
  task   : ship-login
Evidence: Gate instrumentation riding existing logs, and per-branch wake attribution

1. loop saturation - written only once a poll reaches FM_SLOW_POLL_SECS [2026-07-26T19:18:45-0400] slow poll: 1s of 1s over 1 recorded endpoint(s) 2. sustained concurrency - this home's endpoint count on the rare no-change heartbeat [2026-07-26T19:18:45-0400] absorbed heartbeat (no captain-relevant change) fleet=1 3. coordinator attention cost - depth and the oldest wake's wait, at drain time what the coordinator turn received: | 1785107746 1 stale captain:fm-ship-login stale: captain:fm-ship-login [branch=wedge] | 1785107891 1 stale captain:fm-scout-api stale: captain:fm-scout-api (parked at review: 2 finding(s)) [branch=park] [2026-07-26T19:18:53-0400] drain: depth=2 oldest_wait=187s 4. per-branch wake attribution in the existing cycle-exit ledger stale: ... [branch=wedge] -> reason=actionable-stale-wedge stale: ... [branch=park] -> reason=actionable-stale-park stale: ... [branch=pause-resurface] -> reason=actionable-stale-pause-resurface stale: ... [branch=nonterminal] -> reason=actionable-stale-nonterminal stale: captain:fm-ship-login -> reason=actionable-stale

========================================================================
D. gate instrumentation - three measurements on logs that already exist
========================================================================

1. loop saturation - written only once a poll reaches FM_SLOW_POLL_SECS
     [2026-07-26T19:18:43-0400] slow poll: 0s of 1s over 1 recorded endpoint(s)
     [2026-07-26T19:18:45-0400] slow poll: 1s of 1s over 1 recorded endpoint(s)

2. sustained concurrency - this home's endpoint count on the rare no-change heartbeat
     [2026-07-26T19:18:45-0400] absorbed heartbeat (no captain-relevant change) fleet=1
     [2026-07-26T19:18:49-0400] absorbed heartbeat (no captain-relevant change) fleet=1

3. coordinator attention cost - depth and the oldest wake's wait, at drain time
     what the coordinator turn received:
       | 1785107746	1	stale	captain:fm-ship-login	stale: captain:fm-ship-login [branch=wedge]
       | 1785107891	1	stale	captain:fm-scout-api	stale: captain:fm-scout-api (parked at review: 2 finding(s)) [branch=park]
     [2026-07-26T19:18:53-0400] drain: depth=2 oldest_wait=187s

4. per-branch wake attribution in the existing cycle-exit ledger
   (previously 44% of stale wakes could not be attributed to a rule)
     stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 2) [branch=wedge] -> reason=actionable-stale-wedge
     stale: captain:fm-ship-login (parked at review: 2 finding(s)) [branch=park]    -> reason=actionable-stale-park
     stale: captain:fm-scout-api [branch=pause-resurface]                           -> reason=actionable-stale-pause-resurface
     stale: captain:fm-ship-login [branch=nonterminal]                              -> reason=actionable-stale-nonterminal
     stale: captain:fm-ship-login                                                   -> reason=actionable-stale
Evidence: A/B harness driving the real watcher at both commits (wedge, park, pause)
#!/usr/bin/env bash
# Product-level demonstration of the four supervision increments.
#
# Drives the REAL bin/fm-watch.sh (base commit and target commit side by side)
# through the scenarios the intent describes, and prints exactly what the
# coordinator sees: the wake records the watcher enqueues and the drain hands to
# a coordinator turn, or silence.
#
# Usage: demo-supervision-wakes.sh <target-repo> <base-repo> <out-dir>
set -u

TARGET_ROOT=$1
BASE_ROOT=$2
OUT=$3

ROOT="$TARGET_ROOT"
# shellcheck source=/dev/null
. "$TARGET_ROOT/tests/wake-helpers.sh"

TMP_ROOT=$(fm_test_tmproot fm-supervision-demo)

seen_sig() {
  if [ "$(uname)" = Darwin ]; then stat -f '%z:%Fm' "$1" 2>/dev/null; else stat -c '%s:%Y' "$1" 2>/dev/null; fi
}

# Run one watcher cycle. Echoes the wake reason it surfaced (stdout), or nothing
# if it absorbed. Bounded wait either way.
run_cycle() {  # <watch-bin> <dir> <window> [VAR=value...]
  local watch=$1 dir=$2 window=$3
  shift 3
  local out="$dir/watch.out" pid i=0
  : > "$out"
  PATH="$dir/fakebin:$PATH" env FM_FAKE_TMUX_WINDOW="$window" FM_FAKE_TMUX_CAPTURE="$dir/pane.txt" \
    FM_STATE_OVERRIDE="$dir/state" FM_CREW_STATE_BIN="$dir/fakebin/fm-crew-state.sh" \
    FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
    "$@" "$watch" > "$out" 2>&1 &
  pid=$!
  while [ "$i" -lt 60 ]; do
    kill -0 "$pid" 2>/dev/null || break
    sleep 0.1
    i=$((i + 1))
  done
  kill "$pid" 2>/dev/null || true
  wait "$pid" 2>/dev/null || true
  grep -v '^$' "$out" 2>/dev/null || true
}

# How many stale wake records the durable queue holds for a window.
queued_stale() {  # <state> <window>
  [ -f "$1/.wake-queue" ] || { echo 0; return; }
  awk -F '\t' -v w="$2" '$3 == "stale" && $4 == w { n++ } END { print n + 0 }' "$1/.wake-queue"
}

hr() { printf '%s\n' "========================================================================"; }

# ===========================================================================
# 1/2. The headline defect: a HEALTHY worker whose no-mistakes pipeline owns the
#      branch. Its pane never changes (the pipeline renders nothing there), so
#      the wedge escalation fired every threshold, forever.
# ===========================================================================
wedge_scenario() {  # <watch-bin> <label> <mode: healthy|frozen>
  local watch=$1 label=$2 mode=$3
  local dir state window key pane_hash sig round wakes=0 progress reason
  dir=$(make_case "wedge-$label-$mode"); state="$dir/state"
  window="captain:fm-ship-login"
  printf 'no-mistakes running... the pipeline owns this branch\n' > "$dir/pane.txt"
  printf 'window=%s\nkind=ship\n' "$window" > "$state/ship-login.meta"
  printf 'working: handed to no-mistakes validation\n' > "$state/ship-login.status"
  sig=$(seen_sig "$state/ship-login.status"); printf '%s' "$sig" > "$state/.seen-ship-login_status"
  key=$(printf '%s' "$window" | tr ':/.' '___')
  pane_hash=$(hash_text "$(cat "$dir/pane.txt")")
  printf '%s' "$pane_hash" > "$state/.hash-$key"
  printf '%s' "$pane_hash" > "$state/.stale-$key"
  printf '1\n' > "$state/.count-$key"

  for round in 1 2 3 4 5 6; do
    if [ "$mode" = healthy ]; then
      progress="$((2 + round))/ci/running/starting/9f1c2ab"   # a step completes each threshold
    else
      progress="3/ci/running/starting/9f1c2ab"                # frozen at the same step
    fi
    echo $(( $(date +%s) - 500 )) > "$state/.stale-since-$key"
    reason=$(run_cycle "$watch" "$dir" "$window" FM_STALE_ESCALATE_SECS=240 \
      FM_FAKE_CREW_STATE='state: working · source: run-step · ci (running)' \
      FM_FAKE_CREW_PROGRESS="$progress")
    if [ -n "$reason" ]; then
      wakes=$((wakes + 1))
      printf '  threshold %d  pipeline=%-32s  WAKE  %s\n' "$round" "$progress" "$reason"
    else
      printf '  threshold %d  pipeline=%-32s  absorbed - no coordinator turn\n' "$round" "$progress"
    fi
  done
  printf '  ==> %d coordinator wakes over 6 wedge thresholds\n' "$wakes"
}

{
hr
echo "SCENARIO 1  wedge escalation on a HEALTHY worker driving a no-mistakes run"
echo "            (the shape behind 28 of the 61 measured stale wakes)"
echo "            pane byte-identical throughout - the pipeline owns the branch"
hr
echo
echo "BEFORE  base commit a5fe1bc"
wedge_scenario "$BASE_ROOT/bin/fm-watch.sh" base healthy
echo
echo "AFTER   this branch"
wedge_scenario "$TARGET_ROOT/bin/fm-watch.sh" target healthy
echo
hr
echo "SCENARIO 2  the same watcher on a GENUINELY FROZEN worker"
echo "            (the false negative this change must NOT introduce)"
hr
echo
echo "BEFORE  base commit a5fe1bc"
wedge_scenario "$BASE_ROOT/bin/fm-watch.sh" base frozen
echo
echo "AFTER   this branch"
wedge_scenario "$TARGET_ROOT/bin/fm-watch.sh" target frozen
echo
} | tee "$OUT/wedge-before-after.txt"

# ===========================================================================
# 3. A run parked at a decision gate that nobody answers. The only change that
#    ADDS wakes. The pane stays BUSY throughout, so no stale path can see it.
# ===========================================================================
park_scenario() {  # <watch-bin> <label>
  local watch=$1 label=$2 dir state window key gate sig sweep reason
  dir=$(make_case "park-$label"); state="$dir/state"
  window="captain:fm-ship-login"
  gate='parked at review: 2 finding(s)'
  printf 'running the review step... esc to interrupt\n' > "$dir/pane.txt"
  printf 'window=%s\nkind=ship\n' "$window" > "$state/ship-login.meta"
  printf 'working: handed to no-mistakes validation\n' > "$state/ship-login.status"
  sig=$(seen_sig "$state/ship-login.status"); printf '%s' "$sig" > "$state/.seen-ship-login_status"
  key=$(printf '%s' "$window" | tr ':/.' '___')

  for sweep in 1 2 3; do
    rm -f "$state/.last-park-scan"
    reason=$(run_cycle "$watch" "$dir" "$window" FM_PARK_SCAN_SECS=999999 FM_PARK_SCAN_MAX=3 \
      FM_FAKE_CREW_STATE="state: parked · source: run-step · $gate")
    if [ -n "$reason" ]; then
      printf '  t+%-4ds  WAKE  %s\n' "$((sweep * 120))" "$reason"
    else
      printf '  t+%-4ds  silent   (gate recorded on disk: %s)\n' "$((sweep * 120))" \
        "$(cat "$state/.park-$key" 2>/dev/null || echo '<none>')"
    fi
  done

  # The worker answers the gate and the run moves on: park state is forgotten.
  rm -f "$state/.last-park-scan"
  reason=$(run_cycle "$watch" "$dir" "$window" FM_PARK_SCAN_SECS=999999 FM_PARK_SCAN_MAX=3 \
    FM_FAKE_CREW_STATE='state: working · source: run-step · fix_review (running)')
  printf '  gate answered, run advances: %s (this task'"'"'s park markers left on disk: %s)\n' \
    "${reason:-silent}" \
    "$( { [ -e "$state/.park-$key" ] && echo x; [ -e "$state/.park-surfaced-$key" ] && echo x; } | wc -l | tr -d ' ')"

  # The documented kill switch, on a gate that is otherwise due to surface.
  printf '%s' "$gate" > "$state/.park-$key"
  rm -f "$state/.park-surfaced-$key" "$state/.last-park-scan"
  reason=$(run_cycle "$watch" "$dir" "$window" FM_PARK_SCAN_SECS=999999 FM_PARK_SCAN_MAX=0 \
    FM_FAKE_CREW_STATE="state: parked · source: run-step · $gate")
  printf '  FM_PARK_SCAN_MAX=0 on a gate due to surface: %s\n' "${reason:-silent - sweep disabled}"

  # What a coordinator turn actually receives: the drained queue.
  printf '  --- what the coordinator drains (fm-wake-drain.sh) ---\n'
  FM_STATE_OVERRIDE="$state" FM_ROOT_OVERRIDE="$FM_ROOT_OVERRIDE" \
    "$TARGET_ROOT/bin/fm-wake-drain.sh" 2>/dev/null | sed 's/^/  | /' || true
}

{
hr
echo "SCENARIO 3  a run parked at a decision gate nobody answers"
echo "            pane stays BUSY the whole time, so no stale path can see it"
hr
echo
echo "BEFORE  base commit a5fe1bc"
park_scenario "$BASE_ROOT/bin/fm-watch.sh" base
echo
echo "AFTER   this branch"
park_scenario "$TARGET_ROOT/bin/fm-watch.sh" target
echo
} | tee "$OUT/park-before-after.txt"

# ===========================================================================
# 4. A declared pause on a live agent, across four pane redraws (a pane with a
#    ticking counter produces a new hash every poll).
# ===========================================================================
pause_scenario() {  # <watch-bin> <label>
  local watch=$1 label=$2 dir state window key redraw before after statusf
  dir=$(make_case "pause-$label"); state="$dir/state"
  window="captain:fm-scout-api"; statusf="$state/scout-api.status"
  printf 'round 0 output\n' > "$dir/pane.txt"
  printf 'window=%s\nkind=scout\nharness=claude\nbackend=tmux\n' "$window" > "$state/scout-api.meta"
  printf 'paused: waiting on the captain to pick an approach\n' > "$statusf"
  printf '%s' "$(seen_sig "$statusf")" > "$state/.seen-scout-api_status"
  key=$(printf '%s' "$window" | tr ':/.' '___')

  for redraw in 1 2 3 4; do
    # A ticking token counter in an idle pane: a brand new hash every poll.
    printf 'waiting on the captain - %dm 0%ds elapsed - 12.%dk tokens\n' "$redraw" "$redraw" "$redraw" > "$dir/pane.txt"
    before=$(queued_stale "$state" "$window")
    run_cycle "$watch" "$dir" "$window" FM_PAUSE_RESURFACE_SECS=999999 FM_STALE_ESCALATE_SECS=999999 \
      FM_FAKE_CREW_STATE='state: paused · source: status-log · waiting on the captain to pick an approach' \
      > /dev/null
    after=$(queued_stale "$state" "$window")
    if [ "$after" -gt "$before" ]; then
      printf '  pane redraw %d  WAKE  %s\n' "$redraw" \
        "$(awk -F '\t' -v w="$window" '$3 == "stale" && $4 == w { r = $5 } END { print r }' "$state/.wake-queue")"
    else
      printf '  pane redraw %d  absorbed - this pause window already had its one look\n' "$redraw"
    fi
  done
  printf '  ==> %s coordinator wakes across 4 redraws of ONE pause window\n' "$(queued_stale "$state" "$window")"

  # The budget belongs to the pause window, not the task: leaving the pause frees it.
  printf 'working: resumed, approach picked\n' > "$statusf"
  printf '%s' "$(seen_sig "$statusf")" > "$state/.seen-scout-api_status"
  printf 'resumed output\n' > "$dir/pane.txt"
  run_cycle "$watch" "$dir" "$window" FM_PAUSE_RESURFACE_SECS=999999 FM_STALE_ESCALATE_SECS=999999 \
    FM_FAKE_CREW_STATE='state: working · source: pane · resumed' > /dev/null
  if [ -e "$state/.paused-liveprobe-$key" ]; then
    printf '  crew leaves the pause: live-probe marker still held (next pause gets no look)\n'
  else
    printf '  crew leaves the pause: live-probe marker released - the next pause gets its own look\n'
  fi
}

{
hr
echo "SCENARIO 4  a declared pause on a LIVE agent, four pane redraws"
echo "            a pane with a ticking token counter rehashes every poll"
hr
echo
echo "BEFORE  base commit a5fe1bc"
pause_scenario "$BASE_ROOT/bin/fm-watch.sh" base
echo
echo "AFTER   this branch"
pause_scenario "$TARGET_ROOT/bin/fm-watch.sh" target
echo
} | tee "$OUT/pause-before-after.txt"
- Evidence: A/B harness for fm-send, --progress and the daemon's window extraction (local file: /tmp/no-mistakes-evidence/01KYG8NJXYY7ZMAR9SNRZJFVHN/demo-send-and-fingerprint.sh)
Evidence: Harness for the three gate measurements and the ledger branch tags
#!/usr/bin/env bash
# Product-level demonstration of captain decision 3 (minimum-viable gate
# instrumentation): the three measurements ride logs that already exist, and the
# per-branch wake attribution lands in the existing cycle-exit ledger.
#
# Usage: demo-instrumentation.sh <target-repo> <out-dir>
set -u

TARGET_ROOT=$1
OUT=$2
ROOT="$TARGET_ROOT"
# shellcheck source=/dev/null
. "$TARGET_ROOT/tests/wake-helpers.sh"
TMP_ROOT=$(fm_test_tmproot fm-instr-demo)

seen_sig() {
  if [ "$(uname)" = Darwin ]; then stat -f '%z:%Fm' "$1" 2>/dev/null; else stat -c '%s:%Y' "$1" 2>/dev/null; fi
}

{
echo "========================================================================"
echo "D. gate instrumentation - three measurements on logs that already exist"
echo "========================================================================"
echo

dir=$(make_case instr); state="$dir/state"
window="captain:fm-ship-login"
printf 'no-mistakes running... the pipeline owns this branch\n' > "$dir/pane.txt"
printf 'window=%s\nkind=ship\n' "$window" > "$state/ship-login.meta"
printf 'working: handed to no-mistakes validation\n' > "$state/ship-login.status"
printf '%s' "$(seen_sig "$state/ship-login.status")" > "$state/.seen-ship-login_status"
key=$(printf '%s' "$window" | tr ':/.' '___')
pane_hash=$(hash_text "$(cat "$dir/pane.txt")")
printf '%s' "$pane_hash" > "$state/.hash-$key"
printf '1\n' > "$state/.count-$key"

# One watcher run with slow-poll sampling forced on (FM_SLOW_POLL_SECS=0 records
# every poll) and the heartbeat due, so the fleet-size stamp is produced too.
PATH="$dir/fakebin:$PATH" env FM_FAKE_TMUX_WINDOW="$window" FM_FAKE_TMUX_CAPTURE="$dir/pane.txt" \
  FM_STATE_OVERRIDE="$state" FM_CREW_STATE_BIN="$dir/fakebin/fm-crew-state.sh" \
  FM_FAKE_CREW_STATE='state: working · source: run-step · ci (running)' \
  FM_FAKE_CREW_PROGRESS='3/ci/running/starting/9f1c2ab' \
  FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=2 FM_SLOW_POLL_SECS=0 \
  FM_STALE_ESCALATE_SECS=999999 "$TARGET_ROOT/bin/fm-watch.sh" > "$dir/watch.out" 2>&1 &
pid=$!
i=0; while [ "$i" -lt 90 ]; do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; i=$((i+1)); done
kill "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true

echo "1. loop saturation - written only once a poll reaches FM_SLOW_POLL_SECS"
grep -m2 'slow poll' "$state/.watch-triage.log" | sed 's/^/     /' || echo "     (none)"
echo
echo "2. sustained concurrency - this home's endpoint count on the rare no-change heartbeat"
grep -m2 'absorbed heartbeat' "$state/.watch-triage.log" | sed 's/^/     /' || echo "     (none)"
echo

# 3. Supervision latency and drain depth: enqueue two wakes with known ages and
#    let the real drain report what the coordinator turn was handed.
now=$(date +%s)
printf '%s\t1\tstale\t%s\tstale: %s [branch=wedge]\n' "$((now - 187))" "$window" "$window" >> "$state/.wake-queue"
printf '%s\t1\tstale\t%s\tstale: %s (parked at review: 2 finding(s)) [branch=park]\n' "$((now - 42))" "captain:fm-scout-api" "captain:fm-scout-api" >> "$state/.wake-queue"
echo "3. coordinator attention cost - depth and the oldest wake's wait, at drain time"
FM_STATE_OVERRIDE="$state" FM_ROOT_OVERRIDE="$FM_ROOT_OVERRIDE" \
  "$TARGET_ROOT/bin/fm-wake-drain.sh" > "$dir/drain.out" 2>/dev/null || true
echo "     what the coordinator turn received:"
sed 's/^/       | /' "$dir/drain.out"
grep -m1 '^.*drain: depth=' "$state/.watch-triage.log" | sed 's/^/     /' || echo "     (none)"
echo

echo "4. per-branch wake attribution in the existing cycle-exit ledger"
echo "   (previously 44% of stale wakes could not be attributed to a rule)"
for line in \
  'stale: captain:fm-ship-login (idle 500s, possible wedge, escalation 2) [branch=wedge]' \
  'stale: captain:fm-ship-login (parked at review: 2 finding(s)) [branch=park]' \
  'stale: captain:fm-scout-api [branch=pause-resurface]' \
  'stale: captain:fm-ship-login [branch=nonterminal]' \
  'stale: captain:fm-ship-login'
do
  printf '%s\n' "$line" > "$dir/arm-reason.txt"
  got=$( FM_STATE_OVERRIDE="$dir/arm-state" ; export FM_STATE_OVERRIDE
         mkdir -p "$dir/arm-state"
         # shellcheck source=/dev/null
         . "$TARGET_ROOT/bin/fm-watch-arm.sh"
         watch_output_reason_type "$dir/arm-reason.txt" )
  printf '     %-78s -> reason=%s\n' "$line" "$got"
done
} | tee "$OUT/instrumentation.txt"

Pipeline

Updates from git push no-mistakes

... (2 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-crew-state.sh:298 - The progress fingerprint does not distinguish "the run read failed" from "the run changed". crew_progress_fingerprint only populates the run-derived terms when HAVE_RUN=1 and RUN_SOURCE=full; when the bounded 10s no-mistakes axi status call (NM_TIMEOUT, fm-crew-state.sh:80) times out or returns empty, HAVE_RUN stays 0 and the fingerprint becomes ////&lt;head&gt; -- non-empty, and different from the healthy 4/ci/running/starting/abc1234. wedge_timer_check (bin/fm-watch.sh:343) tests only [ -n &#34;$fp&#34; ] &amp;&amp; [ -n &#34;$prev_fp&#34; ] &amp;&amp; [ &#34;$fp&#34; != &#34;$prev_fp&#34; ], so it reads that degradation as forward progress: it absorbs the escalation, restarts the timer and deletes the consecutive-escalation count. It absorbs a second time when the next read succeeds and the fingerprint flips back. The documented guarantee that "an empty or unreadable fingerprint compares equal and escalates" therefore holds only for a totally empty reader output, not for the partial read the timeout path actually produces. Fix by emitting a distinguishable no-evidence token when a run WAS expected but could not be read (KIND=ship with a CREW_BRANCH and no-mistakes on PATH, yet RUN_OUT empty or RUN_SOURCE=coarse) and having wedge_timer_check treat that token as compare-equal, so the reset stays keyed to positive evidence only. Note this must not swallow the legitimate scout / pre-validation case, where no run exists by design and the head sha is the only progress term.
  • ⚠️ bin/fm-watch.sh:910 - The park sweep can never see a ship window past the PARK_SCAN_MAX-th one. recorded_windows (bin/fm-watch.sh:261) iterates "$STATE"/*.meta in glob order, which is stable across sweeps, and the loop breaks once park_scanned reaches PARK_SCAN_MAX (default 3). There is no persisted cursor or rotation, so every sweep reads the same first three ship tasks and the fourth and later ones are never park-scanned at all. For a fleet larger than the cap this leaves the exact silent-failure hole the increment was added to close -- a run parked at a decision gate emitting no wake of any kind -- permanently open for those workers, while also re-spending all three bounded crew_parked_gate calls on the same tasks every interval. A persisted last-scanned window that the next sweep resumes after would keep both bounds intact while giving every window coverage within ceil(N/PARK_SCAN_MAX) sweeps.
  • ⚠️ bin/fm-classify-lib.sh:414 - Two different functions are named crew_progress_fingerprint with incompatible contracts: this one takes an <id> and shells out to "$FM_CREW_STATE_BIN" "$id" --progress, while bin/fm-crew-state.sh:296 takes no arguments and reads resolved globals. bin/fm-crew-state.sh sources fm-classify-lib.sh at its top, so its own definition only wins because it is declared later in the file; the # shellcheck disable=SC2119 at bin/fm-crew-state.sh:591 exists solely to silence shellcheck noticing the collision. If the in-script definition is ever moved above the source line, renamed, or refactored into a sourced helper, fm-crew-state.sh &lt;id&gt; --progress would call the classify-lib version and re-exec itself recursively. Rename the in-script one (e.g. compute_progress_fingerprint) so the two contracts have distinct names and the SC2119 suppression can be dropped.
  • ℹ️ bin/fm-supervise-daemon.sh:468 - The new .paused-liveprobe-<key> marker is absent from every explicit pause-tracking cleanup list: this daemon rm (which was extended for .progress- in the same commit), and clear_pause_state / clear_pause_tracking in bin/fm-watch.sh:401 and :404. Its only release paths are the watcher loop top (bin/fm-watch.sh:1006) and the else branch of surface_nonterminal_stale. When the daemon reconciles a window and clears .paused-, .paused-rechecked- and .paused-resurfaced- while the crew's last status line still reads paused or captain-held, the spent marker survives with nothing left to pair it with, and a later declared pause on that window is denied its one documented live-agent look until the watcher happens to observe a non-paused status. Adding it to the rm lists costs nothing and matches how the sibling markers are already handled.
  • ℹ️ bin/fm-watch-arm.sh:305 - The comment attributes the "[branch=<name>]" tag to "bin/fm-push-transition-lib.sh's stale_reason", but stale_reason is defined in bin/fm-classify-lib.sh:313; fm-push-transition-lib.sh is only one of its six callers. Under the repo's one-owner/cross-reference rule this points a reader at the wrong owner for the tag contract, which is the exact class of misdirection the branch tag was added to prevent.
  • ℹ️ tests/fm-watch-triage.test.sh:1612 - test_stale_reason_branch_tags sources bin/fm-watch-arm.sh into the suite shell, and it is invoked first in the runner list, so the effects apply to every subsequent test in the file. The arm script transitively sources bin/fm-wake-lib.sh, which with FM_HOME unset (the test harness never sets it) resolves FM_HOME and STATE to the repo root and runs mkdir -p &lt;repo&gt;/state, then rebinds the suite-wide STATE, TRIAGE_LOG, FM_WAKE_QUEUE and WATCH variables. The directory is gitignored and the ~45 later tests pass FM_STATE_OVERRIDE per subprocess so nothing breaks today, but the classifier only needs to be called three times: running it in a subshell (( . &#34;$ROOT/bin/fm-watch-arm.sh&#34;; watch_output_reason_type &#34;$out&#34; )) gets the same assertions without mutating the suite environment or writing into the working tree.

🔧 Fix: fix(watch): rotate park sweep and empty degraded progress reads
1 info still open:

  • ℹ️ bin/fm-watch.sh:926 - The park pre-pass calls window_to_task purely as a filter predicate and throws the result away, then the sweep loop recomputes it for each selected window (bin/fm-watch.sh:938). window_to_task greps every state/*.meta until it matches, so the pre-pass is now O(N) of those scans per sweep where the old hard break stopped after PARK_SCAN_MAX windows. This is not a regression worth blocking on -- the main stale loop already runs window_to_task per window on every poll, which is far more often than this sweep's PARK_SCAN_INTERVAL -- but the second call is pure duplication. Collecting the task into a parallel park_tasks array alongside park_windows, indexed identically, removes the recomputation and lets the now-dead [ -n &#34;$task&#34; ] || continue guard inside the loop go with it, since the pre-pass already filtered those windows out.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-crew-state.test.sh tests/fm-watch-triage.test.sh tests/fm-send-secondmate-marker.test.sh tests/fm-send-strict.test.sh tests/fm-gate-refuse.test.sh tests/fm-pending-reply.test.sh tests/fm-wake-queue.test.sh - 7 suites, all green
  • bin/fm-test-run.sh tests/fm-daemon.test.sh tests/fm-transition-lib.test.sh tests/fm-operational-input.test.sh tests/fm-brief.test.sh tests/fm-documentation-audiences.test.sh tests/fm-supervision-events.test.sh tests/fm-watch-checkpoint.test.sh tests/fm-instruction-owners.test.sh - 8 suites, all green
  • Manual A/B harness demo-supervision-wakes.sh: drove the real bin/fm-watch.sh from this branch and from git archive a5fe1bc through six consecutive wedge thresholds for a healthy (advancing) and a frozen pipeline, three park sweeps behind a busy pane, and four pane redraws of one declared pause - counting the wake reasons each watcher actually emitted
  • Manual harness demo-send-and-fingerprint.sh: ran bin/fm-send.sh (base and target) against stubbed tmux recording send-keys -l, printing the literal text landing in the pane for a crewmate steer, a scout steer, /no-mistakes, a codex $-skill, dollar-prefixed prose to a non-codex target, and an explicit endpoint
  • Manual harness demo-send-and-fingerprint.sh: ran bin/fm-crew-state.sh ship-login --progress over a fixture reproducing the live axi status active_steps row verbatim, across two reads where only active_for/last_activity ticked, a real step-status change, and a degraded read
  • Manual harness demo-send-and-fingerprint.sh: exercised stale_reason_window + window_to_task on bare, wedge-tagged, pause-decorated and park-decorated stale reasons, against the base commit's bare ${reason#stale: } strip
  • Manual harness demo-instrumentation.sh: ran a live watcher with FM_SLOW_POLL_SECS=0 and FM_HEARTBEAT=2, then bin/fm-wake-drain.sh over a queue with known record ages, and classified five stale reasons through bin/fm-watch-arm.sh's watch_output_reason_type
⚠️ **Document** - 1 info
  • ℹ️ docs/calm-mode-feasibility.md:135 - The sentence "Current session-start, watcher, turn-end guard, away supervisor, and launch-brief inputs retain their versioned U+2063 static envelopes" enumerates five operational-input kinds; this change adds a sixth, firstmate-steer, to FM_OPERATIONAL_KINDS. I deliberately left it unedited: the page is classified maintainer-verification and records what was actually audited against Pi 0.81.1, so adding the new kind would claim coverage that was never verified. The authoritative kind list stays bin/fm-operational-input.sh. Worth a decision only if the calm-mode audit is re-run, which would also settle the related behavioural question of whether a Pi crewmate with calm on would render a firstmate-steer at all.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Six paths emit a "stale:" wake - the watcher's surface_nonterminal_stale,
handle_paused_stale, wedge_timer_check, terminal-status override and away-mode
one-shot, plus the push fast-path - and all six rendered the same bare
"stale: <window>" opening. Nothing downstream could tell which rule fired, so
per-branch wake attribution had to be reconstructed from truncation patterns in
the absorbed-wake log; that method left 44% of stale wakes unattributed across
two independent readings of this system.

Every stale reason now ends with a "[branch=<name>]" tag, and the arm layer
carries it into the cycle-exit ledger as reason=actionable-stale-<branch>, which
is the existing durable per-wake record - no second collector is added. The tag
is prose and a substring match, never a parsed field.

Decorating every stale reason exposed a latent defect: bin/fm-supervise-daemon.sh
backs a window out of the reason with a bare "${reason#stale: }" prefix strip on
every away-mode wake, which was already silently wrong for the decorated wedge
and pause reasons - it yields "<window> (idle 300s, ...)", matches no recorded
window, and falls through window_to_task's suffix heuristic to a garbage task id.
Extraction moves to one owner, stale_reason_window, which strips decoration.

Two further measurements ride the same two logs, both sampled rather than
continuous so no log is flooded and no parallel monitoring surface appears:
a slow-poll line written only once a poll's classification work reaches
FM_SLOW_POLL_SECS, which is the loop-saturation threshold itself; and this
home's recorded endpoint count stamped onto the rare no-change heartbeat line,
so sustained concurrency has a historical series where previously only an
instantaneous reading existed. After the drain commits its authoritative output,
fm-wake-drain.sh appends how many distinct wakes the turn was handed and how long
the oldest waited - the delay between an event and the coordinator acting on it,
which nothing recorded at all. triage_log moves to fm-wake-lib.sh so the watcher
and the drain share one writer against one bound.

Deliberately not built: a structured supervision event bus with typed events and
schemas. One reader exists. A tag in prose costs fifteen lines; a bus costs a
schema, a version, and a second component that has to agree with the first.

Tests: branch tag rendered and recovered for tagged, detailed, both and bare
legacy forms; ledger classification per branch, including untagged and malformed
tags falling back to the classification they always had; branch assertions on
the wedge, pause-resurface, terminal, nonterminal and away-mode paths.
The wedge escalation asks whether a worker has stopped making progress, but its
only reset conditions were a pane-hash change and a busy signature - precisely
the two things a healthy worker driving a no-mistakes run cannot produce,
because the pipeline owns the branch and renders nothing to that worker's own
pane. A healthy validating worker therefore escalated on the same unchanged hash
every threshold, indefinitely: 28 of 61 stale wakes across one measured
22.5-hour window, on three panes, every inter-wake interval at or above
FM_STALE_ESCALATE_SECS plus one coordinator turn. Each one cost a coordinator
turn and each one concluded the worker was healthy.

Progress is a difference and nothing stored a previous value, so the health
verdict was structurally stateless. bin/fm-crew-state.sh --progress supplies that
value as a short fingerprint, the watcher stores it in state/.progress-<key>, and
wedge_timer_check compares the two.

The fingerprint's composition is the load-bearing part, and the obvious fields
are the wrong ones. Live `axi status` output carries active_for ("2h3m") and
last_activity ("quiet 1h25m ago: ...") on the active step; both embed a ticking
elapsed counter, so a fingerprint containing either differs on every read of an
unchanged run. That would reset the escalation unconditionally and leave a
genuinely frozen worker unable to escalate ever again - a false negative on the
one signal that catches a frozen worker, silent because nothing reports an
escalation that did not happen, and strictly worse than the false positive being
removed. agent_pid is empty on a live healthy step, so its presence proves
nothing; round is the non-numeric token "starting", so it is compared as an
opaque string. What remains is the monotonic completed-step count, the active
step's name and status, that opaque round token, and the worktree head - the head
covering scouts and pre-validation ship work, which have no attributed run and
for which a new commit is the only available evidence.

Three properties keep this from trading one defect for a worse one. The
comparison runs only on the escalation branch, so its one bounded call is spent
exactly where the alternative is a whole coordinator turn. With no stored
baseline it escalates as before, because absence of evidence is not evidence of
progress; what the fingerprint suppresses is the repeat escalations, each backed
by positive evidence that the run moved. An empty or unreadable fingerprint
compares equal and escalates.

Known and accepted narrowing, stated so it is not discovered later: a worker that
freezes while its pipeline keeps advancing is absorbed. That shape is real,
because the pipeline spawns its own agents. It is accepted because the
alternative today is escalating every healthy validating worker. If evidence ever
shows workers freezing under advancing pipelines, the fix is one more
deterministic term in the fingerprint, not a supervisor above this one.

Deliberately not built: a batch supervisor process, and model-mediated
supervision. Both exist to answer "is this worker healthy?", which is the exact
question this makes deterministic; making the existing supervisor
progress-aware removes the reason to add a tier above it.

Tests: two reads of an unchanged run whose ticking fields have both moved produce
a byte-identical fingerprint - the direct guard against those fields being
reintroduced, and it must not be dropped; the fingerprint moves on a completed
step, an active-step status change and a round change; with no attributed run it
is stable and moves on a new commit; a run without an active_steps table and a
torn-down worktree degrade to empty rather than to a false reset. End to end:
first round escalates without a baseline and records one, an advancing pipeline
absorbs and restarts the timer, a frozen pipeline re-escalates, and an unreadable
fingerprint escalates while preserving the stored baseline.
A declared pause on a crew whose agent is not confidently dead fails open to a
surface, deliberately: such a crew may be sitting on a decision gate its own
status line silenced. The defect is that this classification re-runs on every
distinct pane hash, so the documented "looked at once" was implemented as
"looked at once per pane redraw" - one wake per redraw, bypassing the
re-surface throttle that lives only on the sibling dead-agent branch.

The measured production rate was low, five wakes across one 22.5-hour window on
a single live-agent pane. The reason to fix it is the ceiling rather than the
rate: a pane that renders a ticking clock or a token counter produces a new hash
every poll, so the same defect on such a task wakes the coordinator every 45
seconds indefinitely. Bounding an unbounded failure mode is worth forty lines
even when the observed count is small.

state/.paused-liveprobe-<key> records that this pause window has already had its
one live-agent look. It is written where the surface actually happens, not where
the classification decides, because that classification also returns the
fail-open verdict on paths that do not surface and spending the budget there
would consume the documented look without ever taking it. Once spent, a live
agent takes the same bounded FM_PAUSE_RESURFACE_SECS cadence a dead one takes,
so a forgotten pause still cannot rot invisibly. The marker is released when the
crew is observed to no longer declare a pause - and that release is deliberately
not gated on the sibling .paused-<key> file, which the hash-change path removes
while the crew is still paused, so gating would strand the marker for the rest
of the task's life. The first surface, the dead-agent behaviour, and the
secondmate idle-endpoint exemption are all unchanged.

This is only sufficient alongside park detection for an unresponsive gated run:
throttling here can delay a real escalation by up to one re-surface window if a
worker declares a pause and then hits a decision gate, and park detection is
what closes that at ~120s regardless of pause state.

Deliberately not built: hierarchical supervision. Hierarchy is warranted when one
supervisor cannot keep up; one supervisor absorbs 95.9% of events in bash today.
This removes an unbounded wake source without adding a component.

docs/architecture.md changes in the same commit, from "first surfaces one stale
wake" to "surfaces one stale wake per pause window".

Tests: a live-agent declared pause across four pane redraws surfaces exactly once
and records its look on the first; later redraws take the bounded cadence;
leaving the pause releases the marker. The pre-existing dead-agent and
captain-held cases are unchanged.
A crewmate received exactly one marked message in its life - the launch brief
that fm-spawn wraps - and a bare stream of steers afterwards, indistinguishable
from a human typing into its pane. The operating contract forbids a crewmate
from addressing the captain, while the transport withheld the means to tell who
was speaking. Both directions of that gap have been observed: a crewmate composed
"Captain, the pipeline paused on one decision you need to make..." into its own
pane and blocked for ten minutes on a reply that could never arrive, and the
captain separately opened a crewmate pane believing it was firstmate.

A metadata-routed steer to an ordinary crewmate or scout now carries the generic
firstmate-steer kind from bin/fm-operational-input.sh - the carrier already
exists, is already parsed, and already degrades gracefully for a worker that does
not know it. Unlike the secondmate path it opens no reply expectation, because a
steer expects no routed answer. Generated briefs teach the rule at the only
moment a worker reads instructions.

Two carve-outs. An explicit backend target and the key-send path stay bare, as
before. And a message the harness itself must dispatch - a leading "/" slash
command anywhere, or a leading "$" skill invocation on codex - is sent bare,
because any prefix turns the command into plain text and the steer silently fails
to run; "/no-mistakes" is the steer this protects. Those are the same two shapes
fm-send's submit settle already recognizes as harness-dispatched, and the
confusion being closed is about prose that reads like a person speaking, which a
slash command cannot be mistaken for.

Deliberately not built: extending the correlated pending-reply record from
secondmates to crewmates. Acknowledgement protocols answer "sent but not
received". The observed failure is a message never sent - the worker wrote prose
into its own pane - which no acknowledgement protocol can detect, and building
one would add correlation ids, a retry loop and a dead-letter path against zero
observed instances of the failure it does catch. The existing asymmetry is
correct: a secondmate round trip crosses homes with no shared filesystem
convention for the reply, and a crewmate steer does not.

Migration: crewmates spawned before this change receive marked steers their
briefs never explained. The marker is a legible prefix, it degrades to visible
text, and the property self-heals on the next dispatch.

AGENTS.md and docs/architecture.md describe the marking boundary and change here.

Tests: ship and scout selectors marked with firstmate-steer and never with the
secondmate carrier; no pending-reply record created for a steer; slash commands
and codex skill invocations stay bare while dollar-prefixed prose to a non-codex
target is marked; explicit endpoints and the key path unchanged.
…espond

A validation run that reaches a decision gate emits no wake of any kind. The
gate state is computed correctly, but nothing polls for the transition - it is
consulted only after a stale pane has already triggered classification, and a
parked worker's pane may never go stale. A worker that does not answer its gate
is therefore silent until something unrelated happens to look at it; the observed
instance ran ten minutes and was caught only because firstmate inspected the pane
during an unrelated wake. Zero instances appear in the logs, which is the defect
rather than evidence against it: the event produces no log line at all.

A bounded sweep reads at most FM_PARK_SCAN_MAX tasks every FM_PARK_SCAN_SECS and
surfaces a gate only when it is seen unchanged across two sweeps. One sweep
cannot tell an unanswered gate from one that is about to be answered promptly,
and a run that is answered promptly must stay silent. Once surfaced, the same
gate is not surfaced again until it changes. A ten-minute silence becomes a
two-minute wake using the same authoritative state read supervision already
makes, and fm-crew-state.sh stays the only owner of gate detection.

This is the only wake class here that surfaces regardless of pane state, and the
only increment in this series that ADDS wakes rather than removing them, so both
bounds are configuration and FM_PARK_SCAN_MAX=0 switches it off entirely without
a code edit. It sits with the other slow sweep and ahead of the per-wake paths
for the same anti-starvation reason those are ordered that way: waking exits the
cycle, so a sweep placed after them would be starved by a chatty sibling
crewmate exactly when a quiet parked worker most needs noticing.

It is also what makes the declared-pause throttle safe. Throttling a live-agent
pause can delay a real escalation by up to one re-surface window if a worker
declares a pause and then hits a gate; this closes that at roughly two minutes
regardless of pause state.

Known limit: only gates the run itself names are visible. A worker blocked on
something outside its run is not, nothing detects that deterministically today,
and detecting it would require the worker to declare it - which is the status
protocol that already exists.

Deliberately not built: model-mediated supervision, an LLM asked "is this worker
stuck?". The park is a string comparison on a gate name. The genuinely
interpretive question is already routed to firstmate by the deep-inspection
demand, and routing it to a cheaper model yields a second-hand judgement
firstmate has to re-derive - costing a model call and the coordinator turn.

Tests: a first sighting stays silent and records the gate; the same gate on the
next sweep surfaces once, naming it; an already-surfaced gate does not surface
again; a run moving between gates stays silent while advancing its recorded gate;
a run leaving its gate clears both markers so a later park starts its own count;
and a zero cap disables the sweep without advancing any state.
The supervision fixes each declined a larger alternative, and the reasoning for
declining is worth more to a future contributor than the outcome: without it the
same proposals arrive again, are re-argued from scratch, and are adopted or
rejected on taste rather than evidence.

Each record names the problem it would solve, the evidence for and against it
today, and a measurable trigger stated as a number against a named metric. A
record without such a trigger is a graveyard entry rather than a dormant design,
because nothing would ever cause anyone to look at it again; where a trigger is
not collectable with what the system records, that is stated along with the
instrument it needs.

Two of these are captain decisions rather than implementation judgements, and
both are recorded as decided rather than open.

The batch abstraction is deferred, not rejected. The standard argument for
defining an interface before it is exercised is that later migration is
expensive; measured here it is close to free, because task metadata is a flat
key-value file that has already absorbed four independent field additions from
four writers with no migration, no version and no schema. The cost of building it
now is concrete in the other direction: every semantic field in the plan-document
schema is a guess, and the only fan-out-eligible work found in twenty real
backlog items is four instances of one shape, so a schema written now would
encode that shape as if it were general. The implementation stays shaped so the
grouping key can be added when a real batch first exists.

Speculative-parallel execution is dormant and structurally blocked rather than
merely unbuilt. Every losing arm holds unlanded work, cleanup refuses to discard
unlanded work without explicit captain authority, and that refusal exists because
unlanded work has been lost before. An N-way round therefore costs N-1 captain
authorizations forever, which increases the exact cost the mechanism would be
adopted to reduce. Preserving compatibility for it would mean either the batch
abstraction rejected above or relaxing that safety boundary, so no compatibility
work is warranted. If the cleanup contract ever changes, the existing
declared-scratch doctrine for scout worktrees supplies the discard mechanism -
which is why generalizing that doctrine in advance is not needed.

The file also records why instrumentation was preferred over a telemetry
subsystem sized for hypothetical gate metrics, including the concrete reason
continuous per-poll sampling was rejected: it would have written ~5,700 lines a
day into the absorbed-wake log and evicted the very history that log exists for.

Registered as maintainer-architecture and linked from architecture.md, which
remains the owner of how supervision actually works. The progress fingerprint's
field-level contract stays owned by bin/fm-crew-state.sh; this file carries only
the architectural why-not and points at it.
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