Skip to content

feat(terminals): terminal-driver axis v1 → peek/poke on tmux/herdr/plain - #1014

Closed
fujibee wants to merge 67 commits into
integration/terminal-driver-v1from
feat/terminal-driver-v1
Closed

fujibee wants to merge 67 commits into
integration/terminal-driver-v1from
feat/terminal-driver-v1

Conversation

@fujibee

@fujibee fujibee commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Terminal-driver v1 — a new terminals axis (tmux / herdr / plain) so the ONE terminal a member's CLI runs under is abstracted behind a driver contract. peek and poke work on tmux and herdr; on plain (an OS-terminal window with no addressable pane) both are a declared unsupported — poke points the caller at the agent type's native channel, peek is a dead end (today's CLIs expose no read path). Single branch feat/terminal-driver-v1, base main (a non-main base does not run CI, so everything stacks here).

Scope, in one line: move the scattered $TMUX / HERDR_* branches behind a terminal_* contract, place/peek/poke/despawn a member through whichever terminal it runs under, and — where a terminal cannot confirm something — say so, never imply success.

The axis

drivers/terminals/{tmux,herdr,plain}/ (terminal.conf DATA + ops.sh sourced), a terminals facade over driver-registry.sh, structurally clobber-proof loading (unset-all + verify-required, one shared cleanup; errexit lifted around the source so bash 3.2 agrees with 5), and the <terminal>:<id> record scheme (legacy bare %N/@N reads as tmux; first-colon split preserves a herdr id's own :).

Exit taxonomy, uniform across peek/poke on every driver: 10 unreachable (the CLI is not on PATH) · 12 the pane cannot be served (gone, unreadable, or — for poke — has no live agent to receive) · 13 the driver has no such path at all (plain's permanent "no addressable pane"). Only 13 is permanent; a template that reads 13 as "this terminal can never do this" must not receive it for a transient loss. Control ops print a status word on stdout and the reason on stderr; record ops print the id on stdout and nothing else.

Detection & resolution

terminal_detect reports two facts and decides nothing — PRESENCE (exit code) and SELF-ID (stdout, may be empty, reason on stderr). Two callers decide:

  • resolve-for-placement (spawn): the terminal NAME only.
  • resolve-for-name (terminal_name/SessionStart): prefers a candidate that produced a pane id over one that only claimed presence (id-producer wins; declaration order is the tiebreak). Fatal only when no candidate produced a nameable id, with EVERY present candidate's reason — so a genuinely broken herdr fails loudly, never silently to plain. Fixes the nested herdr-in-tmux case.

The override AGMSG_TERMINAL_DRIVER / --terminal-driver is a spawn/name PREFERENCE on a NEW surface (tmux|herdr|plain), distinct from --terminal / AGMSG_TERMINAL (the OS-terminal command TEMPLATE, untouched).

Drivers

  • tmux — faithful pre-axis argv; terminal_name sets @agmsg_agent=<team>:<agent>; spawn validates the id KIND (%N pane / @N window) before returning it. peek/poke use the shared taxonomy (not on PATH → 10, capture/send failure → 12); capture-pane streams to stdout verbatim.
  • herdragent list resolution keyed on agent_session.value (the entry is an OBJECT; MEASURED live, herdr 0.8.0), decided by decidable membership: an entry is decidable if it is a session entry (agent_session object with a text .value) OR a session-less pane recognized by STRUCTURE — not by a value or a key-name set, both of which drift while the pane lives (agent_status changes state; name/display_agent come and go): agent_session KEY ABSENT, a valid pane_id, the fixed identity anchor (agent/terminal_id/tab_id/workspace_id, measured always-present) present, and NO field object- or array-valued. That last condition catches a session moved to a renamed OBJECT or ARRAY key (future_session:{…}, future_sessions:[…], session_ids:[…], inner shape irrelevant) — the measured agent_session is an object, so a structured value where none belongs fails it and the target it hides is never reported not-among. It does NOT catch a session flattened to a SCALAR (a renamed key whose value is the bare target string): that is an unidentifiable residual, named not hidden — the deliberate cost of allowing unknown scalar extensions, which is required because name / display_agent are real scalar fields that come and go, and a named bare pane must still reach not-among. A bare {} or a shape missing the anchor is did-not-answer. The pane-id grammar w<n>:p<x> is one shared authority (resolver SQL + _herdr_pane_id_ok, cross-checked). terminal_peek captures to a temp file, checks rc, then cats the bytes so the pane text reaches stdout VERBATIM (herdr writes its error JSON to stdout on failure — that body goes to stderr). terminal_name's internal key is a collision-resistant SHA-256 derivation (newline-framed; not a literal separator a name can contain).
  • plain — the OS-terminal spawn, moved from spawn.sh INTO the driver (each backend's stdout isolated so only the - placement id reaches the record op's stdout); despawn no-op; peek/poke/name unsupported (no addressable pane), reported as 13 with a reason, not a silent ok.

The peek/poke asymmetry (measured)

peek needs only a pane; poke needs a LIVE agent. On the real machine, peek reads a herdr pane whose agent has exited (rc 0, content returned); poke to that same pane fails, because there is no running agent to receive — it returns 12, not 13. So a member whose agent has exited can be peeked but not poked; the 9 type templates say this where an operator meets it.

Wiring

  • spawn.sh places tmux, herdr AND plain through the drivers (--terminal-driver validated early; $TMUX-first → herdr → OS-terminal detection preserved). It records tmux:%N / herdr:%N via agmsg_terminal_ref; plain has no addressable pane, so it records nothing.
  • Startup is confirmed before it is claimed, and the boot is not typed into a busy shell. This closes a real incident: spawned printed while the agent had not started, because a startup shell prompt ate the first keystroke of the boot. Two guards, before and after typing:
    • Before (herdr only): the boot is typed only once the pane's shell is at its prompt. The signal is structural and environment-independent — herdr pane process-info's foreground_process_group_id == shell_pid (the foreground process group is the shell itself; no prompt string matched). Three outcomes: READY types; NOT-READY (a foreground process running) waits within a bound, then closes the pane and fails rather than lose a keystroke; UNKNOWN (process-info could not answer, validated positively — ""/null/malformed are not "ready") types but warns, worded apart from the after-typing message. It is necessary, not sufficient: a shell reading its OWN prompt returns equal, so a keystroke can still be lost; that residual is caught after typing, said so in the code.
    • After: each placement line says launched … in <terminal> — a placement fact — never spawned. status=ready (the member's watcher attached — a positive observation) is the only proof of startup; a type with no readiness handshake (monitor=no) prints status=launched-unconfirmed note=no-readiness-handshake. The after-typing verdict wins: a watcher that attaches makes it ready regardless of the before-typing warning.
  • Placement records are written atomically (agmsg_write_atomic, temp + rename — the helper six other scripts already use, not a seventh copy). A failed write leaves the previous correct record whole; status=spawned-but-unrecorded is reported only when the write genuinely could not land, never on top of a record it just truncated. The two record writers (spawn.sh and agmsg_terminal_name_self) were the only raw > into a placement record — the rest of the raw > under run/ are ephemeral pidfiles, overwrite-by-design.
  • despawn.sh tears down the recorded placement through terminal_despawn and returns 0 only on CONFIRMED teardown; --force on an unconfirmable teardown keeps the record with status=error note=force-teardown-unconfirmed. despawn reports status=ok for a member it did not tear down, then deletes the placement record its own advice needs #625 defects 1-2: a free actas lock with a placement record is not "gone" — it reports status=needs-force and KEEPS the record.
  • spawn --split targets the active tmux window instead of the caller's #990: a tmux --split targets $TMUX_PANE (the caller's pane), and fails closed if $TMUX_PANE is unset — never the attached client's active window.
  • join / actas / SessionStart name the pane via agmsg_terminal_name_self (record written only on ownership-proven paths — actas/SessionStart, not join).
  • peek.sh / poke.sh / watch.sh guard the ref parser: it fails closed on a corrupt/unknown-scheme ref, so each caller reaches its own failure contract instead of dying at a bare set -e assignment (watch.sh gives an unresolvable ref its own logged branch). On an unsupported (13), poke.sh lets the driver's reason (with its native-channel pointer) be the final line rather than covering it with a generic summary. poke takes --body-file <path> and --body - (stdin) so a shell-hostile body arrives verbatim.
  • 9 type templates and delivery.sh status show the resolved terminal.

Fact boundary (herdr)

MEASURED (live, herdr 0.8.0): the agent list shape (agent_session object, id at .value); the session-less pane structure (agent_session key absent, the identity anchor always-present, all values scalar — recognized by structure, not a drifting value/key-name set); the pane-id grammar; pane process-info's shell_pid / foreground_process_group_id (the readiness gate); the peek/poke live behaviour; and that agent start / agent wait do NOT fit the pre-input gate (recorded in the code so the search is not repeated). ASSERTED, not yet measured against a live call: herdr agent prompt's poke argv and herdr agent rename's name argv (no rename call in main to measure against). One residual is UNMEASURED and said so: whether the readiness gate catches a shell reading its own startup prompt (no child process) — that keystroke-eaten case is caught after typing, not by the gate.

Rides on this branch, declared

  • The errexit-status-reads checker (.github/scripts/check-errexit-status-reads.sh + baseline): a PRODUCT of this work — the same bash-3.2 set -e defect surfaced repeatedly here, so a checker was written to stop the next one. Baseline currently 1.

Tests

tests/test_terminal_registry.bats (68) — resolution (placement/name, id-producer order, override), the herdr membership/grammar/framing/structural-bare-pane rounds (each mutation-verified), the record scheme, the atomic-write-preserves-old-record contract, ABI-completeness, structural clobber, fail-closed, tmux id-kind + --split. tests/test_peek_poke.bats (27) — peek AND poke exit taxonomy on both backends × both failures, the three verbatim byte-exact cases, the unsupported-reason-stands entry rule, the safe poke body, and the corrupt-ref caller guards. tests/test_spawn.bats (88) — the wiring, the atomic placement write, startup-confirmed vs launched-unconfirmed, and the pre-input readiness gate's three arms (ready / not-ready-fail / unknown-warn, with positive validation). tests/test_despawn.bats (15) and tests/test_watch.bats (23, incl. the self-despawn ref guard) green. Enforced-assertions (635) and errexit-status-reads (1) at baseline.

The final gate

One live-matrix run on the maintainer's machine — the three terminals × the two entry paths (spawn-placed, hand-joined) plus the control cells — is the one thing the fake-binary suite cannot answer, because it exercises the real herdr/tmux CLIs this driver only asserts the argv for. It is a manual pre-merge check, not part of the automated suite.

fujibee added a commit that referenced this pull request Aug 30, 2026
…r-proofing, fail-closed resolution, no ambient config (#1014)

co1's four groups + two follow-ups:

(1) ABI completeness / clobber. plain declared capabilities=spawn but ops.sh had
    no terminal_spawn; and switching drivers left the previous driver's ops in the
    shell. Now: plain implements terminal_spawn as a faithful driver-ification of
    the OS-terminal launch (returns '-', no addressable pane); despawn/peek/poke/
    name stay unsupported. And agmsg_terminal_load is structurally clobber-proof —
    it unsets ALL terminal_* before sourcing and VERIFIES every required ABI
    function is defined after, so an incomplete driver fails loudly instead of
    borrowing a leftover. The required set is named ONCE (_AGMSG_TERMINAL_REQUIRED)
    and reused by both the loader and the detect subshell, which also unsets the
    same set so a candidate missing terminal_detect cannot be judged by an
    inherited one. On verify failure nothing partial is left behind.

(2) fail-closed resolution. tmux terminal_detect now requires a non-empty
    $TMUX_PANE ($TMUX alone is not a match — no invalid 'tmux:' record). The
    resolver requires a non-empty self id for an auto-detect match, and an
    override that names no real driver fails loudly instead of resolving to
    '<typo>\t' (agmsg_terminal_dir existence check).

(3) prose. herdr comments now put 'pane read --source' on the MEASURED side
    (seat 0 measured the --source values), matching the PR body; only 'agent
    prompt' argv and the agent-list JSON field names stay ASSERTED.

(4) no new ambient config surface (the #1004 lesson). The tmux/herdr split
    direction moved from AGMSG_TMUX_SPLIT / AGMSG_HERDR_SPLIT into the op's
    <target> argument (window | pane-h | pane-v). poke's gap and arrow are no
    longer env knobs (AGMSG_POKE_GAP / AGMSG_POKE_ARROW removed) — the gap is part
    of the behavior, the arrow is hardcoded Right.

Naming note flagged to tl: the resolver override env is AGMSG_TERMINAL_DRIVER,
NOT AGMSG_TERMINAL — the latter is already the OS-terminal command template read
by plain's spawn, so reusing it would collide.

Tests: 25 total. Added ABI-completeness (every driver defines every required
verb; capabilities= verbs are implemented), structural clobber (tmux->plain runs
plain's ops, not tmux's; an incomplete external driver fails the load leaving
nothing behind), fail-closed (empty $TMUX_PANE is not tmux; a typo override
fails loudly), and plain OS-terminal spawn. Mutation-verified: skipping the ABI
verification and skipping the override existence check each redden their test.
@fujibee
fujibee changed the base branch from main to integration/terminal-driver-v1 August 31, 2026 05:32
…me, and tmux/herdr/plain drivers (#terminal-driver v1, PR1)

New driver axis "terminals": the ONE terminal a member's CLI runs under (tmux,
herdr, or plain), behind a locked ABI, so the terminal operations currently
inlined as $TMUX / HERDR_* branches across spawn/despawn/watch can move behind a
contract and peek/poke can be added. This PR is new files only — nothing existing
is rewired yet (that is PR2/PR3), so no existing behavior can regress.

Contract (5 verbs + terminal_name, per docs/spec/driver-interface.md):
  terminal_check / terminal_describe   ABI-required (deps, metadata)
  terminal_detect <session_id>         RECORD op: ONE observation returns BOTH
                                       which terminal we are under AND this
                                       session's own pane id. Deliberately not two
                                       ops: splitting them lets state change
                                       between the two observations (the defect
                                       shape corrected three times the same night
                                       — _wait_role_count, #1008's four reads).
                                       herdr resolves the pane from the session id
                                       via `agent list` (inherited HERDR_PANE_ID
                                       is NOT trusted); tmux uses $TMUX_PANE; plain
                                       is the exit-0 fallback.
  terminal_spawn / despawn / peek / poke / name

Registry (scripts/lib/terminal-registry.sh): a terminals facade over
driver-registry.sh. terminal.conf is DATA read by a clone of agmsg_type_get;
ops.sh is sourced into the caller. Resolution: --terminal / AGMSG_TERMINAL >
detection (order herdr > tmux > plain, so it never fails) — the single answer
that ends the historical $TMUX-vs-HERDR_* dual system; detection sources each
candidate in a subshell so terminal_* definitions do not clobber across
candidates. Record ref is <terminal>:<id>; reading tolerates the pre-axis forms
(a bare %N/@n reads as tmux, the old herdr:<id> still reads as herdr; first-colon
split so a herdr id's own ':' survives).

Drivers: tmux (faithful to the pre-axis argv), herdr, plain (peek/poke/despawn/
name -> status 13 unsupported, reason on stderr). plain's detect is the fallback.

Tests (tests/test_terminal_registry.bats, 18): resolution incl. precedence and
order, record scheme incl. legacy/inner-colon, conf reader, and every driver op
against fake tmux/herdr binaries that record argv. The fake binaries record that
a binary was CALLED, not that the RIGHT driver was selected — so the resolution
tests assert the returned terminal NAME, and a selection->op test loads the
resolved terminal and asserts the op reaches the herdr binary and never tmux, so
a wrong selection cannot pass as "argv as expected". Mutation-verified: a
single-burst text+Enter poke (dropping the #619 arrow), and a tmux-before-herdr
detection order (which reddens both the order test and the selection->op test),
each redden their test.

MEASURED vs ASSERTED (herdr): the pre-axis calls (pane split/rename/run, tab
create, pane close), `agent list` being JSON, name encoding team__name, and
`pane read --source visible` are grounded in seat-0 measurements and the existing
tree. The EXACT argv of `herdr agent prompt` (poke) and the `agent list` JSON
field names used to extract the pane (agent_session / pane_id) are asserted from
the assignment brief and verified only by the live matrix on the real CLI, not
here; the fixtures pin the control flow and the argv this driver emits, so a
real-CLI mismatch is a localized one-line fix the matrix catches.

Source of the scope (no committed scope/naming-map doc exists): the assignment
messages, the seat-0 measurements, and the existing code + driver ABI.
…r-proofing, fail-closed resolution, no ambient config (#1014)

co1's four groups + two follow-ups:

(1) ABI completeness / clobber. plain declared capabilities=spawn but ops.sh had
    no terminal_spawn; and switching drivers left the previous driver's ops in the
    shell. Now: plain implements terminal_spawn as a faithful driver-ification of
    the OS-terminal launch (returns '-', no addressable pane); despawn/peek/poke/
    name stay unsupported. And agmsg_terminal_load is structurally clobber-proof —
    it unsets ALL terminal_* before sourcing and VERIFIES every required ABI
    function is defined after, so an incomplete driver fails loudly instead of
    borrowing a leftover. The required set is named ONCE (_AGMSG_TERMINAL_REQUIRED)
    and reused by both the loader and the detect subshell, which also unsets the
    same set so a candidate missing terminal_detect cannot be judged by an
    inherited one. On verify failure nothing partial is left behind.

(2) fail-closed resolution. tmux terminal_detect now requires a non-empty
    $TMUX_PANE ($TMUX alone is not a match — no invalid 'tmux:' record). The
    resolver requires a non-empty self id for an auto-detect match, and an
    override that names no real driver fails loudly instead of resolving to
    '<typo>\t' (agmsg_terminal_dir existence check).

(3) prose. herdr comments now put 'pane read --source' on the MEASURED side
    (seat 0 measured the --source values), matching the PR body; only 'agent
    prompt' argv and the agent-list JSON field names stay ASSERTED.

(4) no new ambient config surface (the #1004 lesson). The tmux/herdr split
    direction moved from AGMSG_TMUX_SPLIT / AGMSG_HERDR_SPLIT into the op's
    <target> argument (window | pane-h | pane-v). poke's gap and arrow are no
    longer env knobs (AGMSG_POKE_GAP / AGMSG_POKE_ARROW removed) — the gap is part
    of the behavior, the arrow is hardcoded Right.

Naming note flagged to tl: the resolver override env is AGMSG_TERMINAL_DRIVER,
NOT AGMSG_TERMINAL — the latter is already the OS-terminal command template read
by plain's spawn, so reusing it would collide.

Tests: 25 total. Added ABI-completeness (every driver defines every required
verb; capabilities= verbs are implemented), structural clobber (tmux->plain runs
plain's ops, not tmux's; an incomplete external driver fails the load leaving
nothing behind), fail-closed (empty $TMUX_PANE is not tmux; a typo override
fails loudly), and plain OS-terminal spawn. Mutation-verified: skipping the ABI
verification and skipping the override existence check each redden their test.
…re cleanup, herdr target, plain OS-terminal spawn, name mechanism)

Found the authoritative scope (memory/design/2026-08-28-terminal-driver-v1-scope
.md rev2) and aligned PR1 to it, plus co1's second-round findings.

co1 round-2:
- (1) source-failure cleanup. agmsg_terminal_load's source-failure arm now routes
  through the SAME cleanup as the missing-function arm (unset all terminal_*,
  empty the loaded marker) so a source that defines some functions THEN fails at
  runtime leaves nothing partial behind. Test uses a fixture that defines
  functions then returns non-zero (a parse error would define nothing and not
  exercise this) — mutation-verified.
- (3) herdr spawn target validation. An unknown target is now status 13 (a typo no
  longer silently becomes pane-v); a window target without HERDR_WORKSPACE_ID
  fails explicitly instead of silently splitting a pane. Both pinned + one
  mutation-verified.

Scope alignment:
- plain implements spawn/despawn (scope: "plain implements spawn/despawn"). Its
  terminal_spawn is a faithful move of spawn.sh's OS-terminal launchers ({cmd}
  template wins on any OS; macOS current-terminal / app hint; Linux/Windows reject
  a template without {cmd} and reject headless / unknown OS), returning '-' (no
  addressable pane). terminal_despawn is a no-op ok (an OS window has no handle;
  it closes with its process, as before the axis). peek/poke/name stay
  unsupported. The launch template reads AGMSG_TERMINAL (its EXISTING meaning),
  which is distinct from the resolver override AGMSG_TERMINAL_DRIVER — no collision.
- terminal_name follows the scope's Naming section: canonical separator ':' (both
  team and agent commonly contain '-'). tmux sets a pane user option
  @agmsg_agent=<team>:<agent> as the RESOLVABLE key (tmux is never targeted by
  name — '-t a:b' is session:window) plus select-pane -T as the visible copy.
  herdr does a visible `pane rename <team>:<agent>` plus an internal `agent
  rename` with the label folded to herdr's [a-z][a-z0-9_-]{0,31} agent-name regex.

Open for tl: the override env is AGMSG_TERMINAL_DRIVER, not AGMSG_TERMINAL (scope
names the latter, but it is already the OS-terminal template) — a contract
decision raised with tl. Tests: 29, all green; enforced-assertions at baseline.
… round-3; tl override ruling)

co1 round-3 was all contract-consistency (code vs its own descriptions):

1. Contract said one thing, code did another — aligned to the code (the final
   contract):
   - plain/terminal.conf's HEADER still carried the reverted "spawn/despawn
     unsupported, no capability, OS launch stays in spawn.sh" text directly above
     `capabilities=spawn despawn`. Removed the stale header; plain SPAWNS an OS
     window and DESPAWNS as a no-op, per the scope.
   - The resolver override is AGMSG_TERMINAL_DRIVER (a NEW surface), not the
     existing --terminal / AGMSG_TERMINAL (which stay the OS-terminal command
     template, untouched) — tl's ruling 2026-08-31 ("a new axis gets a new name").
     The error message no longer calls arg2 "--terminal".
   - The override is documented as a spawn/name resolution PREFERENCE: ops on an
     existing member (despawn/peek/poke) read the terminal from the placement
     record, never the env, so a forced driver whose detect fails still resolves
     (driver + empty id) — "spawn into tmux" is valid off-tmux, and a record's
     terminal is never overridden by an inherited env (scope L96-98; tl leaning).

2. The plain template test now proves the BOOT reaches execution THROUGH the
   {cmd} template: the fake boot records that IT ran, so a dropped/garbled {cmd}
   leaves the marker absent (mutation-verified red) instead of the old "touch a
   marker, ignore {cmd}" that passed regardless.

3. terminal_name's `herdr agent rename <id> <folded>` is a new exact argv with no
   existing measured path in main — added to the ASSERTED (pending-live-matrix)
   list in the driver header, alongside agent prompt and the agent-list fields.

Tests: 29, all green; enforced-assertions at baseline 635. The PR body is being
updated to match (override env, plain capabilities, test count, name mechanism).
… not 'pending' (co1 round-4)

The resolver's override comment still said the surface was 'a tl decision,
pending' and the flag 'not wired until that ruling', while the same head's PR
body already stated tl's 2026-08-31 ruling (new axis -> new name:
AGMSG_TERMINAL_DRIVER / --terminal-driver; existing --terminal / AGMSG_TERMINAL
unchanged). A locked-ABI comment must not call a settled contract undecided.
Comment-only; code byte-identical to b8ac192.
…s not escape the guard (co1 source-failure test, macOS CI)

The source-failure cleanup test (co1's own proof) failed on macOS-only in CI:
bash 3.2 fires the caller's set -e from a failing command at the top of a sourced
file even though the source sits on the left of a guard, so the failure trace
escaped agmsg_terminal_load's '|| rc=$?'. Lift errexit around the '. ops.sh' and
read its status separately (the codebase's two-line set +e / set -e pattern, cf.
check-inbox.sh), preserving the caller's original errexit state. Verified: the
test's probe passes on bash 3.2 AND bash 5, cleanup runs, errexit is preserved.
@fujibee
fujibee force-pushed the feat/terminal-driver-v1 branch from 9363be6 to e78d66a Compare August 31, 2026 05:40
@fujibee fujibee changed the title feat(terminals): terminal-driver axis — registry, record scheme, tmux/herdr/plain drivers (v1 PR1) feat(terminals): terminal-driver axis v1 → peek/poke on tmux/herdr/plain (integration) Aug 31, 2026
@fujibee
fujibee changed the base branch from integration/terminal-driver-v1 to main September 1, 2026 05:41
…rue 3-state herdr detect (co1 round-5)

co1's three, all the "one value answering two questions / don't conflate" shape:

1. herdr PRESENCE is HERDR_ENV=1 ALONE. Whether `herdr` is on PATH is a
   terminal_check question ("can I operate it"), not "which terminal am I in".
   Requiring the binary for presence made a herdr session with no herdr on PATH
   place/name as tmux or plain. Now presence = HERDR_ENV; a missing binary yields
   an empty self-id + reason (folded into "agent list did not answer"). Control:
   HERDR_ENV=1, herdr absent (restricted PATH), TMUX also set -> placement picks
   herdr; naming is fatal with a reason (does not fall through). Mutation-verified.

2. resolve_name's reason read no longer leaks errexit. `reason=$([ -f x ] && cat)`
   is a bare failing status when x is /dev/null (mktemp failure) — under set -e it
   exits the caller before the verdict, the same leaf-leak shape fixed for the
   driver source. Guarded with an `if` and cat's status swallowed. Control: force
   mktemp to fail under set -e; the caller still reaches the non-zero verdict.

3. herdr detect distinguishes THREE states, as its comment (and tl's ruling)
   claim: _herdr_pane_for_session now returns 2 for "could not answer" (herdr
   absent / list errored), 0+pane for a match, and 0+empty for "answered but this
   session is not among the live agents" — the last was previously unreachable
   (both collapsed to return 1). Controls assert the two reasons differ.

Tests: 34, all green; enforced-assertions at baseline 635.
…er ops

peek.sh <team> <name> [--lines N] resolves the member's placement record
(run/spawn.<team>__<name>), loads the RECORDED terminal's driver — never
the caller's environment, per the v1 ruling that an override applies to
resolution and not to something already placed — and prints
terminal_peek's output verbatim, propagating its exit status (plain's
'unsupported: <why>' stays non-zero and on stderr, never a quiet 0).

poke.sh <team> <name> <text> does the same resolution and hands the
submission to terminal_poke. How submission happens is the driver's
contract: tmux sends the text and, in a separate later burst, an arrow
key + Enter (#619 — same-burst text+Enter is classified as a paste and
the Enter becomes a newline); herdr's agent prompt submits by itself.
Multi-word unquoted text is refused rather than silently truncated, and
a missing record refuses before any terminal binary runs.

Tests pin the argv SHAPE against fake tmux/herdr binaries: exactly two
tmux invocations, the first the literal text with no Enter (asserted by
whole-line equality, so an Enter rejoining the text burst goes red —
verified by mutating the driver to a single burst and watching the test
fail), the second Right+Enter; herdr exactly one invocation with the
inner-colon pane id intact. What argv cannot see, and the live matrix
must: the inter-burst gap (a sleep) and whether the real Codex reads
the result as a submission. No non-last [[ ]] or negated commands — the
enforced-assertions baseline is unchanged.
…d the peek/poke surface

Three shapes, one family, all measured on bash 3.2.57 (macOS /bin/bash) and
5.3.15 rather than assumed:

  x=$(cmd); rc=$?          shell dies at the assignment on BOTH — the $? line
                           is unreachable and its handler has never run
  local x=$(cmd); rc=$?    survives on both, and rc is ALWAYS 0 — a handler
                           that reads as present and can never fire
  . file; rc=$?            bash 3.2 fires the CALLER's errexit even with
                           `|| rc=$?` on the source line; bash 5 does not.
                           macOS-only death that passes every Linux run.

check-errexit-status-reads.sh reports all three as a count against a baseline
that may only go down, following check-enforced-assertions.sh. Statements
between `set +e` and `set -e` are not reported: that lift is the accepted fix
(agmsg_terminal_load), and a checker that flagged the fix could never be
burned down.

It cannot pass by failing to look. Before reading the tree it scans a fixture
holding one known-bad instance of each kind and requires all three back, plus
the two correct forms absent; a scanner that stops matching exits 2 (could not
answer) instead of 0 (found nothing). Verified by mutation: breaking the
assignment regex gives exit 2, not a green run.

Wired into tests.yml as its own unconditional job, for the same reason the
enforceable-assertions job is unconditional.

Baseline is 2 — the two live instances in the herdr driver, which the checker
found on its own run rather than being told about.

Surface: `/agmsg peek <name>` and `/agmsg poke <name> <text>` documented in
all 9 type templates, identical text in each (one anchor, verified 9/9 by
block hash), and `delivery.sh status` now names the resolved terminal in
three distinguishable states — resolved with a pane, present but unable to
identify this pane, and unknown. The middle one is printed separately because
it is the state where naming and peeking fail while everything else looks fine.
Written against despawn's shape before the entry points existed; the landed
peek.sh/poke.sh take no caller name. Corrected in all 9 templates, still one
identical block (verified by hash, 9/9).
…d-6)

herdr terminal_name: replace the fold of <team>:<agent> (and the spec's
<team>__<agent>) with an injective SHA-256 derivation. Any literal separator
collides because it is legal inside a name (validate.sh forbids only . / \ " [ ]
control chars and a leading -), so ("a-b","c") and ("a","b-c") folded to the same
internal key, and ("a:b","c")/("a","b:c") would too. Derive instead:
'a' + first 24 hex of sha256("<team>\n<agent>"); newline is a control char and thus
forbidden in both names, so the join is unambiguous and the key is injective. The
key satisfies herdr's [a-z][a-z0-9_-]{0,31}. Visible names (pane rename,
report-metadata) stay the free-text <team>:<agent>. Uses the store's canonical
agmsg_sha256 (lib/hash.sh), sourced relative to the driver; a missing SHA-256 tool
leaves the best-effort internal rename skipped, not fatal.

terminal-registry resolve_name: the loop's detect assignment was a bare
`id="$(...)"; rc=$?`, which under bash 3.2 set -e aborts the caller the instant a
candidate is not the current terminal (the common case), before trying the next.
Move it to conditional context (`rc=0; id="$(...)" || rc=$?`), same class as the
herdr helper fix.

tests: assert the naming key is injective for the '-' and ':' fold-collisions
(distinct outputs, not merely "a key is produced"); a bare-call errexit control
that requires the verdict line to PRINT (not just status) under /bin/bash 3.2; a
positive-proof control that exit-0 invalid JSON classifies as did-not-answer, not
no-match. 36 green; enforced-assertions at baseline 635.
The resolver's bare `id="$(...)"; rc=$?` in agmsg_terminal_resolve_name was one of
the two status-reads-after-an-errexit-decision the new checker counts; the prior
commit moved it to conditional context, so the count drops to 1. Lock the baseline
down so it cannot drift back up.
The placement record existed only for members `spawn` had placed, so peek/poke
could never reach a session a human started by hand. This calls the naming step
from the three moments a session takes on an identity, and writes the same
`<terminal>:<id>` record spawn writes, so being addressable no longer depends on
who launched the pane.

SessionStart is not redundant with the other two: herdr drops an agent's name
when the agent exits, so a resumed session is nameless until it is re-applied.

One shared function rather than the same fifteen lines in three scripts —
`agmsg_terminal_name_self` in the registry, with the callers reduced to a line
each. Two forms of one step is how a migration ends up half-applied.

Three outcomes, deliberately not folded together:

  named    the terminal can name a pane and this pane was identified: the driver
           names it, and the record is written afterwards, never before.
  skipped  quiet, 0, no record. Two ways to reach it: the terminal has no `name`
           capability (plain has no addressable pane), or no session id was
           supplied at all. A record whose id cannot be acted on is not a
           placement.
  unnamed  the terminal can name, a session id WAS given, and the pane still
           could not be identified: non-zero with the resolver's reason.

The last distinction is why `join` can call this at all. join has no session id
to give — nothing says which env var carries one for a given type (filed
separately) — and "no input to resolve with" is a different fact from "the lookup
failed". Reported as the latter it would warn on every join under herdr about a
condition nobody can act on; reported as the former, tmux still names the pane
(it needs no session id) and herdr waits for actas or SessionStart, which have
one.

Callers treat a non-zero as a warning and never as a failure of the join, claim
or session start they are performing: naming is additive and must not change what
those commands do. Each source of the registry carries the errexit lift, because
on bash 3.2 a failure inside a sourced file fires the CALLER's `set -e` — a plain
`. x || true` would take the join down rather than skip the naming.

Capability is read from `terminal.conf`, not tested against the driver's name, so
a terminal that grows the ability later needs no change here.

Measured, not assumed: run inside a throwaway tmux server (private socket), a
join under this branch is currently `skipped` rather than `named`, because
`resolve_name` stops at the first terminal that claims presence and herdr claims
it — through inherited HERDR_* env — while returning no pane id, so tmux is never
asked although it answers `%0`. That is the resolver's short-circuit, reported
separately; this layer behaves correctly on both sides of it.

Existing tests unchanged and green: actas_lock 22, actas_integration 14,
role_session 24. Syntax checked on bash 3.2 and 5.
… stays a dead end

koit's design, v1 slice: notifications will one day unify onto poke, and
for a plain-terminal member whose agent type has native messaging the
refusal must not end the conversation. plain's terminal_poke still exits
13 — as a terminal answer 'no pane' is correct — but the reason now says
the member's agent type may offer a native channel and that the type
template says which. Said without asking who the caller is: which
terminal am I in is the driver's question, does this agent have native
messaging is the type's.

peek and poke are deliberately asymmetric, and the reason is measured,
not stylistic: a native write path exists (Claude Code's SendMessage),
but today's CLI has no read path — 'claude logs <id>' serves background
jobs only (interactive ids answer "No job matching") and
'claude agents --json' lists status, never screen content. So
terminal_peek keeps the plain dead-end refusal, and the test asserts the
native-channel pointer is ABSENT from peek's answer, so re-adding one is
a conscious decision. Whether a native poke wakes an idle session is
unverified — delivery and same-turn notice are different facts; the live
matrix measures it.
…tive channel"

plain's poke refuses with 13 but points at the type instead of ending the
conversation; this is the type template answering it, for claude-code only.

Four things, each said because leaving it out is what would cost the next
reader a re-measurement:

  - the channel is this session's own SendMessage (target found via
    ListAgents). There is no pane, and no shell command that substitutes.
  - there is no CLI fallback TODAY, measured: `claude agents --json`
    enumerates but sends nothing, `claude logs <id>` serves background jobs
    only (an interactive id answers "No job matching"), and the daemon
    dispatch path is an unpublished internal. Named so a future send
    subcommand has an obvious place to land.
  - peek has no native equivalent, and the asymmetry is the same measurement:
    a native write path exists, a native read path does not. peek's 13 is
    genuinely the end of the line.
  - whether a native poke WAKES an idle session is unverified. Delivering and
    noticing in that turn are different claims and only the first is shown.

The shared peek/poke block stays byte-identical in all 9 templates (verified
by hash); this is the single type-specific addition, and it is in the one type
that has the channel.
…rder

The release goal (peek/poke on herdr) failed on the real machine: utildev measured
that `herdr agent list`'s agent_session is an OBJECT { agent, kind, source, value }
with the session id at .value, not a scalar. The mock returned a scalar and was
green while resolving zero panes. Fixes, all in cc3's files:

herdr/ops.sh _herdr_pane_for_session:
- Compare .agent_session.value (measured against herdr 0.8.0, read-only), and query
  $.result.agents first (the measured wrapper), keeping the others as fallbacks.
- SCHEMA GATE (co1): a successful json_each is not proof of a recognized list —
  json_each on a valid {} returns 0 rows and would misclassify an unknown schema as
  "answered, not among". Confirm json_type(path)='array' before json_each; count a
  path as queried only when a real array existed. {}/{"unknown":[]} -> did-not-answer;
  only a valid empty ARRAY -> not-among.
- Drop the trailing ';' from the single-statement sqlite SQL: it is unnecessary, and
  the errexit-status-reads checker's statement splitter breaks a line at a ';' inside
  the SQL string, false-flagging the correct `|| rc=$?` form as a bare read.

herdr/ops.sh terminal_name key: narrow the contract wording from "injective" to
COLLISION-RESISTANT (co1: a 96-bit hash of arbitrary input has collisions by
pigeonhole). Newline framing removes the structural '-'/':' ambiguity; uniqueness is
only needed among the dozens of live agents; on the vanishing chance of a collision,
`agent rename` fails non-fatally and the pane record still resolves. Test 19 pins
that the known fold/join collisions do not recur, not injectivity.

terminal-registry resolve_name ORDER (tl, from cc1's nested measurement): prefer a
candidate that produced a nameable id over one that only claimed presence;
declaration order (herdr>tmux>plain) is the tiebreak among id-producers. A nested
herdr-in-tmux makes herdr claim presence though tmux is the real terminal — record
tmux:<pane>. Fatal only when no candidate produced a nameable id AND a non-plain
candidate was present-but-unresolved, printing EVERY such reason — so a genuinely
broken herdr fails LOUDLY instead of falling through to plain's '-' silently.

Tests: real-shape fixture + scalar-shape drift guard; unknown-schema both-sides;
nested/broken-both-reasons/declaration-order controls. 42 green; errexit baseline 1,
enforced-assertions 635. All new guards mutation-verified to redden.
co1 (3rd instance of the same shape): json_type(path)='array' proved only that the
CONTAINER is an array — a non-empty array of the WRONG entry shape ([1], [{}], or the
old scalar agent_session) json_eaches to 0 matching rows and was misread as
"answered, session not among". The answer here depends on the session id being
COMPARED against a real entry, so the positive proof must be "at least one entry of
the EXPECTED SHAPE existed". Three outcomes per candidate array path:
  empty array                    -> answered, no agents (not-among)
  non-empty, 0 expected entries  -> could NOT answer (schema drift, return 2)
  non-empty, >=1 expected entry  -> queried; search the session among them
Expected shape (measured, herdr 0.8.0): entry is object, pane_id is text,
agent_session is object whose .value is text.

tests: the scalar-agent_session drift test asserted the misclassification (not-among)
— corrected to did-not-answer; add the other side (real shape + a different live
session -> not-among) so the pair pins that only a real entry set answers. Mutation:
skipping the entry-shape proof reddens the drift test while the different-session
test stays green. 43 green; errexit baseline 1, enforced-assertions 635.
… record

`agmsg_terminal_name_self` wrote the placement record for every caller, so `join`
wrote one. join is not a claim: the same identity can be joined from a second
session while a first one holds it through actas, and the record is what
peek/poke/despawn resolve a member's pane through. A second pane joining an
already-held identity therefore took the placement over and pointed those
operations at the pane that does NOT have the seat.

The record write becomes the 6th argument, and its default is to NOT write. Only
a caller with positive evidence of ownership passes `record`:

  actas         went through the claim              -> record
  SessionStart  the seat is resolved (ROLE_*)       -> record
  join          no evidence of ownership            -> visible name only

Defaulting to the safe half is deliberate: a caller added later that has not
thought about ownership cannot silently take a placement over.

Showing your own name on your own pane stays harmless and stays in join, so a
hand-started session is still labelled; what it no longer does is declare itself
the seat's placement.

The control asserts the OLD record's SURVIVAL, not that "nothing broke": a
version that wiped the record to empty would pass the weaker form. It also takes
a positive control first — the pane was really named — so a green result cannot
be "the call did nothing". Calibrated: with the record write made unconditional
again, that test and only that test goes red at `[ "$after" = "$before" ]`.

test_terminal_registry 44 passed, 0 failed. actas_lock, actas_integration and
role_session unchanged and green.
co1 (one layer further): ">=1 expected-shape entry" proves some entries are
readable, not that the target session is not hiding in a MALFORMED sibling. A mixed
array [well-formed other-session, malformed] passed the gate (alen=2, well=1) and a
no-match on the well-formed side was misread as "not among". A claim of ABSENCE
against a set requires having compared against EVERY member. So:
  empty array              -> answered, no agents
  every entry well-formed  -> comparable; no match -> not-among
  any entry malformed      -> could NOT answer (the session may be the unread one)
A positive find is still decisive regardless of malformed siblings — we located the
pane. Implemented as: search first (found -> pane), else not-among only when
well == alen, otherwise did-not-answer.

tests: mixed array with target ABSENT -> did-not-answer; mixed array where the target
IS the well-formed entry -> resolves. Mutation: reverting the whole-set comparison to
">=1 well-formed" reddens the absent-target case while the find case stays green.
45 green; errexit baseline 1, enforced-assertions 635.
… set

co1: the absence gate used the 4-condition well-formed predicate, but the
positive-find query searched on only (agent_session object + value match) — weaker.
A malformed entry carrying the target agent_session.value but a NUMERIC pane_id is
not well-formed, yet the search would match it and return pane '123'. The
find-is-decisive claim was not implemented on the find side.

One query now defines the well-formed set once (a CTE) and derives BOTH the count
and the target lookup from it, so the search cannot drift weaker than the count:
entries -> wf (object, text pane_id, agent_session object with text .value) ->
count(entries), count(wf), and the matched pane restricted to wf. Emitted as three
'|'-separated fields.

test: a mixed array whose malformed entry has the target .value but a numeric
pane_id must NOT resolve '123' and must be did-not-answer (a well-formed sibling
makes the set not fully comparable). Mutation: searching `entries` with the weaker
predicate reddens it while the well-formed-target find stays green. 48 green;
errexit baseline 1, enforced-assertions 635.
… quoted string

cc3 hit it on herdr/ops.sh and worked around it by deleting a trailing `;` from
a SQL string. The workaround is harmless; the false positive is not. A checker
that reports the correct form gets routed around, and the next person routes
around it without saying so — at which point it passes while guarding nothing.

Two bugs, both now covered by controls that run on every invocation:

1. The splitter worked line by line, so a double-quoted SQL string that opened
   on one line and closed on another left its `;` looking like a statement
   separator. `x="$(sqlite3 ... "SELECT ...;" )" || rc=$?` was cut in half and
   its `|| rc=$?` guard vanished, so the correct form was reported as bare.

   Quote and `$( )` state now carry across newlines. A command substitution
   opens a FRESH quoting context even inside double quotes, so the quote char
   is pushed on `$(` and restored on `)`. Modelling that as a flat flag is what
   made the first attempt swallow a REAL instance instead — the `"` right after
   `$(` read as closing the outer string. The control caught that before it
   shipped, which is what the control is for.

2. A statement that merely CONTAINS `$?` was treated as a status read, so the
   `|| vrc=$?` on a guarded assignment made the checker report the innocent
   line above it. A status read is now an assignment whose whole value is `$?`.

Controls added (failing any of them exits 2, not 0):
  - a multi-line SQL assignment with `|| rc=$?` must NOT be reported
  - the same multi-line SQL shape WITHOUT a guard must still BE reported, by
    name — otherwise the fix buys a false negative in place of a false positive
Verified by mutation: reverting to the per-line splitter now exits 2.

The count stayed at 1 while its content changed completely, which is the shape
worth naming: the old scanner's single finding was the herdr false positive;
the new one's is scripts/remote.sh:1086, a genuine [bare] assignment continued
with a backslash across two lines that the per-line splitter could never see.
Baseline stays 1 — a different 1.
co1/tl: json_type='text' lets a pane_id carry the very '|' this function frames on
(SELECT total|well|pane, IFS='|' read), or a newline that truncates at the command-
substitution / read boundary — so a pane_id 'w1:p|4' or one with a newline would
corrupt the parse and hand back a garbled id. The delimiter was used against a set
the value can belong to (same shape as the '__' key collision earlier today).

Narrow the well-formed predicate: the pane_id must not only be text but match the
ACTUAL herdr pane-id form, so only [0-9A-Za-z:] values ever reach the shell line and
the framing cannot mis-split. A wrong-form pane_id is not well-formed -> did-not-
answer (it is unusable anyway; the gate already routes it there). Grammar, from
MEASURED output (read-only, herdr 0.8.0 on this machine: w1:p4, w1:pB, w5:p3;
fixtures also use wC:p4 — both admitted), expressed in SQLite GLOB:
  GLOB 'w*:p*'                the w…:p… skeleton (rejects 123, the w1:t1 tab id)
  NOT GLOB '*[^0-9A-Za-z:]*'  only alnum + ':' (rejects '|', newline, space);
                              [^…] is GLOB's negated class, NOT [!…]

tests: a '|' pane_id and a newline pane_id (both passing the skeleton) -> did-not-
answer; the measured real form w1:p4 still RESOLVES (not over-narrowed). Mutation:
dropping the safety class reddens both hazard tests while the real-form find stays
green. 51 green; enforced-assertions 635.
Two findings, one rewrite of _herdr_pane_for_session's classification.

co1: the pane-id skeleton 'w*:p*' was too weak to be a positive proof of the real
form — SQLite GLOB '*' matches zero chars and multiple colons, so w:p / w1:p / w:p4
/ w1:x:p4 all passed. Tightened to the MEASURED form w<n>:p<x> (n,x non-empty, one
colon): GLOB 'w[0-9A-Za-z]*:p[0-9A-Za-z]*' AND NOT GLOB '*:*:*' AND the alnum+':'
safety class.

utildev (live machine): `agent list` there has 12 entries, one of which is a BARE
PANE with no agent_session — a normal herdr member, not schema drift. The
whole-set "well == alen" rule counted it as unreadable, so well never equaled alen,
not-among was unreachable, and every absent session wrongly returned did-not-answer
(the defect showed only on the negative side; a real match short-circuits). Split
"could not read this entry" from "this entry has no session": an entry is
DECIDABLE if it is an object that either has NO agent_session (a bare pane —
definitely not the target) or an agent_session object with a text .value. not-among
requires every entry decidable; an agent_session that is present-but-malformed
(scalar, or object without a text .value) is indeterminate -> did-not-answer.

One CTE now yields alen, the decidable count, a "target found but pane-id unusable"
count (-> did-not-answer, e.g. a matching entry with a numeric/'|' pane_id), and the
grammar-constrained matched pane (the only free-text field, so the '|'/one-line
framing stays safe).

tests: a bare pane + absent target -> not-among; a bare pane + present target ->
resolves; the tightened grammar rejects w:p / w1:p / w:p4 / w1:x:p4 and still
accepts the measured w1:p4 and the fixture wC:p4. Mutations: excluding bare panes
from the decidable set reddens the bare-pane not-among test; dropping the safety
class reddens the '|'/newline tests. 53 green; errexit baseline 1, enforced 635.
…comment is a comment

Two holes in the same scanner, both found by review rather than by the file
itself, which is why the covered/not-covered list is now written into it.

1. A line continuation was split. `. \` + newline + `"$dir/ops.sh"` became two
   statements, so neither was the `rc=$?`'s predecessor and neither matched the
   source check. `source` is one of the three forms this checker names, so for
   that syntax a count of zero proved nothing. The scanner now consumes a
   backslash and the character after it, dropping the pair when that character
   is a newline, and leaves the single-quoted case alone where a backslash is
   literal.

2. Adding the control for (1) exposed the worse one. Its header comment
   contained an apostrophe, `#` only opened a comment at the START of a
   statement, so that apostrophe read as an opening single quote and swallowed
   the text up to the next one -- switching off the sql_bare control several
   functions later. The file then reported three findings instead of four and
   still exited 0. A checker whose own controls can be disabled by a comment is
   measuring nothing, so `#` now opens a comment wherever it starts a word.

Both are pinned. The continuation control sources a path (`continued-ops.sh`)
that nothing else does, because the finding line carries the statement and
never the function name -- the first attempt pinned on the function name and
passed while detecting nothing. Removing either fix turns the control red.

The tree's single finding is unchanged in identity, not merely in count:
scripts/remote.sh:1086, which is itself a continuation and was caught by the
old scanner for an unrelated reason. So the tree really does hold no hidden
continuation-source case; that zero is now measured rather than assumed.
…knows

`actas-claim.sh` passed `$SESSION_ID` to the naming step. By that point
`$SESSION_ID` has been overwritten with the normalized composite `<sid>.<pid>` —
the right token for the exclusivity lock, and a token that exists only inside
agmsg. What a terminal knows is the bare sid the CLI published; herdr stores
exactly that in `agent_session.value`.

So under herdr the claim succeeded and the naming did not. The resolver answered
"cannot identify this pane", which reads as a resolution problem and is an
identifier mismatch, and the `|| true` on the naming call meant the claim still
printed `status=ok`. A hand-started herdr seat took its role and was silently
unreachable to peek/poke.

`BARE_SID` is already computed two lines up, for the role-session record, and for
the same reason. This passes it.

The trap is worth naming: in `session-start.sh` the identically named
`$SESSION_ID` holds the BARE sid — the hook JSON's `session_id` — and that site
was already correct. Same variable name, two different values, and the call
looked right at both. `watch.sh:271` does the same lookup and was corrected the
same way earlier; this was the site that remained.

The control drives `actas-claim.sh` rather than the helper, because the defect is
in what the caller passes. The sid goes in already composite, so the normalizer's
pid discovery cannot change what is under test, and herdr's list carries the bare
one. Two positive controls run first — the claim really happened (`status=ok`),
and the fake herdr really was reached (`agent list` in its argv log) — so "no
rename" cannot be read as "the resolver never ran".

Calibrated: with `$SESSION_ID` restored, the test goes red at the rename, with
both positive controls still passing — which is exactly the shape of the defect.

test_terminal_registry 63 passed, 0 failed. actas_integration 14 and actas_lock
22 unchanged, measured against this branch's HEAD in alternation (3 runs each,
14/0 in both) after a first sighting of 3 failures turned out to be this machine
running several suites at once.
… success (full-head)

Two full-head-review blockers, both in spawn's placement.

plain reroute (tl 2026-09-02, reversing the earlier "don't wire plain"): plain/ops.sh
declares capabilities=spawn despawn but spawn.sh still opened the OS terminal from a
DUPLICATE inline `_launch_os_terminal`, so the plain driver had no production caller —
fixing the driver or its tests changed nothing real. `_launch_os_terminal` now loads
the plain driver and calls terminal_spawn (which owns the {cmd} template / macOS
`open -g -a` / Linux emulator / Windows Terminal launch and the headless guards). The
four inline launchers (launch_with_template / launch_{macos,linux,windows}_terminal)
are deleted — one implementation, not two. plain still writes no placement record (no
addressable pane; the message shapes "via custom terminal template" / "in a new
terminal window" are unchanged).

record-write failure (co1): the tmux/herdr record writes were `> path 2>/dev/null ||
true`, so a disk-full/permission failure left a LIVE pane with no record — the only
authority peek/poke/despawn --force have — while spawn reported success. The pane
already exists, so there is no safe rollback; instead `_record_placement` checks the
write and, on failure, spawn prints `status=spawned-but-unrecorded` with the pane ref
and exits non-zero (a DIFFERENT word from a clean spawn), so the operator knows a
window exists it cannot address.

tests: OS-terminal path (pre-join launch, macOS no-focus-steal, the two override
tests) green through the plain driver; a forced record-write failure ->
status=spawned-but-unrecorded, non-zero. test_spawn 78/0, test_terminal_registry 63,
checkers at baseline.
…rosses the caller's shell

The type templates showed poke.sh <team> <name> "<text>", and a body an
agent generates can carry a backtick or $( ) — which the CALLER's shell
executes, silently deleting that span from what arrives (#507's class,
already measured on send). A quoting rule in a template does not close
this: the template's reader is an agent and the body is generated, so
any rule is eventually broken. A file (or stdin) body never crosses the
sending shell, so there is nothing to teach and nothing to get wrong.

poke.sh <team> <name> --body-file <path>   the form the templates teach
poke.sh <team> <name> --body -             stdin, for pipelines
poke.sh <team> <name> <text>               kept for humans typing plain text

Trailing newlines are stripped from file/stdin bodies (command-
substitution semantics); an empty body, an unreadable file, and --body
with anything but '-' refuse before any terminal binary runs. Tests pin
a shell-hostile body (backtick, $( ), quotes, $VAR) arriving at the
fake driver verbatim by whole-line equality — the vanished-span failure
changes the line. Both entry scripts also gain an explicit refusal when
a record ref resolves to an empty terminal or pane id, naming the ref.
…ting

The nine type templates told an agent to run

    poke.sh <team> <name> "<text>"

which puts a body the agent generates through the agent's OWN shell. A backtick
or $( ) inside it executes there and its span disappears from what arrives --
no error, exit 0, and the member reads a message with a hole in it. That is
#507's failure, reintroduced on a surface that has not shipped yet.

The templates now teach --body-file: write the text to a file, pass the path.
The body never becomes a shell word, so there is no quoting rule to teach and
none to get wrong. Adding "remember to quote it" would not have closed this --
the rule is only as good as the reader, and the two people who hit this today
both had the rule written down at the time. The positional form still works and
is named as fine for a human typing short plain text; the templates just no
longer generate one.

The block also says that send.sh has no such path yet and that a send body must
still be single-quoted, with the issue number (#1032). Two surfaces with
different safety and no explanation is how the unsafe one gets picked; the
difference is now in front of whoever reads it.

Nine templates, one byte-identical block (verified by hashing it in each), and
no template still carries the positional form. --body-file is poke.sh's real
flag as landed, not an invented one -- the last time these blocks were written
the argv was copied from a sibling command and was wrong.
…in's '-' (co1 re-review)

Two remaining full-head blockers.

(1) ref fail-closed stopped at the scheme PREFIX: agmsg_terminal_ref_terminal accepted
tmux:* / herdr:* / plain:* without checking the id, so tmux:%9;kill, tmux:alice (which
tmux's target grammar resolves to a REAL session/window), herdr:<newline> and plain:any
reached the backend as targets. A known scheme is not the same as a known id. Add
_agmsg_terminal_id_ok as the SINGLE per-terminal id authority (tmux %<n>/@<n>, herdr's
canonical pane grammar, plain exactly '-') and validate the id after the scheme;
anything malformed -> non-zero, no output. The herdr driver's _herdr_pane_id_ok now
delegates to it, so the spawn-side extraction, the resolver cross-check, and the ref
guard share ONE grammar.

(2) the plain reroute leaked terminal_spawn's record-op stdout ('-' = placed, no pane)
straight to spawn's stdout — a normal OS-terminal spawn printed a lone '-'. Capture the
result, verify it is exactly '-' (a malformed/empty result is not a success), and do
not echo the protocol value.

tests: each known-scheme-with-a-malformed-id (tmux:garbage / tmux:%9;kill / tmux:alice
/ herdr:junk / plain:x) resolves to nothing, alongside the unknown-scheme cases, while
well-formed refs and the legacy bare %<n>/@<n> still resolve (mutation: dropping the id
check reddens it); a plain spawn emits no lone '-' line. reg 64, despawn 15, spawn 79,
checkers at baseline.
…the record result (co1)

Capturing terminal_spawn's stdout was not enough: plain's terminal_spawn is a record
op, but its backends — a {cmd} template's `bash -c`, `open`, a Linux emulator, `wt` —
write to stdout too, and that ran right before the driver printed '-'. Now that
_launch_os_terminal captures ALL of terminal_spawn's stdout as the placement id, a
custom template emitting a single diagnostic line makes the id "<noise>\n-", which the
`= '-'` check then rejects as an unexpected id — after the OS window was already
launched (the #1015 profile-vs-hook-JSON shape: two producers on one stdout).

Redirect each backend's STDOUT to stderr (kept as a diagnostic, not swallowed), so the
only thing on stdout is the '-' the function prints.

test: a {cmd} template that writes to stdout -> the record-op result captured from
stdout ALONE is exactly '-'; mutation: dropping the redirect reddens it. reg 65,
enforced-assertions at baseline.
… splits the 13 (tl/co1)

utildev confirmed peek works on a real herdr pane — and surfaced a contract defect.
`herdr pane read … || return 13` returned 13 for EVERY failure, and 13 is also the
documented "this terminal cannot peek" (plain). Worse, herdr writes its error JSON to
STDOUT ({"error":{"code":"pane_not_found",…}}), so a caller reading stdout as the pane
content gets the error body as content — "read" and "could-not-read" in the same shape
(the third instance today of two meanings on one channel: #1015, plain's backend, here).

peek now ISOLATES rather than filters: it captures the read, and only the actual pane
CONTENT reaches stdout; an error body goes to stderr as a diagnostic, never to the
content channel. And the single 13 is split so the caller can tell the three cases
apart:
  plain has no peek path        -> 13 (unchanged — documented, and the templates say so)
  the herdr terminal is unreachable (not on PATH) -> 10
  the pane is gone / unreadable -> 12

test: herdr answering with an error JSON on stdout + a non-zero exit -> rc 12, stdout
EMPTY (the error body is not content); herdr not on PATH -> rc 10; the normal read
still returns content on stdout. plain's peek 13 is unchanged. reg 66, enforced at
baseline.
…sing key (live remeasure)

utildev remeasured the live `agent list`: of 11 entries, `agent==""` is ZERO and the
`agent` value set is {claude, grok}; the session-less pane w5:p3 is
{"agent":"grok","agent_status":"done","name":null,"agent_session":null,"pane_id":"w5:p3"}
— the agent_session KEY is present with a null VALUE, and `agent` is the kind, not "".
My bare-pane arm required `agent==""` AND a MISSING agent_session key, so it matched
NOTHING real (SQLite: json_type of a JSON null is 'null'; of an absent key it is SQL
NULL) — not-among was unreachable again (round 8), and every absent session returned
did-not-answer.

Redefine B to the measured shape: agent_session is JSON null (json_type='null') AND a
valid pane_id; drop the agent=="" check. The load-bearing distinction stays — a null
VALUE (a pane that explicitly has no live session -> decidable "not the target") vs a
MISSING key (a {} or a renamed {"future_session":…} where the target could hide ->
indeterminate -> did-not-answer). A scalar/malformed agent_session, or a null one on a
bad pane_id, is still indeterminate.

tests: the bare-pane fixture is the real {agent:grok, agent_session:null} shape; an
absent target -> not-among (reachable again), a present target still resolves, and
{} / a renamed session field / a null-session-on-a-bad-pane / a scalar session all ->
did-not-answer. Mutation: reverting to IS NULL (missing key) reddens the real-shape
not-among. reg 66; enforced at baseline.
The bare-pane arm B of _herdr_pane_for_session was checking the wrong
agent_session shape, so on the real machine it matched zero entries and
not-among became unreachable again (every absent session returned
did-not-answer — the round-8 regression).

Live remeasure (utildev, herdr 0.8.0, raw JSON): a session-less pane has
the agent_session KEY ABSENT entirely (json_type -> SQL NULL, not a JSON
null value), while its `agent` (the kind: grok/codex, never "") and
`agent_status` fields remain on a valid pane_id. B now proves that whole
fingerprint POSITIVELY: key-absent AND a valid pane_id AND both markers
text. A bare {}, a renamed/unknown session field (future_session drift),
or a marker-less entry lacks the fingerprint -> indeterminate, never a
silent not-among.

Also removes backticks from the SQL-comment block: they sat inside the
double-quoted sqlite3 argument and were being run as command
substitution, which corrupted the query for every resolve.

Fixture updated to the real key-absent shape; the POSITIVE test gains
agent-missing / agent_status-missing / bad-pane drift cases. Both the
IS-NULL vs ='null' form and the agent/agent_status discriminator are
mutation-verified.
…rd the callers

Three gaps co1 named on the peek/poke surface:

(1) peek exit taxonomy was only in herdr. tmux terminal_peek returned 13
for BOTH a missing tmux binary and a capture-pane failure, and the
template reads 13 as plain's permanent "no addressable pane" — so a
tmux pane's transient loss was mis-advised as unsupported. tmux now
uses the same taxonomy as herdr: unreachable (not on PATH) = 10, an
answered-but-no-content failure (pane gone) = 12, 13 reserved for a
driver with no peek path at all.

(2) herdr terminal_peek captured content through a command substitution,
which strips every trailing newline; the following printf '%s\n' then
invented exactly one back, so empty content became a lone newline and
0/2+ trailing newlines collapsed to one — a regression of the "visible
text verbatim" READ contract. It now captures to a temp file, checks
rc, and cats the bytes unmodified; the failure body (herdr writes its
error JSON to stdout) goes to stderr, never the caller's content.

(3) peek.sh/poke.sh resolved the pane ref with a bare VAR="$(...)".
The ref parser fails closed (non-zero) on a corrupt/unknown-scheme ref,
so under set -e the shell died AT the assignment and the "did not
resolve" die was unreachable. watch.sh's close_own_placement left
rec_term/rec_id empty and fell through to a misleading "belongs to
someone else" log. Each now guards the assignment (|| VAR="",
errexit-safe on bash 3.2) and reaches its own failure contract.

Controls added on both backends × both failures, the three verbatim
byte-exact cases, and the corrupt-ref guard on all three callers; each
new behaviour is mutation-verified. Static checkers stay at baseline.
…he fact boundary

The session-less-pane arm B accepted any entry with agent_session absent,
a valid pane, and agent/agent_status as text. That was still too broad
(co1): a renamed-session drift — agent text, agent_status "running", the
session moved under a renamed key like future_session, on a valid pane —
meets every type test while HIDING a live target under the renamed key,
so it was miscounted as bare and the target reported not-among.

B now requires the agent_status VALUE, not merely its type: the measured
session-less pane is agent_status = 'done'. A running/idle pane — which
must own a session — with no agent_session is an anomaly and stays
indeterminate (did-not-answer), never a silent not-among. This errs
deliberately narrow: a session-less pane in some other finished-state
string falls to did-not-answer rather than being silently ruled out.

The front comment ("NO agent_session = bare pane, definitely not the
target") is rewritten to match the marker SQL, and the top prose is
unified to a single fact boundary: the agent-list shape, the bare-pane
fingerprint (agent_status=done), and the pane-id grammar are MEASURED
(utildev, live herdr 0.8.0); agent prompt / agent rename argv stay
ASSERTED. Controls gain the two future_session cases that hide sess-mine
itself and a running-no-session anomaly; the value pin is mutation-verified.
…son stand

Two gaps tl caught on the poke surface, both the "one code hides three
meanings" shape co1 first found in peek:

- terminal_poke returned 13 for every herdr/tmux failure, so a pane whose
  agent had EXITED read as plain's permanent "no addressable pane". poke
  now uses peek's taxonomy on both backends: unreachable (CLI not on PATH)
  = 10, a pane that cannot receive (gone, or no live agent) = 12, and 13
  reserved for a driver with no poke path at all (plain). This makes the
  peek/poke asymmetry concrete — peek reads a pane with no live agent,
  poke needs a running agent, so poke has a "no one to receive" (12) peek
  does not.

- poke.sh added a generic "could not poke ..." line after EVERY failure,
  including plain's 13 - where the driver had just said "not a dead end,
  the type template names a native channel". That generic line was the
  last thing the operator read and cancelled the guidance. The entry now
  suppresses its summary on 13 (unsupported), letting the driver's reason
  be the final word; a real 10/12 delivery failure still gets it.

The 9 type templates' peek AND poke exit-code notes are updated to the
10/12/13 taxonomy, including the peek/poke asymmetry (a member whose
agent has exited can be peeked but not poked). Controls added on both
backends x both failures and both entry branches; mutation-verified.
…nt record atomically

Two spawn-surface hardening fixes.

Startup confirmation (tl hit "spawned" printed while the agent had not
started — a startup shell prompt ate the first keystroke of the boot
command). Every placement line now says "launched <type> '<name>' in
<terminal>" — a placement fact, never "spawned". Whether the agent
actually started is a separate line: status=ready (the watcher attached —
a positive observation) is the only proof of startup; a type with no
readiness handshake (monitor=no) prints status=launched-unconfirmed
note=no-readiness-handshake with an explanation instead of letting the
placement line stand as success. An explicit --no-wait stays terse and
never acquires the unconfirmed line (it is the user opting out, not a
type that cannot confirm).

Atomic placement record. spawn.sh _record_placement and
agmsg_terminal_name_self wrote the record with a raw `>`, which truncates
at open — so a write that then fails (ENOSPC / permission), including the
SessionStart/actas re-name of a pane that already had a correct record,
destroyed the authority peek/poke/despawn depend on BEFORE it could
report the failure. Both now route through agmsg_write_atomic (temp +
rename — the helper six other scripts already use, not a seventh copy),
so a failed write leaves the old record whole; status=spawned-but-
unrecorded is reported only when the write genuinely could not land.
The two were the only raw `>` into a placement record (the rest under
run/ are ephemeral pidfiles).

Controls: launched-unconfirmed vs status=ready vs terse --no-wait; the
record-write failure reported without truncating; the atomic write
preserving an existing correct record on failure. Each mutation-verified;
enforced-assertions and errexit checkers at baseline.
…anging value

Pinning B to agent_status = 'done' was an intermittent predicate: on the
real machine the session-less pane was 'working', not 'done', so B matched
nothing and not-among became unreachable again (a nonexistent sid returned
did-not-answer, rc 2). The 'done' was a VALUE at a moment, not a property
of a bare pane — the same trap as reading agent as a value instead of a
kind. A key-NAME allowlist has the same flaw: name/display_agent appear
and vanish on a live pane (naming is this driver's own job), so a named
pane would fall out of the allowlist intermittently.

B now recognizes a bare pane by STRUCTURE (co1/tl 2026-09-04), from four
conditions: (a) the agent_session key is absent, (b) a valid pane_id,
(c) the fixed identity anchor (agent, terminal_id, tab_id, workspace_id —
measured always-present) is present, and (d) NO field is object- or
array-valued. (d) is what closes co1's renamed-session drift for ANY
shape: a session hidden under a renamed key — future_session:{…},
future_sessions:[…], session_ids:[…] — is a structured value and fails
(d), so the entry is indeterminate and the target it hides is never
reported not-among. A scalar extension (name, display_agent) passes, so a
named bare pane still reaches not-among (that state was unobserved as of
2026-09-04; its control is defensive).

Also fixes a real correlation bug the structural check exposed: the
entries table is now aliased (e) so the correlated json_each(e.value)
binds per row — a bare json_each(value) returns the same answer for every
row. Controls: working/idle/done + named + unknown-scalar bare panes all
reach not-among; every object/array hiding shape and a bare {} stay
did-not-answer. The value-pin regression, (c), and (d) are each
mutation-verified; checkers at baseline.
…), in three arms

Before typing the boot into a herdr pane's shell, confirm the shell is at
its prompt so a startup program (an oh-my-zsh update prompt, say) cannot
eat the first keystroke. The signal is structural and environment-
independent (co1/tl 2026-09-04): herdr pane process-info reports shell_pid
and foreground_process_group_id, and the shell is at its prompt iff the
foreground process group is the shell itself. No prompt string is matched
(zsh/bash/Windows alike) and no point-in-time value is baked in — the trap
that bit agent_status='done' and the key allowlist.

The obvious herdr calls do not fit (all measured 2026-09-04, recorded in
the code so nobody re-hunts): agent start takes only a --kind enum, not our
boot script; agent wait waits for an agent state and a bare shell is not in
agent list; agent list has no shell-readiness field. process-info is the one.

Three outcomes, kept distinct like peek's 10/12/13:
  - READY: both ids present and canonical positive integers AND equal -> type.
  - NOT READY: both validated, unequal -> do not type; bounded wait, then
    close the pane and fail with the reason (exit 3).
  - UNKNOWN: command failed / field missing / not a positive integer -> type
    anyway, but flag it (exit 4). "" == "" / null == null / same-malformed
    are UNKNOWN, never READY: equality is read only after positive validation
    (co1). A nonexistent and a malformed pane give the same error, so both
    are UNKNOWN, not split.

spawn.sh maps the arms: arm 4 sets SPAWN_READINESS_UNVERIFIED, which prints a
BEFORE-typing warning worded apart from the AFTER-typing launched-unconfirmed
(utildev) so an operator can tell which check was blind. Per co1's priority
the warning does not decide the startup verdict — a watcher that then attaches
still makes the status ready; a monitor=no type still reports
launched-unconfirmed on its own. NECESSARY, NOT SUFFICIENT, and said so in the
code: a shell reading its OWN prompt (no child) returns equal, so a first
keystroke can still be lost (unmeasured 2026-09-04); that residual is caught
after typing, not here.

Controls: not-ready fails without typing and closes the pane; unknown types
and warns and records; null==null is unknown not ready; ready is silent;
plain never runs the gate; the two unconfirmed reasons read apart. Each arm
and the positive validation are mutation-verified; checkers at baseline.
… placement

The 6th argument's default is "do not write the placement record", and `join` is
the caller that depends on it: the same identity can be joined from a second
session while a first one holds it through actas, and that record is what
peek/poke resolve a member's pane through. A second pane joining an already-held
identity must not take the placement over.

That property went in, and nothing guarded it. Measured rather than reasoned:
adding `record` to join.sh's call — which restores exactly the defect it was
added to fix — leaves `test_terminal_registry` and `test_actas_integration` with
zero failures. The helper's safe default is covered; whether the caller takes
that default is not, and those are different claims.

So the control is driven through `join.sh`, not through the helper. A record for
pane A is in place, the join runs from pane B, and the assertion is that A's
record is UNCHANGED — not that "join did not break anything", which a version
that emptied the file would also pass.

A positive control goes first: the pane really was named. Without it, a join that
skipped the naming step altogether leaves the record alone too, and this test
would read that as the property holding.

Calibrated in both directions, with the mutation checked before its result was
trusted: the `record` argument is confirmed present in the mutated file, the
control then fails at `[ "$(cat "$rec")" = "$before" ]`, and restoring the file
turns it green.

test_terminal_registry 69 passed, 0 failed. check-enforced-assertions 635 and
check-errexit-status-reads 1, both at their baselines.
…ped pids, honest scope

Four findings from the full-head review of the requirement-1 gate:

(1) The classifier was read as `_herdr_pane_input_ready "$pane"; ready_rc=$?`
— a bare command whose non-zero (NOT-READY/UNKNOWN) status is read on the
next line takes a `set -e` caller down BEFORE the three arms classify, so
the gate meant to sort three outcomes dies at the first non-READY one.
Now `ready_rc=0; classifier || ready_rc=$?`. (The spawn.sh tests call
terminal_spawn inside `$(...)`, which masks errexit; a new control drives
it from a NON-conditional set -e caller, and the fix is mutation-verified.)

(2) The wait bound was `${AGMSG_HERDR_INPUT_READY_TRIES:-50}` — an
undeclared, unvalidated env surface. An empty / 0 / non-numeric value would
make the loop never run, silently skipping the observation (UNKNOWN → boot),
the very thing the gate prevents. It is now a fixed 50 (~5s) with no knob.

(3) The pid fields were read with json_extract only, so a JSON string "123"
extracted as 123 and passed the digit check — a numeric string is not a
validated pid. The classifier now requires json_type = 'integer' in the
same payload before the value; a numeric-string equal/unequal is UNKNOWN.

(4) The prose over-claimed. (d‴) rejects object/array-valued fields, so it
catches a session moved to a renamed OBJECT or ARRAY key (inner shape
irrelevant) — the measured agent_session is an object. It does NOT catch a
session flattened to a SCALAR (a renamed key whose value is the bare target
string): that is the deliberate cost of allowing unknown scalar extensions
(name/display_agent are real), and it is now named as a residual in the
code and the PR body, not implied closed.

Controls: the non-conditional set -e reach; numeric-string → UNKNOWN; the
fixed bound still fails a never-ready pane. Checkers at baseline. (The
errexit checker's blind spot for a generic bare command → next-line `$?`,
and the join-record load-bearing control, are tracked separately.)
The checker has a COVERED / NOT COVERED header so that a count of zero says
what it can and cannot prove. That list enumerates SYNTAX the scanner may
mis-split — heredocs, backticks, arithmetic, `case` separators. It does not
enumerate the SHAPES that produce `$?` in the first place, and that is the axis
that failed: a bare command or function call followed by `rc=$?` is not looked
at at all, because the predecessor is only examined when it is an assignment
with a command substitution, or a `source`.

Review found such a site in this branch while the checker stayed green. Under
`set -e` that shape exits the shell before the status can be classified, which
in that case killed a gate whose entire purpose was to classify three outcomes.

The splitter is not at fault — fed that site it returns the two statements
correctly. The predicate is narrow. Widening it, and counting what the tree
then shows, is #1034 and deliberately not done here: the count is unknown, an
attempt to measure it failed its own positive control, and a change of unknown
size does not belong immediately before a freeze.

So this records the gap rather than closing it, and says which axis was missing
so the next reader does not take the syntax list for the whole story.

Comment only: the predicate, the controls and the baseline (1) are untouched.
… string

The control added in the previous commit says the held record is unchanged and
did not check that. `before="$(cat "$rec")"` and `[ "$(cat "$rec")" = "$before" ]`
both go through command substitution, which strips every trailing newline — so a
join that rewrote the record and changed only its trailing newline passed.

Measured, both ways, rather than reasoned about: with the file seeded and one
newline appended, the string form reports equal and `cmp -s` reports different.

    OLD  [ "$(cat …)" = "$before" ]   PASSES  (blind)
    NEW  cmp -s "$rec" "$snapshot"    FAILS   (sees it)

So the seed is snapshotted with `cp` and the assertion is `cmp -s` against that
file. The `%HELD` / `/proj/OLD` greps stay, now as documentation of what the file
says rather than as the check.

The claim was the part that was wrong, not the design: the test name and the
comment said the record was unchanged while the assertion could only see part of
it. Narrowed the assertion to match the claim rather than the other way round.

Calibrated again after the change, mutation verified present first: adding
`record` to join.sh's call turns this red at `cmp -s`, and restoring the file
turns it green. test_terminal_registry 69 passed, 0 failed.
…w it too

Separate from the fix above on purpose: that one is the item co1 named, this is
one test further than the item, and the two should be possible to take or leave
independently.

The test that pins the HELPER's default — "without `record` the seat's existing
placement is untouched" — captured the record with `before="$(cat "$rec")"` and
compared the same way. Command substitution strips trailing newlines, so it made
the same claim about the old value's survival and could not see the same class of
rewrite. Both tests were written here; the blind spot is one habit, not two
mistakes.

Same treatment: snapshot with `cp`, assert with `cmp -s`, keep the `%HELD` grep as
documentation of what the file holds.

Calibrated, mutation verified present before its result was read: making the
helper write the record unconditionally again — the defect the test exists for —
turns it red at `cmp -s`, and restoring the file turns it green.

test_terminal_registry 69 passed, 0 failed.
…either field

The numeric-string readiness control only exercised the EQUAL arm, so a drift
that type-gated one pid field but not the other, or skipped the gate on the
unequal path, would have stayed green (co1/tl 3). It now loops all four shapes
— "5"/"5" (equal), "5"/"6" (unequal, which must reach UNKNOWN, not the
NOT-READY wait/fail), and the two MIXED integer + numeric-string cases — each
required to classify UNKNOWN -> type + before-typing warning. Removing either
field's json_type='integer' gate turns a MIXED case READY, so the control is
mutation-bearing.

(The checker's bare-command blind spot, this control's sibling finding, is
already documented in the checker's NOT COVERED section referencing #1034.)
…tup status totality)

The five-arm startup contract says every successful placement WITHOUT a
post-input readiness observation emits exactly one status=launched-unconfirmed
— but only the monitor=no arm was wired. An explicit --no-wait clears
WAIT_READY at parse while SKIPPED_READINESS_BY_TYPE stays 0, so a
monitor-capable type spawned with --no-wait fell through both the wait and
the monitor=no branches, printing no status line at all — a bare `launched …`
at rc 0 that reads as success though startup was never confirmed (co1
full-head).

The status block gains the else arm: --no-wait also skips the handshake by
request, so it reports status=launched-unconfirmed with note=no-wait — a
distinct note from monitor=no's note=no-readiness-handshake, so the two
no-confirmation reasons stay legible. status=ready (a watcher attached) and
status=timeout (waited and nothing came) are unchanged. The if/elif/else is
mutually exclusive, so a placed spawn emits exactly one status line.

Controls (both inputs, mutation-bearing): a monitor-capable type with
--no-wait now pins status=launched-unconfirmed note=no-wait; the monitor=no
control keeps note=no-readiness-handshake; removing the else arm reddens the
--no-wait control. Full spawn suite and checkers green.
@fujibee
fujibee changed the base branch from main to integration/terminal-driver-v1 September 4, 2026 22:41
fujibee added a commit that referenced this pull request Sep 5, 2026
…feats (#1040)

PINNED_APART exists because these files' @test count does not describe their
cost -- a file that is mostly waiting carries a tiny count while dominating its
shard's wall clock. The seeding then handed the balancer that same count: a 630s
file reporting a load of 9 makes its shard read as very nearly empty, and the
ordinary weighted pass fills it right back up. The pin separated the heavy files
from each other and then let one of them be buried under a full share of
everything else.

Measured, macOS, run 33850939213 attempt 4, on the terminal-driver tree. Per-file
seconds derived from the run's own timestamps (bats runs serially here, so the
test numbering is time-ordered):

  shard 1   32.2 min   holds test_remote_engine_start_refusal.bats -- 630s
                       across 9 @test blocks, 70 s/test, 36% of the shard
  shard 2   17.1 min   holds test_remote_status_liveness.bats -- 347s, 11 s/test
  shard 3   10.8 min
  shard 4   12.2 min

Against a 30-minute cap, shard 1 was cancelled at the cap on four consecutive
attempts -- 395 of 462 tests done, zero failures, still progressing 16 seconds
before the cut -- while shards 3 and 4 sat idle for fifteen minutes.

A pinned file is now seeded at one shard's average share instead, which is the
honest reading of "we cannot weigh this one": it is treated as already worth
about what a shard is supposed to hold. Same tree, same measured per-file
seconds, the same partition code:

  14.5 / 15.8 / 21.0 / 21.1 min      worst case 21.1, nine minutes of headroom

Deliberately not a table of per-file seconds. That alternative was weighed when
the weights were first written and rejected because it goes stale silently --
wrong the moment a fixed `sleep` becomes a poll loop, with nothing to say so.
The share is derived from the tree on every invocation.

The existing pin test cannot catch this regression: seeded by count the two
pinned files still land on different shards, so it stays green through exactly
the failure above. The new test asserts the reservation itself -- a shard holding
a pin carries measurably fewer tests than the average -- and was mutation-checked
to fail when the seed is put back to the count, and to pass otherwise.

Refs #1014
fujibee added a commit that referenced this pull request Sep 5, 2026
…ain (#1014)

Squashed from PR #1014 (head d2bd809, cleared
as a whole head by review) and pushed directly, because the merge API refuses a
workflow-file change from a token without `workflow` scope.

One conflict, resolved by measurement rather than by picking a side:
`.github/errexit-status-reads-baseline` was 1 on the branch and 0 on the
integration branch, because #1030 lowered it by fixing #1025 after #1014 was
cut. Running the checker on the merged tree reports 0 status reads and asks for
a baseline of 0; two canary values (1 and 5) both come back "0 status reads,
below the baseline", so the checker is measuring the tree rather than echoing
the file. Baseline is 0.
@fujibee

fujibee commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Landed on integration/terminal-driver-v1 as 19fa62f, squashed from this
branch's head d2bd809d595bf7d9a289826c02e8802395ba910e and pushed directly —
the merge API refuses a workflow-file change from a token without workflow
scope, so the squash was made locally and pushed over SSH. Same content.

One conflict, .github/errexit-status-reads-baseline: 1 here, 0 on the
integration branch, because #1030 lowered it by fixing #1025 after this branch
was cut. Resolved by running the checker on the merged tree rather than by
picking a side — it reports 0 status reads and asks for a baseline of 0, and two
canary values (1 and 5) both come back "0 status reads, below the baseline", so
it is measuring the tree rather than echoing the file. Baseline is 0.

Closing here; it merges to main with the rest of the release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant