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,479 @@
1
+ """Git-discipline guardrail + bounded top-of-loop self-heal (roadmap Slice G).
2
+
3
+ CONSUMER of the neutral ``@consiliency/contract`` git-discipline contract -- it
4
+ does NOT redefine it. It reads two contract artifacts through the existing thin
5
+ loaders (``consiliency_contract.load_registry`` / ``load_schema``):
6
+
7
+ * registry ``pipeline_ref_classes`` -- the falsifiable, ordered enumeration of
8
+ pipeline-owned ref families plus the human default (everything else).
9
+ * schema ``git_discipline_protocol`` -- the write-footprint allowlist, the
10
+ self-heal scope, and the invariants (``never_delete_human_refs`` ...). All of
11
+ its knobs are JSON-Schema ``const``s, so the allowlist etc. are read out of
12
+ the schema structure at runtime, never hand-copied.
13
+
14
+ Two responsibilities, both bounded and autonomy-first:
15
+
16
+ 1. **GUARDRAIL (verification).** Classify the repo's refs against the registry
17
+ and check the write-footprint / branch-naming invariants. Surfaced as the
18
+ fifth L0 ``.consiliency`` gate (``consiliency_gates.git_discipline``):
19
+ SOFT/warn by default, blocking only under
20
+ ``PHASE_LOOP_CONSILIENCY_GATES=hard`` (opt-in). The gate layer NEVER sets
21
+ ``human_required``.
22
+
23
+ 2. **SELF-HEAL PARTITION (top-of-loop).** Decide which refs a bounded,
24
+ idempotent self-heal may reconcile. The partition is PURE and default-deny: a
25
+ ref is eligible for self-heal deletion ONLY when it is pipeline-owned, in a
26
+ ref-class flagged ``deletable_by_self_heal``, currently leased, AND already
27
+ merged. Every human ref, every unleased pipeline ref, and every non-deletable
28
+ pipeline ref is PROTECTED. This is the machine form of the
29
+ NEVER-DELETE-HUMAN-REFS invariant; ``never_deleted_human_refs`` is provably a
30
+ subset of ``protected`` and disjoint from ``deletable_by_self_heal``.
31
+
32
+ Contract-absent degrade: when the installed ``consiliency_contract`` predates the
33
+ git-discipline contract (< 0.4, no ``pipeline_ref_classes`` registry), every
34
+ entry point degrades to a neutral no-op (``available()`` is ``False``) -- NOT a
35
+ warning -- so existing governed scans are byte-for-byte unaffected until the
36
+ contract carrying it is installed.
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import re
41
+ import subprocess
42
+ from dataclasses import dataclass
43
+ from fnmatch import fnmatchcase
44
+ from pathlib import Path
45
+ from typing import Any, Callable, Mapping, Sequence
46
+
47
+ REF_CLASS_REGISTRY = "pipeline_ref_classes"
48
+ GIT_DISCIPLINE_SCHEMA = "git_discipline_protocol"
49
+
50
+ # One placeholder ``{name}`` OR a glob ``*`` -- each spans exactly one path
51
+ # segment (``[^/]+``). Anchoring the compiled pattern (below) is load-bearing:
52
+ # unanchored, ``pipeline/{phase}-{node}`` would match ``my-pipeline/foo-bar`` as
53
+ # a substring and misclassify a human ref as pipeline-owned/deletable.
54
+ _TOKEN_RE = re.compile(r"\{[^}]+\}|\*")
55
+
56
+
57
+ # --------------------------------------------------------------------------- #
58
+ # Contract loading (guarded; contract-absent -> neutral no-op)
59
+ # --------------------------------------------------------------------------- #
60
+ def load_ref_classes() -> dict[str, Any] | None:
61
+ """The ``pipeline_ref_classes`` registry, or ``None`` when the installed
62
+ contract predates the git-discipline contract (< 0.4)."""
63
+ try:
64
+ from consiliency_contract import load_registry
65
+
66
+ registry = load_registry(REF_CLASS_REGISTRY)
67
+ except Exception:
68
+ return None
69
+ return registry if isinstance(registry, Mapping) else None
70
+
71
+
72
+ def load_protocol() -> dict[str, Any] | None:
73
+ """The ``git_discipline_protocol`` schema, or ``None`` when absent."""
74
+ try:
75
+ from consiliency_contract import load_schema
76
+
77
+ schema = load_schema(GIT_DISCIPLINE_SCHEMA)
78
+ except Exception:
79
+ return None
80
+ return schema if isinstance(schema, Mapping) else None
81
+
82
+
83
+ def available(registry: Mapping[str, Any] | None = None) -> bool:
84
+ """True when the installed contract carries the git-discipline ref-class
85
+ registry (i.e. the guardrail can run against a real contract)."""
86
+ reg = load_ref_classes() if registry is None else registry
87
+ return isinstance(reg, Mapping) and bool(reg.get("ref_classes"))
88
+
89
+
90
+ # --------------------------------------------------------------------------- #
91
+ # Ref classification (pure)
92
+ # --------------------------------------------------------------------------- #
93
+ def _pattern_to_regex(pattern: str) -> re.Pattern[str]:
94
+ """Compile a registry ``pattern`` to an anchored regex. Literal spans are
95
+ ``re.escape``-d; each ``{placeholder}`` / ``*`` becomes one path segment."""
96
+ parts: list[str] = []
97
+ last = 0
98
+ for match in _TOKEN_RE.finditer(pattern):
99
+ parts.append(re.escape(pattern[last : match.start()]))
100
+ parts.append("[^/]+")
101
+ last = match.end()
102
+ parts.append(re.escape(pattern[last:]))
103
+ return re.compile("^" + "".join(parts) + "$")
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class RefClassification:
108
+ name: str
109
+ owner: str # "pipeline" | "human"
110
+ class_id: str
111
+ lease_required: bool
112
+ deletable_by_self_heal: bool
113
+ merge_policy: str | None
114
+
115
+
116
+ def _classification_from(entry: Mapping[str, Any], name: str) -> RefClassification:
117
+ return RefClassification(
118
+ name=name,
119
+ owner=str(entry.get("owner", "human")),
120
+ class_id=str(entry.get("id", "human")),
121
+ lease_required=bool(entry.get("lease_required", False)),
122
+ deletable_by_self_heal=bool(entry.get("deletable_by_self_heal", False)),
123
+ merge_policy=entry.get("merge_policy"),
124
+ )
125
+
126
+
127
+ def classify_ref(name: str, registry: Mapping[str, Any]) -> RefClassification:
128
+ """Classify ``name`` by first-match against ``ref_classes`` in order; a ref
129
+ that matches no pipeline-owned pattern falls through to the human default.
130
+
131
+ Default-deny: anything not provably matching a pipeline-owned pattern is
132
+ human-owned and therefore never eligible for self-heal deletion.
133
+ """
134
+ for entry in registry.get("ref_classes", []):
135
+ pattern = str(entry.get("pattern") or "")
136
+ if not pattern:
137
+ continue
138
+ if _pattern_to_regex(pattern).match(name):
139
+ return _classification_from(entry, name)
140
+ human_default = registry.get("human_default") or {"owner": "human", "id": "human"}
141
+ return _classification_from(human_default, name)
142
+
143
+
144
+ # --------------------------------------------------------------------------- #
145
+ # Self-heal partition (pure) -- the NEVER-DELETE-HUMAN-REFS invariant in code
146
+ # --------------------------------------------------------------------------- #
147
+ @dataclass(frozen=True)
148
+ class RefState:
149
+ """A ref plus the two facts self-heal eligibility turns on."""
150
+
151
+ name: str
152
+ leased: bool = False
153
+ merged: bool = False
154
+
155
+
156
+ def _eligible_for_self_heal(cls: RefClassification, ref: RefState) -> bool:
157
+ """The registry ``self_heal_deletion_rule``: eligible iff owner is
158
+ ``pipeline`` AND the ref-class is ``deletable_by_self_heal`` AND the ref is
159
+ currently leased AND (for the phase-worktree classes -- the only deletable
160
+ ones) already merged. Required uniformly here: any other class is
161
+ non-deletable, so demanding ``merged`` on the deletable ones is exact and
162
+ default-deny for everything else."""
163
+ return (
164
+ cls.owner == "pipeline"
165
+ and cls.deletable_by_self_heal
166
+ and ref.leased
167
+ and ref.merged
168
+ )
169
+
170
+
171
+ def _protected_reason(cls: RefClassification, ref: RefState) -> str:
172
+ if cls.owner == "human":
173
+ return "human ref -- never auto-deleted"
174
+ if not cls.deletable_by_self_heal:
175
+ return f"pipeline ref in non-deletable class {cls.class_id!r} -- reconcile rebases, never deletes"
176
+ if not ref.leased:
177
+ return "pipeline ref in a deletable class but UNLEASED -- self-heal needs a lease"
178
+ if not ref.merged:
179
+ return "pipeline ref leased but not yet merged -- self-heal never moves in-progress work"
180
+ return "protected"
181
+
182
+
183
+ def self_heal_partition(
184
+ refs: Sequence[RefState], registry: Mapping[str, Any]
185
+ ) -> dict[str, Any]:
186
+ """Partition ``refs`` into what a bounded self-heal MAY delete vs MUST
187
+ protect. Pure -- the caller supplies the lease/merge facts.
188
+
189
+ The returned shape mirrors the contract conformance vector
190
+ ``git-discipline-never-delete-human-refs`` so the vector can drive a replay
191
+ test: ``deletable_by_self_heal``, ``protected``, ``human_refs``,
192
+ ``never_deleted_human_refs``, and per-ref ``protected_reasons``.
193
+ """
194
+ deletable: list[str] = []
195
+ protected: list[str] = []
196
+ human_refs: list[str] = []
197
+ reasons: dict[str, str] = {}
198
+ for ref in refs:
199
+ cls = classify_ref(ref.name, registry)
200
+ if cls.owner == "human":
201
+ human_refs.append(ref.name)
202
+ if _eligible_for_self_heal(cls, ref):
203
+ deletable.append(ref.name)
204
+ else:
205
+ protected.append(ref.name)
206
+ reasons[ref.name] = _protected_reason(cls, ref)
207
+ deletable_set = set(deletable)
208
+ return {
209
+ "deletable_by_self_heal": deletable,
210
+ "protected": protected,
211
+ "human_refs": human_refs,
212
+ # A human ref is NEVER eligible, so this is provably == human_refs and
213
+ # disjoint from deletable_by_self_heal (the invariant, asserted in tests).
214
+ "never_deleted_human_refs": [n for n in human_refs if n not in deletable_set],
215
+ "protected_reasons": reasons,
216
+ }
217
+
218
+
219
+ # --------------------------------------------------------------------------- #
220
+ # Guardrail evaluation (pure) -- write-footprint + branch-naming invariants
221
+ # --------------------------------------------------------------------------- #
222
+ def write_footprint_allowlist(protocol: Mapping[str, Any] | None) -> tuple[str, ...]:
223
+ """The pipeline write-footprint allowlist, read out of the schema ``const``
224
+ (never hand-copied). Empty tuple when the schema is absent/misshaped."""
225
+ try:
226
+ allow = protocol["properties"]["write_footprint"]["properties"]["allowlist"]["const"] # type: ignore[index]
227
+ except (KeyError, TypeError):
228
+ return ()
229
+ return tuple(str(p) for p in allow) if isinstance(allow, list) else ()
230
+
231
+
232
+ def _allowlisted(path: str, allowlist: Sequence[str]) -> bool:
233
+ for pattern in allowlist:
234
+ if pattern.endswith("/**"):
235
+ prefix = pattern[:-3]
236
+ if path == prefix or path.startswith(prefix + "/"):
237
+ return True
238
+ elif path == pattern or fnmatchcase(path, pattern):
239
+ return True
240
+ return False
241
+
242
+
243
+ def _pipeline_prefixes(registry: Mapping[str, Any]) -> list[str]:
244
+ """The static (pre-placeholder) prefix of each pipeline-owned pattern, e.g.
245
+ ``consiliency/pipeline/``, ``phase-loop/sched/``, ``pipeline/``. Used to spot
246
+ a branch that SITS under a pipeline family prefix but matches no canonical
247
+ pattern -- i.e. a pipeline-owned ref that drifted from the contract shape."""
248
+ prefixes: list[str] = []
249
+ for entry in registry.get("ref_classes", []):
250
+ if entry.get("owner") != "pipeline":
251
+ continue
252
+ pattern = str(entry.get("pattern") or "")
253
+ token = _TOKEN_RE.search(pattern)
254
+ prefix = pattern[: token.start()] if token else pattern
255
+ if prefix:
256
+ prefixes.append(prefix)
257
+ return prefixes
258
+
259
+
260
+ def evaluate_git_discipline(
261
+ *,
262
+ current_branch: str,
263
+ dirty_paths: Sequence[str],
264
+ local_branches: Sequence[str],
265
+ registry: Mapping[str, Any],
266
+ protocol: Mapping[str, Any] | None,
267
+ ) -> list[dict[str, Any]]:
268
+ """Return git-discipline findings for the repo (pure; the caller injects the
269
+ git facts). Severity is applied by the gate wrapper, not here."""
270
+ findings: list[dict[str, Any]] = []
271
+
272
+ # (1) Write-footprint: on a pipeline-owned working branch, dirty writes must
273
+ # stay inside the contract allowlist (schema policy
274
+ # ``unauthorized-pipeline-write-refused``).
275
+ current_cls = classify_ref(current_branch, registry)
276
+ if current_cls.owner == "pipeline":
277
+ allowlist = write_footprint_allowlist(protocol)
278
+ if allowlist: # only enforce when the contract actually declares one
279
+ for path in dirty_paths:
280
+ if not _allowlisted(path, allowlist):
281
+ findings.append(
282
+ {
283
+ "code": "write_footprint_violation",
284
+ "branch": current_branch,
285
+ "path": path,
286
+ }
287
+ )
288
+
289
+ # (2) Branch-naming: a branch under a pipeline-owned family prefix that
290
+ # matches no canonical pattern (default-deny -> classified human) has
291
+ # drifted from the contract shape.
292
+ prefixes = _pipeline_prefixes(registry)
293
+ for branch in local_branches:
294
+ if classify_ref(branch, registry).owner == "pipeline":
295
+ continue
296
+ if any(branch.startswith(prefix) for prefix in prefixes):
297
+ findings.append({"code": "pipeline_branch_naming_drift", "branch": branch})
298
+
299
+ return findings
300
+
301
+
302
+ # --------------------------------------------------------------------------- #
303
+ # Impure adapters -- gather git facts (kept out of the pure core above)
304
+ # --------------------------------------------------------------------------- #
305
+ def _git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
306
+ return subprocess.run(
307
+ ["git", "-C", str(repo), *args],
308
+ check=False,
309
+ stdout=subprocess.PIPE,
310
+ stderr=subprocess.PIPE,
311
+ text=True,
312
+ )
313
+
314
+
315
+ def gather_repo_ref_facts(repo: Path) -> dict[str, Any]:
316
+ """Best-effort snapshot for :func:`evaluate_git_discipline`. Never raises;
317
+ a git failure degrades to empty facts (the guardrail then finds nothing)."""
318
+ current = _git(repo, "branch", "--show-current")
319
+ current_branch = current.stdout.strip() if current.returncode == 0 else ""
320
+
321
+ branches: list[str] = []
322
+ listed = _git(repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/")
323
+ if listed.returncode == 0:
324
+ branches = [line.strip() for line in listed.stdout.splitlines() if line.strip()]
325
+
326
+ dirty_paths: list[str] = []
327
+ status = _git(repo, "status", "--porcelain", "--untracked-files=all")
328
+ if status.returncode == 0:
329
+ for line in status.stdout.splitlines():
330
+ entry = line[3:].strip() if len(line) > 3 else ""
331
+ if " -> " in entry: # renames: "old -> new"
332
+ entry = entry.split(" -> ", 1)[1]
333
+ if entry:
334
+ dirty_paths.append(entry)
335
+
336
+ return {
337
+ "current_branch": current_branch,
338
+ "dirty_paths": tuple(dirty_paths),
339
+ "local_branches": tuple(branches),
340
+ }
341
+
342
+
343
+ def gather_pipeline_ref_states(
344
+ repo: Path,
345
+ registry: Mapping[str, Any],
346
+ *,
347
+ base_ref: str | None = None,
348
+ lease_predicate: Callable[[str], bool] | None = None,
349
+ ) -> list[RefState]:
350
+ """Snapshot local branches as :class:`RefState` (name / leased / merged) for
351
+ :func:`self_heal_partition`. ``leased`` defaults to ``False`` (conservative
352
+ -> nothing eligible) unless a ``lease_predicate`` is supplied; ``merged`` is
353
+ computed against ``base_ref`` (default: the repo's default branch)."""
354
+ listed = _git(repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/")
355
+ branches = (
356
+ [line.strip() for line in listed.stdout.splitlines() if line.strip()]
357
+ if listed.returncode == 0
358
+ else []
359
+ )
360
+ base = base_ref or _default_branch(repo)
361
+ merged: set[str] = set()
362
+ if base:
363
+ result = _git(repo, "branch", "--format=%(refname:short)", "--merged", base)
364
+ if result.returncode == 0:
365
+ merged = {line.strip() for line in result.stdout.splitlines() if line.strip()}
366
+ predicate = lease_predicate or (lambda _name: False)
367
+ return [
368
+ RefState(name=b, leased=bool(predicate(b)), merged=b in merged) for b in branches
369
+ ]
370
+
371
+
372
+ def _default_branch(repo: Path) -> str:
373
+ result = _git(repo, "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD")
374
+ head = result.stdout.strip() if result.returncode == 0 else ""
375
+ return head.removeprefix("origin/") if head.startswith("origin/") else ""
376
+
377
+
378
+ # --------------------------------------------------------------------------- #
379
+ # Bounded top-of-loop self-heal -- detect + idempotent-safe fix; delegate deletes
380
+ # --------------------------------------------------------------------------- #
381
+ def reconcile_git_discipline(
382
+ repo: Path,
383
+ *,
384
+ registry: Mapping[str, Any] | None = None,
385
+ ref_states: Sequence[RefState] | None = None,
386
+ execute_prune: bool = False,
387
+ ) -> dict[str, Any]:
388
+ """Bounded top-of-loop self-heal. Detection is always safe (pure reporting);
389
+ the only auto-fix is ``git worktree prune`` (removes stale worktree admin
390
+ records -- it never deletes a branch or a live worktree, so it is inherently
391
+ idempotent-safe). Branch/worktree DELETION for the deletable set is NOT
392
+ performed here; it is surfaced as a plan and left to
393
+ :func:`apply_self_heal_deletions` (opt-in), which re-asserts the partition.
394
+
395
+ Returns ``{"status": "skipped"|"reconciled", "partition": {...},
396
+ "findings": [...]}``. ``findings`` are advisory, ``human_required=False``.
397
+ Never raises.
398
+ """
399
+ # Contract-absent: git-discipline is entirely latent when the installed
400
+ # contract predates it (< 0.4) -- nothing to consent to or enforce.
401
+ reg = load_ref_classes() if registry is None else registry
402
+ if not available(reg):
403
+ return {"status": "skipped", "reason": "contract-absent", "findings": []}
404
+ assert reg is not None # narrowed by available()
405
+
406
+ # Consent gate (design §11.5): the git-discipline contract is post-ingestion
407
+ # and opt-in. A repo without a `.consiliency/manifest.json` is a pure no-op --
408
+ # no classification, no advisories, no `git worktree prune`. This mirrors
409
+ # `scan_consiliency_gates`, which the guardrail gate already rides.
410
+ try:
411
+ from .consiliency_layout import find_consiliency_manifest
412
+
413
+ if find_consiliency_manifest(Path(repo)) is None:
414
+ return {"status": "skipped", "reason": "no-consent", "findings": []}
415
+ except Exception:
416
+ return {"status": "skipped", "reason": "no-consent", "findings": []}
417
+
418
+ states = (
419
+ list(ref_states)
420
+ if ref_states is not None
421
+ else gather_pipeline_ref_states(repo, reg)
422
+ )
423
+ partition = self_heal_partition(states, reg)
424
+
425
+ findings: list[dict[str, Any]] = []
426
+ # Drift is advisory: never auto-moved (moving possibly-in-progress work is
427
+ # exactly the failure mode the contract forbids).
428
+ facts = gather_repo_ref_facts(repo)
429
+ for branch in facts["local_branches"]:
430
+ if classify_ref(branch, reg).owner == "pipeline":
431
+ continue
432
+ if any(branch.startswith(p) for p in _pipeline_prefixes(reg)):
433
+ findings.append(
434
+ {
435
+ "code": "pipeline_branch_naming_drift",
436
+ "branch": branch,
437
+ "human_required": False,
438
+ }
439
+ )
440
+
441
+ if execute_prune:
442
+ # `git worktree prune` only garbage-collects admin records for worktrees
443
+ # whose working directory is already gone; it cannot touch a ref.
444
+ _git(repo, "worktree", "prune")
445
+
446
+ return {"status": "reconciled", "partition": partition, "findings": findings}
447
+
448
+
449
+ def apply_self_heal_deletions(
450
+ repo: Path,
451
+ partition: Mapping[str, Any],
452
+ *,
453
+ registry: Mapping[str, Any] | None = None,
454
+ ) -> dict[str, list[str]]:
455
+ """Delete ONLY the refs the partition marked ``deletable_by_self_heal``,
456
+ using ``git branch -d`` (lowercase) so git's own merged-check is a second
457
+ gate behind our merged-check. Opt-in; not called by the top-of-loop hook by
458
+ default.
459
+
460
+ Runtime guard: every candidate is re-classified and re-checked against the
461
+ registry before deletion; a candidate that is NOT provably pipeline-owned +
462
+ deletable is refused (``refused``), so even a caller bug cannot delete a
463
+ human ref. This is the NEVER-DELETE-HUMAN-REFS invariant enforced a second
464
+ time at the mutation boundary.
465
+ """
466
+ reg = load_ref_classes() if registry is None else registry
467
+ deleted: list[str] = []
468
+ refused: list[str] = []
469
+ if not available(reg):
470
+ return {"deleted": deleted, "refused": list(partition.get("deletable_by_self_heal", []))}
471
+ assert reg is not None
472
+ for name in partition.get("deletable_by_self_heal", []):
473
+ cls = classify_ref(name, reg)
474
+ if not (cls.owner == "pipeline" and cls.deletable_by_self_heal):
475
+ refused.append(name)
476
+ continue
477
+ result = _git(repo, "branch", "-d", name)
478
+ (deleted if result.returncode == 0 else refused).append(name)
479
+ return {"deleted": deleted, "refused": refused}
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from fnmatch import fnmatchcase
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from .discovery import PlanOwnership
11
+ from .models import PhaseSourceBundle
12
+
13
+
14
+ def snapshot_git_dirty_paths(repo: Path) -> tuple[str, ...]:
15
+ try:
16
+ output = subprocess.check_output(
17
+ ["git", "-C", str(repo), "status", "--porcelain", "--untracked-files=all"],
18
+ text=True,
19
+ stderr=subprocess.DEVNULL,
20
+ )
21
+ except Exception:
22
+ return ()
23
+ paths: list[str] = []
24
+ for line in output.splitlines():
25
+ if len(line) < 4:
26
+ continue
27
+ path = line[3:]
28
+ if " -> " in path:
29
+ path = path.split(" -> ", 1)[1]
30
+ if path and path not in paths:
31
+ paths.append(path)
32
+ return tuple(paths)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class PipelineWriteBoundaryDiagnostic:
37
+ kind: str
38
+ message: str
39
+ path: str
40
+ blocker_class: str = "contract_bug"
41
+ human_required: bool = False
42
+
43
+ def to_json(self) -> dict[str, object]:
44
+ return {key: value for key, value in asdict(self).items() if value not in (None, "")}
45
+
46
+
47
+ def pipeline_authorized_write_patterns(bundle: "PhaseSourceBundle") -> tuple[str, ...]:
48
+ policy = bundle.delegated_write_policy if isinstance(bundle.delegated_write_policy, dict) else {}
49
+ patterns: list[str] = []
50
+ for value in policy.get("owned_files") or ():
51
+ if isinstance(value, str) and value.strip():
52
+ patterns.append(value.strip())
53
+ if bundle.artifact_target_root:
54
+ root = bundle.artifact_target_root.strip().rstrip("/")
55
+ if root:
56
+ patterns.append(root)
57
+ patterns.append(f"{root}/**")
58
+ return tuple(dict.fromkeys(patterns))
59
+
60
+
61
+ def pipeline_write_boundary_diagnostic(
62
+ repo: Path,
63
+ dirty_paths: tuple[str, ...] | list[str],
64
+ *,
65
+ plan_ownership: "PlanOwnership",
66
+ bundle: "PhaseSourceBundle | None",
67
+ ) -> PipelineWriteBoundaryDiagnostic | None:
68
+ if bundle is None:
69
+ return None
70
+ protected_paths = {source.path for source in bundle.protected_sources}
71
+ authorized = pipeline_authorized_write_patterns(bundle)
72
+ for path in dirty_paths:
73
+ rel_path = _repo_relative_path(repo, path)
74
+ if rel_path in protected_paths:
75
+ if not (plan_ownership.matches(rel_path) and _matches_any(rel_path, authorized)):
76
+ return PipelineWriteBoundaryDiagnostic(
77
+ kind="protected_pipeline_source_write",
78
+ message="Protected Pipeline source write is not authorized by both the phase plan and source bundle",
79
+ path=rel_path,
80
+ )
81
+ continue
82
+ if _is_pipeline_owned(rel_path, bundle):
83
+ if not (plan_ownership.matches(rel_path) and _matches_any(rel_path, authorized)):
84
+ return PipelineWriteBoundaryDiagnostic(
85
+ kind="unauthorized_pipeline_write",
86
+ message="Pipeline-owned write is outside the combined phase-plan and source-bundle boundary",
87
+ path=rel_path,
88
+ )
89
+ return None
90
+
91
+
92
+ def _repo_relative_path(repo: Path, path: str) -> str:
93
+ candidate = Path(path)
94
+ if candidate.is_absolute():
95
+ try:
96
+ return candidate.resolve().relative_to(repo.resolve()).as_posix()
97
+ except ValueError:
98
+ return candidate.as_posix()
99
+ return candidate.as_posix()
100
+
101
+
102
+ def _is_pipeline_owned(path: str, bundle: "PhaseSourceBundle") -> bool:
103
+ if path.startswith(".pipeline/") or path == ".pipeline":
104
+ return True
105
+ if path == "pipeline.definition.json":
106
+ return True
107
+ if path in {source.path for source in bundle.protected_sources}:
108
+ return True
109
+ portal_contracts = {
110
+ source.path for source in bundle.protected_sources if source.category == "portal_contracts"
111
+ }
112
+ return path in portal_contracts
113
+
114
+
115
+ def _matches_any(path: str, patterns: tuple[str, ...]) -> bool:
116
+ return any(path == pattern or fnmatchcase(path, pattern) or _globstar_match(path, pattern) for pattern in patterns)
117
+
118
+
119
+ def _globstar_match(path: str, pattern: str) -> bool:
120
+ if not pattern.endswith("/**"):
121
+ return False
122
+ return path.startswith(pattern[:-3].rstrip("/") + "/")