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
@@ -29,6 +29,7 @@ from .domain.worker_exec import (
29
29
  WorkerExecRequest,
30
30
  )
31
31
  from .domain.worker_presentation import JsonEvents, Presentation
32
+ from .process_group import MemoryWatch, kill_group, memory_cap_bytes
32
33
  from .session_transcript import SessionTranscript
33
34
  from .json_boundary import JsonBoundaryError, write_owned_object_atomic
34
35
 
@@ -42,6 +43,8 @@ _TERM_GRACE_SECONDS = 5
42
43
  # this only has to cover a drain, never a producer.
43
44
  _DRAIN_AFTER_EXIT_SECONDS = 2
44
45
  _TIMEOUT_EXIT_CODE = 124
46
+ # sysexits 의 EX_OSERR. 워커 그룹이 메모리 상한을 넘어 러너가 죽인 run.
47
+ MEMORY_CAP_EXIT_CODE = 71
45
48
  _READ_SIZE = 8192
46
49
  _WRITE_SIZE = 8192
47
50
  _NO_STATUS_EXTRA: Mapping[str, Any] = {}
@@ -66,6 +69,7 @@ def run_worker(
66
69
  _validate_status_extra(status_extra)
67
70
  _validate_request_write_contract(request, status_extra)
68
71
  command = strategy.build_command(request)
72
+ memory_watch = MemoryWatch(memory_cap_bytes(request.project_root))
69
73
  started_monotonic = time.monotonic()
70
74
  status = _started_status(status_extra, log_path)
71
75
  _write_status(status_path, status)
@@ -73,11 +77,12 @@ def run_worker(
73
77
 
74
78
  try:
75
79
  with guard:
76
- exit_code, timed_out, idle_seconds, raw_model, usage = _launch(
80
+ exit_code, timed_out, idle_seconds, raw_model, usage, reaped = _launch(
77
81
  command,
78
82
  log_path,
79
83
  presentation=presentation,
80
84
  idle_timeout_seconds=request.idle_timeout_seconds,
85
+ memory_watch=memory_watch,
81
86
  on_spawn=guard.watch,
82
87
  )
83
88
  at_exit = getattr(command.presentation, "served_model_at_exit", None)
@@ -107,6 +112,11 @@ def run_worker(
107
112
  if failure:
108
113
  status["failure"] = failure
109
114
  exit_code = SERVED_MODEL_MISMATCH_EXIT_CODE
115
+ if reaped:
116
+ status["leftoverProcessesKilled"] = True
117
+ if memory_watch.exceeded_bytes:
118
+ exit_code = MEMORY_CAP_EXIT_CODE
119
+ status.update(failure=memory_watch.failure(), terminated_by="memory-cap")
110
120
  status["exit_code"] = exit_code
111
121
  if timed_out:
112
122
  status.update(
@@ -176,8 +186,9 @@ def _launch(
176
186
  *,
177
187
  presentation: str,
178
188
  idle_timeout_seconds: int,
189
+ memory_watch: MemoryWatch,
179
190
  on_spawn: Callable[[subprocess.Popen[bytes]], None],
180
- ) -> tuple[int, bool, int, str | None, Mapping[str, Any] | None]:
191
+ ) -> tuple[int, bool, int, str | None, Mapping[str, Any] | None, bool]:
181
192
  live = presentation == LIVE
182
193
  transcript = SessionTranscript(log_path, live=live)
183
194
  observation = _ServedModelObservation()
@@ -211,14 +222,18 @@ def _launch(
211
222
  strategy=strategy,
212
223
  presentation=presentation,
213
224
  idle_timeout_seconds=idle_timeout_seconds,
225
+ memory_watch=memory_watch,
214
226
  stdin_text=command.stdin_text,
215
227
  )
228
+ # 워커가 띄운 빌드 워커·서버는 워커가 끝나도 그룹에 남아 계속 자란다.
229
+ reaped = kill_group(process.pid)
216
230
  return (
217
231
  exit_code,
218
232
  timed_out,
219
233
  idle_seconds,
220
234
  observation.raw_model,
221
235
  usage_observation.usage,
236
+ reaped,
222
237
  )
223
238
  finally:
224
239
  transcript.close()
@@ -386,6 +401,7 @@ def _pump(
386
401
  strategy: Presentation,
387
402
  presentation: str,
388
403
  idle_timeout_seconds: int,
404
+ memory_watch: MemoryWatch,
389
405
  stdin_text: str | None = None,
390
406
  ) -> tuple[int, bool, int]:
391
407
  selector = selectors.DefaultSelector()
@@ -418,6 +434,7 @@ def _pump(
418
434
  ):
419
435
  timed_out = True
420
436
  _terminate(process)
437
+ memory_watch.check(process.pid, time.monotonic())
421
438
  drain_deadline = _drain_deadline(process, drain_deadline)
422
439
  if drain_deadline is not None and time.monotonic() >= drain_deadline:
423
440
  _stop_reading(selector)
@@ -457,7 +474,8 @@ def _drain_deadline(
457
474
  holding the write end of the pipe open for as long as it lives. The shell
458
475
  wrappers returned as soon as `wait <pid>` did, so blocking on a grandchild is
459
476
  a regression rather than a policy, and the exit code the caller gets stays
460
- the worker's own.
477
+ the worker's own. Whatever is still in the group afterwards is killed
478
+ (`_launch`).
461
479
 
462
480
  The deadline is set once and never pushed back: output arriving after the
463
481
  worker exited is the grandchild's, and letting it extend the wait would
@@ -7,14 +7,13 @@ task/run status, render-only flag) 만 받아서 WORKFLOW_* 필드 dict 를 돌
7
7
  `okstra_ctl.next_phase` 하나가 소유하고, 값은 리포트가 저작한 라우팅에서
8
8
  투영된다.
9
9
 
10
- task-type 별 PHASE_ALLOWED_OUTPUTS 본문 매핑은 모듈 상수(`PHASE_RULES`)로 보유하고,
11
- PHASE_FORBIDDEN_ACTIONS 는 `prompts/profiles/forbidden-actions.json` SSOT 에서 로드한다.
10
+ 단계별 출력·금지 문구는 선택한 단계의 boundary.json 에서 읽는다.
12
11
  """
13
12
  from __future__ import annotations
14
13
 
15
- import json
16
14
  from pathlib import Path
17
15
  from .json_boundary import load_owned_object
16
+ from .phases.catalog import phase_boundary, phase_record, task_types
18
17
 
19
18
  PHASE_SEQUENCE = [
20
19
  "requirements-discovery",
@@ -35,125 +34,6 @@ ERROR_ANALYSIS_ROUTING_DIRECTIONS = {
35
34
  "implementation-option-selection": "begin-option-selection",
36
35
  }
37
36
 
38
- # Phase 별 allowed outputs. bash heredoc 원문 그대로 옮긴 값 — 들여쓰기와 백틱은
39
- # prompt template 에 그대로 박혀 lead 가 읽는다. forbidden actions 는 이 dict 가
40
- # 아니라 prompts/profiles/forbidden-actions.json (load_phase_forbidden) 이 SSOT.
41
- PHASE_RULES: dict[str, dict[str, str]] = {
42
- "technical-verification": {
43
- "allowed": (
44
- " - falsifiable experiment plans for the frozen unresolved facts\n"
45
- " - dependency installation, source experiments, tests and builds only in this run's experiment copies\n"
46
- " - command logs, observed signals and per-fact supported/refuted/inconclusive/not-run results\n"
47
- " - return to implementation-option-selection with evidence, without adoption or plan approval"
48
- ),
49
- },
50
- "requirements-discovery": {
51
- "allowed": (
52
- " - work-category classification (bugfix / feature / refactor / ops / improvement)\n"
53
- " - routing decision as the `routing` object `{nextTaskType, readyWhen, rationale}`: `nextTaskType` is `error-analysis` or `implementation-option-selection` and nothing else — this phase never hands off directly to planning or implementation. Prose alone does not route the task — Phase 7 projects `workflow.nextRecommendedPhase` from `nextTaskType`\n"
54
- " - missing-input list and clarification requests\n"
55
- " - approval / confirmation checkpoints recorded for the next phase\n"
56
- " - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)"
57
- ),
58
- },
59
- "improvement-discovery": {
60
- "allowed": (
61
- " - improvement candidate discovery within the lens whitelist defined in `scripts/okstra_ctl/improvement_lenses.py`\n"
62
- " - top-N ranking (default 8, brief `candidate-cap` overrides within 1..12)\n"
63
- " - per-candidate columns Lens / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Expected behavior after / Evidence (path:line)\n"
64
- " - Phase 1.5 reflect-back grilling log at `runs/improvement-discovery/<seq>/state/phase-1.5-grilling.md`\n"
65
- " - Phase 5.5 consensus classification (full / partial / contested / worker-unique / unverified)"
66
- ),
67
- },
68
- "project-analysis": {
69
- "allowed": (
70
- " - components, dependency directions, entry points, repositories, and external integrations\n"
71
- " - shallow feature index and scan scope\n"
72
- ),
73
- },
74
- "feature-analysis": {
75
- "allowed": (
76
- " - confirmed target and normal, alternate, and failure flows\n"
77
- " - rules, state transitions, external calls, and test coverage scope\n"
78
- ),
79
- },
80
- "change-impact-analysis": {
81
- "allowed": (
82
- " - preserved behavior, impacted items, and dependency propagation\n"
83
- " - test and operational impact\n"
84
- " - constraints and open decisions passed to implementation-planning\n"
85
- ),
86
- },
87
- "error-analysis": {
88
- "allowed": (
89
- " - symptom and trigger evidence\n"
90
- " - root-cause hypotheses with supporting citations\n"
91
- " - reproduction gaps and missing observability\n"
92
- " - validation paths to confirm or refute hypotheses\n"
93
- " - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)"
94
- ),
95
- },
96
- "implementation-option-selection": {
97
- "allowed": (
98
- " - candidate-comparison evidence for up to three options per worker\n"
99
- " - preselected-validation of exactly ONE candidate: every analyser returns its own feasibility verdict on that one direction, `candidateAudit` stays empty, and no analyser generates an alternative. It is not a re-evaluation of the merged set — `okstra_ctl.implementation_options._validate_mode` rejects a preselected-validation report carrying more than one option or a non-empty audit\n"
100
- " - a ranked display of at most three options with requirement mappings\n"
101
- " - an audit record for every rejected candidate\n"
102
- " - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)"
103
- ),
104
- },
105
- "implementation-planning": {
106
- "allowed": (
107
- " - pre-planning context exploration notes (files/interfaces inspected, recent commits scanned, ambiguities flagged)\n"
108
- " - selected-direction planning: realize the one validated selected direction without reopening candidate comparison, with a File Structure list (Create/Modify/Delete with one-line responsibility per file), affected interfaces, and blast-radius estimate\n"
109
- " - legacy candidate-comparison planning only: at least two implementation option candidates, each with a File Structure list (Create/Modify/Delete with one-line responsibility per file), affected interfaces, and blast-radius estimate\n"
110
- " - legacy candidate-comparison planning only: a trade-off matrix across options (complexity, risk, reversibility, test cost, rollout cost) and recommended option with rationale tied to isolation / single-responsibility / YAGNI principles\n"
111
- " - bite-sized stepwise execution order for the selected direction or legacy recommended option (each step ~2-5 min, exact file paths and commands, TDD ordering when applicable, no placeholders)\n"
112
- " - dependency / migration risk assessment, validation checklist (pre / mid / post with exact commands), rollback strategy with revert path and trigger signal\n"
113
- " - every unresolved ambiguity registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (do NOT create a separate `Open Questions` block under `5.5.x` — the unified table is the single home)\n"
114
- " - report record `frontmatter.approved: false` awaiting `--approve` or the in-session wizard\n"
115
- " - self-review confirmation (spec coverage, placeholder scan, internal consistency, ambiguity, scope)\n"
116
- " - one endStateCoverage row per brief end-state id, each mapped to the R-NNN row that carries it"
117
- ),
118
- },
119
- "implementation": {
120
- "allowed": (
121
- " - approved-plan reference and quoted user-approval evidence\n"
122
- " - commit list with SHA, message, and the plan step each commit satisfies\n"
123
- " - `git diff --stat <base>..HEAD` summary plus per-file one-line change summary\n"
124
- " - `Out-of-plan edits` block listing every file touched outside the approved plan with rationale (empty block preferred)\n"
125
- ' - validation evidence: actual stdout/stderr and exit code for every pre / mid / post command from the plan (no paraphrased "tests pass")\n'
126
- " - TDD evidence for TDD-applicable steps: failing-test output before implementation commit and passing-test output after, with framing SHAs\n"
127
- " - per-verifier sections for every verifier in the resolved roster, with an independent verdict (PASS / CONCERNS / FAIL) and cited diff snippets; dissent is preserved by the Okstra lead\n"
128
- " - rollback verification (advisory, never blocks — record the revert path for a human; `result` is ok / not-applicable / advisory — human-run)\n"
129
- " - routing recommendation as the `routingRecommendation` object `{target, rationale}`: `target` is one of `final-verification`, `error-analysis`, `implementation-planning`, `implementation`, and `rationale` is why that one and nothing else. Prose alone does not route the task — Phase 7 projects `workflow.nextRecommendedPhase` from `target`"
130
- ),
131
- },
132
- "final-verification": {
133
- "allowed": (
134
- " - acceptance verdict with requirement coverage assessment\n"
135
- " - residual risk and regression notes\n"
136
- " - routing recommendation as the `routingRecommendation` object `{target, rationale}`: `target` is one of `release-handoff`, `release-handoff(stage-group)`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `implementation`, `done`, and `rationale` ties that choice to the verdict and the blocker list. Both `release-handoff` forms require a release-ready verdict — `accepted`, or `conditional-accept` with every condition declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`); either verification scope may route there, because release-handoff opens one PR per stage. Prose alone does not route the task — Phase 7 projects `workflow.nextRecommendedPhase` from `target`"
137
- ),
138
- },
139
- "release-handoff": {
140
- "allowed": (
141
- " - entering this phase only when the cited final-verification report's verdict is release-ready — `Verdict Token` exactly `accepted`, or exactly `conditional-accept` with every Conditional Acceptance Condition row declaring `blocksReleaseHandoff: false` (`okstra_ctl.release_gate.release_handoff_allowed`)\n"
142
- " - asking the user (via `AskUserQuestion` / interactive prompt) which delivery action to take: `local checkout`, `push + PR`, or `skip` (end the run)\n"
143
- " - asking the user to pick a PR base branch from `staging` | `preprod` | `prod` | `main` | `dev` | a user-supplied branch name\n"
144
- " - drafting PR title and PR body **inline as the Okstra lead** (no drafter worker and no delegated dispatch); the lead reviews its own draft with the user before any mutating git / gh command runs\n"
145
- " - read-only git inspection: `git status`, `git diff`, `git log`, `git rev-parse`\n"
146
- " - pushing the current feature branch to its origin remote via `git push -u origin <current-branch>` (the feature branch only — NEVER the base branch)\n"
147
- " - creating a pull request via `gh pr create --base <chosen-base> --head <current-branch>`; if a PR with the same head already exists, surface its URL and skip creation\n"
148
- " - the lead writes the final report directly (no `Report writer worker` dispatch); the report still conforms to the standard final-report template"
149
- ),
150
- },
151
- }
152
-
153
- PHASE_RULES_UNKNOWN = {
154
- "allowed": " - outputs defined by the active task type",
155
- }
156
-
157
37
 
158
38
  def render_forbidden(items: list[str]) -> str:
159
39
  """Render a forbidden-action list to the ` - item` bullet block injected
@@ -161,24 +41,30 @@ def render_forbidden(items: list[str]) -> str:
161
41
  return "\n".join(f" - {item}" for item in items)
162
42
 
163
43
 
164
- def load_phase_forbidden(workspace_root: Path) -> dict[str, str]:
165
- """Load the forbidden-actions SSOT and render each phase to its bullet block.
166
-
167
- `workspace_root` is the resolved okstra asset root (repo or runtime); the
168
- JSON lives beside the profile `.md` files at
169
- `prompts/profiles/forbidden-actions.json`.
170
- """
171
- path = Path(workspace_root) / "prompts" / "profiles" / "forbidden-actions.json"
172
- data = load_owned_object(path, artifact="forbidden actions profile")
173
- if "unknown" not in data:
174
- # compute_workflow_state falls back to the "unknown" entry for any
175
- # unmapped task_type; without it a missing/renamed key would surface as
176
- # a KeyError deep in prompt assembly instead of here at the SSOT seam.
44
+ def load_phase_boundary(workspace_root: Path, task_type: str) -> dict[str, str]:
45
+ """선택한 단계의 출력 경계를 읽고 구형 자산 루트의 금지 목록도 보존한다."""
46
+ fallback_path = Path(workspace_root) / "prompts" / "profiles" / "forbidden-actions.json"
47
+ fallback = load_owned_object(fallback_path, artifact="forbidden actions profile")
48
+ if "unknown" not in fallback:
177
49
  raise ValueError(
178
- f"forbidden-actions SSOT at {path} is missing the required "
50
+ f"forbidden-actions SSOT at {fallback_path} is missing the required "
179
51
  "'unknown' fallback entry"
180
52
  )
181
- return {phase: render_forbidden(items) for phase, items in data.items()}
53
+ if task_type not in task_types():
54
+ return {
55
+ "allowed": " - outputs defined by the active task type",
56
+ "forbidden": render_forbidden(fallback.get(task_type, fallback["unknown"])),
57
+ }
58
+ path = phase_boundary(workspace_root, task_type)
59
+ if path is None:
60
+ # 패키지 없는 외부 프로필 루트는 기존 코드의 출력 정책과 자체 금지 목록을 쓴다.
61
+ path = Path(__file__).parent / "phases" / phase_record(task_type).package_name / "boundary.json"
62
+ data = load_owned_object(path, artifact="phase boundary")
63
+ items = fallback.get(task_type, fallback["unknown"])
64
+ else:
65
+ data = load_owned_object(path, artifact="phase boundary")
66
+ items = data["forbidden"]
67
+ return {"allowed": data["allowed"], "forbidden": render_forbidden(items)}
182
68
 
183
69
 
184
70
  def phase_position(task_type: str) -> str:
@@ -203,7 +89,7 @@ def compute_workflow_state(
203
89
  current_run_status: str,
204
90
  current_task_status: str,
205
91
  render_only: bool,
206
- forbidden_by_phase: dict[str, str],
92
+ boundary: dict[str, str],
207
93
  work_category: str = "",
208
94
  ) -> dict:
209
95
  """WORKFLOW_* + PHASE_* 값을 dict 로 돌려준다."""
@@ -234,7 +120,6 @@ def compute_workflow_state(
234
120
  else:
235
121
  checkpoint_label = "run-prepared"
236
122
 
237
- rules = PHASE_RULES.get(task_type, PHASE_RULES_UNKNOWN)
238
123
  last_completed = task_type if current_run_status == "completed" else ""
239
124
 
240
125
  resolved_work_category = (work_category or "").strip() or "unknown"
@@ -256,8 +141,6 @@ def compute_workflow_state(
256
141
  "WORKFLOW_LAST_COMPLETED_PHASE": last_completed,
257
142
  "WORKFLOW_AWAITING_APPROVAL": "false",
258
143
  "WORKFLOW_LAST_SAFE_CHECKPOINT_LABEL": checkpoint_label,
259
- "PHASE_ALLOWED_OUTPUTS": rules["allowed"],
260
- "PHASE_FORBIDDEN_ACTIONS": forbidden_by_phase.get(
261
- task_type, forbidden_by_phase["unknown"]
262
- ),
144
+ "PHASE_ALLOWED_OUTPUTS": boundary["allowed"],
145
+ "PHASE_FORBIDDEN_ACTIONS": boundary["forbidden"],
263
146
  }
@@ -26,6 +26,7 @@ from .domain.write_policy import ( # noqa: F401 — 재노출(값 코어는 도
26
26
  write_policy_digest,
27
27
  write_policy_from_payload,
28
28
  )
29
+ from .conformance import conformance_result_file
29
30
  from .final_report_paths import final_report_data_path
30
31
  from .json_boundary import JsonBoundaryError, load_owned_object
31
32
  from .path_hints import hydrate_active_run_context
@@ -65,11 +66,15 @@ def build_write_policy(
65
66
  ),
66
67
  "externalRoots": [str(path) for path in external_roots],
67
68
  }
69
+ artifact_policy: dict[str, Any] = {
70
+ "allowedRoot": str(project_root),
71
+ "allowedPaths": list(artifact_paths),
72
+ }
73
+ preserved_paths = _relative_paths(invocation.get("preservedPaths", ()))
74
+ if preserved_paths:
75
+ artifact_policy["preservedPaths"] = list(preserved_paths)
68
76
  policy = WritePolicy(
69
- artifact_policy={
70
- "allowedRoot": str(project_root),
71
- "allowedPaths": list(artifact_paths),
72
- },
77
+ artifact_policy=artifact_policy,
73
78
  source_policy=source_policy,
74
79
  git_policy=git_policy,
75
80
  auxiliary_policy=auxiliary_policy,
@@ -94,9 +99,12 @@ def _role_qa_artifact_paths(role: str, task_root: Path | None) -> tuple[Path, ..
94
99
  conformance script, its `tsconfig.json` and any real-IO qa spec under
95
100
  `qa/scripts/`, plus the manifest entry naming them
96
101
  (`_implementation-executor.md` §"Stage conformance script", §"Real-IO test
97
- isolation"). Everything else under `qa/` — the self-mock sidecar and its
98
- diff, the conformance run's `result-*.json` — is written while the verifier
99
- runs its own gates. Keeping the executor's grant to those two entries is
102
+ isolation"), plus `qa/output/` for whatever those scripts write when the
103
+ executor runs them against its own work — a baseline capture, build logs
104
+ (§"Verifier gates are not yours to run" lets it run them). Everything else
105
+ under `qa/` — the self-mock sidecar and its diff, the conformance run's
106
+ `result-*.json` — is written while the verifier runs its own gates.
107
+ Keeping the executor's grant to those three entries is
100
108
  what leaves the audit able to catch an executor that runs the verifier's
101
109
  self-mock gate (`_implementation-executor.md` §"Verifier gates are not
102
110
  yours to run"); widening it to `qa/` would silence that.
@@ -114,6 +122,7 @@ def _role_qa_artifact_paths(role: str, task_root: Path | None) -> tuple[Path, ..
114
122
  if role == "implementer":
115
123
  return (
116
124
  task_qa_dir(task_root) / "scripts",
125
+ task_qa_dir(task_root) / "output",
117
126
  task_conformance_manifest_file(task_root),
118
127
  )
119
128
  if role == "verifier":
@@ -121,6 +130,42 @@ def _role_qa_artifact_paths(role: str, task_root: Path | None) -> tuple[Path, ..
121
130
  return ()
122
131
 
123
132
 
133
+ def _inherited_qa_evidence(
134
+ role: str, task_type: str, task_root: Path | None,
135
+ ) -> tuple[Path, ...]:
136
+ """Existing qa files a final-verification verifier must leave byte-identical.
137
+
138
+ Its grant is the whole `qa/` tree, so re-running a plan step that captures a
139
+ baseline overwrote the implementation's RED evidence and still passed the
140
+ audit (observed 2026-09-26, dev-11054 `qa/baseline/base.json`). New files and
141
+ the conformance results it re-runs (`qa/result-<stageKey>.json`, the only qa
142
+ write `final_verification/boundary.json` names) stay writable.
143
+ """
144
+ if role != "verifier" or task_type != "final-verification" or task_root is None:
145
+ return ()
146
+ qa_dir = task_qa_dir(task_root)
147
+ if not qa_dir.is_dir():
148
+ return ()
149
+ rerun = {
150
+ conformance_result_file(qa_dir, key)
151
+ for key in _conformance_stage_keys(task_root)
152
+ }
153
+ return tuple(sorted(
154
+ path for path in qa_dir.rglob("*")
155
+ if path.is_file() and not path.is_symlink() and path not in rerun
156
+ ))
157
+
158
+
159
+ def _conformance_stage_keys(task_root: Path) -> tuple[str, ...]:
160
+ path = task_conformance_manifest_file(task_root)
161
+ if not path.is_file():
162
+ return ()
163
+ entries = _read_json(path, "conformance manifest").get("entries")
164
+ return tuple(
165
+ entry["stageKey"] for entry in entries or ()
166
+ if isinstance(entry, dict) and isinstance(entry.get("stageKey"), str)
167
+ )
168
+
124
169
 
125
170
  def _technical_experiment_paths(
126
171
  artifact_paths: Sequence[Path], task_root: Path | None,
@@ -145,6 +190,7 @@ def _technical_experiment_paths(
145
190
  def build_invocation_write_contract(
146
191
  *,
147
192
  role: str,
193
+ task_type: str,
148
194
  project_root: Path,
149
195
  worktree: Path | None,
150
196
  artifact_paths: Sequence[Path],
@@ -180,6 +226,10 @@ def build_invocation_write_contract(
180
226
  "plannedPaths": list(source_paths),
181
227
  "plannedPathsDeclared": planned_paths_declared,
182
228
  "protectedPaths": [".okstra", ".git"],
229
+ "preservedPaths": [
230
+ _relative_to_root(path, root, "preserved")
231
+ for path in _inherited_qa_evidence(role, task_type, task_root)
232
+ ],
183
233
  "generatedPaths": list(generated_paths),
184
234
  "scratchRoots": [str(path) for path in scratch_roots],
185
235
  "auxiliaryRoots": [str(path) for path in auxiliary_roots],
@@ -87,6 +87,20 @@ def okstra_root(project_root: Path) -> Path:
87
87
  return Path(project_root) / OKSTRA_RELATIVE
88
88
 
89
89
 
90
+ def ensure_okstra_gitignore(project_root: Path) -> Path:
91
+ """`.okstra/.gitignore` 에 `*` 를 둔다. 이미 있으면 건드리지 않는다.
92
+
93
+ 전역 gitignore 로만 `.okstra` 를 빼면 그것을 읽지 않는 도구가 산출물을 훑는다.
94
+ Tailwind v4(4.3.2) 스캐너는 전역 설정은 무시하고 중첩 `.gitignore` 는 따른다
95
+ (2026-09-27 실측) — 실험 사이트 복사본을 읽어 빌드가 수십 GB 로 커졌다.
96
+ """
97
+ path = okstra_root(project_root) / ".gitignore"
98
+ if not path.exists():
99
+ path.parent.mkdir(parents=True, exist_ok=True)
100
+ path.write_text("*\n", encoding="utf-8")
101
+ return path
102
+
103
+
90
104
  def project_json_path(project_root: Path) -> Path:
91
105
  """`<project_root>/.okstra/project.json` 절대 path."""
92
106
  return Path(project_root) / PROJECT_JSON_RELATIVE
@@ -11,6 +11,7 @@ from typing import Optional
11
11
  from .dirs import (
12
12
  OKSTRA_DIR_NAME,
13
13
  PROJECT_JSON_RELATIVE,
14
+ ensure_okstra_gitignore,
14
15
  project_json_path,
15
16
  )
16
17
 
@@ -154,7 +155,7 @@ def upsert_project_json(project_root: Path, project_id: str, *,
154
155
  if not project_id:
155
156
  raise ResolverError("project_id is required for upsert_project_json")
156
157
  target = project_json_path(project_root)
157
- target.parent.mkdir(parents=True, exist_ok=True)
158
+ ensure_okstra_gitignore(project_root)
158
159
  when = now or _now_iso()
159
160
  abs_root = str(Path(project_root).resolve())
160
161
  if target.is_file():
@@ -137,7 +137,8 @@
137
137
  "required": ["allowedRoot", "allowedPaths"],
138
138
  "properties": {
139
139
  "allowedRoot": {"type": "string", "pattern": "^/"},
140
- "allowedPaths": {"type": "array", "items": {"type": "string", "pattern": "\\S"}, "minItems": 1, "uniqueItems": true}
140
+ "allowedPaths": {"type": "array", "items": {"type": "string", "pattern": "\\S"}, "minItems": 1, "uniqueItems": true},
141
+ "preservedPaths": {"type": "array", "items": {"type": "string", "pattern": "\\S"}, "uniqueItems": true}
141
142
  },
142
143
  "additionalProperties": false
143
144
  },
@@ -345,7 +345,7 @@ If the user picked `Codebase scan`, gather the codebase-scan-specific inputs:
345
345
 
346
346
  1. `AskUserQuestion` (free text):
347
347
  `"Scan scope — comma-separated paths inside the project (e.g. src/foo/, src/bar/baz.py)"` → `scan_scope` (CSV).
348
- 2. `AskUserQuestion` (multi-select, options = the lens values from `~/.okstra/lib/python/okstra_ctl/improvement_lenses.py`'s `LENSES` — code source: `scripts/okstra_ctl/improvement_lenses.py`):
348
+ 2. `AskUserQuestion` (multi-select, options = the lens values from `~/.okstra/lib/python/okstra_ctl/phases/improvement_discovery/lenses.py`'s `LENSES` — code source: `scripts/okstra_ctl/phases/improvement_discovery/lenses.py`):
349
349
  `"Priority lenses (pick 1–4)"` → `priority_lenses` (1..4 values from the enum).
350
350
  Current `LENSES` values (this list mirrors the module — if they diverge, the module wins; Step 6.6 `validate-brief` enforces the subset): `performance`, `security`, `readability`, `architecture`, `test-coverage`, `dx`, `observability`, `accessibility`.
351
351
  3. `AskUserQuestion` (free text):
@@ -362,7 +362,7 @@ If the user picked `Codebase scan`, gather the codebase-scan-specific inputs:
362
362
  Validation (before Step 4 sharpening):
363
363
 
364
364
  - Confirm that each path in `scan_scope` actually exists inside `<PROJECT_ROOT>` via `ls` — if not, correct it with a single question.
365
- - Confirm that `priority_lenses` is a subset (1–4) of `LENSES` in `~/.okstra/lib/python/okstra_ctl/improvement_lenses.py` — on violation, show the enum + re-select. Final enforcement is done by Step 6.6 `validate-brief` (`check_codebase_scope`).
365
+ - Confirm that `priority_lenses` is a subset (1–4) of `LENSES` in `~/.okstra/lib/python/okstra_ctl/phases/improvement_discovery/lenses.py` — on violation, show the enum + re-select. Final enforcement is done by Step 6.6 `validate-brief` (`check_codebase_scope`).
366
366
  - Confirm that `candidate_cap` is an integer in 1–12.
367
367
 
368
368
  Once validation passes, jump to Step 2 (task key).
@@ -632,7 +632,7 @@ These 5 rules apply only to `scope: codebase` briefs. The budget expands from th
632
632
 
633
633
  1. **scan-scope path existence check.** Confirm each path via `ls` / `Read`. If missing, correct it with a single question (offer the nearest path candidate as `Recommended:`).
634
634
  2. **Ambiguous path narrowing.** Narrow abstract paths like `"backend"`, `"payment module"` down to one or more concrete directories. Guess codebase-first, then confirm with a single question.
635
- 3. **priority-lenses whitelist check.** Confirm it is a subset of `LENSES` in `~/.okstra/lib/python/okstra_ctl/improvement_lenses.py`. If out-of-enum, offer the nearest in-enum value as `Recommended:`.
635
+ 3. **priority-lenses whitelist check.** Confirm it is a subset of `LENSES` in `~/.okstra/lib/python/okstra_ctl/phases/improvement_discovery/lenses.py`. If out-of-enum, offer the nearest in-enum value as `Recommended:`.
636
636
  4. **out-of-scope consistency.** Confirm each path in `out-of-scope` is a subset of `scan-scope`. If a path is unrelated, remove it with a single question or absorb it into scan-scope.
637
637
  5. **One line of lens-priority rationale.** If the brief body lacks a line on *why* each priority lens takes precedence in this scope, offer the result of a first codebase pass as `Recommended:` and ask one question.
638
638