0072 — agent-smith is the Agent Harness, and every agent is one row of one registry, one per machine
- Status: Accepted 2026-10-22. In progress. The rulings below are final, and the build is landing in parts. The list under Implemented by is the record of what is live.
- Date: 2026-10-22
- Deciders:
- The intent and rulings R1–R6 are Gera's, given 2026-10-22 in the session that ordered the effort (two rounds of AskUserQuestion, before and after a Fable 5.1 audit of the design).
- The Temporal target naming is the lane's, ruled through fable-decide receipt
f62e82edc. Fable timed out on both passes, so the answer came from Astra. It is recorded as such and goes back for confirmation before the R3 window.
- Systems: agent-smith, being renamed to agent-harness. Both seats,
smithon the mini andclara(Trinity) on Fede's machine.config/agents.tomlandsrc/agent_smith/agent_registry.py, the boot path ofworker.runandslack_socket.main, andsmith doctor. - Implemented by agent-smith:
- registry part 1a (file and loader): #1359
2c968882; - part 1b (rosters built from it): #1364
5d02efbf; - part 2 (machine id, WARN-mode boot check, doctor row): #1365;
- R1
harness-*console-script names: #13607534314f; AGENT_HOME_CHANNEL/AGENT_TEMPORAL_*env names: #1362404698e5;- the dual GitHub name: #1358 (the rename lane).
- registry part 1a (file and loader): #1359
- Amends: ADR-0071's Systems line, which names the seat slug
clara. Under R2 that slug becomestrinity, withclaraas a legacy slug.
The problem
agent-smith started as one agent's code. It now runs two seats from one checkout, and the plan is for more. The code still says "Smith" everywhere, and nothing in it says which agent is which.
The seat roster is hardcoded and scattered. Before 2026-10-22 it lived in
config._PRESENTED_NAMES, two literal bot uids,bridge_claims.SEAT_HUMANS,agent_ident.SEATSand a per-slugif. Adding a third agent meant editing all of them, and nothing would have failed if one was missed.Nothing ties a process to a machine.
AGENT_SLUGcomes from each plist, and when it is unset it defaults tosmithon every machine. A plist copied to the wrong box would boot as the wrong agent without a word.One name means four different things. In (measured 2026-10-22) 7,458
agent_smith, 1,750agent-smithand ~6,000smithreferences, the word is sometimes:- the harness;
- Smith's seat;
- a tool that serves every seat;
- Clara, the leasing product, whose name collides with Trinity's old slug.
A blind replace breaks Smith's own identity and the product.
Gera's intent, verbatim (2026-10-22)
"Everybody needs to inherit." "One agent per machine." "This machine is this agent." "A new agent ... would just add themselves to that list." "They all intrinsically know about each other."
Decisions
The harness is renamed and the seats keep their identity. Every reference falls in one of four classes, and only class A and class C move:
- A, the harness. The repo, the package (
agent_smith→agent_harness, one mechanical PR with a realagent_smithshim for a window), and the docs. This class moves. - B, Smith's seat. Slug
smith, its bot uid, "Agent Smith",~/.claude/smith-state,co.propflow.smith-*,smith_alertandpropflow-agent-smith[bot]. This class stays. - C, tooling wearing Smith's name (R1). Every
smith-*console script gains aharness-*twin. Thesmith-*names stay as aliases until a grep of their callers reaches 0. - D, Clara the leasing product.
clara_brief,clara_hold_review,clara-morning-brief,author == "clara". This class never moves.
- A, the harness. The repo, the package (
One registry, one row per agent.
config/agents.tomlis the list Gera described. Joining is a reviewed PR that adds a row.- Row fields: slug, presented name, bot uid (empty allowed), owner, home channel, home-relative state dir, MCP allowlist, GitHub App, required env key names (never values), legacy slugs, roles, machine (IOPlatformUUID plus a display label), and machine jobs.
- Who reads it: every seat loads the whole file, so every peer is known. The loader is host-side and stdlib-only, and it refuses a broken file. Workflow code never reads it.
agent_ident.SEATSstays a literal, because the Temporal sandbox passes that module through. A test pins it to row order.
This machine is this agent. The machine's IOPlatformUUID names exactly one row. The hostname is a label, because hostnames change. An empty id matches nothing.
- A set
AGENT_SLUGthat names another row is a mismatch, and so is a machine in no row. - An unset slug inherits the machine's row; that inheritance lands with enforcement.
- The check ships in WARN mode first. Both daemons log
registry boot-check OKorMISMATCH (warn only)and run on, andsmith doctorshows the verdict. - It enforces only on presence: every boot on the mini has logged OK and none MISMATCH. Silence is not the gate.
- A set
Duties are roles, not slugs (R4, R6).
triages_alertsandruns_batchare held by exactly one row each, and the loader refuses zero or two. They replace theIS_SMITHbatch gate and the triages-alerts literal. Machine-wide jobs are declared under the row of the machine that runs them. Today all of them sit under the mini's row, so a second machine never double-runs them. The router stays two-seat for now (R4).Trinity's slug becomes
trinity(R2, R5), withlegacy_slugs = ["clara"]. History stampedclara(wf-id prefixes, conversation-log authors) still reads as hers. The Trinity watcher (com.propflow.trinity,#agent-trinity) is a different thing and keeps its name. The move needs one restart on Fede's machine from a checklist. A missing seat policy file becomes a loud refusal, not a silent empty policy.Smith's Temporal names are renamed too, last, in one window (R3). Under
f62e82edc, only harness-branded names move:Smith*Workflowtypes becomeAgent*;smith_*activities becomeagent_*;propflow-batch-queuebecomesharness-batch-queue.
Seat-derived queues and wf-id prefixes stay, because the slug stays.
- Carry-over: long-running types carry over on a
workflow.patched()gate with the old names still registered (theDRIVER_ACTIVITY_NAMES_PATCH_IDprecedent). - Schedules: the 14 schedules are updated in place.
- Untouched: the row leases (1,179 running, measured 2026-10-22) have no Smith name in them.
The window runs only after the plan is approved, and the Astra-answered naming is confirmed in that same approval: the cutover plan lists it as its first open question, so the window cannot start without it.
The local checkout path moves last, with a permanent symlink and
git worktree repair, because ~/.local/bin wrappers and the worktrees (45 on 2026-10-22) hold absolute paths.
Rejected
- One big rename PR for everything. The class B and class D casualties (the audit names them) make a single sed wrong by construction.
- Hostname as machine identity. It is mutable, and two machines can share one.
- An
areasfield for owner routing. #1352 retired per-seat channel ownership. Roles carry the two duties that remained. - Keeping Smith's Temporal names (the audit's recommendation). Gera chose the full cutover.
Open
- Fede's machine id is not recorded yet. Until then Trinity's boot check logs MISMATCH "in no row". That is the signal working.
- The
smith-sync-from-main.shwrapper lives in dotclaude, so its harness name is a dotclaude change. - Seat literals still outside the registry:
clara_brief.py:53,tasks_canvas.py:100,task_intent.py:43,BASE_VOICE_PROMPT,bridge_digest._SEAT_WORDand_BRANCH_RE.
Amendment, 2026-10-24 — one agent per person, and jobs are their own table
Gera answered two of the deep audit's open questions (Q1, Q4) on 2026-10-24. Both change decision 2's row shape, so they are recorded here at decision time (ADR-0004).
- One agent per person (Q1), a rule, not a loader accident. Each person has at most one agent seat. The loader's refusal of two rows with the same
owner_human(case-insensitive) is this rule's enforcement, not a limitation waiting to be lifted.bridge_claims.SEAT_HUMANSand its inversions (bridge_digest._HUMAN_SEATS,bridge_lanes.OWNER_TO_SEAT) depend on it. - Launchd jobs are a separate
[[jobs]]table (Q4), not a row field. Each entry is alabel(the launchd Label, withdeploy/<label>.plistshipped) and ascope:allmeans every agent machine runs it, and a seat slug means only that seat's machine runs it (a legacy slug resolves to its row). This replaces the rows'machine_jobsin decision 2 and the "declared under the row of the machine that runs them" sentence in decision 4. A job a machine should run but has not loaded is a doctor red.- Checked before carrying, not copied. The three entries the audit found dead (F7) were each checked and deleted rather than carried into the table.
inspo-sweepwas disabled on purpose and nobody asked for it since.pr-babysitterwas never installed, and PR watching belongs to the existing merge owner (ADR-0030).maintenance-eval-dailymoved into Temporal'sNightlyQueueWorkflowin July, so reinstalling it would double-run. Fable receipts:fd60e772b,f303c930e.
- Checked before carrying, not copied. The three entries the audit found dead (F7) were each checked and deleted rather than carried into the table.
- The two-seat router does not fail on a third row (refines decision 4 and R4). The router pairs the first two rows. A third row is legal and every peer lookup sees it. The router logs one WARNING and does not route the third row, rather than refusing to import, because adding a row is exactly what decision 2 tells a new agent to do. A slug outside the routed pair has no router peer: the answer is a named
NO_PEER, never an invented seat. Generalising the router is the work that lifts this.