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,215 @@
1
+ """Durable append-only train ledger.
2
+
3
+ Records per-node status transitions for a cross-repo release train. State
4
+ lives **entirely outside any repo's ``.phase-loop/``** — it is coordinator-
5
+ owned.
6
+
7
+ Durability model (self-consistent):
8
+ - Each append is a single ``os.write(fd, encoded)`` on a file opened with
9
+ ``O_APPEND``. The OS guarantees atomicity for writes ≤ PIPE_BUF on
10
+ local filesystems (typically 4 KiB); our records are far smaller.
11
+ - The resume reader is **tolerant**: if the final line of the file is
12
+ malformed (e.g. the coordinator crashed mid-write), that trailing line is
13
+ **silently dropped** — it is not yet committed state. A malformed line
14
+ at any position OTHER than the last fails loud (unexpected corruption).
15
+ - No temp-rename. ``events.py:read_events`` does a bare ``json.loads`` and
16
+ crashes on a truncated final line — this reader is **net-new**, not a
17
+ mirror of that function.
18
+
19
+ Record shape (one JSON object per line)::
20
+
21
+ {
22
+ "node_id": "<repo>/<roadmap>",
23
+ "status": "pending|running|pr_open|approved|merged|blocked",
24
+ "branch": "<branch-name or null>",
25
+ "pr_url": "<PR URL or null>",
26
+ "head_sha": "<draft branch HEAD SHA or null>",
27
+ "upstream_merge_sha": "<merged SHA or null — P4 only>",
28
+ "merge_order": <int or null>,
29
+ "ts": "<ISO-8601 UTC timestamp>"
30
+ }
31
+
32
+ Current state = last-record-wins per ``node_id`` (fold over append log).
33
+ ``pr_open`` records store the draft branch HEAD SHA in ``head_sha`` (set by P3).
34
+ ``merged`` records carry the actual merge-commit SHA in ``upstream_merge_sha``
35
+ (set by P4 after the merge lands). Do NOT put a draft head in
36
+ ``upstream_merge_sha`` — that field is reserved for the real merged SHA so P4
37
+ can gate-on-merge without ambiguity.
38
+
39
+ Zero external deps (stdlib only).
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import datetime
45
+ import json
46
+ import os
47
+ from dataclasses import asdict, dataclass
48
+ from pathlib import Path
49
+ from typing import Dict, List, Optional
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Status vocabulary (IF-0-P2-1)
54
+
55
+ VALID_STATUSES = frozenset(
56
+ {"pending", "running", "pr_open", "approved", "merged", "blocked"}
57
+ )
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Record shape
62
+
63
+ @dataclass
64
+ class LedgerRecord:
65
+ """One append to the train ledger."""
66
+
67
+ node_id: str
68
+ status: str
69
+ branch: Optional[str] = None
70
+ pr_url: Optional[str] = None
71
+ head_sha: Optional[str] = None # draft branch HEAD SHA (P3, pr_open records)
72
+ upstream_merge_sha: Optional[str] = None # merged-commit SHA (P4 only)
73
+ merge_order: Optional[int] = None
74
+ ts: str = "" # ISO-8601 UTC; auto-set on append if blank
75
+
76
+ def __post_init__(self) -> None:
77
+ if self.status not in VALID_STATUSES:
78
+ raise ValueError(
79
+ f"invalid ledger status {self.status!r}; "
80
+ f"expected one of: {', '.join(sorted(VALID_STATUSES))}"
81
+ )
82
+
83
+ def to_dict(self) -> dict:
84
+ return {k: v for k, v in asdict(self).items()}
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Ledger path convention
89
+
90
+ def default_ledger_path(coordinator_dir: Path, train_name: str) -> Path:
91
+ """Return the default ledger file path for a named train.
92
+
93
+ The path is inside ``coordinator_dir`` which must **not** be under any
94
+ repo's ``.phase-loop/``.
95
+ """
96
+ return coordinator_dir / f"train-{train_name}.ledger.jsonl"
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Append
101
+
102
+ def append_record(path: Path, record: LedgerRecord) -> None:
103
+ """Atomically append ``record`` to the ledger at ``path``.
104
+
105
+ Uses ``O_APPEND`` with a single ``os.write`` call for durability. The
106
+ parent directory is created if absent.
107
+
108
+ The caller is responsible for ensuring ``path`` is outside any repo's
109
+ ``.phase-loop/`` directory.
110
+ """
111
+ if not record.ts:
112
+ record = LedgerRecord(
113
+ node_id=record.node_id,
114
+ status=record.status,
115
+ branch=record.branch,
116
+ pr_url=record.pr_url,
117
+ head_sha=record.head_sha,
118
+ upstream_merge_sha=record.upstream_merge_sha,
119
+ merge_order=record.merge_order,
120
+ ts=_utc_now(),
121
+ )
122
+ _assert_not_phase_loop(path)
123
+ path.parent.mkdir(parents=True, exist_ok=True)
124
+ encoded = (json.dumps(record.to_dict(), sort_keys=True) + "\n").encode("utf-8")
125
+ fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o666)
126
+ try:
127
+ os.write(fd, encoded)
128
+ finally:
129
+ os.close(fd)
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Resume reader (tolerant — drops malformed trailing line)
134
+
135
+ def read_ledger(path: Path) -> Dict[str, LedgerRecord]:
136
+ """Read the ledger, returning the **current state** per node (last-wins fold).
137
+
138
+ Tolerant resume reader:
139
+ - An empty file returns ``{}``.
140
+ - A malformed **final** line (crashed mid-write) is silently dropped.
141
+ - A malformed line at any other position raises :exc:`ValueError` (unexpected
142
+ corruption; don't silently paper over mid-file damage).
143
+ """
144
+ if not path.exists():
145
+ return {}
146
+
147
+ raw_lines = path.read_bytes().decode("utf-8").splitlines(keepends=True)
148
+ non_empty = [ln for ln in raw_lines if ln.strip()]
149
+ if not non_empty:
150
+ return {}
151
+
152
+ state: Dict[str, LedgerRecord] = {}
153
+ for i, line in enumerate(non_empty):
154
+ stripped = line.strip()
155
+ is_last = i == len(non_empty) - 1
156
+ try:
157
+ obj = json.loads(stripped)
158
+ except json.JSONDecodeError:
159
+ if is_last:
160
+ # Tolerant: crashed mid-write — drop the trailing line
161
+ break
162
+ raise ValueError(
163
+ f"malformed ledger line {i + 1} (not the final line — unexpected "
164
+ f"corruption): {stripped!r}"
165
+ )
166
+
167
+ try:
168
+ record = _dict_to_record(obj)
169
+ except (KeyError, ValueError) as exc:
170
+ if is_last:
171
+ break
172
+ raise ValueError(
173
+ f"invalid ledger record at line {i + 1}: {exc}"
174
+ ) from exc
175
+
176
+ state[record.node_id] = record
177
+
178
+ return state
179
+
180
+
181
+ def resume_state(path: Path) -> Dict[str, LedgerRecord]:
182
+ """Alias for :func:`read_ledger` — returns the current per-node state."""
183
+ return read_ledger(path)
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Helpers
188
+
189
+ def _dict_to_record(obj: dict) -> LedgerRecord:
190
+ return LedgerRecord(
191
+ node_id=obj["node_id"],
192
+ status=obj["status"],
193
+ branch=obj.get("branch"),
194
+ pr_url=obj.get("pr_url"),
195
+ head_sha=obj.get("head_sha"),
196
+ upstream_merge_sha=obj.get("upstream_merge_sha"),
197
+ merge_order=obj.get("merge_order"),
198
+ ts=obj.get("ts", ""),
199
+ )
200
+
201
+
202
+ def _utc_now() -> str:
203
+ return datetime.datetime.now(datetime.timezone.utc).isoformat()
204
+
205
+
206
+ def _assert_not_phase_loop(path: Path) -> None:
207
+ """Fail loud if the ledger path is inside any repo's ``.phase-loop/``."""
208
+ parts = path.parts
209
+ for part in parts:
210
+ if part in {".phase-loop", "phase-loop"}:
211
+ raise ValueError(
212
+ f"train ledger path '{path}' is inside a '.phase-loop/' directory — "
213
+ f"train state must never touch any repo's .phase-loop/; "
214
+ f"use a coordinator-owned directory instead"
215
+ )
@@ -0,0 +1,339 @@
1
+ """Cross-repo release-train roadmap schema, parser, and gate identity.
2
+
3
+ IF-0-P2-1 contract: train roadmap nodes = ``(repo, roadmap)``, edges =
4
+ ``Depends on: <upstream node>`` plus a per-edge consumption-channel
5
+ descriptor, and a cross-repo gate identity pinned to an upstream merge SHA.
6
+
7
+ The gate identity uses the ``XGATE:`` namespace — **not** the in-repo
8
+ ``IF-0-<alias>-<n>`` token (which is hardwired to a phase alias and has no
9
+ cross-repo consume side). Any attempt to reuse an ``IF-0-...`` token as a
10
+ node identifier or dependency reference is rejected by ``validate_train``.
11
+
12
+ This is a **new parser**, separate from the ``### Phase N (ALIAS)`` regex in
13
+ ``roadmap_lint``. The topo-sort / cycle-check algorithm is extracted from
14
+ ``roadmap_lint.check_dag_acyclic`` and reused unchanged.
15
+
16
+ Train roadmap Markdown format (minimal)::
17
+
18
+ # Release Train: <title>
19
+
20
+ ## Nodes
21
+
22
+ ### Node: <repo> / <roadmap-path>
23
+
24
+ **Depends on:** (none)
25
+ **Channel:** (none)
26
+
27
+ ### Node: <repo2> / <roadmap2-path>
28
+
29
+ **Depends on:** <repo> / <roadmap-path>
30
+ **Channel:** submodule path=vendor/repo
31
+
32
+ Zero external deps (stdlib only).
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import re
38
+ from dataclasses import dataclass, field
39
+ from pathlib import Path
40
+ from typing import Dict, List, Optional, Tuple
41
+
42
+ from .cross_repo_channel import ChannelDescriptor, parse_channel_line
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Data model — IF-0-P2-1
47
+
48
+ @dataclass(frozen=True)
49
+ class TrainNode:
50
+ """Identity of one node in the release train: a ``(repo, roadmap)`` pair."""
51
+
52
+ repo: str
53
+ roadmap: str
54
+
55
+ @property
56
+ def node_id(self) -> str:
57
+ """Canonical string identifier: ``<repo>/<roadmap>``."""
58
+ return f"{self.repo}/{self.roadmap}"
59
+
60
+ def __str__(self) -> str:
61
+ return f"{self.repo} / {self.roadmap}"
62
+
63
+
64
+ @dataclass
65
+ class TrainEdge:
66
+ """A directed dependency edge (upstream → downstream) with a channel.
67
+
68
+ This is the static declaration. ``gate_id(sha)`` produces the runtime
69
+ cross-repo gate identity once the upstream merges.
70
+ """
71
+
72
+ upstream: TrainNode
73
+ downstream: TrainNode
74
+ channel: ChannelDescriptor
75
+
76
+ def gate_id(self, merge_sha: str) -> str:
77
+ """Return the cross-repo gate identity token for a given upstream merge SHA.
78
+
79
+ The ``XGATE:`` namespace is distinct from ``IF-0-<alias>-<n>`` — it
80
+ carries a full node reference and is pinned to a content-addressed
81
+ merge commit rather than a phase alias.
82
+ """
83
+ return f"XGATE:{self.upstream.node_id}@{merge_sha}"
84
+
85
+
86
+ @dataclass
87
+ class TrainRoadmap:
88
+ """Parsed cross-repo release-train roadmap (IF-0-P2-1)."""
89
+
90
+ title: str
91
+ nodes: List[TrainNode] = field(default_factory=list)
92
+ edges: List[TrainEdge] = field(default_factory=list)
93
+
94
+ def topo_order(self) -> List[TrainNode]:
95
+ """Return nodes in a valid serial execution order (Kahn's algorithm).
96
+
97
+ Raises ``ValueError`` if the train is not serially orderable (cycle).
98
+ """
99
+ order = _topo_sort(self.nodes, self.edges)
100
+ return order
101
+
102
+ def node_by_id(self, node_id: str) -> Optional[TrainNode]:
103
+ for n in self.nodes:
104
+ if n.node_id == node_id:
105
+ return n
106
+ return None
107
+
108
+ def edges_for_downstream(self, node: TrainNode) -> List[TrainEdge]:
109
+ return [e for e in self.edges if e.downstream == node]
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Parser
114
+
115
+ # Regex for the train-roadmap headings (new format — NOT the phase-plan regex)
116
+ _TITLE_RE = re.compile(r"^#\s+Release Train:\s*(.+?)\s*$", re.MULTILINE)
117
+ _NODE_HEADING_RE = re.compile(r"^###\s+Node:\s*(.+?)\s*$", re.MULTILINE)
118
+ _FIELD_RE = re.compile(
119
+ r"^\*\*(?P<label>Depends on|Channel):\*\*\s*(?P<value>.+)$",
120
+ re.MULTILINE | re.IGNORECASE,
121
+ )
122
+
123
+ # Reject in-repo IF-gate tokens used as node / dep references
124
+ _IF_GATE_TOKEN_RE = re.compile(r"\bIF-0-[A-Za-z0-9]+-\d+\b")
125
+
126
+ _NONE_VALUES = frozenset({"(none)", "none", ""})
127
+
128
+
129
+ def _parse_node_id(raw: str) -> TrainNode:
130
+ """Parse ``<repo> / <roadmap>`` or ``<repo>/<roadmap>`` into a TrainNode."""
131
+ raw = raw.strip()
132
+ if " / " in raw:
133
+ repo, roadmap = raw.split(" / ", 1)
134
+ elif "/" in raw:
135
+ repo, roadmap = raw.split("/", 1)
136
+ else:
137
+ raise ValueError(f"invalid node identifier (expected '<repo> / <roadmap>'): {raw!r}")
138
+ return TrainNode(repo=repo.strip(), roadmap=roadmap.strip())
139
+
140
+
141
+ def _extract_node_bodies(text: str) -> List[Tuple[str, str]]:
142
+ """Return list of (raw_node_id, body_text) for each ``### Node:`` block."""
143
+ matches = list(_NODE_HEADING_RE.finditer(text))
144
+ result = []
145
+ for i, m in enumerate(matches):
146
+ raw_id = m.group(1)
147
+ body_start = m.end()
148
+ body_end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
149
+ result.append((raw_id, text[body_start:body_end]))
150
+ return result
151
+
152
+
153
+ def parse_train_roadmap(text: str) -> TrainRoadmap:
154
+ """Parse train-roadmap markdown text into a :class:`TrainRoadmap`.
155
+
156
+ Raises :exc:`ValueError` on malformed input (not a validation function —
157
+ use :func:`validate_train` for full semantic checks).
158
+ """
159
+ title_m = _TITLE_RE.search(text)
160
+ if not title_m:
161
+ raise ValueError("missing '# Release Train: <title>' heading")
162
+ title = title_m.group(1)
163
+
164
+ node_bodies = _extract_node_bodies(text)
165
+ if not node_bodies:
166
+ raise ValueError("no '### Node: <repo> / <roadmap>' blocks found")
167
+
168
+ nodes: List[TrainNode] = []
169
+ raw_edges: List[Tuple[TrainNode, str, str]] = [] # (downstream, dep_id, channel_raw)
170
+
171
+ for raw_id, body in node_bodies:
172
+ node = _parse_node_id(raw_id)
173
+ nodes.append(node)
174
+
175
+ fields: Dict[str, str] = {}
176
+ for m in _FIELD_RE.finditer(body):
177
+ fields[m.group("label").lower()] = m.group("value").strip()
178
+
179
+ dep_raw = fields.get("depends on", "(none)")
180
+ channel_raw = fields.get("channel", "(none)")
181
+
182
+ if dep_raw.strip().lower() not in _NONE_VALUES:
183
+ raw_edges.append((node, dep_raw.strip(), channel_raw.strip()))
184
+
185
+ # Resolve edges
186
+ node_by_id: Dict[str, TrainNode] = {n.node_id: n for n in nodes}
187
+ # Also index by "repo / roadmap" string representation for lookup
188
+ node_by_str: Dict[str, TrainNode] = {str(n): n for n in nodes}
189
+
190
+ edges: List[TrainEdge] = []
191
+ for downstream, dep_id, channel_raw in raw_edges:
192
+ # Distinct check: reject in-repo IF-gate tokens before the node lookup so
193
+ # "invalid in-repo token" and "missing node" produce distinguishable errors.
194
+ if _IF_GATE_TOKEN_RE.search(dep_id):
195
+ raise ValueError(
196
+ f"node '{downstream.node_id}' references in-repo IF-gate token "
197
+ f"'{dep_id}' as a dependency; cross-repo edges use '<repo>/<roadmap>' "
198
+ f"node identifiers (XGATE: namespace), not IF-0-... tokens"
199
+ )
200
+ upstream = node_by_id.get(dep_id) or node_by_str.get(dep_id)
201
+ if upstream is None:
202
+ # Try normalising " / " vs "/"
203
+ alt = dep_id.replace(" / ", "/")
204
+ upstream = node_by_id.get(alt)
205
+ if upstream is None:
206
+ raise ValueError(
207
+ f"node '{downstream.node_id}' depends on unknown node '{dep_id}'"
208
+ )
209
+ channel = parse_channel_line(channel_raw)
210
+ edges.append(TrainEdge(upstream=upstream, downstream=downstream, channel=channel))
211
+
212
+ return TrainRoadmap(title=title, nodes=nodes, edges=edges)
213
+
214
+
215
+ def load_train_roadmap(path: Path | str) -> TrainRoadmap:
216
+ """Load and parse a train roadmap from disk."""
217
+ return parse_train_roadmap(Path(path).read_text(encoding="utf-8"))
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Validation — extends roadmap_lint's topo algorithm with train-specific rules
222
+
223
+ def validate_train(roadmap: TrainRoadmap) -> List[str]:
224
+ """Return human-readable validation issues for a parsed :class:`TrainRoadmap`.
225
+
226
+ Checks:
227
+ (T-A) No in-repo ``IF-0-<alias>-<n>`` tokens used as node IDs or deps.
228
+ (T-B) Every edge's upstream node exists in the node list.
229
+ (T-C) Every edge with a dependency carries a non-none channel descriptor.
230
+ (T-D) The cross-repo DAG is acyclic (topo-sort succeeds).
231
+ (T-E) Every dependency edge uses a supported channel kind: ``pin`` (with
232
+ ``file=``) or ``submodule``. ``workspace`` channels are not
233
+ implemented for real consumption and are rejected here so that a
234
+ train with a workspace edge fails the preflight gate — opening zero
235
+ PRs — rather than running until injection fails mid-train.
236
+ """
237
+ errors: List[str] = []
238
+
239
+ # (T-A) Reject in-repo IF-gate tokens
240
+ for node in roadmap.nodes:
241
+ if _IF_GATE_TOKEN_RE.search(node.node_id):
242
+ errors.append(
243
+ f"(T-A) node id '{node.node_id}' contains an in-repo IF-gate token "
244
+ f"(IF-0-...) — use '<repo>/<roadmap>' node identifiers instead"
245
+ )
246
+ for edge in roadmap.edges:
247
+ if _IF_GATE_TOKEN_RE.search(edge.upstream.node_id):
248
+ errors.append(
249
+ f"(T-A) edge depends on '{edge.upstream.node_id}' which contains an "
250
+ f"in-repo IF-gate token — use node identifiers instead"
251
+ )
252
+
253
+ # (T-B) Every upstream in an edge is a declared node
254
+ declared = {n.node_id for n in roadmap.nodes}
255
+ for edge in roadmap.edges:
256
+ if edge.upstream.node_id not in declared:
257
+ errors.append(
258
+ f"(T-B) edge '{edge.downstream.node_id}' depends on "
259
+ f"'{edge.upstream.node_id}' which is not a declared node"
260
+ )
261
+
262
+ # (T-C) Every dependency edge must declare a channel — a bare `(none)` is
263
+ # ambiguous (forgotten channel?). #47: an intentional ORDER-ONLY dependency
264
+ # (B must merge after A but does not consume A's artifact) is declared
265
+ # explicitly with `Channel: order-only`, which is NOT `none` and passes here.
266
+ for edge in roadmap.edges:
267
+ if edge.channel.kind == "none":
268
+ errors.append(
269
+ f"(T-C) edge '{edge.downstream.node_id}' → '{edge.upstream.node_id}' "
270
+ f"has no consumption-channel descriptor; declare a channel "
271
+ f"(pin/submodule) if the downstream consumes the upstream, or "
272
+ f"'order-only' for a merge-order-only (freeze) dependency"
273
+ )
274
+
275
+ # (T-E) Supported channel kinds: pin (with file=), submodule, or order-only
276
+ # (order-only carries no injection — it enforces merge order only, #47).
277
+ _SUPPORTED_KINDS = frozenset({"pin", "submodule", "order-only"})
278
+ for edge in roadmap.edges:
279
+ kind = edge.channel.kind
280
+ if kind == "none":
281
+ continue # T-C already reported this
282
+ if kind not in _SUPPORTED_KINDS:
283
+ errors.append(
284
+ f"(T-E) edge '{edge.downstream.node_id}' → '{edge.upstream.node_id}' "
285
+ f"uses unsupported channel kind '{kind}'; only 'pin' (with file=), "
286
+ f"'submodule', and 'order-only' are supported. "
287
+ f"'workspace' channels are rejected at preflight to prevent hollow injection."
288
+ )
289
+
290
+ # (T-D) Acyclic + serially orderable
291
+ if not errors:
292
+ try:
293
+ _topo_sort(roadmap.nodes, roadmap.edges)
294
+ except ValueError as exc:
295
+ errors.append(f"(T-D) {exc}")
296
+
297
+ return errors
298
+
299
+
300
+ def validate_train_loud(roadmap: TrainRoadmap) -> None:
301
+ """Raise :exc:`ValueError` if the train roadmap has any validation issues."""
302
+ issues = validate_train(roadmap)
303
+ if issues:
304
+ msg = "\n".join(f" • {e}" for e in issues)
305
+ raise ValueError(f"train roadmap has {len(issues)} issue(s):\n{msg}")
306
+
307
+
308
+ # ---------------------------------------------------------------------------
309
+ # Topo-sort (reused from roadmap_lint.check_dag_acyclic — Kahn's algorithm)
310
+
311
+ def _topo_sort(nodes: List[TrainNode], edges: List[TrainEdge]) -> List[TrainNode]:
312
+ """Return nodes in topological order. Raises :exc:`ValueError` on a cycle."""
313
+ adj: Dict[str, List[str]] = {n.node_id: [] for n in nodes}
314
+ indeg: Dict[str, int] = {n.node_id: 0 for n in nodes}
315
+ for edge in edges:
316
+ up = edge.upstream.node_id
317
+ dn = edge.downstream.node_id
318
+ if up in adj and dn in indeg:
319
+ adj[up].append(dn)
320
+ indeg[dn] += 1
321
+
322
+ queue = [nid for nid, d in indeg.items() if d == 0]
323
+ order: List[str] = []
324
+ while queue:
325
+ cur = queue.pop(0)
326
+ order.append(cur)
327
+ for nb in adj[cur]:
328
+ indeg[nb] -= 1
329
+ if indeg[nb] == 0:
330
+ queue.append(nb)
331
+
332
+ if len(order) != len(nodes):
333
+ unresolved = [nid for nid, d in indeg.items() if d > 0]
334
+ raise ValueError(
335
+ f"cycle detected in train dependency DAG; unresolved nodes: {', '.join(unresolved)}"
336
+ )
337
+
338
+ node_map = {n.node_id: n for n in nodes}
339
+ return [node_map[nid] for nid in order]