feat(watcher): add paused external-wait supervision#421
Merged
Conversation
A crew (or firstmate steering it) can declare a deliberate wait on a known external dependency with a paused: <reason> status. Both the always-on watcher and the away-mode daemon absorb such an idle pane through shared fm-classify-lib.sh vocabulary instead of tripping the possible-wedge stale escalation, and re-surface it for a recheck only on a long bounded cadence (FM_PAUSE_RESURFACE_SECS) so a forgotten pause cannot rot invisibly. fm-crew-state.sh reports state: paused distinctly. A crew that goes idle without declaring a pause classifies exactly as before. Docs and brief scaffold state lists updated; tests colocated.
b848bce to
189bdfe
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Add a paused/awaiting-external crew state so an in-flight crewmate intentionally idling on a KNOWN external wait (an upstream tool release, a vendor rate-limit reset, a scheduled window) does not trip the possible-wedge stale escalation like a genuine wedge. This was a deferred item from the 2026-07-07 herdr incident (docs/herdr-backend.md), reproduced live on 2026-07-09/10 in this home when a crew held parked awaiting an upstream release and its idle pane tripped repeated 'stale ... possible wedge' escalations all day.
Design decisions and tradeoffs a diff-only reviewer would not know:
This is a firstmate-repo change; per its no-mistakes contract .no-mistakes/ stays gitignored and CI rejects tracked .no-mistakes paths.
What Changed
paused:crew status for declared external waits, including distinct paused reporting in crew-state output and generated briefs.Risk Assessment
Testing
The supplied baseline full suite had already passed; focused daemon and crew-state suites passed, and controlled production watcher/daemon/tmux scenarios demonstrated that declared pauses suppress wedge escalation, re-surface as awaiting-external checks, and retain actionable AFK daemon handoff. Reviewer-visible transcripts are recorded above.
Evidence: AFK paused-state end-to-end transcript
AFK daemon self-handledstale: crew:fm-external-waitas paused, injected bounded awaiting-external rechecks, and emitted no wedge marker or possible-wedge escalation.Evidence: Watcher pause re-surface transcript
A fresh declared pause was absorbed, then re-surfaced after the shortened pause cadence as awaiting external, never as a wedge.Evidence: Crew-state paused output transcript
Crew-state output reports the declared paused state and reason distinctly.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed (7) ✅
bin/fm-watch.sh:601- Pause mode is decided only for a new pane hash. If firstmate appendspaused:to an already-stale, unchanged pane,sf == hbypassescrew_absorb_classand retains the wedge timer, producing apossible wedge; AFK similarly retains its existing stale marker because no new stale wake is emitted. Reclassify or reset stale state when status enters or leaves pause.bin/fm-crew-state.sh:129- The configurable pause verb is not honored here.status_is_pausedacceptsFM_CLASSIFY_PAUSED_VERB, but this fallback maps only literalpaused, so a custom verb is paused in the daemon but becomesstate: unknownfor the always-on watcher. Use the shared configured verb or helper.🔧 Fix: Captain: fix paused-state transitions
3 errors still open:
bin/fm-watch.sh:657- A paused pane whose output changes every poll never reacheshandle_paused_stale: this branch clears the pause flag and resets the stale hash. A ticking clock or token counter, explicitly meant not to reset pause cadence, can therefore prevent any bounded re-surface.bin/fm-supervise-daemon.sh:906- AFK mode records a pause marker only for astale:wake. A newpaused:append arrives as a routinesignal:, and if the watcher already has the same stale hash from before AFK it emits no later stale wake. The daemon then never performs the promised long-cadence recheck.bin/fm-watch.sh:629- Once a pause flag exists, this path trusts the append-onlypaused:tail without consultingcrew_absorb_class. If the crew starts a validation run without another status append and later becomes idle, the active run is hidden on the hour-long pause cadence instead of returning to wedge tracking, despitefm-crew-state.shtreating run state as authoritative.🔧 Fix: Captain: fix paused-state supervision transitions
3 errors still open:
bin/fm-supervise-daemon.sh:479- When a paused task appends a non-paused status such asworking:, this removes.subsuper-paused-*without restoring normal stale tracking. The watcher has already suppressed the unchanged idle hash, so AFK housekeeping has no marker to recheck and a resumed-but-idle task can evade wedge escalation indefinitely.bin/fm-supervise-daemon.sh:726- Entering AFK after normal-mode pause absorption does not seed.subsuper-paused-*. The retained watcher stale hash prevents a new stale wake, while housekeeping only scans daemon pause markers, so the promised long-cadence recheck never begins.bin/fm-brief.sh:154- Secondmates are instructed to usepaused:, but normal-mode watcher processing skipskind=secondmatebefore pause handling. Their initial signal surfaces once, then no periodic pause recheck is possible outside AFK.🔧 Fix: Captain: fix paused supervision handoffs
3 errors still open:
bin/fm-supervise-daemon.sh:474- A terminal transition such aspaused:->done:orblocked:replaces the pause marker with a normal stale marker. The signal escalates, but this marker remains and housekeeping later emits a falsepossible wedgefor the intentionally idle terminal pane. The watcher-marker migration has the same outcome.bin/fm-watch.sh:554- A secondmate that leavespaused:is skipped before its existing watcher pause markers are cleared. If AFK begins later, migration converts that stale marker into normal stale tracking and the healthy idle secondmate is escalated as a possible wedge.bin/fm-supervise-daemon.sh:485- AFK handoff only migrates an existing watcher.paused-*flag. If a pause signal was surfaced in normal mode before the next stale poll creates that flag, and the pane hash was already suppressed, AFK receives neither a new stale wake nor a daemon pause marker. The declared wait then never gets its bounded recheck.🔧 Fix: Reconcile paused supervision markers
1 error still open:
bin/fm-classify-lib.sh:73-status_is_captain_relevantapplies its unanchored patterns to the entire status line before pause handling. A valid reason such aspaused: waiting for upstream checks greenbecomes captain-relevant: AFK signal handling escalates it, while normal stale handling takes the terminal path before pause absorption. Give a leading pause verb precedence over captain-relevance so a pause's behavior cannot depend on its reason text.🔧 Fix: Captain: prioritize paused states over captain relevance
1 error still open:
bin/fm-watch.sh:675- When the status remainspaused:but the authoritative reader reportsworking, this repeat-stale path clears pause tracking and recreates.stale-since-*on every poll. A frozen unchanged active run therefore never reachesFM_STALE_ESCALATE_SECS; preserve the wedge timer across these periodic working rechecks.🔧 Fix: Captain: preserve paused-working wedge timer
1 warning still open:
bin/fm-brief.sh:155- The classifier supportsFM_CLASSIFY_PAUSED_VERB, but all generated briefs still instruct crews to append literalpaused:. With a configured verb such asawaiting, crews emit an unrecognized state and their declared wait can resume normal stale/wedge handling. Render the configured verb in every brief scaffold.🔧 Fix: Captain: honor configured pause verb in briefs
✅ Re-checked - no issues remain.
🔧 **Test** - 1 issue found → auto-fixed ✅
bin/fm-watch.sh:708- In AFK mode, the changed paused-pane path runs before the watcher’s one-shot daemon handoff, suppressing that handoff. Its later re-surface sendsstale: <window> (paused ...); the daemon treats the decorated text as the window identity, cannot find status, and drops it as transient stale. This leaves paused AFK triage incomplete.command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"Provided successful baseline:command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"bin/fm-session-start.shbash tests/fm-watch-triage.test.shbash tests/fm-daemon.test.shbash tests/fm-crew-state.test.shbash tests/fm-brief.test.shReal-tmux normal paused watcher scenario with shortFM_PAUSE_RESURFACE_SECS, plusbin/fm-crew-state.shandbin/fm-wake-drain.shevidence captureReal-tmux AFK watcher scenario verifying the expected daemon handoff is absentReal-tmuxbin/fm-supervise-daemon.shscenario verifying the decorated paused re-surface is rejected as unresolved transient stale🔧 Fix: Captain: fix AFK paused watcher handoff
✅ Re-checked - no issues remain.
command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"Baseline full suite supplied by the task:command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"(previously successful)bash tests/fm-daemon.test.shbash tests/fm-crew-state.test.shbash tests/fm-watch-triage.test.sh- all scheduled assertions, including paused AFK handoff, appeared in the captured log withoutnot ok; the runner did not return its final wrapper exit lineProductionbin/fm-supervise-daemon.shwith a production watcher child and controlled idle tmux pane in AFK modeProductionbin/fm-watch.shwith controlled pause absorption and bounded re-surfacingbin/fm-crew-state.sh state-waitwith controlled paused status✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.