0022 — A decision carries three facts, and application binds to the row

The failure

blocked outstanding lists decided work with no execution record. On 2026-09-10 that was 176 rows, one of them approved at noon and executed by nobody. This is the second sighting of the failure the Operator role was created for and the third of the same class:

when shape
2026-08-10 40 answers landed; an hour later 28 read unresolved, because nothing was polling.
2026-09-07 6 answers sat 1–6 days; something polled, and the click switched the poll off (ADR-0013).
2026-09-10 176 answers carry no application record, and the section that says "provably not executed" was empty.

ADR-0013 fixed the second and is not wrong. This ADR is the two things it left open — it named them itself: "Full separation (open → answered → executed → closed)… the cleanest end state, and the right eventual shape… Left open deliberately; this ADR does not foreclose it."

Why one status could not hold it — the case that proves three

"Decided; application blocked, because a human identity is required."

That state is real and live. Some actions cannot be performed by any agent by construction: a Slack kickoff to Agent Smith, where both agent routes are structurally dead — the socket drops every bot post at ingest, and the sanctioned CLI injects an empty message id so the gate never runs. It is a human-only action, and it is emphatically not an unanswered question.

Today an agent meeting that wall has two ways to say so, and both lie:

A capability failure must be reportable against an existing ruling, without erasing it and without re-asking. A model that cannot express that sentence is the wrong model, and one status is that model.

The decision

1. Three facts, tracked separately

fact where it lives states
Decision state + resolution pending / decided / superseded
Application execution.state queued / executing / blocked / applied
Verification verification.state pending / passed / failed

Derived in one function per repo (decision_block.decision_facts, blocked's application_of / verification_of), mirrored by copy and not by import — the rule TASK_TERMINAL already keeps, because blocked imports nothing of Smith's and Smith shells nothing of blocked's.

Bucket on a word you recognise, never on the absence of one. This is relay/src/blocks/blocks.ts's rule, learned when state !== 'resolved' was read as "open" and adopted tombstones rendered as questions waiting on Gera forever. An application word nobody wrote a meaning for reads unknown: never applied (which would silently clear the backlog) and never drivable (which would nag a lane about a word nobody defined).

done stays a terminal application spelling forever. Every record written before this carries it, and re-spelling history is how a backlog appears to clear itself with a sed.

2. blocked is an application state, and the sweep stands off from it — visibly

blocked apply <id> blocked --needs <capability> --why "<one line>" writes only execution. It never touches state, resolution or answer: the ruling stands, nothing is re-asked. --needs is required and has no default, because "5 decisions need a human identity" is a sentence somebody can act on and "5 blocked" is not.

The sweep does not drive such a row — no wake fixes a capability nothing in the fleet has, and the raiser is the party that discovered it — and it does not hide it either: it is reported by name in the skip list and counted in the manifest under its capability. Not driven and not tracked are different things, and the difference is the whole of decision 4.

3. Application binds to the ROW, never to the session that asked

ADR-0013's narrowing joined the task ledger on the raiser's own sid, and a sid dies with its pane. An Operator that asks a question, is answered, and is then respawned leaves a task.json naming its successor — so every reader keyed on the raiser reads "that lane is gone" about a lane sitting right there, holding the answer's work.

Measured 2026-09-10, and this is why the section was empty rather than short: of the four newest stranded rows, zero joined the task ledger by raiser sid.

So a row is stamped with its task at raise time, and readers resolve row → task → current holder. The wake follows the lane; the ledger identity (which directory the row's file lives in) stays the raiser. Two identities, because they genuinely part. An unstamped row keeps the old sid join — a fallback, so the change cannot widen what gets driven.

This consumes an existing primitive and mints no new one. Reading who holds a lane is not owning it: nothing here takes a lock, mints a generation or arbitrates. Mutual exclusion between two lanes applying one decision belongs to the fenced row lease keyed on board+row (agent_smith/row_lease.py, in flight on row-lease-fencing), and the seam to consume when we need it is lease_workflow_id("decision-blocks", <block id>). ADR-0003 and Constitution §1 both forbid growing a second one here.

4. The backlog is a number, not a worklist somebody must remember to run

A 176-line list a person has to type a command to see is not a detection mechanism. The sweep already walks this ledger every 600s and already writes decision-drivers.json on every completed path, so it now counts what it just walked: total, by application state, the capabilities decided work is stuck behind, and oldest_age_s — the field that makes a growing backlog visible, since a total can look stable while every row in it ages.

Counted without the liveness narrowing, deliberately. The drive list is narrowed because waking a lane nobody is coming back to does nothing; the count must not be, because a row whose lane is gone is the most outstanding row there is. Narrow the count the same way and it reads zero exactly when things are worst.

Omitted rather than zeroed when a sweep could not count. A zero nobody measured is the frozen-job-reports-green defect in one field.

No daemon and no poller: this extends a path that already runs, and it surfaces through the catalog's signals (ADR-0003 — rungs integrate by signals, never calls).

5. Verification names the revision it was taken against

A historically merged PR stays merged forever while the behaviour it added has since been reverted. "177/177 done" needs evidence still applicable to the integrated revision, so blocked verified <id> --revision <repo@sha|url> --evidence "<what you checked>" requires the revision with no default, for the same reason --evidence has none: a value nobody had to supply is a value nobody checked. An applied record with no revision renders as "verified against no named revision" — a weaker claim, never a passing one.

And it has teeth: a failed verification re-opens an applied row. Never automatically — only a deliberate --failed write does it — so nothing can re-drive work on its own, and a person who goes and finds the revert cannot have that finding swallowed by the applied record beside it.

Why not the alternatives

Leave it at one status and just try harder to execute. This is the option that has failed three times. It also cannot express decision 2's sentence at all, so the best case is that capability walls keep being recorded as withdrawals or as duplicate questions.

Make the agent act silently instead of producing a link. Rejected, and this is precisely what Astra was asked to settle: the founder is right that a link is the correct human-facing output because it is trackable. Silent action is unnecessary; automatic continuation is necessary. Nothing here removes the link; it makes the continuation after the link mechanical.

A new ownership primitive for who applies a decision. Rejected — decision 3. A lease already exists and a second one would race it.

A watcher daemon over the backlog. Rejected: it is a poller, the sweep already walks this exact ledger on a clock, and ADR-0003 says rungs integrate by signals.

What this does NOT fix, stated so nobody assumes it

Consequences