Conversation
…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.
…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.
… placement/name (tl ruling)
…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.
9363be6 to
e78d66a
Compare
…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.
…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
…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.
|
Landed on One conflict, Closing here; it merges to |
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 branchfeat/terminal-driver-v1, basemain(a non-mainbase does not run CI, so everything stacks here).Scope, in one line: move the scattered
$TMUX/HERDR_*branches behind aterminal_*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.confDATA +ops.shsourced), aterminalsfacade overdriver-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/@Nreads as tmux; first-colon split preserves a herdr id's own:).Exit taxonomy, uniform across peek/poke on every driver:
10unreachable (the CLI is not on PATH) ·12the pane cannot be served (gone, unreadable, or — for poke — has no live agent to receive) ·13the driver has no such path at all (plain's permanent "no addressable pane"). Only13is permanent; a template that reads13as "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_detectreports two facts and decides nothing — PRESENCE (exit code) and SELF-ID (stdout, may be empty, reason on stderr). Two callers decide:The override
AGMSG_TERMINAL_DRIVER/--terminal-driveris a spawn/name PREFERENCE on a NEW surface (tmux|herdr|plain), distinct from--terminal/AGMSG_TERMINAL(the OS-terminal command TEMPLATE, untouched).Drivers
terminal_namesets@agmsg_agent=<team>:<agent>; spawn validates the id KIND (%Npane /@Nwindow) before returning it. peek/poke use the shared taxonomy (not on PATH → 10, capture/send failure → 12); capture-pane streams to stdout verbatim.agent listresolution keyed onagent_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 grammarw<n>:p<x>is one shared authority (resolver SQL +_herdr_pane_id_ok, cross-checked).terminal_peekcaptures 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).-placement id reaches the record op's stdout); despawn no-op; peek/poke/name unsupported (no addressable pane), reported as13with a reason, not a silentok.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, not13. 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
--terminal-drivervalidated early;$TMUX-first → herdr → OS-terminal detection preserved). It recordstmux:%N/herdr:%Nviaagmsg_terminal_ref; plain has no addressable pane, so it records nothing.spawnedprinted while the agent had not started, because a startup shell prompt ate the first keystroke of the boot. Two guards, before and after typing:herdr pane process-info'sforeground_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.launched … in <terminal>— a placement fact — neverspawned.status=ready(the member's watcher attached — a positive observation) is the only proof of startup; a type with no readiness handshake (monitor=no) printsstatus=launched-unconfirmed note=no-readiness-handshake. The after-typing verdict wins: a watcher that attaches makes it ready regardless of the before-typing warning.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-unrecordedis reported only when the write genuinely could not land, never on top of a record it just truncated. The two record writers (spawn.shandagmsg_terminal_name_self) were the only raw>into a placement record — the rest of the raw>underrun/are ephemeral pidfiles, overwrite-by-design.terminal_despawnand returns0only on CONFIRMED teardown;--forceon an unconfirmable teardown keeps the record withstatus=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: afreeactas lock with a placement record is not "gone" — it reportsstatus=needs-forceand KEEPS the record.--splittargets$TMUX_PANE(the caller's pane), and fails closed if$TMUX_PANEis unset — never the attached client's active window.agmsg_terminal_name_self(record written only on ownership-proven paths — actas/SessionStart, not join).set -eassignment (watch.sh gives an unresolvable ref its own logged branch). On anunsupported(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.delivery.sh statusshow the resolved terminal.Fact boundary (herdr)
MEASURED (live, herdr 0.8.0): the
agent listshape (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 thatagent start/agent waitdo 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 andherdr agent rename's name argv (no rename call inmainto 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
.github/scripts/check-errexit-status-reads.sh+ baseline): a PRODUCT of this work — the same bash-3.2set -edefect 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) andtests/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.