> ## Documentation Index
> Fetch the complete documentation index at: https://llmwiki.atomicstrata.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Review Queue - Inspect and Approve Generated Wiki Pages

> Use llmwiki compile --review and the review subcommands to inspect, approve, or reject generated pages before they land in your wiki.

By default, `llmwiki compile` writes pages directly to `wiki/`. That's fine for a personal knowledge base where you trust the compiler's output - but when you're building something authoritative, populating a shared wiki, or working with sources that contain conflicting or uncertain information, you may want to review each generated page before it goes live.

The review queue gives you that control. Instead of writing pages, compile deposits them as JSON candidate records in `.llmwiki/candidates/`. You inspect each one, then either approve it (which writes it to `wiki/` and refreshes the index) or reject it (which archives it without touching the wiki). The wiki only changes when you say so.

***

## Entering the review queue

Add `--review` to any `compile` invocation to route all generated pages through the queue:

```bash theme={null}
llmwiki compile --review
```

When the compile finishes, it reports how many pages were written and how many were held:

```
Wrote 8 page(s), held 2 for review - run `llmwiki review list`
```

Pages that were already live and haven't changed are not affected. Only newly generated or updated candidates are held.

<Note>
  `--review` is all-or-nothing on the command line - every generated page goes to the queue. For more granular control (e.g. automatically hold only low-confidence or contradicted pages while writing the rest live), see [Review Policy](/configuration/review-policy).
</Note>

***

## Review subcommands

Before approving concept/query drafts, you can request an advisory citation check:

```bash theme={null}
llmwiki eval --candidates --suite full --out json
```

This evaluates a sample against current source passages without approving or
changing any candidate. Use `llmwiki eval --candidates` for an evidence inventory
without model calls. See [pending draft evaluation](/cli/lint-eval#evaluate-pending-drafts-before-approval)
for costs, evidence changes, sampling limits, and report files.

### `llmwiki review list`

List all pending candidates:

```bash theme={null}
llmwiki review list
```

Each row shows the candidate ID, slug, review mode, reason codes, generation timestamp, and contributing sources. The reason codes tell you why the page was held - useful when you're using a review policy that holds pages selectively.

### `llmwiki review show <id>`

Inspect a single candidate in full:

```bash theme={null}
llmwiki review show <id>
```

The output includes:

* **Title, slug, and summary** - the page's metadata
* **Sources** - which source files contributed to this page
* **Review mode and reason codes** - why the page was held, with detail where available
* **Confidence** - the LLM-reported confidence score, if present
* **Contradiction flag** - whether the page declares `contradictedBy` entries
* **Full page body** - the complete markdown content, exactly as it would be written to `wiki/`
* **Schema violations** - any cross-link rules the page fails, if a schema is configured
* **Provenance violations** - any broken or malformed citation markers
* **Connector draft hash** - for connector candidates, the `draft-content-hash`
  required by `review approve`

Use `review show` to read the proposed page before deciding to approve or reject it.

### `llmwiki review approve <id>`

Promote a candidate into the live wiki:

```bash theme={null}
llmwiki review approve <id>
```

Connector-fetched candidates require an extra approval pin:

```bash theme={null}
llmwiki review show <id>
llmwiki review approve <id> --draft-content-hash <hash-from-show>
```

The hash is computed from the exact candidate body printed by `review show`.
Approval re-reads and re-hashes the candidate under the project lock. If the
body changed after you inspected it, approval refuses and you must review the
candidate again.

Approval does the following in order:

1. Acquires `.llmwiki/lock` to serialize against concurrent compiles or sibling approvals
2. Re-reads the candidate under the lock (to guard against a concurrent reject that may have run between your `review show` and now)
3. Validates the page body
4. Writes the page to its target, normally `wiki/concepts/<slug>.md`; query candidates target `wiki/queries/<slug>.md`
5. Records the approved slug in source state so future compiles track it correctly
6. Refreshes `wiki/index.md`, the Map of Content, and embeddings; generic candidates also run wikilink resolution and repair
7. Removes the candidate file from `.llmwiki/candidates/`

Approval never re-invokes the answer-generation LLM. Generic candidates retain their existing resolution and repair tail. Validated answers publish the exact canonical document checked before the write.

Page writes, refresh work, state bookkeeping, and candidate cleanup are not one transaction. A later failure may leave a published page and a still-pending candidate. Inspect the page and queue, correct the failure, and retry approval; a retry may replace the existing page. Actual write and uncaught refresh failures propagate rather than becoming successful citation checks.

<Warning>
  Embeddings refresh may fail if no provider credentials are configured at approval time. When that happens, a warning is printed but the approval still succeeds - the page is written and the index is updated. Re-run `llmwiki compile` or `llmwiki refresh --stale` later to pick up the missing embeddings.
</Warning>

### `llmwiki review approve-batch --input <manifest.json>`

Approve a selected group with one project lock and one shared refresh of links,
the index, Map of Content, and embeddings. Approval uses the stored candidate
bodies and does not call the generation model.

Create a manifest containing the candidate IDs you have reviewed:

```json theme={null}
{
  "schemaVersion": 1,
  "candidates": [
    { "id": "first-topic-aabbccdd" },
    { "id": "second-topic-eeff0011" }
  ]
}
```

Then run:

```bash theme={null}
llmwiki review approve-batch --input approvals.json --json
```

The manifest must be a regular file of at most **1 MiB (1,048,576 bytes)** and
contain at most **100 candidate entries**, counted before deduplication. Symlink
leaves and special files are refused. Oversized or invalid input fails before
the project lock is acquired or recovery begins; `--json` still returns one
`failed` envelope with exit code `1`. Split larger requests into bounded batches.

Add `draftContentHash` to an entry to bind approval to the SHA-256 of its exact
candidate body. Connector candidates require the hash shown by `review show`;
do not compute a new pin without reviewing changed content. A supplied hash is
checked for ordinary candidates too.

The batch enters the same mutation recovery gate as single approval. Unsettled
operation bundles or unfinished preparation maintenance block approval before
candidate promotion. Candidate storage must also be available and writable
before any live page writes. These failures retain pending candidates.

The command re-reads and validates candidates under the lock before applying
the valid subset. Missing or invalid candidates remain unapproved. Identical IDs
are processed once. Conflicting pins for the same ID, or different candidates
targeting the same page, are refused instead of overwriting each other.
Target comparison uses lower/uppercase and canonical Unicode normalization on every
platform, including case-sensitive filesystems and pages that do not yet exist.
For example, `Alpha` and `alpha` in the same namespace conflict; their original
paths are not renamed. Every contender remains pending, so input order cannot
choose a winner. The same slug in different namespaces remains independent.

Destination preconditions are checked under that same lock. A candidate with
`expectedTargetHash` requires the current page to match that hash;
`expectTargetAbsent` requires the page to remain absent. Changed, removed, or
unverifiable destinations receive an individual `invalid` result. The candidate
body hash does not replace this destination check. Re-propose stale repairs
against the current page before approving them.

Generic candidates use the same citation checks as single approval: broken
targets are refused, pending chains and repairable prefixes remain allowed, and
an unavailable citation check warns without changing generic approval policy.
Validated answer candidates receive `invalid` with instructions to use
`llmwiki review approve <id>` individually. This preserves their exact validated
bytes by keeping them out of the batch link-repair tail. Other valid candidates
in a mixed batch can still proceed.

Candidates that update source state must agree on the recorded hash of every
shared source. If valid candidates contain different revisions of a source,
all candidates referencing that source receive `conflict` and remain pending;
unrelated candidates can proceed. Manifest order and generation timestamps do
not select a winner. Recompile and review candidates from one consistent source
revision before retrying the refused group.

With `--json`, stdout contains one JSON result. The envelope includes:

| Field | Meaning |
| - | - |
| `schemaVersion` | Contract version, currently `1`. |
| `status` | `completed`, `partial` for individual refusals, or `failed` for a technical failure. |
| `finalized` | Whether the shared finalization completed; `false` when no pages were approved. |
| `results` | One entry per unique candidate ID: `id`, `status`, optional relative `pagePath`, and optional `error`. |
| `timingsMs` | Total duration and timings for reached phases such as validation, promotion, link repair, index, and cleanup. |
| `error` | A shared technical error, when present. |

`timingsMs.lockWait` includes lock acquisition and the shared recovery gate.
Recovery is not measured again as a separate batch phase.

Candidate statuses are `approved`, `invalid`, `conflict`, or `failed`. Exit code
`0` means the operation completed without refusals. Exit code `1` covers both
partial results and technical failures: inspect the JSON `status` before
deciding which candidates to retry. An empty manifest completes without running
the finalizer. The existing single-candidate commands remain available.

<Warning>
  A batch is not a transaction over the entire wiki. Page promotion uses the
  write journal, but source-state updates and finalization are an idempotent
  tail. A finalization failure can leave written pages while their candidates
  remain pending. Ordinary candidates without destination preconditions can be
  retried to complete that tail. Preconditions are checked again on every retry:
  if promotion already changed a protected destination, the old proposal is
  refused even when its body matches the live page. Inspect the published page
  and unfinished finalization, and re-propose against the current destination;
  do not remove the precondition to force approval.

  Candidate removal starts only after finalization succeeds. An interruption
  during removal can leave some candidates cleared and others pending; a retry
  reports already-cleared IDs as missing. `finalized: true` does not mean cleanup
  or an external Git commit completed. Callers needing whole-operation rollback
  or durable replay receipts must provide those guarantees around the command.
</Warning>

Batch approval obeys the shared embedding configuration, including
`LLMWIKI_EMBEDDINGS=off`. It refreshes embeddings only for approved pages and
pages actually rewritten by link resolution or repair. Unrelated pending retry
counts, quarantines, and stored vectors remain unchanged. Unlike single approval
and compile, a batch does not drain the older embedding backlog or perform a
whole-store migration. If the existing store is legacy, incompatible with the
active embedding configuration, or unreadable, the embedding refresh is deferred
with a warning; run `llmwiki compile` for full reconciliation. This preserves
durable pending work and its existing attempt counts: requiring a migration
does not consume provider attempts or quarantine pages. Strict embedding mode
still reports refresh failure as an error after preserving that work.

Before promoting any candidate page, the batch validates its intent storage and
records the initially known candidate page IDs in
`.llmwiki/review-embedding-intent.json`, associated with the exact candidate
snapshots. It also records additional affected IDs before changing their links.
A reordered retry, or a retry of a retained subset, recovers that
group's work even when its links were already written before an index or Map of
Content failure. Other interrupted groups and unrelated pending embeddings are
not retried. Entries are cleared only after refresh completes or durably hands
off the work to the embedding retry queue. Explicitly disabling embeddings
also permits completion; a later full compile reconciles missing vectors.

The intent file is confined to the project and capped at **1 MiB**. Unreadable,
invalid, oversized, or unwritable intent storage detected in preflight fails
before candidate promotion or source-state finalization and keeps candidates
pending. Capacity for initially known work is checked at this point; capacity
for newly discovered collateral work is checked before its link writes. A later
failure can still leave earlier writes in place.

Before promoting any candidate, the batch also checks that the embedding retry
queue can record the pages it already knows about, using the same caps as the
refresh itself. A full or unreadable queue refuses the batch with every
candidate still pending and nothing promoted; run `llmwiki compile` to work
through queued embeddings (or repair the queue file), then retry. Pages whose
links are rewritten during finalization are only known at that point: if the
queue cannot hold their work, finalization fails rather than forgetting it, and
the retained candidates recover it on retry. Repair storage or free retry
capacity, then retry the retained candidates. Keep their snapshots
unchanged until recovery finishes: editing them does not erase their intent.
Rejecting a retained candidate hands its recorded work to the embedding retry
queue, so the next `compile` refreshes it (pages still quarantined for unchanged
content stay excluded), and removes only that candidate from
the intent; batch-mates still pending keep their recovery. If snapshots were
removed, restore them from backup to complete recovery; do not delete the
intent file merely to bypass a storage error.

Typed candidates retain their profile, lifecycle, and
precondition checks. A typed candidate entering a state with relation
requirements is refused when the batch would promote multiple candidates:
another page could change the evidence used to validate that transition.
Approve those candidates individually so their checks see the current wiki.
Other valid candidates can still proceed. Review external bodies before
approving them, just as with single-candidate approval.

To compare single and batch approval from a source checkout:

```bash theme={null}
npm run build
npm run benchmark:review -- dist/cli.js single 1500 30
npm run benchmark:review -- dist/cli.js batch 1500 30
```

Each command generates a disposable wiki with 1,500 existing pages and 30
candidates, disables embeddings, reports JSON timings, and removes its own
fixture afterwards. It does not approve candidates in your existing wiki.
Compare runs on the same otherwise-idle machine; these synthetic timings are
not a guarantee for your corpus.

### `llmwiki review reject <id>`

Archive a candidate without touching the wiki:

```bash theme={null}
llmwiki review reject <id>
```

Rejected candidates are moved to `.llmwiki/candidates/archive/`. They no longer appear in `review list`, but they remain on disk for audit purposes. A rejected candidate for an unchanged source won't be re-extracted on the next `compile` - the rejection is sticky until the source itself changes.

If the candidate was retained by an interrupted `approve-batch`, rejection first hands its recorded embedding work to the embedding retry queue (see batch recovery under `review approve-batch` above). When that queue is full, or the intent or queue storage is unreadable, rejection refuses and the candidate stays pending: run `llmwiki compile` to work through queued embeddings, or repair the storage, then reject again. Rejection re-reads the queue to confirm the work was recorded, and refuses if it was not. A page that is quarantined for exactly its current content stays quarantined and does not block the rejection. With embeddings disabled, rejection only releases the intent and never touches the retry files: the next `compile` with embeddings enabled finds the pages' stale vectors and refreshes any whose content changed since a failed attempt.

Malformed or unsupported validated-answer metadata cannot be promoted: `review show` and `review approve` report an invalid-candidate diagnostic, while list and collection paths skip the record with a named warning. You can still reject its explicit safe ID to archive the original queue file through the existing archive mechanism without accepting its metadata. The rename path preserves bytes; the existing text-copy fallback does not guarantee byte preservation. Unsafe IDs, symlinks, non-regular files, missing files, and detected file replacement refuse; raw rejection does not bypass path confinement.

## Validated-answer candidates

`llmwiki query "<question>" --save --review` creates a separate candidate with a versioned `candidateKind` and `citationManifest`. It stores the canonical query document, an empty sources list, a body digest, and the observed citation identities. Staging allows pending links but refuses broken links or unavailable validation. Neither staging nor validation makes an extra model request or changes the live wiki.

Approval freshly resolves the current body under the project lock; stored observations never authorize publication. All recognized links must resolve to retained concept/query pages. In a profile-enabled project the write routes through the planner under the current profile. Pending, broken, unavailable, stale-target, or planner-refused approvals refuse without deleting the candidate. Approve required pending targets first, then retry the answer. See the complete [publication policy matrix](/cli/query#publication-and-review-policy).

The digest covers only the parsed Markdown body, with no additional whitespace normalization. Body edits cause `candidate-edited`: regenerate and restage, then reject the obsolete candidate. Title/summary-only frontmatter edits are outside this digest and still pass through ordinary page validation. The manifest is an audit record, not authenticated provenance or tamper protection: an operator can recompute it or remove both metadata fields, returning the record to generic policy. Even a matching digest still requires fresh link resolution.

Repeated proposals for the same query remain separate IDs with generation timestamps, and each captures a closed precondition on its target page at staging time. Approving a proposal whose target is unchanged since staging warns with its `wiki/queries/<slug>.md` path before replacing it. If the target changed, appeared, or was deleted after staging, approval refuses and asks you to regenerate and restage, keeping the page and the candidate; once one sibling is approved, the others' preconditions are stale and refuse until restaged. Approval removes only the approved ID; siblings stay for explicit approval or rejection. Reject unused siblings by ID. A retained page takes precedence over a sibling pending proposal when resolving links.

Validated-answer approval preserves exact canonical page bytes and refreshes the index, Map of Content, and embeddings unless disabled. It skips the generic outbound resolution and global repair tail, so it does not rewrite inbound links or a same-slug concept. Later ordinary compile or generic approval can still run normal repair.

### Generic citation checks

Ordinary default concept/query candidates, including imported query candidates without validated-answer metadata, allow pending links. They refuse only genuinely broken links that the existing repair pass cannot resolve. A unique retained prefix can remain repairable under the existing minimum-length and boundary rules; ambiguous prefixes, including ambiguity introduced by pending candidates, do not qualify. Typed candidates retain their typed policy.

If this generic citation check cannot be computed, approval warns explicitly and continues its existing policy. This warning fallback applies only to the citation check; connector pins, planner decisions, page writes, and refresh errors keep their existing behavior. For a genuinely broken target, create or approve the target, correct the source and regenerate, or reject the candidate. Manual JSON edits remain possible but are not a new supported editing API.

***

## How candidates record their reason

Every candidate records **why** it was held. The reason codes surface in `review list` (compact) and `review show` (with detail):

| Reason code | Meaning |
| - | - |
| `manual` | Held because `--review` was passed explicitly |
| `low-confidence` | Page `confidence` is below the configured threshold |
| `contradicted` | Page declares `contradictedBy` entries |
| `schema-violating` | Page fails a schema cross-link rule |
| `provenance-violating` | Page has broken or malformed citations |
| `imported-okf` | Page was imported from an OKF bundle |
| `connector-fetched` | Page was fetched by a first-party connector |

When you're using a [review policy](/configuration/review-policy), only pages that trip an enabled reason code are held - the rest are written live.

## Typed and external candidates

Profile-aware imports and connectors can stage typed candidates. `review show`
prints the target entity directory, such as `wiki/papers/<slug>.md`, before you
approve. Approval routes typed candidates through the active profile's field
contracts, lifecycle rules, relation preconditions, and artifact preconditions.

Imported OKF candidates and connector candidates are external content. Treat the
body as untrusted until you have reviewed it. Connector-origin body and mapped
field content are fenced in review, context, and export surfaces so agents see
it as data rather than instructions.

***

## `refresh --stale` and the review queue

`llmwiki refresh --stale` honors the same review policy as `compile`. If you have a policy configured, stale pages recompiled by `refresh --stale` will be held for review rather than written directly when they trip a policy rule.

```bash theme={null}
llmwiki refresh --stale          # repairs stale pages, respects review policy
llmwiki refresh --stale --dry-run  # preview only, no LLM calls or writes
```

***

## Deletion bookkeeping

`compile --review` does **not** orphan-mark deleted sources. If you deleted a source file since the last compile, the pages it contributed won't be marked orphaned until the next non-review `compile`. The `--review` help text advertises this behavior explicitly.

If you need orphan detection, run a non-review compile:

```bash theme={null}
llmwiki compile   # writes pages live AND processes orphan cleanup
```

***

## Checking the queue from an agent

The MCP `wiki_status` tool exposes a `pendingCandidates` field so agents can see how many candidates are waiting for review without running any CLI commands. See [llmwiki serve](/cli/serve) for MCP tool details.

***

## Automated policy-based review

Rather than manually passing `--review` every time, you can declare a review policy in `.llmwiki/config.json` to automatically hold risky pages while writing safe ones live. See [Review Policy](/configuration/review-policy) for the full configuration reference, including all reason codes, the `lowConfidenceThreshold` setting, and fail-closed behavior.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.