okstra 0.37.0 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -257,7 +257,7 @@ Claude launch prompt 본문은 항상 `prompts/launch.template.md` 템플릿에
257
257
  - 메인 Claude는 항상 `Claude lead`이며 synthesis-only로 동작합니다.
258
258
  - 기본 required worker role은 `Claude worker`, `Codex worker`, `Report writer worker`입니다. `Gemini worker`는 옵션 워커로, `--workers` 또는 프로필의 `- Workers:` 섹션에 명시될 때만 required 로 포함됩니다.
259
259
  - `Report writer worker`는 보고서 구조화와 근거 정리에 집중하지만 최종 synthesis owner는 여전히 `Claude lead`입니다.
260
- - 기본 모델 계약은 중앙 기본값에서 계산합니다. 기본 fallback은 `Claude lead`=`opus-4-6`, `Claude worker`=`sonnet`, `Codex worker`=`gpt-5.5`, `Gemini worker`=`auto`(opt-in 시 적용)이며, `Report writer worker`는 별도 override가 없으면 `Claude lead` 모델을 따릅니다(즉, 기본값에서는 `opus-4-6`).
260
+ - 기본 모델 계약은 중앙 기본값에서 계산합니다. 기본 fallback은 `Claude lead`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.5`, `Gemini worker`=`auto`(opt-in 시 적용)이며, `Report writer worker`는 별도 override가 없으면 `Claude lead` 모델을 따릅니다(즉, 기본값에서는 `opus`).
261
261
  - `Gemini worker`는 옵션이므로 명시 포함된 run에 한해서만 시도 대상이 됩니다.
262
262
  - 최종 판단 전에는 현재 run의 worker roster 에 포함된 각 required role별로 결과 또는 명시적인 terminal status(`completed`, `timeout`, `error`, `not-run`)가 필요합니다.
263
263
  - 시도된 worker(`completed`, `timeout`, `error`)는 현재 run의 `prompts/` 아래 assigned worker prompt history file을 반드시 가져야 합니다.
@@ -943,7 +943,7 @@ phase 산출물의 출고 가능 여부를 강제하는 진입점:
943
943
  - run directory 내부는 `manifests/`, `state/`, `prompts/`, `reports/`, `status/`, `sessions/`, `worker-results/`처럼 유형별 하위 폴더로 구성되고, prompt snapshot은 `prompts/` 아래에 먼저 준비됩니다.
944
944
  - worker 생성과 결과 취합은 Claude가 수행합니다.
945
945
  - standard workflow는 `Claude lead` + 기본 worker `Claude worker`, `Codex worker`, `Report writer worker`를 사용하고, `Gemini worker`는 명시할 때만 포함되는 옵션입니다.
946
- - worker 모델은 `--lead-model`, `--claude-model`, `--codex-model`, `--gemini-model`, `--report-writer-model`로 override할 수 있고, 기본값은 `OKSTRA_DEFAULT_*` 환경 변수에서 중앙 관리합니다. fallback 기본값은 `Claude lead`/`Report writer worker`=`opus-4-6`, `Claude worker`=`sonnet`, `Codex worker`=`gpt-5.5`, `Gemini worker`=`auto`입니다.
946
+ - worker 모델은 `--lead-model`, `--claude-model`, `--codex-model`, `--gemini-model`, `--report-writer-model`로 override할 수 있고, 기본값은 `OKSTRA_DEFAULT_*` 환경 변수에서 중앙 관리합니다. fallback 기본값은 `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.5`, `Gemini worker`=`auto`입니다.
947
947
  - `--task-type implementation` 에서는 Executor 역할을 맡을 provider 를 `--executor <claude|codex|gemini>` (또는 `OKSTRA_DEFAULT_EXECUTOR`, fallback `claude`) 로 선택합니다. Executor 만 프로젝트 파일을 mutate 할 수 있고, 나머지 두 provider 와 자기 자신의 provider 가 모두 별도 CLI 세션으로 verifier 로 dispatch 됩니다 (세션 분리만으로도 self-review 안전장치 유지). Executor 의 모델은 선택된 provider 의 worker 모델 플래그(`--claude-model` / `--codex-model` / `--gemini-model`) 를 그대로 재사용하며, run-manifest 의 `teamContract.executor` 블록에 provider / displayName / workerAgent / model 이 기록됩니다.
948
948
  - Executor 별 worktree cwd 주입: codex / gemini executor 는 wrapper(`okstra-codex-exec.sh -C` / `okstra-gemini-exec.sh --include-directories`) 가 CLI layer 에서 cwd 를 worktree 로 고정합니다. Claude executor 는 Bash tool 에 per-call cwd 인자가 없어 cwd 민감 toolchain (`cargo`/`npm`/`pnpm`/`bun`/`pytest`/`make`/`go`) 호출을 같은 Bash invocation 안에서 `cd {{EXECUTOR_WORKTREE_PATH}} && <cmd>` 로 prefix 합니다 — `bash -lc`/`bash -c` 래핑은 금지되며 (`cd` leading token 이 가려져 permission auto-allow 우회 실패), 작업 디렉터리 플래그 (`git -C`, `cargo --manifest-path` 등) 가 있으면 그것을 우선합니다. 자세한 규약은 `prompts/profiles/implementation.md` 의 *Executor Worktree* 블록과 `agents/workers/claude-worker.md` 의 Executor exception 항목 참고.
949
949
  - project-level current-task convenience pointer는 `.okstra/discovery/latest-task.json`입니다.
package/docs/kr/cli.md CHANGED
@@ -302,12 +302,12 @@ scripts/okstra.sh --task-type implementation-planning --workers claude,codex --p
302
302
  ### `--claude-model`
303
303
 
304
304
  `Claude worker`에 사용할 모델을 지정합니다.
305
- 지정하지 않으면 중앙 기본값 `OKSTRA_DEFAULT_CLAUDE_MODEL` 또는 fallback `sonnet`을 사용합니다.
305
+ 지정하지 않으면 중앙 기본값 `OKSTRA_DEFAULT_CLAUDE_MODEL` 또는 fallback `opus`을 사용합니다.
306
306
 
307
307
  ### `--lead-model`
308
308
 
309
309
  `Claude lead`에 사용할 모델을 지정합니다.
310
- 지정하지 않으면 중앙 기본값 `OKSTRA_DEFAULT_LEAD_MODEL` 또는 fallback `opus-4-6`를 사용합니다.
310
+ 지정하지 않으면 중앙 기본값 `OKSTRA_DEFAULT_LEAD_MODEL` 또는 fallback `opus`을 사용합니다.
311
311
 
312
312
  ### `--codex-model`
313
313
 
@@ -337,7 +337,7 @@ fallback 기본값은 아래와 같습니다.
337
337
 
338
338
  - `Claude lead`: `opus`
339
339
  - `Report writer worker`: `opus`
340
- - `Claude worker`: `sonnet`
340
+ - `Claude worker`: `opus`
341
341
  - `Codex worker`: `gpt-5.5`
342
342
  - `Gemini worker`: `auto`
343
343
  - Implementation executor: `claude` (즉 기본은 `Claude executor`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.37.0",
3
+ "version": "0.38.1",
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.37.0",
3
- "builtAt": "2026-05-27T07:29:11.725Z",
2
+ "package": "0.38.1",
3
+ "builtAt": "2026-05-29T06:34:56.309Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -100,19 +100,21 @@ These lines are the only structured signal the user has during a long run. Do NO
100
100
 
101
101
  `okstra-run` (in-session) surfaces these lines to the user directly; the bash-spawned path leaves them in the session jsonl for post-hoc retrieval. Neither path requires any additional formatting from Lead — emit the literal `PROGRESS:` prefix and the rest of the line as plain text.
102
102
 
103
- ## Default model assignments
103
+ ## Model assignments
104
104
 
105
- Unless the task bundle overrides:
105
+ **The lead never invents a model.** Every role's model is read from `task-manifest.json` `resultContract.requiredWorkerRoles[*].modelExecutionValue` (and the lead model metadata). A missing assignment is a manifest defect, not a license to fall back — see [okstra-team-contract](./skills/okstra-team-contract/SKILL.md) "Model Assignment Rules". The manifest is always populated at run-prep time by the CLI, which seeds these values from `OKSTRA_DEFAULT_*_MODEL` (`scripts/okstra_ctl/run.py`).
106
106
 
107
- | Role | Model | subagent_type | Notes |
108
- |------|-------|---------------|-------|
109
- | Claude lead | opus-4-6 | -- | orchestration + synthesis |
110
- | Report writer worker | opus-4-6 | report-writer-worker | authors the final report file (`agents/report-writer-worker.md`) |
111
- | Claude worker | sonnet | claude-worker | defined in `agents/claude-worker.md` |
112
- | Codex worker | gpt-5.5 | codex-worker | defined in `agents/codex-worker.md` |
113
- | Gemini worker | auto | gemini-worker | defined in `agents/gemini-worker.md` |
107
+ The table below documents those prep-time seed values **for reference only** — it is NOT a lead-applied fallback:
114
108
 
115
- If the prepared task bundle contains explicit model assignments, those assignments are canonical for the run. All three analysis workers use dedicated agent definitions; Codex/Gemini wrappers handle external CLI invocation internally; Claude worker runs as an in-process subagent with explicitly registered MCP tools so it does not fall back to `claude --mcp-cli` Bash invocations.
109
+ | Role | Seed model | subagent_type | Source definition |
110
+ |------|-----------|---------------|-------------------|
111
+ | Claude lead | opus | -- | orchestration + convergence supervision + final-report review/approval |
112
+ | Report writer worker | opus | report-writer-worker | `agents/workers/report-writer-worker.md` |
113
+ | Claude worker | opus | claude-worker | `agents/workers/claude-worker.md` |
114
+ | Codex worker | gpt-5.5 | codex-worker | generated from `agents/workers/_cli-wrapper-template.md` + `codex-worker.params.json` |
115
+ | Gemini worker | auto | gemini-worker | generated from `agents/workers/_cli-wrapper-template.md` + `gemini-worker.params.json` |
116
+
117
+ All three analysis workers use dedicated agent definitions; Codex/Gemini wrappers handle external CLI invocation internally; Claude worker runs as an in-process subagent with explicitly registered MCP tools so it does not fall back to `claude --mcp-cli` Bash invocations.
116
118
 
117
119
  ### Implementation phase: Executor binding
118
120
 
@@ -263,7 +265,7 @@ If convergence is disabled, proceed directly to Phase 6 with the raw worker resu
263
265
 
264
266
  ### Authoring ownership (BLOCKING)
265
267
 
266
- If `Report writer worker` is in the selected roster (`recommendedWorkers` / `resultContract.requiredWorkerRoles`), **Lead MUST dispatch it to write `runs/<task-type>/reports/final-report-<task-type>-<seq>.md`**. Lead does NOT write that 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 file.
268
+ 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 [okstra-report-writer](./skills/okstra-report-writer/SKILL.md) "File-author ownership".
267
269
 
268
270
  Before constructing the dispatch prompt, the lead MUST:
269
271
 
@@ -290,7 +292,7 @@ If only one worker result is usable: reduced-confidence synthesis. If evidence i
290
292
 
291
293
  After the Report writer worker draft is reviewed (or after the lead-authored fallback completes), **if** `task_type == "implementation-planning"` **and** `task-manifest.json` `convergence.planBodyVerification.enabled == true` (default), the lead MUST run one additional verification round on the consolidated plan body before declaring Phase 6 complete and entering Phase 7.
292
294
 
293
- This is a Phase 6 sub-step — it does NOT introduce a new top-level lifecycle phase. The 6-phase model (1 Intake → 2 Render 3 Team 4 Workers 5 Synthesis prep 5.5 Convergence 6 Synthesis → 7 Persist) is preserved.
295
+ This is a Phase 6 sub-step — it does NOT introduce a new top-level lifecycle phase. The lead operating-phase model (Phase 1 Intake → Phase 7 Persist, with the labels in the "Quick Reference" table above as the single source of truth) is preserved.
294
296
 
295
297
  **REQUIRED SUB-SKILL:** Invoke [okstra-convergence](./skills/okstra-convergence/SKILL.md) "Plan-body verification mode (implementation-planning only)" for the round protocol, plan-item ID scheme (`P-Opt-*` / `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*`), verdict semantics (`AGREE` / `DISAGREE(a-e)` / `SUPPLEMENT`), classification rules, gate-result resolution, and the state-file schema at `runs/<task-type>/state/plan-body-verification.json`.
296
298
 
@@ -35,7 +35,7 @@ You also write an audit sidecar at the path the lead registers as `**Worker Resu
35
35
  1. The canonical data.json path you wrote (project-relative).
36
36
  2. The rendered markdown path produced by the renderer (project-relative).
37
37
  3. Inputs reconciled (analysis-worker result files + convergence-state file).
38
- 4. Any structural deviations from `schemas/final-report-v1.0.schema.json` and the reason.
38
+ 4. Any structural deviations from the `<instruction-set>/final-report-schema.json` excerpt and the reason.
39
39
 
40
40
  Do NOT duplicate the data.json contents here — the data.json is the canonical artifact; this sidecar is the validator-required pointer / audit record.
41
41
 
@@ -67,8 +67,8 @@ Before writing the data.json, you MUST:
67
67
 
68
68
  For the report writer specifically, the `## Inputs` list always includes:
69
69
 
70
- - `schemas/final-report-v1.0.schema.json` the JSON Schema you must conform to. The renderer + validator both consume it.
71
- - `templates/reports/final-report.template.md` — the Jinja2 template the renderer uses. Read it to understand which data.json fields appear where in the rendered markdown; do NOT edit it.
70
+ - `<instruction-set>/final-report-schema.json` — the **per-task-type excerpt** of the data.json schema (scoped to this run's task-type at prep time: other task-types' deliverable blocks and their unreachable `$defs` are stripped). This is the shape you must author. Read this, NOT the full `schemas/final-report-v1.0.schema.json` (it is not in the task bundle and its `schemas/...` path is not resolvable here). Validation still runs against the full schema post-hoc, so the excerpt never relaxes the contract.
71
+ - `<instruction-set>/final-report-template.md` — the **phase-stripped** Jinja2 template the renderer uses (only this run's §4.x deliverable block remains). Read it to understand which data.json fields appear where in the rendered markdown; do NOT edit it, and do NOT pull the full `templates/reports/final-report.template.md` source.
72
72
  - `templates/reports/i18n/en.json` and `templates/reports/i18n/ko.json`.
73
73
  - Every analysis worker's result file under `worker-results/`.
74
74
  - `state/convergence-<task-type>-<seq>.json` (if present). When present, reproduce its `roundHistory[]`, `round2SkippedReason`, and `finalClassificationCounts` verbatim into the final report's Section 1 Round History sub-table — do not recompute from worker results.
@@ -79,7 +79,7 @@ Write a Reading Confirmation block to your **audit sidecar** at `runs/<task-type
79
79
 
80
80
  ## Authoring Contract
81
81
 
82
- You author the final-report data.json (the JSON SSOT). The schema is `schemas/final-report-v1.0.schema.json` — its `$defs` enumerate every row shape, enum value, and cross-field constraint. The validator rejects data that violates the schema; the renderer refuses to render against invalid data. Both consume the same schema, so a data.json that validates is a data.json that renders correctly.
82
+ You author the final-report data.json (the JSON SSOT). You author it against the `<instruction-set>/final-report-schema.json` excerpt — its `$defs` enumerate every row shape, enum value, and cross-field constraint that applies to this run's task-type. The validator and renderer both consume the **full** `schemas/final-report-v1.0.schema.json` (the excerpt is a faithful task-type-scoped subset of it), so a data.json that satisfies the excerpt is a data.json that validates and renders correctly.
83
83
 
84
84
  The rendered markdown (`final-report-<task-type>-<seq>.md`) is produced by `scripts/okstra-render-final-report.py` immediately after you write the data.json. The HTML view (`*.html`) is produced from the markdown by Phase 7 step 1.5 (`scripts/okstra-render-report-views.py`). The data.json is the only file you write; the rest are derived.
85
85
 
@@ -128,7 +128,10 @@ def main(argv: list[str] | None = None) -> int:
128
128
  css, js = _load_assets()
129
129
  meta = RunMeta(task_key=task_key, task_type=task_type, seq=seq, source_report=source_report)
130
130
  html_path = render_html_view(report_path, run_meta=meta, css=css, js=js)
131
- print(f"html: {html_path}")
131
+ if html_path is None:
132
+ print("html: skipped (no §5 clarification rows — html view carries no interactive forms for this report)")
133
+ else:
134
+ print(f"html: {html_path}")
132
135
  return 0
133
136
 
134
137
 
@@ -81,7 +81,7 @@ Emit one `PROGRESS: <phase-id> <verb-phrase>` line as plain user-facing text at
81
81
  ## Available MCP Servers
82
82
 
83
83
  {{AVAILABLE_MCP_SERVERS}}
84
- - The full usage policy and per-phase rules live in the task brief's `## Available MCP Servers` section. Read them there before dispatching workers and **forward that section verbatim into every worker prompt** during Phase 2 so workers know they are allowed to call these tools.
84
+ - The full usage policy and per-phase rules live in the task brief's `## Available MCP Servers` section. Read them there before dispatching workers and inject only the one-line pointer below into each worker prompt (the brief is already in every worker's [Required reading], so verbatim copy is redundant): `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).`
85
85
  - **Invocation rule (forward to every worker prompt)**: MCP tools are addressed by their tool name through the host's tool interface — **never via `Bash`**. Claude-side workers call the tool directly (e.g. `mcp__<server>__<tool>`). Codex/Gemini workers call through their CLI's own MCP transport (e.g. `codex mcp call ...`). Running the tool name as a shell command is a contract violation and will always fail regardless of permission grants.
86
86
  - Codex worker and Gemini worker run external CLIs; they can only use these MCP servers if their own CLI configs mirror them. If not, instruct the worker to record `MCP not available in this CLI` in its `Missing Information or Assumptions` block rather than guessing or shell-falling-back.
87
87
  - MCP queries are evidence-grade. Cite server, table, and the SELECT used in worker output. MCP must NOT be used as a write path in any phase, including `implementation`.
@@ -8,7 +8,7 @@ profile document.
8
8
  - Team contract (shared):
9
9
  - `Claude lead` is synthesis-only and stays distinct from `Claude worker` (or, in `implementation`, the `Executor` and verifiers).
10
10
  - `Report writer worker` is the **author** of the final-report file; `Claude lead` reviews and approves the produced draft and does NOT write the file itself (see `okstra-team-contract` and `okstra-report-writer` for the authoritative contract).
11
- - default model assignments are resolved from centralised defaults; the fallback values are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`sonnet`, `Codex worker`=`gpt-5.5`, `Gemini worker`=`auto`. Phase-specific overrides (e.g. `implementation`'s executor binding) live in the per-profile document.
11
+ - default model assignments are resolved from centralised defaults; the fallback values are `Claude lead`/`Report writer worker`=`opus`, `Claude worker`=`opus`, `Codex worker`=`gpt-5.5`, `Gemini worker`=`auto`. Phase-specific overrides (e.g. `implementation`'s executor binding) live in the per-profile document.
12
12
  - every required worker listed in the per-profile `Required workers:` block must be attempted; the final verdict waits until each has either a result or an explicit terminal status (`timeout`, `error`, `not-run`).
13
13
  - unnamed generic parallel workers must not replace the required role roster, and no additional sub-agent dispatch is allowed beyond this roster.
14
14
  - Worker interaction model (shared — read before inferring behaviour from the roster):
@@ -11,7 +11,7 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
11
11
 
12
12
  - The verifier slots are `Claude verifier` and `Codex verifier`, plus `Gemini verifier` **only when `gemini` is in the resolved `--workers` roster**. Every verifier in the resolved roster is dispatched regardless of which provider holds the executor role; the executor's own provider is run *separately* as a verifier (a fresh CLI session with no shared context) so that no verdict is produced from the same session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
13
13
  - Session isolation — not model-variant divergence — is the primary self-review safeguard: each verifier is a separate CLI invocation with its own context window, so reusing the same model variant for executor and same-provider verifier is acceptable. Different model variants (e.g. executor=opus / Claude verifier=sonnet) remain recommended when available.
14
- - Phase-specific model defaults override the shared defaults: `Claude verifier`=`sonnet`, `Codex verifier`=`gpt-5.5`, `Gemini verifier`=`auto` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `sonnet`, override to `opus` recommended when this run's executor is claude), codex→`--codex-model` (default `gpt-5.5`), gemini→`--gemini-model` (default `auto`).
14
+ - Phase-specific model defaults override the shared defaults: `Claude verifier`=`opus`, `Codex verifier`=`gpt-5.5`, `Gemini verifier`=`auto` (only when present in the roster). The `Executor`'s model is taken from the provider-specific worker model corresponding to `--executor`: claude→`--claude-model` (default `opus`), codex→`--codex-model` (default `gpt-5.5`), gemini→`--gemini-model` (default `auto`).
15
15
  - Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced. Verifiers remain strictly read-only there.
16
16
 
17
17
  ## Verifier QA duties (independent re-run mandate)
@@ -154,8 +154,8 @@ GEMINI_WORKER_MODEL_EXECUTION_VALUE=""
154
154
  REPORT_WRITER_MODEL=""
155
155
  REPORT_WRITER_MODEL_EXECUTION_VALUE=""
156
156
  DEFAULT_WORKERS="claude,codex,report-writer"
157
- DEFAULT_LEAD_MODEL_NAME="${OKSTRA_DEFAULT_LEAD_MODEL:-opus-4-6}"
158
- DEFAULT_CLAUDE_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CLAUDE_MODEL:-sonnet}"
157
+ DEFAULT_LEAD_MODEL_NAME="${OKSTRA_DEFAULT_LEAD_MODEL:-opus}"
158
+ DEFAULT_CLAUDE_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CLAUDE_MODEL:-opus}"
159
159
  DEFAULT_CODEX_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_CODEX_MODEL:-gpt-5.5}"
160
160
  DEFAULT_GEMINI_WORKER_MODEL_NAME="${OKSTRA_DEFAULT_GEMINI_MODEL:-auto}"
161
161
  DEFAULT_REPORT_WRITER_MODEL_NAME="${OKSTRA_DEFAULT_REPORT_WRITER_MODEL:-$DEFAULT_LEAD_MODEL_NAME}"
@@ -71,8 +71,8 @@ options:
71
71
  --yes Skip interactive prompting and confirmation. Requires all required arguments.
72
72
  --workers Comma-separated worker list for this run. Default: claude,codex,report-writer
73
73
  (Gemini worker is optional; add `gemini` explicitly, e.g. --workers claude,codex,gemini,report-writer)
74
- --lead-model Model for Claude lead. Default: OKSTRA_DEFAULT_LEAD_MODEL or opus-4-6
75
- --claude-model Model for Claude worker. Default: OKSTRA_DEFAULT_CLAUDE_MODEL or sonnet
74
+ --lead-model Model for Claude lead. Default: OKSTRA_DEFAULT_LEAD_MODEL or opus
75
+ --claude-model Model for Claude worker. Default: OKSTRA_DEFAULT_CLAUDE_MODEL or opus
76
76
  --codex-model Model for Codex worker. Default: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.5
77
77
  --gemini-model Model for Gemini worker. Default: OKSTRA_DEFAULT_GEMINI_MODEL or auto
78
78
  --report-writer-model
@@ -98,9 +98,9 @@ options:
98
98
  -h, --help Show this help.
99
99
 
100
100
  model defaults:
101
- Claude lead: OKSTRA_DEFAULT_LEAD_MODEL or opus-4-6
101
+ Claude lead: OKSTRA_DEFAULT_LEAD_MODEL or opus
102
102
  Report writer worker: OKSTRA_DEFAULT_REPORT_WRITER_MODEL or Claude lead default
103
- Claude worker: OKSTRA_DEFAULT_CLAUDE_MODEL or sonnet
103
+ Claude worker: OKSTRA_DEFAULT_CLAUDE_MODEL or opus
104
104
  Codex worker: OKSTRA_DEFAULT_CODEX_MODEL or gpt-5.5
105
105
  Gemini worker: OKSTRA_DEFAULT_GEMINI_MODEL or auto
106
106
  Implementation executor: OKSTRA_DEFAULT_EXECUTOR or claude (one of: claude | codex | gemini)
@@ -144,6 +144,7 @@ def compute_run_paths(
144
144
  final_status = run_status / f"final{suffixes['status']}.status"
145
145
  team_state = run_state / f"team-state{suffixes['state']}.json"
146
146
  final_report_template = instruction_set / "final-report-template.md"
147
+ final_report_schema = instruction_set / "final-report-schema.json"
147
148
  reference_expectations = instruction_set / "reference-expectations.md"
148
149
  claude_resume_command = run_sessions / f"claude-resume{suffixes['sessions']}.sh"
149
150
  latest_task_file = discovery_dir / "latest-task.json"
@@ -205,6 +206,7 @@ def compute_run_paths(
205
206
  "FINAL_STATUS_PATH": str(final_status),
206
207
  "TEAM_STATE_PATH": str(team_state),
207
208
  "FINAL_REPORT_TEMPLATE_PATH": str(final_report_template),
209
+ "FINAL_REPORT_SCHEMA_PATH": str(final_report_schema),
208
210
  "REFERENCE_EXPECTATIONS_FILE": str(reference_expectations),
209
211
  "CLAUDE_RESUME_COMMAND_PATH": str(claude_resume_command),
210
212
  "OKSTRA_LATEST_TASK_FILE": str(latest_task_file),
@@ -264,6 +266,7 @@ def compute_run_paths(
264
266
  ("WORKER_RESULTS_RELATIVE_PATH", worker_results),
265
267
  ("RUN_CARRY_RELATIVE_PATH", run_carry),
266
268
  ("FINAL_REPORT_TEMPLATE_RELATIVE_PATH", final_report_template),
269
+ ("FINAL_REPORT_SCHEMA_RELATIVE_PATH", final_report_schema),
267
270
  ("REFERENCE_EXPECTATIONS_RELATIVE_PATH", reference_expectations),
268
271
  ("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", claude_resume_command),
269
272
  ("RUN_VALIDATOR_RELATIVE_PATH", run_validator_script),
@@ -664,17 +664,41 @@ def serialize_user_response(
664
664
  # Convenience entrypoint for tests + CLI.
665
665
  # --------------------------------------------------------------------------- #
666
666
 
667
+ def report_has_clarification_items(src_md: str) -> bool:
668
+ """True when the final-report MD has at least one §5 ``C-*``
669
+ clarification row. This is the single predicate that gates HTML-view
670
+ generation: the self-contained html's only value over the markdown is
671
+ the embedded ``<form>`` widgets for those rows, so a clarification-free
672
+ report does not get an html sibling. The renderer, the CLI, and
673
+ ``validators/validate-report-views.py`` all key off this same function
674
+ so generation and validation never disagree."""
675
+ return any(
676
+ re.fullmatch(r"C-\d+", item.row_id)
677
+ for item in (parse_clarification_items(src_md) or [])
678
+ )
679
+
680
+
667
681
  def render_html_view(
668
682
  src_md_path: Path,
669
683
  *,
670
684
  run_meta: RunMeta,
671
685
  css: str,
672
686
  js: str,
673
- ) -> Path:
674
- """Write ``<stem>.html`` next to ``src_md_path`` and return its
675
- path. Idempotent overwrites the existing html sibling."""
687
+ ) -> Path | None:
688
+ """Write ``<stem>.html`` next to ``src_md_path`` and return its path,
689
+ or return ``None`` when generation is skipped because the report has
690
+ no §5 clarification rows (see ``report_has_clarification_items``).
691
+ Idempotent — overwrites an existing html sibling, and removes a stale
692
+ one when a previously-clarification-bearing report no longer has rows."""
676
693
  src_text = src_md_path.read_text(encoding="utf-8")
677
694
  html_path = src_md_path.with_name(src_md_path.stem + ".html")
695
+ if not report_has_clarification_items(src_text):
696
+ # Conditional generation: no interactive forms to render. Drop any
697
+ # stale html left over from a prior clarification-bearing run so the
698
+ # validator's "no rows → no html" branch stays consistent.
699
+ if html_path.is_file():
700
+ html_path.unlink()
701
+ return None
678
702
  html_text = render_html(src_text, run_meta=run_meta, css=css, js=js)
679
703
  html_path.write_text(html_text, encoding="utf-8")
680
704
  return html_path
@@ -35,7 +35,9 @@ from .material import (
35
35
  related_tasks_inline,
36
36
  resolve_related_tasks,
37
37
  )
38
+ from .final_report_schema import load_schema
38
39
  from .models import resolve_model_metadata
40
+ from .schema_excerpt import build_schema_excerpt
39
41
  from .path_resolve import relative_to_project_root, resolve_user_file
40
42
  from .render import (
41
43
  apply_lead_prompt_defaults,
@@ -667,8 +669,8 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
667
669
  pr_template_source = resolved_tpl.source
668
670
 
669
671
  # ---- model assignments ----
670
- lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus-4-6")
671
- claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "sonnet")
672
+ lead_default = _default("OKSTRA_DEFAULT_LEAD_MODEL", "opus")
673
+ claude_default = _default("OKSTRA_DEFAULT_CLAUDE_MODEL", "opus")
672
674
  codex_default = _default("OKSTRA_DEFAULT_CODEX_MODEL", "gpt-5.5")
673
675
  gemini_default = _default("OKSTRA_DEFAULT_GEMINI_MODEL", "auto")
674
676
  report_writer_default = _default("OKSTRA_DEFAULT_REPORT_WRITER_MODEL", lead_default)
@@ -932,6 +934,26 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
932
934
  render_template_with_ctx(
933
935
  str(final_report_template), ctx["FINAL_REPORT_TEMPLATE_PATH"], ctx,
934
936
  )
937
+ # Per-task-type schema excerpt for the report-writer worker. The full
938
+ # schema validates the data.json post-hoc (load_schema); the worker only
939
+ # needs the common structure + this run's task-type block, so we write a
940
+ # scoped excerpt into the instruction-set rather than make the worker read
941
+ # the whole 44 KB / all-task-types schema (whose repo `schemas/...` path is
942
+ # not resolvable from a consumer project's task bundle anyway).
943
+ #
944
+ # Guarded: a missing/unreadable schema must NOT break bundle preparation.
945
+ # If the excerpt cannot be produced (e.g. an older install that predates
946
+ # the schemas/ copy step), prep proceeds without it — the report-writer
947
+ # still has the phase-stripped template + skill structure guide, and
948
+ # validation runs against the full schema regardless.
949
+ try:
950
+ _excerpt = build_schema_excerpt(load_schema(), inp.task_type)
951
+ Path(ctx["FINAL_REPORT_SCHEMA_PATH"]).write_text(
952
+ json.dumps(_excerpt, indent=2, ensure_ascii=False) + "\n",
953
+ encoding="utf-8",
954
+ )
955
+ except Exception: # noqa: BLE001 — advisory artifact; never fail prep over it
956
+ pass
935
957
  render_template_with_ctx(
936
958
  str(prompt_template), str(instruction_set / "claude-execution-prompt.md"), ctx,
937
959
  )
@@ -0,0 +1,116 @@
1
+ """Build a task-type-scoped excerpt of the final-report schema.
2
+
3
+ The full schema (``schemas/final-report-v1.0.schema.json``) carries the
4
+ deliverable property blocks for ALL task-types (``implementationPlanning``,
5
+ ``releaseHandoff``, ``implementation``, ``finalVerification``) plus a
6
+ ``$defs`` library (~38% of the file) shared across them. A single run only
7
+ authors ONE task-type's data.json, so the report-writer worker only needs
8
+ the common structure + its own task-type's block + the ``$defs`` those
9
+ reach.
10
+
11
+ This module produces that scoped excerpt, written into the run's
12
+ instruction-set at prep time so the worker reads a smaller, path-local
13
+ file (`instruction-set/final-report-schema.json`) instead of the full
14
+ repo/installed schema (whose `schemas/...` path is not even resolvable
15
+ from inside a consumer project's task bundle).
16
+
17
+ The excerpt is ADVISORY — a reading aid for the author. Validation always
18
+ runs against the FULL schema via ``final_report_schema.load_schema()``, so
19
+ the excerpt never gates correctness; it only trims what the worker reads.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import re
25
+
26
+ # task-type → the per-type deliverable property key it owns. task-types
27
+ # absent from this map (requirements-discovery, error-analysis,
28
+ # improvement-discovery) have no per-type block; their excerpt keeps only
29
+ # the common properties.
30
+ _TASK_TYPE_PROPERTY = {
31
+ "implementation-planning": "implementationPlanning",
32
+ "release-handoff": "releaseHandoff",
33
+ "implementation": "implementation",
34
+ "final-verification": "finalVerification",
35
+ }
36
+ _ALL_PER_TYPE_PROPERTIES = frozenset(_TASK_TYPE_PROPERTY.values())
37
+
38
+ _REF_RE = re.compile(r'"\$ref"\s*:\s*"#/\$defs/([^"]+)"')
39
+
40
+
41
+ def _refs_in(obj) -> set[str]:
42
+ """Every ``#/$defs/<name>`` referenced anywhere inside ``obj``."""
43
+ return set(_REF_RE.findall(json.dumps(obj)))
44
+
45
+
46
+ def _conditional_applies(entry: dict, task_type: str) -> bool:
47
+ """True when an ``allOf`` if/then entry is relevant to *task_type*.
48
+
49
+ Universal entries (no ``header.taskType`` constraint) always apply;
50
+ ``const`` entries apply on exact match; ``enum`` entries apply when
51
+ *task_type* is a member.
52
+ """
53
+ constraint = (
54
+ entry.get("if", {})
55
+ .get("properties", {})
56
+ .get("header", {})
57
+ .get("properties", {})
58
+ .get("taskType", {})
59
+ )
60
+ const = constraint.get("const")
61
+ enum = constraint.get("enum")
62
+ if const is None and enum is None:
63
+ return True
64
+ if const is not None:
65
+ return const == task_type
66
+ return task_type in (enum or [])
67
+
68
+
69
+ def build_schema_excerpt(schema: dict, task_type: str) -> dict:
70
+ """Return a task-type-scoped copy of *schema*.
71
+
72
+ Drops the per-type property blocks that do not belong to *task_type*,
73
+ the ``allOf`` conditionals that cannot fire for it, and the ``$defs``
74
+ that become unreachable as a result (transitive closure preserves any
75
+ def reachable from a kept property or conditional). Top-level metadata
76
+ and ``required`` are preserved (``required`` never lists per-type
77
+ blocks, but it is filtered defensively).
78
+ """
79
+ keep_per_type = _TASK_TYPE_PROPERTY.get(task_type)
80
+ drop_props = _ALL_PER_TYPE_PROPERTIES - ({keep_per_type} if keep_per_type else set())
81
+
82
+ props = {
83
+ k: v for k, v in schema.get("properties", {}).items() if k not in drop_props
84
+ }
85
+ all_of = [e for e in schema.get("allOf", []) if _conditional_applies(e, task_type)]
86
+
87
+ # Reachable $defs = transitive closure of $ref from kept props + allOf.
88
+ defs = schema.get("$defs", {})
89
+ reachable: set[str] = set()
90
+ work = list(_refs_in(props) | _refs_in(all_of))
91
+ while work:
92
+ name = work.pop()
93
+ if name in reachable or name not in defs:
94
+ continue
95
+ reachable.add(name)
96
+ work.extend(_refs_in(defs[name]))
97
+
98
+ excerpt = {
99
+ k: v
100
+ for k, v in schema.items()
101
+ if k not in ("properties", "allOf", "$defs", "required", "description")
102
+ }
103
+ excerpt["description"] = (
104
+ f"Per-task-type excerpt of the okstra final-report schema, scoped to "
105
+ f"`{task_type}`. Reading aid for the report-writer worker — validation "
106
+ f"runs against the full schema (schemas/final-report-v1.0.schema.json)."
107
+ )
108
+ excerpt["properties"] = props
109
+ excerpt["required"] = [
110
+ r for r in schema.get("required", []) if r not in drop_props
111
+ ]
112
+ if all_of:
113
+ excerpt["allOf"] = all_of
114
+ if reachable:
115
+ excerpt["$defs"] = {k: v for k, v in defs.items() if k in reachable}
116
+ return excerpt
@@ -322,8 +322,6 @@ For each finding:
322
322
 
323
323
  Save it to `runs/<task-type>/state/convergence-<task-type>-<seq>.json`.
324
324
 
325
- Schema version `1.1` extends `1.0` (legacy fields kept as aliases for backward-compat with already-shipped reports):
326
-
327
325
  ```json
328
326
  {
329
327
  "schemaVersion": "1.1",
@@ -368,12 +366,7 @@ Schema version `1.1` extends `1.0` (legacy fields kept as aliases for backward-c
368
366
  ],
369
367
  "skippedWorkers": [
370
368
  { "worker": "claude-worker", "reason": "no items to verify" }
371
- ],
372
- "verificationsRequested": 2,
373
- "verificationsCompleted": 2,
374
- "newConsensus": 3,
375
- "remainingInQueue": 0,
376
- "earlyExit": true
369
+ ]
377
370
  }
378
371
  ],
379
372
  "round2SkippedReason": "queue-empty",
@@ -384,12 +377,6 @@ Schema version `1.1` extends `1.0` (legacy fields kept as aliases for backward-c
384
377
  "partialConsensus": 1,
385
378
  "contested": 0,
386
379
  "workerUnique": 1
387
- },
388
- "summary": {
389
- "fullConsensus": 5,
390
- "partialConsensus": 1,
391
- "contested": 0,
392
- "workerUnique": 1
393
380
  }
394
381
  }
395
382
  ```
@@ -408,9 +395,8 @@ Schema rules:
408
395
  - `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.
409
396
  - `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).
410
397
  - `roundHistory[].skippedWorkers[]`: per-worker `{worker, reason}` for workers with no items to verify OR with a non-result dispatch.
411
- - `roundHistory[].verificationsRequested|verificationsCompleted|newConsensus|remainingInQueue|earlyExit`: legacy v1.0 aliases. New runs SHOULD populate them so existing parsers keep working: `verificationsRequested == len(dispatches)`, `verificationsCompleted == len(d for d in dispatches if d.status == "completed")`, `newConsensus == resolvedCount`, `remainingInQueue == carriedForwardCount`, `earlyExit == (round < effectiveMaxRounds AND carriedForwardCount == 0)`.
412
398
  - `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.
413
- - `finalClassificationCounts`: post-loop counts. New required field must equal `summary` 1:1. `summary` is retained as the v1.0 alias.
399
+ - `finalClassificationCounts`: post-loop counts. Required field with keys `fullConsensus`, `partialConsensus`, `contested`, `workerUnique`.
414
400
  - `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 (Task 3's "Worker failure handling in reverify" rule 4). `aborted-non-result` is the new v1.1 value.
415
401
  - `totalRounds`: count of rounds actually executed (not `effectiveMaxRounds`). May be `0` when Round 0 produced no queue items (all findings reached consensus during grouping).
416
402
 
@@ -46,9 +46,11 @@ The prompt MUST include, in this order at the top:
46
46
  4. `**Worker Result Path:** runs/<task-type>/worker-results/report-writer-worker-<task-type>-<seq>.md` — mandatory validator-checked worker-results audit file
47
47
  5. `Assigned worker prompt history path: <absolute-path>`
48
48
  6. `**Model:** Report writer worker, <modelExecutionValue>` (resolved per Phase 5.5 anchor-header rules)
49
- 7. The full `[Required reading]` clause (see [okstra-team-contract](../okstra-team-contract/SKILL.md)) including `schemas/final-report-v1.0.schema.json` and `templates/reports/final-report.template.md` (template is read-only the worker writes the data.json that drives it).
50
- 8. The verbatim `## Available MCP Servers` block from the task brief, if present.
51
- 9. The convergence classifications (Full/Partial/Contested/Worker-Unique), the round history table (`roundHistory[]`), the `round2SkippedReason` value, and pointers to all worker result files under `worker-results/`. The report-writer worker must reproduce a Round History sub-table in Section 1 of the final report so the reader can see which rounds executed, queue sizes, and why Round 2 was (or was not) skipped.
49
+ 7. The full `[Required reading]` clause (see [okstra-team-contract](../okstra-team-contract/SKILL.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:
50
+ - `<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 contract for the data.json shape. 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.)
51
+ - `<instruction-set>/final-report-template.md` the **phase-stripped** template (every other task-type's §4.x deliverable block removed by `render.py`'s `_strip_phase_blocks`, leaving only your run's §4.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).
52
+ 8. A one-line MCP pointer instead of the verbatim block — `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).` The brief is already in the report-writer's Required reading (item 7), so the verbatim block is redundant.
53
+ 9. The convergence classifications (Full/Partial/Contested/Worker-Unique), the round history data (`roundHistory[]`), the `round2SkippedReason` value, and pointers to all worker result files under `worker-results/`. The report-writer worker populates `crossVerification.roundHistory` in the data.json so Section 1 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.
52
54
  10. `**Report Language:** <en|ko>` — must be either `en` or `ko`; `auto`
53
55
  has been resolved by the lead from project.json / global config
54
56
  before the dispatch is constructed. The worker copies this verbatim
@@ -89,7 +91,7 @@ The four steps below MUST execute in this exact order. Reordering them is the re
89
91
  ```
90
92
 
91
93
  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).
92
- 3. **Phase 7 step 1.5 — Render report views** (BLOCKING). Produces the self-contained HTML view from the now-substituted final-report MD:
94
+ 3. **Phase 7 step 1.5 — Render report views** (BLOCKING, conditional output). Always invoke the renderer; it decides whether an html sibling is warranted:
93
95
 
94
96
  ```bash
95
97
  python3 scripts/okstra-render-report-views.py \
@@ -97,9 +99,10 @@ The four steps below MUST execute in this exact order. Reordering them is the re
97
99
  ```
98
100
 
99
101
  Output (idempotent — re-running overwrites):
100
- - `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view. Section 5 `C-*` 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)) that the user pastes to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md`. The original final-report MD is **never** mutated by user input — the sidecar is the single write target.
102
+ - `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` — single-file self-contained human view, **generated only when the report has at least one §5 `C-*` clarification row**. Those 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)) that the user pastes to `runs/<task-type>/user-responses/user-response-<task-type>-<seq>.md`. The original final-report MD is **never** mutated by user input — the sidecar is the single write target.
103
+ - When the report has **no** `C-*` clarification rows, 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 clarification-free runs — `validators/validate-report-views.py` treats "no C-* rows + no html" as a pass, not a missing artifact.
101
104
 
102
- Must run AFTER step 1 (so token placeholders are substituted in the rendered html) and BEFORE step 2 (so the html artifact exists for any validator step that checks it).
105
+ Must run AFTER step 1 (so token placeholders are substituted in any rendered html) and BEFORE step 2 (so the html artifact, when generated, exists for the validator step that checks it).
103
106
  4. **Phase 7 step 2 — Follow-up task spawner** (BLOCKING when Section 7 is non-empty). Turns the report's `## 7. Follow-up Tasks (후속 작업)` rows into `tasks/<task-group>/<new-task-id>/` stubs.
104
107
 
105
108
  ```bash
@@ -255,7 +258,7 @@ Skipping this file because "the real report is in `reports/`" is wrong. Both fil
255
258
 
256
259
  ### Main Body Section
257
260
 
258
- Section numbering follows `templates/reports/final-report.template.md` exactly — that file is the single source of truth. Below is a one-line summary of each section's writer obligation; consult the template for full body structure.
261
+ 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 instruction-set copy of the same template, with other task-types' §4.x deliverable blocks removed); 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.
259
262
 
260
263
  **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 `## 2. Final Verdict` and the first item of `## 6.`. Divergence is `contract-violated`.
261
264
 
@@ -313,7 +316,7 @@ Persistence steps that must be performed in Phase 7:
313
316
  - [ ] 6. **Generate final status file**: `runs/<task-type>/status/final-<task-type>-<seq>.status` (if necessary)
314
317
  - [ ] 7. **Save convergence state**: `runs/<task-type>/state/convergence-<task-type>-<seq>.json` (when convergence is enabled)
315
318
  - [ ] 8. **Spawn follow-up task stubs**: run `scripts/okstra-spawn-followups.py` against the final-report per the canonical spawn rule defined in "Phase 7 follow-up task spawner" above. Do not restate the trigger condition here — that section is the single source of truth. The script is idempotent across reruns.
316
- - [ ] 9. **Human HTML report**: `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` (produced by Phase 7 step 1.5 self-contained, embeds `Export user response` button)
319
+ - [ ] 9. **Human HTML report** (conditional): `runs/<task-type>/reports/final-report-<task-type>-<seq>.html` produced by Phase 7 step 1.5 **only when the report has ≥1 §5 `C-*` clarification row** (self-contained, embeds `Export user response` button). Clarification-free reports legitimately have no html sibling; do not treat its absence as a missing artifact.
317
320
 
318
321
  ### Response after Persistence
319
322
 
@@ -24,10 +24,10 @@ okstra tasks are always operated using the `Claude lead` + required worker team
24
24
  | Role | Core responsibility | Specialization lens (Section 6 only) | subagent_type | Notes |
25
25
  |------|------|------|---------------|------|
26
26
  | Claude lead | orchestration + convergence supervision + final-report review/approval | — | -- | Does NOT author the final-report file when `Report writer worker` is in the roster |
27
- | Claude worker | Answer every brief question across feasibility, requirement interpretation, hidden assumptions, and alternatives — with file:line evidence | broad reasoning depth, hidden assumptions, execution-risk surfacing | claude-worker | `agents/claude-worker.md` |
28
- | Codex worker | Same core responsibility as Claude worker — identical questions, identical sections 1–5 | implementation realism, code-path implications, edge cases, technical trade-offs | codex-worker | `agents/codex-worker.md` |
29
- | Gemini worker | Same core responsibility as Claude worker — identical questions, identical sections 1–5 | requirement interpretation, consistency, safety, alternative viewpoints | gemini-worker | `agents/gemini-worker.md` |
30
- | Report writer worker | **Authors** the final-report file in Phase 6. NOT an analysis worker. | — | report-writer-worker | `agents/report-writer-worker.md`. Excluded from Phase 4/5 and convergence |
27
+ | Claude worker | Answer every brief question across feasibility, requirement interpretation, hidden assumptions, and alternatives — with file:line evidence | broad reasoning depth, hidden assumptions, execution-risk surfacing | claude-worker | `agents/workers/claude-worker.md` |
28
+ | Codex worker | Same core responsibility as Claude worker — identical questions, identical sections 1–5 | implementation realism, code-path implications, edge cases, technical trade-offs | codex-worker | generated from `agents/workers/_cli-wrapper-template.md` + `codex-worker.params.json` |
29
+ | Gemini worker | Same core responsibility as Claude worker — identical questions, identical sections 1–5 | requirement interpretation, consistency, safety, alternative viewpoints | gemini-worker | generated from `agents/workers/_cli-wrapper-template.md` + `gemini-worker.params.json` |
30
+ | Report writer worker | **Authors** the final-report file in Phase 6. NOT an analysis worker. | — | report-writer-worker | `agents/workers/report-writer-worker.md`. Excluded from Phase 4/5 and convergence |
31
31
 
32
32
  **Model assignment has no default.** The model for every role comes from `resultContract.requiredWorkerRoles[*].modelExecutionValue` in `task-manifest.json` (and lead model metadata). There is no per-role hard-coded fallback — see "Model Assignment Rules" below.
33
33
 
@@ -80,7 +80,7 @@ The body must include: role name, task type, task key, required bundle paths, as
80
80
 
81
81
  When a worker reads any project-relative path from the prompt, it MUST resolve it against `Project Root` (e.g. `<Project Root>/<Result Path>`) — never use bare relative paths that depend on cwd.
82
82
 
83
- If the task brief contains an `## Available MCP Servers` section, copy that section verbatim into every analysis worker's prompt (and into the report-writer prompt when it is dispatched in Phase 6). Codex/Gemini workers run external CLIs whose MCP availability is governed by their own CLI configs; still forward the section so they can record `MCP not available in this CLI` cleanly when their CLI lacks the matching server.
83
+ If the task brief contains an `## Available MCP Servers` section, inject only the one-line pointer into every analysis worker's prompt (and into the report-writer prompt when it is dispatched in Phase 6) — the brief is already in every worker's [Required reading], so verbatim copy is redundant: `**MCP servers:** follow the task brief's "## Available MCP Servers" section (already in your Required reading).` Codex/Gemini workers run external CLIs whose MCP availability is governed by their own CLI configs; they can record `MCP not available in this CLI` cleanly after reading that section in the brief.
84
84
 
85
85
  Before dispatching any required worker, lead persists the exact worker prompt to the assigned current-run prompt history path under `runs/<task-type>/prompts/`. Do not use `/tmp/*prompt*.txt` as the canonical artifact path.
86
86
 
@@ -100,7 +100,7 @@ Audience-scoped file enumeration (BLOCKING — performance optimization):
100
100
  | Recipient | Files the lead lists under `## Inputs` |
101
101
  |---|---|
102
102
  | Claude / Codex / Gemini analysis workers | task-brief, analysis-profile, analysis-material (if present), reference-expectations, clarification-response (if carry-in) |
103
- | Report writer worker (Phase 6) | all of the above **plus** `final-report-template.md` |
103
+ | Report writer worker (Phase 6) | all of the above **plus** the instruction-set-local `final-report-template.md` (phase-stripped) and `final-report-schema.json` (per-task-type excerpt) — NOT the full `templates/reports/...` / `schemas/...` sources |
104
104
  | Reverify dispatches | none — the lead provides only the items to reverify |
105
105
 
106
106
  Asymmetry note: `claude-worker` runs in-process and the Agent SDK auto-loads its agent definition; lead's dispatch prompt body for claude-worker can therefore be shorter than for codex/gemini. The Worker Preamble pointer is still emitted for all three so the contract source is identical regardless of dispatch path.
@@ -318,41 +318,33 @@ Every worker result file under `worker-results/` must begin with a standardized
318
318
  ```markdown
319
319
  # <Role> Analysis — <task-key>
320
320
 
321
- **Task:** <task-type>
322
321
  **Target:** <path or scope> <!-- OPTIONAL: include when the run is scoped to a specific file/module -->
323
- **Date:** <YYYY-MM-DD>
324
322
  **Model:** <Role>, <AI model>
325
323
  ```
326
324
 
327
- The `Target:` line is optional. Include it when the run is scoped to a specific path or module; omit it when the run spans the whole project. When included, place it between `Task:` and `Date:` as shown.
325
+ Task-type and date are **not** repeated in this human header — they already live in the YAML frontmatter (`taskType`, `date`), which is the copy Obsidian indexes. Restating them here added a third, un-indexed, machine-unparsed copy with no value; the frontmatter is the single source for both. The `Target:` line is optional include it when the run is scoped to a specific path or module, omit it for whole-project runs; when present, place it between the title and the `Model:` line.
328
326
 
329
327
  Examples:
330
328
 
331
329
  ```markdown
332
330
  # Claude Worker Analysis — jobs:tasks:8852
333
331
 
334
- **Task:** error-analysis
335
332
  **Target:** server/auth.ts
336
- **Date:** 2026-04-06
337
- **Model:** Claude worker, sonnet
333
+ **Model:** Claude worker, opus
338
334
  ```
339
335
 
340
336
  ```markdown
341
337
  # Codex Worker Analysis — jobs:tasks:8852
342
338
 
343
- **Task:** error-analysis
344
339
  **Target:** server/auth.ts
345
- **Date:** 2026-04-06
346
340
  **Model:** Codex worker, <codex-model-id>
347
341
  ```
348
342
 
349
343
  ```markdown
350
344
  # Report Writer Worker Analysis — jobs:tasks:8852
351
345
 
352
- **Task:** error-analysis
353
346
  **Target:** server/auth.ts
354
- **Date:** 2026-04-06
355
- **Model:** Report writer worker, opus-4-6
347
+ **Model:** Report writer worker, opus
356
348
  ```
357
349
 
358
350
  Use the actual model identifier recorded in team-state (never invent a model ID — read it from `resultContract.requiredWorkerRoles[*].modelExecutionValue` or the tool response metadata).
@@ -96,7 +96,9 @@ approved: {{ frontmatter.approved | yaml_scalar }}
96
96
  | {{ t("tokenSummary.rowLead") }} | `{{ tokenUsage.lead.totalTokens | format_int }}` | `{{ tokenUsage.lead.billableTokens | format_int }}` | `{{ tokenUsage.lead.costUsd | format_usd }}` |
97
97
  | {{ t("tokenSummary.rowWorkerTotal") }} | `{{ tokenUsage.worker.totalTokens | format_int }}` | `{{ tokenUsage.worker.billableTokens | format_int }}` | `{{ tokenUsage.worker.costUsd | format_usd }}` |
98
98
  | {{ t("tokenSummary.rowGrandTotal") }} | **`{{ tokenUsage.grand.totalTokens | format_int }}`** | **`{{ tokenUsage.grand.billableTokens | format_int }}`** | **`{{ tokenUsage.grand.costUsd | format_usd }}`** |
99
+ {% if tokenUsage.cli and tokenUsage.cli.costUsd is not none and tokenUsage.cli.costUsd > 0 -%}
99
100
  | {{ t("tokenSummary.rowCliExtra") }} | | | `{{ tokenUsage.cli.costUsd | format_usd }}` |
101
+ {% endif %}
100
102
 
101
103
  {# At Phase 6 numeric cells are null and render as `--`.
102
104
  Phase 7's okstra-token-usage.py populates tokenUsage in data.json then
@@ -107,6 +109,7 @@ approved: {{ frontmatter.approved | yaml_scalar }}
107
109
  {% if crossVerification.roundHistory and not crossVerification.roundHistory.disabled -%}
108
110
  ### 1.0 Round History
109
111
 
112
+ {% if crossVerification.roundHistory.rounds | length > 1 -%}
110
113
  | Round | inputQueueSize | resolvedCount | carriedForwardCount | dispatches (worker:status:durationMs) | skippedWorkers (worker:reason) |
111
114
  |-------|----------------|---------------|----------------------|----------------------------------------|---------------------------------|
112
115
  {% for row in crossVerification.roundHistory.rounds -%}
@@ -114,6 +117,12 @@ approved: {{ frontmatter.approved | yaml_scalar }}
114
117
  {% endfor %}
115
118
 
116
119
  - `round2SkippedReason`: `{{ crossVerification.roundHistory.round2SkippedReason }}` ← {{ t("roundHistory.round2SkippedReasonNote") }}
120
+ {% elif crossVerification.roundHistory.rounds | length == 1 -%}
121
+ {% set r = crossVerification.roundHistory.rounds[0] -%}
122
+ - {{ t("roundHistory.singleRoundPrefix") }} resolved={{ r.resolvedCount }}, carriedForward={{ r.carriedForwardCount }}, round2SkippedReason=`{{ crossVerification.roundHistory.round2SkippedReason }}`
123
+ {% else -%}
124
+ - {{ t("roundHistory.noRoundsNote") }} round2SkippedReason=`{{ crossVerification.roundHistory.round2SkippedReason }}`
125
+ {% endif %}
117
126
 
118
127
  {% endif %}
119
128
  ### 1.1 Consensus
@@ -75,7 +75,9 @@
75
75
  "sourceItemsColumnNote": "`Source items` column rule is the same as §1.1."
76
76
  },
77
77
  "roundHistory": {
78
- "round2SkippedReasonNote": "value is one of `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only`"
78
+ "round2SkippedReasonNote": "value is one of `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only`",
79
+ "singleRoundPrefix": "Single round —",
80
+ "noRoundsNote": "No reverify rounds executed (all findings reached consensus at grouping)."
79
81
  },
80
82
  "implementationPlanning": {
81
83
  "optionInterfacesLabel": "Affected interfaces / public contracts / downstream consumers",
@@ -75,7 +75,9 @@
75
75
  "sourceItemsColumnNote": "`Source items` 컬럼 규칙은 §1.1 과 동일."
76
76
  },
77
77
  "roundHistory": {
78
- "round2SkippedReasonNote": "값은 `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only` 중 하나."
78
+ "round2SkippedReasonNote": "값은 `queue-empty | max-rounds-1 | all-reverify-non-result | not-skipped | convergence-disabled | single-analyser-only` 중 하나.",
79
+ "singleRoundPrefix": "단일 라운드 —",
80
+ "noRoundsNote": "재검증 라운드 미실행 (그룹핑 단계에서 전부 합의)."
79
81
  },
80
82
  "implementationPlanning": {
81
83
  "optionInterfacesLabel": "영향 인터페이스 / 공개 계약 / 다운스트림 소비자",
@@ -17,7 +17,7 @@ Different recipients need different files. Do NOT include `final-report-template
17
17
  | Recipient | Files included in `[Required reading]` |
18
18
  |---|---|
19
19
  | Claude / Codex / Gemini analysis workers | task-brief, analysis-profile, analysis-material (if present), reference-expectations, clarification-response (if carry-in) |
20
- | Report writer worker (Phase 6) | all of the above **plus** `final-report-template.md` |
20
+ | Report writer worker (Phase 6) | all of the above **plus** the instruction-set-local `final-report-template.md` (phase-stripped) and `final-report-schema.json` (per-task-type excerpt) — NOT the full `templates/reports/...` / `schemas/...` sources |
21
21
  | Reverify dispatches (Phase 5.5, lightweight mode) | **do NOT inject `[Required reading]` at all** — see [okstra-convergence](../skills/okstra-convergence/SKILL.md) "Reverify prompt: required-reading suppression". |
22
22
 
23
23
  ### Reading rules
@@ -80,9 +80,16 @@ def validate(report_path: Path) -> list[str]:
80
80
 
81
81
  md = report_path.read_text(encoding="utf-8")
82
82
  html_path = report_path.with_name(report_path.stem + ".html")
83
+ md_ids = _md_response_ids(md)
83
84
 
84
- # (1) sibling artifact exists
85
+ # (1) sibling artifact exists — conditional on §5 clarification rows.
86
+ # The html view's sole value over the MD is its embedded form widgets
87
+ # for §5 C-* rows, so a clarification-free report intentionally has no
88
+ # html sibling (see report_views.report_has_clarification_items). When
89
+ # there are no C-* rows and no html, that is the expected skip state.
85
90
  if not html_path.is_file():
91
+ if not md_ids:
92
+ return []
86
93
  return [f"missing html artifact: {html_path}"]
87
94
 
88
95
  html_text = html_path.read_text(encoding="utf-8")
@@ -119,7 +126,7 @@ def validate(report_path: Path) -> list[str]:
119
126
  # (5) Response ID parity: HTML form rows ↔ §5 C-* rows in MD.
120
127
  # Bidirectional — catches both "MD has C-* the HTML lost" AND
121
128
  # "HTML has stale C-* that the current MD no longer declares".
122
- md_ids = _md_response_ids(md)
129
+ # (md_ids computed once at the top.)
123
130
  html_ids = sorted(set(_RESPONSE_ID_ATTR_RE.findall(html_text)))
124
131
  if md_ids != html_ids:
125
132
  failures.append(
@@ -1540,9 +1540,11 @@ def _rerender_report_views_after_autofix(report_path: Path) -> str:
1540
1540
  source_report=report_path.name,
1541
1541
  )
1542
1542
  try:
1543
- render_html_view(report_path, run_meta=meta, css=css, js=js)
1543
+ rendered = render_html_view(report_path, run_meta=meta, css=css, js=js)
1544
1544
  except Exception as exc: # noqa: BLE001
1545
1545
  return f"report-views re-render failed: {exc}"
1546
+ if rendered is None:
1547
+ return "report-views skipped (no §5 clarification rows)"
1546
1548
  return "report-views re-rendered"
1547
1549
 
1548
1550
 
package/src/install.mjs CHANGED
@@ -589,11 +589,24 @@ export async function runInstall(args) {
589
589
  join(paths.home, "templates"),
590
590
  { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 },
591
591
  );
592
+ // schemas/ tree — final-report-v1.0.schema.json is loaded at runtime by
593
+ // okstra_ctl.final_report_schema.load_schema() (parent-walk from the
594
+ // installed okstra_ctl package finds ~/.okstra/schemas/). It is needed by
595
+ // (a) prepare_task_bundle to write the per-task-type schema excerpt into
596
+ // each run's instruction-set, and (b) validate-run's data.json schema
597
+ // check. Without this step copy-mode installs leave load_schema() unable
598
+ // to locate the schema. Link mode resolves it through the repo symlink.
599
+ const schemasResult = await copyTreeIfChanged(
600
+ join(runtimeRoot, "schemas"),
601
+ join(paths.home, "schemas"),
602
+ { refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 },
603
+ );
592
604
 
593
605
  if (!opts.quiet) {
594
606
  summarise("python", pythonResult, paths.pythonpath);
595
607
  summarise("bin", binResult, paths.bin);
596
608
  summarise("templates", templatesResult, join(paths.home, "templates"));
609
+ summarise("schemas", schemasResult, join(paths.home, "schemas"));
597
610
  }
598
611
 
599
612
  if (pythonResult.missingSource && binResult.missingSource) {
@@ -606,6 +619,11 @@ export async function runInstall(args) {
606
619
  "warning: runtime/templates is empty. report.css / report.js will be missing — re-run the build step.\n",
607
620
  );
608
621
  }
622
+ if (schemasResult.missingSource) {
623
+ process.stderr.write(
624
+ "warning: runtime/schemas is empty. final-report schema validation + excerpt generation will be unavailable — re-run the build step.\n",
625
+ );
626
+ }
609
627
 
610
628
  const skillResult = await installSkillsCopy(runtimeRoot, opts);
611
629
  await writeSkillsManifest(paths.home, skillResult.installed, { dryRun: opts.dryRun });
@@ -682,6 +700,9 @@ export async function runEnsureInstalled(args) {
682
700
  }
683
701
  if (!(await dirExists(paths.pythonpath))) reasons.push(`missing ${paths.pythonpath}`);
684
702
  if (!(await dirExists(paths.agents))) reasons.push(`missing agents dir ${paths.agents}`);
703
+ if (!(await fileExists(join(paths.home, "schemas", "final-report-v1.0.schema.json")))) {
704
+ reasons.push(`missing ${join(paths.home, "schemas", "final-report-v1.0.schema.json")}`);
705
+ }
685
706
  if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
686
707
  reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
687
708
  }