The epic engine — run a whole epic unattended
Don't want to remember any of the commands below? Invoke skill:e11-epic (catalog code EE, or just tell Master "run epic 2") — it finds your epics file, writes the overlay, shows you the preflight graph, launches the run in the background, gives delta check-ins when you ask "how's the epic going?", translates blocked states into the one next move, and walks you through the promotion gate at the end. Everything it reports comes from files you can open yourself; the commands below always work directly.
e11 epic drives every story of one epic through spec → implementation → review → verification, serially, with machine-verified evidence at every step. It is a deterministic CLI kernel — the LLM runs only inside the workers (e11-spec-flow to specify, e11-dev-auto to deliver). Eight invariants hold in any run: an agent's claim is never sufficient (the controller re-runs verification itself and records the exit codes it observed); the canonical spec is digest-bound and immutable; the sprint board is read-only to the engine; your checked-out branch is never touched; the engine never pushes; promotion is a human gate; verification commands come only from trusted config; no prose-inferred parallelism.
Authoring dependencies
Stories declare what they cannot start before — the epics template carries the line:
### Story 1.2: Login
**Depends on:** 1.1none marks intentional independence.
Cross-epic ids are allowed, and you should use them. Coupling routinely crosses epic boundaries — a migration in epic 6 that gating in epic 3 requires — because epics are a product grouping, not a dependency boundary. Write the edge where the coupling actually is:
### Story 3.2: Freemium gating middleware
**Depends on:** 6.1The engine cannot schedule a story from another epic, so it does not pretend to: it records 6.1 as an external precondition, prints it in plan, and refuses to run until the overlay acknowledges it as delivered. Declaring the edge is always safer than leaving it implicit — an undeclared cross-epic dependency is invisible, and the run will happily deliver the dependent first.
satisfiedExternals: ["6.1"] # 6.1 is already delivered — proceedLegacy epics without any declarations run as a conservative authored-order chain. That chain is conservative within the epic only. It orders undeclared stories against each other and infers nothing about other epics — authored order across epics is not a dependency order, and the engine never treats it as one. If your work spans epics, declare the edges; serial execution alone will not save you.
For legacy documents (or to add verification commands), supply an overlay:
# epic-run.yaml
schemaVersion: 1
epic: "1"
dependencies:
"1.1": []
"1.2": ["1.1"]
satisfiedExternals: [] # cross-epic ids already delivered; `run` refuses while any are pending
setup:
- id: install
argv: ["npm", "ci"]
timeoutMs: 600000
verification:
- id: test
argv: ["npm", "test"]
timeoutMs: 900000
workerVerification: # optional: scoped, FAST commands for the worker's inner loop —
- id: unit # working tools, never evidence. The gate above stays the evidence
argv: ["npm", "test", "--workspace", "pkg-a"] # of record, and the controller re-runs it itself.
timeoutMs: 300000
policy:
failFast: true
maxAttemptsPerStory: 2
workerTimeoutMs: 5400000 # cap per worker invocation (default 90 min)
stallTimeoutMs: 600000 # kill a worker after total silence this long (default 10 min)
runTimeoutMs: 14400000 # whole-run budget (default 4 h)
maxTurns: 160 # cap on one worker's turns (default 160)
baselineCheck: true # run `verification` once before the first worker (default true)
deliverModel: sonnet # first-attempt delivery model (default: sonnet — cheap first)
retryModel: opus # attempts after the first escalate here (default: opus)
effort: medium # delivery effort (default medium; retries run retryEffort, default high)
autocompactTokens: 140000 # context ceiling per worker (default 140k; bounds per-turn cost)
# specModel: opus # spec-flow only: distillation model (default: opus, specEffort high)
# maxCostUsd: 250 # optional: arm a whole-run spend ceiling, USD (unset = never blocks)
# maxCostUsdPerStory: 40 # optional: per-story ceiling; caps each worker via --max-budget-usdsetup: commands run once, controller-observed, in the fresh worktree before any worker spends a token — the place for dependency install. Without it the worktree starts bare, and the first worker burns paid turns discovering that npm test can't run.
Three clocks, and big stories need the first one raised. workerTimeoutMs caps a single worker invocation — one e11-spec-flow run, or one e11-dev-auto delivery. A delivery runs an implementation subagent, review layers, and up to five repair iterations, so a large story (a framework migration across a public API, say) can legitimately exceed the 90-minute default; raise it rather than letting the clock fail work that was progressing. runTimeoutMs is the budget for the whole run: it's checked between stories, and it also caps each worker, so a run cannot overrun it by more than one worker window. stallTimeoutMs is the cheap clock: a worker that has produced no output at all for that long is killed early — a test waiting on input or a dead network call should not get to run out the full 90-minute window. None of these clocks auto-retry: a timed-out or stalled worker already burned real spend, so re-running it is an explicit decision (retry --story), never something the engine does behind your back. One nuance for live runs: the run executes against a plan snapshot, so editing this file mid-run does nothing — raise a running epic's budget with retry --story <id> --worker-timeout-ms <n> (or --stall-timeout-ms), which records the raise on the run itself and applies it from the next resume.
The execution profile is cheap-first, escalate-on-evidence. First attempts deliver at sonnet-tier with medium effort; a retry exists because that tier already failed with a full window, so attempts after the first escalate to retryModel (opus) at retryEffort (high) — capability is paid for exactly where evidence says it is needed, and quality is held by the review panel and controller-observed verification either way. autocompactTokens bounds every worker's context: cost is turns × context re-read, and an unbounded context both grows that re-read without limit and (past 200K tokens) bills at the long-context premium. The execution spec holds the durable state, so compaction sheds conversation history, not work. Flagship invocations also carry --fallback-model sonnet, degrading instead of dying when the model is overloaded. Model names are aliases on purpose — they track the runtime's current tier; pin exact IDs to freeze a run.
Delivery runs in phases, each a fresh context. A story is not one long conversation: the engine invokes the worker up to three times — plan (investigate, write the execution spec; runs at specModel/specEffort, since distillation is where the flagship earns its rate), implement (step-03, at the delivery tier), and review (the panel, triage, repair, and commit) — with the execution spec as the entire hand-off. The spec was always the durable program state; the conversation is interpreter RAM, and one conversation spanning all four steps plateaus at maximum context, paying peak price on every remaining turn. Phases restart the ramp instead. The engine advances on the worker's phase-complete result, cross-checks the claimed spec status against the file on disk (a phase hand-off is a worker claim like any other), records story.phase_completed events with per-phase spend, and resumes mid-story at the recorded phase. A story that finishes in its first invocation simply finishes — small stories collapse to one invocation naturally. policy.phaseSplit: false restores single-invocation delivery, and an older skill install without the phase protocol degrades to it automatically with a warning.
Money is visible by default, enforced only on request. Every run records per-story spend (story.cost in the ledger) and heartbeats carry a running spend estimate (~$… in the progress line and the dashboard), so a $150 story looks different from a $15 story while it runs, not after. Nothing blocks on money unless you arm a ceiling: work completing beats work parked, and the defaults are tuned so runs are economical without cutoffs. When you do want a hard rail — an unattended overnight run against a shared quota, say — set maxCostUsd (whole-run, checked between stories exactly like runTimeoutMs; a story that would start past it parks as blocked until you raise the ceiling with retry --max-cost-usd <usd> and resume) and/or maxCostUsdPerStory (caps one story across attempts, hands every worker the story's remaining budget as a hard runtime cap via --max-budget-usd, and refuses to launch an attempt with less than a usable $5 floor rather than burning it blind). Both can also be armed or raised on a live run via retry — the plan snapshot is immutable, the flags are the sanctioned path.
When an overlay is present it must cover every story ([] = independent). Verification commands come only from this trusted config — never from prose inside the epic.
Which repo is which
-d/--dir is the target repo — where the worktree is cut, where the code lands, and where the worker skills must be installed (npx @engineering11/e11-foundry install there). The epics file and the overlay are just paths: they can live in a different repo entirely, which is the normal shape when one product repo plans work that ships in an SDK repo:
e11 epic run ~/dev/product/.foundry-output/planning-artifacts/epics.md \
--epic 1 --config ~/dev/product/.foundry-output/planning-artifacts/epic-run-1.yaml \
--runtime claude -d ~/dev/sdks-nodejs # ← code lands here; install Foundry hererun refuses up front (before any worktree or run record) when the target has no install, and plan prints an advisory — so this never fails deep inside a run.
Plan first — no agents involved
e11 epic plan path/to/epics.md --epic 1 --config epic-run.yaml [--json]Compiles and validates: duplicate/self references, ids that exist in no epic, cycles (reported with the full path), authored order must be topological, every story needs testable acceptance criteria. Same inputs always produce byte-identical plan JSON. plan never spawns an agent — run it freely.
Run, watch, resume
e11 epic run epics.md --epic 1 --config epic-run.yaml --runtime claude
e11 epic status <run-id> # story states at a glance
e11 epic status <run-id> --follow # live: streams every new ledger event until a terminal state
e11 epic dashboard # browser view: pipeline, heartbeats, evidence, spend, replay
e11 epic events <run-id> # the append-only audit ledger
e11 epic resume <run-id> --runtime claude # after a kill/crash — done stories never re-run
e11 epic retry <run-id> --story 1.2 # grant fresh attempts after exhaustion
e11 epic retry <run-id> --story 1.2 --refresh-spec # …and re-specify (default keeps the spec)
e11 epic retry <run-id> --story 1.2 --verify-only # re-judge the delivery on disk; spends no worker window
e11 epic adopt <run-id> --story 1.2 # you finished this one by hand; record it as done
e11 epic cancel <run-id> # terminal cancel (refused while a live controller holds the lock)Every command takes --principal <role> to act as a narrower role for one invocation — see Who is allowed to do what below.
A running worker is never a black box: the engine records a worker.heartbeat event every minute (current phase, turn count, last tool used) and mirrors the worker's full tool-call stream to deliver.stream.jsonl in the attempt folder — --follow shows the heartbeats live, and the stream tail tells you exactly what a slow worker was doing.
The dashboard
For a richer view than the terminal, e11 epic dashboard serves a local browser page, read-only unless you opt in, over the same run store: every run listed with status and spend, the story pipeline live, the worker heartbeat with a budget bar that ambers as silence approaches the stall clock, the controller's verification evidence with click-through to the gate logs, per-story cost bars, and a filterable event timeline with a replay scrubber — point it at a finished or failed run and scrub through what happened. The worker transcript panel shows the LLM's own words and every tool call (with inputs and results) parsed from the attempt's deliver.stream.jsonl, live-tailed while the worker runs. Completed runs also carry each story's change record — files touched, commit subjects, line totals, captured at completion so it survives cleanup — shown as a "What shipped" panel and an epic-wide file tree in the dashboard, and via e11 epic status <run-id> --files in the terminal. It binds to 127.0.0.1 only; closing it (Ctrl-C) never affects a run (--port <n> pins the port, --no-open skips the browser launch, --json emits {url}).
By default it writes nothing: every action button shows the exact CLI command and a click copies it rather than running it. Start it with --allow-actions to let retry/approve/reject/cancel/cleanup run from the page. run and resume are never offered there — they drive an epic for as long as it takes, which is a supervised process rather than a request.
The dashboard is not only the epic engine. A Planning section lists what the planning workflows wrote into the project's output folder — PRDs, architecture, epics, quality reviews — grouped, filterable and rendered, so the document a story came from is one click from the story. Sections appear only when a project has something for them: a repo with planning artifacts and no runs opens on Planning.
One dashboard can serve several projects. Installing Foundry into a repo registers it, so e11 epic dashboard --all-projects finds it with no extra step (install --no-register opts out, and uninstalling removes the entry). e11 project add <dir> registers one by hand — for a repo installed before this existed, or one you want listed without installing — and list / remove manage the set. Each project keeps a stable id, so it survives being moved or renamed.
Before the first worker spends anything, the engine checks that the environment can do the work at all, cheapest check first: the required skills are installed, the runtime answers a one-turn prompt (where a workspace-trust refusal or an expired login surfaces), setup runs, and then the verification battery runs once against the fresh worktree. That last check separates two things a bare exit code does not. A command that could not run — binary missing, killed by a signal, timed out — aborts the run for nothing, because every story would fail the same way after paying for a worker each time. A command that ran and failed is reported and the run continues, since a red baseline may be exactly what the epic is for. policy.baselineCheck: false skips it.
The run gets one worktree on branch e11/epic/<run-id>; stories build on each other inside it. Every story passes five gates before done: canonical spec validated (kernel sections, CAP declarations, companions) and digest-bound; the worker's e11-dev-auto-result.json must be the versioned evidence-bearing shape (legacy four-field results are rejected as unverified), written during the attempt itself — the engine clears the shared result path before each delivery, so a leftover from an earlier story is never read as evidence; the canonical spec must be byte-identical after delivery; and the controller re-runs the configured verification commands itself. Only launch failures (the runtime binary could not start) are transient and retry per policy; a timeout or stall stops the story with its partial work preserved — see failure recovery below. Semantic blocks (blocked: <condition>) stop the story and, under failFast, the run — --continue keeps independent stories going.
What a run costs, and how to spend less
Unattended runs are expensive in a way that is easy to underestimate, so the engine reports spend rather than leaving you to find it on a bill: every worker invocation's cost is recorded per story (e11 epic status shows it, story.cost events carry it, epic-result.json totals it in totalCostUsd). Spend from failed attempts counts too — a worker killed at its timeout burned a full window of tokens, and hiding that would make the most expensive outcomes the invisible ones.
Where the money goes, per story: one e11-dev-auto delivery (plus a separate e11-spec-flow invocation when you opt into specPhase: spec-flow) that internally runs an implementation subagent plus the configured review layers, and repeats that pair on each repair iteration. Review scales with diff size: a small diff (≤120 changed lines by default) gets a single combined reviewer, a larger one gets the full parallel panel — five layers with a five-iteration bound is up to 30 subagent runs for a single story, each re-reading the diff and its context. That is the dominant cost, and it is why a large story is disproportionately more expensive than two small ones, not just slower.
The levers, roughly in order of effect:
| Lever | Where | Effect |
|---|---|---|
| Smaller stories | the epics file | Biggest win — repair loops re-review a smaller diff, fewer iterations are needed, and small diffs stay on the single-reviewer path |
workerVerification | overlay | Scoped inner-loop commands: the worker iterates in seconds on one package instead of minutes on the monorepo, and its context stays clean of full-battery logs. The gate battery is unchanged and controller-observed |
Keep specPhase: story-direct | overlay policy | The default — opting into spec-flow adds one whole worker invocation per story (see below) |
| Fewer review layers | e11-dev-auto's customize.toml ([[workflow.review_layers]]) | Each layer is a full subagent per iteration; dropping from 4 to 2 roughly halves review spend on big diffs |
deliverModel / retryModel / effort | overlay policy | Already cheap-first by default (sonnet, medium; retries escalate to opus, high) — override only to pin exact IDs or change the ladder |
maxAttemptsPerStory | overlay policy | Each extra attempt re-runs delivery end to end |
Rehearse with --runtime fake | epic run --runtime fake | Proves graph, dependencies and wiring for free before spending anything |
Check epic plan and a fake run first; they cost nothing and catch the mistakes that would otherwise be discovered by an expensive worker.
Does the engine re-plan my story? — specPhase
By default, no: story-direct hands the exact story excerpt (plus integrated-dependency context) to the delivery worker as its intent — the same thing e11-dev-auto does when you point it at a story by hand — and the worker derives its execution spec directly from that. Opt into spec-flow when you want an extra distillation pass first: e11-spec-flow turns the story into a canonical SPEC.md kernel (preservation-checked claim by claim — "every load-bearing source claim lands in SPEC.md or a companion"), and the delivery worker treats that kernel as settled intent. It buys the CAP-N capability chain in epic-result.json at the cost of a full extra worker invocation per story.
policy:
specPhase: story-direct # default — one worker per story
# specPhase: spec-flow # add the spec-distillation worker firststory-direct (default) | spec-flow | |
|---|---|---|
| Workers per story | 1 (deliver) | 2 (specify + deliver) |
| Traceability | story → execution spec only | CAP-N capability → task → acceptance criterion |
| Extras | none — the story stands as written | extracted constraints, explicit non-goals, alloy citations, preservation sweep |
| Best when | stories already carry solid Given/When/Then acceptance criteria | stories are thin, or you want the capability chain in epic-result.json |
The intent file stays read-only for the worker either way — mutating it fails the story.
Run memory — what the engine carries between stories
Each story's worker is a fresh context: without help it re-derives the repository from zero, once per story. The engine hands every worker two orientation files instead:
- the epic brief — your project's compiled
epic-<N>-context.md, if it has one, snapshotted into the run'sinput/so a resume reads the same brief it started with. No brief is a warning, not a failure. - run memory — rebuilt after every completed story from the engine's own records: what shipped, the files each story touched, and the commands the controller observed pass. It is assembled by the kernel, never by a model, so it costs nothing to produce.
Both are explicitly orientation, not instruction: they do not change what a story must do, and nothing in them is evidence. The story excerpt stays the intent; the controller's exit codes stay the proof. Memory is bounded — recent stories in full, older ones collapsed — because unbounded context recreates the cost it exists to remove. Both files are visible read-only in the dashboard.
Run knowledge — the part that IS evidence. Investigation is the run's most repeated expense: left alone, every story re-maps the same CI machinery and package anatomy the story before it already mapped. Workers therefore write what their investigators learn into the run's knowledge/areas/<slug>.md, each record ending with a ## Cited files list of the paths it relies on — and the controller certifies freshness: at run start and after every completed story it re-digests every cited file against the worktree and stamps each area fresh or stale in knowledge/index.json (knowledge.certified in the ledger carries the counts). The trust claim is deliberately narrow — certification says the cited files are unchanged since the record was written, not that the record is true — but that is exactly what licenses the next story to stop re-verifying: a fresh area is a current description of the code, so workers read it as evidence and investigate only missing or stale areas. Same invariant as delivery — worker claims, controller observation — applied to memory.
Completion and the promotion gate
When every story is done, the engine confirms the full verification battery one final time — when the last story's verification just ran in this same drive and nothing changed since, it reuses that freshly observed evidence instead of paying for an identical re-run; otherwise (a resumed run, say) it runs the battery again. Then it writes the immutable epic-result.json — per-story records, final evidence, and the independence report (which stories touched overlapping files — the data behind any future parallel mode) — and stops at waiting_promotion:
e11 epic approve <run-id> # records approval; merging/pushing stays yours
e11 epic reject <run-id> # records rejection; everything preserved for inspection
e11 epic cleanup <run-id> # removes worktree + branch; the run ledger stays as audit recordWho is allowed to do what
By default this does nothing. With no configuration, every local command and the local dashboard act as engineer, which holds every capability — the behaviour you had before this existed.
It becomes real when you write .foundry/principal.yaml:
role: planner # observer | planner | ux | engineerRoles are bundles of capabilities; the capability is what is actually checked.
| Role | Can | Cannot |
|---|---|---|
observer | read runs and analysis | everything else |
planner | the above, plus plan stories and produce specs | deliver code, retry, approve, cancel |
ux | the above, plus prototype | deliver code, retry, approve, cancel |
engineer | everything | — |
code:write is the decisive one. Without it the engine itself refuses the delivery phase — before a worker is launched, so a refusal costs nothing. The story lands blocked with the reason, and the spec phase still runs: a planner can produce specs for an engineer to deliver. This is not a hidden button. The same refusal happens whether you go through the CLI, the dashboard, or drive the CLI from somewhere else, because a check that only lived in the browser would protect nobody.
Grants add a single capability without changing role:
role: planner
grants:
- epic:run # may start runs, still may not deliver code--principal <role> overrides the file for one invocation, and is read only from your environment — never from the repository, so checking a file into a project cannot raise anyone's authority. A refused command exits 4 and names the capability it wanted. A malformed principal.yaml is an error rather than a silent fallback to engineer.
Where things land
.foundry/_orchestration/runs/<run-id>/
manifest.json # immutable: epic + config digests, base commit
run.json # current state (atomic)
events.ndjson # every transition and observed exit code
input/ # epic + plan snapshots the run executes against
stories/<id>/spec/ # canonical SPEC.md + .memlog.md + companions
stories/<id>/attempt-N/ # worker logs, archived result + execution spec
epic-result.json # written once, at completion
.foundry/_orchestration/worktrees/<run-id>/ # the run worktree (one branch)Failure recovery, honestly
Kill or crash mid-run →
resume: state persists after every transition; validateddonestories are skipped, nothing re-runs.Attempts exhausted (a real outage, or a bug that wasn't the story's fault) →
retry --storygrants another attempt window; the event ledger keeps the full history. Attempt numbering continues across retries — the new attempt gets a fresh directory, and its worker is pointed at the failed attempt's archive (execution spec with its Code Map, Spec Change Log, and Review Triage Log, plus the result file) and the failure reason, so a retry starts from what the last attempt learned instead of re-rolling the same dice. Retried attempts also escalate toretryModel/retryEffort(opus, high by default): the cheap tier already failed with a full window. The retry keeps the canonical spec the story already validated and goes straight back to delivery — re-specifying would burn the window the retry needs. Add--refresh-specwhen the spec itself was the problem.A worker timed out or stalled → the engine killed it (
workerTimeoutMs, orstallTimeoutMsafter total output silence) and did not auto-retry — a full re-run of a burned window is a spend decision, and it is yours. Read the evidence first: the attempt'sdeliver.stream.jsonltail and the ledger heartbeats show whether the worker was progressing (needs more time — grant it withretry --story 1.2 --worker-timeout-ms <bigger>; a live run executes against its plan snapshot, so editingepic-run.yamlmid-run changes nothing), thrashing on the same files (needs a smaller story), or hung on one command (fix that, not the budget).skill:e11-epicwalks this triage for you and, when escalation is enabled, raises the budget 1.5× (up to a ceiling) before retrying — a timeout is a verdict on the clock, not on the work.Nothing is thrown away. Anything the killed worker already committed stays on the run branch. Anything half-written is moved to the git stash with a label naming the story and attempt (
git stash list,git stash show -p stash@{0}), and the error message tells you the ref. That matters for more than salvage:e11-dev-autorefuses to start on a dirty tree, so leaving the fragments in place would make the next attempt fail for an unrelated reason. The retry then keeps the canonical spec it already validated and goes straight back to delivery, so the whole fresh budget goes to implementation rather than re-specifying.A worker exited 0 without writing its result → the worker ended its turn while verification was still running, usually because it backgrounded the command or armed a watcher for it. Nothing is wrong with the work — and if the worker committed it, nothing is lost either. The engine reads the worktree HEAD around every delivery, so it can tell a delivery that produced nothing from one whose receipt went missing, and the story's reason says which:
- It committed → the reason reads
delivery lost its receiptand names the commit. Finish the story withe11 epic retry <run-id> --story <id> --verify-only, then resume: the controller runs this project's verification against that commit and records the story as done. No worker window is spent. A plainretryis refused in this state andresumewill not re-deliver either — a worker with nothing left to do writes no result and fails exactly the same way, so the engine will not let you buy that answer twice. If you would rather throw the delivery away and start over, reset the commit out of the run worktree; the refusal lifts with it. - It committed nothing → the reason says so. Whatever the worker did is uncommitted in the worktree: verify and commit it by hand, then use
adopt(next entry).
- It committed → the reason reads
A story you salvaged by hand →
e11 epic adopt <run-id> --story <id>, then resume. The engine does not take your word for it: it runs this project's verification commands itself, at the revision you name (the worktree HEAD by default), and refuses the adoption if they fail. A story adopted this way is recordedprovenance: adoptedforever —epic-result.jsonand the promotion gate show it as your delivery that the engine checked, never as one the engine produced. Adoption needscode:write, and it needs the worktree to still exist, since that is what it judges. Reserve it for work you actually finished yourself: a delivery that only lost its receipt is recordedprovenance: recoveredby the entry above, and the promotion gate keeps the two apart so "the engine wrote this" and "you wrote this" never read as the same claim.blockedwith a stable condition → a human decision is genuinely needed; fix the input (or the spec) and resume.Post-run drift → every worker commit ends with a
Spec-Ref: <path>@<digest>trailer;node .foundry/scripts/spec-ref-check.mjsverifies trailers resolve and intent digests still match.
Recently closed
Four recovery gaps that used to need workarounds are closed as of 1.7.0: e11 epic adopt records work you finished by hand (and work a worker left behind when it ended its turn early), a killed controller's orphaned worker is reaped by the next one, and deferred work is written one file per entry so epic branches stop conflicting on a shared file.
Deep dives: Unattended runs for the worker contract · Quick flow for the single-change loop this engine scales up.