okstra 0.206.0 → 0.207.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 (259) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-registry.mjs +7 -1
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/lifecycle/install.mjs +1 -1
  5. package/dist/commands/lifecycle/install.mjs.map +1 -1
  6. package/docs/architecture/storage-model.md +1 -0
  7. package/docs/architecture.md +40 -16
  8. package/docs/cli.md +17 -15
  9. package/docs/contributor-change-matrix.md +3 -2
  10. package/docs/performance-improvement-plan-v2.md +1 -1
  11. package/docs/project-structure-overview.md +43 -20
  12. package/package.json +1 -1
  13. package/runtime/BUILD.json +2 -2
  14. package/runtime/agents/operations/code-review.json +1 -1
  15. package/runtime/bin/lib/okstra/usage.sh +3 -3
  16. package/runtime/bin/okstra-compact-reminder.sh +1 -1
  17. package/runtime/bin/okstra-spawn-followups.py +2 -2
  18. package/runtime/prompts/duties/direction-selection-worker.json +1 -1
  19. package/runtime/prompts/launch.template.md +2 -2
  20. package/runtime/prompts/lead/adapters/cmux.md +4 -3
  21. package/runtime/prompts/lead/context-loader.md +1 -1
  22. package/runtime/prompts/lead/convergence.md +44 -12
  23. package/runtime/prompts/lead/okstra-lead-contract.md +44 -73
  24. package/runtime/prompts/lead/phase-routing.md +64 -0
  25. package/runtime/prompts/lead/report-writer.md +10 -8
  26. package/runtime/prompts/lead/team-contract.md +1 -1
  27. package/runtime/prompts/profiles/_clarification-recommendation.md +4 -4
  28. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  29. package/runtime/prompts/profiles/_common-contract.md +2 -2
  30. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  31. package/runtime/prompts/profiles/forbidden-actions.json +0 -94
  32. package/runtime/prompts/wizard/prompts.ko.json +2 -1
  33. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
  34. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +5 -5
  35. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
  36. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -2
  37. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
  38. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
  39. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +17 -26
  40. package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +183 -0
  41. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +60 -10
  42. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +1 -1
  43. package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +21 -4
  44. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +10 -1
  45. package/runtime/python/okstra_ctl/analysis_inputs.py +0 -39
  46. package/runtime/python/okstra_ctl/analysis_scope.py +31 -0
  47. package/runtime/python/okstra_ctl/approval_decisions.py +32 -2
  48. package/runtime/python/okstra_ctl/asset_roots.py +19 -0
  49. package/runtime/python/okstra_ctl/assignment_resolver.py +8 -0
  50. package/runtime/python/okstra_ctl/blocking_checks.py +7 -0
  51. package/runtime/python/okstra_ctl/code_review_target.py +92 -6
  52. package/runtime/python/okstra_ctl/consumers.py +12 -0
  53. package/runtime/python/okstra_ctl/dispatch_checkpoints.py +121 -0
  54. package/runtime/python/okstra_ctl/dispatch_core.py +54 -32
  55. package/runtime/python/okstra_ctl/dispatch_state.py +34 -5
  56. package/runtime/python/okstra_ctl/doctor.py +2 -1
  57. package/runtime/python/okstra_ctl/domain/provider.py +5 -0
  58. package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -2
  59. package/runtime/python/okstra_ctl/domain/write_policy.py +2 -1
  60. package/runtime/python/okstra_ctl/execution_mutation_audit.py +46 -9
  61. package/runtime/python/okstra_ctl/handoff.py +11 -466
  62. package/runtime/python/okstra_ctl/handoff_error.py +5 -0
  63. package/runtime/python/okstra_ctl/implementation_direction.py +0 -477
  64. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +13 -1
  65. package/runtime/python/okstra_ctl/lead_progress.py +33 -1
  66. package/runtime/python/okstra_ctl/manager_view.py +26 -19
  67. package/runtime/python/okstra_ctl/model_io/lines.py +21 -4
  68. package/runtime/python/okstra_ctl/models.py +4 -1
  69. package/runtime/python/okstra_ctl/operation_invocation.py +11 -2
  70. package/runtime/python/okstra_ctl/option_comparison.py +3 -165
  71. package/runtime/python/okstra_ctl/option_votes.py +3 -191
  72. package/runtime/python/okstra_ctl/paths.py +8 -6
  73. package/runtime/python/okstra_ctl/phases/catalog.py +56 -12
  74. package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
  75. package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
  76. package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
  77. package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
  78. package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
  79. package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
  80. package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
  81. package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
  82. package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
  83. package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
  84. package/runtime/python/okstra_ctl/phases/error_analysis/spec.md +118 -0
  85. package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
  86. package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
  87. package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
  88. package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
  89. package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
  90. package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
  91. package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
  92. package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
  93. package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
  94. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -2
  95. package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
  96. package/runtime/python/okstra_ctl/phases/final_verification/spec.md +1 -1
  97. package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
  98. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
  99. package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
  100. package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
  101. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
  102. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
  103. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +18 -7
  104. package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
  105. package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
  106. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
  107. package/runtime/python/okstra_ctl/phases/implementation/spec.md +238 -0
  108. package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
  109. package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
  110. package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
  111. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
  112. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
  113. package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
  114. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
  115. package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +3 -3
  116. package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
  117. package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +83 -0
  118. package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
  119. package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
  120. package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
  121. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
  122. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
  123. package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
  124. package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
  125. package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
  126. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
  127. package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
  128. package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
  129. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
  130. package/runtime/python/okstra_ctl/phases/implementation_planning/spec.md +204 -0
  131. package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
  132. package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
  133. package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
  134. package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
  135. package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
  136. package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
  137. package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
  138. package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
  139. package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
  140. package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
  141. package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
  142. package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
  143. package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
  144. package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
  145. package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
  146. package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
  147. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
  148. package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
  149. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
  150. package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
  151. package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
  152. package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
  153. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +233 -0
  154. package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
  155. package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
  156. package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
  157. package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
  158. package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
  159. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +132 -0
  160. package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
  161. package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
  162. package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
  163. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
  164. package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +2 -2
  165. package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
  166. package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
  167. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
  168. package/runtime/python/okstra_ctl/plan_approval.py +70 -0
  169. package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
  170. package/runtime/python/okstra_ctl/process_group.py +118 -0
  171. package/runtime/python/okstra_ctl/profile_show.py +3 -3
  172. package/runtime/python/okstra_ctl/render.py +15 -4
  173. package/runtime/python/okstra_ctl/report_assembly.py +28 -92
  174. package/runtime/python/okstra_ctl/report_finalize.py +106 -2
  175. package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
  176. package/runtime/python/okstra_ctl/report_projections.py +1 -36
  177. package/runtime/python/okstra_ctl/report_routing.py +23 -0
  178. package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
  179. package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
  180. package/runtime/python/okstra_ctl/report_views.py +1 -1
  181. package/runtime/python/okstra_ctl/run.py +68 -350
  182. package/runtime/python/okstra_ctl/run_artifact_prune.py +200 -0
  183. package/runtime/python/okstra_ctl/stage_map.py +13 -0
  184. package/runtime/python/okstra_ctl/team.py +108 -9
  185. package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
  186. package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
  187. package/runtime/python/okstra_ctl/wizard/api.py +18 -0
  188. package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
  189. package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
  190. package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
  191. package/runtime/python/okstra_ctl/wizard/steps_options.py +8 -0
  192. package/runtime/python/okstra_ctl/wizard/steps_plan.py +10 -263
  193. package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
  194. package/runtime/python/okstra_ctl/work_categories.py +1 -1
  195. package/runtime/python/okstra_ctl/worker_dispatch.py +44 -3
  196. package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
  197. package/runtime/python/okstra_ctl/worker_prompt_policy.py +19 -0
  198. package/runtime/python/okstra_ctl/worker_runner.py +21 -3
  199. package/runtime/python/okstra_ctl/workflow.py +26 -143
  200. package/runtime/python/okstra_ctl/write_policy.py +57 -7
  201. package/runtime/python/okstra_project/dirs.py +14 -0
  202. package/runtime/python/okstra_project/resolver.py +2 -1
  203. package/runtime/schemas/execution-manifest-v2.schema.json +2 -1
  204. package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
  205. package/runtime/skills/okstra-code-review/SKILL.md +70 -32
  206. package/runtime/skills/okstra-code-review/references/review-calibration.md +26 -6
  207. package/runtime/skills/okstra-run/SKILL.md +3 -3
  208. package/runtime/templates/manager/view.template.html +18 -1
  209. package/runtime/templates/reports/quick-input.template.md +1 -1
  210. package/runtime/templates/reports/task-brief.template.md +1 -1
  211. package/runtime/validators/validate-brief.py +2 -2
  212. package/runtime/validators/validate-run.py +299 -3940
  213. package/runtime/validators/validate_analysis_report.py +14 -126
  214. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
  215. package/runtime/python/okstra_ctl/technical_verification.py +0 -195
  216. /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
  217. /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
  218. /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
  219. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
  220. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
  221. /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
  222. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
  223. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
  224. /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
  225. /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
  226. /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
  227. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
  228. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
  229. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
  230. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
  231. /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
  232. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
  233. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
  234. /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
  235. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
  236. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
  237. /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
  238. /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
  239. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
  240. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
  241. /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
  242. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
  243. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
  244. /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
  245. /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
  246. /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
  247. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
  248. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
  249. /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
  250. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
  251. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
  252. /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
  253. /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
  254. /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
  255. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
  256. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
  257. /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
  258. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
  259. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.md +0 -0
@@ -73,19 +73,19 @@ Plan for the actual worktree layout before approval. Shared documentation direct
73
73
  - one `endStateCoverage` row per brief end-state id, whose `coveredBy` names the `R-NNN` row that carries it. The two tables are a chain, not duplicates: `endStateCoverage` proves no reporter requirement was dropped, `requirementCoverage` proves each one reaches a stage.
74
74
  - a `blocked` `endStateCoverage` row MUST carry `blockedBy` — `kind` one of `clarification` / `finding` / `execution` / `upstream` / `not-observed`, plus a `ref` for the first two. `blocked` on its own carries all five situations at once, and only `clarification` is something a person can answer; without the field nothing downstream can tell "the reporter must decide" from "the server would not boot". A `clarification` ref must be a `C-NNN` row of this report or one carried in — the same row the approval-decision ledger holds, not an id you only named in prose. A `finding` ref is required but not resolved: it legitimately points at the approved plan or an upstream report (`VC-003`, `CA-001`). **Enforced:** `validators/validate-run.py` `_validate_end_state_blocked_by`.
75
75
  - Implementation Design Preparation (`implementation-design-prep-v1`, BLOCKING):
76
- - **Detector SSOT:** the planner MUST run the V1 detector defined by `scripts/okstra_ctl/design_surfaces.py` (`detect_design_surfaces()` over the detector's `RULES`) and MUST NOT invent or copy a second keyword list into the plan or prompt. **Enforced:** `scripts/okstra_ctl/report_projections.py` `project_design` reruns that detector against the plan and raises `ReportProjectionError` when the snapshot's surfaces do not match it, so a hand-authored keyword list cannot survive projection.
77
- - **Exactly-once coverage:** for every detector-produced `(stage, kind)`, exactly one `designSurfaceCoverage` row exists on that stage. **Enforced:** the rows are written once by `scripts/okstra_ctl/design_snapshot.py` `build_design_snapshot` (one per detected trigger, carrying its evidence) and `scripts/okstra_ctl/report_projections.py` `project_design` refuses any snapshot whose `(stage, kind)` set or trigger evidence differs from a fresh detector run — that is where a missing, duplicated, or evidence-drifted row stops; `schemas/final-report-v2.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the row shape.
78
- - **Disposition:** the design-surface detector snapshot uses `inline-contract` only when the stage already states the kind-specific minimum implementation contract; otherwise it uses `prep-item`. `not-applicable` is legal only with a concrete rationale consistent with the stage action. **Enforced:** `scripts/okstra_ctl/report_projections.py::project_design`, `schemas/final-report-v3.0.schema.json`, and `P-Prep-S<stage>-<kind>` verification.
79
- - **AI-prepared proposal:** every referenced PREP item MUST record `kind`, `stageRefs`, `need`, evidence-cited `knownFacts`, `openQuestions`, a concrete evidence-backed `aiProposal` (`summary`, `details`, `assumptions`, `evidence`, `confidence`), `humanConfirmation`, explicit `status`, and the safest reversible default available. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.DesignPrepItem` / `$defs.DesignPrepProposal` enforce required fields; the stage ↔ PREP link is not checked afterwards because it is not authored — `scripts/okstra_ctl/design_snapshot.py` `build_design_snapshot` emits each coverage row's `prepItemId` and the matching item's `stageRefs` in the same pass, so the two sides cannot disagree. `prompts/lead/plan-body-verification.md` rejects empty or non-implementable proposals.
80
- - **Status choice:** prefer `provisional` with a `workingAssumption`, concrete `guardrails`, `reviewAt`, `ifStillOpen`, and canonical `requestPath`; use `blocked` only for business policy, external authority, a destructive migration decision, or the absence of any safe reversible assumption. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.DesignPrepItem` enforces state-specific fields, `validators/validate-run.py` `_validate_design_prep_states` enforces confirmation/request invariants, and `prompts/lead/plan-body-verification.md` judges whether the disposition is justified. A declared `blocked` status does not by itself fail plan-body verification.
81
- - **External-reality anchoring (`external-interface` / `transformation-mapping` surfaces):** these two detector kinds are correct only against data whose shape lives *outside this repository* (a third-party response body / external payload format). For such a surface the referenced PREP item's `aiProposal` MUST derive the assumed shape — selectors, field paths, response structure — from a **captured real sample** and cite it in `knownFacts` / `evidence` (source + capture time); a shape invented from internal reasoning and marked `confidence: high` is the disallowed move, because a plan built on an assumed shape yields an implementation whose parser and fixture only ever agree with each other. When the brief supplies no sample and none is capturable at plan time, the item MUST stay `provisional` with a `workingAssumption` that the external shape is unverified against reality, a `guardrails` line forbidding the implementation from presenting a synthetic-fixture green run as reality-verified, and an `ifStillOpen` that routes to user confirmation against real data — it MUST NOT be dispositioned `inline-contract` / settled. **Enforced (semantic):** the surface's presence is machine-checked by `scripts/okstra_ctl/report_projections.py` `project_design`; whether its proposal's evidence is genuinely external is judged by the §5.5.9 `P-Prep-S<stage>-<kind>` round (this phase runs it adversarially).
82
- - **Planner-only test surface:** add `manual-user-test` only when a test prerequisite changes the implementation interface or acceptance contract; the V1 detector never emits it. **Enforced:** the detector's `RULES` in `scripts/okstra_ctl/design_surfaces.py` contain no `manual-user-test` rule, so every such row is planner-authored by construction and none can arrive through the detector snapshot; `schemas/final-report-v2.0.schema.json` permits its planner-authored shape, and `prompts/lead/plan-body-verification.md` verifies the stage-action rationale.
76
+ - **Detector SSOT:** the planner MUST run the V1 detector defined by `scripts/okstra_ctl/design_surfaces.py` (`detect_design_surfaces()` over the detector's `RULES`) and MUST NOT invent or copy a second keyword list into the plan or prompt. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/report.py` `project_design` reruns that detector against the plan and raises `ReportProjectionError` when the snapshot's surfaces do not match it, so a hand-authored keyword list cannot survive projection.
77
+ - **Exactly-once coverage:** for every detector-produced `(stage, kind)`, exactly one `designSurfaceCoverage` row exists on that stage. **Enforced:** the rows are written once by `scripts/okstra_ctl/design_snapshot.py` `build_design_snapshot` (one per detected trigger, carrying its evidence) and `scripts/okstra_ctl/phases/implementation_planning/report.py` `project_design` refuses any snapshot whose `(stage, kind)` set or trigger evidence differs from a fresh detector run — that is where a missing, duplicated, or evidence-drifted row stops; `schemas/final-report-v2.0.schema.json` `$defs.DesignSurfaceCoverage` enforces the row shape.
78
+ - **Disposition:** the design-surface detector snapshot uses `inline-contract` only when the stage already states the kind-specific minimum implementation contract; otherwise it uses `prep-item`. `not-applicable` is legal only with a concrete rationale consistent with the stage action. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/report.py::project_design`, `schemas/final-report-v3.0.schema.json`, and `P-Prep-S<stage>-<kind>` verification.
79
+ - **AI-prepared proposal:** every referenced PREP item MUST record `kind`, `stageRefs`, `need`, evidence-cited `knownFacts`, `openQuestions`, a concrete evidence-backed `aiProposal` (`summary`, `details`, `assumptions`, `evidence`, `confidence`), `humanConfirmation`, explicit `status`, and the safest reversible default available. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.DesignPrepItem` / `$defs.DesignPrepProposal` enforce required fields; the stage ↔ PREP link is not checked afterwards because it is not authored — `scripts/okstra_ctl/design_snapshot.py` `build_design_snapshot` emits each coverage row's `prepItemId` and the matching item's `stageRefs` in the same pass, so the two sides cannot disagree. `scripts/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md` rejects empty or non-implementable proposals.
80
+ - **Status choice:** prefer `provisional` with a `workingAssumption`, concrete `guardrails`, `reviewAt`, `ifStillOpen`, and canonical `requestPath`; use `blocked` only for business policy, external authority, a destructive migration decision, or the absence of any safe reversible assumption. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.DesignPrepItem` enforces state-specific fields, `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_design_prep_states` enforces confirmation/request invariants, and `scripts/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md` judges whether the disposition is justified. A declared `blocked` status does not by itself fail plan-body verification.
81
+ - **External-reality anchoring (`external-interface` / `transformation-mapping` surfaces):** these two detector kinds are correct only against data whose shape lives *outside this repository* (a third-party response body / external payload format). For such a surface the referenced PREP item's `aiProposal` MUST derive the assumed shape — selectors, field paths, response structure — from a **captured real sample** and cite it in `knownFacts` / `evidence` (source + capture time); a shape invented from internal reasoning and marked `confidence: high` is the disallowed move, because a plan built on an assumed shape yields an implementation whose parser and fixture only ever agree with each other. When the brief supplies no sample and none is capturable at plan time, the item MUST stay `provisional` with a `workingAssumption` that the external shape is unverified against reality, a `guardrails` line forbidding the implementation from presenting a synthetic-fixture green run as reality-verified, and an `ifStillOpen` that routes to user confirmation against real data — it MUST NOT be dispositioned `inline-contract` / settled. **Enforced (semantic):** the surface's presence is machine-checked by `scripts/okstra_ctl/phases/implementation_planning/report.py` `project_design`; whether its proposal's evidence is genuinely external is judged by the §5.5.9 `P-Prep-S<stage>-<kind>` round (this phase runs it adversarially).
82
+ - **Planner-only test surface:** add `manual-user-test` only when a test prerequisite changes the implementation interface or acceptance contract; the V1 detector never emits it. **Enforced:** the detector's `RULES` in `scripts/okstra_ctl/design_surfaces.py` contain no `manual-user-test` rule, so every such row is planner-authored by construction and none can arrive through the detector snapshot; `schemas/final-report-v2.0.schema.json` permits its planner-authored shape, and `scripts/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md` verifies the stage-action rationale.
83
83
  - **Trivial task:** when the detector returns no surfaces, its input uses `designPreparation.mode: no-design-inputs`, an empty `items` array, and a concrete reason tied to the plan. **Enforced:** `schemas/final-report-v3.0.schema.json` and `scripts/okstra_ctl/report_assembly.py`.
84
84
  - Approval gate (phase-specific addendum to shared authority rule):
85
85
  - The report record `frontmatter.approved` field is the only authorised approval gate. report-writer always emits `false`. The user clears it by invoking the next phase with `--approve`, or by confirming approval in the in-session wizard. Editing the full reading copy does not approve the plan. `okstra_ctl.run._validate_approved_plan` reads this field and refuses entry until it is `true`.
86
86
  - Cross-verification mode:
87
87
  - Phase 5.5 finding convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker finding (requirement gap / risk / plan item) by re-inspecting its cited evidence; the burden of proof sits on the claim. See `prompts/lead/convergence.md` §"Adversarial Verification Mode".
88
- - §5.5.9 plan-body verification runs with an **adversarial posture** (`prompts/lead/plan-body-verification.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is majority-based for kinds `b`/`c`/`e`, but a single `DISAGREE` blocks on its own for the concrete, safety-critical kind `a` (path/symbol mismatch) — and `f` on `P-Req-*` items. `P-Var-*` items are excepted from the kind-`a` exception: a variation-point defect takes a majority. Rollback ordering (`d`) is advisory and never blocks the gate — a rollback is executed by a human, not by okstra's workers or verifiers. A majority also needs ≥2 participating votes, so a lone dissent whose peer returned a non-result does not block on a majority-gated kind (see that contract's §"Adversarial plan-body posture").
88
+ - §5.5.9 plan-body verification runs with an **adversarial posture** (`scripts/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is majority-based for kinds `b`/`c`/`e`, but a single `DISAGREE` blocks on its own for the concrete, safety-critical kind `a` (path/symbol mismatch) — and `f` on `P-Req-*` items. `P-Var-*` items are excepted from the kind-`a` exception: a variation-point defect takes a majority. Rollback ordering (`d`) is advisory and never blocks the gate — a rollback is executed by a human, not by okstra's workers or verifiers. A majority also needs ≥2 participating votes, so a lone dissent whose peer returned a non-result does not block on a majority-gated kind (see that contract's §"Adversarial plan-body posture").
89
89
  - **Incremental re-verification scope (clarification re-runs):** when the lead's `okstra incremental-scope` decision is `mode == "incremental"` (procedure in `prompts/launch.template.md` §"Clarification Response Carried In"), workers re-analyze ONLY the stages listed in `reverify_stages` (the downstream closure of the impacted stages). Workers MUST NOT re-open, re-score, or re-judge any stage in `carry_stages` — those stages' prior plan-item verdicts are carried forward verbatim, and a worker never overwrites a carried verdict with its own judgement. When the decision is `mode == "full"` (the default), every stage is re-analyzed as usual.
90
90
  - **Single incremental-scope decision:** the lead calls `okstra incremental-scope` once the inputs are complete, passing the answered `C-NNN` ids through `--answered-clarifications`, changed design-preparation IDs through `--prep-items`, and any lead-resolved stage numbers through `--impacted`; the CLI unions all three before applying the existing dependency closure and cutoff. The clarification ids are resolved to stages by the CLI from the prior report's own `planItems[].clarificationId` and `blocked C-NNN` coverage links — the lead does not map answers to stage numbers. An answer that changes the selected planning payload, Stage Map, or execution approach is not a local impact: pass `--full-reason`, which is the only structural path that still forces `mode == "full"`. A clarification id that traces to no stage returns `mode == "unresolved"` — ask the user for stage numbers and call again with `--impacted`; do not treat it as full and do not silently drop the id. **When this run only ADDS stages** — every prior stage is `done` and none of them is being re-opened — there are no stage numbers to give: pass `--carry-all-reason` instead. Pinning any prior stage as impacted makes `incremental-carry` demand plan-item verdicts from the current snapshot, which no longer holds them (a `done` stage's plan items are `observed`, so they are never dispatched or scored in this run), so that path has no answer that works. The stage's own row is still declared in the narrative — that is the numbering rule above, not a re-verification. `--carry-all-reason` still applies the base-ref check: if the branch moved, the decision degrades to `full` because the prior stages are no longer "as written". Unknown PREP IDs or invalid `stageRefs` still return an explicit full decision instead of being guessed. When the wizard pin is `auto` and the CLI returns `mode == "incremental"`, keep it — do not upgrade to full. When the user pinned a scope at the wizard (`REVERIFY_SCOPE_MODE` / `REVERIFY_SCOPE_STAGES` in `prompts/launch.template.md` §"Clarification Response Carried In" step 0), that pin is an input to this same call — `full` supplies the `--full-reason`, `carry-all` supplies the `--carry-all-reason`, and pinned stage numbers join `--impacted` — never a bypass of the CLI's closure and cutoff.
91
91
  - **Stage-aware carry:** for an incremental decision, `okstra incremental-scope` writes the decision to the record this run's manifest names in `incrementalDecisionPath`. The report writer's duty to copy each `carry_stages` stage row unchanged is not stated here — it is generated into that writer's own authoring contract from the same record, with this run's stage numbers in it (`okstra_ctl.report_synthesis_packet.ReportSynthesisPacket._carry_instructions`). After plan-item seeding, run `okstra incremental-carry --cur-narrative ... --state ... --out-state ...` against that record. The helper rejects a changed or missing carried stage and copies prior `P-Step-*` / `P-Prep-*` verdicts for `carry_stages`, plus unchanged `P-Val-*` / `P-Req-*` / `P-Rb-*` rows whose extract `contentHash` still matches. It rewrites `dispatchQueue` and the sibling `plan-items-*.json` so the next prompt does not re-score a carried checklist row. Overlap, omissions, and canonical conflicts return `CarryError`. On that error, discard the partial state and run full re-verification.
@@ -104,7 +104,7 @@ Plan for the actual worktree layout before approval. Shared documentation direct
104
104
  - admissible — the answer selects between behaviours the code must implement, fixes a requirement the plan would otherwise satisfy incorrectly, or resolves a safety/data-integrity question.
105
105
  - NOT admissible → use `Blocks=none` — QA-harness or tooling scope, report notation and wording, numbering or citation-range cleanup, anything outside the chosen realization, and anything the codebase answers (which the codebase-first rule already forbids raising at all). These belong in `## 5. Missing Information and Risks` or a Working Assumption; they are recorded, not gating.
106
106
  - A row you would answer with "the plan would still produce the same code either way" is by construction `Blocks=none`.
107
- - **Backtrace at authoring (`Blocks=approval` rows).** Every approval blocker this run opens must be traceable to the plan by the time the report is assembled, through one of the two link shapes `okstra incremental-scope` reads: an activity citing the row's `C-NNN` in `clarificationRefs` and the affected `P-*` ids in `planItemIds` (assembly derives the plan items' `clarificationRefs` from exactly this — a row opened before plan items existed gets its backtrace on a later activity once the Stage Map is written), or the blocked requirement's coverage row carrying `blocked C-NNN`. An unlinked id cannot auto-narrow the answered re-run: `incremental-scope` returns `mode == "unresolved"` and the user must name stage numbers by hand. The plan-body promotion path (step 8) already records these links; this rule extends the same obligation to rows raised during planning itself — the observed unlinked rows were all of that class. A genuinely plan-wide blocker links the plan-wide items it judges (`P-Dir-1`, `P-Dep-*`); it still cannot narrow, and that is the honest answer for it. **Enforced (advisory):** `validators/validate-run.py` `_validate_approval_clarification_backtrace` fails an open approval blocker with no link or a link resolving to no stage.
107
+ - **Backtrace at authoring (`Blocks=approval` rows).** Every approval blocker this run opens must be traceable to the plan by the time the report is assembled, through one of the two link shapes `okstra incremental-scope` reads: an activity citing the row's `C-NNN` in `clarificationRefs` and the affected `P-*` ids in `planItemIds` (assembly derives the plan items' `clarificationRefs` from exactly this — a row opened before plan items existed gets its backtrace on a later activity once the Stage Map is written), or the blocked requirement's coverage row carrying `blocked C-NNN`. An unlinked id cannot auto-narrow the answered re-run: `incremental-scope` returns `mode == "unresolved"` and the user must name stage numbers by hand. The plan-body promotion path (step 8) already records these links; this rule extends the same obligation to rows raised during planning itself — the observed unlinked rows were all of that class. A genuinely plan-wide blocker links the plan-wide items it judges (`P-Dir-1`, `P-Dep-*`); it still cannot narrow, and that is the honest answer for it. **Enforced (advisory):** `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_approval_clarification_backtrace` fails an open approval blocker with no link or a link resolving to no stage.
108
108
  - Deliverable completeness contract (BLOCKING — the schema checks data keys, not heading strings):
109
109
  - For a selected-direction plan, the plan-ready schema branch requires `planningContract`, `outcome`, `selectedDirectionRef`, `directionRealization`, `stageMap`, `stages`, `designPreparation`, `dependencyMigrationRisk`, `validationChecklist`, `rollbackStrategy`, `requirementCoverage`, `coverageSummary`, `variationPointAnalysis`, and `planBodyVerification`. Its `direction-invalidated` branch contains no execution fields. A selected-direction `requirementCoverage[]` row is keyed by `originalRequirementId` (the brief's `EB-`/`PB-`/`EO-` id); prose, cross-check statements and coverage claims cite that id. An `R-NNN` reaches the reader only if the row defines it in `id` — a number the plan never defines is a bare token with nothing to land on (2026-09-05, dev-10626 planning-001 cited `R-001`–`R-007` that no row carried).
110
110
  - Each `stages[]` entry requires `stage`, `title`, `sliceValue`, `acceptance`, `carryIn`, `stepwiseExecution` (1–6 rows), `exitContract`, and `stageValidation`. Each `stageMap[]` row requires `stage`, `title`, `dependsOn`, `stepCount`, `exitContractSummary`.
@@ -135,7 +135,7 @@ Plan for the actual worktree layout before approval. Shared documentation direct
135
135
  - The YAML frontmatter carries `implementation-option:` directly under `approved:` so the user can select an Option Candidate after planning.
136
136
  - Required deliverable shape (final report, in addition to the standard sections):
137
137
  - In the selected-direction branch, `directionRealization` is the sole design payload. Its `fileStructure`, interfaces, blast radius, test seams, assumptions, and invariants concretize the snapshot without introducing another option or recommendation.
138
- - **Variation-point analysis (`variationPointAnalysis`, mandatory — every plan emits the block, rendered as §5.5.11):** declare `hasMultipleImplementations`, and when it is `true`, one `points[]` row per varying behavior carrying `behavior`, the two or more `implementations` that serve it, `evidence` (a `path:line`, or the sibling task / stage that already implements it), and an `extractionDecision` of `extract` / `interfaceKind` / `coveredBy` (the Stage Map stage that builds the interface) / `rationale`. **A `false` declaration is not an omission — it is a claim**, so it carries a written `noVariationRationale` and an empty `points` array; the two are mutually exclusive, because declared points would be silently dropped from verification under a `false` header. A project whose `.okstra/project.json` sets `architecture.style: hexagonal` extracts a point as a port (`interfaceKind: "port"`), never as a shared helper. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.VariationPointAnalysis` / `$defs.VariationPoint` make the block required and pin the row shape; `validators/validate-run.py` `_validate_variation_point_analysis` rejects a `false` declaration with no rationale, a `false` declaration carrying points, a `true` declaration with no point, an `extract: true` decision naming no `interfaceKind` or no `coveredBy`, and a hexagonal project extracting as anything but a port; and every point becomes a `P-Var-<N>` plan item judged in §5.5.9 (`prompts/lead/plan-body-verification.md`) — a plan declaring no variation point is still verified, through the lone `P-Var-0`.
138
+ - **Variation-point analysis (`variationPointAnalysis`, mandatory — every plan emits the block, rendered as §5.5.11):** declare `hasMultipleImplementations`, and when it is `true`, one `points[]` row per varying behavior carrying `behavior`, the two or more `implementations` that serve it, `evidence` (a `path:line`, or the sibling task / stage that already implements it), and an `extractionDecision` of `extract` / `interfaceKind` / `coveredBy` (the Stage Map stage that builds the interface) / `rationale`. **A `false` declaration is not an omission — it is a claim**, so it carries a written `noVariationRationale` and an empty `points` array; the two are mutually exclusive, because declared points would be silently dropped from verification under a `false` header. A project whose `.okstra/project.json` sets `architecture.style: hexagonal` extracts a point as a port (`interfaceKind: "port"`), never as a shared helper. **Enforced:** `schemas/final-report-v2.0.schema.json` `$defs.VariationPointAnalysis` / `$defs.VariationPoint` make the block required and pin the row shape; `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_variation_point_analysis` rejects a `false` declaration with no rationale, a `false` declaration carrying points, a `true` declaration with no point, an `extract: true` decision naming no `interfaceKind` or no `coveredBy`, and a hexagonal project extracting as anything but a port; and every point becomes a `P-Var-<N>` plan item judged in §5.5.9 (`scripts/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md`) — a plan declaring no variation point is still verified, through the lone `P-Var-0`.
139
139
  - **Stage Map (mandatory — always emitted, even when N=1):** a table of all stages with `stage | title | depends-on | step-count | exit-contract-summary`. `depends-on` is `(none)` or a comma-separated stage number list. Stages with `depends-on (none)` can be implemented in parallel by two simultaneous `implementation` runs.
140
140
  - **Keep the table at exactly 5 columns** — do NOT add a column. `validators/validate-implementation-plan-stages.py` parses `stage | title | depends-on | step-count | exit-contract-summary` and silently skips any row that is not exactly 5 cells, so a 6th column would drop every stage and bypass S2–S11.
141
141
  - **Multi-project plans only** (the plan's work spans more than one project — see the Project-boundary partition rule below): prefix each stage's `title` cell with a `[<project>]` tag (e.g. `[okstra] Add X`) so the project each stage belongs to is readable at a glance, and add exactly one line directly under the Stage Map table — `Cross-project parallelism: <which per-project stages run in parallel, which are sequenced, and the cross-project dependency that forces each sequencing>`. Single-project plans omit both the tag and the line.
@@ -155,19 +155,19 @@ Plan for the actual worktree layout before approval. Shared documentation direct
155
155
  - **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step. Stage completion records the commit in the consumer ledger without creating or moving git tags.
156
156
  - **Never read an `.okstra/` artifact back out of a git object.** `.okstra/**` is gitignored and never committed — the executor aborts a commit that stages an ignored path and the verifier reports a committed `.okstra` path as a branch defect — so `git cat-file -e <tag>:.okstra/…`, `git show <tag>:.okstra/…`, and every variant of that read can never resolve, at any tag, in any stage. A later stage that needs a QA artifact reads it from the working tree or receives it through the carry sidecar / verifier result; do not design a stage contract around one being reachable from a tag. Validator S12 rejects the read.
157
157
  - **Per-stage conformance declaration (mandatory one line, in the stage section — same placement freedom as `TDD exemption:`):** the stage MUST carry exactly one of:
158
- - `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — declare that a Tier3 verification script will prove this stage's upstream requirements (brief / requirements-discovery / error-analysis / improvement-discovery → this stage's `Acceptance`) hold against **real** DB rows, real endpoints, or the real external API — NOT mocks. This phase emits the line and the `requires` set only. Do NOT write `<task_root>/qa/scripts/stage-<N>.*` and do NOT add a `runCommand` or `conformance-manifest.json` entry here — the matching `implementation` stage run creates the script file and the manifest `runCommand`. A plan that declares tests with no script file on disk is valid at this gate. The data.json `conformanceTests` value carries only the remainder after the `Conformance tests: stage-<N> — ` prefix — never the `stage-<N> — ` label itself (report assembly strips a leftover label at publication, and the implementation entry gate rejects one). The `requires` set MUST cover every surface the stage's own `stepwiseExecution[].plannedPaths` touch — a controller is `http`, a repository or query is `db` — because the implementation run's diff-surface check demands them after the work is done, when the approved plan can no longer change (2026-09-22, dev-10860: `requires=[io]` over a planned `catalog.controller.ts`). **Enforced at planning:** `validators/validate-run.py` `_validate_planning_conformance_declared` runs the implementation gate's surface patterns over those paths (`okstra_ctl.conformance.declared_stage_surface_gaps`) and fails the planning run on a missing surface. A stage the consumer ledger records as `done` is skipped — its body is carried, not authored, and cannot be corrected here.
159
- - `Conformance exemption: <reason>` — only for stages that touch no db/io/http/external surface, or where unit tests fully cover the increment. Exemption stays a planning declaration; do not move it to implementation. (If the eventual `implementation` diff actually touches one of those surfaces, `validate-run.py`'s diff-surface cross-check is BLOCKING — an exemption cannot hide a real db/io/http/external change.) **Enforced at planning:** `validators/validate-run.py` `_validate_planning_conformance_declared` runs the same surface patterns over the exempted stage's `stepwiseExecution[].plannedPaths` (`okstra_ctl.conformance.exempt_stage_surface_conflicts`) and fails the planning run — a plan that exempts a stage while planning a `*repository*` / `*.controller.*` / `*migration*` path is corrected here, where the plan is still editable, not after the implementation is done (observed 2026-09-09, dev-10784 Stage 2).
158
+ - `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — declare that a Tier3 verification script will prove this stage's upstream requirements (brief / requirements-discovery / error-analysis / improvement-discovery → this stage's `Acceptance`) hold against **real** DB rows, real endpoints, or the real external API — NOT mocks. This phase emits the line and the `requires` set only. Do NOT write `<task_root>/qa/scripts/stage-<N>.*` and do NOT add a `runCommand` or `conformance-manifest.json` entry here — the matching `implementation` stage run creates the script file and the manifest `runCommand`. A plan that declares tests with no script file on disk is valid at this gate. The data.json `conformanceTests` value carries only the remainder after the `Conformance tests: stage-<N> — ` prefix — never the `stage-<N> — ` label itself (report assembly strips a leftover label at publication, and the implementation entry gate rejects one). The `requires` set MUST cover every surface the stage's own `stepwiseExecution[].plannedPaths` touch — a controller is `http`, a repository or query is `db` — because the implementation run's diff-surface check demands them after the work is done, when the approved plan can no longer change (2026-09-22, dev-10860: `requires=[io]` over a planned `catalog.controller.ts`). **Enforced at planning:** `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_planning_conformance_declared` runs the implementation gate's surface patterns over those paths (`okstra_ctl.conformance.declared_stage_surface_gaps`) and fails the planning run on a missing surface. A stage the consumer ledger records as `done` is skipped — its body is carried, not authored, and cannot be corrected here.
159
+ - `Conformance exemption: <reason>` — only for stages that touch no db/io/http/external surface, or where unit tests fully cover the increment. Exemption stays a planning declaration; do not move it to implementation. (If the eventual `implementation` diff actually touches one of those surfaces, `validate-run.py`'s diff-surface cross-check is BLOCKING — an exemption cannot hide a real db/io/http/external change.) **Enforced at planning:** `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_planning_conformance_declared` runs the same surface patterns over the exempted stage's `stepwiseExecution[].plannedPaths` (`okstra_ctl.conformance.exempt_stage_surface_conflicts`) and fails the planning run — a plan that exempts a stage while planning a `*repository*` / `*.controller.*` / `*migration*` path is corrected here, where the plan is still editable, not after the implementation is done (observed 2026-09-09, dev-10784 Stage 2).
160
160
  - **External QA outcome guideline:** after satisfying the S11 declaration above, a line whose `requires` contains
161
161
  `db`, `http`, or `external` should name those capabilities here so the later `runCommand` can be written against them.
162
162
  Okstra may start the environment and run it automatically, but `FAIL`, missing evidence, or an
163
163
  unavailable environment is a user-owned follow-up, never a plan approval or
164
164
  later run blocker. `requires=[]` and `requires=[io]` remain blocking.
165
165
  Remote IO should also declare `external`.
166
- Layout split (the implementer writes these, not this phase): executable scripts (conformance + any real-IO test) live under `<task_root>/qa/scripts/`; data sidecars (`conformance-manifest.json`, `result-*.json`) stay at the `qa/` root. This declaration is enforced at four layers: `validators/validate-implementation-plan-stages.py` check **S11** forces every stage to carry one of the two lines; at the planning boundary `validators/validate-run.py` `_validate_planning_conformance_declared` accepts a well-formed `Conformance tests:` line even when the script file and manifest entry are absent (malformed `requires` still fails); the matching `implementation` stage run that inherited `Conformance tests:` fails closed when the script file is missing (`_validate_conformance`); and the manifest JSON structure — including each entry's `script` living under `qa/scripts/` and a `runCommand` that does not change cwd — is enforced by `validate_conformance_manifest` when the implementer writes the entry.
166
+ Layout split (not this phase's writes): executable scripts (conformance + any real-IO test) live under `<task_root>/qa/scripts/`; data sidecars (`conformance-manifest.json`, `result-*.json`) stay at the `qa/` root. A step that runs a script writing its own files (a baseline capture, a `--compare` target, build logs) points those paths at `<task_root>/qa/output/`: it is the only other qa directory the implementer's write audit accepts, and any other qa path discards the attempt (`scripts/okstra_ctl/write_policy.py` `_role_qa_artifact_paths`; observed 2026-09-26, dev-11054 stage 1, `qa/baseline/`). The implementer writes the scripts and the manifest entry. Only the implementation verifier writes `result-*.json`, including the result of an earlier stage's entry whose script this stage rewrites, so a step never assigns a result file to the implementer and never lists one in `plannedPaths`. This declaration is enforced at four layers: `validators/validate-implementation-plan-stages.py` check **S11** forces every stage to carry one of the two lines; at the planning boundary `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_planning_conformance_declared` accepts a well-formed `Conformance tests:` line even when the script file and manifest entry are absent (malformed `requires` still fails); the matching `implementation` stage run that inherited `Conformance tests:` fails closed when the script file is missing (`_validate_conformance`); and the manifest JSON structure — including each entry's `script` living under `qa/scripts/` and a `runCommand` that does not change cwd — is enforced by `validate_conformance_manifest` when the implementer writes the entry.
167
167
  - `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
168
168
  - `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
169
169
  - **Run-executable only (BLOCKING).** A `validationChecklist` row that carries `stageRefs` gates that stage's carry, so it MUST be executable by the implementation run itself — no deployment, no manual walk-through, no observation "by hand", no credentials the run does not hold. The implementation phase forbids deploys and holds no deploy credentials, so such a row can never pass and blocks a stage with zero code defects (observed: dev-10341 VC-013/VC-014). Manual or deployed-environment verification belongs in the brief's `External Gates` and final-verification's user-owned external QA — record it there without `stageRefs`. **Enforced:** `okstra_ctl.implementation_direction.validate_selected_direction_plan` (`stage_validation_executability_errors`) rejects stage-gating rows whose observation text requires a manual or deployed-environment step.
170
- - **Dependency precondition (stages that run the project toolchain).** The planning worktree is created without installed dependencies, so a stage whose steps call `npm` / `yarn` / `pytest` / `cargo` / equivalent cannot have those commands succeed at plan time — they exit `127`, not RED/GREEN. Declare the install **once** as a `phase: pre` row in `### Validation Checklist` (e.g. `VC-008 — the implementation run's stage worktree has workspace dependencies installed`) and have every such stage's `Stage Validation` cite that `VC-NNN` in its `pre:` line. Do not repeat the install commands per stage, and do not silently assume the tooling is present: a plan that never states the precondition produces steps whose commands never resolve, which the §5.5.9 round then reports as unverifiable. **Enforced (advisory):** `validators/validate-run.py` `_detect_missing_dependency_precondition` warns when a toolchain-invoking stage cites no `VC-NNN`, or cites one that is not `phase: pre`. Whether the cited row genuinely covers dependencies is a §5.5.9 judgement, not a machine check. Detection uses the token allowlist in `scripts/okstra_ctl/build_tools.py`; a project overrides it with `buildToolTokens` in `.okstra/project.json`.
170
+ - **Dependency precondition (stages that run the project toolchain).** The planning worktree is created without installed dependencies, so a stage whose steps call `npm` / `yarn` / `pytest` / `cargo` / equivalent cannot have those commands succeed at plan time — they exit `127`, not RED/GREEN. Declare the install **once** as a `phase: pre` row in `### Validation Checklist` (e.g. `VC-008 — the implementation run's stage worktree has workspace dependencies installed`) and have every such stage's `Stage Validation` cite that `VC-NNN` in its `pre:` line. Do not repeat the install commands per stage, and do not silently assume the tooling is present: a plan that never states the precondition produces steps whose commands never resolve, which the §5.5.9 round then reports as unverifiable. **Enforced (advisory):** `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_detect_missing_dependency_precondition` warns when a toolchain-invoking stage cites no `VC-NNN`, or cites one that is not `phase: pre`. Whether the cited row genuinely covers dependencies is a §5.5.9 judgement, not a machine check. Detection uses the token allowlist in `scripts/okstra_ctl/build_tools.py`; a project overrides it with `buildToolTokens` in `.okstra/project.json`.
171
171
  - **Vertical-slice-first partition rule (1st-class):** the grouping anchor is a **thin end-to-end vertical slice** — one stage delivers a single user-observable increment, crossing whatever layers are needed (data → service → API → UI) to make that one increment work. File/module proximity is demoted to the **intra-slice grouping rule**: within a slice, keep steps touching the same file/directory/module together so the diff, PR, and rollback unit stay cohesive. **Horizontal layer-splitting is forbidden** — never carve "the DB layer" into one stage and "the service layer" into the next; that produces stages that ship no standalone user value. A stage is split ONLY when (a) a real `depends-on` data/contract dependency exists, (b) effective steps would exceed 8, or (c) it is a distinct vertical slice (a different user-value increment). Maximising the number of parallel stages is NOT a reason to split — parallelism is an emergent property of independent stages, never a partitioning goal. **Config propagation is not a slice (BLOCKING):** a dependent stage whose planned paths are all configuration/CI files (`Dockerfile`, `docker-compose*`, `*.yml` / `*.yaml`, `.env*`) merely propagates a value another stage introduced — it ships no user-value increment of its own. Fold that work into the introducing stage as its own step(s) and separate it by **commits**, never by stages: each extra stage costs a full implementation run (executor + verifiers, 30min+ observed) for a few config lines. A first stage with no dependency is exempt — a task whose whole scope is configuration is legitimate. **Enforced:** `okstra_ctl.implementation_direction.validate_selected_direction_plan` (`_micro_stage_fold_errors`).
172
172
  - **Project-boundary partition rule (hard boundary):** a *project* boundary is either (a) a different repository / `PROJECT_ROOT`, or (b) a different top-level independently-deployable module within one repo. A stage maps to a single worktree on one repo/branch, so **no stage may contain edits belonging to more than one project** — this is a hard split that overrides the ≤8-step merging allowance; never co-locate two projects' changes in one stage to save a stage. Two cases:
173
173
  - **Same repo, different modules** — put each project/module's increment in its own stage. Their file sets are disjoint, so they default to `depends-on (none)` and run in parallel; add a `depends-on` link ONLY when a real cross-module contract / shared-schema / deploy-order dependency exists.
@@ -182,18 +182,18 @@ Plan for the actual worktree layout before approval. Shared documentation direct
182
182
  - dependency / migration risk assessment (ordering constraints, data backfills, feature-flag prerequisites, repo-internal sequencing)
183
183
  - **Cross-Project Dependencies (conditionally required):** when the plan depends on work in another project / repo / published package, add (a) a `kind: cross-project` DM row to `dependencyMigrationRisk`, and (b) a matching `XP-NNN` row to `crossProjectDependencies`. An upstream-precondition row must have concrete `requiredWork` / `verificationSignal` / `howToStart` — `validators/validate-run.py` enforces that a DM `cross-project` ⇒ at least one `direction: upstream-precondition` XP row, and the schema enforces non-empty row fields. A cross-project dependency is recorded as this structured precondition, not as a soft Recommended Next Step. A single-project plan uses an empty array.
184
184
  - **recommendedNextSteps policy:** keep the substance of cross-project preconditions/carries in `crossProjectDependencies`, and put in `§3 Recommended Next Steps` only a pointer to that section (`§5.4 Cross-Project Dependencies`) — no double recording.
185
- - **Resuming from an approval blocker (BLOCKING).** When this report carries a progress-blocking `blocks: approval` clarification (`open`, or `request-revision` / `reject`), `recommendedNextSteps[0]` MUST be a command the reader can run now: `/okstra-user-response`, and the `--answered-clarifications` re-run in that step's `text` or `commands`. Do not write "The Okstra lead will …" as the first step. Point them at `okstra recap assemble`, which prints the answered ids, the exact flag value, the sidecar paths, and whether the re-verification would fall back to full. Do not restate those values here: they are unknown while you write, because the user has not answered yet. Do not tell the reader to start another planning run before the answers exist. An `accept-risk` / `select` / `answer` already recorded is not this case. **Enforced:** `validators/validate-run.py` `_validate_rerun_guidance`.
186
- - **Asking for approval (BLOCKING).** When `outcome` is `plan-ready` and no `blocks: approval` row still blocks progress, one `recommendedNextSteps` entry MUST tell the reader to approve (`--approve` or the in-session wizard). A remaining `blocked-by-disagreement` whose rows the user already accepted does not send them back to planning. Do not recommend another `implementation-planning` run. **Enforced:** `validators/validate-run.py` `_validate_approval_guidance`.
185
+ - **Resuming from an approval blocker (BLOCKING).** When this report carries a progress-blocking `blocks: approval` clarification (`open`, or `request-revision` / `reject`), `recommendedNextSteps[0]` MUST be a command the reader can run now: `/okstra-user-response`, and the `--answered-clarifications` re-run in that step's `text` or `commands`. Do not write "The Okstra lead will …" as the first step. Point them at `okstra recap assemble`, which prints the answered ids, the exact flag value, the sidecar paths, and whether the re-verification would fall back to full. Do not restate those values here: they are unknown while you write, because the user has not answered yet. Do not tell the reader to start another planning run before the answers exist. An `accept-risk` / `select` / `answer` already recorded is not this case. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/guidance.py` `_validate_rerun_guidance`.
186
+ - **Asking for approval (BLOCKING).** When `outcome` is `plan-ready` and no `blocks: approval` row still blocks progress, one `recommendedNextSteps` entry MUST tell the reader to approve (`--approve` or the in-session wizard). A remaining `blocked-by-disagreement` whose rows the user already accepted does not send them back to planning. Do not recommend another `implementation-planning` run. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/guidance.py` `_validate_approval_guidance`.
187
187
  - validation checklist (pre / mid / post) — each item is an exact command or observable outcome
188
188
  - rollback strategy — exact revert path (commits, flags, migrations) and the signal that triggers rollback
189
189
  - **Requirement admissibility (scope boundary):** a brief line becomes a Requirement Coverage row only when **a stage can satisfy it by changing files in this repository** — source, tests, config, or deployment *manifest files*. A line whose satisfaction needs a person's approval, a ticket status change, or an action against live infrastructure (applying a manifest, a cutover, creating a dashboard or alert, validating in staging/production) is NOT a requirement for this phase: it belongs to the brief's `## External Gates`, and this plan neither creates a stage for it nor cites it in coverage. Briefs generated by `okstra-brief-gen` pre-split these into the end-state sections `## Expected Behavior` / `## Preserved Behavior` / `## Expected Outcome` (admissible) and `## External Gates` (not); when reading an older brief that carries a raw Definition-of-Done checklist, apply the same test line by line. The boundary is the *action*, not the topic — "add the flag to `values-prod.yaml`" is admissible, "apply that manifest to prod" is not. Planning an operational stage this phase cannot execute (see the run-scope rule above forbidding deployments) produces steps whose commands never resolve, which the §5.5.9 gate then correctly blocks — the plan must not create that deadlock in the first place.
190
190
  - **Scope provenance (BLOCKING):** every Requirement Coverage row's `Source` cell must be exactly one of three forms, and every Stage Map stage must be cited by at least one coverage row's `Covered by`. The forms:
191
- - `brief:EB-001` / `brief:PB-001` / `brief:EO-001` — an end-state id the brief declares. Citing a heading instead is rejected when the brief pins ids: every brief carries the same generic headings, so a heading citation cannot say WHICH reporter line the requirement came from. Briefs authored before the end-state sections existed keep the older `brief:<heading>` form, and there a heading the brief does not contain is a fabricated requirement. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance` and `validators/validate_fanout.py` `_check_provenance`, both via `scope_provenance.brief_citation_problem`.
191
+ - `brief:EB-001` / `brief:PB-001` / `brief:EO-001` — an end-state id the brief declares. Citing a heading instead is rejected when the brief pins ids: every brief carries the same generic headings, so a heading citation cannot say WHICH reporter line the requirement came from. Briefs authored before the end-state sections existed keep the older `brief:<heading>` form, and there a heading the brief does not contain is a fabricated requirement. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/validation.py` `_validate_requirement_provenance` and `validators/validate_fanout.py` `_check_provenance`, both via `scope_provenance.brief_citation_problem`.
192
192
  - `derived:R-NNN — <one-line rationale>` — derived from another row in the same table. The chain must terminate at a `brief:` or `contract:` row and must not cycle. Use this for genuine technical consequences: a migration stage is `derived:R-003 — R-003 cannot be satisfied without a schema change`.
193
193
  - `contract:<rule>` — an artifact okstra's own phase contract mandates, so it has no brief line to cite. The allowlist is exactly `decision-record-step` (the §5.4 Decision Drafts materialization step) and `glossary-step` (the glossary proposal step); the SSOT is `scripts/okstra_ctl/scope_provenance.py`. Never widen this form to launder work the brief did not ask for.
194
- An item you can give none of these three sources to is **not a requirement and not a stage**. Its only admissible outlet is a `## 1. Clarification Items` row with `Blocks=approval`, carrying the recommendation format from `_clarification-recommendation.md`. Do not fold it into an option, a stage, or a step "while we are in here" — that is the scope expansion this rule exists to stop. This makes concrete the planning-input rule that any change beyond what `Requirement Summary` explicitly demands is out of scope by default. **Enforced:** `validators/validate-run.py` `_validate_requirement_provenance` (source resolution) and `_validate_stage_has_requirement` (no stage without a requirement).
194
+ An item you can give none of these three sources to is **not a requirement and not a stage**. Its only admissible outlet is a `## 1. Clarification Items` row with `Blocks=approval`, carrying the recommendation format from `_clarification-recommendation.md`. Do not fold it into an option, a stage, or a step "while we are in here" — that is the scope expansion this rule exists to stop. This makes concrete the planning-input rule that any change beyond what `Requirement Summary` explicitly demands is out of scope by default. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/validation.py` `_validate_requirement_provenance` (source resolution) and `_validate_stage_has_requirement` (no stage without a requirement).
195
195
  - **The reach of this gate — do not over-trust it.** What is mechanically enforced is the *form* of each source, that a cited `brief:` id is one the brief actually declares (or, on a pre-end-state brief, that the heading literally exists), that a `derived:` chain terminates without cycling, and that no stage is uncited. What is **not** enforced is whether the cited source genuinely demands the requirement. The id form closes the older loophole — a brief no longer offers generic headings any invented work could be hung on — but it leaves one open: attaching a requirement the cited `EB-NNN` does not actually ask for still parses clean, because no machine reads that id's sentence and compares it to your row. The gate's value is that it forces every item to name a specific reporter line and makes fabrication explicit and auditable — judging whether that line actually demands the item remains a reviewer / `DISAGREE(f)` responsibility, and passing this gate is never evidence that the scope is justified.
196
- - **Stage citation format — enumerate, never range (scale gate):** the reverse check reads each `Covered by` cell as prose, so a stage counts as cited only when its number is anchored to a `Stage` / `Stages` word on the same line. These read: `Stage 2`; `Stage 1, Stage 2, Stage 3`; `Stages 1, 2, 3`; `Stages 1, 2, and 3`; and `and` / `&` conjunctions. A bare number with no `stage` word anchoring it is NOT read as a citation, so `covered by the selected plan, step 4` cites nothing. **A range cites only its two endpoints:** `Stages 1-3` cites 1 and 3, and stage 2 stays uncited — write every stage out. Range syntax (`-`, `to`, `through`) is still parsed, so `Stages 7-8` is a valid two-stage citation; what it cannot do is stand in for an interior nobody named. **Enforced:** `validators/validate-run.py` `_validate_stage_has_requirement` via `okstra_ctl.stage_citations.enumerated_stage_numbers` fails the plan when any Stage Map stage is cited by no coverage row.
196
+ - **Stage citation format — enumerate, never range (scale gate):** the reverse check reads each `Covered by` cell as prose, so a stage counts as cited only when its number is anchored to a `Stage` / `Stages` word on the same line. These read: `Stage 2`; `Stage 1, Stage 2, Stage 3`; `Stages 1, 2, 3`; `Stages 1, 2, and 3`; and `and` / `&` conjunctions. A bare number with no `stage` word anchoring it is NOT read as a citation, so `covered by the selected plan, step 4` cites nothing. **A range cites only its two endpoints:** `Stages 1-3` cites 1 and 3, and stage 2 stays uncited — write every stage out. Range syntax (`-`, `to`, `through`) is still parsed, so `Stages 7-8` is a valid two-stage citation; what it cannot do is stand in for an interior nobody named. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/validation.py` `_validate_stage_has_requirement` via `okstra_ctl.stage_citations.enumerated_stage_numbers` fails the plan when any Stage Map stage is cited by no coverage row.
197
197
  - **Why enumeration is the scale gate.** The number of stages a plan carries is not bounded by any threshold — a genuinely large requirement may need many, and okstra does not guess a ratio. What IS bounded is how cheaply a plan can *claim* coverage of them: one `Stages 1-64` cell used to satisfy the reverse check for the whole map while the planner confirmed nothing, so scale grew for free. Enumeration prices it — every stage you claim costs you the act of naming it and asking whether this requirement is really satisfied there. A plan that cannot bring itself to type the numbers is telling you the stages are not all needed. The typing is the confirmation, so do not batch it mechanically: a row listing `Stages 1, 2, 3, ..., 12` you did not check one by one is the same rubber stamp with more characters.
198
198
  - Because that reader only sees prose, it still cannot tell a citation from a mention: `Stage 1 (superseded by Stage 2)` counts Stage 1 as cited. Cite the stages a requirement is actually satisfied by, not stages merely mentioned.
199
199
  - **Requirement Coverage (mandatory, §5.5.8):** selected-direction plans preserve the original requirement IDs and link each row to `stageRefs`, `stepRefs`, `validationRefs`, and `fileRefs`; exact forward and reverse coverage is enforced by `validate_selected_direction_plan`. Legacy candidate-comparison plans retain one `R-NNN` row per concrete requirement and the existing Option Candidate plus Stage/Step `coveredBy` semantics. The exact `P-Req-*` queue comes from `scripts/okstra_ctl/plan_items.py` in both branches.
@@ -216,13 +216,13 @@ Plan for the actual worktree layout before approval. Shared documentation direct
216
216
  would make this plan item incorrect even if its happy path succeeds?
217
217
  ```
218
218
 
219
- An `AGREE` note records the counterexample considered and its exclusion reason. If the judgement needs unavailable external material, record `verification-error`, not `DISAGREE`. **Enforced:** `validators/validate-run.py` `_validate_plan_item_extraction_completeness` compares the exact deterministic set, independently rejecting missing, unexpected, and duplicate plan-item IDs, including `P-Prep-*`.
220
- - **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead runs a worker peer-review round on the persisted queue. Selected-direction plans begin with `P-Dir-1`; legacy candidate-comparison plans begin with `P-Opt-*`; both continue with the shared execution items. The fixed order remains initial verification → one planner self-fix → targeted re-verification → lead decision or immediate user confirmation. Gate recomputation, extraction completeness, approval-context reconciliation, and self-fix limits remain enforced by `validators/validate-run.py`; verdict details and dissent format are owned by `prompts/lead/plan-body-verification.md`.
219
+ An `AGREE` note records the counterexample considered and its exclusion reason. If the judgement needs unavailable external material, record `verification-error`, not `DISAGREE`. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_plan_item_extraction_completeness` compares the exact deterministic set, independently rejecting missing, unexpected, and duplicate plan-item IDs, including `P-Prep-*`.
220
+ - **§5.5.9 Plan Body Verification (BLOCKING).** After report-writer finishes the draft, the lead runs a worker peer-review round on the persisted queue. Selected-direction plans begin with `P-Dir-1`; legacy candidate-comparison plans begin with `P-Opt-*`; both continue with the shared execution items. The fixed order remains initial verification → one planner self-fix → targeted re-verification → lead decision or immediate user confirmation. Gate recomputation, extraction completeness, approval-context reconciliation, and self-fix limits remain enforced by `validators/validate-run.py`; verdict details and dissent format are owned by `scripts/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md`.
221
221
  - **Approval decision state.** The lead records active and carried decisions through `okstra approval-decision`. Every option carries `disposition`, one `reach`, and optional `scopeEffects`. A resolved decision names existing `A-NNN` checks; report assembly derives `approvalContext`, status, resolution, and reverse links.
222
222
  - `open → answered` when the raw user response is recorded
223
223
  - `answered → resolved` after the selected disposition is applied, when that work completed
224
224
  - `open → obsolete` only when a plan change removes the question
225
- `open` blocks until the user judges. `answered` with `accept-risk` / `select` / `answer` does not block. `request-revision` / `reject` still block unless this report's `supersessionLedger` already incorporated that id. Do not move `answered` back to `open` because a check failed. **Enforced:** `scripts/okstra_ctl/clarification_items.py` `row_blocks_progress`, `validators/validate-run.py` `_validate_approval_context`, run-prep `scripts/okstra_ctl/run.py` `_validate_approved_plan`.
225
+ `open` blocks until the user judges. `answered` with `accept-risk` / `select` / `answer` does not block. `request-revision` / `reject` still block unless this report's `supersessionLedger` already incorporated that id. Do not move `answered` back to `open` because a check failed. **Enforced:** `scripts/okstra_ctl/clarification_items.py` `row_blocks_progress`, `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_approval_context`, run-prep `scripts/okstra_ctl/run.py` `_validate_approved_plan`.
226
226
  - **Terminal approval evidence.** A resolved correctness-critical decision names a later successful evaluation through `resolutionInput.checkRefs`. Each referenced activity carries the same `C-NNN` in `clarificationRefs[]`, the affected `planItemIds[]`, zero-exit commands, and the plan-body state result. Report assembly rejects a missing activity or reverse link before publication. **Enforced:** `scripts/okstra_ctl/report_assembly.py::_clarification_row` and `_attach_plan_backlinks`.
227
227
  - **Decision-record evaluation (sole owner)**: this phase is the **single owner** of decision-record evaluation in the okstra lifecycle. The brief never evaluates or drafts decision records — it only forwards `adr-candidate:*` signals. Every `adr-candidate:*` entry inherited from the brief's `Open Questions` is a mandatory evaluation target. In addition, evaluate every decision the chosen realization introduces against the three criteria:
228
228
  1. **Hard to reverse** — would changing the decision later cost meaningfully more than deciding now?
@@ -242,8 +242,8 @@ Plan for the actual worktree layout before approval. Shared documentation direct
242
242
  3. **Internal consistency** — the chosen realization's file list, interfaces, stages, and validation must agree on paths, names, and signatures. A symbol called `clearLayers()` in one field and `clearFullLayers()` in the steps is a bug.
243
243
  4. **Ambiguity check** — any requirement that could be read two ways must be made explicit or moved to the `## 1. Clarification Items` table as a `Blocks=approval` row.
244
244
  5. **Scope check** — if the plan now spans multiple independent subsystems, split it into separate planning runs rather than shipping an oversized plan. Then walk the plan in the expansion direction: for every stage, name the Requirement Coverage row that demanded it, and for every requirement row, read its `Source` cell as a skeptic — does the cited brief heading actually exist, and does a `derived:` rationale state a real technical consequence rather than a preference? Move anything that fails to a `Blocks=approval` clarification row.
245
- 6. **Review-rule preflight check** — when a project review rule pack applies (cited by the brief, or listed by `okstra model-io project-context --project-root <PROJECT_ROOT> --task-ref <task-ref>`), map each relevant rule to the chosen realization. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
246
- 7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** For every §5.5.9 `planItems[]` entry whose *gate class after stage scope* is `majority-disagree` (in-scope execution only), set that item's `clarificationId` to a `C-<N>` row that MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. Do **not** promote `observed` / `deferred` / `record` items — those belong in `setAside`. Raw vote `majority-disagree` is not enough; promoting it is how a frozen or unreached stage kept opening new `C-NNN` rows. **Enforced:** `validators/validate-run.py` `_validate_plan_body_clarification_matching` uses `_plan_item_gate_class` and fails when an in-scope execution majority-disagree item has no `clarificationId`, or its `clarificationId` is dangling / points at a non-`approval` row. For `partial-consensus` and `dissent-isolated` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
245
+ 6. **Review-rule preflight check** — when a project review rule pack applies (cited by the brief, or listed by `okstra model-io project-context --project-root <PROJECT_ROOT> --task-ref <TASK_KEY>` (the full `Task key` your dispatch prompt names)), map each relevant rule to the chosen realization. Reject the draft if it knowingly creates a violation that the later PR reviewer would flag, unless the plan records a specific rationale and follow-up. In particular, scan for repeated helper stacks across planned files, tests that assert delegation to the same calculator/helper they exercise, public names that hide side effects, domain rules placed in repositories/adapters, and APIs made dead by this change.
246
+ 7. **Plan-body verification reconciliation (BLOCKING for implementation-planning).** For every §5.5.9 `planItems[]` entry whose *gate class after stage scope* is `majority-disagree` (in-scope execution only), set that item's `clarificationId` to a `C-<N>` row that MUST exist in `## 1. Clarification Items` with `Kind` chosen per the standard policy and `Blocks=approval`. Do **not** promote `observed` / `deferred` / `record` items — those belong in `setAside`. Raw vote `majority-disagree` is not enough; promoting it is how a frozen or unreached stage kept opening new `C-NNN` rows. **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_plan_body_clarification_matching` uses `_plan_item_gate_class` and fails when an in-scope execution majority-disagree item has no `clarificationId`, or its `clarificationId` is dangling / points at a non-`approval` row. For `partial-consensus` and `dissent-isolated` plan-items, the dissenting opinion lives in §5.5.9 `Dissent log` and is NOT promoted to §5.
247
247
  8. **Stage Map self-check** — for every stage, count the effective rows of its `Stepwise Execution Order` table by hand; reject the draft if any stage exceeds 8. Confirm each stage declares a non-empty `Slice value:` and `Acceptance:` line, the three `Test case (success|boundary|failure):` lines (or carries a `TDD exemption:` line), and that its first step `action` starts with `RED:` with a later `GREEN:` — this is what validator S10 enforces, including S10d on the test-case lines. Read each stage's three test-case lines as a reviewer: reject any that restates the happy path in all three slots, leaves `boundary` blank, or writes `N/A` where a real edge input exists. Walk the `depends-on` graph and confirm it is a DAG (no cycle, no self-reference). For each `depends-on` link, confirm it encodes a real data/contract dependency — do NOT add links to serialise unrelated work, and do NOT split a stage merely to create more parallel stages. **Parallel-safety:** for every pair of `depends-on (none)` stages, confirm their `Stage Exit Contract` predicted file sets are disjoint; if they share a file, merge them or add a `depends-on` link (validator S9 rejects overlap). **Project-boundary:** confirm no stage mixes edits from two projects (different repo/`PROJECT_ROOT` or different top-level deployable module); if any stage does, split it per project. For multi-project plans, confirm each stage's `title` carries its `[<project>]` tag and the `Cross-project parallelism:` line under the table records the parallel-vs-sequenced determination (with the forcing dependency) for every project pair; for cross-repo work, confirm it is split into separate per-repo runs (required — one run structurally cannot touch another repo) rather than crammed into one task's stages.
248
248
  9. **Cross-project dependency check** — confirm you have not missed a dependency on another repo / another top-level deployable module / a published package. If `dependencyMigrationRisk` has a `kind: cross-project` row, confirm a matching `direction: upstream-precondition` `XP-NNN` row exists in `crossProjectDependencies`, and re-read as a reviewer whether its `requiredWork` is the concrete work the other side must actually build rather than an abstract phrase ("other side's work done") — validator S only checks existence, so concreteness is the self-review's responsibility. Confirm cross-repo work is split into a separate run + XP row instead of being crammed into one task's stages, and that the cross-project substance is not duplicated in `§3 Recommended Next Steps` but lives only in `§5.4 Cross-Project Dependencies`.
249
249
  10. **Decision-draft materialization check** — when `decisionDrafts` is non-empty, confirm as a reviewer which stage's stepwise order contains the matching materialization step (creating `.okstra/decisions/<NNNN>-<slug>.md`) and that the number of drafts corresponds 1:1 with the materialization steps. The validator only checks the *existence* of the step, so the `<NNNN>-<slug>` correctness and count correspondence are the self-review's responsibility.
@@ -251,4 +251,53 @@ Plan for the actual worktree layout before approval. Shared documentation direct
251
251
  12. **Approval blast-radius check (BLOCKING).** Every approval clarification must be reachable from `planItems[].clarificationRefs[]` or a requirement-coverage blocker. Report assembly derives plan-item links from activity `clarificationRefs[]` plus `planItemIds[]`; `okstra incremental-scope` reads the resulting reverse links.
252
252
  - **The link must resolve to a stage, not merely exist.** `incremental-scope` reads the stage number out of a `P-Step-<stage>.<step>` / `P-Prep-S<stage>-<kind>` plan-item id, out of `stageScope` / `stageRefs` on the linked plan item or coverage row, or out of a `Stage N` citation in the blocked coverage row's `coveredBy`. A `P-Req-*` / `P-Val-*` id is positional, so a blocker linked only that way MUST carry `stageRefs` or cite the stage in `coveredBy`. Writing the blocked row's `coveredBy` as prose with no `Stage N` in it — `No stage.`, `Partly covered — …` — satisfies nothing unless `stageRefs` is present: the row passes the link check and the next re-run cannot place the answer without asking for stage numbers.
253
253
  - What to write when no stage covers the requirement yet: name the stage the answer will change, not the stage that satisfies the requirement today. A `Blocks=approval` row is admissible only when, absent an answer, `implementation` would produce wrong or unsafe code (see the admissibility rule above) — so some stage's code is at stake by construction. If you genuinely cannot name one, the row fails the admissibility test and belongs in `## 5. Missing Information and Risks` with `Blocks=none`, not in the approval gate.
254
- **Enforced:** `validators/validate-run.py` `_validate_approval_clarification_backtrace` — one failure for a missing link, a separate one for a link that resolves to no stage.
254
+ **Enforced:** `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_approval_clarification_backtrace` — one failure for a missing link, a separate one for a link that resolves to no stage.
255
+
256
+ ### Phase 6 sub-step: Plan-body verification (implementation-planning only, BLOCKING)
257
+
258
+ After the Report writer worker narrative is reviewed, **if** `task_type == "implementation-planning"` **and** `task-manifest.json` `convergence.planBodyVerification.enabled == true` (default), the lead MUST run the plan-body verification sequence on the consolidated plan body before declaring Phase 6 complete and entering Phase 7.
259
+
260
+ 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, labels in the "Quick Reference" table above as the single source of truth) is preserved. The round's outcome is read from the final report's `### 5.5.9 Plan Body Verification` section and `implementationPlanning.planBodyVerification` in its data.json — it is not a separate lifecycle phase identifier.
261
+
262
+ **REQUIRED RESOURCE:** Read the common procedure of [plan-body-verification](instructions/plan-body-verification.md) for the round protocol, plan-item ID scheme (`P-Dir-1` for selected-direction; `P-Opt-*` for legacy candidate comparison; then `P-Step-*` / `P-Dep-*` / `P-Val-*` / `P-Rb-*` / `P-Req-*` / `P-Prep-*`), verdict semantics (`AGREE` / `DISAGREE(a-f)` / `SUPPLEMENT`), classification rules, gate-result resolution, and state-path authority. Read the state-file schema only when diagnosing state or projection validation. For `P-Dir-1`, compare `directionRealization` with `selectedDirectionRef` and its snapshot: verify the core mechanism, architecture boundaries, planning invariants, and any hidden direction change.
263
+
264
+ Read from the resolved runtime resource path, replacing `<plan-body-contract-path>`
265
+ with that resource's absolute path. This prints the complete common procedure and
266
+ its conditional reading table, stopping before reference examples:
267
+
268
+ ```sh
269
+ awk '/^## Reference material$/ {exit} {print}' '<plan-body-contract-path>'
270
+ ```
271
+
272
+ Follow the conditional reading table for later rounds and validation failures.
273
+ The command keeps the filename in the read trace used by
274
+ `validators/validate_session_conformance.py` `_ENTRY_GUARD_READS`.
275
+ The bounded output is checked by
276
+ `tests/contract/test_contract_examples_execute.py::test_plan_body_common_read_keeps_gate_rules`.
277
+
278
+ Distinct from Phase 5.5 finding convergence:
279
+
280
+ - Phase 5.5 reconciles worker **findings** (F-*) from independent analysis.
281
+ - This sub-step reconciles the **consolidated plan body** (P-*) authored by the Report writer worker.
282
+ - The two rounds use disjoint queues and separate state files — see [plan-body-verification](instructions/plan-body-verification.md) "MUTUAL EXCLUSION (BLOCKING)".
283
+
284
+ Lead's responsibilities in this sub-step (in order):
285
+
286
+ For a new `implementation-planning` run, the fixed order is initial verification → one planner self-fix → targeted re-verification → lead decision or immediate user confirmation. The initial verification is round 1 and the targeted re-verification is round 2. A second automatic self-fix is a contract violation. When `okstra plan-items prepare` reports `"gating": false` (one-stage `no-design-inputs` plan), skip the self-fix loop and the sweep batch: extraction and round 1 still run, then go to the user gate. Two-or-more stages, a PREP item, or non-empty `designPreparation.items` keep `gating: true` and the full order.
287
+
288
+ 1. Build the queue with `okstra plan-items prepare --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`, place the output of `okstra plan-items prompt --run-manifest <run-manifest>` verbatim in every verifier prompt, then run `okstra plan-items validate-prepared --narrative <report-writer-narrative.md> --run-manifest <run-manifest>`. Python resolves the one convergence-owned state path from that run identity. The lead MUST NOT summarise, select, omit, reorder, or renumber the queue. Each prompt uses the compact subject plus the lossless payload, and asks every item:
289
+
290
+ ```text
291
+ What concrete false-positive input, failure ordering, or omitted dependency
292
+ would make this plan item incorrect even if its happy path succeeds?
293
+ ```
294
+
295
+ An `AGREE` response records the considered counterexample and exclusion reason in its note; unverified external material is `verification-error`, not `DISAGREE`.
296
+ 2. Dispatch a single plan-body reverify round to every analyser worker in the roster (`claude`, `codex`, and `antigravity` when opted in). `Report writer worker` is NOT a participant in this round.
297
+ 3. Record each verifier Markdown result through `okstra plan-items apply-verdicts --state <plan-body-verification.json> --result <worker-id>=<result.md> --round <N>`. Python validates every submitted `P-*` identifier against the current convergence state and overwrites only that round's verdicts. Then resolve the gate result to one of `passed` / `passed-with-dissent` / `blocked-by-disagreement` / `aborted-non-result`.
298
+ 4. After `okstra plan-verify` succeeds, run `okstra plan-items complete-round --state <plan-body-verification.json> --run-manifest <current-run-manifest.json> --round <N>`. Python reads the current worker assignments, atomically appends the convergence-owned history, and updates its nested final projection. This state is the only plan-verification input report assembly reads.
299
+ 5. Record every *in-scope execution* `majority-disagree` decision through `okstra approval-decision`; record its plan and clarification links only on activities. Do not promote `observed` / `deferred` / `record` items, and do not append `clarificationItems[]` directly.
300
+ 6. Run report assembly after the final plan-body state, approval ledger, design snapshot, activity ledger, and team state are complete. Assembly writes `implementationPlanning.planBodyVerification` and derived clarification rows while publishing `data.json` once.
301
+ 7. Publish the report record `frontmatter.approved` field as `false`. There is no in-body `- [ ] Approved` marker line — approval lives only in the record (see [plan-body-verification](instructions/plan-body-verification.md) §"Round protocol" step 9). The user may set it to `true` (via `--approve` or the in-session wizard) when the gate is `passed` / `passed-with-dissent`, or under `blocked-by-disagreement` once every remaining `Blocks=approval` row is user-proceeded (`accept-risk` / `select` / `answer`); `aborted-non-result` still withholds approval. **Enforced:** run-prep (`scripts/okstra_ctl/run.py` `_validate_approved_plan` / `_blocking_gate_survives_user_decision`) fail-closes an `approved: true` plan whose blocking gate carries no user disposition, and `scripts/okstra_ctl/phases/implementation_planning/plan_body.py` `_validate_plan_body_gate_recompute` fails a gate value upgraded past what the recorded votes support. Manually flipping a blocked gate to passing is a contract violation.
302
+
303
+ If `convergence.planBodyVerification.enabled == false` (set by `--no-plan-verification` or by `okstra config set plan-verification off`), the entire sub-step is skipped and the top-of-report Approval marker is rendered unconditionally (legacy behaviour). This opt-out is intended for fast iteration only and is not recommended for handoff-ready plans.
@@ -0,0 +1,237 @@
1
+ """구현 계획 보고서의 사용자 표시 모델."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from copy import deepcopy
7
+ from typing import Any, Mapping
8
+
9
+ from okstra_ctl.design_surfaces import DesignSurfaceTrigger, detect_design_surfaces
10
+ from okstra_ctl.report_projections import ReportProjectionError
11
+ from okstra_ctl.scope_provenance import parse_source
12
+
13
+ from okstra_ctl.plan_approval import plan_approval_state
14
+ from okstra_ctl.report_html.common import evidence_index
15
+ from okstra_ctl.report_html.models import HumanReportView, VisualEdge, VisualNode
16
+ from okstra_ctl.report_html.visualizations import stage_map_figure
17
+
18
+
19
+ def _stage_figure(planning: dict):
20
+ nodes = tuple(
21
+ VisualNode(
22
+ f"stage-{row['stage']}",
23
+ f"Stage {row['stage']} · {row['title']}",
24
+ "stage",
25
+ "planned",
26
+ row["exitContractSummary"],
27
+ )
28
+ for row in planning["stageMap"]
29
+ )
30
+ edges = tuple(
31
+ VisualEdge(
32
+ f"stage-{dependency}", f"stage-{row['stage']}", "precedes", "dependency"
33
+ )
34
+ for row in planning["stageMap"]
35
+ for dependency in re.findall(r"\d+", row["dependsOn"])
36
+ )
37
+ return stage_map_figure(
38
+ nodes=nodes, edges=edges, title="Implementation stage dependencies"
39
+ )
40
+
41
+
42
+ # Record fields this template anchors as `id-<row id>` (see
43
+ # `HumanReportView.anchored_fields`). The implementer-facing tables the
44
+ # approver does not read — validation checklist, cross-project dependencies,
45
+ # migration risk, rollback — are not drawn, so their rows land in the ledger.
46
+ ANCHORED_FIELDS = (
47
+ "implementationPlanning.directionRealization.fileStructure",
48
+ "implementationPlanning.directionRealization.planningInvariants",
49
+ "implementationPlanning.optionCandidates.fileStructure",
50
+ "implementationPlanning.designPreparation.items",
51
+ "implementationPlanning.requirementCoverage",
52
+ "implementationPlanning.planBodyVerification.planItems",
53
+ "implementationPlanning.supersessionLedger",
54
+ )
55
+
56
+
57
+ def build_implementation_planning_view(data: dict) -> HumanReportView:
58
+ planning = data["implementationPlanning"]
59
+ figure = _stage_figure(planning) if planning.get("stageMap") else None
60
+ approval = plan_approval_state(data)
61
+ activities = tuple(
62
+ row for row in data.get("agentActivity", []) if isinstance(row, dict)
63
+ )
64
+ decision_cards = tuple(
65
+ row
66
+ for row in data.get("clarificationItems", [])
67
+ if isinstance(row, dict)
68
+ and row.get("blocks") == "approval"
69
+ and isinstance(row.get("approvalContext"), dict)
70
+ )
71
+ context = {
72
+ "humanSummary": data["humanSummary"],
73
+ "planning": planning,
74
+ "narrative": planning.get("userNarrative", {}),
75
+ "stageFigure": figure,
76
+ "approval": approval,
77
+ "openDecisions": [
78
+ row
79
+ for row in data.get("clarificationItems", [])
80
+ if isinstance(row, dict)
81
+ and row.get("blocks") == "approval"
82
+ and row.get("status") in {"open", "answered"}
83
+ ],
84
+ "agentActivities": activities,
85
+ "activityById": {
86
+ row["activityId"]: row for row in activities if row.get("activityId")
87
+ },
88
+ "decisionCards": decision_cards,
89
+ "evidenceIndex": evidence_index(data, ANCHORED_FIELDS),
90
+ }
91
+ return HumanReportView(
92
+ "implementation-planning",
93
+ "html/tasks/implementation-planning.template.html",
94
+ context,
95
+ (figure,) if figure is not None else (),
96
+ anchored_fields=ANCHORED_FIELDS,
97
+ )
98
+
99
+
100
+ def _trigger_rows(trigger: DesignSurfaceTrigger) -> list[dict[str, Any]]:
101
+ return [
102
+ {"step": item.step, "field": item.field, "match": item.match}
103
+ for item in trigger.evidence
104
+ ]
105
+
106
+
107
+ def project_design(
108
+ planning: Mapping[str, Any],
109
+ detector_snapshot: Mapping[str, Any],
110
+ ) -> dict[str, Any]:
111
+ """탐지 결과가 실제 계획 표면과 일치할 때만 설계 블록을 반환한다."""
112
+ triggers = detect_design_surfaces(planning)
113
+ expected = {(row.stage, row.kind): _trigger_rows(row) for row in triggers}
114
+ coverage = detector_snapshot.get("stageCoverage")
115
+ coverage = coverage if isinstance(coverage, list) else []
116
+ actual: dict[tuple[int, str], list[dict[str, Any]]] = {}
117
+ for stage in coverage:
118
+ if not isinstance(stage, Mapping):
119
+ raise ReportProjectionError(
120
+ "owner=design-surface-detector invalid stageCoverage row"
121
+ )
122
+ for row in stage.get("rows") or []:
123
+ if isinstance(row, Mapping):
124
+ key = (int(stage.get("stage") or 0), str(row.get("kind") or ""))
125
+ if key in actual:
126
+ raise ReportProjectionError(
127
+ "owner=design-surface-detector duplicate stageCoverage "
128
+ f"for stage {key[0]} kind `{key[1]}`"
129
+ )
130
+ actual[key] = list(row.get("triggerEvidence") or [])
131
+ if expected != actual:
132
+ raise ReportProjectionError(
133
+ "owner=design-surface-detector trigger coverage mismatch"
134
+ )
135
+ preparation = detector_snapshot.get("designPreparation")
136
+ if not isinstance(preparation, Mapping):
137
+ raise ReportProjectionError(
138
+ "owner=design-surface-detector missing designPreparation"
139
+ )
140
+ return {
141
+ "designPreparation": deepcopy(dict(preparation)),
142
+ "stageCoverage": deepcopy(coverage),
143
+ }
144
+
145
+
146
+ def attach_design_snapshot(
147
+ planning: dict[str, Any], snapshot: Mapping[str, Any]
148
+ ) -> None:
149
+ """검증한 설계 준비 결과를 계획 단계와 연결한다."""
150
+ design = project_design(planning, snapshot)
151
+ planning["designPreparation"] = design["designPreparation"]
152
+ stages = {
153
+ row.get("stage"): row
154
+ for row in planning.get("stages") or []
155
+ if isinstance(row, dict)
156
+ }
157
+ for coverage in design["stageCoverage"]:
158
+ stage = stages.get(coverage.get("stage"))
159
+ if stage is not None:
160
+ stage["designSurfaceCoverage"] = coverage.get("rows") or []
161
+
162
+
163
+ def _sync_human_summary_with_plan_body_gate(
164
+ data: dict[str, Any],
165
+ projection: Mapping[str, Any],
166
+ ) -> None:
167
+ """게이트가 통과했는데 내러티브가 unseeded 로 남으면 사람용 칸을 맞춘다.
168
+
169
+ report-writer 는 PBV 전에 쓰고, 조립이 게이트를 덮어씌운다. blockers 와
170
+ nextStep 이 그때의 문장을 그대로 두면 통과한 계획이 아직 검증 전으로 보인다.
171
+ """
172
+ gate = str(projection.get("gateResult") or "")
173
+ if gate not in {"passed", "passed-with-dissent"}:
174
+ return
175
+ summary = data.get("humanSummary")
176
+ if isinstance(summary, dict) and isinstance(summary.get("blockers"), list):
177
+ summary["blockers"] = [
178
+ row for row in summary["blockers"] if "unseeded" not in str(row).lower()
179
+ ]
180
+ card = data.get("verdictCard")
181
+ if not isinstance(card, dict):
182
+ return
183
+ next_step = str(card.get("nextStep") or "")
184
+ lowered = next_step.lower()
185
+ if "unseeded" not in lowered and "seed" not in lowered:
186
+ return
187
+ next_step = (
188
+ "Plan-body verification passed. Do not start implementation "
189
+ "until `frontmatter.approved` is true."
190
+ )
191
+ card["nextStep"] = next_step
192
+ final = data.get("finalVerdict")
193
+ if isinstance(final, dict):
194
+ final["nextStep"] = next_step
195
+
196
+
197
+ def _fill_end_state_coverage(data: dict[str, Any]) -> None:
198
+ """writer 가 못 쓴 endStateCoverage 를 requirementCoverage brief:EB-001 에서 채운다.
199
+
200
+ 내러티브 스키마가 이 칸을 열어 주면 writer 가 쓸 수 있다. 비어 있으면
201
+ validate-run 이 brief id 마다 한 행을 요구하므로 조립이 채운다.
202
+ """
203
+ existing = data.get("endStateCoverage")
204
+ if isinstance(existing, list) and existing:
205
+ return
206
+ planning = data.get("implementationPlanning")
207
+ if not isinstance(planning, dict):
208
+ return
209
+ derived = _end_state_rows_from_requirement_coverage(planning)
210
+ if derived:
211
+ data["endStateCoverage"] = derived
212
+
213
+
214
+ def _end_state_rows_from_requirement_coverage(
215
+ planning: Mapping[str, Any],
216
+ ) -> list[dict[str, Any]]:
217
+ rows: list[dict[str, Any]] = []
218
+ seen: set[str] = set()
219
+ for item in planning.get("requirementCoverage") or []:
220
+ if not isinstance(item, Mapping):
221
+ continue
222
+ ref = parse_source(str(item.get("source") or ""))
223
+ if not ref.is_end_state_id or ref.value in seen:
224
+ continue
225
+ seen.add(ref.value)
226
+ status = str(item.get("status") or "")
227
+ row: dict[str, Any] = {
228
+ "id": ref.value,
229
+ "disposition": "addressed" if status == "covered" else "deferred",
230
+ "coveredBy": str(item.get("id") or item.get("coveredBy") or ""),
231
+ }
232
+ if row["disposition"] != "addressed":
233
+ row["rationale"] = (
234
+ f"requirementCoverage `{item.get('id')}` status is `{status or 'empty'}`."
235
+ )
236
+ rows.append(row)
237
+ return rows
@@ -53,7 +53,7 @@ taskType: "{{FM_TASK_TYPE}}"
53
53
 
54
54
  > Analysers MUST NOT expand option candidates, file lists, or step plans into items listed here. If an analyser believes an excluded item must be addressed to satisfy the requirement, it is reported as a recommended follow-up task in the final report — never silently folded into a candidate option. If this section is left empty, analysers treat any change beyond what `Requirement Summary` explicitly demands as out of scope by default.
55
55
 
56
- **Enforced (delivery only):** `tests/contract/test_scope_boundary_delivery.py` pins that a filled section reaches every analysis worker through `okstra_ctl.analysis_packet` — drop it from `CANONICAL_BRIEF_SECTIONS` and every run's exclusions vanish silently. The exclusions themselves are prose and are **not** machine-checked: only the codebase-scan `out-of-scope` frontmatter is a path list, and `validators/validate_improvement_report.py` checks that one.
56
+ **Enforced (delivery only):** `tests/contract/test_scope_boundary_delivery.py` pins that a filled section reaches every analysis worker through `okstra_ctl.analysis_packet` — drop it from `CANONICAL_BRIEF_SECTIONS` and every run's exclusions vanish silently. The exclusions themselves are prose and are **not** machine-checked: only the codebase-scan `out-of-scope` frontmatter is a path list, and `scripts/okstra_ctl/phases/improvement_discovery/validation.py` checks that one.
57
57
 
58
58
  ## Planning Concerns
59
59
 
@@ -117,7 +117,7 @@ Enforced: `scripts/okstra_ctl/run.py` `_validate_approved_plan` reads that front
117
117
  ## Phase Boundary
118
118
 
119
119
  - This task type produces a plan only. Source code MUST NOT be modified, builds/migrations/deployments MUST NOT be executed, and follow-up phases MUST NOT be started inside the same run.
120
- Enforced: the forbidden actions for this phase are declared in `prompts/profiles/forbidden-actions.json` and scanned post-hoc by `validators/forbidden_actions.py` `scan_forbidden_actions`; every planning worker runs under a `source-readonly` write policy that `scripts/okstra_ctl/execution_mutation_audit.py` `_source_changes` audits.
120
+ Enforced: the forbidden actions for this phase are declared in `scripts/okstra_ctl/phases/implementation_planning/boundary.json` and scanned post-hoc by `validators/forbidden_actions.py` `scan_forbidden_actions`; every planning worker runs under a `source-readonly` write policy that `scripts/okstra_ctl/execution_mutation_audit.py` `_source_changes` audits.
121
121
  - If workers detect that the plan cannot be completed without further error analysis or new requirements, record that in the final report and recommend re-routing to `error-analysis` or `requirements-discovery` rather than guessing.
122
122
 
123
123
  ## Conversion Note