Operator → Driver — the fleet-tooling rename, audited and planned

Every place the fleet's tooling still says operator, sorted into what should become driver, what must stay because it is the product's word, and what is a wire-level key that needs a compatibility shim rather than a rename. With the four-PR plan to get there without breaking a running fleet.

2026-09-13 · scope: ~/.local/bin (local-bin), ~/.claude (dotclaude), agentflow, agent-smith, propflow-docs, and the agentflow slice of propflowai · status: audit complete, part 1 opened, parts 2–4 planned, ADR amendment drafted (not merged)

One word now means two unrelated things, and only one of them is ours to move. In the product an operator is the property-management company (ConAm is the operator; JP&Co the acting operator — the portfolio architecture's load-bearing entity). In the harness "Operator" was the session that holds a dock's loop. ADR-0021 renamed the harness role to Driver on 2026-09-10 — but only the doctrine moved. The binaries, the tmux prefix, the operatorSid key and the log lines still say the old word, and on 2026-09-13 Gera hit it again: "why do we see operator everywhere? Shouldn't it be driver?"

The three buckets

≈3,400
(R) role usages → "driver"
prose, comments, log and say() lines, usage text, test names; ~1,000 of them in tests
≈2,600
(K) keys / identifiers → shim
task.json fields, binary names, tmux + roster prefixes, workflow ids/types, skill name:, launchd labels
≈9,500
(A) architecture — do not touch
8,668 in propflowai (company/person sense, own ratchet) + 852 in propflow-docs portfolio pages

How the counts were made: git grep -nE "operator|Operator|OPERATOR|\bop-|opSid" per repo and grep -rnE over ~/.local/bin and ~/.claude (excluding logs, .bak, __pycache__ and the six repo-main mirrors under ~/.claude/scripts), then a line-level classifier: a line is (K) if it names a known identifier (operatorSid, operatorFirst, op-task, op-${…}, operator-${…}, start-operator, OperatorTaskWorkflow, name: operator, --actor operator, …), (A) if it lives in a portfolio/product page, else (R). The totals are approximate — a census of matches, not of meanings. Every item named in the tables below was classified by reading the surrounding code, and every (K) reader marked "run" was executed against the live task.json.

(R) — fleet-ROLE usages that should say "driver" (the important ones)

WhereWhat still says operatorPartNote
local-bin canon/rearm-nudge-after-compact.shsay() lines 210/221/230: "not arming a loop (only Operators loop)", "If this session is an Operator its loop just died here", and it prints the raw role token (is a operator)1The case on the token operator is the interface and stays; only the sentence changes. Precedent: loop-keeplist's ROLE_WORD map already prints DRIVER while returning operator (commit dcb244e, test keeplist-says-driver-not-operator.py).
local-bin drive, task-event, nudge, puller, blocked, spawn-pair, watcher-doctordocstrings, usage text and comments: "this session's Operator", "The Operator layer judged…", "only Operators loop"1Usage/print text in part 1; comments as a follow-up at leisure (they blow the line budget and nobody reads them at 3am).
local-bin fleet-grader, loop-keeplistdoneAlready say DRIVER on the terminal (grader row "DRIVER", keep-list prose). Cited as the pattern.
dotclaude hooks/goal-poke.sh20 prose lines + 2 log lines "POKE … -> operator …"; dir jobs/operator/ is (K)1bLog lines parsed by nothing (checked SYSTEMS.md signal patterns: none match "operator").
dotclaude hooks/context-budget.py, scripts/verify-agent-send.sh, scripts/pf-arm-operator.sh, skills/drive, skills/post-updates, skills/blocked, skills/slackcomments and skill prose1bSkill prose is vocabulary per ADR-0021 §3; frontmatter name: is not.
agentflow supervisor/src/index.ts (86 R lines), intake.ts, spawn.ts, exits.ts, coverage.ts, quietstop.ts, escalate.ts, alive.tslog lines a human reads in supervisor.log: "operator X looks dead (…) — 1/3, waiting", "held operator X", "no operator and mint cap reached", "told operator X to close out"; coverage rows "no operator spawned yet", "operator session is gone"; the escalation copy "has burned N Operators"1cSafe: fleet-grader._supervisor_says matches only mint|respawn|cap, never the word. Tests DO assert some lines (/No operator was ever running/, /could not tear down operator session/, /still has no operator session/) — update in the same PR. Escalation kind id operator-respawn-cap is (K).
agentflow relay/src/health/health.ts, digester.ts, pulse/classify.tsrendered text "subagent of the Operator" (asserted by health.test.ts:516,609)1cDisplay text; asserted in tests, so the test moves with it.
agentflow docs/the-four-roles.md, CLAUDE.md, SYSTEMS.md, 18 ADRsvocabulary — mostly already carries the "renamed 2026-09-10" banner; ADR prose stays as written (an ADR is a record)SYSTEMS.md entries that name a binary get edited when the binary does (part 2).
agent-smith types.py (66), phase_table.py (57), workflows/reply.py (47), prompts.py (43), wisdom.py, work_elsewhere.pydocstrings, the phases-card copy, the reply prompt's description of the role1dPrompt text reaches the model — change it with an eval run, not blind. Message prefixes [op-task watcher] are asserted by tests and read by sessions → part 2.
propflow-docs artifacts/operator-architecture.html ("The Operator — how the fleet runs itself"), nine-doors-one-store.html, operators-on-temporal.html, ~40 other fleet pageshistoric pages describing the fleet roleAdd the same "renamed 2026-09-10" banner the ADRs carry; do not rewrite quotes. Verbatim quotes stay (ADR-0021 §4).

(A) — ARCHITECTURE usages that must NOT change (the boundary)

WhereHitsWhy it stays
propflowai src/ (4,249), docs/ (2,957), scripts/ (940), agents/ (209), evals/, lambda/, …8,668The portfolio entity: operator vs owner vs staff, operatorOrgId, operatorDomains, owner-operator. It has its own migration (person-sense operator → staff) with its own ratchet — scripts/operator-rename/ratchet.json (5,006 at HEAD) and protect-list.json. ADR-0021 §2: "nothing in propflowai changes". Exception carved out below: the 9-file agentflow slice and the 14 "fleet-sense" identifiers the protect-list already isolates.
propflow-docs portfolio-architecture-phases/how/deck/options/podcast, operator-migration-kickoff, operator-vocabulary-migration ("The word 'operator' — decision and migration"), situs-*, sales-*, settings-*, vendor-*, adr-0045-namespace-cut, episodes/, artifacts/audio/*.vtt852Product architecture and its own migration's pages. bin/derive-live-lanes reads scripts/operator-rename/ratchet.json from propflowai — that is the product ratchet, not a fleet key.
Verbatim quotes, everywhere"Renaming a word inside a quotation is falsification, not a rename" (ADR-0021 §4).

(K) — load-bearing KEYS and identifiers: shim, do not rename

Each row is a string some process starts, greps, parses or keys on. "Run" means the reader was executed today against the live ~/.claude/jobs/tasks/sess-propflow-ef/task.json (driver 9fa34932, worker 1fdb267c, state working) and consumed the field; "source" means the consumption was read in code but the path could not be exercised (no live incident, or it runs inside Temporal).

Key / identifierWritten byRead byVerifiedShimRetire when
operatorSid in ~/.claude/jobs/tasks/<slug>/task.json (with workerSid, operatorFirst, operatorRespawns, operatorKickoff) supervisor tasks.ts readTasks — rebuilds the file from an allowlist every sweep (any unknown key is erased); index.ts writes it at 508/631/677 · start-operator:656 · spawn-pair:271 · agent-smith thread_operator.py (via start-operator) · drive (criterion only) supervisor index/coverage/exits/alive/redmain/quietstop/inbox.ts · relay roster.ts, intake.ts:373, digester.ts:485, collector.ts:540, health.ts · local-bin loop-keeplist:331, fleet-grader:734, drive:154, driver-brief:157, nudge:1103, roster:169, session-cap:641, blocked:4633, watcher-doctor:434 · dotclaude hooks/guard-bash.sh:177, hooks/goal-poke.sh, canon/drive-gate.py:879 (a PreToolUse hook), scripts/operator-here, propflow-status/operator-heartbeat.py · agent-smith phase_table.py:631, session_sweep.py:404, decision_block.py:1051, task_events.py, activities/operator_task.py run loop-keeplist --roleoperator for 9fa34932, undetermined for a bogus sid (control) · fleet-grader --json"1 driver(s) alive: sess-propflow-ef(9fa34932 3m)" · roster → joins on workerSid first, so the driver row reads UNREGISTERED (a display defect, noted) · supervisor/relay/smith: source Yes — dual-key. Supervisor first (accept + carry + write both), then readers driverSid ?? operatorSid, then writers write both, then relay serves both. A count, not a date: grep -L driverSid ~/.claude/jobs/tasks/*/task.json is empty AND the propflowai + agent-smith deploys that read driverSid are live.
operatorSid in ~/.claude/jobs/red-main/claim.json / status.jsonsupervisor redmain.ts (single writer)agent-smith red_main_owner.py:307 · fleet-grader:167 · SYSTEMS.md prosenot run — no live incidentSame dual-key, same PR as the supervisor's task.json change (one writer).With the task.json shim.
relay GET /api/feed JSON: operatorSid, operatorConnected, taskRole: 'worker'|'operator', operatorFirstrelay index.ts:333, roster.ts:221-242propflowai (Vercel, separate deploy cadence): src/lib/domain/agentflow/session-rows.ts, types.ts RelayTask, intake-helpers.ts, FleetHealthTab.tsx:380, api/agents/message/route.tssource (the propflowai lane reads the deployed relay; not exercised here)Yes — serve both for at least one propflowai deploy; the reader lands first (driverSid ?? operatorSid).After a propflowai deploy reading the new key is confirmed live (/agents shows a driver).
tmux session prefix op-<task>supervisor spawn.ts:379,512 · start-operatorsupervisor kill/adopt namespace (sessionStillExists("op-…"), sidForTmuxSession("op-…") at index.ts:505,627) · propflowai intake-helpers.ts:139,193 excludes op-<slug> rows · agent-smith operator_task.pyrun live: op-sess-propflow-ef, op-red-main-6dd6dff06No shim possible — a tmux name cannot be two strings. Change only with everything ended (a reset, as 2026-09-12), or accept op- forever as a 2-letter namespace that no longer reads as "operator".Recommendation: keep op-. Document it as "the Supervisor's namespace", not an abbreviation.
roster session-name prefix operator-<task>the Driver itself, told to by start-operator:374 / spawn.ts:230 ("RENAME YOUR CLAUDE SESSION to: operator-$TASK")agent-smith operator_task_cli.py:133 (removeprefix("operator-")) · spawn.test.ts:366sourceKickoff says driver-<task>; smith strips either prefix.When no live roster row starts with operator-.
operator sid: <uuid> — the stdout line start-operator printsstart-operatoragent-smith thread_operator.py:186 parses itsourcePrint the new line and keep the old one for one smith deploy.Part 4.
Binaries start-operator, operator-here (byte-identical copies in two repos: ~/.local/bin/ and ~/.claude/scripts/), operator-workflow, verify-operator-here.sh, propflow-status/operator-heartbeat.py, spawn-operator-bound.shsupervisor redmain.ts (spawns start-operator, 34 refs), agent-smith thread_operator.py (23), architect-SKILL.md, fixer-SKILL.md, driver-setup, watcher-doctor, task-event, drive, smith-row-lease, agent-audit skill, 4 launchd plists, propflowai review-webhook/listener.js, SYSTEMS.md signalscallers enumerated by grep (table above)Yes — forwarders. New name is the real file; old name execs it and logs one DEPRECATED-NAME line so part 4 has evidence. Collapse the two operator-here copies to one file + one forwarder.When the deprecation log is empty for 14 days after the last caller PR merges.
Skill name: operator (canon/operator-SKILL.md~/.claude/skills/operator/SKILL.md) and the driver forwarderthe skill loader (keys identity on name:) · supervisor spawn.ts:214 mints Skill tool, name "operator" · loop-doctor PAIRS · goal-poke.shmeasured 2026-09-11 (ADR-0021 amendment): two files sharing one name: collapse to one entryYes — the atomic inversion ADR-0021's amendment prescribes: canon becomes driver-SKILL.md / name: driver; operator becomes a canon-managed forwarder; loop-doctor reports 7/7./operator stays a live door indefinitely (it is minted by the Supervisor).
Temporal workflow types OperatorTaskWorkflow, DriverTaskWorkflow (aliases of DriverWorkflow); ids op-task-<slug>agent-smith worker.py:220-222,476,491 registers all three; operator_task.py:84 OPERATOR_TASK_WF_ID_PREFIX = "op-task-"Temporal; watcher-doctor queries; phase_table.py:808 builds the URLrun temporal workflow count … Running: OperatorTaskWorkflow = 0, DriverTaskWorkflow = 0, DriverWorkflow = 1Retirement is now LICENSED by ADR-0021 §5.4 (the zero-running query). Delete the two alias classes and collapse the WorkflowType IN (…) queries. The op-task- id prefix stays (ids are durable; a running one exists).Now (part 2).
Role tokens: loop-keeplist --roleoperator; task-event --actor operator; relay taskRole: 'operator'; escalation kind operator-respawn-cap; jobs/operator/by-session and jobs/operator/poked dirskeeplist, task-event (JSONL rows on disk), relay, supervisor escalate.ts:222, drive/coverage.tsrearm-nudge-after-compact.sh case · every task's events.jsonl history · propflowai · escalation ledger rows · goal-poke.shrun (keeplist)Accept both tokens on read; emit the new one only after every reader accepts it. Historic events.jsonl rows are never rewritten.Part 4, after a grep of live readers shows only dual-readers.
launchd labels co.propflow.operator-heartbeat and log path state/operator-heartbeat.logthe plistSYSTEMS.md signals (launchd label + log-match-age path) — a renamed script with a stale signal goes green foreverSYSTEMS.md entry readRename script + label + signal in one PR; the catalog entry moves with the file.Part 2.
propflowai "fleet-sense" identifiers: OPERATOR_SID (36), operatorSid (15), OperatorComposer, OPERATOR_CHANNELS, messageOperator, operatorTmux, … (14 ids)propflowai src/app/(operations)/agents/*, src/lib/domain/agentflow/*propflowai itself; protect-list.json baselines them and gates their rename on decision fleet-operator-senseprotect-list readSee the open question below — the ruling on record for these is not "driver".After Gera answers.

The plan — four vertical slices, each ≤300 lines, each off origin/main, never stacked

PartRepo(s)What changesWhat must NOT changeSizeStatus
1 — wordinglocal-bin (gera-propflow/local-bin)The sentences a human reads on a terminal: rearm-nudge-after-compact.sh's three say() lines (with a token→word map so it prints "a driver", never "a operator"); drive and task-event usage prose. One new control test that runs the hook and reads what it printed.Every case token, every file name, every key, every printed line another process parses (operator sid:, RENAME … operator-$TASK).~60 linesPR openedlocal-bin#186 (not merged: merging local-bin is an instant fleet-wide deploy; wording only, safe to merge)
1b / 1c / 1d — the same recipedotclaude · agentflow · agent-smith1b: goal-poke.sh log lines + hook comments. 1c: supervisor/relay log and coverage text (+ the three test regexes that assert them). 1d: smith docstrings and phases-card copy; the reply prompt only with an eval run.1c: operator-respawn-cap id, Skill tool, name "operator", op-/operator- prefixes. 1d: [op-task watcher] prefixes (tests + sessions read them).~80 / ~150 / ~200planned
2 — names with aliaseslocal-bin, dotclaude, agent-smith, agentflow (SYSTEMS.md)(a) start-driver, driver-here, driver-workflow become the real files; start-operator/operator-here/operator-workflow become 3-line exec forwarders that log DEPRECATED-NAME. (b) ~/.claude/scripts/operator-here (the byte-identical second copy) becomes a forwarder to the local-bin file; start-operator's kickoff text points at the one real path. (c) The skill inversion, atomically, exactly as ADR-0021's 2026-09-11 amendment prescribes (canon/driver-SKILL.md, canon/operator-forwarder-SKILL.md, loop-doctor PAIRS 7/7, fresh-context listing shows all four). (d) agent-smith: delete the OperatorTaskWorkflow/DriverTaskWorkflow alias classes and collapse the IN (…) queries — licensed by today's zero-running count. (e) operator-heartbeat.pydriver-heartbeat.py with the launchd label and the SYSTEMS.md signal in the same PR.op- tmux prefix (no alias possible for a tmux name). op-task- workflow ids. operatorSid and every task.json key.2a ~120 · 2b ~40 · 2c ~150 · 2d ~120 · 2e ~90 — five PRsplanned
3 — operatorSiddriverSid, dual-keyagentflow first, then local-bin + dotclaude, then agent-smith, then propflowaiOrder is the whole design: 3a supervisor tasks.ts allowlist accepts and carries both keys and writeTask writes both (the supervisor rewrites every task.json each sweep, so it back-fills the fleet for free); redmain.ts writes both into claim.json. 3b every local-bin/dotclaude reader reads driverSid ?? operatorSid; start-operator/spawn-pair write both. 3c agent-smith readers dual-read. 3d relay serves both fields (driverSid, driverConnected, taskRole: 'driver' alongside the old). 3e propflowai reads the new field with fallback — after the #7545 question is answered.Nothing stops writing operatorSid in part 3. Historic events.jsonl rows. _archive/ task files.3a ~150 · 3b ~200 · 3c ~120 · 3d ~80 · 3e ~100 — five PRsplanned; 3e blocked on the decision
4 — retireallDrop the operatorSid writes, then the fallbacks; delete the forwarder binaries; rename the operator role token once every reader accepts driver; move the 2026-09-13 amendment's status to Done.Verbatim quotes. ADR prose. op-.~200 across 3 PRsgated on the two counts above, not on a date

The ADR finding

ADR-0021 covers the doctrine rename and, in §4, explicitly keeps operatorSid, operatorFirst, operatorKickoff, superviseOperators, the operator-<task> prefix and the op- prefix "on purpose". Its Consequences say only that "the second half is a separate, later change … after the Sunday 2026-09-13 rollout deadline" — a date, and no mechanism. Its 2026-09-11 amendment covers the skill name: inversion (part 2c above, followed verbatim). ADR-0025 superseded §5's target type only (DriverWorkflow). So the identifier-level rename is not covered, and per ADR-0004 it is not done until its record is written. Per ADR-0020 (two records of one decision is the defect) this is an amendment to 0021, not a new ADR. Drafted below; not merged — it goes in with part 3a, by whoever is in the room when 3a opens, and only after the #7545 question is answered.

## Amendment 2026-09-13 — the identifier half: a dual-key cutover, retired by a count

- **Amends:** decision 4 (the list of things that "still say operator, on purpose") and
  the last Consequences bullet ("the second half is a separate, later change"). Nothing
  else changes: the role is the Driver, `/operator` stays a live door, the product's
  `operator` does not move, `op-` stays the Supervisor's namespace.
- **Date:** 2026-09-13. Gera, same day: *"why do we see operator everywhere? Shouldn't
  it be driver? I thought we've gone away from the word operator since it's now being
  claimed by our architecture."*
- **Why an amendment:** decision 4 deferred the identifiers with a date and no
  mechanism. The date has arrived; this supplies the mechanism.

### The decision

1. **`operatorSid` becomes `driverSid` by DUAL KEY, never by rename.** The same file
   carries both for the whole window. `operatorFirst`, `operatorRespawns` and
   `operatorKickoff` move the same way (`driverFirst`, `driverRespawns`,
   `driverKickoff`). The red-main `claim.json` moves with them; it has one writer.
2. **The Supervisor moves first, because it is the only writer that rewrites every
   task file.** `supervisor/src/tasks.ts readTasks` round-trips an allowlist and erases
   any key not on it; until that allowlist carries `driverSid`, a reader that prefers
   `driverSid` reads nothing. So the order is fixed: (a) supervisor accepts, carries and
   writes both; (b) every reader takes `driverSid ?? operatorSid`; (c) every other
   writer writes both; (d) the relay serves both over `/api/feed`; (e) propflowai reads
   the new key with fallback. No step may land before the one above it.
3. **Retirement is a COUNT, not a date.** The old key stops being written when
   `grep -L driverSid ~/.claude/jobs/tasks/*/task.json` prints nothing and the
   agent-smith and propflowai deploys that read `driverSid` are live. The fallbacks
   are deleted one release after that. Written as a command for the same reason §5.4
   was: a retirement condition nobody can execute is a fact somebody has to remember.
4. **Binaries move by FORWARDER, not by rename.** `start-driver`, `driver-here` and
   `driver-workflow` are the files; the old names `exec` them and log one
   `DEPRECATED-NAME` line per call. A forwarder is deleted when its log has been empty
   for 14 days after the last known caller merged. The second copy of `operator-here`
   in `~/.claude/scripts/` collapses into a forwarder in the same PR.
5. **The `op-` tmux prefix and the `op-task-` workflow-id prefix stay.** Neither can
   carry two names at once, and both are namespaces a running process keys on. They
   are documented as the Supervisor's namespace, not as an abbreviation of a role.
6. **§5's retirement condition is MET.** On 2026-09-13 the query returned
   `OperatorTaskWorkflow = 0`, `DriverTaskWorkflow = 0`, `DriverWorkflow = 1` running.
   The alias registrations in `worker.py` and the `WorkflowType IN (…)` queries retire
   now, as §5.4 licensed.
7. **Open, and blocking step (e):** propflowai PR #7545 ruled `fleet-operator-sense`
   → "fleet engineer / fleet agent", and its protect-list gates the fleet-sense
   identifiers on that ruling. Whether "Driver" is the specific role beneath the
   generic "fleet agent", or #7545 is superseded for the harness role, is Gera's to
   say. Until it is said, `operatorSid` in propflowai keeps its name and its baseline.

### Inventory of the surface that states the old shape (ADR-0004 §2)

| Surface | State |
|---|---|
| ADR-0021 decision 4 — the "on purpose" list | Amended: the list is now the *shim* list, each with its retirement count. |
| ADR-0021 Consequences, last bullet ("after the Sunday deadline") | Superseded by decision 3 above — a count, not a date. |
| `CLAUDE.md` (agentflow) vocabulary banner | Edit with 3a: names the dual-key window. |
| `SYSTEMS.md` — `operator-heartbeat`, `start-operator` mentions | Edit with 2e / 2a respectively, in the same PR as the file. |
| `canon/operator-SKILL.md` §top ("what still says operator on purpose") | Rewritten as part of the 2c inversion. |
| propflowai `protect-list.json` fleet-sense group | Untouched until decision 7 is answered. |

What was run, so the page is not a reading

Method note. A grep proves absence only of the spelling searched, so every repo was searched for operator, Operator, OPERATOR, \bop- and opSid. Two things the classifier would have got wrong and were checked by hand: the supervisor's "no operator and mint cap reached" line is parsed by fleet-grader._supervisor_says — but on mint|respawn|cap, not the word, so it is safe to reword; and ~/.claude/hooks/tests/test_context_budget.py mentions "only Operators loop" only in a docstring and a failure message, never in an assertion, so part 1 breaks no test.

PropFlow Docs