okstra 0.125.4 → 0.125.6

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 (34) hide show
  1. package/README.md +1 -1
  2. package/docs/architecture/storage-model.md +1 -1
  3. package/docs/architecture.md +2 -2
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/agents/workers/antigravity-worker.md +2 -1
  7. package/runtime/agents/workers/claude-worker.md +1 -0
  8. package/runtime/agents/workers/codex-worker.md +2 -1
  9. package/runtime/agents/workers/report-writer-worker.md +1 -1
  10. package/runtime/prompts/lead/context-loader.md +1 -1
  11. package/runtime/prompts/lead/convergence.md +12 -6
  12. package/runtime/prompts/lead/okstra-lead-contract.md +5 -3
  13. package/runtime/prompts/lead/plan-body-verification.md +9 -9
  14. package/runtime/prompts/lead/report-writer.md +33 -23
  15. package/runtime/prompts/lead/team-contract.md +2 -2
  16. package/runtime/prompts/profiles/_common-contract.md +1 -1
  17. package/runtime/prompts/profiles/forbidden-actions.json +3 -3
  18. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  19. package/runtime/prompts/profiles/implementation.md +2 -2
  20. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  21. package/runtime/python/okstra_ctl/path_hints.py +10 -1
  22. package/runtime/python/okstra_ctl/paths.py +12 -1
  23. package/runtime/python/okstra_ctl/report_views.py +2 -0
  24. package/runtime/python/okstra_ctl/workers.py +5 -0
  25. package/runtime/python/okstra_ctl/workflow.py +1 -1
  26. package/runtime/schemas/final-report-v1.0.schema.json +12 -1
  27. package/runtime/skills/okstra-brief-gen/SKILL.md +15 -7
  28. package/runtime/templates/reports/final-report.template.md +3 -9
  29. package/runtime/templates/reports/i18n/en.json +1 -1
  30. package/runtime/templates/reports/i18n/ko.json +1 -1
  31. package/runtime/templates/worker-prompt-preamble.md +2 -0
  32. package/runtime/validators/validate-brief.py +43 -0
  33. package/runtime/validators/validate-run.py +161 -1
  34. package/runtime/validators/validate-schedule.py +6 -6
package/README.md CHANGED
@@ -216,7 +216,7 @@ Major workflow changes added to `main` after 0.8.0:
216
216
  - **Brief as translation layer + Step 6.5 reporter batch confirmation** — `okstra-brief-gen` converts external input—an issue ticket, requirements document, or user message—verbatim and marks okstra-added content as labeled augmentation. Step 6.5 asks the user to confirm in one batch whether that conversion changed meaning and records the result in `Reporter Confirmations`. Every analysis profile requires this section before phase analysis begins; `validators/validate-brief.py` enforces the requirement.
217
217
  - **Artifact-home rule (`.okstra/`)** — `<project>/.okstra/` is the only project artifact root owned by okstra. Anything outside this root is not okstra memory and may be read only when explicitly cited in Source Material or Reporter Confirmations. Writing outside the root requires the same explicit requested path. Internal equivalents are `glossary.md` for terminology and `decisions/<NNNN>-<slug>.md` for decision records, evaluated during `implementation-planning`.
218
218
  - **Self-contained HTML final-report view** — After Phase 7 writes `final-report-<task-type>-<seq>.md`, `okstra render-views` automatically creates a sibling self-contained HTML view in the same `reports/` directory, with inline CSS/JavaScript and no external URLs. Its `Export user response` button serializes `## 1. Clarification Items` responses to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md` for the next phase. View generation never changes the source Markdown.
219
- - **`improvement-discovery` task type (sidetrack entry point)** — Within a codebase scope and priority-lens allowlist, multi-worker consensus produces N improvement candidates, with a default of eight and a hard cap of 12. This is a sidetrack entry point outside `PHASE_SEQUENCE`; the user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`. Lens enum SSOT: [`scripts/okstra_ctl/improvement_lenses.py`](scripts/okstra_ctl/improvement_lenses.py). Output section: `## 4.9 Improvement Candidates` (10-column table). Validator: [`validators/validate_improvement_report.py`](validators/validate_improvement_report.py).
219
+ - **`improvement-discovery` task type (sidetrack entry point)** — Within a codebase scope and priority-lens allowlist, multi-worker consensus produces N improvement candidates, with a default of eight and a hard cap of 12. This is a sidetrack entry point outside `PHASE_SEQUENCE`; the user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`. Lens enum SSOT: [`scripts/okstra_ctl/improvement_lenses.py`](scripts/okstra_ctl/improvement_lenses.py). Output section: `## 5.9 Improvement Candidates` (10-column table). Validator: [`validators/validate_improvement_report.py`](validators/validate_improvement_report.py).
220
220
 
221
221
  <a id="ops-commands"></a>
222
222
  ### 3.5 Operations commands
@@ -85,7 +85,7 @@ Unless `--render-only` is used, the handed-off Claude session typically adds the
85
85
  - `user-responses/user-response-<task-type>-<seq>.md` *(A sidecar downloaded under the same name by the HTML `Export user response` button; storing it here lets `--resume-clarification` automatically attach it to `clarification-response.md` in the instruction set — `clarification_items.clarification_response_with_sidecars`)*
86
86
  - `worker-results/<worker>-audit-<task-type>-<seq>.md` *(Per-worker Reading Confirmation sidecar; for auditing rather than report content)*
87
87
  - `status/final-<task-type>-<seq>.status`
88
- - `runs/implementation/stage-<N>/carry/stage-<N>.json` *(implementation only: execution evidence sidecar beneath stage N's resolved run directory; automatically carried into the next stage)*
88
+ - `runs/implementation/carry/stage-<N>.json` *(implementation only: execution evidence sidecar. Stage-SHARED like `consumers.jsonl` — it lives flat under the task-type run dir, NOT under `stage-<N>/`, because the next stage's carry-in and `backfill_done_from_carry` glob it without knowing the producing run's layout)*
89
89
  - `consumers.jsonl` *(implementation-planning only: backlinks to the impl-run that consumed each stage in this plan; append-only)*
90
90
 
91
91
  Design-preparation storage has three separate authorities:
@@ -404,7 +404,7 @@ Common constraints:
404
404
  - After the user fills answers into the `## 1. Clarification Items` section of a final report produced by `requirements-discovery`, `error-analysis`, or `implementation-planning`, carry that file into the next run with `--clarification-response <previous-final-report.md>`.
405
405
  - The carried-in file is copied to the current run's `instruction-set/clarification-response.md`; the lead updates each prior `Q*` row's `Status` (`resolved` / `obsolete`) in Section 0 before proceeding.
406
406
  - To edit answers and rerun in one operation, use `--resume-clarification`. See the `### --resume-clarification` section for details.
407
- - **Stage carry-in (`implementation` → next stage)**: Every `implementation` run writes a `runs/implementation/stage-<N>/carry/stage-<N>.json` evidence sidecar beneath its resolved stage run directory. The next stage automatically carries in this file. Reverse links identifying which `implementation` run consumed each stage accumulate in the shared coordination file `runs/implementation-planning/consumers.jsonl`.
407
+ - **Stage carry-in (`implementation` → next stage)**: Every `implementation` run writes a `runs/implementation/carry/stage-<N>.json` evidence sidecar (flat under the task-type run dir — stage-shared, like `consumers.jsonl`). The next stage automatically carries in this file. Reverse links identifying which `implementation` run consumed each stage accumulate in the shared coordination file `runs/implementation-planning/consumers.jsonl`.
408
408
 
409
409
  ### Fix cycle (post-release bug hotfix history)
410
410
 
@@ -770,7 +770,7 @@ Entry points that enforce whether phase artifacts may be published:
770
770
 
771
771
  Each validator blocks the phase with a `contract-violated` exit code when a contract is breached. Violations are applied when the next phase executes, leaving prior artifacts unchanged.
772
772
 
773
- `contractValidation.status=failed` means the run artifact contract failed; it does not necessarily mean the implementation itself is incomplete. An implementation run derives `phaseOutcome.implementation` from `runs/implementation/stage-<N>/carry/stage-<N>.json`, `runs/implementation-planning/consumers.jsonl`, and the approved Stage Map together. If every Stage Map stage has pass-grade carry evidence, it can retain the contract failure as audit information while correcting `workflow.nextRecommendedPhase` to `final-verification`.
773
+ `contractValidation.status=failed` means the run artifact contract failed; it does not necessarily mean the implementation itself is incomplete. An implementation run derives `phaseOutcome.implementation` from `runs/implementation/carry/stage-<N>.json`, `runs/implementation-planning/consumers.jsonl`, and the approved Stage Map together. If every Stage Map stage has pass-grade carry evidence, it can retain the contract failure as audit information while correcting `workflow.nextRecommendedPhase` to `final-verification`.
774
774
 
775
775
  ## Practical notes
776
776
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.125.4",
3
+ "version": "0.125.6",
4
4
  "description": "Multi-agent cross-verification orchestrator runtime + Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.125.4",
3
- "builtAt": "2026-07-20T11:22:52.583Z",
2
+ "package": "0.125.6",
3
+ "builtAt": "2026-07-20T13:12:23.932Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -91,7 +91,8 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
91
91
  - Otherwise (mtime stale `> 90s`, OR grace already applied): call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms <observed_ms>`, `--message "okstra-antigravity-exec.sh exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, and return `ANTIGRAVITY_CLI_TIMEOUT: agy exec exceeded polling cap`.
92
92
  4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
93
93
  - Do NOT abort the loop on transient `running` status. Only `completed` or the polling cap (initially 30min, optionally extended once to 35min by mtime grace) end it.
94
- - **No external timeout from Lead.** This polling loop is the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before the polling cap is reached (see team-contract "No external timeout on wrapper subagents").
94
+ - **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
95
+ - **No external timeout from Lead.** This polling loop and the script's idle watchdog are together the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before those caps fire (see team-contract "No external timeout on wrapper subagents").
95
96
  - Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
96
97
  - **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct.
97
98
 
@@ -46,6 +46,7 @@ Unlike the Codex / Antigravity workers, you are an in-process Claude subagent
46
46
  4. Anchor all file operations to the absolute `Project Root` from the lead prompt. Use absolute paths — do NOT rely on inherited cwd. Never use `cd` to change directory.
47
47
  - **Executor exception (implementation phase only):** when this worker is dispatched as the `Executor` and the lead prompt provides an `EXECUTOR_WORKTREE_PATH` that differs from the session's inherited cwd, cwd-sensitive Bash commands (`cargo *`, `npm *`, `pnpm *`, `bun *`, `pytest`, `make *`, `go *`, language-toolchain test/build commands) MUST be prefixed with `cd <EXECUTOR_WORKTREE_PATH> && ` in the same Bash invocation — e.g. `cd /Users/.../worktrees/foo && cargo test -p bar`. Do NOT wrap the whole thing in `bash -lc "..."` or `bash -c "..."`; pass the chained command directly to the Bash tool so the leading `cd` token remains visible to the permission layer. The `cd` is scoped to the single Bash subshell and does not mutate the session's shell state, so this does not conflict with the "never use cd" rule above (which prevents the worker from drifting the session cwd across calls).
48
48
  - **Executor coding-conventions preflight (BLOCKING, before your first `Edit` / `Write`):** when dispatched as the `Executor`, you MUST run the coding-conventions preflight defined in the executor sidecar (`prompts/profiles/_implementation-executor.md` → "Pre-implementation context exploration") before writing any code. Use this worker prompt's `**Coding preflight pack:**` anchor header; read that pack's `overview.md` and `clean-code.md`, then follow the routed pack's language → framework → architecture stages, iterating every rule and loading every matching resource (for example `frameworks/node-server.md` and `architectures/hexagonal.md` when their conditions match). The preflight pack is a runtime resource, not an auto-invoked skill; read the files via the Read tool by absolute path.
49
+ - **Executor post-write gates (BLOCKING, before your final commit / before claiming done):** the same dispatch prompt carries two gate blocks the lead appends after the preflight — `Pre-commit diff review sweep` (`prompts/profiles/_implementation-diff-review.md`) and `Implementation self-check` (`prompts/profiles/_implementation-self-check.md`). Execute both and record their coverage lines in your worker result exactly as the blocks specify. The codex/antigravity wrappers refuse to launch when an executor prompt lacks these blocks (`*_POSTWRITE_GATE_MISSING`); this worker runs in-process with no wrapper gate, so the contract lands on you directly — if either block is missing from your dispatch prompt, record a `tool-failure` in the errors sidecar and tell the lead to re-dispatch with the blocks included instead of skipping the gates.
49
50
  - **Verifier QA-gate exception:** verifier roles MAY use the same `cd <WORKTREE> && <cmd>` shape when executing project-declared `qaCommands` (lint / format / typecheck / test) from `project.json`, since those commands are cwd-sensitive by nature. Outside the QA gate, verifiers still read with absolute paths only — do NOT use `cd` for file inspection.
50
51
  - **No extra chaining beyond `cd && cmd`:** the permission matcher only allows the exact two-segment shape `cd <PATH> && <single-command>`. Do NOT append additional pipes, semicolons, redirects, or `&&` chains — e.g. `cd ... && cargo test ... 2>&1 | tail -20; echo "exit:$?"` will trigger a permission prompt every dispatch because the trailing `| tail`, `; echo`, and `2>&1` tokens disqualify the prefix match against `Bash(cargo:*)`. Let Claude Code capture the full stdout/stderr and exit code natively — do not post-process with `tail`, `head`, or `echo "exit:$?"`. If output truncation is genuinely needed, run the command first and read the result in a separate tool call.
51
52
 
@@ -91,7 +91,8 @@ The wrapper exists because Claude Code's Bash permission matcher rejects simple-
91
91
  - Otherwise (mtime stale `> 90s`, OR grace already applied): call `KillShell(shell_id: <shell_id>)`, then record a `cli-failure` event with `--error-type cli-failure`, `--exit-code 124`, `--duration-ms <observed_ms>`, `--message "okstra-codex-exec.sh exceeded polling cap (grace=<applied|not-applied>, last_mtime_age=<n>s)"`, and return `CODEX_CLI_TIMEOUT: codex exec exceeded polling cap`.
92
92
  4. Otherwise continue polling. Read `current_ts` cheaply via another `Bash` call (`date +%s`) at most once per poll iteration.
93
93
  - Do NOT abort the loop on transient `running` status. Only `completed` or the polling cap (initially 30min, optionally extended once to 35min by mtime grace) end it.
94
- - **No external timeout from Lead.** This polling loop is the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before the polling cap is reached (see team-contract "No external timeout on wrapper subagents").
94
+ - **Wrapper-internal idle watchdog.** Independently of this polling loop, the wrapper *script* reaps a silent CLI: if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI, records a `timeout` stage in the `<prompt-path>.status.json` sidecar, and the dispatched shell exits. Your `BashOutput` then reports the exit handle it via step 8b (`cli-failure`), citing the sidecar's `timeout` stage in the message. This is a wrapper-owned mechanism, not an external timeout.
95
+ - **No external timeout from Lead.** This polling loop and the script's idle watchdog are together the SINGLE timeout authority for this dispatch. Lead MUST NOT impose a separate Agent-call timeout that would terminate this subagent before those caps fire (see team-contract "No external timeout on wrapper subagents").
95
96
  - Do NOT issue parallel `BashOutput` calls or speculate about progress between polls.
96
97
  - **No standalone `sleep` between polls.** The harness blocks `sleep` calls of 5 seconds or longer as a circumvention vector and explicitly forbids chaining shorter sleeps to work around it. `BashOutput` itself is the wait primitive — calling it again immediately after a `running` status is correct.
97
98
 
@@ -87,7 +87,7 @@ Rules (the schema enforces most of these — they are listed here so you know *w
87
87
 
88
88
  - `header.reportAuthor` is `"Report writer worker"`; `header.reportOwner` is `"Claude lead"`. Set author to `"Claude lead"` only for `release-handoff` runs (single-lead by design) or a recorded report-writer dispatch failure fallback.
89
89
  - **Source items (worker:item) preservation.** Every `consensus[].sourceItems`, `differences[].workersPosition[].itemId`, and `evidence.primary[].sourceItems` entry MUST carry the worker:item-id pair (e.g. `claude:F-001`, `codex:1.1`, `antigravity:F-3`, or `lead:mcp-1` for lead-only evidence). The schema enforces this via the `SourceItem` regex; bare worker-name lists no longer parse.
90
- - **Verdict Card consistency.** `verdictCard.verdictToken` / `.direction` / `.nextStep` MUST byte-match `finalVerdict.verdictToken` / `.direction` / `.nextStep` and `recommendedNextSteps[0].text`. The renderer pulls both from the same data structureduplicating values across `verdictCard` and `finalVerdict` is intentional so the validator can diff them.
90
+ - **Verdict Card consistency.** `verdictCard.verdictToken` and `verdictCard.direction` MUST byte-match `finalVerdict.verdictToken` / `.direction`; `validators/validate-run.py` diffs both and fails the run on divergence. `verdictCard.nextStep` names the same action as `finalVerdict.nextStep` and `recommendedNextSteps[0].text` but is written as the actionable command the reader runs (e.g. `/okstra-run task-key=… task-type=release-handoff`) where the other two are prose it is deliberately not a byte copy. Duplicating the compared values across `verdictCard` and `finalVerdict` is intentional so the validator can diff them.
91
91
  - **Reader Summary.** Populate `readerSummary` when the schema excerpt exposes it. It is the human-first entrypoint for both Markdown and HTML: one sentence for the decision, one for the human action required, one for blockers, one for audit sections safe to skip on first read, and one runnable recommended command. Do not duplicate raw evidence tables here.
92
92
  - **§7 phase-continuation row (mandatory for non-terminal task-types).** When `header.taskType` is one of `requirements-discovery` / `implementation-planning` / `error-analysis` / `implementation` / `final-verification`, `followUpTasks` MUST contain at least one row whose `origin` is `phase-continuation`, `suggestedTaskType` equals the next phase (byte-identical to `finalVerdict.nextStep`'s referenced phase), `newTaskId` reuses the current task-id, `autoSpawn` is `"no"`, and `priority` is `"P0"`. For `release-handoff` runs, omit the phase-continuation row. Schema `allOf` clause enforces this via `contains`.
93
93
  - **No deprecated sections.** The schema has no `4.5.8 User Approval Request` body field, no `4.5.9 Open Questions`, no `5.1 Additional Material Request`, no `5.2 User Confirmation Questions` — clarifications go under the unified `clarificationItems[]` array.
@@ -37,7 +37,7 @@
37
37
  | `projectId` | Project ID |
38
38
  | `taskGroup` | Task group |
39
39
  | `taskId` | Task ID |
40
- | `taskType` | Analysis type (requirements-discovery, error-analysis, implementation-planning, implementation, final-verification, release-handoff) |
40
+ | `taskType` | Analysis type (requirements-discovery, error-analysis, implementation-planning, implementation, final-verification, release-handoff, plus the sidetrack improvement-discovery) |
41
41
  | `workCategory` | bugfix / feature / improvement / refactor / ops / unknown |
42
42
  | `recommendedWorkers` | List of selected workers |
43
43
  | `currentStatus` | Current task status |
@@ -44,7 +44,7 @@ Configure this in the `convergence` block of `task-manifest.json`. If the block
44
44
  | `verificationMode` | `"lightweight"` | `"lightweight"` or `"full-reanalysis"` |
45
45
  | `adversarial` | phase-aware: `true` for `requirements-discovery` / `error-analysis` / `implementation-planning`, `false` otherwise | When `true`, Phase 5.5 runs in **adversarial mode** (see §"Adversarial Verification Mode"): verifiers actively try to refute each finding, the burden of proof sits on the claim, and `verificationMode` is forced to `"full-reanalysis"` scoped to the finding's cited evidence. Resolved by `scripts/okstra_ctl/render.py` `_build_convergence_block` and recorded in `config.adversarial` of the convergence state artifact. |
46
46
 
47
- **Auto-disable rule (BLOCKING).** Convergence requires ≥2 analyser workers to produce a meaningful consensus tally. When the active profile's `Required workers:` block (see `prompts/profiles/*.md`) resolves to fewer than 2 analyser workers — e.g. `release-handoff` (zero analyser workers, lead-only) — the lead MUST treat `convergence.enabled` as `false` for that run regardless of manifest configuration, skip Phases 5.5 and the plan-body verification round ([plan-body-verification](./plan-body-verification.md)), and record `finalState: "converged"` with `totalRounds: 0` and an explanatory note in `config` (e.g. `"autoDisabled": "fewer-than-two-analysers"`). The plan-body round inherits the same rule via its `gating=false` advisory path.
47
+ **Auto-disable rule (BLOCKING).** Convergence requires ≥2 analyser workers to produce a meaningful consensus tally. When the active profile's `Required workers:` block (see `prompts/profiles/*.md`) resolves to fewer than 2 analyser workers — e.g. `release-handoff` (zero analyser workers, lead-only) — the lead MUST treat `convergence.enabled` as `false` for that run regardless of manifest configuration, skip Phases 5.5 and the plan-body verification round ([plan-body-verification](./plan-body-verification.md)), and record `finalState: "converged"` with `totalRounds: 0`, `round2SkippedReason: "auto-disabled"`, an empty `roundHistory`, and an explanatory note in `config` (e.g. `"autoDisabled": "fewer-than-two-analysers"`). The plan-body round inherits the same rule via its `gating=false` advisory path.
48
48
 
49
49
  ## Finding Category
50
50
 
@@ -136,7 +136,11 @@ WHILE roundIndex < effectiveMaxRounds AND queue is non-empty:
136
136
 
137
137
  # Final classification — runs after the WHILE loop exits (queue empty OR roundIndex == effectiveMaxRounds OR Round 2 gate closed)
138
138
  FOR each finding F still in queue:
139
- IF majority AGREE-or-SUPPLEMENT across all executed rounds:
139
+ IF config.adversarial AND F was carried forward by the adversarial round
140
+ resolution (a surviving `counter-evidence` refute, or a burden-not-met
141
+ majority — see §"Adversarial Verification Mode"):
142
+ F.classification = "contested" # one evidence-backed refute denies consensus, whatever the AGREE tally
143
+ ELIF majority AGREE-or-SUPPLEMENT across all executed rounds:
140
144
  F.classification = "partial-consensus"
141
145
  ELSE:
142
146
  F.classification = "contested"
@@ -235,7 +239,7 @@ ELSE:
235
239
  resolve F as "partial-consensus"
236
240
  ```
237
241
 
238
- `contested` remains a **final classification only** (per §"Scope and Terminology"): a disputed finding is carried forward through intermediate rounds and labelled `contested` only at the last executed round. For `requirements-discovery` (`effectiveMaxRounds = 1`) the single round IS the last round, so a split-with-hard-refute finding is labelled `contested` in that one round. The final-classifier block of §"Convergence Algorithm" is unchanged; this section only changes how each round's verdicts resolve into queue actions.
242
+ `contested` remains a **final classification only** (per §"Scope and Terminology"): a disputed finding is carried forward through intermediate rounds and labelled `contested` only at the last executed round. For `requirements-discovery` (`effectiveMaxRounds = 1`) the single round IS the last round, so a split-with-hard-refute finding is labelled `contested` in that one round. The final-classifier block of §"Convergence Algorithm" honours this: its first branch classifies an adversarially carried-forward finding `contested` regardless of the AGREE tally, so the two sections cannot assign the same finding different labels.
239
243
 
240
244
  Design intent: one `counter-evidence` refute denies a claim consensus (it cannot rise above `contested` however many others AGREE); a lone `burden-not-met` doubt does not sink an otherwise-surviving claim — only a majority of them does. When every non-discoverer refutes (all_others_disagree) the finding is worker-unique regardless of refute basis — only the discoverer still holds it. A SUPPLEMENT/caveat with zero disagrees lands partial-consensus, not full-consensus, because a caveat means the claim does not pass cleanly (unlike the collaborative classifier, where SUPPLEMENT counts as full agreement).
241
245
 
@@ -489,7 +493,7 @@ Schema rules:
489
493
  - `roundHistory[].carriedForwardCount`: queue size at the END of this round — the single definition. In-round insertions into the queue are forbidden, so this always equals `inputQueueSize - resolvedCount`. The pseudocode's per-item `carriedForwardCount += 1` accumulator is a counting convenience that lands on the same value; persist the post-round queue length, not the loop accumulator, if the two ever diverge.
490
494
  - `roundHistory[].dispatches[]`: one entry per worker that was actually dispatched in this round. Each entry is `{worker, status, durationMs}`. `status ∈ {completed, timeout, error, not-run}`. `durationMs` is integer milliseconds and is always present, even for terminal-non-result dispatches (use the elapsed time before the wrapper gave up).
491
495
  - `roundHistory[].skippedWorkers[]`: per-worker `{worker, reason}` for workers with no items to verify OR with a non-result dispatch.
492
- - `round2SkippedReason`: literal enum `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped`. Always present. Use `"not-skipped"` when Round 2 actually ran. Use `"max-rounds-1"` when `effectiveMaxRounds == 1` (Round 2 was never attempted). Use `"queue-empty"` when Round 1 fully drained the queue. Use `"all-reverify-non-result"` when all Round 1 dispatches terminated as non-result.
496
+ - `round2SkippedReason`: literal enum `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | auto-disabled`. Always present. Precedence when more than one condition holds, highest first: `"auto-disabled"` when the auto-disable rule fired (see §"Configuration" — the loop never built a queue; `roundHistory` is empty); `"max-rounds-1"` when `effectiveMaxRounds == 1` (Round 2 was never attempted this wins over `queue-empty` even when Round 1 drained the queue, so a clean `requirements-discovery` run records `max-rounds-1`); `"all-reverify-non-result"` when all Round 1 dispatches terminated as non-result; `"queue-empty"` when Round 1 fully drained the queue (`effectiveMaxRounds >= 2` only, by the precedence above); `"not-skipped"` when Round 2 actually ran.
493
497
  - `finalClassificationCounts`: post-loop counts. Required field with keys `fullConsensus`, `partialConsensus`, `contested`, `workerUnique`.
494
498
  - `finalState ∈ {converged, max-rounds-reached, aborted-non-result}`. Assigned by the lead at WHILE-loop exit: `converged` when the queue is empty at the end of any round; `max-rounds-reached` when the loop exits because `roundIndex == effectiveMaxRounds` with the queue still non-empty; `aborted-non-result` when the loop exits via the Worker-failure BREAK (per the "Worker failure handling in reverify" section, rule 4). `aborted-non-result` is the new v1.1 value.
495
499
  - `totalRounds`: count of rounds actually executed (not `effectiveMaxRounds`). May be `0` when Round 0 produced no queue items (all findings reached consensus during grouping).
@@ -519,11 +523,13 @@ Do NOT restate an existing finding. If nothing is missing, say so explicitly.
519
523
  ```
520
524
 
521
525
  ### Gap verification (1 adversarial reverify round)
522
- Each critic gap enters the verification queue as a finding with `originWorker = "<provider>-critic"` and `source = "critic"`. The lead runs ONE adversarial reverify round (§"Adversarial Verification Mode" classifier) with the Phase 4 analysers (excluding the critic itself) as voters. Only gaps classified `full-consensus` / `partial-consensus` merge into the final report findings; `contested` / `worker-unique` gaps are treated as hallucinations and dropped (recorded in the convergence state, not promoted). If no non-critic analyser is available to vote, the gaps are surfaced as unverified `clarification` items rather than merged, and that fact is recorded.
526
+ Each critic gap enters the verification queue as a finding with `originWorker = "<provider>-critic"` and `source = "critic"`. The lead runs ONE adversarial reverify round (§"Adversarial Verification Mode" classifier) with the Phase 4 analysers (excluding the critic itself) as voters. Only gaps classified `full-consensus` / `partial-consensus` merge into the final report findings; `contested` / `worker-unique` gaps are treated as hallucinations and dropped (recorded in the convergence state, not promoted).
527
+
528
+ **A gap that received no verdict is NOT a rejected gap (BLOCKING).** Dropping applies only to gaps the voters actually judged. A gap can also end the round *unjudged* — the verification dispatch returned a terminal non-result (`timeout`, `error`, no result file), the returned result covered only some of the gaps, or no non-critic analyser was available to vote at all. Nobody inspected those, so classifying them as hallucinations is a fabricated verdict. Each one MUST be recorded as a `## 5. Missing Information and Risks` row (`missingInformation`, `source: "critic-unverified"`) whose `risk` names the gap and the reason verification did not complete, and counted in `config.critic.gapsUnverified`. They are **not** promoted to findings (unverified) and **not** raised as `clarification` items — an unverified gap needs an analyser to verify it on the next run, not a decision from the user. Silently losing them is a contract violation: the batch that times out is exactly the batch of gaps too expensive to check, so the highest-risk items are the ones that vanish.
523
529
 
524
530
  ### State
525
531
  - `convergence.critic` manifest block: `{ enabled, provider, modelExecutionValue }`.
526
- - Convergence state artifact: critic gaps appear in `findings[]` with `source: "critic"`. Add a `config.critic` summary `{ provider, modelExecutionValue, gapsProposed, gapsMerged }`. `source` and `config.critic` are optional v1.2 fields (readers treat absence as null); no enum changes.
532
+ - Convergence state artifact: critic gaps appear in `findings[]` with `source: "critic"`. Add a `config.critic` summary `{ provider, modelExecutionValue, gapsProposed, gapsMerged, gapsUnverified }`. `gapsProposed - gapsMerged - gapsUnverified` is the number the voters judged and rejected, so a gap that quietly disappeared shows up as an arithmetic mismatch rather than as nothing at all. `source` and `config.critic` are optional v1.2 fields (readers treat absence as null); no enum changes.
527
533
 
528
534
  ## Acceptance critic pass (final-verification)
529
535
 
@@ -69,9 +69,11 @@ A single okstra run executes **exactly one** lifecycle phase. The phase is given
69
69
  |-----------------|-----------------|-------------------|
70
70
  | `requirements-discovery` | classification, routing decision, missing-input list, next-phase recommendation | code edits, plan documents, build/test execution that mutates state |
71
71
  | `error-analysis` | evidence, root-cause hypotheses, reproduction gaps, validation paths | code edits, implementation design, build/migration/deploy execution |
72
- | `implementation-planning` | option matrix, trade-offs, dependencies, recommended order, validation/rollback strategy, **explicit user-approval request** | source code edits, file writes outside the run's `reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/` directories, build/migration/deploy execution |
72
+ | `implementation-planning` | option matrix, trade-offs, dependencies, recommended order, validation/rollback strategy, Tier3 conformance scripts + manifest under the task-root `qa/` tree, **explicit user-approval request** | source code edits, file writes outside the run's `reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/` directories and the task-root `qa/` tree, build/migration/deploy execution |
73
73
  | `implementation` | code edits authorised by an approved plan, accompanying tests | starting work without an approved `implementation-planning` final report carried in via `--clarification-response` or referenced in the brief |
74
- | `final-verification` | acceptance verdict, residual risk, regression notes; read-only execution of existing test/validation commands is permitted | source code edits, refactors, scope expansion |
74
+ | `final-verification` | acceptance verdict, residual risk, regression notes; read-only execution of existing test/validation commands, run-artifact writes (qa result sidecars, `okstra handoff record-verified` on acceptance), and qaEnv-replica-only conformance runs are permitted | source code edits, refactors, scope expansion, mutations of the project or shared environments |
75
+ | `release-handoff` | user-selected packaging only: PR creation / push of the implementation branch, handoff summary (lead-only phase) | local commits, history-rewriting pushes, direct pushes to a base branch, release-publishing commands, source edits, worker dispatch of any kind |
76
+ | `improvement-discovery` *(sidetrack — outside `PHASE_SEQUENCE`)* | lens-scoped improvement candidates (`## 5.9` report), per-candidate next-phase recommendation | code edits, planning or root-cause work, candidates outside the lens allowlist or beyond the cap, starting any lifecycle phase inside this run |
75
77
 
76
78
  Phase-transition checklist (lead, end of run):
77
79
 
@@ -203,7 +205,7 @@ These phases are governed by [team-contract](./team-contract.md). It is the cano
203
205
  - Worker prompt anchor headers and body composition rules.
204
206
  - The `[Required reading]` clause (analysis-packet primary input for analysis workers, full source files for report-writer, scoped inputs for reverify dispatches).
205
207
  - The `[Error reporting]` clause and the asymmetry between claude-worker and codex/antigravity-worker prompts.
206
- - Worker output contract (sections 0–5), header standard, terminal statuses, errors-sidecar schema.
208
+ - Worker output contract (sections 1–5 + optional Section 6; the Reading Confirmation block lives in the audit sidecar, never in the worker-results file — the preamble "Reading rules" section is canonical and the validator rejects violations), header standard, terminal statuses, errors-sidecar schema.
207
209
  - Token-usage tracking conventions.
208
210
 
209
211
  `Report writer worker` is NOT an analysis worker. Do not dispatch it in Phase 4/5 alongside analysis workers. It is invoked only in Phase 6 — see [report-writer](./report-writer.md).
@@ -17,7 +17,7 @@ Phase 4 workers produce independent analyses (Findings F-001…)
17
17
  → Phase 5.5 FINDING convergence ([convergence](./convergence.md), sections "Convergence Algorithm" through "Convergence State Artifact")
18
18
  → Phase 6 report-writer authors final-report draft (consolidated Option Candidates / Stepwise Execution Order / Dependency / Validation Checklist / Rollback)
19
19
  → PLAN-BODY VERIFICATION ROUND ← this contract
20
- → User Approval gate (top-of-report `- [ ] Approved` marker is rendered only when this round's Gate result is `passed` or `passed-with-dissent`)
20
+ → User Approval gate (the frontmatter `approved:` flip is honoured by run-prep only when this round's Gate result is `passed` or `passed-with-dissent`)
21
21
  → implementation phase (separate run)
22
22
  ```
23
23
 
@@ -40,10 +40,10 @@ Plan-body verification is configured under `convergence.planBodyVerification` in
40
40
 
41
41
  | Setting | Default | Description |
42
42
  |---------|---------|-------------|
43
- | `enabled` | `true` | If `false`, the round is skipped and the top-of-report Approval marker is rendered unconditionally (legacy behaviour). |
43
+ | `enabled` | `true` | If `false`, the round is skipped and the approval gate is not blocked by this round (legacy behaviour). |
44
44
  | `maxRounds` | `1` | Upper bound. Plan-body verification is consistency / completeness checking, not fact checking — additional rounds rarely help. Range 1–3. |
45
45
  | `selfFixMaxRounds` | `3` | Upper bound on the report-writer self-fix loop (§"Round protocol" step 7). Range 1–5. The loop also stops early on no-progress, so this is a ceiling, not a target. |
46
- | `gating` | `true` | If `true` (default), `majority-disagree` blocks the Approval marker. If `false`, the round is advisory-only and the marker always renders. |
46
+ | `gating` | `true` | If `true` (default), `majority-disagree` blocks approval. If `false`, the round is advisory-only and never blocks approval. |
47
47
 
48
48
  Default values are emitted into the manifest by `scripts/okstra_ctl/render.py` (`_build_convergence_block`). The ctx knob `OKSTRA_PLAN_VERIFICATION=false` flips `planBodyVerification.enabled` to false.
49
49
 
@@ -108,7 +108,7 @@ Exception for `P-Req-*`: verifiers still MUST NOT re-open the original task brie
108
108
 
109
109
  ## Adversarial plan-body posture
110
110
 
111
- When `config.adversarial == true` (the default for `implementation-planning`; see [convergence](./convergence.md) §"Configuration"), the plan-body round runs with an **adversarial posture**. The classification rules and gate arithmetic in §"Round protocol" are UNCHANGED — `majority-disagree` (a *majority* of analysers DISAGREE) remains the only classification that blocks the Approval marker, and `dissent-isolated` still passes the gate. Adversarial mode changes only *how each verifier evaluates an item*:
111
+ When `config.adversarial == true` (the default for `implementation-planning`; see [convergence](./convergence.md) §"Configuration"), the plan-body round runs with an **adversarial posture**. The classification rules and gate arithmetic in §"Round protocol" are UNCHANGED — `majority-disagree` (a *majority* of analysers DISAGREE) remains the only classification that blocks approval, and `dissent-isolated` still passes the gate. Adversarial mode changes only *how each verifier evaluates an item*:
112
112
 
113
113
  - The burden of proof sits on the plan: an item earns `AGREE` only if the verifier actively tried to break it and could not.
114
114
  - The verifier MUST open the file paths / symbols / commands the item cites and confirm they exist and are executable as written. This is the one allowed widening of the lightweight "judge from internal consistency and stated commands / paths" rule — confirming the existence of cited paths is not "re-analyzing the original requirements".
@@ -126,8 +126,8 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
126
126
  - `full-consensus` — all participating analysers `AGREE` (SUPPLEMENT counts as agree on the item itself).
127
127
  - `partial-consensus` — majority `AGREE`, dissenting `DISAGREE` recorded.
128
128
  - `dissent-isolated` — only one worker `DISAGREE`s, others `AGREE` — treat as `partial-consensus` for gate purposes; record dissent. (Distinct from finding-convergence `worker-unique`, which means the *opposite*: only one worker AGREEs. Plan-body classifications use this dedicated label to avoid the collision.)
129
- - `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes), OR any single-vote-blocking kind fires: one `DISAGREE(a)` / `DISAGREE(d)` on any item, or one `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks the Approval marker**.
130
- - `needs-reverify` — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not. This does **not** block the Approval marker — blocking on it would make a worker failure produce a stricter gate than a healthy roster, the same paradox the ≥2-vote majority rule already rules out. The item is re-dispatched in the next round (step 7); if it survives the round budget it is promoted per step 8 with a Statement that says verification never completed. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` returns `needs-reverify` for this shape and `_recompute_plan_body_gate` folds it into `passed-with-dissent`.
129
+ - `majority-disagree` — a *majority* of analysers `DISAGREE` (majority needs ≥2 participating non-error votes), OR any single-vote-blocking kind fires: one `DISAGREE(a)` / `DISAGREE(d)` on any item, or one `DISAGREE(f)` on a `P-Req-*` item (see §"Single-vote-blocking kinds"). This classification **blocks approval**.
130
+ - `needs-reverify` — a single-vote-blocking kind fired but the item has **fewer than 2 participating non-error votes**, i.e. the lone dissent was never cross-verified because its peer returned `verification-error`. A single-vote-blocking kind means "one *confirmed* DISAGREE is enough"; an unconfirmed one is not. This does **not** block approval — blocking on it would make a worker failure produce a stricter gate than a healthy roster, the same paradox the ≥2-vote majority rule already rules out. The item is re-dispatched in the next round (step 7); if it survives the round budget it is promoted per step 8 with a Statement that says verification never completed. **Enforced:** `validators/validate-run.py` `_classify_plan_item_gate` returns `needs-reverify` for this shape and `_recompute_plan_body_gate` folds it into `passed-with-dissent`.
131
131
  - `contested` only meaningful when `maxRounds > 1`; at default `maxRounds=1`, fold any unresolved item into `partial-consensus`.
132
132
  5. Gate result resolution:
133
133
  - any `majority-disagree` item present AND `gating=true` → `blocked-by-disagreement`
@@ -154,7 +154,7 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
154
154
  - `Blocks=approval`
155
155
  - the item's `planItems[].clarificationId` set to that `C-<N>` (1:1 link). `validators/validate-run.py` `_validate_plan_body_clarification_matching` recomputes each item's class and fails when a majority-disagree item's `clarificationId` is missing, dangling, or points at a non-`approval` row.
156
156
  - a `planner-fixable` item that survives the self-fix loop is still promoted, but its `Statement` MUST state "planner self-fix attempted but unresolved" and name the stop reason. `validators/validate-run.py` `_validate_self_fix_before_clarification` fails when a planner-fixable majority item is promoted while the budget is not exhausted — it requires `selfFixRoundsApplied >= 1` **and** `selfFixStopReason` in `{no-progress, max-rounds-reached}`, so neither `all-resolved` nor `not-attempted` can excuse a promotion.
157
- 9. The top-of-report `- [ ] Approved` marker line is rendered if and only if the Gate result is `passed` or `passed-with-dissent`. `validators/validate-run.py` `validate_phase_boundary` enforces this correspondence; manually adding the marker line when the gate did not pass is a contract violation.
157
+ 9. Approval lives in the report's YAML frontmatter `approved:` field — there is no in-body marker line. The user may flip it to `true` only when the Gate result is `passed` or `passed-with-dissent`. **Enforced:** run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan`) fail-closes an `approved: true` plan whose data.json carries a blocking `gateResult` or an open/answered `Blocks: approval` clarification row, and `validators/validate-run.py` `_validate_plan_body_gate_recompute` rejects a declared `gateResult` healthier than the recorded votes.
158
158
 
159
159
  ## `plan-body-verification-<task-type>-<seq>.json` schema
160
160
 
@@ -195,9 +195,9 @@ Plan-body verification stays **lightweight** even under this posture — the `ve
195
195
  }
196
196
  ```
197
197
 
198
- `dispatches[].terminalStatus` mirrors finding convergence (`completed | timeout | error | not-run | cli-failure`).
198
+ `dispatches[].terminalStatus` mirrors finding convergence (`completed | timeout | error | not-run`). A wrapper-recorded `cli-failure` is a run-error-log event, not a terminal status — record that dispatch's `terminalStatus` as `error`.
199
199
 
200
- `planItems[].classification` enum: `full-consensus | partial-consensus | dissent-isolated | majority-disagree | contested`. `contested` only appears when `maxRounds > 1`; at default `maxRounds=1` any otherwise-unresolved item folds into `partial-consensus` per the round protocol above.
200
+ `planItems[].classification` enum: `full-consensus | partial-consensus | dissent-isolated | majority-disagree | needs-reverify | contested`. `needs-reverify` is the peer-error shape from §"Round protocol" step 4 (a single-vote-blocking kind with fewer than 2 participating non-error votes) — it survives into the state file when the round budget runs out before the re-dispatch resolves it, and `_recompute_plan_body_gate` folds it into `passed-with-dissent`. `contested` only appears when `maxRounds > 1`; at default `maxRounds=1` any otherwise-unresolved item folds into `partial-consensus` per the round protocol above.
201
201
 
202
202
  `planItems[].votes.<worker>` is the verbatim verdict token emitted by the worker — `AGREE | DISAGREE(<a|b|c|d|e|f>) | SUPPLEMENT` — or `verification-error` for terminal non-result dispatches. The `DISAGREE` token retains its `<kind>` suffix so the breakage class is recoverable from the state file alone.
203
203
 
@@ -51,7 +51,7 @@ The prompt MUST include, in this order at the top:
51
51
  has been resolved by the lead from project.json / global config
52
52
  before the dispatch is constructed. The worker copies this verbatim
53
53
  into `data.json.meta.reportLanguage`.
54
- 12. For implementation-planning runs: a literal block listing the 10 required English section headings (`Option Candidates`, `Trade-off`, `Recommended Option`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `User Approval Request`, `Plan Body Verification`, `Implementation Design Preparation`). The writer uses these exact substrings as section headings (Korean translation in parentheses is allowed), and the `Plan Body Verification` section carries its required `Gate result:` line.
54
+ 12. For implementation-planning runs: a literal block listing the 12 required English section headings `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `Requirement Coverage`, `Plan Body Verification`, `Cross-Project Dependencies`, `Decision Drafts`. This list is `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`; that tuple is the SSOT and this block must match it exactly. The writer uses these exact substrings as section headings (Korean translation in parentheses is allowed), and the `Plan Body Verification` section carries its required `Gate result:` line.
55
55
  13. An explicit instruction: `You are the author of TWO files: (a) the final-report data.json at <Result Path>, (b) the worker-results audit file at <Worker Result Path>. After writing the data.json, invoke "okstra render-final-report <Result Path>" through the available execution interface so the markdown sibling is rendered before you return. Do not return the report inline. The validator fails the run when (a)'s schema validation fails, when the rendered markdown is absent, or when (b) is missing.`
56
56
  14. The prose budget (dedup contract): `verdictCard.finalConclusion` is the conclusion SSOT — at most 3 sentences. `rationale.*` fields stay within 2 sentences each and reference the verdict card / row IDs instead of restating their prose; `readerSummary` fields are one line each; `summary` stays at 3-5 rows unless the run covers multiple tickets. The schema field descriptions carry the same budgets (`tests/contract/test_report_prose_budget.py` guards both surfaces). Generation time scales with output volume, so exceeding the budget is a cost bug, not extra diligence.
57
57
 
@@ -153,11 +153,13 @@ Milestone 1 keeps the final-report schema unchanged. Read the exact permitted va
153
153
 
154
154
  ### Agent-Specific Execution Status Table
155
155
 
156
+ The template renders this table in the merged record-meta shape (same pattern as §1 / §2.1): one meta cell stacking the short fields, one findings cell. The writer supplies the per-agent fields in data.json; the rendered form is:
157
+
156
158
  ```markdown
157
- | Agent | Role | Model | Status | Raw tokens | Billable tokens | Cost (USD) | Duration | Summary of Key Findings |
158
- |-------|------|-------|--------|-----------|-----------|------------|----------|------------------------|
159
- | <team-state.lead.agent> | <team-state.lead.role> | <model> | completed | <tokens> | <billable> | <cost> | <duration> | Final synthesis status |
160
- | <team-state.workers[].agent> | <team-state.workers[].role> | <model> | <status> | <tokens> | <billable> | <cost> | <duration> | Key findings summary |
159
+ | Record | Summary of Key Findings |
160
+ |--------|-------------------------|
161
+ | **<team-state.lead.agent>**<br>Role: <team-state.lead.role><br>Model: <model><br>Status: completed<br>Raw tokens: <tokens><br>Billable: <billable><br>Cost: <cost><br>Duration: <duration> | Final synthesis status |
162
+ | **<team-state.workers[].agent>**<br>Role: <team-state.workers[].role><br>Model: <model><br>Status: <status><br>Raw tokens: <tokens><br>Billable: <billable><br>Cost: <cost><br>Duration: <duration> | Key findings summary |
161
163
  ```
162
164
 
163
165
  Table Generation Rules:
@@ -184,7 +186,7 @@ Example (English mode shown — the renderer substitutes Korean when `meta.repor
184
186
  | Lead | 10,479,327 | 1,769,798 | $26.55 |
185
187
  | Worker subtotal | 7,988,699 | 2,075,192 | $25.43 |
186
188
  | **Grand total** | **18,468,026** | **3,844,990** | **$51.97** |
187
- | Codex/Antigravity CLI add-on | | | $0.00 |
189
+ | Codex/Antigravity CLI add-on | | | $4.12 |
188
190
 
189
191
  > **How to read**: "Raw tokens" is the total tokens the model actually processed (input + output + cache_creation + cache_read). In long sessions cache_read can account for 95%+ making the number look large. "Billable tokens" weights cache_read at 0.1×, cache_creation at 1.25×, and output at 5× to give an input-equivalent figure closer to actual cost. Costs are estimates based on published Anthropic/OpenAI/Google pricing.
190
192
  ```
@@ -199,27 +201,35 @@ Token Summary Generation Rules:
199
201
  - **Lead** row: `usageSummary.leadTotalTokens` / `usageSummary.leadBillableEquivalentTokens` / `usageSummary.estimatedCostUsd.lead`.
200
202
  - **Worker subtotal** row: `usageSummary.workerTotalTokens` / `usageSummary.workerBillableEquivalentTokens` / `usageSummary.estimatedCostUsd.claudeWorkers`.
201
203
  - **Grand total** row: `usageSummary.grandTotalTokens` / `usageSummary.grandBillableEquivalentTokens` / sum of `lead + claudeWorkers`.
202
- - **Codex/Antigravity CLI add-on** row: `usageSummary.estimatedCostUsd.cliWorkers`. If 0, still show the row so the reader sees that no CLI work was billed under those providers (or that CLI fallback occurred).
204
+ - **Codex/Antigravity CLI add-on** row: `usageSummary.estimatedCostUsd.cliWorkers`. The writer only supplies the value in data.json — the renderer emits the row when the CLI cost is > 0 and omits it entirely at 0 (`final-report.template.md` gates it on `tokenUsage.cli.costUsd > 0`), so do not expect a `$0.00` row in the rendered report.
203
205
  - Format tokens with comma separators (e.g., `32,500`); format USD with two decimals (e.g., `$1.43`).
204
206
  - If `lead` or any `worker.usage` records unavailable evidence, show `--` for that row and append a one-line note (`reason: <note>`).
205
207
  - If pricing for a model is unknown, the script omits `estimatedCostUsd` for that block — show `N/A` in that column and add a note like `pricing missing for model <model>`.
206
208
 
207
209
  ### Implementation-planning section heading contract (BLOCKING)
208
210
 
209
- When the run's `task-type` is `implementation-planning`, the final report MUST contain section headings whose **lines include each of the 10 literal English substrings below**. The validator (`validators/validate-run.py`) does plain substring matching on the report text and validates the design-preparation data contract — missing headings was a real, repeatedly observed failure mode caused by translating the headings to Korean.
211
+ When the run's `task-type` is `implementation-planning`, the final report MUST contain section headings whose **lines include each of the 12 literal English substrings below**. The validator (`validators/validate-run.py`) does plain substring matching on the report text and validates the design-preparation data contract — missing headings was a real, repeatedly observed failure mode caused by translating the headings to Korean.
212
+
213
+ The rows below mirror `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`, which is the SSOT. **Enforced:** `tests/contract/test_planning_required_sections_ssot.py` fails when this table and that tuple diverge — a heading listed here but not required (or required but not listed) sends the writer to author a section the validator rejects, or to omit one it demands.
210
214
 
211
215
  | # | Required substring | Recommended heading form |
212
216
  |---|--------------------|--------------------------|
213
217
  | 1 | `Option Candidates` | `### Option Candidates (Korean gloss)` |
214
218
  | 2 | `Trade-off` | `### Trade-off Matrix (Korean gloss)` |
215
219
  | 3 | `Recommended Option` | `### Recommended Option (Korean gloss)` |
216
- | 4 | `Stepwise Execution Order` | `### Stepwise Execution Order (Korean gloss)` |
217
- | 5 | `Dependency` | `### Dependency / Migration Risk (Korean gloss)` |
218
- | 6 | `Validation Checklist` | `### Validation Checklist (Korean gloss)` |
219
- | 7 | `Rollback` | `### Rollback Strategy (Korean gloss)` |
220
- | 8 | `User Approval Request` | Satisfied by the top-of-report `## User Approval Request (User Approval Gate)` block. Do NOT recreate a `### 5.5.8 User Approval Request` body stub — the validator now fails reports that contain one. |
221
- | 9 | `Plan Body Verification` + `Gate result:` | `### Plan Body Verification (Korean gloss)` containing a `Gate result:` line — copy `templates/reports/final-report.template.md §5.5.9` verbatim. Validator checks both substrings. |
222
- | 10 | `Implementation Design Preparation` | `### Implementation Design Preparation (Korean gloss)` rendered from `implementationPlanning.designPreparation` by `templates/reports/final-report.template.md §5.5.10`. |
220
+ | 4 | `Stage Map` | `## 5.5 Stage Map (Korean gloss)` — the per-stage sections hang off it. |
221
+ | 5 | `Stepwise Execution Order` | `### Stepwise Execution Order (Korean gloss)` |
222
+ | 6 | `Dependency` | `### Dependency / Migration Risk (Korean gloss)` |
223
+ | 7 | `Validation Checklist` | `### Validation Checklist (Korean gloss)` |
224
+ | 8 | `Rollback` | `### Rollback Strategy (Korean gloss)` |
225
+ | 9 | `Requirement Coverage` | `### Requirement Coverage (Korean gloss)` |
226
+ | 10 | `Plan Body Verification` + `Gate result:` | `### Plan Body Verification (Korean gloss)` containing a `Gate result:` line — copy `templates/reports/final-report.template.md §5.5.9` verbatim. Validator checks both substrings. |
227
+ | 11 | `Cross-Project Dependencies` | `### Cross-Project Dependencies (Korean gloss)` — `_(none)_` when the plan is single-project. |
228
+ | 12 | `Decision Drafts` | `### Decision Drafts (Korean gloss)` |
229
+
230
+ **Never author a `User Approval Request` section.** The approval gate moved to the YAML frontmatter `approved: true|false` field, and `validators/validate-run.py` fails any report containing a `## User Approval Request` block or a `### 5.5.8 User Approval Request` stub. This paragraph replaces an earlier row that listed the heading as required — following it produced a report the validator rejected.
231
+
232
+ `Implementation Design Preparation` is NOT in the substring list: `templates/reports/final-report.template.md` §5.5.10 renders that heading from `implementationPlanning.designPreparation`, so it is enforced by the schema + renderer rather than by a heading scan.
223
233
 
224
234
  The Korean translation in parentheses is optional but the English keyword is mandatory. The body of each section is written in the Report Language per the writing rules below. For non-`implementation-planning` runs, omit this entire block — these headings are NOT validator-checked for other task-types.
225
235
 
@@ -279,7 +289,7 @@ Skipping this file because "the real report is in `reports/`" is wrong. Both fil
279
289
 
280
290
  Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the documentation SSOT for section names and ordering. For full body structure at authoring time, consult your run's **phase-stripped** `final-report-template.md` (the per-task-type instruction-set copy defined in Phase 6 dispatch item 8); the "copy that block verbatim" references below mean the §-block as it appears in that stripped copy, not a re-read of the full source.
281
291
 
282
- **Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` / `Direction` / `Next Step` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` and the first item of `## 6.`. Divergence is `contract-violated`.
292
+ **Verdict Card (top-of-report, mandatory).** Render `## Verdict Card` between the report header and the (conditional) Approval block. Its `Verdict Token` / `Direction` / `Next Step` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` and the first item of `## 3. Recommended Next Steps`. Divergence is `contract-violated`.
283
293
 
284
294
  **Background and Rationale (top-of-report, mandatory — every task-type).** Fill the data.json `rationale` object (rendered as `## Background and Rationale`, right after the Verdict Card). It is the reviewer-facing narrative that answers four questions, in order — write each as **prose**, not a table:
285
295
  - `motivation` — why we are doing this work (goal / context).
@@ -292,13 +302,13 @@ Every field MUST anchor its claim with at least one evidence reference — a `pa
292
302
  **Reader-facing prose MUST NOT cite a bare brief/worker-internal ID that this report never surfaces** — `RC-*` (reporter confirmations, defined in the brief), `RF-*` / `F-*` (findings, defined in worker-results) have no anchor in the final report, so a reader hits an opaque token with nothing to click. Either expand it inline (`the confirmed version target 1.27.47→1.27.48`) or, for an audit trail, namespace it (`claude:F-005`). **Enforced:** `_validate_no_opaque_id_references` fails a bare `RC-*` / `RF-*` / non-namespaced `F-*` appearing in `verdictCard` / `finalVerdict` / `rationale` / `clarificationItems[].statement`. (The renderer anchors + links in-report IDs of any digit width, so a surfaced `RC-4`-style id does resolve.) Do NOT restate the Verdict Card or the §5.4 trade-off matrix verbatim — this section is the *why*, in connected prose, that those tables compress.
293
303
 
294
304
  0. **Clarification Response Carried In** — render this `## 0.` heading ONLY when `{{CLARIFICATION_RESPONSE_RELATIVE_PATH}}` is non-empty. Walk every `C-*` row of the prior report's `## 1. Clarification Items` table, reconcile against new evidence, and record the outcome (`resolved` / `obsolete`) with citation before drafting the verdict. When no carry-in path was provided, OMIT the `## 0.` heading entirely — the validator fails an empty Section 0 stub. The lead calls `okstra incremental-scope` exactly once, combining answered-clarification stage impacts (`--impacted`) and changed PREP IDs (`--prep-items`); selected-option, Stage Map, or recommended-approach changes pass both CSVs empty to force full mode. Record that single decision JSON verbatim into `implementationPlanning.incrementalDecision` (`mode`, `reverifyStages`, `carryStages`, `reason`); the renderer emits the `### 0.1 Incremental Re-Verification Scope` audit block from it, and the validator fails an `incremental`-mode run whose Section 0 omits that block. In `incremental` mode this run's `planItems` MUST carry every plan-item id from the re-verified stages forward with its updated verdict; if re-verification concludes a plan item should be REMOVED, that is a signal the answer's blast radius is not local — do not drop it here, tell the lead to abandon incremental and re-route to a FULL re-verification, because the carry merge only adds prior items and would resurrect the removed item's stale verdict. After authoring the current data.json, call `okstra incremental-carry`, passing the decision's `carryStages` CSV to `--carry-stages` and its `reverifyStages` CSV to `--reverify-stages`. A `CarryError` means the stage/PREP ownership contract is unsafe: discard the partial merged output and route the run through full re-verification; never publish a partially merged report.
295
- 1. **Cross Verification Results** — 4 categories (Full / Partial / Contested / Worker-Unique) when convergence is enabled, per `convergence`. Prepend the Round History sub-table (columns: `Round | inputQueueSize | resolvedCount | carriedForwardCount | dispatches | skippedWorkers`) plus a `round2SkippedReason: <value>` note, pulled verbatim from `convergence-<task-type>-<seq>.json`. Empty contested list renders as `- No items lacking consensus.`. Convergence-disabled runs use the legacy Consensus/Differences format and omit the round table.
296
- 2. **Final Verdict** — `Direction` `continue-investigation` / `begin-implementation` / `approve` / `reject` / `hold`. **Verdict Token** is `not-applicable` for every task-type except `final-verification` see "Final-verification verdict token contract" below for that case.
297
- 3. **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.
298
- 4. **Missing Information and Risks** — uncertain / "I don't know" items. `implementation-planning` adds §5.5 (see heading contract below); `release-handoff` adds §5.6.
299
- 5. **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.
300
- 6. **Recommended Next Steps** — prioritized actions. After Phase 7's follow-up spawner runs, append a row per newly created task-key (see "Phase 6 → Phase 7 execution sequence" above). **Approval-gate consistency:** when §1 carries any `Blocks: approval` row with `Status` ∈ {open, answered}, the Verdict Card `Next Step` and the first recommended step MUST point to the clarification rerun (`resume-clarification` of the SAME task-type) — never to "flip frontmatter `approved: true` jump straight to `implementation`". Run-prep enforces this gate (`run.py _validate_approved_plan` fail-closes on those rows and on a blocking data.json `gateResult`), so a direct-implementation next-step is an instruction the reader cannot actually follow. **Cross-project pointer rule:** for cross-project dependencies (another repo / a different top-level deployment module / a published package), `crossProjectDependencies` (§5.4 Cross-Project Dependencies) is authoritative — do NOT duplicate that substance (prerequisite work / verification signals / handoff) into `recommendedNextSteps`; put only a one-line pointer to that section (no double-recording).
301
- 7. **Follow-up Tasks** — auto-spawn-eligible table. Each row drives `okstra-spawn-followups.py`; see template §7 for the row schema.
305
+ 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.
306
+ 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.
307
+ 3. **Recommended Next Steps** — prioritized actions. After Phase 7's follow-up spawner runs, append a row per newly created task-key (see "Phase 6 → Phase 7 execution sequence" above). **Approval-gate consistency:** when §1 carries any `Blocks: approval` row with `Status` ∈ {open, answered}, the Verdict Card `Next Step` and the first recommended step MUST point to the clarification rerun (`resume-clarification` of the SAME task-type) never to "flip frontmatter `approved: true` → jump straight to `implementation`". Run-prep enforces this gate (`run.py _validate_approved_plan` fail-closes on those rows and on a blocking data.json `gateResult`), so a direct-implementation next-step is an instruction the reader cannot actually follow. **Cross-project pointer rule:** for cross-project dependencies (another repo / a different top-level deployment module / a published package), `crossProjectDependencies` (§5.4 Cross-Project Dependencies) is authoritative — do NOT duplicate that substance (prerequisite work / verification signals / handoff) into `recommendedNextSteps`; put only a one-line pointer to that section (no double-recording).
308
+ 4. **Follow-up Tasks** — auto-spawn-eligible table. Each row drives `okstra-spawn-followups.py`; see template §4 for the row schema.
309
+ 5. **Missing Information and Risks** — uncertain / "I don't know" items. `implementation-planning` adds §5.5 (see heading contract below); `release-handoff` adds §5.6.
310
+ 6. **Cross Verification Results** — 4 categories (Full / Partial / Contested / Worker-Unique) when convergence is enabled, per `convergence`. Prepend the Round History sub-table (columns: `Round | inputQueueSize | resolvedCount | carriedForwardCount | dispatches | skippedWorkers`) plus a `round2SkippedReason: <value>` note, pulled verbatim from `convergence-<task-type>-<seq>.json`. Empty contested list renders as `- No items lacking consensus.`. Convergence-disabled runs use the legacy Consensus/Differences format and omit the round table.
311
+ 7. **Final Verdict** — `Direction` ∈ `continue-investigation` / `begin-implementation` / `approve` / `reject` / `hold`. **Verdict Token** is `not-applicable` for every task-type except `final-verification` — see "Final-verification verdict token contract" below for that case.
302
312
 
303
313
  **§5.10 Fix History (data-presence gated).** When the run-manifest carries a `fixCycleId`, fill the data.json `fixCycle` block (`cycle` / `targetReport` / `symptom` / `runs`). Read the values from the task root's `history/fix-cycles.jsonl`: `cycle` MUST equal `fixCycleId`, `targetReport` / `symptom` come from that cycle's `opened` row, and `runs` lists its attached `run` rows (`taskType` / `runSeq` / `runManifest`). The validator (`validators/validate-run.py` → `_validate_fix_cycle`) fails the run when the block is missing or `fixCycle.cycle` does not match `fixCycleId`. When the run-manifest has no `fixCycleId`, OMIT the `fixCycle` block entirely — the renderer omits §5.10.
304
314
 
@@ -196,13 +196,13 @@ omits either header, the worker MUST return `<WORKER>_ERRORS_PATH_MISSING`
196
196
  without proceeding.
197
197
 
198
198
  - `cli-failure` events are recorded by the wrapper subagent itself (Codex / Antigravity), but **directly to the run-level error log** via `okstra error-log append-observed --error-type cli-failure ...` — NOT via the sidecar. The sidecar is an in-process tool-failure channel only.
199
- - **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-antigravity-exec.sh` accept four required positional arguments plus an optional fifth `<role>`: `<project-root> <model> <prompt-path> <worktree-path> [<role>]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise. For codex it becomes `--add-dir <worktree>` (sandbox write access); for antigravity it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>-<pid>` and the sibling trace-pane title `<cli>-<role>-<pid>-tail` (e.g. `codex-executor-93421` ↔ `codex-executor-93421-tail`). `<pid>` is the wrapper's own PID and disambiguates concurrent dispatches of the same role. The selected adapter maps the functional assignment identity to this value; when the value is absent, pass the literal `worker` (the wrapper also defaults to `worker` if the argument is omitted).
199
+ - **Wrapper invocation arity.** Both `okstra-codex-exec.sh` and `okstra-antigravity-exec.sh` take three required positional arguments plus three optional ones, matching their usage line verbatim: `<project-root> <model-execution-value> <prompt-path> [worktree-path] [role] [idle-timeout-seconds]`. The fourth (worktree) argument is **mandatory for implementation phase** and optional otherwise — when passed it must be an existing directory (the wrapper hard-fails otherwise). For codex it becomes `--add-dir <worktree>` (sandbox write access); for antigravity it is appended to `--include-directories`. Omitting it during implementation causes the codex sandbox to reject every Edit/Write targeting the worktree with EPERM. Workers extract the path from the `**Worktree:**` / `EXECUTOR_WORKTREE_PATH` / `cwd for every mutating command:` line in the lead prompt. The optional fifth `<role>` is folded into both the caller (worker) pane title `<cli>-<role>` and the sibling trace-pane title `<cli>-<role>-tail` (e.g. `codex-executor` ↔ `codex-executor-tail`; antigravity uses the `agy-` prefix). The selected adapter maps the functional assignment identity to this value; when the value is absent, pass the literal `worker` (the wrapper also defaults to `worker` if the argument is omitted). The optional sixth `<idle-timeout-seconds>` overrides the wrapper's stdout-idle watchdog (default 600 — see "No external timeout on wrapper subagents" below).
200
200
  - **Background dispatch + polling contract (Codex / Antigravity wrappers).** Both wrapper subagents MUST start their CLI through the selected adapter's asynchronous execution mapping and await 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:
201
201
  - Successful completion: return the wrapper's accumulated stdout from the terminal await result. No log entry.
202
202
  - Non-zero `exit_code`: record a `cli-failure` to the run-level error log with the real `exit_code` and observed `duration-ms`.
203
203
  - 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.
204
204
  - The selected adapter owns runtime-session accounting for the full wrapper window; core retains only the observed start/end event boundaries.
205
- - **No external timeout on wrapper subagents.** The codex/antigravity wrapper subagent's polling loop (with optional mtime grace) is the SINGLE timeout authority for its dispatch. Lead MUST NOT impose a separate `dispatch_worker` timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before its own polling cap is reached. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The wrapper's polling cap (30min + optional 5min grace) is calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, lower it in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
205
+ - **No external timeout on wrapper subagents.** The codex/antigravity wrapper owns BOTH of its timeout mechanisms, and they are the only two: (1) the subagent's polling cap (30min + optional 5min mtime grace), and (2) the wrapper script's internal stdout-idle watchdog — if the CLI writes nothing to its log for `<idle-timeout-seconds>` (6th positional arg, default 600s), the script TERM/KILLs the CLI and records a `timeout` stage in the status sidecar (`okstra-codex-exec.sh` / `okstra-antigravity-exec.sh`). A long-running but *chatty* CLI is never idle-killed; a silent hang is reaped at the idle cap instead of burning the full 30 minutes. Lead MUST NOT impose a separate `dispatch_worker` timeout, an outer `Bash` wall-clock deadline, or any other mechanism that terminates the subagent before the wrapper's own caps fire. Doing so reproduces the historical failure mode that motivated this rule: Lead aborts the subagent at e.g. 18 minutes, the subagent returns nothing, and Lead classifies the role as "no response" while the underlying CLI was actively working. The caps are calibrated so that, combined with Lead's redispatch policy (see "Lead Redispatch Policy on Result-Missing"), a recoverable single-run failure costs at most ~70 minutes of wall-clock — predictable enough to plan around. If a specific run requires a tighter cap, pass a lower `<idle-timeout-seconds>` or lower the polling cap in the wrapper subagent's polling contract (single source of truth), NOT by layering Lead-side timeouts.
206
206
  - `contract-violation` events (C) are recorded by Lead via `okstra error-log append-observed --error-type contract-violation ...` after inspecting worker outputs.
207
207
  - Lead's responsibility regarding the sidecar is to dump it to the run-level error log via `okstra error-log append-from-worker` after each worker terminates; Lead does not write into the sidecar.
208
208
 
@@ -70,7 +70,7 @@ profile document.
70
70
  - the same `final-report.md` file is the canonical artifact carried into the next run; the user appends answers inline before rerunning. The preferred turn-around is `scripts/okstra.sh --resume-clarification --task-key <project-id>:<task-group>:<task-id>` (opens the latest report in `$EDITOR`, then auto-reruns the same phase with `--clarification-response` carry-in). The lower-level form `--clarification-response <path>` remains available for scripted runs.
71
71
  - if a clarification response was carried in for this run, render the conditional `## 0. Clarification Response Carried In From Previous Run` section (the template's `RENDER_IF` guard activates it), walk every `C-*` row of the prior report's `## 1. Clarification Items` table, reconcile each one against new evidence, and update its `Status` to `resolved` or `obsolete` before issuing the next decision/verdict. When no carry-in path was provided, omit the `## 0.` heading entirely — the validator fails reports that emit an empty Section 0 stub (e.g. "No prior clarification response was provided for this run.").
72
72
  - Verdict Card (shared — applies to every final-report regardless of profile):
73
- - The top-of-report `## Verdict Card` block is mandatory in every final-report. Its `Verdict Token`, `Direction`, and `Next Step` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` and the first item of `## 3. Recommended Next Steps`. The validator treats the card as a non-authoritative index when card values diverge from the authoritative sections, the run is `contract-violated`.
73
+ - The top-of-report `## Verdict Card` block is mandatory in every final-report. Its `Verdict Token` and `Direction` cells MUST byte-match the corresponding cells in `## 7. Final Verdict` — the card is a non-authoritative index and divergence makes the run `contract-violated`. **Enforced:** `validators/validate-run.py` `_validate_verdict_card_consistency` (token, on the rendered markdown) and `_validate_verdict_card_fields` (direction, on data.json). The `Next Step` cell points at the SAME action as `## 7. Final Verdict` and the first item of `## 3. Recommended Next Steps`, but is written as the actionable command the reader runs (`/okstra-run task-type=release-handoff`) where §7 states it as prose it is not a byte copy and is not compared.
74
74
  - Cross-worker traceability (shared — applies to every analysis worker output and to the lead's `## 6.` / `## 2.` tables in the final-report):
75
75
  - **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.
76
76
  - **Lead-side ID assignment + source preservation.** When the lead (or `report-writer-worker`) synthesises `## 6.1 Consensus` / `## 6.2 Differences` / `## 2.1 Primary Evidence` rows from worker outputs, the lead assigns a fresh `C-NNN` / `D-NNN` / `E-NNN` row ID. The `Source items` column (or, where the template still calls it `Supporting workers` / `Workers (position)` / `Source`, that same column) MUST list every contributing worker:item pair (e.g. `claude:F-001, codex:1.1, antigravity:F-3`) so a reviewer can trace the synthesised row back to each worker's original wording without re-reading every worker-results file. Bare worker names without item IDs (e.g. `claude, codex, antigravity`) are deprecated for these tables; the validator does not yet fail on them but the readability pass treats it as a contract violation.
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "implementation-planning": [
24
24
  "source code edits of any kind (Edit/Write on project source files is forbidden)",
25
- "file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`); in particular, do not write to `docs/superpowers/specs/` or `docs/superpowers/plans/`",
25
+ "file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`) and the task-root qa tree (`<task_root>/qa/` — the Tier3 conformance scripts, manifest, and tsconfig this phase MUST write per the Stage Map conformance contract); in particular, do not write to `docs/superpowers/specs/` or `docs/superpowers/plans/`",
26
26
  "executing builds, migrations, deployments, or any state-mutating command",
27
27
  "starting `implementation` inside this run (must be a separate run authorised by an approved deliverable from this phase), even if the user says \"다음 단계 진행해\"",
28
28
  "dispatching parallel sub-agents beyond the required worker roster (okstra owns worker fan-out)",
@@ -39,14 +39,14 @@
39
39
  "dispatching parallel sub-agents beyond the required worker roster",
40
40
  "silent scope expansion: every file edited outside the approved plan list MUST appear in the `Out-of-plan edits` block with rationale",
41
41
  "leaving placeholders such as TBD / TODO / \"implement later\" / \"handle edge cases\" in newly-added lines of this run (check via `git diff <base>..HEAD | grep -E '^\\+[^+].*\\b(TBD|TODO|FIXME|XXX|implement later|handle edge cases|similar to|placeholder)\\b'`; pre-existing strings in untouched regions are out of scope)",
42
- "lead substituting its own verdict when every required verifier (`Antigravity verifier`, `Codex verifier`, `Claude verifier`) returned a non-result terminal status (`timeout`/`error`/`not-run`); in that case the run MUST end as `blocked` with routing recommendation back to `error-analysis`, never with a lead-only verdict",
42
+ "lead substituting its own verdict when every verifier present in the resolved roster (`Claude verifier`, `Codex verifier`, plus `Antigravity verifier` when opted in) returned a non-result terminal status (`timeout`/`error`/`not-run`); in that case the run MUST end as `blocked` with routing recommendation back to `error-analysis`, never with a lead-only verdict",
43
43
  "claiming rollback verification on a schema migration, config-format change, or any persisted-state mutation without a recorded dry-run of the rollback step and its captured exit code",
44
44
  "declaring overall task acceptance — that is `final-verification` ownership; this phase reports only \"ready for final-verification\" or \"needs new planning loop\"",
45
45
  "delegating the self-review pass to a generic subagent — `Claude lead` must run it"
46
46
  ],
47
47
  "final-verification": [
48
48
  "source code edits, follow-up bug fixes, or scope expansion",
49
- "state-mutating commands; only read-only execution of pre-existing test or validation commands is permitted",
49
+ "state-mutating commands against the project or shared environments; permitted mutations are limited to the run's own `.okstra` artifacts (reports, state, `<task_root>/qa/result-*.json` sidecars, `okstra handoff record-verified` on acceptance) and Tier3 conformance scripts that mutate only their qaEnv replica datastore — everything else is read-only execution of pre-existing test or validation commands",
50
50
  "starting any follow-up phase inside this run; record findings and end the run"
51
51
  ],
52
52
  "release-handoff": [
@@ -66,7 +66,7 @@
66
66
  {{INCLUDE:_clarification-recommendation.md}}
67
67
  - **Evidence note required inside `Statement`**: every clarification row includes `Evidence checked: <path:line>` or `Evidence checked: none — <human-only reason>` in the `Statement` cell. `none` is allowed ONLY when the row's nature is "only a human can answer this" (reporter intent, business priority, organisational decision). A row with `none` that *could* have been answered by the codebase is a defect of this phase, restated from the pre-planning rule above.
68
68
  - Section heading contract (BLOCKING — validator scans for these literal English substrings):
69
- - The final report MUST include section headings containing each of the following exact strings: `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stage Exit Contract`, `Stage Validation`, `Dependency`, `Cross-Project Dependencies`, `Decision Drafts`, `Validation Checklist`, `Rollback`, `Requirement Coverage`, `Implementation Design Preparation`. (Approval is no longer a body section — it is the YAML frontmatter `approved` field.) `validators/validate-run.py` enforces the planning headings it scans, while `schemas/final-report-v1.0.schema.json` plus `templates/reports/final-report.template.md` enforce and render the design-preparation heading from its required data block.
69
+ - The final report MUST include section headings containing each of the following exact strings — this list mirrors `PLANNING_REQUIRED_SECTIONS` in `validators/validate-run.py`, which is the SSOT: `Option Candidates`, `Trade-off`, `Recommended Option`, `Stage Map`, `Stepwise Execution Order`, `Dependency`, `Validation Checklist`, `Rollback`, `Requirement Coverage`, `Plan Body Verification`, `Cross-Project Dependencies`, `Decision Drafts`. (Approval is no longer a body section — it is the YAML frontmatter `approved` field.) Three further headings are enforced elsewhere, not by that scan: `Stage Exit Contract` and `Stage Validation` are per-stage subsections checked by `validators/validate-implementation-plan-stages.py`, and `Implementation Design Preparation` is enforced and rendered by `schemas/final-report-v1.0.schema.json` plus `templates/reports/final-report.template.md` from its required data block.
70
70
  - Korean translations are allowed in parentheses (e.g. `### Recommended Option (Korean gloss)`), but the English keyword must be present verbatim in the heading line.
71
71
  - The shape and ordering follow `final-report-template.md` sections 5.4 (`Implementation Plan Deliverables`) + 5.5 (`Stage Map`). `validators/validate-run.py` substring-matches the raw report text, so a Korean-only heading fails the gate — the cause of repeated observed failures.
72
72
  - Beyond substring matching, when the Plan Body Verification gate result is `passed` / `passed-with-dissent`, `validators/validate-run.py` runs the **structural** Stage Map validator (`validators/validate-implementation-plan-stages.py`) at the planning boundary — not deferred to the `implementation` entry gate. It enforces: the exact `## 5.5 Stage Map` heading, each `## 5.5.<i> Stage <i>:` section with its four required subsections, the per-stage effective step count (≤8), the `depends-on` DAG, and the per-stage vertical-slice contract (S10). S10 scans for the literal in-section strings `Slice value:`, `Acceptance:`, the three `Test case (success):` / `Test case (boundary):` / `Test case (failure):` lines (S10d), and the Stepwise `action`-cell prefixes `RED:` / `GREEN:` (or a `TDD exemption:` line, which waives both the test-case lines and the RED/GREEN check) — keep these tokens verbatim for the same reason as the heading keywords above.
@@ -14,7 +14,7 @@
14
14
  - Executor provider: `{{EXECUTOR_PROVIDER}}` (one of: `claude` | `codex` | `antigravity`; chosen via `--executor` or `OKSTRA_DEFAULT_EXECUTOR`, default `claude`)
15
15
  - Executor subagent for dispatch: `{{EXECUTOR_WORKER_AGENT}}`
16
16
  - Executor model: `{{EXECUTOR_MODEL_DISPLAY}}` (launch value: `{{EXECUTOR_MODEL_EXECUTION_VALUE}}`)
17
- - Wherever this profile mentions the `Executor`, it refers to the role bound above. The other two providers in the roster (`claude` / `codex` / `antigravity` minus the executor) are dispatched as **verifiers only** for this run and remain strictly read-only.
17
+ - Wherever this profile mentions the `Executor`, it refers to the role bound above. **Every** analysis provider in the resolved roster is also dispatched as a verifier — including the executor's own provider, which runs *separately* as a fresh session with no shared context so no verdict comes from the session that wrote the diff (`_implementation-verifier.md` owns this rule). Verifier dispatches remain strictly read-only.
18
18
  {{INCLUDE:_common-contract.md}}
19
19
  {{INCLUDE:_stage-discipline.md}}
20
20
  - Pre-implementation gate (mandatory — refuse to start if any item fails):
@@ -29,7 +29,7 @@
29
29
  - Base ref: `{{EXECUTOR_WORKTREE_BASE_REF}}` — canonical `<base>` for every `git diff` / `git log` in this run. Independent stages start from the common anchor (the task-key worktree HEAD, fixed once at first stage entry); dependent stages start from the predecessor done commit or the verified merged task worktree head.
30
30
  - Provisioning note: `{{EXECUTOR_WORKTREE_NOTE}}`
31
31
  - Treat the working-tree path as `project_root` for the duration of this run. Do NOT mutate the caller's original checkout. cwd-sensitive Bash commands MUST be prefixed `cd {{EXECUTOR_WORKTREE_PATH}} && ` in the same Bash invocation (never `bash -lc "..."` wrappers — see executor sidecar for full rules).
32
- - Lifecycle: kept after the run completes as this stage's evidence worktree. Later stages get their own stage worktrees. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>` → drop the stage-key registry entry (`<task-key>#stage-<N>`).
32
+ - Lifecycle: kept after the run completes as this stage's evidence worktree. Later stages get their own stage worktrees. Manual cleanup: `git worktree remove <path>` → `git branch -D <branch>` → drop the stage-key registry entry (`<task-key>#stage-<N>`). Exception: whole-task `final-verification` auto-merges every done stage and tears down its worktree/branch/registry key (`stage_integrate.integrate_stages`, `teardown=True`) — do not promise the user the worktree survives past that point.
33
33
  - Approval gate (phase-specific addendum to shared authority rule):
34
34
  - the pre-implementation gate's recorded user approval marker is the only authorised approval gate at this phase — proceed once it is satisfied without further external coordination.
35
35
  - Forbidden actions — universal (any occurrence → terminal status `contract-violated`):
@@ -7,7 +7,7 @@
7
7
  - antigravity
8
8
  - report-writer
9
9
  - Optional workers (opt-in via `--workers`):
10
- - none — every required worker stays required because lens diversity is the load-bearing value of this phase
10
+ - none — the block above is the full roster. As everywhere, `--workers` may narrow within it (`okstra_ctl.workers.validate_workers_against_profile` enforces the allowlist only, minimum 1), but narrowing this phase is strongly discouraged: cross-worker lens diversity is its load-bearing value, and a reduced roster produces candidates no second worker ever challenged.
11
11
  {{INCLUDE:_common-contract.md}}
12
12
  - Brief consumption (phase-specific addendum — shared rules live in `_common-contract.md` under "Brief handoff contract"):
13
13
  - this phase REQUIRES a codebase-scan brief whose frontmatter contains `scope: codebase`. A brief without that marker is rejected before worker dispatch.
@@ -1,6 +1,7 @@
1
1
  """Compact path-hint persistence and legacy context hydration."""
2
2
  from __future__ import annotations
3
3
 
4
+ import re
4
5
  from pathlib import Path
5
6
  from typing import Any, Mapping
6
7
 
@@ -370,7 +371,15 @@ def _run_path_set(
370
371
  "run_sessions": run_dir / "sessions",
371
372
  "run_logs": run_dir / "logs",
372
373
  "worker_results": worker_results,
373
- "run_carry": run_dir / "carry",
374
+ # Carry is implementation-only and stage-shared (flat under the
375
+ # task-type run dir), so strip a `stage-<N>` leaf before anchoring it —
376
+ # mirrors paths.py `run_carry`.
377
+ "run_carry": (
378
+ (run_dir.parent if re.fullmatch(r"stage-\d+", run_dir.name) else run_dir)
379
+ / "carry"
380
+ if task_type_segment == "implementation"
381
+ else run_dir / "carry"
382
+ ),
374
383
  **_run_files(run_dir, run_prompts, worker_results, task_type_segment, sequences, suffixes),
375
384
  }
376
385
 
@@ -188,7 +188,18 @@ def compute_run_paths(
188
188
  run_sessions = run_dir / "sessions"
189
189
  run_logs = run_dir / "logs"
190
190
  worker_results = run_dir / "worker-results"
191
- run_carry = run_dir / "carry"
191
+ # The carry sidecar ledger is implementation-only and stage-SHARED, like
192
+ # consumers.jsonl: every reader/writer (consumers.py,
193
+ # implementation_outcome.py, design_prep.py, the deliverable profile) uses
194
+ # the flat `runs/implementation/carry/`, because stage N+1 must find stage
195
+ # N's sidecar without knowing N's run layout. Never stage-scope it. Other
196
+ # task types keep the (unused) run-local spelling so their run dirs don't
197
+ # grow a stray task-type-level carry/.
198
+ run_carry = (
199
+ runs_dir / task_type_segment / "carry"
200
+ if task_type_segment == "implementation"
201
+ else run_dir / "carry"
202
+ )
192
203
 
193
204
  if run_seq_override is not None:
194
205
  seq_int = int(run_seq_override)
@@ -847,6 +847,8 @@ _NARROW_HEADER_PREFIXES: tuple[str, ...] = (
847
847
  "origin",
848
848
  "priority",
849
849
  "auto-spawn",
850
+ # merged record-meta column (columns.recordMeta) — "Record" in en, "항목" in ko
851
+ "record",
850
852
  # Korean
851
853
  "출처",
852
854
  "에이전트",
@@ -109,6 +109,11 @@ def validate_workers_against_profile(
109
109
  """프로파일이 `Required workers:` 로 로스터를 선언했다면, 사용자
110
110
  override 가 (required ∪ optional) 의 부분집합인지 검증한다.
111
111
 
112
+ `Required workers:` 는 이 시스템에서 **allowlist + 기본 로스터**다 —
113
+ 전원 필수 집합이 아니다. 사용자는 그 안에서 좁혀 고를 수 있고(최소 1),
114
+ wizard 는 분석 워커만 고르게 한 뒤 report-writer 를 뒤에서 강제
115
+ 추가하므로 누락 검증을 여기서 하면 그 흐름이 깨진다.
116
+
112
117
  `profile_workers` 가 비어 있으면(프로파일이 로스터를 선언하지 않은
113
118
  구버전) 검증을 건너뛴다.
114
119
  """
@@ -83,7 +83,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
83
83
  " - `Out-of-plan edits` block listing every file touched outside the approved plan with rationale (empty block preferred)\n"
84
84
  ' - validation evidence: actual stdout/stderr and exit code for every pre / mid / post command from the plan (no paraphrased "tests pass")\n'
85
85
  " - TDD evidence for TDD-applicable steps: failing-test output before implementation commit and passing-test output after, with framing SHAs\n"
86
- " - per-verifier sections (`Antigravity verifier`, `Codex verifier`, `Claude verifier`) with independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by `Claude lead`\n"
86
+ " - per-verifier sections for every verifier in the resolved roster (`Claude verifier`, `Codex verifier`, plus `Antigravity verifier` when opted in) with independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by `Claude lead`\n"
87
87
  " - rollback verification (revert SHA reachable, feature flag toggle works, migration down step valid; dry-run preferred)\n"
88
88
  " - routing recommendation for `final-verification` (ready / needs new error-analysis or planning loop)"
89
89
  ),
@@ -308,6 +308,17 @@
308
308
  }
309
309
  ]
310
310
  },
311
+ "criticGaps": {
312
+ "type": "object",
313
+ "description": "Coverage-critic gap accounting. `proposed - merged - unverified` is the count the voters judged and rejected, so a gap that vanished shows up as a mismatch instead of as nothing. Every `unverified` gap needs a matching `missingInformation` row with source `critic-unverified` (prompts/lead/convergence.md \"Gap verification\").",
314
+ "required": ["proposed", "merged", "unverified"],
315
+ "additionalProperties": false,
316
+ "properties": {
317
+ "proposed": { "type": "integer", "minimum": 0 },
318
+ "merged": { "type": "integer", "minimum": 0 },
319
+ "unverified": { "type": "integer", "minimum": 0 }
320
+ }
321
+ },
311
322
  "consensus": {
312
323
  "type": "array",
313
324
  "items": { "$ref": "#/$defs/ConsensusRow" }
@@ -507,7 +518,7 @@
507
518
  "required": ["h1", "h2b", "h3"],
508
519
  "additionalProperties": false,
509
520
  "properties": {
510
- "h1": { "enum": ["local only", "push + PR", "skip"] },
521
+ "h1": { "enum": ["local checkout", "push + PR", "skip"] },
511
522
  "h2": { "type": "string" },
512
523
  "h2b": {
513
524
  "enum": ["not-run", "clean", "proceed anyway", "change base branch", "cancel"]
@@ -878,9 +878,13 @@ is set (Step 6.5), and **before** printing the Step 7 hand-off message, you
878
878
  MUST validate every brief produced this run. The validator enforces the
879
879
  labelled-handoff contract — required frontmatter keys, Augmentation labels,
880
880
  `intent-inference ↔ intent-check:` auto-mirroring, `parent-id`/`depth`
881
- consistency, `reporter-confirmations` markers, `Related Task Graph`
882
- table shape/relation values, and the codebase-scan `scope`/`Scan Scope`/
883
- `Priority Lenses` rules.
881
+ consistency, `reporter-confirmations` markers, the variant-required sections
882
+ from "Required sections by variant" (`Source Material` / `Problem / Symptom`
883
+ off-codebase, plus `Context` / `Desired Outcome` / `Acceptance Criteria` /
884
+ `External Gates` / `Constraints` / `Related Artifacts` / `Related Task Graph`
885
+ / `Open Questions` everywhere — each present with a non-blank body, `_(none)_`
886
+ allowed), `Related Task Graph` table shape/relation values, and the
887
+ codebase-scan `scope`/`Scan Scope`/`Priority Lenses` rules.
884
888
 
885
889
  1. Run the validator over the briefs directory:
886
890
 
@@ -898,10 +902,14 @@ table shape/relation values, and the codebase-scan `scope`/`Scan Scope`/
898
902
  confirm each brief has all ten required frontmatter keys (`type`,
899
903
  `brief-id`, `parent-id`, `ticket-id`, `source-type`, `task-group`,
900
904
  `depth`, `created`, `generator`, `reporter-confirmations`); every
901
- Augmentation entry carries one of the four labels; every `intent-inference`
902
- has a paired `intent-check:` row; `parent-id` is `self` at depth 0 and a
903
- real parent id below; and for `scope: codebase` briefs, `Scan Scope` is
904
- non-empty and `Priority Lenses` lists 1–4 whitelist lenses.
905
+ section the "Required sections by variant" table marks Required for the
906
+ brief's variant is present with a non-blank body (`_(none)_` allowed)
907
+ in particular `Desired Outcome`, `Acceptance Criteria`, `External Gates`,
908
+ `Related Artifacts`, and `Related Task Graph`, the ones authors drop most;
909
+ every Augmentation entry carries one of the four labels; every
910
+ `intent-inference` has a paired `intent-check:` row; `parent-id` is `self`
911
+ at depth 0 and a real parent id below; and for `scope: codebase` briefs,
912
+ `Scan Scope` is non-empty and `Priority Lenses` lists 1–4 whitelist lenses.
905
913
 
906
914
  ## Step 7: Hand off
907
915
 
@@ -758,15 +758,9 @@ Single scan of every agreed check that this run did NOT confirm — requirement
758
758
  {%- endif %}
759
759
 
760
760
  {% endif %}
761
- {% if header.taskType == 'improvement-discovery' %}
762
- ## 5.9 Improvement Candidates
763
-
764
- > Lens whitelist and column schema enforced by `validators/validate_improvement_report.py`. Row count is bounded by the brief's `candidate-cap` (default 8, absolute max 12).
765
-
766
- | Cand ID | Lens | Title | Scope | Severity | Effort | Consensus | Source workers | Recommended next-phase | Evidence |
767
- |---------|------|-------|-------|----------|--------|-----------|----------------|------------------------|----------|
768
-
769
- {% endif %}
761
+ {# `improvement-discovery` is not in the data.json TaskType enum — its
762
+ `## 5.9 Improvement Candidates` report is authored directly by the report
763
+ writer (report-writer-worker.md), never rendered through this template. #}
770
764
  {% if fixCycle and fixCycle.cycle %}
771
765
  ## 5.10 Fix History
772
766
 
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "emptyState": {
7
7
  "consensusItems": "- No consensus items.",
8
- "differences": "- No significant differences. 1.1 Consensus stands as-is.",
8
+ "differences": "- No significant differences. 6.1 Consensus stands as-is.",
9
9
  "primaryEvidence": "- No primary evidence.",
10
10
  "secondaryEvidence": "- No secondary evidence or alternate interpretations.",
11
11
  "risks": "- No missing information or risks.",
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "emptyState": {
7
7
  "consensusItems": "- 합의 항목 없음.",
8
- "differences": "- 유의미한 차이 없음. 1.1 Consensus 가 그대로 유효합니다.",
8
+ "differences": "- 유의미한 차이 없음. 6.1 Consensus 가 그대로 유효합니다.",
9
9
  "primaryEvidence": "- 주 증거 없음.",
10
10
  "secondaryEvidence": "- 보조 증거 또는 대안 해석 없음.",
11
11
  "risks": "- 누락된 정보·위험 없음.",
@@ -102,6 +102,8 @@ Every worker prompt MUST begin with these anchor headers, in this exact order, b
102
102
  7. `**Errors log path:** <absolute-path>` — run-level JSONL (see Error reporting above).
103
103
  8. `**Errors sidecar path:** <absolute-path>` — per-worker JSON (see Error reporting above).
104
104
 
105
+ **Exception — reverify dispatches.** A Phase 5.5 re-verification prompt (its `**Prompt History Path:**` carries a `-reverify-r<N>-` segment) deliberately omits anchors 5 and 6 and adds a `**Model:**` line — lightweight mode reads neither this file nor the preflight pack. The two errors-path anchors are NOT omitted; that gate applies to reverify prompts unchanged. The reverify anchor set is owned by `prompts/lead/convergence.md` "Required reverify-prompt anchor headers".
106
+
105
107
  For the **implementation phase** specifically, the dispatched prompt MUST also include:
106
108
 
107
109
  - `**Worktree:** <absolute-path>` — the task worktree path.
@@ -459,6 +459,48 @@ def check_scope_split_sections(text: str, errors: list[str]) -> None:
459
459
  )
460
460
 
461
461
 
462
+ # The variant-independent rows of the skill's "Required sections by variant"
463
+ # table (okstra-brief-gen SKILL.md). Desired Outcome and the Acceptance
464
+ # Criteria / External Gates split have dedicated checks with richer messages;
465
+ # Scan Scope / Priority Lenses are codebase-only (check_codebase_scope);
466
+ # Source Material / Problem / Symptom are omitted by the codebase variant, so
467
+ # they are enforced only off it.
468
+ ALWAYS_REQUIRED_SECTIONS = (
469
+ "Context",
470
+ "Constraints",
471
+ "Related Artifacts",
472
+ "Related Task Graph",
473
+ "Open Questions",
474
+ )
475
+ NON_CODEBASE_REQUIRED_SECTIONS = ("Source Material", "Problem / Symptom")
476
+
477
+
478
+ def check_variant_required_sections(
479
+ text: str, scope: str, errors: list[str]
480
+ ) -> None:
481
+ """Presence + non-blank body for the table's remaining Required rows.
482
+
483
+ Downstream phases read these sections by heading; a silently-absent
484
+ heading reads as "the reporter had nothing to say" when the brief was
485
+ simply authored off-template. `_(none)_` stays the explicit empty marker.
486
+ """
487
+ required = ALWAYS_REQUIRED_SECTIONS
488
+ if scope != "codebase":
489
+ required = required + NON_CODEBASE_REQUIRED_SECTIONS
490
+ for heading in required:
491
+ if not has_section_heading(text, heading):
492
+ errors.append(
493
+ f"required section '## {heading}' is missing "
494
+ "(okstra-brief-gen SKILL.md 'Required sections by variant')"
495
+ )
496
+ continue
497
+ if not section_body(text, heading).strip():
498
+ errors.append(
499
+ f"required section '## {heading}' has an empty body "
500
+ "(use _(none)_ for a deliberately-empty section)"
501
+ )
502
+
503
+
462
504
  def check_reporter_confirmations(
463
505
  rc_status: str | None, reporter_rows: list[str], errors: list[str]
464
506
  ) -> None:
@@ -522,6 +564,7 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
522
564
 
523
565
  check_requirement_section(text, errors)
524
566
  check_scope_split_sections(text, errors)
567
+ check_variant_required_sections(text, scope, errors)
525
568
 
526
569
  # 2. brief-id matches filename stem
527
570
  stem = path.stem
@@ -1529,6 +1529,32 @@ def _validate_verdict_card_consistency(content: str, failures: list[str]) -> Non
1529
1529
  )
1530
1530
 
1531
1531
 
1532
+ def _validate_verdict_card_fields(data: dict, failures: list[str]) -> None:
1533
+ """`verdictCard.direction` must byte-match its authoritative home in §7.
1534
+
1535
+ The markdown check above compares only the Verdict Token, so a Card whose
1536
+ Direction contradicted §7 shipped silently. `nextStep` is deliberately NOT
1537
+ compared: every shipped fixture authors the Card's cell as the actionable
1538
+ command (`/okstra-run task-type=release-handoff`) while §7 states the same
1539
+ action as prose ("Proceed to release-handoff."), so a byte-match rule there
1540
+ would reject the reference reports.
1541
+ """
1542
+ card = data.get("verdictCard")
1543
+ final = data.get("finalVerdict")
1544
+ if not isinstance(card, dict) or not isinstance(final, dict):
1545
+ return
1546
+ card_value = str(card.get("direction") or "").strip()
1547
+ final_value = str(final.get("direction") or "").strip()
1548
+ if not card_value or not final_value or card_value == final_value:
1549
+ return
1550
+ failures.append(
1551
+ f"final-report data.json: verdictCard.direction `{card_value}` does not "
1552
+ f"match finalVerdict.direction `{final_value}` — the Card is a "
1553
+ "non-authoritative index and MUST byte-match §7 "
1554
+ "(_common-contract.md Verdict Card)."
1555
+ )
1556
+
1557
+
1532
1558
  def _load_final_report_data(report_path: Path) -> dict:
1533
1559
  """Best-effort parse of the final-report data.json sibling. Returns {} when
1534
1560
  absent or unparseable — those conditions are already surfaced as failures by
@@ -1596,6 +1622,9 @@ def validate_final_report_data(
1596
1622
 
1597
1623
  _validate_rationale_evidence(data, failures)
1598
1624
  _validate_no_opaque_id_references(data, failures)
1625
+ _validate_verdict_card_fields(data, failures)
1626
+ # Phase-agnostic: the coverage critic runs in every finding-producing phase.
1627
+ _validate_unverified_critic_gaps_recorded(data, failures)
1599
1628
 
1600
1629
  task_type = (data.get("header") or {}).get("taskType")
1601
1630
  if task_type == "final-verification":
@@ -1905,6 +1934,27 @@ _PLAN_ITEM_SOURCES = (
1905
1934
  )
1906
1935
 
1907
1936
 
1937
+ def _plan_item_source_count(planning: dict, field: str) -> int:
1938
+ """Row count for one plan-body deliverable array.
1939
+
1940
+ `stepwiseExecution` has two homes: the top-level array is the legacy flat
1941
+ summary (schema: "Legacy flat summary kept for compatibility only. New
1942
+ reports use stageMap/stages") and is absent from `implementationPlanning.
1943
+ required`, while a current report carries the real steps per stage. Counting
1944
+ only the legacy field made this whole check a no-op for the largest
1945
+ category — a modern plan could drop every `P-Step-*` item and still pass.
1946
+ """
1947
+ rows = [r for r in (planning.get(field) or []) if isinstance(r, dict)]
1948
+ if field != "stepwiseExecution":
1949
+ return len(rows)
1950
+ per_stage = sum(
1951
+ len([s for s in (stage.get("stepwiseExecution") or []) if isinstance(s, dict)])
1952
+ for stage in (planning.get("stages") or [])
1953
+ if isinstance(stage, dict)
1954
+ )
1955
+ return per_stage or len(rows)
1956
+
1957
+
1908
1958
  def _validate_plan_item_extraction_completeness(
1909
1959
  data: dict,
1910
1960
  failures: list[str],
@@ -1932,7 +1982,7 @@ def _validate_plan_item_extraction_completeness(
1932
1982
  if isinstance(it, dict)
1933
1983
  ]
1934
1984
  for field, prefix, label in _PLAN_ITEM_SOURCES:
1935
- source_count = len([r for r in (ip.get(field) or []) if isinstance(r, dict)])
1985
+ source_count = _plan_item_source_count(ip, field)
1936
1986
  if source_count == 0:
1937
1987
  continue
1938
1988
  extracted = len(
@@ -2465,6 +2515,55 @@ def _validate_self_fix_before_clarification(data: dict, failures: list[str]) ->
2465
2515
  )
2466
2516
 
2467
2517
 
2518
+ CRITIC_UNVERIFIED_SOURCE = "critic-unverified"
2519
+
2520
+
2521
+ def _validate_unverified_critic_gaps_recorded(data: dict, failures: list[str]) -> None:
2522
+ """A coverage gap nobody judged must survive as a Missing Information row.
2523
+
2524
+ Gap verification drops `contested` / `worker-unique` gaps as hallucinations,
2525
+ which is sound *because voters looked at them*. A gap left unjudged — the
2526
+ verification dispatch timed out, covered only part of the batch, or no
2527
+ eligible analyser voted — has no verdict at all, and treating it as rejected
2528
+ invents one. The failure mode is adversarial: the batch that times out is
2529
+ the batch of gaps too expensive to check, so the highest-risk items are the
2530
+ ones that would silently disappear (prompts/lead/convergence.md
2531
+ "Gap verification").
2532
+ """
2533
+ cross = data.get("crossVerification")
2534
+ if not isinstance(cross, dict):
2535
+ return
2536
+ critic = cross.get("criticGaps")
2537
+ if not isinstance(critic, dict):
2538
+ return
2539
+ unverified = critic.get("unverified")
2540
+ if not isinstance(unverified, int) or unverified <= 0:
2541
+ return
2542
+ recorded = [
2543
+ row for row in (data.get("missingInformation") or [])
2544
+ if isinstance(row, dict) and row.get("source") == CRITIC_UNVERIFIED_SOURCE
2545
+ ]
2546
+ if len(recorded) < unverified:
2547
+ failures.append(
2548
+ "final-report data.json: crossVerification.criticGaps.unverified is "
2549
+ f"{unverified} but only {len(recorded)} missingInformation row(s) carry "
2550
+ f"`source: \"{CRITIC_UNVERIFIED_SOURCE}\"`. A gap the voters never "
2551
+ "judged MUST be recorded as a Missing Information row naming the gap "
2552
+ "and why verification did not complete — dropping it fabricates a "
2553
+ "rejection (prompts/lead/convergence.md \"Gap verification\")."
2554
+ )
2555
+
2556
+ proposed = critic.get("proposed")
2557
+ merged = critic.get("merged")
2558
+ if isinstance(proposed, int) and isinstance(merged, int):
2559
+ if merged + unverified > proposed:
2560
+ failures.append(
2561
+ "final-report data.json: crossVerification.criticGaps has "
2562
+ f"merged({merged}) + unverified({unverified}) > proposed({proposed}) "
2563
+ "— the accounting does not describe a real set of gaps."
2564
+ )
2565
+
2566
+
2468
2567
  # Allowed `fixability` values, mirroring the schema enum
2469
2568
  # (schemas/final-report-v1.0.schema.json planItems[].verdicts[].fixability).
2470
2569
  _FIXABILITY_VALUES = frozenset({"planner-fixable", "needs-user-input"})
@@ -2990,6 +3089,65 @@ def _validate_forbidden_actions(
2990
3089
  failures.extend(f"forbidden-action: {v}" for v in violations)
2991
3090
 
2992
3091
 
3092
+ CLASSIFICATION_COUNT_KEYS = {
3093
+ "full-consensus": "fullConsensus",
3094
+ "partial-consensus": "partialConsensus",
3095
+ "contested": "contested",
3096
+ "worker-unique": "workerUnique",
3097
+ }
3098
+
3099
+
3100
+ def classification_counts_from_findings(findings) -> dict:
3101
+ """Recompute `finalClassificationCounts` from `findings[].classification`.
3102
+
3103
+ Shared with `tests/contract/test_convergence_state_contract.py` so the
3104
+ invariant has one definition — it used to live only in that test, which
3105
+ meant it was checked against four hand-written fixtures and never against
3106
+ the state a lead actually writes.
3107
+ """
3108
+ counts = {value: 0 for value in CLASSIFICATION_COUNT_KEYS.values()}
3109
+ for finding in findings or []:
3110
+ if not isinstance(finding, dict):
3111
+ continue
3112
+ key = CLASSIFICATION_COUNT_KEYS.get(finding.get("classification"))
3113
+ if key:
3114
+ counts[key] += 1
3115
+ return counts
3116
+
3117
+
3118
+ def _validate_convergence_state_counts(run_dir, failures) -> None:
3119
+ """A run's convergence state must agree with its own findings list.
3120
+
3121
+ `finalClassificationCounts` is a lead-authored summary of
3122
+ `findings[].classification`. Nothing recomputed it against the real
3123
+ artifact, so a miscount survived into the report as two different truths
3124
+ (observed on dev-6901 planning 009: declared partial 6 / worker-unique 4
3125
+ against an actual 5 / 5). Downstream readers pick one arbitrarily.
3126
+ """
3127
+ from pathlib import Path as _Path
3128
+
3129
+ state_dir = _Path(run_dir) / "state"
3130
+ if not state_dir.is_dir():
3131
+ return
3132
+ for state_path in sorted(state_dir.glob("convergence-*.json")):
3133
+ try:
3134
+ state = json.loads(state_path.read_text(encoding="utf-8"))
3135
+ except (OSError, json.JSONDecodeError) as exc:
3136
+ failures.append(f"convergence state {state_path.name} unreadable: {exc}")
3137
+ continue
3138
+ declared = state.get("finalClassificationCounts")
3139
+ if not isinstance(declared, dict):
3140
+ continue
3141
+ actual = classification_counts_from_findings(state.get("findings"))
3142
+ if declared != actual:
3143
+ failures.append(
3144
+ f"convergence state {state_path.name}: finalClassificationCounts "
3145
+ f"{declared} does not match findings[].classification {actual}. "
3146
+ "The counts are a summary of the findings list — a mismatch means "
3147
+ "the run reports two different classifications for the same work."
3148
+ )
3149
+
3150
+
2993
3151
  def _validate_requirements_discovery_fanout(run_dir, failures) -> None:
2994
3152
  """requirements-discovery run 에 fan-out/ 이 있으면 packet+index 를 검증해
2995
3153
  실패를 ``requirements-discovery: `` 접두로 folding 한다. fan-out 이 없으면 no-op.
@@ -3427,6 +3585,8 @@ def main() -> int:
3427
3585
  if task_type == "requirements-discovery":
3428
3586
  run_dir = report_path.parent.parent
3429
3587
  _validate_requirements_discovery_fanout(run_dir, failures)
3588
+ # Phase-agnostic: convergence runs in every finding-producing phase.
3589
+ _validate_convergence_state_counts(report_path.parent.parent, failures)
3430
3590
  validate_report_views(report_path, failures)
3431
3591
 
3432
3592
  validation_status = "passed" if not failures else "failed"
@@ -115,8 +115,8 @@ CALENDAR_DATE_RE = re.compile(
115
115
  )
116
116
 
117
117
  DAYS_TOTAL_RE = re.compile(
118
- r"\*\*총\s*\d+개\s*task\s*/\s*예상\s*소요:\s*"
119
- r"\d+(?:\.\d+)?\s*~\s*\d+(?:\.\d+)?\s*days?\s*\(Effort\s*합산\)\*\*"
118
+ r"\*\*\d+\s*tasks\s*total\s*/\s*estimated\s*effort:\s*"
119
+ r"\d+(?:\.\d+)?\s*~\s*\d+(?:\.\d+)?\s*days?\s*\(Effort\s*sum\)\*\*"
120
120
  )
121
121
 
122
122
  # Opaque cross-document identifiers from internal report formats.
@@ -288,7 +288,7 @@ def validate(path: Path) -> list[str]:
288
288
  "report instead"
289
289
  )
290
290
 
291
- # 10. Days total format inside At a Glance: `**총 N task / 예상 소요: X.X ~ Y.Y days (Effort 합산)**`
291
+ # 10. Days total format inside At a Glance: `**N tasks total / estimated effort: X.X ~ Y.Y days (Effort sum)**`
292
292
  if "## At a Glance" in section_positions:
293
293
  start = section_positions["## At a Glance"]
294
294
  rest = "\n".join(lines[start + 1:])
@@ -297,7 +297,7 @@ def validate(path: Path) -> list[str]:
297
297
  if not DAYS_TOTAL_RE.search(body):
298
298
  violations.append(
299
299
  "At a Glance must contain a totals line matching "
300
- "`**총 N task / 예상 소요: X.X ~ Y.Y days (Effort 합산)**`"
300
+ "`**N tasks total / estimated effort: X.X ~ Y.Y days (Effort sum)**`"
301
301
  )
302
302
 
303
303
  # 11. At a Glance row enum check (Effort / Priority / Risk / Phase)
@@ -375,7 +375,7 @@ def validate(path: Path) -> list[str]:
375
375
  next_h = re.search(r"^##\s", rest, re.MULTILINE)
376
376
  body = (rest[: next_h.start()] if next_h else rest).strip()
377
377
 
378
- no_data_marker = "_의존 정보 없음_"
378
+ no_data_marker = "_none_"
379
379
  fence_match = re.search(r"^```([^\n]*)\n(.*?)^```\s*$", body, re.MULTILINE | re.DOTALL)
380
380
 
381
381
  if not body:
@@ -466,7 +466,7 @@ def validate(path: Path) -> list[str]:
466
466
  f"per-task block {heading_line!r}: sub-section {label!r} "
467
467
  "appears out of required order "
468
468
  "(Problem → Solution → Work Breakdown → Verification Commands "
469
- "→ Rollback → 사용자 확인 필요 항목)"
469
+ "→ Rollback)"
470
470
  )
471
471
  break
472
472
  last_pos = pos