Understanding Run Exits All Tiers

What each engage run status means, where your work actually landed, and how to read a run summary.

Field Guide

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 succeedsdoneScoped commit(s) land on the run branch
Own-work failure (no output, review exhausted, runner false)failedAlways preserved to a side-ref — never discarded
Timeoutfailed (reason: timeout)Side-ref, resumable
Driver asked a question or declared background workblockedSide-ref with intact work; you answer, then resume
A dependency never resolvedblocked (reason: dependency_blocked)Never dispatched; distinguishable from an independent failure
A completion hook or freshness suite failedhook_failedCode 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.