fix(bin): stop rechecking a declared pause an armed merge poll already covers - #2750
fix(bin): stop rechecking a declared pause an armed merge poll already covers#2750Parsunic wants to merge 5 commits into
Conversation
A task that declares `paused:` is re-surfaced every FM_PAUSE_RESURFACE_SECS so a forgotten wait cannot rot invisibly, and each of those wakes costs firstmate a full model turn. For a task whose PR merge is already being polled that recheck answers nothing: the validated poll bin/fm-pr-check.sh arms prints exactly one line the moment the PR merges and stays silent on every other outcome, errors included. Observed 2026-08-20, a task parked on an upstream PR with that poll armed spent eight consecutive hourly wakes confirming "still open, nothing changed". fm-classify-lib.sh gains one shared predicate, pause_recheck_covered_by_merge_poll, that both supervisors consult when a recheck comes due. It asks fm-pr-lib.sh's own fm_pr_poll_artifacts_valid rather than re-deciding trust, so only a fully validated poll for that exact task suppresses anything: an unregistered check, a registered custom check, or a poll whose trust binding no longer matches all keep the ordinary recheck. Neither supervisor advances its throttle when suppressing, so a lapse in coverage re-surfaces on the next poll rather than a window later. Suppression is recorded rather than silent. state/.<id>.pause-poll-covered names the covering PR and the moment the wait went quiet, the session-start fleet digest prints it under that task, and the predicate withdraws it as soon as coverage lapses, so a quiet wait reads as deliberately quiet instead of a recheck that broke.
…stead of swallowing them
…unmerged poll outcome
|
Speaking as Kun's firstmate: Corrective — suppress the bounded VISION: aligns (scripts own the poll; agent turns are not spent re-asking; a lost wait is worse than a redundant one). Residual info, not a hold: if the coverage note cannot be written the predicate still returns yes, so an unannounceable suppression can go quiet. Security: no. Overlap (do not land together): semantic conflict with #2749 and #2748. All three rewrite This is waiting on the author, not the captain: merge is CONFLICTING / DIRTY (ahead 5, behind 6). Rebase onto current main and push. Fork CI has not appeared on this HEAD yet ( |
|
Speaking as Kun's firstmate: Corrective intent: a declared pause plus an armed Not merge-eligible: CONFLICTING / DIRTY, no checks on this HEAD, and file-level overlap with #2748 (watch, daemon, classify, afk skill). #2748 is landing first (green captain-hold cadence). Do not resolve these conflicts until this PR is otherwise auto-merge-ready (green CI, no-mistakes, no remaining product ambiguity). Waiting on the author to rebase after #2748 — not waiting on the captain. |
Intent
Stop firstmate re-asking whether a declared external wait still holds when that same task already has an armed merge poll that would tell it.
Problem, with evidence: a task can declare
paused:for a known external wait. The watcher re-surfaces that pause every FM_PAUSE_RESURFACE_SECS (3600 default) as astale:wake reading "awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds". Handling it costs firstmate a full model turn. For a task waiting on a PR merge that is redundant: bin/fm-pr-check.sh arms state/.check.sh, a static validated poll that emits exactly one line when the PR merges and stays silent otherwise, including on every error. It cannot miss the merge and costs nothing while waiting. Observed 2026-08-20: fm-context-restart-handoff was parked on upstream PR 2606 with that poll armed; the pause recheck fired every hour for eight consecutive hours with an otherwise empty fleet, and every time the answer was "still open, nothing changed". A second such task exists (fm-mathpath-worktree-command), and upstream PRs can sit for weeks.What to change: when a task's declared pause coexists with an armed, validated merge poll for that same task, suppress the pause recheck for it and let the poll be the wake. Keep the recheck for a declared pause with NO armed poll - that case is exactly what it is for, since firstmate has no other way to learn the wait cleared, and losing it would turn a bounded wait into a silent one.
bin/fm-classify-lib.sh owns the shared classification predicates used by both the always-on watcher and the away-mode daemon, so the change must hold for both modes: the two must not diverge, and the away-mode daemon's own pause tracking and FM_PAUSE_RESURFACE_SECS housekeeping has to agree with whatever the watcher does.
Design constraints:
Definition of done:
This is firstmate's own shared tracked material, so the firstmate-coding-guidelines skill's rules apply: knowledge-placement decision tree, one-owner rule, AGENTS.md size discipline, one sentence per line in tracked Markdown, plain dash rather than em dash, no agent commit co-author, shellcheck-clean bin scripts via bin/fm-lint.sh, and tests colocated in tests/ extending existing scripts rather than inventing a new runner.
What Changed
bin/fm-classify-lib.shgainspause_recheck_covered_by_merge_poll, a shared predicate that drops theFM_PAUSE_RESURFACE_SECSrecheck for a declaredpaused:wait only whenbin/fm-pr-lib.shpositively validates that same task's armed merge poll. Both supervisors call it -bin/fm-watch.shinhandle_paused_stale(offered to the declared external-wait verb only, never a captain-held transfer, and leaving the.paused-resurfaced-*throttle unadvanced) andbin/fm-supervise-daemon.shin housekeeping (leaving the pause marker unreset), so away mode and always-on agree and any lapse resumes the recheck on the next cycle.state/.<id>.pause-poll-covered, a private one-line note naming the covering PR and the moment the wait went quiet, whichbin/fm-session-start.shprints under that task in the fleet digest aspause recheck: .... So instead of an hourlystale:wake reading "confirm the wait still holds", a supervisor sees one standing digest line saying the recheck is suppressed and which armed poll covers it. The note is withdrawn by both supervisors when the pause stops being declared and bybin/fm-teardown.sh, so it never outlives its wait.bin/fm-pr-poll.shnow also printsclosed-unmergedfor a PR/MR the forge itself reports closed,bin/fm-pr-lib.shrecords it as the identity-bound privatestate/<id>.pr-poll-terminal, andbin/fm-watch.shdrops that result instead of waking - falling back to an ordinarycheck:wake when the record cannot be written.bin/fm-pr-check.shclears any stale record when rearming. Docs updated indocs/architecture.md,docs/configuration.md,docs/gitlab-merge-watch.md(whoseclosedhalf is labelled unverified on this host),AGENTS.md, and the afk skill; tests extendtests/fm-watch-triage.test.sh,tests/fm-session-start.test.sh, andtests/fm-pr-check-security.test.sh.Risk Assessment
✅ Low: The change is well-bounded and satisfies every source-verifiable intent criterion - suppression is narrowed to the declared external-wait verb at the single decision point, the trust anchor is no longer environment-overridable, coverage lapses only on a positively observed terminal non-merged state, both supervisors share one predicate, and the behaviour is pinned by tests that drive real watcher and daemon processes; the three remaining items are informational edge cases that all fail toward extra work rather than lost wakes.
Testing
I read the change, ran the three directly relevant suites (watch-triage, pr-check-security, daemon) - all clean, including the seven new cases covering suppression, the no-poll and untrusted-check disconfirmations, captain-held narrowing, the closed-without-merging lapse, and away-mode agreement - then demonstrated the intent end-to-end by driving the real watcher, away-mode daemon, PR-check arming and session-start digest over the reported 2026-08-20 fixture at both the base commit and this change: the base woke firstmate on 3 of 3 hourly windows with the redundant "confirm the wait still holds" recheck, this change woke it on 0 of 3 and left a coverage note, a triage line, and a "pause recheck:" line in the fleet digest instead, while a pause with no poll, a pause whose registered check.sh is not the validated merge poll, and a covering PR observed closed without merging each kept or resumed the ordinary recheck in both supervision modes. The one problem I hit is not in this change: tests/fm-session-start.test.sh aborts on two cases that fail identically on the base commit on this host (an ambient /usr/bin/node defeats a "MISSING: node" fixture, and a Herdr husk-recovery case emits an unexpected SECONDMATE_LIVENESS line), so I verified the change's own new digest case in isolation and raised the suite's non-hermetic fixtures as a warning. This is a CLI/supervision surface with no rendered UI, so the reviewer-visible evidence is a command transcript rather than a screenshot.
Evidence: Before/after transcript: hourly pause recheck vs. poll-covered silence
Source: Before/after transcript: hourly pause recheck vs. poll-covered silence
BEFORE (base commit 1cb900c) hour 1: firstmate is woken: > stale: fm:fm-context-restart-handoff (paused 5003s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) hour 2: ... same wake ... hour 3: ... same wake ... => 3 of 3 hours woke firstmate, every answer 'still open, nothing changed'. AFTER (this change) hour 1/2/3: firstmate is not woken (watcher still absorbing) => 0 of 3 hours woke firstmate. What a supervisor sees instead of the hourly wake: $ cat state/.fm-context-restart-handoff.pause-poll-covered declared-wait recheck suppressed since 2026-08-21T06:18:58Z: the armed merge poll for #2606 covers this wait and wakes firstmate when that PR merges $ grep 'suppressed paused recheck' state/.watch-triage.log | tail -3 [2026-08-21T02:19:38-0400] suppressed paused recheck (armed merge poll #2606 covers the wait, age 5043s): fm:fm-context-restart-handoff $ fm-session-start.sh (fleet digest, the first thing firstmate reads) --- fm-context-restart-handoff --- window=fm:fm-context-restart-handoff kind=ship pr=#2606: dead (backend=tmux window=fm:fm-context-restart-handoff) pause recheck: declared-wait recheck suppressed since 2026-08-21T06:18:58Z: the armed merge poll for #2606 covers this wait and wakes firstmate when that PR merges AFTER declared pause with NO armed poll - the recheck this is for hour 1: firstmate is woken: > stale: fm:fm-context-restart-handoff (paused 5002s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) coverage note written? no - nothing claims a suppression AFTER declared pause whose check.sh is registered but is NOT the merge poll hour 1: firstmate is woken: > stale: fm:fm-context-restart-handoff (paused 5002s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) coverage note written? no - an untrusted check suppresses nothing AFTER the covering PR is CLOSED without merging - coverage must lapse hour 1 (forge says the PR is still open): firstmate is not woken (watcher still absorbing) coverage note: present hour 2 (forge now says the PR was closed without merging): firstmate is woken: > stale: fm:fm-context-restart-handoff (paused 5019s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds) coverage note: withdrawn - the poll will never speak again durable terminal observation: fm-pr-poll-terminal-v1 fm-context-restart-handoff #2606 closed-unmerged AFTER away mode (bin/fm-supervise-daemon.sh) on the same three fixtures poll-covered pause: nothing escalated pause with no poll: escalated to firstmate: > paused 5000s (awaiting external, recheck whether the wait still holds): fm:fm-context-restart-handoff check.sh that is not the merge poll: escalated to firstmate: > paused 5000s (awaiting external, recheck whether the wait still holds): fm:fm-context-restart-handoff Both supervisors agree on all three fixtures.Evidence: Reproduction harness for the transcript above
Source: Reproduction harness for the transcript above
Self-contained demo driving the real bin/ scripts (fake tmux, fake fm-crew-state, fake gh only). Usage: pause-recheck-demo.sh <target-root> <base-root> <out-dir>, where <base-root> isgit archive 1cb900c | tar -x -C $(mktemp -d).Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-watch.sh:359- Suppression is unbounded, and the covering poll only ever speaks on a merge, so a wait that ends any other way goes silent forever. bin/fm-pr-poll.sh printsmergedonly when state == MERGED and exits 0 silently on every other path (closed-unmerged,ghauth expiry,gh/glabmissing from PATH, renamed or deleted repo). Concrete sequence: crew writespaused: awaiting upstream PR 2606, fm-pr-check.sh arms the validated poll, the recheck is suppressed here, then PR 2606 is closed without merging. The poll stays silent permanently; the recheck never fires again becauseageandrf_ageboth stay above the window; and no other path can surface it -status_is_captain_relevant(bin/fm-classify-lib.sh:112) returns 1 forpaused, so heartbeat_scan_finds_actionable never sees it, and fm-inactive-reconcile.sh only records done/failed crew states. On the 'otherwise empty fleet' the intent describes, there is also no other wake to trigger a session start that would print the coverage note. The task is parked indefinitely with no wake path, which is exactly the outcome the intent's 'a lost one costs a stalled task nobody notices' guardrail is aimed at. The intent authorizes suppressing the hourly cadence, but does not say the wait may become unbounded; a finite outer backstop (for example re-surface once per N x FM_PAUSE_RESURFACE_SECS even while covered, or treat a terminal non-merged PR state as coverage lapse) would keep the redundancy gone while keeping the wait bounded. Your call on whether indefinite silence is the intended trade.bin/fm-watch.sh:387- The suppression is scoped tohandle_paused_stale, which servescaptain-held:as well aspaused:, so it silences a recheck the intent never asked to touch. The intent scopes the change to 'a task's declared pause': 'when a task's declared pause coexists with an armed, validated merge poll for that same task, suppress the pause recheck for it'. But this call site gates onstatus_is_paused_or_captain_held, and so do lines 1168 and 1211 (viapause_state_classreturningpausedon a confidently dead agent). Concrete path: fm-decision-hold.sh:516 appendscaptain-held [key=<key>]: tracked by <hold>as a task's last status line; if that same task already had a merge poll armed for its own PR, its FM_PAUSE_RESURFACE_SECS 'confirm the wait still holds' recheck now disappears. A merge poll reports on the PR merging - it says nothing about whether the captain acted on the held decision, andstatus_is_captain_relevantalso returns 1 forcaptain-held, so there is no heartbeat backstop either. The away-mode daemon does not do this (its housekeeping loop at bin/fm-supervise-daemon.sh:1047 gates onstatus_is_paused, and classify_stale routescaptain-heldto a transient self-handle), so the two modes now answer this case differently. Narrowing the suppression tostatus_is_pausedwould match the stated scope; if silencing captain-held was deliberate, it needs to be stated, since the poll cannot answer that wait.bin/fm-classify-lib.sh:178-FM_CLASSIFY_PR_POLL_TEMPLATEmakes the trust anchor environment-overridable, and nothing uses the hook. The template is whatfm_pr_poll_artifacts_validbyte-compares check.sh against (cmp -s "$template" "$check", bin/fm-pr-lib.sh:594) - it is the whole of the 'byte-identical poll source' guarantee that design constraint 1 relies on ('A check.sh that ... is not the validated PR poll ... must not suppress anything. Getting this wrong turns a real wedge into silence'). An exported value redirects that comparison at any file, so an arbitrary registered custom check plus matching state/ sidecars would read as a validated poll and suppress the recheck. It also lets suppression disagree with execution: the watcher's own check sweep pins the hardcoded$SCRIPT_DIR/fm-pr-poll.sh(bin/fm-watch.sh:945), so a redirected template could mark a check 'covered' that the sweep then runs as a plain custom check or rejects. The comment says the hook 'lets a test point at a fixture copy', but no test in the repo sets it - both new tests arm through the real bin/fm-pr-poll.sh. Pin it to$_FM_CLASSIFY_LIB_DIR/fm-pr-poll.shunconditionally.tests/fm-watch-triage.test.sh:948- The away-mode half of the suppression test asserts only that.subsuper-escalationsis empty, which does not distinguish suppression from the daemon skipping the pause for an unrelated reason. In bin/fm-supervise-daemon.sh's pause loop, an empty escalations file also results when suppression does NOT fire andstale_window_is_busyreturns 0 or 2 - both of thoserm -f "$marker"and escalate nothing. So the assertion would still pass ifpause_recheck_covered_by_merge_pollregressed to returning 1, which is precisely the regression the intent's 'the away-mode and always-on paths agree' requirement is meant to pin. The suppression path is uniquely identifiable: itcontinues with the marker deliberately un-reset, while both drop paths remove it and the escalate path writes to.subsuper-escalations. Add[ -e "$state/.subsuper-paused-$dkey" ] || fail ...alongside the existing negative assertion.bin/fm-classify-lib.sh:226- Because neither supervisor advances a throttle after suppressing, the fullfm_pr_poll_artifacts_validre-runs on every cycle for the lifetime of the wait. Per call that is roughly 3xfm_pr_private_file_valid(each 3 command substitutions overuname+stat), 2xfm_pr_sha256(shasum|awk), 2xfm_pr_file_identity, acmp, and the parses - on the order of 40 processes - and it runs on every 15s watcher poll (FM_POLL default, bin/fm-watch.sh:122) and every daemon housekeeping tick, per covered task, indefinitely. That is ~9600 process spawns/hour replacing the one model turn/hour the change removes, which is still a good trade in the currency the intent cares about, and both code comments state the un-advanced marker is deliberate so a lapse is caught on the very next poll. Noting the cost as a known trade rather than asking for a change; the note-rewrite short-circuit at this line already avoids the write, only the validation itself repeats.bin/fm-classify-lib.sh:230- The coverage note is created with the ambient umask, but AGENTS.md:103 (added by this change) documents it as a 'private note' and every sibling artifact it is listed beside is enforced at 0600 byfm_pr_private_file_valid. The contents are only a PR URL and a timestamp, so this is a convention/doc mismatch rather than a disclosure, but the AGENTS.md entry currently overstates what the code guarantees. Either chmod 0600 after the write or drop 'private' from the AGENTS.md line.🔧 Fix: narrow pause suppression and lapse it on closed PRs
2 issues (1 warning, 1 info) still open:
bin/fm-watch.sh:993- Whenfm_pr_poll_terminal_publishfails, the check output is still discarded (out=at bin/fm-watch.sh:993 runs unconditionally), so the terminal observation is neither recorded nor surfaced and the covered pause stays suppressed forever - the exact failure mode this fix round was accepted to close. Concrete sequence: a task declarespaused:with a validated poll armed, coverage is established and the note written; the PR is closed without merging; the sweep readsclosed-unmerged;fm_pr_poll_terminal_publish(bin/fm-pr-lib.sh:997) returns 1 becausefm_pr_regular_destination_on_device_or_absentrejects astate/<id>.pr-poll-terminalwhose link count is not 1 (a state dir snapshotted withcp -alproduces exactly this) or becausemktempfails on a full state filesystem. No record is written, sopause_recheck_covered_by_merge_pollstill returns 0 on every cycle;outwas cleared, so nocheck:wake is appended;.paused-resurfaced-<key>was never advanced butpause_recheck_covered_by_merge_pollkeeps answering yes, sohandle_paused_stalesuppresses forever; and the daemon reads the same absent record. The only trace is atriage_logline in a bounded debug log, which wakes nobody. Every subsequent poll re-observes CLOSED and re-fails identically, so this does not self-heal. Fix: clearoutonly when the publish succeeded, so an unrecordable terminal observation falls through to the ordinarycheck: <c>: closed-unmergedwake instead of being swallowed. That keeps the intent's "Fail toward the recheck" rule intact: the wait must not go quiet on a state that can be observed but not durably recorded.bin/fm-pr-poll.sh:70- Extending the byte-static poll changes the template bytes, so every check.sh armed by a previous version stops validating:fm_pr_poll_artifacts_validbyte-comparescmp -s "$template" "$check"and re-checksFM_PR_REG_TEMPLATE_HASH(bin/fm-pr-lib.sh:599, :611). This is self-healing -migration_needed(bin/fm-pr-check-migrate.sh:394) is not gated on the completed-migration marker, and the watcher runsfm-pr-check-migrate.sh --checks-safebefore its main loop (bin/fm-watch.sh:782), which rebuilds each canonical poll from the task'spr=meta line. The transitional cost is bounded to a watcher process that is already past its startup migration when the new bytes land: its next sweep rejects every armed poll, emits onecheck: rejected unauthenticated state checkswake, and exits; the restart then rebuilds and re-arms. Worth knowing because the two live tasks the intent names (fm-context-restart-handoff on PR 2606, fm-mathpath-worktree-command) are currently relying on armed polls. No change requested.🔧 Fix: surface unrecordable terminal PR observations instead of swallowing them
3 infos still open:
bin/fm-watch.sh:1001- The accepted round-3 fallback has no throttle, so a terminal state that is observable but permanently unrecordable wakes firstmate once per FM_CHECK_INTERVAL forever. Whenfm_pr_poll_terminal_publishfails (a symlink or non-1-link-count file atstate/<id>.pr-poll-terminal, or a full state filesystem),outstaysclosed-unmerged,fm_wake_append checkruns, andwakeexits the watcher.fm_wake_append(bin/fm-wake-lib.sh:930) does no dedupe, and unlike themergedbranch there is no retirement to stop the repeat, so the next sweep re-observes CLOSED and re-fails identically: with the default CHECK_INTERVAL=300 (bin/fm-watch.sh:125) that is ~12 model turns/hour replacing the 1/hour this change removes. The user's round-2 instruction explicitly chose this wake over silence and explicitly noted the failure is not self-healing, so this is an authorized trade, not a defect - convergence depends on firstmate acting on the wake (tearing down or re-arming the poll), which it can, since the reason names the check and the outcome. Recorded here only so the cadence is a known property rather than a surprise; no change requested.bin/fm-watch.sh:373- The withdrawal of the coverage note on a captain-held line sits INSIDE the due-window guard, so the comment's claim at bin/fm-watch.sh:352 ("A captain-held line also withdraws any coverage note the same task earned while it was still paused, so the note never claims a suppression that is not in force") does not hold for the transition itself. Concrete sequence: a task ispaused:with a validated poll armed, the window comes due, coverage is established andstate/.<id>.pause-poll-coveredis written; fm-decision-hold.sh:516 then appendscaptain-held [key=<k>]: tracked by <hold>to the same status file. That append resets the status mtime, soage(bin/fm-watch.sh:363) drops to ~0 and theif [ "$age" -ge "$PAUSE_RESURFACE_SECS" ]guard at :368 is false - neither branch runs, sopause_poll_coverage_forgetis not reached. No other path clears it either: theclear_pause_trackingguard at :1096 requires! status_is_paused_or_captain_held, and captain-held is in that set. For up to one FM_PAUSE_RESURFACE_SECS window the session-start digest (bin/fm-session-start.sh:817) prints "pause recheck: declared-wait recheck suppressed since ..." for a task whose recheck is in fact NOT suppressed - the opposite of the intent's observability constraint. It self-heals: once age crosses the window the else branch withdraws the note and re-surfaces. The existing test does not catch this because it backdates the status file by 5000s before starting the watcher, so the guard is already satisfied. Fix: move theelse pause_poll_coverage_forgetout of the due-windowif(an unconditionalrm -fon this path is cheap), or reword the comment to say the withdrawal happens at the next due window.bin/fm-classify-lib.sh:252- A coverage note that cannot be written still suppresses the recheck, which is the same "observable but unrecordable" shape the round-3 fix was accepted to close, applied to the announcement rather than the observation. The write is wrapped inif ( umask 077; printf ... > "$note" ) 2>/dev/null; then chmod ...; fiand the function then unconditionallyreturn 0, so if the redirect fails - state filesystem full, state dir read-only, or a directory sitting at the note path - the recheck is dropped with nothing on disk for a supervisor to find and nothing for bin/fm-session-start.sh:817 to print. That contradicts the intent's design constraint "Suppression must be observable. A wait that goes quiet because a poll covers it must be visible somewhere a supervisor can find, so a future reader does not conclude the recheck silently broke", and it is the one remaining place where the module chooses silence over the recheck. Narrow (an unwritable state dir degrades much else too), but the fix is one line: make the failed writereturn 1so an unannounceable suppression falls back to the ordinary FM_PAUSE_RESURFACE_SECS recheck, matching the fail-toward-the-recheck rule the rest of this predicate follows.tests/fm-session-start.test.sh:990- tests/fm-session-start.test.sh cannot run to completion on this machine, for reasons that predate this change. Two cases fail identically on the base commit 1cb900c:test_output_ordering_diagnostics_leadremoves the fakenodefrom its fakebin but the machine has a real/usr/bin/nodeon the fixture's BASE_PATH, so the expectedMISSING: nodediagnostic never appears; andtest_session_start_relaunches_herdr_husk_secondmatefails with an unexpectedSECONDMATE_LIVENESS:line. Because the suite aborts on first failure, the change's own new case never ran in a full-suite pass. I ran that case in isolation (with the sourcing lines repointed at the worktree's tests/lib.sh and wake-helpers.sh) and it passes. Nothing here indicates a defect in this change, but the suite is not a usable regression signal on this host until the fixtures are made hermetic against an ambient node and a herdr husk../bin/fm-test-run.sh tests/fm-watch-triage.test.sh- exit 0, failed=0; includes the seven new cases:test_pause_poll_coverage_predicate_matrix,test_paused_recheck_suppressed_by_armed_merge_poll,test_paused_recheck_survives_untrusted_check,test_captain_held_recheck_survives_armed_merge_poll,test_paused_recheck_resumes_when_covering_pr_is_closed_unmerged,test_unrecordable_terminal_observation_is_surfaced,test_inconclusive_poll_is_not_a_terminal_outcome./bin/fm-test-run.sh tests/fm-pr-check-security.test.sh- exit 0, failed=0 (static poll now reportsclosed-unmergedas its second terminal outcome; teardown removes poll artifacts)./bin/fm-test-run.sh tests/fm-daemon.test.sh- exit 0, failed=0 (away-mode pause tracking after the clear_pause_tracking change)./bin/fm-test-run.sh tests/fm-session-start.test.sh- exit 1 ontest_output_ordering_diagnostics_lead; reproduced identically at base commit 1cb900c, so pre-existing and environmental (real /usr/bin/node on the fixture PATH)FM_TEST_BASE_PATH=<node-free bin> ./bin/fm-test-run.sh tests/fm-session-start.test.sh- the ordering case then passes, buttest_session_start_relaunches_herdr_husk_secondmatefails; also reproduced identically at base commit 1cb900ctest_suppressed_pause_recheck_is_printedrun in isolation from a copy of tests/fm-session-start.test.sh repointed at the worktree's tests/lib.sh and tests/wake-helpers.sh - passesManual end-to-end:pause-recheck-demo.sh <this-change> <base-1cb900c> <evidence-dir>- drives real bin/fm-pr-check.sh, bin/fm-watch.sh, bin/fm-supervise-daemon.sh and bin/fm-session-start.sh over the reported fixture (task parked on an upstream PR with a validated merge poll armed) across three simulated FM_PAUSE_RESURFACE_SECS windows at base and at this change, plus the no-poll, untrusted-check, and closed-without-merging fixtures, plus away-mode housekeeping on all threegit status --porcelainafter testing - clean; every scratch checkout, sanitized PATH dir, and log removed, evidence left only under /tmp/no-mistakes-evidence🔧 **Document** - 2 issues found → auto-fixed ✅
AGENTS.md:403- AGENTS.md section 8'scheck:handling rule ("act on the named poll result, including merges, Relay events, and process-to-event source results") now also covers aclosed-unmergedresult, which reaches the queue only in the rare case where the terminal record could not be written. I judged that too situational for always-loaded AGENTS.md under its size discipline and left docs/architecture.md as the single owner of that fallback; flagging the placement call in case the owner wants it inline.docs/gitlab-merge-watch.md:40- docs/gitlab-merge-watch.md is a maintainer-verification record whose commands were all run on 2026-07-21 against a fixture holding one merged and one open merge request. I corrected its now-stale prose contract line, but the new exact-closed->closed-unmergedmapping has no recorded evidence there, and I will not fabricate command output. Adding it needs a closed-without-merge fixture MR and a real rerun, which is an owner decision beyond this change's scope.🔧 Fix: label unverified closed half of GitLab merge-watch record
✅ Re-checked - no issues remain.
🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix: no lint changes needed; fm-lint.sh clean after actionlint install
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.