okstra 0.176.1 → 0.177.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/commands/execute/team.mjs +14 -4
  2. package/dist/commands/execute/team.mjs.map +1 -1
  3. package/dist/commands/lifecycle/install.mjs +0 -1
  4. package/dist/commands/lifecycle/install.mjs.map +1 -1
  5. package/docs/architecture.md +3 -3
  6. package/docs/cli.md +1 -1
  7. package/docs/project-structure-overview.md +2 -2
  8. package/docs/task-process/final-verification.md +5 -3
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/agents/workers/report-writer-worker.md +2 -2
  12. package/runtime/bin/okstra-compact-reminder.sh +2 -2
  13. package/runtime/bin/okstra-provider-exec.py +2 -7
  14. package/runtime/bin/okstra-render-report-views.py +13 -10
  15. package/runtime/prompts/coding-preflight/overview.md +2 -1
  16. package/runtime/prompts/lead/convergence.md +11 -6
  17. package/runtime/prompts/lead/okstra-lead-contract.md +3 -3
  18. package/runtime/prompts/lead/plan-body-verification.md +4 -2
  19. package/runtime/prompts/lead/report-writer.md +8 -4
  20. package/runtime/prompts/profiles/_common-contract.md +2 -2
  21. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  22. package/runtime/prompts/profiles/final-verification.md +11 -9
  23. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  24. package/runtime/prompts/profiles/release-handoff.md +7 -6
  25. package/runtime/prompts/wizard/prompts.ko.json +2 -2
  26. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +5 -6
  27. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +23 -1
  28. package/runtime/python/okstra_ctl/agent_invocation.py +17 -0
  29. package/runtime/python/okstra_ctl/agent_prompt_cli.py +15 -2
  30. package/runtime/python/okstra_ctl/dispatch_core.py +140 -17
  31. package/runtime/python/okstra_ctl/dispatch_state.py +60 -3
  32. package/runtime/python/okstra_ctl/execution_mutation_audit.py +49 -3
  33. package/runtime/python/okstra_ctl/handoff.py +27 -14
  34. package/runtime/python/okstra_ctl/model_cli.py +11 -2
  35. package/runtime/python/okstra_ctl/model_discovery.py +12 -0
  36. package/runtime/python/okstra_ctl/pane_reclaim.py +49 -43
  37. package/runtime/python/okstra_ctl/release_gate.py +56 -0
  38. package/runtime/python/okstra_ctl/render.py +53 -0
  39. package/runtime/python/okstra_ctl/report_contract.py +1 -0
  40. package/runtime/python/okstra_ctl/report_finalize.py +54 -0
  41. package/runtime/python/okstra_ctl/report_html/render.py +7 -4
  42. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +4 -1
  43. package/runtime/python/okstra_ctl/run.py +119 -18
  44. package/runtime/python/okstra_ctl/stage_targets.py +73 -1
  45. package/runtime/python/okstra_ctl/team.py +84 -14
  46. package/runtime/python/okstra_ctl/tmux.py +2 -3
  47. package/runtime/python/okstra_ctl/wizard.py +19 -10
  48. package/runtime/python/okstra_ctl/worker_liveness.py +3 -1
  49. package/runtime/python/okstra_ctl/worker_runner.py +2 -2
  50. package/runtime/python/okstra_ctl/wrapper_status.py +15 -0
  51. package/runtime/python/okstra_ctl/write_policy.py +9 -1
  52. package/runtime/schemas/final-report-v2.0.schema.json +58 -19
  53. package/runtime/skills/okstra-run/SKILL.md +3 -3
  54. package/runtime/templates/reports/html/base.template.html +1 -2
  55. package/runtime/templates/reports/html/i18n/en.json +4 -0
  56. package/runtime/templates/reports/html/i18n/ko.json +4 -0
  57. package/runtime/templates/reports/html/tasks/final-verification.template.html +7 -1
  58. package/runtime/validators/validate-report-views.py +30 -17
  59. package/runtime/validators/validate-run.py +221 -90
  60. package/runtime/validators/validate_analysis_report.py +2 -5
  61. package/runtime/validators/validate_session_conformance.py +1 -1
  62. package/runtime/bin/okstra-trace-cleanup.sh +0 -185
@@ -257,9 +257,11 @@ Run `okstra agent-prompt verify --run-manifest <path> --metadata
257
257
  <metadataPath> --json` immediately before dispatch. A failed verification is a
258
258
  pre-dispatch contract failure. For `runner=native-session`, pass only the
259
259
  returned `hostModelValue` to the host model argument. For
260
- `runner=cli-wrapper`, invoke `okstra worker-dispatch` and let it consume the
261
- returned `modelExecutionValue`; never pass that value as a native-host model
262
- token. Before a host-native call, run `okstra agent-prompt record-dispatch`
260
+ `runner=cli-wrapper`, follow the planned execution surface after
261
+ `core-pre-dispatch` verification: invoke `okstra team dispatch` when
262
+ `terminalBackend` is `cmux-pane`, otherwise invoke `okstra worker-dispatch`
263
+ and let it consume the returned `modelExecutionValue`; never pass that
264
+ value as a native-host model token. Before a host-native call, run `okstra agent-prompt record-dispatch`
263
265
  with the project root, run manifest, metadata path, and
264
266
  `--enforcement-mode host-native-spec-link-gate`. After its Result Path exists,
265
267
  run `okstra agent-prompt link-result` with the same run manifest,
@@ -607,7 +609,8 @@ materialize` with `--audience scope-critic`, `--assignment-ref critic/scope`,
607
609
  the critic worker ID, and `--dispatch-kind critic`. Verify the returned
608
610
  `metadataPath` before dispatch and use its `promptPath` without modification.
609
611
  For `runner=native-session`, use only `hostModelValue`; for
610
- `runner=cli-wrapper`, use `okstra worker-dispatch`, which consumes
612
+ `runner=cli-wrapper`, use `okstra team dispatch` when `terminalBackend` is
613
+ `cmux-pane`, otherwise `okstra worker-dispatch`, which consumes
611
614
  `modelExecutionValue`. Record host-native linkage with
612
615
  `enforcementMode=host-native-spec-link-gate` and the metadata path. If the
613
616
  persisted assignment or either model value required by its runner is absent,
@@ -727,8 +730,10 @@ accepted — surface candidate acceptance BLOCKERS the verifiers may have missed
727
730
  - regressions or broken error paths,
728
731
  - scope / contract violations.
729
732
  For each, emit a candidate blocker with a one-line statement, evidence (file:line /
730
- log / test output), and a severity (critical / major / minor). Do NOT restate an
731
- existing Acceptance Blocker. If you find none, say so explicitly.
733
+ log / test output), and a severity (critical / major). A finding that would not stop the
734
+ release is not a blocker — say so, and it is recorded as a conditional-acceptance
735
+ condition instead. Do NOT restate an existing Acceptance Blocker. If you find none, say
736
+ so explicitly.
732
737
  ```
733
738
 
734
739
  ### Verification — confirm-or-downgrade (BLOCKING)
@@ -109,7 +109,7 @@ Required checkpoints:
109
109
  - `PROGRESS: phase-5-collect worker=<role> status=<terminal-status>` — once per worker, immediately after the result file is verified. `<role>` is the roster role, same rule as `phase-4-dispatch` above.
110
110
  - `PROGRESS: phase-5.5-convergence round=<N> queue=<count>` — at the start of each convergence round (Phase 5.5).
111
111
  - `PROGRESS: phase-5.6-critic provider=<provider> gaps=<n>` — after the critic result is collected (Phase 5.6, opt-in; the critic dispatch itself fires concurrently with the first 5.5 reverify round). Omitted when `convergence.critic.enabled == false`.
112
- - `PROGRESS: phase-batch-cleanup panes=<n>` — immediately after cleaning up the previous batch's panes, at each batch boundary (① just before the first `phase-5.5-convergence` round ② just before the `phase-6-synthesis` report-writer dispatch). `<n>` is the number of panes reclaimed at that boundary — worker-compute panes plus completed teammate panes, which are panes too — read from the cleanup's `--list` pass taken immediately before the reclaim, never estimated. Expose only the counts and NEVER expose `%NNN`/lead-pane.id/raw worker handles. Just before the first batch (analysis-worker dispatch) there is nothing to clean up, so it is a no-op and the marker is omitted.
112
+ - `PROGRESS: phase-batch-cleanup panes=<n>` — immediately after cleaning up the previous batch's panes, at each batch boundary (① just before the first `phase-5.5-convergence` round ② just before the `phase-6-synthesis` report-writer dispatch). `<n>` is the number of panes closed at that boundary — the panes of dispatches this run recorded and that have since finished — read from the `okstra team reclaim --dry-run` pass taken immediately before the closing pass, never estimated. A pane the harness opened for its own teammate carries no recorded id, so it is not counted and not closed. Expose only the counts and NEVER expose a raw `paneId` or worker handle. Just before the first batch (analysis-worker dispatch) there is nothing to clean up, so it is a no-op and the marker is omitted.
113
113
  - `PROGRESS: phase-6-synthesis dispatching report-writer-worker` — at the start of Phase 6.
114
114
  - `PROGRESS: phase-5.5.9-plan-verify round=<N> items=<count>` — immediately before dispatching each plan-body verification round (`implementation-planning` only; see [plan-body-verification](./plan-body-verification.md) §"Round protocol"). Each round is a worker batch like any other, so round 2 and later MUST be preceded by a `phase-batch-cleanup` line reclaiming the previous round's verifiers. The numbering keeps this line sorted where the work happens — after Phase 6, because the round verifies the drafted plan body.
115
115
  - `PROGRESS: user-confirm <C-NNN> <the question, one line>` — immediately before asking the user about anything that would otherwise become an open `Blocks=approval` row (see "User confirmation before an approval blocker" below). Not tied to a phase: it fires wherever the blocker surfaces. `<C-NNN>` is the id the row will carry, so the answer and the row can be matched afterwards.
@@ -173,7 +173,7 @@ The table below documents those prep-time seed values **for reference only** —
173
173
  | Codex worker | gpt-5.6-sol | codex-worker | duty + task instructions composed per invocation; deterministic `worker-dispatch` execution |
174
174
  | Antigravity worker | gemini-3.1-pro | antigravity-worker | duty + task instructions composed per invocation; deterministic `worker-dispatch` execution |
175
175
 
176
- Each analysis assignment follows its recorded `runner`. `runner=native-session` uses the host's native subagent primitive after `host-native-spec-link-gate`; `runner=cli-wrapper` uses the deterministic `okstra worker-dispatch` process boundary after `core-pre-dispatch` verification. No LLM transport wrapper sits in front of a provider CLI.
176
+ Each analysis assignment follows its recorded `runner`. `runner=native-session` uses the host's native subagent primitive after `host-native-spec-link-gate`. `runner=cli-wrapper` follows the planned execution surface after `core-pre-dispatch` verification: `okstra team dispatch` when `terminalBackend` is `cmux-pane`, otherwise the deterministic `okstra worker-dispatch` process boundary. No LLM transport wrapper sits in front of a provider CLI.
177
177
 
178
178
  ### Implementation phase: Executor binding
179
179
 
@@ -182,7 +182,7 @@ For `--task-type implementation` runs, the task bundle additionally pins one of
182
182
  - `instruction-set/analysis-profile.md` — top "Executor binding" block (provider, display name, model, runner, and dispatch mode)
183
183
  - `runs/implementation/manifests/run-manifest-*.json` — `teamContract.executor` object (the same binding plus `appliesTo: "implementation"`)
184
184
 
185
- Lead MUST dispatch Edit/Write-bearing work only through that executor binding: use the host primitive with `hostModelValue` for `runner=native-session`, or `okstra worker-dispatch` with `modelExecutionValue` for `runner=cli-wrapper`. The other providers in the roster still run as read-only verifiers in the same run; the executor's own provider does not, because its worker ID materializes as the executor on every dispatch — so the diff is reviewed context-isolated by the remaining verifiers. Session isolation is the primary self-review safeguard — a verifier reusing the executor's model variant is acceptable in a distinct session. A different model variant (e.g. executor=opus / Claude verifier=sonnet) is recommended but not mandatory.
185
+ Lead MUST dispatch Edit/Write-bearing work only through that executor binding: use the host primitive with `hostModelValue` for `runner=native-session`. For `runner=cli-wrapper`, use `okstra team dispatch` when `terminalBackend` is `cmux-pane`, or `okstra worker-dispatch` with `modelExecutionValue` otherwise. The other providers in the roster still run as read-only verifiers in the same run; the executor's own provider does not, because its worker ID materializes as the executor on every dispatch — so the diff is reviewed context-isolated by the remaining verifiers. Session isolation is the primary self-review safeguard — a verifier reusing the executor's model variant is acceptable in a distinct session. A different model variant (e.g. executor=opus / Claude verifier=sonnet) is recommended but not mandatory.
186
186
 
187
187
  Executor is chosen at run-prep time via `--executor <claude|codex|antigravity>` (or `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`); the model used by the executor is taken from the corresponding worker model flag (`--claude-model` / `--codex-model` / `--antigravity-model`). For CLI-backed executors, the underlying file mutation happens inside the executor CLI's own auto-edit mode (e.g. `codex exec --sandbox workspace-write`), not through the lead runtime's `write_artifact` operation.
188
188
 
@@ -216,8 +216,10 @@ the host primitive, run `okstra agent-prompt record-dispatch` with the project
216
216
  root, run manifest, metadata path, and `--enforcement-mode
217
217
  host-native-spec-link-gate`, then run `okstra agent-prompt link-result` with
218
218
  `--dispatch-id <invocationId>:attempt-1` and the result path before parsing it;
219
- CLI-wrapper calls go through `okstra worker-dispatch` and consume only
220
- `modelExecutionValue`. A missing or invalid invocation contract blocks the
219
+ CLI-wrapper calls follow the planned execution surface after
220
+ `core-pre-dispatch` verification: `okstra team dispatch` when
221
+ `terminalBackend` is `cmux-pane`, otherwise `okstra worker-dispatch`, and
222
+ consume only `modelExecutionValue`. A missing or invalid invocation contract blocks the
221
223
  round before any host or provider process starts.
222
224
 
223
225
  1. Lead runs `okstra plan-items extract --data <data.json> --output <state>/plan-items-....json`, places the persisted `items[]` verbatim in every verifier prompt with the compact `subject` and lossless `payload`, then runs `okstra plan-items validate --data <data.json> --items <state>/plan-items-....json`. Dispatch only after that exact-match validation succeeds.
@@ -34,7 +34,7 @@ Emit `frontmatter.approved` as `false` and `frontmatter.implementationOption` as
34
34
  2. Write a call-specific task-instructions file containing the anchor headers and audience-specific reading list.
35
35
  3. Run `okstra agent-prompt materialize --audience report-writer --assignment-ref initial/report-writer --worker-id report-writer --dispatch-kind report-writer ...`, then run `okstra agent-prompt verify` against the returned `metadataPath`. Use the returned `promptPath` without appending role prose. A correction redispatch repeats this step with a fresh invocation ID and the same audience and assignment reference.
36
36
  4. Emit the Phase 6 checkpoint.
37
- 5. For `runner=native-session`, first run `okstra agent-prompt record-dispatch` with the project root, run manifest, metadata path, and `--enforcement-mode host-native-spec-link-gate`, then call the host primitive with only the returned `hostModelValue`. After its result exists, run `okstra agent-prompt link-result` with `--dispatch-id <invocationId>:attempt-1` and the result path before accepting it. For `runner=cli-wrapper`, call `okstra worker-dispatch --workers report-writer`, which consumes `modelExecutionValue` and verifies the metadata before starting the provider process. Never combine this Phase 6 call with analysis workers.
37
+ 5. For `runner=native-session`, first run `okstra agent-prompt record-dispatch` with the project root, run manifest, metadata path, and `--enforcement-mode host-native-spec-link-gate`, then call the host primitive with only the returned `hostModelValue`. After its result exists, run `okstra agent-prompt link-result` with `--dispatch-id <invocationId>:attempt-1` and the result path before accepting it. For `runner=cli-wrapper`, call `okstra team dispatch` when `terminalBackend` is `cmux-pane`, or `okstra worker-dispatch --workers report-writer` otherwise, which consumes `modelExecutionValue` and verifies the metadata before starting the provider process. Never combine this Phase 6 call with analysis workers.
38
38
  6. Call `await_workers([handle])` and verify the data.json Result Path, rendered Markdown sibling, and worker-result pointer at Worker Result Path. Verify the separate heartbeat audit sidecar before accepting the run. **Enforced:** both dispatch adapters keep the three completion paths in `WorkerJob.completion_paths`, and `validators/validate_session_conformance.py` validates the audit sidecar.
39
39
 
40
40
  The complete assignment supplies both runner-specific model values and the prompt header in item 9 below. A native host uses `hostModelValue`; a deterministic provider process uses `modelExecutionValue`; the recorded `**Model:**` header remains the canonical assignment label. Missing or unsupported model resolution is a pre-dispatch contract failure; the common contract does not choose a runtime fallback.
@@ -108,8 +108,9 @@ use the returned `promptPath` unchanged. A native-session call uses only
108
108
  manifest, metadata path, and `--enforcement-mode
109
109
  host-native-spec-link-gate`, then run `okstra agent-prompt link-result` with
110
110
  `--dispatch-id <invocationId>:attempt-1` and the translation result before
111
- accepting it. A CLI-wrapper call uses `okstra worker-dispatch` and its
112
- `modelExecutionValue`. The host-native record links the accepted result to a
111
+ accepting it. A CLI-wrapper call uses `okstra team dispatch` when `terminalBackend` is
112
+ `cmux-pane`, or `okstra worker-dispatch` and its `modelExecutionValue`
113
+ otherwise. The host-native record links the accepted result to a
113
114
  verified call specification but does not assert that Okstra observed the host's
114
115
  actual prompt delivery.
115
116
 
@@ -124,7 +125,7 @@ okstra report-finalize \
124
125
  --report <runDirectoryPath>/reports/final-report-<task-type>-<seq>.md
125
126
  ```
126
127
 
127
- Do NOT run the six steps below by hand. Hand-running them is the recurring root cause of reports shipping with stale activity, `--` token cells, a missing html sibling, Section 3 missing follow-up entries, or Section 4 rows never spawning — the order is load-bearing and a skipped step surfaces only later, as a validator `contract-violated`. Every step is idempotent, so after fixing a reported failure just re-run the same command.
128
+ Do NOT run the seven steps below by hand. Hand-running them is the recurring root cause of reports shipping with stale activity, `--` token cells, a missing html sibling, Section 3 missing follow-up entries, or Section 4 rows never spawning — the order is load-bearing and a skipped step surfaces only later, as a validator `contract-violated`. Every step is idempotent, so after fixing a reported failure just re-run the same command.
128
129
 
129
130
  The steps it executes, in this contractual order, and the contract each one carries:
130
131
 
@@ -160,6 +161,8 @@ The steps it executes, in this contractual order, and the contract each one carr
160
161
  The status file is written after routing and follow-up persistence completes.
161
162
  6. **`validate-run` — validate the finished run.** Checks the completed artifact set, including exact canonical-event-to-`agentActivity[]` conformance and the report-views contract that catches a missing or stale html sibling. A failure here names the specific contract; fix it and re-run `okstra report-finalize`.
162
163
 
164
+ 7. **`teardown-stages` — reclaim the stage worktrees.** In-process, and a no-op for every run except a whole-task `final-verification` whose verdict clears the work for release (`accepted`, or `conditional-accept` with no condition blocking release). Whole-task entry merges the done stages but deliberately leaves their worktrees and registry stage-keys in place, because a `blocked` verdict routes straight back to rework on those trees. This step is where they are reclaimed once the verdict says the work is moving on. A stage worktree with uncommitted changes is preserved; stage branches are never deleted.
165
+
163
166
  After `okstra report-finalize` reports `"ok": true`, **execute the run-scoped cleanup gate.** Call `shutdown_workers` only after that success, all persistence work, and explicit user approval under [okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle". If the user keeps resources, leave the selected adapter's resources intact and surface its manual cleanup guidance.
164
167
 
165
168
  ## Schema-v2 report data responsibilities
@@ -355,6 +358,7 @@ Every field MUST anchor its claim with at least one evidence reference — a `pa
355
358
  1. **Clarification Items** — single unified `C-*` table; column schema (4 columns with the short fields stacked in one record-meta cell), ID convention, and rerun behaviour are owned by `_common-contract.md §Clarification request policy` (SSOT). The deprecated `5.5.9 Open Questions` / `1.1 Additional Material Request` / `1.2 User Confirmation Questions` sub-sections are removed; the validator fails reports that reintroduce them.
356
359
  - **Open `Blocks=approval` rows carry `origin` and `userConfirmation`** (same SSOT). Lead's dispatch prompt MUST state, per intended blocker, which `origin` applies and what Lead did about it — the writer cannot observe either. When Lead instructed the writer to raise an item rather than decide it, that row's `origin` is `lead-directed` no matter how the workers subsequently voted on it: an instruction returning as a consensus is not a finding. Before writing such an instruction, run the confirmation sequence in [okstra-lead-contract](./okstra-lead-contract.md) "User confirmation before an approval blocker" — asking first is usually cheaper than the row.
357
360
  2. **Evidence and Detailed Analysis** — primary evidence rows (file path, line, snippet); secondary evidence / alternate interpretations. If `reference-expectations.md` lists explicit expected values, record match/gap per row.
361
+ - **Final-verification added-surface audit.** When `header.taskType` is `final-verification`, populate `finalVerification.addedSurfaceAudit` from the workers' enumeration of what the diff added — one row per identifier / module / configuration entry, each with the callers found across the repository and its disposition. Do not summarise the rows away: this table is the only machine-readable evidence for the over-delivery axis, and requirement coverage answers the opposite question. An `over-delivery` row's `note` MUST cite the `AB-NNN` (no caller) or `CA-NNN` (called, but no requirement) row it became, and that row MUST exist in this report. **Enforced:** `schemas/final-report-v2.0.schema.json` requires the array and rejects an unknown `disposition`; `validators/validate-run.py::_validate_added_surface_audit` enforces the citation and refuses a `traced` row that names no requirement.
358
362
  - **Error-analysis diagnosis and routing.** When `header.taskType` is `error-analysis`, populate the required `errorAnalysis` object. Copy `errorAnalysis.symptomVerbatim` byte-for-byte from the symptom stated in the brief's `Source Material`; do not paraphrase it. Every `causeCandidates[]` row includes the full `supportingEvidence`, `falsifyingEvidenceChecked`, `confidence`, and `disproveWith` fields. When a candidate is a step in a propagation chain rather than a competing explanation — the analysis calls it a downstream step, a second stage, or a consequence of another candidate — set its `downstreamOf` to the ids of the candidates immediately upstream of it; leave the field absent for a candidate that stands on its own. Every id listed MUST be another candidate in the same report, no row may name itself, and the links MUST NOT form a cycle; `validators/validate-run.py::_validate_cause_chain` rejects all three. This is the only place the chain is machine-readable — prose calling a candidate "the second step of the chain" while `downstreamOf` is absent leaves the report's figure claiming the candidates are alternatives. Route `errorAnalysis.routing.nextTaskType=implementation-option-selection` with `direction=begin-option-selection`, or route `errorAnalysis.routing.nextTaskType=error-analysis` with `direction=continue-investigation`; no other pairing is valid. `verdictCard.nextStep`, `finalVerdict.nextStep`, the first `recommendedNextSteps` action and command, and the unique `followUpTasks` row whose `origin` is `phase-continuation` MUST all point to the same `errorAnalysis.routing.nextTaskType` target. The schema enforces only the presence of a `phase-continuation` row; `validators/validate-run.py::_validate_error_analysis_consistency` enforces exact target agreement and uniqueness.
359
363
  - **Implementation-option-selection comparison.** When `header.taskType` is `implementation-option-selection`, populate `implementationOptionSelection` from the converged direction-selection findings. Preserve every merged or rejected raw candidate in `candidateAudit`, and put at most three selectable candidates in `rankedOptions`. Each displayed candidate carries its requirement coverage, scope commitments, criterion scores, feasibility votes, safety blockers, unresolved feasibility facts, planning invariants, and exact coverage summary. In each displayed candidate, `expectedChangeAreas` names direction-level change surfaces, never exact file paths or an exact file list. `expectedVerification` names direction-level verification signals, never a stage list or executable test commands. `schemas/final-report-v2.0.schema.json` enforces the displayed-summary constants and the three-option cap; semantic recalculation belongs to `validators/validate-run.py`.
360
364
  - **Routing.** `implementationOptionSelection.routing` is a required **string enum** — not an object — with exactly three values: `implementation-planning`, `pending-direction-selection`, `blocked`. It is the only field in this report that records where the task goes next, and Phase 7 projects `workflow.nextRecommendedPhase` from it (`scripts/okstra_ctl/next_phase.py`): `implementation-planning` becomes a `ready` pointer naming that phase, while `pending-direction-selection` and `blocked` become `pending` and `blocked` pointers carrying no phase. Only the first proposes a next run.
@@ -12,7 +12,7 @@ profile document.
12
12
  - **Phase 5.5 (convergence — peer review by workers)**: workers peer-review each other's findings across up to `effectiveMaxRounds` rounds; the lead mediates but does not vote. See `prompts/lead/convergence.md` for the round protocol (replay of findings, `AGREE` / `DISAGREE` / `SUPPLEMENT` verdicts), queue invariants, and final classification (`full-consensus` / `partial-consensus` / `contested` / `worker-unique`). For `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `project-analysis`, `feature-analysis`, and `change-impact-analysis` this phase runs in **adversarial mode** (`convergence.adversarial=true`): verifiers try to refute each finding against its cited evidence and the burden of proof sits on the claim — see that skill's §"Adversarial Verification Mode".
13
13
  - Do NOT conclude "no peer review happens" from the roster alone — every profile that lists ≥2 analyser workers runs convergence by default (`convergence.enabled=true` in `task-manifest.json`).
14
14
  - For a new `implementation-planning` run, the plan-body sequence is initial verification → one planner self-fix → targeted re-verification → user gate. The initial verification is round 1, the targeted re-verification is round 2, and a second automatic self-fix is a contract violation. A user-directed correction does not consume the automatic self-fix limit, and a verification failure after that correction does not restart the automatic loop.
15
- - **provider-unavailable fallback (tolerance).** A worker dispatch can fail to produce a result for two distinct reasons, and both take the same recovery path. (1) **Pane budget:** the dispatch is rejected with `no room for another tmux split` (or an equivalent teammate-pane creation failure) — this is the harness running out of room for its own teammate panes, not okstra placing a worker. (2) **Sandbox CLI-start failure:** an external CLI worker wrapper exits non-zero within seconds with empty stdout and its live-log shows `operation not permitted`. In either case the lead spends the one shared retry budget through the assignment's recorded runner. If the provider is still unavailable, record that terminal status and continue only under the convergence quorum rules; never replace it silently with a fixed provider or count a substitute as the original provider's vote. Completed external-CLI workers hold no pane of their own; the harness teammate panes are reclaimed by the selected runtime adapter's resource lifecycle. (This is a prompt instruction, not a code-enforced gate.)
15
+ - **provider-unavailable fallback (tolerance).** A worker dispatch can fail to produce a result for two distinct reasons, and both take the same recovery path. (1) **Pane budget:** the dispatch is rejected because a teammate pane could not be created — this is the harness running out of room for its own teammate panes, not okstra placing a worker. The wording is the host's, so match the condition rather than a fixed string. (2) **Sandbox CLI-start failure:** an external CLI worker wrapper exits non-zero within seconds with empty stdout and its live-log shows `operation not permitted`. In either case the lead spends the one shared retry budget through the assignment's recorded runner. If the provider is still unavailable, record that terminal status and continue only under the convergence quorum rules; never replace it silently with a fixed provider or count a substitute as the original provider's vote. Completed external-CLI workers hold no pane of their own. A pane the harness opened for its own teammate carries no id okstra recorded, so no okstra command closes it — the host and the user own that surface. (This is a prompt instruction, not a code-enforced gate.)
16
16
  - Dual-audience final-report contract (shared):
17
17
  - data.json is the sole authored report artifact. AI handoff Markdown and human HTML are independently derived from it; neither derived artifact is the other's source.
18
18
  - User-facing information belongs in `humanSummary` and the selected task block's `userNarrative`; it must not exist only in Markdown. The HTML human main body explains the result with those fields plus task facts.
@@ -90,7 +90,7 @@ profile document.
90
90
  - When a response is carried in, reconcile every prior `clarificationItems[]` row against new evidence and update its status to `resolved` or `obsolete` before issuing the next verdict. Schema-v1 compatibility Markdown may additionally render its conditional Section 0; schema-v2 AI Markdown records decisions under `## Clarification and User Decisions`.
91
91
  - **Supersession (BLOCKING).** Reconciling the `C-*` row is only half of incorporating an answer. An answer does not merely *add* a decision — it *invalidates* whatever the previous run wrote under the opposite assumption. Before issuing the next decision, walk the prior deliverable prose for every statement the answer makes false and **delete or rewrite it**, then record the retirement. Adding the new decision while leaving the contradicting sentence in place puts two opposite instructions for the same symbol in one document; the implementer must then guess which is live, and the next verification round correctly blocks on it. In `implementation-planning` this record is `implementationPlanning.supersessionLedger[]` — one entry per answered clarification, either `disposition: superseded` (with the retired statement, its replacement, and the sections revised) or `disposition: no-dependent-statement` (with a rationale). **Enforced:** `validators/validate-run.py` `_validate_supersession_ledger` requires an entry per answered clarification; whether the claim is *true* is what the §5.5.9 adversarial round tests.
92
92
  - Verdict Card data consistency (shared; schema-v1 Markdown keeps the legacy visible card):
93
- - `verdictCard.verdictToken` and `.direction` MUST byte-match `finalVerdict`; next-step routing must agree with `recommendedNextSteps[0]`. Schema-v1 Markdown renders the visible `## Verdict Card` / `## 7. Final Verdict` pair. Schema-v2 derives the AI handoff and human summary from the data fields without repeating both visible sections. **Enforced:** `validators/validate-run.py` `_validate_verdict_card_consistency` and `_validate_verdict_card_fields`.
93
+ - The Card carries no verdict token — the token lives once, in `finalVerdict.verdictToken`, and every gate reads it there. `verdictCard.direction` MUST byte-match `finalVerdict.direction`; next-step routing must agree with `recommendedNextSteps[0]`. The AI handoff and human summary are derived from the data fields without repeating both visible sections. **Enforced:** `validators/validate-run.py` `_validate_verdict_card_fields`.
94
94
  - Cross-worker traceability (shared — applies to every analysis worker output and to the lead's `## 6.` / `## 2.` tables in the final-report):
95
95
  - **Worker-side item IDs (free-form but unique within the worker).** Every row item in sections 1–5 (and any optional section 6) of an analysis worker's output MUST carry an item ID that is unique within that one worker's result file. The ID convention is the worker's choice — `F-001` / `F-002` per the suggested schema, `1.1` / `1.2` / `1.3` as Codex tends to use, or any other shape — but it MUST appear as the leading column of the row (for table-form items) or as a `[<ID>]` prefix (for bullet/numbered items). Workers that emit findings without IDs make cross-worker reconciliation impossible.
96
96
  - **Lead-side ID assignment + source preservation.** When the lead (or `report-writer-worker`) synthesises consensus, difference, or primary-evidence rows from worker outputs, the lead assigns a fresh `C-NNN` / `D-NNN` / `E-NNN` row ID. Each `sourceItems` field MUST list every contributing worker:item pair (e.g. `claude:F-001`, `codex:1.1`, `grok:F-3`, `kimi:2.4`) so an agent can trace the synthesised row to the worker result. Bare worker names are rejected. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.SourceItem` pins each entry to `^[a-z][a-z-]*:[A-Za-z0-9._-]+$`, and `ConsensusRow` / `PrimaryEvidenceRow` require non-empty `sourceItems`.
@@ -215,7 +215,7 @@ A mocked unit test cannot observe the SQL a query builder actually emits — `co
215
215
  - **Requirement when fired.** The verifier MUST reproduce a real-DB execution: run the `db-test` tier (Tier 1 = plan `validation` db step; else Tier 2 = `project.json.qaCommands.db-test`) against a **local / replica** datastore (same engine + schema — never shared / staging / prod, consistent with the verifier forbidden-actions list) and record its exact command + exit code. A mock, an in-memory shim that does not parse real SQL, or static reasoning does NOT satisfy this.
216
216
  - **No `db-test` command available → blocking, not a passive skip.** If neither tier declares a `db-test` command, the verifier records the blocking finding `db-test not configured — DB change unverified (mock-only)` and sets the verdict to `FAIL`; it MUST NOT emit only the passive `qa-command not configured` note and pass. Recommended fix: declare a `db-test` command in `project.json.qaCommands` or the plan's validation set.
217
217
  - **Mock-only evidence → unverified.** If the diff's only DB coverage is mocked, the verifier labels the DB portion `static-analysis only …, unverified (not executed)` (never `verified`), records it as a blocking finding, and sets `FAIL`. Never downplay the real run as "too heavy / static proof suffices".
218
- - **Surface it at every layer.** The finding is copied verbatim into the verifier result and MUST survive into the final report's `## 6.` and Verdict Card, so the user sees the DB-unverified state continuously — it is the load-bearing reason a downstream `final-verification` cannot reach `accepted` and `release-handoff` cannot push. **Enforced:** `validators/validate-run.py` `_validate_verifier_fail_blocks_verdict` fails a report whose `verdictCard.verdictToken` is `accepted` / `conditional-accept` while any `implementation.verifierResults[]` row records `verdict: FAIL` — a rejection dropped during synthesis is exactly how rejected work reached `release-handoff`.
218
+ - **Surface it at every layer.** The finding is copied verbatim into the verifier result and MUST survive into the final report's `## 6.` and Verdict Card, so the user sees the DB-unverified state continuously — it is the load-bearing reason a downstream `final-verification` cannot reach `accepted` and `release-handoff` cannot push. **Enforced:** `validators/validate-run.py` `_validate_verifier_fail_blocks_verdict` fails a report whose `finalVerdict.verdictToken` is `accepted` / `conditional-accept` while any `implementation.verifierResults[]` row records `verdict: FAIL` — a rejection dropped during synthesis is exactly how rejected work reached `release-handoff`.
219
219
 
220
220
  ## All-verifier-failure policy
221
221
 
@@ -9,7 +9,7 @@ roles:
9
9
  duty: acceptance-verifier
10
10
  - role: critic
11
11
  min: 0
12
- recommended: 0
12
+ recommended: 1
13
13
  max: 1
14
14
  duty: acceptance-critic
15
15
  - role: report-writer
@@ -19,7 +19,7 @@ roles:
19
19
  duty: report-writer
20
20
  ```
21
21
 
22
- - Purpose: analyse completed work and identify remaining defects, omissions, and delivery risk before final acceptance
22
+ - Purpose: judge the delivered implementation on three axes before final acceptance — does it cover every requirement (under-delivery), does it carry work no requirement asked for (over-delivery), and does it actually do what it claims (defects, and tests that verify nothing). Whether the run followed okstra's own procedure is not one of the axes: the runtime and `validators/validate-run.py` own that, and a finding about it is not an acceptance judgement
23
23
  - Required workers:
24
24
  - claude
25
25
  - codex
@@ -32,6 +32,7 @@ roles:
32
32
  - Primary focus areas (each maps to a deliverable section below):
33
33
  - Acceptance-gating — a failure here pushes the verdict toward `blocked` / `conditional-accept`:
34
34
  - requirement & acceptance coverage — every must-pass point in the brief's `## Expected Behavior` / `## Preserved Behavior` / `## Expected Outcome` (and the approved plan's requirements) is covered with a cited artifact or raised as an Acceptance Blocker; no silent omissions
35
+ - over-delivery — every surface the merged diff **adds** is traced back to a requirement. Enumerate them: each identifier, module, and configuration entry the diff introduces, searched for its callers across the whole repository (a declaration, its own test, or a commented-out line is not a caller). Record one `addedSurfaceAudit` row per surface with its disposition. `traced` names the brief requirement it serves. `exempt` names one of the legitimate exits and cites it — the approved plan reserves it for a named later stage, or something outside project code calls it (framework entrypoint, implemented interface method, migration hook, published-package API). `over-delivery` is everything else, and it is graded by callers: **no caller anywhere** is an Acceptance Blocker (delete it, or fold an added parameter back into its single call site), while **called but serving no requirement** is a Conditional Acceptance Condition with `blocksReleaseHandoff: false` — the judgement there rests on reading intent, so it travels to the PR body instead of stopping the release. The baseline is the brief, not the approved plan: a surface the plan authorised but no requirement asked for is still over-delivery, and this is the last gate that can see it. **Enforced:** `validators/validate-run.py` `_validate_added_surface_audit` refuses a `traced` row naming no requirement, and an `over-delivery` row whose note does not cite the `AB-NNN` / `CA-NNN` row it became.
35
36
  - delivered artifacts match recorded expected values in `reference-expectations` (config files, deployment manifests, other recorded expected states); when reference-expectations are absent, record it as missing information rather than assuming a match
36
37
  - test & validation suite pass status — independently re-run the read-only two-tier command set (Tier 1 = brief/approved-plan `validation`, Tier 2 = `project.json` `qaCommands`) and confirm each passes on the verified head, citing exact command + exit code
37
38
  - test correctness — delivered tests actually assert the intended behaviour: no gutted/weakened assertions, no tautological or always-passing tests, no tests exercising only mocks; new behaviour has matching coverage. For an `external-interface` / `transformation-mapping` surface specifically, treat a test whose only oracle is a self-authored synthetic fixture (no captured-real-sample provenance) as NOT establishing external correctness — it shows only that the parser agrees with its author's assumed shape, never that the shape matches reality; record that surface's external correctness as a user-owned external advisory gap (a Residual Risk carrying the exact "capture a real sample and confirm" next step, per the Coverage check in the self-review pass below), never as covered — the same non-blocking treatment the External QA advisory policy gives a live external check
@@ -48,13 +49,13 @@ roles:
48
49
  - Pre-verification entry gate (resolved & enforced by `okstra render-bundle` prep — the lead does NOT recompute it):
49
50
  - the verification target (scope / worktree / base / stages / source reports / diff stat) is injected as the `VERIFICATION_TARGET` block. The lead MUST treat it as authoritative and MUST NOT re-pick a target from the brief.
50
51
  - **whole-task scope** (`--stage auto`, default): prep has already verified every Stage Map stage is `status:done` in `consumers.jsonl`, every done stage's `head_commit` is an ancestor of the task worktree HEAD (all stage branches merged), and the worktree is clean outside `.okstra/`. If any check failed the run never started (PrepareError); a started whole-task run is therefore a fully-merged, clean target.
51
- - **whole-task is a mutating phase, not a read-only one.** On entry, whole-task mode auto-merges (with `--no-ff`) the done stages not yet merged into the task branch to create an integration commit, then removes the cleanupable stage worktrees and marks their registry stage-keys released. The stage branches are kept as the reviewable stack (the target of `okstra handoff local-checkout --stage <N>`). If a merge conflict occurs it reports the conflicting files and aborts (the user resolves them manually and retries). A stage worktree with uncommitted changes remaining is preserved. Therefore the "fully-merged, clean target" the entry gate above refers to is the state after this auto-integration step completes, and whole-task final-verification must be treated as a mutating phase that creates the integration commit.
52
+ - **whole-task is a mutating phase, not a read-only one.** On entry, whole-task mode auto-merges (with `--no-ff`) the done stages not yet merged into the task branch to create an integration commit. The stage worktrees are NOT removed on entry: they are reclaimed after the verdict, by the Phase 7 `teardown-stages` step, and only when the verdict clears the work for release (`accepted`, or `conditional-accept` with no condition blocking release). A `blocked` verdict therefore leaves every stage worktree in place, so the rework it routes to can start immediately. The stage branches are kept as the reviewable stack (the target of `okstra handoff local-checkout --stage <N>`). If a merge conflict occurs it reports the conflicting files and aborts (the user resolves them manually and retries). A stage worktree with uncommitted changes remaining is preserved. Therefore the "fully-merged, clean target" the entry gate above refers to is the state after this auto-integration step completes, and whole-task final-verification must be treated as a mutating phase that creates the integration commit.
52
53
  - **single-stage scope** (`--stage N`): prep verified stage N is `status:done` and its isolated stage worktree exists and is clean. Other stages' state is irrelevant. A single-stage run is a partial verification: it MUST NOT recommend plain `release-handoff`, but MAY recommend `release-handoff(stage-group)` when the verdict is `accepted` — the stage becomes PR-eligible for a stage-group handoff.
53
54
  - the lead still captures `git status --short` from the injected worktree to confirm the analysis ran against the delivered work-tree state; an unexpected divergence (dirty tree outside `.okstra/`, missing worktree) is a `tool-failure`, not a silent proceed.
54
55
  - Worker verification procedure:
55
- - **Target confirmation:** verify the inline worktree, scope, base/head refs, target path, and digest against `verification-target.md` before analysis. Use the sidecar's stage/report mapping and complete diff stat; a missing sidecar, digest mismatch, dirty worktree outside `.okstra/`, or wrong head is a `tool-failure`, never a silent target reselection.
56
+ - **Target confirmation:** analyse the injected target and nothing else. Read `verification-target.md` for the stage/report mapping and the complete diff stat. Prepare fixed that target and `validators/validate-run.py` `_validate_verification_target_match` re-checks the report against its digest, so the procedure to follow here is simply: if the worktree you can see does not match the injected target, record a `tool-failure` — never reselect a target.
56
57
  - **Evidence:** attach file:line, exact command + exit code, log excerpt, or MCP SELECT evidence to every finding. Mark a requirement as covered only when the cited artifact demonstrates it.
57
- - **Tier 1 and Tier 2 read-only validation:** Tier 1 is the originating brief/approved plan `validation` set; Tier 2 is `<PROJECT_ROOT>/.okstra/project.json` `qaCommands`. Do not auto-detect commands from package manifests. A missing tier is `qa-command not configured: <category>`. Before execution, reject commands containing source/lockfile mutation tokens such as `--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, or `npm install` without `ci`; record the exact denied token.
58
+ - **Tier 1 and Tier 2 read-only validation:** Tier 1 is the originating brief/approved plan `validation` set; Tier 2 is `<PROJECT_ROOT>/.okstra/project.json` `qaCommands`. Do not auto-detect commands from package manifests. A missing tier is `qa-command not configured: <category>`. Before execution, reject commands containing source/lockfile mutation tokens such as `--fix`, `--write`, ` -w`, ` -u`, `--snapshot-update`, `INSTA_UPDATE=<not-no>`, `cargo update`, or `npm install` without `ci`; record the exact denied token. Tier 2 is already screened — prepare refuses to start the run when `project.json` declares such a token (`okstra_ctl.run.validate_project_qa_commands`), so this check is the one that catches a Tier 1 command the brief or plan named.
58
59
  - **External QA outcome policy:** continue to attempt every in-scope Tier 3
59
60
  command. For an entry requiring `db`, `http`, or `external`, record non-PASS
60
61
  as a Tier 3 `advisory` command, add a user-owned Residual Risk and exact
@@ -76,14 +77,15 @@ roles:
76
77
  - **Source-mutation prohibition:** verification may write only assigned okstra run artifacts. Do not edit source, schema, deployment, lockfile, or configuration files; route detected defects to a later phase.
77
78
  - Required deliverable shape (final report, in addition to the standard sections):
78
79
  - **Source Implementation Report(s)** (**Enforced:** `validators/validate-run.py` `_validate_verification_target_match` compares `verificationScope`, `worktreePath`, `implementationBaseRef`, `capturedHeadSha`, and the `stageReports` stage set against the digest-verified `instruction-set/verification-target.md`; a snapshot whose digest no longer checks out is ignored rather than trusted. `verificationScope` in particular gates both stage-group eligibility and release-handoff routing, so it is not the report's to restate): the `VERIFICATION_TARGET` snapshot verbatim — verification scope, worktree path, base/head refs, the list of stages under verification, and one row per stage citing its originating implementation final-report (`report_path` from `consumers.jsonl`; render `(report_path unrecorded)` when absent). Every analyser prompt carries the same compact target identity (`**Verification scope:** / **Worktree:** / **Verification base ref:** / **Verification head ref:** / **Verification target path:** / **Verification target digest:**`) and reads the sidecar on demand for the complete diff stat. A worker that cannot confirm its analysis ran against that worktree's delivered diff MUST record a `tool-failure`.
79
- - **Verdict vocabulary**: Section 7 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed. Each condition MUST be recorded as a row in the **Conditional Acceptance Conditions** deliverable (`id` `CA-NNN`, `condition`, `evidenceRequired`, `blocksReleaseHandoff`). The validator enforces verdict↔deliverable consistency: `accepted` ⇒ zero acceptance blockers, `blocked` ⇒ at least one, `conditional-accept` ⇒ at least one condition, and a `release-handoff` routing recommendation is allowed only when the verdict is `accepted`. **Any Acceptance Blocker therefore forces the verdict off `accepted` (to `conditional-accept` or `blocked`); the gates below cite this rule instead of restating the arithmetic.**
80
- - **Acceptance Blockers block** (under section 4): one row per blocker with `id`, `severity` (`critical` / `major` / `minor`), evidence (file path, log excerpt, or test output), and the recommended follow-up phase: `error-analysis` for a cause problem, `implementation-option-selection` for a direction problem, or `implementation-planning` for a detailed-plan problem. Empty block is acceptable and preferred — render the single line `- No acceptance blockers found.`
80
+ - **Verdict vocabulary**: Section 7 (`Final Verdict`) MUST include a `Verdict Token` field whose value is exactly one of `accepted`, `conditional-accept`, or `blocked`. `conditional-accept` requires an explicit, exhaustive list of conditions; ambiguous verdicts ("looks good", "mostly ready") are not allowed. Each condition MUST be recorded as a row in the **Conditional Acceptance Conditions** deliverable (`id` `CA-NNN`, `condition`, `evidenceRequired`, `blocksReleaseHandoff`). `blocksReleaseHandoff` is a gate, not a note: `false` means this condition alone would not stop the release, and a `conditional-accept` whose conditions are all `false` may route to `release-handoff` (the conditions travel into the PR body as unresolved items). Declare `true` for anything that must be settled before release. The validator enforces verdict↔deliverable consistency: `accepted` ⇒ zero acceptance blockers, `blocked` ⇒ at least one, `conditional-accept` ⇒ at least one condition, and a `release-handoff` routing recommendation is allowed only when the verdict is release-ready by `okstra_ctl.release_gate.release_handoff_allowed`. **Any Acceptance Blocker therefore forces the verdict off `accepted` (to `conditional-accept` or `blocked`); the gates below cite this rule instead of restating the arithmetic.**
81
+ - **Added-surface audit** (`finalVerification.addedSurfaceAudit`): the reverse of requirement coverage — one row per surface the merged diff added (`id` `AS-NNN`, `surface` as `path:line` + the added name, `callers`, `requirement`, `disposition`, `note`). Requirement coverage proves every requirement reached the diff; this table proves every addition answers a requirement. An empty array is a claim that the diff added no identifier, module, or configuration entry, not permission to skip the enumeration. Single-stage scope audits its own stage's diff; whole-task audits the merged diff, which is the only place a surface added by one stage and orphaned by another is visible.
82
+ - **Acceptance Blockers block** (under section 4): one row per blocker with `id`, `severity` (`critical` / `major`), evidence (file path, log excerpt, or test output), and the recommended follow-up phase: `error-analysis` for a cause problem, `implementation-option-selection` for a direction problem, or `implementation-planning` for a detailed-plan problem. Empty block is acceptable and preferred — render the single line `- No acceptance blockers found.`
81
83
  - **Residual Risk block** (under section 4): risks that are not blockers but should be tracked, each with mitigation owner and a trigger that would escalate them to a blocker.
82
84
  - **Validation Evidence**: for every requirement in the originating plan or task brief, cite the artifact (commit SHA, test output, log line, MCP SELECT result) that demonstrates coverage. Paraphrased "verified" claims without an artifact are rejected.
83
85
  - **Read-only command log**: any pre-existing test/validation command touched during this run MUST be listed with its exact command line and one honest status — `executed` (ran; carries its exit code) / `advisory` (external Tier 3 did not PASS; carries observed/expected results and remains user-owned) / `env-unavailable` (should run but cannot in this environment — missing replica DB, container, or service; carries the reason, never a faked pass) / `not-configured` (no such qa-command tier) / `rejected` (a mutating/denied token — skipped, carries the denied token). A check that could not run locally is recorded as `env-unavailable` or `advisory` according to the external QA policy — never silently dropped and never reported as `executed` with an invented exit code. Mutating-command prohibition is the shared read-only boundary (see Non-goals); it is not restated per row.
84
86
  - **Could-not-verify roll-up (§5.8.9)**: the template mechanically aggregates every not-confirmed check into one scannable list — `gap` requirement-coverage rows, `advisory` / `not-configured` / `env-unavailable` / `rejected` command rows, and `blocked` manual tests. You do not hand-author it, but you MUST give those rows their honest status so nothing unverified hides across sections: a check silently recorded as `executed`/`covered` will not surface in the roll-up. This is okstra's answer to "say what could not be verified this run."
85
- - **Routing recommendation**: `finalVerification.routingRecommendation` is an **object** with exactly two fields — `target`, one value of the enum below, and `rationale`, the sentence tying that choice to the verdict and the blocker list. Free routing prose is not the field; a target named only in the prose does not route the task, because Phase 7 projects `workflow.nextRecommendedPhase` from `target` alone. The seven allowed targets are `release-handoff`, `release-handoff(stage-group)`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `implementation`, and `done`. Both `release-handoff` forms are allowed ONLY when the Verdict Token is `accepted`. Plain `release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a `single-stage` accepted run routes to `release-handoff(stage-group)` (or `implementation` / `done`) instead. `done` ends the lifecycle here. Enforcement: `schemas/final-report-v2.0.schema.json` rejects a `target` outside the enum, a missing `rationale`, and a string in place of the object; `validators/validate-run.py` rejects a missing `target`, a non-`accepted` verdict routed to either `release-handoff` form, and a `single-stage` report whose routing cites plain `release-handoff`.
86
- - **Verified-row recording** (single-stage scope only): when the Verdict Token is `accepted`, the lead MUST run `okstra handoff record-verified --plan-run-root <plan-run-root> --stage <N> --report-path <final-report.md path> --data-json <final-report data.json path>` and quote the command + exit code in the report. The helper re-validates taskType/scope/verdict from data.json, so a non-accepted or whole-task report is rejected at the tool layer. **Enforced:** `validators/validate-run.py` `_validate_verified_row_recorded` requires a `verified` row in `runs/implementation-planning/consumers.jsonl` for every accepted stage — the helper validated its own inputs but nothing checked it had ever run, leaving reports that said `accepted` while the registry said unverified, so the stage was never offered for a stage-group PR.
87
+ - **Routing recommendation**: `finalVerification.routingRecommendation` is an **object** with exactly two fields — `target`, one value of the enum below, and `rationale`, the sentence tying that choice to the verdict and the blocker list. Free routing prose is not the field; a target named only in the prose does not route the task, because Phase 7 projects `workflow.nextRecommendedPhase` from `target` alone. The seven allowed targets are `release-handoff`, `release-handoff(stage-group)`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `implementation`, and `done`. Both `release-handoff` forms are allowed ONLY when the verdict is release-ready — `accepted`, or `conditional-accept` with every condition declaring `blocksReleaseHandoff: false`. Plain `release-handoff` is additionally allowed ONLY when the verification scope (the `Verification scope:` line of the injected `VERIFICATION_TARGET` block, recorded as the report's `verificationScope` field) is `whole-task`; a release-ready `single-stage` run routes to `release-handoff(stage-group)` (or `implementation` / `done`) instead. `done` ends the lifecycle here. Enforcement: `schemas/final-report-v2.0.schema.json` rejects a `target` outside the enum, a missing `rationale`, and a string in place of the object; `validators/validate-run.py` rejects a missing `target`, a verdict that is not release-ready routed to either `release-handoff` form (naming the condition ids that block it), and a `single-stage` report whose routing cites plain `release-handoff`.
88
+ - **Verified-row recording** (single-stage scope only): when the verdict is release-ready, the lead MUST run `okstra handoff record-verified --plan-run-root <plan-run-root> --stage <N> --report-path <final-report.md path> --data-json <final-report data.json path>` and quote the command + exit code in the report. The helper re-validates taskType/scope/verdict from data.json against the same release-gate rule, so a blocked, condition-blocked, or whole-task report is rejected at the tool layer, and the row records the token the report actually carries. **Enforced:** `validators/validate-run.py` `_validate_verified_row_recorded` requires a `verified` row in `runs/implementation-planning/consumers.jsonl` for every release-ready stage — the helper validated its own inputs but nothing checked it had ever run, leaving reports that said `accepted` while the registry said unverified, so the stage was never offered for a stage-group PR.
87
89
  - Clarification request policy (phase-specific addendum — shared policy is in `_common-contract.md`):
88
90
  - populate `## 1. Clarification Items` only when a blocker hinges on information only the user can supply (deployment intent, intended target environment, business-rule interpretation); use `Blocks=next-phase` for items that gate continuing to release-handoff
89
91
  - Self-review pass before finalising the report (the Okstra lead runs this; do not delegate it):
@@ -74,7 +74,7 @@ roles:
74
74
  - v1 legacy branch: when validating or rerendering an existing schema-v1 report, preserve its `## 5.9 Improvement Candidates` table and legacy Markdown contract; do not rewrite that historical data into v2 implicitly.
75
75
  - the `## 5.9 Improvement Candidates` table populated with rows that obey the 11-column schema from `validators/validate_improvement_report.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, antigravity}, Recommended next-phase ∈ {requirements-discovery, implementation-option-selection, error-analysis}, Expected behavior after, Evidence as path:line list). `Expected behavior after` states, in one observable sentence, what becomes different once the candidate is applied — it is the seed of the downstream brief's `EB-NNN` / `EO-NNN`. A candidate you cannot write this cell for is a preference, not a finding: drop it rather than filling the cell with a restatement of the title.
76
76
  - `Consensus` cells in `## 5.9 Improvement Candidates` use the table enum exactly: `full`, `partial`, `contested`, `worker-unique`. Map convergence's `full-consensus` / `partial-consensus` labels to `full` / `partial` before writing the table.
77
- - Verdict Token — **branch-specific, and the two branches do not share a vocabulary.** On the current v2 branch use the shared analysis enum: `analysis-complete` when every resolved lens was examined, `analysis-partial` when one could not be, `blocked` when the scan itself could not run. `schemas/final-report-v2.0.schema.json` admits only those three for `verdictCard.verdictToken` / `finalVerdict.verdictToken`, so a v2 report carrying `candidates-ready` fails Phase 7. **Finding no candidates is not a verdict**: it is an empty `candidates[]` plus a `lensCoverage[]` row per lens with `status: no-candidate` and its evidence-backed rationale — the verdict stays `analysis-complete`. `candidates-ready` / `no-candidates` belong to the v1 legacy `## 7. Final Verdict` Markdown alone, where `validators/validate_improvement_report.py` enforces them. Both branches: Direction `routing`; Next Step "ask the user to select K candidates (see the ## 5.9 table)".
77
+ - Verdict Token — **branch-specific, and the two branches do not share a vocabulary.** On the current v2 branch use the shared analysis enum: `analysis-complete` when every resolved lens was examined, `analysis-partial` when one could not be, `blocked` when the scan itself could not run. `schemas/final-report-v2.0.schema.json` admits only those three for `finalVerdict.verdictToken` — the report's single verdict-token home — so a v2 report carrying `candidates-ready` fails Phase 7. **Finding no candidates is not a verdict**: it is an empty `candidates[]` plus a `lensCoverage[]` row per lens with `status: no-candidate` and its evidence-backed rationale — the verdict stays `analysis-complete`. `candidates-ready` / `no-candidates` belong to the v1 legacy `## 7. Final Verdict` Markdown alone, where `validators/validate_improvement_report.py` enforces them. Both branches: Direction `routing`; Next Step "ask the user to select K candidates (see the ## 5.9 table)".
78
78
  - `## 3. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
79
79
  - author the shared schema-v2 report fields plus `improvementDiscovery.candidates[]`, `improvementDiscovery.lensCoverage[]`, `improvementDiscovery.selectionLimit`, and `improvementDiscovery.userNarrative` in data.json. `candidates[]` carries the same 11 logical fields described above; `lensCoverage[]` records either candidate IDs or an evidence-backed no-candidate rationale for every resolved lens. `schemas/final-report-v2.0.schema.json` and `validators/validate_improvement_report.py` enforce this contract. The renderers independently derive AI handoff Markdown and human HTML; never author a free-form report.
80
80
  - Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
@@ -19,9 +19,10 @@ roles: []
19
19
  - The shared "MCP read-only" rule still applies if the brief lists MCP servers, though most release-handoff runs do not use MCP.
20
20
  - Pre-handoff entry gate (mandatory — refuse to start if any item fails):
21
21
  - the run's input document (`release-handoff-input.md`, generated by prepare in place of a task brief — briefs belong to entry phases only) carries a `## Source Verification Report` section with `Mode`, `Stages`, and one table row per cited `final-verification` final-report. The run context exposes the same selection as `HANDOFF_MODE` (`whole-task` | `stage-group`) and `HANDOFF_STAGES` (csv, empty for whole-task).
22
- - **whole-task mode** (`HANDOFF_MODE=whole-task`): the lead opens the cited report and confirms its `Verdict Token` is exactly `accepted` and its `verificationScope` is `whole-task`.
23
- - **stage-group mode** (`HANDOFF_MODE=stage-group`): the lead opens each cited single-stage report and confirms every `Verdict Token` is `accepted`. Eligibility was already enforced at prepare time (`okstra_ctl.handoff.compute_eligibility`) and is re-enforced by `okstra handoff assemble` — the lead never hand-computes it.
24
- - if the verdict is `conditional-accept`, `blocked`, or any other token (including ambiguous phrasing like "looks good"), the run MUST end immediately with status `blocked` and a routing recommendation back to `error-analysis` or `implementation-planning`. Do NOT prompt the user; Do NOT run any git command.
22
+ - **whole-task mode** (`HANDOFF_MODE=whole-task`): the lead opens the cited report and confirms its `verificationScope` is `whole-task` and its verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every **Conditional Acceptance Condition** row declaring `blocksReleaseHandoff: false`. The rule lives in `okstra_ctl.release_gate.release_handoff_allowed`; the lead reads the report against it and does not invent a third case.
23
+ - **stage-group mode** (`HANDOFF_MODE=stage-group`): the lead opens each cited single-stage report and confirms every verdict is release-ready by the same rule as whole-task mode. Eligibility was already enforced at prepare time (`okstra_ctl.handoff.compute_eligibility`) and is re-enforced by `okstra handoff assemble` — the lead never hand-computes it.
24
+ - if the verdict is `blocked`, a `conditional-accept` carrying any condition that blocks release, or any other token (including ambiguous phrasing like "looks good"), the run MUST end immediately with status `blocked` and a routing recommendation back to `error-analysis` or `implementation-planning`. Do NOT prompt the user; Do NOT run any git command.
25
+ - when the cited verdict is `conditional-accept`, the lead MUST carry every **Conditional Acceptance Condition** row into the PR body under a heading naming them as unresolved — id, condition, and the evidence the reviewer would need. These conditions are the whole reason a non-`accepted` verdict was allowed through; a PR body that drops them turns a recorded condition into a silent one.
25
26
  - the lead MUST capture `git status --short` and confirm the working tree is clean. Dirty state aborts the run; release-handoff packages the commits produced by `implementation`, it does not stage or commit changes.
26
27
  - the lead MUST capture `git rev-parse --abbrev-ref HEAD` and record it as the **feature branch**. If the current branch is itself `main`, `master`, `prod`, `preprod`, `staging`, or `dev`, the run MUST end immediately — release-handoff never operates on a base branch.
27
28
  - the lead MUST confirm `git log --oneline <base>..HEAD` contains at least one implementation commit. If it is empty, the run MUST end with status `blocked` and route back to `implementation`.
@@ -73,7 +74,7 @@ roles: []
73
74
  - Forbidden actions (any occurrence → terminal status `contract-violated`):
74
75
  {{PHASE_FORBIDDEN_ACTIONS}}
75
76
  - Required deliverable shape (final report, in addition to the standard sections):
76
- - **Source Verification Report**: relative path of the originating `final-verification` final-report file plus the literal quoted `Verdict Token` row whose value is `accepted`.
77
+ - **Source Verification Report**: relative path of the originating `final-verification` final-report file plus the literal quoted `Verdict Token` row, and — when that token is `conditional-accept` — every Conditional Acceptance Condition row quoted with its `blocksReleaseHandoff` value.
77
78
  - **Feature Branch & Working-Tree State**: branch name from `git rev-parse --abbrev-ref HEAD`, output of `git status --short` at run start.
78
79
  - **User Selections**: a block recording each prompt and the user's verbatim answer.
79
80
  - Q1 action: `local checkout` | `push + PR` | `skip`.
@@ -100,14 +101,14 @@ roles: []
100
101
  - **Stage Group** (stage-group mode only): selected stages, each stage's single-stage verification report path + quoted `Verdict Token` row, collector branch name, merge commit SHAs from assemble, and the dependency-closure verdict (from the assemble output / error).
101
102
  - **Routing recommendation**: explicit `done` token, since release-handoff is the terminal lifecycle phase. If the run ended in `skip` or `cancel`, the recommendation MUST also state whether re-entry into release-handoff is appropriate.
102
103
  - Self-review pass before finalising the report (the Okstra lead runs this):
103
- 1. **Entry-gate audit** — section 2 cites the originating final-verification report path and the literal `Verdict Token` row with value `accepted`. If either is missing, the run is invalid and MUST be re-routed to `final-verification`.
104
+ 1. **Entry-gate audit** — section 2 cites the originating final-verification report path and the literal `Verdict Token` row with a release-ready value. If either is missing, or a `conditional-accept` run's PR body omits the conditions, the run is invalid and MUST be re-routed to `final-verification`.
104
105
  2. **User-selection traceability** — every executed mutating command maps to a user selection captured in the report. Any mutating command without a corresponding user answer is a contract violation.
105
106
  3. **Forbidden-action audit** — scan the run's session transcripts (`git`, `gh` invocations) for every entry in the Forbidden actions list above. Any occurrence means the run has crossed into unsafe territory and MUST be flagged as `contract-violated`.
106
107
  4. **Push-target audit** — for every `git push` recorded, confirm the refspec resolves to the feature branch, not the base branch.
107
108
  5. **Idempotency check** — if a PR with the same head already existed at run start, confirm the report records `PR reused` rather than a fresh `gh pr create` invocation.
108
109
  6. **Merge-conflict probe audit** — for any `push + PR` run, confirm the report's `Merge Conflict Probe` section is present and either records `Clean` or records `Conflicts detected` with the user's verbatim choice. A missing or unparseable probe entry on a `push + PR` run is a contract violation.
109
110
  - Non-goals:
110
- - re-litigating the final-verification verdict — release-handoff trusts the cited `accepted` verdict and does not reopen acceptance checks.
111
+ - re-litigating the final-verification verdict — release-handoff trusts the cited release-ready verdict and does not reopen acceptance checks. Carrying a conditional-accept's conditions into the PR body is transcription, not verification: the lead does not check whether a condition has been satisfied.
111
112
  - creating, amending, squashing, or rewriting commits. Commit production belongs to `implementation`.
112
113
  - opening additional PRs, releases, or deployments beyond the single PR the user chose to create.
113
114
  - merging the PR. Merging is a separate, manual step performed by the user (or by repo automation) after release-handoff ends; the lead MUST NOT call `gh pr merge`.
@@ -497,11 +497,11 @@
497
497
  }
498
498
  },
499
499
  "role_add": {
500
- "label": "선택 역할 {role} 을(를) 이번 run 에 추가할까요? (최대 {maximum}개, 기본: 추가 안 함)",
500
+ "label": "선택 역할 {role} 을(를) 이번 run 에 추가할까요? (최대 {maximum}개)",
501
501
  "echo_template": "role-add: {value}",
502
502
  "options": {
503
503
  "skip": "추가 안 함{default_suffix}",
504
- "add": "{count}개 추가",
504
+ "add": "{count}개 추가{default_suffix}",
505
505
  "default_suffix": " (기본)"
506
506
  }
507
507
  },
@@ -198,11 +198,10 @@ For a `host-text` mapping, render each numbered item as its option label followe
198
198
  ## Run-scoped resource lifecycle
199
199
 
200
200
  - At run start, record `teamName` as the audit label in team-state and populate `lead.sessionId`; the session transcript lives under `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`. You do NOT write `teamCreate`: `okstra team dispatch` records the implicit-team marker (`{ attempted: false, status: "implicit" }`) itself, on every dispatch path, because v2.1.178 made that value a constant rather than a judgment. The one marker that IS yours is the concurrent-run decision — a concurrent run records `teamCreate: { attempted: false, status: "skipped", reason: "concurrent-run" }` **before** the first dispatch, and dispatch then leaves it alone.
201
- - Record the lead pane once with `mkdir -p "<RUN_DIR>/state" && { . "$HOME/.okstra/bin/lib/okstra/tmux-pane.sh" 2>/dev/null && okstra_resolve_caller_pane; } > "<RUN_DIR>/state/lead-pane.id" 2>/dev/null || true`. This is silent setup and must not gate cleanup; the cleanup script protects the lead pane itself.
202
201
  - Collect and persist token usage before any live-roster cleanup, including cleanup between batches and the run-end shutdown sequence.
203
- - Before each new worker batch (and before the next phase's render-bundle), reclaim the prior round's completed teammate panes in two passes, adding `--keep report-writer-worker` to **both** passes while the report writer is in flight. First source the count: `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>" [--keep report-writer-worker]` never kills and prints one `<pane_id>\t<pane_title>` line per pane it would reclaim — count those lines as `<n>`. Then perform the reclaim by running the same command **without** `--list`, and emit the neutral contract's `PROGRESS: phase-batch-cleanup panes=<n>` checkpoint with that count. Call both passes after collecting that round's results and token usage and before the next dispatch, so no in-flight worker pane is caught. This `tmux kill-pane`s the harness teammate panes; `shutdown_request` only idles the agent and never frees the pane, so it stays part of the run-end sequence for roster/token hygiene. In a non-tmux session there are no panes, both passes no-op, and `<n>` is `0` — still emit the checkpoint. The lead pane (read from `<RUN_DIR>/state/lead-pane.id`) is always preserved.
202
+ - Before each new worker batch (and before the next phase's render-bundle), close the panes of the dispatches that finished in the prior round, in two passes. First count: `okstra team reclaim --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>" --dry-run` closes nothing and prints one `<paneId>\t<kind>` line per pane it would close — count those lines as `<n>`. Then run the same command **without** `--dry-run` to close them, and emit the neutral contract's `PROGRESS: phase-batch-cleanup panes=<n>` checkpoint with that count. Call both passes after collecting that round's results and token usage and before the next dispatch. The command reads each dispatch's recorded status, so an in-progress worker keeps its pane whichever moment you call it — you do not scope the pass by hand. It closes only the panes okstra opened and recorded; a pane the harness opened for itself carries no recorded id and is not okstra's to close. A `cli-wrapper` run holds no pane at all, so `<n>` is `0` — still emit the checkpoint.
204
203
  - Reclaiming a pane does not stop the worker's background task. Every `dispatch_worker` Agent runs with `run_in_background: true`, so a worker whose result is already collected stays a live background task for the rest of the session — that residue is what fills the harness's exit-time `Background work is running` list. At the same batch boundary, right after the pane reclaim, call `TaskStop(task_id: "<name>")` once per worker of the completed batch, passing the exact `name` used at dispatch (`<workerId>-worker`, `<workerId>-worker-reverify-r<N>`, `<provider>-worker-critic`, `report-writer`). Stop only workers whose results were already collected — never an in-flight worker, never the lead, and keep `report-writer` while it is in flight, matching the pane pass's `--keep report-writer-worker`. `TaskStop` on an already-finished task is a no-op; treat a failure as benign, record nothing, and continue the boundary. This runs in a non-tmux session too, where the pane passes no-op but the background tasks still exist.
205
- - Before any `prompt_user`/`AskUserQuestion` that follows worker dispatch — an approval, clarification, or decision gate — run the same two-pass reclaim used at a round boundary: `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>" [--keep report-writer-worker]` to count `<n>`, then the same command **without** `--list` to reclaim, and emit `PROGRESS: phase-gate-cleanup panes=<n>`. Then `TaskStop(task_id: "<name>")` each completed worker, exactly as at a batch boundary. A bare `TaskStop` idles the roster task but **does not close** the tmux pane, so it is never cleanup on its own — the `trace-cleanup` call is what frees the pane. In a non-tmux session both passes no-op and `<n>` is `0`; still emit the checkpoint. `--keep report-writer-worker` preserves an in-flight report writer, and the lead pane is always preserved. This keeps the user from being shown a gate while finished worker panes are still open.
204
+ - Before any `prompt_user`/`AskUserQuestion` that follows worker dispatch — an approval, clarification, or decision gate — run the same two passes used at a round boundary: `okstra team reclaim … --dry-run` to count `<n>`, then the same command without `--dry-run` to close, and emit `PROGRESS: phase-gate-cleanup panes=<n>`. Then `TaskStop(task_id: "<name>")` each completed worker, exactly as at a batch boundary. A bare `TaskStop` idles the roster task and closes no pane, so it is never cleanup on its own. This keeps the user from being shown a gate while finished worker panes are still open.
206
205
  - After batch cleanup, record the current live session generation with `okstra token-usage "<TEAM_STATE_PATH>" --record-observed-session --project-root "<PROJECT_ROOT>"`. This protects usage accounting when Claude Code re-issues the session id after resume or compaction.
207
206
  - Claude Code cannot delete the implicit team or surgically remove an idle roster entry. Explain that teammates may remain visible until session end and, when needed, give the manual action `Delete team <teamName> in Teams/FleetView`.
208
207
  - The `SessionEnd` hook runs `$HOME/.okstra/bin/okstra-team-reconcile.sh --session-end` as the safety net for the current live session.
@@ -210,13 +209,13 @@ For a `host-text` mapping, render each numbered item as its option label followe
210
209
  ### Run-end cleanup sequence (BLOCKING)
211
210
 
212
211
  1. Call `collect_usage` through this adapter and persist usage before inspecting or changing any residual resource.
213
- 2. Run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --list --run-dir "<RUN_DIR>"` exactly once and inspect the current live roster without mutating it.
212
+ 2. Run `okstra team teardown --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>" --dry-run` exactly once and inspect the current live roster without mutating it. Unlike the round boundary, this lists every pane the run recorded, finished or not — the run is ending, so none of them is expected to continue.
214
213
  3. If no residual pane and no current live roster exists, skip the question and perform no cleanup operation.
215
214
  4. Otherwise call `prompt_user` exactly once with this binary prompt. This one answer controls the complete residual pane and teammate set; never ask a second teammate-only question.
216
215
  > This phase is ending. The following Okstra panes and worker teammates remain — close and clean them up?
217
216
  > <quoted `--list` output>
218
217
  > (Yes) Close everything and clean up teammates / (No) Keep everything
219
- 5. On `keep`, preserve every residual resource and show `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"` plus the manual Teams/FleetView action. Tell the user that `keep` holds only until the next boundary: if this session goes on to another phase/batch, that transition's round-boundary cleanup reclaims the kept **completed** panes unattended (in-flight resources and the lead pane are never touched).
220
- 6. On approved `clean`, emit the teardown checkpoint, run `$HOME/.okstra/bin/okstra-trace-cleanup.sh --run-dir "<RUN_DIR>"`, then run `$HOME/.okstra/bin/okstra-team-reconcile.sh --project-root "<PROJECT_ROOT>" --fallback-team "session-<lead.sessionId-prefix>"` exactly once. The resolver reads the current live session's `~/.claude/teams/session-<live>/config.json`, falling back to the snapshot directory only when the live directory is absent, and prints `dismissible-member: <name>` records.
218
+ 5. On `keep`, preserve every residual resource and show `okstra team teardown --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>"` plus the manual Teams/FleetView action. Tell the user that `keep` holds only until the next boundary: if this session goes on to another phase/batch, that transition's round-boundary cleanup reclaims the kept **completed** panes unattended (in-flight resources and the lead pane are never touched).
219
+ 6. On approved `clean`, emit the teardown checkpoint, run `okstra team teardown --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>"`, then run `$HOME/.okstra/bin/okstra-team-reconcile.sh --project-root "<PROJECT_ROOT>" --fallback-team "session-<lead.sessionId-prefix>"` exactly once. The resolver reads the current live session's `~/.claude/teams/session-<live>/config.json`, falling back to the snapshot directory only when the live directory is absent, and prints `dismissible-member: <name>` records.
221
220
  7. Send `SendMessage(to: <name>, message: { type: "shutdown_request" })` to each printed, confirmed-complete non-lead member. The `message` MUST be the object literal shown, NEVER a JSON string in a text field. Never target the lead.
222
221
  8. Call `TaskStop(task_id: "<name>")` for every worker this run dispatched, reusing the step-7 names plus any batch worker already reclaimed earlier. `shutdown_request` only idles the roster member and the step-6 pane reclaim only closes the pane — neither ends the background task, so this step is the one that empties the harness's exit-time `Background work is running` list. Never target the lead; a `TaskStop` on an already-finished task is a benign no-op.
@@ -44,12 +44,34 @@ ANTIGRAVITY = {
44
44
  }
45
45
 
46
46
 
47
+ def _catalog_model_id(observed: str) -> str:
48
+ """Undo the dispatch-time tier suffix so the id is catalog-comparable.
49
+
50
+ Dispatch sends `gemini-3.1-pro-low`; agy serves that identity back
51
+ verbatim. The catalog holds the bare `gemini-3.1-pro`, so reading the
52
+ observation as-is made the served-model gate reject a model okstra itself
53
+ had asked for. Only suffixes dispatch can append are stripped, and only
54
+ when what remains is a catalog entry — anything else passes through for
55
+ the gate to judge.
56
+ """
57
+ if observed in ANTIGRAVITY:
58
+ return observed # identity already carries its tier (agy's Claude models)
59
+ for suffix in model_discovery.dispatch_tier_suffixes():
60
+ bare = observed.removesuffix(f"-{suffix}")
61
+ if bare != observed and bare in ANTIGRAVITY:
62
+ return bare
63
+ return observed
64
+
65
+
47
66
  def normalise_served_model(raw_model: str | None) -> ServedModelAttestation:
48
67
  if not raw_model or not raw_model.strip():
49
68
  return ServedModelAttestation.unknown()
50
69
  model_id = raw_model.strip().lower().removeprefix("antigravity/")
51
70
  return ServedModelAttestation(
52
- raw_model, f"antigravity/{model_id}", "exact", "provider-output"
71
+ raw_model,
72
+ f"antigravity/{_catalog_model_id(model_id)}",
73
+ "exact",
74
+ "provider-output",
53
75
  )
54
76
 
55
77
 
@@ -1209,6 +1209,23 @@ def _render_prompt(
1209
1209
  f"**Runner:** {assignment.runner}",
1210
1210
  f"**Host runtime:** {assignment.host_runtime}",
1211
1211
  ]
1212
+ # The dispatch prompt contract requires exactly one of these on every prompt
1213
+ # it checks, and a critic pass is checked under the same contract as the
1214
+ # initial analysis it audits — but only the initial-prompt path emitted one,
1215
+ # so a dynamically materialized critic was refused. A dynamic prompt carries
1216
+ # its instruction inline, which is what `eager-include` states; the initial
1217
+ # path states the host's `initialPromptDeliveryMode` instead and puts it in
1218
+ # `anchor_lines`, so defer to it when it is already there.
1219
+ delivery_mode = "**Prompt Delivery Mode:**"
1220
+ already_declared = any(
1221
+ line.strip().startswith(delivery_mode)
1222
+ for line in (
1223
+ *request.instruction.anchor_lines,
1224
+ *request.instruction.body.splitlines(),
1225
+ )
1226
+ )
1227
+ if not already_declared:
1228
+ header.append(f"{delivery_mode} eager-include")
1212
1229
  if assignment.host_model_value is not None:
1213
1230
  header.append(f"**Host model value:** {assignment.host_model_value}")
1214
1231
  duty_body = f"{common.body.rstrip()}\n\n{duty.body.rstrip()}"
@@ -43,7 +43,7 @@ from .assignment_resolver import AssignmentContext, resolve_dispatch_assignment
43
43
  from .path_hints import hydrate_active_run_context
44
44
  from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
45
45
  from .worker_artifact_paths import audit_sidecar_rel
46
- from .wrapper_status import status_path_for_prompt
46
+ from .wrapper_status import log_path_for_prompt, status_path_for_prompt
47
47
  from .worker_prompt_policy import resolve_prompt_plan_for_manifest
48
48
  from .dispatch_state import (
49
49
  BACKEND_CLI_WRAPPER,
@@ -304,6 +304,19 @@ def _materialize_run(
304
304
  "result",
305
305
  must_exist=False,
306
306
  )
307
+ if args.audience == "report-writer" and not args.audit_source:
308
+ # The report writer is the one audience whose result path is not its own
309
+ # worker result: it writes the report `data.json`, while the audit
310
+ # sidecar is derived from its `.md`. With both collapsed into one value
311
+ # the prompt loses its `**Worker Result Path:**` anchor and the writer
312
+ # puts the report where the audit file belongs — silently, because every
313
+ # header is still present and well-formed. The roster path derives both
314
+ # from the manifest; a dynamic call has to name them.
315
+ raise AgentPromptCliError(
316
+ "report-writer materialization requires --audit-source: --result is "
317
+ "the report data.json, --audit-source the worker-result .md the "
318
+ "audit sidecar is derived from"
319
+ )
307
320
  audit_source_path = (
308
321
  _authorized_path(
309
322
  project_root,
@@ -477,7 +490,7 @@ def _dynamic_verifier_artifact_paths(
477
490
  audit_source_path,
478
491
  project_root / audit_sidecar_rel(audit_rel),
479
492
  status_path_for_prompt(prompt_path),
480
- prompt_path.with_suffix(prompt_path.suffix + ".log"),
493
+ log_path_for_prompt(prompt_path),
481
494
  }
482
495
  error_logs = active_context.get("errorLogs")
483
496
  sidecars = error_logs.get("sidecarsByWorkerId") if isinstance(error_logs, Mapping) else None