Conversation
…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.
…nd-return test honest
…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.
|
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 VISION.md
Blocker: body says merge after #4742. Compare tip |
|
Taking your second option: this PR stays as it is and will be rebased onto main through the pipeline once #4742 lands. |
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(thestale_is_terminalsite), and70dd82f0(documentation).Reading contract
status_wait_lineinbin/fm-classify-lib.shis the one owner.When the latest status event is a
note:and the latest event that is not anote:declares apaused:orcaptain-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 aslast_status_linereads it.It composes with #3753:
last_status_linestill returns thenote:as the latest event for every other reader, thenote: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 inbin/fm-supervise-daemon.sh,bin/fm-push-transition-lib.sh, the captain-held skip inbin/fm-inactive-reconcile.sh, andstatus_current_line(the crew-state reading).Residue
note:written under a livepaused:orcaptain-held:wait ending the wait in the watcher's view - the immediate recheck and the age reset - including anote:that mentions a legacy captain token such asmergedorchecks green.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 thannote: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.note:carrying a legacy captain token as captain-relevant; that is about what anote:means in general, not about whether it ends a wait.Exercised end to end versus reasoned
bin/fm-watch.shwith real status files and real timing and the realbin/fm-crew-state.shreader, forpaused:andcaptain-held:, each with a dead-agent and a live-agent pane: a token-bearingnote: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), andworking:thennote:(wakes as before). These tests fail on the code before this change. Crew-state's paused reading under anote:is also tested against the real reader.note:under a wait through those paths.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
status_wait_linetobin/fm-classify-lib.shas the single owner of the declared-wait reading. When the latest event is anote:and the most recent non-note:event is apaused:orcaptain-held:wait, it returns that wait. In every other case it returns the same line aslast_status_line. Anote: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_scangained askip-notesmode to support this.status_wait_line. This covers the watcher infm-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 infm-supervise-daemon.sh. It also coversstale_is_terminal,status_current_line(the crew-state fallback),fm-inactive-reconcile.shandfm-push-transition-lib.sh. Result: anote:under a live wait no longer triggers an immediate recheck. Anote:written after the worker left the wait still wakes as before, and a changed declaration still surfaces at once.stale_wait_declaration) and a newdeclared_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.mddocuments 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 covernote:under a livepaused:orcaptain-held:wait, anote: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 anote: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 bothpaused:andcaptain-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.
Evidence: Live driver script (real tmux + real fm-watch.sh)
Source: Live driver script (real tmux + real fm-watch.sh)
Evidence: HEAD: paused, dead agent
Source: HEAD: paused, dead agent
Evidence: HEAD: captain-held, dead agent
Source: HEAD: captain-held, dead agent
Evidence: HEAD: paused, live harness pane
Source: HEAD: paused, live harness pane
Evidence: HEAD: captain-held, live harness pane
Source: HEAD: captain-held, live harness pane
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:
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:
Evidence: Key contrast (paused, dead agent)
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.
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.shFM_TEST_ONLY=test_absorbed_wait_cadence_survives_a_status_write_that_keeps_the_wait bash tests/fm-watch-triage.test.shFM_TEST_ONLY=test_live_identical_wait_declared_again_after_leaving_it_surfaces bash tests/fm-watch-triage.test.shLive drivelive-note-under-wait.sh <root> 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 paneLive drivelive-note-under-wait.sh <root> held zsh(captain-held, dead agent)Live drivelive-note-under-wait.sh <root> paused grok(paused, live harness pane)Live drivelive-note-under-wait.sh <root> 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.