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,642 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_plan_doc.py — mechanical lint for phase-plan docs.
4
+
5
+ Usage:
6
+ validate_plan_doc.py <plan-path>
7
+
8
+ Checks the plan doc produced by /claude-plan-phase before it's handed off to
9
+ /claude-execute-phase. Exit 0 on clean pass; non-zero with a human-readable
10
+ failure list on stderr.
11
+
12
+ Checks (in order; all run even if earlier ones fail so the author sees
13
+ every issue at once):
14
+
15
+ (A) Required top-level headings present:
16
+ # <anything>
17
+ ## Context
18
+ ## Interface Freeze Gates
19
+ ## Lane Index & Dependencies
20
+ ## Lanes
21
+ ## Execution Notes
22
+ ## Acceptance Criteria
23
+ ## Verification
24
+ Optional: ## Cross-Repo Gates
25
+
26
+ (B) Lane Index stanzas parse cleanly: each SL-N has
27
+ Depends on: comma-separated SL-IDs or (none)
28
+ Blocks: comma-separated SL-IDs or (none)
29
+ Parallel-safe: yes | no | mixed
30
+
31
+ (C) DAG has no cycles (topological sort succeeds).
32
+
33
+ (D) `Owned files` globs across ### SL-N sections are disjoint.
34
+ If run inside a git repo, expand each glob via `git ls-files`
35
+ and check for overlap. Otherwise fall back to textual prefix
36
+ comparison — flag any exact glob duplication and warn about
37
+ unresolvable cases.
38
+
39
+ (E) Every `impl` task row in a lane's task table has a preceding
40
+ `test` task in the same lane.
41
+
42
+ (F) Every `Interfaces consumed` entry either appears in some upstream
43
+ lane's `Interfaces provided` or is tagged "(pre-existing)".
44
+
45
+ Design: zero external deps (stdlib only). Parses markdown by regex on
46
+ stable headings produced by the claude-plan-phase template — not a full
47
+ Markdown parser, which would be overkill.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import re
53
+ import subprocess
54
+ import sys
55
+ from dataclasses import dataclass, field
56
+ from pathlib import Path
57
+ from typing import Dict, List, Optional, Set, Tuple
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Data model
62
+
63
+ @dataclass
64
+ class Lane:
65
+ sl_id: str # e.g. "SL-1"
66
+ name: str # free-text after —
67
+ depends_on: List[str] = field(default_factory=list)
68
+ blocks: List[str] = field(default_factory=list)
69
+ parallel_safe: str = "" # yes | no | mixed
70
+ owned_globs: List[str] = field(default_factory=list)
71
+ interfaces_provided: List[str] = field(default_factory=list)
72
+ interfaces_consumed: List[str] = field(default_factory=list)
73
+ task_rows: List[dict] = field(default_factory=list) # parsed from the markdown table
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Helpers
78
+
79
+ def _read(path: Path) -> str:
80
+ try:
81
+ return path.read_text(encoding="utf-8")
82
+ except Exception as exc:
83
+ _fail(f"cannot read {path}: {exc}")
84
+ sys.exit(2)
85
+
86
+
87
+ def _top_headings(src: str) -> Set[str]:
88
+ return {
89
+ line[3:].strip()
90
+ for line in src.splitlines()
91
+ if line.startswith("## ")
92
+ }
93
+
94
+
95
+ def _has_h1(src: str) -> bool:
96
+ for line in src.splitlines():
97
+ if line.startswith("# ") and not line.startswith("## "):
98
+ return True
99
+ return False
100
+
101
+
102
+ def _extract_section(src: str, heading: str) -> str:
103
+ """Return the body of a ## heading up to the next ## heading or EOF."""
104
+ lines = src.splitlines()
105
+ want = f"## {heading}"
106
+ in_section = False
107
+ out: List[str] = []
108
+ for line in lines:
109
+ if line.startswith("## ") and line.strip() == want:
110
+ in_section = True
111
+ continue
112
+ if in_section and line.startswith("## "):
113
+ break
114
+ if in_section:
115
+ out.append(line)
116
+ return "\n".join(out)
117
+
118
+
119
+ _SL_HEADER_RE = re.compile(r"^(SL-\d+)\s*—\s*(.+?)\s*$")
120
+ _KEY_VALUE_RE = re.compile(r"^\s*(Depends on|Blocks|Parallel-safe)\s*:\s*(.*?)\s*$")
121
+ _COMMA_SPLIT_RE = re.compile(r"[,\s]+")
122
+
123
+
124
+ def _parse_lane_index(body: str) -> List[Lane]:
125
+ """Parse stanzas of the form:
126
+ SL-1 — name
127
+ Depends on: (none)
128
+ Blocks: SL-3, SL-4
129
+ Parallel-safe: yes
130
+ """
131
+ lanes: List[Lane] = []
132
+ current: Optional[Lane] = None
133
+ for line in body.splitlines():
134
+ if not line.strip():
135
+ continue
136
+ m_header = _SL_HEADER_RE.match(line)
137
+ if m_header:
138
+ if current is not None:
139
+ lanes.append(current)
140
+ current = Lane(sl_id=m_header.group(1), name=m_header.group(2))
141
+ continue
142
+ if current is None:
143
+ continue
144
+ m_kv = _KEY_VALUE_RE.match(line)
145
+ if not m_kv:
146
+ continue
147
+ key, value = m_kv.group(1), m_kv.group(2)
148
+ if key == "Depends on":
149
+ current.depends_on = _parse_sl_list(value)
150
+ elif key == "Blocks":
151
+ current.blocks = _parse_sl_list(value)
152
+ elif key == "Parallel-safe":
153
+ current.parallel_safe = value.split()[0] if value.strip() else ""
154
+ if current is not None:
155
+ lanes.append(current)
156
+ return lanes
157
+
158
+
159
+ def _parse_sl_list(value: str) -> List[str]:
160
+ value = value.strip()
161
+ if not value or value.lower() == "(none)":
162
+ return []
163
+ return [tok.strip() for tok in re.split(r",\s*", value) if tok.strip()]
164
+
165
+
166
+ # Regex for per-lane ### SL-N sections inside ## Lanes
167
+ _LANE_SECTION_RE = re.compile(r"^###\s+(SL-\d+)\b.*$", re.MULTILINE)
168
+
169
+
170
+ def _split_lane_sections(lanes_body: str) -> Dict[str, str]:
171
+ """Map SL-N -> section body (text between this ### heading and the next)."""
172
+ matches = list(_LANE_SECTION_RE.finditer(lanes_body))
173
+ out: Dict[str, str] = {}
174
+ for idx, m in enumerate(matches):
175
+ sl_id = m.group(1)
176
+ start = m.end()
177
+ end = matches[idx + 1].start() if idx + 1 < len(matches) else len(lanes_body)
178
+ out[sl_id] = lanes_body[start:end]
179
+ return out
180
+
181
+
182
+ _BULLET_VALUE_RE = re.compile(
183
+ r"^\s*-\s*\*\*(Scope|Owned files|Interfaces provided|Interfaces consumed)\*\*\s*:\s*(.*?)\s*$",
184
+ re.IGNORECASE,
185
+ )
186
+
187
+
188
+ def _parse_lane_section(body: str) -> dict:
189
+ """Extract Scope, Owned files, Interfaces provided, Interfaces consumed,
190
+ and task rows from a single ### SL-N section body."""
191
+ out = {
192
+ "owned_globs": [],
193
+ "interfaces_provided": [],
194
+ "interfaces_consumed": [],
195
+ "task_rows": [],
196
+ }
197
+ # Collect bullet values (may span multiple lines if formatted oddly;
198
+ # stick to single-line bullets per the template).
199
+ for line in body.splitlines():
200
+ m = _BULLET_VALUE_RE.match(line)
201
+ if not m:
202
+ continue
203
+ key = m.group(1).lower()
204
+ val = m.group(2)
205
+ items = _split_inline_items(val)
206
+ if key == "owned files":
207
+ out["owned_globs"] = items
208
+ elif key == "interfaces provided":
209
+ out["interfaces_provided"] = items
210
+ elif key == "interfaces consumed":
211
+ out["interfaces_consumed"] = items
212
+ # Parse task table rows. The plan template uses:
213
+ # | Task ID | Type | Depends on | Files in scope | Tests owned | Test command |
214
+ out["task_rows"] = _parse_task_table(body)
215
+ return out
216
+
217
+
218
+ _CODE_TOKEN_RE = re.compile(r"`([^`]+)`")
219
+
220
+
221
+ def _split_inline_items(raw: str) -> List[str]:
222
+ """Extract items from an inline list. Prefer backtick-quoted tokens; else
223
+ fall back to comma-splitting. Normalizes '(none)' to [].
224
+
225
+ Propagates an inline '(pre-existing)' annotation onto EVERY extracted
226
+ token so downstream checks (e.g., _check_f_interfaces_trace) can see
227
+ the annotation even when the user wrote it outside the backticks,
228
+ e.g. ``- **Interfaces consumed**: `IPlugin` (pre-existing)``.
229
+ """
230
+ s = raw.strip()
231
+ if not s or s.lower() in {"(none)", "none", "—"}:
232
+ return []
233
+ has_pre_existing = "(pre-existing)" in s.lower()
234
+ tokens = _CODE_TOKEN_RE.findall(s)
235
+ if tokens:
236
+ items = [t.strip() for t in tokens if t.strip()]
237
+ else:
238
+ items = [tok.strip().strip("`") for tok in re.split(r",\s*", s) if tok.strip()]
239
+ if has_pre_existing:
240
+ items = [f"{t} (pre-existing)" for t in items]
241
+ return items
242
+
243
+
244
+ def _parse_task_table(body: str) -> List[dict]:
245
+ """Parse the markdown task table out of a lane body.
246
+
247
+ Expected header (case-insensitive):
248
+ | Task ID | Type | Depends on | Files in scope | Tests owned | Test command |
249
+ """
250
+ rows: List[dict] = []
251
+ lines = body.splitlines()
252
+ header_idx: Optional[int] = None
253
+ for idx, line in enumerate(lines):
254
+ if (
255
+ line.startswith("|")
256
+ and "Task ID" in line
257
+ and "Type" in line
258
+ and "Depends" in line
259
+ ):
260
+ header_idx = idx
261
+ break
262
+ if header_idx is None:
263
+ return rows
264
+ # Skip the separator row (---|---|).
265
+ for line in lines[header_idx + 2:]:
266
+ if not line.startswith("|"):
267
+ break
268
+ cells = [c.strip() for c in line.split("|")[1:-1]]
269
+ if len(cells) < 2:
270
+ continue
271
+ task_id = cells[0].strip("`")
272
+ task_type = cells[1].strip("`").lower()
273
+ rows.append({"task_id": task_id, "type": task_type})
274
+ return rows
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # Checks
279
+
280
+ Findings = List[str] # human-readable error strings
281
+
282
+
283
+ def _check_a_required_headings(src: str) -> Findings:
284
+ out: Findings = []
285
+ if not _has_h1(src):
286
+ out.append("(A) missing top-level `# <PHASE_ID>:` heading")
287
+ required = [
288
+ "Context",
289
+ "Interface Freeze Gates",
290
+ "Lane Index & Dependencies",
291
+ "Lanes",
292
+ "Execution Notes",
293
+ "Acceptance Criteria",
294
+ "Verification",
295
+ ]
296
+ found = _top_headings(src)
297
+ for heading in required:
298
+ if heading not in found:
299
+ out.append(f"(A) missing required heading: `## {heading}`")
300
+ return out
301
+
302
+
303
+ def _check_b_lane_index_parses(lanes: List[Lane]) -> Findings:
304
+ out: Findings = []
305
+ if not lanes:
306
+ out.append("(B) `## Lane Index & Dependencies` has no parseable `SL-N — name` stanzas")
307
+ return out
308
+ for lane in lanes:
309
+ if not lane.parallel_safe:
310
+ out.append(f"(B) {lane.sl_id}: missing `Parallel-safe:` line")
311
+ elif lane.parallel_safe not in {"yes", "no", "mixed"}:
312
+ out.append(
313
+ f"(B) {lane.sl_id}: `Parallel-safe: {lane.parallel_safe}` not in {{yes, no, mixed}}"
314
+ )
315
+ return out
316
+
317
+
318
+ def _check_c_dag_acyclic(lanes: List[Lane]) -> Findings:
319
+ """Kahn's algorithm. Nodes with no incoming edges go first; remove them
320
+ and their outgoing edges; repeat. If we can't drain the graph, there's
321
+ a cycle."""
322
+ out: Findings = []
323
+ if not lanes:
324
+ return out
325
+ # Validate referenced IDs exist.
326
+ known_ids = {lane.sl_id for lane in lanes}
327
+ for lane in lanes:
328
+ for dep in lane.depends_on:
329
+ if dep not in known_ids:
330
+ out.append(f"(C) {lane.sl_id}: depends on unknown lane `{dep}`")
331
+ if out:
332
+ return out
333
+ in_deg: Dict[str, int] = {lane.sl_id: len(lane.depends_on) for lane in lanes}
334
+ reverse_edges: Dict[str, List[str]] = {lane.sl_id: [] for lane in lanes}
335
+ for lane in lanes:
336
+ for dep in lane.depends_on:
337
+ reverse_edges[dep].append(lane.sl_id)
338
+ queue = [sl for sl, d in in_deg.items() if d == 0]
339
+ visited = 0
340
+ while queue:
341
+ node = queue.pop(0)
342
+ visited += 1
343
+ for dependent in reverse_edges[node]:
344
+ in_deg[dependent] -= 1
345
+ if in_deg[dependent] == 0:
346
+ queue.append(dependent)
347
+ if visited != len(lanes):
348
+ unresolved = [sl for sl, d in in_deg.items() if d > 0]
349
+ out.append(f"(C) lane DAG has a cycle; unresolved after topological sort: {unresolved}")
350
+ return out
351
+
352
+
353
+ def _git_ls_files(repo_root: Path) -> Optional[List[str]]:
354
+ try:
355
+ r = subprocess.run(
356
+ ["git", "ls-files"],
357
+ cwd=repo_root,
358
+ capture_output=True,
359
+ text=True,
360
+ check=True,
361
+ )
362
+ except Exception:
363
+ return None
364
+ return [line for line in r.stdout.splitlines() if line]
365
+
366
+
367
+ def _glob_to_regex(pattern: str) -> re.Pattern:
368
+ """Minimal glob to regex: ** → any depth; * → any chars not /; everything else literal."""
369
+ s = pattern.strip("`").strip()
370
+ # Escape regex specials, then un-escape our own glob metacharacters.
371
+ esc = re.escape(s)
372
+ esc = esc.replace(r"\*\*", ".*").replace(r"\*", "[^/]*").replace(r"\?", ".")
373
+ return re.compile("^" + esc + "$")
374
+
375
+
376
+ def _check_d_owned_files_disjoint(
377
+ lane_sections: Dict[str, dict],
378
+ repo_root: Optional[Path],
379
+ ) -> Findings:
380
+ out: Findings = []
381
+ tracked: Optional[List[str]] = None
382
+ if repo_root is not None:
383
+ tracked = _git_ls_files(repo_root)
384
+
385
+ # First: exact glob duplication check.
386
+ glob_origin: Dict[str, str] = {}
387
+ for sl_id, parsed in lane_sections.items():
388
+ for glob in parsed["owned_globs"]:
389
+ norm = glob.strip().strip("`")
390
+ if not norm:
391
+ continue
392
+ if norm in glob_origin and glob_origin[norm] != sl_id:
393
+ out.append(
394
+ f"(D) duplicate owned glob `{norm}` in {glob_origin[norm]} and {sl_id}"
395
+ )
396
+ else:
397
+ glob_origin[norm] = sl_id
398
+
399
+ if tracked is None:
400
+ # No repo context — skip expansion, only the exact-duplicate check ran.
401
+ out.append(
402
+ "(D) WARN: not running inside a git repo; owned-file disjointness only checked for exact duplicates"
403
+ )
404
+ return out
405
+
406
+ # Expand each lane's globs to a concrete file set, then intersect.
407
+ lane_file_sets: Dict[str, Set[str]] = {}
408
+ for sl_id, parsed in lane_sections.items():
409
+ matched: Set[str] = set()
410
+ for glob in parsed["owned_globs"]:
411
+ norm = glob.strip().strip("`")
412
+ if not norm:
413
+ continue
414
+ rex = _glob_to_regex(norm)
415
+ matched |= {p for p in tracked if rex.match(p)}
416
+ lane_file_sets[sl_id] = matched
417
+
418
+ sl_ids = sorted(lane_file_sets.keys())
419
+ for i in range(len(sl_ids)):
420
+ for j in range(i + 1, len(sl_ids)):
421
+ a, b = sl_ids[i], sl_ids[j]
422
+ inter = lane_file_sets[a] & lane_file_sets[b]
423
+ if inter:
424
+ sample = sorted(inter)[:5]
425
+ out.append(
426
+ f"(D) file-ownership overlap between {a} and {b}: {len(inter)} files "
427
+ f"(first few: {sample})"
428
+ )
429
+ return out
430
+
431
+
432
+ def _check_e_test_before_impl(lane_sections: Dict[str, dict]) -> Findings:
433
+ out: Findings = []
434
+ for sl_id, parsed in lane_sections.items():
435
+ seen_test = False
436
+ for row in parsed["task_rows"]:
437
+ tt = row["type"]
438
+ if tt == "test":
439
+ seen_test = True
440
+ elif tt == "impl" and not seen_test:
441
+ out.append(
442
+ f"(E) {sl_id} task {row['task_id']}: `impl` appears before any `test` in the same lane"
443
+ )
444
+ return out
445
+
446
+
447
+ def _check_f_interfaces_trace(
448
+ lane_sections: Dict[str, dict],
449
+ lane_sections_raw: Optional[Dict[str, str]] = None,
450
+ ) -> Findings:
451
+ out: Findings = []
452
+ provided: Set[str] = set()
453
+ for parsed in lane_sections.values():
454
+ for sym in parsed["interfaces_provided"]:
455
+ provided.add(_normalize_interface(sym))
456
+ for sl_id, parsed in lane_sections.items():
457
+ # Fall back to raw body scan when the token's annotation was
458
+ # separated from it during parsing (e.g. ``backtick`` (pre-existing)).
459
+ raw_body = (lane_sections_raw or {}).get(sl_id, "")
460
+ for sym in parsed["interfaces_consumed"]:
461
+ norm = _normalize_interface(sym)
462
+ if not norm:
463
+ continue
464
+ if "pre-existing" in sym.lower():
465
+ continue
466
+ # Raw-body fallback: the token itself appears with a (pre-existing)
467
+ # annotation somewhere in the lane body (maybe on a different line).
468
+ if raw_body:
469
+ pattern = rf"`?{re.escape(norm)}`?\s*\(pre-existing\)"
470
+ if re.search(pattern, raw_body, re.IGNORECASE):
471
+ continue
472
+ if norm not in provided:
473
+ out.append(
474
+ f"(F) WARN: {sl_id} consumes `{sym}` but no upstream lane provides it "
475
+ f"(mark '(pre-existing)' if it exists outside P-this-phase)"
476
+ )
477
+ return out
478
+
479
+
480
+ def _check_g_grep_paired_with_tests(src: str) -> Findings:
481
+ """Every acceptance criterion that uses `rg`/`grep` as its sole assertion
482
+ must cite a test file in the same bullet. A bare grep is defeatable by
483
+ renaming a symbol to pass the regex — pair it with a behavioral test."""
484
+ out: Findings = []
485
+ body = _extract_section(src, "Acceptance Criteria")
486
+ if not body:
487
+ return out
488
+ # Iterate top-level bullets. A bullet is a line starting with "- [ ]" or "- [x]".
489
+ # Multi-line bullets continue until the next top-level bullet or blank line.
490
+ lines = body.splitlines()
491
+ current: List[str] = []
492
+ bullets: List[str] = []
493
+ for line in lines:
494
+ if line.lstrip().startswith("- ["):
495
+ if current:
496
+ bullets.append("\n".join(current))
497
+ current = [line]
498
+ elif current:
499
+ if not line.strip():
500
+ bullets.append("\n".join(current))
501
+ current = []
502
+ else:
503
+ current.append(line)
504
+ if current:
505
+ bullets.append("\n".join(current))
506
+ for bullet in bullets:
507
+ low = bullet.lower()
508
+ has_grep = (
509
+ re.search(r"\brg\s+-?\w*\b", bullet) is not None
510
+ or re.search(r"\bgrep\s+-?\w*\b", bullet) is not None
511
+ )
512
+ if not has_grep:
513
+ continue
514
+ # Cite a test file if anything in the bullet looks like a test path:
515
+ # `tests/` or `test_` or a `.py` with a test-ish stem.
516
+ cites_test = (
517
+ "tests/" in low
518
+ or re.search(r"\btest_\w+\.py\b", low) is not None
519
+ or re.search(r"\.py\b", low) is not None
520
+ and ("test" in low or "pytest" in low)
521
+ )
522
+ if not cites_test:
523
+ first_line = bullet.strip().splitlines()[0][:90]
524
+ out.append(
525
+ f"(G) WARN: acceptance criterion uses grep/rg without citing a paired test "
526
+ f"file — rename-defeat risk: {first_line!r}"
527
+ )
528
+ return out
529
+
530
+
531
+ def _check_h_eager_reexport(src: str) -> Findings:
532
+ """If Execution Notes says SL-0 adds re-exports to a __init__.py, require
533
+ the doc to also specify the __getattr__ lazy pattern. Eager top-level
534
+ re-exports break package load when a later lane drops or renames the
535
+ exported symbol."""
536
+ out: Findings = []
537
+ body = _extract_section(src, "Execution Notes")
538
+ if not body:
539
+ return out
540
+ low = body.lower()
541
+ mentions_reexport = ("re-export" in low) or ("reexport" in low)
542
+ mentions_init_py = "__init__.py" in low
543
+ if mentions_reexport and mentions_init_py and "__getattr__" not in low:
544
+ out.append(
545
+ "(H) WARN: Execution Notes mentions re-exports in __init__.py but does not "
546
+ "specify the `__getattr__` lazy pattern — eager top-level imports break "
547
+ "package load when a later lane drops or renames the exported symbol"
548
+ )
549
+ return out
550
+
551
+
552
+ def _normalize_interface(sym: str) -> str:
553
+ """Strip backticks, parens, and leading/trailing whitespace. Drop annotations like '(pre-existing)'."""
554
+ s = sym.strip().strip("`").strip()
555
+ # Drop any trailing parenthesized annotation.
556
+ s = re.sub(r"\s*\([^)]*\)\s*$", "", s)
557
+ return s.strip()
558
+
559
+
560
+ # ---------------------------------------------------------------------------
561
+ # Main
562
+
563
+ def _fail(msg: str) -> None:
564
+ print(f"validate_plan_doc: {msg}", file=sys.stderr)
565
+
566
+
567
+ def main(argv: List[str]) -> int:
568
+ if len(argv) != 2:
569
+ _fail("usage: validate_plan_doc.py <plan-path>")
570
+ return 2
571
+ path = Path(argv[1])
572
+ if not path.exists():
573
+ _fail(f"plan doc not found: {path}")
574
+ return 2
575
+
576
+ src = _read(path)
577
+
578
+ # Work out if we're inside a git repo for the disjointness expansion.
579
+ repo_root: Optional[Path] = None
580
+ try:
581
+ r = subprocess.run(
582
+ ["git", "rev-parse", "--show-toplevel"],
583
+ cwd=path.parent,
584
+ capture_output=True,
585
+ text=True,
586
+ check=True,
587
+ )
588
+ repo_root = Path(r.stdout.strip())
589
+ except Exception:
590
+ repo_root = None
591
+
592
+ findings: Findings = []
593
+ findings.extend(_check_a_required_headings(src))
594
+
595
+ lane_index_body = _extract_section(src, "Lane Index & Dependencies")
596
+ lanes = _parse_lane_index(lane_index_body)
597
+ findings.extend(_check_b_lane_index_parses(lanes))
598
+ findings.extend(_check_c_dag_acyclic(lanes))
599
+
600
+ lanes_body = _extract_section(src, "Lanes")
601
+ lane_sections_raw = _split_lane_sections(lanes_body)
602
+ lane_sections_parsed = {
603
+ sl_id: _parse_lane_section(body) for sl_id, body in lane_sections_raw.items()
604
+ }
605
+
606
+ # Every lane listed in the Lane Index should also have a ### section.
607
+ missing_sections = [
608
+ lane.sl_id for lane in lanes if lane.sl_id not in lane_sections_parsed
609
+ ]
610
+ for sl_id in missing_sections:
611
+ findings.append(f"(B) {sl_id}: no matching `### {sl_id}` section under `## Lanes`")
612
+
613
+ findings.extend(_check_d_owned_files_disjoint(lane_sections_parsed, repo_root))
614
+ findings.extend(_check_e_test_before_impl(lane_sections_parsed))
615
+ findings.extend(_check_f_interfaces_trace(lane_sections_parsed, lane_sections_raw))
616
+ findings.extend(_check_g_grep_paired_with_tests(src))
617
+ findings.extend(_check_h_eager_reexport(src))
618
+
619
+ # Partition findings into errors vs warnings.
620
+ errors = [f for f in findings if "WARN" not in f]
621
+ warnings = [f for f in findings if "WARN" in f]
622
+
623
+ if warnings:
624
+ for w in warnings:
625
+ print(w, file=sys.stderr)
626
+ if errors:
627
+ for e in errors:
628
+ print(e, file=sys.stderr)
629
+ print(
630
+ f"validate_plan_doc: {len(errors)} error(s), {len(warnings)} warning(s)",
631
+ file=sys.stderr,
632
+ )
633
+ return 1
634
+
635
+ print(
636
+ f"validate_plan_doc: OK — {len(lanes)} lanes, {len(warnings)} warning(s)"
637
+ )
638
+ return 0
639
+
640
+
641
+ if __name__ == "__main__":
642
+ sys.exit(main(sys.argv))