0023 — When a harness word collides with a product word the harness yields, and a dock row is an ACTION ITEM
- Status: Accepted (the rule and the lags). The vocabulary layer ships with this ADR. One half of the brief this ADR was written from is corrected here rather than executed:
leaseis not absent from the code, and the thing the code calls a lease is not the thing the code calls a claim. See decision 4 and The lease half was measured, and half of the premise was false. Nothing inpropflowaichanges. ⚠️ PARTIALLY SUPERSEDED 2026-09-12 by ADR-0025: decision 4's the harness says CLAIM for a work claim is superseded for the DRIVER'S work items — they are assigned, never self-claimed (.claims→.assigned), which is a model change and not a rename: a driver that cannot pick its own work is the fix for a driver that drifted its own goal. Decisions 1, 2, 3, 5, 6, 7 and 8 stand,SmithRowLeaseWorkflowstill keeps its word, and the account-pool lease is still out of scope. (Appended toStatuson purpose: this adds NO line, so every line-number citation into this file stays true.) - Date: 2026-09-11
- Deciders: Gera, 2026-09-11, calling it in conversation the way ADR-0021 was called: a dock "row" becomes an "action item" (short form item), and "lease" becomes "claim". This is the second application of the playbook ADR-0021 established the night before — the harness's word yields to the product's, the prose moves now, and every string a process keys on is listed and left.
The decision
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
operatorto the general rule, so the next collision is a lookup rather than a debate.A unit of work on the phase dock is an ACTION ITEM, short form item. Not a "row".
rowalready means a database row and a property row — the design record is built out of attachment rows, relationship rows,CRED#rows andMAPLOG#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.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 forrowin this repo is therefore not a to-do list; §What was measured says what the count is made of.The harness does not say "lease" for a work claim — it says CLAIM. But
SmithRowLeaseWorkflowis 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.SmithWorkClaimWorkflowis the claim primitive — a key, a holder, an expiry, minted throughwork_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, onmain) is not that workflow and not a synonym for it. Its own docstring draws the line this ADR must not erase:SmithWorkClaimWorkflowcloses 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.
"Task" was considered and REJECTED, and the reason is the most useful line in this record, because
taskis the obvious word and it is already spent.~/.claude/jobs/tasks/<task>/task.json,op-task-sess-*,agent_smith/task_cli.pyand fifteen siblingtask*modules,/api/intake's task ledger, and the typeDriverTaskWorkflowthat ADR-0021 §5 names asOperatorTaskWorkflow'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.These still say
roworlease, 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 keys —
rowsin the#tracker-datablock, andprs/status/context/prod_evidencebeside it.relay/src/digest/phases.tsfails a build with "phases.json has norowsarray";bin/derive-live-workemits{"rows": …};bin/refresh-trackerrewrites that block every 300 s. A key is an interface. - The dock's CSS and DOM —
tr.item-row,.rows,--rowhover.tests/browser/tracker-test.mjscountstr.item-rowliterally. (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/bin—LEASE_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
Statusbanner and changed nothing else, so every line-number citation into those files stayed true.
- The dock's data keys —
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-trackerand the browser controls in the same change. Not before, and never one surface at a time. row_leaseand 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.
- The dock's data keys move when the dock's publish path is next cut over deliberately, with
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 \b — git 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:
SYSTEMS.mdsignalpatternandlabelstrings — grepped forrow,lease,item,claim: zero hits. No catalog signal matches a log line containing these words, so the prose in that file can move without any rung changing colour.relay/src/digest/phases.tsparsesdoc['rows']and emitsphases.json has no "rows" array. Interface. Left.tests/browser/tracker-test.mjscountstr.item-row;tests/sync-dock-phase00-controls.pyasserts check labels containing "the native dock row…". Test labels are not dispatch, but they are assertions on strings, so they move only together with the strings they assert — and neither is in this change.- Skill frontmatter
name:anddescription:— noname:contains either word, so nothing is at risk of the collapse ADR-0021's amendment records. Fourdescription:fields do contain them (canvas-tables,slack-canvas,tmux,pull-link) and all four are the senses this ADR keeps: a markdown table row, a one-line-per-session board row, and the product'slease_…id prefix. Stated rather than reported as "zero hits", because the honest count and the count that supports the change are different numbers here.
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:
- a bare
rowcount would be counting 1,335 legitimate identifiers and every English sentence about a table; - the product owns
lease, so a repo-wide count is counting the customer's vocabulary; - and the recorded failure mode of ratchet pressure is that lanes weaken the thing being counted to get green — a drift test had its path pin cut to a bare basename to lower a count, and the guard silently stopped pinning which script it ran.
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
- Two words are live at once in the code, deliberately.
SmithWorkClaimWorkflowandSmithRowLeaseWorkflowboth exist, mean different things, and both keep their names. Prose about the first says claim; prose about the second says lease and should say fenced row lease in full so the reader knows which one it is. - A grep for
rowproves nothing on its own. After this, a hit is a database row, a rendered table row, a data key, an identifier, or a quotation — the sense has to be read. That is the state decision 3 exists to make legible, and it is the reason the measurement table above is part of the record rather than a scratch note. - The dock's
#tracker-datablock still saysrows, and its per-item prose still says "row". The keys are interfaces and the prose inside them is lane-authored evidence and verbatim decision text, rewritten every 300 s by a job this lane does not own. Moving it is a data migration, not a vocabulary change, and it is listed in decision 7 rather than attempted. ~/.local/binis untouched. It is a live shared checkout onPATHfor every session; a write there is an instant fleet-wide deploy, and nothing in it needed to move for this decision to be true.
Related
- ADR-0021 — the role is the Driver, and the plumbing still says
operator. The direct precedent: the same collision, the same rule, the same vocabulary/interface split, and the amendment that measured what happens when a rename touches something the loader keys on. This ADR is its second application and adds only the general rule (decision 1). - ADR-0022 — application binds to the row. Its rows are decision-ledger rows, not dock items; its citation of the fenced row lease is a citation of the code's name and stays exact.
- ADR-0020 — two records of one decision is the defect, not the fix. Why the glossary links to the product's definition instead of restating it.
- ADR-0004 — a decision that changes how systems relate is written at decision time, by whoever is in the room.
propflowai/docs/planning/portfolio-architecture/how/design-final.md#operator-vocabulary— the product's vocabulary, and the source of the reserved list. Not ours; linked, never copied.