Skip to content

fix(bin): split the silent-pane readout three ways and defer the focus-blocked teardown close - #34

Merged
onyx-space merged 17 commits into
mainfrom
fm/stale-readout-three-states-d1d2
Sep 24, 2026
Merged

onyx-space merged 17 commits into
mainfrom
fm/stale-readout-three-states-d1d2

Conversation

@onyx-space

@onyx-space onyx-space commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Intent

队长 2026-09-24 的答复(经 w 机 firstmate 转达):w 侧诊断出的 D1、D2 这两条由 h 机起 lane 做,w 只留卡跟踪。

两条的原始记录在 w 机(data/backlog.md 的 elmo-stale-d1-teardown-tab-lock、elmo-stale-d2-finished-lane-working),诊报是 w 的 data/elmo-stale-rate-diagnosis/report.md。

D1:收线(teardown)因为「队长当前聚焦的 Herdr 工作区」而拒绝关闭 pane ⇒ 已落地的 endpoint 收不干净,下一轮又被当成 wedge 复查。w 的诊报说单修这一条就能消掉样本里 40% 的 stale。

D2:已完工的 lane 仍被 pause_state_class 读成 working ⇒ wedge 计时器在成品上反复开火(样本里 w29 十七次、w2Z 七次,全是误报)。

同族的第二面(w 2026-09-15 实测):正在等门禁、或正在跑管线的 lane,其 pane 被反复报 stale(同一批 pane 多次命中:elmo-d5-microperf-core ≥3 次、elmo-sa1626-after-docs ≥2 次);当日每次都证伪成功 —— ① pgrep -x pi 后逐个 readlink /proc//cwd 命中该 lane 的工作树(agent 活着)② fm-crew-state.sh 的 source=run-step ③ no-mistakes axi status 的 phase 比上次前进 ④ 等门禁时 status=awaiting_approval。

w 诊报的原话:这是同一条根因 —— busy/stale 读数没有区分「忙」与「静着等外部(门禁/管线)」;D2 是「静着等」被读成 working,同族那面是「静着等」被读成 stale。它建议一起修:一个读数、三种状态(忙 / 静着等 / 真死)。

队长给的边界(w 转达的原话):这两条只改读数与收线语义,不许放松任何安全边界;teardown 的 unlanded-work 检查、焦点保护都必须保留,只是要能正确区分状态。

【本次本地 test 步(务必 intent-targeted,勿跑全套)】CONTRIBUTING.md(line 73-74)规定本地 no-mistakes Test 保持 intent-targeted、不配置 commands.test;请按本改动涉及的面跑,例如 tests/fm-watch-triage.test.sh、tests/fm-daemon.test.sh、tests/fm-teardown.test.sh、tests/fm-herdr-session-cleanup.test.sh、tests/fm-crew-state.test.sh、tests/fm-backend-herdr.test.sh(经 bin/fm-test-run.sh)。不要跑 bin/fm-test-run.sh --all(实测 >31 分钟,会撞 test 步 30 分钟硬顶)。

【2026-09-24 收敛后的范围(firstmate 裁决 013/014/015)】本 PR 只交两件事:① 三态读数本体 —— bin/fm-classify-lib.sh 的 crew_stale_class 与 crew_stale_is_actively_working(六态:busy/advancing/monitoring/landed/paused/none)加 bin/fm-crew-state.sh 的 no recent step activity 标记,含其测试与文档;② D1 收线递延 —— bin/fm-teardown.sh 在「焦点被拒」时仅当 bin/fm-herdr-session-cleanup.sh --candidate-ready 自证会接手该候选时才递延,bin/backends/herdr.sh 报告拒绝原因并提供 agent-free 判据;含其测试与文档。watcher 的 stale/wedge 消费方(pause_state_class 及其所有分支)与 away 姿态(bin/fm-supervise-daemon.sh)已按裁决回到 main 原样,本 PR 不改其行为;静着等的路由/复核退化作为已知缺口记录在案,不在本 PR 补。本地 test 步请保持 intent-targeted:bin/fm-test-run.sh 跑 tests/fm-watch-triage.test.sh tests/fm-crew-state.test.sh tests/fm-teardown.test.sh tests/fm-herdr-session-cleanup.test.sh tests/fm-backend-herdr.test.sh tests/fm-daemon.test.sh,不要跑 --all(全套 >31 分钟会撞 30 分钟硬顶)。

What Changed

  • bin/fm-classify-lib.sh gains crew_stale_class (busy / advancing / monitoring / landed / paused / none) and crew_stale_is_actively_working as the single owner of what a silent pane is, and bin/fm-crew-state.sh appends a no recent step activity marker to an active run-step detail whose client activity verdict is quiet (gated on RUN_SOURCE=full, exempting the ci monitor) so a stopped step is no longer read as an actively advancing one. The readout has no production caller in this release; the watcher's stale/wedge consumers keep their existing classification.
  • bin/backends/herdr.sh now reports why a projected close or focus mutation refused (active-tab, unknown-focus, unverified-focus, unverified-pane, agent-state-mismatch, plan-refused, close-failed, workspace-removal-unconfirmed, focus-restore-failed), carries the emptying plan's refusal reason out of its command substitution, and adds an agent-free close requirement backed by the new fm_backend_herdr_pane_agent_free predicate (no-agent or stale-agent).
  • bin/fm-herdr-session-cleanup.sh gets the read-only --candidate-ready <task-id> [--ignore-focus] [--pending-meta] mode built on the sweep's own proof functions, sharing one candidate-proof sequence with the close path; bin/fm-teardown.sh defers a close only when it was refused specifically for active-tab and that check proves the next sweep would take the candidate, retiring the journal against the pane-presence reading. Every other refusal still holds all records, and the unlanded-work and captain-focus guards are unchanged.
  • Documentation updated in docs/architecture.md, docs/herdr-backend.md, and docs/verification/runtime-backends.md.

Risk Assessment

⚠️ Medium: The change is bounded and intent-conformant with real behavior tests, but it alters destructive herdr teardown so records can be removed while a projection pane stays open, licensing the deferral on a read-only --candidate-ready probe, so the residual risk is real enough to warrant follow-up attention rather than an unqualified low rating.

Testing

通过 bin/fm-test-run.sh 运行了 intent 定向的六套件集合(未使用 --all):6 个全部通过,0 失败、0 gate skip,耗时 23m20s。D1 修复针对真实的 teardown 与 session-cleanup 可执行文件做了端到端驱动:一个 landed task,其投影 pane 位于 captain 的 active tab 上且为 agent-free 的 idle shell,现在能完成其 record cleanup,保持 pane 打开、presentation journal 不变,随后真实的 session-start sweep 恰好关闭该 pane 并 retire 该 journal。两个对抗性守卫被驱动并通过:一个 agent-free 但并非干净 lone idle childless shell 的 pane,以及一个仍持有 live agent 的 pane,二者都保留全部 record 且不关闭任何东西,因此不可能产生 orphaned deferred close。round-1 的 plan-refusal-reason 修复通过真实的 herdr backend 驱动:plan 将 active-tab 带出其 command substitution,因此只有获得许可的 refusal 才能 defer。--candidate-ready 作为真实 subprocess 驱动,仅对确切的 licensed candidate 回答 ready,其余一律以 read-only 方式拒绝。对于 readout 部分,真实的 bin/fm-crew-state.sh 会为本 crew 自身的 quiet run 追加 · no recent step activity,而绝不会把外部 coarse-lane run 的 quiet verdict 断言到本 crew 上;真实的 crew_stale_class/crew_stale_is_actively_working 函数在库级被针对 stub verdict 执行(readout-six-state-transcript.txt,mismatches=0),但由于 intent 有意将生产消费者排除在本 release 之外,该三态 readout 未被任何运行中的产品调用,因此未获实时结果。Focus protection(captain 的 active tab 从未被关闭、无 workspace close)与 unlanded-work refusal 保持完好。Reviewer 可见的 artifacts:完整套件日志、一段聚焦摘录,以及两份手动 CLI transcript。此变更不存在 UI surface,因此未生成截图。

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
D1 happy path:一个 landed task 的投影 pane 位于 captain 自身的 active tab 上且无 live agent;teardown 完成 record cleanup、保持 pane 打开、保留 presentation journal,随后下一次真实 session-start sweep 恰好关闭该 pane 并 retire 该 journal ✅ pass live ok - herdr projection teardown defers a focus-blocked close only when the session-start sweep then takes it, instead of orphaning a landed task(真实的 bin/fm-teardown.sh + 真实的 bin/fm-herdr-session-clea…
D1 守卫:对 agent-free 但并非干净 lone idle childless shell 的 pane 施加同样的 captain-active-tab refusal,会保留全部 durable record 且不关闭任何东西,因此不会制造 orphaned deferred close ✅ pass live ok - herdr projection teardown keeps every record when the focus-blocked pane is agent-free but not a clean idle shell(focused-evidence-excerpt.txt)
D1 守卫:当 pane 仍持有 live agent 时施加同样的 refusal,会保留全部 record 且绝不关闭 captain 的 tab ✅ pass live ok - herdr projection teardown keeps every record when the focus-blocked pane still holds a live agent(focused-evidence-excerpt.txt)
守卫:只有 active-tab refusal 才许可 deferral;plan 级的 focus checkpoint refusal 会将其自身 reason 带出其 command substitution 传给 caller,因此该 reason 不会被丢失或伪造 ✅ pass live ok - herdr presentation cleanup: a plan refusal carries the focus checkpoint&#39;s own reason to the caller 驱动真实的 bin/backends/herdr.sh 函数,并断言 rc=1 reason=active-tab,且无 close/move(focused-evidence-exc…
fm-herdr-session-cleanup.sh --candidate-ready 是一个 read-only gate:仅对确切的 licensed candidate 回答 ready,拒绝所有其他 candidate 或缺失 candidate,并在调用格式错误时以 exit 2 退出,不做任何变更 ✅ pass live ok - candidate-ready answers only for the exact licensed resumable candidate,加上真实 subprocess transcript candidate-ready-cli-contract.txt(usage exit 2、未知 flag exit 2、no-such-candidate exit 1)
no-progress 标记的 producer:真实的 bin/fm-crew-state.sh 会为本 crew 自身的 quiet active run 追加 · no recent step activity,而绝不会把外部 coarse-lane 分支的 quiet verdict 断言到本 crew 上 ✅ pass live ok - a foreign quiet run cannot mark this crew, its own quiet run still does(真实的 bin/fm-crew-state.sh CLI,配合伪造的 no-mistakes/tmux)
三态 readout:crew_stale_class 正确读出 busy / advancing / monitoring / landed / paused / none(包括将 pipeline 自身的 no-progress 标记读为 none),且 crew_stale_is_actively_working 仅在 busy 与 advancing 时为 true ⏸️ untested no 前次 payload 未建立 live 结果:它只是库级契约驱动——source 真实的 bin/fm-classify-lib.sh 并针对 stub 的 fm-crew-state.sh verdict 调用 crew_stale_class/crew_stale_is_actively_working(readout-six-state-transcript.txt),并不是对运行中…
安全边界保持完好:captain 的 active tab 绝不关闭,deferral 时 focus 绝不移动,且 unlanded-work / non-idle-pane refusal 保持不变 ✅ pass live D1 测试断言该 pane 未被关闭且未发生 workspace close;同一套件中既有的 refusal(retains every record when post-close presence is unknown、unlanded-work refusal 用例)仍以 failed=0 通过
Evidence: Intent 定向的 no-mistakes Test 套件日志(6 个套件,23m20s,failed=0,skipped_gate=0)

Source: Intent 定向的 no-mistakes Test 套件日志(6 个套件,23m20s,failed=0,skipped_gate=0)

FM_TEST_SUMMARY total=6 failed=0 skipped_gate=0 duration_ms=1400187

FM_TEST_BEGIN 2026-09-24T15:22:07Z tests/fm-backend-herdr.test.sh family=backend-dispatch expected_gate_skip=none
FM_TEST_BEGIN 2026-09-24T15:22:07Z tests/fm-crew-state.test.sh family=pure-contract-unit expected_gate_skip=none
ok - active run-step is authoritative
ok - stale needs-decision over active run is superseded
ok - stale blocked over active run is superseded
ok - daemon/timeout blocked claim over a live fixing run reads as run alive
ok - socket refusal or missing socket over a stale fixing run reports blocked
ok - socket refusal over a terminal attributed run reports blocked
ok - broken-pipe blocker over a live run keeps the plain superseded reading
ok - genuine daemon-down blocked line still reports blocked
ok - genuine parked run is not flagged superseded
ok - scalar gate parked run is not flagged superseded
ok - gate block parked run is not flagged superseded
ok - ci-ready status log beats monitoring run
ok - ci-monitoring run with checks already green surfaces done
ok - top-level ci status uses ci log green marker
ok - terminal no-checks ci-monitor marker surfaces done
ok - base-advance rearm after green stays working
ok - pending no-checks ci-monitor marker stays working
ok - ci-monitoring run with checks not yet green stays working
ok - a fresh issue after an earlier green reading is not masked
ok - stale checks-green status log does not mask CI relapse
ok - ci fixing is not overridden by an earlier green marker
ok - top-level fixing is not overridden by a stale ci running row
ok - top-level fixing is not overridden by a stale done log
ok - terminal passed run is authoritative
ok - terminal passed run with an open PR reads held-for-merge, never merged
ok - terminal passed run with a proven merge record reads PR merged
ok - an identity-mismatched merge record does not prove this PR merged
ok - the attributed run's PR identity outranks a stale task meta PR
ok - a passed run with no PR identity claims no merge and names no URL
ok - terminal failed run is authoritative
ok - orphaned ci monitor after green reads as held-for-merge done
ok - status-only failed orphaned ci monitor after green reads done
ok - genuinely failing CI keeps the failed verdict
ok - a second failed step disqualifies the orphaned-monitor reclassification
ok - cross-branch run is attributed via the real runs list
ok - socket refusal over a coarse active run reports blocked
ok - failed ledger record reads unknown only when the daemon is provably down
ok - cross-branch attribution picks the branch's most recent row
ok - a live run outranks a terminal run bound to the same worktree
ok - runs-list selection prefers a live row over a newer terminal one
ok - an unfetched live sibling outranks a terminal row at the worktree's exact commit
ok - two terminal rows keep the existing newest-first precedence
ok - an unclassifiable status row keeps the ledger's newest-first precedence
ok - a terminal run with no live sibling is unchanged
ok - coarse run does not probe another branch's ci log
ok - another branch's run is ignored, falls back
ok - no run + a busy semantic record reads working, attributed to its source
ok - a converted adapter never reads working from rendered footer text
ok - grok still reads working through its isolated rendered-tail fallback
ok - herdr's native busy verdict reads working with no record present
ok - a herdr CLI that fails to answer reads unknown/unreachable, never gone
ok - an alive endpoint whose scrollback read failed stays working
ok - a husk pane (agent gone) still reads gone for reclaim
ok - a mid-tool-call crew stays working because its record outranks herdr's generation state
ok - an idle record with idle agent_status stays not-busy (no regression for a human-blocked agent)
ok - no run + idle pane uses the status-log verb
ok - no run + idle pane parses keyed status syntax
ok - no run + idle pane on a paused: status reports state: paused with its reason
ok - no run + idle pane honors the configured paused verb
ok - a trailing resolved: event does not corrupt state render (idle stays idle)
ok - dead window ignores stale status log
ok - a tmux that fails to answer reads unknown/unreachable, never gone
ok - closed pane still reports a terminal run-step
ok - closed pane still reports an active run-step
ok - no timeout command uses perl bound
ok - scout skips the run lookup
ok - torn-down worktree is handled gracefully
ok - fm-crew-state remote: alive endpoint falls through to the routed status log
ok - fm-crew-state remote: an idle alive endpoint reads alive, never gone or dead
ok - fm-crew-state remote: an unreachable host reads unknown-remote, never gone or dead
ok - fm-crew-state remote: the remote host's own dead verdict is reported truthfully
ok - missing meta is handled gracefully
ok - crew_is_provably_working absorbs a validating crew found only via the runs-list fallback
ok - crew_is_provably_working still surfaces a genuinely stopped crew (safety property preserved)
ok - usage error exits 2
ok - historical same-branch rewritten head is not attributed as current
ok - active run with valid descendant fix head remains current
ok - local work advanced past run head invalidates attribution
ok - pipeline-owned active run binds without head equality and beats the failed row
ok - a genuinely failed run with no later run is not hidden
ok - coarse scan anchors the unresolvable active row instead of falling to an older one
ok - coarse scan with a mismatched anchor stays unknown and lets the pane answer
ok - the exemption requires branch_sync.state=pipeline_owned
ok - the exemption never applies to a terminal run
ok - missing run head falls back instead of matching by branch
ok - active fix round with an unfetched pipeline head reads working
ok - unanchored unverifiable active row is never attributed
ok - unresolvable terminal row never reads as current
ok - runs-list continuation attribution works when axi answers another branch
ok - a foreign quiet run cannot mark this crew, its own quiet run still does
ok - herdr stale registration over a shell-only pane reads agent gone, not alive
ok - herdr stale working record never reports a shell-only pane busy
all fm-crew-state tests passed
FM_TEST_END 2026-09-24T15:22:25Z tests/fm-crew-state.test.sh exit=0 duration_ms=18345 gate_skip=false
ok - fm_backend_herdr_version_check: accepts the current protocol (14)
ok - fm_backend_herdr_version_check: refuses an old protocol loudly
ok - fm_backend_herdr_version_check: refuses loudly when herdr is not installed
ok - fm_backend_herdr_workspace_label: a primary home (no marker) resolves to 'firstmate'
ok - fm_backend_herdr_workspace_label: a secondmate home (.fm-secondmate-home) resolves to '2ndmate-<id>'
ok - fm_backend_herdr_workspace_label: trims whitespace around the marker's secondmate id
ok - fm_backend_herdr_workspace_label: an empty marker file falls back to the primary label 'firstmate'
ok - fm_backend_herdr_workspace_label: two different secondmate homes get two different, non-colliding labels
ok - fm_backend_herdr_cli: sets HERDR_SESSION AND appends a trailing --session flag on every call
ok - herdr client selection: a live pane behind a stale shadowing client reads alive
ok - herdr recovery-grade read: a stopped server means missing there, and nowhere else
ok - herdr stale registration: a shell-only pane with a lingering Pi record is agent-free with an explicit reason
ok - herdr stale registration: no registered status can outrank a shell-only process view
ok - herdr stale registration: a registered agent with a live Pi foreground process still reads alive
ok - herdr stale registration: only a shell-only pane demotes a registration
ok - herdr stale registration: a transient prompt helper settles into stale-agent instead of reading live
ok - herdr stale registration: an exhausted settle window still reads a non-shell foreground as live
ok - herdr stale registration: an agent process outside the foreground group still counts as alive
ok - herdr stale registration: the descendant walk reads a spaced executable path whole
ok - herdr stale registration: an unreadable process view refuses instead of guessing either way
ok - herdr stale registration:

... [52790 bytes truncated] ...

━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 2s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  watcher cycles: EMPTY - no readable cycle ledger at /tmp/fm-test-run.t0FPJV/w5/tmp/fm-teardown-tests.hdBSKO/legacy-allow/state/.watch-cycle-exits.log
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair a missing or failed watcher cycle with the Pi tool fm_watch_arm_pi, or restart Pi with -e ~/.no-mistakes/worktrees/e7f71fdfbe8f/01M39YPRE9DWEBZMP67B4YXB59/.pi/extensions/fm-primary-turnend-guard.ts -e ~/.no-mistakes/worktrees/e7f71fdfbe8f/01M39YPRE9DWEBZMP67B4YXB59/.pi/extensions/fm-primary-pi-watch.ts if the extensions are not loaded.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ok - a landed legacy record with a dead endpoint tears down and logs its accepted incarnation
ok - --legacy-record never relaxes the unlanded-work refusal
ok - an endpoint that cannot be confidently read as dead refuses --legacy-record teardown
ok - --legacy-record teardown rolls its stamp back when the close marker write fails
ok - a legacy stamp a failed rollback left behind still faces the endpoint gate
ok - a corrupt spawn_gen is never accepted as a legacy record
ok - provably-stale worktree index.lock (old, no live holder) is cleared and teardown succeeds
ok - live-held worktree index.lock is never removed and teardown refuses
ok - lsof errors leave worktree index.lock in place and refuse teardown
ok - stale lock cleanup rechecks and refuses dirty worktree before return
ok - normal repo index.lock is resolved from the worktree and cleared when stale
ok - lock mtime read failures leave worktree index.lock in place and refuse teardown
ok - transient index.lock cleared after first failed return is retried successfully without force-remove
ok - persistent index.lock exhausts retries and refuses without force-removing the lock
ok - empty retry wait overrides use the default without aborting teardown
ok - fractional legacy retry wait remains supported without arithmetic
ok - a task's own parked no-mistakes run is aborted, not orphaned, before the worker is removed
ok - a parked run the pipeline advanced past the task copy is still concluded from the runs ledger, not orphaned
ok - a ledger row for a different head never authorizes a parked-run abort
ok - a malformed ledger row never authorizes a parked-run abort
ok - an impossible ledger date never authorizes a parked-run abort
ok - a terminal status with a stale gate never reaches ledger cleanup
ok - an advanced head present locally aborts through the strict rule alone - the ledger fallback stays dormant
ok - an unresolvable active row with no same-branch anchor is never concluded (conservative refusal)
ok - an ancestor-only anchor never binds an advanced parked run to this task
ok - a terminal unfetched-head row is stale history and never concludes a run
ok - a terminal newest row anchored at this worktree's head never authorizes an abort
ok - a resolvable diverged newer same-branch row makes every older row stale history; no run is concluded
ok - consecutive unresolvable rows are ambiguous and never conclude a run
ok - a ledger-proven continuation is still left alone while the run is autonomously active
ok - teardown refuses before reap or removal when a task-owned run remains parked
ok - a different run cannot confirm the targeted abort
ok - empty post-abort status is not accepted as confirmation
ok - the CLI's exact run-not-found signal confirms completion
ok - a parked run on another branch is never aborted by this task's teardown (ownership is precise)
ok - a task-owned autonomous running step is left alone rather than aborted
ok - a leaked descendant process rooted under the task's worktree is reaped by teardown, not left surviving
ok - a leaked descendant process rooted under the task's per-task tasktmp is reaped by teardown too
ok - missing lsof falls back to reaping the tmux pane process group
ok - an erroring lsof scan refuses teardown and preserves the task
ok - a reused pid with a different start time is never force-killed
ok - an exec change preserves birth identity and the process is reaped
ok - a process spawned during grace is reaped on a later pass
ok - persistent leaked processes refuse teardown after bounded retries
ok - a process exiting during identity lookup does not block teardown
ok - the run abort and the leaked-process reap both complete before the destructive worktree return
FM_TEST_END 2026-09-24T15:45:09Z tests/fm-teardown.test.sh exit=0 duration_ms=381312 gate_skip=false
FM_TEST_BEGIN 2026-09-24T15:45:09Z tests/fm-herdr-session-cleanup.test.sh family=backend-dispatch expected_gate_skip=none
ok - process proof reads Linux Herdr argv arrays and rejects malformed executable identities
ok - exact stale projection closes one exact pane under task then presentation locks
ok - successful cleanup is idempotent on repeat
ok - the sweep treats the registered post-agent-exit reading as agent-free, with the idle shell proof
ok - a registered post-agent-exit reading still needs the lone idle childless shell proof
warning: herdr session-start projection cleanup: task preserved because immediate revalidation changed or was unreadable
warning: herdr session-start projection cleanup: task preserved because its candidate snapshot was ambiguous
warning: herdr session-start projection cleanup: task preserved because its pane is not a provably idle childless shell
ok - candidate-ready answers only for the exact licensed resumable candidate
ok - malformed title preserves the candidate
ok - missing token preserves the candidate
ok - malformed journal preserves the candidate
ok - duplicate token preserves the candidate
ok - duplicate title token preserves the candidate
ok - zero journal match preserves the candidate
ok - multiple journal matches preserves the candidate
ok - cross-home journal preserves the candidate
ok - v2 workspace binding mismatch preserves the candidate
ok - v2 tab binding mismatch preserves the candidate
ok - v2 pane binding mismatch preserves the candidate
ok - v2 cleanup requires and accepts the exact journal endpoint binding
ok - current task metadata preserves the candidate
ok - registered agent preserves the candidate
ok - unknown agent preserves the candidate
ok - multiple tabs preserves the candidate
ok - multiple panes preserves the candidate
ok - non-idle shell preserves the candidate
ok - child process or shell job preserves the candidate
ok - unreadable snapshot preserves the candidate
ok - unreadable topology check preserves the candidate
ok - revalidation race preserves the candidate
ok - active target preserves the candidate
ok - focus refusal preserves the candidate
ok - standalone bootstrap cannot run lock-owned stale projection cleanup
ok - session start runs cleanup only after acquiring its home lock
all fm-herdr-session-cleanup tests passed
FM_TEST_END 2026-09-24T15:45:27Z tests/fm-herdr-session-cleanup.test.sh exit=0 duration_ms=17785 gate_skip=false
FM_TEST_SUMMARY total=6 failed=0 skipped_gate=0 duration_ms=1400187
FM_TEST_SUMMARY_FAMILY family=backend-dispatch count=2 duration_ms=57029 failed=0
FM_TEST_SUMMARY_FAMILY family=pr-forge count=1 duration_ms=381312 failed=0
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=18345 failed=0
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=2 duration_ms=999809 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-watch-triage.test.sh duration_ms=961479
FM_TEST_SLOWEST rank=2 script=tests/fm-teardown.test.sh duration_ms=381312
FM_TEST_SLOWEST rank=3 script=tests/fm-backend-herdr.test.sh duration_ms=39244
FM_TEST_SLOWEST rank=4 script=tests/fm-daemon.test.sh duration_ms=38330
FM_TEST_SLOWEST rank=5 script=tests/fm-crew-state.test.sh duration_ms=18345
FM_TEST_SLOWEST rank=6 script=tests/fm-herdr-session-cleanup.test.sh duration_ms=17785
Evidence: 聚焦证据摘录:D1 deferral + 守卫、plan-refusal reason、candidate-ready、三态 readout、no-progress 标记

Source: 聚焦证据摘录:D1 deferral + 守卫、plan-refusal reason、candidate-ready、三态 readout、no-progress 标记

## D1 teardown deferral ok - herdr projection teardown defers a focus-blocked close only when the session-start sweep then takes it, instead of orphaning a landed task ok - herdr projection teardown keeps every record when the focus-blocked pane is agent-free but not a clean idle shell ok - herdr projection teardown keeps every record when the focus-blocked pane still holds a live agent ## Round-1 plan-refusal-reason fix ok - herdr presentation cleanup: a plan refusal carries the focus checkpoint's own reason to the caller ## three-state readout ok - crew_stale_class: busy, the quiet external waits (advancing/monitoring/paused), landed, and the dead/no-progress cases each read as their own state

# Intent-targeted no-mistakes Test — focused evidence excerpts
# runner: bin/fm-test-run.sh (6 suites, no --all)

## Suite result
FM_TEST_SUMMARY total=6 failed=0 skipped_gate=0 duration_ms=1400187
FM_TEST_SUMMARY_FAMILY family=backend-dispatch count=2 duration_ms=57029 failed=0
FM_TEST_SUMMARY_FAMILY family=pr-forge count=1 duration_ms=381312 failed=0
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=18345 failed=0
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=2 duration_ms=999809 failed=0

## D1 teardown deferral (real bin/fm-teardown.sh + real bin/fm-herdr-session-cleanup.sh)
ok - herdr projection teardown retires its journal only after confirming the exact recorded pane is gone
ok - herdr projection teardown retains every record when post-close presence is unknown
ok - herdr projection teardown surfaces failed focus restoration without turning confirmed cleanup into a hard failure
ok - herdr projection teardown defers a focus-blocked close only when the session-start sweep then takes it, instead of orphaning a landed task
ok - herdr projection teardown keeps every record when the focus-blocked pane is agent-free but not a clean idle shell
ok - herdr projection teardown keeps every record when the focus-blocked pane still holds a live agent

## Round-1 plan-refusal-reason fix (real bin/backends/herdr.sh)
ok - herdr presentation cleanup: a plan refusal carries the focus checkpoint's own reason to the caller

## candidate-ready read-only gate (real bin/fm-herdr-session-cleanup.sh)
ok - the sweep treats the registered post-agent-exit reading as agent-free, with the idle shell proof
ok - a registered post-agent-exit reading still needs the lone idle childless shell proof
ok - candidate-ready answers only for the exact licensed resumable candidate

## three-state readout (real bin/fm-classify-lib.sh)
ok - crew_stale_class: busy, the quiet external waits (advancing/monitoring/paused), landed, and the dead/no-progress cases each read as their own state

## no-progress marker producer (real bin/fm-crew-state.sh)
ok - a foreign quiet run cannot mark this crew, its own quiet run still does
Evidence: crew_stale_class 六态 readout transcript(真实的 bin/fm-classify-lib.sh,mismatches=0)

Source: crew_stale_class 六态 readout transcript(真实的 bin/fm-classify-lib.sh,mismatches=0)

ok state: working · source: pane · harness busy (native) busy yes ok state: working · source: run-step · validating (running) advancing yes ok state: working · source: run-step · ci running monitoring no ok state: parked · source: run-step · parked at review gate monitoring no ok state: done · source: run-step · run passed: PR held for merge landed no ok state: paused · source: status-log · awaiting upstream paused no ok state: working · source: run-step · validating (running) · no recent step activity none no mismatches=0

fm-crew-state.sh verdict                                     class       actively-working?
------------------------------------------------------------ ----------- ------------------
ok   state: working · source: pane · harness busy (native)  busy        yes
ok   state: working · source: run-step · validating (running) advancing   yes
ok   state: working · source: run-step · validating (fixing) advancing   yes
ok   state: working · source: run-step · ci running         monitoring  no
ok   state: working · source: run-step · run active         monitoring  no
ok   state: parked · source: run-step · parked at review gate monitoring  no
ok   state: done · source: run-step · run passed: PR held for merge landed      no
ok   state: paused · source: status-log · awaiting upstream paused      no
ok   state: working · source: run-step · validating (running) · no recent step activity none        no
ok   state: failed · source: run-step · run failed          none        no
ok   state: unknown · source: none · worktree gone (torn down?) none        no
mismatches=0
Evidence: fm-herdr-session-cleanup.sh --candidate-ready CLI 契约 transcript

Source: fm-herdr-session-cleanup.sh --candidate-ready CLI 契约 transcript

$ bin/fm-herdr-session-cleanup.sh --candidate-ready usage: ... --candidate-ready <task-id> [--ignore-focus] [--pending-meta] exit=2 $ bin/fm-herdr-session-cleanup.sh --candidate-ready --bogus x fm-herdr-session-cleanup.sh: unknown flag --bogus exit=2 $ bin/fm-herdr-session-cleanup.sh --candidate-ready task-x1 exit=1

$ bin/fm-herdr-session-cleanup.sh --candidate-ready        # no id
usage: fm-herdr-session-cleanup.sh --candidate-ready <task-id> [--ignore-focus] [--pending-meta]
exit=2

$ bin/fm-herdr-session-cleanup.sh --candidate-ready --bogus x  # unknown flag
fm-herdr-session-cleanup.sh: unknown flag --bogus
exit=2

$ bin/fm-herdr-session-cleanup.sh --candidate-ready task-x1  # no such candidate
exit=1

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ bin/backends/herdr.sh:1115 - This assignment claims "the gate's own reason is the accurate one whenever it left one", but the only way this branch can carry a reason is the mutation gate called at line 1322 inside fm_backend_herdr_emptying_close_plan, and that function is always invoked through a command substitution (line 1100 here, line 3402 in fm_backend_herdr_kill_serialized). The gate's FM_BACKEND_HERDR_PROJECTION_MUTATION_REFUSAL assignment happens in that subshell and never reaches this shell, so the variable here is empty (falling back to plan-refused) or, if a caller ever calls the gate earlier in the same process, an unrelated stale value. Concrete sequence: the captain moves focus onto the task pane's own tab during the window between the focus snapshot at line 1056 (which still showed the target workspace != focused workspace, so the plan takes the reposition branch at 1298-1325) and the plan's own gate call; the gate returns 1 with reason active-tab, the plan prints refuse, and this line records plan-refused. bin/fm-teardown.sh:3987 only licenses its durable deferral when the reason is active-tab, so it keeps every record and refuses — the landed pane stays alive with no owner and the next supervision round re-reads it as a wedge, which is exactly the D1 failure this change exists to remove. The same unreliability also means a future in-process caller that had earlier hit an active-tab refusal for a different tab could mis-license a deferral. Fix: have the plan carry its own reason out of the subshell (e.g. its last line refuse &lt;reason&gt;) and have the caller use that instead of a variable set inside the command substitution.
  • ℹ️ bin/fm-classify-lib.sh:2252 - The header of crew_stale_class says "bin/fm-crew-state.sh and the no-progress (真死) arm consume it", but bin/fm-crew-state.sh never calls crew_stale_class — it is the producer of the · no recent step activity marker that this readout consumes, and grep over bin/ shows no production caller of crew_stale_class or crew_stale_is_actively_working at all (the stale/wedge consumers were deliberately left on main). The direction is reversed and the only remaining consumers are tests, so a maintainer reading this contract would look for wiring that is not there. State the actual direction (this readout reads the marker fm-crew-state.sh emits) and that no production consumer is wired in this release yet.

🔧 Fix applied.
2 infos still open:

  • ℹ️ docs/architecture.md:13 - The paragraph this change added says crew_stale_class "is consumed by the crew-state no-progress marker, which reports a run step with no activity behind it as no-progress rather than as an active step". The direction is reversed: bin/fm-crew-state.sh emits the · no recent step activity marker and crew_stale_class reads it (bin/fm-classify-lib.sh:2314), which is exactly the reversal this same change corrected in the fm-classify-lib.sh header (line 2252). It also contradicts that corrected header, which states no production consumer is wired in this release. Fix is wording only: state that the readout reads the crew-state no-progress marker, and that nothing in production calls it yet.
  • ℹ️ tests/fm-teardown.test.sh:2787 - test_herdr_projection_teardown_refuses_deferral_for_a_non_idle_shell_pane passes FM_FAKE_HERDR_AGENT_NOT_IDLE_SHELL=1, but that flag only makes the fake's agent get answer agent_not_found. The pane process-info branch that models "agent-free registration over a pane that is NOT a clean idle childless shell" (two foreground processes) is gated on FM_FAKE_HERDR_SHELL_NOT_IDLE, which no test sets, so it is dead code. The test therefore refuses the deferral because pane process-info errors with pane_not_found, not because the lone-idle-childless-shell proof rejected a non-idle shell as its name claims. The refusal assertion still holds, but the named safety boundary is not actually exercised at the teardown level. Fix: set FM_FAKE_HERDR_SHELL_NOT_IDLE=1 there so the intended branch runs.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
D1 happy path:一个 landed task 的投影 pane 位于 captain 自身的 active tab 上且无 live agent;teardown 完成 record cleanup、保持 pane 打开、保留 presentation journal,随后下一次真实 session-start sweep 恰好关闭该 pane 并 retire 该 journal ✅ pass live ok - herdr projection teardown defers a focus-blocked close only when the session-start sweep then takes it, instead of orphaning a landed task(真实的 bin/fm-teardown.sh + 真实的 bin/fm-herdr-session-clea…
D1 守卫:对 agent-free 但并非干净 lone idle childless shell 的 pane 施加同样的 captain-active-tab refusal,会保留全部 durable record 且不关闭任何东西,因此不会制造 orphaned deferred close ✅ pass live ok - herdr projection teardown keeps every record when the focus-blocked pane is agent-free but not a clean idle shell(focused-evidence-excerpt.txt)
D1 守卫:当 pane 仍持有 live agent 时施加同样的 refusal,会保留全部 record 且绝不关闭 captain 的 tab ✅ pass live ok - herdr projection teardown keeps every record when the focus-blocked pane still holds a live agent(focused-evidence-excerpt.txt)
守卫:只有 active-tab refusal 才许可 deferral;plan 级的 focus checkpoint refusal 会将其自身 reason 带出其 command substitution 传给 caller,因此该 reason 不会被丢失或伪造 ✅ pass live ok - herdr presentation cleanup: a plan refusal carries the focus checkpoint&#39;s own reason to the caller 驱动真实的 bin/backends/herdr.sh 函数,并断言 rc=1 reason=active-tab,且无 close/move(focused-evidence-exc…
fm-herdr-session-cleanup.sh --candidate-ready 是一个 read-only gate:仅对确切的 licensed candidate 回答 ready,拒绝所有其他 candidate 或缺失 candidate,并在调用格式错误时以 exit 2 退出,不做任何变更 ✅ pass live ok - candidate-ready answers only for the exact licensed resumable candidate,加上真实 subprocess transcript candidate-ready-cli-contract.txt(usage exit 2、未知 flag exit 2、no-such-candidate exit 1)
no-progress 标记的 producer:真实的 bin/fm-crew-state.sh 会为本 crew 自身的 quiet active run 追加 · no recent step activity,而绝不会把外部 coarse-lane 分支的 quiet verdict 断言到本 crew 上 ✅ pass live ok - a foreign quiet run cannot mark this crew, its own quiet run still does(真实的 bin/fm-crew-state.sh CLI,配合伪造的 no-mistakes/tmux)
三态 readout:crew_stale_class 正确读出 busy / advancing / monitoring / landed / paused / none(包括将 pipeline 自身的 no-progress 标记读为 none),且 crew_stale_is_actively_working 仅在 busy 与 advancing 时为 true ⏸️ untested no 前次 payload 未建立 live 结果:它只是库级契约驱动——source 真实的 bin/fm-classify-lib.sh 并针对 stub 的 fm-crew-state.sh verdict 调用 crew_stale_class/crew_stale_is_actively_working(readout-six-state-transcript.txt),并不是对运行中…
安全边界保持完好:captain 的 active tab 绝不关闭,deferral 时 focus 绝不移动,且 unlanded-work / non-idle-pane refusal 保持不变 ✅ pass live D1 测试断言该 pane 未被关闭且未发生 workspace close;同一套件中既有的 refusal(retains every record when post-close presence is unknown、unlanded-work refusal 用例)仍以 failed=0 通过
  • bin/fm-test-run.sh tests/fm-watch-triage.test.sh tests/fm-crew-state.test.sh tests/fm-teardown.test.sh tests/fm-herdr-session-cleanup.test.sh tests/fm-backend-herdr.test.sh tests/fm-daemon.test.sh (intent 定向,未使用 --all): FM_TEST_SUMMARY total=6 failed=0 skipped_gate=0
  • ok - herdr projection teardown defers a focus-blocked close only when the session-start sweep then takes it, instead of orphaning a landed task
  • ok - herdr projection teardown keeps every record when the focus-blocked pane is agent-free but not a clean idle shell
  • ok - herdr projection teardown keeps every record when the focus-blocked pane still holds a live agent
  • ok - herdr presentation cleanup: a plan refusal carries the focus checkpoint's own reason to the caller
  • ok - candidate-ready answers only for the exact licensed resumable candidate
  • ok - the sweep treats the registered post-agent-exit reading as agent-free, with the idle shell proof 和 ok - a registered post-agent-exit reading still needs the lone idle childless shell proof
  • ok - a foreign quiet run cannot mark this crew, its own quiet run still does
  • ok - crew_stale_class: busy, the quiet external waits (advancing/monitoring/paused), landed, and the dead/no-progress cases each read as their own state
  • 手动驱动:source 真实的 bin/fm-classify-lib.sh,并在 11 组 state/source/detail 输入上调用 crew_stale_class/crew_stale_is_actively_working -> readout-six-state-transcript.txt (mismatches=0)
  • 手动驱动:真实的 bin/fm-herdr-session-cleanup.sh --candidate-ready subprocess -> usage exit 2(缺少 id)、exit 2(未知 flag)、exit 1(无此 candidate)-> candidate-ready-cli-contract.txt
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

firstmate worker and others added 16 commits September 24, 2026 22:56
A pane whose crew's own run is merely holding - validating, the pipeline's ci
monitor, or a run parked at a gate - was read as `working`, so the wedge timer
aged on lanes no human had touched; the same quiet surfaced as a bare stale wake
on the other face of that one root cause.

`crew_stale_class` (bin/fm-classify-lib.sh) now owns the three-state readout and
`pause_state_class` maps it onto the watcher's decision: only a busy pane or an
actively working run step ages the wedge timer, a quiet external wait and an
already-landed delivery take the bounded pause cadence, and a run step the
pipeline itself reports quiet - its own no-progress verdict, carried into the
detail line by fm-crew-state.sh - surfaces at once. The terminal-status override
uses the narrower `crew_stale_is_actively_working`, so a pipeline that is only
monitoring its PR no longer restarts the timer on a delivered lane.

Teardown no longer abandons a landed task whose Herdr projection pane sits on the
captain's active tab: the focus gate and the close helper report why they
refused, the journal is retired only against a confirmed-gone pane, and an
active-tab refusal on an agentless pane finishes the record cleanup while leaving
the close recorded for the session-start presentation cleanup to resume. Every
other refusal, and a refusal on a pane that still holds an agent, keeps every
record exactly as before; the captain's focus is never taken and the
unlanded-work checks are untouched.
…l take it

Review finding F5 (no-mistakes run 01M3950EJT1ZK91KJE08X4RA7J): the deferred close
was licensed by "the focus gate refused and the pane has no agent", while the
session-start sweep that must finish the close requires strictly more - the exact
journal binding, the label/token grammar, one tab and one pane, and a lone idle
childless shell. An agent-free pane that is not that shape was therefore handed a
deferred close the sweep would refuse on every session start, after its records
were already gone.

The deferral now asks the resumer itself. `bin/fm-herdr-session-cleanup.sh
--candidate-ready <task-id> [--ignore-focus] [--pending-meta]` is a read-only mode,
takes no locks, and relaxes exactly the two conditions a deferral means (the
active tab that caused it, and the record this teardown is about to remove). Both
the sweep's close path and that check run the same proof sequence, so they cannot
drift, and anything unproven keeps every record - a close nothing can ever
collect is worse than a refusal.

The post-agent-exit reading is decided from this repository's own contract rather
than guessed: `fm_backend_herdr_pane_agent_state` names `stale-agent` (a Herdr
registration that outlived its process, the crew shape) "the explicit agent-free
reason", so the accepted set is `no-agent|stale-agent`. It lives once in
`fm_backend_herdr_pane_agent_free`, shared by the sweep, the deferral, and the
close helper's new `agent-free` requirement; what licenses closing is still the
process proof that follows it, which rules out both a running agent and the
nested shell `fm_backend_herdr_tab_is_husk` refuses. That husk rule is unchanged,
as are the unlanded-work checks and the focus protection.

Also retires the presentation journal only against a confirmed-gone pane: an
agentless pane that is still present is not a closed pane, and reading its agent
state as one dropped the only durable record of an open pane.

Tests: the deferral is proven to be one the sweep then takes (and the sweep is
run for real to close it), a `stale-agent` pane is accepted by both the deferral
and the sweep while still requiring the idle-shell proof, and an agent-free pane
that is not a clean idle shell keeps every record.
…ng honest

Review findings F6, F2, F4 and F1 (same run as the teardown deferral fix).

F6: `handle_paused_stale`'s `waiting`/`landed` arms were unreachable - every call
site reaches it for a declared or inconclusive wait, and both derived-wait callers
route to `handle_derived_wait_stale`. The arms, their duplicated wording, and the
unused `[class]` parameter are gone, so the derived-wait absorb has one owner.

F2/F4: a derived wait now keeps its cadence anchor and re-surface throttle across
pane churn (only the hash-scoped half resets, exactly as the declared path
protects itself), so a ticking render cannot restart the wait's clock until it
never re-surfaces; and the `working` arm ends a derived chain, so a wait that
ended cannot hand its finished anchor to the next one.

F1: the two registered tests still asserting the superseded "active run step =>
wedge timer" contract are migrated - the wedge timer is exercised with a busy-pane
fake, and the pause transition asserts reclassification into the derived wait,
plus a third phase proving a busy pane still arms the wedge timer.

F3 is answered by documentation rather than a silent behaviour change: the derived
class IS re-derived on every stable stale poll, because the declared path's cheap
window is also guarded by an endpoint-liveness probe every poll and reusing it
would delay both a no-progress surface and the return to wedge tracking.
…de has

Review finding architecture-doc-unrecognized-active-step (run
01M39BH5T8338MWQWC7SE0SSQA): the paragraph claimed every unrecognized detail
"keeps surfacing exactly as before", while a `working · source: run-step` detail
no version of the client vocabulary recognizes is `advancing` and takes the same
bounded-cadence derived-wait absorb as `monitoring` - the very sentence a
maintainer would read before adding a step detail. Only the client's own
no-progress marker moves such a step out of that class. Documentation only; no
behaviour changes.
…lane

Ruling on the ask-user finding away-mode-still-escalates-quiet-owned-lanes (the
D2 root cause on its other, already-existing consumer path, and the posture where
a false escalation costs the most): the away supervisor classified a quiet lane
only from its status line, so a lane whose own run was advancing, holding on its
ci monitor or a gate, or already landed kept being aged into
"stale persisted Ns (possible wedge)".

classify_stale now asks the same single-owner readout the always-on watcher uses
(bin/fm-classify-lib.sh's crew_stale_class) at both places a non-terminal quiet
can reach: the transient `working:` arm and the final persistence-recheck
fallback. A quiet external wait returns the daemon's existing `pause` action, so
the caller records the bounded pause cadence and drops any wedge marker.

The three guards hold, each pinned by tests:
- a genuinely unexplained quiet still ages and escalates: a busy pane, a run step
  the client marks quiet (no progress), a stopped/torn-down crew, and an
  unreadable verdict all keep `self|transient stale` and still escalate;
- an undecidable or unreadable reading is never promoted to a wait;
- nothing outside the away posture changes: the terminal, declared-wait, and
  captain-held paths are untouched, the non-away watcher is untouched, and the
  teardown pre-flight checks and focus protection are untouched.
…e propagation

The stale-pane readout, the wedge timer, and the away-mode recheck interaction
spent four review rounds without settling, so the derived-wait machinery and its
away-mode propagation leave this branch. What stays is the readout itself
(crew_stale_class and its token contract) and the D1 teardown deferral.

Reverted to main: the watcher's pause_state_class and every stale-path consumer it
drives, the daemon's quiet-wait classification and derived-wait records, the
derived-wait anchors and cadence, and the docs describing them.
@onyx-space
onyx-space force-pushed the fm/stale-readout-three-states-d1d2 branch from 553ca37 to 18c2e31 Compare September 24, 2026 15:56
@onyx-space onyx-space changed the title fix(bin): split stale-pane readout into busy, external wait, and no-progress fix(bin): split the silent-pane readout three ways and defer the focus-blocked teardown close Sep 24, 2026
…test code (both findings are in files this change touches; verified against the base diff). Root causes and fixes: 1. tests/fm-herdr-session-cleanup.test.sh (2x SC2329 "function is never invoked"): the new stale-agent sweep cases define `fm_backend_herdr_pane_agent_state` inside a subshell to override the sourced production function; ShellCheck cannot see the indirect call through `fm_herdr_session_cleanup`. Applied the repo's existing idiom in this same file (see its lines 43/49): a `# shellcheck disable=SC2329 # invoked indirectly by the sweep under test.` directive above each override. No behavior change. 2. tests/fm-watch-triage.test.sh (SC2034 `want`/`got` unused): the new `test_crew_stale_three_state_readout` declared leftover locals it never reads. Removed the unused names (`want line got`) from the `local` list. Dead declaration only; behavior unchanged. Verification (CI-parity: ShellCheck 0.11.0, `--norc --external-sources`, full dataflow, SC2329/SC2034 enabled as CI runs them): `bin/fm-lint.sh tests/fm-herdr-session-cleanup.test.sh tests/fm-watch-triage.test.sh` -> rc=0; `bin/fm-lint.sh` over all ten shell roots changed by this PR -> rc=0. Both affected test files execute successfully via `bin/fm-test-run.sh tests/fm-herdr-session-cleanup.test.sh tests/fm-watch-triage.test.sh` -> rc=0 (0 failed, 0 gate-skipped). Changes are confined to the worktree and touch only the two test files
@onyx-space
onyx-space merged commit 9374770 into main Sep 24, 2026
14 checks passed
@onyx-space
onyx-space deleted the fm/stale-readout-three-states-d1d2 branch September 24, 2026 21:59
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