# Okstra Team Contract
## When to Use

- During okstra lead Phases 2–5 (prompt preparation and execution)
- When verifying worker team composition and operational rules
- When applying model assignment rules

**Not applicable to `release-handoff`** — that profile is lead-only and intentionally has no `Required workers:` block (see `prompts/profiles/release-handoff.md`). The worker-dispatch contract in this document does not engage during `release-handoff` runs.

## Team Structure

Okstra tasks use one lead plus the exact worker assignments selected in the prepared bundle. The lead runtime and the worker providers are independent axes.

### Role Definitions

The start screen assigns a **model ref** (`provider/model`) to each **canonical role** slot: `leader`, `analyser`, `critic`, `designer`, `planner`, `implementer`, `verifier`, `report-writer`, `translator`. A provider name is the front of a model ref, not a role. Every `analyser` in the run shares an identical core responsibility. Specialization is additive — it lives in optional Section 6 of the worker output, NOT in differentiated core questions. Cross-verification only converges if every rostered analyser answers the same questions against the same brief.

| Canonical role | Core responsibility | Notes |
|------|------|------|
| `leader` | orchestration + convergence supervision + final-report review/approval | Does not author the final report when `report-writer` is rostered |
| `analyser` | Answer every brief question across feasibility, requirement interpretation, hidden assumptions, and alternatives — with file:line evidence | Same sections 1–5 for every model ref |
| `critic` | Audit coverage gaps or challenge acceptance, per the attached duty | Not an analysis voter |
| `designer` | Compare or validate implementation directions | Used by `implementation-option-selection` |
| `planner` | Produce an executable plan without writing the implementation | Used by `implementation-planning` |
| `implementer` | Sole change author for one approved stage | Used by `implementation` |
| `verifier` | Independent review of the assigned duty's subject | Duty varies by task type |
| `report-writer` | **Authors** the final-report file in Phase 6 | Excluded from Phase 4/5 and convergence |
| `translator` | Translates the designated sidecar only | Phase 7 |

**Dispatch does not invent a missing model assignment.** Launch-time empty slots are filled by the model default chain before the run starts. At dispatch the model for every role comes from `resultContract.requiredWorkerRoles[*].modelExecutionValue` in `task-manifest.json` (and lead model metadata). There is no per-role hard-coded fallback — see "Model Assignment Rules" below.

**Dispatch-prompt invariant.** Lead's dispatch prompt body for every rostered analysis worker MUST be byte-identical except for the role label and wrapper-specific path headers (for example `**Worktree:**`). The role label is the ONLY identity form the normalizer erases, and it erases exactly the label `worker_prompt_body.analysis_worker_label` renders for the run's own worker ids — so a provider outside that function's display map (`grok`, `kimi`, an installed adapter) is covered as it comes. Naming the worker any other way (a model name, a host name, a provider's product name) survives normalization and fails the equality group before publication. **Enforced:** `okstra_ctl.worker_prompt_contract.normalise_analysis_prompt`, pinned by `tests/contract/test_analysis_prompt_identity_normalization.py`. Lead MUST NOT bias the brief by inserting per-worker emphasis sentences ("you focus on X") into the body. Bias-by-prompt reproduces the historical failure mode where Claude commented only on assumptions, Codex only on code paths, and Antigravity only on requirements — leaving convergence with nothing to converge on.

Disjoint initial scopes are invalid triangulation. Every selected analysis worker owns the same common verification requirements; provider diversity supplies independent observations, not separate coverage slices. Do not shard the common scope by worker, provider, or model. Worker-specific depth belongs only in the non-voting Specialization Lens after the shared analysis is complete.

### Model Assignment Rules

1. `resultContract.requiredWorkerRoles` in `task-manifest.json` (and the lead model metadata) is the canonical source. There is no role-level fallback — a missing assignment is a manifest defect, not a license to invent one.
2. Select the execution value from `runner`: `native-session` passes only `hostModelValue` to the host primitive, while `cli-wrapper` passes `modelExecutionValue` to the provider process. Both values remain recorded in the invocation contract; neither may be substituted for the other.
3. **Dispatch-time enforcement (BLOCKING).** The selected adapter receives the complete assignment and must apply the runner-specific value above. The adapter must fail before dispatch if it cannot apply the exact assignment; it must not inherit the lead model, change provider, or choose a nearby alias silently.

### Dynamic Worker Role Determination

**Roster canonical-source rule.** The profile's `Required workers:` block (in `prompts/profiles/<phase>.md`) is the **static roster definition** — the set of roles legal for that phase. `resultContract.requiredWorkerRoles` in `task-manifest.json` is the **per-run instance** — the actual roster materialized for this run, after recommendation, user selection, and any post-recommendation overrides. **On conflict, the task-manifest wins** — it is what the run was actually launched with, and what lead must dispatch against.

Only workers selected from `recommendedWorkers` in `task-manifest.json` and `resultContract.requiredWorkerRoles` become required roles.

- If one worker is selected: "`<role>` is the required worker role for this run."
- If two or more workers are selected: "`<role1>`, `<role2>`, and `<role3>` are required worker roles."
- If Antigravity is selected: "`Antigravity worker` must be attempted in this workflow."
- If Antigravity is not selected: "`Antigravity worker` is not selected for this run, so it does not need to be attempted."

## Operating Rules

0. **Adapter-owned dispatch (BLOCKING).** Every worker start, await, retry, and shutdown goes through the selected runtime adapter. Core state records the outcome but never guesses a host primitive.
1. The lead is responsible for orchestration, convergence supervision, and final-report review/approval. It never overrides worker analysis and never bypasses a rostered Report writer worker.
2. `Report writer worker` is NOT an analysis worker. It is excluded from Phase 4/5 (initial analysis) and Phase 5.5 (convergence re-verification). It is spawned only in Phase 6 and authors only the narrative input consumed by report assembly.
3. When `Report writer worker` is in the roster, Lead MUST dispatch it in Phase 6 as a separate invocation after convergence. Omit it from Phase 4/5 analysis selection and pass `--workers report-writer` for a CLI-backed Phase 6 call. Contract v3 has no lead-authored fallback: an attempted dispatch ending in `error` / `timeout` / `not-run` is retried or leaves the run blocked. **Enforced:** `dispatch_core._validate_report_writer_isolation()` rejects every mixed analysis/report plan before process creation, and the default roster selectors exclude `report-writer`; report-writer write paths exclude the final record.
4. The assigned model for each role is maintained based on `resultContract.requiredWorkerRoles` in task-manifest.json and the lead model metadata.
5. Required roles must not be replaced by unnamed generic parallel workers.
6. Before dispatching any required worker, persist the exact worker prompt to the assigned current-run prompt history path under `runs/<task-type>/prompts/`.
7. Before the final decision, collect results or explicit terminal statuses for each required worker role.
8. If a worker is attempted with status `completed`, `timeout`, or `error`, the corresponding worker prompt history file must actually exist.
9. If a worker result is `completed`, the corresponding worker result file must actually exist.
10. Treat `team-state` as the canonical source; if it differs from the report's role label, follow `team-state`.
11. A validator success status is required before the final completion determination.

## Worker Prompt Composition

`okstra_ctl.initial_prompt_materialization` is the canonical owner of roster-derived initial prompt rendering, validation, and immutable publication. Code-backed `dispatch_worker` mappings leave missing roster prompts to `materialize_initial_prompts()` and pass the selected adapter's declared `initialPromptDeliveryMode`; they do not compose or overwrite those prompts themselves. Native in-process dispatch uses the same headers and the adapter's declared `lazy-path-reference` mode, persists the prompt before dispatch, and remains outside reverify and critic handling.

Every worker prompt MUST start with the anchor headers rendered by `okstra_ctl.worker_prompt_headers.worker_prompt_headers()` (the generating SSOT — never hand-author or reorder them). Every persisted initial prompt also carries exactly one non-empty `**Prompt Delivery Mode:** <mode>` header whose value is `eager-include` or `lazy-path-reference`. Redispatch reuses that persisted initial prompt byte-for-byte; it never regenerates or overwrites it. The generated absolute `**Audit sidecar path:**` is derived by `audit_sidecar_rel()` from the canonical worker result path; workers write to that header and never synthesize a `runs/<task-type>/...` destination. Their meaning and extraction rules for workers are owned by `okstra_ctl.worker_prompt_headers.worker_prompt_headers()`. Phase-specific extra headers (implementation worktree, final-verification target snapshot, improvement-discovery grilling log) are emitted there too.

The `**Coding preflight pack:**` anchor is emitted only for the `implementation-executor` and `implementation-verifier` audiences. An implementation report-writer never receives it. A pane display role named `verifier` during `final-verification` is still an initial analysis worker; it neither receives implementation conventions nor becomes a Phase 5.5 reverify dispatch. Reverify is identified by the `-reverify-r<N>-` prompt/result path contract in [convergence](./convergence.md).

For `improvement-discovery`, `worker_prompt_headers()` resolves and validates the Phase 1.5 grilling log before dispatch and emits its absolute path as `**Phase 1.5 Grilling Log:**`. The lead does not hand-author or guess that path.

The body must include: role name, task type, task key, assigned model, output contract, evidence handling rules, and `analysis-packet.md` as the single primary analysis input. For `final-verification`, the compact target anchors and that packet path are sufficient; do not copy the full diff stat, source/fallback path inventory, profile sections, report-output sections, or lead self-review into the initial prompt.

A `final-verification` prompt may contain exactly one optional `## Run-specific directive` section for a true run delta. It is limited to 40 nonblank lines; the nonblank prompt body excluding common/target anchors is limited to 96 lines. Larger shared material belongs in the instruction set and analysis packet. Before initial dispatch, the selected adapter removes worker-specific header lines (including optional `**Pane role:**`) and requires the remaining normalized analysis bodies to be byte-identical; the semantic `Primary analysis packet` input remains in that comparison.

The Phase 7 run validator enforces the same cross-task rule against the persisted prompt paths recorded by the run manifest, dispatch records, and team-state. `initial_prompt_materialization` validates roster-derived prompts before and after publication, while `validators/validate-run.py` revalidates them through `okstra_ctl.worker_prompt_contract.validate_initial_prompt_records()`. The validator resolves each record through `PromptPlan`, requires the delivery-mode header on every non-reverify initial record, applies audience-specific anchors, compares every equality group with at least two prompts, validates report-writer common anchors without adding it to an analysis group, and ignores `-reverify-r<N>-` prompts because they belong to the lightweight convergence protocol. A dispatch-time bypass therefore remains a run-level contract violation.

When a worker reads any project-relative path from the prompt, it MUST resolve it against `Project Root` (e.g. `<Project Root>/<Result Path>`) — never use bare relative paths that depend on cwd.

Inject only the packet-scoped one-line pointer into every analysis worker's prompt: `**MCP servers:** follow the analysis packet's "Available MCP Servers" section. If the section is absent or says none, treat MCP as unavailable for this run; never infer tools from host configuration.` Codex/Antigravity workers run external CLIs whose MCP availability is governed by their own CLI configs; they record `MCP not available in this CLI` when their CLI does not expose a server the packet names.

Persist the exact worker prompt before dispatch per Operating Rule 6; never use `/tmp/*prompt*.txt` as the canonical artifact path.

Send byte-identical dispatch prompts to every analysis worker per the "Dispatch-prompt invariant" (Role Definitions). Specialization lives in Section 6 of the worker output, not in the dispatch prompt body.

### Audience preambles + shared error contract (SSOT)

The lead does not inline reading or error blocks. It resolves `PromptPlan.audience` and injects exactly one `**Worker Preamble Path:**`: analysis → `templates/worker-prompt-preamble.md`, implementation executor/verifier → `templates/implementation-worker-preamble.md`, report-writer → `templates/report-writer-prompt-preamble.md`. Every initial worker also receives `**Worker Error Contract Path:**` pointing to `templates/worker-error-contract.md`, the sole owner of error schema and write rules. Reverify prompts use neither preamble.

What the lead MUST still do per dispatch:
- Inject the input file enumeration into the dispatch prompt body via an `## Inputs` section (or any heading the recipient agent expects), listing the actual project-relative primary inputs derived from the run's `instruction-set/`. For `final-verification` analysis workers, list only `analysis-packet.md` as the primary input; source files are reached on demand through that packet. Other phases may list source/fallback paths when useful. The preamble describes the rules; the lead provides the specific paths for THIS run.
- Inject `**Evidence ledger:** required-v1` into every initial non-report-writer prompt. The selected audience preamble owns the audit-row syntax; `validators/validate-run.py` `validate_worker_results_audit()` enforces that each backticked `path:line` result citation has a matching evidence-read row. Report-writer and reverify prompts do not carry this marker.
- Inject `**Worker Error Contract Path:**` plus the absolute `**Errors log path:**` header — workers cannot synthesize this path.
- Omit the preamble pointer for reverify dispatches (Phase 5.5 lightweight mode) — see [convergence](./convergence.md) "Reverify prompt: required-reading suppression".

Audience-scoped file enumeration (performance optimization — mandatory):

| Recipient | Files the lead lists under `## Inputs` |
|---|---|
| Any rostered analysis worker | `analysis-packet.md` as primary input; for `final-verification`, no source/fallback list is copied into the prompt |
| Report writer worker (Phase 6) | task-brief, analysis-profile, analysis-material, reference-expectations, clarification-response (if carry-in), **plus** the instruction-set-local `final-report-template.md` (phase-stripped) and `final-report-schema.json` (per-task-type excerpt) — NOT the full `templates/reports/...` / `schemas/...` sources |
| Reverify dispatches | none — the lead provides only the items to reverify |

## Terminal Statuses

Terminal statuses that can be recorded for a worker:

| Status | Meaning |
|--------|------|
| `completed` | Normal completion; prompt history file and result file must exist |
| `timeout` | Timeout, reason recorded; prompt history file must exist |
| `error` | Execution error, reason recorded; prompt history file must exist |
| `not-run` | Not executed, reason recorded |

## Worker-completion detection

1. Build the pending set from the manifest-assigned completion paths.
2. Call `await_workers(handles)` through the selected adapter.
3. On every wake/return, read terminal dispatch records and verify every required completion path.
4. A process exit, spawn acknowledgement, pane creation, or Result Path alone is insufficient when the backend contract requires additional status/audit artifacts.
5. Apply the single shared retry budget through `redispatch_worker`; after the second failure, record the terminal status and proceed with reduced confidence.

### Mid-run liveness probes

Between wakes, `okstra worker-liveness` is the **only** sanctioned way to ask whether a pending worker is still alive. Do NOT hand-roll a polling script, an `ls` / `stat` loop, or any ad-hoc file-existence check: a lead that writes its own probe owns that probe's bugs, and those bugs surface as *worker* failures — a shell quoting slip silently turns the probe into a no-op that reports health it never measured, and a stall test written against an artifact's absolute mtime declares a freshly launched worker dead on its first iteration, because the audit sidecar it inherits from the previous dispatch has not been touched yet.

**Waiting is part of the probe, not something you build around it.** `--wait` polls until every named worker's persisted `resultPath` lands (exit 0), one worker probes unhealthy (exit 1), or `--timeout` passes (exit 2):

```
okstra worker-liveness --wait \
  --team-state <path> --worker <id> \
  --team-state <path> --worker <id2> \
  [--interval 20] [--timeout 2400]
```

Run it as a background command and act on the exit code when it returns. Both the "is it done" test (the persisted `resultPath`, never a filename you assemble) and the "is it dead" test (graces anchored to the row's `startedAt`) live inside the command — which is the whole reason a hand-written loop is forbidden: each one re-derives those two tests, and each re-derivation gets one of them wrong.

One selector serves every worker kind: `--team-state <path> --worker <id>`, repeated once per worker. The worker row's `livenessMode` is authoritative and the probe reads it for you — do not infer the transport from a worker filename or provider name, and do not pass the artifact path yourself.

| `livenessMode` | Backend | What it reads |
|---|---|---|
| `audit-heartbeat` | in-process dispatch (including `claude-worker` and `report-writer-worker`) | the row's `auditSidecarPath` — its newest `- PROGRESS:` heartbeat, measured from `startedAt` |
| `wrapper-status` | CLI wrapper (`codex-worker` / `antigravity-worker`) | the row's `promptPath`, then `<prompt>.log` / `<prompt>.status.json`, measured from `startedAt` |

The mismatch is not intermittent, it is guaranteed: only the `okstra-*-exec.sh` wrappers ever write `<prompt>.log` / `<prompt>.status.json`, so an in-process worker produces neither by construction. That is why the mode decides the artifact and you never name it. Each grace begins at the atomic `in-progress` transition's `startedAt`, never at prompt-materialization time and never at an artifact's mtime.

This is a transport-adapter liveness choice only. It does not change the reducer's worker identity or its verification responsibility: both remain bound to the registered worker instance and canonical artifacts.

Branch on the exit code, not the JSON: without `--wait`, `0` = every probe healthy and `1` = at least one `stalled` / `did-not-launch`; with `--wait`, `0` = every result landed, `1` = a worker died, `2` = the deadline passed. The probe reports; it never kills or re-dispatches. Acting on an unhealthy verdict means spending the existing one-retry budget below.

## Lead Redispatch Policy on Result-Missing

After each worker attempt returns (regardless of role), Lead MUST verify the canonical result file exists at the absolute path resolved from the `**Result Path:**` anchor header (against `**Project Root:**`). The check is identical for host-native workers and deterministic CLI processes.

**Triggers (any of):**

- The deterministic provider process returned an explicit `*_RESULT_MISSING` sentinel.
- The result file is absent at the resolved absolute path even though the worker returned without a `*_RESULT_MISSING` sentinel — for example, claude-worker returned its final assistant message but never persisted the artifact, or the wrapper exited 0 and the codex/antigravity sub-agent forwarded raw stdout despite the contract.
- The result file exists but cannot be parsed (frontmatter unreadable, sections 1–5 entirely missing). A truncated file in the middle of section 5 is NOT covered here — it goes to the validator's regular `error` path, not the retry path.
- `okstra worker-liveness --team-state <path> --worker <id>` reports a **CLI-wrapper** worker (`codex` / `antigravity`) `did-not-launch` — neither `<prompt-path>.log` nor `<prompt-path>.status.json` exists after the persisted `startedAt` plus the launch grace (default 60s). The wrapper writes its status sidecar before invoking the CLI and hard-fails loudly with a distinct exit code on every argument check before that, so the absence of BOTH artifacts means the dispatch itself never reached the script. Without this trigger the only evidence was a lead noticing two missing files by eye, and the run paid the full polling cap for a worker that never started.
- `okstra worker-liveness --team-state <path> --worker <id>` reports an **in-process** worker `stalled` — its registered audit sidecar's newest `- PROGRESS:` heartbeat is older than the cadence budget, or the sidecar carries no heartbeat at all. This is the in-process equivalent of the CLI wrappers' idle watchdog: the wrapper reaps a silent CLI itself, but nothing reaped a silent in-process worker until its deadline. The same selector serves both worker kinds — the sidecar is reused when a worker is re-dispatched, so the probe needs the row's `startedAt` to tell the previous attempt's last heartbeat apart from this dispatch's silence. A budget breach is not a verdict on its own: the probe re-reads the sidecar after half that stage's budget and reports `stalled` only when the newest heartbeat has not advanced, so an unhealthy verdict costs that confirmation window before it returns. That window is what separates a worker inside one long uninterruptible tool call — which cannot append a heartbeat at all — from a worker that died, and measuring it is the probe's job, not yours: do not second-guess a verdict by checking mtimes yourself. Tune it with `--stall-confirm <seconds>`; `0` restores the immediate verdict.
- The result file exists but its audit sidecar does not, at `runs/<task-type>/worker-results/<worker>-audit-<task-type>-<seq>.md`. Workers write both in the same step, so a result without a sidecar means the Reading Confirmation block — the only evidence the worker read its inputs — was never produced. `validate-run.py` fails the run on this at Phase 7 either way (`validate_worker_results_audit`); checking it here spends the existing one-retry budget while the role can still be re-dispatched, instead of surfacing hours later when the worker session is gone.
- `okstra worker-audit-check --run-dir <runs/<task-type>/> --task-type <t> --seq <n> --worker <id>` exits 2 on a backticked `path:line` citation in the worker's result that has no matching Evidence read row in its audit sidecar. Run it the moment you collect each result. Phase 7 enforces the same rules from the same implementation (`okstra_ctl.worker_audit_ledger`), but by then the worker session is gone and the only remaining moves are editing the result yourself — which destroys the audit chain the ledger exists to provide — or ending the run `contract-violated`. While the session is alive, `SendMessage` to the worker so it corrects its own citation; that costs about a minute against a re-dispatch or a failed run.

The same audit check parses command evidence from canonical rows such as `- Evidence command: {"command":"npm run check","cwd":"<project-root>","exitCode":0,"outputSummary":"all checks passed"}`. Workers record only commands that produced or verified a conclusion, not exploratory `rg`, `ls`, or file-opening commands. A malformed row is a contract failure returned by `okstra_ctl.worker_audit_ledger`; send the failure to the worker while its session is still available. Nothing scans these rows for secrets, so a row is never refused for how a setting is named — `AUTH_MODE=off` and `TOKEN_TTL=60` record verbatim. Do not tell a worker to drop or paraphrase a row on those grounds: a worker that cannot record the command it ran has no way to satisfy the evidence ledger that demands it.

**One-retry policy:**

1. On the FIRST result-missing trigger for a given role within a single run, Lead MUST call `redispatch_worker` with the byte-identical prompt — same `**Result Path:**`, same `**Prompt History Path:**`, same model assignment, and same adapter-native assignment identity. The redispatch counts as a second attempt against the existing role slot; do NOT create a new role-id, do NOT change the result file path, do NOT switch to a different model as a "workaround".
2. If the SECOND attempt also fails the same check, Lead records the role's terminal status as `error` with `--message "result-missing after 1 retry"` and proceeds to Phase 5.5 / Phase 6 with the remaining workers' results. Lead MUST NOT retry a third time — convergence and the report writer are designed to operate on reduced-confidence single-or-two-analyser mode when one role is absent (`prompts/lead/okstra-lead-contract.md` "If only one worker result is usable: reduced-confidence synthesis").
3. The retry counter is **per-run, per-role** and is NOT preserved across runs. A subsequent okstra run for the same task-key starts each role's counter fresh.
4. Convergence reverify rounds (Phase 5.5) inherit the same one-retry budget — a reverify dispatch that triggers result-missing may be re-dispatched once.
5. **Reading-plan exception for a deterministic failure.** Byte-identical is the rule because it stops a lead from hiding a failure behind a different model, provider, or result path. It does not fit a failure the prompt *causes*: when the first attempt exhausted its whole time budget with no output and the required reading it named is large enough to explain that, an identical retry buys a second timeout at the same cost. In that case only, Lead MAY change the **reading plan** — which files the worker reads and in what order, including skipping a file whose content is already reachable by another named path. Everything the invariant exists to protect stays fixed: same `**Result Path:**`, same `**Prompt History Path:**`, same model, same adapter-native assignment identity, same role-id, same task.
   - The exception applies only when the first attempt produced **no result file** after consuming its full timeout. A worker that failed fast, errored, or returned a partial artifact is a transient/regular failure — retry it byte-identically.
   - Lead MUST log the deviation with the normal `contract-deviation` entry naming the removed or reordered reads and the measured evidence that motivated it (byte counts, elapsed time). An unlogged reading-plan change is a contract violation, not an exception.
   - This exception never licenses changing what the worker is asked to *produce*. Narrowing the deliverable to make it finish is a contract violation.

**Logging.** Lead records the first attempt's `cli-failure` (already emitted by `worker-dispatch`) as-is. The retry, on success, is logged via the normal worker-completion path; on failure (second `*_RESULT_MISSING`), Lead records a single `contract-violation` entry with `--message "result-missing after 1 retry"` referencing both adapter dispatch-attempt ids and prompt-history paths.

**Diagnostic sidecar (advisory).** Every CLI-worker dispatch writes a heartbeat sidecar at `<prompt-path>.status.json` recording `started_ts`, `ended_ts`, `exit_code`, `duration_ms`, and the canonical `log_path` (written by `scripts/okstra_ctl/worker_runner.py`, which every provider entrypoint shares). Lead MAY read this sidecar when deciding whether the first attempt actually launched the CLI (stage=`exited`, `exit_code=0`, non-zero `duration_ms`) versus failed before reaching it (sidecar absent, or stage=`started` with no exit fields). A run that ended abnormally after launch — an error, a Ctrl-C, or the SIGTERM/SIGHUP a pane kill or session teardown sends — closes as stage=`exited` with a `failure` string and **no** `exit_code`; read that as a failed attempt, not as a success. A SIGKILL cannot be closed by anything, so a sidecar still reading stage=`started` is not evidence that the worker is alive. The sidecar is best-effort — its absence is NOT by itself a reason to skip the retry; the canonical trigger remains the missing result file.

**Rationale.** Observed failure mode: the CLI (codex/antigravity) streams its full analysis to stdout but hits its token budget or a sandbox EPERM mid-`Write` of the result file, exiting 0 with no artifact. Forwarding the partial stdout silently degrades synthesis; classifying the role as `error` without retrying gives up a recoverable signal. A single retry catches the transient class of this failure (re-dispatch with the same prompt typically succeeds when the underlying cause was an intermittent sandbox lock or a token-budget spike) while bounding the retry cost to a known upper bound (~2× the original wrapper budget per role). The reading-plan exception in item 5 covers the class this rationale does not: a worker that timed out because its prompt asked it to read too much fails identically on an identical retry, so the second attempt must change the reading plan or it is knowingly wasted.

## Worker Output Contract

The canonical analysis-worker output contract — frontmatter, sections 1–5 plus optional Section 6, item IDs, and ticket tagging — lives in `templates/worker-prompt-preamble.md`. Implementation output behavior remains in its executor/verifier sidecars; report authoring remains in the report-writer preamble and Phase 6 contract.

Lead-facing duties that stay here:

- Lead and report-writer MUST preserve worker ticket tagging and `worker:item` source pairs when carrying findings into the final report.
- Lead's result-missing redispatch check parses the frontmatter and sections 1–5 presence per the preamble contract (see "Lead Redispatch Policy on Result-Missing").

### Worker error-log command

Workers record a tool failure through the typed `okstra error-log append-observed` command in `templates/worker-error-contract.md`. They do not create a JSON sidecar or pass JSON text. The command receives the assigned worker identity, task key, phase, model, and observed scalar fields; the Python writer owns JSONL serialization and validation.

**Path delivery contract (BLOCKING).** Workers do NOT synthesize the run-level errors log path from `runs/<task-type>/...` template syntax. Lead delivers the absolute `**Errors log path:**` anchor from the launch prompt's `## Run Logs (error-log wiring)` section. If Lead omits the header, the worker MUST return `<WORKER>_ERRORS_PATH_MISSING` without proceeding.

- `cli-failure` events are recorded by `worker-dispatch` directly to the run-level error log via `okstra error-log append-observed --error-type cli-failure ...`.
- **Provider-process invocation arity.** Every `okstra-<provider>-exec.sh` entrypoint takes the same three required positional arguments plus three optional ones: `<project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]`, optionally followed by `--presentation live|quiet`. `worker-dispatch` alone constructs this invocation from the verified `WorkerJob`; leads and host adapters do not assemble it. The fourth argument is mandatory for implementation, the fifth names the functional role, and the sixth controls the shared idle budget (1500s for executor/verifier, 600s otherwise). `live` is reserved for a pane backend; deterministic dispatch uses `quiet`.
- **Background dispatch + polling contract (CLI processes).** `worker-dispatch` starts the selected provider CLI through the adapter's asynchronous execution mapping and awaits the same handle until it reports terminal completion, capped at 30 minutes (1800s) of wall-clock elapsed time. The adapter's await operation is the wait primitive; do not add a standalone sleep or build shorter-sleep loops to bypass a host constraint. This rule applies in **every phase**. Recording responsibilities:
  - Successful completion: return the provider process's accumulated stdout from the terminal await result. No log entry.
  - Non-zero `exit_code`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
  - Polling cap reached: perform a one-shot **mtime-grace check** on the wrapper's live log (`<prompt>.log`). If the log was written within the last 90 seconds and grace has not yet been applied, extend the cap from 1800s to 2100s and continue awaiting. Otherwise call the selected adapter's termination mapping, record `cli-failure` with `--exit-code 124 --duration-ms <observed_ms> --message "<wrapper> exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, then return the language-specific `*_CLI_TIMEOUT` sentinel.
  - The selected adapter owns runtime-session accounting for the full wrapper window; core retains only the observed start/end event boundaries.
- **No external timeout around `worker-dispatch`.** The deterministic dispatch owns BOTH timeout mechanisms: (1) the process polling cap (30min + optional 5min mtime grace), and (2) the shared runner's stream-idle watchdog. If the CLI produces nothing for `<idle-timeout-seconds>`, the runner terminates the process group and marks the status sidecar timed out. Lead MUST NOT layer an earlier host timeout around it.
- `contract-violation` events (C) are recorded by Lead via `okstra error-log append-observed --error-type contract-violation ...` after inspecting worker outputs.

## Convergence Phase Rules

1. Re-verification uses the same worker roles and model assignments as the initial run.
2. Re-verification workers follow a constrained response format (verdict + brief explanation).
3. Workers cannot vote on their own findings (only verify other workers’ work).
4. The `report writer worker` does not participate in re-verification voting. It is responsible only for generating the final report.
5. Division of labor: the lead performs **finding-to-finding matching** (semantic similarity plus ticket-set equality); workers cast AGREE / DISAGREE / SUPPLEMENT verdicts; `ConvergenceEngine` owns queue membership, classifications, round arithmetic, pruning, skip reasons, and final counts. The lead does not vote or hand-edit engine state.
6. Batch processing is performed with one fresh spawn per persisted `plan-round` worker row (not one spawn per finding). The selected adapter transports that exact batch and terminal result without changing membership.
7. These rules do not apply if Convergence is disabled.

## Re-verification Terminal Statuses

| Status | Meaning |
|--------|------|
| `verification-completed` | Re-verification vote completed |
| `verification-timeout` | Re-verification timeout |
| `verification-error` | Re-verification error |

## Usage Tracking

At the start of Phase 7, call `collect_usage(source)` through the selected adapter. `source` is `team-state.leadAdapter.sessionAccounting` plus the dispatch backend's recorded event source. Never read another runtime's session store as a fallback. Persist the resulting `leadUsage`, worker usage, and `usageSummary` in team-state before report substitution and cleanup.

### Resulting team-state shape

```json
{
  "leadUsage": {
    "totalTokens": 10479327,
    "inputTokens": 18,
    "outputTokens": 24360,
    "cacheCreationTokens": 612000,
    "cacheReadTokens": 9842949,
    "toolUses": 47,
    "durationMs": 7253000,
    "source": "<adapter-accounting-source>",
    "sessionId": "<adapter-session-id>",
    "collectedAt": "<utc>"
  },
  "workers": [
    {
      "workerId": "codex",
      "usage": {
        "totalTokens": 2274011,
        "source": "<dispatch-event-source>",
        "sessionId": "<worker-session-id>"
      }
    }
  ],
  "usageSummary": {
    "leadTotalTokens": 10479327,
    "workerTotalTokens": 7988699,
    "grandTotalTokens": 18468026,
    "sessionsFound": 5,
    "teamName": "okstra-DEV-9045"
  }
}
```

### Notes

- If a worker has status `not-run`, `collect_usage` records an `unavailable` block.
- Adapter-native and underlying provider usage are separate accounting sources and must not be added together without an explicit pricing model.
- `durationMs` uses the selected adapter's documented session boundary.

## Team State Persistence

Information to be recorded in the team-state JSON file:
- `teamName` — record the manifest-provided audit label set in Phase 3 (`okstra-<task-key>` + stage suffix) and preserve that exact value consistently within the run. The selected adapter owns any runtime-session matching derived from this audit field.
- Usage metadata for each worker (totalTokens, toolUses, durationMs) under `workers[].usage`
- Lead usage metadata (totalTokens, toolUses, durationMs) under `leadUsage`
- Current status of the entire run
- Path to the run-level error log file (`runs/<task-type>/logs/errors-<task-type>-<seq>.jsonl`) under `errorsLogPath`

### `workers[]` row — the field names, verbatim

Every consumer of this file reads these keys by name. Write them exactly as spelled here; a plausible synonym (`auditPath` for `auditSidecarPath`, `started` for `startedAt`) is not a near-miss, it is a row the probe cannot read, and the failure surfaces as a *worker* problem minutes later rather than as the typo it is.

| Key | Required for | Value |
|---|---|---|
| `workerId` | every row | the selector `okstra worker-liveness --worker` matches |
| `role` | every row | the roster label (`Claude worker`, `Codex worker`, `Report writer worker`) copied into the report's execution-status row |
| `status` | every row | the dispatch's current state |
| `startedAt` | every row | UTC ISO timestamp of the atomic `in-progress` transition — every liveness grace is measured from this, so a missing or re-used value is what makes a live worker read as stalled |
| `endedAt` | terminal rows | UTC ISO timestamp |
| `promptPath` | every row | the persisted prompt; the `wrapper-status` probe derives `<prompt>.log` / `<prompt>.status.json` from it |
| `resultPath` | every row | the canonical result file; `--wait` waits on exactly this path |
| `auditSidecarPath` | `audit-heartbeat` rows | the audit sidecar the in-process heartbeat probe reads |
| `errorsSidecarPath` | every row | the per-worker errors sidecar |
| `livenessMode` | every row | `audit-heartbeat` (in-process) or `wrapper-status` (CLI wrapper) — authoritative, never inferred from the provider |
| `usage` | terminal rows | `{totalTokens, toolUses, durationMs}` |

**Enforced:** `okstra_ctl.worker_liveness.probe_target` refuses a row with a missing/unknown `livenessMode`, a missing artifact field for that mode, or a missing `startedAt`; `--wait` additionally refuses a row with no `resultPath`. `tests/contract/test_team_state_worker_row_fields.py` fails when this table drifts from the keys `dispatch_core` writes.
