okstra 0.202.0 → 0.204.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 (258) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-registry.mjs +7 -7
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/lifecycle/install.mjs +50 -124
  5. package/dist/commands/lifecycle/install.mjs.map +1 -1
  6. package/dist/commands/memory/memory.mjs +41 -8
  7. package/dist/commands/memory/memory.mjs.map +1 -1
  8. package/dist/lib/install-assets.mjs +3 -0
  9. package/dist/lib/install-assets.mjs.map +1 -1
  10. package/dist/lib/runtime-manifest.mjs +2 -1
  11. package/dist/lib/runtime-manifest.mjs.map +1 -1
  12. package/dist/lib/types.d.mts +2 -1
  13. package/docs/architecture/storage-model.md +14 -11
  14. package/docs/architecture.md +25 -19
  15. package/docs/cli.md +15 -12
  16. package/docs/contributor-change-matrix.md +3 -2
  17. package/docs/performance-improvement-plan-v2.md +2 -3
  18. package/docs/project-structure-overview.md +35 -9
  19. package/docs/task-process/README.md +1 -1
  20. package/docs/task-process/common-flow.md +1 -1
  21. package/docs/task-process/final-verification.md +3 -1
  22. package/docs/task-process/implementation.md +1 -1
  23. package/docs/task-process/release-handoff.md +36 -39
  24. package/package.json +1 -2
  25. package/runtime/BUILD.json +2 -2
  26. package/runtime/agents/common.json +28 -0
  27. package/runtime/agents/operations/code-review.json +6 -0
  28. package/runtime/agents/operations/report-translation.json +6 -0
  29. package/runtime/agents/operations/schedule-verification.json +6 -0
  30. package/runtime/agents/roles/analyser.json +18 -0
  31. package/runtime/agents/roles/critic.json +18 -0
  32. package/runtime/agents/roles/designer.json +18 -0
  33. package/runtime/agents/roles/implementer.json +20 -0
  34. package/runtime/agents/roles/leader.json +20 -0
  35. package/runtime/agents/roles/planner.json +18 -0
  36. package/runtime/agents/roles/report-writer.json +19 -0
  37. package/runtime/agents/roles/translator.json +19 -0
  38. package/runtime/agents/roles/verifier.json +18 -0
  39. package/runtime/bin/lib/okstra/usage.sh +5 -5
  40. package/runtime/prompts/duties/acceptance-critic.json +32 -0
  41. package/runtime/prompts/duties/acceptance-verifier.json +32 -0
  42. package/runtime/prompts/duties/analysis-worker.json +32 -0
  43. package/runtime/prompts/duties/code-reviewer.json +32 -0
  44. package/runtime/prompts/duties/diagnosis-worker.json +32 -0
  45. package/runtime/prompts/duties/direction-selection-worker.json +32 -0
  46. package/runtime/prompts/duties/discovery-worker.json +32 -0
  47. package/runtime/prompts/duties/implementation-executor.json +32 -0
  48. package/runtime/prompts/duties/implementation-verifier.json +32 -0
  49. package/runtime/prompts/duties/lead.json +32 -0
  50. package/runtime/prompts/duties/planning-worker.json +36 -0
  51. package/runtime/prompts/duties/report-writer.json +32 -0
  52. package/runtime/prompts/duties/reverification-worker.json +32 -0
  53. package/runtime/prompts/duties/schedule-verifier.json +32 -0
  54. package/runtime/prompts/duties/scope-critic.json +32 -0
  55. package/runtime/prompts/duties/technical-verification-worker.json +32 -0
  56. package/runtime/prompts/duties/translator.json +32 -0
  57. package/runtime/prompts/launch.template.md +2 -1
  58. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  59. package/runtime/prompts/lead/convergence.md +4 -4
  60. package/runtime/prompts/lead/okstra-lead-contract.md +113 -4
  61. package/runtime/prompts/lead/plan-body-verification.md +6 -6
  62. package/runtime/prompts/lead/report-writer.md +3 -3
  63. package/runtime/prompts/profiles/_common-contract.md +2 -2
  64. package/runtime/prompts/profiles/_implementation-executor.md +4 -1
  65. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  66. package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
  67. package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
  68. package/runtime/prompts/profiles/error-analysis.json +39 -0
  69. package/runtime/prompts/profiles/error-analysis.md +0 -25
  70. package/runtime/prompts/profiles/feature-analysis.json +31 -0
  71. package/runtime/prompts/profiles/feature-analysis.md +0 -20
  72. package/runtime/prompts/profiles/final-verification.json +30 -0
  73. package/runtime/prompts/profiles/final-verification.md +3 -22
  74. package/runtime/prompts/profiles/forbidden-actions.json +4 -3
  75. package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
  76. package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
  77. package/runtime/prompts/profiles/implementation-planning.json +40 -0
  78. package/runtime/prompts/profiles/implementation-planning.md +4 -29
  79. package/runtime/prompts/profiles/implementation.json +30 -0
  80. package/runtime/prompts/profiles/implementation.md +1 -20
  81. package/runtime/prompts/profiles/improvement-discovery.json +31 -0
  82. package/runtime/prompts/profiles/improvement-discovery.md +0 -20
  83. package/runtime/prompts/profiles/project-analysis.json +31 -0
  84. package/runtime/prompts/profiles/project-analysis.md +0 -20
  85. package/runtime/prompts/profiles/release-handoff.json +5 -0
  86. package/runtime/prompts/profiles/release-handoff.md +71 -73
  87. package/runtime/prompts/profiles/requirements-discovery.json +39 -0
  88. package/runtime/prompts/profiles/requirements-discovery.md +0 -25
  89. package/runtime/prompts/profiles/technical-verification.json +39 -0
  90. package/runtime/prompts/profiles/technical-verification.md +0 -25
  91. package/runtime/prompts/wizard/prompts.ko.json +12 -17
  92. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
  93. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
  94. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
  95. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
  96. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
  97. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
  98. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
  99. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
  100. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
  101. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
  102. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
  103. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
  104. package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
  105. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
  106. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
  107. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
  108. package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
  109. package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
  110. package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
  111. package/runtime/python/okstra_ctl/cmux.py +69 -20
  112. package/runtime/python/okstra_ctl/code_review_target.py +16 -8
  113. package/runtime/python/okstra_ctl/conformance.py +43 -0
  114. package/runtime/python/okstra_ctl/consumers.py +6 -3
  115. package/runtime/python/okstra_ctl/container.py +31 -8
  116. package/runtime/python/okstra_ctl/context_cost.py +11 -15
  117. package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
  118. package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
  119. package/runtime/python/okstra_ctl/design_prep.py +34 -1
  120. package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
  121. package/runtime/python/okstra_ctl/domain/host.py +5 -0
  122. package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
  123. package/runtime/python/okstra_ctl/error_report.py +4 -3
  124. package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
  125. package/runtime/python/okstra_ctl/handoff.py +167 -277
  126. package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
  127. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
  128. package/runtime/python/okstra_ctl/lead_progress.py +1 -1
  129. package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
  130. package/runtime/python/okstra_ctl/manager_cli.py +92 -4
  131. package/runtime/python/okstra_ctl/manager_launch.py +1 -1
  132. package/runtime/python/okstra_ctl/manager_paths.py +14 -3
  133. package/runtime/python/okstra_ctl/manager_store.py +210 -3
  134. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  135. package/runtime/python/okstra_ctl/manager_view.py +2 -1
  136. package/runtime/python/okstra_ctl/model_discovery.py +30 -0
  137. package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
  138. package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
  139. package/runtime/python/okstra_ctl/models.py +1 -1
  140. package/runtime/python/okstra_ctl/next_phase.py +16 -6
  141. package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
  142. package/runtime/python/okstra_ctl/option_comparison.py +168 -0
  143. package/runtime/python/okstra_ctl/path_hints.py +9 -0
  144. package/runtime/python/okstra_ctl/paths.py +3 -0
  145. package/runtime/python/okstra_ctl/profile_show.py +42 -1
  146. package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
  147. package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
  148. package/runtime/python/okstra_ctl/render.py +50 -0
  149. package/runtime/python/okstra_ctl/report_contract.py +1 -1
  150. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
  151. package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
  152. package/runtime/python/okstra_ctl/report_translation.py +2 -1
  153. package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
  154. package/runtime/python/okstra_ctl/role_requirements.py +142 -129
  155. package/runtime/python/okstra_ctl/rollup.py +3 -1
  156. package/runtime/python/okstra_ctl/run.py +76 -29
  157. package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
  158. package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
  159. package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
  160. package/runtime/python/okstra_ctl/stage_map.py +16 -2
  161. package/runtime/python/okstra_ctl/stage_targets.py +209 -43
  162. package/runtime/python/okstra_ctl/team.py +22 -13
  163. package/runtime/python/okstra_ctl/time_report.py +2 -1
  164. package/runtime/python/okstra_ctl/usage_report.py +3 -1
  165. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
  166. package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
  167. package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
  168. package/runtime/python/okstra_ctl/wizard/state.py +3 -5
  169. package/runtime/python/okstra_ctl/wizard/steps_plan.py +3 -23
  170. package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
  171. package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
  172. package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
  173. package/runtime/python/okstra_ctl/workflow.py +1 -1
  174. package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
  175. package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
  176. package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
  177. package/runtime/python/okstra_token_usage/pricing.py +6 -4
  178. package/runtime/schemas/agent-common-v1.schema.json +34 -0
  179. package/runtime/schemas/agent-duty-v1.schema.json +38 -0
  180. package/runtime/schemas/agent-operation-v1.schema.json +11 -0
  181. package/runtime/schemas/agent-profile-v1.schema.json +46 -0
  182. package/runtime/schemas/agent-role-v1.schema.json +29 -0
  183. package/runtime/schemas/final-report-v2.0.schema.json +118 -97
  184. package/runtime/schemas/final-report-v3.0.schema.json +118 -97
  185. package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
  186. package/runtime/skills/okstra-chat/SKILL.md +2 -2
  187. package/runtime/skills/okstra-code-review/SKILL.md +23 -9
  188. package/runtime/skills/okstra-container-build/SKILL.md +10 -10
  189. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  190. package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
  191. package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
  192. package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
  193. package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
  194. package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
  195. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  196. package/runtime/skills/okstra-inspect/facets/status.md +4 -3
  197. package/runtime/skills/okstra-inspect/facets/time.md +11 -10
  198. package/runtime/skills/okstra-manager/SKILL.md +18 -2
  199. package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
  200. package/runtime/skills/okstra-rollup/SKILL.md +5 -5
  201. package/runtime/skills/okstra-run/SKILL.md +31 -12
  202. package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
  203. package/runtime/skills/okstra-setup/SKILL.md +12 -10
  204. package/runtime/skills/okstra-setup/references/project-config.md +7 -6
  205. package/runtime/skills/okstra-usage/SKILL.md +1 -1
  206. package/runtime/skills/okstra-user-response/SKILL.md +1 -1
  207. package/runtime/templates/manager/view.template.html +1 -0
  208. package/runtime/templates/report-writer-prompt-preamble.md +8 -0
  209. package/runtime/templates/reports/brief.template.md +14 -4
  210. package/runtime/templates/reports/html/i18n/en.json +5 -4
  211. package/runtime/templates/reports/html/i18n/ko.json +5 -4
  212. package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
  213. package/runtime/templates/reports/i18n/en.json +1 -1
  214. package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
  215. package/runtime/templates/reports/release-handoff-input.template.md +6 -4
  216. package/runtime/templates/translator-prompt-preamble.md +36 -0
  217. package/runtime/validators/checks/validate-assets-01.py +7 -8
  218. package/runtime/validators/validate-brief.py +70 -0
  219. package/runtime/validators/validate-implementation-plan-stages.py +2 -1
  220. package/runtime/validators/validate-run.py +59 -9
  221. package/runtime/validators/validate-schedule.py +9 -0
  222. package/docs/for-ai/README.md +0 -68
  223. package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
  224. package/docs/for-ai/skills/okstra-chat.md +0 -34
  225. package/docs/for-ai/skills/okstra-code-review.md +0 -57
  226. package/docs/for-ai/skills/okstra-container-build.md +0 -129
  227. package/docs/for-ai/skills/okstra-inspect.md +0 -262
  228. package/docs/for-ai/skills/okstra-manager.md +0 -86
  229. package/docs/for-ai/skills/okstra-memory.md +0 -126
  230. package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
  231. package/docs/for-ai/skills/okstra-rollup.md +0 -114
  232. package/docs/for-ai/skills/okstra-run.md +0 -250
  233. package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
  234. package/docs/for-ai/skills/okstra-setup.md +0 -167
  235. package/docs/for-ai/skills/okstra-usage.md +0 -29
  236. package/docs/for-ai/skills/okstra-user-response.md +0 -72
  237. package/runtime/agents/workers/claude-worker.md +0 -128
  238. package/runtime/agents/workers/report-writer-worker.md +0 -37
  239. package/runtime/agents/workers/translator-worker.md +0 -63
  240. package/runtime/prompts/duties/acceptance-critic.md +0 -44
  241. package/runtime/prompts/duties/acceptance-verifier.md +0 -44
  242. package/runtime/prompts/duties/analysis-worker.md +0 -44
  243. package/runtime/prompts/duties/code-reviewer.md +0 -44
  244. package/runtime/prompts/duties/common.md +0 -39
  245. package/runtime/prompts/duties/diagnosis-worker.md +0 -44
  246. package/runtime/prompts/duties/direction-selection-worker.md +0 -44
  247. package/runtime/prompts/duties/discovery-worker.md +0 -44
  248. package/runtime/prompts/duties/implementation-executor.md +0 -44
  249. package/runtime/prompts/duties/implementation-verifier.md +0 -44
  250. package/runtime/prompts/duties/lead.md +0 -44
  251. package/runtime/prompts/duties/planning-worker.md +0 -52
  252. package/runtime/prompts/duties/report-writer.md +0 -44
  253. package/runtime/prompts/duties/reverification-worker.md +0 -44
  254. package/runtime/prompts/duties/schedule-verifier.md +0 -44
  255. package/runtime/prompts/duties/scope-critic.md +0 -44
  256. package/runtime/prompts/duties/technical-verification-worker.md +0 -44
  257. package/runtime/prompts/duties/translator.md +0 -44
  258. package/runtime/python/okstra_ctl/pane_title.py +0 -154
@@ -8,7 +8,7 @@ description: |
8
8
 
9
9
  Cross-task roll-up: collect every catalog task's run results (optionally scoped to one task-group) and summarize them together. The `rollup` CLI does all deterministic aggregation — counts, time sums, error tallies, status/category/phase distributions. You resolve scope, call it, render the table, and synthesize the prose digest from the report files. **Never recompute totals or re-tally by hand** — the CLI is the SSOT for the numbers.
10
10
 
11
- This skill is read-only. It never mutates task artifacts.
11
+ The skill itself writes nothing. `okstra rollup` resolves every catalog task by task-key, and that lookup self-heals a finished implementation phase. When a task's current phase is `implementation`, that lookup appends a `done` row to `runs/implementation-planning/consumers.jsonl` for each stage whose carry file is complete but has no settled row, and when every stage of the latest plan has a `done` row and a pass-grade carry it marks the phase completed in `task-manifest.json` (`workflow`, `phaseOutcome.implementation`) and refreshes that task's catalog entry. Nothing else is written.
12
12
 
13
13
  ## Step 0: Preflight
14
14
 
@@ -56,15 +56,15 @@ Convert each returned `ms` label to `HH:MM:SS` (zero-pad; never show raw ms). CP
56
56
  **workStatus:** done 1 · in-progress 1 **category:** bugfix 1 · feature 1
57
57
  ```
58
58
 
59
- Render `Report` as `✓` when the corresponding `Task N report path` is non-empty, else `—`. Use the numbered fixed labels and total labels verbatim; do not re-count tasks.
59
+ Render `Report` as `✓` when the corresponding `Task N report path` holds a path, else `—`. An absent path prints as `-`. Use the numbered fixed labels and total labels verbatim; do not re-count tasks.
60
60
 
61
61
  ## Step 4: Synthesize the digest (the summary)
62
62
 
63
63
  This is the skill's value-add over a bare table. When the user asked to "summarize"/"digest"/"organize" (the common case), produce a short cross-task narrative:
64
64
 
65
- 1. For each task with a non-empty `reportPath` whose file exists under `<projectRoot>/<reportPath>`, read it and write a 1–2 line summary of what it accomplished and its recommended next step.
66
- 2. Above the per-task lines, write a 2–4 sentence group-level synthesis: what was delivered across the group, where the open work sits (use `byWorkStatus`/`byCurrentPhase`), and any error hot-spots (tasks with high `errorCount`).
67
- 3. Cite each per-task claim with the report path as `<reportPath>` so the reader can open it.
65
+ 1. For each task whose `Task N report path` is not `-` and whose file exists under `<projectRoot>/<Task N report path>`, read it and write a 1–2 line summary of what it accomplished and its recommended next step.
66
+ 2. Above the per-task lines, write a 2–4 sentence group-level synthesis: what was delivered across the group, where the open work sits (use the `Work status N name/count` and `Current phase N name/count` lines), and any error hot-spots (tasks with a high `Task N errors`).
67
+ 3. Cite each per-task claim with its `Task N report path` value so the reader can open it.
68
68
 
69
69
  For tasks with no report, state the current phase/workStatus instead of inventing a summary — do not read non-report artifacts to fill the gap.
70
70
 
@@ -32,10 +32,10 @@ Every wizard call returns JSON. The two shapes you'll see:
32
32
  "progress": { "index": 5, "total": 11, "remaining": 6 } } }
33
33
  ```
34
34
 
35
- Every non-terminal `next` carries a `progress` object (`done` / `aborted` omit it). It holds `index` (1-based number of the **current screen**; pick_group counts as one screen), `total` (the wizard's forward estimate of the full screen count — may grow by a few when the user opens a branch, e.g. role-add or extra role-model slots), `remaining` (`total − index`), and **`label`** — the ready-to-render marker the wizard already composed (e.g. `Step 10/11 · 1 step remaining`, or `Step 11/11 · final step` on the final screen). **Always suffix the rendered prompt with `progress.label` verbatim** — see Step 3. Do not recompute the marker or decide "final step" yourself; only the wizard knows whether more screens follow.
35
+ Every non-terminal `next` carries a `progress` object (`done` / `aborted` omit it). It holds `index` (1-based number of the **current screen**; pick_group counts as one screen), `total` (the wizard's forward estimate of the full screen count — may grow by a few when the user opens a branch, e.g. role-add or extra role-model slots), `remaining` (`total − index`), and **`label`** — the ready-to-render marker the wizard already composed (e.g. `Step 10/11 · 앞으로 1 스텝 남음`, or `Step 11/11 · 마지막 단계` on the final screen; the suffix is Korean whatever the report language). **Always suffix the rendered prompt with `progress.label` verbatim** — see Step 3. Do not recompute the marker or decide "final step" yourself; only the wizard knows whether more screens follow.
36
36
 
37
37
  ```json
38
- { "ok": false, "error": "approved plan has no APPROVED marker: ...",
38
+ { "ok": false, "error": "approved plan §1 has 1 unresolved `Blocks=approval` row(s); ...",
39
39
  "current": { "step": "approved_plan", "kind": "text", "label": "..." } }
40
40
  ```
41
41
 
@@ -61,7 +61,7 @@ The final `confirm` step is a normal `pick` step with three options — `Proceed
61
61
 
62
62
  Never invent additional questions. **Never drop, hide, merge, reorder, or truncate** a `pick` / `pick_group` option — relay every `options[]` entry, including entries that carry a `(default)` / `(recommended)` suffix. Do not collapse a multi-option pick into a "recommended + Enter directly / Other" shortlist. The wizard's arrays are the complete authoritative choice sets, regardless of the current host UI's usual option limit. The run-prompt recommendation rule (1–2 recommendations + Enter directly) shapes the **option set** only for prompts this skill authors itself, never for wizard-provided options — you may not add, drop, or reorder a wizard option to produce a shortlist. It does not excuse you from recommending: before relaying a wizard step whose answer turns on something readable (the carried report, the sidecar the user already wrote, the prior Stage Map), read it, put what you found in the question body, and name which of the wizard's own options you recommend and why. Relaying a step with no context and no recommendation hands the whole question back to the user — see the lifecycle core contract "Asking the user (BLOCKING)".
63
63
 
64
- **One recommendation, shown in one place, and it is option 1.** The option carrying `recommended: true` is what this run computed, and the wizard already placed it first — append ` (추천)` to that option's label when you render it, and to no other. A checkbox step (`multi: true`) may flag several leading options: they are the recommended set, rendered the same way. Your prose recommendation names one of the flagged options. If you believe a different option is right, do not quietly recommend it in prose while the flagged one still reads as recommended on screen: say you disagree, name both, and let the user pick. When no option carries the flag, the run computed nothing for this step — recommend one from what you read and mark that one, and do not present the first option as a default just because it is first. **Enforced:** `scripts/okstra_ctl/wizard/state.py` `Prompt.__post_init__` refuses a step whose free-input option is not last, whose single-select recommendation is not exactly one option placed first (a checkbox step's recommended options must be its leading run), or that marks the free-input / abort escape as a recommendation.
64
+ **One recommendation, shown in one place, and it is option 1.** The option carrying `recommended: true` is what this run computed, and the wizard already placed it first — except on model-selection steps (`role-models:*`, `role-model:*`), which keep provider display order, so the flagged model may sit anywhere in the list. Append ` (추천)` to that option's label when you render it, and to no other. A checkbox step (`multi: true`) may flag several leading options: they are the recommended set, rendered the same way. Your prose recommendation names one of the flagged options. If you believe a different option is right, do not quietly recommend it in prose while the flagged one still reads as recommended on screen: say you disagree, name both, and let the user pick. When no option carries the flag, the run computed nothing for this step — recommend one from what you read and mark that one, and do not present the first option as a default just because it is first. **Enforced:** `scripts/okstra_ctl/wizard/state.py` `Prompt.__post_init__` refuses a step whose free-input option is not last, whose single-select recommendation is not exactly one option, whose recommendation is not placed first on a single-select step or is not the leading run on a checkbox step (both position checks are skipped on `role-models:*` / `role-model:*` steps), or that marks the free-input / abort escape as a recommendation.
65
65
 
66
66
  ## Step 1: Preflight
67
67
 
@@ -168,13 +168,13 @@ okstra wizard init \
168
168
  --available-function <each-additional-function-in-the-effective-intersection>
169
169
  ```
170
170
 
171
- Output: the same `{ok, next}` JSON described above. The first `next` is always `step: "task_pick"`.
171
+ Output: the same `{ok, next}` JSON described above. The first `next` is `step: "report_language"` when `.okstra/project.json` has no `reportLanguage` value; otherwise it is `step: "task_pick"`, except that when the brand-new option is the only `task_pick` choice, `init` answers it itself and the first `next` is the task-group step.
172
172
 
173
173
  ## Step 3: Run the prompt loop
174
174
 
175
175
  Repeat until `next.kind == "done"` (or `"aborted"` — terminal cancel, see "How the wizard talks to you"):
176
176
 
177
- 1. **Render** the prompt according to `next.interaction.kind` using the relay rules above. For a native kind, invoke the `function` string from that same `interactions` entry — the Claude Code picker, the Grok picker, and the Codex picker are different names, and substituting one for another is a relay-contract failure. **Always append the progress marker to the rendered question label**: suffix it with ` (<next.progress.label>)` — render `progress.label` exactly as the wizard sent it, never recompute it. Example: label `Step 8/11 · 3 steps remaining` → `Select a model (Step 8/11 · 3 steps remaining)`. Re-prompts after `ok: false` reuse `current.progress.label` the same way. The progress marker is presentation-only — never send it back to the wizard as part of an answer. **The rendered question is the last thing you emit in that turn.** Every host streams assistant text in emission order, so an echo, a restatement, a rationale, or a status line emitted after the question call renders *below* the picker and pushes the question away from where the user answers. Say whatever you need to say before the call, then call and stop.
177
+ 1. **Render** the prompt according to `next.interaction.kind` using the relay rules above. For a native kind, invoke the `function` string from that same `interactions` entry — the Claude Code picker, the Grok picker, and the Codex picker are different names, and substituting one for another is a relay-contract failure. **Always append the progress marker to the rendered question label**: suffix it with ` (<next.progress.label>)` — render `progress.label` exactly as the wizard sent it, never recompute it. Example: label `Step 8/11 · 앞으로 3 스텝 남음` → `Select a model (Step 8/11 · 앞으로 3 스텝 남음)`. Re-prompts after `ok: false` reuse `current.progress.label` the same way. The progress marker is presentation-only — never send it back to the wizard as part of an answer. **The rendered question is the last thing you emit in that turn.** Every host streams assistant text in emission order, so an echo, a restatement, a rationale, or a status line emitted after the question call renders *below* the picker and pushes the question away from where the user answers. Say whatever you need to say before the call, then call and stop.
178
178
  **One pending interaction per wizard state.** For native interactions, display the question and options only through the selected question tool, not as a second text list. An asynchronous acknowledgement such as `{accepted: true}` means the question is pending, not answered. Wait for the actual user reply; do not repeat the tool call or fetch and render the same step again while waiting. If ending the turn while awaiting a reply, do not restate the question in the final message. Re-prompt only on explicit user edits or a validation error after submitting an actual answer. This is a lead relay instruction; wizard state validation does not deduplicate host UI emissions.
179
179
  2. **Submit** the answer — call `okstra wizard step` with the literal state-file path from Step 2 and the literal user answer (no shell variables, no `$(...)`):
180
180
  ```bash
@@ -186,7 +186,7 @@ Repeat until `next.kind == "done"` (or `"aborted"` — terminal cancel, see "How
186
186
  ```bash
187
187
  okstra wizard step --state-file /var/folders/.../okstra-wizard.AbCd.json --answer ""
188
188
  ```
189
- Omitting `--answer` entirely is forbidden. The wizard interprets a missing `--answer` flag as "re-emit the current prompt" (a `get-current-prompt` style no-op), not as "submit empty" — so dropping the flag will loop the same prompt forever. Submitting `--answer ""` is the only way to advance past an intentionally-blank step (e.g. "use phase default").
189
+ Omitting `--answer` entirely is forbidden. The wizard does not read a missing `--answer` flag as "submit empty": without `--answer` or `--no-submit`, `wizard step` returns `ok: false` naming `--answer` and exits 2, and the step is not submitted. Submitting `--answer ""` is the only way to advance past an intentionally-blank step (e.g. "use phase default").
190
190
 
191
191
  **Escaping rule**: if the literal answer contains `"`, escape each occurrence as `\"` inside the double-quoted argument. Empty values must still be `--answer ""` — the flag itself is mandatory, even when the value is empty.
192
192
 
@@ -204,9 +204,9 @@ That is the entire interactive flow. The wizard handles:
204
204
  - analysis-input sub-flow — `project-analysis` has no target or evidence step. `feature-analysis` runs `project_evidence_pick` / `project_evidence`, then `analysis_target_pick` / `analysis_target`; the target is required even when the user skips project evidence. `change-impact-analysis` runs `feature_evidence_pick` / `feature_evidence`, then `project_evidence_pick` / `project_evidence`. The evidence steps show accepted compatible reports first and retain direct-path entry; do not invent a different relation or reorder these steps,
205
205
  - base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
206
206
  - `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = the earliest incomplete stage whose dependencies are satisfied, or a specific stage number). Implementer slots use role-count / role-model like every other role (`executor` is only a compatibility alias for `implementer`). When an approved plan is selected and a `## PLAN DECISION` sidecar carrying `Status: approved`, exported from the report — matching the plan on source-report·seq — is detected in that run's sibling `user-responses/`, the approve-confirm step expands to 3 options (`yes_apply` recommended: approve + apply the option as exported / `yes` approve only / `no` abort) — `yes_apply` validates the option against the plan's `optionCandidates` before applying it via the existing approval·option path,
207
- - `release-handoff`-only sub-flow: after the approved plan auto-resolves, a `handoff_stage_pick` multi-select — choose an eligible stage bundle (stage-group) or the whole task (when an accepted whole-task verification report exists); the result goes out as render-args' `stages` key (csv, empty when whole-task),
207
+ - `release-handoff`-only sub-flow: after the approved plan auto-resolves, a `handoff_stage_pick` multi-select — choose the eligible stages to open a PR for, one PR per stage; the result goes out as render-args' `stages` key (csv; empty takes every eligible stage),
208
208
  - launch selection after identity/worktree steps: one screen per static role, in profile order. A role that can run several instances (`max > 1`) is a checkbox step `role-models:<role>` (`multi: true`) — the number of models checked is the number of instances, there is no separate count question; the label states the profile range and recommended count, every executable candidate is listed on that one screen (defaults first, the recommended set flagged), and when the list exceeds the host's native checkbox limit the runtime either splits it into several checkbox questions on one `pick_group` screen (interaction plan `native-group`; the question steps are `role-models:<role>#1`, `#2`, … and the answer is one JSON object keyed by them, each value a CSV) when the host's native question group holds every option, or returns the interaction plan `numbered-multi` — render the whole list, never a shortlist or pages. An optional role (`min = 0`, e.g. critic) carries a `추가 안 함` row. A fixed single role (`min = max = 1`, e.g. report-writer) is a single pick `role-model:<role>:1`, and a role capped at one model (`max = 1`, e.g. critic) is a single pick on its `role-models:<role>` step; both split the same way when they exceed the native option limit — the tabs are checkbox questions, the answer is the same keyed JSON object, and the wizard rejects a screen whose tabs together name more than one value. current-session lead is this session and is listed on the confirmation summary, not as a wizard step. The wizard does not fork on defaults-vs-customize, does not show a provider roster multi-pick, and does not offer a separate implementer-provider pick. Dynamic verifiers are not chosen at launch. `--workers` is compatibility-only, not a launch picker. Repeated `--role-count` / `--role-model` tokens on `renderArgv` are intentional,
209
- - **resume-clarification (in-session equivalent)** — there is no separate mode or flag matching the shell's `okstra.sh --resume-clarification`; two steps of the standard flow carry out its substance. (1) `reuse_previous` (yes/no to reuse the previous run's settings — in `requirements-discovery` / `error-analysis` / `implementation-planning`, only when prior run-inputs exist): YES prefills role-count·role-model·directive·related-tasks at once. (2) `clarification_pick`: if the **task-type's own** previous `final-report` exists it is auto-recommended as the carry-in input (falling back to the newest by mtime across all phases when absent), and the same run's `user-responses/` sidecar (answers the user filled in) is attached alongside. The chosen path is passed to prepare as `--clarification-response` — the user makes the sidecar via the report's `Export user response`, places it in `runs/<task-type>/user-responses/`, and re-runs the same phase,
209
+ - **resume-clarification (in-session equivalent)** — there is no separate mode or flag matching the shell's `okstra.sh --resume-clarification`; two steps of the standard flow carry out its substance. (1) `reuse_previous` (yes/no to reuse the previous run's settings — in `requirements-discovery` / `error-analysis` / `implementation-planning` / `project-analysis` / `feature-analysis` / `change-impact-analysis`, only when prior run-inputs exist): YES prefills role-count·role-model·directive·related-tasks at once (and, for the analysis types, the target and evidence inputs). (2) `clarification_pick`: a `revision-requested` analysis report for the selected analysis type is recommended first; otherwise the **task-type's own** previous `final-report` is auto-recommended as the carry-in input (for `technical-verification`, the `implementation-option-selection` report), falling back to the newest by mtime across all phases when absent — except for `implementation-planning` and `technical-verification`, which get no cross-phase fallback. The approved plan is never recommended here. The same run's `user-responses/` sidecar (answers the user filled in) is attached alongside. The chosen path is passed to prepare as `--clarification-response` — the user makes the sidecar via the report's `Export user response`, places it in `runs/<task-type>/user-responses/`, and re-runs the same phase,
210
210
  - **re-verification scope (`reverify_scope_pick`, `implementation-planning` clarification re-runs only)** — asked right before `confirm` when the re-run is narrowable **or** an answered `C-NNN` traces to no stage. When every answered id traces to a stage: 3 options — `auto` (recommended — leave it to the lead's `okstra incremental-scope` decision) / `full` (re-verify every stage) / Enter directly (a stage-number CSV, validated against the prior report's Stage Map). When an id is unlinked, `auto` is omitted and the user names stages or picks `full`; that unlinked id does not freeze the run at full. The answer goes out as `--reverify-scope` and reaches the lead prompt as the `REVERIFY_SCOPE_MODE` / `REVERIFY_SCOPE_STAGES` tokens; it shapes that CLI's inputs rather than replacing the decision. The confirmation block's `reverify-scope` line names unlinked ids as needing stage numbers, not as a forced full re-run,
211
211
  - `release-handoff` PR template override + persist scope,
212
212
  - final `Proceed / Edit` confirmation; on `Edit` the wizard asks which step to rewind to and clears every later answer.
@@ -271,7 +271,7 @@ Before invoking it, follow the active host relay's execution-permission guidance
271
271
 
272
272
  Analysis sidetracks therefore forward wizard-owned tokens such as `--analysis-target "<value>"` and `--evidence-inputs "<value>"` when they are present. These are examples of the verbatim token rule, not a separate hard-coded argument list.
273
273
 
274
- Step 3's empty-answer and escaping rules apply verbatim: every flag in `renderArgv` whose following value is the empty string MUST still be passed explicitly (e.g. `--workers ""`, `--directive ""`) — `render-bundle` distinguishes "flag absent" from "flag present with empty value", and the wizard's intent is always the latter.
274
+ Step 3's empty-answer and escaping rules apply verbatim: every flag in `renderArgv` whose following value is the empty string MUST still be passed explicitly (e.g. `--workers ""`, `--directive ""`) — the wizard's intent is always "flag present with empty value", even where prepare's default happens to be the same empty value.
275
275
 
276
276
  `renderArgv` already contains exactly one `--lead-runtime <host-runtime>` pair. Do not add or replace it. Do not enumerate a fixed provider list in this skill because the wizard and role model pool own the ordered tokens.
277
277
 
@@ -299,9 +299,9 @@ Do not continue from the rendered prompt if this command fails. This record
299
299
  associates the current-session lead with a verified invocation specification;
300
300
  it does not claim that the host exposed or attested the delivered prompt bytes.
301
301
 
302
- The python function underneath is mutex-protected (`~/.okstra/.locks/<task-key>.lock`), writes `run-context-*.json` + `run-inputs-*.json` + all manifests + discovery files, and registers the run in `~/.okstra/recent.jsonl` with status `prepared`.
302
+ The python function underneath allocates the run sequence number and writes `run-context-*.json` under the per-task lock (`~/.okstra/.locks/<task-key>.lock`), then writes `run-inputs-*.json` + all manifests + discovery files outside that lock, and registers the run in `~/.okstra/recent.jsonl` with status `prepared` under the central `~/.okstra/.lock` (a registration failure is reported on stderr and does not fail prepare).
303
303
 
304
- **Enforced:** the same `tests-js/wizard.test.mjs` cases cover the argv this step passes through — `orderedRenderArgv` preserves the Python role selection bytes and repeated role order, so an empty value dropped here changes the rendered bundle and the golden comparison fails.
304
+ **Not enforced:** `tests-js/wizard.test.mjs` pins the argv the wizard produces (`orderedRenderArgv` preserves the Python role selection bytes and repeated role order), not the argv you pass. Prepare's parser defaults most flags to `""` (e.g. `--directive`), so a dropped empty value is indistinguishable from its default and nothing rejects it — passing every token verbatim is on you.
305
305
 
306
306
  You can delete the literal state-file path after this point — its job is done. Invoke `command rm` with the literal path (e.g. `command rm /var/folders/.../okstra-wizard.AbCd.json`), not a shell variable. `command` is what keeps a `rm='rm -i'` alias from turning this into a confirmation prompt nobody is there to answer.
307
307
 
@@ -462,9 +462,28 @@ The three branches that end or pause the queue — "Next stage not yet ready", "
462
462
 
463
463
  Do not read the wizard state file directly. `okstra wizard outcome` exposes any release-handoff PR template write as `outcome.persistActions[]`; execute those actions before `render-bundle`.
464
464
 
465
+ ## Comparison material for a direction choice
466
+
467
+ When the user has to choose an implementation direction — a finished `implementation-option-selection` run with routing `pending-direction-selection`, or the planning wizard's `selected_direction_pick` step — and asks for material comparing the directions, build it from the report record, not from memory or a hand read of the JSON:
468
+
469
+ ```bash
470
+ ~/.okstra/bin/okstra option-comparison --report <final-report-implementation-option-selection-NNN.data.json>
471
+ ```
472
+
473
+ At `selected_direction_pick` the report path is the option value. The command prints JSON; every number in the reply comes from it. Use the recorded `weightedScore` as printed; do not recompute it. Write the reply in the user's language, in this order:
474
+
475
+ 1. **Lead-in** — one or two sentences on what the directions share and the axes they split on, drawn from `narrative.comparisonOverview` and each option's `coreMechanism`.
476
+ 2. **Score table** — a Markdown table: one row per `criteria[]` entry in order, a weight column, one column per option (`<id> <short name>`), and a last row with each option's `weightedScore`. Write each criterion as its identifier with a translation in the user's language, e.g. `requirement-fit(요구사항 부합)`.
477
+ 3. **Coverage and blockers** — one line per option, or one line when they agree: `coverage.verdict`, the `coverage.requirementIds` it covers, and the `safetyBlockers` and `unresolvedFeasibilityFacts` counts.
478
+ 4. **Feasibility votes** — a Markdown table: one row per option, one column per `workers[]` entry, each cell that worker's `votes[<worker>].verdict`.
479
+ 5. **Per direction** — one paragraph per option, recommended first and marked as recommended, with its `weightedScore`: what it does (`coreMechanism`), the criteria that carry its score (its high scores on heavy weights), and its main cost, taken from its low `scoreRationales` and the `counterevidence` of its votes. Name a worker who voted `not-feasible` or `uncertain` and give that worker's `rationale`.
480
+ 6. **Remaining steps** — restate `nextStep`: where the direction is chosen (the HTML report's direction-selection Export saved under `user-responses/`) and how planning then picks it up.
481
+
482
+ The layout is guidance for the reply. Nothing checks the reply text; the command guarantees only the numbers.
483
+
465
484
  ## Concurrency
466
485
 
467
- - `prepare_task_bundle` serializes per-task via `~/.okstra/.locks/<task-key>.lock`. Concurrent skill invocations on the same task wait; different tasks proceed in parallel.
486
+ - `prepare_task_bundle` takes the per-task lock `~/.okstra/.locks/<task-key>.lock` only while it allocates the run sequence number and writes the run-context file; concurrent invocations on the same task wait for that step and get distinct sequence numbers, and the rest of prepare is not serialized by it. Different tasks proceed in parallel.
468
487
  - Each wizard run owns its own state file (one per `okstra wizard new-state-file`); two parallel skill invocations do not collide.
469
488
  - The skill must NOT call `okstra.sh` (or any other bash entrypoint) that would re-implement the orchestration. The wizard + `render-bundle` is the single authority.
470
489
 
@@ -52,7 +52,7 @@ restated a table that sat directly above or below them.
52
52
 
53
53
  ## Audience & authority (READ FIRST — drives everything below)
54
54
 
55
- **The schedule is shared with people who do not run okstra.** It is the team's work-plan document, not a record of a run. The body names neither the tool nor its vocabulary — `okstra`, `task-group`, `task-key`, `taskType`, a run's phase names — and the validator refuses a body that does. The title is the work's name, the one metadata line is `> 작성일 <YYYY-MM-DD> · 대상 저장소 <repo>`, and At a Glance identifies each task by name rather than by task-id. That extends to paths: a plan often puts its own scaffolding under the run's working tree — `.okstra/…`, or the `qa/…` scratch it holds — and those are places the reader cannot open and does not own. Name the artefact instead ("계약 캡처 도구", "위반 대장") and keep the path out. Real repository paths — `package.json`, `src/**`, `eval/**`, `.github/workflows/**` — are content and stay.
55
+ **The schedule is shared with people who do not run okstra.** It is the team's work-plan document, not a record of a run. The body names neither the tool nor its vocabulary — `okstra`, `task-group`, `task-key`, `taskType`, a run's phase names. The validator refuses prose that names `okstra`, `task-group`, `task-key`, or `taskType` outside inline code; it does not check phase names or backticked tokens. The title is the work's name, the one metadata line is `> 작성일 <YYYY-MM-DD> · 대상 저장소 <repo>`, and At a Glance identifies each task by name rather than by task-id. That extends to paths: a plan often puts its own scaffolding under the run's working tree — `.okstra/…`, or the `qa/…` scratch it holds — and those are places the reader cannot open and does not own. Name the artefact instead ("계약 캡처 도구", "위반 대장") and keep the path out. Real repository paths — `package.json`, `src/**`, `eval/**`, `.github/workflows/**` — are content and stay.
56
56
 
57
57
  **The schedule is a client-facing work plan.** It assumes the team has all permissions and can proceed without further approval. Even when the underlying per-task reports flag blocking items, missing approvals, or "items requiring user confirmation", **the schedule MUST NOT surface them** — those belong in the internal report. Never emit a decision checklist, a `#### Items requiring user confirmation` sub-section, `Done`/`Ready?`/`Blocking Decisions` columns, or checkbox lists; "Status" reflects work phase only.
58
58
 
@@ -64,13 +64,13 @@ restated a table that sat directly above or below them.
64
64
 
65
65
  **The schedule must be self-contained.** Opaque codes pulled from internal reports (`FC-5`, `UC-12`, `M1`, decision-item letters, …) must not appear unresolved. Choose one per identifier: **Form A** (≤3 codes) — replace the code inline with a 5–20 character one-line description of the item; **Form B** (≥4 recurring codes) — keep the codes and emit a `## Glossary` table as the last section resolving every one. Decision-item letters (`A1`, `B2`, …) are approval items and may not appear at all. TASK-IDs listed in `## At a Glance` need neither.
66
66
 
67
- **Enforced:** `validators/validate-schedule.py` `_validate_format` rejects a schedule body carrying a blocking/approval item, and `_check_self_contained_identifiers` rejects a decision-item code and an unresolved opaque identifier that neither Form A nor a `## Glossary` row resolves.
67
+ **Enforced:** `validators/validate-schedule.py` `_validate_format` rejects a `## Consolidated User Decision Checklist` heading, a `#### 사용자 확인 필요 항목` heading, and any checkbox item; it does not detect other approval content such as `Done`/`Ready?`/`Blocking Decisions` columns or an approval-request sentence. `_check_self_contained_identifiers` rejects a decision-item code and an unresolved opaque identifier that neither Form A nor a `## Glossary` row resolves.
68
68
 
69
69
  ## Contract SSOT — template + validator
70
70
 
71
- The installed template `~/.okstra/templates/reports/schedule.template.md` is the **byte-for-byte SSOT** for the output shape: frontmatter, top header block, the mandatory `##` heading list and order, per-task `Item / Detail` field labels and sub-section order, table column shapes, the ASCII Gantt format (relative day axis, plain fence, bar-only rows), the dependency-graph shapes, and the optional `## Glossary` gate. **Read the template before writing the schedule and follow it exactly** — do not re-derive section shapes from memory. Headings and field labels stay English literals regardless of the source-report language; body prose is Korean. When a section has no data, render its heading with `_none_` — never delete or reorder headings. Never emit mermaid or any graph DSL.
71
+ The installed template `~/.okstra/templates/reports/schedule.template.md` is the **byte-for-byte SSOT** for the output shape: frontmatter, top header block, the mandatory `##` heading list and order, per-task `Item / Detail` field labels and sub-section order, table column shapes, the ASCII Gantt format (relative day axis, plain fence, bar-only rows), the dependency-graph shapes, and the optional `## Glossary` gate. **Read the template before writing the schedule and follow it exactly** — do not re-derive section shapes from memory. Headings and field labels stay English literals regardless of the source-report language; body prose is Korean. When a section has no data, render its heading with `_none_` — never delete or reorder headings, except `## Task Dependency Graph`, which is omitted when no task depends on another (Step 5). Never emit mermaid or any graph DSL.
72
72
 
73
- `~/.okstra/lib/validators/validate-schedule.py` is the enforcement for all of the above (heading order, field labels, controlled vocabulary — e.g. `Med-High` is the canonical risk form — forbidden translations, checkbox bans, Gantt fence rules, unresolved-code detection). The Step 4 ambiguous-classification rationale line is the one rule the validator does not yet enforce — emit it yourself.
73
+ `~/.okstra/lib/validators/validate-schedule.py` enforces most of the above (heading order, field labels, controlled vocabulary — e.g. `Med-High` is the canonical risk form — forbidden translations, checkbox bans, Gantt fence rules, unresolved-code detection). It treats `## Task Dependency Graph` and `## Gantt Chart` as optional, and it does not refuse a risk, cross-task, or next-actions section (Step 4 bans them). The Step 4 ambiguous-classification rationale line is not enforced either — emit it yourself.
74
74
 
75
75
  One computation rule the template scaffold cannot carry inline:
76
76
 
@@ -175,7 +175,7 @@ Do not render `sliceValue` — it argues why the plan sliced the work this way,
175
175
 
176
176
  When the source is a Stage Map, the Gantt bars are selected stages. A row is labelled `Stage <n>` when exactly one task is scheduled, and `<TASK-ID> Stage <n>` when more than one is. Spell the stage out — `S1` costs the reader a lookup and saves five characters. A row is a label and a bar and nothing else: no `days=` annotation (the Work Breakdown owns those numbers, and the validator compares the bar against that column), and no per-row `! crit` / `est` markers, which distinguish nothing when every row carries them. One line above the fence states the column unit and what `█` and `░` mean — that is the whole legend, because those two glyphs are the only notation left. Split the task effort range proportionally by `step_count`; round every stage except the last to 0.5 day and let the last stage absorb the remainder. Cross-stage dependency annotations follow `depends_on`. Already-done and non-selected stages never get a bar. A task tagged `[NEEDS-PLANNING]` contributes no bars.
177
177
 
178
- **Bar geometry is checked, not decorative.** One column is half a day: a bar runs `lower / 0.5` filled cells `█` then `(upper - lower) / 0.5` open cells `░`. A bar drawn to any other width is a validation error, as is an axis whose last tick overshoots the scheduled work by 5 days or more.
178
+ **Bar geometry is checked, not decorative.** One column is half a day: a bar runs `lower / 0.5` filled cells `█` then `(upper - lower) / 0.5` open cells `░`. A bar drawn to any other width is a validation error, as is an axis whose last tick overshoots the scheduled work by more than 5 days.
179
179
 
180
180
  **Every controlled code the schedule prints must be defined on the page.** The template's `### Priority & Risk Scale` resolves `P0`–`P3` and `Very Low`–`High`; the validator requires that subsection and both header literals. Stage labels are spelled out for the same reason — Gantt rows read `Stage 3`, and `Depends On` cells read `None` / `Stage 2` / `Stage 1 (done)` rather than a bare number the reader has to match against the Stage column of the same table. A reader who never saw the source report cannot rank `P0` against `P1`, calibrate `High`, or expand `S3`; those are opaque codes wearing a friendlier shape.
181
181
 
@@ -229,13 +229,18 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart skip
229
229
  3. **Run the deterministic gate first.** Execute `python3 ~/.okstra/lib/validators/validate-schedule.py <draft> --selection-json <selection>`. Do not dispatch the narrative verifier when this exits non-zero.
230
230
  4. **Run the independent LLM verifier second.** Only after the deterministic gate passes, create
231
231
  `.okstra/agent-invocations/schedule-verification/<invocation-id>.instructions.md` containing the draft,
232
- selection JSON, and checks below, but none of the lead's reasoning. Materialize its sibling `.prompt.md`
233
- with `okstra agent-prompt materialize`, `--purpose schedule-verification`,
234
- `--audience schedule-verifier`, the current host runtime, the selected provider, and
235
- `--model-role analyser`. Run `okstra agent-prompt verify` against the returned `metadataPath` before
236
- dispatch. A native host call receives the verified prompt body and `hostModelValue`; a deterministic
237
- provider process runs `okstra worker-dispatch` with the prompt path and `modelExecutionValue`. These
238
- values are not interchangeable.
232
+ selection JSON, and checks below, but none of the lead's reasoning. Ask the runtime what this operation
233
+ runs — `okstra agent-prompt resolve-operation --operation schedule-verification` prints the duty, the
234
+ verifier count, and each slot's provider and model; the contract owns them, so do not choose a role or
235
+ provider here. Materialize its sibling `.prompt.md` with
236
+ `okstra agent-prompt materialize`, `--purpose schedule-verification`, `--audience <dutyId>`, the
237
+ current host runtime, and that slot's `--provider` and `--model <modelRef>`. Run
238
+ `okstra agent-prompt verify` against the returned `metadataPath` before dispatch. A native host call receives the verified prompt body and `hostModelValue`; a deterministic
239
+ provider process runs the provider wrapper
240
+ `~/.okstra/bin/okstra-<provider>-exec.sh <projectRoot> <modelExecutionValue> <prompt-path>`
241
+ (`okstra worker-dispatch` dispatches only a run manifest's assignments, not a standalone prompt). The
242
+ wrapper records the provider's output in the prompt path with `.md` replaced by `.log`. These values are
243
+ not interchangeable.
239
244
  The verifier returns `pass` plus concrete findings. Give it these checks:
240
245
  - **Executable ordering** — stage sequence, `Depends On`, and Gantt bar positions tell the same story; nothing depends on something scheduled after it.
241
246
  - **Arithmetic** — the Work Breakdown `Days` column sums to the At a Glance `Days` cell and to the `Effort sum` line.
@@ -262,7 +267,7 @@ Reached only after both Step 5.5 gates return `pass`. Promote the verified same
262
267
  .okstra/tasks/<task-group-segment>/schedule/<task-group-segment>-plan-<YYYY-MM-DD_HH-MM-SS>.md
263
268
  ```
264
269
 
265
- `<task-group-segment>` comes from the manifest's `taskGroupPathSegment` verbatim. Timestamp is current local time. Auto-create the parent directory. Never overwrite an existing file — on a same-second collision append `-2`, `-3`, ….
270
+ `<task-group-segment>` is the parent directory name of the `Task root` that `okstra stage-map` printed in Step 3, used verbatim. Timestamp is current local time. Auto-create the parent directory. Never overwrite an existing file — on a same-second collision append `-2`, `-3`, ….
266
271
 
267
272
  ### Step 7: Self-validate before reporting completion
268
273
 
@@ -301,7 +306,7 @@ Reached only after both Step 5.5 gates return `pass`. Promote the verified same
301
306
  | either validation gate still fails after two revisions | Final file not written; report residual findings to the user |
302
307
  | `task-group` matches no tasks | "That task-group could not be found." and stop |
303
308
  | Catalog and manifest disagree on `workStatus` | Manifest wins (catalog may be stale) |
304
- | task-group casing / punctuation variants | Normalise both sides (lowercase + strip non-`[a-z0-9]`), compare against `taskGroupPathSegment` only; use the manifest's segment verbatim for path output |
309
+ | task-group casing / punctuation variants | `schedule-input` matches them: it lowercases the argument and the group part of each catalog task key and drops every non-alphanumeric character (Unicode letters and digits stay); use the Step 6 segment verbatim for path output |
305
310
 
306
311
  ## Output Rules
307
312
 
@@ -96,18 +96,19 @@ targets; follow the host's permission mechanism if either remains blocked.
96
96
  okstra preflight --runtime <host-runtime>
97
97
  ```
98
98
 
99
- Read the fixed `Okstra preflight`, `Project root`, `Project JSON`, `Project ID`,
100
- `Reason`, and `Recovery` lines.
99
+ Read the fixed `Okstra preflight` line. On `Okstra preflight: ready` the output
100
+ carries `Project ID` and `Project root`; on `Okstra preflight: failed` it carries
101
+ `Stage`, `Reason`, and `Recovery`, and no `Project root` line.
101
102
 
102
- - `Ok: true` → carry `Project root` as a literal absolute string and paste it into every subsequent command in this skill.
103
- - `Ok: false`, `Stage: resolve` → ask the user (`AskUserQuestion`, free text) for an absolute project root and rerun as a separate Bash tool call with the literal absolute path:
103
+ - `Okstra preflight: ready` → carry `Project root` as a literal absolute string and paste it into every subsequent command in this skill.
104
+ - `Okstra preflight: failed`, `Stage: resolve` → ask the user (`AskUserQuestion`, free text) for an absolute project root and rerun as a separate Bash tool call with the literal absolute path:
104
105
 
105
106
  ```bash
106
107
  okstra preflight --runtime <host-runtime> --cwd /abs/path/from/user
107
108
  ```
108
109
 
109
- - `Ok: false`, `Stage: project_json_missing` → proceed to Step 3 (this is the normal create path).
110
- - `Ok: false`, any other `Stage` (`python`, `parse`, `project_json_invalid`) → show the fixed `Reason` line to the user verbatim and follow the `Recovery` line (typically `okstra doctor --runtime <host-runtime>` to diagnose, then `okstra ensure-installed --runtime <host-runtime>` or re-running the Step 1 install). `Reason` is the SSOT — do not hand-enumerate stage causes.
110
+ - `Okstra preflight: failed`, `Stage: project_json_missing` → proceed to Step 3 (this is the normal create path). The project root appears only inside the `Reason` path (`<root>/.okstra/project.json not found — ...`); take the root from that path.
111
+ - `Okstra preflight: failed`, any other `Stage` (`install`, `helper_scripts_missing`, `python`, `parse`, `project_json_invalid`, `runtime_readiness`) → show the fixed `Reason` line to the user verbatim and follow the `Recovery` line (typically `okstra doctor --runtime <host-runtime>` to diagnose, then `okstra ensure-installed --runtime <host-runtime>` or re-running the Step 1 install). `Reason` is the SSOT — do not hand-enumerate stage causes.
111
112
 
112
113
  ## Step 3: Project metadata setup
113
114
 
@@ -129,10 +130,11 @@ If the file does NOT exist, ask via `AskUserQuestion`:
129
130
 
130
131
  - **Question**: `"Project id for okstra (e.g. INV-1234, my-app, okstra)"`
131
132
  - **Validate**: the answer must be non-empty AND contain at least one
132
- alphanumeric character. Re-ask on empty input — `okstra setup --yes`
133
- with no `--project-id` exits 1 with
134
- `error: --project-id is required (no existing project.json, not a TTY)`,
135
- so passing the user's empty answer through is a silent failure path.
133
+ alphanumeric character. Re-ask on empty input — `okstra setup` exits 1
134
+ on an empty `--project-id` value or an id with no alphanumeric character,
135
+ and `okstra setup --yes` with no `--project-id` and no existing
136
+ `project.json` exits 1 with
137
+ `error: --project-id is required (no existing project.json, not a TTY)`.
136
138
 
137
139
  Then create the file — paste the literal `projectRoot` from Step 2 and the literal `projectId` from the user's answer (no shell variables):
138
140
 
@@ -81,8 +81,9 @@ form only. Representative denied tokens: `--fix` / `--write` (formatters),
81
81
  (`_DENIED_LITERAL_TOKENS` / `_DENIED_SUBSTRINGS` plus the dynamic
82
82
  `npm install` and `INSTA_UPDATE=` checks) — do not re-enumerate it here.
83
83
 
84
- Encountering a denied token aborts the verifier with status
85
- `contract-violated`; re-declare the command in check-only form to recover
84
+ A declared denied token makes okstra refuse to prepare an `implementation`
85
+ or `final-verification` run, with an error naming the token (other task types
86
+ do not read `qaCommands`); re-declare the command in check-only form to recover
86
87
  (e.g. swap `prettier --write` → `prettier --check`).
87
88
 
88
89
  The field is preserved across the runtime's auto-upserts of `project.json`, so
@@ -134,7 +135,7 @@ and `validators/validate-run.py::_validate_conformance`.
134
135
 
135
136
  ## C. Project-local Claude settings symlink and workspace trust
136
137
 
137
- `okstra setup` (and `okstra run` on its first invocation per project)
138
+ `okstra setup` (and every `okstra run` prepare that is not render-only)
138
139
  provisions `<PROJECT_ROOT>/.claude/settings.local.json` as a symlink to
139
140
  `~/.okstra/templates/settings.local.json`. The template contains the Bash
140
141
  permission rules required for the codex/antigravity worker wrappers:
@@ -159,9 +160,9 @@ If a non-symlink `.claude/settings.local.json` already exists, setup backs it
159
160
  up to `.claude/settings.local.json.bak.<timestamp>` before installing the
160
161
  symlink — surface that to the user so they can merge project-specific rules
161
162
  back (the symlinked template is okstra-owned and refreshed on okstra updates).
162
- To opt out (advanced): replace the symlink with a regular file; okstra will
163
- back it up as `.bak.<timestamp>` on its next setup call rather than
164
- overwriting silently.
163
+ There is no persistent opt-out: a regular file or foreign symlink put in
164
+ its place is backed up as `.bak.<timestamp>` and replaced with the symlink on
165
+ the next `okstra setup` or run prepare.
165
166
 
166
167
  ## D. Project PR body template (release-handoff)
167
168
 
@@ -10,7 +10,7 @@ description: >-
10
10
 
11
11
  # OKSTRA Usage
12
12
 
13
- Read-only project usage snapshot. The CLI owns every sum; never re-add rows.
13
+ Project usage snapshot. The skill itself writes nothing. `okstra usage-report` resolves every catalog task by task-key, and that lookup self-heals a finished implementation phase. When a task's current phase is `implementation`, that lookup appends a `done` row to `runs/implementation-planning/consumers.jsonl` for each stage whose carry file is complete but has no settled row, and when every stage of the latest plan has a `done` row and a pass-grade carry it marks the phase completed in `task-manifest.json` (`workflow`, `phaseOutcome.implementation`) and refreshes that task's catalog entry. Nothing else is written. The CLI owns every sum; never re-add rows.
14
14
 
15
15
  ## Step 0: Preflight
16
16
 
@@ -156,7 +156,7 @@ Every value, rationale, and reason body file must be a regular file under `<proj
156
156
  okstra user-response answer --transaction <transaction> --clarification-id <C-NNN> --kind <kind> --disposition <disposition> --value-file <value.md> [--rationale-file <rationale.md>]
157
157
  ```
158
158
 
159
- The two answer forms are mutually exclusive. The command validates that the clarification ID and kind exist and remain open in the selected report. Repeating the same answer command is safe.
159
+ The two answer forms are mutually exclusive. The command validates that the clarification ID exists once in the selected report, that its kind matches, and that its status is `open` or `answered` — an item answered in an earlier response can be answered again. Within one transaction, a repeated answer for the same ID replaces the earlier one, so repeating the same command is safe.
160
160
 
161
161
  ## Step 6: Record an explicit decision when present
162
162
 
@@ -75,6 +75,7 @@ code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; word-break
75
75
  .chip.running { color: var(--running); }
76
76
  .chip.done { color: var(--done); }
77
77
  .chip.blocked { color: var(--blocked); }
78
+ .chip.unlinked { color: var(--missing); }
78
79
  .task-head { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: baseline; margin-bottom: 8px; }
79
80
  .counts { display: flex; flex-wrap: wrap; gap: 6px; }
80
81
  .hint { color: var(--muted); font-size: 12px; margin: 6px 0 12px; }
@@ -42,6 +42,14 @@ The synthesis packet's Authoring Contract names which of these are **required**
42
42
 
43
43
  Any other top-level name is rejected however reasonable it reads — a section title copied out of a lead procedure document (`Clarification Response Carried In`, `Stage Map`, `Rollback Strategy`) is a heading in that document, not a top-level field here. Nested names come from the task's block in `schemas/final-report-v3.0.schema.json`; when a name is refused, the parser's message lists the names allowed at that exact position, so correct against that list rather than guessing a second time.
44
44
 
45
+ **Implementation-planning direction branch.** A selected-direction narrative carries `selectedDirectionRef` and `directionRealization`; the `P-Dir-1` verifier checks that realization against the selected core mechanism, architecture boundaries, planning invariants, and any hidden direction change. A legacy candidate-comparison narrative retains `P-Opt-*` option comparison semantics. These are plan-body judgments, not frontmatter fields owned by the writer. Four `directionRealization` fields — `coreMechanism`, `architectureBoundaries`, `planningInvariants`, `userConstraints` — are verbatim copies of the selected-direction snapshot and report assembly overwrites them from that snapshot at publication: do not paraphrase them; spend the writing on the fields the writer actually owns (`fileStructure` and the other realization fields).
46
+
47
+ **Implementation-option-selection comparison.** Candidate details remain direction-level and must not claim planning precision:
48
+
49
+ ```json
50
+ {"candidateDetailBoundary":{"expectedChangeAreas":"direction-level-only","expectedVerification":"direction-level-signals-only","forbidden":["exact-file-lists","stage-lists","test-commands"]}}
51
+ ```
52
+
45
53
  ## Pointer record
46
54
 
47
55
  The pointer record names the project-relative narrative path and audit sidecar path. It does not contain the narrative, worker result corpus, or any machine-owned ledger.
@@ -34,10 +34,15 @@ candidate-cap: 8 # codebase-scan only: 1..12, de
34
34
 
35
35
  <!-- author guidance — strip out at fill-in time:
36
36
  Paste each source separately and as-is. No paraphrasing, summarizing, or
37
- restructuring. Format conversion (e.g. Jira ADF → Markdown) is allowed and
38
- must be annotated in the header meta. Heading was originally
39
- "Source Material (verbatim — do not modify)" — the parenthetical is a
40
- reviewer note, not body text.
37
+ restructuring. Length is never a reason to excerpt: capture the record in
38
+ full — for a ticket that means title, body, every comment, status, labels,
39
+ assignee and linked/child issue references, not the body alone. Format
40
+ conversion (e.g. Jira ADF -> Markdown) is allowed and must be annotated in
41
+ the header meta. Anything the fetch could not deliver (a truncated body, an
42
+ attachment returned as a link, an opaque embed node) gets a
43
+ `conversion-block:` row in Open Questions naming what is missing. Heading was
44
+ originally "Source Material (verbatim — do not modify)" — the parenthetical
45
+ is a reviewer note, not body text.
41
46
  -->
42
47
 
43
48
  ### Source 1 — <type: file | linear | jira | github | notion | url | user-input>
@@ -145,6 +150,11 @@ than downstream. -->
145
150
 
146
151
  ## Constraints
147
152
 
153
+ <!-- Author guidance: this section also records what the source asked for and
154
+ this task deliberately leaves out. One bullet per dropped item, written as
155
+ `out of scope: <the source's item> — <why>`. Dropping a source requirement
156
+ without that line is indistinguishable from overlooking it. -->
157
+
148
158
  <Deadlines, compatibility, technical/operational limits. Use _(none)_ if
149
159
  none.>
150
160
 
@@ -57,10 +57,6 @@
57
57
  "brief": "Brief",
58
58
  "clarification": "Your answer"
59
59
  },
60
- "handoffMode": {
61
- "whole-task": "Whole task",
62
- "stage-group": "Selected stages"
63
- },
64
60
  "workerStatus": {
65
61
  "completed": "Completed",
66
62
  "error": "Failed",
@@ -652,6 +648,11 @@
652
648
  "handoff-scope": "Handoff scope",
653
649
  "the-behaviour-you-chose": "The behaviour you chose",
654
650
  "handoff-mode": "Handoff mode",
651
+ "stages-shipped": "Stages shipped:",
652
+ "release-base": "Release base:",
653
+ "stage": "Stage",
654
+ "pr-head": "PR head branch",
655
+ "pr-base": "PR base branch",
655
656
  "conflict-check": "Conflict check",
656
657
  "pr-body": "PR Body",
657
658
  "branch-and-pr": "Branch and PR",
@@ -57,10 +57,6 @@
57
57
  "brief": "브리프",
58
58
  "clarification": "사용자 답변"
59
59
  },
60
- "handoffMode": {
61
- "whole-task": "태스크 전체",
62
- "stage-group": "선택한 stage"
63
- },
64
60
  "workerStatus": {
65
61
  "completed": "완료",
66
62
  "error": "실패",
@@ -652,6 +648,11 @@
652
648
  "handoff-scope": "인계 범위",
653
649
  "the-behaviour-you-chose": "선택한 동작",
654
650
  "handoff-mode": "인계 방식",
651
+ "stages-shipped": "내보낸 stage:",
652
+ "release-base": "릴리스 base:",
653
+ "stage": "stage",
654
+ "pr-head": "PR head 브랜치",
655
+ "pr-base": "PR base 브랜치",
655
656
  "conflict-check": "충돌 확인",
656
657
  "pr-body": "PR 본문",
657
658
  "branch-and-pr": "브랜치와 PR",
@@ -12,7 +12,10 @@
12
12
  {% if handoff.get("handoffScope") %}
13
13
  <section data-report-section="handoff-scope" data-report-field="releaseHandoff.handoffScope">
14
14
  <h2>{{ t('tasks.release-handoff.handoff-scope') }}</h2>
15
- <p><strong>{{ handoff.handoffScope.mode | enum_label("handoffMode") }}</strong>{% if handoff.handoffScope.get("stages") %} — stages {{ handoff.handoffScope.stages | join(", ") }}{% endif %}{% if handoff.handoffScope.get("collectorBranch") %} · Collector branch <code>{{ handoff.handoffScope.collectorBranch }}</code>{% endif %}</p>
15
+ <p><strong>{{ t('tasks.release-handoff.stages-shipped') }}</strong> {{ handoff.handoffScope.stages | join(", ") }} · {{ t('tasks.release-handoff.release-base') }} <code>{{ handoff.handoffScope.releaseBase }}</code></p>
16
+ {% if handoff.handoffScope.get("stagePlan") %}
17
+ <table><thead><tr><th>{{ t('tasks.release-handoff.stage') }}</th><th>{{ t('tasks.release-handoff.pr-head') }}</th><th>{{ t('tasks.release-handoff.pr-base') }}</th></tr></thead><tbody>{% for row in handoff.handoffScope.stagePlan %}<tr><td>{{ row.stage }}</td><td><code>{{ row.headBranch }}</code></td><td><code>{{ row.baseBranch }}</code> ({{ row.baseKind }})</td></tr>{% endfor %}</tbody></table>
18
+ {% endif %}
16
19
  </section>
17
20
  {% endif %}
18
21
 
@@ -25,22 +28,22 @@
25
28
  <h2>{{ t('tasks.release-handoff.branch-and-pr') }}</h2>
26
29
  {{ render_narrative(narrative.branchAndPrExplanation, "releaseHandoff.userNarrative.branchAndPrExplanation") }}
27
30
  <p><strong>{{ t('tasks.release-handoff.branch') }}</strong> {{ handoff.featureBranchState.branchName }}</p>
28
- <p data-report-field="releaseHandoff.pullRequestOutcome"><strong>{{ t('tasks.release-handoff.pr-result') }}</strong> {{ handoff.pullRequestOutcome.kind }}{% if prUrl %} · <a href="{{ prUrl }}" rel="noopener noreferrer">{{ t('tasks.release-handoff.pull-request-open') }}</a>{% endif %}</p>
31
+ <ul data-report-field="releaseHandoff.pullRequestOutcomes">{% for row in prOutcomes %}<li><strong>{{ t('tasks.release-handoff.pr-result') }}</strong> stage {{ row.stage }} — {{ row.kind }}{% if row.get("baseBranch") %} → <code>{{ row.baseBranch }}</code>{% endif %}{% if row.get("url") %} · <a href="{{ row.url }}" rel="noopener noreferrer">{{ t('tasks.release-handoff.pull-request-open') }}</a>{% endif %}{% if row.get("reason") %} · {{ row.reason }}{% endif %}</li>{% endfor %}</ul>
29
32
  </section>
30
33
 
31
34
  <section data-report-section="conflicts" data-report-field="releaseHandoff.mergeConflictProbe">
32
35
  <h2>{{ t('tasks.release-handoff.conflict-verdict') }}</h2>
33
36
  {{ render_narrative(narrative.conflictExplanation, "releaseHandoff.userNarrative.conflictExplanation") }}
34
37
  {{ summary_card(handoff.mergeConflictProbe.kind, t('tasks.release-handoff.base-branch') ~ " " ~ (handoff.mergeConflictProbe.baseBranch | default('not-run'))) }}
38
+ {% if handoff.mergeConflictProbe.get("conflictingStages") %}<p>{{ t('tasks.release-handoff.stage') }} {{ handoff.mergeConflictProbe.conflictingStages | join(", ") }}</p>{% endif %}
35
39
  {% if handoff.mergeConflictProbe.get("conflictingPaths") %}<ul>{% for path in handoff.mergeConflictProbe.conflictingPaths %}<li>{{ path }}</li>{% endfor %}</ul>{% endif %}
36
40
  </section>
37
41
 
38
42
  <section data-report-section="delivered-commits" data-report-field="releaseHandoff.commitList">
39
43
  <h2>{{ t('tasks.release-handoff.commits-delivered') }}</h2>
40
44
  {% if handoff.commitList is mapping %}<p>{{ t('tasks.release-handoff.there-are-no-commits') }}</p>{% else %}
41
- <table><thead><tr><th>{{ t('tasks.release-handoff.commit') }}</th><th>{{ t('tasks.release-handoff.subject') }}</th><th>{{ t('tasks.release-handoff.files') }}</th></tr></thead><tbody>{% for row in handoff.commitList %}<tr>{{ row_key(pairs=[(t('macros.layout.sha'), row.shortSha), (t('macros.layout.order'), loop.index)]) }}<td>{{ row.subject | inline_code }}</td><td>{{ file_paths(row.files) }}</td></tr>{% endfor %}</tbody></table>{% endif %}
42
- {% if handoff.get("sourceVerificationReports") %}<p data-report-field="releaseHandoff.sourceVerificationReports"><strong>{{ t('tasks.release-handoff.the-verification-report-this-rests-on') }}</strong>{% for row in handoff.sourceVerificationReports %} — stage {{ row.stage }}: <code>{{ row.path }}</code> · {{ row.verdictTokenQuote | inline_code }}{% endfor %}</p>
43
- {% else %}<p data-report-field="releaseHandoff.sourceVerificationReport"><strong>{{ t('tasks.release-handoff.the-verification-report-this-rests-on') }}</strong> — <code>{{ handoff.sourceVerificationReport.path }}</code> · {{ handoff.sourceVerificationReport.verdictTokenQuote | inline_code }}</p>{% endif %}
45
+ <table><thead><tr><th>{{ t('tasks.release-handoff.stage') }}</th><th>{{ t('tasks.release-handoff.commit') }}</th><th>{{ t('tasks.release-handoff.subject') }}</th><th>{{ t('tasks.release-handoff.files') }}</th></tr></thead><tbody>{% for row in handoff.commitList %}<tr><td>{{ row.get("stage", "") }}</td>{{ row_key(pairs=[(t('macros.layout.sha'), row.shortSha), (t('macros.layout.order'), loop.index)]) }}<td>{{ row.subject | inline_code }}</td><td>{{ file_paths(row.files) }}</td></tr>{% endfor %}</tbody></table>{% endif %}
46
+ <p data-report-field="releaseHandoff.sourceVerificationReports"><strong>{{ t('tasks.release-handoff.the-verification-report-this-rests-on') }}</strong>{% for row in handoff.sourceVerificationReports %} — stage {{ row.stage }}: <code>{{ row.path }}</code> · {{ row.verdictTokenQuote | inline_code }}{% endfor %}</p>
44
47
  </section>
45
48
 
46
49
  <section data-report-section="next-action">
@@ -239,7 +239,7 @@
239
239
  "allowedOptions": "Allowed options"
240
240
  },
241
241
  "h1Body": "Which action should run?",
242
- "h2Body": "PR base branch (when H1 = `push + PR`)",
242
+ "h2Body": "Release base branch (when H1 = `push + PR`)",
243
243
  "h3Body": "How should the PR title/body draft be handled?",
244
244
  "h2DefaultLabel": "(n/a)",
245
245
  "h2OptionsLabel": "staging / preprod / main / user input",
@@ -6,7 +6,7 @@
6
6
  {{ s.section("releaseHandoff.handoffScope", "Handoff Scope") }}
7
7
  {{ s.section("releaseHandoff.userSelections", "User Selections") }}
8
8
  {{ s.section("releaseHandoff.featureBranchState", "Feature Branch State") }}
9
- {{ s.section("releaseHandoff.pullRequestOutcome", "Pull Request Outcome") }}
9
+ {{ s.section("releaseHandoff.pullRequestOutcomes", "Pull Request Outcomes") }}
10
10
  {{ s.section("releaseHandoff.mergeConflictProbe", "Merge Conflict Probe") }}
11
11
  {{ s.section("releaseHandoff.executedCommands", "Executed Commands") }}
12
12
  {{ s.section("releaseHandoff.routingRecommendation", "Routing Recommendation") }}
@@ -25,8 +25,7 @@ taskType: "{{FM_TASK_TYPE}}"
25
25
 
26
26
  ## Source Verification Report
27
27
 
28
- - Mode: `{{HANDOFF_MODE}}`
29
- - Stages: `{{HANDOFF_STAGES}}`
28
+ - Stages: `{{HANDOFF_STAGES}}` — one PR per stage.
30
29
  - Reports (one row per cited `final-verification` final-report; the verdict must be release-ready).
31
30
  **Enforced:** `validators/validate-run.py` routes through
32
31
  `okstra_ctl.release_gate.release_handoff_allowed`, which passes `accepted` and
@@ -47,11 +46,14 @@ taskType: "{{FM_TASK_TYPE}}"
47
46
  - Existing implementation commits (`git log --oneline <base>..HEAD`):
48
47
  - Existing PR for this head, if any (`gh pr list --head <branch> --state open --json url --jq '.[0].url'`):
49
48
 
50
- ## Candidate PR Base Branches
49
+ ## Candidate Release Base Branches
50
+
51
+ The user picks ONE release base for the whole run. Each stage's own PR base is then derived from its `depends-on` by `okstra handoff pr-plan`: a stage with no live dependency targets the release base, a stage with one targets that stage's branch (a stacked PR), and a stage with several targets a branch merging them.
51
52
 
52
53
  - Default options offered to the user: `staging` | `preprod` | `prod` | `main` | `dev` | custom input
53
54
  - Repo-specific preference, if known (e.g. `main` is the integration branch):
54
55
  - Branches that are off-limits as a base in this repo (security / freeze rules):
56
+ - Merge order the stack implies (merge a stage's PR only after its base PR):
55
57
 
56
58
  ## PR Draft Inputs
57
59
 
@@ -69,7 +71,7 @@ taskType: "{{FM_TASK_TYPE}}"
69
71
  ## User-Selection Defaults (advisory only — the user still chooses interactively)
70
72
 
71
73
  - Suggested action (Q1): `local checkout` | `push + PR` | `skip`
72
- - Suggested base (Q2): one of the candidate base branches above
74
+ - Suggested release base (Q2): one of the candidate base branches above
73
75
  - Suggested message handling (Q3): `use as-is` | `edit then proceed`
74
76
 
75
77
  > These suggestions help the lead phrase its `AskUserQuestion` prompts. They are NOT pre-approvals — every mutating command still requires an explicit user pick at run time.