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?"
name:, launchd labelsHow 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.
| Where | What still says operator | Part | Note |
|---|---|---|---|
local-bin canon/rearm-nudge-after-compact.sh | say() 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) | 1 | The 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-doctor | docstrings, usage text and comments: "this session's Operator", "The Operator layer judged…", "only Operators loop" | 1 | Usage/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-keeplist | — | done | Already say DRIVER on the terminal (grader row "DRIVER", keep-list prose). Cited as the pattern. |
dotclaude hooks/goal-poke.sh | 20 prose lines + 2 log lines "POKE … -> operator …"; dir jobs/operator/ is (K) | 1b | Log 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/slack | comments and skill prose | 1b | Skill 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.ts | log 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" | 1c | Safe: 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.ts | rendered text "subagent of the Operator" (asserted by health.test.ts:516,609) | 1c | Display text; asserted in tests, so the test moves with it. |
agentflow docs/the-four-roles.md, CLAUDE.md, SYSTEMS.md, 18 ADRs | vocabulary — 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.py | docstrings, the phases-card copy, the reply prompt's description of the role | 1d | Prompt 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 pages | historic pages describing the fleet role | — | Add the same "renamed 2026-09-10" banner the ADRs carry; do not rewrite quotes. Verbatim quotes stay (ADR-0021 §4). |
| Where | Hits | Why it stays |
|---|---|---|
propflowai src/ (4,249), docs/ (2,957), scripts/ (940), agents/ (209), evals/, lambda/, … | 8,668 | The 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/*.vtt | 852 | Product 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). |
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 / identifier | Written by | Read by | Verified | Shim | Retire 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 --role → operator 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.json | supervisor redmain.ts (single writer) | agent-smith red_main_owner.py:307 · fleet-grader:167 · SYSTEMS.md prose | not run — no live incident | Same 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', operatorFirst | relay index.ts:333, roster.ts:221-242 | propflowai (Vercel, separate deploy cadence): src/lib/domain/agentflow/session-rows.ts, types.ts RelayTask, intake-helpers.ts, FleetHealthTab.tsx:380, api/agents/message/route.ts | source (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-operator | supervisor 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.py | run live: op-sess-propflow-ef, op-red-main-6dd6dff06 | No 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:366 | source | Kickoff says driver-<task>; smith strips either prefix. | When no live roster row starts with operator-. |
operator sid: <uuid> — the stdout line start-operator prints | start-operator | agent-smith thread_operator.py:186 parses it | source | Print 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.sh | — | supervisor 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 signals | callers 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 forwarder | — | the skill loader (keys identity on name:) · supervisor spawn.ts:214 mints Skill tool, name "operator" · loop-doctor PAIRS · goal-poke.sh | measured 2026-09-11 (ADR-0021 amendment): two files sharing one name: collapse to one entry | Yes — 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 URL | run temporal workflow count … Running: OperatorTaskWorkflow = 0, DriverTaskWorkflow = 0, DriverWorkflow = 1 | Retirement 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 --role → operator; task-event --actor operator; relay taskRole: 'operator'; escalation kind operator-respawn-cap; jobs/operator/by-session and jobs/operator/poked dirs | keeplist, task-event (JSONL rows on disk), relay, supervisor escalate.ts:222, drive/coverage.ts | rearm-nudge-after-compact.sh case · every task's events.jsonl history · propflowai · escalation ledger rows · goal-poke.sh | run (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.log | the plist | SYSTEMS.md signals (launchd label + log-match-age path) — a renamed script with a stale signal goes green forever | SYSTEMS.md entry read | Rename 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-sense | protect-list read | See the open question below — the ruling on record for these is not "driver". | After Gera answers. |
| Part | Repo(s) | What changes | What must NOT change | Size | Status |
|---|---|---|---|---|---|
| 1 — wording | local-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 lines | PR opened — local-bin#186 (not merged: merging local-bin is an instant fleet-wide deploy; wording only, safe to merge) |
| 1b / 1c / 1d — the same recipe | dotclaude · agentflow · agent-smith | 1b: 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 / ~200 | planned |
| 2 — names with aliases | local-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.py → driver-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 PRs | planned |
3 — operatorSid → driverSid, dual-key | agentflow first, then local-bin + dotclaude, then agent-smith, then propflowai | Order 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 PRs | planned; 3e blocked on the decision |
| 4 — retire | all | Drop 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 PRs | gated on the two counts above, not on a date |
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. |
loop-keeplist --role with CLAUDE_CODE_SESSION_ID set to the live driver sid → operator; to a bogus sid → undetermined. The instrument is not uniform.fleet-grader --json → the DRIVER row names sess-propflow-ef(9fa34932 3m), read from operatorSid.roster → the task joins on workerSid first (roster:169), so the driver 9fa34932 renders as UNREGISTERED while its worker 1fdb267c carries the task. A display defect worth its own small fix; not a rename question.temporal workflow count ×3 with SMITH_TEMPORAL_* from propflowai/.env.local → 0 / 0 / 1 (above).tmux ls → op-sess-propflow-ef, op-red-main-6dd6dff06 live under the op- namespace.cmp ~/.claude/scripts/operator-here ~/.local/bin/operator-here → identical.tests/keeplist-says-driver-not-operator.py case 6 is red on local-bin origin/main today — it asserts the substring "do not assume driver" and the live line now reads "do not assume architect coordinator, driver, watched session, worker". A one-line test fix, not a rename question; noted in local-bin#186.phase_table.py (runs inside Temporal). Marked "source" above, never "confirmed".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.