feat(bin): add prime-agent as a verified harness adapter - #1966
feat(bin): add prime-agent as a verified harness adapter#1966eduardstan wants to merge 14 commits into
Conversation
Prime Agent 0.7.1 already runs firstmate workers today through fm-spawn's raw-launch escape hatch, and Herdr already reads their busy/idle state from prime-agent's own built-in reporter. This change ships only the three things that path genuinely cannot do, plus the minimum harness registration they need. 1. Teardown retires prime-agent's detached daemon prime-agent runs every root session in a detached daemon worker under one per-user supervisor. Closing the pane detaches the client, and so does an explicit `/quit`: both leave the worker `live`, holding its launch directory as cwd and a lease on its transcript. Observed twice - a torn-down task's session was still `lifecycle=live` on that worktree hours later, and a quit session stayed `live` too. Killing the endpoint does not end the agent, and the generic leaked-process reaper only SIGTERMs the survivors. `bin/fm-prime-agent-lib.sh` is the one owner of retiring those sessions, selected by a recorded session cwd that is the target directory or a path inside it. Two callers need exactly this, which is why it is a lib rather than a copy in each: teardown before its reaper, and secondmate relaunch (below). Never `prime-agent shutdown` - one supervisor serves the whole user, so it would stop the captain's own sessions and every other home's workers. `prime-agent status` is deliberately not consulted: it marks even a live session's forkserver `stale`, so that word is not a health signal. 2. Composer recognition, so fm-send verifies instead of false-failing prime-agent's prompt glyph is a plain `>`, which the fleet-wide composer rule correctly treats as a dead shell on an unstructured row. That rule is NOT relaxed. The Herdr adapter instead proves the container with two independent signals: the pane's live foreground process IS prime-agent (kernel-level) AND Herdr's native reporter says so. Both are required - `/quit` leaves the reporter identity behind on a pane that has already returned to a login shell, and on the reporter alone a shell with `PS1='> '` was verified live to become an injection target. Without this, every mid-turn steer to a BUSY prime-agent pane returned `verdict=unknown` and reported a false failure, because Herdr confirms an idle-baseline submit from native agent state but falls back to the composer read whenever the pane is already working. Verified live in both directions: empty / pending / (shell) unknown, and a busy-pane steer now returns `empty` and lands as a queued Steering message. 3. Secondmate capability `.prime/agent/extensions/` holds the tracked primary pair - turn-end guard and watcher - which prime-agent auto-discovers with no trust gate, unlike Pi. They are separate files rather than shared with the Pi pair because the two harnesses differ on the exact event the guard is built on: prime-agent has no `agent_settled` at all, and registering one is silently accepted and never fires. The guard reconstructs the settle from `agent_end` following prime-agent's own built-in Herdr reporter - hold through an auto-retry grace window on an error stop, skip an end with queued messages behind it, otherwise settle now. `ctx.isIdle()` is deliberately not the gate: it was measured false throughout a live run. The watcher extension is the Pi one minus its Calm rendering layer, which prime-agent's API cannot support (no `registerEntryRenderer`) and never sees anyway (it discovers only `.prime/`). The session lock needed one extra rule to make this sound. It records the harness ancestor pid, which for prime-agent IS that detached worker, so a home whose pane died would keep a live lock holder and every relaunch would land read-only. `fm-spawn --secondmate` therefore retires the worker bound to that home before launching its replacement, which is what makes the `prime-agent` entry in `bin/fm-session-lock-lib.sh` safe. Minimum registration those three require: detection, the launch template, `--model`/`--thinking`, the secondmate positional allowlist, the secondmate liveness allowlist, the crew-dispatch verified list, and the supervision protocol plus its session-start extension-loaded check. Detection is the one non-obvious piece. prime-agent exports the same `PI_CODING_AGENT=true` as pi, so the family marker cannot split it, and `FM_PI_HARNESS` - which works as the launch boundary for pi-signed - never reaches a prime-agent tool subprocess at all: a resident worker inherits the long-lived supervisor's environment rather than the launching client's. The same inheritance means a supervisor first started from a Claude session hands `CLAUDECODE=1` to every later prime-agent worker, which no launch-side `env -u` can reach - reproduced live as a crewmate reporting itself as `claude`. Detection therefore keys on prime-agent's own per-tool-call markers (`PRIME_AGENT_CODING_AGENT_DIR`, `PRIME_AGENT_INTERNAL_DAEMON_WORKER`) alongside the family marker, tested before the `CLAUDECODE` fast path. Both directions are pinned by tests: the vendor marker outranks an inherited `CLAUDECODE`, and a lone stale `PRIME_AGENT_*` with no family marker still resolves to `claude`. Registered name `prime-agent` matches the binary; a hyphen is precedented by `pi-signed`, and no existing glob in any allowlist, regex, or `case` arm can match it. Dropped per the narrowed scope No busy-source extension: prime-agent's own built-in reporter already publishes pane state, so the per-task extension carries only the `turn_end` wake touch and nothing is armed - a seeded busy record would have no firstmate writer to clear it. The cost is explicit: an IDLE prime-agent worker classifies `unknown missing` rather than `idle`, while a busy one classifies `busy herdr-native`. That is the escape hatch's existing behaviour, and unknown surfaces rather than hides. Also dropped: tmux agent-process classification (this home runs Herdr, and that check is harness-dependent on rendered output, so it needs proving against a real prime-agent pane under real tmux before it ships), the full fact table's model-discovery row, and the remote-secondmate allowlists - remote secondmates stay refused on prime-agent. Everything relied on about prime-agent's Herdr integration comes from reading `dist/core/extensions/builtin/herdr-agent-state.js`: it ships zero mentions of that integration across all 36 of its doc files. That dependency is recorded in the adapter's own section so a future upgrade re-reads the source rather than trusting a release note. Live verification (Herdr backend, free model) Crewmate, spawned through the normal path with no escape hatch: brief delivered, worktree isolated, `state/<id>.turn-ended` touched, busy readable as `busy herdr-native`, mid-turn steers delivered and verified on both idle and busy panes, `bin/fm-harness.sh` reporting `prime-agent` from inside the crewmate, and a clean teardown leaving no surviving process. The daemon-stop lib was separately exercised against a real live session and printed its own retirement line. Secondmate: launched into a real home, auto-discovered BOTH tracked extensions (`.prime-turnend-extension-loaded` and `.prime-watch-extension-loaded`), ran its own session start, acquired its own session lock, armed its watcher through `fm_watch_arm_prime` with the arm child parented by the prime-agent worker, and answered a redundant call with the ownership-based `watcher: unchanged` no-op. The lock wedge and its fix were both reproduced concretely rather than argued: with the pane closed, the recorded lock pid was still a live prime-agent worker and `fm_harness_pid_alive` returned true (the read-only wedge); the relaunch then printed `prime-agent: stopping detached session ... bound to <home>`, the old worker died, and the new session took the lock instead of being refused.
…wn, locks and liveness
The new fm_prime_agent_stop_sessions_under call is a cleanup courtesy, so guard it with || true exactly like the leaked-process reaper two lines below it: a best-effort step must never fail the whole teardown. The measured cause of the tests/fm-gotmp.test.sh failure was one level earlier, though. fm-teardown.sh sources bin/fm-prime-agent-lib.sh unconditionally, and that suite's two fake-root builders symlink each sibling teardown requires - the new lib was missing from both, so the source line itself aborted under set -e with a valid tasktmp. Both builders now link it, matching how the other thirteen siblings are handled, and all three cases pass.
4e98a89 to
83e4ca5
Compare
|
Closing this in favour of a sequence of smaller pull requests. This PR carries the whole prime-agent adapter: 41 files, 2811 lines. That is well past the 20-file / 1000-line limit the auto-merge evaluator applies, so it could never clear that gate regardless of its contents. Rather than leave a large PR open alongside the split, I am closing it so there is exactly one live version of this work. The same content, split along dependency seams and each measured against the cap:
Each was verified to stand alone against current Review order is #2062 first; the other two are independent of each other. One note that may be worth a maintainer's attention independently: since 2026-08-08 both workflow runs on fork pull requests here complete with |
Intent
firstmate gains prime-agent as a verified harness adapter - crewmate, scout and secondmate capable - with its own supervision protocol, primary guard extensions under the .prime/agent layout, teardown that stops its daemon session, and composer recognition so steers verify.
Scope context a reviewer reading only the diff would not have. The task was deliberately NARROWED partway through by the captain, because prime-agent already runs firstmate workers today through fm-spawn's raw-launch escape hatch and Herdr already reads their busy/idle state from prime-agent's own built-in reporter. The instruction was to ship only what that escape hatch genuinely cannot do - (1) teardown shutting down prime-agent's detached daemon, (2) composer recognition so bin/fm-send.sh verifies instead of false-failing, (3) secondmate capability - plus the minimum harness registration those three require, and nothing beyond it.
Deliberate exclusions the captain asked for, so their absence is intended and not an oversight:
Deliberate design decisions that may look surprising in the diff:
prime-agent shutdown, which would stop the captain's own sessions and every other home's workers, and it deliberately ignoresprime-agent status, which marks even a live session's forkserver "stale".Everything relied on about prime-agent's Herdr integration comes from reading dist/core/extensions/builtin/herdr-agent-state.js, because prime-agent ships zero mentions of that integration across all 36 of its doc files. That source dependency is recorded in the adapter section so a future upgrade re-reads the source rather than trusting a release note.
Delivery constraints for this run: the branch fm/prime-adapter must not be reset or replaced. Do not change any git remote - origin is the parent kunchenguid/firstmate and the gate pushes to the fork eduardstan/firstmate exactly as CONTRIBUTING.md prescribes. The branch sits on upstream main, so a rebase warning about bundling ~52 commits is a fork/upstream divergence artifact and the rebase step is to be skipped rather than rebased onto origin/main. The PR must open against the parent repo, not the fork.
What Changed
prime-agentacross the harness surface:bin/fm-harness.shdetects it from its ownPRIME_AGENT_*markers alongsidePI_CODING_AGENT=truebefore theCLAUDECODEfast path (its daemon worker inherits the long-lived supervisor's environment, so a supervisor first started under Claude otherwise misreports every later worker),bin/fm-spawn.shgains crewmate and secondmate launch shapes plus--model/--thinkingmapping, and it is accepted as a verified harness by bootstrap crew dispatch and secondmate liveness. Adds aprime-agentsupervision protocol doc and a separate pair of primary extensions under.prime/agent/extensions/(turn-end guard reconstructing settle fromagent_end, and a watcher extension), wired into session-start load reporting and the supervision instructions.bin/fm-prime-agent-lib.shas the single owner of retiring prime-agent's detached daemon sessions by recorded cwd (bounded CLI calls, neverprime-agent shutdown).bin/fm-teardown.shstops sessions bound to a worktree or removed home before the generic process reaper,fm-spawn --secondmateretires the worker bound to a home before relaunching it, andbin/fm-session-lock-lib.shlists prime-agent as a lock-holding harness while treating a worker with no attached client and no in-flight activity as not alive.prime-agentbut whose process subtree holds none is reportedno-agent(its reporter identity survives/quit), and a bare>row is promoted from dead shell to composer only when the live foreground process and the native reporter both say prime-agent. New tests cover detection in both directions, launch shapes, composer promotion and its refusal, daemon retirement, lock ancestry, and the watcher extension.Risk Assessment
dist/JSON shapes that an upgrade can change silently, but every fix-round claim verified against that source holds and only two low-severity issues remain, so it is safe to merge with those as follow-ups.Testing
Ran the five test scripts this change touches — the new prime-agent harness, prime watcher-extension, session-lock ancestry and the herdr backend suite all pass; the Pi/prime TypeScript no-emit check gate-skips here because neither tsc nor the global pi-coding-agent package is installed and installing them is outside the worktree boundary, so CI owns that one. Because the machine has real prime-agent 0.7.1 and real herdr, I went past the fixtures: I launched a real prime-agent session in a temp directory, confirmed its detached daemon worker outlived its client, and watched fm_prime_agent_stop_sessions_under retire exactly that session while the captain's live session and one unrelated session stayed untouched. I fed the real inherited environment of a live prime-agent daemon worker into both the base and target fm-harness.sh: base answers "claude" (the reported live misidentification), target answers "prime-agent". In an isolated fm-herdr-lab session I ran the real prime-agent TUI in a pane — its bare ">" composer classifies "empty" on the target but "unknown" at base, which is exactly the false-fail steers were hitting — then reproduced the /quit hazard, where the reporter still claims agent: prime-agent on a login shell with PS1='> ' and the target still refuses it as a composer and reports no-agent. tmux-side classification is intentionally out of scope per the intent, and this home has no tmux, so nothing there was exercised. Evidence is CLI transcripts rather than screenshots because every surface here is shell-level; the herdr pane renders were captured as text through herdr's own read API.Evidence: Live teardown of a real detached prime-agent daemon session (only the target worktree's session stops)
$ ps -o pid,stat,args -p 3321054 # daemon worker outlived the pane's client process 3321054 Ssl prime-agent BEFORE: [{"id":"7d92f3b43dd7","lifecycle":"live","cwd":"/home/eduard/.treehouse/fm-7bab20/1/fm"},{"id":"13171cb33bf2",...},{"id":"c32a348db616","lifecycle":"draft","cwd":"/tmp/fm-prime-e2e.fMp7f8"}] $ . bin/fm-prime-agent-lib.sh; fm_prime_agent_stop_sessions_under /tmp/fm-prime-e2e.fMp7f8 prime-agent: stopping detached session c32a348db616 bound to /tmp/fm-prime-e2e.fMp7f8 AFTER: [{"id":"7d92f3b43dd7","lifecycle":"live",...},{"id":"13171cb33bf2",...}]Evidence: Harness detection under the real environment of a live prime-agent daemon worker (base vs target)
relevant inherited vars (pid 2210194, prime-agent 0.7.1): CLAUDECODE=1 PI_CODING_AGENT=true PRIME_AGENT_INTERNAL_DAEMON_WORKER=1 $ /tmp/fm-harness-base.sh # base 833a9a2 (before this change) claude $ bin/fm-harness.sh # target 1894f52 (this change) prime-agentEvidence: Composer recognition on a live prime-agent TUI in a real herdr pane (base false-fails, target verifies)
$ herdr pane read (bottom of the real TUI) > Try "add tests for @<filepath>" ← agents/resume Claude Opus 5 • medium ? f… 0 (0%) $ herdr agent get w1:p1 {"agent":"prime-agent","agent_status":"idle"} $ fm_backend_herdr_composer_state base 1894f52^: unknown target 1894f52 : empty $ fm_backend_herdr_pane_agent_state (target) liveEvidence: The /quit hazard: reporter identity survives onto a `> ` login shell and is still refused
$ herdr agent get w1:p1 # still claims a prime-agent is here {"agent":"prime-agent","agent_status":"idle"} pane render: > PS1='> ' > $ fm_backend_herdr_composer_state (target) unknown <- injection target refused $ fm_backend_herdr_pane_agent_state (target) no-agent <- quit pane no longer counted aliveEvidence: Session-lock reclaim query run against the real prime-agent 0.7.1 listing
[{"id":"7d92f3b43dd7","workerPid":2210194,"attachedClients":1,...},{"id":"13171cb33bf2","workerPid":2657115,"attachedClients":0,...}] pid 2210194 -> live/unknown (lock kept) pid 2657115 -> abandoned (lock reclaimable) pid 999999 -> live/unknown (lock kept)Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-teardown.sh:2362- Teardown of a prime-agent SECONDMATE never retires that home's detached daemon worker. The new retirement calls cover task worktrees (line 2218, guarded by[ "$KIND" != secondmate ]) and a home's child task worktrees (line 2067), but theKIND=secondmatepath runsremove_firstmate_home "$HOME_PATH"with nofm_prime_agent_stop_sessions_underanywhere ahead of it. Concrete sequence: spawn a prime-agent secondmate into home H;bin/fm-teardown.sh <id> --force; the worker whose recorded cwd is H is stilllifecycle=live(the same survival the change documents as verified for task worktrees), whileremove_firstmate_homeeithertreehouse returns H to the pool orrm -rfs it. A latertreehouse getthen hands that same worktree to a new task with a foreign live agent process still holding it as cwd and holding a transcript lease. This is the one path the intent's required behavior ("teardown that stops its daemon session") plus required secondmate capability intersect on and it is missed. Earliest shared boundary that closes it for every home-removal caller, including nested secondmate homes reached viacleanup_firstmate_home_children: onefm_prime_agent_stop_sessions_under "$abs_home_path"insideremove_firstmate_home()(bin/fm-teardown.sh:1679), aftervalidate_firstmate_home_for_removalresolves the path and before the treehouse return /safe_rm_rf.bin/fm-bootstrap.sh:689- A/quitprime-agent pane classifiesalive, so the relaunch that the change relies on as its own safety proof can never fire. The diff establishes as verified fact (bin/backends/herdr.sh:2802-2807, and it is the entire reason the composer arm needs two signals) that after/quitthe pane returns to a login shell while Herdr still reportsagent: prime-agent, agent_status: idle.fm_backend_herdr_pane_agent_state(bin/backends/herdr.sh:1905-1909) mapsidlestraight tolive, andfm_backend_herdr_agent_statemaps that toalive. Concrete sequence: a prime-agent secondmate's operator types/quit; the pane is now an unusable login shell butsecondmate_liveness_onetakes thealivebranch and prints nothing actionable;fm-spawn.sh --secondmateis never invoked, so the newfm_prime_agent_stop_sessions_under "$PROJ_ABS"retirement (bin/fm-spawn.sh:1368) never runs; and the detached worker keeps satisfyingFM_HARNESS_REas a live lock holder. That is exactly the wedge bin/fm-session-lock-lib.sh:17-23 says the spawn-side retirement is what makes safe, so the stated invariant does not hold for the quit path. The change already ships the correct proof: gate theliveverdict for a prime-agent-identified pane onfm_backend_herdr_pane_prime_agent_foreground, the same kernel-level check the composer arm uses, so a quit pane classifiesdeadand the existing respawn-plus-retire path takes over.bin/fm-harness.sh:57- An inheritedPRIME_AGENT_CODING_AGENT_DIRoutranks the explicitFM_PI_HARNESSlaunch-boundary stamp, misidentifying pi and pi-signed workers. The new branch fires onPI_CODING_AGENT=trueplus any prime-agent marker without consultingFM_PI_HARNESS, so it wins before the pi/pi-signed resolution at line 68. Concrete sequence, newly reachable because this change makes prime-agent a supported primary: the captain runs prime-agent as primary; a spawn from its bash tool subprocess starts the tmux/herdr server, which storesPRIME_AGENT_CODING_AGENT_DIRfrom that subprocess in its session environment (the same stored-environment hazard the surrounding comment already documents); a laterpi-signedcrewmate is launched into that server withFM_PI_HARNESS=pi-signedprepended by bin/fm-spawn.sh:960; inside it,bin/fm-harness.shprintsprime-agentinstead ofpi-signed, discarding the marker the file itself calls "the authoritative selection marker". Fix is to make the prime branch yield when the launch boundary explicitly names another Pi-family member, e.g. add&& [ "${FM_PI_HARNESS:-}" != pi ] && [ "${FM_PI_HARNESS:-}" != pi-signed ]to the condition; the existing both-directions detection test can be extended with that pair.README.md:61- README's verified-primary list is now inconsistent with the shipped adapter, and it is the declared owner of that set. docs/configuration.md:209 (changed by this diff) states "README requirements own the set supported for the primary session", and this change makes prime-agent primary-capable: it has docs/supervision-protocols/prime-agent.md, a bin/fm-session-start.sh extension-load report, a turn-end guard, and secondmate support (a secondmate IS a firstmate primary). README.md:61 still reads "Claude Code, Grok, Pi,pi-signed, Codex, or OpenCode", and README.md:214's supervision-protocols enumeration likewise omits it, while AGENTS.md:178, docs/architecture.md:60, and docs/configuration.md:209 were all updated. Concrete effect: a reader following the one-owner pointer from configuration.md to README concludes prime-agent is not supported for a primary session, contradicting the adapter that just shipped.bin/backends/herdr.sh:2869- The prime promotion costs one or two extra Herdr RPCs per composer read on non-prime panes whose capture window contains a markdown blockquote.prime_lineis set for any row matching^>( |$), and the promotion runs wheneverprime_line > generic_line; for a Pi pane (separated composer, no prompt glyph or border, sogeneric_lineis often 0) any> quoted texttranscript row in the last 20 lines triggerspane process-infoand thenagent geton every call. Concrete case: away-mode's pre-injection guard polling a Pi pane whose transcript ends with a quoted block pays two extra round-trips per poll. The verdict is still correct (both signals fail, the row is not promoted) and the cost is bounded, so this is a note rather than a defect; a cheap guard would be to skip the promotion unless the held-aside row is the last non-blank row of the capture.🔧 Fix: let explicit FM_PI_HARNESS outrank stale prime-agent markers
6 issues (5 warnings, 1 info) still open:
bin/fm-teardown.sh:2362- Teardown of a prime-agent SECONDMATE never retires that home's detached daemon worker. The new retirement calls cover task worktrees (line 2219, guarded by[ "$KIND" != secondmate ]) and a home's child task worktrees (line 2067), but theKIND=secondmatepath goes straight toremove_firstmate_home "$HOME_PATH"with nofm_prime_agent_stop_sessions_underanywhere ahead of it - the comment at line 2211 says a secondmate home's runtime lifecycle is owned by 'the process-event and firstmate-home removal machinery further below', and that machinery has no prime-agent step. Concrete sequence: spawn a prime-agent secondmate into home H; runbin/fm-teardown.sh <id> --force; the worker whose recorded cwd is H is stilllifecycle=live(the exact survival this change documents and verifies for task worktrees), whileremove_firstmate_homeeithertreehouse returns H to the pool orsafe_rm_rfs it. A latertreehouse getthen hands that same worktree to a new task while a foreign live agent holds it as cwd and holds a transcript lease. This is where the intent's required 'teardown that stops its daemon session' and required secondmate capability intersect, and it is the one intersection with no coverage: tests/fm-prime-agent-harness.test.sh:267 exercises a SCOUT teardown, which takes the non-secondmate branch. Earliest shared boundary that closes it for every home-removal caller, including nested homes reached viacleanup_firstmate_home_children: onefm_prime_agent_stop_sessions_under "$abs_home_path"insideremove_firstmate_home()(bin/fm-teardown.sh:1679) aftervalidate_firstmate_home_for_removalresolves the path and before the treehouse return /safe_rm_rf.bin/fm-session-lock-lib.sh:24- Addingprime-agenttoFM_HARNESS_REmakes an in-place restart of a prime-agent home land read-only, and the retirement that is supposed to make this entry safe only exists on thefm-spawn --secondmatepath. The lock records the harness ancestor pid, which for prime-agent is the detached worker, and this change's own verified fact is that/quitleaves that workerlive. Concrete sequence, driven by instructions this same change ships:bin/fm-session-start.sh:648prints 'restart prime-agent from this repo root so <ext> and <ext> auto-load', and docs/supervision-protocols/prime-agent.md step 2 says the same; the operator quits and relaunchesprime-agentin that same pane (a fresh session, not--resume);bin/fm-lock.sh:67then reads the old worker pid,fm_harness_pid_alivereturns true because its comm is stillprime-agent, and the new session is refused with 'another live firstmate session holds the lock; operate read-only until resolved' - the identical wedge the commit message says was reproduced live, now with no caller to clear it, becausefm_prime_agent_stop_sessions_underruns only from bin/fm-spawn.sh:1368 and bin/fm-teardown.sh. Recovery requires a manualprime-agent stop, which nothing in the shipped docs mentions. Earliest shared boundary: the live-holder refusal in bin/fm-lock.sh, which for prime-agent needs to treat an alive-but-unbound worker as reclaimable (or the restart instructions in fm-session-start.sh and the supervision protocol must retire the home's session first).bin/fm-bootstrap.sh:689- A/quitprime-agent secondmate pane classifiesalive, so the relaunch path that the rest of the design leans on never fires. The change establishes as verified fact (bin/backends/herdr.sh:2802-2807 - it is the whole reason the composer arm needs two signals) that after/quitthe pane is a login shell while Herdr still reportsagent: prime-agent, agent_status: idle.fm_backend_herdr_pane_agent_state(bin/backends/herdr.sh:1905-1909) mapsidletolive, andfm_backend_herdr_agent_statemaps that toalive. Concrete sequence: a prime-agent secondmate's operator types/quit; the pane is now an unusable shell butsecondmate_liveness_onetakes thealivebranch and prints nothing actionable;fm-spawn.sh --secondmateis never invoked, so the retirement at bin/fm-spawn.sh:1368 never runs and the detached worker keeps satisfyingFM_HARNESS_REas a lock holder. The change already ships the correct discriminator: gate theliveverdict for a prime-agent-identified pane onfm_backend_herdr_pane_prime_agent_foreground, the same kernel-level check the composer arm uses, so a quit pane classifies dead and the existing respawn-plus-retire path takes over.bin/fm-harness.sh:62- The fix-round commit (bc19394) inverts the precedence the intent marks as required, and re-opens the misdetection in the other direction. The intent states: 'bin/fm-harness.sh tests prime-agent's own markers BEFORE the CLAUDECODE fast path. This ordering is required, not stylistic' - because a prime-agent worker inherits the long-lived per-user SUPERVISOR's environment, not the launching client's. The new&& [ "${FM_PI_HARNESS:-}" != pi ] && [ "${FM_PI_HARNESS:-}" != pi-signed ]conjunct makes an inherited FM_PI_HARNESS outrank the per-tool-call vendor markers, but FM_PI_HARNESS is subject to exactly the same supervisor inheritance: if the daemon supervisor is first started from a context that carriesFM_PI_HARNESS=pi-signed(a pi-signed worker launched by bin/fm-spawn.sh:960 runningprime-agentfrom its bash tool, or the raw-launch escape hatch the intent says is in daily use), every later prime-agent worker inherits it. Detection then skips the prime branch and falls into theCLAUDECODEfast path on the very next line, so such a worker reportspi-signed- orclaudewhen that supervisor also captured CLAUDECODE=1, which is the exact live-reproduced failure the ordering exists to prevent. The added tests (tests/fm-prime-agent-harness.test.sh:56-62) pin only the pi-side direction and would still pass with this hole open. Both markers are ambiguous under stored/inherited environments, so this is a precedence call the author should make rather than the pipeline: PRIME_AGENT_CODING_AGENT_DIR is injected per tool call and PRIME_AGENT_INTERNAL_DAEMON_WORKER=1 is set by the worker itself, which is why the original ordering ranked them highest.README.md:61- README's verified-primary list is now inconsistent with the shipped adapter, and it is the declared owner of that set. docs/configuration.md:209 (changed by this diff) states 'README requirements own the set supported for the primary session', and this change makes prime-agent primary-capable: docs/supervision-protocols/prime-agent.md, the bin/fm-session-start.sh:635 extension-load report, the turn-end guard, and secondmate support (a secondmate IS a firstmate primary). README.md:61 still reads 'Claude Code, Grok, Pi,pi-signed, Codex, or OpenCode', and README.md:214's supervision-protocols enumeration likewise omits it, while AGENTS.md:178, docs/architecture.md:60, and docs/configuration.md:209 were all updated. A reader following the one-owner pointer from configuration.md to README concludes prime-agent is unsupported for a primary session, contradicting the adapter that just shipped..prime/agent/extensions/fm-primary-prime-watch.ts:1- The two new tracked extensions (729 lines, the guard and watcher a prime-agent secondmate actually runs) sit outside both contracts their Pi siblings are held to: tests/fm-pi-primary-types.test.sh copies and strict-typechecks only the three.pi/extensions/*.tsfiles, and tests/fm-pi-watch-extension.test.sh behaviorally drives the Pi watcher's generation ownership, retry, and unchanged-no-op paths against a mock API - the prime pair has neither, so its evidence is the live run recorded in the commit message alone. Note before extending the typecheck: both files importtype { ExtensionAPI }from@earendil-works/pi-coding-agentwhile registering prime-agent's own event set (agent_start/agent_endwithctx.hasPendingMessages), so adding them to the existing tsconfig may need a local type shim rather than being a pure test edit - which is why this is a call for the author rather than a mechanical fix.🔧 Fix: reconcile prime-agent daemon workers with teardown, locks and liveness
5 issues (4 warnings, 1 info) still open:
bin/fm-prime-agent-lib.sh:85-fm_prime_agent_worker_abandoneddecides liveness fromattachedClientsALONE, so a detached worker that is still running a turn is reported dead and its home's lock can be taken by a second session. The change's own evidence is that the worker outlives the pane mid-flight ("a torn-down task's session was stilllifecycle=live"). Concrete sequence: a prime-agent secondmate is mid-turn when its pane dies (herdr/tmux restart, pane kill); the daemon worker keeps streaming and writing into the home, but no client is attached, soattachedClientsis 0. The operator follows this change's own in-place-restart instruction (bin/fm-session-start.sh:648, docs/supervision-protocols/prime-agent.md), and bin/fm-lock.sh:64/89 now sees the recorded holder as not alive, overwritesstate/.lock, and the new session starts writing the same home while the old worker is still working - the exact double-writer the fleet lock exists to prevent. The vendor publishes the discriminator in the same summary the query already reads: its own idle-eviction predicate is!session.isSessionActive && session.attachedClients === 0(dist/bundle/chunk-VNU2AJHD.js:42035). RequiringisSessionActive == false(andisStreaming == false) alongside the client count keeps the quit/dead-pane reclaim the user asked for while refusing to steal the lock from a worker that is still running. Flagging rather than fixing because narrowing the reclaim touches the "an in-place restart must reacquire its own lock" instruction directly.bin/fm-prime-agent-lib.sh:83- The abandoned query joins onworkerPid- which identifies a WORKER - but then answers from.[0].attachedClients, one arbitrary session of that worker. A prime-agent worker hosts MANY sessions: itslisthandler returnsArray.from(this.sessions.values())(dist/modes/daemon/daemon-mode.js:2810) and the supervisor stamps every one of them with the sameworkerPidviapublicSummary(dist/modes/daemon/daemon-supervisor.js:2635-2643), includingkind: "subagent"/ RLM child sessions, which no client ever attaches to and which therefore always reportattachedClients: 0. Concrete sequence: a prime-agent secondmate with an operator attached (root session, attachedClients 1) spawns a subagent, then does/newor/resumeso the replacement root session is inserted into the worker's session map after the subagent entry; the first entry matching the lock pid is now the subagent with 0 clients,fm_harness_pid_alivereturns false for a fully live client-attached session, andbin/fm-lock.sh:89lets another process overwrite that home's lock. Ordering is the only thing standing between this and a wrong answer, and nothing in the protocol guarantees it. Fix is mechanical: aggregate over every matching session instead of the first, e.g.map(select(...)) | map(.attachedClients // 0) | max // empty, so "abandoned" means no client is attached to ANY session on that worker.bin/backends/herdr.sh:1921- The new liveness demotion treats "prime-agent is not in the pane's FOREGROUND process group" as proof of a husk, but prime-agent deliberately removes itself from the foreground on Ctrl-Z: its interactive mode self-suspends the whole group withprocess.kill(0, "SIGTSTP")(dist/modes/interactive/interactive-mode.js:5739), which hands the terminal back to the login shell while the agent and its daemon worker stay alive. Concrete sequence: an operator presses Ctrl-Z in a prime-agent secondmate pane;pane process-inforeads cleanly and returns 1 (prime-agent absent from the foreground group), sofm_backend_herdr_pane_agent_stateprintsno-agent,fm_backend_herdr_agent_statemaps that todead, andsecondmate_liveness_one(bin/fm-bootstrap.sh:701-707) takes the dead branch:fm_backend_killdestroys the pane andfm-spawn.sh --secondmaterelaunches, whose new retirement (bin/fm-spawn.sh:1368) then runsprime-agent stopon the still-live session, discarding its in-flight turn.fm_backend_herdr_tab_is_husklikewise licenses close-and-replace on the same verdict. The husk-close path in bin/fm-herdr-session-cleanup.sh is protected by its extra idle-childless-shell proof, but the recovery path is not. A tighter discriminator (for example also requiring the pane's foreground process to be the pane's own shell with no stopped prime-agent child, or keepingliveunless the reporter has also gone) would still make the/quitpane classify dead without demoting a suspended live agent.tests/fm-claude-stop-autoarm.test.sh:33- bin/fm-session-lock-lib.sh:14-17 now unconditionally sourcesfm-prime-agent-lib.shfrom its own directory, but the three fixtures that copy the lib's dependency set into a lab checkout were not updated: tests/fm-claude-stop-autoarm.test.sh:33, tests/fm-turnend-guard.test.sh:1123, and tests/fm-session-lock-ancestry.test.sh:232 all copy fm-primary-scope-lib.sh, fm-supervision-lib.sh, fm-wake-lib.sh, fm-session-lock-lib.sh and fm-lock.sh, and none copies fm-prime-agent-lib.sh. Reproduced against the current tree:cp bin/fm-session-lock-lib.sh bin/fm-lock.sh <lab>/bin/ && <lab>/bin/fm-lock.sh statusprints<lab>/bin/fm-prime-agent-lib.sh: No such file or directorybefore its result. The scripts onlyset -u, so they continue, but every hook invocation in those labs now emits that line into the capturedstate/hook.outthose tests assert against, andfm_prime_agent_worker_abandonedis undefined there. Fix is onecpline per fixture (or guard the source with a readability test).bin/fm-prime-agent-lib.sh:79-fm_harness_pid_alivewas pureps/kill -0; it now shells out toprime-agent list --jsonwith no bound whenever the recorded holder is a prime-agent process. That call makes the supervisor refresh every worker's summaries and runsyncAgentPeersbefore answering (dist/modes/daemon/daemon-supervisor.js:1556-1563), so a wedged or slow daemon socket now stallsbin/fm-lock.sh- and therefore session start, bin/fm-sessionstart-run.sh and bin/fm-startup-network.sh, which all source this lib - instead of failing fast. The repo already owns a bound runner (fm_run_timed, bin/fm-timeout-lib.sh); wrapping the listing in a few-second bound would keep the existing fail-safe (any unknown answers "not abandoned"). Noted rather than blocking because the herdr backend's CLI calls are unbounded today too.🔧 Fix: bound prime-agent RPCs and fix worker-abandoned verdict
3 issues (2 warnings, 1 info) still open:
bin/backends/herdr.sh:1918- The new liveness demotion destroys a LIVE prime-agent secondmate that the operator merely suspended.fm_backend_herdr_pane_agent_statenow demotes aprime-agent-identified pane tono-agentwheneverfm_backend_herdr_pane_prime_agent_foregroundreturns 1, i.e. prime-agent is absent from the pane's foreground process group. prime-agent deliberately removes itself from the foreground on Ctrl+Z:app.suspendis bound (dist/modes/interactive/interactive-mode.js:3246->handleCtrlZ, hinted as "to suspend" at :956) and the handler stops the whole group withprocess.kill(0, "SIGTSTP")(:5739), handing the terminal back to the login shell while the agent and its detached daemon worker stay alive. The reporter keeps answeringagent: prime-agent, agent_status: idlebecause it lives in the daemon worker, not the suspended client process group. Concrete sequence: operator presses Ctrl+Z in a prime-agent secondmate pane; the next bootstrap sweep readsno-agent,fm_backend_herdr_agent_statemaps it todead, andsecondmate_liveness_one(bin/fm-bootstrap.sh:701-707) takes the dead branch -fm_backend_killdestroys the pane, thenfm-spawn.sh --secondmateruns the new retirement at bin/fm-spawn.sh:1368, which issuesprime-agent stopagainst the still-live session and discards its in-flight turn. The added test (tests/fm-backend-herdr.test.sh:73-111) covers quit / live / unreadable / other-harness but not suspend, and a suspended pane is indistinguishable from a quit pane under this discriminator. Note also thatfm_backend_herdr_tab_is_husklicenses close-and-replace on the same verdict; only the husk-close path in bin/fm-herdr-session-cleanup.sh carries the extra idle-childless-shell proof, the recovery path does not. The user's instruction offered two boundaries - "a quit pane must NOT classify alive, OR the relaunch path must not depend on that classification" - and only the first was implemented. The second is the one that discriminates: before killing and relaunching a prime-agent secondmate, ask prime-agent whether any client is still attached to the worker bound to that home (the sameattachedClientsanswer bin/fm-prime-agent-lib.sh already reads). A quit or pane-killed client is gone from the daemon's client set; a Ctrl+Z-suspended one still holds its socket.bin/fm-prime-agent-lib.sh:114-fm_prime_agent_worker_abandoneddecides liveness fromattachedClientsALONE, so a detached worker that is still running a turn is reported not-alive and its home's lock can be handed to a second session. The change's own verified fact is that the worker outlives the pane mid-flight. Concrete sequence: a prime-agent home's pane dies mid-turn (terminal closed, herdr restart, pane kill); the daemon worker keeps streaming and writing into that home's worktree, but no client socket remains, so every one of its sessions reportsattachedClients: 0andmaxis 0. The operator follows this change's own in-place-restart instruction (bin/fm-session-start.sh:648, docs/supervision-protocols/prime-agent.md step 2) and launches prime-agent in that home;bin/fm-lock.sh:64and:89now see the recorded holder as not alive, overwritestate/.lock, and the new session starts writing the same worktree while the old worker is still working - the double-writer the fleet lock exists to prevent. Nothing else closes this:fm_prime_agent_stop_sessions_underruns only frombin/fm-spawn.sh --secondmateandbin/fm-teardown.sh, neither of which a manual in-place restart goes through. The vendor publishes the missing discriminator in the same listing this query already reads:publicSummaryspreads the full summary (dist/modes/daemon/daemon-supervisor.js:2635-2643), soisSessionActiveand the derivedstatus("running"whenisSessionActive || hasRunningRlmChildren, dist/modes/daemon/daemon-session-list.js:23-31) are both present per session, and the vendor's own idle-eviction predicate is!isSessionActive && attachedClients === 0(dist/bundle/chunk-VNU2AJHD.js:42035). Requiring no running session alongside the zero client count keeps the quit/dead-pane reclaim the user asked for while refusing to steal the lock from a worker that is still mid-turn. Flagged rather than auto-fixed because narrowing the reclaim touches the "an in-place restart must reacquire its own lock" instruction directly.bin/fm-session-lock-lib.sh:20- Sourcingbin/fm-session-lock-lib.shnow turns onset -uin the caller's shell, contradicting the contract stated three lines above the new source block ("This file is sourced by scripts and has no side effects on source"). Chain: fm-session-lock-lib.sh:20 -> fm-prime-agent-lib.sh:37 -> fm-timeout-lib.sh, which runsset -uat file scope. Reproduced against the current tree:bash -c '. bin/fm-session-lock-lib.sh; echo $-'prints flags containinguwhere the parent shell had none. No caller breaks today - every executable that reaches this chain (includingbin/backends/tmux.sh:24, which is sourced lazily into fm-send/fm-peek/fm-bootstrap and is itself written without nounset) already sets-uat its top - so this is a latent trap rather than a live defect. Worth a note because the four backend libs are deliberately nounset-free source-only files and this quietly changes the mode they load under.🔧 Fix: keep suspended and mid-turn prime-agent sessions alive
2 issues (1 warning, 1 info) still open:
bin/fm-prime-agent-lib.sh:141- The new busy predicate treats a MISSING field as busy, but prime-agent's own listing returns sessions of two different shapes under the sameworkerPid, and the inactive shape omits two of the six flags - soabandonedis unreachable for any worker whose root session has a persisted, non-resident subagent, and the in-place-restart reclaim the previous round was asked to deliver is silently gone again. Source chain, all in the installed prime-agent 0.7.1: the worker'slisthandler answers withbuildSessionListWithPassiveRlmSubagents(activeSessions, [], scheduledJobs)(dist/modes/daemon/daemon-mode.js:2809), which walks EVERY resident session's RLM-subagent registry regardless of the empty saved list (dist/modes/daemon/daemon-mode.js:736-745, :792-820) and renders each non-resident child throughsummaryForInactiveSession(dist/modes/daemon/daemon-session-list.js:172-190). That shape publishesisSessionActive,isStreaming,isCompactingandunfinishedActionCountbut NOTisBashRunning,hasRunningRlmChildrenorisRunningTools. The supervisor then stampsworkerPidon every summary it hands back, inactive ones included (publicSummaryspread, dist/modes/daemon/daemon-supervisor.js:2635-2643, applied to all ofworker.summariesat :1563). Sobusy_flag(.isBashRunning)sees null, returns true,anyis true, and the verdict islive. Reproduced against the shipped jq expression with a two-row listing (one complete active row, attachedClients 0 and every flag false; one passive subagent row missing the two active-only flags): outputlive, where the same listing without the passive row givesabandoned. Concrete sequence: a prime-agent secondmate spawns a subagent, the child finishes and stops being resident, the operator quits, and the operator then follows this change's own instruction (bin/fm-session-start.sh:648, docs/supervision-protocols/prime-agent.md step 2) to restart prime-agent in place;bin/fm-lock.sh:64reads the old worker pid,fm_harness_pid_alivegetsliveforever, and the session lands read-only with no documented recovery. The added test cannot catch this: tests/fm-session-lock-ancestry.test.sh:414 (unknown-activity) is a listing with NO activity fields at all, which is the intended unknown case, and no case mixes a complete active row with an incomplete sibling row. Fix keeps both requirements: judge only summaries that actually describe a resident session (map(select(.activeSessionId != null)), the fieldsummaryForActiveSessionalways sets andsummaryForInactiveSessionnever does), or require booleans only for the four flags both shapes publish and default the active-only three to false.isSessionActivealready subsumesisBashRunning(dist/core/agent-session.js:4738-4746), so no mid-turn protection is lost.docs/configuration.md:213- Two owner docs still describe the pre-round-4 rules. docs/configuration.md:213 says an in-place restart "needs no retirement at all: session-lock liveness asks prime-agent whether any client is still attached to the recorded worker" - the verdict now also requires every session of that worker to prove it is idle, so a worker detached mid-turn deliberately keeps the lock and the restart DOES land read-only until it finishes. The same paragraph's "a quit pane classifies dead rather than alive" no longer states the discriminator that was added: absence from the pane's foreground is not enough, the pane's whole process subtree must hold no prime-agent process, precisely so a Ctrl+Z-suspended agent stays alive. .agents/skills/harness-adapters/SKILL.md:342, the declared owner of adapter facts, still says the fm-spawn retirement "is what makes theprime-agententry inbin/fm-session-lock-lib.shsafe", which is now the abandoned-worker query in bin/fm-prime-agent-lib.sh, and its composer section never mentions the suspend case a future reader would otherwise re-break. Docs-only; no behavior depends on it.🔧 Fix: judge prime-agent liveness on resident sessions only
2 infos still open:
bin/backends/herdr.sh:2858-fm_backend_herdr_pane_prime_agent_foregroundreturns 1 (a positive "prime-agent is NOT in the foreground" claim) when jq errors on an unexpectedforeground_processeselement shape, contradicting the three-outcome contract documented directly above it, which reserves 2 for "the read itself is unusable (RPC failure, unexpected shape, missing field). Never treat this as evidence either way." The firstjq -evalidates only thatforeground_processesis an array, not its element shapes; the second one dies on a malformed element. Reproduced against the shipped expression:echo '{"result":{"process_info":{"pane_id":"p","foreground_processes":[{"name":"bash","argv":"bash"}]}}}' | jq -e '...'printsCannot index string with numberand exits 5, so|| return 1turns a failed read into evidence.fm_backend_herdr_pane_agent_state(line 1929) then falls through to the subtree probe instead of keeping the registeredliveverdict. Impact is bounded - the ps-based subtree probe must independently prove absence before the pane is demoted tono-agent, and the composer arm accepts only 0 - so this is a contract gap rather than a live wedge. Fix: fold the per-element field validation into the firstjq -e, or wrap the predicate intry ... catch, so an unparseable element answers 2.bin/backends/herdr.sh:2884-fm_backend_herdr_pane_prime_agent_in_subtreeissues a secondpane process-infoRPC for a payload its only liveness caller fetched one line earlier, and it consumes just one field (shell_pid) that the first response already carries. Infm_backend_herdr_pane_agent_state(lines 1927-1930)..._foregroundrunspane process-info, and when it returns 1 the next call runs the identical RPC again, so every prime-agent pane whose agent is not in the foreground costs two round-trips per classification - paid per pane per pass by the bootstrap secondmate-liveness sweep. The new fixture has to stage the same canned response twice (resp/3.outandresp/4.out) to model it. Takingshell_pidas an argument, or having the foreground probe publish it, would halve the RPCs and close the split-read window between the two calls. Not a defect: a disagreement across the gap only ever makes the verdict more conservative.✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh tests/fm-prime-agent-harness.test.sh tests/fm-prime-watch-extension.test.sh tests/fm-session-lock-ancestry.test.sh tests/fm-pi-primary-types.test.sh— 3 pass, pi-primary-types gate-skips (skip: tsc not found)bin/fm-test-run.sh tests/fm-backend-herdr.test.sh— pass, incl. the new prime composer-shape casesLive: launched a realprime-agent --cwd <tmp>session, killed its client, confirmed the daemon worker survived, then. bin/fm-prime-agent-lib.sh; fm_prime_agent_stop_sessions_under <tmp>— only that session stopped, two pre-existing sessions untouchedLive: ran basebin/fm-harness.sh(833a9a2) and targetbin/fm-harness.shunder the real environ of live prime-agent worker pid 2210194 (CLAUDECODE=1+PI_CODING_AGENT=true+PRIME_AGENT_INTERNAL_DAEMON_WORKER=1) — base printsclaude, target printsprime-agentLive:fm_prime_agent_worker_abandonedagainst the realprime-agent list --json— attached worker keeps the lock, zero-client idle worker reclaimable, unknown pid keeps the lockLive: isolatedbin/fm-herdr-lab.sh provision/run/teardownsession, realprime-agent --offlineTUI in panew1:p1—fm_backend_herdr_composer_statereturnsempty(base returnsunknown),fm_backend_herdr_pane_agent_statereturnsliveLive: sent/quitthenPS1='> 'into the same pane —herdr agent getstill reportsagent: prime-agent, yetfm_backend_herdr_composer_state=unknownandfm_backend_herdr_pane_agent_state=no-agentCleanup: lab session torn down, temp session stopped, temp dirs removed,git status --porcelainclean🔧 **Document** - 3 issues found → auto-fixed ✅
docs/subagent-guard.md:187- docs/subagent-guard.md now carries a prime-agent row marked "not reviewed" because its delegation-tool surface cannot be enumerated without a live prime-agent session (the repo's own rule forbids wiring a harness hook that was not validated against the real binary). Its detached-worker lib comments reference RLM child sessions, so a delegation surface plausibly exists. Enumerate its tools on a host with the binary and wire or rule out the guard.docs/verification/supervision.md:152- All prime-agent verification evidence (2026-08-08, 0.7.1) lives in the agent-runtime harness-adapters skill; docs/verification/supervision.md gained no prime-agent rows in its busy-state, session-start, or turn-end evidence tables. Per docs/documentation-audiences.md, dated maintainer-verification evidence is owned by that file. I did not transcribe evidence I did not run. A follow-up should move or restate the dated live records there.docs/scripts.md:8- docs/scripts.md does not list the new bin/fm-prime-agent-lib.sh, but it already omits 27 other tracked bin scripts (fm-lint.sh, fm-transition-lib.sh, fm-procevent*.sh, the fm-remote-* family, and more), so the inventory is not exhaustive and this change did not make it stale. Restoring or explicitly bounding that inventory is an out-of-scope consolidation worth its own change.🔧 Fix: rule out delegation guard for prime-agent harness
✅ Re-checked - no issues remain.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.