Understanding Run Exits All Tiers
What each engage run status means, where your work actually landed, and how to read a run summary.
This page distills the engage run-exit contract into a working mental model. It explains outcomes you'll see, not how to configure anything.
The core idea: every task ends in exactly one state
When engage dispatches a task, it doesn't just succeed or fail. There are four mutually-exclusive terminal statuses, and a task occupies exactly one of them — never two, never none:
| Status | What it means | Where your work is |
|---|---|---|
done |
Acceptance criteria verified, code committed and landed, hooks green, tree clean | On the run branch, headed for the PR |
failed |
The task's own work didn't succeed — no meaningful output, the review loop was exhausted, the runner returned false, or it timed out | Preserved on a side-ref (engage/<task>-failed), never discarded |
blocked |
The driver deliberately stopped for an external reason (a question, declared background work) with its work intact, or it sits downstream of a dependency that never resolved | Preserved on a side-ref (engage/<task>-blocked); resumable once the blocker clears |
hook_failed |
Code landed, but a completion hook, freshness gate, or test suite the task was responsible for came back red | Landed on the branch, but flagged as a broken artifact — needs a follow-up fix |
The one thing to internalize: blocked is not a kind of failed. A blocked task did its job responsibly — it stopped because it needed you, not because it broke. A failed task is different: its own work didn't land. Reading a run summary that conflates the two hides the difference between "needs your attention" and "needs debugging."
Why "clean tree" matters
Every task starts against a verified-clean working tree and must end against one too. "Clean" here tolerates the lifecycle bookkeeping engage is expected to touch (state.md, config, telemetry) but nothing else — no loose files, no staged-but-uncommitted index left behind. This is what makes a run summary trustworthy: if a task's boundary isn't clean, the run halts rather than quietly carrying dirt into the next task. A dirty exit is itself treated as a reportable run failure, not something to paper over.
The practical payoff: a stray file from task A can never get silently swept into task B's commit, and a crashed run never leaves a half-staged index for you to discover days later.
Reading a run's exit path
Different ways a task can end have different residue-handling rules. This is the shape you'll actually see when a run finishes:
| What happened | On-disk status | What happens to the work |
|---|---|---|
| Task succeeds | done | Scoped commit(s) land on the run branch |
| Own-work failure (no output, review exhausted, runner false) | failed | Always preserved to a side-ref — never discarded |
| Timeout | failed (reason: timeout) | Side-ref, resumable |
| Driver asked a question or declared background work | blocked | Side-ref with intact work; you answer, then resume |
| A dependency never resolved | blocked (reason: dependency_blocked) | Never dispatched; distinguishable from an independent failure |
| A completion hook or freshness suite failed | hook_failed | Code stays landed, but the broken artifact is named |
Nothing in this list is ever silently skipped. If a commit couldn't land, if a path had to be pruned, if a task got demoted after landing — the summary names it. The governing rule: the run result is the source of truth for what happened; logs are corroboration, never the only signal.
A summary that adds up
Because the four statuses are a true partition of every dispatched task, done + failed + hook_failed + blocked + skipped should always equal the total task count. If a summary line doesn't add up, that's a bug worth reporting, not something to mentally reconcile yourself — the whole point of the run-exit contract is that this arithmetic is guaranteed, not aspirational.