0022 — A decision carries three facts, and application binds to the row
- Status: Accepted (the model). The mechanism is in agent-smith#450 and gera-propflow/local-bin#118. Written at decision time per ADR-0004, not on merge.
- Date: 2026-09-10 ⚠️ VOCABULARY: the role this ADR calls the Operator is the Driver since 2026-09-10 — see ADR-0021. Field names, sids and
task.json'soperatorSidare live invocations and deliberately lag. (Appended toDateon purpose: this adds NO line.) - Deciders: Gera — "a decision link is an acceptable output because it is
trackable" — and a third reviewer (Astra,
gpt-6-astra, high effort) asked to settle it against Fable's objection that a link nobody's hand can reach is worthless. Astra's ruling, and the reason this ADR exists rather than a choice between them: "A decision link is the right human-facing output. Silent action is unnecessary; automatic continuation is necessary." - Scope. This is the state model and the ownership rule for what happens after a ruling exists. It is downstream of ADR-0019 (how a block is decided) and of ADR-0020 (the ruling reaching the raiser), and it restates neither. ADR-0020 ends at delivered; every word here is about what a row can then say about itself.
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:
- withdraw the row — which erases a ruling Gera gave; or
blocked raiseagain — which asks a settled question twice, and puts a decision Gera already made back on his plate as though he had not made it.
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
- Historical rows carry no
taskstamp and are not retro-fitted. They keep the sid join, which is the behaviour they have today, so the 176-row backlog becomes countable and triageable immediately but not automatically inherited — only rows raised from here on are. Back-filling the stamp is a separate, cheap change nobody has asked for yet. - Nothing decides whether a stale answer is still right. Driving or inheriting a row delivers the ruling; it does not vouch for it. ADR-0013 said this and it is still true.
- No revert detector. The revision is recorded, not checked against git history — a checker would be the daemon this ADR refuses. What exists is the ability for a person or lane that looks to record what it found.
Consequences
blockedgrows two verbs (apply,verified) andexecutedgrows--revision.blocked apply <id> appliedroutes intoexecutedso there is one writer of the terminal record and no second idiom for a solved problem (Constitution §2).- The
decision-blockscatalog entry'sjobstring and its "what this system does NOT do" paragraph both described an OPEN-only sweep. That has been false since ADR-0013 shipped, and is the same defect class as a health signal reporting green on a frozen job. Corrected in this PR, with the backlog counts added as the entry'sdetail. DecisionBlockRequestgainswake_sid, additive with a default, so runs recorded before it deserialize with""and take theraiser_sidbranch forever. No command is added, removed or reordered, so noworkflow.patched()gate is owed.
Related
- ADR-0013 — answered is not executed, and the sweep chases both. This ADR is the separation that one left open by name.
- ADR-0020 — the ruling reaches the raiser. That ADR ends at delivered; this one starts there, and answers what happens when the raiser it was delivered to is no longer the session that owns the work.
- ADR-0003 — rungs integrate by signals, never calls.
- ADR-0004 — a decision that changes how systems relate is written at decision time.
- ADR-0009 — a signal must fail when the system fails. Why the backlog count is omitted rather than zeroed when nobody could take it.