okstra 0.206.0 → 0.207.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-registry.mjs +7 -1
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/lifecycle/install.mjs +1 -1
  5. package/dist/commands/lifecycle/install.mjs.map +1 -1
  6. package/docs/architecture/storage-model.md +1 -0
  7. package/docs/architecture.md +40 -16
  8. package/docs/cli.md +17 -15
  9. package/docs/contributor-change-matrix.md +3 -2
  10. package/docs/performance-improvement-plan-v2.md +1 -1
  11. package/docs/project-structure-overview.md +43 -20
  12. package/package.json +1 -1
  13. package/runtime/BUILD.json +2 -2
  14. package/runtime/agents/operations/code-review.json +1 -1
  15. package/runtime/bin/lib/okstra/usage.sh +3 -3
  16. package/runtime/bin/okstra-compact-reminder.sh +1 -1
  17. package/runtime/bin/okstra-spawn-followups.py +2 -2
  18. package/runtime/prompts/duties/direction-selection-worker.json +1 -1
  19. package/runtime/prompts/launch.template.md +2 -2
  20. package/runtime/prompts/lead/adapters/cmux.md +4 -3
  21. package/runtime/prompts/lead/context-loader.md +1 -1
  22. package/runtime/prompts/lead/convergence.md +44 -12
  23. package/runtime/prompts/lead/okstra-lead-contract.md +44 -73
  24. package/runtime/prompts/lead/phase-routing.md +64 -0
  25. package/runtime/prompts/lead/report-writer.md +10 -8
  26. package/runtime/prompts/lead/team-contract.md +1 -1
  27. package/runtime/prompts/profiles/_clarification-recommendation.md +4 -4
  28. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  29. package/runtime/prompts/profiles/_common-contract.md +2 -2
  30. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  31. package/runtime/prompts/profiles/forbidden-actions.json +0 -94
  32. package/runtime/prompts/wizard/prompts.ko.json +2 -1
  33. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
  34. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +5 -5
  35. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
  36. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -2
  37. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
  38. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
  39. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +17 -26
  40. package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +183 -0
  41. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +60 -10
  42. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +1 -1
  43. package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +21 -4
  44. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +10 -1
  45. package/runtime/python/okstra_ctl/analysis_inputs.py +0 -39
  46. package/runtime/python/okstra_ctl/analysis_scope.py +31 -0
  47. package/runtime/python/okstra_ctl/approval_decisions.py +32 -2
  48. package/runtime/python/okstra_ctl/asset_roots.py +19 -0
  49. package/runtime/python/okstra_ctl/assignment_resolver.py +8 -0
  50. package/runtime/python/okstra_ctl/blocking_checks.py +7 -0
  51. package/runtime/python/okstra_ctl/code_review_target.py +92 -6
  52. package/runtime/python/okstra_ctl/consumers.py +12 -0
  53. package/runtime/python/okstra_ctl/dispatch_checkpoints.py +121 -0
  54. package/runtime/python/okstra_ctl/dispatch_core.py +54 -32
  55. package/runtime/python/okstra_ctl/dispatch_state.py +34 -5
  56. package/runtime/python/okstra_ctl/doctor.py +2 -1
  57. package/runtime/python/okstra_ctl/domain/provider.py +5 -0
  58. package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -2
  59. package/runtime/python/okstra_ctl/domain/write_policy.py +2 -1
  60. package/runtime/python/okstra_ctl/execution_mutation_audit.py +46 -9
  61. package/runtime/python/okstra_ctl/handoff.py +11 -466
  62. package/runtime/python/okstra_ctl/handoff_error.py +5 -0
  63. package/runtime/python/okstra_ctl/implementation_direction.py +0 -477
  64. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +13 -1
  65. package/runtime/python/okstra_ctl/lead_progress.py +33 -1
  66. package/runtime/python/okstra_ctl/manager_view.py +26 -19
  67. package/runtime/python/okstra_ctl/model_io/lines.py +21 -4
  68. package/runtime/python/okstra_ctl/models.py +4 -1
  69. package/runtime/python/okstra_ctl/operation_invocation.py +11 -2
  70. package/runtime/python/okstra_ctl/option_comparison.py +3 -165
  71. package/runtime/python/okstra_ctl/option_votes.py +3 -191
  72. package/runtime/python/okstra_ctl/paths.py +8 -6
  73. package/runtime/python/okstra_ctl/phases/catalog.py +56 -12
  74. package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
  75. package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
  76. package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
  77. package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
  78. package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
  79. package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
  80. package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
  81. package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
  82. package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
  83. package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
  84. package/runtime/python/okstra_ctl/phases/error_analysis/spec.md +118 -0
  85. package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
  86. package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
  87. package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
  88. package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
  89. package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
  90. package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
  91. package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
  92. package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
  93. package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
  94. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -2
  95. package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
  96. package/runtime/python/okstra_ctl/phases/final_verification/spec.md +1 -1
  97. package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
  98. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
  99. package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
  100. package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
  101. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
  102. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
  103. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +18 -7
  104. package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
  105. package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
  106. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
  107. package/runtime/python/okstra_ctl/phases/implementation/spec.md +238 -0
  108. package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
  109. package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
  110. package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
  111. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
  112. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
  113. package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
  114. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
  115. package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +3 -3
  116. package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
  117. package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +83 -0
  118. package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
  119. package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
  120. package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
  121. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
  122. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
  123. package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
  124. package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
  125. package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
  126. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
  127. package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
  128. package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
  129. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
  130. package/runtime/python/okstra_ctl/phases/implementation_planning/spec.md +204 -0
  131. package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
  132. package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
  133. package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
  134. package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
  135. package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
  136. package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
  137. package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
  138. package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
  139. package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
  140. package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
  141. package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
  142. package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
  143. package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
  144. package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
  145. package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
  146. package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
  147. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
  148. package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
  149. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
  150. package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
  151. package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
  152. package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
  153. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +233 -0
  154. package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
  155. package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
  156. package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
  157. package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
  158. package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
  159. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +132 -0
  160. package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
  161. package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
  162. package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
  163. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
  164. package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +2 -2
  165. package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
  166. package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
  167. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
  168. package/runtime/python/okstra_ctl/plan_approval.py +70 -0
  169. package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
  170. package/runtime/python/okstra_ctl/process_group.py +118 -0
  171. package/runtime/python/okstra_ctl/profile_show.py +3 -3
  172. package/runtime/python/okstra_ctl/render.py +15 -4
  173. package/runtime/python/okstra_ctl/report_assembly.py +28 -92
  174. package/runtime/python/okstra_ctl/report_finalize.py +106 -2
  175. package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
  176. package/runtime/python/okstra_ctl/report_projections.py +1 -36
  177. package/runtime/python/okstra_ctl/report_routing.py +23 -0
  178. package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
  179. package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
  180. package/runtime/python/okstra_ctl/report_views.py +1 -1
  181. package/runtime/python/okstra_ctl/run.py +68 -350
  182. package/runtime/python/okstra_ctl/run_artifact_prune.py +200 -0
  183. package/runtime/python/okstra_ctl/stage_map.py +13 -0
  184. package/runtime/python/okstra_ctl/team.py +108 -9
  185. package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
  186. package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
  187. package/runtime/python/okstra_ctl/wizard/api.py +18 -0
  188. package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
  189. package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
  190. package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
  191. package/runtime/python/okstra_ctl/wizard/steps_options.py +8 -0
  192. package/runtime/python/okstra_ctl/wizard/steps_plan.py +10 -263
  193. package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
  194. package/runtime/python/okstra_ctl/work_categories.py +1 -1
  195. package/runtime/python/okstra_ctl/worker_dispatch.py +44 -3
  196. package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
  197. package/runtime/python/okstra_ctl/worker_prompt_policy.py +19 -0
  198. package/runtime/python/okstra_ctl/worker_runner.py +21 -3
  199. package/runtime/python/okstra_ctl/workflow.py +26 -143
  200. package/runtime/python/okstra_ctl/write_policy.py +57 -7
  201. package/runtime/python/okstra_project/dirs.py +14 -0
  202. package/runtime/python/okstra_project/resolver.py +2 -1
  203. package/runtime/schemas/execution-manifest-v2.schema.json +2 -1
  204. package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
  205. package/runtime/skills/okstra-code-review/SKILL.md +70 -32
  206. package/runtime/skills/okstra-code-review/references/review-calibration.md +26 -6
  207. package/runtime/skills/okstra-run/SKILL.md +3 -3
  208. package/runtime/templates/manager/view.template.html +18 -1
  209. package/runtime/templates/reports/quick-input.template.md +1 -1
  210. package/runtime/templates/reports/task-brief.template.md +1 -1
  211. package/runtime/validators/validate-brief.py +2 -2
  212. package/runtime/validators/validate-run.py +299 -3940
  213. package/runtime/validators/validate_analysis_report.py +14 -126
  214. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
  215. package/runtime/python/okstra_ctl/technical_verification.py +0 -195
  216. /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
  217. /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
  218. /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
  219. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
  220. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
  221. /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
  222. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
  223. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
  224. /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
  225. /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
  226. /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
  227. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
  228. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
  229. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
  230. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
  231. /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
  232. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
  233. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
  234. /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
  235. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
  236. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
  237. /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
  238. /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
  239. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
  240. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
  241. /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
  242. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
  243. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
  244. /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
  245. /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
  246. /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
  247. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
  248. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
  249. /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
  250. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
  251. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
  252. /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
  253. /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
  254. /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
  255. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
  256. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
  257. /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
  258. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
  259. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.md +0 -0
@@ -0,0 +1,183 @@
1
+ """dispatch 배치 하나를 한 번의 호출로 materialize 하고, 원하면 jobs 파일까지 쓴다.
2
+
3
+ 항목마다 단일 `materialize` 호출과 같은 파서와 `_materialize` 를 거치므로, 항목은
4
+ 그 호출이 낼 오류와 똑같이 실패한다. 여기에는 반복과 jobs 넘기기만 있다.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ from ..invocation import AgentInvocationError
13
+ from ...dispatch_state import DispatchError
14
+ from ...json_boundary import JsonBoundaryError, load_owned_object
15
+ from ...worker_prompt_headers import WorkerPromptHeaderError
16
+ from .emit import _prepared_payload
17
+ from .inputs import AgentPromptCliError, _project_input, _project_root
18
+ from .jobs import check_jobs_target, generate_jobs
19
+ from .materialize import _materialize
20
+
21
+
22
+ _ENTRY_FAILURES = (
23
+ AgentPromptCliError, AgentInvocationError, DispatchError,
24
+ WorkerPromptHeaderError, OSError, ValueError,
25
+ )
26
+ _REQUIRED_FLAGS = ("invocation-id", "audience", "instruction", "prompt")
27
+ _STANDALONE_FLAGS = frozenset({
28
+ "host-runtime", "terminal-backend", "provider", "model-role", "model", "purpose",
29
+ })
30
+ _ENTRY_FLAGS = frozenset({
31
+ "invocation-id", "audience", "instruction", "prompt", "worker-id",
32
+ "dispatch-kind", "assignment-ref", "source-role-execution-ref", "result",
33
+ "corrections", "audit-source", "replace-undispatched",
34
+ })
35
+
36
+
37
+ def materialize_batch(
38
+ args: argparse.Namespace,
39
+ parser_factory: Callable[[], argparse.ArgumentParser],
40
+ ) -> dict[str, object]:
41
+ entries, dispatch_kind = _validated_batch(args, parser_factory)
42
+ prepared = _publish(entries)
43
+ jobs_path = write_jobs(args, prepared, dispatch_kind) if args.jobs_out else None
44
+ return {"ok": True, "invocations": prepared, "jobsPath": jobs_path}
45
+
46
+
47
+ def _validated_batch(
48
+ args: argparse.Namespace,
49
+ parser_factory: Callable[[], argparse.ArgumentParser],
50
+ ) -> tuple[list[argparse.Namespace], str]:
51
+ """배치 전체를 발행 전에 검사한다 — 어느 항목도 쓰기 전에 거부할 수 있는 것은 전부 여기서."""
52
+ if not args.run_manifest:
53
+ raise AgentPromptCliError("materialize --batch requires --run-manifest")
54
+ stray = sorted(
55
+ "--" + flag for flag in _ENTRY_FLAGS | _STANDALONE_FLAGS
56
+ if getattr(args, flag.replace("-", "_"))
57
+ )
58
+ if stray:
59
+ raise AgentPromptCliError(
60
+ "with --batch pass only --project-root, --run-manifest, --jobs-out "
61
+ "and --json; per-invocation flags belong in the batch entries: "
62
+ + ", ".join(stray)
63
+ )
64
+ project = _project_root(args.project_root)
65
+ entries = [
66
+ _entry_args(args, index, entry, parser_factory)
67
+ for index, entry in enumerate(_load_entries(project, args.batch), start=1)
68
+ ]
69
+ ids = [entry.invocation_id for entry in entries]
70
+ if len(set(ids)) != len(ids):
71
+ raise AgentPromptCliError("batch repeats an invocation id")
72
+ kinds = {entry.dispatch_kind for entry in entries}
73
+ if args.jobs_out and len(kinds) != 1:
74
+ raise AgentPromptCliError(
75
+ "--jobs-out needs one dispatch kind; this batch has "
76
+ + ", ".join(sorted(str(kind) for kind in kinds))
77
+ )
78
+ if args.jobs_out:
79
+ check_jobs_target(args.project_root, args.run_manifest, args.jobs_out)
80
+ return entries, next(iter(kinds))
81
+
82
+
83
+ def _publish(entries: list[argparse.Namespace]) -> list[dict[str, object]]:
84
+ prepared: list[dict[str, object]] = []
85
+ for index, entry in enumerate(entries, start=1):
86
+ try:
87
+ prepared.append(_prepared_payload(_materialize(entry), entry.audience))
88
+ except _ENTRY_FAILURES as exc:
89
+ raise AgentPromptCliError(
90
+ f"batch entry {index} ({entry.invocation_id}): {exc}; "
91
+ + _published_note(prepared)
92
+ ) from exc
93
+ return prepared
94
+
95
+
96
+ def write_jobs(
97
+ args: argparse.Namespace,
98
+ prepared: list[dict[str, object]],
99
+ dispatch_kind: str,
100
+ ) -> str:
101
+ """materialize 된 호출들을 그대로 `agent-prompt jobs` 에 넘긴다."""
102
+ try:
103
+ return str(generate_jobs(argparse.Namespace(
104
+ project_root=args.project_root,
105
+ run_manifest=args.run_manifest,
106
+ dispatch_kind=dispatch_kind,
107
+ metadata=[str(row["metadataPath"]) for row in prepared],
108
+ out=args.jobs_out,
109
+ ))["jobsPath"])
110
+ except _ENTRY_FAILURES as exc:
111
+ raise AgentPromptCliError(
112
+ f"jobs file not written: {exc}; " + _published_note(prepared)
113
+ ) from exc
114
+
115
+
116
+ def _published_note(prepared: list[dict[str, object]]) -> str:
117
+ if not prepared:
118
+ return "no entry was materialized"
119
+ # 발행된 항목은 발행된 채 남는다. 같은 배치를 다시 돌리면 새로 쓰지 않고
120
+ # 동일한 프롬프트·메타데이터·예약을 재사용한다.
121
+ return (
122
+ "already materialized and reused unchanged by an identical rerun: "
123
+ + ", ".join(str(row["invocationId"]) for row in prepared)
124
+ )
125
+
126
+
127
+ def _load_entries(project: Path, raw: str) -> list[object]:
128
+ path = _project_input(project, raw, "batch")
129
+ try:
130
+ payload = load_owned_object(path, artifact="agent prompt batch")
131
+ except JsonBoundaryError as exc:
132
+ raise AgentPromptCliError(f"batch file is invalid: {exc}") from exc
133
+ entries = payload.get("invocations")
134
+ if set(payload) != {"invocations"} or not isinstance(entries, list) or not entries:
135
+ raise AgentPromptCliError(
136
+ 'batch file must be {"invocations": [...]} with at least one entry'
137
+ )
138
+ return entries
139
+
140
+
141
+ def _entry_args(
142
+ args: argparse.Namespace,
143
+ index: int,
144
+ entry: object,
145
+ parser_factory: Callable[[], argparse.ArgumentParser],
146
+ ) -> argparse.Namespace:
147
+ if not isinstance(entry, dict):
148
+ raise AgentPromptCliError(f"batch entry {index} must be an object")
149
+ unknown = sorted(set(entry) - _ENTRY_FLAGS)
150
+ if unknown:
151
+ raise AgentPromptCliError(
152
+ f"batch entry {index} has keys outside the materialize flags: "
153
+ + ", ".join(unknown)
154
+ )
155
+ argv = [
156
+ "materialize",
157
+ "--project-root", str(args.project_root),
158
+ "--run-manifest", str(args.run_manifest),
159
+ ]
160
+ for key, value in entry.items():
161
+ if value is True and key == "replace-undispatched":
162
+ argv.append(f"--{key}")
163
+ elif isinstance(value, str) and key != "replace-undispatched":
164
+ argv.extend([f"--{key}", value])
165
+ else:
166
+ raise AgentPromptCliError(f"batch entry {index}: invalid value for {key}")
167
+ try:
168
+ parsed = parser_factory().parse_args(argv)
169
+ except SystemExit as exc:
170
+ raise AgentPromptCliError(
171
+ f"batch entry {index}: invalid materialize arguments"
172
+ ) from exc
173
+ require_invocation_flags(parsed, f"batch entry {index}")
174
+ return parsed
175
+
176
+
177
+ def require_invocation_flags(args: argparse.Namespace, label: str) -> None:
178
+ missing = [
179
+ "--" + flag for flag in _REQUIRED_FLAGS
180
+ if not getattr(args, flag.replace("-", "_"))
181
+ ]
182
+ if missing:
183
+ raise AgentPromptCliError(f"{label} requires: " + ", ".join(missing))
@@ -16,6 +16,8 @@ from ...error_log_write import record_runtime_failure
16
16
  from ...dispatch_state import BACKEND_CLI_WRAPPER, BACKEND_CMUX_PANE, DispatchError
17
17
  from .emit import _emit, _prepared_payload
18
18
  from .inputs import AgentPromptCliError
19
+ from .batch import materialize_batch, require_invocation_flags, write_jobs
20
+ from .jobs import check_jobs_target
19
21
  from .materialize import _apply_corrections, _check_corrections, _materialize
20
22
  from .jobs import generate_jobs
21
23
  from .results import (
@@ -31,7 +33,8 @@ from .results import (
31
33
 
32
34
 
33
35
  _CLI_EPILOG = r"""Usage:
34
- okstra agent-prompt materialize [options]
36
+ okstra agent-prompt materialize [options] [--jobs-out <path>]
37
+ okstra agent-prompt materialize --project-root <dir> --run-manifest <path> --batch <file> [--jobs-out <path>]
35
38
  okstra agent-prompt jobs --project-root <dir> --run-manifest <path> --dispatch-kind <kind> --metadata <path> [--metadata <path>] --out <path>
36
39
  okstra agent-prompt check-corrections --project-root <dir> --run-manifest <path> --corrections <file>
37
40
  okstra agent-prompt apply-corrections --project-root <dir> --run-manifest <path> --corrections <file>
@@ -63,6 +66,10 @@ def _parser() -> argparse.ArgumentParser:
63
66
  help="resolve the role, worker count and models an operation requires",
64
67
  )
65
68
  operation.add_argument("--operation", required=True)
69
+ operation.add_argument(
70
+ "--count", type=int, default=None,
71
+ help="slots to open, from 1 to the operation contract's count (default: the contract's count)",
72
+ )
66
73
  operation.add_argument("--json", action="store_true")
67
74
  jobs = commands.add_parser("jobs", help="generate a verified jobs file without dispatching")
68
75
  _common_paths(jobs)
@@ -77,10 +84,11 @@ def _parser() -> argparse.ArgumentParser:
77
84
  def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
78
85
  materialize = commands.add_parser("materialize")
79
86
  _common_paths(materialize)
80
- materialize.add_argument("--invocation-id", required=True)
81
- materialize.add_argument("--audience", required=True)
82
- materialize.add_argument("--instruction", required=True)
83
- materialize.add_argument("--prompt", required=True)
87
+ single_only = "required unless --batch is given"
88
+ materialize.add_argument("--invocation-id", help=single_only)
89
+ materialize.add_argument("--audience", help=single_only)
90
+ materialize.add_argument("--instruction", help=single_only)
91
+ materialize.add_argument("--prompt", help=single_only)
84
92
  # 두 갈래의 인자를 한 파서가 받는다. 어느 갈래의 것인지 help 에 적지 않으면
85
93
  # `--help` 가 조건 없이 나열한 인자를 run 갈래가 거절해 놀라게 된다.
86
94
  run_only = "run mode only (with --run-manifest)"
@@ -130,6 +138,20 @@ def _add_materialize_parser(commands: argparse._SubParsersAction) -> None:
130
138
  "reservations are append-only, so a rewrite needs a fresh "
131
139
  "invocation id and prompt path",
132
140
  )
141
+ materialize.add_argument(
142
+ "--batch",
143
+ help="run mode only: a JSON file {\"invocations\": [...]} whose "
144
+ "entries map the per-invocation flags above (without the leading "
145
+ "--) to values, one entry per worker of the batch; each entry is "
146
+ "materialized exactly as a single call would be",
147
+ )
148
+ materialize.add_argument(
149
+ "--jobs-out",
150
+ help="run mode only: also write the verified jobs file for "
151
+ "`okstra team dispatch --jobs-file` from the materialized "
152
+ "invocations, as `okstra agent-prompt jobs` would; every batch "
153
+ "entry must share one --dispatch-kind",
154
+ )
133
155
  materialize.add_argument("--json", action="store_true")
134
156
 
135
157
  check = commands.add_parser(
@@ -277,7 +299,7 @@ def _refreeze_contracts(args: argparse.Namespace) -> dict[str, object]:
277
299
  }
278
300
 
279
301
 
280
- def _resolve_operation(operation_id: str) -> dict[str, object]:
302
+ def _resolve_operation(operation_id: str, count: int | None) -> dict[str, object]:
281
303
  """이 작업이 열 칸과 각 칸의 모델. 기술은 작업 이름만 알면 된다."""
282
304
  from ...model_pool import ModelPool
283
305
  from ...operation_invocation import (
@@ -298,6 +320,7 @@ def _resolve_operation(operation_id: str) -> dict[str, object]:
298
320
  operation_id,
299
321
  contract_root=root,
300
322
  pool=ModelPool.from_registry(default_provider_registry()),
323
+ count=count,
301
324
  )
302
325
  except OperationPreparationError as exc:
303
326
  raise AgentPromptCliError(str(exc)) from exc
@@ -318,11 +341,38 @@ def _resolve_operation(operation_id: str) -> dict[str, object]:
318
341
  }
319
342
 
320
343
 
344
+ def _materialize_batch_command(args: argparse.Namespace) -> int:
345
+ payload = materialize_batch(args, _parser)
346
+ if args.json:
347
+ _emit(payload, True)
348
+ return 0
349
+ for row in payload["invocations"]:
350
+ print(row["promptPath"])
351
+ if payload["jobsPath"]:
352
+ print(payload["jobsPath"])
353
+ return 0
354
+
355
+
356
+ def _materialize_single_command(args: argparse.Namespace) -> int:
357
+ require_invocation_flags(args, "materialize")
358
+ if args.jobs_out:
359
+ if not args.run_manifest:
360
+ raise AgentPromptCliError("--jobs-out requires --run-manifest")
361
+ check_jobs_target(args.project_root, args.run_manifest, args.jobs_out)
362
+ payload = _prepared_payload(_materialize(args), args.audience)
363
+ if args.jobs_out:
364
+ payload["jobsPath"] = write_jobs(args, [payload], args.dispatch_kind)
365
+ _emit(payload, args.json)
366
+ if args.jobs_out and not args.json:
367
+ print(payload["jobsPath"])
368
+ return 0
369
+
370
+
321
371
  def main(argv: list[str] | None = None) -> int:
322
372
  try:
323
373
  args = _parser().parse_args(argv)
324
374
  if args.command == "resolve-operation":
325
- payload = _resolve_operation(args.operation)
375
+ payload = _resolve_operation(args.operation, args.count)
326
376
  if args.json:
327
377
  _emit(payload, True)
328
378
  else:
@@ -346,9 +396,9 @@ def main(argv: list[str] | None = None) -> int:
346
396
  print(payload["jobsPath"])
347
397
  return 0
348
398
  if args.command == "materialize":
349
- prepared = _materialize(args)
350
- _emit(_prepared_payload(prepared, args.audience), args.json)
351
- return 0
399
+ if args.batch:
400
+ return _materialize_batch_command(args)
401
+ return _materialize_single_command(args)
352
402
  if args.command == "check-corrections":
353
403
  payload = _check_corrections(args)
354
404
  if args.json:
@@ -25,7 +25,7 @@ from ..activity import (
25
25
  )
26
26
  from ...final_report_schema import load_schema_version
27
27
  from ...json_boundary import load_owned_object
28
- from ...implementation_options import validate_implementation_option_selection
28
+ from ...phases.implementation_option_selection.validation import validate_implementation_option_selection
29
29
  from ...report_contract import TASK_TYPE_DATA_PROPERTY
30
30
  from ...report_assembly import validate_plan_draft
31
31
  from ...report_corrections import (
@@ -19,13 +19,30 @@ from .inputs import (
19
19
  )
20
20
 
21
21
 
22
- def generate_jobs(args: argparse.Namespace) -> dict[str, object]:
23
- project = _project_root(args.project_root)
24
- manifest_path = _project_input(project, args.run_manifest, "run manifest")
22
+ def check_jobs_target(project_root: str, run_manifest: str, out: str) -> None:
23
+ """jobs 파일을 쓸 수 있는 run 인지 — `materialize --jobs-out` 이 발행 전에 부른다.
24
+
25
+ 발행 뒤에 이 검사에서 거부되면 프롬프트만 발행된 채 남는다.
26
+ """
27
+ _jobs_target(project_root, run_manifest, out)
28
+
29
+
30
+ def _jobs_target(
31
+ project_root: str, run_manifest: str, out: str,
32
+ ) -> tuple[Path, Path, dict, Path]:
33
+ project = _project_root(project_root)
34
+ manifest_path = _project_input(project, run_manifest, "run manifest")
25
35
  manifest = _read_json_object(manifest_path, "run manifest")
26
36
  if manifest.get("executionIdentityVersion") != 2:
27
37
  raise AgentPromptCliError("agent-prompt jobs requires a v2 run manifest")
28
- output = _authorized_path(project, args.out, [manifest["runDirectoryPath"]], "jobs", must_exist=False)
38
+ output = _authorized_path(project, out, [manifest["runDirectoryPath"]], "jobs", must_exist=False)
39
+ return project, manifest_path, manifest, output
40
+
41
+
42
+ def generate_jobs(args: argparse.Namespace) -> dict[str, object]:
43
+ project, manifest_path, manifest, output = _jobs_target(
44
+ args.project_root, args.run_manifest, args.out,
45
+ )
29
46
  paths = [_project_input(project, value, "metadata") for value in args.metadata]
30
47
  rows = [_metadata_job(project, manifest_path, manifest, path, args.dispatch_kind) for path in paths]
31
48
  refs = [(row["invocationRef"], row["attempt"]) for row in rows]
@@ -34,7 +34,11 @@ from ...assignment_environment import load_assignment_context
34
34
  from ...assignment_resolver import AssignmentContext, resolve_dispatch_assignment
35
35
  from ...path_hints import hydrate_active_run_context
36
36
  from ...worker_prompt_headers import worker_prompt_headers
37
- from ...worker_prompt_contract import complete_reverify_instruction, validate_reverify_prompt
37
+ from ...worker_prompt_contract import (
38
+ complete_reverify_instruction,
39
+ validate_plan_verify_dispatch_identity,
40
+ validate_reverify_prompt,
41
+ )
38
42
  from ...worker_prompt_policy import (
39
43
  critic_assignment_ref,
40
44
  is_plan_critic_verification,
@@ -340,6 +344,11 @@ def _materialize_run(
340
344
  active_context=active_context,
341
345
  ))
342
346
  body = instruction_path.read_text(encoding="utf-8")
347
+ identity_errors = validate_plan_verify_dispatch_identity(
348
+ body, dispatch_kind=args.dispatch_kind, result_name=result_path.name,
349
+ )
350
+ if identity_errors:
351
+ raise AgentPromptCliError("; ".join(identity_errors))
343
352
  is_reverify = is_verification_dispatch_kind(args.dispatch_kind)
344
353
  if is_reverify:
345
354
  body = _complete_run_reverify_body(args, manifest, active_context, assignment, body, instruction_path)
@@ -73,7 +73,6 @@ _EVIDENCE_RELATION_BY_TYPES = {
73
73
  _REPORT_NAME_RE = re.compile(
74
74
  r"^final-report-.+-(?P<run_seq>[^-]+)\.data\.json$"
75
75
  )
76
- _FEATURE_ID_RE = re.compile(r"PF-\d{3}")
77
76
  _FULL_COMMIT_RE = re.compile(r"[0-9a-f]{40}")
78
77
  _RUN_SEQ_RE = re.compile(r"[0-9]{3}")
79
78
 
@@ -347,41 +346,3 @@ def resolve_evidence_inputs(
347
346
  freshness="exact" if candidate.source_commit == current_commit else "stale",
348
347
  ))
349
348
  return tuple(resolved)
350
-
351
-
352
- def resolve_analysis_target(
353
- raw_target: str,
354
- evidence_inputs: Sequence[ResolvedEvidenceInput],
355
- candidates: Mapping[Path, AnalysisReportCandidate],
356
- ) -> dict[str, object]:
357
- """Normalize a feature-index identifier or a non-empty free-text target."""
358
- target = raw_target.strip()
359
- if not target:
360
- raise AnalysisInputError("analysis target must not be empty")
361
- if not _FEATURE_ID_RE.fullmatch(target):
362
- return {"inputMode": "free-text", "requestedValue": target}
363
- matches: list[dict[object, object]] = []
364
- for evidence in evidence_inputs:
365
- if evidence.relation != "project-context":
366
- continue
367
- candidate = candidates.get(evidence.report_path)
368
- if candidate is None:
369
- candidate = next(
370
- (value for path, value in candidates.items() if path.resolve() == evidence.report_path),
371
- None,
372
- )
373
- if candidate is None:
374
- continue
375
- matches.extend(
376
- feature for feature in candidate.feature_index
377
- if isinstance(feature, dict) and feature.get("id") == target
378
- )
379
- if len(matches) != 1:
380
- raise AnalysisInputError(
381
- f"exactly one project-context feature must match {target}"
382
- )
383
- return {
384
- "inputMode": "feature-index",
385
- "requestedValue": target,
386
- "feature": dict(matches[0]),
387
- }
@@ -0,0 +1,31 @@
1
+ """분석 증거와 단계 내용 검증이 공유하는 프로젝트 상대 경로 판독."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import PurePosixPath
5
+
6
+
7
+ def _normalized_project_path(path: object) -> PurePosixPath | None:
8
+ if not isinstance(path, str):
9
+ return None
10
+ raw = path.strip().rstrip("/")
11
+ if not raw or raw == "." or raw.startswith("/"):
12
+ return None
13
+ if any(part in {"", ".", ".."} for part in raw.split("/")):
14
+ return None
15
+ normalized = PurePosixPath(raw)
16
+ if normalized.is_absolute():
17
+ return None
18
+ return normalized
19
+
20
+
21
+ def _path_is_included(path: object, included_paths: list[object]) -> bool:
22
+ candidate = _normalized_project_path(path)
23
+ if candidate is None:
24
+ return False
25
+ for raw in included_paths:
26
+ included = _normalized_project_path(raw)
27
+ if included is not None and (
28
+ candidate == included or included in candidate.parents
29
+ ):
30
+ return True
31
+ return False
@@ -222,7 +222,7 @@ def open_decision(
222
222
  clarification_id: str, ticket_id: str, statement: str, expected_form: str,
223
223
  classification: str, origin: str, user_confirmation: str,
224
224
  unblock_condition: str, recommended_disposition: str,
225
- options: Sequence[DecisionOption],
225
+ options: Sequence[DecisionOption], supersedes: Sequence[str] = (),
226
226
  ) -> None:
227
227
  row = _decision_row(
228
228
  clarification_id=clarification_id, ticket_id=ticket_id,
@@ -237,10 +237,30 @@ def open_decision(
237
237
  raise ApprovalDecisionError("activeClarifications must be an array")
238
238
  if any(item.get("id") == clarification_id for item in active if isinstance(item, Mapping)):
239
239
  raise ApprovalDecisionError(f"duplicate active clarification: {clarification_id}")
240
+ if supersedes:
241
+ row["supersedes"] = _superseded_carried_ids(ledger, supersedes)
240
242
  active.append(row)
241
243
  write_json_atomic(ledger_path, ledger)
242
244
 
243
245
 
246
+ def _superseded_carried_ids(
247
+ ledger: Mapping[str, Any], supersedes: Sequence[str],
248
+ ) -> list[str]:
249
+ carried = ledger.get("carriedDecisions")
250
+ carried_ids = {
251
+ entry["decision"].get("id")
252
+ for entry in (carried if isinstance(carried, list) else [])
253
+ if isinstance(entry, Mapping) and isinstance(entry.get("decision"), Mapping)
254
+ }
255
+ ids = list(dict.fromkeys(_required(value, "supersedes") for value in supersedes))
256
+ unknown = [value for value in ids if value not in carried_ids]
257
+ if unknown:
258
+ raise ApprovalDecisionError(
259
+ f"supersedes must name carried decisions of this ledger: {unknown}"
260
+ )
261
+ return ids
262
+
263
+
244
264
  def resolve_decision(
245
265
  ledger_path: Path, clarification_id: str, *, disposition: str,
246
266
  user_text: str, user_response_ref: str, check_refs: Sequence[str],
@@ -412,10 +432,20 @@ def _add_open_arguments(parser: argparse.ArgumentParser) -> None:
412
432
  )
413
433
  for flag in repeated:
414
434
  parser.add_argument(f"--option-{flag}", action="append", default=[])
435
+ parser.add_argument(
436
+ "--supersedes",
437
+ action="append",
438
+ default=[],
439
+ help=(
440
+ "a carried decision this question replaces; once this row is "
441
+ "resolved, report assembly publishes that carried row as obsolete. "
442
+ "Repeat for several ids."
443
+ ),
444
+ )
415
445
 
416
446
 
417
447
  _CLI_EPILOG = r"""Usage:
418
- okstra approval-decision open --ledger <path> [decision fields]
448
+ okstra approval-decision open --ledger <path> [decision fields] [--supersedes <C-NNN> ...]
419
449
  okstra approval-decision resolve --ledger <path> --clarification-id <C-NNN> [resolution fields]
420
450
  okstra approval-decision carry --ledger <path> --from-responses <clarification-response.md> --clarification-id <C-NNN> [--clarification-id <C-NNN> ...]
421
451
  okstra approval-decision carry --ledger <path> --source-ledger <path> --source-run-ref <ref> --clarification-id <C-NNN>
@@ -0,0 +1,19 @@
1
+ """Ancestor-based runtime asset lookup independent of run and phase state."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Callable, Sequence
5
+ from pathlib import Path
6
+
7
+
8
+ def walk_to_asset_root(
9
+ relative: Sequence[str],
10
+ *,
11
+ start: Path,
12
+ is_present: Callable[[Path], bool] = Path.is_file,
13
+ ) -> Path | None:
14
+ """Return the nearest ancestor carrying the asset without home overrides."""
15
+ here = Path(start).resolve()
16
+ for parent in [here, *here.parents]:
17
+ if is_present(parent.joinpath(*relative)):
18
+ return parent
19
+ return None
@@ -327,6 +327,14 @@ def resolve_model_selection(
327
327
  raw_model: str,
328
328
  ) -> ResolvedModel:
329
329
  """Resolve one explicit or provider-default catalog model without binding."""
330
+ if raw_model and "/" in raw_model:
331
+ # `agent-prompt resolve-operation` prints slots as model refs.
332
+ selected = pool.resolve(raw_model)
333
+ if selected.provider_id != provider.strip().lower():
334
+ raise AssignmentResolutionError(
335
+ f"model {raw_model!r} does not belong to provider {provider!r}"
336
+ )
337
+ return selected
330
338
  if raw_model:
331
339
  return pool.resolve_alias(provider, raw_model)
332
340
  selected = pool.default_candidate(provider, role)
@@ -239,6 +239,13 @@ _BLOCKING: tuple[tuple[str, str], ...] = (
239
239
  "같은 이유 — 검증 범위가 열거값 밖이거나 단일 stage 검증이 전체 "
240
240
  "release-handoff 를 요구한다.",
241
241
  ),
242
+ (
243
+ "for release but `runs/implementation-planning/consumers.jsonl` carries no",
244
+ "통과 판정이 stage 에 `verified` 행을 남기지 않았다 — handoff.py 의 "
245
+ "compute_eligibility 가 그 행으로만 PR 대상을 고르므로 통과한 stage 가 "
246
+ "release-handoff 에서 영영 제외된다. report-finalize 의 record-verified "
247
+ "단계가 이 검증 직전에 행을 쓰므로 정상 run 은 항상 만족한다.",
248
+ ),
242
249
  )
243
250
 
244
251
  # 2026-08-28 에 제거한 조각과 근거 (감사 보고서 §2):