Skip to content

fix(bin): stop rechecking a declared pause an armed merge poll already covers - #2750

Open
Parsunic wants to merge 5 commits into
kunchenguid:mainfrom
Parsunic:fm/fm-pause-recheck-redundant-with-merge-poll
Open

fix(bin): stop rechecking a declared pause an armed merge poll already covers#2750
Parsunic wants to merge 5 commits into
kunchenguid:mainfrom
Parsunic:fm/fm-pause-recheck-redundant-with-merge-poll

Conversation

@Parsunic

Copy link
Copy Markdown

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 a stale: 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:

  • The poll must be genuinely armed and trusted, not merely present. A check.sh that exists but is not the validated PR poll, or whose trust binding does not match, must not suppress anything. Getting this wrong turns a real wedge into silence.
  • 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.
  • Fail toward the recheck. If a valid merge poll covering this exact task cannot be positively established, keep re-surfacing. A redundant wake costs a turn; a lost one costs a stalled task nobody notices.
  • Do not change FM_PAUSE_RESURFACE_SECS itself or widen the interval globally. Other declared pauses still need it.

Definition of done:

  • The redundant recheck is gone for poll-covered pauses and intact for everything else.
  • Tests cover: a declared pause WITH a trusted armed merge poll (suppressed), one WITHOUT (still re-surfaces), one whose check exists but is not a trusted validated poll (still re-surfaces), and that the away-mode and always-on paths agree.
  • shellcheck clean on every script touched, per the repo's own rules.
  • The PR body states what a supervisor now sees instead of the hourly wake.

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.sh gains pause_recheck_covered_by_merge_poll, a shared predicate that drops the FM_PAUSE_RESURFACE_SECS recheck for a declared paused: wait only when bin/fm-pr-lib.sh positively validates that same task's armed merge poll. Both supervisors call it - bin/fm-watch.sh in handle_paused_stale (offered to the declared external-wait verb only, never a captain-held transfer, and leaving the .paused-resurfaced-* throttle unadvanced) and bin/fm-supervise-daemon.sh in housekeeping (leaving the pause marker unreset), so away mode and always-on agree and any lapse resumes the recheck on the next cycle.
  • Suppression is observable rather than silent: a covered wait writes state/.<id>.pause-poll-covered, a private one-line note naming the covering PR and the moment the wait went quiet, which bin/fm-session-start.sh prints under that task in the fleet digest as pause recheck: .... So instead of an hourly stale: 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 by bin/fm-teardown.sh, so it never outlives its wait.
  • Coverage lapses on a terminal non-merged outcome: bin/fm-pr-poll.sh now also prints closed-unmerged for a PR/MR the forge itself reports closed, bin/fm-pr-lib.sh records it as the identity-bound private state/<id>.pr-poll-terminal, and bin/fm-watch.sh drops that result instead of waking - falling back to an ordinary check: wake when the record cannot be written. bin/fm-pr-check.sh clears any stale record when rearming. Docs updated in docs/architecture.md, docs/configuration.md, docs/gitlab-merge-watch.md (whose closed half is labelled unverified on this host), AGENTS.md, and the afk skill; tests extend tests/fm-watch-triage.test.sh, tests/fm-session-start.test.sh, and tests/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.

############################################################
# Declared pause + armed merge poll: what firstmate is asked
############################################################

Fixture (all rounds):  task fm-context-restart-handoff
                       status line 'paused: awaiting upstream PR 2606'
                       parked 5000s, one validated merge poll armed on
                       https://github.com/kunchenguid/firstmate/pull/2606 (still open on the forge)
Each round below is one FM_PAUSE_RESURFACE_SECS window - one hour in production.

------------------------------------------------------------------
BEFORE  (base commit, bin/ from 1cb900c)
------------------------------------------------------------------
armed poll: fm-context-restart-handoff.check.sh fm-context-restart-handoff.meta fm-context-restart-handoff.pr-poll fm-context-restart-handoff.pr-poll-registration fm-context-restart-handoff.status 

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)
  (a wake costs firstmate one full model turn)

hour 2:
  firstmate is woken:
    > stale: fm:fm-context-restart-handoff (paused 5007s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
  (a wake costs firstmate one full model turn)

hour 3:
  firstmate is woken:
    > stale: fm:fm-context-restart-handoff (paused 5011s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
  (a wake costs firstmate one full model turn)

=> 3 of 3 hours woke firstmate, every answer 'still open, nothing changed'.

------------------------------------------------------------------
AFTER   (this change)
------------------------------------------------------------------
hour 1:
  firstmate is not woken (watcher still absorbing)

hour 2:
  firstmate is not woken (watcher still absorbing)

hour 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 https://github.com/kunchenguid/firstmate/pull/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 https://github.com/kunchenguid/firstmate/pull/2606 covers the wait, age 5043s): fm:fm-context-restart-handoff
    [2026-08-21T02:19:40-0400] suppressed paused recheck (armed merge poll https://github.com/kunchenguid/firstmate/pull/2606 covers the wait, age 5046s): fm:fm-context-restart-handoff
    [2026-08-21T02:19:43-0400] suppressed paused recheck (armed merge poll https://github.com/kunchenguid/firstmate/pull/2606 covers the wait, age 5048s): 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=https://github.com/kunchenguid/firstmate/pull/2606
    endpoint: 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 https://github.com/kunchenguid/firstmate/pull/2606 covers this wait and wakes firstmate when that PR merges
    status tail (last 5 line(s), each capped at 220 characters, wake-EVENT history, not current state; full log: /tmp/fm-pause-demo.nBBR5B/after/state/fm-context-restart-handoff.status):
    paused: awaiting upstream PR 2606
    

------------------------------------------------------------------
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)
  (a wake costs firstmate one full model turn)

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)
  (a wake costs firstmate one full model turn)

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)
  (a wake costs firstmate one full model turn)
  coverage note: withdrawn - the poll will never speak again
  durable terminal observation: fm-pr-poll-terminal-v1 fm-context-restart-handoff https://github.com/kunchenguid/firstmate/pull/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> is git archive 1cb900c | tar -x -C $(mktemp -d).

#!/usr/bin/env bash
# End-to-end demonstration of the pause-recheck suppression change.
#
# Reproduces the reported 2026-08-20 case with the real bin/ scripts:
# a task parked on an upstream PR with a validated merge poll armed, whose
# declared pause was re-surfaced as an hourly "confirm the wait still holds"
# wake even though the poll would report the merge by itself.
#
# Each round below is one FM_PAUSE_RESURFACE_SECS window ("one hour" in
# production). The demo drives bin/fm-watch.sh for real - fake tmux, fake
# fm-crew-state and fake gh only - at the BASE commit and then at the change,
# and finishes with the fleet digest a supervisor actually reads.
#
# Usage: pause-recheck-demo.sh <target-root> <base-root> <out-dir>
#   <target-root>  a checkout of this change
#   <base-root>    a checkout of the base commit, e.g.
#                    base=$(mktemp -d); git archive 1cb900c | tar -x -C "$base"
#   <out-dir>      where pause-recheck-demo.txt is written
set -u

TARGET_ROOT=$1
BASE_ROOT=$2
OUT=$3
WORK=$(mktemp -d "${TMPDIR:-/tmp}/fm-pause-demo.XXXXXX")
trap 'rm -rf "$WORK"' EXIT

PR_URL=https://github.com/kunchenguid/firstmate/pull/2606
TASK=fm-context-restart-handoff
WINDOW="fm:$TASK"
PANE='idle - waiting on upstream PR 2606'
AGE=5000          # seconds the pause has been standing
RESURFACE=60      # stands in for the 3600s production FM_PAUSE_RESURFACE_SECS

hash_text() { printf '%s' "$1" | md5sum | cut -d' ' -f1; }
seen_sig() { stat -c '%s:%Y' "$1" 2>/dev/null; }
say() { printf '%s\n' "$*"; }
rule() { printf '%s\n' "------------------------------------------------------------------"; }

# A hermetic fixture home: fake tmux/fm-crew-state/gh, a task parked on the PR.
# <root> is the checkout whose bin/ arms the poll, so the base round is armed
# and polled entirely by base code.
make_home() {  # <root> <name> <status-line> <arm-poll:yes|no|custom-check>
  local root=$1 name=$2 line=$3 arm=$4 dir state
  dir="$WORK/$name"; state="$dir/state"
  mkdir -p "$state" "$dir/fakebin" "$dir/pr-root/bin"

  cat > "$dir/fakebin/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "${1:-}" in
  list-windows) [ -n "${FM_FAKE_TMUX_WINDOW:-}" ] && printf '%s\n' "${FM_FAKE_TMUX_WINDOW#*:}"; exit 0 ;;
  capture-pane) [ -n "${FM_FAKE_TMUX_CAPTURE:-}" ] && cat "$FM_FAKE_TMUX_CAPTURE"; exit 0 ;;
  display-message) case "$*" in *pane_current_command*) printf '%s\n' "${FM_FAKE_TMUX_CURRENT_COMMAND:-}"; exit 0 ;; esac ;;
esac
exit 1
SH
  cat > "$dir/fakebin/fm-crew-state.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "${FM_FAKE_CREW_STATE:-state: unknown}"
SH
  # The forge as the poll sees it: the PR is still open, so the poll stays silent.
  cat > "$dir/fakebin/gh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "${FM_FAKE_PR_STATE:-OPEN}"
SH
  printf '#!/usr/bin/env bash\nexit 0\n' > "$dir/pr-root/bin/fm-guard.sh"
  chmod +x "$dir/fakebin/tmux" "$dir/fakebin/fm-crew-state.sh" "$dir/fakebin/gh" "$dir/pr-root/bin/fm-guard.sh"

  printf '%s' "$PANE" > "$dir/pane.txt"
  printf 'window=%s\nkind=ship\n' "$WINDOW" > "$state/$TASK.meta"
  printf '%s\n' "$line" > "$state/$TASK.status"
  touch -m -d "@$(( $(date +%s) - AGE ))" "$state/$TASK.status"

  case "$arm" in
    yes)
      FM_ROOT_OVERRIDE="$dir/pr-root" FM_HOME="$dir" FM_STATE_OVERRIDE="$state" \
        PATH="$dir/fakebin:$PATH" "$root/bin/fm-pr-check.sh" "$TASK" "$PR_URL" >/dev/null || return 1 ;;
    custom-check)
      # A legitimate registered check that is NOT the validated merge poll.
      printf '#!/usr/bin/env bash\nexit 0\n' > "$state/$TASK.check.sh"
      chmod 0700 "$state/$TASK.check.sh"
      FM_STATE_OVERRIDE="$state" "$root/bin/fm-check-register.sh" "$TASK" >/dev/null || return 1 ;;
  esac

  printf '%s' "$(seen_sig "$state/$TASK.status")" > "$state/.seen-${TASK}_status"
  local key; key=$(printf '%s' "$WINDOW" | tr ':/.' '___')
  printf '%s' "$(hash_text "$PANE")" > "$state/.hash-$key"
  printf '1\n' > "$state/.count-$key"
  printf '%s\n' "$dir"
}

# One supervision window against the always-on watcher. Prints whatever the
# watcher would hand firstmate: a wake reason costs a model turn, silence is free.
watch_round() {  # <root> <dir>
  local root=$1 dir=$2 state key pid out
  state="$dir/state"
  out="$dir/watch.out"; : > "$out"
  # Each round is a fresh supervision window, so the registered-check sweep is
  # due again exactly as it would be an hour later in production.
  rm -f "$state/.last-check"
  key=$(printf '%s' "$WINDOW" | tr ':/.' '___')
  PATH="$dir/fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$WINDOW" FM_FAKE_TMUX_CAPTURE="$dir/pane.txt" \
    FM_FAKE_TMUX_CURRENT_COMMAND=zsh FM_FAKE_PR_STATE="${FM_FAKE_PR_STATE:-OPEN}" \
    FM_FAKE_CREW_STATE='state: paused · source: status-log · awaiting upstream PR 2606' \
    FM_STATE_OVERRIDE="$state" FM_CREW_STATE_BIN="$dir/fakebin/fm-crew-state.sh" \
    FM_ROOT_OVERRIDE="$dir/pr-root" \
    FM_PAUSE_RESURFACE_SECS="$RESURFACE" FM_POLL=1 FM_SIGNAL_GRACE=1 \
    FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$root/bin/fm-watch.sh" > "$out" 2>"$dir/watch.err" &
  pid=$!
  local i=0
  while [ "$i" -lt 60 ]; do
    kill -0 "$pid" 2>/dev/null || break
    sleep 0.25; i=$((i + 1))
  done
  if kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true; fi
  wait "$pid" 2>/dev/null || true

  if [ -s "$out" ]; then
    say "  firstmate is woken:"
    sed 's/^/    > /' "$out"
    say "  (a wake costs firstmate one full model turn)"
    # Ack the durable queue so the next window starts clean, as a real drain would.
    FM_STATE_OVERRIDE="$state" FM_ROOT_OVERRIDE="$dir/pr-root" \
      "$root/bin/fm-wake-drain.sh" >/dev/null 2>"$dir/drain.err" || true
    local seq gen
    seq=$(sed -n 's/.*--ack-through \([0-9][0-9]*\) .*/\1/p' "$dir/drain.err")
    gen=$(sed -n 's/.*--recovery-generation \([A-Za-z0-9._-][A-Za-z0-9._-]*\)$/\1/p' "$dir/drain.err")
    [ -n "$seq" ] && FM_STATE_OVERRIDE="$state" FM_ROOT_OVERRIDE="$dir/pr-root" \
      "$root/bin/fm-wake-drain.sh" --ack-through "$seq" --recovery-generation "$gen" >/dev/null 2>&1 || true
    # Age the re-surface throttle so the next window is due, as an hour would.
    [ -e "$state/.paused-resurfaced-$key" ] \
      && touch -m -d "@$(( $(date +%s) - AGE ))" "$state/.paused-resurfaced-$key"
    return 0
  else
    say "  firstmate is not woken (watcher still absorbing)"
    # This demo restarts the watcher each window; a killed watcher leaves a
    # downtime-recovery marker that would resurface its own restart next round.
    rm -f "$state/.watcher-down"
    return 1
  fi
}

exec > >(tee "$OUT/pause-recheck-demo.txt") 2>&1

say "############################################################"
say "# Declared pause + armed merge poll: what firstmate is asked"
say "############################################################"
say
say "Fixture (all rounds):  task $TASK"
say "                       status line 'paused: awaiting upstream PR 2606'"
say "                       parked ${AGE}s, one validated merge poll armed on"
say "                       $PR_URL (still open on the forge)"
say "Each round below is one FM_PAUSE_RESURFACE_SECS window - one hour in production."
say

rule
say "BEFORE  (base commit, bin/ from $(git -C "$TARGET_ROOT" rev-parse --short HEAD~3 2>/dev/null || echo base))"
rule
before_dir=$(make_home "$BASE_ROOT" before 'paused: awaiting upstream PR 2606' yes) \
  || { say "could not arm the base fixture"; exit 1; }
say "armed poll: $(ls "$before_dir/state" | tr '\n' ' ')"
say
wakes=0
for h in 1 2 3; do
  say "hour $h:"
  watch_round "$BASE_ROOT" "$before_dir" && wakes=$((wakes + 1))
  say
done
say "=> $wakes of 3 hours woke firstmate, every answer 'still open, nothing changed'."
say

rule
say "AFTER   (this change)"
rule
after_dir=$(make_home "$TARGET_ROOT" after 'paused: awaiting upstream PR 2606' yes) \
  || { say "could not arm the fixture"; exit 1; }
wakes=0
for h in 1 2 3; do
  say "hour $h:"
  watch_round "$TARGET_ROOT" "$after_dir" && wakes=$((wakes + 1))
  say
done
say "=> $wakes of 3 hours woke firstmate."
say
say "What a supervisor sees instead of the hourly wake:"
say
say "  \$ cat state/.$TASK.pause-poll-covered"
sed 's/^/    /' "$after_dir/state/.$TASK.pause-poll-covered" 2>/dev/null || say "    (missing)"
say
say "  \$ grep 'suppressed paused recheck' state/.watch-triage.log | tail -3"
grep -F 'suppressed paused recheck' "$after_dir/state/.watch-triage.log" 2>/dev/null | tail -3 | sed 's/^/    /'
say
say "  \$ fm-session-start.sh   (fleet digest, the first thing firstmate reads)"
FM_HOME="$after_dir" FM_STATE_OVERRIDE="$after_dir/state" \
  "$TARGET_ROOT/bin/fm-session-start.sh" 2>/dev/null \
  | grep -A 8 -F -e "--- $TASK ---" | sed 's/^/    /' || say "    (digest unavailable)"
say

rule
say "AFTER   declared pause with NO armed poll - the recheck this is for"
rule
plain_dir=$(make_home "$TARGET_ROOT" plain 'paused: awaiting upstream PR 2606' no)
say "hour 1:"
watch_round "$TARGET_ROOT" "$plain_dir" || true
say
say "coverage note written? $( [ -e "$plain_dir/state/.$TASK.pause-poll-covered" ] && echo yes || echo 'no - nothing claims a suppression')"
say

rule
say "AFTER   declared pause whose check.sh is registered but is NOT the merge poll"
rule
custom_dir=$(make_home "$TARGET_ROOT" custom 'paused: awaiting upstream PR 2606' custom-check)
say "hour 1:"
watch_round "$TARGET_ROOT" "$custom_dir" || true
say
say "coverage note written? $( [ -e "$custom_dir/state/.$TASK.pause-poll-covered" ] && echo yes || echo 'no - an untrusted check suppresses nothing')"
say

rule
say "AFTER   the covering PR is CLOSED without merging - coverage must lapse"
rule
closed_dir=$(make_home "$TARGET_ROOT" closed 'paused: awaiting upstream PR 2606' yes)
say "hour 1 (forge says the PR is still open):"
FM_FAKE_PR_STATE=OPEN watch_round "$TARGET_ROOT" "$closed_dir" || true
say "  coverage note: $( [ -e "$closed_dir/state/.$TASK.pause-poll-covered" ] && echo present || echo absent)"
say
say "hour 2 (forge now says the PR was closed without merging):"
FM_FAKE_PR_STATE=CLOSED watch_round "$TARGET_ROOT" "$closed_dir" || true
say "  coverage note: $( [ -e "$closed_dir/state/.$TASK.pause-poll-covered" ] && echo present || echo 'withdrawn - the poll will never speak again')"
say "  durable terminal observation: $( [ -e "$closed_dir/state/$TASK.pr-poll-terminal" ] && tr '\n' ' ' < "$closed_dir/state/$TASK.pr-poll-terminal" || echo none)"
say

rule
say "AFTER   away mode (bin/fm-supervise-daemon.sh) on the same three fixtures"
rule
run_housekeeping() {  # <dir>
  local dir=$1 state dkey
  state="$dir/state"
  dkey=$(printf '%s' "$TASK" | tr ':/.' '___')
  echo $(( $(date +%s) - AGE )) > "$state/.subsuper-paused-$dkey"
  : > "$state/.subsuper-escalations"
  PATH="$dir/fakebin:$PATH" FM_FAKE_TMUX_WINDOW="$WINDOW" FM_FAKE_TMUX_CAPTURE="$dir/pane.txt" \
    FM_STATE_OVERRIDE="$state" FM_ROOT_OVERRIDE="$dir/pr-root" \
    FM_PAUSE_RESURFACE_SECS="$RESURFACE" FM_ESCALATE_BATCH_SECS=999999 \
    bash -c 'set -u; . "$1/bin/fm-supervise-daemon.sh"; housekeeping "$2"' _ "$TARGET_ROOT" "$state" >/dev/null 2>&1
  if [ -s "$state/.subsuper-escalations" ]; then
    say "  escalated to firstmate:"
    sed 's/^/    > /' "$state/.subsuper-escalations"
  else
    say "  nothing escalated"
  fi
}
say "poll-covered pause:"
run_housekeeping "$after_dir"
say "pause with no poll:"
run_housekeeping "$plain_dir"
say "check.sh that is not the merge poll:"
run_housekeeping "$custom_dir"
say
say "Both supervisors agree on all three fixtures."
- Outcome: ⚠️ 1 warning across 1 run (30m50s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ⚠️ 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 prints merged only when state == MERGED and exits 0 silently on every other path (closed-unmerged, gh auth expiry, gh/glab missing from PATH, renamed or deleted repo). Concrete sequence: crew writes paused: 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 because age and rf_age both stay above the window; and no other path can surface it - status_is_captain_relevant (bin/fm-classify-lib.sh:112) returns 1 for paused, 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 to handle_paused_stale, which serves captain-held: as well as paused:, 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 on status_is_paused_or_captain_held, and so do lines 1168 and 1211 (via pause_state_class returning paused on a confidently dead agent). Concrete path: fm-decision-hold.sh:516 appends captain-held [key=&lt;key&gt;]: tracked by &lt;hold&gt; 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, and status_is_captain_relevant also returns 1 for captain-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 on status_is_paused, and classify_stale routes captain-held to a transient self-handle), so the two modes now answer this case differently. Narrowing the suppression to status_is_paused would 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_TEMPLATE makes the trust anchor environment-overridable, and nothing uses the hook. The template is what fm_pr_poll_artifacts_valid byte-compares check.sh against (cmp -s &#34;$template&#34; &#34;$check&#34;, 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.sh unconditionally.
  • ⚠️ tests/fm-watch-triage.test.sh:948 - The away-mode half of the suppression test asserts only that .subsuper-escalations is 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 and stale_window_is_busy returns 0 or 2 - both of those rm -f &#34;$marker&#34; and escalate nothing. So the assertion would still pass if pause_recheck_covered_by_merge_poll regressed 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: it continues with the marker deliberately un-reset, while both drop paths remove it and the escalate path writes to .subsuper-escalations. Add [ -e &#34;$state/.subsuper-paused-$dkey&#34; ] || fail ... alongside the existing negative assertion.
  • ℹ️ bin/fm-classify-lib.sh:226 - Because neither supervisor advances a throttle after suppressing, the full fm_pr_poll_artifacts_valid re-runs on every cycle for the lifetime of the wait. Per call that is roughly 3x fm_pr_private_file_valid (each 3 command substitutions over uname + stat), 2x fm_pr_sha256 (shasum|awk), 2x fm_pr_file_identity, a cmp, 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 by fm_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 - When fm_pr_poll_terminal_publish fails, 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 declares paused: with a validated poll armed, coverage is established and the note written; the PR is closed without merging; the sweep reads closed-unmerged; fm_pr_poll_terminal_publish (bin/fm-pr-lib.sh:997) returns 1 because fm_pr_regular_destination_on_device_or_absent rejects a state/&lt;id&gt;.pr-poll-terminal whose link count is not 1 (a state dir snapshotted with cp -al produces exactly this) or because mktemp fails on a full state filesystem. No record is written, so pause_recheck_covered_by_merge_poll still returns 0 on every cycle; out was cleared, so no check: wake is appended; .paused-resurfaced-&lt;key&gt; was never advanced but pause_recheck_covered_by_merge_poll keeps answering yes, so handle_paused_stale suppresses forever; and the daemon reads the same absent record. The only trace is a triage_log line 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: clear out only when the publish succeeded, so an unrecordable terminal observation falls through to the ordinary check: &lt;c&gt;: closed-unmerged wake 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_valid byte-compares cmp -s &#34;$template&#34; &#34;$check&#34; and re-checks FM_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 runs fm-pr-check-migrate.sh --checks-safe before its main loop (bin/fm-watch.sh:782), which rebuilds each canonical poll from the task's pr= 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 one check: rejected unauthenticated state checks wake, 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. When fm_pr_poll_terminal_publish fails (a symlink or non-1-link-count file at state/&lt;id&gt;.pr-poll-terminal, or a full state filesystem), out stays closed-unmerged, fm_wake_append check runs, and wake exits the watcher. fm_wake_append (bin/fm-wake-lib.sh:930) does no dedupe, and unlike the merged branch 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 is paused: with a validated poll armed, the window comes due, coverage is established and state/.&lt;id&gt;.pause-poll-covered is written; fm-decision-hold.sh:516 then appends captain-held [key=&lt;k&gt;]: tracked by &lt;hold&gt; to the same status file. That append resets the status mtime, so age (bin/fm-watch.sh:363) drops to ~0 and the if [ &#34;$age&#34; -ge &#34;$PAUSE_RESURFACE_SECS&#34; ] guard at :368 is false - neither branch runs, so pause_poll_coverage_forget is not reached. No other path clears it either: the clear_pause_tracking guard 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 the else pause_poll_coverage_forget out of the due-window if (an unconditional rm -f on 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 in if ( umask 077; printf ... &gt; &#34;$note&#34; ) 2&gt;/dev/null; then chmod ...; fi and the function then unconditionally return 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 write return 1 so 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.
⚠️ **Test** - 1 warning
  • ⚠️ 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_lead removes the fake node from its fakebin but the machine has a real /usr/bin/node on the fixture's BASE_PATH, so the expected MISSING: node diagnostic never appears; and test_session_start_relaunches_herdr_husk_secondmate fails with an unexpected SECONDMATE_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 reports closed-unmerged as 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 on test_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=&lt;node-free bin&gt; ./bin/fm-test-run.sh tests/fm-session-start.test.sh - the ordering case then passes, but test_session_start_relaunches_herdr_husk_secondmate fails; also reproduced identically at base commit 1cb900c
  • test_suppressed_pause_recheck_is_printed run 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 - passes
  • Manual end-to-end: pause-recheck-demo.sh &lt;this-change&gt; &lt;base-1cb900c&gt; &lt;evidence-dir&gt; - 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 three
  • git status --porcelain after 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's check: handling rule ("act on the named poll result, including merges, Relay events, and process-to-event source results") now also covers a closed-unmerged result, 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-unmerged mapping 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 ✅
  • ⚠️ linter found issues (exit code 127)

🔧 Fix: no lint changes needed; fm-lint.sh clean after actionlint install
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

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.
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Corrective — suppress the bounded paused: recheck only when this same task already has a trusted armed merge poll, fail toward the recheck, and leave a digest-visible coverage note. No enable flag, but this is a named defect (hourly model turns that always answered "still open"), not a new default cadence.

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 bin/fm-watch.sh pause absorb (handle_paused_stale / pause_state_class). #2750 and #2748 also both edit bin/fm-classify-lib.sh, bin/fm-supervise-daemon.sh housekeeping, docs/architecture.md, docs/configuration.md, and the afk skill. Same-file/function neighbors: #2419, #2598, #2738, #2496.

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 (aeafcb2); I will approve the workflow runs when GitHub queues them.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Corrective intent: a declared pause plus an armed fm-pr-check.sh merge poll should not cost an hourly firstmate turn to reconfirm the wait. That aligns (scripts own the poll; tokens stay lean).

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.

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.

2 participants