Skip to content

fix(bin): keep a declared wait in force under an informational note: line - #4818

Open
mremond wants to merge 7 commits into
kunchenguid:mainfrom
mremond:fm/fm-note-line-ends-declared-wait
Open

mremond wants to merge 7 commits into
kunchenguid:mainfrom
mremond:fm/fm-note-line-ends-declared-wait

Conversation

@mremond

@mremond mremond commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #4742 - merge after it

This branch is stacked on #4742 and should merge after it.
The dependency is real: before #4742 any status write resets a declared wait's recheck cadence, so "no wake before the cadence" cannot hold without it.
The first three commits are #4742's; this PR's own change is the top four commits: 9a5d1f42 (the reading contract and the site moves), 2d375896 (documenting the re-declaration residue), 23d9395c (the stale_is_terminal site), and 70dd82f0 (documentation).

Reading contract

status_wait_line in bin/fm-classify-lib.sh is the one owner.
When the latest status event is a note: and the latest event that is not a note: declares a paused: or captain-held: wait, that wait is still in force, with its identity, recheck cadence, and age; in every other case the result is the latest event, exactly as last_status_line reads it.
It composes with #3753: last_status_line still returns the note: as the latest event for every other reader, the note: still wakes firstmate as a status write, and it is still presented as unread status; only whether it ends a declared wait changes.
Only note: is looked through.
Sites that decide whether a declared wait is in force and now read it: every such check in bin/fm-watch.sh (pause class, stale surfacing, busy-turn bound, wedge-wait evidence, the loop-top pause-tracking clear), stale_is_terminal (which gates the watcher's declared-wait branch), the away-mode daemon's wait checks in bin/fm-supervise-daemon.sh, bin/fm-push-transition-lib.sh, the captain-held skip in bin/fm-inactive-reconcile.sh, and status_current_line (the crew-state reading).

Residue

  • Closed: an informational note: written under a live paused: or captain-held: wait ending the wait in the watcher's view - the immediate recheck and the age reset - including a note: that mentions a legacy captain token such as merged or checks green.
  • Remains: the note:'s own status-write wake (intended, per fix(bin): read the latest status event so buried declarations and open decisions aren't lost #3753); any status verb other than note: still ends a wait; a word-for-word re-declaration within one poll of leaving the wait, or while the watcher is down, inherits the previous wait's recheck timing and age (the write itself still wakes firstmate at once); and "possible wedge, escalation 1" wakes with a live run step, which go through a different path and are not claimed.
  • Known separate question, not in this PR: the away-mode daemon's status-span classification treats any note: carrying a legacy captain token as captain-relevant; that is about what a note: means in general, not about whether it ends a wait.

Exercised end to end versus reasoned

  • End to end: the real bin/fm-watch.sh with real status files and real timing and the real bin/fm-crew-state.sh reader, for paused: and captain-held:, each with a dead-agent and a live-agent pane: a token-bearing note: under the live wait (no stale wake before the cadence, and the recheck keeps the declaration's age), the cadence elapsing, a changed declaration (immediate wake), and working: then note: (wakes as before). These tests fail on the code before this change. Crew-state's paused reading under a note: is also tested against the real reader.
  • Reasoned, covered only by existing suites: the away-mode daemon, push-transition, and inactive-reconcile sites received the same one-call swap and their suites pass, but no test drives a note: under a wait through those paths.
  • The tests record each note:'s own status-write wake as already handled, so they measure only the stale path's reading of the wait.

Intent

A note: line appended under a declared paused: or captain-held: wait must not end that wait in the watcher's view.
Since upstream #3753 a note: line counts as its own status event, and bin/fm-watch.sh reads whether a declared wait is still in force from last_status_line at several sites, so an informational line written under a declared wait produces an immediate recheck.
This is the residue class left open by #4742, which closed the re-written-declaration class.
Establish the reading contract first, across every site that decides whether a declared wait is still in force, and state how it composes with #3753's design that a note: is a first-class status event; then implement it with tests for both paused: and captain-held: covering a note: under a live wait (no wake before the cadence), a note: after the worker has genuinely left the wait (wake as today), and a changed declaration (immediate wake).
Say in the delivery which residue class this closes and which remains.

Substance of the referenced PRs: upstream #3753 (commit 334fa12) made last_status_line return the latest recognized status event, with note: among the recognized verbs, so a note: is its own event rather than continuation prose. #4742 keys the declared-wait re-surface throttle and the wait's age on the declaration content (the wait line's verb and reason) instead of the status file signature, so a status write that leaves a paused: or captain-held: wait unchanged (a repeated identical declaration, continuation prose) no longer resets the cadence or age, while a changed declaration still surfaces immediately; it explicitly left open that a note: line written under a declared wait still produces a recheck because the latest-event reader reads the note: as ending the wait.

What Changed

  • Added status_wait_line to bin/fm-classify-lib.sh as the single owner of the declared-wait reading. When the latest event is a note: and the most recent non-note: event is a paused: or captain-held: wait, it returns that wait. In every other case it returns the same line as last_status_line. A note: stays a first-class status event (fix(bin): read the latest status event so buried declarations and open decisions aren't lost #3753) for all other readers. It is informational only: it does not leave a wait and does not declare one. _fm_status_event_scan gained a skip-notes mode to support this.
  • Moved every site that decides whether a declared wait still holds over to status_wait_line. This covers the watcher in fm-watch.sh (pause class, stale surfacing, the busy-turn bound, wedge evidence, clearing pause tracking) and the daemon's pause markers, housekeeping and wake handling in fm-supervise-daemon.sh. It also covers stale_is_terminal, status_current_line (the crew-state fallback), fm-inactive-reconcile.sh and fm-push-transition-lib.sh. Result: a note: under a live wait no longer triggers an immediate recheck. A note: written after the worker left the wait still wakes as before, and a changed declaration still surfaces at once.
  • The re-surface throttle identity (stale_wait_declaration) and a new declared_wait_age (recorded in .paused-since-<key> and cleared with the rest of the pause state) are now keyed on the declaration's verb and reason instead of the status file's signature or mtime. docs/architecture.md documents the contract and one remaining gap: an identical wait re-declared within one poll of leaving it, or while the watcher is down, keeps the previous wait's recheck timing and age. The watcher tests now cover note: under a live paused: or captain-held: wait, a note: after the worker left the wait, a changed declaration, and waits re-declared identically. The crew-state tests now cover the fallback read.

Risk Assessment

✅ Low: One reader, status_wait_line, now owns whether a declared wait still holds, and every site that decides this uses it. It changes the result only when the latest event is a note: and the latest other event declares a wait, so everything else behaves as before; the tests cover a note under a live wait, a changed declaration, and a note after leaving the wait, for both paused: and captain-held:.

Testing

I ran the five targeted watcher tests this branch adds or changes; all pass. The two regression tests (the note-under-wait watcher round and the stale_is_terminal check with a token-bearing note:) also fail on the base commit, so they reproduce the bug. I then ran the real watcher, crew-state reader and wake drain against a real tmux server on a private socket. This covered paused: and captain-held:, each with a dead-agent (zsh) pane and a live-harness (grok) pane. The steps were: first sight; a note: saying 'merged … checks green' under the wait; the cadence elapsing; a changed declaration; and working: followed by a note:. Every HEAD run behaved as the intent requires. The same drive on the base commit gets an immediate stale wake right after the note:. This is a CLI/daemon change with no rendered UI, so the evidence is terminal transcripts. The worktree is clean and the temporary base checkout has been removed.

  • Live validation: ✅ go - 7 of 7 scenarios driven live against the product
Scenario Result Live Evidence
paused: wait + note: under it (dead agent): the note: wakes firstmate once as a status event, then there is no stale wake before the cadence, and the recheck reports the declaration's age ✅ pass live live-paused-dead-agent.head.log steps 2a/2b/3: signal wake, then quiet for 15s, then a recheck 'paused 551s' once the cadence elapsed; the base log wakes stale 4s after the note:
captain-held: wait + note: under it (dead agent): no stale wake before the cadence, and the recheck reports the declaration's age ✅ pass live live-held-dead-agent.head.log steps 2b/3: quiet for 15s, then 'captain-held 547s' recheck
paused: and captain-held: + note: under a live harness pane: no stale wake before the cadence ✅ pass live live-paused-live-agent.head.log and live-held-live-agent.head.log step 2b quiet; live-held-live-agent.BASE.log step 2b wakes stale
Adversarial: a note: that carries legacy captain tokens ('merged', 'checks green') under the wait does not take the terminal-stale path ✅ pass live The live-drive note text is 'upstream PR merged and its checks green, waiting for the tag' and no wake follows it on HEAD; FM_TEST_ONLY=test_stale_is_terminal_classifier passes on HEAD and fails on ba…
Changed declaration (with a note: after it) wakes at once for paused: and captain-held: ✅ pass live Step 4 in all four HEAD logs: watcher woke in 2 to 6s ('paused 2s' / 'captain-held 2s' on the dead-agent path)
note: after the worker genuinely left the wait (working: then note:) wakes as today and clears pause tracking ✅ pass live Step 5 in all four HEAD logs: a plain stale wake in 4 to 6s, and '.paused-live_fm-noted present after leaving: no'
A note: stays a first-class status event (#3753 composition): the latest event is still the note:, and firstmate sees it as unread status ✅ pass live Step 2a signal wake with 'UNREAD STATUS ... noted note: upstream PR merged ...'; last_status_line prints the note: while status_wait_line prints the wait
Evidence: Live driver script (real tmux + real fm-watch.sh)

Source: Live driver script (real tmux + real fm-watch.sh)

#!/usr/bin/env bash
# Live drive: real bin/fm-watch.sh + real fm-crew-state.sh + real fm-wake-drain.sh
# against a REAL tmux server on a private socket. Usage: <repo-root> <paused|held> <zsh|grok>
set -u
ROOT=$1 MODE=$2 CMD=$3
REAL_TMUX=$(command -v tmux)
W=$(mktemp -d "${TMPDIR:-/tmp}/fm-live-note.XXXXXX")
SOCK="fm-live-note-$$"
STATE="$W/state"; mkdir -p "$STATE" "$W/bin" "$W/wt" "$W/pane"
cleanup() { "$REAL_TMUX" -L "$SOCK" kill-server >/dev/null 2>&1; rm -rf "$W"; }
trap cleanup EXIT
printf '#!/usr/bin/env bash\nexec "%s" -L "%s" "$@"\n' "$REAL_TMUX" "$SOCK" > "$W/bin/tmux"; chmod +x "$W/bin/tmux"
if [ "$CMD" = zsh ]; then PANE_CMD="env PS1=%# /bin/zsh -f"; else printf '#include <stdio.h>\nint main(void){char b[512];while(fgets(b,sizeof b,stdin)){fputs(b,stdout);fflush(stdout);}return 0;}\n' > "$W/e.c"; cc -o "$W/pane/grok" "$W/e.c"; PANE_CMD="$W/pane/grok"; fi         # pane process whose name is $CMD (zsh = dead agent, grok = live harness)
export PATH="$W/bin:$PATH"
. "$ROOT/bin/fm-classify-lib.sh"
size_of() { LC_ALL=C wc -c < "$1" | tr -d '[:space:]'; }
seen_sig() { printf 'v2\t%s\t%s@%s' "$(status_observed_signature "$1")" "$(size_of "$1")" "$(_fm_open_decisions_file_ident "$1")"; }
prime() { printf '%s' "$(seen_sig "$STATE/noted.status")" > "$STATE/.seen-noted_status"; }
if [ "$MODE" = paused ]; then
  DECL='paused: waiting on validation run one'; CHANGED='paused: waiting on validation run two'
else
  DECL='captain-held [key=route]: awaiting the routing call'; CHANGED='captain-held [key=route]: awaiting the release call'
fi
WIN="live:fm-noted"; KEY=live_fm-noted
tmux new-session -d -s live -n fm-noted -x 120 -y 30 "$PANE_CMD"
sleep 0.5
echo "tmux pane_current_command: $(tmux display-message -p -t "$WIN" '#{pane_current_command}')"
printf 'window=%s\nkind=scout\nharness=grok\nbackend=tmux\nworktree=%s\n' "$WIN" "$W/wt" > "$STATE/noted.meta"
printf '%s\n' "$DECL" > "$STATE/noted.status"
touch -t "$(date -r $(( $(date +%s) - 500 )) +%Y%m%d%H%M.%S)" "$STATE/noted.status"
prime
say() { tmux send-keys -t "$WIN" -l "$1"; tmux send-keys -t "$WIN" Enter; sleep 0.3; }
ack() {
  local err="$W/drain.err" seq gen
  FM_STATE_OVERRIDE="$STATE" "$ROOT/bin/fm-wake-drain.sh" >"$W/drain.out" 2>"$err" || true
  echo "  drained: $(tr '\n' ' ' < "$W/drain.out")"
  seq=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--ack-through \([0-9]*\) --recovery-generation .*/\1/p' "$err")
  gen=$(sed -n 's/^WAKE_ACK_REQUIRED:.*--recovery-generation \([A-Za-z0-9._-]*\)$/\1/p' "$err")
  [ -n "$seq" ] && FM_STATE_OVERRIDE="$STATE" "$ROOT/bin/fm-wake-drain.sh" --ack-through "$seq" --recovery-generation "$gen" >/dev/null 2>&1
}
# Run the watcher up to <secs>; print how it ended.
round() {  # <label> <secs>
  local label=$1 secs=$2 pid t=0
  FM_STATE_OVERRIDE="$STATE" FM_CREW_STATE_BIN="$ROOT/bin/fm-crew-state.sh" FM_WATCH_HANDLING_SUCCESSOR=1 \
    FM_PAUSE_RESURFACE_SECS=${RESURF:-45} FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
    "$ROOT/bin/fm-watch.sh" > "$W/watch.out" 2>&1 &
  pid=$!
  while [ "$t" -lt "$secs" ] && kill -0 "$pid" 2>/dev/null; do sleep 1; t=$((t+1)); done
  if kill -0 "$pid" 2>/dev/null; then kill "$pid"; wait "$pid" 2>/dev/null; echo "[$label] watcher stayed quiet for ${secs}s (no wake)"
  else wait "$pid"; echo "[$label] watcher WOKE after ~${t}s: $(tr '\n' ' ' < "$W/watch.out")"; fi
  echo "  queue: $(cut -f3- "$STATE/.wake-queue" 2>/dev/null | tr '\n' '|')"
}
echo "=== MODE=$MODE pane=$CMD  declaration: $DECL"
say "idle on the wait"
round "1 first sight of declared wait" 20; ack
START=$(date +%s)
echo "--- appending: note: upstream PR merged and its checks green, waiting for the tag"
printf '%s\n' 'note: upstream PR merged and its checks green, waiting for the tag' >> "$STATE/noted.status"
say "idle after the note"
round "2a note: status write (first-class event)" 20; ack
round "2b note under live wait, before cadence" 15
echo "  latest event (last_status_line): $(last_status_line "$STATE/noted.status")"
echo "  wait in force (status_wait_line): $(status_wait_line "$STATE/noted.status")"
rem=$(( ${RESURF:-45} - ($(date +%s) - START) ))
round "3 cadence elapses (resurface secs ${RESURF:-45})" $(( rem > 0 ? rem + 25 : 25 )); ack
echo "--- appending changed declaration + note: $CHANGED"
printf '%s\n%s\n' "$CHANGED" 'note: the release call replaced the routing call' >> "$STATE/noted.status"; prime
say "idle after the changed declaration"
round "4 changed declaration" 20; ack
echo "--- appending: working: validation finished, back on the task / note: picking the next step"
printf '%s\n%s\n' 'working: validation finished, back on the task' 'note: picking the next step' >> "$STATE/noted.status"; prime
say "idle after leaving the wait"
round "5 note after genuinely leaving the wait" 20; ack
echo "  .paused-$KEY present after leaving: $([ -e "$STATE/.paused-$KEY" ] && echo yes || echo no)"
Evidence: HEAD: paused, dead agent

Source: HEAD: paused, dead agent

tmux pane_current_command: zsh
=== MODE=paused pane=zsh  declaration: paused: waiting on validation run one
[1 first sight of declared wait] watcher WOKE after ~3s: stale: live:fm-noted (paused 503s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
  queue: stale	live:fm-noted	stale: live:fm-noted (paused 503s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)|
  drained: 1789724640	1	stale	live:fm-noted	stale: live:fm-noted (paused 503s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
--- appending: note: upstream PR merged and its checks green, waiting for the tag
[2a note: status write (first-class event)] watcher WOKE after ~3s: signal: <state>/noted.status 
  queue: signal	noted.status	signal: <state>/noted.status|signal	noted.status	signal: <state>/noted.status|
  drained: 1789724645	3	signal	noted.status	signal: <state>/noted.status wake annotation: unread wake-EVENT since last drain, not current state: noted.status: paused: waiting on validation run one wake annotation: latest wake-EVENT observed at drain, not current state: noted.status: note: upstream PR merged and its checks green, waiting for the tag UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: upstream PR merged and its checks green, waiting for the tag STATUS OUTCOME BACKSTOP (newest captain-facing task event has no covering branch outcome): noted note: upstream PR merged and its checks green, waiting for the tag 
[2b note under live wait, before cadence] watcher stayed quiet for 15s (no wake)
  queue: 
  latest event (last_status_line): note: upstream PR merged and its checks green, waiting for the tag
  wait in force (status_wait_line): paused: waiting on validation run one
[3 cadence elapses (resurface secs 45)] watcher WOKE after ~24s: stale: live:fm-noted (paused 551s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
  queue: stale	live:fm-noted	stale: live:fm-noted (paused 551s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)|
  drained: 1789724688	4	stale	live:fm-noted	stale: live:fm-noted (paused 551s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
--- appending changed declaration + note: paused: waiting on validation run two
[4 changed declaration] watcher WOKE after ~2s: stale: live:fm-noted (paused 2s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
  queue: stale	live:fm-noted	stale: live:fm-noted (paused 2s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)|
  drained: 1789724693	5	stale	live:fm-noted	stale: live:fm-noted (paused 2s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: the release call replaced the routing call 
--- appending: working: validation finished, back on the task / note: picking the next step
[5 note after genuinely leaving the wait] watcher WOKE after ~4s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724699	6	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: picking the next step 
  .paused-live_fm-noted present after leaving: no
Evidence: HEAD: captain-held, dead agent

Source: HEAD: captain-held, dead agent

tmux pane_current_command: zsh
=== MODE=held pane=zsh  declaration: captain-held [key=route]: awaiting the routing call
[1 first sight of declared wait] watcher WOKE after ~2s: stale: live:fm-noted (captain-held 501s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold) 
  queue: stale	live:fm-noted	stale: live:fm-noted (captain-held 501s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)|
  drained: 1789724712	1	stale	live:fm-noted	stale: live:fm-noted (captain-held 501s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold) 
--- appending: note: upstream PR merged and its checks green, waiting for the tag
[2a note: status write (first-class event)] watcher WOKE after ~3s: signal: <state>/noted.status 
  queue: signal	noted.status	signal: <state>/noted.status|signal	noted.status	signal: <state>/noted.status|
  drained: 1789724716	3	signal	noted.status	signal: <state>/noted.status wake annotation: unread wake-EVENT since last drain, not current state: noted.status: captain-held [key=route]: awaiting the routing call wake annotation: latest wake-EVENT observed at drain, not current state: noted.status: note: upstream PR merged and its checks green, waiting for the tag UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: upstream PR merged and its checks green, waiting for the tag STATUS OUTCOME BACKSTOP (newest captain-facing task event has no covering branch outcome): noted note: upstream PR merged and its checks green, waiting for the tag 
[2b note under live wait, before cadence] watcher stayed quiet for 15s (no wake)
  queue: 
  latest event (last_status_line): note: upstream PR merged and its checks green, waiting for the tag
  wait in force (status_wait_line): captain-held [key=route]: awaiting the routing call
[3 cadence elapses (resurface secs 45)] watcher WOKE after ~22s: stale: live:fm-noted (captain-held 547s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold) 
  queue: stale	live:fm-noted	stale: live:fm-noted (captain-held 547s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)|
  drained: 1789724757	4	stale	live:fm-noted	stale: live:fm-noted (captain-held 547s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold) 
--- appending changed declaration + note: captain-held [key=route]: awaiting the release call
[4 changed declaration] watcher WOKE after ~2s: stale: live:fm-noted (captain-held 2s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold) 
  queue: stale	live:fm-noted	stale: live:fm-noted (captain-held 2s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)|
  drained: 1789724761	5	stale	live:fm-noted	stale: live:fm-noted (captain-held 2s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold) UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: the release call replaced the routing call 
--- appending: working: validation finished, back on the task / note: picking the next step
[5 note after genuinely leaving the wait] watcher WOKE after ~6s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724770	6	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: picking the next step 
  .paused-live_fm-noted present after leaving: no
Evidence: HEAD: paused, live harness pane

Source: HEAD: paused, live harness pane

tmux pane_current_command: grok
=== MODE=paused pane=grok  declaration: paused: waiting on validation run one
[1 first sight of declared wait] watcher WOKE after ~5s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724716	1	stale	live:fm-noted	stale: live:fm-noted 
--- appending: note: upstream PR merged and its checks green, waiting for the tag
[2a note: status write (first-class event)] watcher WOKE after ~3s: signal: <state>/noted.status 
  queue: signal	noted.status	signal: <state>/noted.status|signal	noted.status	signal: <state>/noted.status|
  drained: 1789724720	3	signal	noted.status	signal: <state>/noted.status wake annotation: unread wake-EVENT since last drain, not current state: noted.status: paused: waiting on validation run one wake annotation: latest wake-EVENT observed at drain, not current state: noted.status: note: upstream PR merged and its checks green, waiting for the tag UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: upstream PR merged and its checks green, waiting for the tag STATUS OUTCOME BACKSTOP (newest captain-facing task event has no covering branch outcome): noted note: upstream PR merged and its checks green, waiting for the tag 
[2b note under live wait, before cadence] watcher stayed quiet for 15s (no wake)
  queue: 
  latest event (last_status_line): note: upstream PR merged and its checks green, waiting for the tag
  wait in force (status_wait_line): paused: waiting on validation run one
[3 cadence elapses (resurface secs 45)] watcher WOKE after ~23s: stale: live:fm-noted (paused 45s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
  queue: stale	live:fm-noted	stale: live:fm-noted (paused 45s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)|
  drained: 1789724763	4	stale	live:fm-noted	stale: live:fm-noted (paused 45s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
--- appending changed declaration + note: paused: waiting on validation run two
[4 changed declaration] watcher WOKE after ~6s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724772	5	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: the release call replaced the routing call 
--- appending: working: validation finished, back on the task / note: picking the next step
[5 note after genuinely leaving the wait] watcher WOKE after ~4s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724779	6	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: picking the next step 
  .paused-live_fm-noted present after leaving: no
Evidence: HEAD: captain-held, live harness pane

Source: HEAD: captain-held, live harness pane

tmux pane_current_command: grok
=== MODE=held pane=grok  declaration: captain-held [key=route]: awaiting the routing call
[1 first sight of declared wait] watcher WOKE after ~5s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724716	1	stale	live:fm-noted	stale: live:fm-noted 
--- appending: note: upstream PR merged and its checks green, waiting for the tag
[2a note: status write (first-class event)] watcher WOKE after ~2s: signal: <state>/noted.status 
  queue: signal	noted.status	signal: <state>/noted.status|signal	noted.status	signal: <state>/noted.status|
  drained: 1789724720	3	signal	noted.status	signal: <state>/noted.status wake annotation: unread wake-EVENT since last drain, not current state: noted.status: captain-held [key=route]: awaiting the routing call wake annotation: latest wake-EVENT observed at drain, not current state: noted.status: note: upstream PR merged and its checks green, waiting for the tag UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: upstream PR merged and its checks green, waiting for the tag STATUS OUTCOME BACKSTOP (newest captain-facing task event has no covering branch outcome): noted note: upstream PR merged and its checks green, waiting for the tag 
[2b note under live wait, before cadence] watcher stayed quiet for 15s (no wake)
  queue: 
  latest event (last_status_line): note: upstream PR merged and its checks green, waiting for the tag
  wait in force (status_wait_line): captain-held [key=route]: awaiting the routing call
[3 cadence elapses (resurface secs 45)] watcher WOKE after ~24s: stale: live:fm-noted (captain-held 45s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold) 
  queue: stale	live:fm-noted	stale: live:fm-noted (captain-held 45s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)|
  drained: 1789724762	4	stale	live:fm-noted	stale: live:fm-noted (captain-held 45s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold) 
--- appending changed declaration + note: captain-held [key=route]: awaiting the release call
[4 changed declaration] watcher WOKE after ~6s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724770	5	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: the release call replaced the routing call 
--- appending: working: validation finished, back on the task / note: picking the next step
[5 note after genuinely leaving the wait] watcher WOKE after ~4s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724778	6	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: picking the next step 
  .paused-live_fm-noted present after leaving: no
Evidence: BASE (before fix): paused, dead agent: stale wake right after the note:

Source: BASE (before fix): paused, dead agent: stale wake right after the note:

tmux pane_current_command: zsh
=== MODE=paused pane=zsh  declaration: paused: waiting on validation run one
[1 first sight of declared wait] watcher WOKE after ~3s: stale: live:fm-noted (paused 502s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
  queue: stale	live:fm-noted	stale: live:fm-noted (paused 502s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)|
  drained: 1789724712	1	stale	live:fm-noted	stale: live:fm-noted (paused 502s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) 
--- appending: note: upstream PR merged and its checks green, waiting for the tag
[2a note: status write (first-class event)] watcher WOKE after ~3s: signal: <state>/noted.status 
  queue: signal	noted.status	signal: <state>/noted.status|signal	noted.status	signal: <state>/noted.status|
  drained: 1789724717	3	signal	noted.status	signal: <state>/noted.status wake annotation: unread wake-EVENT since last drain, not current state: noted.status: paused: waiting on validation run one wake annotation: latest wake-EVENT observed at drain, not current state: noted.status: note: upstream PR merged and its checks green, waiting for the tag UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: upstream PR merged and its checks green, waiting for the tag STATUS OUTCOME BACKSTOP (newest captain-facing task event has no covering branch outcome): noted note: upstream PR merged and its checks green, waiting for the tag 
[2b note under live wait, before cadence] watcher WOKE after ~4s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  latest event (last_status_line): note: upstream PR merged and its checks green, waiting for the tag
~/.no-mistakes/evidence/01M2SXM5NDEWK2KY5TG8DDYJMV/live-note-under-wait.sh: line 63: status_wait_line: command not found
  wait in force (status_wait_line): 
[3 cadence elapses (resurface secs 45)] watcher stayed quiet for 62s (no wake)
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724723	4	stale	live:fm-noted	stale: live:fm-noted 
--- appending changed declaration + note: paused: waiting on validation run two
[4 changed declaration] watcher WOKE after ~5s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724796	5	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: the release call replaced the routing call 
--- appending: working: validation finished, back on the task / note: picking the next step
[5 note after genuinely leaving the wait] watcher WOKE after ~6s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724803	6	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: picking the next step 
  .paused-live_fm-noted present after leaving: no
Evidence: BASE (before fix): captain-held, live pane: stale wake right after the note:

Source: BASE (before fix): captain-held, live pane: stale wake right after the note:

tmux pane_current_command: grok
=== MODE=held pane=grok  declaration: captain-held [key=route]: awaiting the routing call
[1 first sight of declared wait] watcher WOKE after ~5s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724715	1	stale	live:fm-noted	stale: live:fm-noted 
--- appending: note: upstream PR merged and its checks green, waiting for the tag
[2a note: status write (first-class event)] watcher WOKE after ~2s: signal: <state>/noted.status 
  queue: signal	noted.status	signal: <state>/noted.status|signal	noted.status	signal: <state>/noted.status|
  drained: 1789724719	3	signal	noted.status	signal: <state>/noted.status wake annotation: unread wake-EVENT since last drain, not current state: noted.status: captain-held [key=route]: awaiting the routing call wake annotation: latest wake-EVENT observed at drain, not current state: noted.status: note: upstream PR merged and its checks green, waiting for the tag UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: upstream PR merged and its checks green, waiting for the tag STATUS OUTCOME BACKSTOP (newest captain-facing task event has no covering branch outcome): noted note: upstream PR merged and its checks green, waiting for the tag 
[2b note under live wait, before cadence] watcher WOKE after ~4s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  latest event (last_status_line): note: upstream PR merged and its checks green, waiting for the tag
~/.no-mistakes/evidence/01M2SXM5NDEWK2KY5TG8DDYJMV/live-note-under-wait.sh: line 63: status_wait_line: command not found
  wait in force (status_wait_line): 
[3 cadence elapses (resurface secs 45)] watcher stayed quiet for 62s (no wake)
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724725	4	stale	live:fm-noted	stale: live:fm-noted 
--- appending changed declaration + note: captain-held [key=route]: awaiting the release call
[4 changed declaration] watcher WOKE after ~5s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724797	5	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: the release call replaced the routing call 
--- appending: working: validation finished, back on the task / note: picking the next step
[5 note after genuinely leaving the wait] watcher WOKE after ~5s: stale: live:fm-noted 
  queue: stale	live:fm-noted	stale: live:fm-noted|
  drained: 1789724805	6	stale	live:fm-noted	stale: live:fm-noted UNREAD STATUS (new since last drain, not re-printed after this presentation): noted note: picking the next step 
  .paused-live_fm-noted present after leaving: no
Evidence: Key contrast (paused, dead agent)
HEAD: [2b note under live wait, before cadence] watcher stayed quiet for 15s (no wake)
latest event (last_status_line): note: upstream PR merged and its checks green, waiting for the tag
wait in force (status_wait_line): paused: waiting on validation run one
[3 cadence elapses] watcher WOKE: stale: live:fm-noted (paused 551s, awaiting external ...)
BASE: [2b note under live wait, before cadence] watcher WOKE after ~4s: stale: live:fm-noted

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 7 scenarios driven live against the product
Scenario Result Live Evidence
paused: wait + note: under it (dead agent): the note: wakes firstmate once as a status event, then there is no stale wake before the cadence, and the recheck reports the declaration's age ✅ pass live live-paused-dead-agent.head.log steps 2a/2b/3: signal wake, then quiet for 15s, then a recheck 'paused 551s' once the cadence elapsed; the base log wakes stale 4s after the note:
captain-held: wait + note: under it (dead agent): no stale wake before the cadence, and the recheck reports the declaration's age ✅ pass live live-held-dead-agent.head.log steps 2b/3: quiet for 15s, then 'captain-held 547s' recheck
paused: and captain-held: + note: under a live harness pane: no stale wake before the cadence ✅ pass live live-paused-live-agent.head.log and live-held-live-agent.head.log step 2b quiet; live-held-live-agent.BASE.log step 2b wakes stale
Adversarial: a note: that carries legacy captain tokens ('merged', 'checks green') under the wait does not take the terminal-stale path ✅ pass live The live-drive note text is 'upstream PR merged and its checks green, waiting for the tag' and no wake follows it on HEAD; FM_TEST_ONLY=test_stale_is_terminal_classifier passes on HEAD and fails on ba…
Changed declaration (with a note: after it) wakes at once for paused: and captain-held: ✅ pass live Step 4 in all four HEAD logs: watcher woke in 2 to 6s ('paused 2s' / 'captain-held 2s' on the dead-agent path)
note: after the worker genuinely left the wait (working: then note:) wakes as today and clears pause tracking ✅ pass live Step 5 in all four HEAD logs: a plain stale wake in 4 to 6s, and '.paused-live_fm-noted present after leaving: no'
A note: stays a first-class status event (#3753 composition): the latest event is still the note:, and firstmate sees it as unread status ✅ pass live Step 2a signal wake with 'UNREAD STATUS ... noted note: upstream PR merged ...'; last_status_line prints the note: while status_wait_line prints the wait
  • FM_TEST_ONLY=test_note_under_declared_wait_keeps_the_wait bash tests/fm-watch-triage.test.sh (passes on HEAD; fails on base 9bc051ff with '[paused-dead] a note: under the declared wait woke the supervisor')
  • FM_TEST_ONLY=test_stale_is_terminal_classifier bash tests/fm-watch-triage.test.sh (passes on HEAD; fails on base with 'a note: mentioning a legacy token ended a declared wait as terminal')
  • FM_TEST_ONLY=test_status_wait_line_looks_through_notes_only bash tests/fm-watch-triage.test.sh
  • FM_TEST_ONLY=test_absorbed_wait_cadence_survives_a_status_write_that_keeps_the_wait bash tests/fm-watch-triage.test.sh
  • FM_TEST_ONLY=test_live_identical_wait_declared_again_after_leaving_it_surfaces bash tests/fm-watch-triage.test.sh
  • Live drive live-note-under-wait.sh &lt;root&gt; paused zsh: real bin/fm-watch.sh, fm-crew-state.sh and fm-wake-drain.sh against a real tmux server on a private -L socket, dead-agent pane
  • Live drive live-note-under-wait.sh &lt;root&gt; held zsh (captain-held, dead agent)
  • Live drive live-note-under-wait.sh &lt;root&gt; paused grok (paused, live harness pane)
  • Live drive live-note-under-wait.sh &lt;root&gt; held grok (captain-held, live harness pane)
  • Same live drive against the base-commit tree (paused/zsh and held/grok) for before/after contrast
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…e status file

A status write that left a paused: or captain-held: wait as declared (a
repeated identical line, continuation prose) changed the status file
signature the re-surface throttle was scoped to, so the watcher re-surfaced
the wait at once and reported its age from the write instead of the
declaration.

Scope the throttle to the latest status event as last_status_line reads it
(verb and reason), and anchor the wait's age on when that declaration was
first seen, recorded beside it in .paused-since-<key>. A changed verb or
reason is a new declaration and still surfaces on first inspection.
…line

status_wait_line in fm-classify-lib.sh owns the declared-wait reading
contract: when the latest status event is a note: and the latest non-note
event declares a paused: or captain-held: wait, that wait is still in
force, with its identity, re-surface cadence, and age. Every site in the
watcher, the away-mode daemon, the push-transition handler, the inactive
reconciler, and crew-state's current-line read uses it, while
last_status_line keeps returning the note: as the latest event for every
other reader.
…ing note: cannot end it

stale_is_terminal gates the watcher's declared-wait branch, and it still
read the latest event, so a note: under a live wait that mentions a legacy
captain token such as `merged` or `checks green` took the terminal path
and woke the supervisor at once. It now reads status_wait_line; with no
wait in force the verdict is unchanged.
@mremond mremond changed the title fix(bin): keep a declared wait in force under a note: line and key its cadence on the declaration fix(bin): keep a declared wait in force under an informational note: line Sep 18, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: triage of this PR.

Outcome: waiting-author (stacked on open #4742; tips have diverged — cannot merge this stack as-is)

contract-class: restore — informational note: was ending a live paused:/captain-held: wait (recheck/age reset). status_wait_line keeps the declared wait in force under note: only; other verbs unchanged. Corrective of broken declared-wait path, not a new default-on surface.

VISION.md

  • One captain one interface: pass — fewer false wait-clears / noise wakes.
  • Authority explicit never inferred: pass — does not invent captain decisions; note: stays status, not consent.
  • Scripts own mechanics: pass — single owner in classify-lib; watch/daemon/push sites read it.
  • Restart non-event: pass — durable status event semantics.
  • Delegation with spine: pass — wait contract preserved under informational notes.
  • Fleet outlives vendor: pass.
  • Scope: pass.

Blocker: body says merge after #4742. Compare tip 70dd82f044c196ef4c8f2c6cf62c31d6a943ac96 vs #4742 9feea610b615b78f62951410b71430b53355e8bf → diverged (this PR ahead of an older #4742 base, behind #4742's current tip). Please rebase this branch onto current #4742 (or wait for #4742 to land, then rebase onto main) and refresh attestation if HEAD moves.
Attestation: MATCH at current HEAD · CI: all green incl. NM SUCCESS · Draft: no · Mergeable: MERGEABLE/CLEAN · workflow-approved: n/a (already running) · Firstmate-flag: no

@mremond

mremond commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Taking your second option: this PR stays as it is and will be rebased onto main through the pipeline once #4742 lands.

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.

2 participants