fix: enforce supervision guards across harnesses - #5471
Conversation
…on-Pi supervision host Lease liveness is now the pure record test in every calling context, so an unmarked main honors a live branch lease held by a separate process, and a lease file engages the guard's claim serialization for any caller; a home with no lease files still takes no lock. bin/fm-harness.sh honors FM_SUPERVISION_PRIMARY_HARNESS while FM_SUPERVISION_ACTOR=branch, so a supervision branch running under another harness resolves own, crew, and secondmate to the primary's harness. fm_tasks_axi's watchdog moves into bin/fm-timeout-lib.sh as fm_exec_timed with a separate grace: the perl watchdog is preferred, runs the command in its own process group against wall-clock deadlines, forwards TERM/INT/HUP, and reaps the group, so a descendant holding captured output can no longer keep the caller waiting past the bound on a host without timeout. The Claude Stop auto-arm header records that Claude drops the exit 2 of a hook it terminated at the configured timeout, re-measured on Claude Code 2.1.281.
…wn process group Live runs of real Claude and Pi engine turns under the bound showed both CLIs start every tool command in a process group of its own, so those processes end through the engine's own TERM handling rather than the group signal or reap. Also clears the new timeout test's ShellCheck findings.
fm-afk-host-guards-r1: live validation, 2026-09-23Scope under test: step 2 rung 3a-guards of the AFK revamp.
Isolation
1. Complex sessions, before vs afterA. Claude primary with its Stop-hook watcher cycle (no lease files anywhere)Captain request: dispatch a worker to add
Same shape in both: dispatch, tokenless auto-arm, one rewake on the worker's ready signal, independent test run, local landing, cleanup. B. Claude primary against a live branch lease (the change)The primary dispatched Before (
After (
C. Pi primary on a guarded Herdr lab, attendedSame divide request. D. Pi away posture with only queued workBoth trees: queue a modulo task, then E. Pi away posture with a live workerBoth trees: dispatch the queued modulo task attended, then 2. After-only live demonstrations
3. Standard live guards, before vs afterRun from each tree in a detached tmux server with a scrubbed environment.
The Pi branch timeout is a flake on a path this change does not alter (1 of 12 runs across both trees). Deterministic suites on the after tree: 4. Findings and follow-ups
Raw evidence (panes, state listings, backlogs, project logs, branch outcome stores, away records, guard outputs, demo outputs) is kept with the private task record in the operating home ( |
* feat(bin): guard the partition, harness pin, and bounded exec for a non-Pi supervision host Lease liveness is now the pure record test in every calling context, so an unmarked main honors a live branch lease held by a separate process, and a lease file engages the guard's claim serialization for any caller; a home with no lease files still takes no lock. bin/fm-harness.sh honors FM_SUPERVISION_PRIMARY_HARNESS while FM_SUPERVISION_ACTOR=branch, so a supervision branch running under another harness resolves own, crew, and secondmate to the primary's harness. fm_tasks_axi's watchdog moves into bin/fm-timeout-lib.sh as fm_exec_timed with a separate grace: the perl watchdog is preferred, runs the command in its own process group against wall-clock deadlines, forwards TERM/INT/HUP, and reaps the group, so a descendant holding captured output can no longer keep the caller waiting past the bound on a host without timeout. The Claude Stop auto-arm header records that Claude drops the exit 2 of a hook it terminated at the configured timeout, re-measured on Claude Code 2.1.281. * fix(bin): state that fm_exec_timed cannot reach a descendant in its own process group Live runs of real Claude and Pi engine turns under the bound showed both CLIs start every tool command in a process group of its own, so those processes end through the engine's own TERM handling rather than the group signal or reap. Also clears the new timeout test's ShellCheck findings. * no-mistakes(document): Clarify cross-harness lease documentation
* feat(bin): guard the partition, harness pin, and bounded exec for a non-Pi supervision host Lease liveness is now the pure record test in every calling context, so an unmarked main honors a live branch lease held by a separate process, and a lease file engages the guard's claim serialization for any caller; a home with no lease files still takes no lock. bin/fm-harness.sh honors FM_SUPERVISION_PRIMARY_HARNESS while FM_SUPERVISION_ACTOR=branch, so a supervision branch running under another harness resolves own, crew, and secondmate to the primary's harness. fm_tasks_axi's watchdog moves into bin/fm-timeout-lib.sh as fm_exec_timed with a separate grace: the perl watchdog is preferred, runs the command in its own process group against wall-clock deadlines, forwards TERM/INT/HUP, and reaps the group, so a descendant holding captured output can no longer keep the caller waiting past the bound on a host without timeout. The Claude Stop auto-arm header records that Claude drops the exit 2 of a hook it terminated at the configured timeout, re-measured on Claude Code 2.1.281. * fix(bin): state that fm_exec_timed cannot reach a descendant in its own process group Live runs of real Claude and Pi engine turns under the bound showed both CLIs start every tool command in a process group of its own, so those processes end through the engine's own TERM handling rather than the group signal or reap. Also clears the new timeout test's ShellCheck findings. * no-mistakes(document): Clarify cross-harness lease documentation
* feat(bin): guard the partition, harness pin, and bounded exec for a non-Pi supervision host Lease liveness is now the pure record test in every calling context, so an unmarked main honors a live branch lease held by a separate process, and a lease file engages the guard's claim serialization for any caller; a home with no lease files still takes no lock. bin/fm-harness.sh honors FM_SUPERVISION_PRIMARY_HARNESS while FM_SUPERVISION_ACTOR=branch, so a supervision branch running under another harness resolves own, crew, and secondmate to the primary's harness. fm_tasks_axi's watchdog moves into bin/fm-timeout-lib.sh as fm_exec_timed with a separate grace: the perl watchdog is preferred, runs the command in its own process group against wall-clock deadlines, forwards TERM/INT/HUP, and reaps the group, so a descendant holding captured output can no longer keep the caller waiting past the bound on a host without timeout. The Claude Stop auto-arm header records that Claude drops the exit 2 of a hook it terminated at the configured timeout, re-measured on Claude Code 2.1.281. * fix(bin): state that fm_exec_timed cannot reach a descendant in its own process group Live runs of real Claude and Pi engine turns under the bound showed both CLIs start every tool command in a process group of its own, so those processes end through the engine's own TERM handling rather than the group signal or reap. Also clears the new timeout test's ShellCheck findings. * no-mistakes(document): Clarify cross-harness lease documentation
* feat(bin): guard the partition, harness pin, and bounded exec for a non-Pi supervision host Lease liveness is now the pure record test in every calling context, so an unmarked main honors a live branch lease held by a separate process, and a lease file engages the guard's claim serialization for any caller; a home with no lease files still takes no lock. bin/fm-harness.sh honors FM_SUPERVISION_PRIMARY_HARNESS while FM_SUPERVISION_ACTOR=branch, so a supervision branch running under another harness resolves own, crew, and secondmate to the primary's harness. fm_tasks_axi's watchdog moves into bin/fm-timeout-lib.sh as fm_exec_timed with a separate grace: the perl watchdog is preferred, runs the command in its own process group against wall-clock deadlines, forwards TERM/INT/HUP, and reaps the group, so a descendant holding captured output can no longer keep the caller waiting past the bound on a host without timeout. The Claude Stop auto-arm header records that Claude drops the exit 2 of a hook it terminated at the configured timeout, re-measured on Claude Code 2.1.281. * fix(bin): state that fm_exec_timed cannot reach a descendant in its own process group Live runs of real Claude and Pi engine turns under the bound showed both CLIs start every tool command in a process group of its own, so those processes end through the engine's own TERM handling rather than the group signal or reap. Also clears the new timeout test's ShellCheck findings. * no-mistakes(document): Clarify cross-harness lease documentation
* feat(bin): guard the partition, harness pin, and bounded exec for a non-Pi supervision host Lease liveness is now the pure record test in every calling context, so an unmarked main honors a live branch lease held by a separate process, and a lease file engages the guard's claim serialization for any caller; a home with no lease files still takes no lock. bin/fm-harness.sh honors FM_SUPERVISION_PRIMARY_HARNESS while FM_SUPERVISION_ACTOR=branch, so a supervision branch running under another harness resolves own, crew, and secondmate to the primary's harness. fm_tasks_axi's watchdog moves into bin/fm-timeout-lib.sh as fm_exec_timed with a separate grace: the perl watchdog is preferred, runs the command in its own process group against wall-clock deadlines, forwards TERM/INT/HUP, and reaps the group, so a descendant holding captured output can no longer keep the caller waiting past the bound on a host without timeout. The Claude Stop auto-arm header records that Claude drops the exit 2 of a hook it terminated at the configured timeout, re-measured on Claude Code 2.1.281. * fix(bin): state that fm_exec_timed cannot reach a descendant in its own process group Live runs of real Claude and Pi engine turns under the bound showed both CLIs start every tool command in a process group of its own, so those processes end through the engine's own TERM handling rather than the group signal or reap. Also clears the new timeout test's ShellCheck findings. * no-mistakes(document): Clarify cross-harness lease documentation
* feat(bin): guard the partition, harness pin, and bounded exec for a non-Pi supervision host Lease liveness is now the pure record test in every calling context, so an unmarked main honors a live branch lease held by a separate process, and a lease file engages the guard's claim serialization for any caller; a home with no lease files still takes no lock. bin/fm-harness.sh honors FM_SUPERVISION_PRIMARY_HARNESS while FM_SUPERVISION_ACTOR=branch, so a supervision branch running under another harness resolves own, crew, and secondmate to the primary's harness. fm_tasks_axi's watchdog moves into bin/fm-timeout-lib.sh as fm_exec_timed with a separate grace: the perl watchdog is preferred, runs the command in its own process group against wall-clock deadlines, forwards TERM/INT/HUP, and reaps the group, so a descendant holding captured output can no longer keep the caller waiting past the bound on a host without timeout. The Claude Stop auto-arm header records that Claude drops the exit 2 of a hook it terminated at the configured timeout, re-measured on Claude Code 2.1.281. * fix(bin): state that fm_exec_timed cannot reach a descendant in its own process group Live runs of real Claude and Pi engine turns under the bound showed both CLIs start every tool command in a process group of its own, so those processes end through the engine's own TERM handling rather than the group signal or reap. Also clears the new timeout test's ShellCheck findings. * no-mistakes(document): Clarify cross-harness lease documentation
* feat(bin): guard the partition, harness pin, and bounded exec for a non-Pi supervision host Lease liveness is now the pure record test in every calling context, so an unmarked main honors a live branch lease held by a separate process, and a lease file engages the guard's claim serialization for any caller; a home with no lease files still takes no lock. bin/fm-harness.sh honors FM_SUPERVISION_PRIMARY_HARNESS while FM_SUPERVISION_ACTOR=branch, so a supervision branch running under another harness resolves own, crew, and secondmate to the primary's harness. fm_tasks_axi's watchdog moves into bin/fm-timeout-lib.sh as fm_exec_timed with a separate grace: the perl watchdog is preferred, runs the command in its own process group against wall-clock deadlines, forwards TERM/INT/HUP, and reaps the group, so a descendant holding captured output can no longer keep the caller waiting past the bound on a host without timeout. The Claude Stop auto-arm header records that Claude drops the exit 2 of a hook it terminated at the configured timeout, re-measured on Claude Code 2.1.281. * fix(bin): state that fm_exec_timed cannot reach a descendant in its own process group Live runs of real Claude and Pi engine turns under the bound showed both CLIs start every tool command in a process group of its own, so those processes end through the engine's own TERM handling rather than the group signal or reap. Also clears the new timeout test's ShellCheck findings. * no-mistakes(document): Clarify cross-harness lease documentation
Intent
start on opus now - i will reset my quota if it gets close to running out. this is a major architectural revamp so i want it to do very careful live validation including regression in isolated live environments with some real complex sessions before calling it done. it's ok to use my real llm tokens here
This starts step 2 of the AFK revamp: rung 3 of the ladder in data/fm-afk-slices23-plan-s1/report.md, the shared non-Pi supervision host, which lands as two PRs - this guards-first PR, then the host core. The design was decided on the 2026-09-20 review board (report section 11), including "Slice 3: go, starting with the spike" and taking slice 3 "All the way (3a to 3e): away, attended, /quiet on the host, daemon deleted". The spike (data/fm-supervision-host-spike-s1/report.md) returned GO for the Claude engine. The captain ruled Pi keeps its in-process supervision and no Pi engine is built now (plan section 15). The live validation of the words model it builds on passed, with the abort step cleared by a real-worker replay (data/fm-afk-words-live-validation-s1/report.md, data/fm-afk-abort-run-replay-s1/report.md).
Substance of the referenced plan and reports. Rung 3 is a supervision host for non-Pi primary harnesses: a headless engine session (starting with the Claude engine) running beside the primary under the same contract as Pi's in-process supervision branch - the same branch prompt, the same records (away-posture record, outcome store, per-task leases, wake queue), and FM_SUPERVISION_ACTOR=branch in its environment so every guarded script applies the same main/branch partition - replacing the away daemon over rungs 3a to 3e; the review board also chose "The primary harness's own headless mode where verified, configurable per home, starting with Claude and Pi engines" as the engine. The spike resized 3a so it must land as two PRs: this small guards-first PR, then the host core (host loop, engine lib, report and dispatch CLIs, Claude arm swap behind a default-off config/supervision-host flag). The guards-first PR, as the spike report defines it, is: (1) generalize fm_lease_live / fm_lease_guard activation to the pure record test, because off Pi the partition is one-sided today - a branch lease reads live and refuses main only when main's process also carries an actor variable, while an unmarked main reads it as stale and overwrites it; (2) honor a primary-harness pin in bin/fm-harness.sh while FM_SUPERVISION_ACTOR=branch, because an engine detects its own harness (a Pi engine detects as pi) and would otherwise dispatch crewmates on its own harness instead of the primary's; (3) a shared bounded-exec helper (a wall-clock watchdog with TERM, a grace, then KILL, because both engines keep running after TERM mid-tool and macOS has no timeout binary), extracted from fm_tasks_axi; (4) one sentence in bin/fm-claude-stop-autoarm.sh's header that a timeout-terminated hook's exit 2 is not delivered as a rewake (measured: Claude sends SIGTERM to the hook tree at exactly the configured timeout, and an exit 2 from a hook it terminated does not wake main, while an exit 2 before the timeout does). No behavior change for any home without lease files; the host itself is the next PR.
What Changed
Risk Assessment
🚨 High: The guards-first partition has a reachable first-claim race during the intended non-Pi supervision flow, and its remedy must be reconciled with the no-behavior-change constraint before merge.
Testing
Live CLI checks confirmed cross-process lease refusal and staleness, branch harness pinning, bounded termination, and the no-lease fast path. A named Herdr lab completed its multi-home spawn, restart, send, and cleanup smoke checks. Targeted tests passed; the broader backlog test command reached its 240-second limit. The Claude timeout statement is documentation, not a runtime change.
bash tests/fm-branch-supervision.test.shtests/fm-harness-precedence.test.shtests/fm-timeout-lib.test.shbash tests/fm-branch-supervision.test.shbash tests/fm-backend-herdr-smoke.test.shEvidence: Live guards-first CLI transcript
Source: Live guards-first CLI transcript
Evidence: Isolated Herdr multi-home smoke
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-lease-lib.sh:197- An unmarked non-Pi main skips the command lock when a task has no lease file. It can begin an fm-send steer; the branch can then claim that task through bin/fm-lease.sh:115 and mutate it while the steer is still running. The same window affects the guarded lifecycle operation at bin/fm-control.sh:304 and teardown at bin/fm-teardown.sh:354. The comment acknowledges this window, but it leaves the intended cross-process task partition unenforced at the first claim. Closing it requires deciding how to serialize an unmarked main before any lease exists without violating the stated requirement of no behavior change for homes without lease files; that remedy needs authorization.✅ **Test** - passed
✅ No issues found.
bash tests/fm-branch-supervision.test.shtests/fm-harness-precedence.test.shtests/fm-timeout-lib.test.shbash tests/fm-branch-supervision.test.shbash tests/fm-backend-herdr-smoke.test.shtests/fm-timeout-lib.test.shtests/fm-harness-precedence.test.shbash tests/fm-branch-supervision.test.shbash tests/fm-backlog-atomicity.test.sh(stopped at the 240-second command limit after the relevant bounded-execution checks passed)Manualfm-lease.sh,fm-harness.sh, andfm_exec_timedchecks in a disposable homebash tests/fm-backend-herdr-smoke.test.shin a guarded, non-default Herdr lab✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.