fix(watch): absorb routine working-progress signals without a busy proof - #7
Merged
Merged
Conversation
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
Stop routine non-actionable worker progress from producing captain-facing Pi turns and the mandated Captain, shipshape acknowledgement. In ordinary active-session mode, a fresh status batch whose latest events are only working progress must produce no watcher reason, durable wake, or model turn even when Codex busy state is unknown or unverified. Keep done, needs-decision, blocked, and failed immediately actionable; any mixed batch containing an actionable status actionable; and bare turn-end, stopped/finished-without-report, unreadable-worker, stale-worker, wedge, durable queue ordering, seen markers, heartbeat backstop, all supported primary harnesses, and all runtime providers conservative and safe. A worker that writes working and then stops must surface through the existing three-observation stale path, adding at most two FM_POLL intervals or 30 seconds by default. Keep away-mode ownership and classification unchanged unless direct compatibility evidence requires otherwise. Prevent the turn at the producer/classification/delivery boundary rather than changing or hiding the Captain, shipshape wording, and keep worker status discipline sparse without relying on model prose compliance. Treat the documented Firstmate-root Pi launch, active project trust, guard notifications, and registered fm_watch_arm_pi tool as proof that both tracked primary extensions loaded, so launch misconfiguration is not causal. Preserve README Calm semantics: hidden operational inputs remain ordinary user-role messages and Calm only hides presentation, so it cannot suppress an assistant reply after delivery. Treat intermittent missing-cycle alarms as a separate continuity failure unless causal evidence connects them. Add executable regression coverage for unknown-busy working progress, all actionable verbs, mixed batches, bare turn-end, bounded stale resurfacing, and a loaded quiet Pi extension producing no follow-up; update authoritative headers/help and maintained documentation only where behavior changes. Run focused tests, bin/fm-lint.sh, bin/fm-doc-audience-check.sh, and the full watcher supervision family. Ship through no-mistakes and a PR, but do not merge.
What Changed
bin/fm-classify-lib.shgainssignal_is_routine_working_progress(with astatus_file_kindmeta reader) andbin/fm-watch.shconsults it before the costly runtime-state read: in ordinary active-session mode, an ordinary direct report's status-only batch whose every latest event carries theworkingverb is absorbed on event semantics alone, so unknown or unverified busy state no longer turns routine progress into a queued wake. A persistent secondmate'sworking:report is explicitly excluded and stays on the provably-working path, since the stale loop skips an idle secondmate endpoint and the heartbeat rescan only covers captain-relevant statuses. Captain-relevant verbs, mixed batches, bare turn-ends, unknown file shapes, stale/wedge escalation, heartbeat backstop, and away-mode ownership keep their existing classification.bin/fm-backend.shnow go through a newfm_backend_source_filehelper that checks readability before., so a missing or unreadable adapter returns nonzero to its caller instead of aborting the whole shell under stock macOS Bash 3.2set -e.working:; added a Pi extension case asserting a quiet watcher cycle produces no user-role follow-up; replaced two sleep-based races with explicit handshakes intests/fm-test-run.test.shandtests/fm-watcher-lock.test.sh; and updateddocs/architecture.md, the watcher/classifier headers, and the brief status-protocol wording to match the new absorb boundary.Risk Assessment
✅ Low: The follow-up commit closes the one substantive gap exactly as instructed — narrowing routine absorb to ordinary direct reports while restoring the secondmate path verbatim — with matching classifier-level and end-to-end regression coverage, leaving only a non-blocking duplicate-parser cleanup.
Testing
Completed 1 recorded test check.
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
⏭️ **Rebase** - skipped
.agents/skills/afk/SKILL.md- branch carries 164 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (293 file(s)) into the PR:Push main to origin, or rebase your branch onto origin/main, before gating.
bin/fm-watch.sh:905- Absorbing all-working:batches removes the only surfacing path for secondmate reports. The stale loop explicitly skips secondmate windows unless the last status ispaused:(bin/fm-watch.sh:950-952), and the heartbeat backstop only rescans captain-relevant statuses (scan_captain_relevant_statusesfiltersworkingout, bin/fm-classify-lib.sh:434), so nothing else can surface it. Before this change a secondmateworking [key=...]line surfaced whenever the secondmate was not provably working. The charter brief documents that exact line as a sparse, supervisor-actionable material phase report and as a valid correlated answer to a marked from-firstmate request (bin/fm-brief.sh:193-197;fm_pending_reply_line_resolvesmatches any line carryingcorr=<id>, verb-agnostic). Failure scenario: main firstmate routes a marked request; the secondmate repliesworking [key=audit]: findings in data/audit.md corr=<id>and goes idle; the parent absorbs the signal, the pending-reply record resolves, the secondmate window is skipped by the stale loop, and firstmate is never woken to read the answer. The same gap applies to any task whose meta resolves no backend target (recorded_windows, bin/fm-watch.sh:250-262) or whose pane capture fails (line 953|| continue). This also makes the new unconditional claim at docs/architecture.md:22 ("the unchanged-pane stale path still surfaces it after three total pane observations") false for those endpoints, and it contradicts the intent's required behavior that "a worker that writes working and then stops must surface through the existing three-observation stale path". Suggested resolution: either exclude secondmate-kind status files fromsignal_is_routine_working_progress(keeping them on the conservative provably-working path), or scope the doc claim to ordinary crewmate endpoints with a resolvable, capturable window.tests/fm-watch-triage.test.sh:649-[ "$elapsed" -le 4 ]is a wall-clock assertion over a nominally ~3s path: withFM_POLL=1the watcher needs three pane observations (write hash, n=1, n=2), i.e. ~2s of sleeps plus three full poll cycles that each runrecorded_windows, a fake-tmux capture, hashing,pause_state_class(which shells out to the fakefm-crew-state.sh), and — on the first cycle, because.last-checkdoes not exist — the check block. Plusdate +%ssecond-granularity truncation adds up to 1s of error in either direction. Failure scenario: the watcher-supervision test family runs on a loaded or parallel CI machine, the three cycles take 4.2s, and the test fails with "stopped worker exceeded the three-observation stale bound" even though classification is correct. Assert the observation count (e.g..count-$keyreaching 2, or that.stale-since-$keywas never written so no wedge timer was used) rather than elapsed seconds, or raise the bound well clear of the nominal path.bin/fm-watch.sh:877- TheSIGNAL_GRACEre-scan coalesces every changed signal file across the whole fleet into one batch, andsignal_is_routine_working_progressrequires the entire batch to be.statusfiles. So a sibling task's.turn-endedmarker landing inside the 30s grace window (Claude, Pi, OpenCode, Grok and Kimi all arm turn-end hooks in bin/fm-spawn.sh) demotes the batch to the ambiguous path, where any task with an unverified busy source (Codex) forces a surface. Failure scenario: a Codex crewmate writesworking: step 3while a Claude sibling ends a turn two seconds later; the batch contains a.turn-ended,signal_crew_provably_workingreturns 1 for the Codex task, and the routine progress produces a model turn anyway. This is the conservative behavior the intent requires for bare turn-end, so no change is implied — noting it because it bounds how much of the reported noise the fix actually removes on multi-task fleets.bin/fm-classify-lib.sh:373- The change renames the "no-verb signal" concept to "ambiguous signal" across the watcher and classifier headers and docs/architecture.md, but two references were missed:crew_is_provably_working's own doc comment still says "a no-verb turn-end or stale wake is absorbed ONLY when this returns 0" (bin/fm-classify-lib.sh:373), and docs/architecture.md:26 still says a declared pause's "initial normal-mode status signal still surfaces through the no-verb path" while bin/fm-watch.sh's header for the same sentence was updated to "ambiguous status signal". Failure scenario: a future reader greps for "no-verb" to find the classification boundary, lands on these two, and concludes the retired predicate name is still the live contract. Both are one-line wording updates.🔧 Fix: scope routine working absorb to ordinary tasks
1 info still open:
bin/fm-classify-lib.sh:327-status_file_kindis a secondkind=parser alongsidewindow_kind(bin/fm-watch.sh:210-220), and the two now form complementary halves of one safety argument:status_file_kinddecides whether a task is exempt from routine absorb,window_kinddecides whether it is exempt from stale detection. They resolve the same meta by different routes (status path vsfm_backend_meta_for_window) and parse differently (grep | tail -1 | cutvsgrep | cut). Failure scenario: if the two ever disagree about a task's kind — e.g. a meta whosewindow=does not resolve back to it, or a future meta format wherekind=appears more than once — a task classifiedsecondmateby one andshipby the other is either exempt from both paths (absorbed with no stale backstop) or from neither. Extracting one shared reader (e.g. havingwindow_kinddelegate tostatus_file_kindafter mapping window → id) removes the drift surface. Not blocking: both parsers agree for every metafm-spawn.sh:1611actually writes.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"🔧 Fix: guard missing backend adapter source; deflake two timing races
1 error still open:
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"docs/architecture.md- Scoping judgment call, disclosed rather than fixed. The supplied base commit (80ecc8d) is 169 commits behind the target and is the merge-base with a stale origin/main mirror, so a literal base..target diff spans 296 files and ~80k inserted lines of already-landed PR work. I scoped this documentation pass to the 4 branch commits on top of cd73e75 (the last landed PR fix(bin): retire terminal process events and surface queued wakes kunchenguid/firstmate#1500), which are the commits the stated intent describes: the routine working-progress absorb (def2350), its Pi delivery-boundary test (eaad40d), the ordinary-task scoping review fix (36a3050), and the adapter-source guard plus test deflakes (b3b33b2). Documentation owned by the 165 earlier commits was treated as already current and was not re-audited. If the intent really was to re-audit the full divergence from published main, that is a much larger pass and should be commissioned separately.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.