ADR 0187: Clankie hires his own seats
Status: accepted (acceptance date unverified). Amends the app repo's ADR 0013 ("compose is hiring"): hiring is no longer only the operator's act. Builds on ADR 0185 — a hire Clankie makes may name its model and effort the same way — and shares the authority gate of ADR 0186. Amended by ADR 0207, which removes automatic terminal-input fallback. The fallback descriptions below are historical.
Context
Clankie routes work to fleet seats, but every seat first had to be hired by
the operator from the compose page. Telling Clankie "do this with pi on a
strong model" stalled on a human round-trip: he could describe the hire he
wanted, or start a bare process over bash with herdr agent start, but the
latter lands a stranger — no persona, no bound conversation, no watch — which
the roster only notices on its next poll, and which bypasses the typed
failure outcomes the compose path gets.
Decision
The captain has a hire_agent tool, and it is the same hire the compose
page makes. One hireSeat closure in the captain serves both the
spawn_seat service op and the tool: herdr starts the harness, and the seat
is adopted as a persona, bound to its conversation, and watched atomically
with its pane. There is no second hire path to drift.
- Authority is the shell's authority. Hiring starts a process on the
operator's machine, so the tool exists exactly where a turn could already
start one by shell: the operator lane, or a Discord room holding the
authenticated machine-access grant — the same gate ADR 0186 gives
herdr_watch. A lane without that grant never sees the tool; a tool list is a boundary, not a prompt instruction. - An autonomous turn proposes, never executes. Following
create_goal: a goal continuation or scheduled wake may ask for a hire conversationally, but the tool throws. Starting processes and spending model budget stays answerable to a live person. - Failure stays typed. The tool returns the same
OperatorSeatSpawnResultas the compose page —unknown_directory,harness_unavailable,not_ready,herdr_unreachable— so Clankie can say plainly why a hire did not happen instead of reading a shell's stderr. - Model and effort ride along. The tool takes the optional
modelandeffortof ADR 0185, spelled the harness's own way, with the same typed refusal for a harness that has no wired flag.
Consequences
"Herdr leadership goes through bash + the herdr skill" keeps its two exceptions named in one place: the completion wake (a shell wait cannot resume a finished model turn) and hiring (a shell command cannot wire a persona). Everything else — panes, sends, waits — stays on the CLI.
A hire Clankie makes is attributed to the conversation that asked for it and appears in the same roster as an operator's hire; cleanup follows the standing rule that only the creator closes a temporary worker. Giving rooms without the machine-access grant a way to request hires (a proposal the operator approves) is a possible follow-up; it is not this decision.
Amendment: he briefs what he hires
Accepted 2026-09-26 with VUH-1373.
A hired Herdr seat is not a Swarm actor, so a swarm_send to anything a hire
returns (persona, conversation) fails stale_recipient, and herdr agent get
does not know the terminal id the hire returns as its seatId. In model evals
every pi hire came up idle and stayed idle: the only lane into it, the seat's
conversation, was reachable from the operator's surfaces and not from his tools.
hire_agenttakes an optionalbrief, submitted after startup readiness through Herdr's agent prompt, and verifies the complete native transcript receipt before reporting delivery (VUH-1450 amendment below).message_seatsends a follow-up down the conversation lane (mailbox when polling, else the pane), by seatId, personaId or conversationId — the third named exception above, for the same reason: a raw pane send skips the mailbox and the seat ledger, and nothing points him at it.herdr_watchaccepts the seatId a hire returns.
Swarm stays the path for enrolled peers; these reach only seats he can see.
Startup readiness (VUH-1373)
A hire gives herdr agent start its explicit 30-second readiness deadline and
allows 35 seconds for the CLI process to return. Ordinary Herdr queries retain
their 5-second deadline. Applying that shorter query timeout to startup killed
the CLI while pi was still loading under CPU contention, then closed a healthy
worker's pane before its session report arrived.
After Herdr reports readiness, the existing bounded 10-second poll still requires
a durable session report before publishing the seat. This is the integration's
reported identity, not the existence of a transcript file: pi writes the file
when its first turn starts. A startup timeout or missing session remains the
protocol's typed failed / not_ready outcome with diagnostic detail, and closes
only the pane created by that hire. There is no second launch or blind retry.
Verified briefs (VUH-1450, 2026-09-29)
All initial briefs now use Herdr's paste-aware agent prompt after startup
readiness. The raw pane send-text fallback lost the beginning of a 4,486-byte
Claude brief even in a ready seat. A hire reports delivery only after matching
the complete operator prompt in the native transcript, allowing Claude's paste
envelope. A missing or partial receipt fails as not_ready with
brief_delivery_unverified; the new pane is closed. Transcript display limits
and redaction can prevent verification; a brief file with a short pointer avoids
those limits. Follow-up pane delivery also uses agent prompt; mailbox and
Codex queue delivery remain available.
Amendment: seats are driven through their harness (VUH-1458, 2026-09-30)
Following the ADR 0203 amendment, a hire is controlled through the harness's own extension points, and the worker stays the real interactive harness in its herdr pane, where the owner can type into it at any time. It is never a headless process with herdr as a mere view (an earlier headless stream-json design was built, then withdrawn at James's direction before it landed).
- One seam.
HarnessSeatAdapterin@clankie/agent-hostsstarts a seat in its pane under its persona name (herdr agent start) and returns aSeatControl:sendacknowledged by the harness,settled,interrupt,close, andattachafter a restart.HerdrWatchStoreroutes a briefed local hire, its messages, completion watches and closing through the adapter for that harness while it holds the seat; the adapter's status outranks the terminal's. Codex implements it over its app-server (VUH-1459). - Claude uses the
clankie-worker@clankieplugin, enabled for that session only and loaded with--channels. The brief and messages are channel notifications from the plugin's server, which serves the seat mailbox (clankie mcp --seat). A message counts as delivered only once it appears whole in the native transcript. The plugin's Stop and StopFailure hooks, forwarded byclankie seat-hook, settle the turn with Claude's own final text; a watch wake quotes that text as data, never as instructions. - Consent belongs to the owner. Claude honors a custom channel unattended
only when the plugin is installed and managed policy approves it. Nothing
accepts the development-channel warning on the owner's behalf. Without that
approval the adapter returns
blocked(consent_required) before launching anything, and the hire continues on the terminal lane in the same pane. The hire tool's result names the missing step (control.mode: "terminal", withfix). VUH-1478 also exposes control in the compose/API result:channelfor Claude,adapterfor Codex, andterminalwith a reason for every fallback. Fleet routing preserves the local runner's adapter capabilities. Every hire logs its lane and reason; a visible folder-trust prompt returnstrust_requiredbefore the failed pane is closed. - Terminal typing is the fallback only, with the VUH-1450 paste-and-verify
receipt and its typed
brief_delivery_unverifiedfailure. An adapter failure after launch closes the pane and returns its typed outcome; it is never retried on the terminal.
Interactive Claude has no programmatic interrupt; the owner presses Esc in the pane. (Superseded for the owner's exact-task Stop by the VUH-2680 amendment below.)
The terminal fallback launches Claude without any development channel, and the
service never answers Claude's development-channel warning. It previously
pressed Enter on it for server:clankie-seat, which accepted consent that is
the owner's to give (ADR 0194). If that warning ever appears, the hire fails
not_ready with detail consent_required: … naming the managed-policy fix,
and closes its pane. Without a channel, fallback seats get their messages
through Herdr's paste-aware prompt, verified in the transcript.
Amendment: a hire starts only the MCP servers it needs (VUH-2388, 2026-10-17)
A local hire no longer inherits every MCP server the owner registered for their
own sessions. It starts Clankie's worker bridge, the servers its checkout
declares, and the owner servers its hire profile names in mcpServers, opt-ins
adding up across fleet.hire, the project role and the hire. Claude enforces
that list through allowedMcpServers in the launch settings; Codex switches its
other user-layer servers off for the session. The owner's configuration files
are not edited, remote hires are unchanged, and the operator seat keeps its own
launch. Before the change, 27 Codex worker app-servers and their MCP children
held about 16.8 GB, mostly copies of the owner's video, IDE and 3D servers, one
set per loaded Codex thread. Details and the opt-in commands are in
agent hosts.
Amendment: the owner's Stop presses Esc for Claude (VUH-2680, 2026-10-23)
James asked to stop Clankie or a worker from the iPhone and to see clearly that
it stopped. Clankie's own lead runs as interactive Claude, so without a Claude
Stop the owner could never interrupt him from the app. The lead approved
reversing "Interactive Claude has no programmatic interrupt" for exactly one
route: the owner's authenticated stop_task
(ADR 0207).
That route presses the owner's own key, one Esc, through herdr pane send-keys.
It keeps stop_task's authority and adds terminal-specific fences:
- the pane, terminal and native session must match the observed binding, checked again with owner authority immediately before the key;
- Herdr must show the session working; idle is
already_finished, and a seat waiting on the owner is refused; - the styled input line, read last, just before the key, must be provably empty (pane drafts); a draft, or a line that cannot be read, is refused, because Esc would clear it;
- one Esc per request, shared by concurrent requests and never retried;
stoppedonly after Claude is seen idle again, otherwiseuncertain; - Claude's Stop hook does not run after an interrupt, so a confirmed stop settles that exact session's turn as waiting itself, releasing Queued messages at once.
The durable "stopped" marker comes from Claude's own interrupt record in its
transcript, published as the existing turn/cancelled event with a run ID
derived from that record. It is not a new event type, so older app builds still
parse replay pages. The Stop path itself publishes no marker, so each interrupt
yields exactly one. An Esc before Claude's first reply writes no interrupt
record: Claude withdraws the request and restores the prompt as an unsent draft.
The service publishes no marker then either. A synthesized one would have no
native record to key it, and the owner's own draft already shows what happened.
Everything else stays as above: interrupt() remains false, a hire's brief and
messages never use terminal keys, and no other automation types into a Claude
pane.
