okstra 0.206.0 → 0.206.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/lifecycle/install.mjs +1 -1
  3. package/dist/commands/lifecycle/install.mjs.map +1 -1
  4. package/docs/architecture.md +12 -12
  5. package/docs/cli.md +4 -4
  6. package/docs/contributor-change-matrix.md +3 -2
  7. package/docs/performance-improvement-plan-v2.md +1 -1
  8. package/docs/project-structure-overview.md +39 -18
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/okstra-spawn-followups.py +2 -2
  12. package/runtime/prompts/launch.template.md +1 -1
  13. package/runtime/prompts/lead/context-loader.md +1 -1
  14. package/runtime/prompts/lead/convergence.md +3 -3
  15. package/runtime/prompts/lead/okstra-lead-contract.md +13 -54
  16. package/runtime/prompts/lead/phase-routing.md +64 -0
  17. package/runtime/prompts/lead/report-writer.md +2 -2
  18. package/runtime/prompts/lead/team-contract.md +1 -1
  19. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  20. package/runtime/prompts/profiles/_common-contract.md +1 -1
  21. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  22. package/runtime/prompts/profiles/forbidden-actions.json +0 -94
  23. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +1 -1
  24. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +10 -1
  25. package/runtime/python/okstra_ctl/analysis_inputs.py +0 -39
  26. package/runtime/python/okstra_ctl/analysis_scope.py +31 -0
  27. package/runtime/python/okstra_ctl/asset_roots.py +19 -0
  28. package/runtime/python/okstra_ctl/consumers.py +12 -0
  29. package/runtime/python/okstra_ctl/dispatch_state.py +22 -0
  30. package/runtime/python/okstra_ctl/doctor.py +2 -1
  31. package/runtime/python/okstra_ctl/execution_mutation_audit.py +27 -1
  32. package/runtime/python/okstra_ctl/handoff.py +11 -466
  33. package/runtime/python/okstra_ctl/handoff_error.py +5 -0
  34. package/runtime/python/okstra_ctl/implementation_direction.py +0 -477
  35. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +8 -1
  36. package/runtime/python/okstra_ctl/option_comparison.py +3 -165
  37. package/runtime/python/okstra_ctl/option_votes.py +3 -191
  38. package/runtime/python/okstra_ctl/paths.py +8 -6
  39. package/runtime/python/okstra_ctl/phases/catalog.py +56 -12
  40. package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
  41. package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
  42. package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
  43. package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
  44. package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
  45. package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
  46. package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
  47. package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
  48. package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
  49. package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
  50. package/runtime/python/okstra_ctl/phases/error_analysis/spec.md +118 -0
  51. package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
  52. package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
  53. package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
  54. package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
  55. package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
  56. package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
  57. package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
  58. package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
  59. package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
  60. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -1
  61. package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
  62. package/runtime/python/okstra_ctl/phases/final_verification/spec.md +1 -1
  63. package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
  64. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
  65. package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
  66. package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
  67. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
  68. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
  69. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +4 -4
  70. package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
  71. package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
  72. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
  73. package/runtime/python/okstra_ctl/phases/implementation/spec.md +238 -0
  74. package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
  75. package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
  76. package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
  77. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
  78. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
  79. package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
  80. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
  81. package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +2 -2
  82. package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
  83. package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +83 -0
  84. package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
  85. package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
  86. package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
  87. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
  88. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
  89. package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
  90. package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
  91. package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
  92. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
  93. package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
  94. package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
  95. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
  96. package/runtime/python/okstra_ctl/phases/implementation_planning/spec.md +204 -0
  97. package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
  98. package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
  99. package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
  100. package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
  101. package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
  102. package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
  103. package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
  104. package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
  105. package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
  106. package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
  107. package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
  108. package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
  109. package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
  110. package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
  111. package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
  112. package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
  113. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
  114. package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
  115. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
  116. package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
  117. package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
  118. package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
  119. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +233 -0
  120. package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
  121. package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
  122. package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
  123. package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
  124. package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
  125. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +132 -0
  126. package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
  127. package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
  128. package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
  129. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
  130. package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +1 -1
  131. package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
  132. package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
  133. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
  134. package/runtime/python/okstra_ctl/plan_approval.py +70 -0
  135. package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
  136. package/runtime/python/okstra_ctl/profile_show.py +3 -3
  137. package/runtime/python/okstra_ctl/render.py +9 -2
  138. package/runtime/python/okstra_ctl/report_assembly.py +11 -90
  139. package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
  140. package/runtime/python/okstra_ctl/report_projections.py +1 -36
  141. package/runtime/python/okstra_ctl/report_routing.py +23 -0
  142. package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
  143. package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
  144. package/runtime/python/okstra_ctl/report_views.py +1 -1
  145. package/runtime/python/okstra_ctl/run.py +67 -349
  146. package/runtime/python/okstra_ctl/stage_map.py +13 -0
  147. package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
  148. package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
  149. package/runtime/python/okstra_ctl/wizard/api.py +18 -0
  150. package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
  151. package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
  152. package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
  153. package/runtime/python/okstra_ctl/wizard/steps_plan.py +10 -263
  154. package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
  155. package/runtime/python/okstra_ctl/work_categories.py +1 -1
  156. package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
  157. package/runtime/python/okstra_ctl/workflow.py +26 -143
  158. package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
  159. package/runtime/skills/okstra-run/SKILL.md +1 -1
  160. package/runtime/templates/reports/quick-input.template.md +1 -1
  161. package/runtime/templates/reports/task-brief.template.md +1 -1
  162. package/runtime/validators/validate-brief.py +2 -2
  163. package/runtime/validators/validate-run.py +299 -3940
  164. package/runtime/validators/validate_analysis_report.py +14 -126
  165. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
  166. package/runtime/python/okstra_ctl/technical_verification.py +0 -195
  167. /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
  168. /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
  169. /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
  170. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
  171. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
  172. /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
  173. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
  174. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
  175. /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
  176. /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
  177. /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
  178. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
  179. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
  180. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
  181. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
  182. /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
  183. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
  184. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
  185. /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
  186. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
  187. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
  188. /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
  189. /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
  190. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
  191. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
  192. /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
  193. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
  194. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
  195. /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
  196. /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
  197. /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
  198. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
  199. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
  200. /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
  201. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
  202. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
  203. /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
  204. /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
  205. /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
  206. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
  207. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
  208. /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
  209. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
  210. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.md +0 -0
@@ -19,7 +19,7 @@ import sys
19
19
  from pathlib import Path
20
20
 
21
21
  from .paths import find_asset_root
22
- from .phases.catalog import PhaseAssetError, UnknownTaskType, profile_markdown
22
+ from .phases.catalog import PhaseAssetError, UnknownTaskType, profile_markdown, profile_sidecar
23
23
  from .role_requirements import RoleProfileError, load_role_profile
24
24
  from .run import PrepareError, _expand_profile_includes
25
25
 
@@ -29,7 +29,7 @@ _PROFILES_REL = ("prompts", "profiles")
29
29
  # `prompts/profiles/_implementation-executor.md`. Reading the list from the
30
30
  # profile body rather than hard-coding it here is deliberate: a list in code
31
31
  # goes stale the moment a profile adds a sidecar, and does so silently.
32
- _SIDECAR_RE = re.compile(r"`(prompts/profiles/_[\w-]+\.md)`")
32
+ _SIDECAR_RE = re.compile(r"`((?:prompts/profiles|scripts/okstra_ctl/phases/implementation/instructions)/_[\w-]+\.md)`")
33
33
 
34
34
 
35
35
  def workspace_root(start: Path | None = None) -> Path:
@@ -77,7 +77,7 @@ def sidecar_bodies(root: Path, profile_text: str) -> list[str]:
77
77
  if relative in seen:
78
78
  continue
79
79
  seen.add(relative)
80
- path = root / relative
80
+ path = profile_sidecar(root, relative)
81
81
  if not path.is_file():
82
82
  continue
83
83
  body = _expand_profile_includes(path)
@@ -17,6 +17,8 @@ session id 등) 를 덧붙여 전달한다.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ from .phases.catalog import profile_sidecar
21
+
20
22
  import hashlib
21
23
  import json
22
24
  import re
@@ -2632,7 +2634,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2632
2634
  home_prompts / "lead" / "convergence.md"
2633
2635
  )
2634
2636
  plan_body_verification_path = ctx.get("OKSTRA_PLAN_BODY_VERIFICATION_PATH") or str(
2635
- home_prompts / "lead" / "plan-body-verification.md"
2637
+ profile_sidecar(home_prompts.parent, "prompts/lead/plan-body-verification.md")
2636
2638
  )
2637
2639
  report_writer_path = ctx.get("OKSTRA_REPORT_WRITER_PATH") or str(
2638
2640
  home_prompts / "lead" / "report-writer.md"
@@ -2971,7 +2973,12 @@ def render_template_with_ctx(template_path: str, output_path: str, ctx: dict) ->
2971
2973
  Optional defaults (VALIDATION_STATUS etc.) should be filled by calling
2972
2974
  `apply_lead_prompt_defaults(ctx)` in the same setup step.
2973
2975
  """
2974
- template = Path(template_path).read_text(encoding="utf-8")
2976
+ from .phases.catalog import migrated_report_template
2977
+
2978
+ source = Path(template_path)
2979
+ if source.parent.name == "reports" and source.parent.parent.name == "templates":
2980
+ source = migrated_report_template(source.parent.parent.parent, source.name) or source
2981
+ template = source.read_text(encoding="utf-8")
2975
2982
 
2976
2983
  fm_ctx = dict(ctx)
2977
2984
  fm_ctx.setdefault("DOC_TYPE", _doc_type_from_template_path(template_path))
@@ -2,9 +2,10 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import copy
5
- import json
6
5
  import os
7
6
  import tempfile
7
+ from okstra_ctl.phases.implementation_planning.validation import validate_selected_direction_plan
8
+
8
9
  from dataclasses import dataclass
9
10
  from pathlib import Path
10
11
  from typing import Any, Callable, Mapping, Sequence
@@ -25,22 +26,24 @@ from .report_synthesis_packet import (
25
26
  )
26
27
  from .conformance import strip_stage_declaration_label
27
28
  from .design_prep import DesignPrepError, materialize_design_prep_requests
28
- from .implementation_options import validate_blocked_answer_channel
29
- from .technical_verification import validate_technical_verification_report
29
+ from .phases.implementation_option_selection.validation import validate_blocked_answer_channel
30
+ from .phases.technical_verification.validation import validate_technical_verification_report
31
+ from .report_routing import technical_verification_routing_errors
30
32
  from .implementation_direction import (
31
33
  load_selected_direction_snapshot,
32
- validate_selected_direction_plan,
33
34
  )
34
35
  from .paths import task_dir
35
36
  from .phases.final_verification.report import verification_scope
36
37
  from .report_projections import (
37
38
  ReportProjectionError,
38
39
  project_convergence,
39
- project_design,
40
40
  project_execution,
41
41
  project_token_usage,
42
42
  )
43
- from .scope_provenance import parse_source
43
+ from .phases.implementation_planning.report import (
44
+ attach_design_snapshot, _sync_human_summary_with_plan_body_gate,
45
+ _fill_end_state_coverage,
46
+ )
44
47
 
45
48
  from .write_policy import planned_path_declaration_errors
46
49
 
@@ -689,15 +692,9 @@ def _attach_machine_planning(
689
692
  if design_row is None:
690
693
  return
691
694
  try:
692
- design = project_design(planning, _read_json(design_row))
695
+ attach_design_snapshot(planning, _read_json(design_row))
693
696
  except ReportProjectionError as exc:
694
697
  _fail(design_row.owner, design_row.path, "$", exc)
695
- planning["designPreparation"] = design["designPreparation"]
696
- stages = {row.get("stage"): row for row in planning.get("stages") or [] if isinstance(row, dict)}
697
- for coverage in design["stageCoverage"]:
698
- stage = stages.get(coverage.get("stage"))
699
- if stage is not None:
700
- stage["designSurfaceCoverage"] = coverage.get("rows") or []
701
698
  verification_row = inputs.get("plan-body-verification")
702
699
  if verification_row is None:
703
700
  return
@@ -717,83 +714,6 @@ def _attach_machine_planning(
717
714
  _sync_human_summary_with_plan_body_gate(data, projection)
718
715
 
719
716
 
720
- def _sync_human_summary_with_plan_body_gate(
721
- data: dict[str, Any], projection: Mapping[str, Any],
722
- ) -> None:
723
- """게이트가 통과했는데 내러티브가 unseeded 로 남으면 사람용 칸을 맞춘다.
724
-
725
- report-writer 는 PBV 전에 쓰고, 조립이 게이트를 덮어씌운다. blockers 와
726
- nextStep 이 그때의 문장을 그대로 두면 통과한 계획이 아직 검증 전으로 보인다.
727
- """
728
- gate = str(projection.get("gateResult") or "")
729
- if gate not in {"passed", "passed-with-dissent"}:
730
- return
731
- summary = data.get("humanSummary")
732
- if isinstance(summary, dict) and isinstance(summary.get("blockers"), list):
733
- summary["blockers"] = [
734
- row for row in summary["blockers"]
735
- if "unseeded" not in str(row).lower()
736
- ]
737
- card = data.get("verdictCard")
738
- if not isinstance(card, dict):
739
- return
740
- next_step = str(card.get("nextStep") or "")
741
- lowered = next_step.lower()
742
- if "unseeded" not in lowered and "seed" not in lowered:
743
- return
744
- next_step = (
745
- "Plan-body verification passed. Do not start implementation "
746
- "until `frontmatter.approved` is true."
747
- )
748
- card["nextStep"] = next_step
749
- final = data.get("finalVerdict")
750
- if isinstance(final, dict):
751
- final["nextStep"] = next_step
752
-
753
-
754
- def _fill_end_state_coverage(data: dict[str, Any]) -> None:
755
- """writer 가 못 쓴 endStateCoverage 를 requirementCoverage brief:EB-001 에서 채운다.
756
-
757
- 내러티브 스키마가 이 칸을 열어 주면 writer 가 쓸 수 있다. 비어 있으면
758
- validate-run 이 brief id 마다 한 행을 요구하므로 조립이 채운다.
759
- """
760
- existing = data.get("endStateCoverage")
761
- if isinstance(existing, list) and existing:
762
- return
763
- planning = data.get("implementationPlanning")
764
- if not isinstance(planning, dict):
765
- return
766
- derived = _end_state_rows_from_requirement_coverage(planning)
767
- if derived:
768
- data["endStateCoverage"] = derived
769
-
770
-
771
- def _end_state_rows_from_requirement_coverage(
772
- planning: Mapping[str, Any],
773
- ) -> list[dict[str, Any]]:
774
- rows: list[dict[str, Any]] = []
775
- seen: set[str] = set()
776
- for item in planning.get("requirementCoverage") or []:
777
- if not isinstance(item, Mapping):
778
- continue
779
- ref = parse_source(str(item.get("source") or ""))
780
- if not ref.is_end_state_id or ref.value in seen:
781
- continue
782
- seen.add(ref.value)
783
- status = str(item.get("status") or "")
784
- row: dict[str, Any] = {
785
- "id": ref.value,
786
- "disposition": "addressed" if status == "covered" else "deferred",
787
- "coveredBy": str(item.get("id") or item.get("coveredBy") or ""),
788
- }
789
- if row["disposition"] != "addressed":
790
- row["rationale"] = (
791
- f"requirementCoverage `{item.get('id')}` status is `{status or 'empty'}`."
792
- )
793
- rows.append(row)
794
- return rows
795
-
796
-
797
717
  def _schema_owner(error: str) -> str:
798
718
  field = error.split(" ", 1)[0]
799
719
  owners = {
@@ -897,6 +817,7 @@ def assemble_report(
897
817
  target = Path(value)
898
818
  target = target if target.is_absolute() else project_root / target
899
819
  verification_errors = validate_technical_verification_report(data, target, project_root)
820
+ verification_errors.extend(technical_verification_routing_errors(data))
900
821
  if verification_errors:
901
822
  raise ReportAssemblyError(tuple(
902
823
  AssemblyIssue("report-writer", str(target), "technicalVerification", error)
@@ -247,7 +247,7 @@ def approved_plan_links(
247
247
  except JsonBoundaryError:
248
248
  return {}
249
249
  from .common import anchor_index
250
- from .view_models.implementation_planning import ANCHORED_FIELDS
250
+ from okstra_ctl.phases.implementation_planning.report import ANCHORED_FIELDS
251
251
 
252
252
  page = html_view_path(record)
253
253
  href = Path(os.path.relpath(page, data_path.resolve().parent)).as_posix()
@@ -4,10 +4,8 @@ from __future__ import annotations
4
4
  from okstra_token_usage.blocks import accounting_workers
5
5
 
6
6
  import json
7
- from copy import deepcopy
8
- from typing import Any, Mapping, Sequence
7
+ from typing import Any, Mapping
9
8
 
10
- from .design_surfaces import DesignSurfaceTrigger, detect_design_surfaces
11
9
  from .report_contract import execution_roles_from_manifest
12
10
  from .usage_cells import duration_ms_from_bounds
13
11
 
@@ -220,39 +218,6 @@ def project_token_usage(team_state: Mapping[str, Any]) -> dict[str, Any]:
220
218
  }
221
219
 
222
220
 
223
- def _trigger_rows(trigger: DesignSurfaceTrigger) -> list[dict[str, Any]]:
224
- return [
225
- {"step": item.step, "field": item.field, "match": item.match}
226
- for item in trigger.evidence
227
- ]
228
-
229
-
230
- def project_design(
231
- planning: Mapping[str, Any], detector_snapshot: Mapping[str, Any],
232
- ) -> dict[str, Any]:
233
- """탐지 결과가 실제 계획 표면과 일치할 때만 설계 블록을 반환한다."""
234
- triggers = detect_design_surfaces(planning)
235
- expected = {(row.stage, row.kind): _trigger_rows(row) for row in triggers}
236
- coverage = detector_snapshot.get("stageCoverage")
237
- coverage = coverage if isinstance(coverage, list) else []
238
- actual: dict[tuple[int, str], list[dict[str, Any]]] = {}
239
- for stage in coverage:
240
- if not isinstance(stage, Mapping):
241
- raise ReportProjectionError("owner=design-surface-detector invalid stageCoverage row")
242
- for row in stage.get("rows") or []:
243
- if isinstance(row, Mapping):
244
- actual[(int(stage.get("stage") or 0), str(row.get("kind") or ""))] = list(row.get("triggerEvidence") or [])
245
- if expected != actual:
246
- raise ReportProjectionError("owner=design-surface-detector trigger coverage mismatch")
247
- preparation = detector_snapshot.get("designPreparation")
248
- if not isinstance(preparation, Mapping):
249
- raise ReportProjectionError("owner=design-surface-detector missing designPreparation")
250
- return {
251
- "designPreparation": deepcopy(dict(preparation)),
252
- "stageCoverage": deepcopy(coverage),
253
- }
254
-
255
-
256
221
  def _round_history(state: Mapping[str, Any]) -> dict[str, Any]:
257
222
  if not (state.get("config") or {}).get("enabled", True):
258
223
  return {"disabled": True}
@@ -0,0 +1,23 @@
1
+ """보고서 생산 단계와 분리된 공통 이동 적합성 검사."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from typing import Any
6
+
7
+
8
+ def technical_verification_routing_errors(data: Mapping[str, Any]) -> list[str]:
9
+ """기술 검증의 후속 목적지가 후보 비교인지 검사하며 목적지를 추론하지 않는다."""
10
+ if (data.get("header") or {}).get("taskType") != "technical-verification":
11
+ return []
12
+ block = data.get("technicalVerification") or {}
13
+ if (block.get("routing") or {}).get("nextTaskType") != "implementation-option-selection":
14
+ return ["technical verification returns only to implementation-option-selection"]
15
+ return []
16
+
17
+
18
+ def fanout_routing_errors(packet_name: str, target: str | None) -> list[str]:
19
+ """작업 분해 패킷의 후속 단계가 허용된 값인지 검사한다."""
20
+ allowed = ("error-analysis", "implementation-option-selection")
21
+ if target not in allowed:
22
+ return [f"{packet_name}: recommended-next-phase not in {allowed}"]
23
+ return []
@@ -15,22 +15,14 @@ from okstra_token_usage.blocks import accounting_workers
15
15
 
16
16
  from .analysis_packet import reference_source_extracts
17
17
  from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
18
- from .implementation_options import (
19
- CANDIDATE_COMPARISON_ROUTING,
20
- EVALUATION_CRITERIA,
21
- MAX_CRITERION_VALUE,
22
- MAX_RANKED_OPTIONS,
23
- MAX_RAW_CANDIDATES_PER_ANALYSER,
24
- MIN_CRITERION_VALUE,
25
- MIN_FEASIBLE_VOTES,
26
- NO_VALID_OPTIONS_ROUTING,
18
+ from .phases.implementation_option_selection.authoring import (
19
+ requirement_instructions as selection_requirement_instructions,
27
20
  )
28
21
  from .report_narrative import writer_owned_data, writer_owned_schema
29
22
  from .schema_excerpt import build_schema_excerpt
30
23
  from .stage_ledger import build_stage_ledger
31
24
  from .scope_provenance import brief_end_state_id_sequence
32
25
 
33
- from .exact_coverage import COVERAGE_VERDICT_PRECEDENCE
34
26
  from .final_report_schema import follow_up_task_rules, task_block_rules, verdict_token_rule
35
27
  from .report_contract import TASK_TYPE_DATA_PROPERTY
36
28
  from .report_markdown import humanise
@@ -251,70 +243,9 @@ class ReportSynthesisPacket:
251
243
  return self._planning_requirement_instructions()
252
244
  if self.task_type != "implementation-option-selection":
253
245
  return []
254
- criteria = ", ".join(f"`{item}`" for item in EVALUATION_CRITERIA)
255
- requirement_ids = ", ".join(
256
- f"`{item}`" for item in self.original_requirement_ids
257
- )
258
- analysers = ", ".join(
259
- f"`{item}`" for item in self.participating_analysers
246
+ return selection_requirement_instructions(
247
+ self.original_requirement_ids, self.participating_analysers,
260
248
  )
261
- return [
262
- f"Original requirement ids, in order: {requirement_ids}.",
263
- f"Participating analysers: {analysers}.",
264
- f"Evaluation criteria, in exact order: {criteria}.",
265
- "Every `rankedOptions` and `candidateAudit` row must contain one "
266
- "`requirementCoverage` row per original requirement id and one "
267
- "`criterionScores` row per evaluation criterion. Weights and scores "
268
- f"are integers from {MIN_CRITERION_VALUE} through {MAX_CRITERION_VALUE}; "
269
- "`weightedScore` is the recalculated weighted mean.",
270
- "`proposedBy` names exactly one participating analyser id, never a "
271
- "list. Counting `rankedOptions` and `candidateAudit` together, each "
272
- f"analyser may propose at most {MAX_RAW_CANDIDATES_PER_ANALYSER} raw "
273
- "candidates and the run may hold at most "
274
- f"{MAX_RAW_CANDIDATES_PER_ANALYSER} times the analyser count.",
275
- "`coverageSummary` is recalculated from the row's `requirementCoverage` "
276
- "statuses and `scopeCommitments`, and every field must equal the "
277
- "recalculation: `totalCount` = number of original requirement ids; "
278
- "`coveredCount` = rows with status `covered`; `coveragePercent` = "
279
- "round(coveredCount / totalCount * 100, 2); `unmappedCommitments` = "
280
- "commitment ids whose `requirementIds` is empty or names an id outside "
281
- "the original set; `scopePrecisionPercent` = round((commitments - "
282
- "unmapped) / commitments * 100, 2); `contradictedRequirements` = "
283
- "original ids with status `contradicted`, in original order; "
284
- "`coverageVerdict` = the first that applies of "
285
- + ", ".join(f"`{verdict}`" for verdict in COVERAGE_VERDICT_PRECEDENCE)
286
- + " (any contradicted; coveredCount below totalCount; any unmapped; "
287
- "otherwise).",
288
- "A ranked option is valid only when every participating analyser "
289
- "supplied one feasibility vote, at least "
290
- f"{MIN_FEASIBLE_VOTES} votes are `feasible`, and both `safetyBlockers` "
291
- "and `unresolvedFeasibilityFacts` are empty.",
292
- ("Classify each `unresolvedFeasibilityFacts` entry with `resolutionKind`: "
293
- "`user-decision` requires nonempty `clarificationRefs` linking that fact "
294
- "to actual C-NNN records, including answered records; `technical-verification` "
295
- "requires no user question. Preserve the fact, whyItMatters, and evidence. "
296
- "Historical unclassified facts remain readable but require classification "
297
- "before blocked report reassembly. Saving does not lift blocked routing."),
298
- ("After a linked decision is answered, preserve its disposition and selected "
299
- "value. Update humanSummary.actions, verdictCard.nextStep, and selection "
300
- "guidance to name remaining technical verification separately from unanswered "
301
- "user decisions; never reopen an answered question to permit saving."),
302
- "Each `feasibilityVotes` row states that analyser's own verdict, "
303
- "rationale, and strongest counterevidence as its result gives them. "
304
- "Two non-`uncertain` votes with identical rationale and "
305
- "counterevidence are rejected as one worker's text copied under "
306
- "another's name.",
307
- f"Rank at most {MAX_RANKED_OPTIONS} valid options by descending "
308
- "`requirement-fit`, `correctness-risk`, `architecture-fit`, "
309
- "`weightedScore`, then id. With valid options set routing to "
310
- f"`{CANDIDATE_COMPARISON_ROUTING}`; without a valid option use "
311
- f"`{NO_VALID_OPTIONS_ROUTING}`.",
312
- "In `candidate-comparison`, keep `preselectedDirection` null and do "
313
- "not route directly to `implementation-planning`. In "
314
- "`preselected-validation`, emit exactly one validated option, an empty "
315
- "`candidateAudit`, a cited `preselectedDirection`, and routing "
316
- "`implementation-planning`.",
317
- ]
318
249
 
319
250
  def _planning_requirement_instructions(self) -> list[str]:
320
251
  """계획 저작 계약에 브리프 요구사항 id 순서를 박아 넣는다.
@@ -0,0 +1,38 @@
1
+ """보고서 검증 시 파일 이름에서 실행 식별자를 읽는다."""
2
+
3
+ from pathlib import Path
4
+ import re
5
+
6
+ _REPORT_BASENAME_SEQ_RE = re.compile(r"-(?P<seq>\d{3})(?:\.data)?\.(?:md|json)$")
7
+
8
+
9
+ _REPORT_BASENAME_TASK_TYPE_RE = re.compile(
10
+ r"^final-report-(?P<task_type>[a-z][a-z-]*?)-\d{3}(?:\.data)?\.(?:md|json)$"
11
+ )
12
+
13
+
14
+ def _report_run_seq(report_path: Path) -> str | None:
15
+ """This run's seq, read off `final-report-<task-type>-<seq>.md`. ``None``
16
+ when the name does not carry one, so callers fall back to not filtering
17
+ rather than silently checking nothing."""
18
+ match = _REPORT_BASENAME_SEQ_RE.search(report_path.name)
19
+ return match.group("seq") if match else None
20
+
21
+
22
+ def _report_task_type(report_path: Path) -> str:
23
+ """This run's task type, read off `final-report-<task-type>-<seq>`.
24
+
25
+ The report's own `header.taskType` is the first source, but it is not
26
+ always reachable. `report_narrative._allowed_top_level()` has no `header` —
27
+ it is not a writer-owned block — so a gate scored from a narrative, which is
28
+ the only input a report-contract-3.0 run has before assembly, carries no
29
+ task type at all. Globbing with an empty one matched nothing and reported
30
+ every verdict as unbacked under a `runs//worker-results/` path.
31
+
32
+ The filename carries it in every caller: the full-run path passes the report
33
+ itself, and `_report_path_for_state` builds the same canonical name from the
34
+ state file. Returns `""` when the name does not carry one, so a caller can
35
+ tell "not resolvable" from a real task type.
36
+ """
37
+ match = _REPORT_BASENAME_TASK_TYPE_RE.match(report_path.name)
38
+ return match.group("task_type") if match else ""
@@ -41,7 +41,7 @@ from .user_response_values import ( # noqa: F401 — 재노출(값 타입은 le
41
41
  )
42
42
  from .final_report_paths import final_report_data_path
43
43
  from .json_boundary import JsonBoundaryError, load_owned_object
44
- from .report_html.view_models.implementation_planning import (
44
+ from .plan_approval import (
45
45
  PlanApprovalState,
46
46
  plan_approval_state,
47
47
  resolve_recommended_option,