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

# Review loop

> Record a bounded generation, review, and repair cycle.

`convergence` is a pure graph form. It folds recorded form events into state
and returns the next command.

The form has one generator, one done check, one repair node, and one or more
review seats. A review seat is an independent reviewer node.

## Run the example

From an Obversa checkout, run:

```bash theme={null}
pnpm example:review-loop
```

The example uses scripted events for two passes from separate providers and
model families. It does not call an engine. It prints this JSON report:

```json theme={null}
{"decision":{"kind":"complete","output":{"iterations":1,"restarts":0,"seats":{"claude-review":"accepted","codex-review":"accepted"},"findings":[]}},"events":10,"planDigest":"sha256:499b4c4187dfb574d3a22653145a0b3289c7c6ed25eb106eb981ae86f4136aa1","bounds":{"dispatches":{"min":{"kind":"known","value":4},"max":{"kind":"known","value":11}},"maxConcurrency":{"kind":"known","value":2},"maxFanOut":{"kind":"known","value":2}}}
```

The clean-consumer check compiles and runs the same source from the packed
package with TypeScript 6 and TypeScript 7. It compares the report on this page
with the example’s output. Its source is
`examples/packages/review-loop.ts`.

## Define a review loop

The next fragment is from `examples/packages/review-loop.ts`.

```ts theme={null}
import {
  compileGraph,
  convergence,
  type ConvergenceDefinition,
} from '@obversa/runtime';

const claudeLane = {
  id: 'claude-review',
  requested: {
    adapter: 'mock', provider: 'anthropic', modelFamily: 'claude', model: 'mock-claude', tools: [],
  },
  knownSubstitutions: [],
} as const;
const codexLane = {
  id: 'codex-review',
  requested: {
    adapter: 'mock', provider: 'openai', modelFamily: 'gpt', model: 'mock-gpt', tools: [],
  },
  knownSubstitutions: [],
} as const;

const definition: ConvergenceDefinition = {
  id: 'release-review',
  definitionVersion: 1,
  data: {
    maxIterations: 2,
    maxReviewRestarts: 1,
    quorum: 2,
    requireDiversity: true,
    skippableSeats: [],
    seatConcurrency: 2,
    retryCapPerNode: 0,
  },
  nodes: [
    { id: 'draft', data: { role: 'generator' } },
    { id: 'done-check', data: { role: 'evaluator' } },
    {
      id: 'claude-review',
      data: { role: 'seat', lane: claudeLane, evidencePaths: ['draft'] },
    },
    {
      id: 'codex-review',
      data: { role: 'seat', lane: codexLane, evidencePaths: ['draft'] },
    },
    { id: 'repair', data: { role: 'repair' } },
  ],
  edges: [],
};

const graph = compileGraph(convergence, definition);
```

## Recorded behaviour

* **Node records:** The form accepts dispatch, completion, failure, pause, and
  resume records for each node attempt.
* **Engine records:** Each call through the graph executor records its
  requested and reported adapter, provider, model family, and model in an
  `engine-attempt-recorded` event. A primary call and a fallback call have
  separate records. A call
  without a reported identity records `effective: null`.
* **Rejected engine records:** For scripted events, the loop status records
  invalid receipts in `engineReceiptRejections`. Each record has code
  `INVALID_ENGINE_RECEIPT`, the node ID, position, sequence, and reason.
  The field is absent until a receipt is rejected. A rejection leaves accepted
  engine identities and the review quorum unchanged.
* **Review records:** A seat pass includes its confidence, input hashes, and
  workspace fingerprint. Evaluator evidence also names one proof artifact
  digest. Every seat dispatch receives that digest, and every seat result must
  echo it. A missing or changed digest makes that result invalid and dispatches
  the seat again while its retry limit permits. Findings from that result cannot
  enter repair inputs or finding counts. The seat's engine record supplies
  its provider and model family. `evidencePaths` names the input hashes that can
  invalidate that seat; omitting it makes every input hash relevant. The form
  checks whether the accepted passes meet the quorum. With `requireDiversity`,
  that quorum must contain pairwise-distinct providers and model families.
* **Repair records:** Findings can send the form to its repair node. A later
  cycle keeps valid seat passes and reruns invalid seats.
* **Policy records:** New evaluator evidence invalidates each seat whose named
  input hash changed. A producer can also invalidate an in-flight seat or record
  a limit pause.

The form consumes the graph executor's upper-case failure codes. `ABORTED`
pauses the run. `ENGINE_UNAVAILABLE` skips a declared skippable seat and pauses
for a required seat. Other node failures use the declared retry cap before the
required seat pauses as unresolved.

## Excluding the writer

For engine calls managed by the graph executor, a reviewer cannot count toward
the quorum if its reported provider or model family matches any writer
or repair call.
This check applies even when `requireDiversity` is false. It also applies to
a cached pass after a repair. A reviewer without a reported provider and
model family cannot count toward the quorum.

The definition must keep every writer and repair target, including all
declared substitutions, separate from every review target and substitution
by both provider and model family. Different adapters, models, or lane names
do not remove a conflict. Skippable seats follow the same rule.

The guarantee is proven on what the engine reported; a call that died before
reporting counts as its declared targets. The executor records an unknown
identity when it recovers an unfinished engine attempt. The run can resume
and complete with reviewers outside the writer's declared set. The runtime
does not independently verify the provider behind an engine's report.
Calls made inside an engine adapter or wrapper are not recorded separately.
The runtime uses only the identity that engine reports.

Data-only nodes have no engine identity. Their results do not establish
provider separation for work performed outside the runtime's engine calls.

## Stored plans and evaluator failures

The convergence graph type uses version 3. A stored plan for version 1 or 2 is
refused by `createGraphExecutor` with `STORED_GRAPH_MISMATCH` before a node
starts. The executor does not convert the stored plan. Start a new run with a
plan compiled from version 3 to use this evidence contract.

When the evaluator completes without valid review evidence, the form returns
`fail` with code `CONVERGENCE_REVIEW_EVIDENCE_INVALID`. It dispatches no review
seat. Reopening the run preserves that failure. Correct the evaluator and
start a new run.

## Limits and output

`maxIterations`, `maxReviewRestarts`, and `retryCapPerNode` bound the described
dispatch count. `seatConcurrency` sets the review batch size.

The complete output states the cycle count, repair count, result for each review
seat, and any blocking findings returned by seats outside the accepted quorum.
