0001 — Narrow the Architect to an enforced observe+spawn singleton

Why this ADR exists at all

Two evaluations of the Architect role disagreed. A tiebreaker was run with both positions in front of it and came back C — narrow the role to observe+spawn, and enforce the singleton at high confidence.

This file records that verdict with its evidence, because the losing position's central plank was an artifact of when somebody happened to look, and a future evaluation reading the same ledger a week later will find the same clean surface and re-argue from it. The point of writing it down is to make the next evaluation argue against the measurement rather than against the residue.

Context — the two facts the verdict turned on

1. The zero was manufactured, minutes earlier, by hand

The competing position's load-bearing claim was:

"blocked adopt --list shows zero orphans, so Operators already handle cross-task blocks."

That was false as framed. The zero existed because the incumbent Architect hand-adopted two blocks minutes before the command was run. Measured off disk on 2026-08-08:

file which field value state
jobs/blocked/cc4ad587-…/b86054955.json file mtime 14:55:03 adopted, adopted_by: 0d84ff5f-…
jobs/blocked/cc4ad587-…/b86069107.json file mtime 14:55:44 adopted, adopted_by: 0d84ff5f-…
jobs/blocked/0d84ff5f-…/b86054955.json adopted_at 14:55:03 open, adopted_from: cc4ad587-…
jobs/blocked/0d84ff5f-…/b86069107.json adopted_at 14:55:44 open, adopted_from: cc4ad587-…

⚠️ The column used to be headed mtime for all four rows, and that was a false attribution — one worth fixing precisely, because an ADR arguing that a claim needs an input that could have falsified it will be checked by someone running stat on its own citation. Two things the split makes visible:

blocked adopt --list reports open blocks whose owner is provably dead. Two rows had been moved onto a live owner seconds earlier, so the command could not have returned anything else. A claim graded on an input that could not have falsified it is unverified, not confirmed. The general form, which is the part worth keeping: a green reading taken after somebody cleaned by hand measures the cleaning, not the system.

⚠️ The same class shows up as ABSENCE, and that is the harder half to see. A missing reading is not a good reading. Live instance on the day of this ADR: session dd7d479d was measured at 95.6% context by self-compact while /api/roster served contextPct: null for the same session — two instruments, one number and one silence. null there means nobody could look; it does not mean healthy, and it does not mean zero. Any surface that renders it as either has re-created the manufactured zero without anyone having to clean anything by hand. The rule this ADR asks future readers to carry is therefore symmetric: a claim needs an input that could have falsified it, and an absent input never could.

2. The class of block has no owner in an Operator+Worker-only model

The same ledger shows three generations of coordinator-to-coordinator adoption on those two ids:

9a7a3194  →  cc4ad587  →  0d84ff5f

These are cross-task questions. They belong to no single task, so no Operator owns them, and:

So removing the Architect does not remove the class of block. It removes the only thing that could ever act on one. That is the argument for keeping the role, and — because the ledger is keyed by sid — it is also the entire argument for making it a singleton.

3. Independent corroboration, from the ledger side

The two facts above come from the tiebreaker's reading. They were corroborated the same day, from a different vantage point, by session propflow-40 · 0d84ff5f — the incumbent Architect itself, which was holding the adopted rows and reported what it could see from inside its own ledger. It observed the same 14:55 adoption and the same three-generation chain directly, where the tiebreaker had inferred it. Two independent observers, one event.

It also surfaced something the tiebreaker did not: adoption transfers the row and nothing else.

⚠️ One half of that did not reproduce, and is recorded as such. Re-checked later on 2026-08-08 against origin/main in propflow-docs: all three of artifacts/blocked-0d84ff5f.html, …-9a7a3194.html and …-cc4ad587.html were present. So the specific "not on origin" reading was a moment in time. What is structural, and will recur, is the disagreement: a local-ledger check and an origin check answer different questions and one of them is optimistic. Reporting only the half that still reproduces would flatter the fix; reporting only the original claim would assert something now false.

Decision

The Architect is an enforced observe+spawn singleton.

  1. It holds no task and never drives work. Its one active power is minting Operators via start-operator, and it authors the --criterion. ~/.claude/skills/architect/SKILL.md states this in its header and in §0e.

  2. The raw-tmux worker-spawning recipe (§2b) is deleted. It bypassed start-operator, whose header says "EVERY DOOR LANDS HERE… so 'how work starts' cannot drift into three answers." A session started by hand has no task.json, no criterion, no STATE.md, no board row and no Supervisor respawn — it is a different thing that looks like the right one for about ten minutes.

  3. The §0e delegate table is deleted. A table listing what the role owns is an invitation to own more of it. The role is now defined by one power, so there is nothing adjacent to drift into. The old carve-out — "loop infrastructure stays here" — is retired with it: a carve-out on a rule about drift is exactly where drift lives.

  4. The singleton is enforced by SID, in code. The sid is pinned at ~/.claude/jobs/architect/SID; ~/.local/bin/start-architect refuses while that sid is alive, mirroring start-operator's existing refusal pattern. Resume or attach is the only way in.

  5. blocked gained blocked alive <sid> — 0 alive / 1 dead / 2 undeterminable — so start-architect reuses the fleet's single liveness predicate rather than adding a fourth copy of it.

  6. A successor's FIRST action is blocked adopt --list → adopt → check. Operator §4b's "adopt; never re-raise" is generalised into the architect skill (§2d) as the reason the role exists.

    ⚠️ THE LEDGER IS THE AUDIT PREDICATE, AND task.json MAY NOT BE USED AS ONE. readTasks (supervisor/src/tasks.ts) rebuilds a task file from an explicit allowlist — task, criterion, workerSid, operatorSid, state, repo, createdAt, operatorRespawns, lastSupervisedAt, and a handful of opt-ins. Any other field is silently erased on the next supervisor write. Measured on this very task: startedFrom: "tmux" was present at mint and had vanished by the next read, with nothing logged.

    The consequence is a trap that has already nearly closed live lanes twice: a field like parkedOn is not durable, so "parked with no block id, therefore nothing is waiting" reads an ERASED field as an ABSENT question and closes a lane around a live block. That is the same absence-reads-as-fact error this ADR opens with, arriving through a different door — and it is why the successor step above names blocked adopt --list and blocked check rather than anything derived. The ledger directory is the record; task.json is derived and lossy.

    ⚠️ And §2d states the limitation rather than implying wholeness. Adoption transfers the ledger row; it does not republish the decision page under the new owner's sid, the original raiser's page can remain the live human-visible surface, and no CLI closes that gap today. Writing this step as a clean "adopt and you are whole" instruction would be a documentation-only guarantee — the exact failure class item 3 of this decision exists to remove, and we do not get to commit it inside the change that names it. Adopting is still mandatory: an unadopted block has no live executor, which is strictly worse than one with a stale page.

What is deliberately NOT promised

Name reservation. Nothing reserves the name architect, and saying otherwise would be a documentation-only guarantee — the exact class of failure this change exists to remove. Verified 2026-08-08:

A second observation, and it is deliberately stated narrowly. The pinned Architect sid 0d84ff5f-00e0-4b4e-aeb9-8ea47743befe resolved, in one claude agents --json snapshot, to two rows at oncepropflow-40 (idle, pid 93109) and propflow-c2 (busy, pid 99329).

⚠️ This is not a new discovery, and an earlier draft of this ADR wrongly presented it as one. The primary source is dedupeBySession in relay/src/roster/roster.ts, which exists because of this shape: "claude agents --json can emit the SAME sessionId twice — observed live for cc4ad587… both rows alive, identical staleness, but one says status: busy and the other status: idle (they also carry different pids)… A count that lies is worse than a count that is merely doubled." It resolves the conflict by preferring alive, then busy. The observation above is a second live instance of a documented shape, not a finding.

So the claim this ADR rests on is the narrower one that survives scrutiny:

That is a weaker statement than "a session's name is not even singular", and it is the one that is actually true. It still supports the only use made of it: keying identity on the pinned SID, which is unaffected by either problem. It is also the same hazard that forced session_alive to stop taking hit[0].

⚠️ Said precisely, because an earlier draft of this ADR contradicted itself about it. One sentence here treated a SID-keyed /agents architect tab as though it existed, while Consequences correctly called it unbuilt. It does not exist — searching relay/src finds it only in comments. What is enforced today is start-architect and the pinned SID file; the tab is a proposal.

And the surface that actually resolves "the Architect" right now is not SID-keyed at all: relay/src/ask/ask.ts hardcodes DEFAULT_TO to cc4ad587-…the dead predecessor, which holds zero roster rows. So the system's live default addressee points two generations back, at the very session whose death produced the orphaned blocks in the evidence table above. That is owned by task default-addressee-fix (ee8915e4). It is cited here as the fix rather than used to soften the claim: the rule is right, the implementation is one route behind it, and naming which is which is the whole discipline this ADR is arguing for.

⚠️ AND IT IS NO LONGER LATENT — IT HAS SWALLOWED A HANDOVER. Measured 2026-08-10: a completed Operator (dock-partial-degradation) sent its entire closing report to "the architect", which resolved to cc4ad587. From its own delivery-status section:

Attempted msg to the architect (cc4ad587) at 2026-08-10T04:32:18Z.
REFUSED: 'architect [cc4ad587] has no live pane — it is not running'.
msg does not park for a non-running session, so this report was NOT
delivered.

Every step behaved correctly. msg refused honestly rather than parking into a dead session's inbox. The sender drew the only conclusion available to it — that the Architect was not running — and wrote the report to a file. The live Architect (0d84ff5f, five live processes at the time) received nothing, and had no signal that anything was missing. The report sat undelivered for six hours and would have sat there forever: a closed Operator has stopped its loop, so nothing was left to retry.

⚠️ Note the shape, because it is the ADR's own thesis pointed at the routing layer: the sender saw a clean, actionable refusal and the recipient saw silence. Neither side could distinguish "nothing was sent" from "something was sent to a corpse". A name-shaped default addressee fails exactly the way a name-shaped identity does — which is the argument for the pinned SID, arriving as an incident rather than an argument.

⚠️ AND THE SAME CLASS REACHES THE HUMAN — an instruction typed into a composer and never submitted is INVISIBLE. Measured 2026-08-10, on this task: Gera had typed "merge #45 then check #42 under the real gate" into this Operator's composer line and it was never submitted. It does not queue, does not retry, and never appears in mail. He had every reason to believe he had given the instruction; the Operator had no way to know one existed. A second lane was hit identically the same night, with "latch it — hold once per episode, then go quiet" parked the same way, so it is not a one-off.

The second half is the structural part, and it is worse than the loss: recovering the instruction changes who it came from. The Architect found the text, captured the pane before clearing it, and re-sent it byte-identical — and it arrived stamped [msg from unknown via drive-send · 0d84ff5f], because the relay strips the original author. So the only available repair downgrades the authority of the thing it repairs. Here that inversion decided the outcome: #45 touches .github/*, the human-merge floor, where "merge it" from Gera is authorisation and the identical words from an agent are an agent pushing a peer across a human-only gate. The rescue turned the first into something indistinguishable from the second, and the Operator correctly refused and asked for the author's own words.

The lesson generalises past this ADR: unsubmitted human input should be surfaced to its author, not rescued by a third party. Any repair that routes a human's instruction through an agent converts authorisation into hearsay, and no amount of care by the relayer — this one captured the pane first and did not paraphrase — can restore what the envelope no longer carries.

The name is not the identity and never was. So impostor handling is described honestly as reactive detection: start-architect --impostors reports live sessions named architect that are not the pin, and its own output says it is detection rather than prevention. It cannot stop one appearing.

claude --resume semantics — tested, not assumed

The "resume, never create" rule only holds if resuming preserves the sid, so it was measured rather than read off the flag's help. A throwaway session was pinned to a known uuid:

run result
claude --resume <sid> same sid — one transcript file, grown by the new turn
claude --resume <sid> --fork-session new sid — a second transcript file appeared

The fork run is the positive control: it proves the instrument (transcript-file enumeration) can see a new sid, so the plain run's single file is evidence and not a blind spot. Consequence: the pin survives a resume, and --fork-session is the one flag that would silently orphan the ledger.

Consequences

Alternatives considered