Skip to content

feat: add opt-in Claude away supervision host - #5488

Merged
kunchenguid merged 8 commits into
mainfrom
fm/fm-afk-host-core-r1
Sep 24, 2026
Merged

kunchenguid merged 8 commits into
mainfrom
fm/fm-afk-host-core-r1

Conversation

@kunchenguid

@kunchenguid kunchenguid commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

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 continues step 2 of the AFK revamp: rung 3 of the ladder in the AFK slices 2-3 implementation plan, the shared non-Pi supervision host, which lands as two PRs. The guards-first PR merged as #5471 (cross-process lease honoring, the supervision-branch primary-harness pin, the shared bounded-exec helper fm_exec_timed, and the Claude timeout-terminated Stop hook note). This is the second PR, the host core. The design was decided on the 2026-09-20 review board, including "Slice 3: go, starting with the spike", taking slice 3 "All the way (3a to 3e): away, attended, /quiet on the host, daemon deleted", and choosing "The primary harness's own headless mode where verified, configurable per home, starting with Claude and Pi engines" as the engine. The spike returned GO for the Claude engine and sized 3a-core as: the host loop, the Claude engine lib, fm-branch-report.sh, the dispatch CLI entry, and the Claude arm swap behind a default-off config/supervision-host flag, plus docs. The captain ruled Pi keeps its in-process supervision and no Pi engine is built now. The host runs a headless engine session beside a non-Pi 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.

Substance of the plan and spike referenced above:

  • The plan's slice 3 design: bin/fm-supervision-host.sh park is a loop that owns watcher cycles through bin/fm-watch-arm.sh and, on each actionable close, computes the rows the branch may claim with the same rules as .pi/extensions/lib/fm-branch-dispatch.ts through a thin CLI entry (one owner of eligibility), starts the successor watcher cycle so supervision stays live, runs one bounded headless engine turn with the generated branch prompt (bin/fm-branch-prompt.sh, made host-neutral) plus the away tail carrying the away record's verbatim read-back, and counts the wake handled only when that turn appended a durable report through bin/fm-branch-report.sh, a command twin of the Pi fm_branch_report tool with the same task scoping. Every path that cannot finish a wake falls back to exiting with the close's reason line so the harness's existing wake path delivers it to main; the host adds no delivery machinery, no pane injection, and no authority the Pi branch lacks. In this PR the host runs only on a Claude primary and only takes away-posture wakes, launched by the Stop asyncRewake hook (bin/fm-claude-stop-autoarm.sh) in place of bin/fm-watch-arm.sh for a home opted in with config/supervision-host; bin/fm-afk-launch.sh start-native refuses the away daemon on such a home; docs/supervision-host.md owns the design and docs/configuration.md the opt-in; homes without the file behave exactly as before. Later PRs: 3b (the other harnesses), 3c (attended posture and /quiet on the host), 3d (default on), 3e (the away daemon deleted).
  • The spike's measured Claude engine facts: claude -p with --safe-mode (not --bare, which never reads the captain's OAuth) loads none of the home's hooks, CLAUDE.md, or session-start nudge and never touches state/.lock; --permission-mode dontAsk with --allowedTools Bash Read never prompts; FM_SUPERVISION_ACTOR reaches the engine's shell; each turn needs a TERM-grace-KILL wall-clock bound; a resumed conversation with a byte-stable prompt reuses about 98% of its reads from cache, and per-wake cost grows with conversation length, so the conversation rotates at every main session start and after a number of wakes; the default engine model is sonnet; a home outside the code root needs --add-dir; stdin must be /dev/null; and a Claude Stop hook terminated at its configured timeout never delivers its exit 2, so the host must end its own park below the hook timeout with a cycle-boundary wake.

What Changed

  • Add a bounded headless Claude supervision host that handles away-posture watcher wakes and hands unfinished wakes back to main.
  • Share branch wake dispatch and prompts with Pi, and add a scoped command-line outcome report for host turns.
  • Route opted-in Claude homes through the host instead of the away daemon, with configuration, documentation, and host tests; homes without the opt-in retain their existing behavior.

Risk Assessment

⚠️ Medium: This is a substantial opt-in supervision change with complex process and wake handoffs, though this pass found no additional substantiated issue.

Testing

Targeted host, hook, launcher, watcher, and branch checks passed after correcting the test harness identity; real Claude handled and resumed both single-task and two-task away wakes. The launcher check used an isolated named Herdr lab. Real-hook failure and 27,000-second boundary behavior were not driven live; no UI surface was changed, so no visual artifact was captured.

  • Live validation: ✅ go - 3 of 6 scenarios driven live against the product
Scenario Result Live Evidence
An opted-in Claude home receives an away status wake; the real engine reports and acknowledges it while main stays parked ✅ pass live host-live.log records a handled turn and a persisted task outcome; the live check verified the wake was acknowledged and main was not woken.
Two task statuses arrive together; the real engine reports both and handles a later wake in the same conversation ✅ pass live host-multi-live.log records two reports on the first turn and a handled second turn; the live check verified both outcomes, an empty granted wake, and conversation reuse.
A Claude home opts into the host; away daemon entry is refused while quiet entry remains available, and an ordinary home retains its launcher behavior ✅ pass live afk-launch.log records launcher entry checks and isolated named-Herdr-lab terminal lifecycle checks.
A malformed engine result or an unacknowledged grant hands the durable wake back to main ⏸️ untested no Real Claude output could not be made malformed or deliberately leave an acknowledged grant pending without substituting its engine; a controllable real-engine test endpoint would enable a live failure…
A real Stop hook preserves every host outcome line while capping ordinary wake lines ⏸️ untested no This run did not have an isolated interactive Claude primary configured to fire its actual Stop hook with a multi-outcome handback; provide that isolated hook session to drive the banner live.
The host ends its park before Claude's Stop-hook timeout and does not start a turn that crosses the boundary ⏸️ untested no The production 27,000-second park was not held open in a real Claude Stop-hook session; an isolated long-running hook session would enable that live timing check.
Evidence: Real Claude engine handles and resumes an away wake

Source: Real Claude engine handles and resumes an away wake

# first turn: handled	turn=host-9948-1790220599.1	rc=0	reports=1
# outcome: {"seq":1,"epoch":1790220618,"task":"demo","wake":"signal: ~/.no-mistakes/worktrees/016d88035d58/01M38MHPWBY9SKDE0A67RMXE3P/.test-tmp/fm-supervision-host-live.C81CnZ/fm/state/demo.status","verdict":"routine","summary":"Demo task's cleanup finished on its own; worktree already gone and nothing further needed. Per away instructions, no merge or dispatch performed.","silent":false,"statusEndpoint":72,"statusIdent":"strong:16777232:299629929:1790220600.225552216"}
# second turn: handled	turn=host-9948-1790220599.2	rc=0	reports=1
ok - supervision host live (2.1.281 (Claude Code)): a real engine handles and resumes away wakes under the branch contract without waking main
Evidence: Real Claude engine reports two task outcomes in one wake

Source: Real Claude engine reports two task outcomes in one wake

# first turn: handled	turn=host-86483-1790220789.1	rc=0	reports=2
# outcome: {"seq":1,"epoch":1790220808,"task":"audit","wake":"signal: ~/.no-mistakes/worktrees/016d88035d58/01M38MHPWBY9SKDE0A67RMXE3P/.test-tmp/fm-supervision-host-live.JjitLb/fm/state/audit.status ~/.no-mistakes/worktrees/016d88035d58/01M38MHPWBY9SKDE0A67RMXE3P/.test-tmp/fm-supervision-host-live.JjitLb/fm/state/demo.status","verdict":"routine","summary":"per your away instructions: reviewed the away-posture audit-status signal; audit completed with no merge or dispatch authorized, worktree already torn down, no action needed.","silent":false,"statusEndpoint":71,"statusIdent":"strong:16777232:299648101:1790220790.745237802"}
# second turn: handled	turn=host-86483-1790220789.2	rc=0	reports=1
ok - supervision host live (2.1.281 (Claude Code)): a real engine handles and resumes away wakes under the branch contract without waking main
Evidence: Launcher behavior in an isolated Herdr lab

Source: Launcher behavior in an isolated Herdr lab

ok - clear-stale: removes escalations buffer, sidecar, and wedge marker
ok - clear-stale: leaves the durable wake-queue intact (no pending work dropped)
ok - enter: one call writes the record with the words, expected return, and spend cap, reads it back without asking for a go, and launches no daemon
ok - enter: the retired --grant flag is refused by name and leaves the standing record alone
ok - enter: refuses while the prior return catch-up is pending
ok - propose: the retired wait-for-go step is refused by name, writes nothing, and releases the launcher lock
ok - confirm: the retired wait-for-go step is refused by name, writes nothing, and releases the launcher lock
ok - pi: start refuses to launch the daemon and writes no state
ok - pi: start-native refuses to prepare a daemon
ok - pi-signed: start refuses to launch the daemon and writes no state
ok - pi-signed: start-native refuses to prepare a daemon
ok - pi enter stop: reports that no daemon terminal was running
ok - daemon entry: no daemon lifecycle starts without the away-posture record
ok - daemon entry: enter then start-native run back to back with no confirmation between them
ok - failed start: preserves the posture record enter wrote
ok - stop: clears the away flag and archives the posture record under its entry time
ok - launcher paths: relative home and state ignore CDPATH before daemon command construction
ok - launcher paths: absolute symlink spellings are preserved
ok - launcher paths: unresolved relative FM_HOME fails loudly
ok - launcher paths: unresolved relative FM_STATE_OVERRIDE fails loudly
ok - refresh: daemon already alive - stale artifacts preserved (current session's buffer kept)
ok - mode: a fresh entry with FM_AFK_MODE=quiet writes quiet
ok - mode: a fresh entry with FM_AFK_MODE unset defaults to away
ok - mode: a bare refresh (FM_AFK_MODE unset) of an already-running quiet daemon preserves quiet, never resets to away
ok - mode: an empty (legacy pre-mode) flag reads as away
ok - mode: a bare-epoch-timestamp (legacy pre-mode) flag reads as away
ok - mode: unrecognized content falls back to away
ok - mode: a missing flag reads as away
ok - stop-ordering: daemon SIGTERM'd while .afk still present (flush is not a no-op)
ok - stop-ordering: .afk cleared last
ok - stop-ordering: daemon-terminal record removed
ok - stop identity: stale lock cannot signal an unrelated live process
ok - failed start: away flag and delivery artifacts roll back
ok - concurrent start: one serialized daemon terminal remains tracked
ok - launcher lock: incomplete publication receives initialization grace
ok - launcher signal: TERM exits and releases the lifecycle lock
fm-afk-launch: daemon launched in non-visible herdr workspace ws-partial (pane lab:pane-exact), supervising lab:captain
ok - herdr create: malformed response recovers durable exact ownership
fm-afk-launch: herdr create failed after returning exact ids; closing lab:pane-exact
fm-afk-launch: recorded terminal teardown is unconfirmed; preserving exact id
ok - herdr create error: unconfirmed exact id is persisted for reconciliation
fm-afk-launch: failed to run daemon in herdr pane lab:pane-exact; closing it
fm-afk-launch: recorded terminal teardown is unconfirmed; preserving exact id
ok - herdr run failure: unconfirmed exact id remains reconcilable
fm-afk-launch: failed to persist daemon terminal record; closing tmux:exact-session
ok - record failure: newly created terminal is closed by exact id
fm-afk-launch: daemon did not become ready; closing tmux:exact-session
ok - readiness failure: exact terminal and durable record roll back
fm-afk-launch: daemon did not become ready; closing tmux:exact-session
fm-afk-launch: recorded terminal teardown is unconfirmed; preserving exact id
ok - readiness failure: unconfirmed terminal retains its reconciliation id
ok - tmux absence: clean missing differs from transport probe failure
ok - native lifecycle: launcher owns state with no terminal
ok - native lifecycle: uniform stop clears state without closing a terminal
ok - supervision host: away start-native on a claude home refuses the daemon and keeps the record
ok - supervision host: quiet start-native and a plain refresh of the quiet daemon still prepare the daemon
ok - native entry: launcher-prepared lifecycle state is not rewritten
fm-afk-launch: reconciling leaked daemon terminal tmux:exact-session
fm-afk-launch: recorded terminal teardown is unconfirmed; preserving exact id
ok - teardown failure: exact terminal record is preserved
ok - record publication: failed atomic rename preserves the complete prior record
fm-afk-launch: daemon terminal record is malformed; refusing to act on it
ok - record read: malformed record fails closed without acting on a partial id
fm-afk-launch: daemon terminal record is malformed; refusing to act on it
fm-afk-launch: malformed daemon terminal record; refusing to stop away mode
ok - stop: malformed terminal record preserves away state and fails closed
fm-afk-launch: failed to create detached tmux daemon session 'fm-afk-daemon-4114616292-3238-27125-1790220589'
ok - tmux launch: planned exact target is recorded before creation and removed on failure
fm-afk-launch: failed to create detached tmux daemon session 'fm-afk-daemon-3294367961-3297-20565-1790220589'
ok - tmux launch: unique names eliminate collision teardown
ok - stop validation: malformed record causes no daemon or state side effects
ok - launcher lock: incomplete metadata fails acquisition and releases lock
fm-afk-launch: failed to clear away-mode flag
fm-afk-launch: away mode stopped; terminal teardown or the record archive remains recorded for retry
ok - stop state: away-flag removal failure is surfaced
fm-afk-launch: away-mode daemon did not exit after SIGTERM; preserving lifecycle state
ok - stop liveness: captured live daemon preserves lifecycle state after lock release
fm-afk-launch: an away-posture record is required; run enter before starting the daemon
fm-afk-launch: an away-posture record is required; run enter before starting the daemon
ok - refresh record: malformed terminal identity fails closed
fm-afk-launch: an away-posture record is required; run enter before starting the daemon
ok - clear failure: native entry aborts and restores prior state
fm-afk-launch: reconciling leaked daemon terminal tmux:exact-session
fm-afk-launch: terminal close command failed, but exact absence was confirmed
ok - confirmed absence: cleanup succeeds and removes the stale record
fm-afk-launch: rollback restoration incomplete; backup retained at ~/.no-mistakes/worktrees/016d88035d58/01M38MHPWBY9SKDE0A67RMXE3P/.test-tmp/fm-afk-restore-fail.JwGCIv/state/.afk-launch-backup.md0U2n
ok - rollback restore: incomplete restoration retains its recovery backup
fm-afk-launch: an away-posture record is required; run enter before starting the daemon
ok - flag failure: lifecycle aborts without active state
ok - herdr e2e: captain tab pane count unchanged after start (no split)
ok - herdr e2e: daemon launched in a separate non-visible workspace
ok - herdr e2e: daemon pane is NOT in the captain's tab
ok - herdr e2e: daemon terminal scoped to the lab session
ok - herdr e2e: captain tab pane count restored after stop
ok - herdr e2e: daemon workspace removed by exact id on stop
ok - herdr e2e: record + .afk cleared on stop
ok - tmux e2e: captain window pane count unchanged after start (no split-window)
ok - tmux e2e: daemon launched in a separate detached session
ok - tmux e2e: captain window pane count unchanged after stop
ok - tmux e2e: daemon session killed by exact id on stop
ok - tmux e2e: record + .afk cleared on stop

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed (3) ✅
  • 🚨 bin/fm-supervision-host.sh:587 - The host calls activate before checking session-lock and auto-arm generation ownership at line 591. A superseded host invocation can therefore stop the current host and watcher and release its branch leases (lines 216–237) before standing down. Check ownership before predecessor cleanup or any shared-state mutation.
  • 🚨 bin/fm-supervision-host.sh:329 - The park deadline is checked only while an arm remains alive. If actionable closes are already ready on successive iterations, await_close skips that check and the host can continue past the Claude Stop-hook timeout, whose exit-2 wake is then lost. The loop at line 598 and successor/turn path at lines 644–653 must honor the deadline even when an arm has closed.
  • 🚨 bin/fm-supervision-host.sh:563 - A clean engine exit and one report receipt count the wake handled without checking whether its claimed rows were acknowledged. An engine can report successfully but fail its printed --ack-through command; the host then parks silently at lines 664–669 while those rows remain queued for repeat handling. Verify consumption of the granted rows before suppressing the handback.
  • ⚠️ bin/fm-supervision-host.sh:653 - If the captain returns during a turn, the engine writes an outcome, then exits unsuccessfully, this failure branch hands back only the close and error. The late-outcome relay at lines 658–661 runs only after a successful turn, so that outcome misses both the already-rendered return brief and the handback. Include recorded turn outcomes on failure handbacks too.
  • ⚠️ bin/fm-supervision-engine-lib.sh:246 - A tool can spawn a long-lived process in its own process group and the engine can exit between the initial descendant snapshot and the next one-second poll. That process never enters the ledger and survives the turn despite the claimed reap bound. The remedy needs a reliable way to retain descendant identity across that interval; authorize that extension rather than merely shortening the polling interval.

🔧 Fix applied.
5 issues (3 errors, 2 warnings) still open:

  • 🚨 bin/fm-supervision-host.sh:587 - The host calls activate before checking session-lock and auto-arm generation ownership at line 591. A superseded host invocation can therefore stop the current host and watcher and release its branch leases (lines 216–237) before standing down. Check ownership before predecessor cleanup or any shared-state mutation.
  • 🚨 bin/fm-supervision-host.sh:329 - The park deadline is checked only while an arm remains alive. If actionable closes are already ready on successive iterations, await_close skips that check and the host can continue past the Claude Stop-hook timeout, whose exit-2 wake is then lost. The loop at line 598 and successor/turn path at lines 644–653 must honor the deadline even when an arm has closed.
  • 🚨 bin/fm-supervision-host.sh:563 - A clean engine exit and one report receipt count the wake handled without checking whether its claimed rows were acknowledged. An engine can report successfully but fail its printed --ack-through command; the host then parks silently at lines 664–669 while those rows remain queued for repeat handling. Verify consumption of the granted rows before suppressing the handback.
  • ⚠️ bin/fm-supervision-host.sh:653 - If the captain returns during a turn, the engine writes an outcome, then exits unsuccessfully, this failure branch hands back only the close and error. The late-outcome relay at lines 658–661 runs only after a successful turn, so that outcome misses both the already-rendered return brief and the handback. Include recorded turn outcomes on failure handbacks too.
  • ⚠️ bin/fm-supervision-host.sh:680 - Round 1's boundary fix left a sibling gap: the check runs before start_successor, which may take up to READY_TIMEOUT (line 681), and prompt preparation (lines 503–564). A close arriving with exactly TURN_TIMEOUT + ENGINE_GRACE remaining can therefore start its engine turn at line 567 after that time has elapsed and run past the park boundary without a boundary handback. Recheck immediately before starting the turn and hand the already-read close to main if insufficient time remains.

🔧 Fix applied.
8 issues (4 errors, 4 warnings) still open:

  • 🚨 bin/fm-supervision-host.sh:587 - The host calls activate before checking session-lock and auto-arm generation ownership at line 591. A superseded host invocation can therefore stop the current host and watcher and release its branch leases (lines 216–237) before standing down. Check ownership before predecessor cleanup or any shared-state mutation.
  • 🚨 bin/fm-supervision-host.sh:329 - The park deadline is checked only while an arm remains alive. If actionable closes are already ready on successive iterations, await_close skips that check and the host can continue past the Claude Stop-hook timeout, whose exit-2 wake is then lost. The loop at line 598 and successor/turn path at lines 644–653 must honor the deadline even when an arm has closed.
  • 🚨 bin/fm-supervision-host.sh:563 - A clean engine exit and one report receipt count the wake handled without checking whether its claimed rows were acknowledged. An engine can report successfully but fail its printed --ack-through command; the host then parks silently at lines 664–669 while those rows remain queued for repeat handling. Verify consumption of the granted rows before suppressing the handback.
  • ⚠️ bin/fm-supervision-host.sh:653 - If the captain returns during a turn, the engine writes an outcome, then exits unsuccessfully, this failure branch hands back only the close and error. The late-outcome relay at lines 658–661 runs only after a successful turn, so that outcome misses both the already-rendered return brief and the handback. Include recorded turn outcomes on failure handbacks too.
  • ⚠️ bin/fm-supervision-host.sh:680 - Round 1's boundary fix left a sibling gap: the check runs before start_successor, which may take up to READY_TIMEOUT (line 681), and prompt preparation (lines 503–564). A close arriving with exactly TURN_TIMEOUT + ENGINE_GRACE remaining can therefore start its engine turn at line 567 after that time has elapsed and run past the park boundary without a boundary handback. Recheck immediately before starting the turn and hand the already-read close to main if insufficient time remains.
  • 🚨 bin/fm-supervision-host.sh:116 - The required design says the host must end its park below Claude's 28,800-second Stop-hook timeout. This hunk accepts any positive FM_SUPERVISION_HOST_PARK_SECONDS; setting it to 28,800 or more makes boundary_reached (line 327) wait until the hook has terminated and dropped its exit-2 wake. The remedy is a policy decision about whether to reject or cap an explicitly supplied value below that timeout.
  • ⚠️ bin/fm-claude-stop-autoarm.sh:398 - A turn can record several outcomes, which the host prints at bin/fm-supervision-host.sh:403 and :705–711, but this head -8 drops later outcome lines from the Stop-hook feedback. For example, a close followed by a handback and eight recorded outcomes cannot deliver them all for main to relay. Preserve the host's outcome lines when translating its close.
  • ⚠️ bin/fm-supervision-engine-lib.sh:290 - A clean-exiting engine that reports and acknowledges can emit {}; this parser calls it error=0 with zero cost, and the host counts the wake handled at bin/fm-supervision-host.sh:613. Reject an incomplete result rather than defaulting missing fields to success. Validate the consumed type, subtype, is_error, total_cost_usd, num_turns, and usage token fields (input, cache-read, cache-creation, output).

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 3 of 6 scenarios driven live against the product
Scenario Result Live Evidence
An opted-in Claude home receives an away status wake; the real engine reports and acknowledges it while main stays parked ✅ pass live host-live.log records a handled turn and a persisted task outcome; the live check verified the wake was acknowledged and main was not woken.
Two task statuses arrive together; the real engine reports both and handles a later wake in the same conversation ✅ pass live host-multi-live.log records two reports on the first turn and a handled second turn; the live check verified both outcomes, an empty granted wake, and conversation reuse.
A Claude home opts into the host; away daemon entry is refused while quiet entry remains available, and an ordinary home retains its launcher behavior ✅ pass live afk-launch.log records launcher entry checks and isolated named-Herdr-lab terminal lifecycle checks.
A malformed engine result or an unacknowledged grant hands the durable wake back to main ⏸️ untested no Real Claude output could not be made malformed or deliberately leave an acknowledged grant pending without substituting its engine; a controllable real-engine test endpoint would enable a live failure…
A real Stop hook preserves every host outcome line while capping ordinary wake lines ⏸️ untested no This run did not have an isolated interactive Claude primary configured to fire its actual Stop hook with a multi-outcome handback; provide that isolated hook session to drive the banner live.
The host ends its park before Claude's Stop-hook timeout and does not start a turn that crosses the boundary ⏸️ untested no The production 27,000-second park was not held open in a real Claude Stop-hook session; an isolated long-running hook session would enable that live timing check.
  • tests/fm-supervision-host.test.sh
  • tests/fm-claude-stop-autoarm.test.sh
  • tests/fm-afk-launch.test.sh under a Claude-named harness; an initial run inherited Pi ancestry and was rerun with the correct harness
  • bash tests/fm-branch-supervision.test.sh
  • bash tests/fm-watch-arm.test.sh
  • bash tests/fm-supervision-instructions.test.sh
  • FM_SUPERVISION_HOST_LIVE_E2E=1 tests/fm-supervision-host-live-e2e.test.sh with real Claude Code
  • A temporary two-task variant of the live Claude test, removed after execution
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Add the supervision host (bin/fm-supervision-host.sh): beside a Claude
primary it owns the watcher cycle for the Stop auto-arm and, while the
away-posture record exists, hands each wake to a bounded headless Claude
engine session that runs the supervision branch's contract - the same
generated prompt, row eligibility, wake grant, per-actor drain, outcome
store, leases, and away relocation the Pi branch uses. Attended wakes pass
straight to main. Every path that cannot finish a wake hands it to main
with a supervision-host line; the park ends itself before the Stop hook
timeout with a cycle-boundary wake.

- bin/fm-supervision-engine-lib.sh: opt-in parse, verified engines
  (claude, default sonnet), one bounded engine turn, and a reap of engine
  tool processes that sit in their own process groups.
- bin/fm-branch-report.sh: the command twin of fm_branch_report, scoped
  to the tasks the current host turn claimed.
- bin/fm-branch-dispatch.mjs: command entry to the Pi dispatch module, so
  eligibility and the wake prompt have one owner.
- bin/fm-claude-stop-autoarm.sh runs the host in the arm's place when
  config/supervision-host exists; nothing changes without the file.
- bin/fm-watch-arm.sh --stop: home-scoped stop without a re-arm.
- bin/fm-lease-lib.sh: an opted-in home takes the lease-command lock for
  unmarked main too, closing the first-claim race; the refusal tells the
  caller to leave the lease alone and retry.
- /afk launches no away daemon on an opted-in Claude home; /quiet still
  does. Session start renders the host's main-side protocol there.
…urn, and log per-turn engine cost

Live validation found two supervision host gaps. A captain who returns while
an engine turn is running gets a return brief rendered before that turn's
outcomes exist, so the host now hands the close to main with those outcomes.
Claude reports a resumed conversation's running cost, so the engine lib now
derives each turn's cost from the total the host records, and the host log
records every close's destination.
The dated live results behind docs/supervision-host.md: the Claude engine's
live guard, the away-wake cases against real workers, the engine's cost
reporting, and the flag-off before-and-after regression.
@kunchenguid

Copy link
Copy Markdown
Owner Author

Dated live validation for this PR (2026-09-23, Claude Code 2.1.281, Pi 0.87.0, Herdr 0.9.0, macOS 26.6.2) is recorded in docs/verification/supervision.md, "Supervision host".

It covers:

  • Eleven host sessions on isolated lab homes with a real Claude primary supervising real Pi workers: attended pass-through, away wakes handled by the real engine (decisions answered, workers steered and resumed), host SIGKILL and TERM recovery, lease refusal of a main steer during an engine turn, and a captain return during an engine turn.
  • Flag-off regression before (ac2ed3b2) and after the change: a Claude primary landing a worker, and a Pi primary in an isolated Herdr lab, attended and away.
  • The standard live guards (Claude Stop auto-arm, Pi branch, Pi branch responsiveness, Pi Herdr return, and the new supervision host guard), passing identically before and after.

Two bugs that live validation found are fixed on this branch: outcomes from a turn during which the captain returned were lost, and the engine's cumulative conversation cost was logged as per-turn cost.
The review fixes after that (ownership before activation, the park boundary on every pass and before each turn, the unacknowledged-row handback, outcomes on failed turns, the park tunable range, the full handback banner, and strict engine-result parsing) are covered by hermetic tests, and the pipeline's live guard runs above show the real engine handling and resuming wakes on the fixed code.
Every lab resource was torn down, and nothing touched a live home, the Herdr default session, the shared no-mistakes daemon, or a real repository.

@kunchenguid
kunchenguid merged commit 9284978 into main Sep 24, 2026
20 checks passed
@kunchenguid
kunchenguid deleted the fm/fm-afk-host-core-r1 branch September 24, 2026 04:03
mituso89 pushed a commit to mituso89/firstmate that referenced this pull request Sep 26, 2026
* feat(bin): supervision host core behind config/supervision-host

Add the supervision host (bin/fm-supervision-host.sh): beside a Claude
primary it owns the watcher cycle for the Stop auto-arm and, while the
away-posture record exists, hands each wake to a bounded headless Claude
engine session that runs the supervision branch's contract - the same
generated prompt, row eligibility, wake grant, per-actor drain, outcome
store, leases, and away relocation the Pi branch uses. Attended wakes pass
straight to main. Every path that cannot finish a wake hands it to main
with a supervision-host line; the park ends itself before the Stop hook
timeout with a cycle-boundary wake.

- bin/fm-supervision-engine-lib.sh: opt-in parse, verified engines
  (claude, default sonnet), one bounded engine turn, and a reap of engine
  tool processes that sit in their own process groups.
- bin/fm-branch-report.sh: the command twin of fm_branch_report, scoped
  to the tasks the current host turn claimed.
- bin/fm-branch-dispatch.mjs: command entry to the Pi dispatch module, so
  eligibility and the wake prompt have one owner.
- bin/fm-claude-stop-autoarm.sh runs the host in the arm's place when
  config/supervision-host exists; nothing changes without the file.
- bin/fm-watch-arm.sh --stop: home-scoped stop without a re-arm.
- bin/fm-lease-lib.sh: an opted-in home takes the lease-command lock for
  unmarked main too, closing the first-claim race; the refusal tells the
  caller to leave the lease alone and retry.
- /afk launches no away daemon on an opted-in Claude home; /quiet still
  does. Session start renders the host's main-side protocol there.

* fix(bin): relay a host turn's outcomes when the captain returns mid-turn, and log per-turn engine cost

Live validation found two supervision host gaps. A captain who returns while
an engine turn is running gets a return brief rendered before that turn's
outcomes exist, so the host now hands the close to main with those outcomes.
Claude reports a resumed conversation's running cost, so the engine lib now
derives each turn's cost from the total the host records, and the host log
records every close's destination.

* docs(verification): record the supervision host's live evidence

The dated live results behind docs/supervision-host.md: the Claude engine's
live guard, the away-wake cases against real workers, the engine's cost
reporting, and the flag-off before-and-after regression.

* docs: describe the supervision host ledger as covering every close

* no-mistakes(review): Harden supervision host ownership, boundary, ack, and late outcomes

* no-mistakes(review): Recheck park boundary just before starting an engine turn

* no-mistakes(review): Cap park boundary, deliver all host lines, reject incomplete results

* no-mistakes(document): Correct supervision host documentation and stale pointers
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 26, 2026
* feat(bin): supervision host core behind config/supervision-host

Add the supervision host (bin/fm-supervision-host.sh): beside a Claude
primary it owns the watcher cycle for the Stop auto-arm and, while the
away-posture record exists, hands each wake to a bounded headless Claude
engine session that runs the supervision branch's contract - the same
generated prompt, row eligibility, wake grant, per-actor drain, outcome
store, leases, and away relocation the Pi branch uses. Attended wakes pass
straight to main. Every path that cannot finish a wake hands it to main
with a supervision-host line; the park ends itself before the Stop hook
timeout with a cycle-boundary wake.

- bin/fm-supervision-engine-lib.sh: opt-in parse, verified engines
  (claude, default sonnet), one bounded engine turn, and a reap of engine
  tool processes that sit in their own process groups.
- bin/fm-branch-report.sh: the command twin of fm_branch_report, scoped
  to the tasks the current host turn claimed.
- bin/fm-branch-dispatch.mjs: command entry to the Pi dispatch module, so
  eligibility and the wake prompt have one owner.
- bin/fm-claude-stop-autoarm.sh runs the host in the arm's place when
  config/supervision-host exists; nothing changes without the file.
- bin/fm-watch-arm.sh --stop: home-scoped stop without a re-arm.
- bin/fm-lease-lib.sh: an opted-in home takes the lease-command lock for
  unmarked main too, closing the first-claim race; the refusal tells the
  caller to leave the lease alone and retry.
- /afk launches no away daemon on an opted-in Claude home; /quiet still
  does. Session start renders the host's main-side protocol there.

* fix(bin): relay a host turn's outcomes when the captain returns mid-turn, and log per-turn engine cost

Live validation found two supervision host gaps. A captain who returns while
an engine turn is running gets a return brief rendered before that turn's
outcomes exist, so the host now hands the close to main with those outcomes.
Claude reports a resumed conversation's running cost, so the engine lib now
derives each turn's cost from the total the host records, and the host log
records every close's destination.

* docs(verification): record the supervision host's live evidence

The dated live results behind docs/supervision-host.md: the Claude engine's
live guard, the away-wake cases against real workers, the engine's cost
reporting, and the flag-off before-and-after regression.

* docs: describe the supervision host ledger as covering every close

* no-mistakes(review): Harden supervision host ownership, boundary, ack, and late outcomes

* no-mistakes(review): Recheck park boundary just before starting an engine turn

* no-mistakes(review): Cap park boundary, deliver all host lines, reject incomplete results

* no-mistakes(document): Correct supervision host documentation and stale pointers
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 26, 2026
* feat(bin): supervision host core behind config/supervision-host

Add the supervision host (bin/fm-supervision-host.sh): beside a Claude
primary it owns the watcher cycle for the Stop auto-arm and, while the
away-posture record exists, hands each wake to a bounded headless Claude
engine session that runs the supervision branch's contract - the same
generated prompt, row eligibility, wake grant, per-actor drain, outcome
store, leases, and away relocation the Pi branch uses. Attended wakes pass
straight to main. Every path that cannot finish a wake hands it to main
with a supervision-host line; the park ends itself before the Stop hook
timeout with a cycle-boundary wake.

- bin/fm-supervision-engine-lib.sh: opt-in parse, verified engines
  (claude, default sonnet), one bounded engine turn, and a reap of engine
  tool processes that sit in their own process groups.
- bin/fm-branch-report.sh: the command twin of fm_branch_report, scoped
  to the tasks the current host turn claimed.
- bin/fm-branch-dispatch.mjs: command entry to the Pi dispatch module, so
  eligibility and the wake prompt have one owner.
- bin/fm-claude-stop-autoarm.sh runs the host in the arm's place when
  config/supervision-host exists; nothing changes without the file.
- bin/fm-watch-arm.sh --stop: home-scoped stop without a re-arm.
- bin/fm-lease-lib.sh: an opted-in home takes the lease-command lock for
  unmarked main too, closing the first-claim race; the refusal tells the
  caller to leave the lease alone and retry.
- /afk launches no away daemon on an opted-in Claude home; /quiet still
  does. Session start renders the host's main-side protocol there.

* fix(bin): relay a host turn's outcomes when the captain returns mid-turn, and log per-turn engine cost

Live validation found two supervision host gaps. A captain who returns while
an engine turn is running gets a return brief rendered before that turn's
outcomes exist, so the host now hands the close to main with those outcomes.
Claude reports a resumed conversation's running cost, so the engine lib now
derives each turn's cost from the total the host records, and the host log
records every close's destination.

* docs(verification): record the supervision host's live evidence

The dated live results behind docs/supervision-host.md: the Claude engine's
live guard, the away-wake cases against real workers, the engine's cost
reporting, and the flag-off before-and-after regression.

* docs: describe the supervision host ledger as covering every close

* no-mistakes(review): Harden supervision host ownership, boundary, ack, and late outcomes

* no-mistakes(review): Recheck park boundary just before starting an engine turn

* no-mistakes(review): Cap park boundary, deliver all host lines, reject incomplete results

* no-mistakes(document): Correct supervision host documentation and stale pointers
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 27, 2026
* feat(bin): supervision host core behind config/supervision-host

Add the supervision host (bin/fm-supervision-host.sh): beside a Claude
primary it owns the watcher cycle for the Stop auto-arm and, while the
away-posture record exists, hands each wake to a bounded headless Claude
engine session that runs the supervision branch's contract - the same
generated prompt, row eligibility, wake grant, per-actor drain, outcome
store, leases, and away relocation the Pi branch uses. Attended wakes pass
straight to main. Every path that cannot finish a wake hands it to main
with a supervision-host line; the park ends itself before the Stop hook
timeout with a cycle-boundary wake.

- bin/fm-supervision-engine-lib.sh: opt-in parse, verified engines
  (claude, default sonnet), one bounded engine turn, and a reap of engine
  tool processes that sit in their own process groups.
- bin/fm-branch-report.sh: the command twin of fm_branch_report, scoped
  to the tasks the current host turn claimed.
- bin/fm-branch-dispatch.mjs: command entry to the Pi dispatch module, so
  eligibility and the wake prompt have one owner.
- bin/fm-claude-stop-autoarm.sh runs the host in the arm's place when
  config/supervision-host exists; nothing changes without the file.
- bin/fm-watch-arm.sh --stop: home-scoped stop without a re-arm.
- bin/fm-lease-lib.sh: an opted-in home takes the lease-command lock for
  unmarked main too, closing the first-claim race; the refusal tells the
  caller to leave the lease alone and retry.
- /afk launches no away daemon on an opted-in Claude home; /quiet still
  does. Session start renders the host's main-side protocol there.

* fix(bin): relay a host turn's outcomes when the captain returns mid-turn, and log per-turn engine cost

Live validation found two supervision host gaps. A captain who returns while
an engine turn is running gets a return brief rendered before that turn's
outcomes exist, so the host now hands the close to main with those outcomes.
Claude reports a resumed conversation's running cost, so the engine lib now
derives each turn's cost from the total the host records, and the host log
records every close's destination.

* docs(verification): record the supervision host's live evidence

The dated live results behind docs/supervision-host.md: the Claude engine's
live guard, the away-wake cases against real workers, the engine's cost
reporting, and the flag-off before-and-after regression.

* docs: describe the supervision host ledger as covering every close

* no-mistakes(review): Harden supervision host ownership, boundary, ack, and late outcomes

* no-mistakes(review): Recheck park boundary just before starting an engine turn

* no-mistakes(review): Cap park boundary, deliver all host lines, reject incomplete results

* no-mistakes(document): Correct supervision host documentation and stale pointers
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 27, 2026
* feat(bin): supervision host core behind config/supervision-host

Add the supervision host (bin/fm-supervision-host.sh): beside a Claude
primary it owns the watcher cycle for the Stop auto-arm and, while the
away-posture record exists, hands each wake to a bounded headless Claude
engine session that runs the supervision branch's contract - the same
generated prompt, row eligibility, wake grant, per-actor drain, outcome
store, leases, and away relocation the Pi branch uses. Attended wakes pass
straight to main. Every path that cannot finish a wake hands it to main
with a supervision-host line; the park ends itself before the Stop hook
timeout with a cycle-boundary wake.

- bin/fm-supervision-engine-lib.sh: opt-in parse, verified engines
  (claude, default sonnet), one bounded engine turn, and a reap of engine
  tool processes that sit in their own process groups.
- bin/fm-branch-report.sh: the command twin of fm_branch_report, scoped
  to the tasks the current host turn claimed.
- bin/fm-branch-dispatch.mjs: command entry to the Pi dispatch module, so
  eligibility and the wake prompt have one owner.
- bin/fm-claude-stop-autoarm.sh runs the host in the arm's place when
  config/supervision-host exists; nothing changes without the file.
- bin/fm-watch-arm.sh --stop: home-scoped stop without a re-arm.
- bin/fm-lease-lib.sh: an opted-in home takes the lease-command lock for
  unmarked main too, closing the first-claim race; the refusal tells the
  caller to leave the lease alone and retry.
- /afk launches no away daemon on an opted-in Claude home; /quiet still
  does. Session start renders the host's main-side protocol there.

* fix(bin): relay a host turn's outcomes when the captain returns mid-turn, and log per-turn engine cost

Live validation found two supervision host gaps. A captain who returns while
an engine turn is running gets a return brief rendered before that turn's
outcomes exist, so the host now hands the close to main with those outcomes.
Claude reports a resumed conversation's running cost, so the engine lib now
derives each turn's cost from the total the host records, and the host log
records every close's destination.

* docs(verification): record the supervision host's live evidence

The dated live results behind docs/supervision-host.md: the Claude engine's
live guard, the away-wake cases against real workers, the engine's cost
reporting, and the flag-off before-and-after regression.

* docs: describe the supervision host ledger as covering every close

* no-mistakes(review): Harden supervision host ownership, boundary, ack, and late outcomes

* no-mistakes(review): Recheck park boundary just before starting an engine turn

* no-mistakes(review): Cap park boundary, deliver all host lines, reject incomplete results

* no-mistakes(document): Correct supervision host documentation and stale pointers
mehulbhagwani pushed a commit to mehulbhagwani/firstmate that referenced this pull request Sep 27, 2026
* feat(bin): supervision host core behind config/supervision-host

Add the supervision host (bin/fm-supervision-host.sh): beside a Claude
primary it owns the watcher cycle for the Stop auto-arm and, while the
away-posture record exists, hands each wake to a bounded headless Claude
engine session that runs the supervision branch's contract - the same
generated prompt, row eligibility, wake grant, per-actor drain, outcome
store, leases, and away relocation the Pi branch uses. Attended wakes pass
straight to main. Every path that cannot finish a wake hands it to main
with a supervision-host line; the park ends itself before the Stop hook
timeout with a cycle-boundary wake.

- bin/fm-supervision-engine-lib.sh: opt-in parse, verified engines
  (claude, default sonnet), one bounded engine turn, and a reap of engine
  tool processes that sit in their own process groups.
- bin/fm-branch-report.sh: the command twin of fm_branch_report, scoped
  to the tasks the current host turn claimed.
- bin/fm-branch-dispatch.mjs: command entry to the Pi dispatch module, so
  eligibility and the wake prompt have one owner.
- bin/fm-claude-stop-autoarm.sh runs the host in the arm's place when
  config/supervision-host exists; nothing changes without the file.
- bin/fm-watch-arm.sh --stop: home-scoped stop without a re-arm.
- bin/fm-lease-lib.sh: an opted-in home takes the lease-command lock for
  unmarked main too, closing the first-claim race; the refusal tells the
  caller to leave the lease alone and retry.
- /afk launches no away daemon on an opted-in Claude home; /quiet still
  does. Session start renders the host's main-side protocol there.

* fix(bin): relay a host turn's outcomes when the captain returns mid-turn, and log per-turn engine cost

Live validation found two supervision host gaps. A captain who returns while
an engine turn is running gets a return brief rendered before that turn's
outcomes exist, so the host now hands the close to main with those outcomes.
Claude reports a resumed conversation's running cost, so the engine lib now
derives each turn's cost from the total the host records, and the host log
records every close's destination.

* docs(verification): record the supervision host's live evidence

The dated live results behind docs/supervision-host.md: the Claude engine's
live guard, the away-wake cases against real workers, the engine's cost
reporting, and the flag-off before-and-after regression.

* docs: describe the supervision host ledger as covering every close

* no-mistakes(review): Harden supervision host ownership, boundary, ack, and late outcomes

* no-mistakes(review): Recheck park boundary just before starting an engine turn

* no-mistakes(review): Cap park boundary, deliver all host lines, reject incomplete results

* no-mistakes(document): Correct supervision host documentation and stale pointers
RooseveltAdvisors pushed a commit to RooseveltAdvisors/firstmate that referenced this pull request Sep 29, 2026
* feat(bin): supervision host core behind config/supervision-host

Add the supervision host (bin/fm-supervision-host.sh): beside a Claude
primary it owns the watcher cycle for the Stop auto-arm and, while the
away-posture record exists, hands each wake to a bounded headless Claude
engine session that runs the supervision branch's contract - the same
generated prompt, row eligibility, wake grant, per-actor drain, outcome
store, leases, and away relocation the Pi branch uses. Attended wakes pass
straight to main. Every path that cannot finish a wake hands it to main
with a supervision-host line; the park ends itself before the Stop hook
timeout with a cycle-boundary wake.

- bin/fm-supervision-engine-lib.sh: opt-in parse, verified engines
  (claude, default sonnet), one bounded engine turn, and a reap of engine
  tool processes that sit in their own process groups.
- bin/fm-branch-report.sh: the command twin of fm_branch_report, scoped
  to the tasks the current host turn claimed.
- bin/fm-branch-dispatch.mjs: command entry to the Pi dispatch module, so
  eligibility and the wake prompt have one owner.
- bin/fm-claude-stop-autoarm.sh runs the host in the arm's place when
  config/supervision-host exists; nothing changes without the file.
- bin/fm-watch-arm.sh --stop: home-scoped stop without a re-arm.
- bin/fm-lease-lib.sh: an opted-in home takes the lease-command lock for
  unmarked main too, closing the first-claim race; the refusal tells the
  caller to leave the lease alone and retry.
- /afk launches no away daemon on an opted-in Claude home; /quiet still
  does. Session start renders the host's main-side protocol there.

* fix(bin): relay a host turn's outcomes when the captain returns mid-turn, and log per-turn engine cost

Live validation found two supervision host gaps. A captain who returns while
an engine turn is running gets a return brief rendered before that turn's
outcomes exist, so the host now hands the close to main with those outcomes.
Claude reports a resumed conversation's running cost, so the engine lib now
derives each turn's cost from the total the host records, and the host log
records every close's destination.

* docs(verification): record the supervision host's live evidence

The dated live results behind docs/supervision-host.md: the Claude engine's
live guard, the away-wake cases against real workers, the engine's cost
reporting, and the flag-off before-and-after regression.

* docs: describe the supervision host ledger as covering every close

* no-mistakes(review): Harden supervision host ownership, boundary, ack, and late outcomes

* no-mistakes(review): Recheck park boundary just before starting an engine turn

* no-mistakes(review): Cap park boundary, deliver all host lines, reject incomplete results

* no-mistakes(document): Correct supervision host documentation and stale pointers
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