0021 — The role is the Driver, and the plumbing still says operator
- Status: Accepted (the rule and the lag). The vocabulary half ships with this ADR. The Temporal type half is an additive cutover, specified below and implemented in the agent-smith PR that accompanies this one; it is not complete until a live query returns zero running
OperatorTaskWorkflow. Three were running when this was written. ⚠️ AMENDED 2026-09-11 — THE SKILL-ALIAS HALF IS MEASURED WRONG. The loader labels a skill by its directory but keys identity on the frontmattername:, so twoSKILL.mdfiles declaring onename:collapse into a single index entry — the arrangement the original Consequences bullet prescribed (bothskills/operator/andskills/driver/symlinked to one canon file) deletes/operatorfrom the listing, silently. Decisions 3 and 4 and that bullet are corrected in place; the end state now inverts rather than extends. Read Amendment 2026-09-11 at the foot before touching~/.local/binor~/.claude/skills. (Appended toStatuson purpose: this adds NO line.) ⚠️ PARTIALLY SUPERSEDED 2026-09-12 by ADR-0025: decision 5's additive-cutover TARGET is superseded — the type becomesDriverWorkflow, notDriverTaskWorkflow. Its retirement QUERY is not superseded and still governs: the 2026-09-12 reset ends every running execution first so the rename can happen in place with the old names deleted outright, and that is licensed by this decision's own zero-running query returning 0, never by the date. Decisions 1, 2, 3, 4, 6, 7 and the 2026-09-11 amendment stand: the role is still the Driver,/operatoris still a live door, and the product'soperatorstill does not move. (Appended toStatuson purpose: this adds NO line, so every line-number citation into this file stays true.) - Date: 2026-09-10
- Deciders: Gera, 2026-09-10, on hitting the ambiguity in conversation: the product's
operatoris a property-management company, a customer-facing term we do not control, and the harness role is ours — so ours yields. The name is Driver, which the fleet had already half-adopted:~/.claude/skills/drive,~/.local/bin/drive,drive --replace, and "drive"/"driver" throughout his own vocabulary. The role name was the last piece out of step.
The decision
- The harness role is the DRIVER. Not "agent operator" — that still contains the colliding word and does not disambiguate when spoken, which is where the collision actually bites. Renaming removes an inconsistency rather than adding one: every other artifact in the loop already said "drive".
operatorbelongs to the product, in the COMPANY sense, and nothing inpropflowaichanges. The person-senseoperator→staffmigration inside the product is a different migration with its own ratchet (scripts/operator-rename/ratchet.json) and is not ours. Touching it would also add pressure to a ratchet already at thin margin.- The vocabulary layer renames now; the invocation layer deliberately lags. Docs, ADR prose,
SYSTEMS.md, and a skill's prose are vocabulary. A string another process starts, greps, or keys on is an interface, not vocabulary, and it keeps its name until it can be changed safely. ⚠️ Corrected 2026-09-11: this rule originally counted “the skill and its trigger text” as vocabulary. By this rule's own test it is not — aSKILL.md's frontmattername:is an interface: the loader keys skill identity on it, a typed/operatorresolves against it, and two files declaring the samename:collapse into one entry. It renames only as part of the atomic cutover in Amendment 2026-09-11. - These still say
operator, ON PURPOSE, and are not bugs:- Temporal workflow IDs —
op-task-sess-<name>. Deterministic, and derived identically by every door. Changing the scheme mid-flight orphans a live execution that nothing would then be watching. - On-disk task directories under
~/.claude/jobs/tasks/. - The
operator-hereandstart-operatorbinaries, and everything in~/.local/bin— a live shared checkout on PATH for every session, where a rename is an instant fleet-wide deploy. - The
/operatortrigger phrase, which humans and running coordinators keep minting for hours after any rename — the Supervisor's own respawn kickoff mints it verbatim (supervisor/src/spawn.ts:213,Skill tool, name "operator"). It stays a live door onto the Driver doctrine. ⚠️ Corrected 2026-09-11: “live alias” must mean a second skill entry carrying its own distinctname: operator, never two paths sharing onename:. The loader cannot index one skill under two names; asking it to do so drops one of them without erroring. See Amendment 2026-09-11. operatorFirst,operatorKickoff,operatorSid,superviseOperators, theoperator-<task>session-name prefix and theop-tmux prefix — round-tripped markers and parsed prefixes.- Verbatim quotes. Renaming a word inside a quotation is falsification, not a rename. Quotes stay as spoken; the pages that carry them say so at the top.
- Temporal workflow IDs —
- The workflow TYPE renames by ADDITIVE CUTOVER, never in place.
OperatorTaskWorkflow→DriverTaskWorkflow, in this order and no other:The worker registers both type names, serving one implementation.
Every new arming starts
DriverTaskWorkflow.Existing executions finish under the old name as their sessions close. They do not drain on a schedule —
_continuecallsworkflow.continue_as_new()with no workflow argument, which preserves the type across hops, so a long-lived Driver staysOperatorTaskWorkflowfor its whole life however many runs that spans.The old registration retires only when a live query returns zero running executions of that type — not on a guess, and not on a date. The query, with
SMITH_TEMPORAL_*frompropflowai/.env.local:temporal workflow count --query \ "WorkflowType = 'OperatorTaskWorkflow' AND ExecutionStatus = 'Running'"It read 3 when this was written. At 0: delete the
OperatorTaskWorkflowalias class inworkflows/operator_task.py, drop it fromworker.py's registration list, and collapse the twoWorkflowType IN (...)queries — plus~/.local/bin/watcher-doctor's — back to=. Written as commands because a retirement condition nobody can execute is a fact somebody has to remember.
- Every visibility query that names the type must name BOTH for the whole dual window.
WorkflowType = 'OperatorTaskWorkflow'appears in the boot reconciler and in the mint cap's counter. Left alone, the cap counts only old-type executions while new ones are invisible to it — and the cap's entire reason for existing is spawn-side runaway. A rename that half-updates the census is worse than no rename. - The condition for closing the gap is the Sunday 2026-09-13 rollout deadline, plus, for the workflow type, the zero-running query above. Until both are met this ADR is the record that the mismatch is intentional.
Why
The word was doing two jobs. In the product an operator is a property-management company — the customer. In the harness an "Operator" was the session that holds the loop and drives sub-agents. Both were said out loud, in the same conversations, to the same people, and on 2026-09-10 the founder hit the ambiguity in conversation. Only one of the two is ours to move.
The argument for Driver specifically is not that it is a better word in the abstract. It is that the fleet had already adopted it everywhere except the role name: the skill is drive, the binary is drive, the goal CLI is drive --replace, and the founder's own vocabulary was "drive"/"driver" throughout. The role name was the one piece still out of step, so this rename reduces the number of names in play rather than raising it.
Why the plumbing does not move with the prose
A name that a human reads is a word. A name that a process starts, greps, or keys on is an interface with an installed base. op-task-sess-propflow-b1 was RUNNING while this was written, watching the very session doing the rename; two siblings were running beside it, all three mapped to live tmux panes, zero zombies. Nothing about a mid-flight id change fails loudly — the execution simply stops being driven, and a watcher that has silently stopped watching looks exactly like a watcher with nothing to report.
One correction to the obvious model of the type half, because it changes how alarming this is. Dropping the old registration does not orphan the execution beyond recovery. The SDK raises ApplicationError(..., type="NotFoundError") for an unregistered type; that error is retryable, so it reports a workflow task failure rather than a workflow failure. The execution stays Running and the task is retried until the Workflow Execution Timeout, which is unlimited by default. So the real shape is quiet, not silent, and recoverable, not fatal: no data is lost, and re-registering the name lets the watcher resume where it was. It is quiet in the way that matters, though — it surfaces only in worker logs (temporalio.worker._workflow), as repeated WorkflowTaskFailed events in history, and in the workflow_task_execution_failed metric. The workflow's own status shows nothing wrong, which is precisely why nobody would look. (Verified against temporalio 1.30.0, the pinned version.)
The same asymmetry is why the workflow type is additive rather than a rename. A running execution carries its type name in its history, and a worker can only continue it if that name is still registered. The type name here is pinned by a string — @workflow.defn(name="OperatorTaskWorkflow") — and not by the Python class, which is what makes the additive shape cheap: the class can be renamed freely, and the string is the contract. Cheap is not the same as free, which is the point of decision 6.
A faster drain exists and was deliberately not taken: workflow.continue_as_new(..., workflow=DriverTaskWorkflow.run) is supported by the SDK and the command proto, and would migrate each old execution to the new type on its next hop under the same workflow id. It is rejected here for two reasons — it is doc-silent (the Python continue-as-new guide only shows same-type), and it changes a live execution's type mid-life, which is a determinism question layered on top of a rename rather than instead of one. Letting three executions finish under the old name costs nothing and asks no question.
Why this is written down instead of remembered
An undocumented mismatch becomes a fact somebody has to remember. The next reader greps operator, finds ~40 live hits in the plumbing, and has to decide from scratch whether each one is an oversight or a decision — and the cheapest wrong answer available to them is "the rename was sloppy, let me finish it", which is precisely the action that breaks running work.
This failure mode is not hypothetical here. docs/the-four-roles.md declares itself the tiebreaker and rotted nine minutes after the merge that invalidated it, in the same session, by someone who knew the new rule: the doors table was updated because that was what the author was looking at, and the argument two sections down was not. A page that must be remembered to stay true will become false. So the lag is recorded as a decision with a closing condition, and the pages that still show the old word carry the reason at the top rather than in anyone's head.
Consequences
- Two names are live at once, deliberately — as TWO index entries with two distinct
name:values, one of them a forwarder./operatorand/driverboth reach the Driver doctrine for as long as running coordinators keep minting the old phrase. A missing skill is worse than a superseded one — the call errors and the session proceeds with no doctrine at all, which is quieter and harder to notice. This is the same reasoning that keptgod/childas forwarders. - ⚠️ SUPERSEDED BY Amendment 2026-09-11 — this bullet described an arrangement the loader cannot hold. It read: “
loop-doctor's canon map gains a second entry, and keeps the first. It heals~/.claude/skills/<role>/SKILL.mdfrom~/.local/bin/canon/<role>-SKILL.mdby an explicit pair list. A rename that updated only the new pair would leave the old live path unhealed; dropping the old pair entirely would strand any session that still resolves it.” Two pairs pointing at one canon file give twoSKILL.mdfiles onename:, and the loader keeps only one of them — measured, see the amendment. The map does gain and lose entries, but the shape is an inversion, and the forwarder side of it is not somethingloop-doctorcan heal as it stands. - The grep for
operatorno longer means "work left to do". After this, a hit is either the product's company sense, a live invocation named in decision 4, or a quote. That is the state this ADR exists to make readable. - The second half is a separate, later change and inherits this ADR's ordering: ids and task dirs move only when nothing is running against the old scheme, after the Sunday 2026-09-13 rollout deadline.
Amendment 2026-09-11 — the loader labels by directory and keys identity on name:
- Amends: this ADR's decisions 3 and 4 and the first
loop-doctorbullet under Consequences, all corrected in place above. Nothing else in ADR-0021 changes: the rename, the product/harness split, the Temporal additive cutover and the census rule (decisions 1, 2, 5, 6, 7) stand as written and are untouched by this. - Date: 2026-09-11
- Why an amendment and not an ADR-0022: the decision is unchanged — the role is still the Driver, and
/operatoris still a live door. What was wrong was a mechanism claim inside it, and ADR-0020's rule applies: two records of one decision is the defect, not the fix. This follows ADR-0016's precedent, where a correction was written into the file it corrects.
The measurement
A lane built exactly what the old bullet prescribed: ~/.claude/skills/operator/SKILL.md and ~/.claude/skills/driver/SKILL.md, both symlinked to the one canon file — therefore both declaring the same frontmatter name:. It then read the skill listing from a fresh-context agent, which is the only instrument that shows what the loader actually indexed; the listing held by the session that made the change is stale and would have shown the old, healthy state.
The listing showed driver, and no operator at all. Not an error, not a warning, not a duplicate — one entry, one name, the other door simply absent. The arrangement was reverted within two minutes, and all four of operator, driver, drive and worker were verified listed again afterwards.
The mechanism, stated as measured: the loader labels an entry by its directory name, and keys identity on the frontmatter name: field. Two SKILL.md files declaring the same name: collapse into a single index entry. Which of the two survives is not specified by anything we can read, so it is not a choice — it is a coin-flip whose losing side is a missing front door.
Therefore an alias must carry its own distinct name:. A second symlink onto one canon file is not a second door; it is a wager that the door you need is the one that wins.
Why this is urgent rather than tidy
~/.claude/skills/operator/SKILL.md is not an editable file. It is a symlink into ~/.local/bin/canon/operator-SKILL.md, and all 74 KB of Driver doctrine — including the name: and description: the loader reads — lives there.
So the moment the local-bin PR renames canon to name: driver, /operator stops existing: not with an error anyone would see, just absent from the listing. It is a hand-typed front door, it is minted by the Supervisor's respawn kickoff (supervisor/src/spawn.ts:213), and it is referenced by automation across the machine. A session that types /operator into a fleet where the skill is gone does not stop — it proceeds with no doctrine at all, which is the exact failure this ADR's own Consequences section says is worse than a superseded skill.
The end state INVERTS today's arrangement
| today | after the canon rename | |
|---|---|---|
~/.claude/skills/driver/SKILL.md |
regular file, name: driver, a one-hop forwarder pointing at the operator skill |
symlink into canon, carrying the doctrine and name: driver |
~/.claude/skills/operator/SKILL.md |
symlink into canon, name: operator, carries the doctrine |
regular file, name: operator, a one-hop forwarder pointing at the driver skill |
| canon file | canon/operator-SKILL.md, name: operator |
canon/driver-SKILL.md, name: driver |
The named forwarder and the named doctrine swap sides. Both /operator and /driver keep working throughout, and at no point do two entries share a name:. The old bullet's error was to read the arrangement as something you extend with a second link, when the only legal move is to invert which name holds the bytes.
The swap is ONE atomic step, because the collision has a second form
There is a second collapse waiting one step later, and it is the reason this cannot be staged. The instant canon declares name: driver, today's skills/driver/SKILL.md forwarder — which already declares name: driver — is a duplicate, and one of the two is dropped with no rule saying which. A cutover that renames canon first and fixes the skills directory afterwards therefore has a window in which the Driver doctrine itself may be the entry that loses.
So the local-bin PR performs all of this as one step, with no intermediate state in which two SKILL.md files share a name::
- rename
canon/operator-SKILL.md→canon/driver-SKILL.md, frontmatter toname: driver; - replace
skills/driver/SKILL.md(today's forwarder) with the symlink into the renamed canon file; - replace
skills/operator/SKILL.md(today's symlink) with thename: operatorforwarder; - update
loop-doctor'sPAIRSin the same commit (below).
loop-doctor cannot perform this swap, and cannot heal a forwarder
Its check 1 is symlink integrity only, and its four cases are explicit: MISSING → link it; RIGHT LINK → nothing; WRONG LINK → report; REGULAR FILE → REPORT ONLY, NEVER TOUCH. That last rule is the scar from 2026-08-07 and 2026-08-19, when a cp that could not tell a casualty from a conflict ate a fix and then 64 lines of the worker skill. It is not to be weakened for this.
Two consequences follow, and both are load-bearing:
- Every step of the swap is a hand step in the PR. Today's
skills/driver/SKILL.mdis a regular fileloop-doctormay not replace, andskills/operator/SKILL.mdre-pointed at a new canon file reads as WRONG LINK → report.loop-doctormaintains the end state; it cannot reach it. - A plain-file forwarder is unhealed. If it is deleted or clobbered,
loop-doctorreports and moves on, and the front door stays gone.
The decision: canon grows the forwarder, so the front door is healable. Add ~/.local/bin/canon/operator-forwarder-SKILL.md (name: operator, its own description:, no doctrine), and in the same commit PAIRS becomes:
- drops
operator-SKILL.md:$HOME/.claude/skills/operator/SKILL.md - adds
driver-SKILL.md:$HOME/.claude/skills/driver/SKILL.md - adds
operator-forwarder-SKILL.md:$HOME/.claude/skills/operator/SKILL.md
The cost, stated rather than implied: the forwarder's bytes become canon-managed, so editing that page is a fleet-wide deploy like any other canon edit; and the map goes from six parts to seven, so loop-doctor --version reports 7. No signal parses that count — checked across SYSTEMS.md and ~/.local/bin, the only other hit is an unrelated 6/6 done in operator-here — so the change costs a human re-reading, not a red rung.
The rejected alternative is to leave operator out of PAIRS entirely and let the forwarder be an ordinary unhealed file — the god/child precedent. That is right for a dead door nothing mints any more. It is wrong for this one: /operator is minted by a human's fingers and by the Supervisor on every respawn, and its failure mode is silent absence rather than a loud error. A door that automation keeps knocking on is worth a pair.
Sequencing
This amendment lands before the ~/.local/bin rename PR opens. That ordering is not courtesy. The correct shape of that PR — invert, atomically, with a forwarder in canon — is derived from the measurement recorded here, and the shape it would otherwise have taken is the one this ADR used to prescribe. A PR written against the superseded bullet would delete /operator and look like it had followed the ledger doing it.
Acceptance for the local-bin PR (ADR-0004's liveness clause)
Not "the files look right". Two checks, both after the swap:
- A fresh-context agent's skill listing shows all four of
operator,driver,drive,worker. A listing taken in the session that made the change is not the instrument — that is the trap this amendment exists because someone avoided. loop-doctor --versionreports7/7 parts linked. Six-of-seven means one of the three skill paths is not a symlink into canon, which is the same silent state in slower motion.
Inventory of the surface that states the old shape (ADR-0004 §2)
| Surface | State |
|---|---|
ADR-0021 Consequences, the loop-doctor bullet |
✅ Superseded in place in this commit, with the original text quoted so the reasoning survives. |
| ADR-0021 decision 3 ("the skill and its trigger text are vocabulary") | ✅ Corrected in this commit. The frontmatter name: is an interface by decision 3's own test. |
| ADR-0021 decision 4 ("stays a live alias onto the Driver skill") | ✅ Corrected in this commit — an alias is a second entry with its own name:. |
~/.claude/skills/driver/SKILL.md — "read ~/.claude/skills/operator/SKILL.md", "/operator stays a live alias onto this same role" |
⚠️ True today, backwards after the inversion. It is rewritten as step 2 of the atomic swap, not here: editing it now would describe a state that does not exist yet, and it is a live file thirteen running lanes can load. |
SYSTEMS.md, the fed-Driver entry — "driver/SKILL.md §1 already said…" |
⚠️ Already false in the other direction: that path is today's 2 KB forwarder and has no §1; the quoted doctrine is in operator/SKILL.md. It needs no edit — the inversion makes it true. Recorded because it shows the prose had already assumed which name holds the bytes. |
~/.local/bin/loop-doctor PAIRS |
⚠️ Changes in the local-bin PR, exactly as listed above. Not edited here — that checkout is on PATH for every session and a write to it is an instant fleet-wide deploy. |
supervisor/src/spawn.ts:213 — Skill tool, name "operator" |
✅ Unchanged on purpose. It mints the name the inversion keeps alive. It is the reason the forwarder must exist, not a thing to fix. |
The premise this got wrong, and where else it had reached
The root error was decision 3's classification, not the bullet that expressed it. ADR-0021 drew exactly the right line — a string a process keys on is an interface — and then put a skill's name: on the wrong side of it, so the two places that reasoned from it (the "live alias" in decision 4 and the "second entry" in the Consequences) both inherited an impossible mechanism.
The ADR also contained its own refutation. Consequences bullet 1 cites god/child as the precedent — and those are distinct-name forwarders, each with its own name: and its own description:, which is the shape this amendment restores. The precedent was cited correctly and then not followed; the very next bullet prescribed an arrangement god/child do not use and could not have survived.