<!-- source: openspec/current/forge/commands.md -->

---
title: Forge — Commands
status: stable
owner: pavel
audience: [pavel, ai, engineer]
tldr: Per-command reference for Forge — all command definitions plus review orchestration agents, finding schema, verifier, and cost guard.
---
# Forge — commands

> Part of openspec/current/forge/ — split from forge.md on 2026-05-12

---

## Command surface

The CLI dispatches on `args[0]` with a hard whitelist at `src/cli.ts:37-56`. Eighteen commands today: `audit`, `spec`, `review`, `estimate`, `render`, `mirror-export`, `inbox`, `inspect`, `plan`, `build`, `epic`, `conductor`, `status`, `cancel`, `resume`, `archive`, `install-skills`, and `mcp-server`. `tail` (RAP-405) is deferred — it appears in the `forge --help` usage text but is **not** in the cli.ts whitelist, so invoking it prints usage and exits 1. See `entities.md § Builder mode` and `spec.md § Architecture`.

### Legacy six

| Command | Purpose | Source |
|---|---|---|
| `forge audit <module> [<resolver-arg>] [--agent=<n>] [--estimate] [--persist] [--create-issues[=critical|=significant]] [--include=<role>] [--exclude=<role>] [--verbose] [--json]` | Run N parallel agents per module config; aggregate via Opus verifier; optionally open Linear issues and persist to Supabase. Two module families: **code modules** (e.g. `web`, `studio`, `forge`) — roles are pm / architect / tech-lead / ui-designer / security / qa / seo / growth / performance / a11y / risk / component-api against source code; **OpenSpec-change module** (`openspec-change <slug>`) — roles are principal-architect / product-expert / security-privacy / sre-reliability / data-architect / qa-test / spec-editor / adversarial-reviewer plus opt-in counsel-compliance + ux-frontend, run against `openspec/changes/<slug>/` artefacts (proposal, design, tasks, verification). `openspec-change` verdicts gate Stage 2 → Stage 3 advance per `workflow.md § Stage 2 — Spec`. See `add-openspec-audit-pass/design.md` for the nine-to-eleven-role contract. | `cli.ts:63`, orchestrator `commands/audit/orchestrator.ts:27`, OpenSpec input resolver `commands/audit/input-resolvers/openspec-change.ts` |
| `forge spec <module> [--all] [--fix] [--sonnet] [--estimate] [--persist] [--verbose] [--json]` | Bidirectional spec-vs-code coverage check per module. `--fix` generates a change-proposal scaffold under a configurable output path for `MISSING-DOC` findings. | `cli.ts:163`, checker `commands/spec/checker.ts:21`, fix `commands/spec/fix.ts:12` |
| `forge review [--sha=<hash>] [--file=<path>] [--orchestrate[=<agents>]] [--pr=<number>] [--post] [--strict[=critical]] [--estimate] [--verbose] [--json]` | Diff vs `main`/`master` (or a commit/file or remote PR via `gh`), reads ±50 surrounding lines per change. Two modes: **single** (default — one Opus call) and **orchestrated** (`--orchestrate` — N parallel specialist agents + Opus verifier; see § Review orchestration). Writes JSON+MD reports under `.forge/reviews/`, optional non-zero exit on findings, optional `--post` to comment the aggregated MD on a PR via `gh`. | `cli.ts` review handler, single-mode `commands/review/reviewer.ts`, orchestrator `commands/review/orchestrator.ts` |
| `forge estimate <issue-key> [issue-key…] [--persist] [--verbose] [--json]` | Fetches one or more Linear issues via GraphQL, optionally pulls referenced specs from issue body's `## Relevant specs` section, asks Sonnet for complexity/risk/SP/hours estimate, optionally persists to `forge_estimates` + `forge_events`. | `cli.ts:248`, estimator `commands/estimate/estimator.ts:20` |
| `forge render <slug> --target=<name> [--audience=<role>] [--specs=<csv>] [--estimate] [--verbose] [--json]` | Read `openspec/mirror/<slug>/perception.md` via `loadMirrorForRender` (audience-projected; the `@rapoport-studio/muse/mirror` subexport) plus a target-specific set of capability specs from `openspec/current/`; call Opus once with a single-shot system prompt instructing vocabulary substitution, tone match, key-screen anchoring, forbidden-pattern + out-of-scope avoidance; validate the JSON file-set response with Zod (path-traversal guarded); atomically write to `openspec/renders/<slug>/<target>/` (write-to-temp + rename swap). Full regeneration each run. v1 target: `landing` (reads `web.md`, `design-system.md`, `mirror.md`). `--specs=<csv>` overrides the relevance heuristic. `--audience=<role>` overrides the render registry's `defaultAudience` for the target. Soft cost warn at $0.50 in TTY mode. Rendered HTML carries `<meta name="rapoport-audience" content="<role>">` in `<head>`. | `cli.ts` render handler, `commands/render/orchestrator.ts` |
| `forge mirror-export <slug> [--target=<path>] [--verbose] [--json]` | Bundle `openspec/brands/<slug>/` and (when present) `openspec/renders/<slug>/` into a deterministic `.tar.gz` (gzip, portable mode, fixed mtime). Default output: `<repoRoot>/.forge/exports/<slug>-<ISO-stamp>.tar.gz`; `--target=<path>` overrides (resolved relative to cwd). Re-running with the same inputs produces a byte-identical archive. | `cli.ts` mirror-export handler, `commands/mirror-export/orchestrator.ts` |

### `forge render` — audience flag and render registry (applied 2026-05-13 from change `add-document-zone-permissions`)

`forge render` now accepts `--audience=<role>` to override the render target's default audience. Audience resolution priority:

1. `--audience=<role>` (explicit flag)
2. Render target's `defaultAudience` from the render registry (`packages/forge/src/commands/render/types.ts`)
3. Fallback: `client` (most-restrictive safe default)

Render registry defaults per target:

| Target | `defaultAudience` |
|---|---|
| `landing` | `client` |
| `prototype` | `client` |
| `brochure` | `client` |
| `dashboard` | `collaborator` |
| `internal-onepager` | `collaborator` |

The Mirror projection passed to the Claude API contains only fields visible to the effective audience (`loadMirrorForRender`). A sentinel comment `<!-- audience: <role> -->` appears in the user message for API log reviewers. Rendered HTML files carry `<meta name="rapoport-audience" content="<role>">` in `<head>`. Authoritative source: `archive/2026-05-13-add-document-zone-permissions/design.md § 5`.

### Builder mode

Surfaced under RAP-48; see `entities.md § Builder mode` for the full identity / checkpoint / lessons / failure model.

| Command | Purpose | Source |
|---|---|---|
| `forge inbox [--mine] [--all] [--limit=N] [--project=<slug>] [--json]` | List Linear issues that are candidates for autonomous build — by default `state ∈ {Backlog, Todo}` AND (`label = auto-build` OR assignee = bot). `--mine` switches to Pavel-assigned; `--all` removes the label/assignee filter. | `cli.ts` inbox handler, `commands/inbox/orchestrator.ts` |
| `forge inspect <issue-key> [--project=<slug>] [--json]` | Read a Linear issue by key, parse its body for `openspec/changes/<slug>/` references, surface a "Forge readiness" snapshot (spec exists, hard prereqs merged, identity available, programmatic API ready). Read-only diagnostic; no code changes. | `cli.ts` inspect handler, `commands/inspect/orchestrator.ts` |
| `forge plan <issue-key> [--wave=N] [--sandbox] [--sonnet] [--effort=<level>] [--max-output-tokens=N] [--no-write] [--no-comment] [--project=<slug>] [--json] [--verbose]` | Generate an execution plan against a Linear issue + linked specs + repo state snapshot + lessons. Writes plan to `~/.forge/plans/<RAP-NN>.md`, posts a Linear comment with plan summary. Read-only — no branches, no code edits. `--max-output-tokens=N` overrides the architect-call output budget (default 32768 per RAP-744 F-NEW-11); use only when the default still overflows on foundational-sweep changes. `--effort=<level>` overrides the planner role's reasoning-effort default (see § Reasoning-effort flag). | `cli.ts:534` plan handler, `commands/plan/orchestrator.ts`, prompt assembly `commands/plan/plan-builder.ts` |
| `forge build <issue-key> [--wave=N] [--wait-lock[=N]] [--force] [--auto-approve-plan] [--auto-plan] [--auto-migrations] [--auto-push] [--auto-pr] [--review-code] [--dry-run] [--use-stale-plan] [--effort=<level>] [--unsafe-host-bash] [--sandbox=<docker\|host>] [--project=<slug>] [--json] [--verbose]` | Execute an approved plan: spawn Claude Agent SDK with Builder mode prompt, walk the five programmatic checkpoints (plan ack, post-migrations, optional code review, pre-push, pre-PR), then stage + commit (`git/finalize.ts § commitWorktreeChanges` with message `forge(<rap-key>): <issue.title>`), push (`git/finalize.ts § pushBranch`), and open PR via `GitHubClient.createPullRequest`. Exits at the permanent post-PR boundary (Forge never merges). The `--auto-push` / `--auto-pr` flags drive both the checkpoint auto-pass AND the git/GitHub side effects — pre-RAP-392 they only auto-passed the prompts (memory `forge_auto_push_does_not_push`). See § `forge build` flag reference for the full per-flag table. | `cli.ts:609` build handler, `commands/build/orchestrator.ts`, `git/finalize.ts`, SDK call shape `commands/build/subprocess.ts`, gating `commands/build/checkpoints.ts` |
| `forge epic <parent-key> [--status] [--validate] [--watch] [--close ...] [--project=<slug>] [--json]` | Five modes off one command, selected by flag (see § `forge epic` modes). Bare `forge epic <key>` runs the one-shot conductor (discover → plan → dispatch → finalize). `--status` prints conductor state, `--validate` lints the epic + sub-issue bodies, `--watch` runs the merge-advance daemon, `--close` is the legacy serial close-driver. | `cli.ts:719` epic handler, `commands/epic/orchestrator.ts`, `epic/conductor.ts`, `epic/watcher.ts`, topo `commands/epic/topo-sort.ts`, merge poller `commands/epic/merge-poller.ts`, archive `commands/epic/archive.ts` |
| `forge conductor [--status] [--pause-all] [--pause=<key>] [--resume=<key>] [--poll-interval=N] [--auto-plan] [--auto-migrations] [--auto-push] [--auto-pr] [--sandbox] [--verbose] [--project=<slug>]` | Long-running daemon that polls Linear for `label=auto-build` epics, dispatches them through the same orchestrator path as `forge epic --close`, and persists per-epic state under `~/.forge/conductor/state.json`. Subcommand flags (`--status`, `--pause-all`, `--pause`, `--resume`) inspect or mutate the persisted state without entering the polling loop. `--poll-interval` is in minutes (default supplied by `forge.config`). Shipped in PR #857 (`add-forge-epic-conductor` Phase 5). | `cli.ts` conductor handler, `commands/conductor/orchestrator.ts` (`runConductorDaemon`, `runConductorStatus`, `pauseEpicInConductor`, `pauseAllInConductor`, `resumeEpicInConductor`) |
| `forge status <key> [--verbose] [--project=<slug>] [--json]` | Per-issue run summary. `--verbose` prints every artefact path Forge maintains for the run (`state.json`, plan markdown, worktree path, build dir, transcript, lessons-learned line, build-lock state) and each path's last-modified timestamp — answers "what is RAP-NN doing right now?" in one command. | `cli.ts` status handler, `commands/status/orchestrator.ts` |
| `forge cancel <RAP-NN> [--project=<slug>]` | Abort an in-flight build. Same-process path: calls `AbortController.abort()` in the in-process `build-registry.ts` registry. Cross-process path (separate terminal): writes empty sentinel at `~/.forge/state/<RAP-NN>.cancel-requested`; build orchestrator polls at every checkpoint gate and throws `CancelledByExternalSignal` on observation, routing through the same `aborted` branch. | `cli.ts` cancel handler, `commands/cancel/orchestrator.ts`, sentinel `core/build-registry.ts § cancelSentinelPath` |
| `forge resume <issue-key> [--auto-resume] [--json] [--project=<slug>]` | Resume an interrupted build from its persisted `BuildState`. Reads `~/.forge/state/<RAP-NN>.json`, detects worktree drift against the recorded HEAD, and either continues from the recorded checkpoint or aborts cleanly with a structured `ResumeResult`. `--auto-resume` skips the interactive confirmation prompt. Shipped in PR #833 (RAP-406, `forge-w2-followups`). | `cli.ts` resume handler, `commands/resume/orchestrator.ts` (`runResume`, `formatResumeText`) |
| `forge archive <slug> [--date=YYYY-MM-DD] [--commit]` | Standalone archive driver: `git mv openspec/changes/<slug>` → `openspec/archive/<date>-<slug>` (date defaults to today UTC), rewrites every `openspec/changes/<slug>` ref across tracked `*.md` files to the new path, then runs the refs guard and rolls back the `git mv` if any broken ref remains. `--commit` auto-stages and commits with `docs(archive-<slug>): archive on <date>`; without it, prints the suggested commit line. Same operation the epic conductor performs at auto-archive (`commands/epic/archive.ts`), exposed as a CLI for manual archival flows. Shipped in PR #967 (RAP-681, P1). | `cli.ts` archive handler, `commands/archive/orchestrator.ts` (`runArchive`) |

`tail` (RAP-405) is specified but not yet wired into the `cli.ts` whitelist — it will appear here when it ships.

### Sandbox flags (`forge build`) (applied 2026-05-16 from change `add-forge-docker-sandbox`)

Two flags control the execution sandbox used by `forge build`. See `spec.md § Sandbox` for the full isolation model, threat model mapping, and configuration.

#### `--unsafe-host-bash`

| Aspect | Detail |
|---|---|
| **Synopsis** | `forge build <key> --unsafe-host-bash` |
| **Default** | Off — configured provider (default: `docker`) is used |
| **Effect** | Overrides `forge.config.mjs § sandbox.provider` to `host` for this run only. Every bash command the agent issues runs directly on the host filesystem with the full host environment. No filesystem, environment, or network isolation applies. |
| **Loud-log requirement** | `[UNSAFE] Host bash provider in use for build <buildId>` is emitted to stderr on every `sandboxProvider.start()` call. Every Anthropic prompt for the build receives a top-of-prompt line `[UNSAFE] Host bash in use; sandbox disabled.` (visible in chat output). |
| **Use when** | Docker daemon is unavailable or the build cannot proceed with Docker (image-build failure, kernel panic, etc.). Document the reason in the PR description. |
| **Cross-reference** | `spec.md § Sandbox § --unsafe-host-bash escape hatch` |

#### `--sandbox=<provider>`

| Aspect | Detail |
|---|---|
| **Synopsis** | `forge build <key> --sandbox=docker` or `forge build <key> --sandbox=host` |
| **Syntax** | `=`-delimited only. The parser (`getFlag` in `cli.ts:1192`) reads `--sandbox=<value>`; a space-separated `--sandbox docker` does **not** parse — `docker` is treated as a positional and the flag resolves to `undefined`. Value must be exactly `docker` or `host` or `cli.ts:651` exits 1. |
| **Default** | Reads `forge.config.mjs § sandbox.provider` (default: `docker` once Phase 5 perf gate passes; `host` until then) |
| **Effect** | Overrides the configured provider for this run only. `--sandbox=docker` forces the Docker provider even if the config defaults to `host`. `--sandbox=host` is equivalent to `--unsafe-host-bash` but without the loud-log requirement (intended for explicit developer opt-out, not emergency bypass). |
| **Use when** | Explicit per-run provider selection — e.g. `--sandbox=docker` to test the Docker path on a build that would otherwise default to `host` during the Phase 4/5 transition. |
| **Cross-reference** | `spec.md § Sandbox § Default provider` |

### `forge build` flag reference

Every flag `cli.ts:609-673` parses for `forge build`. Flags are boolean presence-checks unless a value form is shown.

| Flag | Effect |
|---|---|
| `--wave=N` | Build the wave-`N` slice of a multi-wave plan. Positive integer; `cli.ts` rejects non-integers. |
| `--wait-lock[=N]` | Wait up to `N` seconds for the global build lock (`~/.forge/locks/build.lock`) when another build holds it; omit `N` to wait indefinitely. Without the flag a locked repo fails immediately with `BuildLockedError`. |
| `--force` | Skip the terminal-state guard. Without it, a build whose `BuildState` is in a non-`failed` terminal state (`cancelled`, `completed`) is rejected; `--force` re-runs it anyway (e.g. after a cancelled build). |
| `--auto-approve-plan` | Auto-pass the plan-acceptance checkpoint without the interactive prompt. Required for non-TTY / CI dispatch — without it the build blocks waiting on stdin. The approval half of the deprecated `--auto-plan`. |
| `--auto-plan` | **Deprecated alias.** In-code (`orchestrator.ts:130`) it means "use the cached plan only / skip auto-invoke" — passing it makes the build **fail** if no cached plan file exists, rather than auto-invoking `forge plan`. Omit the flag to get auto-invoke. Migrate to the split flags (`--auto-approve-plan` for approval). |
| `--auto-migrations` | Auto-pass the post-migrations checkpoint. |
| `--auto-push` | Auto-pass the pre-push checkpoint AND execute the real commit + push (RAP-392 — the flag now drives side effects, not just the prompt). |
| `--auto-pr` | Auto-pass the pre-PR checkpoint AND call GitHub to open the PR (RAP-392). |
| `--review-code` | Run the post-build Reviewer role (Haiku, with Sonnet escalation) and the manual code-review checkpoint. Off by default. |
| `--dry-run` | Skip the commit, push, and PR checkpoints — exercise the build without side effects. |
| `--use-stale-plan` | Skip the plan-freshness check. Use when the plan is stale only because of a cosmetic Linear-issue update (e.g. a status flip); emergency use otherwise (memory `forge_plan_staleness_on_issue_update`). |
| `--effort=<level>` | Override the builder role's reasoning-effort default. One of `low`, `medium`, `high`, `xhigh`, `max` (`VALID_EFFORT_LEVELS`, `cli.ts:24`); any other value exits 1. See § Reasoning-effort flag. |
| `--unsafe-host-bash` | Force the `host` sandbox provider for this run with a loud-log warning (see § Sandbox flags). |
| `--sandbox=<docker\|host>` | Per-run sandbox provider override; `=`-delimited only (see § Sandbox flags). |
| `--json` | Emit the `BuildResult` as JSON to stdout instead of formatted text. |
| `--verbose` | Show detailed per-checkpoint output. |

### Reasoning-effort flag

Both `forge plan` and `forge build` accept `--effort=<level>` (`cli.ts:573` and `cli.ts:641`). The level overrides the per-role reasoning-effort default resolved by `resolveEffort` in `core/anthropic.ts` — `forge plan` overrides the `planner` role default (`xhigh`), `forge build` overrides the `builder` role default (`medium`). Valid levels, lowest to highest computational budget: `low`, `medium`, `high`, `xhigh`, `max`. Any other value makes `cli.ts` exit 1 with a `--effort must be one of …` message. See `spec.md § Per-role effort` for how effort composes with model tiering.

### `forge epic` modes

`forge epic <key>` is one command with five modes, selected by flag (`cli.ts:719-1017`). The legacy `--close` mode is documented in detail under § Epic conductor below; the other four:

| Invocation | Mode | Behaviour |
|---|---|---|
| `forge epic <key>` | One-shot conductor | Bare invocation (no mode flag). Runs the full discover → plan → dispatch → finalize sequence via `epic/conductor.ts § runConductor`. Accepts `--auto-plan`, `--auto-migrations`, `--auto-push`, `--auto-pr`, `--sandbox`, `--skip-plan`, `--skip-finalize`, `--verbose`. Prints a one-line phase summary; exits 3 when any wave failed. |
| `forge epic <key> --status` | Status read | Prints the persisted conductor state from `~/.forge/epics/<key>.json` via `loadConductorStatus` / `formatConductorStatus`. Read-only. |
| `forge epic <key> --validate` | Body lint | Lints the parent epic body and every sub-issue body for missing recommended sections (`## Scope`, `## Wave`, `## Spec source`, `## Auto-merge policy`, `## OpenSpec change`). `--json` for machine output. Exits 1 when there are errors. |
| `forge epic <key> --watch` | Merge-advance daemon | Polls open PRs and advances downstream waves as each merges, via `epic/watcher.ts § watchAndAdvance`. Requires the project to have `repo` configured (PR polling). Accepts `--poll-interval-seconds=N`, `--timeout-hours=N`, `--verbose`. |
| `forge epic <key> --close [...]` | Legacy close-drive | Serial close-driver — see § Epic conductor for the full flag set. |

### Utility commands

Cross-cutting utilities that don't fit the Legacy / Builder / Conductor groupings. Both ship under `cli.ts:51-52` and are dispatched ahead of the standard config-init path (mcp-server in particular bypasses `initForge()` so it can boot without a project config).

| Command | Purpose | Source |
|---|---|---|
| `forge install-skills [--target-dir=<path>]` | Copy seed skills shipped under `packages/forge/dist/seed-skills/` into the user's `~/.forge/skills/` directory (or `--target-dir` override). Idempotent: returns `{ copied, skipped }`, where `skipped` covers entries already present at destination. RAP-553 Phase 3 (RAP-722); shipped PR #993. | `cli.ts:130`, `commands/install-skills.ts` (`runInstallSkills`) |
| `forge mcp-server` | Start a Model Context Protocol stdio server exposing six Forge tools (`build`, `plan`, `dispatch`, `status`, `archive`, `epic-close`) to MCP-aware clients (IDE integrations). Bypasses `initForge()` since the MCP server itself spawns the per-tool forge binary on each invocation. RAP-553 Phase 4 (RAP-723); shipped PR #992. | `cli.ts:82-87`, `mcp-server/index.ts` (`startMcpServer`), tool registrations under `mcp-server/tools/` |

### Epic conductor (RAP-408)

`forge epic <key> --close` gained merge-polling, GitHub auto-merge, and auto-archive capabilities in RAP-408 (shipped PR #680). This section documents the additional flags and the behavior they unlock.

#### Merge-polling flags

| Flag | Default | Effect |
|---|---|---|
| `--wait-for-merge` | off | After each sub-issue PR is opened, poll GitHub until the PR is merged (or closed / timed-out) before dispatching the next sub-issue. |
| `--merge-timeout-hours=N` | `4` | Maximum hours the poller waits for a single PR before declaring `timeout` and stopping the epic with exit 3. |
| `--merge-poll-interval-seconds=N` | `30` | How often (in seconds) the poller calls `gh pr view` to check merge state. |

`--wait-for-merge` is the prerequisite for auto-archive (see below). Without it, the epic exits after the last build PR is opened — same as pre-RAP-408 behaviour.

#### Auto-merge flag

| Flag | Default | Effect |
|---|---|---|
| `--auto-merge` | off | Enable GitHub auto-merge (`gh pr merge --auto --squash`) for sub-issues labelled `auto-merge-safe`. Implies `--wait-for-merge`. Also requires `--auto-push` and `--auto-pr` to be set (guard at `cli.ts`). |

`auto-merge-safe` is an opt-in label applied per sub-issue. When `--auto-merge` is active but a sub-issue does not carry the label, the poller still waits for the merge — it just does not queue auto-merge. The operator merges manually; the epic continues automatically once the merge lands.

When `--auto-merge` is passed without `--auto-push` or `--auto-pr`, `cli.ts` prints an error and exits 1.

#### Auto-archive

After all sub-issues are dispatched AND `--wait-for-merge` is active AND every PR has `mergeOutcome === 'merged'`, Forge automatically opens a `docs/<epic-key-lower>/archive-<slug>` PR that:

1. Parses the parent epic's Linear body for an `## OpenSpec change` section to extract `<slug>`.
2. Runs `git mv openspec/changes/<slug> openspec/archive/<YYYY-MM-DD>-<slug>`.
3. Updates all `openspec/changes/<slug>` refs in `*.md` files to `openspec/archive/<date>-<slug>`.
4. Runs `node tools/build-openspec-bundle.mjs` and commits the result.
5. Pushes the branch and opens the PR via `gh pr create`.

Auto-archive is **disabled** by `--no-auto-archive`. It is also silently skipped when the default `--wait-for-merge`-off path is used (no merges confirmed → archive gate never fires). Archive failures (dirty working tree, missing `## OpenSpec change` section, missing change folder) log a warning and exit 0 — the epic already shipped.

If `--auto-merge` was active, `runArchive` also queues auto-merge on the docs PR itself (`autoMergeDocsPr: true`).

#### Exit codes (updated)

| Code | Condition |
|---|---|
| `0` | All sub-issues dispatched + (if `--wait-for-merge`) all merged + archive opened or cleanly skipped. |
| `1` | Argument / usage error (e.g. `--auto-merge` without `--auto-push`). |
| `3` | Stopped early: first non-shipping build outcome; OR poller timeout; OR PR closed without merge; OR `getPrMergeState` fails 3 consecutive times. |

Archive failures never produce exit 3 — they populate `EpicResult.archive` with `{ kind: 'aborted' | 'skipped' }` and exit 0.

#### New types

```ts
// EpicChildResult gains:
mergeOutcome?: 'merged' | 'closed-without-merge' | 'timeout' | 'api-error';

// EpicResult gains:
archive?: ArchiveResult;  // undefined when archive not attempted

// EpicOptions gains:
waitForMerge?: boolean;
autoMerge?: boolean;
noAutoArchive?: boolean;
mergeTimeoutHours?: number;
mergePollIntervalSeconds?: number;
```

`ArchiveResult` is `{ kind: 'archived'; prUrl: string } | { kind: 'skipped'; reason: string } | { kind: 'aborted'; reason: string }` from `commands/epic/archive.ts`.

#### New source files

- `packages/forge/src/commands/epic/merge-poller.ts` — `pollUntilMerged()` state machine; dep-injected `GitHubClient` for testability.
- `packages/forge/src/commands/epic/archive.ts` — `runArchive()` performing the `git mv` + ref-update + bundle-regen + PR-open sequence.
- `packages/forge/src/core/github-client.ts` — two new methods: `enableAutoMerge(prUrl, method?)` and `getPrMergeState(prUrl)`, both shell out via the existing `runGh` wrapper.

The library-only plan prompt builders in `src/worker-prompts.ts` continue to exist alongside the `forge plan` subcommand — they're used by Cloudflare Worker runtimes that embed plan generation directly without spawning the CLI. The `commands/plan/plan-builder.ts` orchestrator and the worker-prompts library are deliberately separate surfaces sharing no prompt strings beyond the documented contract.

### Conductor control plane (applied 2026-05-18 from change `add-github-actions-control-plane`)

`forge conductor` gained a **one-shot dispatch tick** alongside the
long-running daemon. The system splits into two planes: an **execution plane**
of parallel `forge-build.yml` runners, and a **control plane** — a scheduled
workflow that decides *what* to build and *when*, off the laptop. The
conductor tick never builds in-process; it discovers and dispatches, then
exits.

| Flag | Effect |
|---|---|
| `--once` | Run a single discover → decide → dispatch tick and exit, instead of the daemon poll loop. |
| `--dispatch=github-actions` | Emit `gh workflow run forge-build.yml` per ready issue instead of building in-process. `github-actions` is the only mode (D-FCI-1 — rapoport.studio only). |
| `--max-dispatches=N` | Per-tick dispatch cap. Default `conductor.maxDispatchesPerTick` (`forge.config.mjs`, default 8). The backlog drains across successive ticks. |

One tick: lists in-progress `forge-build.yml` runs (idempotency — never
double-dispatch — plus a stuck-build watchdog: a run older than the build
timeout × 1.5 is logged `zombie_suspected` with no replacement dispatched),
discovers `auto-build` Linear issues + epic waves, applies wave gating (lowest
non-terminal wave is ready; higher waves `deferred`), and `gh workflow run`s
each ready issue up to the cap. Every considered issue emits one `TickDecision`
JSON line (`commands/conductor/decision-log.ts`). Source:
`commands/conductor/dispatch-tick.ts § runConductorTick`.

The daemon mode (`forge conductor` with no `--once`) is unchanged and remains
a fallback.

`.github/workflows/maestro-conductor.yml` runs the tick on a 30-minute cron
(`workflow_dispatch` also supported). It authenticates with `FORGE_BOT_TOKEN`
for both forge's Octokit and the `gh workflow run` dispatch — the built-in
`GITHUB_TOKEN` cannot trigger a downstream `workflow_dispatch`; the PAT must
carry the `actions` scope.

`budgets.build` (`forge.config.mjs`, RAP-1029; default $15) is the whole-build
USD cap — a backstop on the sum of all role costs, enforced in `runBuild`
after every cost event. Together with the per-tick dispatch cap it bounds
unattended spend: no single tick fans out a runaway number of builds, and no
single build burns unbounded $.

### Library surface

- `initForge(options?)` → `Forge` handle (`src/index.ts:18`) — resolves config, loads project context.
- `Forge.projectContext`, `Forge.issuePrefix`, `Forge.config` getters (`src/index.ts:31`).
- `loadConfig(options?)` (`src/config.ts:53`) — explicit > `forge.config.{mjs,js,json}` walked up from cwd > env (`FORGE_PROJECT_CONTEXT_PATH`, `FORGE_ISSUE_PREFIX`).
- `LinearClient` + `parseIssueKey` / `formatIssueKey` / `fetchIssue` / `addComment` (`src/core/linear-client.ts:15`).
- `parseRelevantSpecs`, `extractSection`, `buildSpecContext` (`src/core/spec-reader.ts`) — pure-string utilities, Worker-safe.
- Plan prompt builders: `buildPlanUserPrompt`, `buildIterateUserPrompt`, `buildImproveUserPrompt`, `buildPlanGeneratorSystem`, `buildPlanIteratorSystem`, `buildPlanImproverSystem`, `PLAN_VALIDATION_PROMPT` (`src/worker-prompts.ts`).

---

## Review orchestration

`forge review --orchestrate` fans out to specialized review-agents in parallel, each with a focused prompt and minimal context. An Opus verifier aggregates findings. The default `forge review` (no flag) keeps the single-Opus path unchanged.

### Default agent set

Six agents, in `packages/forge/src/commands/review/agents/`:

| Agent | Slug | Prompt file | Reads (in addition to diff) |
|---|---|---|---|
| Security | `security` | `prompts/security.md` | `CLAUDE.md § Hard rules #6` (secrets) |
| Accessibility | `a11y` | `prompts/a11y.md` | `design-system.md § Hard constraints` (contrast) |
| Design system fidelity | `design-system` | `prompts/design-system.md` | `openspec/current/design-system.md` |
| Architecture | `architect` | `prompts/architect.md` | `CLAUDE.md § Package boundaries` table |
| QA / test coverage | `qa` | `prompts/qa.md` | `package.json` test scripts in changed packages |
| Spec fidelity | `spec-fidelity` | `prompts/spec-fidelity.md` | All `openspec/current/*.md` files resolved from changed paths via the heuristic mapping below |

Each agent receives:

1. The diff (up to 50 surrounding lines per hunk, same as single-mode).
2. Its specific reference document(s).
3. A shared output schema (`Finding[]` — see below).
4. The project context (`projectContextPath`).

The `spec-fidelity` agent resolves which capability spec to read by mapping changed paths:

| Path glob | Spec |
|---|---|
| `apps/web/**` | `openspec/current/web.md` (plus `home.md` when `/` is touched) |
| `apps/studio/**` | `openspec/current/studio-workspace/README.md` |
| `packages/forge/**` | `openspec/current/forge/README.md` |
| `packages/ui/**` | `openspec/current/ui.md` |
| `packages/db/**` | `openspec/current/db/README.md` |
| `packages/i18n/**` | `openspec/current/i18n.md` |
| `openspec/current/**` | the file itself, plus `platform.md` for cross-cutting changes |
| anything else | skipped — agent emits empty `[]` rather than reading nothing |

### Finding schema

All agents emit JSON conforming to:

```ts
type Finding = {
  /** "critical" | "major" | "minor" | "info" */
  severity: "critical" | "major" | "minor" | "info";
  /** File path relative to repo root. */
  file: string;
  /** 1-indexed line range; single line uses startLine === endLine. */
  startLine: number;
  endLine: number;
  /** One short title (≤ 80 chars). */
  title: string;
  /** Body — Markdown allowed; should reference the spec section that supports the finding. */
  body: string;
  /** Optional fix suggestion as a code snippet or short prose. */
  suggestedFix?: string;
};
```

Output is validated by a Zod schema in `commands/review/types.ts`. Invalid responses surface as a per-agent warning, not a fatal error — the orchestrator collects what it got and continues.

### Verifier

`commands/review/orchestrator.ts` collects all agents' `Finding[]` arrays, then sends them with the original diff to an Opus verifier (`prompts/_verifier.md`). The verifier:

- Dedupes findings whose `(file, startLine, endLine)` overlaps and whose titles are semantically similar.
- Reconciles severity: when two agents disagree, the higher severity wins.
- Cites every contributing agent in the merged finding via `agentTags`.
- Returns `AggregatedReport`:

```ts
type AggregatedReport = {
  findings: (Finding & { agentTags: string[] })[];
  summary: string;        // 2–4 sentence overall verdict
  criticalCount: number;
  majorCount: number;
};
```

### CLI semantics

- `--orchestrate` with no value → all six default agents run.
- `--orchestrate=security,a11y` → only the named agents.
- `--orchestrate=*` → reserved for "all registered agents" semantics once review-side consumer-supplied agents land.
- Without `--orchestrate`, behavior is identical to single-mode review.
- `--strict` (no value) → exit 1 if any finding exists.
- `--strict=critical` → exit 1 iff `aggregated.criticalCount > 0`.
- `--estimate` → projected `inputTokens × agentCount + verifierInputTokens` summed; no API calls.
- `--pr=<number>` → resolves base/head via `gh pr view <number> --json baseRefName,headRefOid`, runs `gh pr diff <number>` for the diff body. Falls back to local working tree vs `main` when omitted.
- `--post` → after generating the report, runs `gh pr comment <number> --body-file <md>` posting the aggregated MD as a single comment. Requires `--pr=<number>` or an active PR detected via `gh pr status --json number`.

### Cost guard

Before fanout, the orchestrator computes projected cost from `agentCount × estimatedInputTokens × MODELS.opus` rates. If `> $5 USD` and stdin is a TTY (i.e. not `--json` mode), prints a confirmation prompt:

```
forge: orchestrated review will cost approximately $7.43 USD
       (6 agents × 12,400 input tokens + 8,200 verifier tokens, Opus rates).
       continue? [y/N]
```

`--json` mode skips the prompt and runs unconditionally — machine consumers handle their own gating.

### Report output

Two files in `.forge/reviews/`:

- `<timestamp>-orchestrated.json` — structured: `{ mode: "orchestrated", diff, agents: { [slug]: { findings, tokenUsage } }, aggregated, totalCost }`.
- `<timestamp>-orchestrated.md` — human-readable, layout:

```markdown
# Review — orchestrated · <branch> · <timestamp>

## Summary

<2–4 sentences from verifier>

**Critical:** N · **Major:** N · **Minor:** N · **Info:** N

## Aggregated findings

<findings sorted by severity desc, then file>

## Per-agent findings

<details><summary>Security (n findings)</summary>...</details>
<details><summary>Accessibility (n findings)</summary>...</details>
…
```

Single-mode reports keep their existing layout (`<timestamp>-single.{json,md}`); the two formats coexist under `.forge/reviews/` distinguished by suffix.

### Shared orchestration primitive

`runAgentsInParallel<T>(agents: Agent[], dispatcher: (agent: Agent) => Promise<T>)` lives at `core/orchestration.ts` and is consumed by both `commands/audit/orchestrator.ts` and `commands/review/orchestrator.ts`. Per-agent error isolation (one failed agent does not fail the whole run), partial-success handling, and token-usage aggregation are implemented once in the primitive.


## Change-folder PR merge as primary dispatch (applied 2026-05-24 from change complete-idea-to-forge-loop)

**RAP-1207 (Phase 5 of complete-idea-to-forge-loop).** Forge's primary dispatch source is now `.github/workflows/forge-on-change-merge.yml` — a push-to-main trigger with path filter `openspec/changes/**/proposal.md`. When a change-folder PR lands on main, the workflow:

1. Extracts the RAP-id from the merge commit's first-parent subject (PR-title convention per `docs/linear-agent-guidance.md`).
2. Verifies the change folder's `proposal.md` is actually in the diff (defensive vs path-filter false-positives).
3. Dispatches `forge-build.yml` with `issue=<RAP-NN>` and `triggered_by=change-folder-merge`.
4. Resolves the dispatched run URL via `gh run list` (≤10s grace for GitHub's eventual-consistency).
5. PATCHes `ideas.forge_state_url` + `forge_last_checkpoint='plan-ack'` + `forge_outcome=NULL` via Supabase REST, keyed by slug.

`forge-build.yml` gains a `triggered_by` input (`manual | conductor | change-folder-merge`) so post-hoc auditing can attribute any dispatch to its source.

The conductor's legacy Linear-label `auto-build` poll path remains shipping behind `conductor.legacyLinearPollEnabled` (default `true` for one release after RAP-1207, flipped to `false` in Phase 7 / RAP-1209). When the flag is `false`, the Linear-label backlog discovery is skipped entirely; epic wave-gating dispatch is unaffected.

**Dispatch sources:**

| Source | Workflow | Trigger | Owner |
|---|---|---|---|
| Change-folder merge | `forge-on-change-merge.yml` | push to main + path filter | RAP-1207 |
| Conductor (legacy Linear poll) | conductor dispatch-tick | `gh workflow run forge-build.yml` from `auto-build` backlog | gated by `conductor.legacyLinearPollEnabled` |
| Conductor (epic wave-gating) | conductor dispatch-tick | `gh workflow run forge-build.yml` for downstream waves after upstream merge | always active |
| Manual | Actions UI / `gh workflow run` | operator-driven | always active |

Source: `openspec/archive/2026-05-24-complete-idea-to-forge-loop/`.
