okstra 0.205.1 → 0.206.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/lifecycle/install.mjs +2 -1
  3. package/dist/commands/lifecycle/install.mjs.map +1 -1
  4. package/docs/architecture.md +16 -16
  5. package/docs/cli.md +4 -4
  6. package/docs/contributor-change-matrix.md +3 -2
  7. package/docs/performance-improvement-plan-v2.md +1 -1
  8. package/docs/project-structure-overview.md +45 -21
  9. package/package.json +2 -3
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/okstra-spawn-followups.py +2 -2
  12. package/runtime/prompts/launch.template.md +1 -1
  13. package/runtime/prompts/lead/context-loader.md +1 -1
  14. package/runtime/prompts/lead/convergence.md +3 -3
  15. package/runtime/prompts/lead/okstra-lead-contract.md +14 -55
  16. package/runtime/prompts/lead/phase-routing.md +82 -0
  17. package/runtime/prompts/lead/report-writer.md +2 -2
  18. package/runtime/prompts/lead/team-contract.md +1 -1
  19. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  20. package/runtime/prompts/profiles/_common-contract.md +1 -1
  21. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  22. package/runtime/prompts/profiles/forbidden-actions.json +0 -94
  23. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +1 -1
  24. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +10 -1
  25. package/runtime/python/okstra_ctl/analysis_inputs.py +0 -39
  26. package/runtime/python/okstra_ctl/analysis_scope.py +31 -0
  27. package/runtime/python/okstra_ctl/asset_roots.py +19 -0
  28. package/runtime/python/okstra_ctl/consumers.py +12 -0
  29. package/runtime/python/okstra_ctl/contract_graph.py +75 -10
  30. package/runtime/python/okstra_ctl/dispatch_state.py +22 -0
  31. package/runtime/python/okstra_ctl/doctor.py +15 -4
  32. package/runtime/python/okstra_ctl/execution_mutation_audit.py +27 -1
  33. package/runtime/python/okstra_ctl/handoff.py +11 -466
  34. package/runtime/python/okstra_ctl/handoff_error.py +5 -0
  35. package/runtime/python/okstra_ctl/implementation_direction.py +9 -485
  36. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +8 -1
  37. package/runtime/python/okstra_ctl/next_phase.py +2 -2
  38. package/runtime/python/okstra_ctl/option_comparison.py +3 -165
  39. package/runtime/python/okstra_ctl/option_votes.py +3 -191
  40. package/runtime/python/okstra_ctl/paths.py +22 -6
  41. package/runtime/python/okstra_ctl/phases/__init__.py +4 -0
  42. package/runtime/python/okstra_ctl/phases/catalog.py +260 -0
  43. package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
  44. package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
  45. package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
  46. package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
  47. package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
  48. package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
  49. package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
  50. package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
  51. package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
  52. package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
  53. package/{docs/task-process/error-analysis.md → runtime/python/okstra_ctl/phases/error_analysis/spec.md} +22 -7
  54. package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
  55. package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
  56. package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
  57. package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
  58. package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
  59. package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
  60. package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
  61. package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
  62. package/runtime/python/okstra_ctl/phases/final_verification/__init__.py +4 -0
  63. package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
  64. package/runtime/python/okstra_ctl/phases/final_verification/entry.py +166 -0
  65. package/runtime/{prompts/profiles/final-verification.md → python/okstra_ctl/phases/final_verification/profile.md} +5 -5
  66. package/runtime/python/okstra_ctl/{report_html/view_models/final_verification.py → phases/final_verification/report.py} +12 -3
  67. package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
  68. package/{docs/task-process/final-verification.md → runtime/python/okstra_ctl/phases/final_verification/spec.md} +42 -25
  69. package/runtime/python/okstra_ctl/phases/final_verification/target.py +296 -0
  70. package/runtime/python/okstra_ctl/phases/final_verification/validation.py +190 -0
  71. package/runtime/python/okstra_ctl/phases/final_verification/wizard.py +38 -0
  72. package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
  73. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
  74. package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
  75. package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
  76. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
  77. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
  78. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +4 -4
  79. package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
  80. package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
  81. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
  82. package/{docs/task-process/implementation.md → runtime/python/okstra_ctl/phases/implementation/spec.md} +20 -8
  83. package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
  84. package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
  85. package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
  86. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
  87. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
  88. package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
  89. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
  90. package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +2 -2
  91. package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
  92. package/{docs/task-process/implementation-option-selection.md → runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md} +20 -7
  93. package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
  94. package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
  95. package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
  96. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
  97. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
  98. package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
  99. package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
  100. package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
  101. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
  102. package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
  103. package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
  104. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
  105. package/{docs/task-process/implementation-planning.md → runtime/python/okstra_ctl/phases/implementation_planning/spec.md} +30 -6
  106. package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
  107. package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
  108. package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
  109. package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
  110. package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
  111. package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
  112. package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
  113. package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
  114. package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
  115. package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
  116. package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
  117. package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
  118. package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
  119. package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
  120. package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
  121. package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
  122. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
  123. package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
  124. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
  125. package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
  126. package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
  127. package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
  128. package/{docs/task-process/release-handoff.md → runtime/python/okstra_ctl/phases/release_handoff/spec.md} +22 -9
  129. package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
  130. package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
  131. package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
  132. package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
  133. package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
  134. package/{docs/task-process/requirements-discovery.md → runtime/python/okstra_ctl/phases/requirements_discovery/spec.md} +25 -6
  135. package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
  136. package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
  137. package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
  138. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
  139. package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +1 -1
  140. package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
  141. package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
  142. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
  143. package/runtime/python/okstra_ctl/plan_approval.py +70 -0
  144. package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
  145. package/runtime/python/okstra_ctl/profile_show.py +9 -3
  146. package/runtime/python/okstra_ctl/render.py +9 -2
  147. package/runtime/python/okstra_ctl/render_final_report.py +3 -2
  148. package/runtime/python/okstra_ctl/report_assembly.py +14 -93
  149. package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
  150. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  151. package/runtime/python/okstra_ctl/report_html/router.py +9 -33
  152. package/runtime/python/okstra_ctl/report_projections.py +1 -36
  153. package/runtime/python/okstra_ctl/report_routing.py +23 -0
  154. package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
  155. package/runtime/python/okstra_ctl/report_template_loader.py +35 -0
  156. package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
  157. package/runtime/python/okstra_ctl/report_views.py +18 -2
  158. package/runtime/python/okstra_ctl/run.py +113 -503
  159. package/runtime/python/okstra_ctl/stage_map.py +13 -0
  160. package/runtime/python/okstra_ctl/stage_targets.py +9 -286
  161. package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
  162. package/runtime/python/okstra_ctl/user_response.py +199 -4
  163. package/runtime/python/okstra_ctl/verification_target.py +1 -1
  164. package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
  165. package/runtime/python/okstra_ctl/wizard/api.py +18 -0
  166. package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
  167. package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
  168. package/runtime/python/okstra_ctl/wizard/state.py +6 -2
  169. package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
  170. package/runtime/python/okstra_ctl/wizard/steps_plan.py +27 -278
  171. package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
  172. package/runtime/python/okstra_ctl/work_categories.py +1 -1
  173. package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
  174. package/runtime/python/okstra_ctl/workflow.py +26 -143
  175. package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
  176. package/runtime/skills/okstra-run/SKILL.md +1 -1
  177. package/runtime/skills/okstra-user-response/SKILL.md +23 -4
  178. package/runtime/templates/reports/quick-input.template.md +1 -1
  179. package/runtime/templates/reports/task-brief.template.md +1 -1
  180. package/runtime/validators/validate-brief.py +2 -2
  181. package/runtime/validators/validate-run.py +287 -4091
  182. package/runtime/validators/validate_analysis_report.py +14 -126
  183. package/docs/task-process/README.md +0 -82
  184. package/docs/task-process/common-flow.md +0 -173
  185. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
  186. package/runtime/python/okstra_ctl/technical_verification.py +0 -195
  187. /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
  188. /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
  189. /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
  190. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
  191. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
  192. /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
  193. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
  194. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
  195. /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
  196. /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
  197. /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
  198. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
  199. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
  200. /package/runtime/{prompts/profiles/final-verification.json → python/okstra_ctl/phases/final_verification/profile.json} +0 -0
  201. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.html +0 -0
  202. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.md +0 -0
  203. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
  204. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
  205. /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
  206. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
  207. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
  208. /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
  209. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
  210. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
  211. /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
  212. /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
  213. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
  214. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
  215. /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
  216. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
  217. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
  218. /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
  219. /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
  220. /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
  221. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
  222. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
  223. /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
  224. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
  225. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
  226. /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
  227. /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
  228. /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
  229. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
  230. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
  231. /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
  232. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
  233. /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))
@@ -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,191 +17,24 @@ 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
26
+ from okstra_ctl.phases.final_verification.wizard import (
27
+ StageAnswerError,
28
+ validate_stage_answer,
29
+ whole_task_verification_allowed,
30
+ )
32
31
  from okstra_project.state import read_task_manifest
33
32
 
34
- from .ids import (
35
- ALL_STAGES,
36
- PICK_OTHER,
37
- PICK_TYPE_CUSTOM,
38
- PICK_USE_DEFAULT,
39
- SCOPE_SHAPE_LINKED,
40
- SCOPE_SHAPE_NO_ANSWERS,
41
- SCOPE_SHAPE_UNLINKED,
42
- S_APPROVED_PLAN,
43
- S_APPROVED_PLAN_PICK,
44
- S_APPROVE_PLAN_CONFIRM,
45
- S_FIX_CYCLE_CONFIRM,
46
- S_HANDOFF_STAGE_PICK,
47
- S_REVERIFY_SCOPE_PICK,
48
- S_REVERIFY_SCOPE_STAGES,
49
- S_SELECTED_DIRECTION_PICK,
50
- S_STAGE_PICK,
51
- _REPORT_PREFIX,
52
- )
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
53
34
  from .state import Option, Prompt, WizardError, WizardState
54
35
  from .prompts import _opt, _p
55
- from .sources import (
56
- _latest_implementation_planning_report,
57
- _list_implementation_planning_reports,
58
- _parse_stage_objects,
59
- _prior_stage_numbers,
60
- _project_relative_path,
61
- _require_file,
62
- _reserved_stage_numbers,
63
- _reverify_scope_pick_required,
64
- _reverify_scope_preview,
65
- _reverify_scope_shape,
66
- _stage_lifecycle_snapshot,
67
- )
68
-
69
-
70
- # 후보는 리포트 **기록**으로 찾는다. 전체 열람본(`.md`)은 `2bcd575` 이후 완료
71
- # 산출물이 아니라 요청 시 렌더라, `.md` 를 글롭하는 동안 이 목록은 항상 비었고
72
- # 방향 선택 단계 자체가 뜨지 않았다 — 비교가 끝나도 계획으로 갈 길이 없었다.
73
- _SELECTION_REPORT_RE = re.compile(
74
- r"^final-report-implementation-option-selection-(?P<seq>\d{3,})\.data\.json$"
75
- )
76
-
77
-
78
- def _selected_direction_candidates(state: WizardState) -> list[str]:
79
- if not state.project_root or not state.task_group or not state.task_id:
80
- return []
81
- project_root = Path(state.project_root).resolve()
82
- task_root = lexical_absolute_path(
83
- task_dir(project_root, state.task_group, state.task_id)
84
- )
85
- reports = (
86
- task_runs_dir(project_root, state.task_group, state.task_id)
87
- / "implementation-option-selection"
88
- / "reports"
89
- )
90
- candidates: list[tuple[int, Path]] = []
91
- for report in reports.glob(
92
- "final-report-implementation-option-selection-*.data.json"
93
- ):
94
- match = _SELECTION_REPORT_RE.fullmatch(report.name)
95
- if match is None:
96
- continue
97
- try:
98
- validated = validate_task_artifact_path(
99
- report, task_root, "selection report"
100
- )
101
- except DirectionSelectionError:
102
- continue
103
- candidates.append((int(match.group("seq")), validated))
104
- return [
105
- _project_relative_path(path, project_root)
106
- for _, path in sorted(candidates, reverse=True)[:3]
107
- ]
108
-
109
-
110
- def _planning_rerun_selected(state: WizardState) -> bool:
111
- if (
112
- not state.clarification_response_path
113
- or not state.project_root
114
- or not state.task_group
115
- or not state.task_id
116
- ):
117
- return False
118
- project_root = Path(state.project_root).resolve()
119
- raw_path = Path(state.clarification_response_path).expanduser()
120
- path = lexical_absolute_path(
121
- raw_path if raw_path.is_absolute() else project_root / raw_path
122
- )
123
- task_root = lexical_absolute_path(
124
- task_dir(project_root, state.task_group, state.task_id)
125
- )
126
- reports = lexical_absolute_path(
127
- task_runs_dir(project_root, state.task_group, state.task_id)
128
- / "implementation-planning"
129
- / "reports"
130
- )
131
- try:
132
- validate_task_artifact_path(path, task_root, "planning report")
133
- except DirectionSelectionError:
134
- return False
135
- return (
136
- path.is_file()
137
- and not path.is_symlink()
138
- and re.fullmatch(
139
- r"final-report-implementation-planning-\d{3,}\.(?:md|data\.json)", path.name
140
- )
141
- is not None
142
- and path.parent == reports
143
- )
144
-
145
-
146
- def _build_selected_direction_pick(state: WizardState) -> Prompt:
147
- candidates = _selected_direction_candidates(state)
148
- t = _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)
149
- if not candidates:
150
- raise WizardError(t["errors"]["none"])
151
- return Prompt(
152
- step=S_SELECTED_DIRECTION_PICK,
153
- kind="pick",
154
- label=t["label"],
155
- options=[
156
- _opt(path, _selection_option_label(state, path, t))
157
- for path in candidates
158
- ],
159
- echo_template=t["echo_template"],
160
- )
161
-
162
-
163
- def _selection_option_label(state: WizardState, rel_path: str, t: dict) -> str:
164
- """후보 경로에 그 리포트가 실제로 무엇을 담았는지 붙인다.
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
165
37
 
166
- 목록은 최신순이라 후보가 0건인 최근 재실행이 맨 위에 온다. 경로만 보이면
167
- 사용자가 그것을 고르고, 방향 없는 리포트로 계획을 열려다 막힌다.
168
- """
169
- record = Path(state.project_root) / rel_path
170
- try:
171
- data = load_owned_object(record, artifact="implementation option data")
172
- except (OSError, JsonBoundaryError):
173
- return rel_path
174
- selection = data.get("implementationOptionSelection")
175
- if not isinstance(selection, Mapping):
176
- return rel_path
177
- rows = selection.get("rankedOptions")
178
- ids = [
179
- str(row.get("id"))
180
- for row in rows
181
- if isinstance(row, Mapping) and row.get("id")
182
- ] if isinstance(rows, list) else []
183
- if not ids:
184
- return t["labels"]["no_options"].format(path=rel_path)
185
- return t["labels"]["options"].format(path=rel_path, ids=", ".join(ids))
186
-
187
-
188
- def _submit_selected_direction_pick(
189
- state: WizardState, value: str
190
- ) -> Optional[str]:
191
- t = _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)
192
- candidates = _selected_direction_candidates(state)
193
- if value not in candidates:
194
- raise WizardError(t["errors"]["unknown"].format(value=value))
195
- try:
196
- resolve_selected_direction(
197
- Path(state.project_root) / value,
198
- expected_task_key=f"{state.project_id}:{state.task_group}:{state.task_id}",
199
- )
200
- except DirectionSelectionError as exc:
201
- raise WizardError(str(exc)) from exc
202
- state.selected_direction_path = value
203
- # 두 입력은 상호 배타다 — 방향이 정해진 순간 이 런은 새 계획이고,
204
- # clarification 자리에 남은 값은 render-bundle 이 거절할 이유일 뿐이다.
205
- # 거절을 여기서 앞당기는 대신 값을 비우고, 무엇이 비워졌는지 echo 로
206
- # 알린다. 사용자 답변은 손실되지 않는다: 그 사이드카는
207
- # `--selected-direction` 경로로 계획 런에 첨부된다(`run.py` 참조).
208
- if state.clarification_response_path:
209
- state.clarification_response_path = ""
210
- return t["echo_variants"]["cleared_clarification"].format(value=value)
211
- return f"selected-direction: {value}"
212
38
 
213
39
 
214
40
  def _classify_approved_plan(path_str: str, project_root: Path) -> tuple[Path, bool]:
@@ -422,7 +248,10 @@ def _whole_task_allowed(
422
248
  return False
423
249
  if done is None:
424
250
  done = _stage_lifecycle_snapshot(state, stages).done_stages
425
- return all(s.stage_number in done for s in stages)
251
+ return whole_task_verification_allowed(
252
+ stage_numbers=tuple(stage.stage_number for stage in stages),
253
+ done_stages=done,
254
+ )
426
255
 
427
256
 
428
257
  def _build_approved_plan_pick(state: WizardState) -> Prompt:
@@ -608,35 +437,19 @@ def _build_stage_pick(state: WizardState) -> Prompt:
608
437
  # Presentation keys are mapped explicitly rather than interpolated from the
609
438
  # status, so renaming a Stage Lifecycle status is a compile-time-visible edit
610
439
  # here instead of a KeyError raised while drawing the picker.
611
- _STAGE_MARKER_KEYS = {
612
- "done": "mark_done",
613
- "active": "mark_active",
614
- "ready": "mark_ready",
615
- "blocked": "mark_blocked",
616
- }
617
-
618
-
619
- def _impl_stage_marker(t, lifecycle) -> str:
620
- return t["options"][_STAGE_MARKER_KEYS[lifecycle.status]]
621
440
 
622
441
 
623
442
  def _submit_stage_pick(state: WizardState, answer: str) -> Optional[str]:
624
443
  if state.task_type == "implementation":
625
444
  return _submit_impl_stage_pick(state, answer)
626
- # final-verification: 단일선택 유지 (whole-task 또는 단일 정수; auto 불가)
627
- if not answer:
628
- raise WizardError("value required")
629
- if answer == WHOLE_TASK_STAGE:
630
- if not _whole_task_allowed(state):
631
- raise WizardError(
632
- "whole-task verification requires final-verification "
633
- "with all stages done")
634
- else:
635
- try:
636
- int(answer)
637
- except ValueError:
638
- raise WizardError(
639
- f"answer must be whole-task or a stage number, got {answer!r}")
445
+ try:
446
+ validate_stage_answer(
447
+ answer,
448
+ whole_task_token=WHOLE_TASK_STAGE,
449
+ whole_task_allowed=answer == WHOLE_TASK_STAGE and _whole_task_allowed(state),
450
+ )
451
+ except StageAnswerError as exc:
452
+ raise WizardError(str(exc)) from exc
640
453
  state.selected_stage = answer
641
454
  return f"stage: {answer}"
642
455
 
@@ -668,34 +481,6 @@ def _submit_impl_stage_pick(state: WizardState, answer: str) -> Optional[str]:
668
481
  return t["echo_variants"]["plain"].format(stages=state.selected_stages)
669
482
 
670
483
 
671
- def _impl_chosen_stages(t, picks, answer, all_nums, occupied) -> set:
672
- if ALL_STAGES in picks:
673
- if len(picks) > 1:
674
- raise WizardError(t["errors"]["all_exclusive"])
675
- chosen = {n for n in all_nums if n not in occupied}
676
- if not chosen:
677
- raise WizardError(t["errors"]["nothing_selectable"])
678
- return chosen
679
- try:
680
- nums = {int(p) for p in picks}
681
- except ValueError:
682
- raise WizardError(t["errors"]["bad_number"].format(answer=answer))
683
- unknown = sorted(nums - all_nums)
684
- if unknown:
685
- raise WizardError(t["errors"]["unknown_stage"].format(
686
- bad=", ".join(map(str, unknown))))
687
- bad = sorted(nums & occupied)
688
- if bad:
689
- raise WizardError(t["errors"]["occupied"].format(
690
- bad=", ".join(map(str, bad))))
691
- return nums
692
-
693
-
694
- def _handoff_msgs(state: WizardState) -> dict:
695
- """handoff_stage_pick 의 JSON 텍스트 묶음 (label 미사용 조회용)."""
696
- return _p(state.workspace_root, "handoff_stage_pick", blocked="")
697
-
698
-
699
484
  def _resolve_handoff_plan(state: WizardState) -> Path:
700
485
  """release-handoff 의 approved plan 을 질문 없이 자동 해소한다.
701
486
 
@@ -717,7 +502,7 @@ def _resolve_handoff_plan(state: WizardState) -> Path:
717
502
 
718
503
  def _handoff_eligibility(state: WizardState) -> list:
719
504
  """stage 별 PR 자격 — okstra_ctl.handoff 의 SSOT 판정을 그대로 재사용한다."""
720
- from okstra_ctl.handoff import compute_eligibility
505
+ from okstra_ctl.phases.release_handoff.operations import compute_eligibility
721
506
  from okstra_ctl.consumers import read_consumers
722
507
  plan = _resolve_handoff_plan(state)
723
508
  try:
@@ -731,53 +516,17 @@ def _handoff_eligibility(state: WizardState) -> list:
731
516
  # 자격 stage 전체를 한 번에 고르는 값. stage 번호는 숫자뿐이라 충돌하지 않는다.
732
517
  # CLI 는 `--stages` 를 비우면 전체를 쓰지만, 위저드 pick 은 빈 선택을 거절하므로
733
518
  # 그 뜻을 표현할 값이 따로 필요하다.
734
- 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
+ )
735
522
 
736
523
 
737
524
  def _build_handoff_stage_pick(state: WizardState) -> Prompt:
738
- elig = _handoff_eligibility(state)
739
- eligible = [e for e in elig if e["eligible"]]
740
- blocked = [e for e in elig if not e["eligible"]]
741
- msgs = _handoff_msgs(state)
742
- blocked_summary = ("; ".join(
743
- f"stage {e['stage']} ({', '.join(e['reasons'])})" for e in blocked)
744
- or msgs["labels"]["blocked_none"])
745
- if not eligible:
746
- raise WizardError(
747
- msgs["errors"]["nothing_eligible"].format(blocked=blocked_summary))
748
- t = _p(state.workspace_root, "handoff_stage_pick", blocked=blocked_summary)
749
- stage_label = t["labels"]["stage"]
750
- options: list[Option] = []
751
- if len(eligible) > 1:
752
- options.append(_opt(ALL_ELIGIBLE_STAGES, t["labels"]["all_stages"].format(
753
- stages=", ".join(str(e["stage"]) for e in eligible))))
754
- for e in eligible:
755
- deps = ", ".join(str(d) for d in e["depends_on"]) or "-"
756
- options.append(_opt(str(e["stage"]),
757
- stage_label.format(stage=e["stage"], deps=deps)))
758
- return Prompt(
759
- step=S_HANDOFF_STAGE_PICK, kind="pick", multi=True,
760
- label=t["label"], options=options,
761
- echo_template=t["echo_template"],
762
- )
525
+ return build_handoff_stage_pick(state, _handoff_eligibility(state))
763
526
 
764
527
 
765
528
  def _submit_handoff_stage_pick(state: WizardState, value: str) -> Optional[str]:
766
- t = _handoff_msgs(state)
767
- picks = [v.strip() for v in (value or "").split(",") if v.strip()]
768
- if not picks:
769
- raise WizardError(t["errors"]["none_selected"])
770
- eligible = {str(e["stage"]) for e in _handoff_eligibility(state)
771
- if e["eligible"]}
772
- if ALL_ELIGIBLE_STAGES in picks:
773
- picks = sorted(eligible, key=int)
774
- bad = [p for p in picks if p not in eligible]
775
- if bad:
776
- raise WizardError(t["errors"]["not_eligible"].format(
777
- bad=", ".join(bad), eligible=", ".join(sorted(eligible))))
778
- nums = sorted({int(p) for p in picks})
779
- state.handoff_stages = ",".join(str(n) for n in nums)
780
- return t["echo_variants"]["stages"].format(stages=state.handoff_stages)
529
+ return submit_handoff_stage_pick(state, value, lambda: _handoff_eligibility(state))
781
530
 
782
531
 
783
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
@@ -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,