okstra 0.146.1 → 0.147.0

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 (64) hide show
  1. package/README.md +2 -2
  2. package/docs/architecture/storage-model.md +8 -7
  3. package/docs/architecture.md +17 -11
  4. package/docs/cli.md +2 -2
  5. package/docs/project-structure-overview.md +16 -14
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/agents/workers/report-writer-worker.md +10 -10
  9. package/runtime/bin/okstra-render-final-report.py +4 -4
  10. package/runtime/bin/okstra-render-report-views.py +100 -12
  11. package/runtime/prompts/lead/okstra-lead-contract.md +4 -4
  12. package/runtime/prompts/lead/report-writer.md +15 -11
  13. package/runtime/prompts/profiles/_common-contract.md +13 -7
  14. package/runtime/prompts/profiles/improvement-discovery.md +3 -1
  15. package/runtime/python/okstra_ctl/final_report_schema.py +37 -12
  16. package/runtime/python/okstra_ctl/render_final_report.py +136 -28
  17. package/runtime/python/okstra_ctl/report_contract.py +124 -0
  18. package/runtime/python/okstra_ctl/report_finalize.py +1 -1
  19. package/runtime/python/okstra_ctl/report_html/__init__.py +10 -0
  20. package/runtime/python/okstra_ctl/report_html/common.py +50 -0
  21. package/runtime/python/okstra_ctl/report_html/models.py +59 -0
  22. package/runtime/python/okstra_ctl/report_html/render.py +69 -0
  23. package/runtime/python/okstra_ctl/report_html/router.py +40 -0
  24. package/runtime/python/okstra_ctl/report_html/view_models/__init__.py +1 -0
  25. package/runtime/python/okstra_ctl/report_html/view_models/change_impact_analysis.py +38 -0
  26. package/runtime/python/okstra_ctl/report_html/view_models/error_analysis.py +48 -0
  27. package/runtime/python/okstra_ctl/report_html/view_models/feature_analysis.py +38 -0
  28. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +46 -0
  29. package/runtime/python/okstra_ctl/report_html/view_models/implementation.py +46 -0
  30. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +102 -0
  31. package/runtime/python/okstra_ctl/report_html/view_models/improvement_discovery.py +42 -0
  32. package/runtime/python/okstra_ctl/report_html/view_models/project_analysis.py +54 -0
  33. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +53 -0
  34. package/runtime/python/okstra_ctl/report_html/view_models/requirements_discovery.py +54 -0
  35. package/runtime/python/okstra_ctl/report_html/visualizations.py +113 -0
  36. package/runtime/python/okstra_ctl/report_view_artifacts.py +4 -1
  37. package/runtime/python/okstra_ctl/report_views.py +15 -43
  38. package/runtime/python/okstra_ctl/run.py +12 -6
  39. package/runtime/python/okstra_ctl/schema_excerpt.py +7 -17
  40. package/runtime/schemas/final-report-v2.0.schema.json +3923 -0
  41. package/runtime/templates/reports/final-report-v2.template.md +66 -0
  42. package/runtime/templates/reports/html/assets/base.css +38 -0
  43. package/runtime/templates/reports/html/assets/base.js +5 -0
  44. package/runtime/templates/reports/html/base.template.html +65 -0
  45. package/runtime/templates/reports/html/macros/forms.html +47 -0
  46. package/runtime/templates/reports/html/macros/layout.html +19 -0
  47. package/runtime/templates/reports/html/macros/visualizations.html +17 -0
  48. package/runtime/templates/reports/html/tasks/change-impact-analysis.template.html +40 -0
  49. package/runtime/templates/reports/html/tasks/error-analysis.template.html +40 -0
  50. package/runtime/templates/reports/html/tasks/feature-analysis.template.html +40 -0
  51. package/runtime/templates/reports/html/tasks/final-verification.template.html +39 -0
  52. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +47 -0
  53. package/runtime/templates/reports/html/tasks/implementation.template.html +40 -0
  54. package/runtime/templates/reports/html/tasks/improvement-discovery.template.html +29 -0
  55. package/runtime/templates/reports/html/tasks/project-analysis.template.html +57 -0
  56. package/runtime/templates/reports/html/tasks/release-handoff.template.html +36 -0
  57. package/runtime/templates/reports/html/tasks/requirements-discovery.template.html +37 -0
  58. package/runtime/validators/validate-report-views.py +86 -4
  59. package/runtime/validators/validate-run.py +62 -9
  60. package/runtime/validators/validate_improvement_report.py +55 -0
  61. package/src/commands/lifecycle/install.mjs +18 -13
  62. package/src/commands/report/finalize.mjs +2 -3
  63. package/src/commands/report/render-final-report.mjs +4 -2
  64. package/src/commands/report/render-views.mjs +8 -8
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env python3
2
- """CLI entrypoint for Phase 7 step 1.5 — render the self-contained HTML
3
- view of an okstra final-report markdown.
2
+ """CLI entrypoint for Phase 7 step 1.5 — render a final-report HTML view.
4
3
 
5
4
  Usage:
6
- okstra-render-report-views.py <path-to-final-report.md>
5
+ okstra-render-report-views.py <path-to-final-report.data.json|final-report.md>
7
6
  [--task-key <task-group/task-id>]
8
7
  [--task-type <profile>]
9
8
  [--seq <NNN>]
10
9
  [--source-report <relative-path>]
11
10
 
12
- When the optional flags are omitted, the script infers what it can from
13
- the report path (``runs/<task-type>/reports/final-report-<task-type>-<seq>.md``)
14
- and the report's frontmatter / ``- Task Key:`` / ``- Task Type:`` lines.
11
+ Schema-v2 ``.data.json`` input is rendered directly into the dedicated task
12
+ HTML template and always produces an HTML sibling. Markdown input resolves its
13
+ data sibling first; schema-v1 and quick reports keep the legacy conditional
14
+ Markdown renderer.
15
15
 
16
16
  Output (idempotent — overwrites):
17
17
  - <stem>.html — single-file self-contained HTML view
@@ -22,13 +22,19 @@ This script is the canonical single-reference-point. The Node CLI
22
22
  from __future__ import annotations
23
23
 
24
24
  import argparse
25
+ import json
25
26
  import os
27
+ import re
26
28
  import sys
27
29
  from pathlib import Path
28
30
 
29
31
  REPO_ROOT = Path(__file__).resolve().parents[1]
30
32
  SCRIPTS_DIR = REPO_ROOT / "scripts"
31
- HOME_LIB = Path(os.environ.get("OKSTRA_HOME", str(Path.home() / ".okstra"))) / "lib" / "python"
33
+ HOME_LIB = (
34
+ Path(os.environ.get("OKSTRA_HOME", str(Path.home() / ".okstra")))
35
+ / "lib"
36
+ / "python"
37
+ )
32
38
 
33
39
  # Prefer dev sources when present, fall back to install. Without this
34
40
  # order, a stale install (~/.okstra/lib/python/okstra_ctl) without the
@@ -43,6 +49,10 @@ elif HOME_LIB.is_dir() and str(HOME_LIB) not in sys.path:
43
49
  sys.path.insert(0, str(HOME_LIB))
44
50
 
45
51
  from okstra_ctl.report_views import infer_run_meta, render_html_view # noqa: E402
52
+ from okstra_ctl.final_report_paths import ( # noqa: E402
53
+ final_report_data_path,
54
+ final_report_markdown_path,
55
+ )
46
56
 
47
57
 
48
58
  _OKSTRA_HOME = Path(os.environ.get("OKSTRA_HOME", str(Path.home() / ".okstra")))
@@ -77,6 +87,60 @@ def _load_assets() -> tuple[str, str]:
77
87
  return css_text, js_text
78
88
 
79
89
 
90
+ def _templates_root() -> Path:
91
+ for directory in _TEMPLATES_DIRS:
92
+ if (directory / "html" / "base.template.html").is_file():
93
+ return directory
94
+ raise SystemExit(
95
+ "schema-v2 HTML templates not found. Looked under: "
96
+ + ", ".join(str(directory) for directory in _TEMPLATES_DIRS)
97
+ )
98
+
99
+
100
+ def _report_pair(report_path: Path) -> tuple[Path, Path]:
101
+ if report_path.name.endswith(".data.json"):
102
+ return report_path, final_report_markdown_path(report_path)
103
+ return final_report_data_path(report_path), report_path
104
+
105
+
106
+ def _load_data_if_present(data_path: Path) -> dict | None:
107
+ if not data_path.is_file():
108
+ return None
109
+ try:
110
+ data = json.loads(data_path.read_text(encoding="utf-8"))
111
+ except json.JSONDecodeError as exc:
112
+ raise SystemExit(
113
+ f"final-report data is not valid JSON: {data_path} ({exc})"
114
+ ) from exc
115
+ if not isinstance(data, dict):
116
+ raise SystemExit(f"final-report data must be a JSON object: {data_path}")
117
+ return data
118
+
119
+
120
+ def _v2_run_meta(data: dict, markdown_path: Path, args: argparse.Namespace):
121
+ from okstra_ctl.report_html.models import HtmlRunMeta
122
+
123
+ header = data.get("header", {})
124
+ task_key = args.task_key or header.get("taskKey")
125
+ task_type = args.task_type or header.get("taskType")
126
+ match = re.search(r"-(\d+)\.md$", markdown_path.name)
127
+ seq = (
128
+ args.seq
129
+ or data.get("analysisCommon", {}).get("runSeq")
130
+ or (match.group(1) if match else None)
131
+ )
132
+ if not all(
133
+ isinstance(value, str) and value for value in (task_key, task_type, seq)
134
+ ):
135
+ raise SystemExit("schema-v2 report metadata requires task-key, task-type, and seq")
136
+ return HtmlRunMeta(
137
+ task_key,
138
+ task_type,
139
+ seq,
140
+ args.source_report or markdown_path.name,
141
+ )
142
+
143
+
80
144
  def main(argv: list[str] | None = None) -> int:
81
145
  parser = argparse.ArgumentParser(
82
146
  description="Render the self-contained HTML view of an okstra final-report."
@@ -88,18 +152,42 @@ def main(argv: list[str] | None = None) -> int:
88
152
  parser.add_argument("--source-report", default=None)
89
153
  args = parser.parse_args(argv)
90
154
 
91
- report_path: Path = args.report_path.resolve()
155
+ report_path = args.report_path.resolve()
92
156
  if not report_path.is_file():
93
157
  parser.error(f"final-report not found: {report_path}")
158
+ data_path, markdown_path = _report_pair(report_path)
159
+ data = _load_data_if_present(data_path)
160
+
161
+ if data is not None and data.get("schemaVersion") == "2.0":
162
+ if not markdown_path.is_file():
163
+ parser.error(f"schema-v2 markdown sibling not found: {markdown_path}")
164
+ from okstra_ctl.report_html.render import render_v2_html_view
165
+
166
+ html_path = render_v2_html_view(
167
+ data_path,
168
+ markdown_path,
169
+ run_meta=_v2_run_meta(data, markdown_path, args),
170
+ templates_root=_templates_root(),
171
+ )
172
+ print(f"html: {html_path}")
173
+ return 0
94
174
 
175
+ if not markdown_path.is_file():
176
+ parser.error(f"legacy markdown sibling not found: {markdown_path}")
95
177
  meta = infer_run_meta(
96
- report_path, task_key=args.task_key, task_type=args.task_type,
97
- seq=args.seq, source_report=args.source_report,
178
+ markdown_path,
179
+ task_key=args.task_key,
180
+ task_type=args.task_type,
181
+ seq=args.seq,
182
+ source_report=args.source_report,
98
183
  )
99
184
  css, js = _load_assets()
100
- html_path = render_html_view(report_path, run_meta=meta, css=css, js=js)
185
+ html_path = render_html_view(markdown_path, run_meta=meta, css=css, js=js)
101
186
  if html_path is None:
102
- print("html: skipped (no §1 clarification rows — html view carries no interactive forms for this report)")
187
+ print(
188
+ "html: skipped (no §1 clarification rows — html view carries no "
189
+ "interactive forms for this report)"
190
+ )
103
191
  else:
104
192
  print(f"html: {html_path}")
105
193
  return 0
@@ -202,7 +202,7 @@ If previous run reports exist, use as historical context only. If discovery meta
202
202
 
203
203
  For `project-analysis`, `feature-analysis`, and `change-impact-analysis`, Lead MUST use the run manifest's immutable pre-dispatch `analysisScopeConfirmation` snapshot as the structured reporter-confirmation evidence before Phase 4 worker dispatch. Its `status` MUST be `complete`; its `taskBriefPath` and `briefSha256` bind that status to the exact brief bytes captured when the run manifest was created. A later edit to the live brief, clarification prose, or inferred consent cannot substitute for this snapshot. `project-analysis` confirms which areas remain shallow; `feature-analysis` confirms the exact feature target and covered flows; `change-impact-analysis` confirms the proposed change, preserved behavior, and dependency boundary.
204
204
 
205
- If that snapshot is incomplete, missing, or malformed, Lead MUST follow the shared Reporter Confirmation Required / Clarification Items contract, stop before dispatch, and publish a `blocked` report. It MUST NOT silently widen the target. **Enforcement:** `scripts/okstra_ctl/render.py` records the brief path, reporter-confirmation status, and brief byte digest in the run manifest before the lead can dispatch workers; `validators/validate_analysis_report.py` validates only that immutable snapshot, rejects required-worker execution before a complete snapshot, and recomputes the analysis verdict; `validators/validate-run.py` runs that check only after schema validation succeeds.
205
+ If that snapshot is incomplete, missing, or malformed, Lead MUST follow the shared Reporter Confirmation Required / Clarification Items contract and stop before dispatch. A scope reduction must be explicitly confirmed in the brief's `## Reporter Confirmations` before a fresh bundle records `analysisScopeConfirmation.status=complete`. Do not dispatch workers and then defer a foreseeable scope conflict to a final HTML question. **Enforcement:** `scripts/okstra_ctl/render.py` records the brief path, reporter-confirmation status, and brief byte digest in the run manifest before the lead can dispatch workers; `validators/validate_analysis_report.py` validates only that immutable snapshot, rejects required-worker execution before a complete snapshot, and recomputes the analysis verdict; `validators/validate-run.py` runs that check only after schema validation succeeds.
206
206
 
207
207
  ## Phase 2 — Phase 5: Prompt preparation, teammate setup, execution, completion poll
208
208
 
@@ -308,7 +308,7 @@ If convergence is disabled, `seed`/`finalize` produce the auto-disabled final st
308
308
 
309
309
  ### Authoring ownership (BLOCKING)
310
310
 
311
- If `Report writer worker` is in the selected roster (`recommendedWorkers` / `resultContract.requiredWorkerRoles`), **Lead MUST dispatch it to author the final report**. The worker writes the JSON SSOT at `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json` and invokes `scripts/okstra-render-final-report.py` to produce the sibling `final-report-<task-type>-<seq>.md` Lead writes neither file. Lead's role in this phase is: prepare the report-writer prompt (carrying convergence output, all worker results, and reference expectations), dispatch, then review the produced files. See [report-writer](./report-writer.md) "File-author ownership".
311
+ If `Report writer worker` is in the selected roster (`recommendedWorkers` / `resultContract.requiredWorkerRoles`), **Lead MUST dispatch it to author the final report data.json**. The worker writes the JSON SSOT at `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json` and invokes `scripts/okstra-render-final-report.py` to produce the sibling AI handoff Markdown. Phase 7 renders human HTML directly from the same data.json. Lead writes none of these files; it prepares the prompt, dispatches, and reviews both audience artifacts. See [report-writer](./report-writer.md) "File-author ownership".
312
312
 
313
313
  Before constructing the dispatch prompt, the lead MUST:
314
314
 
@@ -358,8 +358,8 @@ Lead's responsibilities in this sub-step (in order):
358
358
  2. Dispatch a single plan-body reverify round to every analyser worker in the roster (`claude`, `codex`, and `antigravity` when opted in). `Report writer worker` is NOT a participant in this round.
359
359
  3. Aggregate verdicts and resolve the gate result to one of `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result`.
360
360
  4. Write `runs/<task-type>/state/plan-body-verification.json` (schema in the plan-body-verification contract), appending one `roundHistory[]` entry per round — including every self-fix re-verification round, since data.json keeps only the final verdicts.
361
- 5. Populate `### 5.5.9 Plan Body Verification` in the final-report file (template at `templates/reports/final-report.template.md` §5.5.9 Round count, Gate result, per-item verdict tables grouped under each plan item's `subject`, Dissent log).
362
- 6. For every `majority-disagree` plan item, append a row to `## 1. Clarification Items` with `Blocks=approval` and the 1:1 ID match in the verdict table's `Classification` column (`majority-disagree → C-<N>`). Do NOT create a parallel `Open Questions` block — see `prompts/profiles/implementation-planning.md` self-review step 6 for the orphan-on-either-side contract.
361
+ 5. Populate `implementationPlanning.planBodyVerification` in data.json with round count, gate result, per-item verdicts, and dissent log. The AI handoff task-deliverable block carries this structure without a second prose rendering.
362
+ 6. For every `majority-disagree` plan item, append one `clarificationItems[]` row with `blocks=approval` and the 1:1 ID match in the verdict classification (`majority-disagree → C-<N>`). Do not create a parallel open-questions structure.
363
363
  7. Publish the YAML frontmatter `approved:` field as `false`. There is no in-body `- [ ] Approved` marker line — approval lives only in the frontmatter (see [plan-body-verification](./plan-body-verification.md) §"Round protocol" step 9). The user may flip it to `true` only when the gate is `passed` or `passed-with-dissent`. **Enforced:** `validators/validate-run.py` `validate_phase_boundary` fails a report shipping `approved: true` under `blocked-by-disagreement` / `aborted-non-result`, and run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan`) fail-closes the same case. Manually flipping a blocked gate to passing is a contract violation.
364
364
 
365
365
  If `convergence.planBodyVerification.enabled == false` (set by `--no-plan-verification` or by `okstra config set plan-verification off`), the entire sub-step is skipped and the top-of-report Approval marker is rendered unconditionally (legacy behaviour). This opt-out is intended for fast iteration only and is not recommended for handoff-ready plans.
@@ -4,9 +4,9 @@
4
4
 
5
5
  The final-report data.json is authored by `Report writer worker` when that role is in the roster. The lead reviews both rendered artifacts but does not write them. Lead-authored fallback is legal only after a real `dispatch_worker` attempt records `error`, `timeout`, or `not-run` with a concrete reason. `release-handoff` remains the intentional single-lead exception.
6
6
 
7
- The JSON SSOT path is `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json`. The user-facing markdown at `runs/<task-type>/reports/final-report-<task-type>-<seq>.md` is produced by `scripts/okstra-render-final-report.py` from the data.json. The worker-result pointer at `**Worker Result Path:**` records those two paths and the reconciled convergence input. These three completion artifacts land on disk before the worker returns; the heartbeat audit sidecar remains a separate required audit artifact.
7
+ The JSON SSOT path is `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json`. Two audience artifacts are independently derived from the data.json: `scripts/okstra-render-final-report.py` produces the AI handoff Markdown sibling, and Phase 7 produces the task-specific human HTML sibling directly from data.json. The worker-result pointer at `**Worker Result Path:**` records the data and Markdown paths plus reconciled convergence input. These three completion artifacts land on disk before the worker returns; HTML follows during finalization and the heartbeat sidecar remains a separate audit artifact.
8
8
 
9
- The data.json schema is `schemas/final-report-v1.0.schema.json`. The renderer + the run-validator both consume that schema, so a data.json that validates is guaranteed to render into a markdown that passes the contract checks.
9
+ New bundles use `schemas/final-report-v2.0.schema.json`. The Markdown keeps verdict, routing, evidence, one structured task deliverable, and audit data for the next agent. The HTML uses `humanSummary`, task `userNarrative`, and structured facts for the user. Raw worker discussion, convergence mechanics, and usage belong to audit structures and never to the HTML human main body.
10
10
 
11
11
  Two `frontmatter` approval fields are always emitted with their unset default — never pre-fill them: `frontmatter.approved` is emitted as `false`, and `frontmatter.implementationOption` is emitted as an empty string `""`. The user later flips `approved` to `true` (via `--approve` or manual edit) and fills `implementationOption` with the chosen Option Candidate name (via `--implementation-option <name>` or manual edit) to authorise and scope the next `implementation` run.
12
12
 
@@ -46,8 +46,8 @@ The prompt MUST include, in this order at the top:
46
46
  8. `**Prompt Delivery Mode:** <mode>` — the selected adapter's declared `initialPromptDeliveryMode`.
47
47
  9. `**Model:** Report writer worker, <modelExecutionValue>` (resolved per Phase 5.5 anchor-header rules)
48
48
  10. The full `[Required reading]` clause (see [team-contract](./team-contract.md)) — for Phase 6 it adds two **per-task-type, instruction-set-local** read-only files, both scoped to this run's task-type by `okstra-ctl` at prep time:
49
- - `<instruction-set>/final-report-schema.json` — a task-type excerpt of the data.json schema (the other task-types' deliverable blocks and their unreachable `$defs` are stripped; ~38% of the full schema is `$defs` alone). This is your authoring aid for the data.json shape the installed schema, not the excerpt, is what the run is judged against. Do **NOT** pull the full `schemas/final-report-v1.0.schema.json` it carries all task-types and its `schemas/...` path is not part of the task bundle. (Validation still runs against the full schema post-hoc via the renderer, so the excerpt never relaxes the contract.)
50
- - `<instruction-set>/final-report-template.md` — the **phase-stripped** template (every other task-type's §5.x deliverable block removed by `render.py`'s `_strip_phase_blocks`, leaving only your run's §5.x). Do **NOT** also pull the full `templates/reports/final-report.template.md` source (it re-adds ~330 lines of other phases' deliverables and is not in the task bundle).
49
+ - `<instruction-set>/final-report-schema.json` — a task-type excerpt of schema v2. This is the binding authoring shape; the installed full schema is what the run is judged against. Do **NOT** pull the full repository schema because it is outside the task bundle.
50
+ - `<instruction-set>/final-report-template.md` — the AI handoff Markdown template. It shows the agent-facing ledger shape, not the human presentation. The task-specific HTML renderer reads data.json separately.
51
51
  11. A one-line MCP pointer instead of the verbatim block (redundant — the brief is already in the report-writer's Required reading, item 10): `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).`
52
52
  12. `Convergence state: runs/<task-type>/state/convergence-<task-type>-<seq>.json`, followed by pointers to all analysis-worker result files under `worker-results/`. The convergence path is deterministic and is listed even before Phase 5.5 creates the file. Read its classifications (Full/Partial/Contested/Worker-Unique), `roundHistory[]`, `round2SkippedReason`, and `finalClassificationCounts`; populate `crossVerification.roundHistory` in data.json so Section 6 can show which rounds executed, queue sizes, and why Round 2 was (or was not) skipped. The renderer prints the full per-round table only when more than one round ran; single-round or zero-round histories are auto-collapsed to a one-line summary.
53
53
  13. `**Report Language:** <en|ko>` — must be either `en` or `ko`; `auto`
@@ -56,7 +56,7 @@ The prompt MUST include, in this order at the top:
56
56
  into `data.json.meta.reportLanguage`.
57
57
  14. 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.
58
58
  15. An explicit instruction: `You are the author of THREE files: (a) the final-report data.json at <Result Path>, (b) its rendered Markdown sibling produced through "okstra render-final-report <Result Path>", and (c) the worker-result pointer at <Worker Result Path>. Maintain the separate heartbeat audit sidecar at <Audit sidecar path>. Do not return the report inline. The dispatch fails when any of the three completion artifacts is missing, and session conformance fails when the audit sidecar is missing or invalid.`
59
- 16. 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.
59
+ 16. The prose budget (dedup contract): `verdictCard.finalConclusion` is the conclusion SSOT — at most 3 sentences. `rationale.*` fields stay within 2 sentences each; `humanSummary` entries stay concise; task `userNarrative` explains each user-facing section once with evidence references. Do not copy these narratives into the AI Markdown. `summary` stays at 3-5 rows unless the run covers multiple tickets. Generation time scales with output volume, so exceeding the budget is a cost bug, not extra diligence.
60
60
 
61
61
  **Fix-run incremental authoring (applies when the run's profile carries a "Fix-Run Carry" block).** Do not author the data.json from scratch. Start by copying the previous run's data.json (the `Previous report` path in the Fix-Run Carry block) to this run's Result Path, then update ONLY the blocks the fix run changed: `meta`/`header` (run seq, dates), `executionStatus`, `implementation.verifierResults`, `implementation.validationEvidence`, `implementation.commitList` / `diffSummary`, `crossVerification`, `verdictCard`, `finalVerdict`, and any `evidence` rows the fix touched. Deliverable prose for unchanged sections is carried forward verbatim — do not re-generate it. Then invoke the renderer exactly as in a full run. The schema validation and renderer contract are unchanged, so an incrementally-authored data.json passes the same post-hoc gates. The lead's dispatch prompt MUST include the previous data.json path when the carry block is present.
62
62
 
@@ -81,7 +81,7 @@ Speculative reasons such as "session resume constraint", "runtime state is unava
81
81
 
82
82
  Phase 6 first produces the final-report data.json at `runs/<task-type>/reports/final-report-<task-type>-<seq>.data.json` and its rendered markdown sibling. Token Usage cells are `null` at this point, and Section 3 does not yet include auto-spawned follow-ups.
83
83
 
84
- For an implementation-planning run, the Report writer worker owns the Phase 6 design assessment snapshot: it writes `designPreparation` and every stage's `designSurfaceCoverage` into data.json from the detector output and consolidated plan. It does not create user inputs, consume a user answer as if it were part of that snapshot, or materialize `design-prep-requests/`; `schemas/final-report-v1.0.schema.json` and `validators/validate-run.py` `_validate_design_prep_contract` enforce the snapshot shape, detector coverage, and references.
84
+ For an implementation-planning run, the Report writer worker owns the Phase 6 design assessment snapshot: it writes `designPreparation` and every stage's `designSurfaceCoverage` into data.json from the detector output and consolidated plan. It does not create user inputs, consume a user answer as if it were part of that snapshot, or materialize `design-prep-requests/`; `schemas/final-report-v2.0.schema.json` and `validators/validate-run.py` `_validate_design_prep_contract` enforce the snapshot shape, detector coverage, and references.
85
85
 
86
86
  Phase 7 post-processing is **one command**. `okstra report-finalize` owns the ordered sequence — it is the same code path the Codex lead adapter runs automatically, so a Claude-led run and a Codex-led run finalize identically:
87
87
 
@@ -101,12 +101,12 @@ The steps it executes, in this contractual order, and the contract each one carr
101
101
  The data.json paths populated: `tokenUsage.lead.{totalTokens,billableTokens,costUsd}`, the `worker` / `grand` rows, `tokenUsage.cli.costUsd`, and each `executionStatus[].{totalTokens,billableTokens,costUsd,durationMs,cliTotalTokens,cliCostUsd}` for rows whose role matches a team-state worker. The data.json MUST already exist (Phase 6 output).
102
102
 
103
103
  For implementation-planning, this Phase 7 canonical render calls `materialize_design_prep_requests()` after token substitution and creates deterministic request files only for `provisional` / `blocked` items. Later answers are append-only user-input sidecars; request generation and user input never rewrite the assessment fields, so the source report remains immutable as the design-input snapshot after this render. `validators/validate-run.py` `_validate_design_prep_requests` enforces request existence, canonical path, content, and assessment fingerprint.
104
- 2. **`render-views` — render the human report artifact.** Runs against the substituted markdown; the renderer itself decides whether an html sibling is warranted.
104
+ 2. **`render-views` — render the human report artifact.** Runs against the substituted v2 data.json and its Markdown sibling.
105
105
 
106
106
  Output (idempotent — re-running overwrites):
107
- - `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view, **generated when the report has at least one §1 `C-*` clarification row OR an implementation-planning Plan Approval widget target** (a sibling `final-report-*.data.json` carrying `implementationPlanning.optionCandidates`). Clarification rows with `Status` ∈ {`open`, `answered`} embed form widgets (`<select>` for enum-style decisions, `<input>` for material / data-point kinds, `<textarea>` fallback); an `Export user response` button serialises form values to a markdown sidecar (schema in [`templates/reports/user-response.template.md`](../../templates/reports/user-response.template.md)) and downloads it as `user-response-<task-type>-<seq>.md`; the user saves it to `runs/<task-type>/user-responses/` (the renderer pre-creates that directory), and `--resume-clarification` auto-appends every sidecar found there to the next run's `clarification-response.md` (`clarification_items.clarification_response_with_sidecars`). The original final-report MD is **never** mutated by user input — the sidecar is the single write target.
107
+ - `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view, always generated for schema v2 from the dedicated template registered for that task type. Clarification rows with `Status` ∈ {`open`, `answered`} embed response controls and export a `user-response-<task-type>-<seq>.md` sidecar. The original data and Markdown artifacts are never mutated by user input.
108
108
  - the implementation-planning report renders a **Plan Approval** section at the end of the body (implementation-option `<select>` + an approval checkbox) — disabled while any §1 `Blocks: approval` row is unresolved. Checking approval and exporting embeds a `## APPROVAL` block in the sidecar body, and the implementation-start wizard's approve-confirm step detects it and, after user confirmation, applies it through the existing `--approve` / `--implementation-option` path.
109
- - When the report has **no** `C-*` clarification rows and is **not** a Plan Approval widget target, the html carries no interactive forms (it would only duplicate the MD), so the renderer prints `html: skipped (...)` and writes nothing. This is the expected state for such runs — `validators/validate-report-views.py` treats "no C-* rows + no approval target + no html" as a pass, not a missing artifact.
109
+ - Schema-v1 and quick compatibility reports retain the legacy conditional HTML path; this does not change the schema-v2 always-generated contract.
110
110
 
111
111
  It runs after usage collection so token placeholders are substituted in any rendered html, and before routing persistence so the html artifact, when generated, exists for the validator step that checks it.
112
112
  3. **`spawn-followups` — routing and follow-up persistence.** Turns the report's `## 4. Follow-up Tasks` rows into `tasks/<task-group>/<new-task-id>/` stubs.
@@ -128,9 +128,13 @@ The steps it executes, in this contractual order, and the contract each one carr
128
128
 
129
129
  After `okstra report-finalize` reports `"ok": true`, **execute the run-scoped cleanup gate.** Call `shutdown_workers` only after that success, all persistence work, and explicit user approval under [okstra-lead-contract](./okstra-lead-contract.md) "Run-scoped worker-resource lifecycle". If the user keeps resources, leave the selected adapter's resources intact and surface its manual cleanup guidance.
130
130
 
131
- ## Final Report Structure
131
+ ## Schema-v2 report data responsibilities
132
132
 
133
- The final report follows the structure encoded in `schemas/final-report-v1.0.schema.json` the single source of truth for section names, row shapes, enum values, and task-type-conditional blocks. The Jinja2 template `templates/reports/final-report.template.md` produces the human-readable form from any data.json that validates against the schema. The structure description below is a reading guide for writers; the schema is the binding contract.
133
+ The binding authoring shape is the task bundle's `instruction-set/final-report-schema.json`. Populate both audiences in data.json: agent-facing verdict, routing, evidence, task facts, and audits; user-facing `humanSummary` and task `userNarrative`. The AI handoff template deliberately omits the full user narrative, while the task-specific HTML deliberately moves `crossVerification`, `executionStatus`, and `tokenUsage` into collapsed audit details.
134
+
135
+ ## Legacy schema-v1 Markdown structure reference
136
+
137
+ The remaining numbered-section guide exists only for rendering or diagnosing historical schema-v1 data. New report-writer runs do not author against it; their instruction-set schema and AI handoff template are authoritative.
134
138
 
135
139
  ### Report Header
136
140
 
@@ -12,6 +12,11 @@ profile document.
12
12
  - **Phase 5.5 (convergence — peer review by workers)**: workers peer-review each other's findings across up to `effectiveMaxRounds` rounds; the lead mediates but does not vote. See `prompts/lead/convergence.md` for the round protocol (replay of findings, `AGREE` / `DISAGREE` / `SUPPLEMENT` verdicts), queue invariants, and final classification (`full-consensus` / `partial-consensus` / `contested` / `worker-unique`). For `requirements-discovery`, `error-analysis`, `implementation-planning`, `project-analysis`, `feature-analysis`, and `change-impact-analysis` this phase runs in **adversarial mode** (`convergence.adversarial=true`): verifiers try to refute each finding against its cited evidence and the burden of proof sits on the claim — see that skill's §"Adversarial Verification Mode".
13
13
  - Do NOT conclude "no peer review happens" from the roster alone — every profile that lists ≥2 analyser workers runs convergence by default (`convergence.enabled=true` in `task-manifest.json`).
14
14
  - **provider-unavailable fallback (tolerance).** A worker dispatch can fail to produce a result for two distinct reasons, and both take the same recovery path. (1) **Pane budget:** the dispatch is rejected with `no room for another tmux split` (or an equivalent teammate-pane creation failure). (2) **Sandbox CLI-start failure (non-tmux path):** an external CLI worker wrapper exits non-zero within seconds with empty stdout and its live-log shows `operation not permitted` (e.g. `agy`'s `listen tcp 127.0.0.1:0: bind` under a seatbelt sandbox around a non-tmux subagent's Bash tool). In either case the lead retries that worker in-process without `run_in_background`; if it was an external CLI worker, the lead instead **substitutes** an in-process `claude` analysis. Either way the lead records the substitution as provider unavailable in the run log and the convergence notes. Completed external-CLI worker trace panes are reclaimed automatically by the `SubagentStop` hook, but okstra cannot directly reclaim the teammate panes the harness creates, so this fallback is the last line of defence against pane-budget exhaustion and sandbox-blocked CLI workers. (This is a prompt instruction, not a code-enforced gate.)
15
+ - Dual-audience final-report contract (shared):
16
+ - data.json is the sole authored report artifact. AI handoff Markdown and human HTML are independently derived from it; neither derived artifact is the other's source.
17
+ - User-facing information belongs in `humanSummary` and the selected task block's `userNarrative`; it must not exist only in Markdown. The HTML human main body explains the result with those fields plus task facts.
18
+ - Agent coordination and audit details belong in `crossVerification`, `executionStatus`, and `tokenUsage`. HTML may expose them only inside collapsed audit details, never as the primary result.
19
+ - Clarification and approval controls are rendered from data.json IDs. A question that could have been resolved before dispatch through the profile or Reporter Confirmations is an intake failure, not a final-report question.
15
20
  - Tooling — read-only MCP availability (shared):
16
21
  - MCP is not implicit context; query a server only when the task brief explicitly lists it as source material for this run. Any MCP-derived finding MUST cite server, table, and the SELECT used. MCP MUST NEVER be a write path — schema/data mutations go through repository migration files reviewed by humans.
17
22
  - Resource boundary (shared — artifact-home rule):
@@ -54,7 +59,8 @@ profile document.
54
59
  - Any decision in this run that contradicts the brief's `Source Material` must be raised back to the reporter via a `Clarification Items` row; it must NOT be silently overridden. Disagreement with the reporter is allowed only after the row is resolved.
55
60
  - This contract is the single authority on brief consumption. Phase-specific addenda may *tighten* these rules but may not relax them.
56
61
  - Clarification request policy (shared — applies whenever a profile uses `## 1. Clarification Items`):
57
- - **Canonical column schema (SSOT must match `templates/reports/final-report.template.md` §1 exactly):** every `## 1. Clarification Items` table has exactly these 4 columns, in this order:
62
+ - Schema-v2 final reports author `clarificationItems[]` in data.json; task-specific HTML renders the question and response controls directly from those IDs, and AI handoff Markdown serializes the same array for the next agent. The remaining table-layout rules describe schema-v1 compatibility and analysis-worker result tables only.
63
+ - **Legacy canonical column schema (must match `templates/reports/final-report.template.md` §1 exactly):** every `## 1. Clarification Items` table has exactly these 4 columns, in this order:
58
64
  `| <record-meta> | Statement | Expected form | User input |` (the first header is the i18n `columns.recordMeta` label — `Record`).
59
65
  The five short fields (ID, Ticket ID, Kind, Blocks, Status) are stacked inside the single record-meta cell, one per line separated by `<br>`, in this fixed order (mirrors the §2.1 Primary-Evidence meta column):
60
66
 
@@ -64,19 +70,19 @@ profile document.
64
70
 
65
71
  The labels `Ticket:` / `Kind:` / `Blocks:` / `Status:` stay English in every locale so the approval-gate parser (`clarification_items.parse_meta_cell`) reads them regardless of report language.
66
72
  Profile-specific addenda may tighten cell content but MUST NOT add, remove, rename, or reorder columns, nor change the meta-cell field order. The `ID` is `C-NNN` (3-digit zero-padded), the `Status` ∈ `{open, answered, resolved, obsolete}`, and the `Kind` / `Blocks` legal values are listed below.
67
- - section 1 is a **single unified table** per `final-report-template.md`. Every clarification item — whether the user must attach a file, choose between options, or supply a single number/path — is one row of that table. Do not split it into sub-sections (`1.1 Additional Material Request` / `1.2 User Confirmation Question` / `5.5.9 Open Questions` are removed and the validator fails reports that reintroduce them), do not create a parallel table elsewhere in the report, and do not duplicate the same item into an approval block or any other section.
73
+ - In schema-v1 Markdown and worker-result tables, section 1 is a **single unified table** per `final-report-template.md`. Every clarification item is one row. Do not split it into sub-sections or create a parallel question table.
68
74
  - each row's `Kind` column picks one of `{material, decision, data-point}`: `material` for files / snapshots / logs / screenshots the user must attach (the `User input` cell will hold a path or URL); `decision` for choices and yes/no confirmations only the user can make; `data-point` for a single number, ID, date, or short string the user can answer inline. A `decision` alternative must be a terminal choice the user can pick as-is; if acting on an alternative still requires the user to supply a concrete value (a path, string, number, or file), that value is its own `data-point` / `material` row — never phrase a data-entry action (e.g. "specify the path", "enter a value") as a selectable `decision` option, because the rendered `<select>` cannot capture the value the option demands. Items that mix "yes/no + file path if yes" are one row of `Kind=material` with the combined expectation written into `Expected form`.
69
75
  - each row's `Blocks` column picks one of `{approval, next-phase, none}`. `approval` is reserved for items that gate an approval action, especially the `implementation-planning` `approved:` frontmatter flip; outside `implementation-planning`, unresolved brief reporter-confirmation rows use `next-phase` instead. `next-phase` blocks the next run from starting cleanly. `none` is informational/audit-only.
70
76
  - write every entry in full, descriptive sentences that a non-developer can act on without further context. Avoid abbreviations and internal jargon. The `Statement` cell must state *what* is needed, *why* the answer / attachment changes the next step, and (for `material`) *where* the user can find it and *where* to place it. The `Expected form` cell must state the answer shape (yes/no, one of the options, number/date, file path, short description, etc.); supply concrete option choices when applicable.
71
77
  - if a phase requires a recommended answer, alternatives, or an evidence-check note, encode it inside the existing 4-column schema: put evidence notes in `Statement` as `Evidence checked: <path:line>` or `Evidence checked: none — <human-only reason>`, and put recommendations/options in `Expected form` as `Recommended: (a) <answer> — <rationale>; Alternatives: (b) <option> (c) <option>`. The recommended answer is always the first option and MUST carry the `(a)` label; alternatives continue the same letter sequence from `(b)` (a lone alternative is `(b) <option>`, never restart at `(a)`), so the full option set reads `(a) (b) (c) …` in order and renders each as its own selectable option. Do **not** append a pick-one answer-space summary such as `(pick 1 of A / B)` or `(pick N of …)` to `<options>` — the rendered `<select>` already enforces single choice, and that annotation leaks verbatim into an option label. Do not add `Recommended`, `Evidence`, `Alternatives`, or `evidence-checked` columns, and do not break the merged record-meta cell back into separate columns.
72
- - 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.
73
- - 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.").
78
+ - For schema v2, data.json is canonical and the HTML exports answers to a user-response sidecar; the source report is never edited. `--resume-clarification` carries those answers into the next run. The lower-level `--clarification-response <path>` remains available for scripted runs.
79
+ - When a response is carried in, reconcile every prior `clarificationItems[]` row against new evidence and update its status to `resolved` or `obsolete` before issuing the next verdict. Schema-v1 compatibility Markdown may additionally render its conditional Section 0; schema-v2 AI Markdown records decisions under `## Clarification and User Decisions`.
74
80
  - **Supersession (BLOCKING).** Reconciling the `C-*` row is only half of incorporating an answer. An answer does not merely *add* a decision — it *invalidates* whatever the previous run wrote under the opposite assumption. Before issuing the next decision, walk the prior deliverable prose for every statement the answer makes false and **delete or rewrite it**, then record the retirement. Adding the new decision while leaving the contradicting sentence in place puts two opposite instructions for the same symbol in one document; the implementer must then guess which is live, and the next verification round correctly blocks on it. In `implementation-planning` this record is `implementationPlanning.supersessionLedger[]` — one entry per answered clarification, either `disposition: superseded` (with the retired statement, its replacement, and the sections revised) or `disposition: no-dependent-statement` (with a rationale). **Enforced:** `validators/validate-run.py` `_validate_supersession_ledger` requires an entry per answered clarification; whether the claim is *true* is what the §5.5.9 adversarial round tests.
75
- - Verdict Card (shared applies to every final-report regardless of profile):
76
- - 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.
81
+ - Verdict Card data consistency (shared; schema-v1 Markdown keeps the legacy visible card):
82
+ - `verdictCard.verdictToken` and `.direction` MUST byte-match `finalVerdict`; next-step routing must agree with `recommendedNextSteps[0]`. Schema-v1 Markdown renders the visible `## Verdict Card` / `## 7. Final Verdict` pair. Schema-v2 derives the AI handoff and human summary from the data fields without repeating both visible sections. **Enforced:** `validators/validate-run.py` `_validate_verdict_card_consistency` and `_validate_verdict_card_fields`.
77
83
  - Cross-worker traceability (shared — applies to every analysis worker output and to the lead's `## 6.` / `## 2.` tables in the final-report):
78
84
  - **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.
79
- - **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 rejected. **Enforced:** `schemas/final-report-v1.0.schema.json` `$defs.SourceItem` pins each entry to `^[a-z][a-z-]*:[A-Za-z0-9._-]+$`, and `ConsensusRow` / `PrimaryEvidenceRow` require a non-empty `sourceItems`, so a bare worker name fails schema validation.
85
+ - **Lead-side ID assignment + source preservation.** When the lead (or `report-writer-worker`) synthesises consensus, difference, or primary-evidence rows from worker outputs, the lead assigns a fresh `C-NNN` / `D-NNN` / `E-NNN` row ID. Each `sourceItems` field MUST list every contributing worker:item pair (e.g. `claude:F-001`, `codex:1.1`, `antigravity:F-3`) so an agent can trace the synthesised row to the worker result. Bare worker names are rejected. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.SourceItem` pins each entry to `^[a-z][a-z-]*:[A-Za-z0-9._-]+$`, and `ConsensusRow` / `PrimaryEvidenceRow` require non-empty `sourceItems`.
80
86
  - Audit sidecar (shared): Reading Confirmation placement follows the audience-selected preamble named by `**Worker Preamble Path:**`. Profiles do not restate it; the main worker-results body starts at section 1.
81
87
 
82
88
  - Markdown authoring (shared — applies to markdown documents not already governed by an okstra template/schema):
@@ -48,11 +48,13 @@
48
48
  - uncertainty and overlap relationships kept explicit for downstream consensus classification
49
49
  - every candidate's `Expected behavior after` is checked against the brief's `## Preserved Behavior` items before the row is written. A candidate whose observable change contradicts a `PB-NNN` item is dropped, or raised as a `## 1. Clarification Items` row — never softened into a vaguer cell. The scan brief's PB set is the upper bound on what this phase may propose changing.
50
50
  - Report assembly instructions:
51
+ - current branch — `schemaVersion: 2.0`: author the structured data contract below and let the independent renderers produce AI handoff Markdown and task-specific human HTML.
52
+ - v1 legacy branch: when validating or rerendering an existing schema-v1 report, preserve its `## 5.9 Improvement Candidates` table and legacy Markdown contract; do not rewrite that historical data into v2 implicitly.
51
53
  - the `## 5.9 Improvement Candidates` table populated with rows that obey the 11-column schema from `validators/validate_improvement_report.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, antigravity}, Recommended next-phase ∈ {requirements-discovery, implementation-planning, error-analysis}, Expected behavior after, Evidence as path:line list). `Expected behavior after` states, in one observable sentence, what becomes different once the candidate is applied — it is the seed of the downstream brief's `EB-NNN` / `EO-NNN`. A candidate you cannot write this cell for is a preference, not a finding: drop it rather than filling the cell with a restatement of the title.
52
54
  - `Consensus` cells in `## 5.9 Improvement Candidates` use the table enum exactly: `full`, `partial`, `contested`, `worker-unique`. Map convergence's `full-consensus` / `partial-consensus` labels to `full` / `partial` before writing the table.
53
55
  - `## 7. Final Verdict` Verdict Token ∈ {`candidates-ready`, `no-candidates`, `blocked`}; Direction `routing`; Next Step "ask the user to select K candidates (see the ## 5.9 table)"
54
56
  - `## 3. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
55
- - this report is authored free-form (improvement-discovery is not in the data.json schema enum); after the markdown is written, the report-writer runs `okstra inject-report-index <report.md> --report-language <en|ko>` to add the top-of-report Index + `I-NNN`/`C-NNN` scroll anchors. The run validator fails the report when the Index anchor is missing.
57
+ - author the shared schema-v2 report fields plus `improvementDiscovery.candidates[]`, `improvementDiscovery.lensCoverage[]`, `improvementDiscovery.selectionLimit`, and `improvementDiscovery.userNarrative` in data.json. `candidates[]` carries the same 11 logical fields described above; `lensCoverage[]` records either candidate IDs or an evidence-backed no-candidate rationale for every resolved lens. `schemas/final-report-v2.0.schema.json` and `validators/validate_improvement_report.py` enforce this contract. The renderers independently derive AI handoff Markdown and human HTML; never author a free-form report.
56
58
  - Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
57
59
  - if scan-scope or priority-lenses cannot be made concrete during Phase 1.5, end the run with Verdict Token `blocked`, populate `## 1. Clarification Items` with `Blocks=next-phase` rows, and do not run worker dispatch
58
60
  {{INCLUDE:_clarification-recommendation.md}}
@@ -259,20 +259,45 @@ def validate(data: Any, schema: dict) -> list[str]:
259
259
  return v.errors
260
260
 
261
261
 
262
+ SCHEMA_FILENAMES = {
263
+ "1.0": "final-report-v1.0.schema.json",
264
+ "2.0": "final-report-v2.0.schema.json",
265
+ }
266
+
267
+
268
+ def _locate_and_load_schema(filename: str, start: Path | None = None) -> dict:
269
+ here = (start or Path(__file__)).resolve()
270
+ if here.is_file():
271
+ here = here.parent
272
+ for parent in [here, *here.parents]:
273
+ candidate = parent / "schemas" / filename
274
+ if candidate.is_file():
275
+ return json.loads(candidate.read_text(encoding="utf-8"))
276
+ raise SchemaError(f"could not locate schemas/{filename}")
277
+
278
+
279
+ def load_schema_version(version: str, start: Path | None = None) -> dict:
280
+ """Load the final-report schema identified by its data version."""
281
+ try:
282
+ filename = SCHEMA_FILENAMES[version]
283
+ except KeyError as exc:
284
+ raise SchemaError(f"unsupported final-report schemaVersion: {version}") from exc
285
+ return _locate_and_load_schema(filename, start=start)
286
+
287
+
288
+ def load_schema_for_data(data: dict, start: Path | None = None) -> dict:
289
+ """Select a schema from the explicit ``schemaVersion`` in *data*."""
290
+ version = data.get("schemaVersion")
291
+ if not isinstance(version, str) or not version:
292
+ raise SchemaError("final-report data has no schemaVersion")
293
+ return load_schema_version(version, start=start)
294
+
295
+
262
296
  def load_schema(schema_path: Path | None = None) -> dict:
263
297
  """Load the final-report schema. If ``schema_path`` is None, locate
264
298
  ``schemas/final-report-v1.0.schema.json`` relative to this file's
265
299
  repo root.
266
300
  """
267
- if schema_path is None:
268
- here = Path(__file__).resolve()
269
- for parent in [here, *here.parents]:
270
- candidate = parent / "schemas" / "final-report-v1.0.schema.json"
271
- if candidate.is_file():
272
- schema_path = candidate
273
- break
274
- if schema_path is None:
275
- raise SchemaError(
276
- "could not locate schemas/final-report-v1.0.schema.json"
277
- )
278
- return json.loads(Path(schema_path).read_text(encoding="utf-8"))
301
+ if schema_path is not None:
302
+ return json.loads(Path(schema_path).read_text(encoding="utf-8"))
303
+ return load_schema_version("1.0")