Four Words and a Sentence
Once the engine has pulled a report out of a final, it hands each per-item entry to Engine.report. The first check happens before the store is even touched: the outcome must be one of exactly four words, done, released, abandoned or blocked, and the body must not be empty. Anything else is a ValueError with a message that says what is missing: a per-item outcome and explanation are required.
Four is a deliberate number. Done means the item met its acceptance criteria. Released means the worker hands it back untouched or unfinished so someone else can take it. Abandoned means the item should not be done at all. Blocked means the worker is still on it and cannot move without something from outside. There is no partially done, no mostly done, no done pending review. Those are all either a blocked report with a sentence or a done that should not have been sent yet.
Who Is Allowed to Say It
Inside the transaction, three facts must hold. The session must be the current generation and not exited, because a replaced process is fenced and its words are stale. The binding must belong to this session at this generation and still be active, because a binding that already resolved has nothing left to report. And the item's revision must equal the revision the worker accepted, or the engine answers Accepted work revision changed; reconcile before reporting.
That last check is subtler than it looks. Dad edits todos and replies to them while work runs, and those changes do not count against the worker: an edit or reply moves the active binding's accepted revision forward with it, and the new text reaches the worker as a reply review (lesson four). What the check catches is any other change to the item under the worker's feet. A report about a contract that has since been rewritten by someone else is not a report about this work.
Blocked Is Not an Ending
Three of the outcomes end the binding. One does not, and the code treats it as a different kind of event entirely.
| Outcome | Ledger operation | Binding | Obligation | Dad hears |
|---|---|---|---|---|
done | done_todo (with reply outcomes) or close_request | resolved | resolved, pointing at the report | ✅ done |
released | release_todo or release_request | resolved | resolved | ↩️ released |
abandoned | abandon_todo, or close_request with the body prefixed Abandoned: | resolved | resolved | ⛔ abandoned |
blocked | none | stays active | due now, unparked | ⏸ worker blocked |
A blocked report writes a report row, makes the binding's pending obligation due immediately and clears any park on it, and enqueues an attention line with the body excerpt and the worker's for_dad lines. It returns status pending. The work stays bound to the same worker, which keeps holding the context, and the coordinating chief is woken to look at it now rather than at the next timer. The adapter keeps observing that chat, so the same worker can send a done later, once the blocker clears.
One Transaction for the Ending
For the three terminal outcomes, Engine.report calls the domain operation and then _record_outcome, and both run inside the same engine command, one BEGIN IMMEDIATE transaction with an idempotency key. The domain borrows the engine's connection rather than opening its own. In that one transaction:
- the report is inserted with its body and evidence;
- the binding becomes
resolved; - any pending reply review for that binding is cancelled, since there is no longer active work to amend;
- the obligation is resolved, and its disposition is the new report's id, so the debt points at the receipt that paid it;
- if no active binding remains on the dispatch, the dispatch is resolved and any work envelope nobody has received yet is cancelled;
- an
outcomenotification is enqueued carrying the worker'sfor_dadlines.
If the domain refuses (a todo with an unanswered reply, for example), everything rolls back together. There is no state in which the todo says done and the obligation still says owed, or the reverse.
Per Item, and Evidence That Says Which Kind
An envelope can carry several bindings, and each gets its own entry, its own command and its own key. One rejected entry leaves the others committed, and the dispatch resolves only when its last active binding does. The architecture says it flatly: each selected item gets its own disposition; partial success never closes the whole batch.
The same section is strict about what counts as evidence. It must distinguish commits, tests, installed or served artifacts and actual runtime verification, because a commit or a passing test has no universal meaning of task completion. A pushed commit does not mean the app on Dad's phone changed. A green suite does not mean the service that serves it was restarted. The engine stores evidence as given and does not grade it. The worker's own prompt asks for the same distinction in plain words: say what was done and where it landed, a pushed commit, a TestFlight build, a service.