# Okstra Convergence Contract

## Index

- [Scope and Terminology (BLOCKING)](#scope-and-terminology-blocking)
- [When to Use](#when-to-use)
- [Configuration](#configuration)
- [Finding Category](#finding-category)
- [Convergence Algorithm](#convergence-algorithm)
  - [Round 0: Parse worker results](#round-0-parse-worker-results)
  - [Round 1-N: Re-verification Loop (queue-pruned)](#round-1-n-re-verification-loop-queue-pruned)
  - [Convergence Test](#convergence-test)
- [Verification Mode](#verification-mode)
- [Adversarial Verification Mode](#adversarial-verification-mode)
- [Re-verification Dispatch](#re-verification-dispatch)
- [Convergence State Artifact](#convergence-state-artifact)
- [Coverage critic pass](#coverage-critic-pass)
- [Acceptance critic pass (final-verification)](#acceptance-critic-pass-final-verification)
- [Output](#output)
- [Convergence Disabled](#convergence-disabled)
- [Plan-body verification mode (implementation-planning only)](#plan-body-verification-mode-implementation-planning-only)

## Scope and Terminology (BLOCKING)

This contract governs **Phase 5.5 (Convergence loop)** — a *lead operating phase* inside a single okstra run, not a task-type lifecycle phase. It leaves the 7 task-type lifecycle phases (`requirements-discovery` → `error-analysis` → `implementation-option-selection` → `implementation-planning` → `implementation` → `final-verification` → `release-handoff`, see [okstra-lead-contract](./okstra-lead-contract.md) "Lifecycle Phase Boundaries") unchanged; the lead operating phases (Phase 1 Intake → Phase 7 Persist, see [okstra-lead-contract](./okstra-lead-contract.md) "Quick Reference") drive a *single* task-type run.

**`contested` is a final classification only.** It is NEVER an intermediate queue label. The verification queue carries findings that are *unique to a single worker* (entered in Round 0) or *mixed/unresolved after a re-verification round* (carried forward). The `contested` label is assigned only when the **last executed round** completes and the queue is still non-empty.

When this contract says "queue" without qualifier, it means the *verification queue*: the set of findings that are still candidates for re-verification in subsequent rounds. The queue shrinks monotonically as findings get classified as `full-consensus`, `partial-consensus`, or `worker-unique`. Findings classified into any of these three categories MUST NOT appear in any subsequent round's reverify prompt, for any worker.

An initial pane role `verifier` is still a Phase 4/5 analysis worker; it does not mean Phase 5.5 reverify. Only a queue-scoped dispatch whose prompt/result path carries `-reverify-r<N>-` performs the reverify step described by this contract.

The end-to-end artifact lifecycle is worker results → Round 0 grouping → reducer-owned queue → analyser-instance re-verification → optional `okstra convergence apply-critic-gaps` transition → validated terminal state (newly finalized v1.3, or unchanged historical v1.0–v1.2 from `reuse-final`) → report-writer narrative → deterministic report assembly. Cross-verification is queue-scoped: it does not mean that one worker reviews another worker's complete result. The reducer asks independent analyser instances to vote only on non-consensus findings selected by the persisted plan; the report writer never votes.

Initial and reverify worker prompts carry `**Audit sidecar path:**`. Initial workers write their reading confirmation there; reverify workers use their own canonical sidecar for the reverify session without reopening the initial worker's full reading packet.

## When to Use

- When okstra lead Phase 5.5 (convergence loop) begins — immediately after all workers complete Phases 4 and 5
- When findings need systematic classification by consensus level

## Configuration

Configure this in the `convergence` block of `task-manifest.json`. If the block is missing, the default values are used.

| Setting | Default | Description |
|------|--------|------|
| `enabled` | `true` | If `false`, skip the convergence loop and use the existing consensus/divergence method |
| `maxRounds` | phase-aware: `1` for `requirements-discovery`, `2` otherwise (range 1–3) | Maximum number of re-verification rounds. Discovery's routing/missing-input outputs gain little from a second round; other phases (especially `error-analysis`) keep `2`. Lead resolves the effective value when the manifest omits the key and records it in `config.effectiveMaxRounds` of the convergence state artifact. |
| `verificationMode` | `"lightweight"` | `"lightweight"` or `"full-reanalysis"` |
| `adversarial` | phase-aware: `true` for `requirements-discovery` / `error-analysis` / `implementation-option-selection` / `implementation-planning` / `project-analysis` / `feature-analysis` / `change-impact-analysis`, `false` otherwise | When `true`, Phase 5.5 runs in **adversarial mode** (see §"Adversarial Verification Mode"): verifiers actively try to refute each finding, the burden of proof sits on the claim, and `verificationMode` is forced to `"full-reanalysis"` scoped to the finding's cited evidence. Resolved by `scripts/okstra_ctl/render.py` `_build_convergence_block` and recorded in `config.adversarial` of the convergence state artifact. |

**Auto-disable rule (BLOCKING).** Convergence requires ≥2 analyser workers to produce a meaningful consensus tally. When the active profile's `Required workers:` block (see `prompts/profiles/*.md`) resolves to fewer than 2 analyser workers — e.g. `release-handoff` (zero analyser workers, lead-only) — the lead MUST treat `convergence.enabled` as `false` for that run regardless of manifest configuration, skip Phases 5.5 and the plan-body verification round ([plan-body-verification](./plan-body-verification.md)), and record `finalState: "converged"` with `totalRounds: 0`, `round2SkippedReason: "auto-disabled"`, an empty `roundHistory`, and an explanatory note in `config` (e.g. `"autoDisabled": "fewer-than-two-analysers"`). The plan-body round inherits the same rule via its `gating=false` advisory path.

## Finding Category

| Category | Definition | Included in Report |
|------|------|------------|
| `full-consensus` | All participating workers agree | Required |
| `partial-consensus` | Majority of workers agree; dissenting opinions are recorded | Required |
| `contested` | Final classification only. Assigned to a finding that remains in the verification queue after the **last executed round** completes (round index = `effectiveMaxRounds`). Each worker's position across all executed rounds is recorded. NEVER used as an intermediate label. | Required |
| `worker-unique` | Only the discoverer confirms and ALL other non-error votes are `DISAGREE`. `verification-error` votes are excluded from the tally per §"Worker failure handling in reverify"; a finding where every non-discoverer vote is `verification-error` is carried forward, never classified `worker-unique`. | Required |

## Convergence Algorithm

**Majority definition (BLOCKING).** "Majority" means *strictly greater than half* of the non-error votes for that finding (`verification-error` votes are excluded from both numerator and denominator). Ties — including the 1-AGREE / 1-DISAGREE case in a two-analyser roster — are NOT a majority: in intermediate rounds the finding is **carried forward**; in the final executed round the finding is classified `contested`. This rule applies identically to the plan-body verification round ([plan-body-verification](./plan-body-verification.md)) where the same verdict tokens are reused.

### Round 0: Parse worker results

Read the worker result files generated in Phase 4/5 and extract individual findings.

**Convergence scope.** Convergence operates on sections 1–5 of the worker output (the common core, see the worker preamble §"Worker output sections"). Section 6 ("Specialization Lens") is additive worker-specific depth and MUST NOT be fed into the consensus grouping, the verification queue, or the round-N reverify prompts. Carry Section 6 forward into the final report verbatim through the report-writer worker — do not let it inflate `unique` counts or trigger spurious `verification-error` statuses.

**Incremental re-verification scope (implementation-planning clarification re-runs).** When the lead's `okstra incremental-scope` decision is `mode == "incremental"` (procedure in `prompts/launch.template.md` §"Clarification Response Carried In"), only findings the lead attributes to a stage in `reverify_stages` enter the verification queue. Findings and plan-item verdicts carried forward for `carry_stages` are NOT re-queued. The report writer preserves those stage rows in its narrative, and `okstra incremental-carry` verifies that they are unchanged before copying their prior verdicts into the convergence-owned plan state. When the decision is `mode == "full"` (the default), every finding enters the queue as usual.

1. In the "Findings" section of each worker's results, identify individual items by number (F-001, F-002, ...) and parse the ticket identifier attached to each item:
   - For table-form findings, read the `Ticket ID` column.
   - For bullet/numbered findings, parse `[TICKETID: <id>]` from the item title.
   - Items with multiple tickets (e.g. `TICKET-123, TICKET-456`) expand to a set of ticket keys.
   - Items tagged `unknown` keep the literal `unknown` as their ticket key.
2. For each finding, record the summary, evidence (file path, line number, basis), the discovering worker, **the worker-internal item ID that worker assigned** (e.g. `F-001`, `1.1`, `F-3` — see `prompts/profiles/_common-contract.md` "Cross-worker traceability" SSOT), and the parsed ticket set. Persist the item ID as `findings[].discoveredBy.<worker>.itemId` and each cross-worker confirmation as `findings[].sourceItems[]` (one entry per contributing `<worker>:<item-id>` pair). The final-report `## 6.1 Consensus` / `## 6.2 Differences` / `## 2.1 Primary Evidence` tables read this verbatim into their `Source items` columns; without it the synthesised `C-NNN` row loses its link back to the original worker wording.
3. The lead groups findings based on semantic similarity AND ticket-set equality:
  - Same semantics + same ticket set across 2+ workers → one multi-source group.
  - Same semantics but disjoint ticket sets → separate groups (do NOT over-merge across tickets).
  - Only one worker confirms a finding → one single-source group.
4. When grouping is ambiguous, prefer splitting over merging (avoid over-merging). Semantic matching, ticket-set equality, and evidence interpretation remain lead judgments; the engine does not perform fuzzy matching or decide whether evidence is credible.
5. Author the fixed grouping Markdown accepted by `okstra convergence prepare-groups --run-manifest <run-manifest> --input <grouping.md>`, then run that command. Python owns the artifact identifier, target path, schema version, task identity, run-manifest reference, and every participant reference. Each Markdown group records ticket IDs, origin worker and evidence, discovering workers, source worker item IDs, and optional captured evidence. An analysis sidetrack with no ticket uses an empty `Tickets:` value, never a placeholder. Use the ordered functional roster: finding workers have the `analysis` audience, the report author has `report-writer`, and the lead uses `lead`. A lead source never votes. Never infer live evidence or functional scope from wording, provider, model, or execution label.

   The command sets each worker's paired `participantRef` and `sourceRoleExecutionRef` from the run manifest's canonical role state. It sets `sourceRoleExecutionRef` to the selected source `RoleExecution` row's `roleExecutionRef`, not that row's `sourceRoleExecutionRef` field.
6. Do not write a queue or classification in this grouped-input artifact. `okstra convergence seed` classifies Round 0 by mode:
   - Collaborative mode: multi-source groups become `full-consensus` immediately; only single-source groups enter the working queue.
   - Adversarial mode: every finding enters the working queue regardless of source count. Semantic grouping merges provenance only; it does not decide a finding is reliable.
   Section 6 never enters the grouped input.

### Round 1-N: Re-verification Loop (queue-pruned)

The `ConvergenceEngine` reducer owns the working queue, classification strategy, pruning, round arithmetic, gate precedence, skip reason, final state, and classification counts. The lead owns only semantic grouping plus translation of observed worker outcomes into the structured round-results schema. Runtime adapters transport persisted batches and terminal outcomes only.

Working artifacts are stored beside the final state:

```text
convergence-groups-<task-type>-<seq>.json
convergence-work-<task-type>-<seq>.json
convergence-round-<N>-plan-<task-type>-<seq>.json
convergence-round-<N>-results-<task-type>-<seq>.json
convergence-<task-type>-<seq>.json
```

Follow this protocol exactly:

0. Version-selected schemas describe what the reducer reads: `schemas/convergence-groups-v1.0.schema.json` accepts only legacy groups, `schemas/convergence-groups-v2.0.schema.json` accepts only explicit v2 execution identity, and `schemas/convergence-round-results-v1.0.schema.json` feeds step 4's `apply-round --results`. `schemas/convergence-critic-results-v1.0.schema.json` is a fourth shape but **not** a reducer input — it describes the critic worker's own result document. Step 6's `apply-critic-gaps --results` takes the coverage batch you assemble from those candidates plus each analyser's vote (`{schemaVersion, taskKey, mode, provider, modelExecutionValue, dispatches[], gaps[]}`, spelled out in §"Coverage critic pass" §"State"); feeding the critic document straight in is rejected, by design. `okstra convergence example --kind <groups|round-results|critic-results>` prints a deterministic valid v1 instance of each and writes only JSON to stdout.
1. Run `okstra convergence seed --groups <groups> --run-manifest <current-run-manifest> --work-state <work> --final-state <final> --migration-dir <state/migrations>` for a v2 worker roster. `--run-manifest` must be the exact current manifest named by the groups document's `runManifestPath`; a previous run from the same task is not interchangeable. Omit the flag for a legacy v1 roster. A `reuse-final` action means validate the existing final and continue to Phase 6. `create-work`, `resume-work`, and `restart-round0` continue with planning.
2. Run `okstra convergence plan-round --work-state <work> --plan <round-plan>`. This is read-only with respect to the working state.
3. When the plan action is `dispatch`, create exactly one reverify prompt for each `dispatches[]` row and dispatch it through the selected runtime adapter. Its findings are exactly that row's `findingIds`.
4. Convert parsed verdicts and every terminal dispatch outcome into `convergence-round-<N>-results-<task-type>-<seq>.json`; then run `okstra convergence apply-round --work-state <work> --plan <round-plan> --results <round-results>`.
5. Repeat `plan-round` and `apply-round` until the plan action is `finalize`.
6. When the coverage critic is enabled, convert its verification batch to the canonical `dispatches[]` / `gaps[]` result shape and run `okstra convergence apply-critic-gaps --work-state <work> --results <critic-results>` exactly once. The reducer, not the lead, merges verified gaps.
7. Run `okstra convergence finalize --work-state <work> --output <final>`.
8. Run `okstra convergence validate --state <final> --kind final`. Newly finalized convergence output is schema v1.3. Under `reuse-final`, a valid historical final schema v1.0, v1.1, or v1.2 remains consumable by the report-writer without rewrite; do not finalize, upgrade, or otherwise rewrite that reused artifact. Deliver the validated terminal state to the report-writer, which does not vote.

The planner preserves roster and queue order, excludes that finding's origin worker, emits at most one batch per analysis worker per round, and ensures the report-writer never appears in `dispatches` or `skippedWorkers`. Queue pruning is monotonic: a finding absent from the current queue cannot reappear in a later plan.

After `seed`, the lead MUST NOT hand-edit queue IDs, classifications, `roundHistory` arithmetic, `round2SkippedReason`, `finalState`, `totalRounds`, or `finalClassificationCounts`. The persisted plan is the assignment audit record; adapters and leads may not recalculate it. `scripts/okstra_ctl/convergence_engine.py` and `okstra convergence validate` enforce these rules.

#### Resume and legacy-state recovery (BLOCKING)

A valid historical final schema v1.0, v1.1, or v1.2 is reused unchanged under `reuse-final`; it remains read-only and is not upgraded in place. A malformed, unreadable, or partial legacy final is archived byte-for-byte under `state/migrations/` and restarted from Round 0 because its queue provenance cannot be reconstructed. A matching valid working state resumes. A malformed new-engine working state fails closed and names the explicit `--restart-from-round0` recovery flag; when supplied, every invalid existing state is archived before replacement. The final path can be replaced only when its recorded migration archive still matches the original bytes.

#### Engine-owned Round 2 gate

`plan-round` applies gate precedence in one place: auto-disabled, all reverify non-result, effective maximum of one, empty queue, then maximum rounds reached. `finalize` maps that internal reason to the public `round2SkippedReason` and `finalState`. The lead and adapters never reproduce this predicate.

#### Worker failure handling in reverify (BLOCKING)

A reverify dispatch that returns a **terminal non-result** (`timeout`, `error`, no result file, or the wrapper records `cli-failure`) MUST NOT be aggregated as `DISAGREE`. Misclassifying a worker failure as DISAGREE biases the queue toward `contested`/`worker-unique` and produces meaningless final classifications.

Rules:

1. For each failed dispatch, put its actual terminal status and duration in the round-results `dispatches[]`; do not invent a vote. `okstra convergence apply-round` appends `votes[W].verdict = "verification-error"` with the terminal reason for every affected finding. A completed dispatch may separately return `UNVERIFIABLE` for a particular finding; the input alias is persisted as `verification-error` with its required non-empty explanation while the dispatch remains `completed`.
2. Record one event per failed dispatch via `okstra error-log append-observed --error-type cli-failure --agent <worker> ...` (the worker wrapper does this for wrapper failures; for in-process worker timeouts the lead does it).
3. `apply-round` adds the worker to the persisted round's `skippedWorkers[]` with `{worker: <W>, reason: "dispatch-non-result", terminalStatus: <timeout|error|not-run>}`.
4. If at least one dispatch was issued and every dispatch terminates as non-result, `apply-round` records the `all-reverify-non-result` stop state. The next `plan-round` returns `action: "finalize"`; record one `contract-violation` event per non-result dispatch.
5. Section 6 (Specialization Lens) of a worker output is OUT of convergence scope per "Convergence scope" above — its absence is NEVER a `verification-error`.

The engine's classifiers treat `verification-error` as "no usable vote" — it counts neither toward AGREE nor toward DISAGREE and is excluded from both numerator and denominator.

The public shorthand is `UNVERIFIABLE → verification-error`: `UNVERIFIABLE` is the worker-facing input token, while `verification-error` is the persisted state token. This mapping records unavailable evidence honestly and never turns inability to verify into a negative vote.

### Convergence Test

The executable source is `scripts/okstra_ctl/convergence_engine.py`; `okstra convergence validate --kind final` replays its persisted-state invariants. This document defines orchestration and semantic boundaries, not a second implementation of the reducer.

## Verification Mode

### Lightweight (Default)

Decide solely on the findings and evidence other reviewers present; do not reanalyze the original code or data. Fast and cost-effective, but accuracy drops when evidence is insufficient.

### Full Re-analysis (opt-in)

Use each finding as a guide but reanalyze the original code/data yourself. High accuracy at 2–3× the cost and time.

## Adversarial Verification Mode

Active only when `config.adversarial == true` (default for `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `project-analysis`, `feature-analysis`, and `change-impact-analysis`; see §"Configuration"); when `false`, every rule in this section is inert and the collaborative behaviour elsewhere in this contract applies unchanged. In adversarial mode the verifier's job inverts: instead of confirming a peer's finding, the verifier **tries to break it**, and the burden of proof sits on the claim — a finding survives only if refutation attempts fail.

### Read-only analysis task contract

For `project-analysis`, `feature-analysis`, and `change-impact-analysis`, every analysis worker independently analyses the full confirmed target. Provider or model diversity is an independent evidence source, never a reason to split the target into disjoint worker assignments. Only the `project-analysis` first exploration pass may divide navigation by component; every worker then returns to the whole confirmed target before producing findings.

A single evidence-backed refutation makes the affected finding `contested` while that refutation remains unresolved. Lead MUST NOT use majority voting to override it and MUST NOT promote a lead-only finding into confirmed facts. The report writer records current-run refutations and resolutions from the convergence state under `crossVerification`. `analysisReviewResolution` is reserved for a prior report's `## ANALYSIS REVIEW` carry-in and MUST remain empty without that carry-in. A `still-unresolved` carry-in item cannot appear in `analysisCommon.confirmedFacts`. **Enforcement:** `validators/validate_analysis_report.py` rejects non-empty `analysisReviewResolution` without a prior review and rejects a still-unresolved reviewed ID that appears in confirmed facts; the convergence-state validator preserves the engine's `contested` classification.

If every required analysis worker produces a non-result, the run verdict is `blocked`; Lead synthesis is not a worker result. A partial worker failure stays in `executionStatus`, but it does not by itself change the deterministic `analysis-complete` / `analysis-partial` scope verdict. **Enforcement:** `validators/validate_analysis_report.py` recomputes these verdict conditions from structured `data.json`.

### Scoped full-reanalysis

Adversarial mode forces `verificationMode = "full-reanalysis"`, but the re-analysis is **scoped to the evidence the finding under attack cites** (the file paths / line ranges / log lines in its `originEvidence`), plus the immediately surrounding context. The verifier MUST NOT re-read the whole task brief, instruction-set, or `final-report-template.md`. This keeps the documented "single largest avoidable cost in requirements-discovery, error-analysis, and implementation-planning" (see §"Reverify prompt: required-reading suppression") bounded while making the refutation real rather than a text-only argument.

### Adversarial verdict semantics

The persisted `verdict` enum is unchanged (`agree | disagree | supplement | verification-error`). The prompt-facing labels are adversarial and map down on persistence:

| Prompt label | Persisted `verdict` | Meaning |
|---|---|---|
| SURVIVES | `agree` | Actively tried to refute and failed — the claim withstood the attack. |
| SURVIVES-WITH-CAVEAT | `supplement` | Holds, but a scope limit / extra condition / precondition was found. |
| REFUTED | `disagree` | The claim was broken (or failed to prove itself). MUST carry a `disagreeBasis`. |
| UNVERIFIABLE | `verification-error` | Capability, credential, network, or service state prevented verification of this finding. |

Each `disagree` vote records a new field `disagreeBasis`:

| `disagreeBasis` | Meaning |
|---|---|
| `counter-evidence` | The verifier opened and inspected the cited evidence and found a contradiction (`file:line` / log line) recorded in `explanation`. A **hard refute**. |
| `burden-not-met` | The verifier opened and inspected the cited evidence, but its contents were insufficient to establish the claim. |

A `disagree` with `disagreeBasis == null` is a contract violation in adversarial mode — every refutation must state which of the two grounds it rests on. Bare "I disagree" without re-inspection is not allowed. If capability, credential, network, or service state prevents that inspection, the verdict is `UNVERIFIABLE`, persisted as `verification-error`; verifier failure is never converted to `DISAGREE` or `burden-not-met`. A live/external claim without `evidenceArtifacts` remains schema-valid, but a verifier that needs the missing artifact and cannot independently access the source MUST answer `UNVERIFIABLE` with a non-empty explanation.

### Adversarial classification (replaces the §"Convergence Algorithm" per-round classifier when `adversarial == true`)

`verification-error` votes are excluded from numerator and denominator exactly as in the collaborative classifier. For each finding `F` in the queue at a round:

```text
disagrees    = [v for v in non-error votes if v.verdict == "disagree"]
hard_refutes = [v for v in disagrees if v.disagreeBasis == "counter-evidence"]
all_others_disagree = (every non-discoverer non-error vote is "disagree")

IF len(disagrees) == 0:
    resolve F as "partial-consensus" if SUPPLEMENT/caveat votes are a majority of
    non-error votes, otherwise "full-consensus"
ELIF all_others_disagree:
    resolve F as "worker-unique"    # only the discoverer still holds it
ELIF len(hard_refutes) >= 1:
    # an evidence-backed refute exists and the roster is split → the claim is disputed
    carry F forward; at the LAST executed round classify it "contested"
ELIF burden-not-met disagrees are a majority of non-error votes (per the Majority definition in the Convergence Algorithm section):
    carry F forward; at the LAST executed round classify it "contested"
ELSE:
    # a lone weak (burden-not-met) doubt against an otherwise-surviving claim
    resolve F as "partial-consensus"
```

`contested` remains a **final classification only** (per §"Scope and Terminology"): a disputed finding is carried forward through intermediate rounds and labelled `contested` only at the last executed round. For `requirements-discovery` (`effectiveMaxRounds = 1`) the single round IS the last round, so a split-with-hard-refute finding is labelled `contested` in that one round. The final-classifier block of §"Convergence Algorithm" honours this: its first branch classifies an adversarially carried-forward finding `contested` regardless of the AGREE tally, so the two sections cannot assign the same finding different labels.

Design intent: one `counter-evidence` refute denies a claim consensus (it cannot rise above `contested` however many others AGREE); later-round agreement does not erase that refutation history. The only resolution that overrides prior `counter-evidence` is a later round where every non-discoverer non-error worker disagrees, producing `worker-unique`. A lone `burden-not-met` doubt does not sink an otherwise-surviving claim — only a majority of them does. When every non-discoverer refutes (all_others_disagree) the finding is worker-unique regardless of refute basis — only the discoverer still holds it. A caveat is weighed the same way a weak doubt is: with zero disagrees, SUPPLEMENT lands partial-consensus only when caveats are a **majority** of the non-error votes. A single verifier's scope note does not by itself deny a claim the rest of the roster passed cleanly — the caveat is still recorded in the dissent log either way, so the majority rule changes the label, never the record. (The collaborative classifier is more permissive still: there SUPPLEMENT counts as full agreement at any count.)

## Re-verification Dispatch

### Invocation materialization gate (BLOCKING)

For every finding reverify row and critic-gap verification row, first write a
call-specific task-instructions file under the current run's `state/`
directory. For a v2 run, take `participantRef` and
`sourceRoleExecutionRef` from the canonical round-plan dispatch row or the
matching canonical working-state worker row. That stored reference is the
selected source `RoleExecution` row's `roleExecutionRef`, not that row's
`sourceRoleExecutionRef` field. Then run `okstra agent-prompt
materialize` with `--audience
reverification-worker`, `--assignment-ref reverify/<workerId>`,
`--source-role-execution-ref
<sourceRoleExecutionRef>`, the exact `--worker-id`, `--dispatch-kind
reverify-r<N>`, and the authorized prompt/result/audit paths. Do not derive the
source role execution from `workerId`, provider, model, or execution-label
text. A legacy v1 run omits `--source-role-execution-ref`; the materializer
keeps its legacy read-only identity resolution for that schema version. The
returned `promptPath` is the only body that may be dispatched; do not append
role prose or reconstruct model headers after materialization.

If the dispatch gate then rejects that prompt, preserve its prompt, metadata,
reservation, and append-only Invocation bytes. A v2 reverify correction uses a
fresh `--invocation-id` and fresh prompt/metadata path; the materializer rejects
`--replace-undispatched` for this dynamic identity path. A legacy v1 correction
may fix the task-instructions file and re-run the same `materialize` call with
`--replace-undispatched` while keeping the `--invocation-id`. Once any dispatch
row names the invocation, replacement is rejected even for v1 and the prompt is
history. Do not delete prompt, metadata, or reservation files by hand.

Run `okstra agent-prompt verify --run-manifest <path> --metadata
<metadataPath> --text` immediately before dispatch. A failed verification is a
pre-dispatch contract failure. For `runner=native-session`, pass only the
returned `hostModelValue` to the host model argument. For
`runner=cli-wrapper`, follow the planned execution surface after
`core-pre-dispatch` verification: invoke `okstra team dispatch` when
`terminalBackend` is `cmux-pane`, otherwise invoke `okstra worker-dispatch`
and let it consume the returned `modelExecutionValue`; never pass that
value as a native-host model token. Before a host-native call, run `okstra agent-prompt record-dispatch`
with the project root, run manifest, metadata path, and
`--enforcement-mode host-native-spec-link-gate`. After its Result Path exists,
run `okstra agent-prompt link-result` with the same run manifest,
`--dispatch-id <invocationId>:attempt-1`, and that result path before reading
the result. This link proves that the accepted result belongs to a verified
call specification; it does not prove which bytes the host primitive delivered.

### Sponsorship Optimization

For each persisted round plan, build exactly one prompt per `dispatches[]` row and call `redispatch_worker(assignment, prompt, reason)` once through the selected runtime adapter. The prompt contains exactly that row's `findingIds` in plan order and MUST NOT add, remove, or reorder findings. This excludes Section 6, every resolved finding, and every finding owned by the receiving origin worker because none can appear in the engine row. The assignment, model, prompt path, Result Path, worker-results path, errors paths, and `dispatchKind` come from the current run artifacts. Every reverify is a fresh one-shot session.

The persisted round plan is the audit record for batch membership. The lead and adapter do not branch on task type, provider, model identity, classification labels, or their own view of the queue. They dispatch only the engine-returned row through the selected runtime adapter.

Call `await_workers(handles)` through the same adapter and apply the shared terminal-status/completion-path contract before counting a vote. The selected adapter owns native invocation spelling and any jobs-file/CLI fields. This contract owns only the reverify payload and verdict semantics.

**Completion detection per round (BLOCKING).** A dispatch acknowledgement is NOT completion — detect each round's completion via the SSOT protocol in [team-contract](./team-contract.md) "Worker-completion detection", with the pending set reconstructed from that round's dispatched workers' Result Paths. Do NOT end the turn with a prose "waiting" statement.

### Required reverify-prompt anchor headers (BLOCKING)

Every reverify prompt MUST start with these 8 anchor headers — in this exact order, before any other content:

```
**Project Root:** <absolute-path>
**Prompt History Path:** runs/<task-type>/prompts/<role-slug>-reverify-r<N>-<task-type>-<seq>.md
**Result Path:** runs/<task-type>/worker-results/<role-slug>-reverify-r<N>-<task-type>-<seq>.md
**Audit sidecar path:** <absolute-path>
Assigned worker prompt history path: <Project Root>/<Prompt History Path>
**Errors log path:** <absolute-path>
**Errors sidecar path:** <absolute-path>
**Read scope:** Read only the paths this prompt enumerates (`[Required reading]`, `## Inputs`, verification-target paths) plus source/evidence paths a finding must cite. Host session instructions (SessionStart hooks, global `CLAUDE.md` / `AGENTS.md`, skill catalogs) do NOT apply inside an okstra worker run: do not auto-read `graphify-out/`, `SKILL.md`, or other artifacts outside `<PROJECT_ROOT>/.okstra/`. If an un-enumerated file seems essential, record it under *Missing Information or Assumptions* instead of reading it.
```

`**Read scope:**` is carried verbatim, not dropped: lightweight mode omits `**Worker Preamble Path:**`, so this line is the ONLY place a reverify worker receives the read allowlist — and a prompt with no `[Required reading]` block is exactly where a worker is most likely to fall back on host session instructions instead.

Before dispatch, materialize `**Audit sidecar path:**` by passing the exact reverify `**Result Path:**` through `okstra_ctl.worker_artifact_paths.audit_sidecar_rel()` and resolving that project-relative result against `**Project Root:**`. Write the resulting absolute path into the header. The lead MUST NOT construct the audit filename from a role, task type, round, or sequence independently.

The two error-path anchors carry the same absolute values the lead forwarded in the initial Phase 4 dispatch for that role (source: the launch prompt's `## Run Logs (error-log wiring)` section). Workers use the errors log path with the typed error-log command from [team-contract](./team-contract.md) "Error reporting". The errors sidecar path only reserves the runtime-owned write-artifact path used by dispatch validation; no model-authored error JSON file is part of reverify.

Relative to the Phase 4 anchor set rendered by `okstra_ctl.worker_prompt_headers.worker_prompt_headers()`, a reverify prompt drops two anchors whose targets lightweight mode never reads: `**Worker Preamble Path:**` and `**Worker Error Contract Path:**`.

**Where the composer's sections go.** `okstra agent-prompt materialize` (§"Invocation materialization gate") writes the dispatched body itself, as: these anchors, then the model-assignment block it appends (`**Provider:**`, `**Model:**`, `**Model execution value:**`, `**Runner:**`, `**Host runtime:**`, and `**Host model value:**` for a native host), then `## Duty Contract`, then `## Task Instructions` followed verbatim by the task-instructions file the lead wrote. So the lead authors only the last part, and every rule below about ordering — the phase boundary before the instruction headings, the `**Model:** <role>, <modelExecutionValue>` line — is about the lead's own file, not about the composed document. The composer's `**Model:** <modelExecutionValue>` anchor is a different line with a different shape; do not try to reshape it, and do not count it among the 8.

For an `antigravity` assignment, append the exact `PLAIN_FILE_WRITE_HEADER`
value from `okstra_ctl.worker_prompt_headers` immediately after
`**Read scope:**` and before the prompt body. Copy it verbatim from that role's
persisted initial Phase 4 prompt; do not paraphrase or reconstruct it. If the
persisted initial prompt does not contain that generated header, abort the
reverify dispatch and record a `contract-violation` event instead of
dispatching without the plain-file safeguard. This provider-specific header is
outside the common 8-header count above. Other providers do not receive it.

The rationale for both drops is §"Reverify prompt: required-reading suppression" below.

The task-instructions file the lead writes MUST open with this block, before any
`##` heading of its own:

```markdown
**Model:** <role>, <modelExecutionValue>
**Task Type:** <task-manifest taskType>
**Forbidden actions:**
<active-run-context workflow.forbiddenActions, verbatim>
```

This is the same placement `okstra_ctl.worker_prompt_body` uses for an initial
Phase 4 prompt, and it is where the checks look: `validate_reverify_prompt()`
reads the region after `## Task Instructions`, so a `**Model:**` line left in
the anchors is invisible to it, and a phase boundary written below the file's
first heading fails. Both rules are about this file — the composer's own
`## Duty Contract` heading sits above everything here and is not what they
measure against.

Do not summarize, shorten, or reconstruct the forbidden-actions text. The
selected adapter validates the task type and exact block through
`okstra_ctl.worker_prompt_contract.validate_reverify_prompt()` before starting
the wrapper. `validators/validate-run.py` separately fails the run when the
run-level error log records a phase-boundary `contract-violation`; a correct
finding does not make evidence obtained across the phase boundary admissible.

`<modelExecutionValue>` MUST be resolved from one of these canonical sources, in priority order:

1. `task-manifest.json` → `resultContract.requiredWorkerRoles[].modelExecutionValue` for the receiving role
2. `team-state-<task-type>-<seq>.json` → `workers[].usage.cliModel` for that role (initial run's actual execution value)
3. The `**Model:**` line of the initial Phase 4 prompt for that role (read from its persisted prompt-history file)

If none of the three is available, **abort the reverify dispatch for that role** and record a `contract-violation` event via `okstra error-log append-observed`. Do NOT guess or fall back to a runtime default. The selected adapter receives the exact value through the assignment and owns how it reaches the worker runtime. The current Codex catalog default is `gpt-5.6-sol`, but that reference value never replaces the manifest assignment.

**Enforced:** `okstra_ctl.dispatch_state` hands the job's own `modelExecutionValue` to `validate_reverify_prompt(expected_model=...)`, so a `**Model:**` header that disagrees with the model this dispatch will run fails **before** launch. Carrying a stale value from an earlier round's prompt — the round-1 model into a round-2 header — is the shape this catches; without it the mismatch surfaces as a provider 400 that reads as a worker fault. `tests/contract/test_reverify_dispatch_anchors.py` pins both the check and the dispatch-side hand-off.

### Required reverify output contract (BLOCKING)

Every lightweight, adversarial, full-reanalysis, and plan-body reverify prompt
MUST append this block verbatim after its variant-specific response format:

```markdown
## Output Contract

- Write the complete response to the exact `**Result Path:**` before returning.
- Write the reverify-session audit to `**Audit sidecar path:**` before returning; list the evidence paths actually inspected, or state that the prompt required no external evidence reads.
- Do not return only an inline response. Inline text is diagnostic output, not the persisted worker result.
```

### Reverify prompt: required-reading suppression

Reverify prompts MUST NOT inject the Phase 2 `[Required reading]` clause:

Lightweight reverify does not require the original `analysis-packet.md`, `analysis-profile.md`, `task-brief.md`, or instruction set. Its complete input is the receiving worker's current engine-planned `findingIds` batch and the evidence embedded in those findings.

- **Lightweight mode**: the clause directly contradicts the "Do NOT re-analyze the original source materials" instruction below. Including it forces workers to re-read the entire instruction-set per round per worker (3 workers × 2 rounds × 5+ files in the worst case) for no quality gain.
- **Full-reanalysis mode**: workers DO need to re-read source materials, but only the analysis-worker file list (no `final-report-template.md`). If lead chooses to inject a reading clause here, it MUST mirror the audience-scoped enumeration in [okstra-lead-contract](./okstra-lead-contract.md) Phase 2 (no template).

This is the single largest avoidable cost in `requirements-discovery`, `error-analysis`, `implementation-option-selection`, and `implementation-planning` runs. Treat as mandatory.

### Lightweight Re-verification Prompt

```
Perform re-verification for <task-key> (round <N>).

## Instructions

Review the following findings discovered by other workers.
For EACH finding, respond with exactly one verdict:

- **AGREE**: The finding is valid based on the evidence presented
- **DISAGREE**: The finding is incorrect or unsupported (explain briefly why)
- **SUPPLEMENT**: The finding is valid AND you have additional supporting evidence or context
- **UNVERIFIABLE**: Capability, credential, network, or service state prevents you
  from checking this finding. Explain the unavailable capability; do not substitute DISAGREE.

Do NOT re-analyze the original source materials. Judge based on the evidence provided.

## Findings to verify

### F-001: <one-line summary>
**Origin**: <worker role>
**Evidence**: <file paths, line numbers, reasoning from origin worker>

### F-002: <one-line summary>
**Origin**: <worker role>
**Evidence**: <...>

## Response format

For each finding, respond as:

### F-001
**Verdict**: AGREE | DISAGREE | SUPPLEMENT | UNVERIFIABLE
**Explanation**: <2-3 sentences>

### F-002
**Verdict**: ...
```

### Adversarial Re-verification Prompt

Used instead of the lightweight/full-reanalysis prompt when `config.adversarial == true`. The required anchor headers (§"Required reverify-prompt anchor headers") are identical. The `[Required reading]` clause is suppressed; only the cited-evidence paths of the items under attack are injected (see §"Adversarial Verification Mode" → Scoped full-reanalysis).

```
Perform ADVERSARIAL re-verification for <task-key> (round <N>).

## Instructions

Your job is to BREAK each finding below, not to confirm it. For EACH finding,
open the cited evidence directly and actively search for evidence that the claim
is wrong, overstated, or unproven. Then respond with exactly one verdict:

- **REFUTED**: You broke the claim. State the basis:
  - counter-evidence — you found contradicting evidence (give file:line or log line), OR
  - burden-not-met — you re-inspected the cited evidence and could neither confirm
    nor refute it (the claim has not proven itself).
- **SURVIVES**: You actively tried to refute it and failed — the claim withstood the attack.
- **SURVIVES-WITH-CAVEAT**: It holds, but a scope limit / extra condition / missing
  precondition exists (state it).
- **UNVERIFIABLE**: Capability, credential, network, or service state prevents you
  from opening or reproducing the cited evidence. Do not use REFUTED as a substitute.

The burden of proof is on the claim. If after inspecting the cited evidence you remain
uncertain, your verdict is REFUTED with basis = burden-not-met.

Inspect ONLY the evidence each finding cites and its immediate surroundings. Do NOT
re-read the task brief, instruction-set, or report template.

## Findings to verify

### F-001: <one-line summary>
**Origin**: <worker role>
**Cited evidence**: <file paths, line numbers, log lines from origin worker>

### F-002: <one-line summary>
...

## Response format

### F-001
**Verdict**: REFUTED | SURVIVES | SURVIVES-WITH-CAVEAT | UNVERIFIABLE
**Basis** (only if REFUTED): counter-evidence | burden-not-met
**Explanation**: <2-3 sentences; for counter-evidence include the file:line you found>

### F-002
...
```

When persisting votes, map SURVIVES→`agree`, SURVIVES-WITH-CAVEAT→`supplement`, REFUTED→`disagree`, and UNVERIFIABLE→`unverifiable`; copy the stated Basis into `votes.<worker>.disagreeBasis` (null for non-REFUTED verdicts). Every vote requires a non-empty `explanation`.

UNVERIFIABLE is **not** `verification-error`. A verifier that opened the evidence and could not check it participated in the round; a `verification-error` is a verifier that failed to answer. The classifier counts only non-error votes, so folding the two shrinks the participating roster without saying so — a round where one analyser answers UNVERIFIABLE throughout would read as a two-way cross-check while reporting three voters. **Enforced:** `okstra convergence collect-results` applies this mapping (`okstra_ctl.verdict_blocks.ADVERSARIAL_VERDICTS`); do not transcribe votes by hand.

### Full Re-analysis Re-verification Prompt

```
Perform deep re-verification for <task-key> (round <N>).

## Instructions

Independently verify the following findings by examining the original materials.
Use each finding as a starting point, NOT as a confirmed conclusion.
If capability, credential, network, or service state prevents access to evidence
needed for a finding, you MUST answer UNVERIFIABLE with a non-empty explanation.
You MUST NOT answer DISAGREE merely because access or reproduction failed;
UNVERIFIABLE is persisted as `verification-error` and excluded from consensus.

## Task bundle paths
- Instruction set: <instruction-set path>
- Task brief: <task-brief path>
- Analysis material: <analysis-material path>
- Reference expectations: <reference-expectations path>

## Findings to verify

### F-001: <one-line summary>
**Origin**: <worker role>
**Original evidence**: <file paths, line numbers>

### F-002: <one-line summary>
**Origin**: <worker role>
**Original evidence**: <...>

## Response format

For each finding:

### F-001
**Verdict**: AGREE | DISAGREE | SUPPLEMENT | UNVERIFIABLE
**Your evidence**: <your independent evidence trail with file paths and line numbers>
**Explanation**: <detailed analysis>
```

## Convergence State Artifact

Save it to `runs/<task-type>/state/convergence-<task-type>-<seq>.json`.

```json
{
  "schemaVersion": "1.3",
  "taskKey": "<task-key>",
  "config": {
    "enabled": true,
    "adversarial": false,
    "maxRounds": 2,
    "effectiveMaxRounds": 2,
    "verificationMode": "lightweight"
  },
  "findings": [
    {
      "findingId": "F-001",
      "summary": "<one-line summary>",
      "category": "<bug|risk|missing|observation|...>",
      "ticketIds": ["TICKET-123"],
      "originWorker": "claude-worker",
      "originEvidence": "<evidence text>",
      "classification": "full-consensus",
      "rounds": [
        {
          "round": 1,
          "votes": {
            "codex-worker": { "verdict": "agree", "disagreeBasis": null, "explanation": "<brief>" },
            "antigravity-worker": { "verdict": "supplement", "explanation": "<brief>" }
          }
        }
      ],
      "consensusWorkers": ["claude-worker", "codex-worker", "antigravity-worker"],
      "dissentingWorkers": []
    }
  ],
  "roundHistory": [
    {
      "round": 1,
      "inputQueueSize": 3,
      "resolvedCount": 3,
      "carriedForwardCount": 0,
      "dispatches": [
        { "worker": "codex-worker",  "status": "completed", "durationMs": 184221 },
        { "worker": "antigravity-worker", "status": "completed", "durationMs": 201337 }
      ],
      "skippedWorkers": [
        { "worker": "claude-worker", "reason": "no items to verify" }
      ]
    }
  ],
  "round2SkippedReason": "queue-empty",
  "finalState": "converged",
  "totalRounds": 1,
  "finalClassificationCounts": {
    "fullConsensus": 5,
    "partialConsensus": 1,
    "contested": 0,
    "workerUnique": 1
  }
}
```

> Abbreviated example: `findings[]` shows only `F-001` though `finalClassificationCounts` totals 7 — a real artifact has one `findings[]` entry per finding. This is a clean one-round queue-drained run; a Round 2 run adds a second `roundHistory[]` entry of the same shape.

Schema rules:

- `schemaVersion`: literal string `"1.3"` for all new runs — both adversarial and collaborative. Historical readers accept `"1.0"` / `"1.1"` / `"1.2"` unchanged and never rewrite those artifacts during validation. v1.3 adds the strict coverage-critic ledger and rejects unknown top-level fields; work-state remains v1.0.
- `config.adversarial`: boolean. `true` when this run used adversarial verification (default for `requirements-discovery` / `error-analysis` / `implementation-option-selection` / `implementation-planning` / `project-analysis` / `feature-analysis` / `change-impact-analysis`). When `true`, `config.verificationMode` is `"full-reanalysis"` (scoped) and every `disagree` vote carries a non-null `disagreeBasis`.
- `config.effectiveMaxRounds`: the integer the lead actually used after resolving the phase-aware default (`1` for `requirements-discovery`, `2` otherwise). MUST equal `config.maxRounds` when the manifest explicitly set it.
- `findings[].ticketIds`: array of ticket keys from Phase 4 grouping (parsed per the Round 0 step 5 rule). It is empty when the phase does not require ticket tagging; `"unknown"` is not a ticket key and must not be synthesized.
- `findings[].rounds[].votes.<worker>.verdict`: enum, one of `agree | disagree | supplement | verification-error`. Lower-case tokens; map upper-case AGREE/DISAGREE/SUPPLEMENT verdicts emitted by workers to their lower-case form and map the input alias `unverifiable` to persisted `verification-error`. The latter represents either a terminal non-result dispatch or a completed dispatch that could not verify a particular finding (§"Worker failure handling in reverify"). Every vote has a non-empty `explanation`.
- `findings[].rounds[].votes.<worker>.disagreeBasis`: enum `counter-evidence | burden-not-met | null`. Non-null only when `verdict == "disagree"` AND `config.adversarial == true`; `null` (or absent, treated as null) otherwise. See §"Adversarial Verification Mode".
- `findings[].classification`: enum, one of `full-consensus | partial-consensus | worker-unique | contested`. No other value is permitted.
- `roundHistory[].inputQueueSize`: queue size at the start of this round.
- `roundHistory[].resolvedCount`: number of findings that exited the queue this round (sum of full+partial+worker-unique classifications produced this round).
- `roundHistory[].carriedForwardCount`: queue size at the END of this round — the single definition. In-round insertions into the queue are forbidden, so this always equals `inputQueueSize - resolvedCount`. The pseudocode's per-item `carriedForwardCount += 1` accumulator is a counting convenience that lands on the same value; persist the post-round queue length, not the loop accumulator, if the two ever diverge.
- `roundHistory[].dispatches[]`: one entry per worker that was actually dispatched in this round. Each entry is `{worker, status, durationMs}`. `status ∈ {completed, timeout, error, not-run}`. `durationMs` is integer milliseconds and is always present, even for terminal-non-result dispatches (use the elapsed time before the wrapper gave up).
- `roundHistory[].skippedWorkers[]`: per-worker `{worker, reason}` for workers with no items to verify OR with a non-result dispatch.
- `round2SkippedReason`: literal enum `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | auto-disabled`. Always present and derived by `finalize`; the engine owns precedence so the lead never writes it manually.
- `finalClassificationCounts`: post-loop counts. Required field with keys `fullConsensus`, `partialConsensus`, `contested`, `workerUnique`.
- `finalState ∈ {converged, max-rounds-reached, aborted-non-result}`. Derived by `finalize` from the validated queue, history, and stop reason; the lead does not assign it.
- `totalRounds`: count of rounds actually executed (not `effectiveMaxRounds`). May be `0` when Round 0 produced no queue items (all findings reached consensus during grouping).

## Coverage critic pass

Runs when `convergence.critic.enabled == true`. Critic is required on `requirements-discovery`, `error-analysis`, `implementation-planning`, and `final-verification` (role `min`/`max` 1; the user picks the model). `--critic off` is rejected. For `final-verification` the critic runs in a different mode — see §"Acceptance critic pass (final-verification)". This pass targets **scope in both directions** — findings that are missing (coverage) and work the findings propose that no requirement asked for (over-scope) — distinct from convergence, which targets **agreement quality** among the findings already raised. The pass keeps its `coverage` mode id and `gaps` vocabulary for both halves; the two are told apart by each candidate's `category`, so no schema or reducer distinguishes them. In `implementation-planning` the same critic slot also settles plan-body analyser 1-1 splits as `critic-worker`.

### When

The critic input is the Round 0 consolidated finding list. Reverify rounds only classify findings — they never add or remove them (in-round queue insertions are forbidden, see §"Convergence State Artifact" `carriedForwardCount`) — so the critic dispatch MUST NOT wait for classification to finish:

- **Dispatch**: immediately after Round 0 grouping, CONCURRENTLY with the first reverify round's dispatches. When the verification queue is empty after Round 0 (no reverify round runs), dispatch right after grouping. Concurrent dispatch to the same provider is safe — the critic result path (`<provider>-worker-critic-...`) never collides with a reverify result path.
- **Gap verification + merge**: only after BOTH the finding-convergence loop has exited AND the critic result is collected, and BEFORE the Phase 6 report-writer dispatch. If the loop exited `aborted-non-result`, do NOT dispatch a gap-verification round — record every gap in `unverifiedGaps[]` per §"Gap verification".

### Dispatch (fresh one-shot)
Write the critic-only task instructions, then run `okstra agent-prompt
materialize` with `--audience scope-critic`, `--assignment-ref critic/scope`,
the critic worker ID, and `--dispatch-kind critic`. Verify the returned
`metadataPath` before dispatch and use its `promptPath` without modification.
For `runner=native-session`, use only `hostModelValue`; for
`runner=cli-wrapper`, use `okstra team dispatch` when `terminalBackend` is
`cmux-pane`, otherwise `okstra worker-dispatch`, which consumes
`modelExecutionValue`. Record host-native linkage with
`enforcementMode=host-native-spec-link-gate` and the metadata path. If the
persisted assignment or either model value required by its runner is absent,
record `critic-skipped: model-unresolved`; never resolve a replacement model.
Result path: `runs/<task-type>/worker-results/<provider>-worker-critic-<task-type>-<seq>.md`.

**What the critic task-instructions file MUST contain (BLOCKING).** A critic
dispatch is not a reverify dispatch: `dispatchKind = "critic"` keeps
`audience = "analysis"`, so `worker_prompt_contract.validate_initial_prompts`
judges it by the full initial-analysis contract. Two of those requirements are
satisfied by the generated body for a Phase 4 worker
(`okstra_ctl.worker_prompt_body`) and by nothing at all for a critic, whose
instructions the lead writes — the materializer's anchor block supplies neither.
Put both in the instructions file:

```markdown
**Prompt Delivery Mode:** eager-include
```

and, under the file's `## Inputs`, exactly one line in this shape — the literal
label and the backticks are what the check matches, so a bare path or a
differently-worded label counts as zero:

```markdown
- Primary analysis packet: `<path ending in analysis-packet.md>`
```

Omitting either one fails `okstra team dispatch --dispatch-kind critic` before
any process starts, reported as `<task-type> prompt contract: <worker>: exactly
one Primary analysis packet path is required (found 0)` and `exactly one
non-empty **Prompt Delivery Mode:** header is required`. Fix the instructions
file and re-materialize with `--replace-undispatched` (§"Invocation
materialization gate") rather than editing the published prompt.

The `-worker-` token is load-bearing, not decoration: the critic prompt carries the same generated anchor headers as every other worker ([team-contract](./team-contract.md) §"Worker prompts"), and its `**Audit sidecar path:**` comes from passing that result path through `okstra_ctl.worker_artifact_paths.audit_sidecar_rel()`, which inserts `-audit-` after the token and raises without it. A `<provider>-critic-...` name leaves the lead choosing between breaking the contract and hand-inventing the sidecar name. Note that `originWorker` stays `"<provider>-critic"` — that is a worker id in the convergence state, not a filename, and the two do not have to match.

The critic prompt carries the full analysis contract those anchor headers belong to — worker preamble, error-contract path, audit sidecar, packet boundary — but it is **exempt from the initial-analysis equality group**. Initial analysis workers must receive byte-identical normalized bodies (`worker_prompt_contract.validate_analysis_prompt_set`), and a critic body is deliberately unlike them, so `okstra_ctl.worker_prompt_policy.resolve_prompt_plan` resolves `dispatchKind = "critic"` to `equality_group = None` while keeping `audience = "analysis"`. Never reshape a critic prompt to match the initial one to satisfy that check — matching it would delete the pass. **Enforced:** `tests/contract/test_critic_prompt_equality_exemption.py` pins both directions — the critic is exempt, and two mismatched *initial* prompts still fail.

The critic prompt seeds the consolidated findings and asks for two things only — coverage gaps and unrequested work. The second half exists because every other scope check in the lifecycle runs *later*: the plan-body `P-Opt-*` verdict judges options this phase has not authored yet (report-writer writes the plan in Phase 6), and the `implementation` verifier judges a diff. Here the unit is a **finding**, which is the input those stages build on — catching unrequested work while it is still a finding is the cheapest place to catch it at all.

Required reading before proposing a gap or an over-scope candidate:
- the current run's `analysis-packet.md` for requirements and phase scope — for the over-scope half this is the authority you search against, so read it before judging any finding unrequested;
- `convergence-groups-<task-type>-<seq>.json` for the complete Round 0 ledger;
- every initial analysis-worker result named by team-state;
- each matching audit sidecar, to distinguish an uninspected path from a claim that was inspected but summarized during grouping.

Operational guardrails are not task requirements. A gap must trace to a brief requirement, an analysis-packet scope item, a source path the packet authorizes, or an evidence claim in a worker result. Do NOT infer missing verification from a one-line summary; open the named result and audit sidecar first.

```
Inspect scope coverage for <task-key>. Below are the consolidated findings the
workers produced. Your job has exactly two halves. Answer both.

(1) MISSING — name what nobody covered:
- files / directories / execution paths nobody inspected,
- requirements or acceptance points with zero findings,
- claims raised but never verified.
For each, emit a NEW finding with evidence (file:line or the requirement quote).

(2) UNREQUESTED — name work these findings propose that no requirement asked for:
- a finding whose proposed change serves no requirement, scope item, or
  acceptance point you can QUOTE from the analysis packet,
- an abstraction, configuration knob, or generalization proposed for a caller or
  a case nobody has stated,
- a rewrite, migration, or cleanup of code the requirements never mention.
For each, emit a candidate with `category: "unrequested-scope"`, quote the
proposed work verbatim, and state which requirement you searched for and did not
find.

Do NOT restate an existing finding. Judge (2) against the analysis packet's
requirements and scope, never against your own preference for how the code should
look — "I would have done it differently" is not unrequested work, and neither is
work the packet authorizes but you consider unnecessary. If a half has nothing,
say so explicitly for that half; silence on one half is an incomplete result.
```

### Gap verification (1 adversarial reverify round)
Each critic gap enters the verification queue as a finding with `originWorker = "<provider>-critic"` and `source = "critic"`. The lead runs ONE adversarial reverify round (§"Adversarial Verification Mode" classifier) with **every Phase 4 analyser as a voter**. Choosing a critic provider that is already in the analyser roster costs nothing: the critic is a different role contract, a different duty and a different session, so an analyser is not disqualified by sharing its provider name (ADR-0017 — provider and model are not role identity, and the same model assigned to two roles gets two independent workers). The critic cannot judge its own gaps because it is not an analyser: the voter roster is `workers[]` filtered to `audience == "analysis"`, and a critic is not even representable there (the allowed values are `analysis` / `lead` / `report-writer`). `okstra apply-critic-gaps` refuses a vote from anyone outside that roster (`critic voter must be a non-critic analyser`). Only gaps classified `full-consensus` / `partial-consensus` merge into the final report findings; `contested` / `worker-unique` gaps are treated as hallucinations and dropped (recorded in the convergence state, not promoted).

**A gap that received no verdict is NOT a rejected gap (BLOCKING).** Dropping applies only to gaps the voters actually judged. A gap can also end the round *unjudged* — the verification dispatch returned a terminal non-result (`timeout`, `error`, no result file), the returned result covered only some of the gaps, or no non-critic analyser was available to vote at all. Nobody inspected those, so classifying them as hallucinations is a fabricated verdict. Each one MUST be recorded as a `## 5. Missing Information and Risks` row (`missingInformation`, `source: "critic-unverified"`) whose `risk` names the gap and the reason verification did not complete, and counted in `config.critic.gapsUnverified`. They are **not** promoted to findings (unverified) and **not** raised as `clarification` items — an unverified gap needs an analyser to verify it on the next run, not a decision from the user. Silently losing them is a contract violation: the batch that times out is exactly the batch of gaps too expensive to check, so the highest-risk items are the ones that vanish.

**`category: "unrequested-scope"` candidates are classified the same way but disposed of differently.** A coverage gap the voters contest is a hallucination — nothing was actually missing, so dropping it costs one wasted verification. An over-scope candidate the voters contest is a *disagreement about whether the work was asked for*, and dropping that silently returns the run to the state this half exists to change. So:

- `full-consensus` / `partial-consensus` → merge as a finding, exactly like a coverage gap. The merged finding names the unrequested work and the requirement search that came up empty; the phase's own deliverable rules decide whether it lands as a dropped item or a clarification row.
- `contested` / `worker-unique` → counted in `config.critic.gapsRejected` (unchanged accounting) but ALSO recorded as a `## 5. Missing Information and Risks` row with `source: "critic-unconfirmed-scope"`, whose `risk` quotes the proposed work and the split verdict. It is **not** promoted to a finding: a contested over-scope claim must not block a plan on one worker's taste. The user reads the row and decides.
- no verdict at all → the `critic-unverified` rule above applies unchanged.

The asymmetry is deliberate and runs the opposite way from the coverage half: a false "you missed something" costs a verification, while a false "you built too much" costs a real requirement — so the first may be dropped outright and the second is recorded either way. This is the same reasoning that makes the `final-verification` acceptance critic never drop a candidate, applied to the one direction where dropping is otherwise the default.

### State
- `convergence.critic` manifest block: `{ enabled, provider, modelExecutionValue }`.
- Each candidate's `category` tells the two halves apart: literal `"unrequested-scope"` for the over-scope half, any other value for a coverage gap. `schemas/convergence-critic-results-v1.0.schema.json` leaves `category` a free string, so this needs no schema or reducer change — but it also means nothing machine-checks the spelling. A misspelled category is read as a coverage gap and silently takes the drop-on-contested path.
- The lead passes one canonical coverage batch with `{ schemaVersion, taskKey, mode, provider, modelExecutionValue, dispatches, gaps }`; each gap carries its candidate fields plus `gapId` and `votes`. `dispatches[]` contains exactly one row for every Phase 4 analyser except the critic, even when execution did not produce a result: persist `status: timeout | error | not-run` and the elapsed `durationMs` instead of omitting that analyser. `apply-critic-gaps` rejects a non-terminal main queue, duplicate or missing analysers, unknown workers, critic dispatches/votes, votes without a completed dispatch, and a second batch.
- Convergence state artifact: merged gaps appear in `findings[]` with `source: "critic"` and `rounds: []`. The separate `criticVerification.gaps[]` ledger retains each gap's `summary`, `category`, `ticketIds`, `originEvidence`, optional `evidenceArtifacts`, classification, merge link, and votes. Strict v1.3 validation deterministically replays each complete critic-origin finding from that ledger; a critic batch never increments `roundHistory` or `totalRounds` and never creates a fake main round.
- `config.critic` is `{ provider, modelExecutionValue, gapsProposed, gapsMerged, gapsRejected, gapsUnverified }`, with `gapsProposed = gapsMerged + gapsRejected + gapsUnverified`. `full-consensus` / `partial-consensus` gaps merge, `contested` / `worker-unique` gaps count as rejected, and gaps with no usable analyser vote appear in both the ledger and final `unverifiedGaps[]`.

## Acceptance critic pass (final-verification)

The `final-verification` phase uses the same fresh one-shot `redispatch_worker` pattern and the same dispatch timing as §"Coverage critic pass" §"When" (provider + `config.critic.modelExecutionValue` from the `convergence.critic` block; critic is required; same model-unresolved skip rule) — the delivered work the critic inspects is likewise fixed before the reverify round starts. Only the prompt, the verification semantics, and the output sink differ — final-verification's findings are defects/blockers, so the critic acts as an **acceptance devil's advocate** (find reasons NOT to accept), and its candidate blockers are NEVER dropped (that would suppress real defects).

Before that call, write the acceptance-only task instructions and run `okstra
agent-prompt materialize` with `--audience acceptance-critic`,
`--assignment-ref critic/acceptance`, the critic worker ID, and
`--dispatch-kind critic`. Verify the returned `metadataPath`, dispatch only the
returned `promptPath`, and select `hostModelValue` for a native host or
`modelExecutionValue` through `okstra worker-dispatch`. Native dispatch linkage
uses `enforcementMode=host-native-spec-link-gate`; it does not claim prompt
delivery was observed.

### Prompt

```
Challenge acceptance for <task-key>. The delivered work is about
to be judged for acceptance. Your ONLY job is to find reasons it should NOT be
accepted — surface candidate acceptance BLOCKERS the verifiers may have missed:
- requirements / acceptance points with no covering evidence,
- DB / IO / SQL changes lacking real-execution evidence,
- regressions or broken error paths,
- scope / contract violations.
For each, emit a candidate blocker with a one-line statement, evidence (file:line /
log / test output), and a severity (critical / major). A finding that would not stop the
release is not a blocker — say so, and it is recorded as a conditional-acceptance
condition instead. Do NOT restate an existing Acceptance Blocker. If you find none, say
so explicitly.
```

### Verification — confirm-or-downgrade (BLOCKING)

Each candidate blocker is verified by the Phase 4 analysers — all of them, on the same roster rule as §"critic gaps" above; sharing the critic's provider name does not disqualify an analyser. Do NOT use the adversarial finding classifier's "uncertain → reject" rule here.
- Do NOT run `apply-critic-gaps` for this mode. That reducer implements coverage merge/drop semantics and rejects `acceptance-devils-advocate` input.
- **Confirmed** (an analyser reproduces it or cites supporting evidence) → promote to a `## 5.8 Acceptance Blockers` row (keep severity + recommended follow-up phase).
- **Not confirmed** (cannot reproduce, or evidence is weak) → **downgrade to a Residual Risk row — never drop it.** Record the escalation trigger so the user can re-judge a high-severity-but-unconfirmed candidate.

### Verdict impact

Promoted blockers enter `## 5.8 Acceptance Blockers`; since `accepted` requires zero blockers, the verdict moves to `conditional-accept` / `blocked` automatically. The existing verdict↔blocker consistency validator (`validators/validate-run.py` `_validate_final_verification_consistency`) enforces this unchanged — no new enum or validator.

### State

Critic output lives in the run's `worker-results/` directory (`runs/final-verification/worker-results/` for whole-task verification, `runs/final-verification/stage-<N>/worker-results/` for single-stage), filename `<provider>-worker-critic-final-verification-<seq>.md` (same `-worker-` token rule as §"Coverage critic pass" — the audit sidecar is derived from it). The convergence state `config.critic` summary records `mode: "acceptance-devils-advocate"`, `candidatesProposed`, `confirmedBlockers`, `downgradedToResidual`; v1.3 enforces `candidatesProposed = confirmedBlockers + downgradedToResidual`, so no candidate can be silently dropped.

## Output

Information to be passed to Phase 6 after completing this contract:

- Newly finalized convergence output is schema v1.3. Under `reuse-final`, a valid historical final schema v1.0, v1.1, or v1.2 remains consumable by the report-writer without rewrite. Either validated terminal artifact contains the four-category classification of all findings; the report-writer consumes it and does not vote
- Round history and votes per worker for each finding
- Path to the convergence state artifact
- Convergence summary (count per category)
- Whether early convergence occurred and the total number of rounds

## Convergence Disabled

If `convergence.enabled: false`, this contract is skipped. Phase 6 operates using the existing consensus/divergence method.

## Plan-body verification mode (implementation-planning only)

Moved to its own contract: [plan-body-verification](./plan-body-verification.md). It fires only for `task-type = implementation-planning`, as a Phase 6 sub-step after the report-writer draft — read that file at that sub-step, NOT during the Phase 5.5 finding convergence this contract governs. The finding queue (`F-*`, this contract) and the plan-item queue (`P-*`, that contract) are disjoint — see its "MUTUAL EXCLUSION" section.
