0023 — When a harness word collides with a product word the harness yields, and a dock row is an ACTION ITEM

The decision

  1. The rule, stated first, because it outlives these three words. WHEN A HARNESS WORD COLLIDES WITH A PRODUCT-DOMAIN WORD, THE HARNESS YIELDS. The product's vocabulary is customer-facing, is spoken to people who do not work here, and is not ours to move; the harness's is ours. This is ADR-0021's reasoning promoted from a one-off argument about operator to the general rule, so the next collision is a lookup rather than a debate.

  2. A unit of work on the phase dock is an ACTION ITEM, short form item. Not a "row". row already means a database row and a property row — the design record is built out of attachment rows, relationship rows, CRED# rows and MAPLOG# rows, and the dock's own phase labels — "P1 · the org row", "P3 · rows, dark" — are product rows sitting on a page whose units of work were also called rows. Two senses, one page.

  3. A rendered table row is still a row, and so is a database row. This rename is the dock's unit of work, nothing else. A catalog row on the Systems tab, a roster row, a ledger row, a phase-table row and a PROP# row keep the word — it is the right word for them and they collide with nothing. The grep for row in this repo is therefore not a to-do list; §What was measured says what the count is made of.

  4. The harness does not say "lease" for a work claim — it says CLAIM. But SmithRowLeaseWorkflow is a different mechanism and KEEPS the word, for now. This is the half of the brief that did not survive contact with the code, and it is stated in full because getting it wrong would merge two mechanisms into one name:

    • work_claims.SmithWorkClaimWorkflow is the claim primitive — a key, a holder, an expiry, minted through work_claims.row_key. The code always said claim. Prose that described this as "a lease on a row" was a coordinator's word layered on top in briefs; it stops, and nothing has to migrate because the code was never wrong.
    • row_lease.SmithRowLeaseWorkflow (agent-smith #449, on main) is not that workflow and not a synonym for it. Its own docstring draws the line this ADR must not erase: SmithWorkClaimWorkflow closes when its lease expires — that is how the key becomes claimable again — so a monotonic fencing generation cannot live in it, and a long-lived authority that outlives the claim holds the generation instead. Calling that "the row claim" would give two workflows, with opposite lifetimes, one name. So "lease" survives there as a named lag, not as an oversight, and its closing condition is decision 7.
  5. "Task" was considered and REJECTED, and the reason is the most useful line in this record, because task is the obvious word and it is already spent. ~/.claude/jobs/tasks/<task>/task.json, op-task-sess-*, agent_smith/task_cli.py and fifteen sibling task* modules, /api/intake's task ledger, and the type DriverTaskWorkflow that ADR-0021 §5 names as OperatorTaskWorkflow's successor all already mean one Driver's task — and Temporal has its own workflow task primitive underneath all of it. Making dock units "tasks" would recreate, on a second word, exactly the one-word-two-things collision ADR-0021 spent a night untangling. "Action item" collides with nothing in either domain.

  6. These still say row or lease, ON PURPOSE, and are not bugs. Every one is a string something starts, greps, parses or keys on — ADR-0021 decision 3's test, applied again:

    • The dock's data keysrows in the #tracker-data block, and prs / status / context / prod_evidence beside it. relay/src/digest/phases.ts fails a build with "phases.json has no rows array"; bin/derive-live-work emits {"rows": …}; bin/refresh-tracker rewrites that block every 300 s. A key is an interface.
    • The dock's CSS and DOMtr.item-row, .rows, --rowhover. tests/browser/tracker-test.mjs counts tr.item-row literally. (The class already says item, which is a small piece of luck: the page's own markup was ahead of its prose.)
    • agent_smith.work_claims.row_key(board_id, row_id), row_lease.LEASE_WF_PREFIX = "row-lease-", lease_workflow_id(...), SmithRowLeaseWorkflow, row_lease_cli, activities/row_lease.py. Workflow ids derived from these are running; ADR-0021 decision 5's additive-cutover rule governs any move, and no move is proposed here.
    • The Claude-account pool's leases in ~/.local/binLEASE_TABLE, lease_holder, lease_machine, lease_claim. A third sense of the word (a lease on an account seat), persisted in a DynamoDB table whose name is the string. Out of scope in both directions: it is not the work claim, and renaming it would collide it with the claim that is.
    • Verbatim quotations, everywhere, including the decision records embedded in the dock's own data (q / why / pick). Renaming a word inside a quotation is falsification, not a rename — ADR-0021 decision 4's last bullet, restated because a lane caught itself doing exactly this during that rename.
    • Historical ADRs and their filenames. ADR-0022 is "…application binds to the row" and its rows are decision-ledger rows; ADR-0007's are phase-table rows; ADR-0001's and ADR-0008's "no board row" is the dock sense and is left. Bodies are not rewritten — that is PR #141's precedent, which appended a one-line Status banner and changed nothing else, so every line-number citation into those files stayed true.
  7. The conditions for closing each gap, because a lag with no condition is just a mess with a note on it:

    • The dock's data keys move when the dock's publish path is next cut over deliberately, with phases.ts, derive-live-work, refresh-tracker and the browser controls in the same change. Not before, and never one surface at a time.
    • row_lease and its workflow ids move only when somebody gives that mechanism its own non-colliding noun — it cannot be "claim", which is taken by the workflow beside it, and it cannot stay "lease" — and then by ADR-0021 §5's additive cutover, never in place, with the old type registered until a live query returns zero running executions.
    • The account-pool lease is not scheduled to move at all. It is recorded so the next reader does not "finish" a rename nobody started.
  8. The canonical list lives in ONE place, and it is not this ADR. docs/the-four-roles.md § The vocabulary — the rule, the words, and where the old ones went carries the table every session reads; this ADR carries the decision and the reasoning. Two documents defining one vocabulary is the defect ADR-0020 ruled against, so the glossary links to the product's own definition (design-final.md#operator-vocabulary) for every product word rather than restating it — a copy of a definition is a copy that will drift.

Why

The two collisions are real, and one of them is on a single page

row. The phase dock renders 177 units of work, and the same page's phase labels are "the org row" and "rows, dark" — DynamoDB rows in the portfolio architecture. A sentence like "the org row is blocked" is genuinely ambiguous on that page today: it is either a unit of work or a storage row, and the reader cannot tell. The product owns row twice over (a database row, a property row), so under decision 1 the harness's use is the one that moves.

lease. A tenant's lease agreement is a core product entity with its own renewals saga, its own workflows and its own page. It is the least available word in the building. And, unlike row, the harness had a correct word for the thing already — SmithWorkClaimWorkflow — so this half is mostly stopping a wrong word from spreading rather than migrating a right one.

The lease half was measured, and half of the premise was false

The brief this ADR was written from says "'lease' was never the code's word". Measured on agent-smith@main: 365 occurrences of lease across 54 files, including src/agent_smith/row_lease.py (66), tests/test_row_lease.py (21), src/agent_smith/activities/row_lease.py (11), row_lease_cli.py (10), LEASE_WF_PREFIX, LeaseGrant, LeaseInput, lease_workflow_id — merged as #449 and cited by ADR-0022 as the seam decision blocks would consume.

That is not a stray word to sweep. row_lease.py's docstring argues for the distinction in its own text: the claim workflow closes on expiry so the id becomes claimable again, which is exactly why a fencing generation cannot live inside it and needs an authority that outlives the claim. Renaming its prose to "claim" would have produced two workflows named claim with opposite lifetimes — and the rename would have looked like it was following the founder's instruction while doing it. This is the class of defect the brief asked to be reported: a rename that ends with two things sharing one name. So the instruction is executed where it is true (briefs and prose about the work claim) and recorded as a lag where it is not.

"Item" is not perfectly free either, and the short form is where it bites

item is a live query parameter in the product?item= deep-links the Clara playground, the traffic viewer and the review surface, and URLSearchParams.get('item') is read in several components. It is also a MAPLOG field name. None of that is on the dock, and none of it is prose, so the collision is survivable — but it is the reason "action item" is the term and "item" is only the short form: in any sentence where the reader could be looking at a product surface, write it in full. A rename whose short form is another system's URL key should say so out loud rather than discover it later.

What was measured, before anything was edited

Counts are word-boundary matches on HEAD, not the working tree, and not with \bgit grep does not honour \b and silently matched browser for \brow on the first pass, which would have made this whole measurement a fiction. grep -P throughout.

surface row lease what the count is actually made of
agentflow-relay markdown 312 5 Overwhelmingly not the dock sense: catalog rows on the Systems tab ("this row goes stale"), ledger rows, roster rows, phase-table rows. Dock-sense hits: 6 (four in SYSTEMS.md, and ADR-0001's and ADR-0008's "no board row"). ⚠️ The fourth was found by the reviewer, not by me, and how it hid is worth more than the hit: my sense-classification pass filtered row lines by the words `tracker
agentflow-relay TypeScript + JSON 1,335 0 Identifiers and parsed keys — rows, rowFor, rowFiles, rowId. Untouched, per decision 6.
propflow-docs — the dock artifact 577 4 31 move — 14 in the notes block, 17 in the prose a reader sees. The other ~546 stay: the #tracker-data block (parsed keys, lane-authored context, verbatim q/why/pick records), the CSS and DOM, the code comments that annotate the lagging identifiers, and "the org row" / "rows, dark", which are product rows.
propflow-docs — tracker tooling 398 0 bin/refresh-tracker, derive-live-work, derive-live-lanes, sync-dock-phase00 and their control tests. Identifiers and payload keys. Untouched.
agent-smith 365 row_lease and its tests, plus the product's own lease vocabulary in marketing and renewals prompts. Untouched; decision 4.
~/.local/bin 179 The account-seat lease (LEASE_TABLE, lease_holder). A third sense. Untouched.

The shape is the same one ADR-0021 found and correctly refused to act on: the vocabulary layer is small and the interface layer is enormous. 35 prose sites move — 31 on the dock, 4 in SYSTEMS.md — against roughly 2,500 identifier, key and other-sense sites that do not. The gap between "how many hits are there" and "how many should move" is the entire content of this measurement, and it is the reason the first number was never the plan.

What keys on the literal word

Checked before editing, because the night before, two skill files sharing one frontmatter name: silently deleted /operator from the skill listing:

Why this is written down rather than remembered

Because the identical failure is already in the record twice. ADR-0021 has docs/the-four-roles.md rotting nine minutes after the merge that invalidated it. And on the night this was written, a SYSTEMS.md entry was found describing an OPEN-only sweep that its own repository had contradicted eleven days earlier — nothing failed, nothing went red, and the page kept reading as true. A document that has to be remembered to stay true will become false. So the glossary is one file, it links rather than copies, and the guard below is proposed rather than assumed.

The guard, proposed and deliberately NOT landed here

propflowai/scripts/operator-rename/ is the precedent: counts over HEAD, a protected-group baseline, a self-test that proves the guard can fail. Generalising it bare-word to row and lease is the wrong move and is rejected here:

What is proposed instead, as its own PR: a phrase-level guard over prose files only (*.md plus the dock artifact), counting the retired compounds — dock row, board row, tracker row, phase row, row lease — with an explicit path carve-out for docs/adr/ at or below 0022 and for quoted blocks, so that neither history nor a quotation can be made to fail. It must be reviewable by the question what made this green — a count that fell because a file was deleted or a sentence was gutted is not a pass. Until it exists, decision 8's single home and this ADR's lag list are the mechanism, and that is stated as a weakness rather than papered over.

Consequences