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
@@ -12,7 +12,6 @@ from okstra_ctl.analysis_inputs import (
12
12
  AnalysisReportCandidate,
13
13
  list_evidence_candidates,
14
14
  load_analysis_report_candidate,
15
- resolve_analysis_target,
16
15
  resolve_evidence_inputs,
17
16
  )
18
17
  from okstra_ctl.design_prep import (
@@ -25,13 +24,9 @@ from okstra_ctl.design_prep import (
25
24
  from .ids import (
26
25
  PICK_SKIP,
27
26
  PICK_TYPE_CUSTOM,
28
- S_ANALYSIS_TARGET,
29
- S_ANALYSIS_TARGET_PICK,
30
27
  S_DESIGN_PREP_CONFIRM,
31
28
  S_DESIGN_PREP_DECISION,
32
29
  S_DESIGN_PREP_OVERRIDES,
33
- S_FEATURE_EVIDENCE,
34
- S_FEATURE_EVIDENCE_PICK,
35
30
  S_PROJECT_EVIDENCE,
36
31
  S_PROJECT_EVIDENCE_PICK,
37
32
  _RESUME_REUSE_PHASES,
@@ -128,32 +123,6 @@ def _submit_analysis_evidence_pick(
128
123
  return t["echo_variants"]["selected"].format(path=candidate.report_path)
129
124
 
130
125
 
131
- def _build_feature_evidence_pick(state: WizardState) -> Prompt:
132
- return _build_analysis_evidence_pick(
133
- state, step=S_FEATURE_EVIDENCE_PICK, relation="feature-baseline"
134
- )
135
-
136
-
137
- def _submit_feature_evidence_pick(state: WizardState, value: str) -> Optional[str]:
138
- return _submit_analysis_evidence_pick(
139
- state, value, step=S_FEATURE_EVIDENCE_PICK,
140
- field_name="feature_evidence_path",
141
- pending_name="feature_evidence_pending_text",
142
- )
143
-
144
-
145
- def _build_feature_evidence(state: WizardState) -> Prompt:
146
- t = _p(state.workspace_root, S_FEATURE_EVIDENCE)
147
- return Prompt(step=S_FEATURE_EVIDENCE, kind="text", label=t["label"],
148
- echo_template=t["echo_template"])
149
-
150
-
151
- def _submit_feature_evidence(state: WizardState, value: str) -> Optional[str]:
152
- candidate = _accept_analysis_evidence(state, value, "feature_evidence_path")
153
- state.feature_evidence_pending_text = False
154
- return f"feature-evidence: {candidate.report_path}"
155
-
156
-
157
126
  def _build_project_evidence_pick(state: WizardState) -> Prompt:
158
127
  return _build_analysis_evidence_pick(
159
128
  state, step=S_PROJECT_EVIDENCE_PICK, relation="project-context"
@@ -181,72 +150,6 @@ def _submit_project_evidence(state: WizardState, value: str) -> Optional[str]:
181
150
  return f"project-evidence: {candidate.report_path}"
182
151
 
183
152
 
184
- def _feature_description(feature: dict[object, object]) -> str:
185
- name = str(feature.get("name") or "")
186
- summary = str(feature.get("summary") or "")
187
- entry_point = str(
188
- feature.get("entryPoint") or feature.get("entrypoint") or ""
189
- )
190
- return " · ".join(value for value in (name, summary, entry_point) if value)
191
-
192
-
193
- def _build_analysis_target_pick(state: WizardState) -> Prompt:
194
- t = _p(state.workspace_root, S_ANALYSIS_TARGET_PICK)
195
- try:
196
- candidate = load_analysis_report_candidate(
197
- Path(state.project_root), Path(state.project_evidence_path)
198
- )
199
- except AnalysisInputError as exc:
200
- raise WizardError(str(exc)) from exc
201
- options = [
202
- _opt(str(feature["id"]), str(feature["id"]), _feature_description(feature))
203
- for feature in candidate.feature_index
204
- if isinstance(feature, dict) and isinstance(feature.get("id"), str)
205
- ][:3]
206
- options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
207
- return Prompt(step=S_ANALYSIS_TARGET_PICK, kind="pick", label=t["label"],
208
- options=options, echo_template=t["echo_template"])
209
-
210
-
211
- def _accept_analysis_target(state: WizardState, value: str) -> str:
212
- candidates: dict[Path, AnalysisReportCandidate] = {}
213
- for path in _analysis_evidence_paths(state):
214
- try:
215
- candidate = load_analysis_report_candidate(Path(state.project_root), path)
216
- except AnalysisInputError as exc:
217
- raise WizardError(str(exc)) from exc
218
- candidates[candidate.report_path] = candidate
219
- try:
220
- target = resolve_analysis_target(
221
- value, _resolve_analysis_evidence(state), candidates
222
- )
223
- except AnalysisInputError as exc:
224
- raise WizardError(str(exc)) from exc
225
- requested_value = target["requestedValue"]
226
- if not isinstance(requested_value, str):
227
- raise WizardError("analysis target resolver returned an invalid requestedValue")
228
- state.analysis_target = requested_value
229
- state.analysis_target_pending_text = False
230
- return requested_value
231
-
232
-
233
- def _submit_analysis_target_pick(state: WizardState, value: str) -> Optional[str]:
234
- if value == PICK_TYPE_CUSTOM:
235
- state.analysis_target_pending_text = True
236
- return None
237
- return f"analysis-target: {_accept_analysis_target(state, value)}"
238
-
239
-
240
- def _build_analysis_target(state: WizardState) -> Prompt:
241
- t = _p(state.workspace_root, S_ANALYSIS_TARGET)
242
- return Prompt(step=S_ANALYSIS_TARGET, kind="text", label=t["label"],
243
- echo_template=t["echo_template"])
244
-
245
-
246
- def _submit_analysis_target(state: WizardState, value: str) -> Optional[str]:
247
- return f"analysis-target: {_accept_analysis_target(state, value)}"
248
-
249
-
250
153
  def _design_prep_items_by_id(state: WizardState) -> dict[str, dict[str, Any]]:
251
154
  try:
252
155
  items = load_design_prep_items(Path(state.approved_plan_path))
@@ -206,12 +206,20 @@ def _suggest_sibling_task_ids(state: WizardState) -> str:
206
206
  return ",".join(siblings)
207
207
 
208
208
 
209
+ def _sibling_count_note(state: WizardState, suggestion: str) -> str:
210
+ """라벨은 CSV 앞부분만 보여 주므로, 선택하면 몇 개가 들어가는지 따로 적는다."""
211
+ count = len([tid for tid in suggestion.split(",") if tid])
212
+ note = _p(state.workspace_root, "related_tasks_pick")["labels"]["siblings_count"]
213
+ return note.format(count=count)
214
+
215
+
209
216
  _RELATED_TASKS_PICK_SPEC = _OptionalCachedPickSpec(
210
217
  step=S_RELATED_TASKS_PICK, prompt_key="related_tasks_pick",
211
218
  recommend_token=_SIBLINGS_TOKEN, label_key="siblings", echo_suffix_key="siblings",
212
219
  suggest=_suggest_sibling_task_ids, snippet_style="prefix",
213
220
  cache_attr="last_siblings_cached", target_attr="related_tasks_raw",
214
221
  pending_attr="related_tasks_pending_text",
222
+ recommend_note=_sibling_count_note,
215
223
  )
216
224
 
217
225
 
@@ -3,18 +3,11 @@ from __future__ import annotations
3
3
 
4
4
  import re
5
5
  from pathlib import Path
6
- from collections.abc import Mapping
7
6
  from typing import Optional
8
7
 
9
8
  from okstra_ctl.clarification_items import scan_approval_gate
10
9
  from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
11
10
  from okstra_ctl.incremental_scope import CARRY_ALL_SCOPE
12
- from okstra_ctl.implementation_direction import (
13
- DirectionSelectionError,
14
- lexical_absolute_path,
15
- resolve_selected_direction,
16
- validate_task_artifact_path,
17
- )
18
11
  from okstra_ctl.final_report_paths import final_report_data_path
19
12
  from okstra_ctl.run import (
20
13
  PrepareError,
@@ -24,11 +17,12 @@ from okstra_ctl.run import (
24
17
  _reject_blocking_plan_body_gate,
25
18
  _set_data_json_approved_true_if_present,
26
19
  )
20
+ from okstra_ctl.phases.implementation.wizard import _impl_stage_marker, _impl_chosen_stages
27
21
  from okstra_ctl.stage_map import StageMapError, parse_stage_map_file, stage_map_records
28
22
  from okstra_ctl.user_response import PlanDecisionRecord, parse_plan_decision
29
23
  from okstra_ctl.wizard_stage_intent import WHOLE_TASK_STAGE
30
24
  from okstra_ctl import fix_cycles
31
- from okstra_ctl.paths import task_dir, task_runs_dir
25
+ from okstra_ctl.paths import task_dir
32
26
  from okstra_ctl.phases.final_verification.wizard import (
33
27
  StageAnswerError,
34
28
  validate_stage_answer,
@@ -36,184 +30,11 @@ from okstra_ctl.phases.final_verification.wizard import (
36
30
  )
37
31
  from okstra_project.state import read_task_manifest
38
32
 
39
- from .ids import (
40
- ALL_STAGES,
41
- PICK_OTHER,
42
- PICK_TYPE_CUSTOM,
43
- PICK_USE_DEFAULT,
44
- SCOPE_SHAPE_LINKED,
45
- SCOPE_SHAPE_NO_ANSWERS,
46
- SCOPE_SHAPE_UNLINKED,
47
- S_APPROVED_PLAN,
48
- S_APPROVED_PLAN_PICK,
49
- S_APPROVE_PLAN_CONFIRM,
50
- S_FIX_CYCLE_CONFIRM,
51
- S_HANDOFF_STAGE_PICK,
52
- S_REVERIFY_SCOPE_PICK,
53
- S_REVERIFY_SCOPE_STAGES,
54
- S_SELECTED_DIRECTION_PICK,
55
- S_STAGE_PICK,
56
- _REPORT_PREFIX,
57
- )
33
+ from .ids import ALL_STAGES, PICK_OTHER, PICK_TYPE_CUSTOM, PICK_USE_DEFAULT, SCOPE_SHAPE_LINKED, SCOPE_SHAPE_NO_ANSWERS, SCOPE_SHAPE_UNLINKED, S_APPROVED_PLAN, S_APPROVED_PLAN_PICK, S_APPROVE_PLAN_CONFIRM, S_FIX_CYCLE_CONFIRM, S_REVERIFY_SCOPE_PICK, S_REVERIFY_SCOPE_STAGES, S_STAGE_PICK, _REPORT_PREFIX
58
34
  from .state import Option, Prompt, WizardError, WizardState
59
35
  from .prompts import _opt, _p
60
- from .sources import (
61
- _latest_implementation_planning_report,
62
- _list_implementation_planning_reports,
63
- _parse_stage_objects,
64
- _prior_stage_numbers,
65
- _project_relative_path,
66
- _require_file,
67
- _reserved_stage_numbers,
68
- _reverify_scope_pick_required,
69
- _reverify_scope_preview,
70
- _reverify_scope_shape,
71
- _stage_lifecycle_snapshot,
72
- )
73
-
36
+ from .sources import _latest_implementation_planning_report, _list_implementation_planning_reports, _parse_stage_objects, _prior_stage_numbers, _require_file, _reserved_stage_numbers, _reverify_scope_pick_required, _reverify_scope_preview, _reverify_scope_shape, _stage_lifecycle_snapshot
74
37
 
75
- # 후보는 리포트 **기록**으로 찾는다. 전체 열람본(`.md`)은 `2bcd575` 이후 완료
76
- # 산출물이 아니라 요청 시 렌더라, `.md` 를 글롭하는 동안 이 목록은 항상 비었고
77
- # 방향 선택 단계 자체가 뜨지 않았다 — 비교가 끝나도 계획으로 갈 길이 없었다.
78
- _SELECTION_REPORT_RE = re.compile(
79
- r"^final-report-implementation-option-selection-(?P<seq>\d{3,})\.data\.json$"
80
- )
81
-
82
-
83
- def _selected_direction_candidates(state: WizardState) -> list[str]:
84
- if not state.project_root or not state.task_group or not state.task_id:
85
- return []
86
- project_root = Path(state.project_root).resolve()
87
- task_root = lexical_absolute_path(
88
- task_dir(project_root, state.task_group, state.task_id)
89
- )
90
- reports = (
91
- task_runs_dir(project_root, state.task_group, state.task_id)
92
- / "implementation-option-selection"
93
- / "reports"
94
- )
95
- candidates: list[tuple[int, Path]] = []
96
- for report in reports.glob(
97
- "final-report-implementation-option-selection-*.data.json"
98
- ):
99
- match = _SELECTION_REPORT_RE.fullmatch(report.name)
100
- if match is None:
101
- continue
102
- try:
103
- validated = validate_task_artifact_path(
104
- report, task_root, "selection report"
105
- )
106
- except DirectionSelectionError:
107
- continue
108
- candidates.append((int(match.group("seq")), validated))
109
- return [
110
- _project_relative_path(path, project_root)
111
- for _, path in sorted(candidates, reverse=True)[:3]
112
- ]
113
-
114
-
115
- def _planning_rerun_selected(state: WizardState) -> bool:
116
- if (
117
- not state.clarification_response_path
118
- or not state.project_root
119
- or not state.task_group
120
- or not state.task_id
121
- ):
122
- return False
123
- project_root = Path(state.project_root).resolve()
124
- raw_path = Path(state.clarification_response_path).expanduser()
125
- path = lexical_absolute_path(
126
- raw_path if raw_path.is_absolute() else project_root / raw_path
127
- )
128
- task_root = lexical_absolute_path(
129
- task_dir(project_root, state.task_group, state.task_id)
130
- )
131
- reports = lexical_absolute_path(
132
- task_runs_dir(project_root, state.task_group, state.task_id)
133
- / "implementation-planning"
134
- / "reports"
135
- )
136
- try:
137
- validate_task_artifact_path(path, task_root, "planning report")
138
- except DirectionSelectionError:
139
- return False
140
- return (
141
- path.is_file()
142
- and not path.is_symlink()
143
- and re.fullmatch(
144
- r"final-report-implementation-planning-\d{3,}\.(?:md|data\.json)", path.name
145
- )
146
- is not None
147
- and path.parent == reports
148
- )
149
-
150
-
151
- def _build_selected_direction_pick(state: WizardState) -> Prompt:
152
- candidates = _selected_direction_candidates(state)
153
- t = _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)
154
- if not candidates:
155
- raise WizardError(t["errors"]["none"])
156
- return Prompt(
157
- step=S_SELECTED_DIRECTION_PICK,
158
- kind="pick",
159
- label=t["label"],
160
- options=[
161
- _opt(path, _selection_option_label(state, path, t))
162
- for path in candidates
163
- ],
164
- echo_template=t["echo_template"],
165
- )
166
-
167
-
168
- def _selection_option_label(state: WizardState, rel_path: str, t: dict) -> str:
169
- """후보 경로에 그 리포트가 실제로 무엇을 담았는지 붙인다.
170
-
171
- 목록은 최신순이라 후보가 0건인 최근 재실행이 맨 위에 온다. 경로만 보이면
172
- 사용자가 그것을 고르고, 방향 없는 리포트로 계획을 열려다 막힌다.
173
- """
174
- record = Path(state.project_root) / rel_path
175
- try:
176
- data = load_owned_object(record, artifact="implementation option data")
177
- except (OSError, JsonBoundaryError):
178
- return rel_path
179
- selection = data.get("implementationOptionSelection")
180
- if not isinstance(selection, Mapping):
181
- return rel_path
182
- rows = selection.get("rankedOptions")
183
- ids = [
184
- str(row.get("id"))
185
- for row in rows
186
- if isinstance(row, Mapping) and row.get("id")
187
- ] if isinstance(rows, list) else []
188
- if not ids:
189
- return t["labels"]["no_options"].format(path=rel_path)
190
- return t["labels"]["options"].format(path=rel_path, ids=", ".join(ids))
191
-
192
-
193
- def _submit_selected_direction_pick(
194
- state: WizardState, value: str
195
- ) -> Optional[str]:
196
- t = _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)
197
- candidates = _selected_direction_candidates(state)
198
- if value not in candidates:
199
- raise WizardError(t["errors"]["unknown"].format(value=value))
200
- try:
201
- resolve_selected_direction(
202
- Path(state.project_root) / value,
203
- expected_task_key=f"{state.project_id}:{state.task_group}:{state.task_id}",
204
- )
205
- except DirectionSelectionError as exc:
206
- raise WizardError(str(exc)) from exc
207
- state.selected_direction_path = value
208
- # 두 입력은 상호 배타다 — 방향이 정해진 순간 이 런은 새 계획이고,
209
- # clarification 자리에 남은 값은 render-bundle 이 거절할 이유일 뿐이다.
210
- # 거절을 여기서 앞당기는 대신 값을 비우고, 무엇이 비워졌는지 echo 로
211
- # 알린다. 사용자 답변은 손실되지 않는다: 그 사이드카는
212
- # `--selected-direction` 경로로 계획 런에 첨부된다(`run.py` 참조).
213
- if state.clarification_response_path:
214
- state.clarification_response_path = ""
215
- return t["echo_variants"]["cleared_clarification"].format(value=value)
216
- return f"selected-direction: {value}"
217
38
 
218
39
 
219
40
  def _classify_approved_plan(path_str: str, project_root: Path) -> tuple[Path, bool]:
@@ -616,16 +437,6 @@ def _build_stage_pick(state: WizardState) -> Prompt:
616
437
  # Presentation keys are mapped explicitly rather than interpolated from the
617
438
  # status, so renaming a Stage Lifecycle status is a compile-time-visible edit
618
439
  # here instead of a KeyError raised while drawing the picker.
619
- _STAGE_MARKER_KEYS = {
620
- "done": "mark_done",
621
- "active": "mark_active",
622
- "ready": "mark_ready",
623
- "blocked": "mark_blocked",
624
- }
625
-
626
-
627
- def _impl_stage_marker(t, lifecycle) -> str:
628
- return t["options"][_STAGE_MARKER_KEYS[lifecycle.status]]
629
440
 
630
441
 
631
442
  def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
@@ -670,34 +481,6 @@ def _submit_impl_stage_pick(state: WizardState, answer: str) -> Optional[str]:
670
481
  return t["echo_variants"]["plain"].format(stages=state.selected_stages)
671
482
 
672
483
 
673
- def _impl_chosen_stages(t, picks, answer, all_nums, occupied) -> set:
674
- if ALL_STAGES in picks:
675
- if len(picks) > 1:
676
- raise WizardError(t["errors"]["all_exclusive"])
677
- chosen = {n for n in all_nums if n not in occupied}
678
- if not chosen:
679
- raise WizardError(t["errors"]["nothing_selectable"])
680
- return chosen
681
- try:
682
- nums = {int(p) for p in picks}
683
- except ValueError:
684
- raise WizardError(t["errors"]["bad_number"].format(answer=answer))
685
- unknown = sorted(nums - all_nums)
686
- if unknown:
687
- raise WizardError(t["errors"]["unknown_stage"].format(
688
- bad=", ".join(map(str, unknown))))
689
- bad = sorted(nums & occupied)
690
- if bad:
691
- raise WizardError(t["errors"]["occupied"].format(
692
- bad=", ".join(map(str, bad))))
693
- return nums
694
-
695
-
696
- def _handoff_msgs(state: WizardState) -> dict:
697
- """handoff_stage_pick 의 JSON 텍스트 묶음 (label 미사용 조회용)."""
698
- return _p(state.workspace_root, "handoff_stage_pick", blocked="")
699
-
700
-
701
484
  def _resolve_handoff_plan(state: WizardState) -> Path:
702
485
  """release-handoff 의 approved plan 을 질문 없이 자동 해소한다.
703
486
 
@@ -719,7 +502,7 @@ def _resolve_handoff_plan(state: WizardState) -> Path:
719
502
 
720
503
  def _handoff_eligibility(state: WizardState) -> list:
721
504
  """stage 별 PR 자격 — okstra_ctl.handoff 의 SSOT 판정을 그대로 재사용한다."""
722
- from okstra_ctl.handoff import compute_eligibility
505
+ from okstra_ctl.phases.release_handoff.operations import compute_eligibility
723
506
  from okstra_ctl.consumers import read_consumers
724
507
  plan = _resolve_handoff_plan(state)
725
508
  try:
@@ -733,53 +516,17 @@ def _handoff_eligibility(state: WizardState) -> list:
733
516
  # 자격 stage 전체를 한 번에 고르는 값. stage 번호는 숫자뿐이라 충돌하지 않는다.
734
517
  # CLI 는 `--stages` 를 비우면 전체를 쓰지만, 위저드 pick 은 빈 선택을 거절하므로
735
518
  # 그 뜻을 표현할 값이 따로 필요하다.
736
- ALL_ELIGIBLE_STAGES = "all"
519
+ from okstra_ctl.phases.release_handoff.wizard import (
520
+ _handoff_msgs, build_handoff_stage_pick, submit_handoff_stage_pick,
521
+ )
737
522
 
738
523
 
739
524
  def _build_handoff_stage_pick(state: WizardState) -> Prompt:
740
- elig = _handoff_eligibility(state)
741
- eligible = [e for e in elig if e["eligible"]]
742
- blocked = [e for e in elig if not e["eligible"]]
743
- msgs = _handoff_msgs(state)
744
- blocked_summary = ("; ".join(
745
- f"stage {e['stage']} ({', '.join(e['reasons'])})" for e in blocked)
746
- or msgs["labels"]["blocked_none"])
747
- if not eligible:
748
- raise WizardError(
749
- msgs["errors"]["nothing_eligible"].format(blocked=blocked_summary))
750
- t = _p(state.workspace_root, "handoff_stage_pick", blocked=blocked_summary)
751
- stage_label = t["labels"]["stage"]
752
- options: list[Option] = []
753
- if len(eligible) > 1:
754
- options.append(_opt(ALL_ELIGIBLE_STAGES, t["labels"]["all_stages"].format(
755
- stages=", ".join(str(e["stage"]) for e in eligible))))
756
- for e in eligible:
757
- deps = ", ".join(str(d) for d in e["depends_on"]) or "-"
758
- options.append(_opt(str(e["stage"]),
759
- stage_label.format(stage=e["stage"], deps=deps)))
760
- return Prompt(
761
- step=S_HANDOFF_STAGE_PICK, kind="pick", multi=True,
762
- label=t["label"], options=options,
763
- echo_template=t["echo_template"],
764
- )
525
+ return build_handoff_stage_pick(state, _handoff_eligibility(state))
765
526
 
766
527
 
767
528
  def _submit_handoff_stage_pick(state: WizardState, value: str) -> Optional[str]:
768
- t = _handoff_msgs(state)
769
- picks = [v.strip() for v in (value or "").split(",") if v.strip()]
770
- if not picks:
771
- raise WizardError(t["errors"]["none_selected"])
772
- eligible = {str(e["stage"]) for e in _handoff_eligibility(state)
773
- if e["eligible"]}
774
- if ALL_ELIGIBLE_STAGES in picks:
775
- picks = sorted(eligible, key=int)
776
- bad = [p for p in picks if p not in eligible]
777
- if bad:
778
- raise WizardError(t["errors"]["not_eligible"].format(
779
- bad=", ".join(bad), eligible=", ".join(sorted(eligible))))
780
- nums = sorted({int(p) for p in picks})
781
- state.handoff_stages = ",".join(str(n) for n in nums)
782
- return t["echo_variants"]["stages"].format(stages=state.handoff_stages)
529
+ return submit_handoff_stage_pick(state, value, lambda: _handoff_eligibility(state))
783
530
 
784
531
 
785
532
  def _reverify_scope_step_pending(state: WizardState) -> bool:
@@ -12,7 +12,8 @@ from okstra_ctl.domain.host import (
12
12
  from okstra_ctl.registry.host_registry import default_host_registry
13
13
  from okstra_ctl.registry.provider_registry import default_provider_registry
14
14
  from okstra_ctl.ids import slugify_task_segment
15
- from okstra_ctl.analysis_inputs import AnalysisInputError, resolve_analysis_target
15
+ from okstra_ctl.analysis_inputs import AnalysisInputError
16
+ from okstra_ctl.phases.feature_analysis.entry import resolve_analysis_target
16
17
  from okstra_ctl.models import (
17
18
  UnknownModelError,
18
19
  UnknownProviderError,
@@ -3,7 +3,7 @@
3
3
  profile / validators / wizard 는 이 모듈에서 enum 을 import 해야 한다. 다른 곳에서
4
4
  재정의하면 single-reference-point 위반이며 tests/test_work_categories.py 가 거부한다.
5
5
 
6
- 다섯째 값은 `ops` 가 canonical 이다(`--work-category` CLI help 및 workflow.PHASE_RULES
6
+ 다섯째 값은 `ops` 가 canonical 이다(`--work-category` CLI help 및 phases.catalog
7
7
  와 일치). 구버전 비표준 철자는 폐기된다.
8
8
  """
9
9
  from __future__ import annotations
@@ -10,12 +10,18 @@ from typing import Any, Mapping, Sequence
10
10
  from . import dispatch_core
11
11
  from .adapters.dispatch.cli_wrapper import CliWrapperDispatchPort
12
12
  from .application.dispatch_assignments import dispatch_assignments
13
- from .dispatch_state import DispatchError
13
+ from .dispatch_checkpoints import (
14
+ record_collect_checkpoints,
15
+ record_dispatch_checkpoints,
16
+ settled_initial_dispatches,
17
+ )
18
+ from .dispatch_state import DispatchError, load_json_object
14
19
  from .models import provider_wrappers
15
20
  from .ports.worker_dispatch import WorkerDispatchRequest
16
21
 
17
22
 
18
23
  SUPPORTED_CLI_WORKERS = provider_wrappers("analyser")
24
+ _SOURCE = "okstra worker-dispatch"
19
25
 
20
26
 
21
27
  def main(argv: Sequence[str] | None = None) -> int:
@@ -38,14 +44,42 @@ def main(argv: Sequence[str] | None = None) -> int:
38
44
  if args.dry_run:
39
45
  _print_json(_backend_payload(backend_plan, dry_run=True))
40
46
  return 0
41
- code = dispatch_core.dispatch_cli_wrapper_plan(backend_plan)
42
- _print_json({**_backend_payload(backend_plan, dry_run=False), "exitCode": code})
47
+ code, lines = _dispatch_recording_checkpoints(backend_plan)
48
+ _print_json({
49
+ **_backend_payload(backend_plan, dry_run=False),
50
+ "exitCode": code,
51
+ "progressLines": lines,
52
+ })
43
53
  return code
44
54
  except DispatchError as exc:
45
55
  print(f"error: {exc}", file=sys.stderr)
46
56
  return 2
47
57
 
48
58
 
59
+ def _dispatch_recording_checkpoints(backend_plan) -> tuple[int, list[str]]:
60
+ if not backend_plan.jobs:
61
+ return dispatch_core.dispatch_cli_wrapper_plan(backend_plan), []
62
+ before = load_json_object(backend_plan.team_state_path, "team-state")
63
+ lines: list[str] = []
64
+ code = dispatch_core.dispatch_cli_wrapper_plan(
65
+ backend_plan,
66
+ before_start=lambda prepared: lines.extend(record_dispatch_checkpoints(
67
+ prepared.project_root, prepared.manifest_path,
68
+ load_json_object(prepared.team_state_path, "team-state"),
69
+ prepared.jobs, source=_SOURCE,
70
+ )),
71
+ )
72
+ lines.extend(record_collect_checkpoints(
73
+ backend_plan.project_root, backend_plan.manifest_path,
74
+ settled_initial_dispatches(before),
75
+ settled_initial_dispatches(
76
+ load_json_object(backend_plan.team_state_path, "team-state")
77
+ ),
78
+ source=_SOURCE,
79
+ ))
80
+ return code, lines
81
+
82
+
49
83
  def _dispatch_request(args: argparse.Namespace) -> WorkerDispatchRequest:
50
84
  return WorkerDispatchRequest(
51
85
  project_root=Path(args.project_root),
@@ -75,6 +109,13 @@ whose persisted runner is 'cli-wrapper'. It verifies each invocation contract
75
109
  before execution and uses the persisted provider, model, and registered CLI.
76
110
  Native-session assignments remain owned by the active host session.
77
111
 
112
+ It records the lead checkpoints it owns and prints them as `progressLines`:
113
+ `phase-3-team-create` when it writes the implicit-team marker,
114
+ `phase-4-dispatch` for each `initial` job, `phase-6-synthesis` on the first
115
+ report-writer dispatch, and `phase-5-collect` for each `initial` dispatch it
116
+ settled. The lead emits those lines instead of calling
117
+ `okstra lead-progress append` for them.
118
+
78
119
  Missing worker prompt files are generated automatically from the immutable run snapshot.
79
120
  Existing prompt and metadata files are never overwritten.
80
121
  When report-writer completes, this command also runs the idempotent post-report
@@ -23,6 +23,7 @@ from .worker_prompt_policy import (
23
23
  PromptPlan,
24
24
  is_plan_verify_dispatch_kind,
25
25
  resolve_prompt_plan_for_manifest,
26
+ verification_dispatch_round,
26
27
  )
27
28
  from .worker_prompt_headers import EVIDENCE_LEDGER_HEADER
28
29
 
@@ -367,6 +368,41 @@ def validate_reverify_prompt(
367
368
  return errors
368
369
 
369
370
 
371
+ def validate_plan_verify_dispatch_identity(
372
+ text: str, *, dispatch_kind: str, result_name: str,
373
+ ) -> list[str]:
374
+ """Tie a plan item queue to the dispatch kind and result name validate-run counts.
375
+
376
+ `validate_reverify_prompt` only runs for a verification kind, so a queue
377
+ rendered by `okstra plan-items prompt` under any other kind skipped every
378
+ check and dispatched. validate-run then counts plan-body votes only from
379
+ `plan-verify-r<N>` rows whose result file carries `-plan-verify-r<N>-`
380
+ (`_plan_verify_dispatched_workers`), so the round's votes surfaced later as
381
+ unbacked. Observed 2026-09-24 (jobs dev-9065 implementation-planning 001):
382
+ kinds `plan-body-r1` / `plan-body-r2`, results `*-plan-body-r2-*.md`.
383
+ """
384
+ normalized = text.replace("\r\n", "\n").replace("\r", "\n")
385
+ if not is_plan_verify_dispatch_kind(dispatch_kind):
386
+ instructions = normalized[_task_instructions_offset(normalized):]
387
+ if PLAN_VERIFY_RENDERED_BY_LINE not in instructions:
388
+ return []
389
+ return [
390
+ "the instruction is an `okstra plan-items prompt` queue but the "
391
+ f"dispatch kind is `{dispatch_kind}` — a plan-body round takes "
392
+ "--dispatch-kind plan-verify-r<N>, matching the round in its result "
393
+ "file name; validate-run counts no plan-body vote from any other kind"
394
+ ]
395
+ token = f"-plan-verify-r{verification_dispatch_round(dispatch_kind)}-"
396
+ if token in result_name:
397
+ return []
398
+ return [
399
+ f"`{dispatch_kind}` result file `{result_name}` does not carry `{token}` "
400
+ "— name it <role>-worker-plan-verify-r<N>-<task-type>-<seq>.md with the "
401
+ "same <N> as the dispatch kind; validate-run counts no plan-body vote "
402
+ "from another result name"
403
+ ]
404
+
405
+
370
406
  def complete_reverify_instruction(
371
407
  body: str, *, task_type: str, forbidden_actions: str,
372
408
  model: str, output_contract: str,
@@ -35,6 +35,10 @@ ERRORS_PATH_HEADERS = (
35
35
  # itself: the lead's launch prompt carries them, and no worker reads that.
36
36
  APPROVED_PLAN_HEADER = "**Approved plan:**"
37
37
  IMPLEMENTATION_STAGE_HEADER = "**Stage for this implementation run:**"
38
+ # A stage's verifiers share one worktree and run as one batch. The Tier 3 script
39
+ # (builds, fixed ports) and the self-mock mutation probe (edits sources in place)
40
+ # must each run once, by the verifier named here.
41
+ STAGE_QA_OWNER_HEADER = "**Stage QA owner:**"
38
42
  IMPLEMENTATION_HEADERS = (
39
43
  "**Worktree:**",
40
44
  APPROVED_PLAN_HEADER,
@@ -295,6 +299,21 @@ def worker_assignment(
295
299
  return None
296
300
 
297
301
 
302
+ def stage_qa_owner(manifest: Mapping[str, Any]) -> str:
303
+ """The first verifier in roster order, or '' when the roster has none.
304
+
305
+ Derived from the manifest alone: prompts are published once and reused, and
306
+ the lead may materialize or re-dispatch one verifier at a time, so an owner
307
+ taken from the dispatch batch could disagree with a published prompt.
308
+ """
309
+ roster = manifest.get("recommendedWorkers")
310
+ for worker_id in roster if isinstance(roster, list) else []:
311
+ row = worker_assignment(manifest, worker_id) if isinstance(worker_id, str) else None
312
+ if row is not None and row.get("role") == "verifier":
313
+ return worker_id
314
+ return ""
315
+
316
+
298
317
  def _plan(
299
318
  audience: PromptAudience,
300
319
  *,