Skip to content

fix(watch): absorb routine working-progress signals without a busy proof - #7

Merged
withally merged 5 commits into
mainfrom
fm/fm-routine-working-wake-noise-fix-n12
Aug 2, 2026
Merged

fix(watch): absorb routine working-progress signals without a busy proof#7
withally merged 5 commits into
mainfrom
fm/fm-routine-working-wake-noise-fix-n12

Conversation

@withally

@withally withally commented Aug 2, 2026

Copy link
Copy Markdown
Owner

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

  • Watcher signal triagebin/fm-classify-lib.sh gains signal_is_routine_working_progress (with a status_file_kind meta reader) and bin/fm-watch.sh consults 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 the working verb is absorbed on event semantics alone, so unknown or unverified busy state no longer turns routine progress into a queued wake. A persistent secondmate's working: 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.
  • Backend adapter sourcing — adapter loads in bin/fm-backend.sh now go through a new fm_backend_source_file helper 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.2 set -e.
  • Tests and docs — added classifier and end-to-end watcher cases for unknown-busy working progress, secondmate delivery, an all-verb mixed batch, and the bounded three-observation stale resurfacing of a worker left on 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 in tests/fm-test-run.test.sh and tests/fm-watcher-lock.test.sh; and updated docs/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.

  • Outcome: ⚠️ 1 error across 2 runs (2h31m18s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped

Push main to origin, or rebase your branch onto origin/main, before gating.

⚠️ **Review** - 1 info
  • ⚠️ 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 is paused: (bin/fm-watch.sh:950-952), and the heartbeat backstop only rescans captain-relevant statuses (scan_captain_relevant_statuses filters working out, bin/fm-classify-lib.sh:434), so nothing else can surface it. Before this change a secondmate working [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_resolves matches any line carrying corr=<id>, verb-agnostic). Failure scenario: main firstmate routes a marked request; the secondmate replies working [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 from signal_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: with FM_POLL=1 the watcher needs three pane observations (write hash, n=1, n=2), i.e. ~2s of sleeps plus three full poll cycles that each run recorded_windows, a fake-tmux capture, hashing, pause_state_class (which shells out to the fake fm-crew-state.sh), and — on the first cycle, because .last-check does not exist — the check block. Plus date +%s second-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-$key reaching 2, or that .stale-since-$key was 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 - The SIGNAL_GRACE re-scan coalesces every changed signal file across the whole fleet into one batch, and signal_is_routine_working_progress requires the entire batch to be .status files. So a sibling task's .turn-ended marker 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 writes working: step 3 while a Claude sibling ends a turn two seconds later; the batch contains a .turn-ended, signal_crew_provably_working returns 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_kind is a second kind= parser alongside window_kind (bin/fm-watch.sh:210-220), and the two now form complementary halves of one safety argument: status_file_kind decides whether a task is exempt from routine absorb, window_kind decides whether it is exempt from stale detection. They resolve the same meta by different routes (status path vs fm_backend_meta_for_window) and parse differently (grep | tail -1 | cut vs grep | cut). Failure scenario: if the two ever disagree about a task's kind — e.g. a meta whose window= does not resolve back to it, or a future meta format where kind= appears more than once — a task classified secondmate by one and ship by the other is either exempt from both paths (absorbed with no stale backstop) or from neither. Extracting one shared reader (e.g. having window_kind delegate to status_file_kind after mapping window → id) removes the drift surface. Not blocking: both parsers agree for every meta fm-spawn.sh:1611 actually writes.
⚠️ **Test** - 1 error
  • 🚨 tests failed with exit code 1
  • 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:

  • 🚨 tests failed with exit code 1
  • 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"
⚠️ **Document** - 1 info
  • ℹ️ 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.

@withally
withally merged commit 56f5a86 into main Aug 2, 2026
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