Skip to content

Latest commit

 

History

History
1592 lines (1192 loc) · 112 KB

File metadata and controls

1592 lines (1192 loc) · 112 KB

Runtime backend verification

Audience: maintainer verification.

This record contains reusable version-scoped evidence for active runtime guarantees. The backend guides own current setup, safety boundaries, and limitations. Exact task chronology, branch names, temporary homes, local paths, process ids, thread ids, and delivery transcripts remain in private reports or PR evidence.

tmux

Foreground-process behavior was verified on 2026-07-07 with tmux 3.6a on macOS.

tmux new-session -d -s fmtest -n testwin
tmux display-message -p -t fmtest:testwin '#{pane_current_command}'
tmux send-keys -t fmtest:testwin 'sleep 30' Enter
tmux display-message -p -t fmtest:testwin '#{pane_current_command}'
tmux send-keys -t fmtest:testwin C-c
tmux display-message -p -t fmtest:testwin '#{pane_current_command}'

Observed output:

zsh
sleep
zsh

A persistent parent shell waiting for a child remained reported as the parent process, while a shell that directly execed a simple command changed identity with the process itself. Pi and pi-signed 0.82.0 were reverified on 2026-07-27 through real isolated fm-spawn.sh launches.

Agent liveness name sources

The earlier record that every harness is observed under its own #{pane_current_command} no longer holds and has been replaced by the per-harness evidence below. In this macOS run that reading reflected a rewritable process title rather than stable executable identity, so it is now one of two independent name sources rather than the sole basis of a verdict.

The seven primary-capable adapters were relaunched on 2026-08-03 with tmux 3.6a on macOS 26.5.2 arm64, each on a private socket in an isolated lab.

tmux -L "$socket" new-window -d -t "$session:" -n "$harness" -c "$wt" -- "$bin"
tmux -L "$socket" display-message -p -t "$session:$harness" '#{pane_current_command}'
ps -t "${tty#/dev/}" -o pgid=,tpgid=,comm=      # rows where pgid = tpgid

Observed identities, and the resulting verdict:

Harness Version #{pane_current_command} Foreground comm Verdict
claude 2.1.220 2.1.220 claude alive
codex codex-cli 0.146.0 codex codex alive
opencode 1.18.11 opencode opencode alive
pi 0.82.0 pi-launcher pi-signed, pi alive
pi-signed 0.82.0 pi-launcher pi-signed, pi alive
grok 0.2.118 grok-0.2.118-ma grok alive
kimi 0.31.1 kimi kimi alive

In that 2026-08-03 seven-adapter run, Claude Code was the only harness whose title did not attribute it; every other adapter was attributed by both sources. Codex reported codex-aarch64-a at 0.145.0 and codex at 0.146.0, and Kimi Code reported kimi-code as its foreground comm at 0.29.1 and kimi at 0.31.1, so these identities move between ordinary patch releases in both directions. That is the evidence for treating any single process name as a surface under vendor control rather than a stable contract.

The crewmate-only Muse Code 0.1.0-R708.1 adapter was verified separately on 2026-08-05 against tmux on macOS arm64. Its installed muse-bin-0.1.0-R708.1 foreground identity classified alive, while musescore, amuse, muse-binary, and muse-bind remained ambiguous in the portable regression. muse.md owns the artifact identity and launcher evidence for that verification.

The crewmate/scout-only Rovo CLI 202609.1.2 adapter added *rovo* to the same glob family as *grok*/*kimi* in fm_backend_tmux_classify_process_name, and was relaunched live under tmux 3.6a in an isolated private socket. #{pane_current_command} reported the truncated on-disk binary name atlassian_cli_r - macOS's 15-char comm truncation cuts atlassian_cli_rovodev off just before the rovo substring begins, the same truncation-volatility class codex/kimi's own patch-release name drift shows above - while the foreground ps-based comm correctly reported rovo, so fm_backend_tmux_agent_state returned alive through that primary source; the two-independent-name-sources design is exactly why the truncated title does not break the verdict. rovo.md owns the fuller record, including the busy/interrupt/exit facts captured in that same live tmux session and the herdr agent-detection gap found when herdr placement was verified live in an isolated lab session.

Bounded observed output:

foreground comms:
  zsh
  .../instbin/muse-bin-0.1.0-R708.1
classify each:
  zsh                            -> shell
  muse-bin-0.1.0-R708.1          -> agent
fm_backend_agent_state tmux museliv:zsh
alive

#{pane_current_command} and foreground ps -o comm= read different name fields, but which one preserves executable identity is platform-dependent. On macOS the pane command reflected the rewritable title while the full install path could survive in ps -o comm=; in the Linux portable regression those roles reversed for the version-named native executable, with the identifying path retained in argv[0]. The classifier therefore accepts a harness basename first, then an exact harness path component in the full executable path, then the same component in argv[0], without depending on which field carries it on a given platform.

The portable regression is CI-enforced. The real-harness drift guard spends no model tokens, so under the policy in .agents/skills/firstmate-coding-guidelines/SKILL.md it runs by default wherever tmux is installed and reports a capability skip elsewhere; FM_HARNESS_LIVENESS_DRIFT=1 additionally turns an absent tool into a failure. Run the live guard after any harness upgrade and before trusting or refreshing the table above:

FM_HARNESS_LIVENESS_DRIFT=1 bin/fm-test-run.sh tests/fm-harness-liveness-drift-live-e2e.test.sh

2026-09-06 default-on drift refresh, and the Cursor editor CLI collision

Running the guard with no variable set on macOS 26.5.2 arm64 checked 8 installed harnesses and classified every one alive:

# claude 2.1.263 (Claude Code): title='2.1.263' foreground=[/Users/kunchen/.local/bin/claude <defunct> <defunct> ]
# codex codex-cli 0.147.0: title='codex' foreground=[/opt/homebrew/bin/codex ]
# opencode 1.18.29: title='opencode' foreground=[/opt/homebrew/bin/opencode ]
# pi 0.84.4: title='pi-launcher' foreground=[/opt/homebrew/bin/pi-signed .../pi ]
# pi-signed 0.84.4: title='pi-launcher' foreground=[/opt/homebrew/bin/pi-signed .../pi ]
# grok grok 1.0.13 (5e9a58528b76) [stable]: title='grok-1.0.13-mac' foreground=[/Users/kunchen/.local/bin/grok ]
# cursor 2026.09.02-c22c1a3: title='node' foreground=[/Users/kunchen/.local/bin/cursor-agent ]
# muse Muse Code 1.0.3 (1.0.3-R2198.1): title='muse-bin-1.0.3-' foreground=[/Users/kunchen/.local/bin/muse-bin-1.0.3-R2198.1 ]
# checked 8 installed harness(es)

The first default-on run failed on Cursor with LIVENESS DRIFT: cursor unknown is running but classifies 'missing', observed title zsh. The classifier was not at fault: the guard resolved the harness through a generic command -v cursor, which on a machine that also has the Cursor editor finds ~/.local/bin/cursor - the editor launcher, not the agent. That binary exits immediately, leaving a bare shell in the pane. The guard now asks fm_cursor_resolve_binary first for cursor, which is the same verified owner bin/fm-spawn.sh uses, so the probe launches cursor-agent and the editor CLI can no longer masquerade as the harness.

Bounded output from the 2026-08-03 run that produced the first table above:

ok - harness liveness: claude 2.1.220 (Claude Code) classifies alive
# claude 2.1.220 (Claude Code): title='2.1.220' foreground=[claude ]
# checked 7 installed harness(es)

Installed-wrapper checks:

basename "$(command -v pi-signed)"
pi-signed --version
pi --version

Observed bounded output:

pi-signed
0.82.0
0.82.0

Harness-adapter instruction routing

Two checks keep the evidence boundaries separate. tests/fm-harness-adapter-references.test.sh parses the router's declared JSON contract as normalized data and proves every selected reference is readable, which is structural evidence only. tests/fm-harness-adapter-instructions-live-e2e.test.sh is an opt-in development check that sends the directly loaded router and every operation scenario across all nine harness identities to a local Ollama model, requires the generated plan as normalized JSON, and makes no external-provider call.

FM_HARNESS_ADAPTER_INSTRUCTION_EVAL=1 FM_HARNESS_ADAPTER_LOCAL_MODEL=ambient-router-gemma4:e4b bin/fm-test-run.sh tests/fm-harness-adapter-instructions-live-e2e.test.sh

That local evaluation demonstrates instruction-driven scenario selection, but it does not claim that a native harness loaded the selected files. The guard prints the exact installed version or unavailable status for every native harness so absent tools and unexercised provider transports remain explicit rather than becoming passes. Native loader behavior still requires the applicable live agent-tool check; no uniform deterministic zero-provider transport currently spans Claude, Codex, OpenCode, and Pi, and the other five tools remain unavailable where their binaries are absent.

Bounded output from the 2026-08-29 local run:

ok - local model ambient-router-gemma4:e4b selected every operation scenario and all nine harness identities
# native loader not claimed: claude 2.1.220 (Claude Code) is installed, but this harness-neutral evaluation does not exercise its provider transport
# native loader not claimed: codex 0.147.0-alpha.6+local.4 is installed, but this harness-neutral evaluation does not exercise its provider transport
# native loader not claimed: opencode 1.14.48 is installed, but this harness-neutral evaluation does not exercise its provider transport
# native loader not claimed: pi 0.84.0 is installed, but this harness-neutral evaluation does not exercise its provider transport
# unverified native loader: pi-signed is not installed on this machine
# unverified native loader: grok is not installed on this machine
# unverified native loader: kimi is not installed on this machine
# unverified native loader: cursor is not installed on this machine
# unverified native loader: muse is not installed on this machine
# installed native tools recorded without overstating loader coverage: 4
# unavailable native tools: pi-signed grok kimi cursor muse

The isolated process and endpoint checks used:

tmux display-message -p -t "$target" '#{pane_current_command}'
ps -o comm= -p "$wrapper_pid"
ps -o comm= -p "$engine_pid"
FM_HOME="$fixture_home" bin/fm-crew-state.sh "$task_id"

Observed bounded shapes:

pi-launcher
.../pi-signed
.../Pi Launcher.app/Contents/Resources/pi/pi
state: done ...

Both launches executed a submitted tool instruction and touched the generated turn_end marker. The pi-signed launch retained harness=pi-signed, while the plain comparison retained harness=pi. The exact wrapper ancestry was pi-signed parent to Pi engine child, and the plain Pi Launcher path also traversed the signed wrapper on this installation. That shared plain-Pi path is retained as disconfirming evidence against using ancestry as runtime-selection authority. Firstmate therefore sets the exact FM_PI_HARNESS selection marker on both worker launch paths, while an unmarked Pi-family process remains pi. Both recorded runtime identities now classify the exact pi-launcher foreground command as alive.

Backend applicability was reviewed across every spawn adapter. Tmux needs the exact pi-launcher, pi-signed, pi, and Pi process identities for recovery-grade liveness. Herdr uses native registered-agent state and needs no process-name branch. Zellij has no verified recovery-grade agent process probe, while Orca and cmux do not support secondmate spawns, so those three retain their existing generic ordinary-launch semantics without a new liveness matcher.

The current classifier matrix and its refresh guard are recorded in Composer classification matrix, with portable shape coverage in tests/fm-composer-lib.test.sh and tests/fm-composer-ghost.test.sh. Kimi pointer delivery and OpenCode 1.18.4 busy-queue behavior remain pinned by tests/fm-kimi-harness.test.sh, tests/fm-tmux-submit-busy.test.sh, and tests/fm-composer-lib.test.sh. Herdr's Claude idle-native submit confirmation is pinned by tests/fm-backend-herdr.test.sh and refreshed by FM_HERDR_SUBMIT_CONFIRM_LIVE=1 tests/fm-herdr-submit-confirm-live-e2e.test.sh.

Cleanup endpoint identity

The cleanup identity boundary was validated on 2026-07-28 with tmux 3.6a and metadata fixtures for every supported backend.

tests/fm-teardown-endpoint-safety.test.sh
tests/fm-teardown.test.sh
tests/fm-backend-herdr.test.sh
tests/fm-backend-zellij.test.sh
tests/fm-backend-orca.test.sh
tests/fm-backend-cmux.test.sh

Bounded output from the incident regression:

ok - fm-teardown: missing, empty, malformed, ambiguous, and task-mismatched endpoints refuse before every mutation or runtime call
ok - cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses
ok - tmux backend: direct empty target returns nonzero without invoking tmux
ok - process cleanup: creation-time PID identity removes only the exact child and preserves the control child
ok - fm-teardown: dedicated-socket invalid cleanup preserves target/control and valid cleanup removes only the exact target

The dedicated tmux cell removed ambient tmux variables, required a socket-bound wrapper, kept one target and one independent control window, and proved the wrapper was not called for invalid metadata or a direct empty target. Valid cleanup removed only the exact task-bound target and left the control window live. The metadata-only validation covers tmux, Herdr, Zellij, Orca, and cmux before backend dispatch. Claude, Codex, OpenCode, Pi, pi-signed, Grok, Kimi, Cursor, and Muse share that backend cleanup boundary; their harness-specific hook files, tokens, transcript bindings, and session-log sidecars are cleaned only after it, so no harness needs a separate endpoint parser.

Claude workspace trust

Verified 2026-09-03 on Claude Code 2.1.259. Claude gates a folder it has never seen behind an interactive workspace-trust dialog, and the CLI documents the only bypass as non-interactive mode, which a crewmate pane is not.

claude --version
claude --help | grep -A 5 'workspace trust dialog'
2.1.259 (Claude Code)
                                        pipes). Note: The workspace trust dialog
                                        is skipped when Claude is run in
                                        non-interactive mode (via -p, or when
                                        stdout is not a TTY, e.g. piped or
                                        redirected output). Only use this in
                                        directories you trust. Settings files

--dangerously-skip-permissions is a permission control and is absent from that bypass, so an interactive worker in a fresh worktree still reaches the dialog. Firstmate cannot answer it either, because its key plane carries only Enter, Escape, and C-c with no arrow navigation. Suppression itself was then observed directly on the same date and version, with a control arm and a treatment arm.

The control arm launched a fresh linked worktree with no pre-registration, the way bin/fm-spawn.sh launches one.

tmux new-session -d -s tp-a -c /tmp/trustproof/wt-a \
  "CLAUDE_CONFIG_DIR=<cfg> CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions '<brief>'"
Accessing workspace: /tmp/trustproof/wt-a
Quick safety check: Is this a project you created or one you trust? ...
Claude Code'll be able to read, edit, and execute files here.
> No, exit
  Yes, I trust this folder
Enter to confirm . Esc to cancel

That pane confirms two load-bearing claims at once: the dialog fires despite --dangerously-skip-permissions, and the selection cursor sits on No, exit, so a sent Enter would have exited the worker.

The treatment arm pre-registered an equivalent fresh worktree and launched it identically against the operator's real config.

bin/fm-claude-trust.sh /tmp/trustproof/wt-c /tmp/trustproof/proj
tmux new-session -d -s tp-c -c /tmp/trustproof/wt-c \
  "CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions 'reply with exactly: BRIEF-REACHED'"
trusted: /tmp/trustproof/wt-c
Claude Code v2.1.259 ... /tmp/trustproof/wt-c
> reply with exactly: BRIEF-REACHED
. BRIEF-REACHED

No dialog appeared and the worker executed its brief with zero keypresses. The scratch repo was deleted and the test entries were removed from the store and verified absent. That verification is point-in-time rather than a durable guarantee, because a concurrent Claude session can re-add a path it visited: one entry reappeared after an earlier zero-residual check, most plausibly flushed by a session as it exited, and was removed again.

One limitation belongs beside that result. An intermediate arm run against an isolated CLAUDE_CONFIG_DIR holding only a copied .claude.json cleared the trust dialog but then surfaced the separate machine-scoped Bypass Permissions warning. That warning rendered in the same shape as the trust dialog, with the selection cursor on No, exit and the footer Enter to confirm . Esc to cancel, so a sent Enter would end that worker too. That gate is not a production blocker, because a normal environment has already accepted it and the treatment arm above ran against the real config and saw neither dialog. This change does not address that warning and does not claim to.

bin/fm-spawn.sh therefore pre-registers the task worktree through bin/fm-claude-trust.sh before launch, and tests/fm-claude-trust.test.sh pins both halves of the scope contract: a fresh worktree is trusted, and an out-of-scope path is refused. That automated spawn case runs against a fake claude, so it asserts the store entry and the launch command and nothing more; the live arms above are what establish that the entry actually suppresses the dialog. The composer-classification record below observes the same gate from the other side, where an untrusted worktree left Claude, Grok, and Muse unverified because the guard reads a first-launch trust dialog as an unreadable composer.

Composer classification matrix

The shared composer classifier (bin/fm-composer-lib.sh, fm_composer_classify_screen) owns every composer shape fleet-wide; each backend contributes only a capture and a capability descriptor. The live half of that guarantee was verified on 2026-08-10 from an already-trusted checkout at the branch's final validated head, against every installed harness then covered by the empty-composer matrix on tmux 3.6a, macOS arm64, on an isolated private socket, with no prompt submitted to any harness. An earlier untrusted-worktree run left Claude, Grok, and Muse unverified because the guard treats first-launch trust dialogs as an unreadable-composer state and never confirms them; this trusted-checkout rerun supersedes those missing results.

FM_COMPOSER_MATRIX_LIVE=1 tests/fm-composer-matrix-live-e2e.test.sh

Observed output:

ok - claude (2.1.227 (Claude Code)): real idle composer classifies empty
ok - codex (codex-cli 0.146.0): real idle composer classifies empty
ok - opencode (1.14.46): real idle composer classifies empty
ok - pi (0.84.0): real idle composer classifies empty
ok - grok (grok 1.0.0 (3cd0d0cbcebe)): real idle composer classifies empty
# harness absent, not verified here: kimi
ok - muse (Muse Code 0.1.0 (0.1.0-R708.1)): real idle composer classifies empty
ok - strict posture live: a blank shell row classifies unknown and injection defers
ok - zellij (zellij 0.44.0): unrelated pane change never confirms delivery (verdict: unknown)
ok - live composer-matrix guard verified 8 live surface(s)

All six installed harnesses' real idle composers reached a proven empty (Claude auto-updated to 2.1.227 between the audit and this rerun, so the shipped classifier is proven against the newer release as well), including Pi through the tmux foreground-process identity probe, Grok through the titled-bottom-border tolerance, and OpenCode through the left-bar shape; Codex and OpenCode first parked on vendor update-available modals that the strict classifier correctly refused until the guard's single non-submitting Escape dismissed them. The strict blank-row posture held live (a blank shell row deferred injection), and a zellij pane changing for reasons unrelated to submission never confirmed a delivery, replacing the retired content-diff heuristic's false positive. Kimi was not installed on the verification machine; its bordered shape is pinned by the portable byte-capture regressions in tests/fm-composer-lib.test.sh, which also carry the other five adapters' capability profiles for every harness under both a UTF-8 locale and LC_ALL=C. This guard is the refresh command after an upgrade to any matrix-covered harness; rerun it and update the versions above rather than trusting this table across releases. Known staleness: on 2026-08-23 the steering-inbox doorbell run observed grok 1.0.5's idle composer classifying unknown (and sometimes pending-family), never empty, so the grok row above is stale for 1.0.5 and owes a refresh; steering is unaffected because the send path's composer check is advisory, but empty-requiring consumers (away-daemon injection, spawn readiness) should not trust the 1.0.0 grok result. Cursor is deliberately outside this cursor-anchored empty-composer matrix because its terminal cursor is parked outside the composer; tmux's Cursor-specific, process-identity-gated cursorless fallback is covered by the Cursor Agent CLI section's separate live evidence and drift guard.

zellij action dump-screen --pane-id <id> --ansi was verified at zellij 0.44.0 to preserve ANSI styling (real Claude Code rendered inside a zellij pane dumped ESC[m U+00A0 for its idle composer row), which is the capability the zellij composer classifier reads.

Steering-inbox doorbell

The steering channel's one behavioral assumption - a real worker agent follows the constant self-describing doorbell line (list the inbox, read and act on its records in numeric order, then mv each into handled/) - was verified on 2026-08-23 against every installed verified harness, on tmux 3.6a, macOS arm64, on an isolated private socket, driving the REAL bin/fm-send.sh end to end (durable record plus doorbell, with one mid-wait re-ring playing the watcher's role).

FM_SEND_INBOX_LIVE_E2E=1 tests/fm-send-inbox-doorbell-live-e2e.test.sh

Observed output (combined across the full run and the grok rerun after the advisory-skip narrowing landed):

ok - claude (2.1.241 (Claude Code)): the doorbell reached a real worker, which acted and acked with the mv
ok - codex (codex-cli 0.147.0): the doorbell reached a real worker, which acted and acked with the mv
ok - opencode (1.18.21): the doorbell reached a real worker, which acted and acked with the mv
ok - pi (0.84.1): the doorbell reached a real worker, which acted and acked with the mv
# grok (grok 1.0.5 (5115b46bc909) [stable]): idle composer never classified empty; proceeding as production does (advisory check skips only on pending)
ok - grok (grok 1.0.5 (5115b46bc909) [stable]): the doorbell reached a real worker, which acted and acked with the mv
# harness absent, not verified here: kimi
ok - muse (Muse Code 0.2.1 (0.2.1-R1215.1)): the doorbell reached a real worker, which acted and acked with the mv

All six installed harnesses honored the doorbell contract with real model turns: each listed the inbox named by the doorbell, read its record, executed the instruction inside it, and acknowledged with the atomic mv. Two findings from the run shaped the shipped behavior: an OpenCode vendor update modal swallowed the first doorbell and the single re-ring recovered it, which is exactly the watcher ladder's job; and grok 1.0.5's idle composer never classifies empty (a classifier drift owned by the Composer classification matrix guard, whose refresh for grok 1.0.5 is still owed), which is why the ring's advisory pre-check skips only on an exact proven pending verdict - a doorbell into an ambiguous composer is a recoverable constant line, while skipping on ambiguity would starve steering for any harness the classifier cannot positively identify. Kimi was not installed on the verification machine; its receive path is the same one-line-plus-shell contract, and the portable ladder and enqueue regressions in tests/fm-task-inbox.test.sh and tests/fm-send-inbox.test.sh cover every harness-independent half. This guard is the refresh command after any harness upgrade; it spends a small number of real tokens per installed harness, reports an absent harness explicitly, and refuses a run that verified nothing.

Gemini

The Gemini crewmate adapter was verified on 2026-09-04 with gemini-cli 0.58.0 on Linux, Node v24.20.0, tmux 3.4. Every check below ran in throwaway scratch worktrees against the real CLI; no task worktree was used and no agent was left running. The credential was supplied only through the GEMINI_API_KEY environment variable and its value appears nowhere in this record.

Trust options are not equivalent

The refusal an untrusted launch produces, and its headless exit status:

gemini -p 'say OK'; echo "rc=$?"
Gemini CLI is not running in a trusted directory. To proceed, either use `--skip-trust`, set the `GEMINI_CLI_TRUST_WORKSPACE=true` environment variable, or trust this directory in interactive mode.
rc=55

Both documented options clear that refusal, but only one loads project configuration. The A/B below ran twice in ONE worktree carrying a project AfterAgent hook, with the same config home and the same prompt, changing only the trust mechanism:

--skip-trust                 project-hook-fired=NO   untrusted-warnings=0   turn=✦ ECHO
TRUST_WORKSPACE=true         project-hook-fired=YES  untrusted-warnings=0   turn=✦ FOXTROT

gemini skills list names the cause directly in the untrusted case:

Skipping project agents due to untrusted folder. To enable, ensure that the project root is trusted.
Project hooks disabled because the folder is not trusted.

This is why bin/fm-spawn.sh launches with GEMINI_CLI_TRUST_WORKSPACE=true and why --skip-trust must not be substituted for it.

Busy signal

A full turn was captured every two seconds. The status row above the separator carries the one ASCII token, and the phase text beside it is model-generated:

busy_1 | 1 |  ⠸ Thinking... (esc to cancel, 1s)
busy_5 | 1 |  ⠸ Begin Counting Methodically (esc to cancel, 9s)
busy_6 | 1 |  ⠇ Continue Enumerating Concepts (esc to cancel, 11s)
busy_7 | 0 |
busy_12 | 0 |

The idle capture taken before the prompt also contained no (esc to cancel,. Because the phase text varies per turn and the spinner is braille, neither is usable; (esc to cancel, is the only stable rendered token, and the adapter uses the semantic hooks below as its actual state source.

Hook lifecycle

BeforeAgent, AfterAgent, SessionStart, and SessionEnd were registered on one probe that appends its event name, then driven through a normal turn, an Escape interrupt, and /quit:

--- after startup ---   SessionStart
--- mid-turn ---        SessionStart, BeforeAgent
--- after INTERRUPT --- SessionStart, BeforeAgent, AfterAgent
--- after /quit ---     SessionStart, BeforeAgent, AfterAgent, SessionEnd, SessionEnd

Two facts the adapter depends on come from that run: AfterAgent closes a turn that was CANCELLED, not only one that completed, and SessionEnd fired TWICE for a single /quit, so the repeated idle event must be harmless. The AfterAgent payload carried the worktree as cwd, which is what binds a hook to its task:

keys: ['cwd', 'hook_event_name', 'prompt', 'prompt_response', 'session_id', 'stop_hook_active', 'timestamp', 'transcript_path']
hook_event_name = AfterAgent
stop_hook_active = False
prompt = 'Say the single word CEDAR and stop.'

On the cancelled turn the same payload carried prompt_response = '[no response text]'.

Autonomous end-to-end worker

One throwaway worker was launched exactly as the spawn launches one - positional brief, -y, workspace trust - and observed from start to idle:

t=5s   (esc to cancel, 2s)
t=30s  busy marker present
idle at ~34s, busy marker count 0
worker-output.txt: DELIVERED
turn-end hook fires: 1

Its pane showed ✓ WriteFile worker-output.txt → Accepted (+1, -0) with no approval gate, confirming -y runs unattended, and Executing Hook: fm-turn-end in the status row after the turn.

The adapter was then driven through the REAL bin/fm-spawn.sh and bin/fm-control.sh against a real Gemini pane in an isolated home:

spawned gm-e2e harness=gemini kind=ship mode=no-mistakes yolo=off window=... worktree=...
hooks installed by spawn (state/<id>.gemini-settings.json): ['BeforeAgent', 'AfterAgent', 'SessionEnd']
t=4s   state: working · source: pane · harness busy (gemini-hook)
t=8s   state: working · source: pane · harness busy (gemini-hook)
after turn: v1 gen=... state=idle source=gemini-hook event=after-agent
e2e-output.txt: SEAWORTHY
interrupt-delivered gm-e2e harness=gemini backend=tmux verified=agent-alive cancel=unconfirmed
after interrupt: v1 gen=... state=idle source=gemini-hook event=after-agent
stopped gm-e2e harness=gemini backend=tmux endpoint=... worktree=...

The interrupt line is the one worth keeping: AfterAgent closed the record on a CANCELLED turn, which is why a gemini interrupt needs no fm-interrupt fallback event. A single Escape on a long turn printed ℹ Request cancelled., dropped the busy token, and left the agent running; /quit then exited with status 0 and printed To resume this session: gemini --resume <session-id>, and resuming by that id restored the full transcript.

Identity markers

Env var NAMES were read from a real Gemini tool process launched under a Claude primary; no value of GEMINI_API_KEY was read.

GEMINI_CLI=[1]
AI_AGENT=[claude-code_2-1-260_agent]
TRUSTWS=[true]
CLAUDECODE=[1]

GEMINI_CLI is unset in the launching environment, so it is Gemini's own; CLAUDECODE is inherited, which is why bin/fm-harness.sh tests GEMINI_CLI first. AI_AGENT carried the CLAUDE primary's value and is therefore an inherited launcher marker, never a Gemini identity.

Ancestry cannot substitute for the marker on this platform:

node -e 'const{execSync}=require("child_process");console.log(execSync("ps -o comm= -p "+process.pid).toString().trim())'
MainThread

The shipped CLI is a node bundle, so its live process never presents as node and neither ancestry arm matches it.

The same shape breaks pane liveness, which the end-to-end run surfaced as a hard refusal rather than a silent wrong answer:

error: task gm-e2e's endpoint reads 'ambiguous' rather than a positively classified state; refusing to send a lifecycle key into an unattributed endpoint

A live gemini pane's foreground group read comm=MainThread and argv0=/home/<user>/.local/node/bin/node, so bin/fm-gemini-lib.sh now identifies it from argv[1] instead. After that change the same endpoint classified alive while the worker ran and dead once it exited, so the rule is not simply always positive. tests/fm-gemini-harness.test.sh pins that boundary so it is not later documented away, and tests/fm-busy-adapter-wiring.test.sh drives the generated hooks through the real writer and classifier.

bin/fm-test-run.sh tests/fm-gemini-harness.test.sh tests/fm-busy-adapter-wiring.test.sh

Credential wedge and its blast radius

With no resolvable credential the pane wedges on Enter Gemini API Key rather than failing. That dialog is a credential FIELD, so lifecycle text sent to a wedged pane is submitted into it and stored. Observed after an ordinary exit was delivered to such a pane: a ~/.gemini/gemini-credentials.json (mode 0600) appeared that had not existed before, and a later credential-less run stopped refusing cleanly and instead reached the API:

before: rc=41  When using Gemini API, you must specify the GEMINI_API_KEY environment variable.
after : rc=1   API key not valid. Please pass a valid API key.  (API_KEY_INVALID)

Clearing that stored credential restored both behaviours:

GEMINI_API_KEY=<from the operator's own store> gemini --skip-trust -p 'Reply with exactly the word NOVEMBER.'  ->  NOVEMBER  rc=0
env -u GEMINI_API_KEY gemini --skip-trust -p hi                                                              ->  rc=41

The adapter reference records the operational rule this produces: never drive lifecycle text into a gemini pane showing that dialog; treat it as a credential blocker and retire the endpoint instead. The credential must also be present before the session-provider daemon starts, since a long-lived tmux or Herdr server hands panes the environment it was started with.

Settings placement

Firstmate's hooks are NOT written into the worktree's .gemini/settings.json, because unlike Claude's settings.local.json that path is the project's own committed settings file. They go to a firstmate-owned state/<id>.gemini-settings.json reached through GEMINI_CLI_SYSTEM_SETTINGS_PATH. Two measurements support that choice. Hooks from the system layer fired under --skip-trust in an untrusted folder, so the busy contract does not depend on the trust decision:

=== events (UNTRUSTED workspace, --skip-trust) ===
BeforeAgent
AfterAgent

And hook arrays MERGE across layers rather than overriding, so a project's own hooks keep running alongside firstmate's:

=== which AfterAgent hooks ran (trusted workspace, both layers define AfterAgent) ===
PROJECT
SYSTEM

A second end-to-end spawn against a project that already committed its own .gemini/settings.json confirmed the file was untouched, that git status reported only the worker's own new output file, and that teardown removed firstmate's settings file:

t=4s   state: working · source: pane · harness busy (gemini-hook)
t=8s   state: working · source: pane · harness busy (gemini-hook)
after turn: state=idle source=gemini-hook event=after-agent
e2e2-output.txt: ANCHOR
project .gemini/settings.json: {"context":{"fileName":"GEMINI.md"}}   (unchanged)
interrupt-delivered gm2 harness=gemini backend=tmux verified=agent-alive cancel=unconfirmed
stopped gm2 harness=gemini backend=tmux
teardown gm2 complete; state/gm2.gemini-settings.json removed

Not verified

Gemini as a PRIMARY or SECONDMATE runtime is unverified and is refused by bin/fm-spawn.sh: no wake protocol exists under docs/supervision-protocols/ and no turn-end guard adapter was built or exercised. No reasoning-effort axis was found; gemini --help on 0.58.0 exposes no effort, reasoning, or thinking flag, so the record-and-omit contract applies.

Herdr

The compatibility floor is protocol 14. The whole real-Herdr lane's latest active verification uses both Herdr 0.7.4 protocol 16 and Herdr 0.8.0 protocol 19 on macOS aarch64, while focused Herdr 0.7.5 protocol 17, earlier protocol-16, protocol-14, and 0.7.3 evidence is retained where it defines current behavior or fallbacks. Protocol 17 keeps every protocol-16 feature gate satisfied; the event and workspace-move floors remain 16. Default-on presentation projection has its own floor at Herdr 0.8.0, protocol 19, verified below.

Core read-only probes:

herdr --version
herdr status --json | jq -c '{client:.client.protocol,server:.server.protocol}'
herdr api schema --json | jq -c '.schemas.subscription_event["$defs"].SubscriptionEventKind.enum'

Observed protocol-16 compatibility shapes:

herdr 0.7.5
{"client":17,"server":17}
["pane.output_matched","pane.agent_status_changed","pane.scroll_changed"]

The CLI matrix was checked directly:

Guarantee Command shape Result
Explicit session routing herdr <verb> ... --session <name> Reached the named session even while another server was running.
Literal send herdr pane send-text <pane> <text> --session <name> Left text unsubmitted until Enter.
Keys `herdr pane send-keys enter escape
Capture herdr pane read <pane> --source recent --lines N Small N could return empty below viewport height; a 200-line request plus local trim was stable.
Native state herdr agent get <pane> Working and done transitions were visible on some harnesses; live Claude Code 2.1.236 on Herdr 0.8.0 kept agent_status=idle for an entire landed turn, including a multi-second tool call, so submit confirmation falls through to the shared composer verdict. Native busy remains positive activity evidence, while native idle cannot close a turn and the adapter's semantic lifecycle decides worker state.
Restart guarded named-session stop then start Workspace, tab, pane, and labels persisted; the agent process and registration did not.
Close herdr pane close <pane> --session <name> The exact one-pane task tab closed; closing a final tab could remove the workspace.

All destructive verification used bin/fm-herdr-lab.sh with a non-default fm-lab- name and a byte-identical default-session tripwire. No ambient herdr server stop command is a supported test operation.

fm-remote server birth and login-keychain access

Measured 2026-09-09 on macOS 26 (Darwin 25.6.0) aarch64 with Claude Code 2.1.266 and Herdr 0.9.0, the guarantee behind bin/fm-remote-herdr-guard.sh and the doctor's herdr-server check: login-keychain access follows the audit session a process was born into, never the launch shape or the shell.

Same user, same HOME, same login keychain item, three births, probed with launchctl managername, getaudit_addr (a compiled probe), security find-generic-password -a "$USER" -w -s "Claude Code-credentials" (output withheld), and claude auth status:

Birth managername audit session security ... -w claude auth status
gui/501 LaunchAgent, bare ProgramArguments, launchctl bootstrap + kickstart -k mid-session Aqua asid 100038 (the gui/501 asid), HAS_GRAPHIC_ACCESS HAS_TTY HAS_CONSOLE_ACCESS HAS_AUTHENTICATED exit 0 loggedIn: true
gui/501 LaunchAgent, zsh -l -c 'exec ...', same reload Aqua asid 100038, same flags exit 0 loggedIn: true
user/501 LaunchAgent (LimitLoadToSessionType=Background), same reload Background asid 100056, flags 0x0 exit 36 User interaction is not allowed., item metadata still readable loggedIn: false, authMethod: none

Claude Code 2.1.266 maps that exit 36 (and 44) to "no keychain data" and reads ~/.claude/.credentials.json instead; with a stale file it prints Failed to authenticate: OAuth session expired and could not be refreshed (interactive: Login expired · Please run /login).

Candidate birth markers were read with ps -Eww -o command= -p <pid> for own-uid processes, noting that macOS hides the environment of Apple platform binaries such as /bin/sleep and that a herdr server is never one.

launchd-born herdr server (child of launchd, gui/501):  XPC_SERVICE_NAME=org.nix-community.home.herdr-server  no SSH_*
SSH-born herdr server (child of `herdr --session fm-remote remote-client-bridge` under `sshd-session: user@notty`):  SSH_CLIENT=... SSH_CONNECTION=...  no XPC_SERVICE_NAME

XPC_SERVICE_NAME identifies a launchd label but does not identify its domain, because the Background user/501 job also carried that variable while lacking keychain access. The owner classifier therefore accepts that label only when launchctl print gui/<uid>/<label> identifies the owner pid or the label is loaded in gui/<uid> but not user/<uid>. XPC_SERVICE_NAME=0, including a value inherited by a herdr live-handoff child, remains unknown. FM_REMOTE_JOB_ACTIVE=1 proves the Aqua worker only when dev.firstmate.remote-job is loaded in gui/<uid> but not user/<uid>.

The SSH-born row was read on the remote host whose dev.firstmate.herdr.fm-remote job showed state = spawn scheduled, runs = 239, last exit code = 1 and a log repeating error: herdr server is already running: herdr's remote attach had started the session's server as its own child before the login session existed, and launchd's copy lost the socket on every retry. pgrep -f did not list the herdr server's argv on macOS; lsof -U -a -c herdr -F pn named the socket owner.

A separate foreground-supervision check ran on 2026-09-09 on macOS 26 (Darwin 25.6.0) with Herdr 0.9.0 using the throwaway Aqua launch agent dev.fm-rca.herdr-fg. Its ProgramArguments ran /run/current-system/sw/bin/zsh -l -c "exec /etc/profiles/per-user/kunchen/bin/herdr server --session fm-lab-fg-90381-18985", with KeepAlive={SuccessfulExit=false} and ThrottleInterval=10, after launchctl bootstrap gui/501 <plist> and launchctl kickstart -k gui/501/dev.fm-rca.herdr-fg. launchctl print gui/501/dev.fm-rca.herdr-fg reported state = running and pid = 4806. lsof -U -a -c herdr -F pn named pid 4806 as the owner of ~/.config/herdr/sessions/fm-lab-fg-90381-18985/herdr.sock. ps -o pid,ppid,command -p 4806 reported 4806 1 /etc/profiles/per-user/kunchen/bin/herdr server --session fm-lab-fg-90381-18985, and its environment carried XPC_SERVICE_NAME=dev.fm-rca.herdr-fg. No other herdr process existed for that session, and after 15 seconds the job remained running with pid 4806. After a guarded herdr session stop, the job reported state = not running and last exit code = 0, and it stayed at rest through the throttle interval. A second launchctl kickstart -k gui/501/dev.fm-rca.herdr-fg started pid 45574, which was also the new socket owner. This proves that herdr server remains in the foreground as the launchd job, so the guard's final exec supplies the intended supervision and the earlier server that survived launchctl bootout was the unrelated SSH-bridge-born process.

bin/fm-test-run.sh tests/fm-remote-herdr-guard.test.sh pins the resulting decision table against real marker-carrying processes, and tests/fm-remote-doctor.test.sh pins the doctor's verdicts on the same markers.

Client selection

Measured 2026-09-08 on a macOS aarch64 host running a Herdr 0.9.0 server (protocol 22) for the fm-remote session while ~/.local/bin/herdr still held the self-updated 0.8.2 client (protocol 20) ahead of the Nix-managed 0.9.0 client on the remote-job PATH.

~/.local/bin/herdr --version
~/.local/bin/herdr pane get wCY:p2 --session fm-remote; echo "rc=$?"
~/.local/bin/herdr status --json --session fm-remote | jq -c '{c:.client.protocol,s:{running:.server.running,protocol:.server.protocol,compatible:.server.compatible}}'
herdr status --json --session fm-remote | jq -c '{c:.client.protocol,s:{running:.server.running,protocol:.server.protocol,compatible:.server.compatible}}'
herdr 0.8.2
{"id":"cli:pane:get","error":{"code":"protocol_mismatch","message":"client protocol 20 is older than server protocol 22; upgrade the Herdr client before using this command"}}
rc=1
{"c":20,"s":{"running":true,"protocol":22,"compatible":false}}
{"c":22,"s":{"running":true,"protocol":22,"compatible":true}}

The refusal is a JSON error on stderr with exit 1 and empty stdout, and both client generations report .server.compatible and .server.protocol per named session, which is what the selection in bin/backends/herdr.sh reads. tests/fm-backend-herdr.test.sh pins the bypass, same-process same-session caching, cross-session isolation, forced reselection, and both status shapes against fakes; tests/fm-backend-herdr-smoke.test.sh refreshes the real status normalization against the installed binary's running lab server.

Submit confirmation

Measured 2026-08-19 against Herdr 0.8.0 and Claude Code 2.1.236 in an isolated fm-lab- session.

herdr agent get reported agent_status=idle on every sample across a landed one-word turn and an 8-second sleep tool call, while the pane rendered Pontificating… then Sock-hopping… (11s · ↓ 234 tokens). fm_backend_herdr_send_text_submit therefore cannot treat native idle as proof of a swallow. The portable regressions in tests/fm-backend-herdr.test.sh and tests/fm-composer-lib.test.sh pin the verdicts: native idle plus a cleared composer is delivery, proven pending plus idle is a swallow, and proven pending plus a generating busy signal is a queued Enter. Refresh the live Claude proof with:

FM_HERDR_SUBMIT_CONFIRM_LIVE=1 tests/fm-herdr-submit-confirm-live-e2e.test.sh

Observed 2026-08-19:

ok - live Herdr submit confirm: Claude Code (2.1.236 (Claude Code)) on herdr 0.8.0 reports empty for a landed idle steer

Prune and respawn

The real label-collision reproduction is owned by:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-prune-safety-e2e.test.sh

Observed guarantee: a pre-existing captain-owned workspace with a seed-shaped tab was adopted for routing but its tab was never eligible for prune because the current create call did not return that seed id.

Restart-husk replacement is owned by:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-respawn-idem-e2e.test.sh

Observed guarantee: a restored no-agent tab was replaced create-before-close, while a registered live agent caused refusal.

Launcher workspace placement

Herdr exports its pane identity into every process it manages, checked on 2026-07-30 against Herdr 0.7.5 protocol 17 inside a guarded lab pane:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh
"$HERDR_LAB_HELPER" run "$LAB" pane run "$PANE" "sh -c 'env | grep ^HERDR | sort > /tmp/env.txt'"
HERDR_ENV=1
HERDR_PANE_ID=w1:p1
HERDR_SESSION=fm-lab-fm-herdr-env-pro-65961-25535
HERDR_SOCKET_PATH=/Users/kunchen/.config/herdr/sessions/fm-lab-fm-herdr-env-pro-65961-25535/herdr.sock
HERDR_TAB_ID=w1:t1
HERDR_WORKSPACE_ID=w1

This complete injection shape is verified only for Herdr 0.7.5. Firstmate requires both HERDR_PANE_ID and HERDR_SOCKET_PATH before accepting claimed launcher ancestry.

pane get reports the pane's current owning tab and workspace, which is what placement resolves from; the injected HERDR_TAB_ID and HERDR_WORKSPACE_ID are creation-time snapshots and are not read as current identity:

"$HERDR_LAB_HELPER" run "$LAB" pane get w1:p1 | jq -c '.result.pane | {pane_id,tab_id,workspace_id}'
{"pane_id":"w1:p1","tab_id":"w1:t1","workspace_id":"w1"}

Placement is owned by:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-launcher-workspace-e2e.test.sh

Observed guarantees on 2026-07-30 against Herdr 0.7.5 protocol 17:

ok - real herdr E2E: with one 'firstmate' workspace and no herdr parent, a crewmate still lands in this home's own workspace without stealing focus
ok - real herdr E2E: the normal unique-label path is unchanged when the launcher's own pane identifies the workspace
ok - real herdr E2E: presentation spaces still create the isolated child workspace and bind it under the launcher's exact parent, without stealing focus
ok - real herdr E2E: with two 'firstmate' workspaces, a worker spawned from inside the second one lands in that exact workspace
ok - real herdr E2E: the duplicate-labeled sibling workspace is left entirely untouched and focus is preserved
ok - real herdr E2E: with a duplicated home label, a projected worker still hangs off the launcher's exact workspace and the sibling stays untouched
ok - real herdr E2E: an ambiguous home label with no launcher identity refuses before any worker endpoint exists
ok - real herdr E2E: a launcher pane that no longer exists refuses before any worker endpoint exists
ok - real herdr E2E: a secondmate launching its own worker gets the same exact-workspace guarantee, and its same-labeled sibling is untouched
ok - real herdr E2E: a --secondmate launch still stands up that secondmate's own workspace instead of inheriting the launcher's
ok - real herdr E2E: teardown closes only the worker's own pane and leaves the launcher, its workspace, and the same-labeled sibling intact

That suite's headline case runs bin/fm-spawn.sh inside a real Herdr pane, so the parent identity comes from Herdr's own injection rather than a composed environment. Cross-session and contradictory bindings are covered deterministically in tests/fm-backend-herdr.test.sh, which can script a second server's socket without provisioning one.

Per-home and presentation topology

Per-home behavior is owned by:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-workspace-per-home-e2e.test.sh

Observed guarantee: the primary and secondmate used distinct home workspaces, a child launched by the secondmate stayed in that secondmate workspace, list-live remained home-scoped, and exact cleanup did not affect sibling homes.

The complete projection suite ran on 2026-07-21 against Herdr 0.7.4 protocol 16:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-presentation-e2e.test.sh

Observed guarantees included:

ok - real Herdr lab: primary and two secondmate homes each own a top-level contiguous child block
ok - real Herdr lab: concurrent primary/A/B spawns stay session-locked with zero focus drift
ok - real Herdr lab: session lock contention from a secondmate home falls back flat with no journal
ok - real Herdr lab: legacy projection labels and flat secondmate tabs are left unmigrated
ok - real Herdr lab: multi-home exact-pane teardowns restore captain focus without workspace close authority
ok - real Herdr lab validation completed on Herdr 0.7.4 with the default-session tripwire intact

The suite also covers lost or failed move responses, active-tab refusal, restart husks, missing and duplicate tokens, manual renames, concurrent cleanup, and exact focus restoration.

The mandatory projection suite ran again on 2026-07-24 against Herdr 0.7.5 protocol 16:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-presentation-e2e.test.sh

Observed restart-reclaim guarantees:

ok - real Herdr lab: Hi Bit and Wheelhouse-style same-identity restarts reclaim one nested space with exact focus and idempotence
ok - real Herdr lab: secondmate restart binding and reclaim stay isolated to the exact child home and parent
ok - real Herdr lab: concurrent cross-home recoveries replace exact husks under one session lock with no focus drift
ok - real Herdr lab: missing, renamed, and duplicate tokens trigger zero destructive or adoptive calls, and live duplicate risk refuses launch
ok - real Herdr lab validation completed on Herdr 0.7.5 with the default-session tripwire intact

The projection suite ran again on 2026-08-04 against Herdr 0.8.0 protocol 19 for the default-on flip, where an absent config/herdr-presentation-spaces enables the projection and the value off opts out; since 2026-08-05 an absent file enables the projection only at or above the 0.8.0 floor recorded under "Presentation version floor" below, and on is the explicit opt-in that survives the floor:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-presentation-e2e.test.sh

Observed default and opt-out guarantees:

ok - real Herdr lab: an opted-out spawn retains the Stage 1 Herdr command sequence with zero ordering calls
ok - real Herdr lab: a home that configured nothing is projected by default
ok - real Herdr lab: the primary presentation setting inherits into real secondmate homes
ok - real Herdr lab validation completed on Herdr 0.8.0 with the default-session tripwire intact

The projected spawn in that run used the historical empty opt-in file, so a home that had already enabled the projection keeps it without any migration step. One concurrent cross-home recovery case refused under contention on a loaded machine and passed on an immediate rerun; recovery-path presentation lock contention is a deliberate hard refusal rather than a flat fallback, which default-on now makes reachable from any Herdr home. That run measured the default-on projection on Herdr 0.8.0 only, while the focus-flash regression below was last run on 0.7.5 before the flip, so neither run covered a defective release under default-on projection; the version floor and the focus-flash suite's Part C close that gap.

The restored-shell session-start cleanup ran on 2026-07-24 against Herdr 0.7.5 protocol 17:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-herdr-session-cleanup-e2e.test.sh

Observed guarantee: one exact home-local, journal-correlated, one-tab and one-pane childless idle shell was closed after restoration while the exact non-target focus and default fleet session remained unchanged, and a repeat run was a no-op.

Workspace-removal focus safety

The focus-flash regression ran on 2026-08-05 against both Herdr 0.7.5 protocol 17 and Herdr 0.8.0 protocol 19 on macOS aarch64, with the 0.7.5 run using the pinned upstream release binary first on PATH:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-focus-flash-e2e.test.sh

Observed output on Herdr 0.7.5:

ok - old path: the explicit last-pane close of a non-focused workspace stole focus (w3	w3:t1 -> w2	w2:t1)
ok - mitigation: every in-operation sample preserved exact focus while the doomed workspace was removed
ok - mitigation: no explicit close and no corrective focus were needed on the defective release
ok - fallback: a doomed pane holding a persistent child exhausts the proof and takes the plain explicit close
ok - fallback on a defective release: a bounded wrong-focus window of 4 samples was fully restored to the anchor
ok - version floor: herdr 0.7.5 protocol 17 remains conservatively below the floor with steal_live=1
ok - version floor: an unconfigured home falls back flat on herdr 0.7.5 and the explicit opt-in still projects
evidence: herdr=0.7.5 protocol=17 steal_live=1 floor_verdict=1 default-session-tripwire=armed

Observed output on Herdr 0.8.0:

ok - old path note: this Herdr release preserves focus across the explicit close; continuing with outcome-only assertions
ok - mitigation: every in-operation sample preserved exact focus while the doomed workspace was removed
ok - fallback: a doomed pane holding a persistent child exhausts the proof and takes the plain explicit close
ok - fallback on a focus-preserving release: the plain explicit close preserved exact focus throughout
ok - version floor: herdr 0.8.0 protocol 19 is at or above the floor and preserves focus
ok - version floor: an unconfigured home stays projected on herdr 0.8.0 and the explicit opt-in agrees
evidence: herdr=0.8.0 protocol=19 steal_live=0 floor_verdict=0 default-session-tripwire=armed

The same guarded named-lab command passed on 2026-09-03 against Herdr 0.8.2 after this regression joined the required real-herdr-gated lane. It reported steal_live=0 floor_verdict=0 default-session-tripwire=armed, with the fleet's default session unchanged before and after.

Part C is the case the suite could not reach before: a doomed pane whose shell holds a persistent background child fails the lone-idle-shell proof on every sample, so the plan takes the plain explicit close, in the geometry where the closing workspace's right neighbour is a spacer rather than the focused anchor. On 0.7.5 that fallback exposed a bounded four-sample wrong-focus window and restored the anchor exactly; on 0.8.0 the same fallback exposed none, which is why default-on projection is floored at 0.8.0 rather than mitigated further below it. The suite also cross-checks its own Part A measurement against the floor classifier on whatever release it runs, so a drifted protocol-to-release mapping fails there rather than silently gating on the wrong thing.

Presentation version floor

Default-on presentation projection is floored at Herdr 0.8.0. The floor's structural signal is the selected running server's protocol number, falling back to the client protocol only when that selected session positively reports no running server, and the release mapping was measured on 2026-08-05 by running each pinned upstream macOS aarch64 release asset's own status --json through the guarded lab helper:

Release Reported version Protocol Carries both upstream focus fixes Floor verdict
v0.7.3 0.7.3 16 no below
v0.7.4 0.7.4 16 no below
v0.7.5 0.7.5 17 no below
preview-2026-07-21-0f10e1453a7f 0.7.5-preview.2026-07-21-0f10e1453a7f 17 no below
preview-2026-07-29-44b3adb12552 0.7.5-preview.2026-07-29-44b3adb12552 18 yes below
preview-2026-08-04-d78e3d3b5126 0.8.0-preview.2026-08-04-d78e3d3b5126 19 yes above
v0.8.0 0.8.0 19 yes above

No build lacking both fixes reaches protocol 19, and every pre-fix build tops out at 17, so protocol 19 is a safe structural expression of the 0.8.0 floor. The one post-fix build below it is a preview that still reports a 0.7.5 version, so it is conservatively treated as below the floor, which costs a preview build its projection and never lets an unfixed build through. The 2026-08-05 named-lab cross-version probe started a server from Herdr 0.7.5 and queried it with the installed 0.8.0 client; status reported client version 0.8.0 protocol 19, server version 0.7.5 protocol 17, server running true, and server compatible false. That ordinary post-upgrade shape proves the running server owns the focus behavior, so the unconfigured default composes client and selected-server verdicts conservatively and rechecks after server ensure before publishing a journal or creating a workspace.

Refresh this table with the opt-in guard, which re-downloads the pinned assets, verifies their digests, and fails naming any release whose reported version, protocol, or verdict has moved:

FM_HERDR_VERSION_FLOOR_LIVE_E2E=1 tests/fm-herdr-version-floor-live-e2e.test.sh

The classifier itself, the config preference it composes with, and the one-warning-per-release behavior are pinned portably with no Herdr installed:

tests/fm-backend-herdr.test.sh

Observed guarantees: every measured release classifies as the table records; either the protocol or the version signal alone carries an at-or-above verdict, and each divergent pair flips once the carrying signal is removed; client and running selected-session server verdicts compose conservatively, an unreadable server-running state and losing both release signals report indeterminate and fall back flat, the default is rechecked after server ensure before projection publication, an unconfigured home is projected only at or above the floor, an explicit on, including the historical empty opt-in file, is honored below it, and the below-floor warning is emitted once per home per detected release rather than once per spawn.

The whole real-Herdr lane was run on 2026-08-05 against both the CI-pinned Herdr 0.7.4 protocol 16, which is below the floor, and Herdr 0.8.0 protocol 19, which is at it:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh bin/fm-test-run.sh --lane real-herdr-gated

Both runs reported family=real-herdr-gated count=11 failed=0. The projection suite's unconfigured-home case is release-aware rather than pinned to one outcome, so it proves the projected default on 0.8.0 and the flat fallback with its naming warning on 0.7.4:

ok - real Herdr lab: a home that configured nothing is projected by default on herdr 0.8.0
ok - real Herdr lab: a home that configured nothing falls back flat on below-floor herdr 0.7.4 with one naming warning

Every other case in that suite uses an explicit opt-in or opt-out, so the floor leaves them unchanged on both releases.

Direct lab probes on 2026-07-28 established the removal rules the emptying-close plan relies on, each verified with workspace list focus reads around one mutation in a guarded fm-lab- session:

  • An explicit pane close that emptied a non-focused workspace moved focus off the focused workspace in both before-focus and after-focus geometries.
  • Ending a workspace's lone shell preserved the focused workspace exactly when the dying workspace sat behind it or the focused workspace was last, and moved focus to the focused workspace's right neighbor otherwise.
  • The production focus-preserving close in the dangerous geometry repositioned the doomed workspace, ended its proved shell, and left every concurrent focus sample on the exact anchor with no corrective tab focus issued.

Two real-hardware conditions were required for the pane-death path to engage and are now encoded in the adapter and its unit fixtures: BSD ps reports a login shell's comm as -zsh, and an idle shell transiently hosts a prompt helper (starship) as a second foreground process immediately after a workspace.move relayout, which the bounded settle window absorbs.

The rules match the v0.7.5 tag source (close_selected_workspace reassigns focus from the closing workspace's index; handle_pane_died only clamps the stale focused index), and the upstream default branch resolves both paths by workspace id (PR #1877, commit 165dca45, for the explicit close; PR #1912, commit a979916, for pane death), so the plan degrades to a harmless reorder-then-remove once a release carries them.

The full projection and restored-shell suites were re-run on 2026-07-28 on Herdr 0.7.5 with the updated close path; the presentation suite completed with real Herdr lab validation completed on Herdr 0.7.5 with the default-session tripwire intact, and the restored-shell cleanup guarantee above was unchanged.

The teardown-level record-retention gate was verified on 2026-07-28 with metadata fixtures and a live contending lock holder:

tests/fm-teardown.test.sh
tests/fm-backend-herdr.test.sh

Observed guarantees: a contended presentation lock refused the teardown before the isolated copy was returned, with the task branch, every durable record, and the endpoint intact and no pane close attempted; the retry after the contention cleared returned the copy, closed the pane under the lock, and removed the records; an unknown structured-presence result after an attempted projected close retained the journal and every record with a nonzero exit; and every presence-gate mode accepted only a structured not-found as gone.

The same fixtures verified three further boundaries on 2026-07-29: missing or malformed endpoint identity and an unparseable pane presence refused record removal with everything retained; the SIGKILL escalation re-read the exact pane's process information and refused to signal when a different shell pid owned the pane, falling back to the plain close with the original process untouched; and a reposition whose removal then failed on every path restored the exact original workspace order through a second verified move and reported the close as failed.

The teardown fixture was re-run on 2026-07-31 after extending the same fail-closed boundary through forced secondmate cleanup, including recursive cleanup of a nested secondmate whose Herdr grandchild close remains unconfirmed.

Observed output:

ok - forced secondmate teardown preflights every Herdr child before cleanup mutation
ok - forced secondmate teardown retains Herdr child identity until exact pane disappearance
ok - forced teardown retains a nested secondmate home and its grandchild's Herdr identity when the grandchild close is unconfirmed

Composer and operational input

Real captures verified these active distinctions:

  • Claude and Codex use bare and agent composers.
  • Pi uses content between complete separator rows and requires exact native Pi identity.
  • Dim or faint suggestion text is ghost content, while normally styled text is pending input.
  • Grok dark truecolor placeholders are ghost content, while bright truecolor typed input remains pending.
  • A bare shell prompt has no safe agent-composer container and is unknown.

tests/fm-composer-ghost.test.sh, tests/fm-composer-lib.test.sh, and the Herdr composer cases pin the exact captured ANSI bytes. The U+2063 operational and routed-request separators were exercised through a real Pi-on-Herdr path; the byte-exact active regression is:

FM_SEND_MARKER_HERDR_E2E=1 \
  tests/fm-send-secondmate-marker-herdr-e2e.test.sh

Native blocked event

The protocol-16 event path was measured on 2026-07-11 with Herdr 0.7.3 and Python 3.13:

HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-backend-herdr-eventwait-smoke.test.sh

Observed output:

ok - real herdr: events.subscribe capability gate passes
ok - real herdr: a driven idle->blocked transition returns the blocked record in 0.129s
ok - real herdr: the watcher fast-path enqueues a stale wake naming the task window

Polling remained active and is covered as the fallback for capability, connect, subscribe, and repeated reader failure.

Agent lifecycle control

Herdr is one of the two backends whose recovery-grade agent-state classifier the control plane may trust (agent-control.md), so its lifecycle gating is measured against the real binary; reverified 2026-08-08 on Herdr 0.8.0, and first measured 2026-08-02 on Herdr 0.7.5 with identical results:

tests/fm-control-herdr-smoke.test.sh

Observed output:

ok - real herdr: exit on a pane with no registered agent is idempotent success
ok - real herdr: interrupt refuses when herdr's own agent registry reports no agent
ok - real herdr: interrupt delivers the harness's key and proves the agent survived it
ok - real herdr: no control verb removed the endpoint or the task's local copy
ok - real herdr: an agent that does not stop fails closed instead of being reported as stopped

The registry read through herdr pane report-agent is the same source fm_backend_herdr_agent_state classifies, so registering and not registering an agent on a plain shell pane exercises exactly the gate every lifecycle verb depends on, with no real agent launched. That command is the guard that refreshes this record; run it after every Herdr upgrade rather than trusting the version above.

Away-mode transport

The away daemon is no longer launched on Pi; the away posture there is the record bin/fm-afk-contract.sh owns. The Pi/Herdr away posture and return path was verified on 2026-09-08 against a real Pi primary in an isolated Herdr lab session, Herdr 0.9.0 and Pi 0.82.0:

FM_AFK_PI_HERDR_E2E=1 HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \
  tests/fm-afk-pi-herdr-return-e2e.test.sh
ok - real Pi primary: the away posture is recorded with no daemon launched
ok - real Pi/Herdr: nothing injects into the captain pane under the away posture
ok - real unmarked Pi return renders the brief, opens catch-up, and blocks Bearings before the unresolved blocker can be deferred
ok - resolved return catch-up allows Bearings and a clean idempotent away re-entry
evidence: herdr=herdr 0.9.0 pi=0.82.0 target=fm-lab-fm-afk-pi-return-37189-7133:w1:p1 archived-records=2

Observed guarantees: fm-afk-launch.sh start refused on the Pi primary and confirm recorded the posture with no daemon pid, flag, or terminal; a pending real Pi draft was left untouched with nothing submitted into the captain pane; the unmarked return request was recognized as the return, rendered the brief health first, opened the catch-up gate on the live blocker, and refused Bearings; resolving the blocker cleared the gate, and a clean re-entry and return left exactly one archived record per away window. The fixture captures submitted input through Pi's input extension hook, so the lab agent directory needs no provider credentials. The daemon injection transport into a live composer keeps its coverage in tests/fm-afk-inject-herdr-e2e.test.sh for the harnesses that still run the daemon, and the dedicated Herdr daemon workspace topology is covered by tests/fm-afk-launch.test.sh and preserves the captain tab's pane count.

Zellij

The current compatibility floor and latest verification are Zellij 0.44.0 with jq on macOS aarch64. All real tests use a uniquely named session and tests/zellij-test-safety.sh; they never touch a session named firstmate or call all-session deletion.

Guarantee Command shape Result
Headless session zellij attach -b <name> without a TTY Created a persistent background session and returned.
Session list zellij list-sessions --short --no-formatting Returned one plain name per line without starting a session.
Create tab zellij action new-tab --cwd <dir> --name <title> Returned a numeric tab id and focused the new tab when a client was attached.
Pane discovery zellij action list-panes --json Included terminal pane id, tab id, plugin flag, and top-level pane_cwd.
Literal send zellij action paste --pane-id <id> -- <text> Left text unsubmitted.
Keys send-keys --pane-id <id> Enter, Esc, and one argument Ctrl c All three shared operations worked.
Capture dump-screen --pane-id <id> or --full Worked with no attached client; no line-bound flag exists.
Styled capture dump-screen --pane-id <id> --ansi Preserved ANSI styling ("Composer classification matrix" above); feeds the zellij composer classifier.
Close close-tab-by-id <id> Removed the live task pane and tab together.
Failure exit actions against missing targets Returned exit 0, requiring structural preflight and output-shape validation.

pane_cwd stayed frozen when a foreground subshell changed directory. The marker-delimited pwd probe returned the live nested cwd and is covered by the real smoke. The focus mitigation restored the previously active tab after new-tab, with the unavoidable narrow race documented in the operator guide.

tests/fm-backend-zellij.test.sh
tests/fm-backend-zellij-smoke.test.sh

The real lifecycle smoke proved spawn, metadata, nested-subshell worktree discovery, send, capture, unlanded-work refusal, approved local landing, exact tab cleanup, and session cleanup without retaining task-specific ids or branch names here.

Orca

Real readiness was verified against /usr/local/bin/orca with /Applications/Orca.app bundle version 1.4.116.

orca status --json

Observed fields:

result.runtime.reachable=true
result.runtime.state=ready

orca terminal create --json returned result.terminal.handle. orca worktree create returned result.worktree.id and result.worktree.path. Speculative bare ids and nested terminal fields were deliberately rejected.

tests/fm-backend-orca.test.sh
tests/fm-backend.test.sh
tests/fm-bootstrap.test.sh

The fake-Orca suite covers readiness, registration, create response parsing, metadata routing, popup-safe submit, and path-matched release refusal.

cmux

The current compatibility floor is cmux 0.64, and the active live evidence uses 0.64.17 build 97 on macOS aarch64. Real tests use only exact fm-test- workspaces guarded by tests/cmux-test-safety.sh and never quit or relaunch the captain's app.

cmux version
cmux ping

Observed version:

cmux 0.64.17 (97) [9ed29d81a]

Source and live checks established the five control modes:

  • off starts no listener.
  • cmuxOnly rejects an external Firstmate process by ancestry.
  • automation uses an owner-only 0600 socket with no handshake.
  • password uses the same 0600 socket plus auth <password>.
  • allowAll uses a 0666 socket with no authentication.

The live default rejection was Access denied - only processes started inside cmux can connect. The live password challenge was Authentication required - send auth <password> first. The app configuration writer did not retain a hand-added socket password, which is why the operator guide requires Settings and a local Firstmate password source.

Current active CLI findings:

Guarantee Command shape Result
Create new-workspace --name <title> --cwd <dir> --focus false --id-format uuids Created one workspace with one surface without focusing it.
Fresh readiness list-panes --workspace <id> --json --id-format uuids Found a brand-new surface before content existed.
Fresh read counterexample read-screen before any write Returned internal_error: Failed to read terminal text.
Literal send send --workspace <id> --surface <id> -- <text> Left text unsubmitted.
Keys `send-key ... enter escape
Nested cwd current_directory plus foreground subshell Structured cwd froze; the marker-delimited pwd probe found the live cwd.
Last surface close-surface on the only surface Refused with invalid_state: Cannot close the last surface.
Last workspace close-workspace on the only workspace in a window Printed success but left the workspace present.

The last-workspace workaround was reverified on 2026-07-10 in Automation mode. After creating one unfocused unnamed sibling in the same window, close-workspace removed the exact task workspace and left only cmux's default sibling. A selected non-last workspace closed directly, proving that window cardinality rather than selection is the trigger.

Source inspection confirmed each workspace constructor creates a new UUID with no restored-id input. Recovery therefore remains title-based. The bundled Claude wrapper was observed stripping CMUX_* variables on its failed socket-probe path while retaining the app bundle id, supporting the macOS-only bundle-id and ancestry fallbacks.

tests/fm-backend-cmux.test.sh
tests/fm-backend-cmux-smoke.test.sh

The real smoke proves socket access, fresh readiness, current-path probing, send and keys, bounded capture, title identity, and guarded exact cleanup.

Claude composer confirmation

The borderless Claude composer confirmation was verified on 2026-08-09 with cmux 0.64.22 build 102 and Claude Code 2.1.226 on macOS aarch64. An isolated real Claude worker rendered a bare plus U+00A0 row between horizontal rules. The cmux classifier returned empty, and one fm-send.sh --resolve-key <key> ALBATROSS command - which used the typed path before ordinary task steers moved to the inbox - appended the matching resolved event before the worker reported completion. The terminal capture contained exactly one submitted ❯ ALBATROSS row. The dated proof used this command:

FM_CMUX_CLAUDE_COMPOSER_LIVE=1 bin/fm-test-run.sh tests/fm-cmux-claude-composer-live-e2e.test.sh

That guard still addresses the worker by task selector, so it no longer reaches the typed submit path and is not a current refresh entry point for this guarantee. The portable classifier regression is tests/fm-backend-cmux.test.sh.

Codex App host tools

A reusable Desktop host-tool smoke ran on 2026-07-06 against Codex Desktop bundle version 26.623.101652, build 4674, bundle id com.openai.codex. Local paths and task-specific ids are intentionally not retained here.

The host-tool sequence was:

  1. list a saved project;
  2. create a Desktop-owned worktree thread;
  3. recover and read the thread while active and after completion;
  4. verify the thread appended a Firstmate status line and wrote its report;
  5. send a follow-up to the same thread;
  6. read the completed follow-up;
  7. archive the exact thread;
  8. read the archived transcript with state notLoaded.

Observed guarantee: a Desktop-owned thread can write Firstmate lifecycle files when the prompt provides an authorized absolute path, and create, send, read, and archive work at the Desktop host-tool layer. The missing guarantee remains a supported shell-callable bridge that lets Firstmate perform those operations against the same visible Desktop endpoint. App-server partial methods and raw socket experiments do not satisfy that bridge contract.

Cursor Agent CLI

Cursor runs crewmate, scout, secondmate, and primary work; supervision.md owns the primary evidence. The evidence below was produced on 2026-08-11 against the installed signed CLI on macOS 26.5.2 arm64 with tmux 3.6a, running as kunchenguid, and extended on 2026-08-13 with the tmux composer verdict below.

  • Binary: ~/.local/bin/cursor-agent, canonicalizing into ~/.local/share/cursor-agent/versions/2026.08.11-e8db854/cursor-agent.
  • Version: cursor-agent --version reported 2026.08.11-e8db854, and cursor-agent status reported a logged-in account.
  • Both installed names, cursor-agent and the legacy alias agent, resolve into that same versioned install tree.

Resolution prints the STABLE launcher rather than the canonical target, because the canonical path carries a version the CLI replaces on its own auto-update.

Process identity

#{pane_current_command} and ps -o comm= disagree for cursor, which is why identity reads both:

Source Observed value
#{pane_current_command} node
ps -o comm= /Users/<user>/.local/bin/cursor-agent
child argv .../bin/cursor-agent --use-system-ca .../versions/2026.08.11-e8db854/index.js --trust --yolo

node matches no harness name pattern, so a cursor pane is identified from Cursor's own name or install tree in the path or argv[0]. An unrelated node or agent matches neither and classifies other, which the liveness callers fold into ambiguous rather than dead. A live cursor pane returned alive; a plain shell pane in the same run returned dead.

Environment markers and detection ordering

Read from the live agent process and from a tool subprocess it spawned:

Marker Where observed
CURSOR_INVOKED_AS=cursor-agent the agent process itself, and its children
CURSOR_AGENT=1 child/tool processes only
CURSOR_CONVERSATION_ID=<uuid> child/tool processes
AGENT_TRANSCRIPTS=<projects-root>/<slug>/agent-transcripts child/tool processes

Cursor does not clear an inherited CLAUDECODE, so ordering decides the verdict. With both markers set, bin/fm-harness.sh reports cursor; with CLAUDECODE alone it still reports claude.

Composer

Cursor's composer is a BARE row whose prompt glyph is (U+2192); there is no border. Its idle placeholder is Plan, search, build anything in a fresh session and Add a follow-up after a completed turn.

The styled capture of an idle composer row was:

ESC[48;2;21;21;21m ESC[2m→ ESC[0;7mESC[48;2;21;21;21mPESC[0;2mESC[48;2;21;21;21mlan, search, build anythingESC[0m

The glyph and the placeholder tail are dim (SGR 2), but the cell under the terminal cursor is reverse video (SGR 0;7). Reverse video is neither dim nor a dark foreground, so ghost stripping leaves a lone P and an idle composer read pending before the fix. After teaching the shared classifier the glyph, both placeholders, and the plain-row remnant rule, the same captures read empty on the styled cursorless backends, while real typed text - including text typed to exactly match the placeholder - still read pending. An unstyled capture has no ghost-strip proof and correctly stays unknown.

tmux composer verdict, corrected 2026-08-13

The 2026-08-11 record that a Cursor pane's tmux composer verdict is unknown in every state described the cursor-ANCHORED read, which remains true: #{cursor_y} was 25 with #{cursor_flag} 0 on an idle pane, pointing below the footer, so tmux's cursor row is not a composer locator for Cursor. Read cursorlessly, the same live capture classifies correctly, so the composite verdict is no longer unknown:

cursor_y=25  cursor_flag=0
with-cursor : unknown      cursorless : empty     (idle composer)
with-cursor : unknown      cursorless : pending   (real typed text, not submitted)
with-cursor : unknown      cursorless : unknown   (agent exited to a shell)

bin/fm-tmux-lib.sh therefore reclassifies cursorlessly only when the pane's foreground process group is provably Cursor, so every other harness keeps the strict blank-cursor-row posture. That supplies the genuine composer-empty proof required for away-mode escalation delivery. A live injection through bin/fm-supervise-daemon.sh's own inject_msg into a real Cursor pane returned 0 and the pane processed the typed FIRSTMATE_OP: v1 away-supervisor: escalation.

tests/fm-tmux-agent-liveness.test.sh pins this with real processes and no Cursor installed: it asserts the cursor-anchored source is blind, that the composite still reads empty idle and pending with typed text, that an identical screen stays unknown when the pane is not Cursor, and that a stale Cursor screen over a dead shell never reads empty.

Busy state

Cursor writes a per-conversation transcript at <projects-root>/<workspace-slug>/agent-transcripts/<conversation-id>/<conversation-id>.jsonl. Each turn is bracketed by a role:user open and a typed {"type":"turn_ended","status":...} close. Observed closes: success for a completed turn, and aborted with "error":"User aborted/interrupted manually." after a single Escape.

The trailing close landed 0 seconds after the pane's busy footer cleared on a normal turn. The transcript does NOT accumulate one close per turn, so a count of closes is not a progress signal; only the trailing record is. After an interrupt the aborted close was observed within seconds in some runs and not within twenty seconds in others, so bin/fm-control-lib.sh deliberately claims no cancellation acknowledgement for cursor.

Binding never reconstructs cursor's workspace-slug directory name, which collapses path separators. Cursor records the exact absolute workspace path in each project directory's .workspace-trusted, and the binding matches on that value.

Rendered busy token, delivery only

Mid-turn the pane showed a braille spinner plus a verb, and ctrl+c to stop on the composer row; both the verb line and that token were absent the instant the turn ended. The same version rendered Working in one turn and Running in the next, so the TOKEN is matched and the verb is not. This row is a delivery guard for submit acknowledgement only; recorded worker state comes from the transcript fold.

Launch, lifecycle, and skills

Fact Observed
Workspace trust --trust suppressed the prompt; --yolo alone did NOT, and the prompt blocks a fresh worktree
Autonomy --yolo (alias of --force); the footer renders Run Everything
Worktree -w/--worktree allocates a SECOND worktree under ~/.cursor/worktrees and is never passed
Effort no effort flag exists; requested effort stays in task metadata
Interrupt single Escape; the pane showed Cancelled and the composer returned to its placeholder, so no clear key is needed
Exit /exit
Skill invocation /<skill>; cursor discovers firstmate's user-level skills, and /no-mistakes autocompleted with firstmate's own description and invoked the skill
Slash popup real: the first Enter closes the popup and a SECOND Enter submits, the same hazard as grok, covered by the submit core's retried Enter

End-to-end

A throwaway scout was spawned through bin/fm-spawn.sh --scout --backend tmux on a real cursor worker and driven to completion:

  1. the launch delivered its brief positionally and the agent executed it;
  2. state/<id>.cursor-session was written with the task worktree;
  3. the transcript fold read busy mid-turn and idle after it;
  4. bin/fm-send.sh delivered a steer through the then-current typed path and exited 0;
  5. bin/fm-control.sh <id> interrupt cancelled a running turn;
  6. bin/fm-control.sh <id> exit stopped the agent;
  7. bin/fm-teardown.sh refused until the scout's report and decision gate were satisfied, then removed the session record.

Herdr backend

The tmux run above is the reference; this section is the separate Herdr proof, produced on 2026-08-12 against Herdr 0.8.0 (client and server, protocol 19) and the same signed cursor-agent 2026.08.11-e8db854 on macOS 26.5.2 arm64. Every step ran inside an isolated fm-lab- session provisioned by bin/fm-herdr-lab.sh, launched from a neutral parent outside any Herdr pane, with the live default session's pane count checked before, during, and after; it stayed at 7 throughout.

Herdr's native agent state is unusable for Cursor. A 60-sample probe of agent get across a full turn reported agent_status=blocked in every state - idle, mid-turn, and after. The typed submit path's idle baseline is therefore structurally unreachable for Cursor, and every typed send falls into the composer branch.

Pane state Composer verdict Rendered footer
Idle empty no busy token
Text typed, not submitted pending no busy token
Mid-turn pending (placeholder plus ctrl+c to stop on one row) ctrl+c to stop

Herdr draws the composer's rules with the half-block glyphs U+2584 and U+2580 rather than the box-drawing family. Before those were taught to the shared edge detector, a bare composer's wrap region ran through its own closing rule and swallowed the model and path footer, so an idle pane read pending. Measured as an A/B on the same live pane, the pre-fix classifier returned pending and the current one returned empty.

The idle fix alone did not confirm typed delivery, because the composer branch reads the mid-turn row instead. With the rendered-footer transition in place, a typed-plane bin/fm-send.sh invocation exited 0 and the steer executed in the pane; the same send previously exited 1 with delivery unconfirmed; verdict=pending on a message that had actually landed.

The rest of the lifecycle was driven end to end on that worker:

  1. bin/fm-spawn.sh --scout --backend herdr placed the worker and it executed its brief;
  2. the transcript fold read busy mid-turn and idle after, unchanged from tmux, so the recorded worker state is backend-agnostic;
  3. bin/fm-control.sh <id> interrupt reported cancel=unconfirmed by design and the pane showed Cancelled, with the footer and the fold both returning to idle;
  4. bin/fm-control.sh <id> exit stopped the agent through the slash popup and the pane returned to its shell;
  5. bin/fm-teardown.sh refused until the scout's report and decision gate were satisfied, then removed the session record and returned the worktree.

Other harnesses on Herdr are unaffected by the edge-detector change. All seven live panes of the running default session - one Pi, four Claude, two plain shells - classified identically under the pre-fix and current classifiers.

Typed-submit confirmation is verified on tmux and Herdr only. Zellij, cmux, and Orca share a submit core that never consults the busy footer, so a typed-plane Cursor send there lands but fm-send reports delivery unconfirmed and exits non-zero; ordinary text steers ride the durable inbox and exit 0 at enqueue. Teaching that shared core the same transition is deliberately separate work, because it changes the submit path for every harness on those three backends and needs its own live validation on each.

The portable regression is tests/fm-cursor-harness.test.sh, the composer captures are pinned in tests/fm-composer-lib.test.sh, and the Herdr submit and footer behavior is pinned in tests/fm-backend-herdr.test.sh. Refresh this harness-dependent proof before accepting a cursor upgrade:

FM_HARNESS_LIVENESS_DRIFT=1 bin/fm-test-run.sh tests/fm-harness-liveness-drift-live-e2e.test.sh

Pi supervision branch

The supervision-branch extension (.pi/extensions/fm-branch-supervision.ts, docs/pi-supervision-branch.md) builds its second session through the Pi SDK surface: createAgentSession (including its model, modelRuntime, and thinkingLevel options), DefaultResourceLoader with extensionFactories, SessionManager, createBashToolDefinition with a spawnHook, sendCustomMessage for routine notes, appendEntry and registerEntryRenderer for captain outcomes, the before_provider_request hook, the command context's model registry for picker candidates, a fresh ModelRuntime for isolated-branch resolution, and Pi's own getSupportedThinkingLevels/clampThinkingLevel plus its getThinkingLevel and thinking_level_select extension surface for effort. In TUI mode, its /supervision-model model list is drawn with Pi's own SelectList, Input, fuzzyFilter, and DynamicBorder through the extension context's ui.custom surface, which is what bounds and searches a long catalog.

Evidence produced 2026-08-25 on macOS 26.5.2 arm64, Node v24.13.1:

  • Historical real-SDK guard: FM_PI_BRANCH_LIVE_E2E=1 bin/fm-test-run.sh tests/fm-pi-branch-live-e2e.test.sh against the globally installed @earendil-works/pi-coding-agent 0.81.1 printed ok - real Pi SDK 0.81.1 accepts the branch session construction and preserves an unpromptable wake. The guard read no credentials and made no provider call: an isolated empty PI_CODING_AGENT_DIR left model resolution empty, so the branch's first prompt failed fast and exercised the former direct-branch fallback. That fallback probe predates watcher-owned settlement and is not current evidence for the replacement-safe delivery boundary. The same run confirms that a real ModelRegistry over that empty agent dir still exposes the picker-facing availability surface, then pins openai/no-such-live-model and proves that the branch's own ModelRuntime refuses the unresolvable pin instead of silently running supervision on main's model.
  • Model-pin precedence: the same guard run printed ok - real Pi SDK 0.81.1 applies an explicit branch model on create and over a reopened session's recorded model. It declares a local fm-live-fake provider in an isolated models.json, never contacts it, and proves through session.model that an explicit model is applied on create, still wins over the model a reopened session recorded, and is absent-pin-restorable - the SDK behavior needed when a model or effort change reopens the current main session's branch conversation.
  • Effort-pin vendor contract: the same guard run printed ok - real Pi SDK 0.81.1 reports its own supported effort levels and applies an explicit branch effort over a reopened session's recorded level. Over its own local never-contacted provider it confirms that getSupportedThinkingLevels still returns ["off","minimal","low","medium","high","xhigh","max"] for a model mapping every extended level, narrows to ["off","minimal","low","medium","high"] for a reasoning model mapping none, returns ["off"] for a non-reasoning model, and that clampThinkingLevel lowers max to high on the narrow model while collapsing an unrecognized token to off - which is why the extension rejects an unrecognized pin before that clamp can see it. It then proves through session.thinkingLevel that an explicit effort is applied on create, that a reopened session with no override restores its own recorded level, that an explicit effort beats that recorded level, and that an over-ceiling effort is clamped rather than refused. The recorded-level cases need a session file Pi will actually restore from, and Pi flushes one only once an assistant message exists, so the guard appends the level change and that message through the real SessionManager rather than hand-writing the format.
  • Picker primitives: on 2026-08-26, after the final portable-shell and sentinel fixes, bin/fm-test-run.sh tests/fm-pi-branch-extension.test.sh again printed ok - the installed Pi still bounds the picker's list and ranks its search against the same installed 0.81.1 package. That case imports the real SelectList, Input, fuzzyFilter, and DynamicBorder, renders a 42-row catalog through the real SelectList at the visible bound the extension asks for, and fails naming the installed version if Pi stops exporting a primitive or stops bounding what it renders; it skips when no npm package is installed, and the portable stubbed cases in the same file hold the ordering, search, and branch-only-pin behavior everywhere.
  • Strict typecheck: tests/fm-pi-primary-types.test.sh printed ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.81.1 with the branch extension and its imported libraries included. This typecheck is also the enforcement for the extension's declared effort vocabulary: its bidirectional assertion against Pi's own getThinkingLevel return type fails the moment Pi adds or removes a thinking level, so the runtime list used to reject an unrecognized hand-edited pin cannot drift into a stale Firstmate catalog.
  • Historical custom-message provider conversion: on 2026-08-26, FM_PI_BRANCH_LIVE_E2E=1 bin/fm-test-run.sh tests/fm-pi-branch-live-e2e.test.sh against installed @earendil-works/pi-coding-agent 0.84.1 printed ok - real Pi SDK 0.84.1 delivers a custom message to the provider as user text carrying only content, so the captain outcome's typed envelope is what reaches the model. The guard passes a typed captain outcome and a plain rendered routine note through Pi's exported convertToLlm, proves that customType and display are not model-visible identity, and classifies the resulting provider text with bin/fm-operational-input.sh. This evidence explains the superseded model-relay path but is no longer the captain-delivery contract.

2026-08-28 Pi 0.84.4 SDK compatibility refresh

The credential-free live guard and strict typecheck were rerun against the installed @earendil-works/pi-coding-agent 0.84.4 package after the Pi primary compatibility repair. The live guard used an isolated empty PI_CODING_AGENT_DIR, inspected no credentials, and made no provider call.

npm exec --yes --package=typescript@5.9.3 -- bash tests/fm-pi-primary-types.test.sh
FM_PI_BRANCH_LIVE_E2E=1 bin/fm-test-run.sh tests/fm-pi-branch-live-e2e.test.sh
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.84.4
ok - real Pi SDK 0.84.4 accepts the branch session construction and preserves an unpromptable wake
ok - real Pi SDK 0.84.4 applies an explicit branch model on create and over a reopened session's recorded model
ok - real Pi SDK 0.84.4 reports its own supported effort levels and applies an explicit branch effort over a reopened session's recorded level
ok - real Pi SDK 0.84.4 delivers a custom message to the provider as user text carrying only content, so the captain outcome's typed envelope is what reaches the model
FM_TEST_END 2026-08-29T01:01:01Z tests/fm-pi-branch-live-e2e.test.sh exit=0 duration_ms=2520 gate_skip=false

The focused extension suite also exercised the installed Pi 0.84.4 picker and outcome-renderer consumers; calm-mode-feasibility.md owns the version-scoped renderer evidence.

2026-08-29 deterministic captain-outcome delivery

The credential-free live guard, focused extension suite, store suite, and strict typecheck were run against the locally installed @earendil-works/pi-coding-agent 0.84.3 package. No model was selected or prompted, no provider call was made, and the active Pi session was not changed.

bin/fm-test-run.sh tests/fm-pi-branch-extension.test.sh
bin/fm-test-run.sh tests/fm-branch-supervision.test.sh
npm exec --yes --package=typescript@5.9.3 -- bash tests/fm-pi-primary-types.test.sh
FM_PI_BRANCH_LIVE_E2E=1 bin/fm-test-run.sh tests/fm-pi-branch-live-e2e.test.sh
ok - captain outcomes are exact and exactly once across crash, reload, busy main, compaction, and an unrelated assistant response
ok - startup replay cannot advance the cursor across an unrendered captain outcome
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.84.3
ok - real Pi SDK 0.84.3 immediately renders appendEntry in the active transcript, persists it across reopen, and excludes it from model context

The live probe loads the extension through Pi's real resource loader and AgentSession, subscribes a stock InteractiveMode, verifies ExtensionAPI.appendEntry synchronously inserts the exact registered custom row into its active chat once, reopens the resulting session file to verify exact structured data, and verifies the entry is absent from buildSessionContext().messages. The focused regression recreates the incident topology with stale compaction framing and an immediately preceding unrelated assistant response, then covers idle and busy delivery, cold startup with late fleet-lock acquisition, the crash boundary after entry persistence but before cursor advancement, and repeated reload without duplication.

2026-09-01 sequence-keyed captain-outcome processing

The focused extension suite, store suite, strict typecheck, and credential-free live guard were run against a locally installed @earendil-works/pi-coding-agent 0.84.4 package selected with FM_PI_PACKAGE_DIR, on macOS 26.5.0 arm64, Node v24.13.1. No model was selected or prompted, no provider call was made, and the active Pi session was not changed.

FM_PI_PACKAGE_DIR=<pi-0.84.4 package> bin/fm-test-run.sh tests/fm-pi-branch-extension.test.sh
bin/fm-test-run.sh tests/fm-branch-supervision.test.sh
FM_PI_PACKAGE_DIR=<pi-0.84.4 package> npm exec --yes --package=typescript@5.9.3 -- bash tests/fm-pi-primary-types.test.sh
FM_PI_BRANCH_LIVE_E2E=1 FM_PI_PACKAGE_DIR=<pi-0.84.4 package> bin/fm-test-run.sh tests/fm-pi-branch-live-e2e.test.sh
ok - a captain outcome reaches main's model as one typed, sequence-keyed processing request while routine notes stay plain
ok - a captain outcome opens one sequence-keyed processing turn, survives empty and unrelated answers, is re-presented at run end and session start, and closes only on its acknowledgement
ok - the processed marker is sequence-bound, never ahead of the read cursor, never backwards, and migrates delivered history once
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.84.4
ok - real Pi SDK 0.84.4 immediately renders appendEntry in the active transcript, persists it across reopen, and excludes it from model context

The focused regression recreates the two 2026-08-31 incident shapes against the real store scripts: a delivered decision outcome whose processing turn returns an empty assistant message, and one whose turn repeats an unrelated prior answer. In both, the processed marker holds, the same sequence is presented again at the run boundary and after a session replacement, the triggered-turn budget gives way to a next-prompt copy without duplicates, and only fm_branch_processed with the presented sequence closes the outcome; a routine outcome never enters the path, and delivered history from before the marker existed is migrated once rather than re-presented. On this machine the globally installed npm package is 0.81.1, whose stock ToolExecutionComponent rendering differs from the 0.84 line and fails the suite's first rendering-consumer case before any delivery case runs, which is why FM_PI_PACKAGE_DIR points at the 0.84.4 install above.

2026-09-02 historical post-construction provider-error fallback

The focused extension suite, strict typecheck, and real-SDK guard were run against the npm @earendil-works/pi-coding-agent 0.84.4 package on macOS 26.5.0 arm64, Node v24.13.1, before fallback ownership moved from the branch extension to the watcher. The real-SDK case configured an isolated local OpenAI-compatible model, intercepted its only fetch in-process with the incident's non-retryable 429 Monthly usage limit reached response, read no user credential, and allowed no external provider request. It proved that Pi persisted an assistant message with stopReason: "error" and resolved the constructed branch prompt normally, after which the extension released the claimed-row grant, retained the durable queue row, and returned the exact wake to main as a follow-up.

FM_PI_PACKAGE_DIR="$HOME/.npm/_npx/1f276a68aabfc75c/node_modules/@earendil-works/pi-coding-agent" bash tests/fm-pi-branch-extension.test.sh
FM_PI_PACKAGE_DIR="$HOME/.npm/_npx/1f276a68aabfc75c/node_modules/@earendil-works/pi-coding-agent" bash tests/fm-pi-primary-types.test.sh
FM_PI_BRANCH_LIVE_E2E=1 FM_PI_PACKAGE_DIR="$HOME/.npm/_npx/1f276a68aabfc75c/node_modules/@earendil-works/pi-coding-agent" bash tests/fm-pi-branch-live-e2e.test.sh
ok - a settled branch turn without a durable outcome falls back and releases its grant for main replay
ok - post-construction provider errors fall back immediately and repeated failures defer later wakes directly to main
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.84.4
ok - real Pi SDK 0.84.4 returns a post-construction 429 wake to main without losing its durable row

The current portable regression proves that only consecutive provider errors count toward the two-error broken-branch latch: a durable report between errors resets the streak, the error that reaches the threshold rejects to watcher-owned fallback, and the next wake remains on main without another branch prompt. tests/fm-pi-watch-extension.test.sh owns the provider-free integration evidence that watcher fallback remains pending until Pi accepts the main follow-up or the branch settles successfully, and that a follow-up accepted while main is streaming neither stalls the successor chain nor escapes replacement replay until Pi consumes it. pi-supervision-branch.md owns the current cooldown, recovery, and re-latch contract and points to the regression that now covers it.

Scope of the earlier evidence: the installed signed pi CLI (0.82.0 at verification time) is a compiled binary whose bundled SDK is not importable from Node, so the importable npm package is the only surface the guard and the typecheck can pin. The extension executes inside the signed CLI's own runtime, so a CLI upgrade can drift ahead of the pinned npm surface; refresh the SDK construction, picker, renderer, and type evidence after every Pi upgrade by rerunning the applicable live guard probes, picker regression, and strict typecheck above (point FM_PI_PACKAGE_DIR at a matching npm install when one exists). The live guard now drives both extensions through the watcher-owned settlement handshake, requires rejected branch settlement before main delivery, and verifies successor-delivery confirmation; rerun it against the matching importable Pi package to refresh end-to-end fallback evidence.

2026-09-02 streaming-time watcher delivery

The focused watcher suite, strict typecheck, and credential-free live guard were run against the npm @earendil-works/pi-coding-agent 0.84.4 package selected with FM_PI_PACKAGE_DIR, on macOS 26.6.2 arm64, Node v24.14.1, after the watcher extension stopped waiting for before_agent_start before settling a main delivery. No credential was read, no request left the machine, and the active Pi session was not changed.

bin/fm-test-run.sh tests/fm-pi-watch-extension.test.sh
FM_PI_PACKAGE_DIR=<pi-0.84.4 package> npm exec --yes --package=typescript@5.9.3 -- bash tests/fm-pi-primary-types.test.sh
FM_PI_BRANCH_LIVE_E2E=1 FM_PI_PACKAGE_DIR=<pi-0.84.4 package> bin/fm-test-run.sh tests/fm-pi-branch-live-e2e.test.sh
ok - Pi hung successor falls back to one typed actionable wake
ok - Pi streaming-time wake delivery keeps the successor chain and replays only unconsumed wakes
ok - Pi retries a verified successor that failed during wake delivery once that delivery settles
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.84.4
ok - real Pi SDK 0.84.4 queues a streaming-time watcher wake without before_agent_start, keeps the successor chain, and surfaces consumption of both follow-ups

The live probe loads the tracked watcher extension through Pi's real resource loader into a real AgentSession whose only provider is a local fake with its fetch intercepted in-process and held open mid-stream. It proved that a follow-up the extension sends while main is streaming raises no before_agent_start at queue time or when the run reaches it, joins the run as a user message_start carrying the exact wake text in its own model turn, and is followed by a verified successor and delivery of the next close; a follow-up sent to the idle main raises before_agent_start with the exact text before its user message_start. The portable regression drives the same shape with a fake main that never raises before_agent_start while streaming, then proves a replacement replays only the follow-up Pi had not consumed and that an exhausted restoration delivers its typed failure without launching a further arm. A second regression holds a branch settlement open while the verified successor exits with a failure, and proves that failure takes the ordinary bounded retry once the delivery settles rather than leaving the generation with no watcher and no retry.

2026-09-04 off-thread supervision outcome delivery

The real-TUI responsiveness guard, focused extension suite, store suite, and strict typecheck were run on macOS 26.5.0 arm64, Node v24.13.1, tmux 3.6a, against the signed Pi launcher 0.82.0 for the TUI arms and the npm @earendil-works/pi-coding-agent 0.81.1 package for the typecheck. The lab used a scratch FM_HOME, a scratch project holding a copy of the tracked extension, a private tmux socket, a scratch session directory, and --offline; only /new was ever sent, so no model turn ran and no request left the machine, and the captain's own Pi session was not touched.

FM_PI_BRANCH_RESPONSIVENESS_E2E=1 bash tests/fm-pi-branch-responsiveness-live-e2e.test.sh
bin/fm-test-run.sh tests/fm-pi-branch-extension.test.sh
bin/fm-test-run.sh tests/fm-branch-supervision.test.sh
npm exec --yes --package=typescript@5.9.3 -- bash tests/fm-pi-primary-types.test.sh
pi 0.82.0 keystroke echo, worst observed: floor 22.6 ms, extension idle 35.6 ms, extension delivering 36.8 ms
ok - supervision outcome delivery keeps the real Pi 0.82.0 TUI echoing keystrokes at its unloaded floor
ok - outcome delivery keeps the event loop running and interleaved reports stay ordered and exactly once
ok - a session replaced mid-delivery cancels cleanly and the stored outcome still arrives exactly once
ok - a failing store script surfaces to the branch and its outcome is neither lost nor delivered twice
ok - a failed cursor write re-delivers a routine note exactly once more while a captain outcome stays deduplicated
skip: installed Pi 0.81.1 predates the stock renderer contract 0.84.4 this case compares against
ok - tracked Pi extensions pass strict no-emit typecheck against Pi 0.81.1

That skip is the renderer case declining to render a verdict on a Pi older than the contract it compares against: since 0.84.4 the stock renderer no longer supplies an implicit reset at multiline boundaries and the extension emits that reset itself, so an older installed Pi differs legitimately. It names the installed version and the floor rather than degrading quietly, and a package whose version cannot be read at all is still a failure.

The same guard against the pre-change extension in the same lab measured a 676.9 ms worst keystroke echo while delivering two outcomes and a 295.3 ms worst echo with nothing to deliver, against a 49.2 ms extension-free floor, and failed as designed. Measured through the same real fm_branch_report tool and real bin/ scripts with a 1 ms interval timer, the largest single block of the JavaScript thread fell from 273 ms to 2.0 ms for a routine outcome, from 286 ms to 2.0 ms for a captain outcome, and from 134 ms to 1.9 ms for main's acknowledgement, against a 1.3-2.2 ms idle-loop floor. Those absolute figures are specific to this host and Pi version; the guards assert the relationship (delivery must stay in the class of the same machine's own floor) rather than a remembered millisecond number.

Native Codex through Pi

Verified on 2026-09-08 with Pi 0.85.1 and the installed pi-codex-native 0.2.1 adapter. Run this token-free guard after updating Pi, Codex, or the adapter:

FM_PI_CODEX_NATIVE_LIVE=1 bash tests/fm-pi-codex-native.test.sh

Observed result: "result": "PASS". The guard runs the real Pi runtime, native adapter, three FirstMate primary extensions, native MCP transport, and FirstMate's durable outcome scripts in an isolated home. It verifies native ultra on initial and operational turns and after restart, startup operational input, watcher arming, a notification while main is idle, outcome read and one acknowledgement, refusal of a duplicate acknowledgement, and no reprocessing after restart. Its native App Server peer and watcher-close process are deterministic fixtures; it does not claim a real backend or a live model was tested by that command. tests/fm-busy-state.test.sh, tests/fm-busy-adapter-wiring.test.sh, and tests/fm-watch-triage.test.sh cover separate progress notification, unchanged semantic busy state, rejection of a superseded worker's events, and progress refreshing the busy-age bound without fabricating a completed turn.

Oh My Pi (omp)

omp runs crewmate, scout, secondmate, and primary work; supervision.md owns the primary evidence. The evidence below was produced on 2026-09-05 against omp 18.1.11 (~/.local/bin/omp, a Bun-compiled single binary) on macOS 26 arm64 through the Herdr backend with the openai-codex/gpt-6-astra model, building on the 2026-09-02 adapter investigation against 18.1.2.

Process identity and markers

ps -o comm= reports the bare name omp for the agent process, from both its ! bash path and the model's bash tool, so identity is the anchored name; ompd and comp never match. omp publishes no harness marker: PI_CODING_AGENT is absent from the binary, and the default profile sets neither PI_CODING_AGENT_DIR nor OMP_PROFILE in the process environment. FM_OMP_HARNESS=omp is Firstmate's own launch marker and wins over an inherited CLAUDECODE only under a real omp ancestor; tests/fm-omp-harness.test.sh pins both directions with real processes.

Composer

Under the captain's unicode symbol preset the idle screen through Herdr was a bare (U+276F) row followed directly by the status row:

❯
 π  · ◔ GPT-6-Astra · 🌳 …-workspace · ⑂ detached · ◫ 15.4%/272K ⟲ · (sub)

Before the status-row rule the shared classifier folded that row into the bare composer's wrap region and read the idle pane pending, so bin/fm-send.sh skipped its doorbell on the first live omp worker. After the rule, the same live Herdr capture read empty, a steer's doorbell landed, and the worker opened a turn on it. tests/fm-composer-lib.test.sh pins the unicode idle row, the nerd-preset idle row, the busy spinner row, and typed text over the same fixture in both locales.

Busy state and lifecycle

Fact Observed
Semantic source omp-ext: busy source=omp-ext event=agent-start on the brief, idle source=omp-ext event=agent-end at its natural end, busy again on a steer, idle after a control-plane interrupt
Rendered busy row ⎋ Working… (U+2026) above the composer and a braille spinner plus elapsed cell (⠧ 36s) in the status row; the omp busy regex accepts only those two TUI signals, not the Working... that headless -p writes to stderr, since no supervised omp pane runs headless
Interrupt bin/fm-control.sh <id> interrupt delivered a single Escape (verified=agent-alive cancel=unconfirmed), the composer read empty, and omp raised agent_end
Exit bin/fm-control.sh <id> exit typed /quit; Herdr then reported the pane dead
Extension loading a file named both by -e and by <cwd>/.omp/extensions loads twice; discovery is top-level and cwd-only
Extension tools the openai-codex model invokes a registered tool by writing xd://<tool> through omp's virtual-file bridge

End-to-end

A throwaway scout was spawned through bin/fm-spawn.sh --scout --harness omp --model openai-codex/gpt-6-astra --effort low on Herdr and driven to completion:

  1. the launch delivered its brief positionally under the tracked posture overlay and the agent executed it with no approval prompt;
  2. the busy record moved seed, busy, idle exactly as the extension contract states;
  3. the report landed and the done: status line was appended;
  4. bin/fm-send.sh rang the doorbell once the composer read empty, and the worker opened a turn on the inbox record;
  5. bin/fm-control.sh <id> interrupt cancelled the running turn;
  6. bin/fm-control.sh <id> exit stopped the agent and bin/fm-teardown.sh returned the worktree and closed the item.

FM_OMP_LIVE_E2E=1 tests/fm-omp-primary-live-e2e.test.sh refreshes the primary evidence; the worker path above is refreshed by repeating the scout dispatch after any omp upgrade.