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
@@ -5,10 +5,8 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import json
7
7
  import os
8
- import posixpath
9
8
  import re
10
9
  import stat
11
- from collections import Counter
12
10
  from dataclasses import dataclass
13
11
  from pathlib import Path
14
12
  from typing import Any, Mapping
@@ -19,7 +17,6 @@ from .final_report_paths import (
19
17
  final_report_markdown_path,
20
18
  is_full_reading_copy_path,
21
19
  )
22
- from .exact_coverage import ExactCoverageError, calculate_plan_exact_coverage
23
20
  from .final_report_schema import (
24
21
  SchemaError,
25
22
  load_schema_for_data,
@@ -35,7 +32,6 @@ from .report_views import (
35
32
  normalize_direction_selection_identity,
36
33
  )
37
34
  from .qa_commands import find_denied_tokens
38
- from .scope_provenance import brief_end_state_id_sequence
39
35
  from .user_response import UserResponseError, parse_direction_selection
40
36
 
41
37
 
@@ -436,28 +432,6 @@ def write_selected_direction_snapshot(
436
432
  return destination
437
433
 
438
434
 
439
- _PLAN_EXECUTION_FIELDS = (
440
- "directionRealization",
441
- "stageMap",
442
- "stages",
443
- "designPreparation",
444
- "dependencyMigrationRisk",
445
- "validationChecklist",
446
- "rollbackStrategy",
447
- "requirementCoverage",
448
- "coverageSummary",
449
- "variationPointAnalysis",
450
- "planBodyVerification",
451
- "stepwiseExecution",
452
- "supersessionLedger",
453
- "crossProjectDependencies",
454
- "decisionDrafts",
455
- "skippedAdrCandidates",
456
- "incrementalDecision",
457
- "userNarrative",
458
- )
459
-
460
-
461
435
  def load_selected_direction_snapshot(
462
436
  snapshot_path: Path,
463
437
  ) -> tuple[SelectedDirectionSnapshot | None, list[str]]:
@@ -522,341 +496,6 @@ def validate_selected_direction_ref(
522
496
  return failures
523
497
 
524
498
 
525
- def _direction_realization_errors(
526
- realization: object, snapshot: SelectedDirectionSnapshot
527
- ) -> list[str]:
528
- if not isinstance(realization, Mapping):
529
- return ["directionRealization is missing from a plan-ready result"]
530
- direction = snapshot.data.get("direction")
531
- if not isinstance(direction, Mapping):
532
- return ["selected direction snapshot is missing its direction object"]
533
- comparisons = {
534
- "coreMechanism": direction.get("coreMechanism"),
535
- "architectureBoundaries": direction.get("architectureBoundaries"),
536
- "planningInvariants": snapshot.data.get("planningInvariants"),
537
- "userConstraints": snapshot.data.get("userConstraints"),
538
- }
539
- return [
540
- f"directionRealization.{field} must exactly preserve the selected-direction snapshot"
541
- for field, expected in comparisons.items()
542
- if realization.get(field) != expected
543
- ]
544
-
545
-
546
- def _plan_reference_sets(
547
- planning: Mapping[str, Any],
548
- ) -> tuple[set[int], set[str], set[str], tuple[str, ...]]:
549
- stages = [row for row in planning.get("stages") or () if isinstance(row, Mapping)]
550
- stage_ids = {
551
- row["stage"] for row in stages if isinstance(row.get("stage"), int)
552
- }
553
- step_ids = {
554
- f"{stage['stage']}.{step['step']}"
555
- for stage in stages
556
- if isinstance(stage.get("stage"), int)
557
- for step in stage.get("stepwiseExecution") or ()
558
- if isinstance(step, Mapping) and isinstance(step.get("step"), int)
559
- }
560
- validation_ids = {
561
- str(row.get("id"))
562
- for row in planning.get("validationChecklist") or ()
563
- if isinstance(row, Mapping) and row.get("id")
564
- }
565
- realization = planning.get("directionRealization")
566
- file_rows = (
567
- realization.get("fileStructure") or ()
568
- if isinstance(realization, Mapping)
569
- else ()
570
- )
571
- file_paths = tuple(
572
- str(row.get("path"))
573
- for row in file_rows
574
- if isinstance(row, Mapping) and row.get("path")
575
- )
576
- return stage_ids, step_ids, validation_ids, file_paths
577
-
578
-
579
- def _duplicate_values(values: list[object]) -> tuple[object, ...]:
580
- return tuple(value for value, count in Counter(values).items() if count > 1)
581
-
582
-
583
- def _normalized_plan_file_path(value: object) -> str:
584
- return posixpath.normpath(str(value).replace("\\", "/"))
585
-
586
-
587
- def _plan_structure_errors(planning: Mapping[str, Any]) -> list[str]:
588
- failures: list[str] = []
589
- stage_map = [
590
- row for row in planning.get("stageMap") or () if isinstance(row, Mapping)
591
- ]
592
- stages = [
593
- row for row in planning.get("stages") or () if isinstance(row, Mapping)
594
- ]
595
- stage_map_numbers = [
596
- row["stage"] for row in stage_map if isinstance(row.get("stage"), int)
597
- ]
598
- stage_numbers = [
599
- row["stage"] for row in stages if isinstance(row.get("stage"), int)
600
- ]
601
- duplicate_stage_map = set(_duplicate_values(stage_map_numbers))
602
- duplicate_stages = set(_duplicate_values(stage_numbers))
603
- failures.extend(
604
- f"stageMap contains duplicate stage {number}"
605
- for number in sorted(duplicate_stage_map)
606
- )
607
- failures.extend(
608
- f"stages contains duplicate stage {number}"
609
- for number in sorted(duplicate_stages)
610
- )
611
-
612
- stage_map_set = set(stage_map_numbers)
613
- stage_set = set(stage_numbers)
614
- failures.extend(
615
- f"stageMap is missing stages stage {number}"
616
- for number in sorted(stage_set - stage_map_set)
617
- )
618
- failures.extend(
619
- f"stages is missing stageMap stage {number}"
620
- for number in sorted(stage_map_set - stage_set)
621
- )
622
- unique_stage_map = {
623
- row["stage"]: row
624
- for row in stage_map
625
- if isinstance(row.get("stage"), int)
626
- and row["stage"] not in duplicate_stage_map
627
- }
628
- unique_stages = {
629
- row["stage"]: row
630
- for row in stages
631
- if isinstance(row.get("stage"), int) and row["stage"] not in duplicate_stages
632
- }
633
- failures.extend(
634
- f"stageMap and stages stage {number} title must match"
635
- for number in sorted(unique_stage_map.keys() & unique_stages.keys())
636
- if unique_stage_map[number].get("title")
637
- != unique_stages[number].get("title")
638
- )
639
-
640
- for stage in stages:
641
- stage_number = stage.get("stage")
642
- step_numbers = [
643
- step["step"]
644
- for step in stage.get("stepwiseExecution") or ()
645
- if isinstance(step, Mapping) and isinstance(step.get("step"), int)
646
- ]
647
- failures.extend(
648
- f"stages stage {stage_number} step {step_number} is duplicate"
649
- for step_number in sorted(_duplicate_values(step_numbers))
650
- )
651
-
652
- validation_ids = [
653
- str(row["id"])
654
- for row in planning.get("validationChecklist") or ()
655
- if isinstance(row, Mapping) and row.get("id")
656
- ]
657
- failures.extend(
658
- f"validationChecklist id {validation_id} is duplicate"
659
- for validation_id in sorted(_duplicate_values(validation_ids))
660
- )
661
-
662
- realization = planning.get("directionRealization")
663
- file_rows = (
664
- [row for row in realization.get("fileStructure") or () if isinstance(row, Mapping)]
665
- if isinstance(realization, Mapping)
666
- else []
667
- )
668
- file_ids = [str(row["id"]) for row in file_rows if row.get("id")]
669
- failures.extend(
670
- f"directionRealization.fileStructure id {file_id} is duplicate"
671
- for file_id in sorted(_duplicate_values(file_ids))
672
- )
673
- normalized_paths = [
674
- _normalized_plan_file_path(row["path"])
675
- for row in file_rows
676
- if row.get("path")
677
- ]
678
- failures.extend(
679
- f"directionRealization.fileStructure path {path} is duplicate after normalization"
680
- for path in sorted(_duplicate_values(normalized_paths))
681
- )
682
- return failures
683
-
684
-
685
- def _coverage_reference_errors(
686
- rows: list[Mapping[str, Any]],
687
- stage_ids: set[int],
688
- step_ids: set[str],
689
- validation_ids: set[str],
690
- file_paths: tuple[str, ...],
691
- dependencies: list[Mapping[str, Any]],
692
- ) -> list[str]:
693
- failures: list[str] = []
694
- reference_sets = {
695
- "stageRefs": stage_ids,
696
- "stepRefs": step_ids,
697
- "validationRefs": validation_ids,
698
- "fileRefs": set(file_paths),
699
- "crossProjectDependencyRefs": {row.get("id") for row in dependencies},
700
- }
701
- for row in rows:
702
- requirement_id = str(row.get("originalRequirementId") or "<missing>")
703
- external_refs = row.get("crossProjectDependencyRefs") or ()
704
- if row.get("status") == "externally-tracked":
705
- if not external_refs:
706
- failures.append(
707
- f"requirementCoverage {requirement_id} requires crossProjectDependencyRefs"
708
- )
709
- for ref in external_refs:
710
- matches = [item for item in dependencies if item.get("id") == ref]
711
- if (
712
- len(matches) != 1
713
- or any(
714
- not isinstance(matches[0].get(field), str)
715
- or not matches[0][field].strip()
716
- for field in (
717
- "project",
718
- "requiredWork",
719
- "verificationSignal",
720
- "linkedWork",
721
- "howToStart",
722
- )
723
- )
724
- or matches[0].get("direction")
725
- not in {"upstream-precondition", "downstream-carry"}
726
- ):
727
- failures.append(
728
- f"requirementCoverage {requirement_id} crossProjectDependencyRefs must resolve to one complete dependency: {ref}"
729
- )
730
- elif external_refs:
731
- failures.append(
732
- f"requirementCoverage {requirement_id} external references require externally-tracked status"
733
- )
734
- elif any(
735
- not row.get(field)
736
- for field in ("stageRefs", "stepRefs", "validationRefs", "fileRefs")
737
- ):
738
- failures.append(
739
- f"requirementCoverage {requirement_id} requires local stage, step, validation and file references"
740
- )
741
- for field, valid_values in reference_sets.items():
742
- dangling = [
743
- value for value in row.get(field) or () if value not in valid_values
744
- ]
745
- if dangling:
746
- failures.append(
747
- f"requirementCoverage {requirement_id} {field} contains "
748
- f"dangling reference(s): {dangling}"
749
- )
750
- return failures
751
-
752
-
753
- def _requirement_ids_for_scope(
754
- rows: list[Mapping[str, Any]], field: str, value: object
755
- ) -> tuple[str, ...]:
756
- return tuple(
757
- str(row.get("originalRequirementId"))
758
- for row in rows
759
- if value in (row.get(field) or ()) and row.get("originalRequirementId")
760
- )
761
-
762
-
763
- def _coverage_summary_errors(
764
- planning: Mapping[str, Any], original_ids: tuple[str, ...]
765
- ) -> list[str]:
766
- rows = [
767
- row
768
- for row in planning.get("requirementCoverage") or ()
769
- if isinstance(row, Mapping)
770
- ]
771
- row_ids = tuple(str(row.get("originalRequirementId") or "") for row in rows)
772
- failures: list[str] = []
773
- if not original_ids:
774
- return ["original requirement denominator from taskBriefPath is empty"]
775
- if row_ids != original_ids:
776
- failures.append(
777
- "original requirement rows must match the taskBriefPath requirement "
778
- "sequence exactly once"
779
- )
780
- return failures
781
- structure_failures = _plan_structure_errors(planning)
782
- failures.extend(structure_failures)
783
- stage_ids, step_ids, validation_ids, file_paths = _plan_reference_sets(planning)
784
- failures.extend(
785
- _coverage_reference_errors(
786
- rows,
787
- stage_ids,
788
- step_ids,
789
- validation_ids,
790
- file_paths,
791
- [
792
- row
793
- for row in planning.get("crossProjectDependencies") or ()
794
- if isinstance(row, Mapping)
795
- ],
796
- )
797
- )
798
- if failures:
799
- return failures
800
- # 검증된 외부 작업 연결은 계획 범위에만 포함하며 실제 완료 상태는 바꾸지 않는다.
801
- statuses = {
802
- row_id: "covered"
803
- if row.get("status") == "externally-tracked"
804
- else str(row.get("status") or "")
805
- for row_id, row in zip(row_ids, rows)
806
- }
807
- stage_requirements = {
808
- stage: _requirement_ids_for_scope(rows, "stageRefs", stage)
809
- for stage in sorted(stage_ids)
810
- }
811
- file_requirements = {
812
- path: _requirement_ids_for_scope(rows, "fileRefs", path) for path in file_paths
813
- }
814
- try:
815
- result, unmapped_stages, unmapped_files = calculate_plan_exact_coverage(
816
- original_ids, statuses, stage_requirements, file_requirements
817
- )
818
- except ExactCoverageError as exc:
819
- return [*failures, f"plan-ready requires exact 100% coverage: {exc}"]
820
- failures.extend(
821
- f"unmapped stage {stage} has no original requirement"
822
- for stage in unmapped_stages
823
- )
824
- failures.extend(
825
- f"unmapped file change {path} has no original requirement"
826
- for path in unmapped_files
827
- )
828
- failures.extend(
829
- _declared_coverage_summary_errors(
830
- planning.get("coverageSummary"), result, unmapped_stages, unmapped_files
831
- )
832
- )
833
- if result.verdict != "exact":
834
- failures.append("plan-ready requires exact 100% coverage")
835
- return failures
836
-
837
-
838
- def _declared_coverage_summary_errors(
839
- summary: object,
840
- result: Any,
841
- unmapped_stages: tuple[int, ...],
842
- unmapped_files: tuple[str, ...],
843
- ) -> list[str]:
844
- if not isinstance(summary, Mapping):
845
- return ["coverageSummary is missing from a plan-ready result"]
846
- expected = {
847
- "coveragePercent": result.coverage_percent,
848
- "scopePrecisionPercent": result.scope_precision_percent,
849
- "coverageVerdict": result.verdict,
850
- "unmappedStages": list(unmapped_stages),
851
- "unmappedFileChanges": list(unmapped_files),
852
- }
853
- return [
854
- f"coverageSummary.{field} does not match recalculated plan coverage"
855
- for field, value in expected.items()
856
- if summary.get(field) != value
857
- ]
858
-
859
-
860
499
  # stage 를 게이트하는 검증 행이 implementation run 안에서 실행 가능함을 강제하는
861
500
  # 마커. 실측 실패값(dev-10341 VC-013/VC-014)에서 왔다: 수동 워크스루·배포 전제
862
501
  # 문구가 stageRefs 를 단 채 실리면, implementation run 은 배포 금지·자격증명
@@ -910,119 +549,3 @@ def stage_validation_executability_errors(
910
549
  "or final-verification's user-owned external QA"
911
550
  )
912
551
  return failures
913
-
914
-
915
- def _is_config_only_path(path: str) -> bool:
916
- name = posixpath.basename(posixpath.normpath(str(path).replace("\\", "/")))
917
- if name == "Dockerfile" or name.startswith("Dockerfile."):
918
- return True
919
- if name.startswith("docker-compose") or name.startswith(".env"):
920
- return True
921
- return name.endswith((".yml", ".yaml"))
922
-
923
-
924
- def _micro_stage_fold_errors(planning: Mapping[str, Any]) -> list[str]:
925
- """설정 파일 전용 종속 stage 는 독립 stage 가 아니다 — 커밋으로만 나눈다.
926
-
927
- 실측(dev-10341): 다른 stage 가 도입한 플래그를 Dockerfile/compose 2줄,
928
- workflow 2줄로 전파하는 작업이 stage 2·3 을 각각 차지해 run 2회(회당
929
- 30분+)와 검증 차단 1회를 낳았다. 값 전파는 도입 stage 의 스텝이고,
930
- 분리가 필요하면 커밋을 나눈다. 첫 stage(의존 없음)는 예외다 — 설정만
931
- 바꾸는 태스크 자체는 정당하다.
932
- """
933
- dependent: dict[int, bool] = {}
934
- for row in planning.get("stageMap") or ():
935
- if isinstance(row, Mapping) and isinstance(row.get("stage"), int):
936
- raw = str(row.get("dependsOn") or "").strip().lower()
937
- dependent[row["stage"]] = raw not in ("", "(none)", "none")
938
- failures: list[str] = []
939
- for stage in planning.get("stages") or ():
940
- if not isinstance(stage, Mapping):
941
- continue
942
- number = stage.get("stage")
943
- if not isinstance(number, int) or not dependent.get(number):
944
- continue
945
- paths = [
946
- str(path)
947
- for step in stage.get("stepwiseExecution") or ()
948
- if isinstance(step, Mapping)
949
- for path in step.get("plannedPaths") or ()
950
- ]
951
- if paths and all(_is_config_only_path(path) for path in paths):
952
- failures.append(
953
- f"stage {number} is a config-only propagation stage "
954
- f"({sorted(set(paths))}); fold it into the stage that introduces "
955
- "the value and split the work by commits, not stages"
956
- )
957
- return failures
958
-
959
-
960
- def _direction_invalidation_errors(planning: Mapping[str, Any]) -> list[str]:
961
- failures: list[str] = []
962
- invalidation = planning.get("directionInvalidation")
963
- if not isinstance(invalidation, Mapping):
964
- failures.append("direction-invalidated requires directionInvalidation evidence")
965
- else:
966
- for field in ("reasons", "codeEvidence"):
967
- values = invalidation.get(field)
968
- if not isinstance(values, list) or not values or any(
969
- not isinstance(value, str) or not value.strip() for value in values
970
- ):
971
- failures.append(f"directionInvalidation.{field} must contain evidence")
972
- if planning.get("routing") != "implementation-option-selection":
973
- failures.append(
974
- "direction-invalidated routing must be implementation-option-selection"
975
- )
976
- for field in _PLAN_EXECUTION_FIELDS:
977
- if field in planning:
978
- failures.append(
979
- f"direction-invalidated must not contain execution field {field}"
980
- )
981
- return failures
982
-
983
-
984
- def validate_selected_direction_plan(
985
- data: Mapping[str, Any], brief_path: Path, snapshot_path: Path
986
- ) -> list[str]:
987
- """Validate selected-direction planning semantics against independent inputs."""
988
- planning = data.get("implementationPlanning")
989
- if not isinstance(planning, Mapping):
990
- return ["implementationPlanning is missing"]
991
- if planning.get("planningContract") != "selected-direction":
992
- return ["implementationPlanning.planningContract must be selected-direction"]
993
- snapshot, failures = load_selected_direction_snapshot(snapshot_path)
994
- if snapshot is None:
995
- return failures
996
- header = data.get("header")
997
- plan_task_key = header.get("taskKey") if isinstance(header, Mapping) else None
998
- snapshot_task_key = snapshot.data.get("taskKey")
999
- if not isinstance(plan_task_key, str) or not plan_task_key:
1000
- failures.append("plan header.taskKey must be a non-empty string")
1001
- if not isinstance(snapshot_task_key, str) or not snapshot_task_key:
1002
- failures.append("selected direction snapshot taskKey must be a non-empty string")
1003
- elif isinstance(plan_task_key, str) and snapshot_task_key != plan_task_key:
1004
- failures.append(
1005
- "selected direction snapshot taskKey must exactly match plan header.taskKey"
1006
- )
1007
- ref = planning.get("selectedDirectionRef")
1008
- if not isinstance(ref, Mapping):
1009
- failures.append("selectedDirectionRef is missing")
1010
- else:
1011
- failures.extend(validate_selected_direction_ref(ref, snapshot))
1012
- outcome = planning.get("outcome")
1013
- if outcome == "direction-invalidated":
1014
- failures.extend(_direction_invalidation_errors(planning))
1015
- return failures
1016
- if outcome != "plan-ready":
1017
- return [*failures, f"selected-direction outcome is invalid: {outcome!r}"]
1018
- failures.extend(
1019
- _direction_realization_errors(planning.get("directionRealization"), snapshot)
1020
- )
1021
- failures.extend(
1022
- _coverage_summary_errors(
1023
- planning, brief_end_state_id_sequence(Path(brief_path))
1024
- )
1025
- )
1026
- failures.extend(stage_validation_executability_errors(planning))
1027
- failures.extend(_micro_stage_fold_errors(planning))
1028
- return failures
@@ -44,8 +44,10 @@ from .worker_prompt_headers import source_worktree_headers, worker_prompt_header
44
44
  from .worker_prompt_policy import (
45
45
  APPROVED_PLAN_HEADER,
46
46
  IMPLEMENTATION_STAGE_HEADER,
47
+ STAGE_QA_OWNER_HEADER,
47
48
  PromptPlan,
48
49
  resolve_prompt_plan_for_manifest,
50
+ stage_qa_owner,
49
51
  )
50
52
 
51
53
 
@@ -958,7 +960,14 @@ def _required_resource_path_candidates(
958
960
  paths = (profiles / "_implementation-verifier.md",)
959
961
  else:
960
962
  return ()
961
- return paths
963
+ from .phases.catalog import profile_sidecar
964
+
965
+ root = context.request.runtime_root.resolve()
966
+ return tuple(
967
+ profile_sidecar(root, str(path.relative_to(root)))
968
+ if path.is_relative_to(root) else path
969
+ for path in paths
970
+ )
962
971
 
963
972
 
964
973
  def _executor_profile_path(context: _MaterializationContext) -> Path:
@@ -1462,6 +1471,9 @@ def _implementation_anchor_lines(
1462
1471
  lines.append(
1463
1472
  f"{IMPLEMENTATION_STAGE_HEADER} {_required_stage_number(context)}"
1464
1473
  )
1474
+ owner = stage_qa_owner(context.manifest)
1475
+ if plan.audience == "implementation-verifier" and owner:
1476
+ lines.append(f"{STAGE_QA_OWNER_HEADER} {owner}")
1465
1477
  return lines
1466
1478
 
1467
1479
 
@@ -220,6 +220,38 @@ def record_progress(
220
220
  return event, warning
221
221
 
222
222
 
223
+ def record_checkpoint(
224
+ project_root: Path,
225
+ run_manifest: str | Path,
226
+ phase: str,
227
+ *,
228
+ source: str,
229
+ worker: str | None = None,
230
+ fields: Sequence[tuple[str, str]] = (),
231
+ ) -> str | None:
232
+ """단계를 수행하는 명령이 체크포인트를 기록하고 그 줄을 돌려준다.
233
+
234
+ 기록이 실패해도 그 기록이 붙은 dispatch·await·reclaim·finalize 는 실패시키지
235
+ 않는다. 장부 한 줄 때문에 작업 자체를 잃는 쪽이 더 나쁘다. 실패는 stderr 에
236
+ 출력하고, 이후 검증기가 체크포인트 누락으로 보고한다.
237
+ """
238
+ root = project_root.resolve()
239
+ try:
240
+ event, warning = record_progress(
241
+ root,
242
+ _resolve_project_path(root, run_manifest),
243
+ phase,
244
+ worker=worker,
245
+ fields=fields,
246
+ )
247
+ except (ProgressRecordError, OSError) as exc:
248
+ print(f"{source}: could not record `{phase}`: {exc}", file=sys.stderr)
249
+ return None
250
+ if warning:
251
+ print(f"{source}: {warning}", file=sys.stderr)
252
+ return str(event.details["line"])
253
+
254
+
223
255
  def _append(args: argparse.Namespace) -> int:
224
256
  fields = [_parse_field(raw) for raw in args.field]
225
257
  event, warning = record_progress(
@@ -243,7 +275,7 @@ def _append(args: argparse.Namespace) -> int:
243
275
 
244
276
  _CLI_EPILOG = r"""Usage:
245
277
  okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-1-intake
246
- okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-4-dispatch --worker codex-worker --field model=gpt-6-sol
278
+ okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-4-dispatch --worker codex-worker --field model=gpt-6.1-sol
247
279
  okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-5-collect --worker codex-worker --field status=completed
248
280
 
249
281
  `progressLine` in the output is the line to emit to the user, verbatim.
@@ -45,7 +45,7 @@ def _table(headers: tuple[str, ...], rows: list[list[str]], empty: str) -> str:
45
45
  return f'<div class="scroll"><table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table></div>'
46
46
 
47
47
 
48
- def _file_link(project_root: str, path: str) -> str:
48
+ def _file_link(project_root: str, path: str, label: str) -> str:
49
49
  """하위 프로젝트 기준 상대 경로를 절대 file 링크로 바꾼다. 빈 값은 `-`."""
50
50
  if not path:
51
51
  return "-"
@@ -53,8 +53,8 @@ def _file_link(project_root: str, path: str) -> str:
53
53
  if not target.is_absolute() and project_root:
54
54
  target = Path(project_root) / target
55
55
  if not target.is_absolute():
56
- return f"<code>{_e(path)}</code>"
57
- return f'<a href="{_e(target.as_uri())}"><code>{_e(path)}</code></a>'
56
+ return f'<span title="{_e(path)}">{_e(label)}</span>'
57
+ return f'<a href="{_e(target.as_uri())}" title="{_e(target)}">{_e(label)}</a>'
58
58
 
59
59
 
60
60
  def _command(*args: str) -> str:
@@ -93,23 +93,30 @@ def _child_rows(children: list[dict], roots: dict[str, str]) -> list[list[str]]:
93
93
  assignment = " — ".join(part for part in (child.get("role"), child.get("assignment")) if part)
94
94
  # `task split` 하위 태스크는 assignment 대신 범위 목록을 가진다.
95
95
  assignment = assignment or "; ".join(child.get("scope") or [])
96
+ status_fields = (
97
+ ("Work status", child.get("workStatus") or "-"),
98
+ ("Phase", f"{phase} ({phase_state})" if phase and phase_state else phase or "-"),
99
+ ("Verdict", child.get("finalVerdict") or "-"),
100
+ ("Launch", launch.get("status") or "-"),
101
+ )
102
+ status = "".join(f"<dt>{_e(label)}</dt><dd>{_e(value)}</dd>" for label, value in status_fields)
103
+ links = "".join(
104
+ _file_link(root, str(path), label)
105
+ for root, path, label in (
106
+ ("", child.get("briefPath"), "Brief"),
107
+ (roots.get(str(child.get("projectId") or ""), ""), child.get("latestReportRecordPath"), "Latest report"),
108
+ )
109
+ if path
110
+ )
111
+ error = f'<p class="child-error"><strong>Error:</strong> {_e(child["error"])}</p>' if child.get("error") else ""
112
+ ticket = f'<strong class="child-ticket">{_e(child["ticketId"])}</strong>' if child.get("ticketId") else ""
96
113
  rows.append(
97
114
  [
98
- f"<code>{_e(child.get('taskKey'))}</code>"
115
+ ticket + f"<code class=\"child-key\">{_e(child.get('taskKey'))}</code>"
99
116
  + ("" if child.get("projectLinked", True) else " " + _chip("unlinked")),
100
- _e(child.get("ticketId") or "-"),
101
- _chip(str(child.get("summaryBucket") or "planned")),
102
- _e(child.get("workStatus") or "-"),
103
- _e(f"{phase} ({phase_state})" if phase and phase_state else phase or "-"),
104
- _e(child.get("finalVerdict") or "-"),
105
- _e(launch.get("status") or "-"),
106
- f'<span class="wrap">{_e(assignment or "-")}</span>',
107
- _e(child.get("error") or "-"),
108
- _file_link("", str(child.get("briefPath") or "")),
109
- _file_link(
110
- roots.get(str(child.get("projectId") or ""), ""),
111
- str(child.get("latestReportRecordPath") or ""),
112
- ),
117
+ _chip(str(child.get("summaryBucket") or "planned")) + f'<dl class="child-status">{status}</dl>',
118
+ f'<div class="child-assignment">{_e(assignment or "-")}</div>'
119
+ + error + (f'<div class="child-links">{links}</div>' if links else ""),
113
120
  ]
114
121
  )
115
122
  return rows
@@ -161,11 +168,11 @@ def _task_panel(home: Path, manager_id: str, task: dict, status: dict, roots: di
161
168
  f"<code>{_e(sync_command)}</code>, then re-run the view command.</p>"
162
169
  )
163
170
  children = _table(
164
- ("Child", "Ticket", "Summary", "Work status", "Phase", "Verdict", "Launch", "Assignment", "Error",
165
- "Brief", "Latest report"),
171
+ ("Child", "Status", "Assignment"),
166
172
  _child_rows(status["children"], roots),
167
173
  "No child tasks.",
168
174
  )
175
+ children = f'<div class="children">{children}</div>'
169
176
  directives = read_jsonl(directives_jsonl_path(home, manager_id, group, task_id))
170
177
  events = read_jsonl(events_jsonl_path(home, manager_id, group, task_id))
171
178
  details = (