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,415 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path, PurePosixPath
8
+ from typing import Any
9
+
10
+ from .discovery import PLAN_RE
11
+
12
+
13
+ SCHEMA_VERSION = 1
14
+ # Repo-relative default only; resolution always joins this against an explicit
15
+ # repo root supplied by the caller (see _manifest_path) -- never an implicit
16
+ # fleet-absolute hardcode (DECOUPLE SL-2).
17
+ MANIFEST_PATH = PurePosixPath("plans/manifest.json")
18
+ PLAN_TYPES = {"phase", "detailed"}
19
+ PLAN_STATUSES = {"imported", "committed", "executing", "completed", "failed", "orphaned"}
20
+ TRANSITIONS = {
21
+ "imported": {"executing", "orphaned"},
22
+ "committed": {"executing", "orphaned"},
23
+ "executing": {"completed", "failed", "orphaned"},
24
+ }
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class DotfilesPlanRef:
29
+ slug: str
30
+ file: str
31
+ type: str
32
+ status: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class DotfilesPlanLifecycleEvent:
37
+ transition: str
38
+ by: str
39
+ at: str
40
+ metadata: dict[str, Any] = field(default_factory=dict)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class DotfilesPlanEntry:
45
+ slug: str
46
+ file: str
47
+ type: str
48
+ status: str
49
+ created_at: str
50
+ updated_at: str
51
+ owner_skill: str
52
+ handoff_ref: str | None = None
53
+ reflection_ref: str | None = None
54
+ task_summary: str | None = None
55
+ acceptance_criteria_count: int | None = None
56
+ roadmap_ref: DotfilesPlanRef | None = None
57
+ phase_alias: str | None = None
58
+ if_gates_produced: tuple[str, ...] = ()
59
+ lanes: tuple[str, ...] = ()
60
+ lifecycle: tuple[DotfilesPlanLifecycleEvent, ...] = ()
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class DotfilesPlanManifest:
65
+ schema_version: int = SCHEMA_VERSION
66
+ plans: tuple[DotfilesPlanEntry, ...] = ()
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class ValidationResult:
71
+ valid: bool
72
+ errors: tuple[str, ...] = ()
73
+
74
+
75
+ def read_manifest(repo: Path) -> DotfilesPlanManifest:
76
+ manifest_path = _manifest_path(repo)
77
+ if not manifest_path.exists():
78
+ return DotfilesPlanManifest()
79
+ try:
80
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
81
+ except json.JSONDecodeError as exc:
82
+ raise ValueError(f"manifest JSON is malformed at line {exc.lineno} column {exc.colno}") from exc
83
+ manifest = _manifest_from_json(data)
84
+ if manifest.schema_version != SCHEMA_VERSION:
85
+ raise ValueError(f"unsupported manifest schema_version: {manifest.schema_version}")
86
+ return manifest
87
+
88
+
89
+ def append_entry(repo: Path, entry: DotfilesPlanEntry) -> None:
90
+ manifest = read_manifest(repo)
91
+ entries = {existing.slug: existing for existing in manifest.plans}
92
+ entries[entry.slug] = entry
93
+ _write_manifest(repo, DotfilesPlanManifest(plans=tuple(entries[slug] for slug in sorted(entries))))
94
+
95
+
96
+ def update_lifecycle(repo: Path, slug: str, transition: str, by: str, metadata: dict[str, Any]) -> None:
97
+ if not isinstance(metadata, dict):
98
+ raise ValueError("metadata must be an object")
99
+ manifest = read_manifest(repo)
100
+ now = _utc_now()
101
+ entries: list[DotfilesPlanEntry] = []
102
+ updated = False
103
+ for entry in manifest.plans:
104
+ if entry.slug != slug:
105
+ entries.append(entry)
106
+ continue
107
+ allowed = TRANSITIONS.get(entry.status, set())
108
+ if transition not in allowed:
109
+ raise ValueError(f"invalid lifecycle transition for {slug}: {entry.status} -> {transition}")
110
+ event = DotfilesPlanLifecycleEvent(transition=transition, by=by, at=now, metadata=metadata)
111
+ entries.append(
112
+ DotfilesPlanEntry(
113
+ slug=entry.slug,
114
+ file=entry.file,
115
+ type=entry.type,
116
+ status=transition,
117
+ created_at=entry.created_at,
118
+ updated_at=now,
119
+ owner_skill=entry.owner_skill,
120
+ handoff_ref=entry.handoff_ref,
121
+ reflection_ref=entry.reflection_ref,
122
+ task_summary=entry.task_summary,
123
+ acceptance_criteria_count=entry.acceptance_criteria_count,
124
+ roadmap_ref=entry.roadmap_ref,
125
+ phase_alias=entry.phase_alias,
126
+ if_gates_produced=entry.if_gates_produced,
127
+ lanes=entry.lanes,
128
+ lifecycle=(*entry.lifecycle, event),
129
+ )
130
+ )
131
+ updated = True
132
+ if not updated:
133
+ raise ValueError(f"manifest entry not found: {slug}")
134
+ _write_manifest(repo, DotfilesPlanManifest(plans=tuple(entries)))
135
+
136
+
137
+ def validate_manifest(manifest_path: Path) -> ValidationResult:
138
+ errors: list[str] = []
139
+ try:
140
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
141
+ except FileNotFoundError:
142
+ return ValidationResult(False, ("manifest file does not exist",))
143
+ except json.JSONDecodeError as exc:
144
+ return ValidationResult(False, (f"manifest JSON is malformed at line {exc.lineno} column {exc.colno}",))
145
+ if not isinstance(data, dict):
146
+ return ValidationResult(False, ("manifest must be an object",))
147
+ if data.get("schema_version") != SCHEMA_VERSION:
148
+ errors.append("schema_version must be 1")
149
+ plans = data.get("plans")
150
+ if not isinstance(plans, list):
151
+ errors.append("plans must be an array")
152
+ return ValidationResult(False, tuple(errors))
153
+ repo = manifest_path.parent.parent
154
+ seen: set[str] = set()
155
+ for index, raw_entry in enumerate(plans):
156
+ label = f"plans[{index}]"
157
+ if not isinstance(raw_entry, dict):
158
+ errors.append(f"{label} must be an object")
159
+ continue
160
+ slug = raw_entry.get("slug")
161
+ if not isinstance(slug, str) or not slug:
162
+ errors.append(f"{label}.slug is required")
163
+ elif slug in seen:
164
+ errors.append(f"{label}.slug duplicates {slug}")
165
+ else:
166
+ seen.add(slug)
167
+ _validate_common_entry(label, raw_entry, repo, errors)
168
+ if raw_entry.get("type") == "phase":
169
+ _validate_phase_entry(label, raw_entry, errors)
170
+ elif raw_entry.get("type") == "detailed":
171
+ _validate_detailed_entry(label, raw_entry, errors)
172
+ _validate_lifecycle(label, raw_entry.get("lifecycle"), errors)
173
+ return ValidationResult(not errors, tuple(errors))
174
+
175
+
176
+ def import_existing_phase_plans(repo: Path) -> DotfilesPlanManifest:
177
+ entries: list[DotfilesPlanEntry] = []
178
+ for plan_path in sorted((repo / "plans").glob("phase-plan-v*-*.md")):
179
+ match = PLAN_RE.search(plan_path.name)
180
+ if not match:
181
+ continue
182
+ version, phase_alias = match.groups()
183
+ rel_path = plan_path.relative_to(repo).as_posix()
184
+ slug = f"{version}-{phase_alias}"
185
+ timestamp = _timestamp_for_path(plan_path)
186
+ roadmap_file = _frontmatter_value(plan_path, "roadmap")
187
+ roadmap_ref = (
188
+ DotfilesPlanRef(
189
+ slug=Path(roadmap_file).stem,
190
+ file=roadmap_file,
191
+ type="phase",
192
+ status="imported",
193
+ )
194
+ if roadmap_file
195
+ else None
196
+ )
197
+ entries.append(
198
+ DotfilesPlanEntry(
199
+ slug=slug,
200
+ file=rel_path,
201
+ type="phase",
202
+ status="imported",
203
+ created_at=timestamp,
204
+ updated_at=timestamp,
205
+ owner_skill="codex-plan-phase",
206
+ roadmap_ref=roadmap_ref,
207
+ phase_alias=phase_alias,
208
+ if_gates_produced=_extract_if_gates(plan_path),
209
+ lanes=_extract_lanes(plan_path),
210
+ )
211
+ )
212
+ return DotfilesPlanManifest(plans=tuple(entries))
213
+
214
+
215
+ def _manifest_path(repo: Path) -> Path:
216
+ return Path(repo) / MANIFEST_PATH
217
+
218
+
219
+ def _write_manifest(repo: Path, manifest: DotfilesPlanManifest) -> None:
220
+ manifest_path = _manifest_path(repo)
221
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
222
+ payload = json.dumps(_manifest_to_json(manifest), indent=2, sort_keys=True) + "\n"
223
+ if manifest_path.exists() and manifest_path.read_text(encoding="utf-8") == payload:
224
+ return
225
+ manifest_path.write_text(payload, encoding="utf-8")
226
+
227
+
228
+ def _manifest_to_json(manifest: DotfilesPlanManifest) -> dict[str, Any]:
229
+ return {
230
+ "schema_version": manifest.schema_version,
231
+ "plans": [_entry_to_json(entry) for entry in manifest.plans],
232
+ }
233
+
234
+
235
+ def _entry_to_json(entry: DotfilesPlanEntry) -> dict[str, Any]:
236
+ return {
237
+ "acceptance_criteria_count": entry.acceptance_criteria_count,
238
+ "created_at": entry.created_at,
239
+ "file": entry.file,
240
+ "handoff_ref": entry.handoff_ref,
241
+ "if_gates_produced": list(entry.if_gates_produced),
242
+ "lanes": list(entry.lanes),
243
+ "lifecycle": [_event_to_json(event) for event in entry.lifecycle],
244
+ "owner_skill": entry.owner_skill,
245
+ "phase_alias": entry.phase_alias,
246
+ "reflection_ref": entry.reflection_ref,
247
+ "roadmap_ref": _ref_to_json(entry.roadmap_ref) if entry.roadmap_ref else None,
248
+ "slug": entry.slug,
249
+ "status": entry.status,
250
+ "task_summary": entry.task_summary,
251
+ "type": entry.type,
252
+ "updated_at": entry.updated_at,
253
+ }
254
+
255
+
256
+ def _event_to_json(event: DotfilesPlanLifecycleEvent) -> dict[str, Any]:
257
+ return {
258
+ "at": event.at,
259
+ "by": event.by,
260
+ "metadata": event.metadata,
261
+ "transition": event.transition,
262
+ }
263
+
264
+
265
+ def _ref_to_json(ref: DotfilesPlanRef) -> dict[str, Any]:
266
+ return {"file": ref.file, "slug": ref.slug, "status": ref.status, "type": ref.type}
267
+
268
+
269
+ def _manifest_from_json(data: Any) -> DotfilesPlanManifest:
270
+ if not isinstance(data, dict):
271
+ raise ValueError("manifest must be an object")
272
+ plans = data.get("plans", [])
273
+ if not isinstance(plans, list):
274
+ raise ValueError("manifest plans must be an array")
275
+ return DotfilesPlanManifest(
276
+ schema_version=int(data.get("schema_version", 0)),
277
+ plans=tuple(_entry_from_json(entry) for entry in plans),
278
+ )
279
+
280
+
281
+ def _entry_from_json(data: Any) -> DotfilesPlanEntry:
282
+ if not isinstance(data, dict):
283
+ raise ValueError("manifest entry must be an object")
284
+ roadmap_ref = data.get("roadmap_ref")
285
+ return DotfilesPlanEntry(
286
+ slug=str(data.get("slug", "")),
287
+ file=str(data.get("file", "")),
288
+ type=str(data.get("type", "")),
289
+ status=str(data.get("status", "")),
290
+ created_at=str(data.get("created_at", "")),
291
+ updated_at=str(data.get("updated_at", "")),
292
+ owner_skill=str(data.get("owner_skill", "")),
293
+ handoff_ref=data.get("handoff_ref"),
294
+ reflection_ref=data.get("reflection_ref"),
295
+ task_summary=data.get("task_summary"),
296
+ acceptance_criteria_count=data.get("acceptance_criteria_count"),
297
+ roadmap_ref=_ref_from_json(roadmap_ref) if roadmap_ref is not None else None,
298
+ phase_alias=data.get("phase_alias"),
299
+ if_gates_produced=tuple(data.get("if_gates_produced") or ()),
300
+ lanes=tuple(data.get("lanes") or ()),
301
+ lifecycle=tuple(_event_from_json(event) for event in data.get("lifecycle") or ()),
302
+ )
303
+
304
+
305
+ def _event_from_json(data: Any) -> DotfilesPlanLifecycleEvent:
306
+ if not isinstance(data, dict):
307
+ raise ValueError("manifest lifecycle event must be an object")
308
+ metadata = data.get("metadata", {})
309
+ if not isinstance(metadata, dict):
310
+ raise ValueError("manifest lifecycle metadata must be an object")
311
+ return DotfilesPlanLifecycleEvent(
312
+ transition=str(data.get("transition", "")),
313
+ by=str(data.get("by", "")),
314
+ at=str(data.get("at", "")),
315
+ metadata=metadata,
316
+ )
317
+
318
+
319
+ def _ref_from_json(data: Any) -> DotfilesPlanRef:
320
+ if not isinstance(data, dict):
321
+ raise ValueError("manifest plan ref must be an object")
322
+ return DotfilesPlanRef(
323
+ slug=str(data.get("slug", "")),
324
+ file=str(data.get("file", "")),
325
+ type=str(data.get("type", "")),
326
+ status=str(data.get("status", "")),
327
+ )
328
+
329
+
330
+ def _validate_common_entry(label: str, entry: dict[str, Any], repo: Path, errors: list[str]) -> None:
331
+ for field_name in ("file", "type", "status", "created_at", "updated_at", "owner_skill"):
332
+ if not isinstance(entry.get(field_name), str) or not entry.get(field_name):
333
+ errors.append(f"{label}.{field_name} is required")
334
+ if entry.get("type") not in PLAN_TYPES:
335
+ errors.append(f"{label}.type must be phase or detailed")
336
+ if entry.get("status") not in PLAN_STATUSES:
337
+ errors.append(f"{label}.status is invalid")
338
+ file_value = entry.get("file")
339
+ if isinstance(file_value, str) and file_value:
340
+ if Path(file_value).is_absolute() or ".." in PurePosixPath(file_value).parts:
341
+ errors.append(f"{label}.file must be repo-relative")
342
+ elif not (repo / file_value).exists():
343
+ errors.append(f"{label}.file does not exist")
344
+
345
+
346
+ def _validate_phase_entry(label: str, entry: dict[str, Any], errors: list[str]) -> None:
347
+ if not isinstance(entry.get("phase_alias"), str) or not entry.get("phase_alias"):
348
+ errors.append(f"{label}.phase_alias is required for phase entries")
349
+ if entry.get("task_summary") is not None or entry.get("acceptance_criteria_count") is not None:
350
+ errors.append(f"{label} mixes detailed-only fields into a phase entry")
351
+ if not isinstance(entry.get("if_gates_produced"), list):
352
+ errors.append(f"{label}.if_gates_produced must be an array")
353
+ if not isinstance(entry.get("lanes"), list):
354
+ errors.append(f"{label}.lanes must be an array")
355
+ roadmap_ref = entry.get("roadmap_ref")
356
+ if roadmap_ref is not None and not isinstance(roadmap_ref, dict):
357
+ errors.append(f"{label}.roadmap_ref must be an object or null")
358
+
359
+
360
+ def _validate_detailed_entry(label: str, entry: dict[str, Any], errors: list[str]) -> None:
361
+ if not isinstance(entry.get("task_summary"), str) or not entry.get("task_summary"):
362
+ errors.append(f"{label}.task_summary is required for detailed entries")
363
+ if not isinstance(entry.get("acceptance_criteria_count"), int):
364
+ errors.append(f"{label}.acceptance_criteria_count is required for detailed entries")
365
+ if entry.get("roadmap_ref") is not None or entry.get("phase_alias") is not None:
366
+ errors.append(f"{label} mixes phase-only fields into a detailed entry")
367
+ if entry.get("if_gates_produced") not in (None, []):
368
+ errors.append(f"{label}.if_gates_produced must be empty for detailed entries")
369
+ if entry.get("lanes") not in (None, []):
370
+ errors.append(f"{label}.lanes must be empty for detailed entries")
371
+
372
+
373
+ def _validate_lifecycle(label: str, lifecycle: Any, errors: list[str]) -> None:
374
+ if not isinstance(lifecycle, list):
375
+ errors.append(f"{label}.lifecycle must be an array")
376
+ return
377
+ for index, event in enumerate(lifecycle):
378
+ event_label = f"{label}.lifecycle[{index}]"
379
+ if not isinstance(event, dict):
380
+ errors.append(f"{event_label} must be an object")
381
+ continue
382
+ for field_name in ("transition", "by", "at"):
383
+ if not isinstance(event.get(field_name), str) or not event.get(field_name):
384
+ errors.append(f"{event_label}.{field_name} is required")
385
+ if not isinstance(event.get("metadata"), dict):
386
+ errors.append(f"{event_label}.metadata must be an object")
387
+
388
+
389
+ def _utc_now() -> str:
390
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
391
+
392
+
393
+ def _timestamp_for_path(path: Path) -> str:
394
+ return datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
395
+
396
+
397
+ def _frontmatter_value(path: Path, field_name: str) -> str | None:
398
+ text = path.read_text(encoding="utf-8")
399
+ if not text.startswith("---\n"):
400
+ return None
401
+ end = text.find("\n---", 4)
402
+ if end == -1:
403
+ return None
404
+ match = re.search(rf"^{re.escape(field_name)}:\s*(.+?)\s*$", text[4:end], re.MULTILINE)
405
+ return match.group(1).strip() if match else None
406
+
407
+
408
+ def _extract_if_gates(path: Path) -> tuple[str, ...]:
409
+ text = path.read_text(encoding="utf-8")
410
+ return tuple(dict.fromkeys(re.findall(r"\bIF-[A-Za-z0-9._-]+", text)))
411
+
412
+
413
+ def _extract_lanes(path: Path) -> tuple[str, ...]:
414
+ text = path.read_text(encoding="utf-8")
415
+ return tuple(dict.fromkeys(re.findall(r"^###\s+(SL-\d+[A-Z]?)\b", text, re.MULTILINE)))
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import re
5
+ from typing import Iterable
6
+
7
+ from .models import (
8
+ BLOCKER_CLASSES,
9
+ DISPATCH_CAPABILITIES,
10
+ EXECUTORS,
11
+ PHASE_STATUSES,
12
+ PRODUCT_LOOP_ACTIONS,
13
+ )
14
+
15
+
16
+ VERIFICATION_STATUSES = ("not_run", "passed", "failed", "blocked")
17
+ EXECUTION_POLICY_DEFAULT_SELECTORS = ("work-unit defaults",)
18
+
19
+ DISPATCH_SECTION_RE = re.compile(
20
+ r"^#{2,3}\s+Dispatch Hints\s*$\n(?P<body>.*?)(?=^#{1,3}\s+\S|\Z)",
21
+ re.IGNORECASE | re.MULTILINE | re.DOTALL,
22
+ )
23
+ DISPATCH_SUBSECTION_RE = re.compile(
24
+ r"^#{3,4}\s+(?P<label>[^\n#]+?)\s*$\n(?P<body>.*?)(?=^#{3,4}\s+\S|\Z)",
25
+ re.IGNORECASE | re.MULTILINE | re.DOTALL,
26
+ )
27
+ DISPATCH_LINE_RE = re.compile(
28
+ r"^\s*-\s*(?:(?P<action>[A-Za-z][\w-]*)\s+)?"
29
+ r"(?P<label>preferred executors|allowed executors|fallback executors|disabled executors|required capabilities)"
30
+ r"\s*:\s*(?P<value>.+?)\s*$",
31
+ re.IGNORECASE | re.MULTILINE,
32
+ )
33
+ EXECUTION_POLICY_SECTION_RE = re.compile(
34
+ r"^#{2,3}\s+Execution Policy\s*$\n(?P<body>.*?)(?=^#{1,3}\s+\S|\Z)",
35
+ re.IGNORECASE | re.MULTILINE | re.DOTALL,
36
+ )
37
+ EXECUTION_POLICY_LINE_RE = re.compile(r"^\s*-\s*(?P<selector>[^:]+?)\s*:\s*(?P<value>.+?)\s*$", re.MULTILINE)
38
+ EXECUTOR_ASSIGNMENT_RE = re.compile(r"(?:^|[,;])\s*executor\s*=\s*(?P<value>`[^`]+`|[^,;]+)", re.IGNORECASE)
39
+ CLOSEOUT_LITERAL_RE = re.compile(
40
+ r"(?:^|[{,\n])\s*[\"']?(?P<field>terminal_status|verification_status|blocker_class)[\"']?\s*[:=]\s*"
41
+ r"(?P<value>`[^`]+`|\"[^\"]+\"|'[^']+'|[^,\n}]+)",
42
+ re.MULTILINE,
43
+ )
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ValidationFinding:
48
+ field_path: str
49
+ literal: str
50
+ allowed_values: tuple[str, ...]
51
+ suggested_fix: str
52
+
53
+
54
+ def validate_plan_dispatch_hints(
55
+ plan_text: str,
56
+ *,
57
+ dispatch_capabilities: Iterable[str] | None = None,
58
+ executors: Iterable[str] | None = None,
59
+ product_loop_actions: Iterable[str] | None = None,
60
+ ) -> list[ValidationFinding]:
61
+ """Validate planner-emitted protocol literals without mutating state."""
62
+
63
+ dispatch_capabilities = tuple(dispatch_capabilities or DISPATCH_CAPABILITIES)
64
+ executors = tuple(executors or EXECUTORS)
65
+ product_loop_actions = tuple(product_loop_actions or PRODUCT_LOOP_ACTIONS)
66
+ text = str(plan_text or "")
67
+ findings: list[ValidationFinding] = []
68
+
69
+ findings.extend(_validate_dispatch_hints(text, dispatch_capabilities, executors, product_loop_actions))
70
+ findings.extend(_validate_execution_policy(text, executors, product_loop_actions))
71
+ findings.extend(_validate_closeout_literals(text))
72
+ return findings
73
+
74
+
75
+ def _validate_dispatch_hints(
76
+ text: str,
77
+ dispatch_capabilities: tuple[str, ...],
78
+ executors: tuple[str, ...],
79
+ product_loop_actions: tuple[str, ...],
80
+ ) -> list[ValidationFinding]:
81
+ findings: list[ValidationFinding] = []
82
+ for section in DISPATCH_SECTION_RE.finditer(text):
83
+ bodies = [("default", section.group("body"))]
84
+ bodies.extend((m.group("label").strip().lower(), m.group("body")) for m in DISPATCH_SUBSECTION_RE.finditer(section.group("body")))
85
+ for bucket, body in bodies:
86
+ if bucket not in {"default", "roadmap default", "plan default"} and bucket not in product_loop_actions:
87
+ findings.append(_finding("dispatch_hints.selector", bucket, product_loop_actions))
88
+ for line in DISPATCH_LINE_RE.finditer(body):
89
+ action = (line.group("action") or bucket).strip().lower()
90
+ if action not in {"default", "roadmap default", "plan default"} and action not in product_loop_actions:
91
+ findings.append(_finding("dispatch_hints.selector", action, product_loop_actions))
92
+ label = line.group("label").strip().lower().replace(" ", "_")
93
+ values = _split_literals(line.group("value"))
94
+ allowed = dispatch_capabilities if label == "required_capabilities" else executors
95
+ for index, literal in enumerate(values):
96
+ if literal not in allowed:
97
+ findings.append(_finding(f"dispatch_hints.{label}[{index}]", literal, allowed))
98
+ return _dedupe(findings)
99
+
100
+
101
+ def _validate_execution_policy(
102
+ text: str,
103
+ executors: tuple[str, ...],
104
+ product_loop_actions: tuple[str, ...],
105
+ ) -> list[ValidationFinding]:
106
+ findings: list[ValidationFinding] = []
107
+ allowed_selectors = (*EXECUTION_POLICY_DEFAULT_SELECTORS, *product_loop_actions, "SL-<N>")
108
+ for section in EXECUTION_POLICY_SECTION_RE.finditer(text):
109
+ for line in EXECUTION_POLICY_LINE_RE.finditer(section.group("body")):
110
+ selector = _clean_literal(line.group("selector"))
111
+ selector_key = selector.lower()
112
+ if not (
113
+ selector_key in EXECUTION_POLICY_DEFAULT_SELECTORS
114
+ or selector_key in product_loop_actions
115
+ or re.fullmatch(r"SL-\d+[A-Z]?", selector, re.IGNORECASE)
116
+ ):
117
+ findings.append(_finding("execution_policy.selector", selector, allowed_selectors))
118
+ for assignment in EXECUTOR_ASSIGNMENT_RE.finditer(line.group("value")):
119
+ literal = _clean_literal(assignment.group("value"))
120
+ if literal not in executors:
121
+ findings.append(_finding(f"execution_policy.{selector}.executor", literal, executors))
122
+ return _dedupe(findings)
123
+
124
+
125
+ def _validate_closeout_literals(text: str) -> list[ValidationFinding]:
126
+ allowed_by_field = {
127
+ "terminal_status": PHASE_STATUSES,
128
+ "verification_status": VERIFICATION_STATUSES,
129
+ "blocker_class": (*BLOCKER_CLASSES, "none"),
130
+ }
131
+ findings: list[ValidationFinding] = []
132
+ for match in CLOSEOUT_LITERAL_RE.finditer(text):
133
+ field = match.group("field")
134
+ literal = _clean_literal(match.group("value"))
135
+ if literal in {"null", "None"}:
136
+ continue
137
+ allowed = allowed_by_field[field]
138
+ if literal not in allowed:
139
+ findings.append(_finding(f"closeout.{field}", literal, allowed))
140
+ return _dedupe(findings)
141
+
142
+
143
+ def _split_literals(value: str) -> list[str]:
144
+ return [literal for literal in (_clean_literal(part) for part in value.split(",")) if literal]
145
+
146
+
147
+ def _clean_literal(value: str) -> str:
148
+ literal = str(value or "").strip().rstrip(",")
149
+ literal = literal.split(" #", 1)[0].strip()
150
+ if (literal.startswith("`") and literal.endswith("`")) or (literal.startswith('"') and literal.endswith('"')) or (
151
+ literal.startswith("'") and literal.endswith("'")
152
+ ):
153
+ literal = literal[1:-1].strip()
154
+ return literal
155
+
156
+
157
+ def _finding(field_path: str, literal: str, allowed_values: Iterable[str]) -> ValidationFinding:
158
+ allowed = tuple(allowed_values)
159
+ return ValidationFinding(
160
+ field_path=field_path,
161
+ literal=literal,
162
+ allowed_values=allowed,
163
+ suggested_fix=f"Use one of: {', '.join(allowed)}",
164
+ )
165
+
166
+
167
+ def _dedupe(findings: Iterable[ValidationFinding]) -> list[ValidationFinding]:
168
+ seen: set[tuple[str, str]] = set()
169
+ deduped: list[ValidationFinding] = []
170
+ for finding in findings:
171
+ key = (finding.field_path, finding.literal)
172
+ if key in seen:
173
+ continue
174
+ seen.add(key)
175
+ deduped.append(finding)
176
+ return deduped