0072 — agent-smith is the Agent Harness, and every agent is one row of one registry, one per machine

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.

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

  1. 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 real agent_smith shim 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_alert and propflow-agent-smith[bot]. This class stays.
    • C, tooling wearing Smith's name (R1). Every smith-* console script gains a harness-* twin. The smith-* 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.
  2. One registry, one row per agent. config/agents.toml is 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.SEATS stays a literal, because the Temporal sandbox passes that module through. A test pins it to row order.
  3. 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_SLUG that 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 OK or MISMATCH (warn only) and run on, and smith doctor shows the verdict.
    • It enforces only on presence: every boot on the mini has logged OK and none MISMATCH. Silence is not the gate.
  4. Duties are roles, not slugs (R4, R6). triages_alerts and runs_batch are held by exactly one row each, and the loader refuses zero or two. They replace the IS_SMITH batch 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).

  5. Trinity's slug becomes trinity (R2, R5), with legacy_slugs = ["clara"]. History stamped clara (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.

  6. Smith's Temporal names are renamed too, last, in one window (R3). Under f62e82edc, only harness-branded names move:

    • Smith*Workflow types become Agent*;
    • smith_* activities become agent_*;
    • propflow-batch-queue becomes harness-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 (the DRIVER_ACTIVITY_NAMES_PATCH_ID precedent).
    • 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.

  7. 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

Open

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).

  1. 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_HUMANS and its inversions (bridge_digest._HUMAN_SEATS, bridge_lanes.OWNER_TO_SEAT) depend on it.
  2. Launchd jobs are a separate [[jobs]] table (Q4), not a row field. Each entry is a label (the launchd Label, with deploy/<label>.plist shipped) and a scope: all means 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_jobs in 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-sweep was disabled on purpose and nobody asked for it since. pr-babysitter was never installed, and PR watching belongs to the existing merge owner (ADR-0030). maintenance-eval-daily moved into Temporal's NightlyQueueWorkflow in July, so reinstalling it would double-run. Fable receipts: fd60e772b, f303c930e.
  3. 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.