---
description: >-
  Drive an Epic from `agent::ready` to a merged pull request against `main`.
  The ten-phase flow runs the wave loop, close-validation, epic-audit,
  code-review, retro, finalize, watch-and-iterate, conditional auto-merge,
  and local branch cleanup. When the run is end-to-end clean (zero manual
  interventions, zero 🔴/🟠 review findings, compact retro) the PR
  auto-merges via `gh pr merge --squash --delete-branch`; otherwise the
  workflow falls back to the operator-merges-button path so a human
  inspects the surface area.
recommendedModel: opus
---

<!-- recommendedModel rationale: ten-phase delivery orchestrator coordinates wave fan-out, change-set audits, code review, retro, and merge gating — reasoning-heavy, advisory hint for operators. -->

# /epic-deliver #[Epic ID]

## Overview

`/epic-deliver` is the **single SDL execution command** in the 5.40 surface.
It opens a PR against `main` and auto-merges when every signal certifies a
clean run; otherwise it falls back to the operator-merges-button path.

```text
/epic-deliver <epicId>
  → Phase 1 — prepare              (epic-deliver-prepare.js)
  → Phase 2 — wave loop            (wave-tick.js + Agent fan-out × concurrencyCap)
  → Phase 3 — close-validation     (lint + test + ratchets on epic/<id>)
  → Phase 4 — epic-audit           (helpers/epic-audit.md — change-set audits via selectAudits)
  → Phase 5 — code-review          (helpers/epic-code-review.md)
  → Phase 6 — retro                (.agents/scripts/lib/orchestration/retro-runner.js)
  → Phase 7 — finalize             (lifecycle-emit → epic.close.end → open PR to main)
  → Phase 8 — watch-and-iterate    (poll `gh pr checks`; fix locally until green)
  → Phase 8.5 — auto-merge gate    (lifecycle-emit → epic.automerge.start)
  → Phase 9 — cleanup              (BranchCleaner + Cleaner lifecycle listeners on epic.cleanup.start / epic.merge.armed; fire via lifecycle-emit → epic.merge.armed)
```

The argument is always an Epic ID (`type::epic`). Story IDs go to
[`/story-deliver`](story-deliver.md); Tasks are not directly executable.
Story dispatch is in-session via the Agent tool — no subprocess is
spawned.

---

## Arguments

```text
/epic-deliver <epicId> [--skip-epic-audit] [--skip-code-review] [--skip-retro] [--full-retro]
```

- `epicId` — must carry `type::epic`. Otherwise STOP and tell the operator
  to use `/story-deliver <id>` or open the parent Epic.
- `--skip-epic-audit` — skip Phase 4 (log the override). Use only when the
  change-set audits are known to be irrelevant (e.g., docs-only Epic).
- `--skip-code-review` — skip Phase 5 (log the override).
- `--skip-retro` — skip Phase 6 (use sparingly).
- `--full-retro` — force the six-section retro regardless of manifest
  cleanliness. `--skip-retro` wins over `--full-retro`.

Every other runtime modifier is sourced from the Epic's labels or from
`delivery.deliverRunner` in `.agentrc.json`.

---

## Contract

- **Idempotent by checkpoint.** Re-runs resume from `epic-run-state`.
- **Single pause point.** Only `agent::blocked` halts execution. No
  clarifying questions — if stuck, flip to `agent::blocked`, post a
  friction comment, park.
- **Two-level dispatch.** Host LLM fans out per-Story Agent calls
  directly with `subagent_type: general-purpose`. Sub-agents do not
  carry the `Agent` tool, so this stays flat.
- **Operator-merges-PR exit.** Phase 7 opens the PR; the workflow
  never merges to `main` itself. Phase 8.5 may fire auto-merge when
  every signal is clean.
- **Lifecycle bus is the runner model.** Phase transitions, ticket
  state flips, structured comments, and notifications are emitted as
  typed events on the in-session lifecycle bus; a fixed roster of
  listeners performs the side effects. Phase 7, 8.5, and 9 each fire
  exactly one lifecycle event via the generic
  [`lifecycle-emit.js`](../scripts/lifecycle-emit.js) CLI
  (`--event epic.close.end` / `--event epic.automerge.start` /
  `--event epic.merge.armed`); the matching listener chain runs the
  bus-driven side effects (acceptance reconcile, automerge-armer,
  branch cleanup). PR open + planning-artifact close are
  operator-/host-LLM-driven in the current wiring — see Phase 7.1 for
  the canonical manual sequence and `finalizer.js` for the listener's
  no-op disclaimer. The append-only NDJSON ledger at
  `temp/epic-<id>/lifecycle.ndjson` is the resume target. See
  [`docs/LIFECYCLE.md`](../../docs/LIFECYCLE.md) for the bus
  contract, event taxonomy, ledger format, and listener model.

---

## Phase 1 — Prepare the Epic run

```bash
node .agents/scripts/epic-deliver-prepare.js --epic <epicId>
```

Validates `type::epic`, enumerates `type::story` descendants, parses
`blocked by #N` plus explicit `dependencies`, runs `Graph.computeWaves()`,
and upserts the `epic-run-state` checkpoint. Treat the printed JSON as
`state`: `{ epicId, totalWaves, concurrencyCap, plan, checkpointInitializedAt }`.
`plan[N]` is the Stories assigned to wave `N`. Flip the Epic to
`agent::executing` (idempotent) after the CLI returns.

> **Acceptance-spec start gate.** Before the wave loop fans out, the
> snapshot phase
> ([`lib/orchestration/epic-runner/phases/snapshot.js`](../scripts/lib/orchestration/epic-runner/phases/snapshot.js))
> asserts that the Epic either carries the `acceptance::n-a` waiver
> label **or** has a linked `context::acceptance-spec` ticket. The
> ticket's GitHub state (open / closed) is **not** checked —
> presence is sufficient, matching the PRD and Tech Spec contract.
> The reviewer's OK during `/epic-plan` Phase 7 is the approval
> signal, not a manual ticket-close action. Neither condition met →
> the snapshot throws a clear error
> (`[epic-deliver] Epic #<id> cannot launch: …`) and `runAsCli`
> maps it to `process.exit(1)`. Operator remediation: either run
> `/epic-plan` Phase 7 to author the spec, or apply the
> `acceptance::n-a` label to opt out.

---

## Phase 2 — Wave loop

The wave-loop state machine lives in
[`lib/wave-runner/tick.js`](../scripts/lib/wave-runner/tick.js) — one
stateless `tick({ epic })` call returns one `WaveTickResult` describing
the next action. The slash command's job is to call `tick()` via its CLI
shim, dispatch from `nextAction.stories` via the Agent tool, persist the
outcome, and loop until terminal.

### 2a. Tick — plan the next action

```bash
node .agents/scripts/wave-tick.js --epic <epicId>
```

Stdout is one `WaveTickResult` envelope:

```json
{
  "nextAction":
      { "kind": "dispatch",      "stories": [{ "id": <n>, "title": "…", "worktree"?: "…" }, ...] }
    | { "kind": "observe",       "waitingOn": [<storyId>, ...] }
    | { "kind": "wave-complete", "index": <n> }
    | { "kind": "epic-complete" },
  "blockedStories": [{ "storyId": <n>, "reason": "…", "detail"?: "…" }, ...],
  "gateFailures":   [{ "storyId": <n>, "gate": "…", "detail"?: "…" }, ...],
  "currentWave":    <n>,
  "totalWaves":     <n>
}
```

The CLI emits `wave-tick` (every call) plus `wave-start` /
`wave-complete` / `epic-complete` at transitions to the per-Epic
`signals.ndjson`; the
[`signals` helper](helpers/signals.md) (`node .agents/scripts/signals-view.js`)
renders them in the span-tree view when needed.

### 2b. Dispatch — fan out per-Story Agent calls

*You* (the LLM running this skill) are the wave dispatcher; you never
invoke `/story-deliver` yourself. Emit **one `Agent` tool call per
Story** in `nextAction.stories` (even when `length === 1` — the
parent-child boundary keeps the return-parser uniform). The *children*
run `/story-deliver`. Use `subagent_type: general-purpose`.

Emit **one assistant turn** with **N parallel `Agent` calls** where
`N === min(nextAction.stories.length, concurrencyCap)`. When the wave
exceeds `concurrencyCap`, dispatch the first `concurrencyCap` Stories
as background calls (`run_in_background: true`) and refill from
`nextAction.stories` immediately as each child returns — never exceed
the cap, never wait for a whole batch before refilling.

Each Agent call's prompt must (1) name the Story + Epic ids, (2)
instruct the child to invoke `/story-deliver <storyId>`, (3) state the
**return contract** below, (4) remind the child of the
**non-interactive contract** (no clarifying questions; transition to
`agent::blocked` and exit if stuck), and (5) ask the child to suppress
per-Task chat relay and include its **terminal** `renderedBody` in the
JSON return.

```json
{
  "storyId": <number>,
  "status": "done" | "blocked" | "failed",
  "phase": "init|implementing|closing|blocked|done",
  "tasksDone": <number>, "tasksTotal": <number>,
  "branchDeleted": <boolean>,
  "blockerCommentId": <string|null>,
  "detail": <string|undefined>,
  "renderedBody": <string|undefined>
}
```

**Dispatch-model resolution.** For `model:`, precedence (highest
wins): per-call literal → workflow `dispatchModel` frontmatter →
inherit (emit no argument). The unset case is today's behaviour — full
inheritance via the `general-purpose` sub-agent definition. Children
inherit the parent's worktree context; no
`--dangerously-skip-permissions` (no subprocess is spawned).

### 2c. Record the wave outcome

Once every dispatched Story has returned, persist via
`epic-execute-record-wave.js`:

```bash
# Mode A — host LLM already parsed each child return.
node .agents/scripts/epic-execute-record-wave.js \
  --epic <epicId> --wave <N> [--concurrency-cap <N>] \
  --results @<file>|<inline-json>

# Mode B — pipe the raw per-Story sub-agent return texts directly.
node .agents/scripts/epic-execute-record-wave.js \
  --epic <epicId> --wave <N> [--concurrency-cap <N>] \
  --returns @<file>|<inline-json>
# `<inline-json>` shape: [{ "storyId": <n>, "returnText": "<raw text>" }]
```

**Prefer mode B** when the host LLM can't fully verify every child's
return is a parseable envelope. The CLI reconciles parse failures from
GitHub, aggregates terminal status, appends to `state.waves[]`,
re-renders `epic-run-progress`, and prints
`{ status, nextAction, renderedBody, ... }`. Print `renderedBody`
verbatim, then optionally append a short **Notable** section (0–5
bullets on newly blocked / failed / slow Stories, friction,
elapsed-time surprises).

### 2d. Loop on `nextAction`

After `2c`, re-run `wave-tick.js`. Branch on the new envelope:

- `dispatch` → repeat 2b/2c for the same wave (refill) or the next wave.
- `observe` → poll the Epic (children may still be in flight, or some
  are `agent::blocked`). If `blockedStories` is non-empty, post a
  friction comment, flip Epic to `agent::blocked`, park.
- `wave-complete` → loop to the next wave.
- `epic-complete` → proceed to Phase 3.

---

## Phase 3 — Close-validation

Run lint + test + ratchets against `epic/<epicId>` before opening the PR:

```bash
node .agents/scripts/evidence-gate.js \
  --epic-id <epicId> --scope-id <epicId> --gate lint -- npm run lint
node .agents/scripts/evidence-gate.js \
  --epic-id <epicId> --scope-id <epicId> --gate test -- npm test
```

If either gate fails: STOP, fix on a hotfix branch, merge back to the
Epic branch, restart this phase.

### 3.1 Refresh ratcheted baselines

Inspect the scripts in `.husky/pre-push` (typecheck, lint, maintainability,
design tokens, dependency audits, bundle-size budgets). Run each against
the Epic branch; if any drifts, refresh and commit
`chore(baselines): refresh <name> for Epic #<epicId>`.

---

## Phase 4 — Epic audit (change-set lenses)

Skip when `--skip-epic-audit`. Otherwise auto-invoke
[`helpers/epic-audit.md`](helpers/epic-audit.md) inline. The helper runs
[`epic-audit-prepare.js`](../scripts/epic-audit-prepare.js) to ask the
[`selectAudits`](../scripts/lib/audit-suite/index.js) SDK which lenses fire
at the `gate3` close gate, then dispatches each selected lens through
[`runAuditSuite`](../scripts/lib/audit-suite/index.js). Findings are
persisted as an `audit-results` structured comment on the Epic.

- **Any 🔴 Critical Blocker** — STOP. Relay to the operator.
- **Only 🟠/🟡/🟢** — log as non-blocking and continue.
- **Selector reports `degraded: true`** — STOP. Propagate the
  `reason`/`detail`, post a friction comment, do not fall back to a
  full-roster audit.
- **`selectedAudits` is empty** (docs-only change set) — log the
  short-circuit and continue to Phase 5.

---

## Phase 5 — Code review

Skip when `--skip-code-review`. Otherwise auto-invoke
[`helpers/epic-code-review.md`](helpers/epic-code-review.md) inline
(read-only audit). The helper persists findings as a `code-review`
structured comment on the Epic.

- **Any 🔴 Critical Blocker** — STOP. Relay to the operator.
- **Only 🟠/🟡/🟢** — log as non-blocking and continue.

---

## Phase 6 — Retro

Skip when `--skip-retro`. Otherwise post the `epic-perf-report` via
`node .agents/scripts/analyze-execution.js --epic <epicId>` (failure →
warn and continue; the retro runner falls back). Then invoke the retro
runner inline — the canonical surface lives at
[`.agents/scripts/lib/orchestration/retro-runner.js`](../scripts/lib/orchestration/retro-runner.js)
and is driven by `epic-deliver.js`. Propagate `--full-retro` to bypass
the compact-path heuristic.

Retro fires here (before the PR opens) so it stays in the operator's
local session with full env access (env vars, credentials, MCP).

After the GitHub upsert succeeds, the retro body is also **mirrored
locally** to the per-Epic temp tree at `temp/epic-<epicId>/retro.md`
(path resolved via
[`lib/config/temp-paths.js`](../scripts/lib/config/temp-paths.js)'s
`epicRetroMirrorPath`, which honours `project.paths.tempRoot`).
Operators can read the retro without re-fetching from GitHub. GitHub
remains the source of truth — a mirror-write failure only logs a warn
and never fails the phase.

---

## Phase 7 — Finalize (open PR to main)

### 7.0 — Sync Epic branch from `main` (Story #2580)

Before firing the close-tail emit, sync the Epic branch with
`origin/main` so the PR opens with the latest base commits already
integrated. The Epic branch may be behind `main` if other PRs merged
during the wave loop; without this step, the Epic→`main` PR opens
"behind base" and (with branch-protection's `up-to-date branch` rule)
stalls at the merge gate.

```bash
git checkout epic/<epicId>
node .agents/scripts/sync-branch-from-base.js \
  --branch epic/<epicId> --base main
```

Outcomes:

- **`fast-forward` / `merge-commit` / `noop-already-current`** → push
  the resulting tip and continue to Phase 7.1: `git push origin epic/<epicId>`.
- **`conflict`** → resolve in the Epic checkout (`git merge --no-edit
  origin/main`, fix conflicts, `git commit --no-edit`), then re-run the
  sync command. Once it exits 0, continue. Operator-recoverable; not an
  agent loop.
- **`fetch-failed`** → re-check network / `origin` access and re-run.

This is a workflow-level step (operator-driven), not part of the
close-tail listener chain — the listener delegates to a `runFinalizeFn`
that does not currently push or open the PR in production (see
[`finalizer.js`](../scripts/lib/orchestration/lifecycle/listeners/finalizer.js)
docstring). When a future Story lifts the push + `gh pr create` flow
into the listener body, the sync may move with it; until then, run this
step manually.

### 7.1 — Fire the close-tail emit

```bash
node .agents/scripts/lifecycle-emit.js --epic <epicId> --event epic.close.end
```

Emits `epic.close.end` onto the lifecycle bus. **In the current
production wiring only the first of the three close-time
responsibilities below runs inside the listener chain; the other two
are operator/host-LLM responsibilities until the listener's
`runFinalizeFn` is lifted out of its no-op state (see
[`finalizer.js`](../scripts/lib/orchestration/lifecycle/listeners/finalizer.js)
docstring).** Treat this section as both a runtime contract and a
runbook.

1. **Acceptance-spec reconciliation — bus-driven.** The
   `AcceptanceReconciler` listener invokes
   [`acceptance-spec-reconciler.js`](../scripts/acceptance-spec-reconciler.js)
   to diff the AC IDs declared in the linked `context::acceptance-spec`
   body against `@ac-*` / `@pending` tags in `tests/features/**`. A
   non-OK reconciliation throws (per
   [`rules/orchestration-error-handling.md`](../rules/orchestration-error-handling.md)),
   aborting finalize **before** any PR is opened or planning artifacts
   are closed — so the PRD, Tech Spec, and Acceptance Spec stay open
   until the AC coverage gap is fixed. The reconciler returns
   `status: 'waived'` without scanning features when the Epic carries
   `acceptance::n-a`, and defends against direct CLI invocation by
   refusing to run when no spec is linked and no waiver is set (the
   start gate in Phase 1 would normally catch that first).
2. **PR open and auto-merge arm — operator-/host-LLM-driven (today).**
   The `Finalizer` listener fires after `AcceptanceReconciler`, but its
   `runFinalizeFn` is a no-op in production. The operator (or the host
   LLM driving `/epic-deliver`) MUST run the canonical PR-open sequence
   inline after the close-tail emit returns:

   ```bash
   gh pr create --base main --head epic/<epicId> \
     --title "Epic #<epicId>: <title>" \
     --body-file <run-progress + code-review + retro summary>
   gh pr merge <prNumber> --auto --squash --delete-branch
   ```

   Failure of `gh pr merge --auto` is non-fatal — the operator merges
   through the GitHub UI instead. Once a future Story lifts this flow
   into the `Finalizer` listener body, this step will become
   bus-driven and this prose updates to match.
3. **Planning-artifact close + hand-off — operator-/host-LLM-driven
   (today).** Close the three planning context tickets manually so the
   Epic's `Closes #<id>` auto-close path is not blocked by open
   sub-issues:

   ```bash
   gh issue close <prdId> --reason completed
   gh issue close <techSpecId> --reason completed
   gh issue close <acceptanceSpecId> --reason completed
   ```

   When the `acceptance::n-a` waiver is set, the Acceptance Spec
   ticket need not be authored; if no spec was ever opened, the third
   `gh issue close` is skipped.

Branch cleanup is out-of-band (Phase 9 reaps local refs after merge; the
rare "scrap and reset" case for an unmerged Epic is handled manually).

---

## Phase 8 — Watch-and-iterate until CI is green

The host LLM owns the green-bar loop until the operator merges. Use
the shared watch-and-recover helper, which wraps `gh pr checks --watch`
and additionally auto-recovers from `mergeStateStatus: BEHIND` by
calling `gh pr update-branch` once every required check is green
(branch-protection rules requiring "up to date before merging"
otherwise park the PR until the operator clicks **Update branch**
manually):

```bash
node <agentRoot>/scripts/pr-watch-with-update.js --pr <prNumber>
```

`<agentRoot>` resolves from `project.paths.agentRoot` (default
`.agents`). Pass `--max-updates N` (default 3) to cap update-branch
calls per session and `--poll-interval-ms MS` (default 10000) to
override the polling cadence.

Exit 0 → proceed to Phase 8.5. Non-zero → remediate (below) and re-run
the helper. Auto-merge stays armed across retries; the
`epic.automerge.start` emit in Phase 8.5 re-runs the `AutomergeArmer`
listener, which re-checks `mergeStateStatus` before firing merge, so a
second BEHIND that arrives between the helper exiting clean and Phase
8.5 starting is also caught.

### 8.1 Remediation

For each failed required check: fetch the log
(`gh run view <runId> --log-failed`), classify and fix:

- **lint / format** → `npm run lint` + `npx biome check --apply` (or
  `format --write`); commit, push.
- **maintainability / crap baseline drift** → re-run the ratcheted
  script. Refresh the baseline only when drift is justified by the
  diff; otherwise fix at source.
- **test failure** → reproduce with `npm test`, fix source or test.
- **coverage threshold** → add tests (preferred); refresh baseline only
  when the diff demonstrably can't be covered.
- **anything else** → read the log, fix at source.

Push to `epic/<epicId>` and re-run
`node <agentRoot>/scripts/pr-watch-with-update.js --pr <prNumber>`.

### 8.2 When to halt

Three consecutive iterations on the same failure class without
convergence → friction comment, flip to `agent::blocked`, park. Unknown
failure class on first encounter → attempt source-level fix; log
friction if diagnosis takes more than one round.

### 8.3 Hard prohibitions

**Never** `gh pr merge` from Phase 8 (Phase 8.5 is the only merge
site). **Never** force-push to `main`. **Never** push empty commits or
refresh baselines to dodge a red check.

---

## Phase 8.5 — Auto-merge gate

After Phase 8 exits 0, evaluate the auto-merge predicate by emitting
`epic.automerge.start`:

```bash
node .agents/scripts/lifecycle-emit.js --epic <epicId> \
  --event epic.automerge.start --pr-url <prUrl>
```

The `AutomergeArmer` listener subscribes to the downstream
`epic.merge.ready` outcome and fires `gh pr merge --auto --squash
--delete-branch` only when `clean: true`. `clean: true` requires
**all** of:

- `state.manualInterventions[]` is empty;
- every wave's `status === "complete"`;
- no story envelope carries a `blockerCommentId` or non-`done` status;
- code-review reports `0` 🔴 + `0` 🟠 findings;
- the retro is the compact "🟢 Clean sprint" body.

When clean, the listener fires `gh pr merge --squash --delete-branch`.
Otherwise the listener records disqualifying reasons via
`epic.merge.blocked` and exits without merging — operator merges
manually.

### Recording manual interventions

Whenever you step outside the happy path during a delivery, record it
(each entry disqualifies auto-merge):

```bash
node .agents/scripts/epic-deliver-note-intervention.js \
  --epic <epicId> --reason "<one-line description>"
```

Triggers: `AskUserQuestion` mid-run; `git restore`/`reset` against the
tree; child-reported `--no-ff` recovery, stash dance, or out-of-band
merge surgery; child closes via `--skipValidation`; force-pushing or
empty-committing to dodge CI diagnosis.

---

## Phase 9 — Local branch cleanup

Phase 9 runs **automatically** inside the lifecycle bus once auto-merge
arms: the `BranchCleaner` listener subscribes to `epic.cleanup.start`
and reaps local refs before `Cleaner` archives the `temp/epic-<id>/`
tree. No operator step is required on the auto-merge path.

What gets reaped (in order, all in-process):

1. The main checkout is switched off `epic/<id>` to `baseBranch` when
   needed (otherwise `git branch -D epic/<id>` is refused).
2. Every `story-<id>` listed in the `epic-run-state` checkpoint, plus
   `epic/<id>`. Attached worktrees are removed with the standard
   `git worktree remove` → `--force` → filesystem-rm fallback (the
   last step covers Windows file-locks).
3. `git remote prune <remote>` drops stale `<remote>/...` tracking
   refs left behind by `gh pr merge --delete-branch`.
4. The `wt-branch` scratch ref left by `story-close.js`'s internal
   merge worktree is deleted when no worktree still points at it.

Per-branch failures aggregate into the listener's classification log
(`reaped` / `failed` / `no-state` / `skipped-duplicate`) and are
visible in `temp/epic-<id>/lifecycle.ndjson`. They do not block the
rest of cleanup.

For out-of-band cleanup re-entry (resume after a crash, or operator
override), fire `epic.merge.armed` via the lifecycle-emit helper:

```bash
node .agents/scripts/lifecycle-emit.js --epic <epicId> \
  --event epic.merge.armed --pr-url <prUrl>
```

If Phase 8.5 fell back to the operator-merges-button path (`gh pr
merge --auto` was declined), the `epic.merge.armed` event never fires
inside this run and Phase 9 will not run automatically. After the
operator merges the PR, `epic/<epicId>` and each `story-<id>` ref can
be reaped manually:

```bash
git checkout main
git pull --ff-only origin main
git branch -D epic/<epicId>
git branch -D story-<id1> story-<id2> ...
git remote prune origin
```

Note that `git-cleanup.js` alone will not catch `story-<id>` refs in
this case because the epic PR squash-merges break the `git branch
--merged main` signal and the stories never had their own PRs. Wiring
a CLI surface that drives the BranchCleaner listener for this
fallback is tracked as follow-up to Story #2398.

---

## Idempotence and resume

Re-runs pick up at the next undispatched wave (in-flight Stories finish
via `/story-deliver`'s own checkpointing). The PR from Phase 7 is
updated in place on subsequent runs. The authoritative live view is
the `epic-run-progress` structured comment.

---

## Constraints

- **Never** merge `epic/<epicId>` to `main` outside Phase 8.5.
- **Never** dispatch more than one wave at a time; concurrency lives
  inside a single wave's fan-out, capped at `concurrencyCap`.
- **Never** flip Story-level labels from this skill; **never** invoke
  `/story-deliver` yourself (children run it via Agent fan-out, even
  for single-Story waves); **never** spawn a subprocess for dispatch.
- **Always** checkpoint via `epic-deliver-prepare.js` /
  `epic-execute-record-wave.js`; never write run state elsewhere.
- **Always** post a friction structured comment before a non-`complete`
  outcome.
- **Always** auto-invoke the epic-audit, code-review, and retro helpers
  (Phases 4–6) when their artefacts aren't already present.
- **Always** drive Phase 8 to green CI before returning control — the
  host LLM owns the loop until the PR is mergeable or the Epic is
  parked at `agent::blocked`.
