0017 — The Operator is fed; it does not poll
- Status: Accepted (the rule). The rung is specified in gera-propflow/local-bin#113 and built in gera-propflow/local-bin#114, both open, not merged. This ADR does not wait on those merges, because ADR-0004 puts the record at decision time. ⚠️ VOCABULARY: the role this ADR calls the Operator is the Driver since 2026-09-10 —
operatoris the product's word for a PM company. Decision text below is unchanged; ids, binaries, trigger phrases and theOperatorTaskWorkflowtype that still sayoperatorare live invocations and deliberately lag. See ADR-0021. (Appended toStatuson purpose: this adds NO line, so every line-number citation into this file stays true.) - Date: 2026-09-10
- Deciders: Gera, 2026-09-10: "maybe you should be just in charge… we have something that pulls and then surfaces things that need actions… you're just kind of the delegator, but we have to have something feeding you." And, folding the decision queue into the same rung: "those decisions should really be raised to the operator that's polling. The one that's polling should be the one that sees them. And then they forward it to Astra."
The decision
Watching is a scheduled process's job. Judging is the Operator's. They are different rungs and they integrate by a FILE, not by a call.
- A scheduled deriver reads the fleet's surfaces and writes one digest. PR state, required contexts with their durations, real review verdicts,
main's CI, and the decision queue. The Operator reads that file at the wakes it already has and does not re-derive any of it. - The puller never acts. It does not merge, resolve a decision, approve, label, drop
hold-for-review, toggle draft→ready, re-trigger a review, write to production, or writetask.json/STATE.md/ any registry. One writer per artifact (ADR-0003); the only artifacts it writes are its own. A puller that acts is a second Operator, and two Operators on one board is the failure that let a PR merge out from under the first one on the night this was written. - It is not a wake source and not a nudge watcher. It writes to disk and exits. The operator skill caps judgement at roughly 30 firings per task rather than 400, on purpose; a puller that messaged on every delta would rebuild that cost one layer up. And a second looper beside the Operator's watcher is ordered killed on sight — correctly — so this must be distinguishable from one: launchd, no loop, no GATE, never in the
nudgeroster. - Deltas, not state, and the first digest is a declared BASELINE. Restating everything makes the Operator re-read what it already knows, which is the entire cost this removes. A first run has nothing to diff against, and a baseline silently rendered as "no changes" is a green that means nothing.
- Every source carries exactly one of
changed/unchanged/UNREAD(<why>), and UNREAD is never folded into unchanged. This is ADR-0009 applied per source rather than per system: not proving a thing must never look like proving it. All four classes print every time, empties printed as empty — an absent section and an empty section are different claims, and only one of them means nobody looked. - The puller MAY route a decision to the ladder. It may NOT resolve one. An open block with no ruling is handed to
fable-decide. ARESOLVEDreceipt is reported as apply it, addressed to the Operator. Reading a ruling and executing it are different acts, and the second belongs to the rung that holds the acceptance criterion. - A rung that was REACHED and DECLINED is never re-asked — by the puller either. ADR-0016 forbids laundering a refusal into a ruling inside the ladder; this extends the same rule to the thing that feeds the ladder, because a scheduled job that re-asks a declined question every five minutes would launder it by attrition.
NEEDS HUMANis terminal in the puller's ledger, forever. OnlyERROR/WALLED— could-not-be-reached — are re-askable, and only after the receipt'sretry_after. - Provenance travels with the answer. The digest names the rung that answered, the model, and the receipt id. A receipt carrying neither field renders
provenance UNKNOWN, never "Fable".
Why
The Operator polled by hand. Every wake, it swept PR states, check conclusions, review verdicts and the decision queue one at a time, mostly to learn that nothing had changed. On 2026-09-10 that shape cost three measurable things in one evening: a PR merged out from under the Operator, a regenerating alarm it was losing to one withdrawal at a time, and twice Gera noticed a problem before the Operator did. The last one is the tell. A coordinator that a human has to correct about the state of its own board is not coordinating.
The obvious fix — a live agent that watches — is the wrong shape and would be more expensive than the disease. Watching is mostly the production of "nothing changed", and a full-price model context producing that sentence on a loop is exactly the per-session polling the fleet already retired once when it moved the nudge loop off every worker and onto one Operator per task. The fleet already has the right shape working twice: derive-live-work and decision-ladder-canary both read on a clock, call no model, and write a file somebody else reads.
Why the decision queue belongs to the same rung
It was two separate hand-jobs, and they failed the same way. On the night this was written, two questions sat on the founder's decisions page over rulings Astra had already made, at high effort, with the rulings sitting in the block's own recommendation field. Nothing was broken; nobody had looked. The thing that reads the board every five minutes is the thing that should have noticed, and it is the only actor in the loop whose noticing is free.
So the flow is: the puller sees an open decision → the puller routes it through the ladder → only what the ladder genuinely cannot settle reaches the human. That closes the waste in both directions: a question nobody asked gets asked, and a question already answered stops being presented as though it were waiting on a person.
Why "may route" and "may not resolve" is a real line and not ceremony
They look like one act and they are two. Routing produces evidence — a receipt, with a model's name on it. Resolving spends that evidence against a goal, and it needs the acceptance criterion, which the puller does not hold and cannot hold: it is a stateless process with no memory of what the task is for. Doctrine already says a model-RESOLVED question is APPLIED by the operator and cannot even be re-raised; a watcher that self-applied would be a third path to the same act, with the least context of the three.
Why the traps had to be encoded rather than trusted
A digest that repeats a lying instrument is worse than no digest, because it launders a bad reading into something the Operator was explicitly told not to re-derive. Every one of these was measured on 2026-09-10:
- a
reviewcheck-run may be a 0-second copy of an older verdict, in both the green and the red direction — duration is the discriminator; - a
successconclusion onreviewis not 🟢; only a formalAPPROVEDat the head SHA with a 🟢 body is; - merging a docs-heavy
maininto a PR makes path filters classify the merge commit's files and stamp every required context green in 0s. This was verified live onmainitself while the puller was being written:propflowai@038e3fdcarriedBuild,Type Check,Unit TestsandLintallsuccesswithstarted_at == completed_atandoutput.title = "docs-only change — skipped". A rollup reads that as an all-green main; - a cancelled run reports
failureon contexts that never failed on merit; skippedposted aftersuccesson one context is BLOCKED while the rollup says SUCCESS;- the branch-protection sub-endpoint fabricates defaults when the rule is absent — so an unreadable protection object must yield UNKNOWN, never
[].all([])isTrue, and a vacuous green over no checks would call every PR ready.
The rule that generalises past this list: any required context whose duration is zero is UNKNOWN, in either direction. Not green, not red. A stamp is not a run.
Consequences
- The Operator's wake becomes read one file and decide, not derive the board and then decide. Its own instrument list stays — the digest never authorises, and a merge it directs, a production write, or a terminal
task.jsonstate is still verified at the outcome layer. - Something now calls a paid model without a human in the loop, on a clock. It is capped (2 asks per tick), ledgered by block id plus a hash of the question so an edited question is a new one, off by default in the tool and armed only in the scheduled path — and it can only ever ask a question a session already thought was worth raising to Gera.
- The catalog gains a row that can go stale (
puller, ADR-0009): the signal is the literalpuller OKline, which stops being written the moment a source cannot be read.