# The forge CLI

Maistik Forge ships as a single Go binary named **`forge`**. This page is the
reference for that terminal binary — every command, its real flags, and the exit
codes the **enforced gates** return. Run `forge --help` any time to print the
same synopsis the binary reports.

`forge` is one of **two front doors** to the same stateful engine. In a shell you
type `forge <command>`; inside a configured agent you type `/forge.<command>`.
This page documents the **binary**. The `/forge.*` agent slash commands are
cataloged separately.

::: tip Looking for the slash commands?
The in-agent `/forge.<command>` equivalents — the workflow spine, foundation, and
cross-cutting commands — live in [Commands](/guide/commands). This page does not
duplicate that catalog; it documents the terminal binary and its harness gates.
:::

## Invocation

Run `forge` with no arguments to launch the **interactive TUI**. Pass a command
to run it directly.

```text
$ forge --help
forge — Maistik Forge: Ecosystem, Frameworks, Workflows

USAGE
  forge                     Launch interactive TUI
  forge <command> [flags]
# exit 0
```

Two global flags apply everywhere:

| Flag | Meaning |
| --- | --- |
| `--help`, `-h` | Print the global synopsis. Every `review` subcommand also honors it. |
| `--cwd <repo>` | Target a repo other than the current directory. Most stateful commands accept it. |

Print the binary version at any time:

```sh
forge version
```

::: info Where state lives
Every stateful command reads and writes **`.forge/`** in the target repo — specs,
changes, ledgers, receipts, and timelines. Use `--cwd <repo>` to point a command
at a repo other than your shell's working directory.
:::

## Setup

Bootstrap Forge and configure the agents on this machine.

### install

`forge install` detects installed agents and writes the Forge command set into
them. It backs up existing config first and is idempotent — run it again and it
converges to the same state.

```text
USAGE
  forge install [flags]

FLAGS
  --agent, --agents <list>           Agents to install
  --component, --components <list>   Components to install
  --skill, --skills <list>           Skills to install
  --persona <name>                   Persona to apply
  --preset <name>                    Preset to apply
  --sdd-mode single|multi            SDD orchestrator mode
  --scope global|workspace           Install scope
  --channel stable|beta|nightly      Release channel; nightly is an alias for beta
  --dry-run                          Preview plan without executing
  --help, -h                         Show this help
```

```sh
forge install --scope workspace          # this project only
forge install --agents claude-code,cursor
forge install --dry-run                  # preview the plan, write nothing
```

### init

`forge init` initializes this repo's **`.forge/config.yaml`**. Run it with no
flags for the wizard, or pass any headless flag to skip it.

```text
forge init [--yes] [--preset <name>] [--agents <list>] [--scope workspace|global] [--reconfigure]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--yes` | off | Skip the wizard and initialize with detected defaults |
| `--preset <name>` | *(empty)* | Ecosystem preset to apply |
| `--agents <list>` | *(empty)* | Comma-separated agents to configure; repeatable |
| `--scope workspace\|global` | *(empty → workspace)* | Install scope |
| `--reconfigure` | off | Headless: rebuild an existing `.forge/config.yaml` from scratch instead of update-only merging |

Passing **any** of `--yes`, `--preset`, `--agents`, `--scope`, or
`--reconfigure` requests the headless path — the wizard never runs. Each flag
also falls back to an env var when unset: `FORGE_INIT_YES`,
`FORGE_INIT_PRESET`, `FORGE_INIT_AGENTS`, `FORGE_INIT_SCOPE`, and
`FORGE_INIT_RECONFIGURE`. The flag always wins over the env var.

```sh
forge init                                  # interactive wizard
forge init --yes                            # accept defaults, no prompts
forge init --agents claude-code --scope workspace
forge init --yes --reconfigure              # rebuild config.yaml from scratch
```

### config explain

`forge config explain` resolves the **effective** `.forge/config.yaml` — every
field with its resolved value and the layer it came from (built-in default,
file, or environment). A missing config file is not an error: the resolver
treats it as zero-config and falls back to the built-in defaults.

```text
forge config explain [--cwd <path>]
```

### config edit

`forge config` — with no subcommand, or spelled `forge config edit` — opens an
interactive section editor for `.forge/config.yaml`. Pick a section, walk its
fields, then choose **Save and exit**. `ctrl+c` quits without writing anything.

```text
forge config [edit] [--cwd <path>]
```

It edits the **project layer only**: just the fields you actually change are
written, so every untouched key keeps its `default` origin instead of being
baked into the file. The section menu annotates each field with its effective
value and origin layer, so a field already overridden by `config.local.yaml` or
a `FORGE_*` variable says so before you edit it.

The editor needs a terminal. In scripts and CI, use `config set` instead.

Fields whose values are free-form rather than a fixed set (`docsite.out`,
`persona.language`, `review.provider`, `workflow.phases`, `docsite.sections`,
`docsite.exclude`, `agents.enabled`, `models`, `repo.hosts`, `repo.related`)
are not in the menu — set them with `config set`, or edit the YAML directly.

### config set

`forge config set` is the headless single-key equivalent, for scripts and CI.
It writes exactly the key named and nothing else.

```text
forge config set <key> <value> [--cwd <path>]
```

```sh
forge config set persona.mode forge
forge config set workflow.breaker_threshold 5
forge config set budget.max_usd 12.50
forge config set mcp.servers context7,playwright
forge config set learn.threshold ""          # unset: fall back to the default
```

An empty value **unsets** the key, removing it from the file so the next lower
layer takes over again. Lists are comma-separated. Machine-specific keys —
`agents.enabled` — are written to `.forge/config.local.yaml` instead, merged
into whatever is already there. Both commands snapshot `.forge/` before
writing, so any edit is reversible via `forge restore`.

An unknown key is an error; run `forge config explain` to see the resolved key
list. Note that both writers round-trip the file through the schema, so
comments and out-of-schema keys in an edited file are not preserved.

### sync

`forge sync` re-syncs your **global** (home-scoped) agent configs and skills
to the current binary version — the agents it targets come from
`DiscoverAgents`, which inspects your machine, not the repo you're standing
in. Run it after upgrading the binary to bring your global managed files back
into agreement.

```sh
forge sync
```

::: warning `sync` never touches a workspace-scoped project
`sync` has no `--scope` flag — it cannot write into a repo's local `.claude/`,
`.codeium/windsurf/`, or similar directories. If your project uses
`agents.scope: workspace` (see `.forge/config.yaml`), `forge sync` is not the
command that refreshes it, no matter how recently you upgraded the binary.
See [Updating an existing project](#updating-an-existing-project) below for
the command that actually does.
:::

### uninstall

`forge uninstall` removes Forge-managed files from this machine. See
[Installation](/installation#snapshots-and-cleanup) for the snapshot and cleanup
workflow around it.

### doctor

`forge doctor` inspects your environment and reports problems. Run bare, it is
**read-only**. It is **not read-only with `--fix`** — that flag installs the
missing dependencies it finds.

```text
forge doctor [--fix] [--selftest]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--fix` | off | **Mutating.** Install the missing dependencies the check reports, then print the report |
| `--selftest` | off | Run the binary's self-test instead of the environment check and exit |

`--selftest` is checked first and short-circuits: with it present, no
environment check and no fixing happens at all. `doctor` always runs against
the process working directory — it takes no `--cwd`.

```sh
forge doctor              # read-only report
forge doctor --fix        # report AND install what is missing
forge doctor --selftest   # binary self-test only
```

::: tip Verify and maintain
`forge update` reports available updates; `forge upgrade` applies binary-only
updates. See [Installation](/installation) for the full maintenance loop.
:::

### import

`forge import` migrates an existing **Spec-Kit** (`.specify/`) or **OpenSpec**
(`openspec/`) project into the unified `.forge/` layout, non-destructively.

```text
forge import specify|openspec [--from <path>] [--cwd <dir>]
                              [--dry-run] [--force] [--include-generated]
```

| Flag | Default | Meaning |
| --- | --- | --- |
| `--from <path>` | auto-detected | Source project root |
| `--dry-run` | off | Print the plan; write nothing |
| `--force` | off | Overwrite existing `.forge/` content |
| `--include-generated` | off | Also import regenerable `site/` and `scripts/` |

Content that has no place in the `.forge/` layout is preserved losslessly under
`.forge/_imported/` rather than dropped. Regenerable `site/` and `scripts/`
directories are skipped unless `--include-generated` is passed.

### migrate

`forge migrate` brings an existing `.forge/changes/{name}/` folder created by
an **older** forge-kit version up to date with the **current** artifact
conventions (for example, the `design.md` → `plan.md` rename), so an old
change folder never becomes unusable just because the files it generated
don't match what the current version expects.

```text
forge migrate [--cwd <dir>] [--dry-run]
```

| Flag | Default | Meaning |
| --- | --- | --- |
| `--cwd <dir>` | current directory | Repo root to migrate |
| `--dry-run` | off | Print what each step would change; write nothing |

Non-destructive (never overwrites an existing canonical file), snapshotted
before any write (reversible via `forge restore`), and idempotent — safe to
run repeatedly, and safe to run against a change folder that's already
current, which reports "no changes needed".

### Updating an existing project

There is no single command that brings an already-initialized project fully
up to date with a newer forge-kit version — it's three separate steps, each
owning a different piece of `.forge/` state. Run them from inside the
project's repo root, in this order:

```sh
forge migrate --dry-run          # 1. preview which change folders need updating
forge migrate                    # 2. apply — snapshotted, reversible via `forge restore`

forge skill-registry refresh     # 3. rebuild .forge/skill-registry.md

forge install --scope workspace --agents <your-agents>   # 4. refresh the installed
                                                          #    skill/command files
```

Two gotchas worth knowing before you run this:

- **`forge install` takes no `--cwd`** — unlike `migrate` and `skill-registry`,
  you must actually be `cd`'d into the target repo. `--scope workspace` writes
  into that repo's local agent directories (`.claude/`, `.codeium/windsurf/`,
  etc.); the default `--scope global` writes into your home directory instead
  — pass `--scope workspace` explicitly, don't rely on a project default.
- **`forge sync` is not part of this sequence.** It only refreshes your
  global, home-scoped configs (see the [warning above](#sync)) — it has no way
  to target a specific project's workspace-scoped install.

If you skip straight to `forge install --scope workspace` without first
running `forge migrate`, an old change folder using a stale filename (e.g. a
pre-rename `design.md`) stays exactly as stale as it was — `install` only
refreshes the agent-side skill/command files, it never touches `.forge/changes/`
content.

## Workflow & status

Drive and inspect the SDD lifecycle from the shell. These commands are safe to
run at any time — the status readers are read-only.

| Command | Signature | What it does |
| --- | --- | --- |
| `sdd-status` | `sdd-status [change]` | Print native SDD phase status for orchestrators. Read-only. |
| `sdd-continue` | `sdd-continue [change]` | Print native SDD dispatcher routing output. |
| `status` | `status [change] [--cwd <repo>] [--json]` | Colorized, single-shot snapshot of a change's progress for a human. Read-only. |
| `analyze` | `analyze [<spec-path>] [--portfolio] [--save] [--json] [--cwd <repo>]` | Reproducible 100-point spec quality score. Read-only unless `--save`. |
| `health` | `health [--save] [--cwd <repo>]` | Portfolio-wide risk dashboard: every active change's phase progress, verify state, and a deterministic HIGH/MEDIUM/LOW/UNKNOWN risk classification, plus a prioritized action list. Read-only unless `--save`. Distinct from `analyze` — see [Spec quality](/features/quality). |
| `archive` | `archive [change] [--cwd <repo>]` | Promote a completed change — merge its delta specs into `.forge/specs/` and move it to `.forge/changes/archive/`, once every task in `tasks.md` is Done. |

`analyze` scores a single spec when you pass a `<spec-path>`, or the whole
`.forge/specs` portfolio when you pass none. Add `--json` for stable
machine-readable output and `--save` to persist the score.

```text
$ forge analyze
No specs found under .forge/specs or .forge/changes; nothing to analyze.
# exit 0
```

`sdd-status` prints the phase rollup orchestrators route on:

```text
$ forge sdd-status
## SDD Status: continuous-learning-loop

next: spec

### Summary
- apply: blocked
- verify: blocked
- tasks: 51/51 complete
# exit 0
```

### Migration

`forge import` migrates an existing **Spec-Kit** (`.specify`) or **OpenSpec**
(`openspec`) project into the unified `.forge/` layout, non-destructively.

```text
forge import specify|openspec [--from <path>] [--dry-run] [--force] [--include-generated]
```

Regenerable `site/` and `scripts/` are skipped unless you pass
`--include-generated`. See [Migration](/features/migration) for the full flow.

`forge migrate` separately brings an existing `.forge/changes/{name}/` folder
created by an older forge-kit version up to date with the current artifact
conventions:

```text
forge migrate [--cwd <dir>] [--dry-run]
```

It is non-destructive, snapshotted (reversible via `forge restore`), and
idempotent — safe to run repeatedly, including against a change folder that's
already current.

### board

`forge board` derives the phase-status board from the active changes — one row
per change, one column per SDD phase — and prints it as markdown. Read-only
unless you pass `--write`.

```text
forge board [--cwd <repo>] [--write] [--output <path>]
```

| Flag | Effect |
| --- | --- |
| `--cwd <repo>` | Repo root (defaults to the current directory) |
| `--write` | Also save the board to `.forge/board.md` |
| `--output <path>` | Write to `<path>` instead of the default `.forge/board.md` (implies `--write`'s intent; only takes effect with `--write`) |

### roadmap

**Deprecated** — use [`forge board`](#board) instead. `forge roadmap` still
works and produces the same output, but prints a deprecation notice to
stderr and, for backward compatibility, defaults `--write` to
`.forge/roadmap.md` instead of `.forge/board.md`.

```text
forge roadmap [--cwd <repo>] [--write] [--output <path>]
```

| Flag | Effect |
| --- | --- |
| `--cwd <repo>` | Repo root (defaults to the current directory) |
| `--write` | Also save the board to `.forge/roadmap.md` (legacy path) |
| `--output <path>` | Write to `<path>` instead of the default `.forge/roadmap.md` |

### wireframe

`forge wireframe` reads a spec, decides whether it describes a UI, and — when it
does — embeds a managed **`## Wireframe`** section (ASCII + Mermaid) into that
spec. The injection is replace-or-append-once, so re-running is idempotent.

```text
forge wireframe <spec-path> [--ui | --no-ui] [--cwd <repo>]
```

The UI decision is a keyword heuristic; override it explicitly with `--ui` or
`--no-ui`.

### drift

`forge drift` classifies every spec-code cross-reference into **Ghost**
(the spec points at code that is gone), **Phantom** (code claims a spec that
does not exist), and **Mutated** (both sides exist but disagree). It is
**read-only** — the classifier writes nothing and neither does the command.

```text
forge drift [--cwd <dir>]
```

`scan` is the default subcommand — `forge drift` and `forge drift scan` are the
same command. `waive` accepts a finding by fingerprint; `reset` clears the
waiver ledger.

| Flag | Default | Meaning |
| --- | --- | --- |
| `--cwd <dir>` | current directory | Repository root to classify |
| `--change <c>` | active change | Scope the waiver ledger to one change |
| `--enforce` | **off** | Turn the report into a gate — see exit codes below |
| `--fail-on <csv>` | `mutated,phantom` | Which categories `--enforce` treats as blocking |
| `--fingerprint <fp>` | *(none)* | The finding `waive` accepts |

By default `drift` **reports and exits `0`**, even with discrepancies. It only
gates when you opt in with `--enforce`, which returns non-zero if any unwaived
**Certain** finding falls into a `--fail-on` category. `Ghost` is excluded from
the default `--fail-on` set, so a spec pointing at deleted code does not block
until you ask it to.

```text
$ forge drift
No drift detected: every spec-code cross-reference resolved cleanly.
# exit 0
```

```text
$ forge drift
Drift: 2 discrepancy(ies)

Ghost (1):
  - .forge/specs/checkout.md :: internal/checkout/total.go -- referenced file no longer exists

Phantom (1):
  - .forge/specs/(none) :: internal/billing/retry.go -- code references a spec that is not present

Mutated (0):
  (none)
# exit 0
```

### archive

`forge archive` promotes a completed change: it merges the change's delta specs
into canonical `.forge/specs/` and moves the change into the dated, immutable
`.forge/changes/archive/YYYY-MM-DD-<name>/` trail.

```text
forge archive [change] [--cwd <dir>]
```

Archive is **gated on honest task status**: it refuses while any task in
`tasks.md` is not `Done`. That refusal is the point — a successful archive is a
real signal that the change is finished, not a formality you can run early.

### analyze

`forge analyze` prints the reproducible **100-point spec quality score**. With a
`<spec-path>` it scores one spec; with none (or with `--portfolio`) it scores the
whole `.forge/specs` portfolio.

```text
forge analyze [<spec-path>] [--portfolio] [--save] [--json] [--cwd <dir>]
```

| Flag | Default | Meaning |
| --- | --- | --- |
| `--portfolio` | off | Force portfolio rollup even when a path is given |
| `--save` | off | Persist the score; **the only writing mode** |
| `--json` | off | Stable machine-readable output |

The score is a pure function of the spec text — same input, same score, every
run. A constitution conflict caps the total regardless of the other dimensions.
Read-only unless `--save`. See [Spec quality](/features/quality).

### health

`forge health` is a distinct, portfolio-wide command with its own engine
(`internal/health`) — not an alias of `analyze`. Where `analyze` scores ONE
change's spec/plan/tasks for cross-artifact consistency, `health` classifies
EVERY active change's current risk (`HIGH`/`MEDIUM`/`LOW`/`UNKNOWN`) from
structured verify, task-blocking, circuit-breaker, and review-gate state, and
prints a prioritized "what needs attention" list.

```text
forge health [--save] [--cwd <dir>]
```

| Flag | Default | Meaning |
| --- | --- | --- |
| `--save` | off | Write the rendered report to `.forge/memory/health-report.md` |

Read-only unless `--save`. It reports risk and state, not dates or ETAs —
forge stores no per-phase timestamps. `/forge.health` is its agent-side slash
command.

### estimate

`forge estimate` is a distinct command with its own engine
(`internal/execgraph`) — not an alias of `tasks`. It idempotently refreshes a
change's `tasks.md` **`## Execution Graph`** JSON block (waves, blockers,
critical path, per-task hour estimates, summary) by re-parsing the CURRENT
checklist and recomputing the graph from scratch, mechanically applying the
same derivation rules `sdd-tasks` uses. It never rewrites task descriptions,
tiers, or `[P]` markers — only the derived JSON block. Running it twice in a
row with no checklist changes produces byte-identical output.

```text
forge estimate [--change <c>] [--cwd <dir>]
```

| Flag | Default | Meaning |
| --- | --- | --- |
| `--change <c>` | active change | Which change's `tasks.md` to refresh |
| `--cwd <dir>` | current directory | Repository root |

Use this after hand-editing a checklist (bumping a tier, adding/removing a
`[P]` marker, adding a task) so `forge.roadmap` and `forge.swarm` schedule
work from an up-to-date graph instead of a stale one. `/forge.estimate` is
its agent-side slash command.

### reqid

`forge reqid check` is an **advisory** scan of requirement IDs across every
spec: it flags malformed IDs, duplicates, and IDs whose area prefix does not
match the spec domain they live in.

```text
forge reqid check [--cwd <dir>]
```

It **always exits `0`**. It reports and never rewrites an ID, because renumbering
requirements silently would break every traceability reference pointing at them.

### sdd-status

`forge sdd-status` prints the native SDD phase status for a change, in the shape
orchestrators consume.

```text
forge sdd-status [change]
```

This is the deterministic Go implementation of what an agent would otherwise
have to infer by reading `.forge/changes/<c>/` itself.

### sdd-continue

`forge sdd-continue` prints the native SDD dispatcher routing output — which
phase comes next for a change, and what should run it.

```text
forge sdd-continue [change]
```

Like `sdd-status`, it is a read-only projection of change state: it decides
nothing on its own and writes nothing.

### status

`forge status` prints a colorized, single-shot snapshot of one change's
progress for a human glancing at a terminal: a phase checklist (done/active/
pending), task completion, budget spend, review/remediation gates, and a
verify circuit-breaker warning when failures are accumulating. It resolves
state once and exits — no live refresh, no `--watch`.

```text
forge status [change] [--cwd <repo>] [--json]
```

```text
$ forge status api-rate-limiting
Change: api-rate-limiting
========================
✓ propose
✓ spec
✓ design
✓ tasks
⠇ apply
○ verify
○ archive

Tasks
14/18 complete

Budget
$2.41 / $10.00
# exit 0
```

Colors auto-degrade for a non-tty or `NO_COLOR` output. `--json` emits the
same underlying `sdd-status` fields plus the budget ledger/limits and
circuit-breaker state, which live in their own per-change files and are not
part of `sdd-status`'s own schema.

## Harness gates

These are the terminal-facing controls of the harness. The **enforced gates**
return a **non-zero exit** to block a runaway loop; a host hook or CI step reads
that exit and stops. The four enforced controls are **verify** (circuit-breaker),
**budget** (spend ceiling), **secscan**, and the **review** delivery gates.

### verify

`forge verify` runs the configured tests and lint plus spec scenario coverage.
With `--change`, it arms a **circuit-breaker**: a single `FAIL` still exits `0`
but increments the breaker; once **N consecutive failures** accumulate
(`--max-failures`, else `workflow.breaker_threshold` in config, else 3), it
returns non-zero so the implement loop stops and escalates instead of retrying
forever.

```text
verify [--change <c>] [--max-failures N] [--reset-breaker] [--isolated] [--cwd <repo>]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--change <c>` | *(none)* | Opt into circuit-breaker tracking for that change |
| `--max-failures N` | *(from `workflow.breaker_threshold` in config, else `3`)* | Override the consecutive-failure threshold for this invocation |
| `--reset-breaker` | off | Clear the breaker for `--change` instead of running verification |
| `--isolated` | **off** | Run the configured commands against an ephemeral copy of the working tree instead of the working tree itself |
| `--cwd <repo>` | current dir | Repo root |

```text
$ forge verify --change c1
Verdict: FAIL
Breaker: 1/3 consecutive failures for change "c1" — fix the smallest cause and re-run.
# exit 0

$ forge verify --change c1     # third consecutive failure
Verdict: FAIL
Breaker: CIRCUIT OPEN — 3 consecutive verify failures for change "c1".
STOP looping and escalate to a human.
# exit 1
```

Clear an open breaker after a human intervenes:

```sh
forge verify --reset-breaker --change c1
```

See [the circuit-breaker](/harness/circuit-breaker) for the full state model.

### budget

`forge budget` enforces a per-change **spend ceiling**. `record` accumulates
reported spend and returns non-zero the moment a ceiling is **strictly**
crossed; `check` gives the same verdict read-only; `reset` clears the ledger.
Each ceiling comes from its `--max-*` flag, else from the matching `budget.*`
key in `.forge/config.yaml`.

```text
budget record|check|reset --change <c> [--tokens N] [--usd X] [--tool-calls K]
                          [--max-tokens N] [--max-usd X] [--max-tool-calls K] [--cwd <repo>]
```

The **delta** flags report spend; the **`--max-*`** flags set the ceiling
compared against the accumulated ledger. Both parse into the same struct, so
either group is accepted on any subcommand — only `record` applies the deltas.

| Flag | Default | Effect |
| --- | --- | --- |
| `--tokens N` | `0` | Tokens to add to the ledger |
| `--usd X` | `0` | Dollars to add to the ledger (accepts a decimal) |
| `--tool-calls K` | `0` | Tool calls to add to the ledger |
| `--max-tokens N` | *(from `budget.max_tokens` in config, else no ceiling)* | Token ceiling |
| `--max-usd X` | *(from `budget.max_usd` in config, else no ceiling)* | Dollar ceiling (accepts a decimal) |
| `--max-tool-calls K` | *(from `budget.max_tool_calls` in config, else no ceiling)* | Tool-call ceiling |

```text
$ forge budget record --change checkout --tokens 600 --max-tokens 1000
Budget[checkout]: tokens=600 usd=0.00 tool_calls=0 (records=1)
Budget: within limits
# exit 0

$ forge budget record --change checkout --tokens 600 --max-tokens 1000
Budget[checkout]: tokens=1200 usd=0.00 tool_calls=0 (records=2)
BUDGET EXCEEDED — tokens 1200/1000. STOP and escalate to a human.
# exit 1
```

::: warning No ceiling means no blocking
With no `--max-*` flag **and** no matching `budget.*` key in config, the ledger
accumulates spend and never blocks. Reaching a limit *exactly* is within
budget — only strictly going over returns non-zero.
:::

See [Spend Budget](/harness/budget) for the ledger format and the honest limits
of token accounting.

### secscan

`forge secscan` is an **enforced static gate**. `scan` inspects files for leaked
secrets, PII, and prompt-injection strings and returns non-zero on any **unwaived**
finding; `waive` accepts a fingerprint; `reset` deletes the change's **waivers**.

::: warning `reset` clears waivers, not findings
It removes `.forge/changes/<change>/secscan.yaml`, so every previously waived
finding **blocks again** on the next scan. It makes the gate stricter, not
looser.
:::

```text
secscan scan|waive|reset --change <c> [--path P ...] [--min-severity high|medium] [--fingerprint FP] [--cwd <repo>]
```

`--change` is required by all three subcommands. `--path` defaults to `.` and
repeats; `--min-severity` defaults to `high`; `--fingerprint` is required for
`waive`.

```text
$ forge secscan scan --change checkout --path leak.txt
secscan[checkout] high secret leak.txt:1 fp=0c9ab6bbabac aws-access-key AKIA…
secscan[checkout] high secret leak.txt:2 fp=657437b6dcc8 github-pat ghp_…
SECSCAN BLOCKED — 2 unwaived finding(s). STOP and escalate to a human; a human waives with: forge secscan waive --change checkout --fingerprint <fp>, or resets with: forge secscan reset --change checkout
# stderr: Error: secscan blocked for change "checkout": 2 unwaived finding(s)
# exit 1
```

Waive a reviewed finding by its **fingerprint**, then re-run to pass:

```sh
forge secscan waive --change checkout --fingerprint 0c9ab6bbabac
```

See [Secret & PII Scan](/harness/secscan) for the waiver workflow and the
scanner's limits.

### clarify-gate

`forge clarify-gate` is the **Sensor** half of forced interactive
clarification — not one of the four enforced gates above. It reads two
persisted marker scopes (zero model judgment) and warns on any unresolved
clarifying question, but by design never hard-blocks: it deliberately never
returns Claude Code's `PreToolUse`-only exit `2`, and lets the session
continue either way.

```text
clarify-gate [--cwd <repo>] [--session-id <id>] [--change <name>] [--format text|json]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--cwd <repo>` | current dir | Repo root |
| `--session-id <id>` | *(none)* | Per-session marker scope; an empty, missing, or path-like id silently degrades to change-scope-only |
| `--change <name>` | *(active change)* | Per-change marker scope; explicit flag wins, else the single active change under `.forge/changes/` |
| `--format text\|json` | `text` | Output shape |

```text
$ forge clarify-gate --cwd . --change checkout
clarify-gate: clean (no unresolved clarifying questions)
# exit 0

$ forge clarify-gate --cwd . --change checkout
CLARIFY GATE — unresolved clarifying question(s) found. This is a warning, not a hard block; the session continues.
  [change:checkout] 1 unresolved:
    - q1: Which auth model?
A human answers via the interactive tool, or waives with: forge clarify waive --id <qid> [--change <c>|--session <id>]
# exit 1
```

See [Interactive Clarification](/harness/clarify-gate) for the marker
lifecycle, the Claude Code `Stop`-hook and OpenCode `session.idle` wiring,
and the honest gap across the other ten supported harnesses.

### clarify

`forge clarify open|answer|waive|list` is the **write group** behind the
marker `clarify-gate` reads: `open` records a new unresolved question,
`answer` and `waive` are the two ways to clear one, and `list` prints what a
scope still has unresolved.

```text
clarify open|answer|waive|list --id <qid>
      [--question <text> --asked-in <skill-or-command>]
      [--tool AskUserQuestion|question --answer-digest <12hex>]
      [--reason <text>]
      [--change <c> | --session <id>] [--cwd <repo>]
```

| Flag | Applies to | Effect |
| --- | --- | --- |
| `--id <qid>` | all | The caller-supplied, stable question id. **Required** |
| `--question <text>` | `open` | The question text. **Required** |
| `--asked-in <name>` | `open` | The skill/command that asked. **Required** |
| `--tool AskUserQuestion\|question` | `answer` | The recognised interactive tool the answer came through. **Required** |
| `--answer-digest <12hex>` | `answer` | 12 lowercase hex chars — a sha256 fingerprint of the answer text, same discipline as `secscan`. **Required** |
| `--reason <text>` | `waive` | Optional note; echoed to stdout only, not persisted |
| `--change <c>` \| `--session <id>` | all | Mutually exclusive scope selector; neither given falls back to the single active change |
| `--cwd <repo>` | all | Repo root |

```sh
forge clarify open   --id q1 --question "Which auth model?" --asked-in sdd-propose --change checkout
forge clarify answer --id q1 --tool AskUserQuestion --answer-digest a1b2c3d4e5f6 --change checkout
forge clarify waive  --id q1 --reason "confirmed in standup" --change checkout
forge clarify list --change checkout
```

::: warning `answer` is agent-runnable and best-effort
`answer` requires a recognised tool name and a well-formed digest, but forge
cannot verify the agent didn't invent either. `waive` is the only command
that always sources `waived_by` from the resolved git/environment identity
(`identity.Resolve`), never from caller-supplied text — mirroring
`secscan`'s "a human waives, never the agent" discipline. See
[Interactive Clarification](/harness/clarify-gate#the-trust-gap-answer-is-best-effort)
for the full trust-gap caveat.
:::

### trace

`forge trace` is the per-change **session event timeline**. The verify breaker and
budget gates emit into it automatically; you can also `record` your own events
and `show` the log.

```text
trace record|show --change <c> [--kind <k>] [--summary <s>] [--actor <a>] [--field k=v] [--cwd <repo>]
```

| Flag | Effect |
| --- | --- |
| `--kind <k>` | Event kind |
| `--summary <s>` | One-line event summary |
| `--actor <a>` | Who produced the event — attributes the entry to an agent, a human, or a gate |
| `--field k=v` | Structured field; repeatable. Rejects a value with no `=` |

`record` and `show` are the only subcommands.

```text
$ forge trace show --change checkout
[1] 2026-07-18T21:17:28+02:00 budget — budget +600t/0tc (exceeded=false, tokens=600)
[2] 2026-07-18T21:17:28+02:00 budget — budget +600t/0tc (exceeded=true, tokens=1200)
# exit 0
```

See [the session timeline](/harness/timeline) for how gates attribute events.

### review

`forge review` is the bounded review lifecycle. It produces a **content-bound
receipt**: `start` opens a review against the current scope, `finalize` records
lens results and evidence, and `validate` checks the receipt against a delivery
gate. Repository scope, authority, and canonical artifacts are derived by the
binary.

```text
review start    [--cwd <repo>] [--focus <risk|resilience|readability|reliability>]
                [--lineage <id>] [--policy <file>] [--trace <path>]
review finalize [--cwd <repo>] [--result <review.json> ...] [--evidence <path>]
                [--validation <file>] [--refuter <file>] [--correction-lines N]
                [--failed] [--lineage <id>] [--trace <path>]
review validate --gate <gate> [--cwd <repo>] [--lineage <id>] [--base-ref <ref>]
                [--pre-pr-ci-attestation <file>] [--policy <file>]
                [--release-configuration <file>] [--release-generated <file>]
                [--release-provenance <file>] [--release-publication-boundary <file>]
                [--release-evidence-freshness <file>]
```

**`review start`** freezes live Git scope and derives the tier, lenses, and
correction budget.

| Flag | Default | Effect |
| --- | --- | --- |
| `--cwd <repo>` | `.` | Repository path |
| `--focus <lens>` | `reliability` | Dominant standard-risk focus: `risk`, `resilience`, `readability`, or `reliability` |
| `--lineage <id>` | *(derived)* | Explicit review lineage identifier; defaults to `review-` plus the snapshot identity prefix |
| `--policy <file>` | *(native)* | Custom review policy file; the native bounded policy is used otherwise |
| `--trace <path>` | *(none)* | Diagnostic operation-metadata trace path |

**`review finalize`** canonicalizes reviewer output and evidence, performs the
required native transitions, and materializes the terminal receipt.

| Flag | Default | Effect |
| --- | --- | --- |
| `--result <file>` | *(none)* | Reviewer result JSON, or `-` for stdin. **Repeatable**, in selected-lens order |
| `--evidence <file>` | *(none)* | Final test/verification evidence, or `-` for stdin |
| `--validation <file>` | *(none)* | Targeted correction-validation JSON, or `-` for stdin |
| `--refuter <file>` | *(none)* | Refuter outcomes JSON, or `-` for stdin |
| `--correction-lines N` | `0` | Positive predicted correction changed-lines forecast, supplied **before** editing |
| `--failed` | off | Bind the supplied final evidence as a *failed* verification |
| `--lineage <id>` | *(discovered)* | Lineage override when discovery is ambiguous |
| `--trace <path>` | *(none)* | Diagnostic operation-metadata trace path |

Only **one** of the file inputs may read from stdin per invocation — passing `-`
to more than one is an error.

**`review validate`** auto-discovers the authoritative state and receipt, then
checks them against live Git. `--gate` is **required**.

| Flag | Default | Effect |
| --- | --- | --- |
| `--gate <gate>` | *(required)* | `post-apply`, `pre-commit`, `pre-push`, `pre-pr`, or `release` |
| `--cwd <repo>` | `.` | Repository path |
| `--lineage <id>` | *(discovered)* | Lineage override when discovery is ambiguous |
| `--base-ref <ref>` | *(none)* | Expected remote publication base, for `pre-pr` |
| `--pre-pr-ci-attestation <file>` | *(none)* | Signed exact-merged-tree CI attestation for a compatible base advance |
| `--policy <file>` | *(native)* | Explicit custom policy carrying compatible-base CI trust |
| `--release-configuration <file>` | *(none)* | Release configuration artifact |
| `--release-generated <file>` | *(none)* | Generated-artifact manifest |
| `--release-provenance <file>` | *(none)* | Release provenance artifact |
| `--release-publication-boundary <file>` | *(none)* | Sealed publication-boundary artifact |
| `--release-evidence-freshness <file>` | *(none)* | Current release-evidence freshness artifact |

```sh
forge review start --focus risk
forge review finalize --result review.json --evidence evidence.txt
forge review validate --gate pre-commit
```

The delivery gates (`pre-commit`, `pre-push`, and the rest) **validate** the
existing receipt against the staged content and return non-zero when the tree no
longer matches — they never open a new review. See [Review](/features/review) for
the lifecycle and lens selection.

#### Compatibility commands

Six top-level `review-*` commands remain as **legacy v1 compatibility
surfaces**. They are not the modern path and are documented here only so an
existing script that calls them is recognizable. `review-start` and
`review-step` are **read-only**: they reject mutation and refuse to create new
authority. Reach for the `review start` / `review finalize` / `review validate`
subcommands above instead.

| Legacy command | Status | Modern equivalent |
| --- | --- | --- |
| `review-start` | Read-only; creates no authority | `forge review start` |
| `review-step` | Read-only; mutation rejected | `forge review finalize` |
| `review-resume` | Re-emits current authority without consuming budget | `forge review finalize` (re-run; native discovery resumes) |
| `review-validate` | Receipt validation via `--request` or native authority flags | `forge review validate --gate <gate>` |
| `review-bundle-export` | Exports compact authority or a read-only legacy v1 chain | *(no modern replacement; still the export path)* |
| `review-bundle-import` | Recovers compact authority or installs a read-only legacy v1 chain | *(no modern replacement; still the import path)* |

::: warning Legacy authority is read-only
A lineage that already carries a legacy v1 chain is refused by `review start` —
choose a new lineage for compact authority. Mixing the two stores makes review
authority ambiguous and `review validate` fails closed rather than guessing.
:::

### run

`forge run` is the **bounded autonomous loop**. It drives a user-supplied
executor through repeated iterations and stops the moment an enforced harness
gate opens.

::: danger Forge has no model
`forge run` cannot write code. It **orchestrates** an executor you name with
`--cmd` — typically a coding-agent CLI invocation — and enforces the gates
between iterations. Without `--cmd` the command refuses to run at all.
:::

```text
run [--change <c>] --cmd <shell command> [--max-iterations N]
    [--max-tokens N] [--max-tool-calls K] [--dry-run] [--yes] [--cwd <dir>]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--cmd <command>` | *(required)* | The shell command that performs **one** iteration. Run via `sh -c` in the repo root |
| `--change <c>` | *(active change)* | The change the gates are evaluated against |
| `--max-iterations N` | **`5`** | Iteration ceiling. Must be between `1` and **`50`** — a value outside that range is a parse error |
| `--max-tokens N` | *(from `budget.max_tokens` in config, else no ceiling)* | Token ceiling for the spend-budget gate |
| `--max-tool-calls K` | *(from `budget.max_tool_calls` in config, else no ceiling)* | Tool-call ceiling for the spend-budget gate |
| `--yes` | off | **Actually execute.** Without it nothing runs |
| `--dry-run` | — | Explicit form of the default posture |
| `--cwd <dir>` | current dir | Repo root |

**Dry run is the default.** Bare `forge run --cmd '…'` prints the plan and
executes nothing; `--yes` is what turns it into a real run. The `--max-iterations`
ceiling of 50 is deliberate and cannot be raised at the command line — a run
that would need more iterations needs a human, not a bigger number.

The three gates — the **verify circuit-breaker**, the **spend budget**, and
**secscan** — are all read-only here: the loop reads persisted state and never
records new gate state. The budget gate takes each ceiling from its flag, else
from the matching `budget.*` key in `.forge/config.yaml`; with none configured
either way it has no ceiling and can never open. `budget.max_usd` has no `run`
flag, so config is the only way to enforce a dollar ceiling in the loop. Secscan uses the same scope and
threshold as `forge secscan scan`: the whole working tree, blocking on high
severity.

Gates are checked **before the first iteration and again after each one**. Three
consequences follow, and all of them are deliberate:

- A gate that is **already open** refuses to start — the loop exits with **zero**
  executor runs.
- A gate that opens **mid-run** halts the loop immediately and escalates.
- **Exhausting the iteration cap is not success.** Any outcome other than the
  executor reporting completion returns a non-zero exit so a human is pulled in.

A non-zero exit from your `--cmd` is also an error: that iteration failed, so the
loop stops rather than looping over a broken executor.

```text
$ forge run --change checkout --cmd 'claude -p "implement the next open task"'
Plan: up to 5 iteration(s) on change "checkout"
  executor: sh -c "claude -p \"implement the next open task\""
  gates:    verify circuit-breaker, spend budget, secscan
  stop:     any gate opening halts the loop and escalates to a human

dry run: nothing executed (pass --yes to run)
# exit 0
```

```text
$ forge run --change checkout --cmd './agent.sh' --max-iterations 3 --yes
Plan: up to 3 iteration(s) on change "checkout"
  executor: sh -c "./agent.sh"
  gates:    verify circuit-breaker, spend budget, secscan
  stop:     any gate opening halts the loop and escalates to a human

run: STOPPED after 0 iteration(s) — gate open
  gate:   verify circuit-breaker
  detail: circuit breaker open after 3 consecutive verify failures
escalate: a human should review before continuing
# exit 1
```

See [the autonomous run](/harness/autonomous-run) for the loop's full stop
model.

### mcp

`forge mcp serve` exposes the harness as MCP tools over **stdio**, so an agent
can query forge directly instead of only through shell hooks.

```text
forge mcp serve [--cwd <dir>]
```

The `serve` subcommand is **required** — there is no default, because starting a
stdio JSON-RPC session is never something a caller should do by accident. Like
`hook`, it stays silent on stdout apart from the protocol: no system detection,
no self-update chatter, either of which would corrupt the session.

::: warning MCP is queryable, not enforcing
Exposing the gates over MCP makes the harness **queryable**, not enforcing. The
enforcing paths stay exactly what they were: the non-zero exits consumed by
hooks, pre-commit, and CI. An agent can *ask* the harness for a verdict over MCP
and read the answer as data; it cannot be *compelled* by MCP alone, because
nothing on this path blocks anything. A refused gate arrives here as a tool
result, not as a stop.
:::

Each tool handler invokes the same `Run*` function the shell command uses, with
`--cwd` pinned to the resolved root, and returns its captured stdout. A gate's
refusal surfaces as an `isError` result — readable data, not a transport
failure. See [Harness as MCP](/harness/mcp).

### plugin

`forge plugin` assembles forge's skills, subagent templates, and MCP/hook
wiring into a portable **VS Code Agent Plugin** bundle — the schema at
`https://agent-plugins.org/schemas/1.0.0/plugin.schema.json` that lets a
third-party tool ship skills, MCP servers, custom agents, and lifecycle hooks
as one installable unit.

```text
forge plugin build [--out <dir>] [--cwd <dir>]
forge plugin install [--out <dir>]
```

| Subcommand | Effect |
| --- | --- |
| `build` | Write the bundle to `<dir>` (default `.forge/vscode-plugin/`, relative to `--cwd`) for manual **"Chat: Install Plugin From Source"** use |
| `install` | Write the bundle straight to VS Code Copilot's auto-discovery location (default `~/.copilot/installed-plugins/forge/`) and register it in VS Code's `settings.json` under `chat.pluginLocations` — no manual VS Code command needed |

Both subcommands are idempotent: rerunning either never duplicates or
corrupts existing content. `install` merges into `settings.json` rather than
overwriting it, so any other settings you already have stay intact.

::: warning Auto-discovery path and settings key are unverified
The `~/.copilot/installed-plugins/<name>/` auto-discovery directory and the
`chat.pluginLocations` setting are implemented as specified, but neither is
confirmed against primary VS Code or Agent Plugins documentation (the
reachable docs at build time didn't name either). Treat `install` as
best-effort until independently verified; `build` plus VS Code's own "Chat:
Install Plugin From Source" command is the documented, schema-verified path.
:::

See [VS Code Agent Plugin](/features/vscode-plugin) for what gets bundled,
how to verify an install, and the full set of schema-uncertainty caveats.

### metrics

`forge metrics` answers "is my harness actually working?" by rolling up the
per-change state forge already records — session timeline, verify
circuit-breaker, runtime budget, secscan waivers — into one report. It is
**read-only**: it aggregates and prints, and never writes.

```text
forge metrics [--json] [--cwd <dir>]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--json` | off | Emit the whole report as indented JSON (stable machine output) |
| `--cwd <dir>` | current dir | Repo root |

```text
$ forge metrics
Harness metrics (3 change(s): 1 active, 2 archived)

Verify      no runs recorded
# exit 0
```

With no recorded runs the report says so rather than printing "0% pass", which
would read as failure rather than as absence of data. See
[Harness metrics](/harness/metrics).

### hook

`forge hook` is the **live-agent feeder**. Your coding host calls it after each
tool use and passes the hook payload on **stdin**, so real activity lands in the
timeline and the budget ledger.

```text
forge hook post-tool-use        # payload on stdin
```

`post-tool-use` is the supported event. The command is deliberately fast and
silent and never triggers system detection or self-update — it is best-effort
plumbing, not a gate. See [Connect a live agent](/harness/live-agent-hooks).

## Delivery

Turn a change's tasks into issues, and a completed change into a merge or pull
request. Both commands are **provider-native**: the host is resolved through the
git-provider adapters plus the `repo:` config, and the CLI and argv come from
that provider — so the same command works on GitLab (`glab`) and GitHub (`gh`).

### issues

`forge issues create` reads the change's `tasks.md`, skips tasks already done,
and creates one issue per remaining task on the resolved provider.

```text
forge issues create [--change <c>] [--dry-run] [--label L]... [--workspace] [--cwd <dir>]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--change <c>` | *(active change)* | The change whose tasks are exported |
| `--dry-run` | off | Print the exact commands and execute nothing |
| `--label L` | *(none)* | Label to apply; **repeatable** |
| `--workspace` | off | Run the same export in **every** workspace repo carrying the change |
| `--cwd <dir>` | current dir | Repo root |

`create` is the only subcommand and it is required. A change with no open tasks
is reported and exits `0` — there is nothing to create.

```text
$ forge issues create --change checkout --dry-run
  [dry-run] glab issue create --title "T2 Add retry to the charge call" ...
# exit 0
```

### deliver

`forge deliver` opens a merge/pull request for a completed change. It refuses an
incomplete change — mirroring `archive` — unless `--allow-incomplete` is passed.

```text
forge deliver [--change <c>] [--dry-run] [--draft] [--title T] [--label L]...
              [--base B] [--allow-incomplete] [--require-ci] [--workspace] [--cwd <dir>]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--change <c>` | *(active change)* | The change to deliver |
| `--title T` | *(derived from the change name)* | Change-request title |
| `--base B` | *(provider default)* | Target branch |
| `--label L` | *(none)* | Label to apply; **repeatable** |
| `--draft` | off | Open as a draft |
| `--allow-incomplete` | off | Deliver even with tasks not done; prints a warning per pending task |
| `--require-ci` | **off** | Opt into the CI-status gate |
| `--workspace` | off | Run the same delivery in **every** workspace repo carrying the change |
| `--dry-run` | off | Print the provider argv and execute nothing |
| `--cwd <dir>` | current dir | Repo root |

The body is rendered from the change's own artifacts: the proposal when present,
a checklist of every task, and a provenance footer. A missing proposal is
skipped, never fatal.

::: warning `--require-ci` fails open
The CI gate blocks **only** on a confident "failed" verdict. An unresolvable
branch, a provider with no CI support, a runner failure, a still-running
pipeline, and unparseable output are all reported as warnings and let delivery
proceed. Determining CI status means parsing provider CLI output, which is
brittle — so the gate never blocks on uncertainty. The check is read-only and
therefore runs under `--dry-run` too, so the preview stays honest about whether
delivery would actually be refused.
:::

Both commands' `--workspace` fan-out shares the same isolation contract: a
target that is not a local checkout is reported as **skipped** and counts as
neither a delivery nor a failure; each remaining repo runs independently so one
repo's failure never aborts the others; and the command still exits non-zero
when any repo failed.

```text
$ forge deliver --change checkout --workspace
Workspace delivery for change "checkout" across 2 repo(s): api, web

── api (GitLab) ──
opened merge request

── web (GitHub) ──
opened pull request

delivered 2/2 repo(s)
# exit 0
```

## Docs & memory

Generate the offline dashboard and surface prior context.

### docs

`forge docs` has two distinct modes. `build` renders the offline, **read-only**
spec-viewer from `.forge/` to a directory (`--publish` also emits a Pages-ready
`public/`). `serve` runs the embedded, **interactive**
[web control panel](/features/web-ui) — it reads `.forge/` live and can also act
on it.

```text
docs build|serve [--out <dir>] [--publish] [--addr <host:port>] [--cwd <repo>]
```

```sh
forge docs build --out public --publish
forge docs serve                        # http://127.0.0.1:4321 (loopback)
forge docs serve --addr localhost:4000
```

::: warning `serve` is a mutating surface
The panel can run verify, archive a change, clear a tripped breaker or budget,
and waive a secscan finding. Mutations require `POST` + same-origin + a
per-session `X-Forge-Token`, and the server binds loopback by default — think
twice before overriding `--addr`.
:::

### docsite

`forge docsite` hosts **two architecturally separate families** under one noun.
They share no state, no manifest, no config key, and no output directory.

- **`install` / `dev` / `build`** — "forge's own site": scaffold and run the
  generator Forge installs into **`.forge/` itself**. `docsite.target` in
  `.forge/config.yaml` has no role here — the installed target lives in
  `.forge/.docsite-install.json`, so `dev`/`build` always read the manifest,
  never config.
- **`publish`** — "your site": renders forge's `.forge/` state into a
  **public** documentation site you already own, distinct from `docs` (which
  renders forge's internal dashboard) and from `install`'s own site.

```text
forge docsite install [--target <t>] [--force] [--no-gitignore]
                       [--dry-run] [--json] [--cwd <dir>]
forge docsite dev     [--print] [--cwd <dir>]
forge docsite build   [--out <dir>] [--print] [--cwd <dir>]
forge docsite publish [--target <t>] [--out <dir>] [--source-only]
                       [--dry-run] [--json] [--cwd <dir>]
```

| Subcommand | What it does |
| --- | --- |
| `install` | Scaffold the chosen generator into `.forge/`. Refuses a conflicting already-installed target unless `--force`; `--no-gitignore` skips the managed `.gitignore` block. |
| `dev` | Exec the installed generator's dev server in the foreground over live `.forge/` content. `--print` prints the command instead of running it. Errors naming `install` if nothing is installed, or by name if the installed target has no dev backend. |
| `build` | Run the installed generator's static build — no derive stage, no config drift: the same generated config `dev` serves. `--out` controls the output directory. |
| `publish` | Derive fresh from `.forge/specs`/`changes`, then render the site with ownership tracking. `--source-only` stops after staging, before any render. |

Targets: VitePress (the only `install`-able target today), or — for
`publish` only — VitePress, Docusaurus, Mintlify, Starlight, MkDocs Material,
or plain CommonMark. `detect`, `plan`, `render`, and `derive` were removed
with no aliases; running one names its replacement (`detect` → `install
--dry-run`, `render`/`derive` → `publish`, `plan` → no direct replacement).
See [Documentation Site Generator](/features/docsite).

```sh
forge docsite install --target vitepress
forge docsite dev
forge docsite build --out .forge/.vitepress/dist
forge docsite publish --target vitepress --out site
```

### panel

`forge panel` launches the **observe-only** lifecycle control panel as a TUI. It
reads state and never mutates it.

```sh
forge panel            # current directory
forge panel -cwd /path/to/repo
```

### recall

`forge recall` is **advisory** — it surfaces prior specs and decisions relevant to
a change's intent from memory to inform explore and spec. It is **not a gate** and
never blocks.

```text
recall --change <c> [--query Q] [--limit N] [--cwd DIR]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--change <c>` | *(required)* | The change whose intent seeds the query |
| `--query Q` | *(derived from the change intent)* | Explicit query, overriding the derived one |
| `--limit N` | `5` | Cap on how many prior observations are surfaced |
| `--cwd DIR` | current dir | Repo root |

`--change` is required. Omitting it is an **error**, not an advisory no-op —
the message goes to **stderr** and the process exits **1**:

```text
$ forge recall
# (stdout is empty)
# exit 1

$ forge recall 2>&1 >/dev/null
Error: recall requires --change <name>
```

That exit code is the binary's uniform error path, not a recall gate: once
`--change` is supplied, recall stays advisory and a failed search, an absent
intent, or zero results all print a note and exit `0`.

### memory

`forge memory` is the front door to the project memory store that backs
`recall`. Every subcommand resolves the effective config, auto-detects the
project from the repo root, and delegates to the provider.

```text
forge memory save <text> [--cwd <dir>]
forge memory search <query> [--cwd <dir>]
forge memory context [--cwd <dir>]
forge memory sync [--import] [--cwd <dir>]
```

| Subcommand | Effect |
| --- | --- |
| `save <text>` | Write an observation. The text is every non-flag token, joined |
| `search <query>` | Search the project's memory. No results is not an error |
| `context` | Print the project's accumulated context. Takes **no** positional arguments |
| `sync` | Export the project's memory into `.forge/memory/`. Takes **no** positional arguments |

`--cwd <dir>` is accepted by all four. The only other flag is **`--import`**, on
`sync`: it stages memory already committed at `.forge/memory/` — a teammate's
checked-in memory from a fresh clone — as the provider's import input before
syncing, so it is reconciled into the local store rather than only ever being
overwritten by a fresh export.

```sh
forge memory save "checkout retries are capped at 3 by policy"
forge memory search retries
forge memory context
forge memory sync --import        # reconcile a fresh clone's committed memory
```

::: warning There is no `--project` flag
`memory` derives the project from the repo root and accepts no override.
Passing `--project` is rejected as an unknown argument and the command
hard-errors. Use `--cwd` to point at a different repo instead.
:::

Memory operations are explicit and user-invoked, so a missing `engram` binary
exits non-zero — there is no meaningful success to report. It degrades cleanly
though: one clear message telling you to install it, never a panic or a raw
stack trace.

### codegraph

`forge codegraph init` initializes the codebase-graph provider for a project.
It is a deliberately narrow, Forge-owned boundary used by generated agent
guidance, and accepts no raw provider arguments.

```text
forge codegraph init --cwd <project-root>
```

### learn

`forge learn` is the **continuous-learning ledger**: it mines a change's session
log and git diff for recurring patterns and, only on explicit human approval,
promotes them into memory.

```text
forge learn extract|review|promote|reset [--change <c>] [--cwd <dir>] [--approve] [--threshold N]
```

| Subcommand | Effect |
| --- | --- |
| `extract` | Mine the change's session log and diff into deduped candidates |
| `review` | List the pending candidates |
| `promote` | Write eligible candidates into memory — **requires `--approve`** |
| `reset` | Clear the ledger |

| Flag | Default | Effect |
| --- | --- | --- |
| `--change <c>` | *(active change)* | The change whose session log and diff are mined |
| `--approve` | off | Authorize promotion. `promote` without it does not write |
| `--threshold N` | *(from `learn.threshold` in config)* | Override the promotion threshold for this invocation — meaningful to `review` and `promote` only |
| `--cwd <dir>` | current dir | Repo root |

::: warning Advisory, never a gate
Promotion is **human-gated** and deliberately never blocks: a skipped or
degraded `learn` run does not return a failing exit code, unlike
`verify` / `budget` / `secscan`.
:::

## Repo detection

All three commands are **read-only** — they detect and report, and write
nothing.

### profile

`forge profile` reports the repo-adaptivity profile Forge derives for this repo:
detected stack, configured test and lint commands, git-hosting provider, size
bucket, related repos, and which skills are relevant versus skippable.

```text
forge profile [--cwd <dir>]
```

### provider

`forge provider` reports the git-hosting provider Forge resolves for this repo
and any related repos, tying the provider adapters to the `repo:` section of
`.forge/config.yaml`. It never writes or calls a host API.

```text
forge provider [show] [--cwd <dir>]
```

### workspace

`forge workspace` resolves this repo plus every sibling declared under
`repo.related` in `.forge/config.yaml`, and reports each one's provider,
location, and active SDD changes — followed by the changes that span more than
one repository.

```text
forge workspace [status] [--change <c>] [--json] [--cwd <dir>]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--change <c>` | *(none)* | Report which repos carry that change instead of the full status |
| `--json` | off | Emit the resolved workspace as indented JSON |
| `--cwd <dir>` | current dir | Repo root |

`status` is the only subcommand and it is the default when omitted. Config
resolution is best-effort: a missing or invalid `.forge/config.yaml` yields a
single-repo workspace rather than an error.

```text
$ forge workspace --change checkout
Change "checkout" spans 2 repo(s): api, web
# exit 0
```

## Design system

Two non-destructive CREATE flows that seed `.forge/design/`. Authoring the
actual content is **agent-driven** — these commands lay down the scaffold and
hand you off to the `/forge.brand` and `/forge.design` agent commands. Existing
files are never clobbered, and `.forge/` is backed up first so the write is
reversible.

### brand

`forge brand` seeds only `.forge/design/brand.md`.

```text
forge brand [--cwd <dir>]
```

`--cwd` is the only flag; any other argument is an error.

### design

Bare `forge design` seeds both `.forge/design/brand.md` and
`.forge/design/design-system.md`. The three subcommands do real work on top of
that scaffold.

```text
forge design [detect|apply|sync] [--save] [--dry-run] [--cwd <dir>]
```

| Subcommand | Effect |
| --- | --- |
| *(none)* | Seed the scaffold files when absent |
| `detect` | Structural scan of existing design standards — CSS custom properties, Tailwind/theme config, component dirs, an existing `design-system.md` — printed as an inventory + gap report |
| `apply` | Materialize the authored tokens into the repo: a CSS custom-properties file plus a stack-agnostic tokens JSON fallback |
| `sync` | Classify design-token drift between the design system and the materialized code, rendered exactly like `forge drift` |

| Flag | Default | Applies to | Effect |
| --- | --- | --- | --- |
| `--save` | off | `detect` | Also write the report to `.forge/design/design-system.detected.md` |
| `--dry-run` | off | `apply` | Compute and print the same content without writing anything |
| `--cwd <dir>` | current dir | all | Repo root |

`detect` without `--save` and `sync` are both fully read-only — no filesystem
write of any kind. `apply` without `--dry-run` is the one mutating path, and it
is backed up and reversible.

## Extensions

Install third-party extensions from the catalog, and build a new skill from a
natural-language description.

### extension

`forge extension` manages installed extensions and the embedded
official + community catalog. It works fully offline: both the installed list
and the default catalog are local.

```text
forge extension <list|add|remove|search|info|update|enable|disable> [args] [--cwd <dir>]
```

| Subcommand | Positional | Effect |
| --- | --- | --- |
| `list` | *(none)* | Installed extensions, then every catalog entry not already installed |
| `add` | `<id\|url\|path>` | Install from the catalog, a URL, or a local directory |
| `remove` | `<id>` | Uninstall |
| `search` | `<query>` | Search the catalog; all positional tokens are joined into one query |
| `info` | `<id>` | Installed → the real on-disk manifest; otherwise the catalog metadata |
| `update` | `<id>` | Reinstall at the catalog's version, if it differs from what is installed |
| `enable` / `disable` | `<id>` | Toggle the registry entry's enabled flag |

`--cwd <dir>` is the only flag, accepted by every subcommand. `add` resolves its
argument in three forms, in order: anything containing `://` is downloaded as a
zip and extracted, **bypassing the catalog**; an existing local directory is
used as-is; anything else is looked up as a catalog ID.

`update` requires the extension to be both installed *and* present in the
catalog — the catalog is the only source of "what is the newer version". A
catalog load failure during `list` degrades to a warning rather than failing the
command, since the installed list is still useful on its own.

```sh
forge extension list
forge extension add forge-python
forge extension add ./my-local-extension
forge extension info forge-python
forge extension disable forge-python
```

### agent-build

`forge agent-build` generates a validated skill from a natural-language
description, previews it, installs it into **every** configured agent's skills
directory, and registers the result as an extension — so it shows up in
`forge extension list` and is removable with `forge extension remove`.

```text
forge agent-build <description> [--engine <id>] [--cwd <dir>]
```

| Flag | Default | Effect |
| --- | --- | --- |
| `--engine <id>` | *(first available)* | The AI CLI that drives generation. Must be available on `PATH` |
| `--cwd <dir>` | current dir | Repo root |

Every non-flag token is joined into the description, so it needs no quoting —
though quoting is clearer. An empty description is a usage error.

Without `--engine`, forge tries a fixed candidate order and takes the first one
available on `PATH`: **`claude-code`, `opencode`, `gemini-cli`, `codex`**. If
none is available the command fails with that list. It also fails when no
configured agent exposes a skills directory — configure at least one agent
first.

The preview is printed as soon as generation succeeds, **before** anything
touches disk, and stays printed even when the subsequent install partially or
wholly fails. Every agent-built skill registers at version `0.1.0`: there is no
version input for a single-shot description build.

```sh
forge agent-build "review Terraform plans for destructive changes"
forge agent-build "summarize failing CI logs" --engine opencode
```

## Skills

Manage the skill registry the engine delegates to.

### skill-registry

`forge skill-registry refresh` rebuilds **`.forge/skill-registry.md`** with a
cache-hit fast path; `list` prints the resolved registry.

```text
$ forge skill-registry list
algorithmic-art       user      /Users/you/.agents/skills/algorithmic-art/SKILL.md
cognitive-doc-design  project   /repo/skills/cognitive-doc-design/SKILL.md
# exit 0
```

```sh
forge skill-registry refresh
```

### skill-discover

`forge skill-discover` discovers and installs **external** framework skills from
configured sources (skills.sh and community git registries) into
`.forge/skills/` — distinct from Forge's embedded foundation and SDD skills.

```text
usage: forge skill-discover [--source <name>] [search terms...] [--install <id>] [--cwd <dir>]
```

```sh
forge skill-discover                       # list everything every source advertises
forge skill-discover react                 # search all sources for "react"
forge skill-discover --source skills-sh react
forge skill-discover --install react-19    # install into .forge/skills/react-19/
```

A source that is unreachable is reported and skipped — it never aborts the rest
of the command. Run `forge skill-registry refresh` after installing to index the
new skill for delegation.

## The binary itself

These commands act on the forge installation rather than on a project.

### version

```text
forge version
forge --version
forge -v
```

Prints the version resolved at build time. The three forms are identical.

### help

```text
forge help
forge --help
forge -h
```

Prints global help. Every `review` subcommand also accepts `--help` for its own
usage.

### completion

`forge completion` prints a shell completion script generated from the same
command table that backs the CLI, so completions never drift from the binary.

```text
forge completion zsh|bash|fish
```

```sh
# zsh, via Homebrew's site-functions
forge completion zsh > "$(brew --prefix)/share/zsh/site-functions/_forge"
```

### update

```text
forge update
```

Checks for available updates to forge and the tools it manages, and reports what
is available. It **only checks** — nothing is downloaded or replaced.

### upgrade

`forge upgrade` applies the updates `update` reports.

```text
forge upgrade [--yes] [--no-backup]
```

| Flag | Default | Meaning |
| --- | --- | --- |
| `--yes` | off | Skip the confirmation prompt |
| `--no-backup` | off | Do **not** back up managed configs before writing |

A config backup is taken before anything is replaced unless you pass
`--no-backup`; `restore` is what reads it back.

Binaries come from the private GitHub release repo, so every download carries
your token — see [Installation](/installation).

### restore

`forge restore` puts a managed-config backup back.

```text
forge restore [latest] [--list] [--yes] [--cwd <dir>]
```

| Flag | Default | Meaning |
| --- | --- | --- |
| `--list` | off | List available backups instead of restoring |
| `--yes` | off | Skip the confirmation prompt |
| `--cwd` | current directory | Repo root to check for repo-scoped backups |

`latest` restores the most recent backup, which is the common case after an
`upgrade` you want to undo.

`restore` checks **both** backup locations: the home-scoped `~/.forge/backups/`
written by `install`/`sync`/`upgrade`/`uninstall`, and, when the current
directory (or `--cwd`) is a project with its own `.forge/` tree, that
project's `<repoRoot>/.forge/backups/` too — the location `import` and
`migrate` snapshot into before they touch anything. `--list` labels each
entry `[home]` or `[repo]` so you can tell which one applies to the project
you're standing in.

## Next steps

- [Commands](/guide/commands) — the `/forge.*` agent slash commands, the in-agent counterpart to this binary.
- [Harness overview](/harness/overview) — the four enforced controls and how they fit together.
- [The circuit-breaker](/harness/circuit-breaker) — the verify gate's state model.
- [The autonomous run](/harness/autonomous-run) — `forge run`'s bounds and stop conditions.
- [Harness as MCP](/harness/mcp) — the queryable, non-enforcing tool surface.
- [Spend Budget](/harness/budget) — the ledger format and enforcement rules.
- [Installation](/installation) — install, maintain, and update the binary.
