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
@@ -27,7 +27,8 @@ from . import code_review_paths, stage_targets, worktree_registry
27
27
  from .code_review_paths import branch_review_dir, next_branch_review
28
28
  from .paths import RunRef
29
29
  from .stage_map import StageMapError, load_latest_plan_stage_map
30
- from .fixed_text import line
30
+ from .fixed_text import line, scalar
31
+ from .report_language import ReportLanguageError, resolve_report_language
31
32
 
32
33
  DEFAULT_BRANCH_CANDIDATES = ("main", "master")
33
34
 
@@ -101,9 +102,12 @@ def _branch_snapshot(repo: Path, branch: str, base: str, review_date: str) -> di
101
102
  _CLI_EPILOG = r"""Usage:
102
103
  okstra code-review target --task-key <k> --stage <N> [--base <ref>] [--project-root <dir>] [--cwd <dir>] --json
103
104
  okstra code-review target --branch <name> [--base <ref>] [--date <YYYY-MM-DD>] [--project-root <dir>] --json
105
+ okstra code-review check-lines --base <ref> --head <ref> --cite <path:line> [--cite ...] [--project-root <dir>]
104
106
 
105
107
  Output: JSON { ok, mode, worktreePath, branch, baseCommit, headCommit,
106
- reviewPath, round }. `--base` sets the base in either mode. Without it, stage
108
+ reviewPath, round, reportLanguage }. `check-lines` prints `ok`, `not-changed`
109
+ (with the file's changed lines) or `not-in-diff` per citation and exits 1 when
110
+ any citation is not a line the range changed. `--base` sets the base in either mode. Without it, stage
107
111
  mode reads the base commit from the stage registry row and branch mode falls
108
112
  back to the merge-base with the default branch. An empty worktreePath means the review reads the branch ref
109
113
  instead of a checked-out tree. This is read-only — it never creates
@@ -116,11 +120,9 @@ def _build_parser() -> argparse.ArgumentParser:
116
120
  epilog=_CLI_EPILOG,
117
121
  formatter_class=argparse.RawDescriptionHelpFormatter,
118
122
  prog="okstra code-review",
119
- description="Resolve a code review's diff range and result file path."
123
+ description="Resolve a code review's diff range and result file path, or check finding citations."
120
124
  )
121
- # 서브커맨드는 하나뿐이지만 공개 명령이 `okstra code-review target` 이므로
122
- # 그 토큰을 파서가 직접 받는다.
123
- parser.add_argument("command", choices=("target",))
125
+ parser.add_argument("command", choices=("target", "check-lines"))
124
126
  parser.add_argument("--task-key", default="", help="project-id:task-group:task-id")
125
127
  parser.add_argument("--stage", type=int, default=0, help="stage number to review")
126
128
  parser.add_argument("--branch", default="", help="branch to review")
@@ -128,6 +130,11 @@ def _build_parser() -> argparse.ArgumentParser:
128
130
  "--base", default="",
129
131
  help="explicit base ref; overrides the stage registry base or the branch merge-base",
130
132
  )
133
+ parser.add_argument("--head", default="", help="check-lines: head ref of the reviewed range")
134
+ parser.add_argument(
135
+ "--cite", action="append", default=[],
136
+ help="check-lines: a finding's `path:line` (repeatable)",
137
+ )
131
138
  parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
132
139
  parser.add_argument("--project-root", default="", help="explicit project root")
133
140
  parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
@@ -160,6 +167,7 @@ def render_code_review_target_text(payload: dict) -> str:
160
167
  ("Worktree path", "worktreePath"), ("Branch", "branch"),
161
168
  ("Base commit", "baseCommit"), ("Head commit", "headCommit"),
162
169
  ("Review path", "reviewPath"), ("Round", "round"),
170
+ ("Report language", "reportLanguage"),
163
171
  ("Task key", "taskKey"), ("Task root", "taskRoot"), ("Stage", "stage"),
164
172
  ("Failure stage", "stage"), ("Failure reason", "reason"),
165
173
  )
@@ -170,6 +178,77 @@ def render_code_review_target_text(payload: dict) -> str:
170
178
  return "".join(rows)
171
179
 
172
180
 
181
+ _HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
182
+
183
+
184
+ def changed_lines(repo: Path, base: str, head: str) -> dict[str, set[int]]:
185
+ """Head-side line numbers each file's diff touched.
186
+
187
+ A pure deletion changes no head line, so it anchors on the line before the
188
+ cut — the line a reviewer quotes when it points at what was removed.
189
+ """
190
+ diff = _git_out(repo, "diff", "-U0", "--no-color", f"{base}..{head}")
191
+ lines: dict[str, set[int]] = {}
192
+ current: set[int] | None = None
193
+ for row in diff.splitlines():
194
+ if row.startswith("+++ "):
195
+ path = row[4:]
196
+ current = None if path == "/dev/null" else lines.setdefault(path[2:], set())
197
+ continue
198
+ match = _HUNK_RE.match(row)
199
+ if match and current is not None:
200
+ start = int(match[1])
201
+ count = 1 if match[2] is None else int(match[2])
202
+ current.update(range(start, start + count) if count else {max(start, 1)})
203
+ return lines
204
+
205
+
206
+ def render_check_lines(changed: dict[str, set[int]], cites: list[str]) -> tuple[str, bool]:
207
+ rows = ["Okstra code review citation check\n"]
208
+ all_ok = True
209
+ for cite in cites:
210
+ path, _, number = cite.rpartition(":")
211
+ touched = changed.get(path)
212
+ label = f"- `{scalar(cite)}`: "
213
+ if touched is None:
214
+ all_ok = False
215
+ rows.append(label + "`not-in-diff`\n")
216
+ elif number.isdigit() and int(number) in touched:
217
+ rows.append(label + "`ok`\n")
218
+ else:
219
+ all_ok = False
220
+ rows.append(
221
+ label + "`not-changed` — changed lines "
222
+ f"{_line_ranges(sorted(touched))}\n"
223
+ )
224
+ return "".join(rows), all_ok
225
+
226
+
227
+ def _line_ranges(numbers: list[int]) -> str:
228
+ ranges: list[str] = []
229
+ for number in numbers:
230
+ if ranges and number == int(ranges[-1].split("-")[-1]) + 1:
231
+ ranges[-1] = f"{ranges[-1].split('-')[0]}-{number}"
232
+ else:
233
+ ranges.append(str(number))
234
+ return ", ".join(ranges)
235
+
236
+
237
+ def _check_lines(args: argparse.Namespace, project_root: Path) -> int:
238
+ if not (args.base and args.head and args.cite):
239
+ print("okstra code-review check-lines: pass --base, --head and at least one --cite",
240
+ file=sys.stderr)
241
+ return 2
242
+ try:
243
+ changed = changed_lines(project_root, args.base, args.head)
244
+ except StateError as exc:
245
+ print(f"okstra code-review check-lines: {exc}", file=sys.stderr)
246
+ return 2
247
+ text, all_ok = render_check_lines(changed, args.cite)
248
+ print(text, end="")
249
+ return 0 if all_ok else 1
250
+
251
+
173
252
  def main(argv: list[str] | None = None) -> int:
174
253
  args = _build_parser().parse_args(argv)
175
254
  try:
@@ -181,8 +260,15 @@ def main(argv: list[str] | None = None) -> int:
181
260
  print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
182
261
  return 2
183
262
 
263
+ if args.command == "check-lines":
264
+ return _check_lines(args, Path(project_root))
184
265
  try:
185
266
  snapshot = _resolve(args, Path(project_root))
267
+ snapshot["reportLanguage"] = resolve_report_language(Path(project_root), {}, {})
268
+ except ReportLanguageError as exc:
269
+ payload = {"ok": False, "stage": "report-language", "reason": str(exc)}
270
+ print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
271
+ return 1
186
272
  except StateError as exc:
187
273
  payload = {
188
274
  "ok": False,
@@ -430,3 +430,15 @@ def backfill_done_from_carry(plan_run_root: Path) -> int:
430
430
  settled_stages.add(stage)
431
431
  recovered += 1
432
432
  return recovered
433
+
434
+
435
+ def require_done_task_key(rows: List[Dict[str, Any]], stage: int) -> str:
436
+ from .handoff_error import HandoffError
437
+
438
+ done = latest_done_by_stage(rows)
439
+ row = done.get(stage)
440
+ if not row:
441
+ raise HandoffError(
442
+ f"stage {stage} has no done row in consumers.jsonl — "
443
+ "finish the implementation stage first")
444
+ return row.get("impl_task_key", "")
@@ -0,0 +1,121 @@
1
+ """워커를 띄우고 거두는 명령이 대신 기록하는 리드 체크포인트.
2
+
3
+ dispatch 를 정하고 결과를 확정하는 곳은 `team dispatch` / `team await`(pane)와
4
+ `worker-dispatch`(CLI, 블로킹)이다. 그래서 `phase-3-team-create`,
5
+ `phase-4-dispatch`, `phase-5-collect`, `phase-6-synthesis` 가 말하는 사실을 이
6
+ 명령들이 갖고 있다. 리드가 같은 사실을 `okstra lead-progress append` 로 다시
7
+ 적으면 체크포인트마다 전체 컨텍스트를 한 턴씩 쓰는데, 여기서 기록하면 턴이 들지 않는다.
8
+
9
+ 워커별 체크포인트는 `initial` dispatch 만 센다. reverify·critic·plan-verify 는
10
+ 리드가 따로 알리는 이후 라운드이고, 여기서 기록하면 activity-contract run 이
11
+ 존재하지 않는 `worker-dispatched` / `worker-completed` 행을 요구받게 된다.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import Any, Mapping, Sequence
17
+
18
+ from .dispatch_state import (
19
+ TERMINAL_WORKER_STATUSES,
20
+ dispatch_worker_key,
21
+ has_recorded_team_create,
22
+ worker_dispatch_records,
23
+ )
24
+ from .lead_progress import record_checkpoint
25
+ from .worker_prompt_body import REPORT_WRITER_WORKER_ID
26
+
27
+ _INITIAL = "initial"
28
+
29
+
30
+ def record_dispatch_checkpoints(
31
+ project_root: Path,
32
+ run_manifest: str | Path,
33
+ team_state: Mapping[str, Any],
34
+ jobs: Sequence[Any],
35
+ *,
36
+ source: str,
37
+ ) -> list[str]:
38
+ """곧 띄울 배치의 체크포인트. 배치가 시작되기 전의 team-state 로 판정한다.
39
+
40
+ `phase-3-team-create` 는 이 dispatch 가 implicit-team 마커를 쓸 때만 기록한다
41
+ (`dispatch_state.record_dispatch_facts`). 리드가 미리 적은 마커(concurrent-run
42
+ `skipped` 결정)는 리드가 알린다. `phase-6-synthesis` 는 run 의 첫 report-writer
43
+ dispatch 에서만 기록해서, 교정 라운드가 Phase 6 을 다시 열지 않게 한다.
44
+ """
45
+ if not jobs:
46
+ return []
47
+ lines: list[str | None] = []
48
+ if not has_recorded_team_create(team_state):
49
+ lines.append(record_checkpoint(
50
+ project_root, run_manifest, "phase-3-team-create", source=source,
51
+ ))
52
+ for job in jobs:
53
+ if job.dispatch_kind == _INITIAL:
54
+ lines.append(record_checkpoint(
55
+ project_root, run_manifest, "phase-4-dispatch", source=source,
56
+ worker=job.worker_id,
57
+ fields=(("model", job.model_execution_value),),
58
+ ))
59
+ dispatches_writer = any(job.worker_id == REPORT_WRITER_WORKER_ID for job in jobs)
60
+ writer_dispatched_before = bool(
61
+ worker_dispatch_records(team_state, REPORT_WRITER_WORKER_ID)
62
+ )
63
+ if dispatches_writer and not writer_dispatched_before:
64
+ lines.append(record_checkpoint(
65
+ project_root, run_manifest, "phase-6-synthesis", source=source,
66
+ ))
67
+ return [line for line in lines if line]
68
+
69
+
70
+ def settled_initial_dispatches(
71
+ team_state: Mapping[str, Any],
72
+ ) -> dict[str, Mapping[str, Any]]:
73
+ """최신 attempt 가 종결 상태에 이른 `initial` dispatch 들.
74
+
75
+ 재시도해도 바뀌지 않는 prompt 경로를 키로 쓴다. 실패 후 재시도 중인 attempt 는
76
+ 재시도까지 끝나야 거둔 결과로 친다.
77
+ """
78
+ latest: dict[str, Mapping[str, Any]] = {}
79
+ for record in worker_dispatch_records(team_state):
80
+ if record.get("kind") != _INITIAL:
81
+ continue
82
+ key = str(record.get("promptPath") or "")
83
+ current = latest.get(key)
84
+ if current is None or _attempt(record) >= _attempt(current):
85
+ latest[key] = record
86
+ return {
87
+ key: record
88
+ for key, record in latest.items()
89
+ if record.get("status") in TERMINAL_WORKER_STATUSES
90
+ }
91
+
92
+
93
+ def record_collect_checkpoints(
94
+ project_root: Path,
95
+ run_manifest: str | Path,
96
+ before: Mapping[str, Mapping[str, Any]],
97
+ after: Mapping[str, Mapping[str, Any]],
98
+ *,
99
+ source: str,
100
+ ) -> list[str]:
101
+ """이 명령이 확정한 dispatch 마다 `phase-5-collect` 를 기록한다.
102
+
103
+ 확정이 곧 체크포인트가 알리는 결과 검증이다: 필요한 산출물이 있고, 변경 감사를
104
+ 통과했고, 결과가 dispatch 에 연결됐거나(`dispatch_core._finish_attempt`),
105
+ attempt 가 `error` / `timeout` 으로 닫혔다.
106
+ """
107
+ lines = []
108
+ for key, record in after.items():
109
+ previous = before.get(key)
110
+ if previous is not None and _attempt(previous) == _attempt(record):
111
+ continue
112
+ lines.append(record_checkpoint(
113
+ project_root, run_manifest, "phase-5-collect", source=source,
114
+ worker=dispatch_worker_key(record),
115
+ fields=(("status", str(record.get("status"))),),
116
+ ))
117
+ return [line for line in lines if line]
118
+
119
+
120
+ def _attempt(record: Mapping[str, Any]) -> int:
121
+ return int(record.get("attempt", 1))
@@ -9,7 +9,7 @@ from dataclasses import dataclass, field, replace
9
9
  from datetime import datetime, timezone
10
10
  import os
11
11
  from pathlib import Path
12
- from typing import Any, Mapping, Sequence
12
+ from typing import Any, Callable, Mapping, Sequence
13
13
 
14
14
  from .dispatch_state import abandon_agent_dispatch_attempt
15
15
  from .agent.invocation import invocation_input_digest
@@ -33,7 +33,7 @@ from .dispatch_state import (
33
33
  link_agent_dispatch_result as _link_agent_dispatch_result,
34
34
  missing_completion_paths as _missing_completion_paths,
35
35
  unusable_result_defect,
36
- _dispatch_worker_key,
36
+ dispatch_worker_key,
37
37
  _plan_verify_result_aliases,
38
38
  mutate_team_state as _mutate_team_state,
39
39
  require_string as _require_string,
@@ -408,7 +408,17 @@ def build_dispatch_plan(
408
408
  )
409
409
 
410
410
 
411
- def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
411
+ BeforeStart = Callable[["DispatchPlan"], None]
412
+
413
+
414
+ def dispatch_plan(
415
+ plan: DispatchPlan, *, wait: bool = True, before_start: BeforeStart | None = None,
416
+ ) -> int:
417
+ """`before_start` 는 복구·검증을 통과한 plan 으로, 워커를 띄우기 직전에 한 번 불린다.
418
+
419
+ 거부된 배치와 증거 보완으로 빠진 작업은 여기까지 오지 않으므로, 띄울 워커에
420
+ 대한 사실(체크포인트)은 이 시점에 기록해야 실제로 뜬 워커만 남는다.
421
+ """
412
422
  plan = _recover_declaration_failures(plan)
413
423
  if not plan.jobs:
414
424
  return 0
@@ -416,21 +426,19 @@ def dispatch_plan(plan: DispatchPlan, *, wait: bool = True) -> int:
416
426
  _validate_report_writer_isolation(plan.jobs)
417
427
  _validate_implementation_phase_order(plan.jobs)
418
428
  _revalidate_plan_role_executions(plan)
419
- if wait:
420
- pane_backends = sorted(
421
- {
422
- job.backend
423
- for job in plan.jobs
424
- if job.backend == BACKEND_CMUX_PANE
425
- }
429
+ pane_backends = sorted(
430
+ {job.backend for job in plan.jobs if job.backend == BACKEND_CMUX_PANE}
431
+ )
432
+ if wait and pane_backends:
433
+ raise DispatchError(
434
+ f"wait=True dispatch does not support {'/'.join(pane_backends)} "
435
+ "workers: the per-job blocking loop would serialize panes "
436
+ "instead of running them concurrently; dispatch panes with "
437
+ "wait=False"
426
438
  )
427
- if pane_backends:
428
- raise DispatchError(
429
- f"wait=True dispatch does not support {'/'.join(pane_backends)} "
430
- "workers: the per-job blocking loop would serialize panes "
431
- "instead of running them concurrently; dispatch panes with "
432
- "wait=False"
433
- )
439
+ if before_start is not None:
440
+ before_start(plan)
441
+ if wait:
434
442
  _record_dispatch_facts(plan.team_state_path, _dispatch_mode(plan.jobs))
435
443
  for job in plan.jobs:
436
444
  result = _dispatch_job_with_retry(plan, job)
@@ -509,7 +517,9 @@ def _round_artifact_paths(plan: DispatchPlan) -> tuple[Path, ...]:
509
517
  )
510
518
 
511
519
 
512
- def dispatch_cli_wrapper_plan(plan: DispatchPlan) -> int:
520
+ def dispatch_cli_wrapper_plan(
521
+ plan: DispatchPlan, *, before_start: BeforeStart | None = None,
522
+ ) -> int:
513
523
  """Start one dependency-free CLI batch before collecting any worker."""
514
524
  if any(job.backend != BACKEND_CLI_WRAPPER for job in plan.jobs):
515
525
  raise DispatchError("concurrent CLI dispatch requires cli-wrapper jobs only")
@@ -520,6 +530,8 @@ def dispatch_cli_wrapper_plan(plan: DispatchPlan) -> int:
520
530
  _validate_report_writer_isolation(plan.jobs)
521
531
  _validate_implementation_phase_order(plan.jobs)
522
532
  _revalidate_plan_role_executions(plan)
533
+ if before_start is not None:
534
+ before_start(plan)
523
535
  _record_dispatch_facts(plan.team_state_path, _dispatch_mode(plan.jobs))
524
536
  final_codes = _dispatch_cli_wrapper_batch(plan, plan.jobs)
525
537
  return next((final_codes[job.worker_id] for job in plan.jobs
@@ -1855,6 +1867,7 @@ def _canonical_write_contract(
1855
1867
  try:
1856
1868
  return build_invocation_write_contract(
1857
1869
  role=execution.role,
1870
+ task_type=str(plan.manifest.get("taskType") or ""),
1858
1871
  project_root=plan.project_root,
1859
1872
  worktree=worktree,
1860
1873
  artifact_paths=artifacts,
@@ -2269,14 +2282,23 @@ def _mutation_failure_reason(mutation, outcome: WorkerOutcome | None = None) ->
2269
2282
  failure = _wrapper_failure_text(outcome)
2270
2283
  if failure:
2271
2284
  reason = f"{reason}: {failure}"
2272
- paths = list(mutation.changed_paths)
2273
- if not paths:
2274
- return reason
2285
+ # 산출물 위반은 소스 변경 목록에 없다 — 그 목록만 보이면 정책 안에 있던
2286
+ # 소스 파일을 범인으로 읽는다 (실측 2026-09-26, dev-11054 `qa/baseline/`).
2287
+ for label, paths in (
2288
+ ("outside artifact policy", mutation.violating_artifact_paths),
2289
+ ("changed", mutation.changed_paths),
2290
+ ):
2291
+ if paths:
2292
+ reason += f" [{label}: {_capped_path_list(paths)}]"
2293
+ return reason
2294
+
2295
+
2296
+ def _capped_path_list(paths: Sequence[str]) -> str:
2275
2297
  shown = ", ".join(paths[:_MUTATION_REASON_PATH_LIMIT])
2276
2298
  remaining = len(paths) - _MUTATION_REASON_PATH_LIMIT
2277
2299
  if remaining > 0:
2278
2300
  shown += f", +{remaining} more"
2279
- return f"{reason} [changed: {shown}]"
2301
+ return shown
2280
2302
 
2281
2303
 
2282
2304
  def _finish_attempt(
@@ -2751,7 +2773,7 @@ def _record_wrapper_failure(
2751
2773
  out_path=out_path,
2752
2774
  task_key=_string_value(plan.manifest.get("taskKey")),
2753
2775
  phase=_workflow_phase(plan.manifest),
2754
- agent=_error_log_agent(job.worker_id),
2776
+ agent=_error_log_agent(job),
2755
2777
  agent_role=(
2756
2778
  "report-writer"
2757
2779
  if job.worker_id == REPORT_WRITER_WORKER_ID
@@ -2790,16 +2812,16 @@ def _job_stored_identity(job: WorkerJob, attempt: int) -> dict[str, Any]:
2790
2812
  }
2791
2813
 
2792
2814
 
2793
- def _error_log_agent(worker_id: str) -> str:
2794
- """The error log's `--agent` enum value for a worker id.
2815
+ def _error_log_agent(job: WorkerJob) -> str:
2816
+ """The error log's `--agent` enum value for a job.
2795
2817
 
2796
- The log's own allow-list is the authority on what it accepts; a worker whose
2797
- name is outside it is reported as such by `append_observed` rather than
2798
- silently rewritten into some other agent's records.
2818
+ The enum is keyed by provider (`codex-worker`), not by worker id: a role
2819
+ worker id such as `codex-verifier` produced `codex-verifier-worker`, which
2820
+ the allow-list rejected, so no wrapper failure reached the log.
2799
2821
  """
2800
- if worker_id == REPORT_WRITER_WORKER_ID:
2822
+ if job.worker_id == REPORT_WRITER_WORKER_ID:
2801
2823
  return REPORT_WRITER_WORKER_ID
2802
- return f"{worker_id}-worker"
2824
+ return f"{job.provider}-worker"
2803
2825
 
2804
2826
 
2805
2827
  def _workflow_phase(manifest: Mapping[str, Any]) -> str:
@@ -3349,7 +3371,7 @@ def _record_missing_completion_paths(record: Mapping[str, Any]) -> tuple[Path, .
3349
3371
  result_path = Path(_string_value(record.get("resultPath")))
3350
3372
  worker_result = Path(_string_value(record.get("workerResultPath")))
3351
3373
  missing: list[Path] = []
3352
- worker_id = _dispatch_worker_key(record)
3374
+ worker_id = dispatch_worker_key(record)
3353
3375
  dispatch_kind = _string_value(record.get("dispatchKind"))
3354
3376
  for path in _record_completion_paths(record):
3355
3377
  if path.is_file():
@@ -3366,7 +3388,7 @@ def _record_missing_completion_paths(record: Mapping[str, Any]) -> tuple[Path, .
3366
3388
 
3367
3389
  def _record_artifact_defects(record: Mapping[str, Any]) -> tuple[str, ...]:
3368
3390
  defect = unusable_result_defect(
3369
- _dispatch_worker_key(record),
3391
+ dispatch_worker_key(record),
3370
3392
  Path(_string_value(record.get("resultPath"))),
3371
3393
  _string_value(record.get("dispatchKind")),
3372
3394
  )
@@ -59,6 +59,7 @@ from .worker_prompt_body import REPORT_WRITER_WORKER_ID
59
59
  from .worker_prompt_contract import (
60
60
  PromptRecord,
61
61
  validate_initial_prompt_records,
62
+ validate_plan_verify_dispatch_identity,
62
63
  validate_prompt_model_header,
63
64
  validate_reverify_prompt,
64
65
  )
@@ -364,6 +365,12 @@ def resolve_required_path(
364
365
  def worktree_path(
365
366
  manifest: Mapping[str, Any], active_context: Mapping[str, Any]
366
367
  ) -> str:
368
+ # Must match the prompt's `**Worktree:**` anchor
369
+ # (`worker_prompt_headers._final_verification_target_headers`).
370
+ if manifest.get("taskType") == "final-verification":
371
+ target = active_context.get("verificationTarget")
372
+ if isinstance(target, Mapping) and target.get("worktreePath"):
373
+ return str(target["worktreePath"])
367
374
  worktree = active_context.get("executorWorktree")
368
375
  if not isinstance(worktree, Mapping):
369
376
  return ""
@@ -861,6 +868,7 @@ def _agent_write_contract(
861
868
  try:
862
869
  return build_invocation_write_contract(
863
870
  role=execution.role,
871
+ task_type=str(authority.get("taskType") or ""),
864
872
  project_root=project_root,
865
873
  worktree=worktree,
866
874
  artifact_paths=artifacts,
@@ -1625,12 +1633,12 @@ def record_dispatch_facts(team_state_path: Path, dispatch_mode: str) -> None:
1625
1633
  with _team_state_lock(team_state_path):
1626
1634
  payload = load_json_object(team_state_path, "team-state")
1627
1635
  payload["dispatchMode"] = dispatch_mode
1628
- if not _has_recorded_team_create(payload):
1636
+ if not has_recorded_team_create(payload):
1629
1637
  payload["teamCreate"] = {"attempted": False, "status": "implicit"}
1630
1638
  write_json(team_state_path, payload)
1631
1639
 
1632
1640
 
1633
- def _has_recorded_team_create(team_state: Mapping[str, Any]) -> bool:
1641
+ def has_recorded_team_create(team_state: Mapping[str, Any]) -> bool:
1634
1642
  existing = team_state.get("teamCreate")
1635
1643
  return (isinstance(existing, dict)
1636
1644
  and bool(str(existing.get("status") or "").strip()))
@@ -1770,7 +1778,7 @@ def worker_dispatch_records(
1770
1778
  집계, v2 명부 행 `scope` 는 `promptPath` 가 비어 0회).
1771
1779
 
1772
1780
  v1 행은 `workerId`, v2 행은 `assignmentRef` 마지막 마디로 워커에 묶인다
1773
- (`_dispatch_worker_key`). 리스트가 아니면 빈 결과 — `worker_session_ids`
1781
+ (`dispatch_worker_key`). 리스트가 아니면 빈 결과 — `worker_session_ids`
1774
1782
  와 같은 이유로 호출자를 깨지 않는다.
1775
1783
  """
1776
1784
  dispatches = team_state.get("workerDispatches")
@@ -1778,13 +1786,13 @@ def worker_dispatch_records(
1778
1786
  for record in dispatches if isinstance(dispatches, list) else []:
1779
1787
  if not isinstance(record, Mapping):
1780
1788
  continue
1781
- if worker_id is not None and _dispatch_worker_key(record) != worker_id:
1789
+ if worker_id is not None and dispatch_worker_key(record) != worker_id:
1782
1790
  continue
1783
1791
  records.append(record)
1784
1792
  return records
1785
1793
 
1786
1794
 
1787
- def _dispatch_worker_key(record: Mapping[str, Any]) -> str:
1795
+ def dispatch_worker_key(record: Mapping[str, Any]) -> str:
1788
1796
  """The `workers[]` row a dispatch record belongs to.
1789
1797
 
1790
1798
  A v1 record names it as `workerId`. A v2 record carries execution identity
@@ -1905,6 +1913,26 @@ def validate_initial_prompts(
1905
1913
  raise DispatchError(f"{task_type} prompt contract: " + "; ".join(errors))
1906
1914
 
1907
1915
 
1916
+ def _validate_plan_verify_identities(jobs: Sequence[WorkerJob]) -> None:
1917
+ errors: list[str] = []
1918
+ for job in jobs:
1919
+ try:
1920
+ text = job.prompt_path.read_text(encoding="utf-8")
1921
+ except OSError as exc:
1922
+ errors.append(f"{job.worker_id}: cannot read prompt {job.prompt_path}: {exc}")
1923
+ continue
1924
+ errors.extend(
1925
+ f"{job.worker_id}: {error}"
1926
+ for error in validate_plan_verify_dispatch_identity(
1927
+ text,
1928
+ dispatch_kind=job.dispatch_kind,
1929
+ result_name=job.worker_result_path.name,
1930
+ )
1931
+ )
1932
+ if errors:
1933
+ raise DispatchError("plan-body dispatch contract: " + "; ".join(errors))
1934
+
1935
+
1908
1936
  def validate_dispatch_prompts(
1909
1937
  manifest: Mapping[str, Any],
1910
1938
  active_context: Mapping[str, Any],
@@ -1912,6 +1940,7 @@ def validate_dispatch_prompts(
1912
1940
  ) -> None:
1913
1941
  if isinstance(manifest.get("agentContract"), Mapping):
1914
1942
  _validate_agent_invocations(manifest, jobs)
1943
+ _validate_plan_verify_identities(jobs)
1915
1944
  initial_jobs = [
1916
1945
  job for job in jobs if not is_verification_dispatch_kind(job.dispatch_kind)
1917
1946
  ]
@@ -11,7 +11,8 @@ from typing import Iterable
11
11
  from okstra_project import ResolverError, project_json_path, resolve_project_root
12
12
  from okstra_project.resolver import resolve_review_rule_packs
13
13
 
14
- from . import improvement_lenses, worktree_registry
14
+ from . import worktree_registry
15
+ from .phases.improvement_discovery import lenses as improvement_lenses
15
16
  from .models import provider_wrappers
16
17
  from .phases.catalog import PhaseAssetError, UnknownTaskType, profile_markdown
17
18
  from .workers import resolve_profile_workers
@@ -65,6 +65,9 @@ class ModelSpec:
65
65
  supported_roles: frozenset[str] | None
66
66
  selectable: bool
67
67
  pricing: Optional[tuple[float, float, float]]
68
+ # 이 모델을 실행할 때마다 공급자 CLI 에 덧붙는 인자. 사용자 CLI 설정에 맡기면
69
+ # 기계마다 달라지는 값(예: reasoning effort)을 카탈로그가 고정한다.
70
+ cli_args: tuple[str, ...]
68
71
 
69
72
  def __init__(
70
73
  self,
@@ -79,6 +82,7 @@ class ModelSpec:
79
82
  pricing: Optional[tuple[float, float, float]] = None,
80
83
  *,
81
84
  in_picker: bool | None = None,
85
+ cli_args: tuple[str, ...] = (),
82
86
  ) -> None:
83
87
  """Accept the former display/execution form while adapters migrate."""
84
88
  is_legacy = execution_value is _MISSING or not isinstance(execution_value, str)
@@ -102,6 +106,7 @@ class ModelSpec:
102
106
  object.__setattr__(self, "supported_roles", supported_roles)
103
107
  object.__setattr__(self, "selectable", selectable if in_picker is None else in_picker)
104
108
  object.__setattr__(self, "pricing", resolved_pricing)
109
+ object.__setattr__(self, "cli_args", cli_args)
105
110
 
106
111
  @property
107
112
  def display(self) -> str:
@@ -93,13 +93,18 @@ class SplitText:
93
93
  가야 하므로 종결 텍스트로 돌려주고, 진행은 기록과 화면에만 남는다.
94
94
  """
95
95
 
96
+ _answer: list[str] = field(default_factory=list, repr=False, compare=False)
97
+
96
98
  def merges_stderr(self) -> bool:
97
99
  return False
98
100
 
99
101
  def sinks(self, writer: TranscriptWriter) -> tuple[SinkSpec, ...]:
100
102
  def result(line: str) -> str | None:
101
103
  writer.write(WORKER, line)
102
- return strip_terminal_colors(line)
104
+ # 러너는 마지막으로 돌려받은 값만 호출자에게 넘기므로, 그 줄이 아니라
105
+ # 지금까지의 답 전체를 돌려준다.
106
+ self._answer.append(strip_terminal_colors(line))
107
+ return "\n".join(self._answer).strip("\n") or None
103
108
 
104
109
  def progress(line: str) -> str | None:
105
110
  writer.write(WORKER, line)
@@ -140,6 +145,9 @@ class JsonEvents:
140
145
  _saw_conclusion: list[bool] = field(
141
146
  default_factory=list, repr=False, compare=False
142
147
  )
148
+ # 마지막 도구 호출 뒤에 온 산문. Result 없는 어휘에서는 이것이 최종 답이다.
149
+ _trailing_text: list[str] = field(default_factory=list, repr=False, compare=False)
150
+ _saw_result: list[bool] = field(default_factory=list, repr=False, compare=False)
143
151
 
144
152
  def merges_stderr(self) -> bool:
145
153
  return True
@@ -164,11 +172,17 @@ class JsonEvents:
164
172
  if not isinstance(event, dict):
165
173
  return None
166
174
  self.observe(event)
167
- if self.is_conclusion is not None and self.is_conclusion(event):
175
+ concludes = self.is_conclusion is not None and self.is_conclusion(event)
176
+ if concludes:
168
177
  self._saw_conclusion.append(True)
169
178
  closing: str | None = None
170
179
  for entry in self.normalise(event):
171
180
  closing = self._write_entry(writer, entry) or closing
181
+ if concludes and closing is None and not self._saw_result:
182
+ # 종결 표지가 본문을 싣지 않는 어휘다. 돌려줄 것이
183
+ # 없으면 quiet 호출자의 stdout 이 비고 답은 로그에만 남는다.
184
+ joiner = "" if self.join_adjacent_text else "\n"
185
+ closing = joiner.join(self._trailing_text).strip() or None
172
186
  return closing
173
187
 
174
188
  return (("stdout", emit),)
@@ -179,6 +193,10 @@ class JsonEvents:
179
193
  def _write_entry(
180
194
  self, writer: TranscriptWriter, entry: StreamEvent
181
195
  ) -> str | None:
196
+ if isinstance(entry, Text):
197
+ self._trailing_text.append(entry.body)
198
+ else:
199
+ self._trailing_text.clear()
182
200
  if isinstance(entry, Text) and self.join_adjacent_text:
183
201
  self._pending_text.append(entry.body)
184
202
  self._publish_complete_lines(writer)
@@ -197,6 +215,7 @@ class JsonEvents:
197
215
  # Result 이벤트는 어휘와 무관한 종결이다. 빈 결과 텍스트도 종결이므로
198
216
  # closing_text 의 진릿값과 따로 기록한다.
199
217
  self._saw_conclusion.append(True)
218
+ self._saw_result.append(True)
200
219
  return closing
201
220
 
202
221
  def _publish_complete_lines(self, writer: TranscriptWriter) -> None: