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
@@ -0,0 +1,118 @@
1
+ """CLI 워커 프로세스 그룹의 메모리 상한과 종료 뒤 잔여 프로세스 정리.
2
+
3
+ 워커가 실행한 빌드는 워커와 같은 프로세스 그룹에 들어간다(`worker_runner` 가
4
+ `start_new_session=True` 로 띄운다). fontsninja-v3-site 에서 Turbopack PostCSS 워커가
5
+ 57GB, `next build` 가 78GB 까지 커져 커널 패닉(watchdog timeout)이 났고, 본 프로세스가
6
+ 죽은 뒤에도 PostCSS 워커가 남아 계속 커졌다(2026-09-27).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import signal
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+ from okstra_project.dirs import okstra_home, project_json_path
16
+
17
+ from .json_boundary import JsonBoundaryError, load_owned_object
18
+
19
+ CONFIG_KEY = "workerMemoryCapMb"
20
+ # 이 프로젝트의 정상 `next build` 는 5.4–5.6GB 였다. 상한은 그보다 넉넉하되, 물리
21
+ # 메모리가 작은 기기에서는 절반을 넘지 않게 한다.
22
+ _DEFAULT_CEILING_BYTES = 8 * 1024 ** 3
23
+
24
+
25
+ class MemoryCapConfigError(ValueError):
26
+ pass
27
+
28
+
29
+ def physical_memory_bytes() -> int:
30
+ return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
31
+
32
+
33
+ def memory_cap_bytes(project_root: Path, global_config_path: Path | None = None) -> int:
34
+ """워커 그룹 RSS 합계 상한. 0 은 감시하지 않음.
35
+
36
+ 프로젝트 `.okstra/project.json` → `~/.okstra/config.json` → 기본값 순서다.
37
+ """
38
+ for label, path in (
39
+ ("project", project_json_path(project_root)),
40
+ ("global", global_config_path or okstra_home() / "config.json"),
41
+ ):
42
+ value = _configured_cap_mb(label, path)
43
+ if value is not None:
44
+ return value * 1024 ** 2
45
+ return min(_DEFAULT_CEILING_BYTES, physical_memory_bytes() // 2)
46
+
47
+
48
+ def _configured_cap_mb(label: str, path: Path) -> int | None:
49
+ if not path.is_file():
50
+ return None
51
+ try:
52
+ payload = load_owned_object(path, artifact=f"{label} configuration")
53
+ except JsonBoundaryError as exc:
54
+ raise MemoryCapConfigError(f"{label} config is unreadable: {path}") from exc
55
+ value = payload.get(CONFIG_KEY) if isinstance(payload, dict) else None
56
+ if value is None:
57
+ return None
58
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
59
+ raise MemoryCapConfigError(
60
+ f"{CONFIG_KEY} must be a non-negative integer (MB), got {value!r}: {path}")
61
+ return value
62
+
63
+
64
+ def group_rss_bytes(pgid: int) -> int:
65
+ """프로세스 그룹 `pgid` 에 속한 프로세스들의 RSS 합계."""
66
+ try:
67
+ output = subprocess.run(
68
+ ["ps", "-A", "-o", "pgid=,rss="],
69
+ capture_output=True, text=True, check=True,
70
+ ).stdout
71
+ except (OSError, subprocess.CalledProcessError):
72
+ return 0
73
+ total_kb = 0
74
+ for row in output.splitlines():
75
+ fields = row.split()
76
+ if len(fields) == 2 and fields[0] == str(pgid) and fields[1].isdigit():
77
+ total_kb += int(fields[1])
78
+ return total_kb * 1024
79
+
80
+
81
+ def kill_group(pgid: int) -> bool:
82
+ """그룹에 남은 프로세스를 SIGKILL 한다. 보낼 대상이 있었으면 True."""
83
+ try:
84
+ os.killpg(pgid, signal.SIGKILL)
85
+ except ProcessLookupError:
86
+ return False
87
+ return True
88
+
89
+
90
+ class MemoryWatch:
91
+ """주기적으로 그룹 RSS 를 재고, 상한을 넘으면 그룹을 죽인다."""
92
+
93
+ interval_seconds = 2.0
94
+
95
+ def __init__(self, cap_bytes: int) -> None:
96
+ self.cap_bytes = cap_bytes
97
+ self.exceeded_bytes = 0
98
+ self._next_sample = 0.0
99
+
100
+ def check(self, pgid: int, now: float) -> bool:
101
+ """상한을 넘어 그룹을 죽였으면 True."""
102
+ if not self.cap_bytes or self.exceeded_bytes or now < self._next_sample:
103
+ return False
104
+ self._next_sample = now + self.interval_seconds
105
+ rss = group_rss_bytes(pgid)
106
+ if rss <= self.cap_bytes:
107
+ return False
108
+ self.exceeded_bytes = rss
109
+ kill_group(pgid)
110
+ return True
111
+
112
+ def failure(self) -> str:
113
+ mb = 1024 ** 2
114
+ return (
115
+ f"memory-cap exceeded: worker process group used {self.exceeded_bytes // mb} MB, "
116
+ f"cap {self.cap_bytes // mb} MB ({CONFIG_KEY} in .okstra/project.json "
117
+ "or ~/.okstra/config.json)"
118
+ )
@@ -19,7 +19,7 @@ import sys
19
19
  from pathlib import Path
20
20
 
21
21
  from .paths import find_asset_root
22
- from .phases.catalog import PhaseAssetError, UnknownTaskType, profile_markdown
22
+ from .phases.catalog import PhaseAssetError, UnknownTaskType, profile_markdown, profile_sidecar
23
23
  from .role_requirements import RoleProfileError, load_role_profile
24
24
  from .run import PrepareError, _expand_profile_includes
25
25
 
@@ -29,7 +29,7 @@ _PROFILES_REL = ("prompts", "profiles")
29
29
  # `prompts/profiles/_implementation-executor.md`. Reading the list from the
30
30
  # profile body rather than hard-coding it here is deliberate: a list in code
31
31
  # goes stale the moment a profile adds a sidecar, and does so silently.
32
- _SIDECAR_RE = re.compile(r"`(prompts/profiles/_[\w-]+\.md)`")
32
+ _SIDECAR_RE = re.compile(r"`((?:prompts/profiles|scripts/okstra_ctl/phases/implementation/instructions)/_[\w-]+\.md)`")
33
33
 
34
34
 
35
35
  def workspace_root(start: Path | None = None) -> Path:
@@ -77,7 +77,7 @@ def sidecar_bodies(root: Path, profile_text: str) -> list[str]:
77
77
  if relative in seen:
78
78
  continue
79
79
  seen.add(relative)
80
- path = root / relative
80
+ path = profile_sidecar(root, relative)
81
81
  if not path.is_file():
82
82
  continue
83
83
  body = _expand_profile_includes(path)
@@ -17,6 +17,8 @@ session id 등) 를 덧붙여 전달한다.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ from .phases.catalog import profile_sidecar
21
+
20
22
  import hashlib
21
23
  import json
22
24
  import re
@@ -2632,7 +2634,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2632
2634
  home_prompts / "lead" / "convergence.md"
2633
2635
  )
2634
2636
  plan_body_verification_path = ctx.get("OKSTRA_PLAN_BODY_VERIFICATION_PATH") or str(
2635
- home_prompts / "lead" / "plan-body-verification.md"
2637
+ profile_sidecar(home_prompts.parent, "prompts/lead/plan-body-verification.md")
2636
2638
  )
2637
2639
  report_writer_path = ctx.get("OKSTRA_REPORT_WRITER_PATH") or str(
2638
2640
  home_prompts / "lead" / "report-writer.md"
@@ -2785,8 +2787,12 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2785
2787
  "`shutdown_workers`, `record_lead_event`, and `collect_usage` only "
2786
2788
  "through its mapping.\n"
2787
2789
  "- Do not call a primitive documented by an unselected adapter.\n"
2788
- "- Record the adapter-required Phase 3 state and emit the canonical "
2789
- "`PROGRESS: phase-3-team-create ...` checkpoint before dispatch.\n"
2790
+ "- Record the adapter-required Phase 3 state. When team-state has "
2791
+ "no `teamCreate` yet, do not append `phase-3-team-create`: the "
2792
+ "first `team dispatch` (cmux-pane run) or `worker-dispatch` writes "
2793
+ "the implicit-team marker, records the checkpoint and prints the "
2794
+ "line to emit. Otherwise append `PROGRESS: phase-3-team-create ...` "
2795
+ "before dispatch.\n"
2790
2796
  f"{adapter_setup_block}\n"
2791
2797
  "- The run manifest is authoritative for concurrent-run metadata, "
2792
2798
  "dispatch backend, and artifact paths."
@@ -2971,7 +2977,12 @@ def render_template_with_ctx(template_path: str, output_path: str, ctx: dict) ->
2971
2977
  Optional defaults (VALIDATION_STATUS etc.) should be filled by calling
2972
2978
  `apply_lead_prompt_defaults(ctx)` in the same setup step.
2973
2979
  """
2974
- template = Path(template_path).read_text(encoding="utf-8")
2980
+ from .phases.catalog import migrated_report_template
2981
+
2982
+ source = Path(template_path)
2983
+ if source.parent.name == "reports" and source.parent.parent.name == "templates":
2984
+ source = migrated_report_template(source.parent.parent.parent, source.name) or source
2985
+ template = source.read_text(encoding="utf-8")
2975
2986
 
2976
2987
  fm_ctx = dict(ctx)
2977
2988
  fm_ctx.setdefault("DOC_TYPE", _doc_type_from_template_path(template_path))
@@ -2,9 +2,10 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import copy
5
- import json
6
5
  import os
7
6
  import tempfile
7
+ from okstra_ctl.phases.implementation_planning.validation import validate_selected_direction_plan
8
+
8
9
  from dataclasses import dataclass
9
10
  from pathlib import Path
10
11
  from typing import Any, Callable, Mapping, Sequence
@@ -25,22 +26,24 @@ from .report_synthesis_packet import (
25
26
  )
26
27
  from .conformance import strip_stage_declaration_label
27
28
  from .design_prep import DesignPrepError, materialize_design_prep_requests
28
- from .implementation_options import validate_blocked_answer_channel
29
- from .technical_verification import validate_technical_verification_report
29
+ from .phases.implementation_option_selection.validation import validate_blocked_answer_channel
30
+ from .phases.technical_verification.validation import validate_technical_verification_report
31
+ from .report_routing import technical_verification_routing_errors
30
32
  from .implementation_direction import (
31
33
  load_selected_direction_snapshot,
32
- validate_selected_direction_plan,
33
34
  )
34
35
  from .paths import task_dir
35
36
  from .phases.final_verification.report import verification_scope
36
37
  from .report_projections import (
37
38
  ReportProjectionError,
38
39
  project_convergence,
39
- project_design,
40
40
  project_execution,
41
41
  project_token_usage,
42
42
  )
43
- from .scope_provenance import parse_source
43
+ from .phases.implementation_planning.report import (
44
+ attach_design_snapshot, _sync_human_summary_with_plan_body_gate,
45
+ _fill_end_state_coverage,
46
+ )
44
47
 
45
48
  from .write_policy import planned_path_declaration_errors
46
49
 
@@ -160,12 +163,21 @@ def _clarifications(
160
163
  _clarification_row(row, activity_by_id, path)
161
164
  for row in active
162
165
  ]
163
- rows.extend(_carried_clarification_rows(ledger, {row.get("id") for row in rows}, path))
166
+ superseded = {
167
+ cid
168
+ for row in active
169
+ if isinstance(row.get("resolutionInput"), Mapping)
170
+ for cid in row.get("supersedes") or []
171
+ }
172
+ rows.extend(_carried_clarification_rows(
173
+ ledger, {row.get("id") for row in rows}, path, superseded,
174
+ ))
164
175
  return rows
165
176
 
166
177
 
167
178
  def _carried_clarification_rows(
168
179
  ledger: Mapping[str, Any], seen_ids: set[object], path: Path,
180
+ superseded: set[str],
169
181
  ) -> list[dict[str, Any]]:
170
182
  """이월 결정은 active 질문이 아니다. 이번 런 활동 원장을 요구하지 않는다."""
171
183
  carried = ledger.get("carriedDecisions")
@@ -185,7 +197,13 @@ def _carried_clarification_rows(
185
197
  cid = decision.get("id")
186
198
  if cid in seen:
187
199
  continue
188
- rows.append(_carried_clarification_row(decision))
200
+ row = _carried_clarification_row(decision)
201
+ if cid in superseded:
202
+ # 다음 run 의 seed 는 `obsolete` 행을 이월하지 않는다. `answered` 로
203
+ # 남기면 대체된 답과 대체한 답이 함께 이월된다.
204
+ row["status"] = "obsolete"
205
+ row.pop("resolution", None)
206
+ rows.append(row)
189
207
  seen.add(cid)
190
208
  return rows
191
209
 
@@ -689,15 +707,9 @@ def _attach_machine_planning(
689
707
  if design_row is None:
690
708
  return
691
709
  try:
692
- design = project_design(planning, _read_json(design_row))
710
+ attach_design_snapshot(planning, _read_json(design_row))
693
711
  except ReportProjectionError as exc:
694
712
  _fail(design_row.owner, design_row.path, "$", exc)
695
- planning["designPreparation"] = design["designPreparation"]
696
- stages = {row.get("stage"): row for row in planning.get("stages") or [] if isinstance(row, dict)}
697
- for coverage in design["stageCoverage"]:
698
- stage = stages.get(coverage.get("stage"))
699
- if stage is not None:
700
- stage["designSurfaceCoverage"] = coverage.get("rows") or []
701
713
  verification_row = inputs.get("plan-body-verification")
702
714
  if verification_row is None:
703
715
  return
@@ -717,83 +729,6 @@ def _attach_machine_planning(
717
729
  _sync_human_summary_with_plan_body_gate(data, projection)
718
730
 
719
731
 
720
- def _sync_human_summary_with_plan_body_gate(
721
- data: dict[str, Any], projection: Mapping[str, Any],
722
- ) -> None:
723
- """게이트가 통과했는데 내러티브가 unseeded 로 남으면 사람용 칸을 맞춘다.
724
-
725
- report-writer 는 PBV 전에 쓰고, 조립이 게이트를 덮어씌운다. blockers 와
726
- nextStep 이 그때의 문장을 그대로 두면 통과한 계획이 아직 검증 전으로 보인다.
727
- """
728
- gate = str(projection.get("gateResult") or "")
729
- if gate not in {"passed", "passed-with-dissent"}:
730
- return
731
- summary = data.get("humanSummary")
732
- if isinstance(summary, dict) and isinstance(summary.get("blockers"), list):
733
- summary["blockers"] = [
734
- row for row in summary["blockers"]
735
- if "unseeded" not in str(row).lower()
736
- ]
737
- card = data.get("verdictCard")
738
- if not isinstance(card, dict):
739
- return
740
- next_step = str(card.get("nextStep") or "")
741
- lowered = next_step.lower()
742
- if "unseeded" not in lowered and "seed" not in lowered:
743
- return
744
- next_step = (
745
- "Plan-body verification passed. Do not start implementation "
746
- "until `frontmatter.approved` is true."
747
- )
748
- card["nextStep"] = next_step
749
- final = data.get("finalVerdict")
750
- if isinstance(final, dict):
751
- final["nextStep"] = next_step
752
-
753
-
754
- def _fill_end_state_coverage(data: dict[str, Any]) -> None:
755
- """writer 가 못 쓴 endStateCoverage 를 requirementCoverage brief:EB-001 에서 채운다.
756
-
757
- 내러티브 스키마가 이 칸을 열어 주면 writer 가 쓸 수 있다. 비어 있으면
758
- validate-run 이 brief id 마다 한 행을 요구하므로 조립이 채운다.
759
- """
760
- existing = data.get("endStateCoverage")
761
- if isinstance(existing, list) and existing:
762
- return
763
- planning = data.get("implementationPlanning")
764
- if not isinstance(planning, dict):
765
- return
766
- derived = _end_state_rows_from_requirement_coverage(planning)
767
- if derived:
768
- data["endStateCoverage"] = derived
769
-
770
-
771
- def _end_state_rows_from_requirement_coverage(
772
- planning: Mapping[str, Any],
773
- ) -> list[dict[str, Any]]:
774
- rows: list[dict[str, Any]] = []
775
- seen: set[str] = set()
776
- for item in planning.get("requirementCoverage") or []:
777
- if not isinstance(item, Mapping):
778
- continue
779
- ref = parse_source(str(item.get("source") or ""))
780
- if not ref.is_end_state_id or ref.value in seen:
781
- continue
782
- seen.add(ref.value)
783
- status = str(item.get("status") or "")
784
- row: dict[str, Any] = {
785
- "id": ref.value,
786
- "disposition": "addressed" if status == "covered" else "deferred",
787
- "coveredBy": str(item.get("id") or item.get("coveredBy") or ""),
788
- }
789
- if row["disposition"] != "addressed":
790
- row["rationale"] = (
791
- f"requirementCoverage `{item.get('id')}` status is `{status or 'empty'}`."
792
- )
793
- rows.append(row)
794
- return rows
795
-
796
-
797
732
  def _schema_owner(error: str) -> str:
798
733
  field = error.split(" ", 1)[0]
799
734
  owners = {
@@ -897,6 +832,7 @@ def assemble_report(
897
832
  target = Path(value)
898
833
  target = target if target.is_absolute() else project_root / target
899
834
  verification_errors = validate_technical_verification_report(data, target, project_root)
835
+ verification_errors.extend(technical_verification_routing_errors(data))
900
836
  if verification_errors:
901
837
  raise ReportAssemblyError(tuple(
902
838
  AssemblyIssue("report-writer", str(target), "technicalVerification", error)
@@ -62,7 +62,10 @@ from .final_report_paths import (
62
62
  final_report_markdown_path,
63
63
  sidecar_source_rel,
64
64
  )
65
- from .paths import task_dir, task_manifest_file
65
+ from .run_artifact_prune import prune_run_artifacts
66
+ from .handoff import record_verified
67
+ from .handoff_error import HandoffError
68
+ from .paths import RunRef, task_dir, task_manifest_file
66
69
  from .report_view_artifacts import html_view_path
67
70
  from .release_gate import release_handoff_allowed
68
71
  from .report_translation_dispatch import TranslateOutcome, translate_report
@@ -73,6 +76,7 @@ from .stage_targets import (
73
76
  integrate_and_teardown_whole_task,
74
77
  )
75
78
  from .session import observe_lead_session
79
+ from .lead_progress import record_checkpoint
76
80
  from .error_log_write import record_runtime_failure
77
81
 
78
82
  # 포인터 값 타입만 쓴다. `okstra_project.phase_pointer` 는 okstra 안의
@@ -91,10 +95,12 @@ STEP_TRANSLATE = "translate"
91
95
  STEP_TOKEN_USAGE = "token-usage"
92
96
  STEP_RENDER_VIEWS = "render-views"
93
97
  STEP_SPAWN_FOLLOWUPS = "spawn-followups"
98
+ STEP_RECORD_VERIFIED = "record-verified"
94
99
  STEP_VALIDATE_RUN = "validate-run"
95
100
  STEP_RECORD_GROUP_MEMORY = "record-group-memory"
96
101
  STEP_TEARDOWN_STAGES = "teardown-stages"
97
102
  STEP_PREFLIGHT = "preflight"
103
+ STEP_PRUNE_RUN_ARTIFACTS = "prune-run-artifacts"
98
104
 
99
105
  STEP_ORDER = (
100
106
  STEP_PROJECT_ACTIVITY,
@@ -102,13 +108,15 @@ STEP_ORDER = (
102
108
  STEP_TOKEN_USAGE,
103
109
  STEP_RENDER_VIEWS,
104
110
  STEP_SPAWN_FOLLOWUPS,
111
+ STEP_RECORD_VERIFIED,
105
112
  STEP_VALIDATE_RUN,
106
113
  # After validation: the group's sibling tasks read this run's conclusion
107
114
  # from `group-context.md`, and only a validated record is worth handing on.
108
115
  STEP_RECORD_GROUP_MEMORY,
109
- # Last, and only after the run validated: it removes the stage worktrees a
116
+ # Only after the run validated: it removes the stage worktrees a
110
117
  # blocked verdict would send the user straight back to.
111
118
  STEP_TEARDOWN_STAGES,
119
+ STEP_PRUNE_RUN_ARTIFACTS,
112
120
  )
113
121
 
114
122
  V3_STEP_ORDER = (
@@ -119,9 +127,12 @@ V3_STEP_ORDER = (
119
127
  STEP_TRANSLATE,
120
128
  STEP_RENDER_VIEWS,
121
129
  STEP_SPAWN_FOLLOWUPS,
130
+ # Needs the assembled record; `validate-run` requires the row it writes.
131
+ STEP_RECORD_VERIFIED,
122
132
  STEP_VALIDATE_RUN,
123
133
  STEP_RECORD_GROUP_MEMORY,
124
134
  STEP_TEARDOWN_STAGES,
135
+ STEP_PRUNE_RUN_ARTIFACTS,
125
136
  )
126
137
 
127
138
  # 앞 단계가 실패하면 건너뛰는 단계. 하나는 worktree 를 거두고, 하나는 검증 안 된
@@ -391,6 +402,10 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
391
402
  ctx.task_key,
392
403
  ],
393
404
  ),
405
+ (
406
+ STEP_RECORD_VERIFIED,
407
+ ["<in-process>", "record-verified", str(ctx.data_path)],
408
+ ),
394
409
  (STEP_VALIDATE_RUN, _validate_run_command(ctx, ctx.data_path)),
395
410
  (
396
411
  STEP_RECORD_GROUP_MEMORY,
@@ -400,6 +415,10 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
400
415
  STEP_TEARDOWN_STAGES,
401
416
  ["<in-process>", "teardown-stages", str(ctx.data_path)],
402
417
  ),
418
+ (
419
+ STEP_PRUNE_RUN_ARTIFACTS,
420
+ ["<in-process>", "prune-run-artifacts", str(ctx.data_path)],
421
+ ),
403
422
  ]
404
423
  if ctx.report_contract_version != "3.0":
405
424
  return commands
@@ -502,6 +521,51 @@ def _next_in_group(steps: Sequence[Mapping[str, Any]]) -> dict[str, str] | None:
502
521
  return None
503
522
 
504
523
 
524
+ def _record_verified_stages(
525
+ ctx: FinalizeContext,
526
+ command: list[str],
527
+ ) -> subprocess.CompletedProcess:
528
+ """Write a `verified` row for every stage a release-ready verdict clears.
529
+
530
+ `handoff record-verified` needs the assembled `data.json` and `validate-run`
531
+ requires its rows, and both happen inside this one call, so the lead had no
532
+ point at which to run it (observed 2026-09-26, dev-11054). Re-recording the
533
+ same report is a no-op, so a resumed finalize may run this again.
534
+ """
535
+ def _done(payload: Mapping[str, Any]) -> subprocess.CompletedProcess:
536
+ return subprocess.CompletedProcess(command, 0, json.dumps(payload), "")
537
+
538
+ if ctx.task_type != "final-verification":
539
+ return _done({"skipped": "not a final-verification run"})
540
+ try:
541
+ data = load_owned_object(Path(ctx.data_path), artifact="final report record")
542
+ except JsonBoundaryError as exc:
543
+ return subprocess.CompletedProcess(
544
+ command, 1, "", f"cannot read final-report data.json: {exc}")
545
+ if data.get("verificationScope") not in ("single-stage", "whole-task"):
546
+ return _done({"skipped": "report names no verification scope"})
547
+ if not release_handoff_allowed(data):
548
+ return _done({"skipped": "verdict does not clear the work for release"})
549
+ stages = sorted({
550
+ row["stage"]
551
+ for row in (data.get("finalVerification") or {}).get("stageReports") or ()
552
+ if isinstance(row, Mapping) and isinstance(row.get("stage"), int)
553
+ })
554
+ try:
555
+ plan_run_root = (
556
+ RunRef.from_report_path(Path(ctx.data_path).resolve())
557
+ .sibling("implementation-planning").run_dir
558
+ )
559
+ for stage in stages:
560
+ record_verified(
561
+ plan_run_root=plan_run_root, stage=stage,
562
+ report_path=str(ctx.data_path), data_json=str(ctx.data_path),
563
+ )
564
+ except (HandoffError, ValueError, OSError) as exc:
565
+ return subprocess.CompletedProcess(command, 1, "", str(exc))
566
+ return _done({"recorded": stages})
567
+
568
+
505
569
  def _teardown_stage_worktrees(
506
570
  ctx: FinalizeContext,
507
571
  command: list[str],
@@ -538,6 +602,27 @@ def _teardown_stage_worktrees(
538
602
  return _done(result)
539
603
 
540
604
 
605
+ def _prune_run_artifacts(
606
+ ctx: FinalizeContext,
607
+ command: list[str],
608
+ ) -> subprocess.CompletedProcess:
609
+ """이 run 디렉터리에서 run 종료 뒤 읽는 곳이 없는 산출물을 지운다.
610
+
611
+ 실패한 run 에서도 돈다. 빌드 디렉터리는 남은 lockfile 로 다시 만들고, dispatch
612
+ 스냅샷은 검증을 통과한 run 의 것만 지운다(`run_artifact_prune`).
613
+ """
614
+ reports_dir = Path(ctx.data_path).resolve().parent
615
+ if reports_dir.name != "reports":
616
+ return subprocess.CompletedProcess(
617
+ command, 1, "", f"report record is not under a run's reports/: {ctx.data_path}")
618
+ run_dir = reports_dir.parent
619
+ try:
620
+ result = prune_run_artifacts(ctx.project_root, run_dir, apply=True)
621
+ except OSError as exc:
622
+ return subprocess.CompletedProcess(command, 1, "", str(exc))
623
+ return subprocess.CompletedProcess(command, 0, json.dumps(result), "")
624
+
625
+
541
626
  def _validate_run_command(ctx: FinalizeContext, report_record_path: Path) -> list[str]:
542
627
  command = [
543
628
  sys.executable,
@@ -875,8 +960,12 @@ def _run_finalize_step(
875
960
  return _run_translate(ctx, command)
876
961
  if name == STEP_TEARDOWN_STAGES:
877
962
  return _teardown_stage_worktrees(ctx, command)
963
+ if name == STEP_PRUNE_RUN_ARTIFACTS:
964
+ return _prune_run_artifacts(ctx, command)
878
965
  if name == STEP_RECORD_GROUP_MEMORY:
879
966
  return _record_group_memory(ctx, command)
967
+ if name == STEP_RECORD_VERIFIED:
968
+ return _record_verified_stages(ctx, command)
880
969
  if name == STEP_VALIDATE_RUN:
881
970
  try:
882
971
  _link_lead_result_for_validation(ctx)
@@ -1066,6 +1155,9 @@ Runs the Phase 7 steps in their contractual order against one final-report:
1066
1155
  5. spawn-followups turn section 4 rows into task stubs
1067
1156
  6. validate-run validate the finished run artifacts
1068
1157
  7. record-group-memory / 8. teardown-stages after a clean validation
1158
+ 9. prune-run-artifacts remove node_modules and .next under this run
1159
+ directory, and the dispatch snapshots and publication
1160
+ locks of a validated run; runs after a failure too
1069
1161
 
1070
1162
  Every step is idempotent, so re-running after a fixed failure is safe. The
1071
1163
  sequence stops at the first non-zero exit and reports which step failed, except
@@ -1075,6 +1167,10 @@ exits non-zero the command prints the --only flags that resume the sequence
1075
1167
  from the earliest failing step — follow those rather than rerunning the failing
1076
1168
  step alone, which would leave the html rendered before the tokens landed.
1077
1169
 
1170
+ A full run (no --only) first records the lead's `phase-7-persist` checkpoint
1171
+ and returns it as `progressLines`; the lead emits that line instead of calling
1172
+ `okstra lead-progress append` for it.
1173
+
1078
1174
  This is the same code path the Codex lead adapter runs automatically after its
1079
1175
  report-writer completes, so a Claude-led and a Codex-led run finalize
1080
1176
  identically.
@@ -1264,7 +1360,15 @@ def main(argv: Sequence[str] | None = None) -> int:
1264
1360
  # the generations resume and compaction split it into. It runs ahead of the
1265
1361
  # sequence because the token-usage step below reads `leadSessionIds`.
1266
1362
  observe_lead_session(ctx.project_root, ctx.team_state_path)
1363
+ # 재개(`--only`)는 Phase 7 을 새로 시작하지 않고 다시 들어가는 것이다. 이 행은
1364
+ # 그것을 요구하는 `validate-run` 보다 먼저 있어야 한다.
1365
+ progress_line = None if args.only else record_checkpoint(
1366
+ ctx.project_root, ctx.manifest_path, "phase-7-persist",
1367
+ source="okstra report-finalize",
1368
+ )
1267
1369
  result = run_finalize(ctx, only=args.only or None)
1370
+ if progress_line:
1371
+ result["progressLines"] = [progress_line]
1268
1372
  order = V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
1269
1373
  print(json.dumps(result, indent=2, ensure_ascii=False))
1270
1374
  print("finalize steps:", file=sys.stderr)
@@ -247,7 +247,7 @@ def approved_plan_links(
247
247
  except JsonBoundaryError:
248
248
  return {}
249
249
  from .common import anchor_index
250
- from .view_models.implementation_planning import ANCHORED_FIELDS
250
+ from okstra_ctl.phases.implementation_planning.report import ANCHORED_FIELDS
251
251
 
252
252
  page = html_view_path(record)
253
253
  href = Path(os.path.relpath(page, data_path.resolve().parent)).as_posix()
@@ -4,10 +4,8 @@ from __future__ import annotations
4
4
  from okstra_token_usage.blocks import accounting_workers
5
5
 
6
6
  import json
7
- from copy import deepcopy
8
- from typing import Any, Mapping, Sequence
7
+ from typing import Any, Mapping
9
8
 
10
- from .design_surfaces import DesignSurfaceTrigger, detect_design_surfaces
11
9
  from .report_contract import execution_roles_from_manifest
12
10
  from .usage_cells import duration_ms_from_bounds
13
11
 
@@ -220,39 +218,6 @@ def project_token_usage(team_state: Mapping[str, Any]) -> dict[str, Any]:
220
218
  }
221
219
 
222
220
 
223
- def _trigger_rows(trigger: DesignSurfaceTrigger) -> list[dict[str, Any]]:
224
- return [
225
- {"step": item.step, "field": item.field, "match": item.match}
226
- for item in trigger.evidence
227
- ]
228
-
229
-
230
- def project_design(
231
- planning: Mapping[str, Any], detector_snapshot: Mapping[str, Any],
232
- ) -> dict[str, Any]:
233
- """탐지 결과가 실제 계획 표면과 일치할 때만 설계 블록을 반환한다."""
234
- triggers = detect_design_surfaces(planning)
235
- expected = {(row.stage, row.kind): _trigger_rows(row) for row in triggers}
236
- coverage = detector_snapshot.get("stageCoverage")
237
- coverage = coverage if isinstance(coverage, list) else []
238
- actual: dict[tuple[int, str], list[dict[str, Any]]] = {}
239
- for stage in coverage:
240
- if not isinstance(stage, Mapping):
241
- raise ReportProjectionError("owner=design-surface-detector invalid stageCoverage row")
242
- for row in stage.get("rows") or []:
243
- if isinstance(row, Mapping):
244
- actual[(int(stage.get("stage") or 0), str(row.get("kind") or ""))] = list(row.get("triggerEvidence") or [])
245
- if expected != actual:
246
- raise ReportProjectionError("owner=design-surface-detector trigger coverage mismatch")
247
- preparation = detector_snapshot.get("designPreparation")
248
- if not isinstance(preparation, Mapping):
249
- raise ReportProjectionError("owner=design-surface-detector missing designPreparation")
250
- return {
251
- "designPreparation": deepcopy(dict(preparation)),
252
- "stageCoverage": deepcopy(coverage),
253
- }
254
-
255
-
256
221
  def _round_history(state: Mapping[str, Any]) -> dict[str, Any]:
257
222
  if not (state.get("config") or {}).get("enabled", True):
258
223
  return {"disabled": True}
@@ -0,0 +1,23 @@
1
+ """보고서 생산 단계와 분리된 공통 이동 적합성 검사."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from typing import Any
6
+
7
+
8
+ def technical_verification_routing_errors(data: Mapping[str, Any]) -> list[str]:
9
+ """기술 검증의 후속 목적지가 후보 비교인지 검사하며 목적지를 추론하지 않는다."""
10
+ if (data.get("header") or {}).get("taskType") != "technical-verification":
11
+ return []
12
+ block = data.get("technicalVerification") or {}
13
+ if (block.get("routing") or {}).get("nextTaskType") != "implementation-option-selection":
14
+ return ["technical verification returns only to implementation-option-selection"]
15
+ return []
16
+
17
+
18
+ def fanout_routing_errors(packet_name: str, target: str | None) -> list[str]:
19
+ """작업 분해 패킷의 후속 단계가 허용된 값인지 검사한다."""
20
+ allowed = ("error-analysis", "implementation-option-selection")
21
+ if target not in allowed:
22
+ return [f"{packet_name}: recommended-next-phase not in {allowed}"]
23
+ return []