phase-loop-runtime 0.2.0__py3-none-any.whl

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 (370) hide show
  1. phase_loop_runtime/__init__.py +20 -0
  2. phase_loop_runtime/_contract_docs/phase-loop/extraction-readiness.md +90 -0
  3. phase_loop_runtime/_contract_docs/phase-loop/granular-execution-policy.md +137 -0
  4. phase_loop_runtime/_contract_docs/phase-loop/harness-capability-matrix.md +510 -0
  5. phase_loop_runtime/_contract_docs/phase-loop/harness-skill-matrix.md +125 -0
  6. phase_loop_runtime/_contract_docs/phase-loop/harness-substrate-manifest.md +232 -0
  7. phase_loop_runtime/_contract_docs/phase-loop/pi-loop-control.md +247 -0
  8. phase_loop_runtime/_contract_docs/phase-loop/protocol.md +2759 -0
  9. phase_loop_runtime/_contract_docs/phase-loop/runtime-boundary.md +289 -0
  10. phase_loop_runtime/_contract_docs/phase-loop/spec-discovery-roots.md +47 -0
  11. phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-governed-pipeline.md +42 -0
  12. phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-portal-projection.md +34 -0
  13. phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-regenesis.md +29 -0
  14. phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-report.md +32 -0
  15. phase_loop_runtime/_contract_docs/runtime/verification-evidence-contract.md +52 -0
  16. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/commands.txt +2 -0
  17. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/manifest.json +16 -0
  18. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/mixed.json +5 -0
  19. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-provider-timeout/manifest.json +16 -0
  20. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-provider-timeout/timeout-note.txt +2 -0
  21. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-redacted-summary/manifest.json +16 -0
  22. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-redacted-summary/redacted-summary.json +6 -0
  23. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-small-sample/manifest.json +16 -0
  24. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-small-sample/sample.json +7 -0
  25. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-threshold-edge/edge.txt +2 -0
  26. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-threshold-edge/manifest.json +16 -0
  27. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/manifest.json +16 -0
  28. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-a.txt +1 -0
  29. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-b.txt +1 -0
  30. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-c.txt +1 -0
  31. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-missing-references/claimed-results.json +5 -0
  32. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-missing-references/manifest.json +16 -0
  33. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-subtle-low-variance/manifest.json +16 -0
  34. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-subtle-low-variance/metrics.json +9 -0
  35. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-templated-prose/findings.md +10 -0
  36. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-templated-prose/manifest.json +16 -0
  37. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/manifest.json +16 -0
  38. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/scores.json +4 -0
  39. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/summary.txt +1 -0
  40. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/bundle-check.json +8 -0
  41. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/manifest.json +16 -0
  42. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/operator-note.txt +2 -0
  43. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/events-sample.jsonl +3 -0
  44. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/manifest.json +16 -0
  45. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/receipt.txt +1 -0
  46. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/evidence.json +13 -0
  47. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/manifest.json +16 -0
  48. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/summary.txt +2 -0
  49. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/manifest.json +16 -0
  50. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/matrix.json +9 -0
  51. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/notes.md +4 -0
  52. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/console-summary.txt +2 -0
  53. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/manifest.json +16 -0
  54. phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/screenshot-standin.ppm +5 -0
  55. phase_loop_runtime/adoption_bundle.py +273 -0
  56. phase_loop_runtime/advisor_board/CONTRACTS.md +257 -0
  57. phase_loop_runtime/advisor_board/__init__.py +288 -0
  58. phase_loop_runtime/advisor_board/backing.py +137 -0
  59. phase_loop_runtime/advisor_board/backing_omnigent.py +498 -0
  60. phase_loop_runtime/advisor_board/config.py +225 -0
  61. phase_loop_runtime/advisor_board/events.py +115 -0
  62. phase_loop_runtime/advisor_board/fixtures/advisor-boards.example.toml +59 -0
  63. phase_loop_runtime/advisor_board/fixtures.py +91 -0
  64. phase_loop_runtime/advisor_board/harness_mapping.py +125 -0
  65. phase_loop_runtime/advisor_board/matrix.py +146 -0
  66. phase_loop_runtime/advisor_board/observability.py +481 -0
  67. phase_loop_runtime/advisor_board/presets.py +188 -0
  68. phase_loop_runtime/advisor_board/registries.py +309 -0
  69. phase_loop_runtime/advisor_board/resolver.py +253 -0
  70. phase_loop_runtime/advisor_board/schema.py +269 -0
  71. phase_loop_runtime/advisor_board/standin.py +166 -0
  72. phase_loop_runtime/advisor_board/validation.py +159 -0
  73. phase_loop_runtime/agent_runtime_provider.py +676 -0
  74. phase_loop_runtime/baml_modular.py +656 -0
  75. phase_loop_runtime/baml_src/dotfiles_adoption_manifest.baml +37 -0
  76. phase_loop_runtime/baml_src/dotfiles_c4_document.baml +14 -0
  77. phase_loop_runtime/baml_src/dotfiles_plan_manifest.baml +50 -0
  78. phase_loop_runtime/baml_src/dotfiles_runtime_projection.baml +16 -0
  79. phase_loop_runtime/baml_src/dotfiles_task_catalog.baml +20 -0
  80. phase_loop_runtime/baml_src/emit_phase_closeout.baml +86 -0
  81. phase_loop_runtime/baml_src/evaluate_suspected_fake_evidence.baml +36 -0
  82. phase_loop_runtime/baml_src/verification_evidence.baml +40 -0
  83. phase_loop_runtime/broker.py +181 -0
  84. phase_loop_runtime/build_bundle.py +294 -0
  85. phase_loop_runtime/capability_registry.py +835 -0
  86. phase_loop_runtime/classifier.py +87 -0
  87. phase_loop_runtime/claude_agent_view.py +625 -0
  88. phase_loop_runtime/claude_channel_sidecar.py +1086 -0
  89. phase_loop_runtime/cli.py +2599 -0
  90. phase_loop_runtime/closeout.py +865 -0
  91. phase_loop_runtime/closeout_classifier.py +120 -0
  92. phase_loop_runtime/closeout_evidence_audit.py +112 -0
  93. phase_loop_runtime/closeout_validation.py +144 -0
  94. phase_loop_runtime/closeout_validators.py +203 -0
  95. phase_loop_runtime/consiliency_gates.py +374 -0
  96. phase_loop_runtime/consiliency_ingest.py +399 -0
  97. phase_loop_runtime/consiliency_layout.py +176 -0
  98. phase_loop_runtime/consiliency_scaffold.py +305 -0
  99. phase_loop_runtime/cross_repo_channel.py +331 -0
  100. phase_loop_runtime/discovery.py +2368 -0
  101. phase_loop_runtime/dispatch_lock.py +167 -0
  102. phase_loop_runtime/doc_delta_validator.py +98 -0
  103. phase_loop_runtime/docs_audit.py +285 -0
  104. phase_loop_runtime/docs_freshness.py +468 -0
  105. phase_loop_runtime/docs_surfaces.py +150 -0
  106. phase_loop_runtime/dotfiles_profile_plugin.py +96 -0
  107. phase_loop_runtime/events.py +154 -0
  108. phase_loop_runtime/events_migration.py +96 -0
  109. phase_loop_runtime/evidence_audit.py +1082 -0
  110. phase_loop_runtime/evidence_audit_config.py +172 -0
  111. phase_loop_runtime/fleet_map.py +426 -0
  112. phase_loop_runtime/fleet_metrics.py +298 -0
  113. phase_loop_runtime/fleet_metrics_export.py +196 -0
  114. phase_loop_runtime/git_discipline.py +479 -0
  115. phase_loop_runtime/git_ops.py +122 -0
  116. phase_loop_runtime/git_topology.py +252 -0
  117. phase_loop_runtime/governed_bundle.py +137 -0
  118. phase_loop_runtime/governed_premerge.py +219 -0
  119. phase_loop_runtime/governed_review.py +264 -0
  120. phase_loop_runtime/handoff.py +932 -0
  121. phase_loop_runtime/handoff_path.py +14 -0
  122. phase_loop_runtime/injection.py +740 -0
  123. phase_loop_runtime/install_status.py +88 -0
  124. phase_loop_runtime/lane_scheduler.py +317 -0
  125. phase_loop_runtime/launcher.py +2760 -0
  126. phase_loop_runtime/lease_store.py +553 -0
  127. phase_loop_runtime/maintenance.py +401 -0
  128. phase_loop_runtime/migrate_handoffs.py +201 -0
  129. phase_loop_runtime/models.py +2144 -0
  130. phase_loop_runtime/observability.py +1290 -0
  131. phase_loop_runtime/panel_invoker.py +1638 -0
  132. phase_loop_runtime/phase_loop_drift_audit.py +388 -0
  133. phase_loop_runtime/phase_worktree_executor.py +433 -0
  134. phase_loop_runtime/pipeline_adapter/__init__.py +12 -0
  135. phase_loop_runtime/pipeline_adapter/branch_ops.py +274 -0
  136. phase_loop_runtime/pipeline_adapter/flag.py +55 -0
  137. phase_loop_runtime/pipeline_adapter/markers.py +13 -0
  138. phase_loop_runtime/pipeline_adapter/merge_policy.py +72 -0
  139. phase_loop_runtime/pipeline_adapter/ratification.py +100 -0
  140. phase_loop_runtime/pipeline_adapter/sibling_matcher.py +123 -0
  141. phase_loop_runtime/plan_ir.py +523 -0
  142. phase_loop_runtime/plan_manifest.py +415 -0
  143. phase_loop_runtime/planner_validation.py +176 -0
  144. phase_loop_runtime/profiles.py +462 -0
  145. phase_loop_runtime/prompts.py +365 -0
  146. phase_loop_runtime/provenance.py +165 -0
  147. phase_loop_runtime/publishing.py +339 -0
  148. phase_loop_runtime/py.typed +1 -0
  149. phase_loop_runtime/reconcile.py +1550 -0
  150. phase_loop_runtime/redaction.py +184 -0
  151. phase_loop_runtime/reflection_sync.py +293 -0
  152. phase_loop_runtime/release_guard.py +186 -0
  153. phase_loop_runtime/render.py +432 -0
  154. phase_loop_runtime/repo_validation.py +427 -0
  155. phase_loop_runtime/review_summary.py +150 -0
  156. phase_loop_runtime/roadmap_lint.py +425 -0
  157. phase_loop_runtime/route_log.py +59 -0
  158. phase_loop_runtime/route_policy/__init__.py +11 -0
  159. phase_loop_runtime/route_policy/fixtures/route_selection.golden.json +263 -0
  160. phase_loop_runtime/runner.py +8400 -0
  161. phase_loop_runtime/runtime_paths.py +225 -0
  162. phase_loop_runtime/runtime_projection.py +113 -0
  163. phase_loop_runtime/runtime_resources.py +36 -0
  164. phase_loop_runtime/schema_export.py +292 -0
  165. phase_loop_runtime/schemas/phase_loop_closeout.schema.json +366 -0
  166. phase_loop_runtime/skill_install.py +176 -0
  167. phase_loop_runtime/skill_inventory.py +662 -0
  168. phase_loop_runtime/skill_paths.py +42 -0
  169. phase_loop_runtime/skill_sources_plugin.py +37 -0
  170. phase_loop_runtime/skills_bundle/claude-advisor-board/README.md +1 -0
  171. phase_loop_runtime/skills_bundle/claude-advisor-board/SKILL.md +38 -0
  172. phase_loop_runtime/skills_bundle/claude-advisor-panel/README.md +1 -0
  173. phase_loop_runtime/skills_bundle/claude-advisor-panel/SKILL.md +38 -0
  174. phase_loop_runtime/skills_bundle/claude-execute-detailed/README.md +1 -0
  175. phase_loop_runtime/skills_bundle/claude-execute-detailed/SKILL.md +152 -0
  176. phase_loop_runtime/skills_bundle/claude-execute-phase/README.md +1 -0
  177. phase_loop_runtime/skills_bundle/claude-execute-phase/SKILL.md +745 -0
  178. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/allocate_worktree_name.sh +33 -0
  179. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/audit_lane_file_touches.py +197 -0
  180. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
  181. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
  182. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
  183. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
  184. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
  185. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/state.py +274 -0
  186. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
  187. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/team_teardown.sh +32 -0
  188. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/validate_plan_doc.py +642 -0
  189. phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/verify_harness.sh +96 -0
  190. phase_loop_runtime/skills_bundle/claude-phase-loop/README.md +1 -0
  191. phase_loop_runtime/skills_bundle/claude-phase-loop/SKILL.md +115 -0
  192. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/README.md +1 -0
  193. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/SKILL.md +439 -0
  194. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/assets/review_prompt.md +19 -0
  195. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
  196. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/references/roadmap-template.md +193 -0
  197. phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
  198. phase_loop_runtime/skills_bundle/claude-plan-detailed/README.md +1 -0
  199. phase_loop_runtime/skills_bundle/claude-plan-detailed/SKILL.md +334 -0
  200. phase_loop_runtime/skills_bundle/claude-plan-detailed/assets/review_prompt.md +25 -0
  201. phase_loop_runtime/skills_bundle/claude-plan-phase/README.md +1 -0
  202. phase_loop_runtime/skills_bundle/claude-plan-phase/SKILL.md +776 -0
  203. phase_loop_runtime/skills_bundle/claude-plan-phase/assets/review_prompt.md +17 -0
  204. phase_loop_runtime/skills_bundle/claude-plan-phase/scripts/validate_plan_doc.py +1092 -0
  205. phase_loop_runtime/skills_bundle/claude-run-train/README.md +1 -0
  206. phase_loop_runtime/skills_bundle/claude-run-train/SKILL.md +75 -0
  207. phase_loop_runtime/skills_bundle/claude-skill-editor/README.md +1 -0
  208. phase_loop_runtime/skills_bundle/claude-skill-editor/SKILL.md +243 -0
  209. phase_loop_runtime/skills_bundle/claude-skill-editor/assets/editor_prompt.md +72 -0
  210. phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/README.md +1 -0
  211. phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/SKILL.md +249 -0
  212. phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
  213. phase_loop_runtime/skills_bundle/claude-task-contextualizer/README.md +1 -0
  214. phase_loop_runtime/skills_bundle/claude-task-contextualizer/SKILL.md +74 -0
  215. phase_loop_runtime/skills_bundle/claude-task-contextualizer/references/task-templates.md +103 -0
  216. phase_loop_runtime/skills_bundle/codex-advisor-board/README.md +1 -0
  217. phase_loop_runtime/skills_bundle/codex-advisor-board/SKILL.md +38 -0
  218. phase_loop_runtime/skills_bundle/codex-advisor-panel/README.md +1 -0
  219. phase_loop_runtime/skills_bundle/codex-advisor-panel/SKILL.md +38 -0
  220. phase_loop_runtime/skills_bundle/codex-execute-detailed/README.md +1 -0
  221. phase_loop_runtime/skills_bundle/codex-execute-detailed/SKILL.md +154 -0
  222. phase_loop_runtime/skills_bundle/codex-execute-phase/README.md +1 -0
  223. phase_loop_runtime/skills_bundle/codex-execute-phase/SKILL.md +231 -0
  224. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/allocate_worktree_name.sh +33 -0
  225. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/audit_lane_file_touches.py +197 -0
  226. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
  227. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
  228. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
  229. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
  230. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
  231. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/state.py +274 -0
  232. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
  233. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/team_teardown.sh +32 -0
  234. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/validate_plan_doc.py +642 -0
  235. phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/verify_harness.sh +96 -0
  236. phase_loop_runtime/skills_bundle/codex-phase-loop/README.md +1 -0
  237. phase_loop_runtime/skills_bundle/codex-phase-loop/SKILL.md +248 -0
  238. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/README.md +1 -0
  239. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/SKILL.md +150 -0
  240. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/assets/review_prompt.md +19 -0
  241. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
  242. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/references/roadmap-template.md +193 -0
  243. phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
  244. phase_loop_runtime/skills_bundle/codex-plan-detailed/README.md +1 -0
  245. phase_loop_runtime/skills_bundle/codex-plan-detailed/SKILL.md +119 -0
  246. phase_loop_runtime/skills_bundle/codex-plan-detailed/assets/review_prompt.md +25 -0
  247. phase_loop_runtime/skills_bundle/codex-plan-phase/README.md +1 -0
  248. phase_loop_runtime/skills_bundle/codex-plan-phase/SKILL.md +307 -0
  249. phase_loop_runtime/skills_bundle/codex-plan-phase/assets/review_prompt.md +17 -0
  250. phase_loop_runtime/skills_bundle/codex-plan-phase/scripts/validate_plan_doc.py +1092 -0
  251. phase_loop_runtime/skills_bundle/codex-run-train/README.md +1 -0
  252. phase_loop_runtime/skills_bundle/codex-run-train/SKILL.md +75 -0
  253. phase_loop_runtime/skills_bundle/codex-skill-editor/README.md +1 -0
  254. phase_loop_runtime/skills_bundle/codex-skill-editor/SKILL.md +94 -0
  255. phase_loop_runtime/skills_bundle/codex-skill-editor/assets/editor_prompt.md +72 -0
  256. phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/README.md +1 -0
  257. phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/SKILL.md +114 -0
  258. phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
  259. phase_loop_runtime/skills_bundle/codex-task-contextualizer/README.md +1 -0
  260. phase_loop_runtime/skills_bundle/codex-task-contextualizer/SKILL.md +102 -0
  261. phase_loop_runtime/skills_bundle/codex-task-contextualizer/references/task-templates.md +103 -0
  262. phase_loop_runtime/skills_bundle/gemini-advisor-board/README.md +1 -0
  263. phase_loop_runtime/skills_bundle/gemini-advisor-board/SKILL.md +38 -0
  264. phase_loop_runtime/skills_bundle/gemini-advisor-panel/README.md +1 -0
  265. phase_loop_runtime/skills_bundle/gemini-advisor-panel/SKILL.md +38 -0
  266. phase_loop_runtime/skills_bundle/gemini-execute-detailed/README.md +1 -0
  267. phase_loop_runtime/skills_bundle/gemini-execute-detailed/SKILL.md +144 -0
  268. phase_loop_runtime/skills_bundle/gemini-execute-phase/README.md +1 -0
  269. phase_loop_runtime/skills_bundle/gemini-execute-phase/SKILL.md +198 -0
  270. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/allocate_worktree_name.sh +33 -0
  271. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/audit_lane_file_touches.py +197 -0
  272. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
  273. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
  274. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
  275. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
  276. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
  277. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/state.py +274 -0
  278. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
  279. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/team_teardown.sh +32 -0
  280. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/validate_plan_doc.py +642 -0
  281. phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/verify_harness.sh +96 -0
  282. phase_loop_runtime/skills_bundle/gemini-phase-loop/README.md +1 -0
  283. phase_loop_runtime/skills_bundle/gemini-phase-loop/SKILL.md +111 -0
  284. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/README.md +1 -0
  285. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/SKILL.md +147 -0
  286. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/assets/review_prompt.md +19 -0
  287. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
  288. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/references/roadmap-template.md +193 -0
  289. phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
  290. phase_loop_runtime/skills_bundle/gemini-plan-detailed/README.md +1 -0
  291. phase_loop_runtime/skills_bundle/gemini-plan-detailed/SKILL.md +119 -0
  292. phase_loop_runtime/skills_bundle/gemini-plan-detailed/assets/review_prompt.md +25 -0
  293. phase_loop_runtime/skills_bundle/gemini-plan-phase/README.md +1 -0
  294. phase_loop_runtime/skills_bundle/gemini-plan-phase/SKILL.md +217 -0
  295. phase_loop_runtime/skills_bundle/gemini-plan-phase/assets/review_prompt.md +17 -0
  296. phase_loop_runtime/skills_bundle/gemini-plan-phase/scripts/validate_plan_doc.py +1092 -0
  297. phase_loop_runtime/skills_bundle/gemini-run-train/README.md +1 -0
  298. phase_loop_runtime/skills_bundle/gemini-run-train/SKILL.md +75 -0
  299. phase_loop_runtime/skills_bundle/gemini-skill-editor/README.md +1 -0
  300. phase_loop_runtime/skills_bundle/gemini-skill-editor/SKILL.md +87 -0
  301. phase_loop_runtime/skills_bundle/gemini-skill-editor/assets/editor_prompt.md +72 -0
  302. phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/README.md +1 -0
  303. phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/SKILL.md +109 -0
  304. phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
  305. phase_loop_runtime/skills_bundle/gemini-task-contextualizer/README.md +1 -0
  306. phase_loop_runtime/skills_bundle/gemini-task-contextualizer/SKILL.md +98 -0
  307. phase_loop_runtime/skills_bundle/gemini-task-contextualizer/references/task-templates.md +103 -0
  308. phase_loop_runtime/skills_bundle/opencode-advisor-board/README.md +1 -0
  309. phase_loop_runtime/skills_bundle/opencode-advisor-board/SKILL.md +38 -0
  310. phase_loop_runtime/skills_bundle/opencode-advisor-panel/README.md +1 -0
  311. phase_loop_runtime/skills_bundle/opencode-advisor-panel/SKILL.md +38 -0
  312. phase_loop_runtime/skills_bundle/opencode-execute-detailed/README.md +1 -0
  313. phase_loop_runtime/skills_bundle/opencode-execute-detailed/SKILL.md +144 -0
  314. phase_loop_runtime/skills_bundle/opencode-execute-phase/README.md +1 -0
  315. phase_loop_runtime/skills_bundle/opencode-execute-phase/SKILL.md +196 -0
  316. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/allocate_worktree_name.sh +33 -0
  317. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/audit_lane_file_touches.py +197 -0
  318. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
  319. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
  320. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
  321. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
  322. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
  323. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/state.py +274 -0
  324. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
  325. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/team_teardown.sh +32 -0
  326. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/validate_plan_doc.py +642 -0
  327. phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/verify_harness.sh +96 -0
  328. phase_loop_runtime/skills_bundle/opencode-phase-loop/README.md +1 -0
  329. phase_loop_runtime/skills_bundle/opencode-phase-loop/SKILL.md +113 -0
  330. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/README.md +1 -0
  331. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/SKILL.md +147 -0
  332. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/assets/review_prompt.md +19 -0
  333. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
  334. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/references/roadmap-template.md +193 -0
  335. phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
  336. phase_loop_runtime/skills_bundle/opencode-plan-detailed/README.md +1 -0
  337. phase_loop_runtime/skills_bundle/opencode-plan-detailed/SKILL.md +119 -0
  338. phase_loop_runtime/skills_bundle/opencode-plan-detailed/assets/review_prompt.md +25 -0
  339. phase_loop_runtime/skills_bundle/opencode-plan-phase/README.md +1 -0
  340. phase_loop_runtime/skills_bundle/opencode-plan-phase/SKILL.md +211 -0
  341. phase_loop_runtime/skills_bundle/opencode-plan-phase/assets/review_prompt.md +17 -0
  342. phase_loop_runtime/skills_bundle/opencode-plan-phase/scripts/validate_plan_doc.py +1092 -0
  343. phase_loop_runtime/skills_bundle/opencode-run-train/README.md +1 -0
  344. phase_loop_runtime/skills_bundle/opencode-run-train/SKILL.md +75 -0
  345. phase_loop_runtime/skills_bundle/opencode-skill-editor/README.md +1 -0
  346. phase_loop_runtime/skills_bundle/opencode-skill-editor/SKILL.md +87 -0
  347. phase_loop_runtime/skills_bundle/opencode-skill-editor/assets/editor_prompt.md +72 -0
  348. phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/README.md +1 -0
  349. phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/SKILL.md +109 -0
  350. phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
  351. phase_loop_runtime/skills_bundle/opencode-task-contextualizer/README.md +1 -0
  352. phase_loop_runtime/skills_bundle/opencode-task-contextualizer/SKILL.md +98 -0
  353. phase_loop_runtime/skills_bundle/opencode-task-contextualizer/references/task-templates.md +103 -0
  354. phase_loop_runtime/state.py +156 -0
  355. phase_loop_runtime/state_degradation.py +136 -0
  356. phase_loop_runtime/state_ops.py +676 -0
  357. phase_loop_runtime/train_ledger.py +215 -0
  358. phase_loop_runtime/train_roadmap.py +339 -0
  359. phase_loop_runtime/train_runner.py +1197 -0
  360. phase_loop_runtime/verification_evidence.py +650 -0
  361. phase_loop_runtime/verification_evidence_validator.py +75 -0
  362. phase_loop_runtime/visual_evidence_validator.py +36 -0
  363. phase_loop_runtime/worker_pool.py +123 -0
  364. phase_loop_runtime/worktree_index.py +270 -0
  365. phase_loop_runtime-0.2.0.data/data/share/phase-loop-runtime/protocol/protocol.md +2475 -0
  366. phase_loop_runtime-0.2.0.dist-info/METADATA +106 -0
  367. phase_loop_runtime-0.2.0.dist-info/RECORD +370 -0
  368. phase_loop_runtime-0.2.0.dist-info/WHEEL +5 -0
  369. phase_loop_runtime-0.2.0.dist-info/entry_points.txt +9 -0
  370. phase_loop_runtime-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2144 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from datetime import datetime, timezone
5
+ import fnmatch
6
+ import hashlib
7
+ from pathlib import Path
8
+ from typing import Any, Iterable
9
+
10
+
11
+ PHASE_STATUSES = (
12
+ "unplanned",
13
+ "planned",
14
+ "executing",
15
+ "executed",
16
+ "awaiting_phase_closeout",
17
+ "complete",
18
+ "blocked",
19
+ "unknown",
20
+ )
21
+ EVENT_STATUSES = PHASE_STATUSES + ("plan_skipped",)
22
+
23
+ COMMANDS = ("run", "resume", "status", "dry-run", "maintain-skills", "monitor", "hotfix")
24
+ MODEL_PROFILES = ("roadmap", "plan", "execute", "repair", "review", "skill-maintenance")
25
+ EXECUTORS = ("codex", "claude", "gemini", "opencode", "pi", "command", "manual")
26
+ # Vendor-agnostic model roles (model-routing-v1). "class" not "tier" — tier
27
+ # already denotes evidence-audit budgets (--tier-2/--tier-3).
28
+ MODEL_CLASSES = ("planner", "implementer", "worker")
29
+ WORK_UNIT_KINDS = (
30
+ "roadmap_build",
31
+ "phase_plan",
32
+ "lane_execute",
33
+ "lane_review",
34
+ "phase_reducer",
35
+ "phase_verify",
36
+ "repair",
37
+ "closeout",
38
+ )
39
+ HOTFIX_WORK_UNITS = ("hotfix",)
40
+ HARNESS_WORK_UNIT_PROMPT_KINDS = (
41
+ "implementation",
42
+ "review",
43
+ "reducer",
44
+ "verify",
45
+ "closeout",
46
+ )
47
+ WORK_UNIT_STATUSES = (
48
+ "pending",
49
+ "running",
50
+ "complete",
51
+ "blocked",
52
+ "skipped",
53
+ "superseded",
54
+ "awaiting-closeout",
55
+ )
56
+ WORK_UNIT_METRIC_SCHEMA_VERSION = "work_unit_metric.v1"
57
+ NORMALIZED_EFFORT_LEVELS = ("minimal", "low", "medium", "high", "xhigh", "max")
58
+ UNSUPPORTED_POLICY_BEHAVIORS = ("block", "fallback", "inherit_default")
59
+ CLOSEOUT_MODES = ("manual", "commit", "push")
60
+ # v45 IF-0-FOUND-1: cross-phase scheduler surface. "off"/"serialized" preserve
61
+ # today's one-phase-at-a-time behavior; "concurrent" dispatches the full ready
62
+ # wave. Wiring into the runner main-loop lands in v45 Phase 3 (SCHED).
63
+ PHASE_SCHEDULER_MODES = ("off", "serialized", "concurrent")
64
+ # Closeout-exception vocabulary (roadmap v40 PROTO freeze). GATE/BREAKGLASS import
65
+ # these; they do not redefine them. Sensitivity classes drive the graduated gate:
66
+ # SAFE classes may auto-pass as a recorded soft exception; UNSAFE classes require an
67
+ # explicit break-glass reason. Any path that matches no class is treated as UNSAFE
68
+ # (deny-by-default).
69
+ SAFE_SENSITIVITY_CLASSES = ("docs", "plans", "handoffs", "config_nonsource")
70
+ UNSAFE_SENSITIVITY_CLASSES = ("source", "ci", "secrets", "lockfile")
71
+ SENSITIVITY_CLASSES = SAFE_SENSITIVITY_CLASSES + UNSAFE_SENSITIVITY_CLASSES
72
+ CLOSEOUT_EXCEPTION_KINDS = ("soft", "break_glass")
73
+ CLOSEOUT_EXCEPTIONS_METADATA_KEY = "closeout_exceptions"
74
+ PERMISSION_POSTURES = ("explicit", "permissive", "manual", "unknown")
75
+ SUBAGENT_POSTURES = ("native", "limited", "none", "unknown")
76
+ CLAUDE_EXECUTION_MODES = ("solo", "subagent", "agent_team")
77
+ CLAUDE_WORKTREE_POSTURES = ("phase_loop_managed", "manual_only")
78
+ DISPATCH_HINT_FIELDS = (
79
+ "preferred_executors",
80
+ "allowed_executors",
81
+ "fallback_executors",
82
+ "disabled_executors",
83
+ "required_capabilities",
84
+ )
85
+ DISPATCH_CAPABILITIES = (
86
+ "live_launch",
87
+ "dry_run",
88
+ "skill_bundle_injection",
89
+ "inline_instructions",
90
+ "context_file_instructions",
91
+ "manual_handoff",
92
+ "subagents",
93
+ "explicit_approval_controls",
94
+ "structured_output",
95
+ "browser_automation",
96
+ )
97
+ DISPATCH_SELECTION_PATHS = ("fixed_action_policy", "preferred", "fallback")
98
+ DELEGATION_PRIORITIES = ("low", "normal", "high", "urgent")
99
+ DELEGATION_STATUSES = ("approved", "denied")
100
+ LIVE_PROOF_GATES = ("none", "disposable_proof_required", "disposable_proof_recorded")
101
+ AUTH_PREFLIGHT_MODES = ("none", "metadata_only")
102
+ TIMEOUT_POSTURES = ("runner_managed", "executor_managed", "unknown")
103
+ OUTPUT_CAPTURE_FORMATS = ("combined_output", "json_stream", "terminal_summary")
104
+ PROMOTION_STATUSES = ("live", "proof_gated", "manual_only")
105
+ FAILURE_KINDS = ("adapter_failure", "phase_failure")
106
+ BLOCKER_POSTURES = ("human_required", "repairable_non_human")
107
+ OPERATOR_MATURITY_LABELS = ("live_supported", "proof_blocked", "experimental", "manual_only")
108
+
109
+ BLOCKER_CLASSES = (
110
+ "missing_secret",
111
+ "account_or_billing_setup",
112
+ "admin_approval",
113
+ "destructive_operation",
114
+ "ambiguous_roadmap_selection",
115
+ "product_decision_missing",
116
+ "dirty_worktree_conflict",
117
+ "branch_sync_conflict",
118
+ "stalled_child_observation",
119
+ "repeated_verification_failure",
120
+ "sandbox_command_restriction",
121
+ "upstream_phase_unmet",
122
+ "contract_bug",
123
+ "gold_record_amendment",
124
+ "closeout_evidence_drift",
125
+ "closeout_scope_violation",
126
+ "unretryable_external_outage",
127
+ "stuck_loop",
128
+ "merge_conflict",
129
+ "operator_override_missing_reason",
130
+ "concurrent_dispatch",
131
+ "verification_evidence_missing",
132
+ "review_gate_block",
133
+ "docs_freshness_stale",
134
+ "consiliency_gate_blocked",
135
+ )
136
+
137
+ LANE_IR_DIAGNOSTIC_KINDS = (
138
+ "cycle",
139
+ "overlapping_write_ownership",
140
+ "unsafe_concurrent_lane",
141
+ "stale_worktree_assignment",
142
+ "active_work_unit",
143
+ "human_required_blocked_work_unit",
144
+ "missing_producer_dependency",
145
+ "missing_owned_files",
146
+ "malformed_owned_files",
147
+ "malformed_dependencies",
148
+ "unsupported_lane_policy",
149
+ "missing_lane_sections",
150
+ )
151
+
152
+ LANE_REDUCER_KINDS = (
153
+ "none",
154
+ "acceptance_reducer",
155
+ "compatibility_reducer",
156
+ "verification_reducer",
157
+ "summary_reducer",
158
+ )
159
+
160
+ LANE_SCHEDULER_MODES = ("off", "serialized", "concurrent")
161
+ LANE_WAVE_STATUSES = ("ready", "blocked", "empty")
162
+ WORKTREE_ISOLATION_MODES = ("main_worktree", "git_worktree")
163
+ DIRTY_PATH_CLASSIFICATIONS = (
164
+ "pre_existing",
165
+ "lane_owned",
166
+ "peer_owned",
167
+ "reducer_owned",
168
+ "unowned",
169
+ )
170
+ PIPELINE_MODE_LITERALS = ("standalone", "pipeline_optional", "pipeline_required")
171
+ PIPELINE_PROTECTED_SOURCE_CATEGORIES = (
172
+ "specs",
173
+ "diagrams",
174
+ "adapter_config",
175
+ "definition_files",
176
+ "portal_contracts",
177
+ "phase_artifacts",
178
+ )
179
+ PIPELINE_PROTECTED_SOURCE_ROLES = (
180
+ "seed_spec",
181
+ "predecessor_spec",
182
+ "active_canonical_spec",
183
+ "archived_spec",
184
+ "managed_mirror_file",
185
+ "unmanaged_spec_input",
186
+ "legacy_specs_bundle",
187
+ "root_specs_intake",
188
+ "pipeline_specs_canonical",
189
+ "adapter_configured_intake_root",
190
+ "mirror_manifest",
191
+ "archive_manifest",
192
+ )
193
+ PIPELINE_PROTECTED_SOURCE_LEGACY_ROLES = ("protected",)
194
+ PIPELINE_METADATA_DIAGNOSTIC_KINDS = (
195
+ "invalid_pipeline_mode",
196
+ "missing_source_bundle",
197
+ "missing_source_bundle_sha256",
198
+ "missing_source_bundle_file",
199
+ "mismatched_source_bundle_sha256",
200
+ "malformed_source_bundle",
201
+ "unknown_phase_id",
202
+ "missing_protected_source_entries",
203
+ "missing_protected_source_file",
204
+ "mismatched_protected_source_sha256",
205
+ )
206
+ PHASE_SOURCE_BUNDLE_SCHEMA = "phase-source-bundle.v1"
207
+ PIPELINE_CLOSEOUT_SCHEMA = "phase_loop_closeout.v1"
208
+ PIPELINE_CLOSEOUT_OUTCOMES = (
209
+ "complete",
210
+ "blocked",
211
+ "stale_input",
212
+ "failed_verification",
213
+ "human_required",
214
+ )
215
+ CHANGED_PATH_CATEGORIES = (
216
+ "code",
217
+ "tests",
218
+ "docs",
219
+ "specs",
220
+ "active_canonical_spec",
221
+ "managed_root_mirror_spec",
222
+ "mirror_manifest",
223
+ "archive_manifest",
224
+ "archived_spec",
225
+ "unmanaged_spec",
226
+ "pipeline_sources",
227
+ "portal_contract_refs",
228
+ "greenfield_authority_refs",
229
+ "unknown",
230
+ )
231
+ CANONICAL_REFRESH_REASON_CODES = (
232
+ "docs_source_truth_touched",
233
+ "specs_source_truth_touched",
234
+ "active_specs_touched",
235
+ "managed_mirror_specs_touched",
236
+ "mirror_manifests_touched",
237
+ "archive_manifests_touched",
238
+ "archived_specs_touched",
239
+ "unmanaged_specs_touched",
240
+ "adoption_contracts_touched",
241
+ "contract_refs_touched",
242
+ "pipeline_sources_touched",
243
+ "portal_contract_refs_touched",
244
+ "greenfield_authority_refs_touched",
245
+ )
246
+ REDACTION_POSTURES = ("metadata_only", "rejected_forbidden_metadata")
247
+ SPEC_DELTA_CLOSEOUT_SCHEMA = "spec_delta_closeout.v1"
248
+ SPEC_DELTA_DECISIONS = (
249
+ "no_spec_delta",
250
+ "roadmap_amendment",
251
+ "canonical_spec_update",
252
+ "governed_pipeline_refresh",
253
+ "mirror_cutover_required",
254
+ "dotfiles_skill_source_update",
255
+ "human_source_judgment_required",
256
+ )
257
+ SPEC_DELTA_TARGET_SURFACES = (
258
+ "shared/phase-loop/protocol.md",
259
+ "vendor/phase-loop-runtime/protocol/protocol.md",
260
+ "vendor/phase-loop-runtime/src/phase_loop_runtime/baml_src/emit_phase_closeout.baml",
261
+ "codex-config/skills/**",
262
+ "claude-config/claude-skills/**",
263
+ "gemini-config/skills/**",
264
+ "opencode-config/skills/**",
265
+ "vendor/phase-loop-skills/**",
266
+ "vendor/phase-loop-runtime/tests/**",
267
+ )
268
+
269
+ # --- rigor-v1 P1: shared review-gate contracts -----------------------------
270
+ # One canonical term for a phase's definition of done, replacing the fragmented
271
+ # "Acceptance criteria" / "Exit criteria" vocabulary across skills/validators.
272
+ DEFINITION_OF_DONE_TERM = "acceptance_criteria"
273
+
274
+ # doc_delta_closeout.v1 — code↔doc currency decision, mirroring spec_delta.
275
+ DOC_DELTA_CLOSEOUT_SCHEMA = "doc_delta_closeout.v1"
276
+ DOC_DELTA_DECISIONS = (
277
+ "no_doc_delta", # public surface unchanged, or change needs no docs
278
+ "docs_updated", # docs updated to match the change
279
+ "docs_follow_up_filed", # tracked for a later phase
280
+ )
281
+
282
+ # Globs whose change implies a user-visible public surface that docs may track.
283
+ PUBLIC_SURFACE_GLOBS = (
284
+ "**/cli.py",
285
+ "**/*.proto",
286
+ "**/openapi*.json",
287
+ "**/openapi*.yaml",
288
+ "**/*.openapi.*",
289
+ "**/schema*.json",
290
+ "README.md",
291
+ "**/README.md",
292
+ "CHANGELOG.md",
293
+ "CHANGELOG*",
294
+ "**/CHANGELOG.md",
295
+ "RELEASE_NOTES*",
296
+ "RELEASES*",
297
+ "**/_contract_docs/**",
298
+ )
299
+
300
+ # Verification-evidence policy: OFF by default (warn-only) to preserve autonomy;
301
+ # opt-in promotes a missing-evidence finding to `block`. Declining evidence when
302
+ # opted in records one of these typed reason codes.
303
+ VERIFICATION_EVIDENCE_REQUIRED_DEFAULT = False
304
+ VERIFICATION_EVIDENCE_OPT_OUT_REASONS = (
305
+ "no_executable_verification",
306
+ "verification_deferred_to_later_phase",
307
+ "operator_attested_manual",
308
+ )
309
+
310
+ # UI/visual surfaces: a change here means a screenshot/visual check is *expected*
311
+ # (absence is a `warn` finding by default).
312
+ UI_GLOBS = (
313
+ "**/*.tsx",
314
+ "**/*.jsx",
315
+ "**/*.vue",
316
+ "**/*.svelte",
317
+ "**/*.css",
318
+ "**/*.scss",
319
+ "**/components/**",
320
+ )
321
+
322
+ TERMINAL_SUMMARY_FIELDS = (
323
+ "terminal_status",
324
+ "terminal_blocker",
325
+ "verification_status",
326
+ "next_action",
327
+ "dirty_paths",
328
+ "phase_owned_dirty",
329
+ "phase_owned_dirty_paths",
330
+ "previous_phase_owned_paths",
331
+ "unowned_dirty_paths",
332
+ "pre_existing_dirty_paths",
333
+ "artifact_paths",
334
+ )
335
+
336
+ from .baml_modular import export_function_schema
337
+
338
+
339
+ CLOSEOUT_SCHEMA: dict[str, Any] = export_function_schema("EmitPhaseCloseout")
340
+
341
+ INJECTION_MODES = ("prompt_only", "inline", "stdin", "context_file", "manual")
342
+ PRODUCT_LOOP_ACTIONS = ("roadmap", "plan", "execute", "repair", "review", "maintain-skills")
343
+ COMMAND_ADAPTER_SUPPORTED_ACTIONS = ("roadmap", "plan", "execute", "repair", "review")
344
+
345
+
346
+ def utc_now() -> str:
347
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
348
+
349
+
350
+ def require_literal(value: str, allowed: tuple[str, ...], label: str) -> str:
351
+ if value not in allowed:
352
+ raise ValueError(f"invalid {label}: {value}")
353
+ return value
354
+
355
+
356
+ @dataclass(frozen=True)
357
+ class ModelSelection:
358
+ profile: str
359
+ model: str
360
+ effort: str
361
+ source: str = "default"
362
+ override_reason: str | None = None
363
+ # model-routing-v1 P4: the resolved role (planner/implementer/worker) when a
364
+ # model_class policy chose the model; None on the legacy/empty-policy path.
365
+ model_class: str | None = None
366
+
367
+ def __post_init__(self) -> None:
368
+ require_literal(self.profile, MODEL_PROFILES, "model profile")
369
+
370
+ def to_json(self) -> dict[str, Any]:
371
+ return clean_dict(asdict(self))
372
+
373
+
374
+ @dataclass(frozen=True)
375
+ class CloseoutException:
376
+ """A recorded, visible exception to the ownership gate (roadmap v40).
377
+
378
+ Frozen by PROTO so GATE (soft) and BREAKGLASS (break_glass) construct the same
379
+ record. Recorded under the ``closeout.<CLOSEOUT_EXCEPTIONS_METADATA_KEY>``
380
+ metadata key, never counted as a clean pass.
381
+ """
382
+
383
+ paths: tuple[str, ...]
384
+ exception_kind: str
385
+ sensitivity_class: str
386
+ reason: str | None = None
387
+ verification_status: str = "passed"
388
+
389
+ def __post_init__(self) -> None:
390
+ require_literal(self.exception_kind, CLOSEOUT_EXCEPTION_KINDS, "closeout exception kind")
391
+ require_literal(self.sensitivity_class, SENSITIVITY_CLASSES, "sensitivity class")
392
+
393
+ def to_json(self) -> dict[str, Any]:
394
+ return clean_dict(asdict(self))
395
+
396
+
397
+ @dataclass(frozen=True)
398
+ class PromptBundle:
399
+ workflow_command: str
400
+ body: str
401
+ injection_mode: str
402
+ context_body: str | None = None
403
+ expected_skill_pack: tuple[str, ...] = ()
404
+ product_action: str | None = None
405
+ skill_bundle_id: str | None = None
406
+ skill_bundle_sha256: str | None = None
407
+ fallback_mode: str | None = None
408
+ context_path: str | None = None
409
+ recommended_installed_roots: tuple[str, ...] = ()
410
+ installed_skill_roots: tuple[str, ...] = ()
411
+ installed_skill_warnings: tuple[str, ...] = ()
412
+ bridge_skill_inventory: tuple[dict[str, Any], ...] = ()
413
+
414
+ def __post_init__(self) -> None:
415
+ require_literal(self.injection_mode, INJECTION_MODES, "injection mode")
416
+ if self.product_action is not None:
417
+ require_literal(self.product_action, PRODUCT_LOOP_ACTIONS, "product action")
418
+
419
+ def render_prompt(self) -> str:
420
+ body = self.body.strip()
421
+ if not body:
422
+ return self.workflow_command
423
+ return f"{self.workflow_command}\n\n{body}"
424
+
425
+ def render_context(self) -> str:
426
+ body = (self.context_body if self.context_body is not None else self.body).strip()
427
+ if not body:
428
+ return self.workflow_command
429
+ return f"{self.workflow_command}\n\n{body}"
430
+
431
+ def body_sha256(self) -> str:
432
+ return hashlib.sha256(self.body.encode("utf-8")).hexdigest()
433
+
434
+ def context_sha256(self) -> str:
435
+ return hashlib.sha256(self.render_context().encode("utf-8")).hexdigest()
436
+
437
+ def body_line_count(self) -> int:
438
+ return len(self.body.splitlines())
439
+
440
+ def body_char_count(self) -> int:
441
+ return len(self.body)
442
+
443
+ def context_line_count(self) -> int:
444
+ return len(self.render_context().splitlines())
445
+
446
+ def context_char_count(self) -> int:
447
+ return len(self.render_context())
448
+
449
+ def to_json(self) -> dict[str, Any]:
450
+ return clean_dict(
451
+ {
452
+ "workflow_command": self.workflow_command,
453
+ "injection_mode": self.injection_mode,
454
+ "context_sha256": self.context_sha256(),
455
+ "expected_skill_pack": self.expected_skill_pack,
456
+ "product_action": self.product_action,
457
+ "skill_bundle_id": self.skill_bundle_id,
458
+ "skill_bundle_sha256": self.skill_bundle_sha256,
459
+ "fallback_mode": self.fallback_mode,
460
+ "context_path": self.context_path,
461
+ "recommended_installed_roots": self.recommended_installed_roots,
462
+ "installed_skill_roots": self.installed_skill_roots,
463
+ "installed_skill_warnings": self.installed_skill_warnings,
464
+ "bridge_skill_inventory": self.bridge_skill_inventory,
465
+ "body_sha256": self.body_sha256(),
466
+ "body_line_count": self.body_line_count(),
467
+ "body_char_count": self.body_char_count(),
468
+ "context_line_count": self.context_line_count(),
469
+ "context_char_count": self.context_char_count(),
470
+ }
471
+ )
472
+
473
+
474
+ @dataclass(frozen=True)
475
+ class InjectionMetadata:
476
+ harness_target: str
477
+ injection_mode: str
478
+ context_sha256: str | None = None
479
+ context_line_count: int | None = None
480
+ context_char_count: int | None = None
481
+ expected_skill_pack: tuple[str, ...] = ()
482
+ skill_bundle_id: str | None = None
483
+ skill_bundle_sha256: str | None = None
484
+ context_path: str | None = None
485
+ fallback_mode: str | None = None
486
+ recommended_installed_roots: tuple[str, ...] = ()
487
+ installed_skill_roots: tuple[str, ...] = ()
488
+ installed_skill_warnings: tuple[str, ...] = ()
489
+ bridge_skill_inventory: tuple[dict[str, Any], ...] = ()
490
+
491
+ def __post_init__(self) -> None:
492
+ require_literal(self.harness_target, EXECUTORS, "harness target")
493
+ require_literal(self.injection_mode, INJECTION_MODES, "injection mode")
494
+
495
+ def to_json(self) -> dict[str, Any]:
496
+ return clean_dict(asdict(self))
497
+
498
+
499
+ @dataclass(frozen=True)
500
+ class DispatchHints:
501
+ preferred_executors: tuple[str, ...] = ()
502
+ allowed_executors: tuple[str, ...] = ()
503
+ fallback_executors: tuple[str, ...] = ()
504
+ disabled_executors: tuple[str, ...] = ()
505
+ required_capabilities: tuple[str, ...] = ()
506
+ source: str = "default"
507
+ action: str | None = None
508
+
509
+ def __post_init__(self) -> None:
510
+ for field_name in (
511
+ "preferred_executors",
512
+ "allowed_executors",
513
+ "fallback_executors",
514
+ "disabled_executors",
515
+ ):
516
+ values = getattr(self, field_name)
517
+ for value in values:
518
+ require_literal(value, EXECUTORS, field_name)
519
+ for capability in self.required_capabilities:
520
+ require_literal(capability, DISPATCH_CAPABILITIES, "required capability")
521
+ if self.action is not None:
522
+ require_literal(self.action, PRODUCT_LOOP_ACTIONS, "product action")
523
+
524
+ def to_json(self) -> dict[str, Any]:
525
+ return clean_dict(asdict(self))
526
+
527
+ def is_empty(self) -> bool:
528
+ return not any(getattr(self, field_name) for field_name in DISPATCH_HINT_FIELDS)
529
+
530
+
531
+ @dataclass(frozen=True)
532
+ class ProviderPolicyCapability:
533
+ provider: str
534
+ executor: str
535
+ supported_work_units: tuple[str, ...]
536
+ supported_efforts: tuple[str, ...]
537
+ unsupported_policy_behavior: str = "block"
538
+ named_fallback: str | None = None
539
+ default_effort: str | None = None
540
+ effort_map: dict[str, str] = field(default_factory=dict)
541
+ model_aliases: dict[str, str] = field(default_factory=dict)
542
+ requires_run_local_user_scope: bool = False
543
+ notes: tuple[str, ...] = ()
544
+
545
+ def __post_init__(self) -> None:
546
+ require_literal(self.executor, EXECUTORS, "executor")
547
+ for work_unit in self.supported_work_units:
548
+ require_literal(work_unit, WORK_UNIT_KINDS, "work-unit kind")
549
+ for effort in self.supported_efforts:
550
+ require_literal(effort, NORMALIZED_EFFORT_LEVELS, "normalized effort")
551
+ require_literal(self.unsupported_policy_behavior, UNSUPPORTED_POLICY_BEHAVIORS, "unsupported policy behavior")
552
+ if self.default_effort is not None:
553
+ require_literal(self.default_effort, NORMALIZED_EFFORT_LEVELS, "default effort")
554
+ for effort in self.effort_map:
555
+ require_literal(effort, NORMALIZED_EFFORT_LEVELS, "effort map key")
556
+ require_literal(self.effort_map[effort], NORMALIZED_EFFORT_LEVELS, "effort map value")
557
+ for work_unit in self.model_aliases:
558
+ require_literal(work_unit, WORK_UNIT_KINDS, "model alias work-unit kind")
559
+ if self.unsupported_policy_behavior == "fallback" and not self.named_fallback:
560
+ raise ValueError("unsupported fallback policy requires named_fallback")
561
+
562
+ def to_json(self) -> dict[str, Any]:
563
+ return clean_dict(asdict(self))
564
+
565
+
566
+ @dataclass(frozen=True)
567
+ class WorkUnitPolicy:
568
+ work_unit_kind: str
569
+ effort: str | None = None
570
+ unsupported_policy_behavior: str = "block"
571
+ fallback: str | None = None
572
+ inherit_default: bool = False
573
+
574
+ def __post_init__(self) -> None:
575
+ require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "work-unit kind")
576
+ if self.effort is not None:
577
+ require_literal(self.effort, NORMALIZED_EFFORT_LEVELS, "normalized effort")
578
+ require_literal(self.unsupported_policy_behavior, UNSUPPORTED_POLICY_BEHAVIORS, "unsupported policy behavior")
579
+ if self.unsupported_policy_behavior == "fallback" and not self.fallback:
580
+ raise ValueError("unsupported fallback policy requires fallback")
581
+ if self.unsupported_policy_behavior == "inherit_default" and not self.inherit_default:
582
+ raise ValueError("inherit_default policy must set inherit_default")
583
+
584
+ def to_json(self) -> dict[str, Any]:
585
+ return clean_dict(asdict(self))
586
+
587
+
588
+ @dataclass(frozen=True)
589
+ class ExecutionPolicyRule:
590
+ selector: str = "default"
591
+ action: str | None = None
592
+ lane: str | None = None
593
+ executor: str | None = None
594
+ model: str | None = None
595
+ model_class: str | None = None
596
+ effort: str | None = None
597
+ work_unit_kind: str | None = None
598
+ unsupported_policy_behavior: str = "block"
599
+ fallback: str | None = None
600
+ inherit_default: bool = False
601
+ source: str = "default"
602
+ override_reason: str | None = None
603
+
604
+ def __post_init__(self) -> None:
605
+ if self.action is not None:
606
+ require_literal(self.action, PRODUCT_LOOP_ACTIONS, "execution policy action")
607
+ if self.executor is not None:
608
+ require_literal(self.executor, EXECUTORS, "execution policy executor")
609
+ if self.model_class is not None:
610
+ require_literal(self.model_class, MODEL_CLASSES, "execution policy model class")
611
+ if self.effort is not None:
612
+ require_literal(self.effort, NORMALIZED_EFFORT_LEVELS, "execution policy effort")
613
+ if self.work_unit_kind is not None:
614
+ require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "execution policy work-unit kind")
615
+ require_literal(self.unsupported_policy_behavior, UNSUPPORTED_POLICY_BEHAVIORS, "unsupported policy behavior")
616
+ if self.model is not None and not self.model.strip():
617
+ raise ValueError("execution policy model must not be empty")
618
+ if self.unsupported_policy_behavior == "fallback" and not self.fallback:
619
+ raise ValueError("execution policy fallback requires fallback")
620
+ if self.unsupported_policy_behavior == "inherit_default" and not self.inherit_default:
621
+ raise ValueError("execution policy inherit_default requires inherit_default")
622
+
623
+ def work_unit_policy(self) -> WorkUnitPolicy | None:
624
+ if self.work_unit_kind is None:
625
+ return None
626
+ return WorkUnitPolicy(
627
+ work_unit_kind=self.work_unit_kind,
628
+ effort=self.effort,
629
+ unsupported_policy_behavior=self.unsupported_policy_behavior,
630
+ fallback=self.fallback,
631
+ inherit_default=self.inherit_default,
632
+ )
633
+
634
+ def to_json(self) -> dict[str, Any]:
635
+ return clean_dict(asdict(self))
636
+
637
+
638
+ @dataclass(frozen=True)
639
+ class ExecutionPolicyParseError:
640
+ path: str
641
+ line_number: int
642
+ raw_line: str
643
+ detail: str
644
+
645
+ def to_json(self) -> dict[str, Any]:
646
+ return {"path": self.path, "line_number": self.line_number, "raw_line": self.raw_line, "detail": self.detail}
647
+
648
+
649
+ @dataclass(frozen=True)
650
+ class ExecutionPolicyDocument:
651
+ rules: tuple[ExecutionPolicyRule, ...] = ()
652
+ source: str = "default"
653
+ parse_error: ExecutionPolicyParseError | None = None
654
+
655
+ def to_json(self) -> dict[str, Any]:
656
+ payload: dict[str, Any] = {"source": self.source, "rules": tuple(rule.to_json() for rule in self.rules)}
657
+ if self.parse_error is not None:
658
+ payload["parse_error"] = self.parse_error.to_json()
659
+ return clean_dict(payload)
660
+
661
+ def is_empty(self) -> bool:
662
+ return not self.rules
663
+
664
+
665
+ @dataclass(frozen=True)
666
+ class PipelinePlanMetadata:
667
+ source_bundle: str | None = None
668
+ source_bundle_sha256: str | None = None
669
+ pipeline_phase_id: str | None = None
670
+ pipeline_mode: str | None = None
671
+
672
+ def __post_init__(self) -> None:
673
+ if self.pipeline_mode is not None:
674
+ require_literal(self.pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
675
+
676
+ @property
677
+ def required(self) -> bool:
678
+ return self.pipeline_mode == "pipeline_required"
679
+
680
+ @property
681
+ def empty(self) -> bool:
682
+ return not any((self.source_bundle, self.source_bundle_sha256, self.pipeline_phase_id, self.pipeline_mode))
683
+
684
+ def to_json(self) -> dict[str, Any]:
685
+ return clean_dict(asdict(self))
686
+
687
+
688
+ @dataclass(frozen=True)
689
+ class PipelineProtectedSource:
690
+ path: str
691
+ category: str
692
+ sha256: str | None = None
693
+ role: str | None = None
694
+
695
+ def __post_init__(self) -> None:
696
+ if not self.path.strip():
697
+ raise ValueError("protected source path must not be empty")
698
+ require_literal(self.category, PIPELINE_PROTECTED_SOURCE_CATEGORIES, "protected source category")
699
+ if self.role is not None and self.role not in PIPELINE_PROTECTED_SOURCE_LEGACY_ROLES:
700
+ require_literal(self.role, PIPELINE_PROTECTED_SOURCE_ROLES, "protected source role")
701
+
702
+ def to_json(self) -> dict[str, Any]:
703
+ return clean_dict(asdict(self))
704
+
705
+
706
+ @dataclass(frozen=True)
707
+ class PipelineMetadataDiagnostic:
708
+ kind: str
709
+ message: str
710
+ metadata: PipelinePlanMetadata | None = None
711
+ expected_sha256: str | None = None
712
+ actual_sha256: str | None = None
713
+ blocker_class: str = "contract_bug"
714
+ human_required: bool = False
715
+
716
+ def __post_init__(self) -> None:
717
+ require_literal(self.kind, PIPELINE_METADATA_DIAGNOSTIC_KINDS, "pipeline metadata diagnostic")
718
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
719
+
720
+ def to_json(self) -> dict[str, Any]:
721
+ return clean_dict(
722
+ {
723
+ "kind": self.kind,
724
+ "message": self.message,
725
+ "metadata": self.metadata.to_json() if self.metadata else None,
726
+ "expected_sha256": self.expected_sha256,
727
+ "actual_sha256": self.actual_sha256,
728
+ "blocker_class": self.blocker_class,
729
+ "human_required": self.human_required,
730
+ }
731
+ )
732
+
733
+
734
+ @dataclass(frozen=True)
735
+ class PhaseSourceBundle:
736
+ path: str
737
+ sha256: str
738
+ phase_id: str
739
+ phase_alias: str
740
+ phase_plan_path: str
741
+ roadmap_path: str
742
+ roadmap_sha256: str
743
+ protected_sources: tuple[PipelineProtectedSource, ...]
744
+ delegated_write_policy: dict[str, Any] = field(default_factory=dict)
745
+ source_files: tuple[dict[str, Any], ...] = ()
746
+ artifact_target_root: str | None = None
747
+ freshness: dict[str, Any] = field(default_factory=dict)
748
+ pipeline_mode: str = "pipeline_optional"
749
+
750
+ def __post_init__(self) -> None:
751
+ if not self.path.strip():
752
+ raise ValueError("source bundle path must not be empty")
753
+ if not self.sha256.strip():
754
+ raise ValueError("source bundle sha256 must not be empty")
755
+ if not self.phase_id.strip():
756
+ raise ValueError("source bundle phase_id must not be empty")
757
+ if not self.phase_alias.strip():
758
+ raise ValueError("source bundle phase_alias must not be empty")
759
+ require_literal(self.pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
760
+
761
+ def plan_metadata(self) -> PipelinePlanMetadata:
762
+ return PipelinePlanMetadata(
763
+ source_bundle=self.path,
764
+ source_bundle_sha256=self.sha256,
765
+ pipeline_phase_id=self.phase_id,
766
+ pipeline_mode=self.pipeline_mode,
767
+ )
768
+
769
+ def to_json(self) -> dict[str, Any]:
770
+ return clean_dict(
771
+ {
772
+ "path": self.path,
773
+ "sha256": self.sha256,
774
+ "phase_id": self.phase_id,
775
+ "phase_alias": self.phase_alias,
776
+ "phase_plan_path": self.phase_plan_path,
777
+ "roadmap_path": self.roadmap_path,
778
+ "roadmap_sha256": self.roadmap_sha256,
779
+ "protected_sources": tuple(source.to_json() for source in self.protected_sources),
780
+ "delegated_write_policy": self.delegated_write_policy,
781
+ "source_files": self.source_files,
782
+ "artifact_target_root": self.artifact_target_root,
783
+ "freshness": self.freshness,
784
+ "pipeline_mode": self.pipeline_mode,
785
+ }
786
+ )
787
+
788
+
789
+ @dataclass(frozen=True)
790
+ class PhaseLoopAutomation:
791
+ status: str
792
+ next_skill: str | None = None
793
+ next_command: str | None = None
794
+ next_model_hint: str | None = None
795
+ next_effort_hint: str | None = None
796
+ human_required: bool = False
797
+ blocker_class: str | None = None
798
+ blocker_summary: str | None = None
799
+ required_human_inputs: tuple[str, ...] = ()
800
+ verification_status: str = "not_run"
801
+ artifact: str | None = None
802
+ artifact_state: str | None = None
803
+
804
+ def __post_init__(self) -> None:
805
+ require_literal(self.status, PHASE_STATUSES, "automation status")
806
+ if self.blocker_class is not None:
807
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "automation blocker class")
808
+ if self.next_effort_hint is not None:
809
+ require_literal(self.next_effort_hint, NORMALIZED_EFFORT_LEVELS, "automation effort hint")
810
+
811
+ def to_json(self) -> dict[str, Any]:
812
+ return clean_dict(asdict(self))
813
+
814
+
815
+ @dataclass(frozen=True)
816
+ class PhaseLoopArtifacts:
817
+ plan_path: str
818
+ plan_sha256: str
819
+ artifact_paths: dict[str, str] = field(default_factory=dict)
820
+ changed_paths: tuple[str, ...] = ()
821
+ evidence_refs: tuple[dict[str, Any], ...] = ()
822
+
823
+ def to_json(self) -> dict[str, Any]:
824
+ return clean_dict(asdict(self))
825
+
826
+
827
+ @dataclass(frozen=True)
828
+ class PhaseLoopVerification:
829
+ status: str
830
+ commands: tuple[str, ...] = ()
831
+ results: tuple[dict[str, Any], ...] = ()
832
+ agent_reported_verification_status: str | None = None
833
+
834
+ def to_json(self) -> dict[str, Any]:
835
+ return clean_dict(asdict(self))
836
+
837
+
838
+ @dataclass(frozen=True)
839
+ class PhaseLoopBlocker:
840
+ human_required: bool = False
841
+ blocker_class: str | None = None
842
+ blocker_summary: str | None = None
843
+ required_human_inputs: tuple[str, ...] = ()
844
+ access_attempts: tuple[dict[str, Any], ...] = ()
845
+
846
+ def __post_init__(self) -> None:
847
+ if self.blocker_class is not None:
848
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
849
+
850
+ def to_json(self) -> dict[str, Any]:
851
+ return clean_dict(asdict(self))
852
+
853
+
854
+ @dataclass(frozen=True)
855
+ class PhaseLoopSourceBundle:
856
+ path: str | None = None
857
+ sha256: str | None = None
858
+ phase_id: str | None = None
859
+ pipeline_mode: str = "standalone"
860
+ protected_sources: tuple[dict[str, Any], ...] = ()
861
+
862
+ def __post_init__(self) -> None:
863
+ require_literal(self.pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
864
+
865
+ def to_json(self) -> dict[str, Any]:
866
+ data = {
867
+ "path": self.path,
868
+ "sha256": self.sha256,
869
+ "phase_id": self.phase_id,
870
+ "pipeline_mode": self.pipeline_mode,
871
+ "protected_sources": self.protected_sources or None,
872
+ }
873
+ return clean_dict(data)
874
+
875
+
876
+ @dataclass(frozen=True)
877
+ class SourceTruthImpact:
878
+ changed_path_boundaries: tuple[dict[str, str], ...] = ()
879
+ canonical_refresh_recommended: bool = False
880
+ canonical_refresh_reason_codes: tuple[str, ...] = ()
881
+ redaction_posture: str = "metadata_only"
882
+
883
+ def __post_init__(self) -> None:
884
+ require_literal(self.redaction_posture, REDACTION_POSTURES, "redaction posture")
885
+ for boundary in self.changed_path_boundaries:
886
+ require_literal(str(boundary.get("category")), CHANGED_PATH_CATEGORIES, "changed path category")
887
+ for reason in self.canonical_refresh_reason_codes:
888
+ require_literal(reason, CANONICAL_REFRESH_REASON_CODES, "canonical refresh reason code")
889
+
890
+ def to_json(self) -> dict[str, Any]:
891
+ return clean_dict(asdict(self))
892
+
893
+
894
+ @dataclass(frozen=True)
895
+ class SpecDeltaCloseout:
896
+ decision: str
897
+ target_surfaces: tuple[str, ...] = ()
898
+ evidence_paths: tuple[str, ...] = ()
899
+ redaction_posture: str = "metadata_only"
900
+ blocker_class: str | None = None
901
+ schema: str = SPEC_DELTA_CLOSEOUT_SCHEMA
902
+
903
+ def __post_init__(self) -> None:
904
+ if self.schema != SPEC_DELTA_CLOSEOUT_SCHEMA:
905
+ raise ValueError(f"invalid spec delta closeout schema: {self.schema}")
906
+ require_literal(self.decision, SPEC_DELTA_DECISIONS, "spec delta decision")
907
+ require_literal(self.redaction_posture, REDACTION_POSTURES, "redaction posture")
908
+ if self.blocker_class is not None:
909
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
910
+
911
+ def to_json(self) -> dict[str, Any]:
912
+ return clean_dict(asdict(self))
913
+
914
+
915
+ def _glob_touched(paths: Iterable[str], globs: Iterable[str]) -> bool:
916
+ """True if any path matches any glob (POSIX, case-sensitive)."""
917
+ pats = tuple(globs)
918
+ for raw in paths:
919
+ path = str(raw).strip()
920
+ if not path:
921
+ continue
922
+ for pattern in pats:
923
+ if fnmatch.fnmatchcase(path, pattern):
924
+ return True
925
+ # `**/x` should also match a top-level `x`.
926
+ if pattern.startswith("**/") and fnmatch.fnmatchcase(path, pattern[3:]):
927
+ return True
928
+ return False
929
+
930
+
931
+ def public_surface_touched(changed_paths: Iterable[str]) -> bool:
932
+ """True if a changed path looks like a user-visible public surface (rigor-v1 P1/P2)."""
933
+ return _glob_touched(changed_paths, PUBLIC_SURFACE_GLOBS)
934
+
935
+
936
+ def ui_change_detected(changed_paths: Iterable[str]) -> bool:
937
+ """True if a changed path is a UI/visual surface (rigor-v1 P1/P6)."""
938
+ return _glob_touched(changed_paths, UI_GLOBS)
939
+
940
+
941
+ @dataclass(frozen=True)
942
+ class DocDeltaCloseout:
943
+ """doc_delta_closeout.v1 — records the code↔doc currency decision for a phase."""
944
+
945
+ decision: str
946
+ target_surfaces: tuple[str, ...] = ()
947
+ evidence_paths: tuple[str, ...] = ()
948
+ justification: str | None = None
949
+ schema: str = DOC_DELTA_CLOSEOUT_SCHEMA
950
+
951
+ def __post_init__(self) -> None:
952
+ if self.schema != DOC_DELTA_CLOSEOUT_SCHEMA:
953
+ raise ValueError(f"invalid doc delta closeout schema: {self.schema}")
954
+ require_literal(self.decision, DOC_DELTA_DECISIONS, "doc delta decision")
955
+
956
+ def to_json(self) -> dict[str, Any]:
957
+ return clean_dict(asdict(self))
958
+
959
+
960
+ @dataclass(frozen=True)
961
+ class VisualEvidence:
962
+ """Screenshot/artifact evidence that a UI change was visually verified (rigor-v1 P1/P6)."""
963
+
964
+ artifact_paths: tuple[str, ...] = ()
965
+ observed: str | None = None
966
+
967
+ def to_json(self) -> dict[str, Any]:
968
+ return clean_dict(asdict(self))
969
+
970
+
971
+ @dataclass(frozen=True)
972
+ class PhaseLoopCloseout:
973
+ phase: str
974
+ terminal_status: str
975
+ automation: PhaseLoopAutomation
976
+ artifacts: PhaseLoopArtifacts
977
+ verification: PhaseLoopVerification
978
+ blocker: PhaseLoopBlocker
979
+ source_bundle: PhaseLoopSourceBundle
980
+ source_truth_impact: SourceTruthImpact | None = None
981
+ spec_delta_closeout: SpecDeltaCloseout | None = None
982
+ doc_delta_closeout: DocDeltaCloseout | None = None
983
+ schema: str = PIPELINE_CLOSEOUT_SCHEMA
984
+
985
+ def __post_init__(self) -> None:
986
+ if self.schema != PIPELINE_CLOSEOUT_SCHEMA:
987
+ raise ValueError(f"invalid closeout schema: {self.schema}")
988
+ require_literal(self.terminal_status, PIPELINE_CLOSEOUT_OUTCOMES, "terminal status")
989
+
990
+ def to_json(self) -> dict[str, Any]:
991
+ return clean_dict(
992
+ {
993
+ "schema": self.schema,
994
+ "phase": self.phase,
995
+ "terminal_status": self.terminal_status,
996
+ "automation": self.automation.to_json(),
997
+ "artifacts": self.artifacts.to_json(),
998
+ "verification": self.verification.to_json(),
999
+ "blocker": self.blocker.to_json(),
1000
+ "source_bundle": self.source_bundle.to_json(),
1001
+ "source_truth_impact": self.source_truth_impact.to_json() if self.source_truth_impact else None,
1002
+ "spec_delta_closeout": self.spec_delta_closeout.to_json() if self.spec_delta_closeout else None,
1003
+ "doc_delta_closeout": self.doc_delta_closeout.to_json() if self.doc_delta_closeout else None,
1004
+ }
1005
+ )
1006
+
1007
+
1008
+ @dataclass(frozen=True)
1009
+ class WorkUnitIdentity:
1010
+ phase: str
1011
+ kind: str
1012
+ lane_id: str
1013
+ attempt: int
1014
+
1015
+ def __post_init__(self) -> None:
1016
+ require_literal(self.kind, WORK_UNIT_KINDS, "work-unit kind")
1017
+ if not self.phase.strip():
1018
+ raise ValueError("work-unit phase must not be empty")
1019
+ if not self.lane_id.strip():
1020
+ raise ValueError("work-unit lane id must not be empty")
1021
+ if self.attempt < 1:
1022
+ raise ValueError("work-unit attempt must be positive")
1023
+
1024
+ @property
1025
+ def work_unit_id(self) -> str:
1026
+ return f"{self.phase}.{self.kind}.{self.lane_id}.{self.attempt}"
1027
+
1028
+ @classmethod
1029
+ def from_id(cls, work_unit_id: str) -> "WorkUnitIdentity":
1030
+ parts = work_unit_id.split(".")
1031
+ if len(parts) < 4:
1032
+ raise ValueError(f"invalid work-unit id: {work_unit_id}")
1033
+ phase = parts[0]
1034
+ kind = parts[1]
1035
+ lane_id = ".".join(parts[2:-1])
1036
+ try:
1037
+ attempt = int(parts[-1])
1038
+ except ValueError as exc:
1039
+ raise ValueError(f"invalid work-unit attempt: {parts[-1]}") from exc
1040
+ return cls(phase=phase, kind=kind, lane_id=lane_id, attempt=attempt)
1041
+
1042
+ @classmethod
1043
+ def from_json(cls, data: dict[str, Any]) -> "WorkUnitIdentity":
1044
+ if data.get("work_unit_id") and not all(key in data for key in ("phase", "kind", "lane_id", "attempt")):
1045
+ return cls.from_id(str(data["work_unit_id"]))
1046
+ return cls(
1047
+ phase=str(data["phase"]),
1048
+ kind=str(data["kind"]),
1049
+ lane_id=str(data["lane_id"]),
1050
+ attempt=int(data["attempt"]),
1051
+ )
1052
+
1053
+ def to_json(self) -> dict[str, Any]:
1054
+ return clean_dict({**asdict(self), "work_unit_id": self.work_unit_id})
1055
+
1056
+
1057
+ @dataclass(frozen=True)
1058
+ class WorkUnitAttempt:
1059
+ identity: WorkUnitIdentity
1060
+ status: str = "pending"
1061
+ parent_phase_event_id: str | None = None
1062
+ policy: dict[str, Any] = field(default_factory=dict)
1063
+ artifacts: dict[str, str] = field(default_factory=dict)
1064
+ started_at: str | None = None
1065
+ finished_at: str | None = None
1066
+ heartbeat_path: str | None = None
1067
+ terminal_summary_path: str | None = None
1068
+ retry_of: str | None = None
1069
+ superseded_by: str | None = None
1070
+
1071
+ def __post_init__(self) -> None:
1072
+ require_literal(self.status, WORK_UNIT_STATUSES, "work-unit status")
1073
+
1074
+ def to_json(self) -> dict[str, Any]:
1075
+ return clean_dict(
1076
+ {
1077
+ "identity": self.identity.to_json(),
1078
+ "status": self.status,
1079
+ "parent_phase_event_id": self.parent_phase_event_id,
1080
+ "policy": self.policy,
1081
+ "artifacts": self.artifacts,
1082
+ "started_at": self.started_at,
1083
+ "finished_at": self.finished_at,
1084
+ "heartbeat_path": self.heartbeat_path,
1085
+ "terminal_summary_path": self.terminal_summary_path,
1086
+ "retry_of": self.retry_of,
1087
+ "superseded_by": self.superseded_by,
1088
+ }
1089
+ )
1090
+
1091
+
1092
+ @dataclass(frozen=True)
1093
+ class WorkUnitCloseout:
1094
+ identity: WorkUnitIdentity
1095
+ status: str
1096
+ automation: dict[str, Any] = field(default_factory=dict)
1097
+ terminal_summary: dict[str, Any] = field(default_factory=dict)
1098
+ closeout_summary: dict[str, Any] = field(default_factory=dict)
1099
+ wave_id: str | None = None
1100
+ worktree_path: str | None = None
1101
+ changed_paths: tuple[str, ...] = ()
1102
+ verification_status: str | None = None
1103
+ evidence_refs: tuple[dict[str, Any], ...] = ()
1104
+ human_required: bool = False
1105
+ blocker_class: str | None = None
1106
+ blocker_summary: str | None = None
1107
+ required_human_inputs: tuple[str, ...] = ()
1108
+
1109
+ def __post_init__(self) -> None:
1110
+ require_literal(self.status, WORK_UNIT_STATUSES, "work-unit status")
1111
+ if self.blocker_class is not None:
1112
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
1113
+
1114
+ def to_json(self) -> dict[str, Any]:
1115
+ return clean_dict(
1116
+ {
1117
+ "identity": self.identity.to_json(),
1118
+ "status": self.status,
1119
+ "automation": self.automation,
1120
+ "terminal_summary": self.terminal_summary,
1121
+ "closeout_summary": self.closeout_summary,
1122
+ "wave_id": self.wave_id,
1123
+ "worktree_path": self.worktree_path,
1124
+ "changed_paths": self.changed_paths,
1125
+ "verification_status": self.verification_status,
1126
+ "evidence_refs": self.evidence_refs,
1127
+ "human_required": self.human_required,
1128
+ "blocker_class": self.blocker_class,
1129
+ "blocker_summary": self.blocker_summary,
1130
+ "required_human_inputs": self.required_human_inputs,
1131
+ }
1132
+ )
1133
+
1134
+
1135
+ @dataclass(frozen=True)
1136
+ class WorkUnitEventMetadata:
1137
+ identity: WorkUnitIdentity
1138
+ status: str
1139
+ event_type: str = "status"
1140
+ timestamp: str = field(default_factory=utc_now)
1141
+ launch_metadata: dict[str, Any] = field(default_factory=dict)
1142
+ heartbeat_path: str | None = None
1143
+ terminal_summary_path: str | None = None
1144
+ closeout_summary: dict[str, Any] = field(default_factory=dict)
1145
+ retry_of: str | None = None
1146
+ superseded_by: str | None = None
1147
+ blocker: dict[str, Any] | None = None
1148
+
1149
+ def __post_init__(self) -> None:
1150
+ require_literal(self.status, WORK_UNIT_STATUSES, "work-unit status")
1151
+
1152
+ def to_json(self) -> dict[str, Any]:
1153
+ return clean_dict(
1154
+ {
1155
+ "event_kind": "work_unit",
1156
+ "event_type": self.event_type,
1157
+ "timestamp": self.timestamp,
1158
+ "work_unit": {
1159
+ **self.identity.to_json(),
1160
+ "status": self.status,
1161
+ "launch_metadata": self.launch_metadata,
1162
+ "heartbeat_path": self.heartbeat_path,
1163
+ "terminal_summary_path": self.terminal_summary_path,
1164
+ "closeout_summary": self.closeout_summary,
1165
+ "retry_of": self.retry_of,
1166
+ "superseded_by": self.superseded_by,
1167
+ "blocker": self.blocker,
1168
+ },
1169
+ }
1170
+ )
1171
+
1172
+
1173
+ @dataclass(frozen=True)
1174
+ class WorkUnitState:
1175
+ identity: WorkUnitIdentity
1176
+ status: str = "pending"
1177
+ created_at: str = field(default_factory=utc_now)
1178
+ updated_at: str = field(default_factory=utc_now)
1179
+ parent_phase_event_id: str | None = None
1180
+ policy: dict[str, Any] = field(default_factory=dict)
1181
+ artifacts: dict[str, str] = field(default_factory=dict)
1182
+ heartbeat_path: str | None = None
1183
+ terminal_summary_path: str | None = None
1184
+ closeout_summary: dict[str, Any] = field(default_factory=dict)
1185
+ retry_of: str | None = None
1186
+ superseded_by: str | None = None
1187
+ blocker: dict[str, Any] | None = None
1188
+ human_required: bool = False
1189
+
1190
+ def __post_init__(self) -> None:
1191
+ require_literal(self.status, WORK_UNIT_STATUSES, "work-unit status")
1192
+
1193
+ @property
1194
+ def work_unit_id(self) -> str:
1195
+ return self.identity.work_unit_id
1196
+
1197
+ @classmethod
1198
+ def from_json(cls, data: dict[str, Any]) -> "WorkUnitState":
1199
+ identity_data = data.get("identity") if isinstance(data.get("identity"), dict) else data
1200
+ identity = WorkUnitIdentity.from_json(identity_data)
1201
+ return cls(
1202
+ identity=identity,
1203
+ status=str(data.get("status", "pending")),
1204
+ created_at=str(data.get("created_at") or utc_now()),
1205
+ updated_at=str(data.get("updated_at") or data.get("created_at") or utc_now()),
1206
+ parent_phase_event_id=data.get("parent_phase_event_id"),
1207
+ policy=dict(data.get("policy", {})) if isinstance(data.get("policy"), dict) else {},
1208
+ artifacts={str(k): str(v) for k, v in dict(data.get("artifacts", {})).items()} if isinstance(data.get("artifacts"), dict) else {},
1209
+ heartbeat_path=data.get("heartbeat_path"),
1210
+ terminal_summary_path=data.get("terminal_summary_path"),
1211
+ closeout_summary=dict(data.get("closeout_summary", {})) if isinstance(data.get("closeout_summary"), dict) else {},
1212
+ retry_of=data.get("retry_of"),
1213
+ superseded_by=data.get("superseded_by"),
1214
+ blocker=dict(data.get("blocker")) if isinstance(data.get("blocker"), dict) else None,
1215
+ human_required=bool(data.get("human_required", False)),
1216
+ )
1217
+
1218
+ def with_status(self, status: str, **updates: Any) -> "WorkUnitState":
1219
+ require_literal(status, WORK_UNIT_STATUSES, "work-unit status")
1220
+ data = {**asdict(self), **updates, "status": status, "updated_at": updates.get("updated_at", utc_now())}
1221
+ data["identity"] = self.identity
1222
+ return WorkUnitState(**data)
1223
+
1224
+ def to_json(self) -> dict[str, Any]:
1225
+ return clean_dict(
1226
+ {
1227
+ "identity": self.identity.to_json(),
1228
+ "work_unit_id": self.work_unit_id,
1229
+ "status": self.status,
1230
+ "created_at": self.created_at,
1231
+ "updated_at": self.updated_at,
1232
+ "parent_phase_event_id": self.parent_phase_event_id,
1233
+ "policy": self.policy,
1234
+ "artifacts": self.artifacts,
1235
+ "heartbeat_path": self.heartbeat_path,
1236
+ "terminal_summary_path": self.terminal_summary_path,
1237
+ "closeout_summary": self.closeout_summary,
1238
+ "retry_of": self.retry_of,
1239
+ "superseded_by": self.superseded_by,
1240
+ "blocker": self.blocker,
1241
+ "human_required": self.human_required,
1242
+ }
1243
+ )
1244
+
1245
+
1246
+ @dataclass(frozen=True)
1247
+ class LaneDependency:
1248
+ source_lane_id: str
1249
+ target_lane_id: str
1250
+ relation: str = "depends_on"
1251
+
1252
+ def to_json(self) -> dict[str, Any]:
1253
+ return clean_dict(asdict(self))
1254
+
1255
+
1256
+ @dataclass(frozen=True)
1257
+ class LaneTaskSet:
1258
+ test: tuple[str, ...] = ()
1259
+ impl: tuple[str, ...] = ()
1260
+ verify: tuple[str, ...] = ()
1261
+ other: tuple[str, ...] = ()
1262
+
1263
+ def to_json(self) -> dict[str, Any]:
1264
+ return clean_dict(asdict(self))
1265
+
1266
+
1267
+ @dataclass(frozen=True)
1268
+ class LaneIRDiagnostic(Exception):
1269
+ kind: str
1270
+ message: str
1271
+ lane_id: str | None = None
1272
+ blocker_class: str = "contract_bug"
1273
+ human_required: bool = False
1274
+ details: dict[str, Any] = field(default_factory=dict)
1275
+
1276
+ def __post_init__(self) -> None:
1277
+ require_literal(self.kind, LANE_IR_DIAGNOSTIC_KINDS, "lane IR diagnostic kind")
1278
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "lane IR blocker class")
1279
+
1280
+ def to_json(self) -> dict[str, Any]:
1281
+ return clean_dict(asdict(self))
1282
+
1283
+
1284
+ @dataclass(frozen=True)
1285
+ class PhasePlanLane:
1286
+ lane_id: str
1287
+ name: str
1288
+ heading: str
1289
+ owned_files: tuple[str, ...] = ()
1290
+ read_only: bool = False
1291
+ depends_on: tuple[str, ...] = ()
1292
+ blocks: tuple[str, ...] = ()
1293
+ interfaces_provided: tuple[str, ...] = ()
1294
+ interfaces_consumed: tuple[str, ...] = ()
1295
+ tasks: LaneTaskSet = field(default_factory=LaneTaskSet)
1296
+ verification_commands: tuple[str, ...] = ()
1297
+ parallel_safe: bool = False
1298
+ reducer_kind: str = "none"
1299
+ execution_policy: ExecutionPolicyRule | None = None
1300
+
1301
+ def __post_init__(self) -> None:
1302
+ require_literal(self.reducer_kind, LANE_REDUCER_KINDS, "lane reducer kind")
1303
+
1304
+ def to_json(self) -> dict[str, Any]:
1305
+ data = asdict(self)
1306
+ if self.execution_policy is not None:
1307
+ data["execution_policy"] = self.execution_policy.to_json()
1308
+ return clean_dict(data)
1309
+
1310
+
1311
+ @dataclass(frozen=True)
1312
+ class PhasePlanIR:
1313
+ plan_path: str
1314
+ metadata: dict[str, str]
1315
+ lanes: tuple[PhasePlanLane, ...] = ()
1316
+ dependencies: tuple[LaneDependency, ...] = ()
1317
+ diagnostics: tuple[LaneIRDiagnostic, ...] = ()
1318
+ execution_policy: ExecutionPolicyDocument | None = None
1319
+ dispatch_hints: dict[str, DispatchHints] = field(default_factory=dict)
1320
+ merge_policy: Any | None = None
1321
+
1322
+ @property
1323
+ def valid(self) -> bool:
1324
+ return not self.diagnostics
1325
+
1326
+ def to_json(self) -> dict[str, Any]:
1327
+ return clean_dict(
1328
+ {
1329
+ "plan_path": self.plan_path,
1330
+ "metadata": self.metadata,
1331
+ "lanes": tuple(lane.to_json() for lane in self.lanes),
1332
+ "dependencies": tuple(dependency.to_json() for dependency in self.dependencies),
1333
+ "diagnostics": tuple(diagnostic.to_json() for diagnostic in self.diagnostics),
1334
+ "execution_policy": self.execution_policy.to_json() if self.execution_policy else None,
1335
+ "dispatch_hints": {
1336
+ key: value.to_json() for key, value in self.dispatch_hints.items()
1337
+ },
1338
+ "merge_policy": self.merge_policy.to_json() if hasattr(self.merge_policy, "to_json") else self.merge_policy,
1339
+ }
1340
+ )
1341
+
1342
+
1343
+ @dataclass(frozen=True)
1344
+ class LaneWorktreeAssignment:
1345
+ lane_id: str
1346
+ worktree_path: str
1347
+ isolation_mode: str = "main_worktree"
1348
+ branch: str | None = None
1349
+ base_sha: str | None = None
1350
+
1351
+ def __post_init__(self) -> None:
1352
+ if not self.lane_id.strip():
1353
+ raise ValueError("lane worktree assignment lane_id must not be empty")
1354
+ if not self.worktree_path.strip():
1355
+ raise ValueError("lane worktree assignment worktree_path must not be empty")
1356
+ require_literal(self.isolation_mode, WORKTREE_ISOLATION_MODES, "worktree isolation mode")
1357
+
1358
+ def to_json(self) -> dict[str, Any]:
1359
+ return clean_dict(asdict(self))
1360
+
1361
+
1362
+ @dataclass(frozen=True)
1363
+ class LaneWave:
1364
+ wave_id: str
1365
+ lane_ids: tuple[str, ...]
1366
+ mode: str = "serialized"
1367
+ assignments: tuple[LaneWorktreeAssignment, ...] = ()
1368
+
1369
+ def __post_init__(self) -> None:
1370
+ if not self.wave_id.strip():
1371
+ raise ValueError("lane wave id must not be empty")
1372
+ require_literal(self.mode, LANE_SCHEDULER_MODES, "lane scheduler mode")
1373
+ if self.mode == "off":
1374
+ raise ValueError("lane wave mode cannot be off")
1375
+ if not self.lane_ids:
1376
+ raise ValueError("lane wave must include at least one lane")
1377
+
1378
+ def to_json(self) -> dict[str, Any]:
1379
+ return clean_dict(
1380
+ {
1381
+ "wave_id": self.wave_id,
1382
+ "lane_ids": self.lane_ids,
1383
+ "mode": self.mode,
1384
+ "assignments": tuple(assignment.to_json() for assignment in self.assignments),
1385
+ }
1386
+ )
1387
+
1388
+
1389
+ @dataclass(frozen=True)
1390
+ class LaneWaveDecision:
1391
+ status: str
1392
+ mode: str
1393
+ ready_wave: LaneWave | None = None
1394
+ pending_lane_ids: tuple[str, ...] = ()
1395
+ completed_lane_ids: tuple[str, ...] = ()
1396
+ blocked_lane_ids: tuple[str, ...] = ()
1397
+ diagnostics: tuple[LaneIRDiagnostic, ...] = ()
1398
+
1399
+ def __post_init__(self) -> None:
1400
+ require_literal(self.status, LANE_WAVE_STATUSES, "lane wave status")
1401
+ require_literal(self.mode, LANE_SCHEDULER_MODES, "lane scheduler mode")
1402
+ if self.status == "ready" and self.ready_wave is None:
1403
+ raise ValueError("ready lane wave decision requires ready_wave")
1404
+
1405
+ def to_json(self) -> dict[str, Any]:
1406
+ return clean_dict(
1407
+ {
1408
+ "status": self.status,
1409
+ "mode": self.mode,
1410
+ "ready_wave": self.ready_wave.to_json() if self.ready_wave else None,
1411
+ "pending_lane_ids": self.pending_lane_ids,
1412
+ "completed_lane_ids": self.completed_lane_ids,
1413
+ "blocked_lane_ids": self.blocked_lane_ids,
1414
+ "diagnostics": tuple(diagnostic.to_json() for diagnostic in self.diagnostics),
1415
+ }
1416
+ )
1417
+
1418
+
1419
+ HARNESS_CLOSEOUT_REQUIRED_FIELDS = (
1420
+ "automation.status",
1421
+ "automation.next_skill",
1422
+ "automation.next_command",
1423
+ "automation.human_required",
1424
+ "automation.blocker_class",
1425
+ "automation.verification_status",
1426
+ )
1427
+
1428
+
1429
+ @dataclass(frozen=True)
1430
+ class HarnessLaneAssignment:
1431
+ phase: str
1432
+ lane_id: str
1433
+ work_unit_kind: str
1434
+ prompt_kind: str = "implementation"
1435
+ wave_id: str | None = None
1436
+ owned_files: tuple[str, ...] = ()
1437
+ read_only_refs: tuple[str, ...] = ()
1438
+ consumed_interfaces: tuple[str, ...] = ()
1439
+ depends_on: tuple[str, ...] = ()
1440
+ execution_policy: dict[str, Any] = field(default_factory=dict)
1441
+ worktree_assignment: LaneWorktreeAssignment | None = None
1442
+ harness_route: str | None = None
1443
+ model: str | None = None
1444
+ effort: str | None = None
1445
+ fallback_reason: str | None = None
1446
+ closeout_schema_required: tuple[str, ...] = HARNESS_CLOSEOUT_REQUIRED_FIELDS
1447
+ reducer_kind: str = "none"
1448
+ metadata: dict[str, Any] = field(default_factory=dict)
1449
+
1450
+ def __post_init__(self) -> None:
1451
+ if not self.phase.strip():
1452
+ raise ValueError("harness lane assignment phase must not be empty")
1453
+ if not self.lane_id.strip():
1454
+ raise ValueError("harness lane assignment lane_id must not be empty")
1455
+ require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "harness work-unit kind")
1456
+ require_literal(self.prompt_kind, HARNESS_WORK_UNIT_PROMPT_KINDS, "harness prompt kind")
1457
+ require_literal(self.reducer_kind, LANE_REDUCER_KINDS, "lane reducer kind")
1458
+ if self.work_unit_kind in {"lane_execute", "lane_review"} and not self.owned_files:
1459
+ raise ValueError("harness lane assignment requires owned_files for lane work")
1460
+
1461
+ @classmethod
1462
+ def from_lane(
1463
+ cls,
1464
+ *,
1465
+ phase: str,
1466
+ lane: PhasePlanLane,
1467
+ work_unit_kind: str = "lane_execute",
1468
+ prompt_kind: str = "implementation",
1469
+ worktree_assignment: LaneWorktreeAssignment | None = None,
1470
+ metadata: dict[str, Any] | None = None,
1471
+ ) -> "HarnessLaneAssignment":
1472
+ return cls(
1473
+ phase=phase,
1474
+ lane_id=lane.lane_id,
1475
+ work_unit_kind=work_unit_kind,
1476
+ prompt_kind=prompt_kind,
1477
+ owned_files=lane.owned_files,
1478
+ read_only_refs=lane.interfaces_consumed,
1479
+ consumed_interfaces=lane.interfaces_consumed,
1480
+ depends_on=lane.depends_on,
1481
+ execution_policy=lane.execution_policy.to_json() if lane.execution_policy else {},
1482
+ worktree_assignment=worktree_assignment,
1483
+ reducer_kind=lane.reducer_kind,
1484
+ metadata=dict(metadata or {}),
1485
+ )
1486
+
1487
+ @classmethod
1488
+ def from_json(cls, data: dict[str, Any]) -> "HarnessLaneAssignment":
1489
+ assignment_data = data.get("worktree_assignment")
1490
+ return cls(
1491
+ phase=str(data.get("phase") or ""),
1492
+ lane_id=str(data.get("lane_id") or ""),
1493
+ work_unit_kind=str(data.get("work_unit_kind") or ""),
1494
+ prompt_kind=str(data.get("prompt_kind") or "implementation"),
1495
+ wave_id=data.get("wave_id"),
1496
+ owned_files=tuple(str(item) for item in data.get("owned_files") or ()),
1497
+ read_only_refs=tuple(str(item) for item in data.get("read_only_refs") or ()),
1498
+ consumed_interfaces=tuple(str(item) for item in data.get("consumed_interfaces") or ()),
1499
+ depends_on=tuple(str(item) for item in data.get("depends_on") or ()),
1500
+ execution_policy=dict(data.get("execution_policy") or {}),
1501
+ worktree_assignment=(
1502
+ LaneWorktreeAssignment(
1503
+ lane_id=str(assignment_data.get("lane_id") or ""),
1504
+ worktree_path=str(assignment_data.get("worktree_path") or ""),
1505
+ isolation_mode=str(assignment_data.get("isolation_mode") or "main_worktree"),
1506
+ branch=assignment_data.get("branch"),
1507
+ base_sha=assignment_data.get("base_sha"),
1508
+ )
1509
+ if isinstance(assignment_data, dict)
1510
+ else None
1511
+ ),
1512
+ harness_route=data.get("harness_route"),
1513
+ model=data.get("model"),
1514
+ effort=data.get("effort"),
1515
+ fallback_reason=data.get("fallback_reason"),
1516
+ closeout_schema_required=tuple(str(item) for item in data.get("closeout_schema_required") or HARNESS_CLOSEOUT_REQUIRED_FIELDS),
1517
+ reducer_kind=str(data.get("reducer_kind") or "none"),
1518
+ metadata=dict(data.get("metadata") or {}),
1519
+ )
1520
+
1521
+ def to_json(self) -> dict[str, Any]:
1522
+ return clean_dict(
1523
+ {
1524
+ "phase": self.phase,
1525
+ "lane_id": self.lane_id,
1526
+ "work_unit_kind": self.work_unit_kind,
1527
+ "prompt_kind": self.prompt_kind,
1528
+ "wave_id": self.wave_id,
1529
+ "owned_files": self.owned_files,
1530
+ "read_only_refs": self.read_only_refs,
1531
+ "consumed_interfaces": self.consumed_interfaces,
1532
+ "depends_on": self.depends_on,
1533
+ "execution_policy": self.execution_policy,
1534
+ "worktree_assignment": self.worktree_assignment.to_json() if self.worktree_assignment else None,
1535
+ "harness_route": self.harness_route,
1536
+ "model": self.model,
1537
+ "effort": self.effort,
1538
+ "fallback_reason": self.fallback_reason,
1539
+ "closeout_schema_required": self.closeout_schema_required,
1540
+ "reducer_kind": self.reducer_kind,
1541
+ "metadata": self.metadata,
1542
+ }
1543
+ )
1544
+
1545
+
1546
+ @dataclass(frozen=True)
1547
+ class DirtyPathClassification:
1548
+ path: str
1549
+ classification: str
1550
+ lane_id: str | None = None
1551
+
1552
+ def __post_init__(self) -> None:
1553
+ if not self.path.strip():
1554
+ raise ValueError("dirty path classification path must not be empty")
1555
+ require_literal(self.classification, DIRTY_PATH_CLASSIFICATIONS, "dirty path classification")
1556
+
1557
+ def to_json(self) -> dict[str, Any]:
1558
+ return clean_dict(asdict(self))
1559
+
1560
+
1561
+ @dataclass(frozen=True)
1562
+ class ResolvedExecutionPolicy:
1563
+ action: str
1564
+ lane: str | None
1565
+ executor: str
1566
+ model: str
1567
+ effort: str
1568
+ work_unit_kind: str
1569
+ fallback: str | None = None
1570
+ unsupported_policy_behavior: str = "block"
1571
+ execution_policy_source: str = "registry defaults"
1572
+ execution_policy_override_reason: str | None = None
1573
+ executor_source: str = "registry defaults"
1574
+ model_source: str = "registry defaults"
1575
+ effort_source: str = "registry defaults"
1576
+ fallback_source: str = "registry defaults"
1577
+ fallback_applied: bool = False
1578
+ # model-routing-v1 P4: the role the winning policy requested, for route logs.
1579
+ model_class: str | None = None
1580
+
1581
+ def __post_init__(self) -> None:
1582
+ require_literal(self.action, PRODUCT_LOOP_ACTIONS, "execution policy action")
1583
+ require_literal(self.executor, EXECUTORS, "execution policy executor")
1584
+ require_literal(self.effort, NORMALIZED_EFFORT_LEVELS, "execution policy effort")
1585
+ require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "execution policy work-unit kind")
1586
+ require_literal(self.unsupported_policy_behavior, UNSUPPORTED_POLICY_BEHAVIORS, "unsupported policy behavior")
1587
+
1588
+ def to_json(self) -> dict[str, Any]:
1589
+ return clean_dict(asdict(self))
1590
+
1591
+
1592
+ @dataclass(frozen=True)
1593
+ class WorkUnitMetric:
1594
+ metric_id: str
1595
+ schema_version: str
1596
+ timestamp: str
1597
+ work_unit_id: str | None
1598
+ work_unit_kind: str
1599
+ phase: str
1600
+ action: str
1601
+ executor: str
1602
+ provider: str
1603
+ model: str
1604
+ effort: str | None = None
1605
+ thinking_level: str | None = None
1606
+ lane_id: str | None = None
1607
+ wave_id: str | None = None
1608
+ policy_source: str | None = None
1609
+ policy_override_reason: str | None = None
1610
+ profile_source: str | None = None
1611
+ fallback_applied: bool = False
1612
+ fallback: str | None = None
1613
+ fallback_reason: str | None = None
1614
+ duration_seconds: float | None = None
1615
+ returncode: int | None = None
1616
+ terminal_status: str | None = None
1617
+ verification_status: str | None = None
1618
+ blocker_class: str | None = None
1619
+ artifact_paths: dict[str, str] = field(default_factory=dict)
1620
+
1621
+ def __post_init__(self) -> None:
1622
+ if self.schema_version != WORK_UNIT_METRIC_SCHEMA_VERSION:
1623
+ raise ValueError(f"invalid work-unit metric schema_version: {self.schema_version}")
1624
+ require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "work-unit kind")
1625
+ require_literal(self.action, PRODUCT_LOOP_ACTIONS, "metric action")
1626
+ require_literal(self.executor, EXECUTORS, "metric executor")
1627
+ if self.effort is not None:
1628
+ require_literal(self.effort, NORMALIZED_EFFORT_LEVELS, "metric effort")
1629
+ if self.blocker_class is not None:
1630
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "metric blocker class")
1631
+
1632
+ def to_json(self) -> dict[str, Any]:
1633
+ return clean_dict(asdict(self))
1634
+
1635
+
1636
+ @dataclass(frozen=True)
1637
+ class ExecutorCapabilityRecord:
1638
+ executor: str
1639
+ supported_actions: tuple[str, ...]
1640
+ capabilities: tuple[str, ...]
1641
+ strengths: tuple[str, ...] = ()
1642
+ limits: tuple[str, ...] = ()
1643
+ injection_mode: str = "manual"
1644
+ permission_posture: str = "unknown"
1645
+ subagent_posture: str = "unknown"
1646
+ live_available: bool = False
1647
+ dry_run_available: bool = True
1648
+ live_proof_gate: str = "disposable_proof_required"
1649
+ promotion_status: str = "proof_gated"
1650
+ promotion_requirements: tuple[str, ...] = ()
1651
+ auth_preflight_mode: str = "none"
1652
+ auth_preflight_probes: tuple[str, ...] = ()
1653
+ timeout_posture: str = "runner_managed"
1654
+ output_capture_format: str = "combined_output"
1655
+ terminal_summary_artifact: str = "terminal-summary.json"
1656
+ default_model_profiles: dict[str, str] = field(default_factory=dict)
1657
+ known_failure_cases: tuple[str, ...] = ()
1658
+ default_claude_execution_mode: str | None = None
1659
+ claude_execution_policies: tuple["ClaudeTeamPolicy", ...] = ()
1660
+
1661
+ def __post_init__(self) -> None:
1662
+ require_literal(self.executor, EXECUTORS, "executor")
1663
+ require_literal(self.injection_mode, INJECTION_MODES, "injection mode")
1664
+ require_literal(self.permission_posture, PERMISSION_POSTURES, "permission posture")
1665
+ require_literal(self.subagent_posture, SUBAGENT_POSTURES, "subagent posture")
1666
+ require_literal(self.live_proof_gate, LIVE_PROOF_GATES, "live proof gate")
1667
+ require_literal(self.promotion_status, PROMOTION_STATUSES, "promotion status")
1668
+ require_literal(self.auth_preflight_mode, AUTH_PREFLIGHT_MODES, "auth preflight mode")
1669
+ require_literal(self.timeout_posture, TIMEOUT_POSTURES, "timeout posture")
1670
+ require_literal(self.output_capture_format, OUTPUT_CAPTURE_FORMATS, "output capture format")
1671
+ if self.default_claude_execution_mode is not None:
1672
+ require_literal(self.default_claude_execution_mode, CLAUDE_EXECUTION_MODES, "Claude execution mode")
1673
+ for action in self.supported_actions:
1674
+ require_literal(action, PRODUCT_LOOP_ACTIONS, "supported action")
1675
+ for capability in self.capabilities:
1676
+ require_literal(capability, DISPATCH_CAPABILITIES, "capability")
1677
+ for action, profile in self.default_model_profiles.items():
1678
+ require_literal(action, PRODUCT_LOOP_ACTIONS, "default model profile action")
1679
+ require_literal(profile, MODEL_PROFILES, "default model profile")
1680
+
1681
+ def to_json(self) -> dict[str, Any]:
1682
+ return clean_dict(asdict(self))
1683
+
1684
+
1685
+ @dataclass(frozen=True)
1686
+ class ClaudeTeamPolicy:
1687
+ execution_mode: str
1688
+ maturity_label: str
1689
+ live_proof_gate: str
1690
+ promotion_status: str
1691
+ launch_default: bool = False
1692
+ max_teammates: int = 0
1693
+ max_native_tasks: int = 0
1694
+ max_delegation_depth: int = 0
1695
+ max_fanout: int = 0
1696
+ default_model: str | None = None
1697
+ default_effort: str | None = None
1698
+ budget_guidance: dict[str, Any] = field(default_factory=dict)
1699
+ allowed_actions: tuple[str, ...] = ()
1700
+ disallowed_actions: tuple[str, ...] = ()
1701
+ allowed_tools: tuple[str, ...] = ()
1702
+ disallowed_tools: tuple[str, ...] = ()
1703
+ worktree_posture: str = "phase_loop_managed"
1704
+ requires_disjoint_owned_files: bool = False
1705
+ allows_read_only_lanes: bool = True
1706
+ direct_teammate_messaging_allowed: bool = False
1707
+ task_lifecycle_supported: bool = False
1708
+
1709
+ def __post_init__(self) -> None:
1710
+ require_literal(self.execution_mode, CLAUDE_EXECUTION_MODES, "Claude execution mode")
1711
+ require_literal(self.maturity_label, OPERATOR_MATURITY_LABELS, "operator maturity label")
1712
+ require_literal(self.live_proof_gate, LIVE_PROOF_GATES, "live proof gate")
1713
+ require_literal(self.promotion_status, PROMOTION_STATUSES, "promotion status")
1714
+ require_literal(self.worktree_posture, CLAUDE_WORKTREE_POSTURES, "Claude worktree posture")
1715
+ for action in self.allowed_actions:
1716
+ require_literal(action, PRODUCT_LOOP_ACTIONS, "allowed action")
1717
+ for action in self.disallowed_actions:
1718
+ require_literal(action, PRODUCT_LOOP_ACTIONS, "disallowed action")
1719
+
1720
+ def to_json(self) -> dict[str, Any]:
1721
+ return clean_dict(asdict(self))
1722
+
1723
+
1724
+ @dataclass(frozen=True)
1725
+ class PhaseTeamEligibility:
1726
+ allowed_execution_modes: tuple[str, ...]
1727
+ default_execution_mode: str
1728
+ eligible_for_native_team: bool
1729
+ has_disjoint_write_lanes: bool
1730
+ has_only_read_only_lanes: bool
1731
+ unmanaged_write_risk: bool
1732
+ reason: str
1733
+ invalid_reasons: tuple[str, ...] = ()
1734
+ lane_summaries: tuple[dict[str, Any], ...] = ()
1735
+
1736
+ def __post_init__(self) -> None:
1737
+ require_literal(self.default_execution_mode, CLAUDE_EXECUTION_MODES, "default Claude execution mode")
1738
+ for mode in self.allowed_execution_modes:
1739
+ require_literal(mode, CLAUDE_EXECUTION_MODES, "allowed Claude execution mode")
1740
+
1741
+ def to_json(self) -> dict[str, Any]:
1742
+ return clean_dict(asdict(self))
1743
+
1744
+
1745
+ @dataclass(frozen=True)
1746
+ class DispatchDecision:
1747
+ action: str
1748
+ selected_executor: str | None
1749
+ source: str
1750
+ preferred_executors: tuple[str, ...] = ()
1751
+ allowed_executors: tuple[str, ...] = ()
1752
+ fallback_executors: tuple[str, ...] = ()
1753
+ disabled_executors: tuple[str, ...] = ()
1754
+ required_capabilities: tuple[str, ...] = ()
1755
+ considered_executors: tuple[str, ...] = ()
1756
+ blocked_reason: str | None = None
1757
+ blocked_summary: str | None = None
1758
+ fallback_applied: bool = False
1759
+ selected_via: str | None = None
1760
+
1761
+ def __post_init__(self) -> None:
1762
+ require_literal(self.action, PRODUCT_LOOP_ACTIONS, "product action")
1763
+ if self.selected_executor is not None:
1764
+ require_literal(self.selected_executor, EXECUTORS, "selected executor")
1765
+ if self.selected_via is not None:
1766
+ require_literal(self.selected_via, DISPATCH_SELECTION_PATHS, "dispatch selection path")
1767
+ for field_name in (
1768
+ "preferred_executors",
1769
+ "allowed_executors",
1770
+ "fallback_executors",
1771
+ "disabled_executors",
1772
+ "considered_executors",
1773
+ ):
1774
+ values = getattr(self, field_name)
1775
+ for value in values:
1776
+ require_literal(value, EXECUTORS, field_name)
1777
+ for capability in self.required_capabilities:
1778
+ require_literal(capability, DISPATCH_CAPABILITIES, "required capability")
1779
+
1780
+ @property
1781
+ def blocked(self) -> bool:
1782
+ return self.selected_executor is None
1783
+
1784
+ def to_json(self) -> dict[str, Any]:
1785
+ return clean_dict(asdict(self))
1786
+
1787
+
1788
+ @dataclass(frozen=True)
1789
+ class PermissionPolicy:
1790
+ sandbox_mode: str
1791
+ bypass_approvals: bool = False
1792
+
1793
+ def to_json(self) -> dict[str, Any]:
1794
+ return clean_dict(asdict(self))
1795
+
1796
+
1797
+ @dataclass(frozen=True)
1798
+ class CommandAdapterConfig:
1799
+ name: str
1800
+ template: str
1801
+ delivery_mode: str = "context_file"
1802
+ supported_actions: tuple[str, ...] = COMMAND_ADAPTER_SUPPORTED_ACTIONS
1803
+
1804
+ def __post_init__(self) -> None:
1805
+ require_literal(self.delivery_mode, INJECTION_MODES, "command adapter delivery mode")
1806
+ for action in self.supported_actions:
1807
+ require_literal(action, PRODUCT_LOOP_ACTIONS, "command adapter supported action")
1808
+
1809
+ def to_json(self) -> dict[str, Any]:
1810
+ return clean_dict(asdict(self))
1811
+
1812
+
1813
+ @dataclass(frozen=True)
1814
+ class LaunchRequest:
1815
+ executor: str
1816
+ action: str
1817
+ repo: Path
1818
+ roadmap: Path
1819
+ phase: str | None
1820
+ plan: Path | None
1821
+ model_selection: ModelSelection
1822
+ prompt_bundle: PromptBundle
1823
+ injection_metadata: InjectionMetadata
1824
+ permission_policy: PermissionPolicy
1825
+ command_adapter: CommandAdapterConfig | None = None
1826
+ dispatch_decision: DispatchDecision | None = None
1827
+ harness_lane_assignment: HarnessLaneAssignment | None = None
1828
+ delegation_request: "DelegationRequest | None" = None
1829
+ parent_child_metadata: "ParentChildRunMetadata | None" = None
1830
+ claude_execution_mode: str | None = None
1831
+ claude_team_policy: ClaudeTeamPolicy | None = None
1832
+ phase_team_eligibility: PhaseTeamEligibility | None = None
1833
+ json_output: bool = False
1834
+ bypass_approvals: bool = False
1835
+ launch_timeout_seconds: int | None = None
1836
+
1837
+ def __post_init__(self) -> None:
1838
+ require_literal(self.executor, EXECUTORS, "executor")
1839
+ if self.claude_execution_mode is not None:
1840
+ require_literal(self.claude_execution_mode, CLAUDE_EXECUTION_MODES, "Claude execution mode")
1841
+
1842
+ def to_json(self) -> dict[str, Any]:
1843
+ return {
1844
+ "executor": self.executor,
1845
+ "action": self.action,
1846
+ "repo": str(self.repo),
1847
+ "roadmap": str(self.roadmap),
1848
+ "phase": self.phase,
1849
+ "plan": str(self.plan) if self.plan is not None else None,
1850
+ "model_selection": self.model_selection.to_json(),
1851
+ "prompt_bundle": self.prompt_bundle.to_json(),
1852
+ "injection_metadata": self.injection_metadata.to_json(),
1853
+ "permission_policy": self.permission_policy.to_json(),
1854
+ "command_adapter": self.command_adapter.to_json() if self.command_adapter else None,
1855
+ "dispatch_decision": self.dispatch_decision.to_json() if self.dispatch_decision else None,
1856
+ "harness_lane_assignment": self.harness_lane_assignment.to_json() if self.harness_lane_assignment else None,
1857
+ "delegation_request": self.delegation_request.to_json() if self.delegation_request else None,
1858
+ "parent_child_metadata": self.parent_child_metadata.to_json() if self.parent_child_metadata else None,
1859
+ "claude_execution_mode": self.claude_execution_mode,
1860
+ "claude_team_policy": self.claude_team_policy.to_json() if self.claude_team_policy else None,
1861
+ "phase_team_eligibility": self.phase_team_eligibility.to_json() if self.phase_team_eligibility else None,
1862
+ "json_output": self.json_output,
1863
+ "bypass_approvals": self.bypass_approvals,
1864
+ "launch_timeout_seconds": self.launch_timeout_seconds,
1865
+ }
1866
+
1867
+
1868
+ @dataclass(frozen=True)
1869
+ class DelegationBudget:
1870
+ max_tokens: int | None = None
1871
+ max_seconds: int | None = None
1872
+ max_cost_usd: float | None = None
1873
+ notes: str | None = None
1874
+
1875
+ def to_json(self) -> dict[str, Any]:
1876
+ return clean_dict(asdict(self))
1877
+
1878
+ def is_defined(self) -> bool:
1879
+ return any(value is not None for value in (self.max_tokens, self.max_seconds, self.max_cost_usd)) or bool(self.notes)
1880
+
1881
+
1882
+ @dataclass(frozen=True)
1883
+ class DelegationRequest:
1884
+ request_id: str
1885
+ product_action: str
1886
+ target_executor: str
1887
+ reason: str
1888
+ owned_files: tuple[str, ...]
1889
+ expected_output: str
1890
+ priority: str = "normal"
1891
+ review_context: str | None = None
1892
+ repair_context: str | None = None
1893
+ budget: DelegationBudget | None = None
1894
+ metadata: dict[str, Any] = field(default_factory=dict)
1895
+
1896
+ def __post_init__(self) -> None:
1897
+ require_literal(self.product_action, PRODUCT_LOOP_ACTIONS, "product action")
1898
+ require_literal(self.target_executor, EXECUTORS, "target executor")
1899
+ require_literal(self.priority, DELEGATION_PRIORITIES, "delegation priority")
1900
+
1901
+ def to_json(self) -> dict[str, Any]:
1902
+ return clean_dict(
1903
+ {
1904
+ "request_id": self.request_id,
1905
+ "product_action": self.product_action,
1906
+ "target_executor": self.target_executor,
1907
+ "reason": self.reason,
1908
+ "owned_files": self.owned_files,
1909
+ "expected_output": self.expected_output,
1910
+ "priority": self.priority,
1911
+ "review_context": self.review_context,
1912
+ "repair_context": self.repair_context,
1913
+ "budget": self.budget.to_json() if self.budget else None,
1914
+ "metadata": self.metadata,
1915
+ }
1916
+ )
1917
+
1918
+
1919
+ @dataclass(frozen=True)
1920
+ class DelegationDecision:
1921
+ request_id: str
1922
+ status: str
1923
+ reason_code: str
1924
+ summary: str
1925
+ selected_executor: str | None = None
1926
+ dispatch_decision: dict[str, Any] | None = None
1927
+ human_required: bool = False
1928
+ blocker_class: str | None = None
1929
+ observed_depth: int | None = None
1930
+ observed_fanout: int | None = None
1931
+ validation_order: tuple[str, ...] = (
1932
+ "active_loop_mode",
1933
+ "ownership_boundaries",
1934
+ "depth_limit",
1935
+ "fanout_limit",
1936
+ "budget_metadata",
1937
+ "dispatch_policy",
1938
+ )
1939
+
1940
+ def __post_init__(self) -> None:
1941
+ require_literal(self.status, DELEGATION_STATUSES, "delegation status")
1942
+ if self.selected_executor is not None:
1943
+ require_literal(self.selected_executor, EXECUTORS, "selected executor")
1944
+ if self.blocker_class is not None:
1945
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
1946
+
1947
+ @property
1948
+ def approved(self) -> bool:
1949
+ return self.status == "approved"
1950
+
1951
+ def to_json(self) -> dict[str, Any]:
1952
+ return clean_dict(asdict(self))
1953
+
1954
+
1955
+ @dataclass(frozen=True)
1956
+ class ParentChildRunMetadata:
1957
+ parent_phase: str
1958
+ parent_action: str
1959
+ parent_executor: str | None = None
1960
+ parent_run_id: str | None = None
1961
+ child_phase: str | None = None
1962
+ child_action: str | None = None
1963
+ child_run_id: str | None = None
1964
+ request_id: str | None = None
1965
+ child_executor: str | None = None
1966
+ observed_launch_path: str | None = None
1967
+ child_artifact_root: str | None = None
1968
+ child_worktree_root: str | None = None
1969
+ child_closeout_result: dict[str, Any] | None = None
1970
+
1971
+ def __post_init__(self) -> None:
1972
+ require_literal(self.parent_action, PRODUCT_LOOP_ACTIONS, "parent action")
1973
+ if self.parent_executor is not None:
1974
+ require_literal(self.parent_executor, EXECUTORS, "parent executor")
1975
+ if self.child_action is not None:
1976
+ require_literal(self.child_action, PRODUCT_LOOP_ACTIONS, "child action")
1977
+ if self.child_executor is not None:
1978
+ require_literal(self.child_executor, EXECUTORS, "child executor")
1979
+
1980
+ def to_json(self) -> dict[str, Any]:
1981
+ return clean_dict(asdict(self))
1982
+
1983
+
1984
+ @dataclass(frozen=True)
1985
+ class Blocker:
1986
+ human_required: bool = False
1987
+ blocker_class: str | None = None
1988
+ blocker_summary: str | None = None
1989
+ required_human_inputs: tuple[str, ...] = ()
1990
+
1991
+ def __post_init__(self) -> None:
1992
+ if self.blocker_class is not None:
1993
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
1994
+
1995
+ def to_json(self) -> dict[str, Any]:
1996
+ return clean_dict(asdict(self))
1997
+
1998
+
1999
+ # CS-2.1 SA — fleet-metric ledger event types. These populate the three
2000
+ # LEDGER-FAITHFUL named fleet-trend metrics (velocity / burn_down /
2001
+ # promise_broken_duration) reserved by the CS-2.1 spine. They are emitted to a
2002
+ # SEPARATE append-only sibling ledger (``.phase-loop/fleet-metrics.jsonl``) so
2003
+ # the existing ``events.jsonl`` bytes and reconcile semantics stay untouched.
2004
+ FLEET_METRIC_SCHEMA = "fleet_metric.v1"
2005
+ FLEET_METRIC_KINDS = (
2006
+ "velocity", # throughput: cumulative completed phases + timestamp
2007
+ "burn_down", # remaining-vs-completed scope trajectory
2008
+ "promise_broken", # an IF-gate promise declared-but-not-produced (break)
2009
+ "promise_repaired", # a previously-broken IF-gate promise later produced
2010
+ )
2011
+
2012
+
2013
+ @dataclass(frozen=True)
2014
+ class FleetMetricEvent:
2015
+ """One append to the fleet-metric ledger (CS-2.1 SA).
2016
+
2017
+ ``event_kind`` is fixed to ``fleet_metric`` and ``metric_kind`` selects the
2018
+ series. ``payload`` carries only the small numeric/label fields that series
2019
+ needs (never a filesystem path, secret, or session internal — the export
2020
+ bridge re-asserts that before anything leaves the enforcement side).
2021
+ """
2022
+
2023
+ metric_kind: str
2024
+ timestamp: str = field(default_factory=utc_now)
2025
+ phase: str | None = None
2026
+ payload: dict[str, Any] = field(default_factory=dict)
2027
+ schema: str = FLEET_METRIC_SCHEMA
2028
+
2029
+ def __post_init__(self) -> None:
2030
+ require_literal(self.metric_kind, FLEET_METRIC_KINDS, "fleet metric kind")
2031
+ if self.schema != FLEET_METRIC_SCHEMA:
2032
+ raise ValueError(f"invalid fleet metric schema: {self.schema}")
2033
+
2034
+ def to_json(self) -> dict[str, Any]:
2035
+ return clean_dict(
2036
+ {
2037
+ "event_kind": "fleet_metric",
2038
+ "schema": self.schema,
2039
+ "metric_kind": self.metric_kind,
2040
+ "timestamp": self.timestamp,
2041
+ "phase": self.phase,
2042
+ "payload": self.payload,
2043
+ }
2044
+ )
2045
+
2046
+
2047
+ @dataclass(frozen=True)
2048
+ class LoopEvent:
2049
+ timestamp: str
2050
+ repo: str
2051
+ roadmap: str
2052
+ phase: str
2053
+ action: str
2054
+ status: str
2055
+ model: str
2056
+ reasoning_effort: str
2057
+ source: str
2058
+ override_reason: str | None = None
2059
+ command: list[str] | None = None
2060
+ blocker: dict[str, Any] | None = None
2061
+ metadata: dict[str, Any] | None = None
2062
+ git_topology: dict[str, Any] | None = None
2063
+ selected_executor: str | None = None
2064
+ schema_version: int = 2
2065
+ roadmap_sha256: str | None = None
2066
+ phase_sha256: str | None = None
2067
+
2068
+ def __post_init__(self) -> None:
2069
+ require_literal(self.status, EVENT_STATUSES, "event status")
2070
+ if self.selected_executor is not None:
2071
+ require_literal(self.selected_executor, EXECUTORS, "selected executor")
2072
+
2073
+ def to_json(self) -> dict[str, Any]:
2074
+ return clean_dict(asdict(self))
2075
+
2076
+
2077
+ @dataclass(frozen=True)
2078
+ class StateSnapshot:
2079
+ timestamp: str
2080
+ repo: str
2081
+ roadmap: str
2082
+ phases: dict[str, str] = field(default_factory=dict)
2083
+ current_phase: str | None = None
2084
+ last_action: str | None = None
2085
+ model: str | None = None
2086
+ reasoning_effort: str | None = None
2087
+ source: str | None = None
2088
+ override_reason: str | None = None
2089
+ human_required: bool = False
2090
+ blocker_class: str | None = None
2091
+ blocker_summary: str | None = None
2092
+ required_human_inputs: tuple[str, ...] = ()
2093
+ access_attempts: tuple[dict[str, Any], ...] = ()
2094
+ dirty_paths: tuple[str, ...] = ()
2095
+ phase_owned_dirty_paths: tuple[str, ...] = ()
2096
+ previous_phase_owned_paths: tuple[str, ...] = ()
2097
+ unowned_dirty_paths: tuple[str, ...] = ()
2098
+ pre_existing_dirty_paths: tuple[str, ...] = ()
2099
+ phase_owned_dirty: bool = False
2100
+ terminal_summary: dict[str, Any] | None = None
2101
+ latest_metric: dict[str, Any] | None = None
2102
+ metrics_summary: dict[str, Any] | None = None
2103
+ closeout_terminal_status: str | None = None
2104
+ closeout_summary: dict[str, Any] | None = None
2105
+ work_units: dict[str, dict[str, Any]] = field(default_factory=dict)
2106
+ latest_work_unit: dict[str, Any] | None = None
2107
+ pipeline_mode: str = "standalone"
2108
+ schema_version: int = 2
2109
+ roadmap_sha256: str | None = None
2110
+ phase_sha256: dict[str, str] = field(default_factory=dict)
2111
+ ledger_warnings: tuple[dict[str, Any], ...] = ()
2112
+ ledger_duplicates_skipped: tuple[dict[str, Any], ...] = ()
2113
+ git_topology: dict[str, Any] | None = None
2114
+
2115
+ def __post_init__(self) -> None:
2116
+ for status in self.phases.values():
2117
+ require_literal(status, PHASE_STATUSES, "phase status")
2118
+ if self.blocker_class is not None:
2119
+ require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
2120
+ require_literal(self.pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
2121
+ for key, value in self.work_units.items():
2122
+ if not isinstance(value, dict):
2123
+ raise ValueError(f"invalid work-unit state record: {key}")
2124
+ WorkUnitState.from_json(value)
2125
+
2126
+ def to_json(self) -> dict[str, Any]:
2127
+ return clean_dict(asdict(self))
2128
+
2129
+
2130
+ def clean_dict(value: Any) -> Any:
2131
+ if isinstance(value, dict):
2132
+ return {k: clean_dict(v) for k, v in value.items() if v is not None}
2133
+ if isinstance(value, list):
2134
+ return [clean_dict(v) for v in value]
2135
+ if isinstance(value, tuple):
2136
+ return [clean_dict(v) for v in value]
2137
+ return value
2138
+
2139
+
2140
+ def metadata_command(command: list[str], prompt: str | None = None) -> list[str]:
2141
+ redacted = list(command)
2142
+ if prompt is not None and redacted and redacted[-1] == prompt:
2143
+ redacted[-1] = f"<prompt redacted sha256={hashlib.sha256(prompt.encode('utf-8')).hexdigest()}>"
2144
+ return redacted