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,167 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import time
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from .runtime_paths import ensure_phase_loop_excluded, phase_loop_dir
11
+ from .runtime_paths import _path_slug
12
+
13
+ # Platform-conditional locking primitives (issue #16).
14
+ # POSIX uses fcntl.flock; Windows uses msvcrt.locking on a 1-byte region at
15
+ # the file's current seek position. Both surface non-blocking exclusive lock
16
+ # acquisition + release through the same _acquire_exclusive / _release_exclusive
17
+ # helpers so the rest of this module stays platform-agnostic.
18
+ if sys.platform == "win32":
19
+ import msvcrt
20
+
21
+ _LOCK_REGION_BYTES = 1
22
+
23
+ def _acquire_exclusive(handle) -> None:
24
+ # msvcrt.locking locks a region of length _LOCK_REGION_BYTES starting
25
+ # at the file's current seek position. Seek to start so concurrent
26
+ # holders contend on the same byte. LK_NBLCK = non-blocking exclusive.
27
+ # Raises OSError (subclass of BlockingIOError on Py3.10+? actually
28
+ # raises OSError with errno EACCES) when contended; normalize to
29
+ # BlockingIOError to match POSIX flock semantics.
30
+ handle.seek(0)
31
+ try:
32
+ msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, _LOCK_REGION_BYTES)
33
+ except OSError as exc:
34
+ raise BlockingIOError(str(exc)) from exc
35
+
36
+ def _release_exclusive(handle) -> None:
37
+ handle.seek(0)
38
+ try:
39
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, _LOCK_REGION_BYTES)
40
+ except OSError:
41
+ # Already released or process is exiting — let cleanup proceed.
42
+ pass
43
+
44
+ else:
45
+ import fcntl
46
+
47
+ def _acquire_exclusive(handle) -> None:
48
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
49
+
50
+ def _release_exclusive(handle) -> None:
51
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class DispatchLockContention(RuntimeError):
56
+ lock_path: Path
57
+ holder_pid: int | None
58
+ holder_started_at: float | None
59
+ roadmap: str | None
60
+
61
+ @property
62
+ def elapsed_seconds(self) -> int | None:
63
+ if self.holder_started_at is None:
64
+ return None
65
+ return max(0, int(time.time() - self.holder_started_at))
66
+
67
+ def blocker_summary(self, roadmap: Path) -> str:
68
+ pid = self.holder_pid if self.holder_pid is not None else "unknown"
69
+ elapsed = self.elapsed_seconds
70
+ elapsed_text = f"{elapsed}s" if elapsed is not None else "unknown seconds"
71
+ holder_roadmap = self.roadmap or str(roadmap)
72
+ return (
73
+ "Concurrent dispatch refused: roadmap "
74
+ f"{holder_roadmap} is locked by PID {pid} for {elapsed_text} "
75
+ f"(lock: {self.lock_path})."
76
+ )
77
+
78
+
79
+ class DispatchLock:
80
+ def __init__(self, repo: Path, roadmap: Path) -> None:
81
+ self.repo = repo
82
+ self.roadmap = roadmap
83
+ self.path = dispatch_lock_path(repo, roadmap)
84
+ self._handle = None
85
+
86
+ def acquire(self) -> "DispatchLock":
87
+ ensure_phase_loop_excluded(self.repo)
88
+ self.path.parent.mkdir(parents=True, exist_ok=True)
89
+ handle = self.path.open("a+", encoding="utf-8")
90
+ try:
91
+ _acquire_exclusive(handle)
92
+ except BlockingIOError as exc:
93
+ holder = _read_holder_metadata(handle)
94
+ handle.close()
95
+ raise DispatchLockContention(
96
+ lock_path=self.path,
97
+ holder_pid=_int_or_none(holder.get("pid")),
98
+ holder_started_at=_float_or_none(holder.get("started_at")),
99
+ roadmap=str(holder.get("roadmap") or "") or None,
100
+ ) from exc
101
+
102
+ handle.seek(0)
103
+ handle.truncate()
104
+ json.dump(
105
+ {
106
+ "pid": os.getpid(),
107
+ "started_at": time.time(),
108
+ "repo": str(self.repo),
109
+ "roadmap": str(self.roadmap),
110
+ },
111
+ handle,
112
+ sort_keys=True,
113
+ )
114
+ handle.write("\n")
115
+ handle.flush()
116
+ os.fsync(handle.fileno())
117
+ self._handle = handle
118
+ return self
119
+
120
+ def release(self) -> None:
121
+ if self._handle is None:
122
+ return
123
+ try:
124
+ _release_exclusive(self._handle)
125
+ finally:
126
+ self._handle.close()
127
+ self._handle = None
128
+
129
+ def __enter__(self) -> "DispatchLock":
130
+ if self._handle is not None:
131
+ return self
132
+ return self.acquire()
133
+
134
+ def __exit__(self, exc_type, exc, tb) -> None:
135
+ self.release()
136
+
137
+
138
+ def dispatch_lock_path(repo: Path, roadmap: Path) -> Path:
139
+ try:
140
+ roadmap_key = str(roadmap.resolve().relative_to(repo.resolve()))
141
+ except ValueError:
142
+ roadmap_key = str(roadmap.resolve())
143
+ return phase_loop_dir(repo) / _path_slug(roadmap_key) / "dispatch.lock"
144
+
145
+
146
+ def _read_holder_metadata(handle) -> dict[str, object]:
147
+ try:
148
+ handle.seek(0)
149
+ raw = handle.read().strip()
150
+ payload = json.loads(raw) if raw else {}
151
+ except (OSError, json.JSONDecodeError):
152
+ return {}
153
+ return payload if isinstance(payload, dict) else {}
154
+
155
+
156
+ def _int_or_none(value: object) -> int | None:
157
+ try:
158
+ return int(value)
159
+ except (TypeError, ValueError):
160
+ return None
161
+
162
+
163
+ def _float_or_none(value: object) -> float | None:
164
+ try:
165
+ return float(value)
166
+ except (TypeError, ValueError):
167
+ return None
@@ -0,0 +1,98 @@
1
+ """Doc-delta closeout validator (rigor-v1 P2).
2
+
3
+ When a phase's diff touches a user-visible public surface but the closeout
4
+ records no doc-delta decision, raise a finding. Autonomy-first: the finding is
5
+ ``block``-severity, so under the default ``PHASE_LOOP_REVIEW=warn`` it is merely
6
+ recorded (the loop continues) and only blocks when an operator opts in. The
7
+ agent self-satisfies it by updating docs or recording a ``no_doc_delta``
8
+ decision in the terminal summary — no human required.
9
+
10
+ The executor records its decision as ``doc_delta_decision: <literal>`` in the
11
+ terminal summary (one of ``models.DOC_DELTA_DECISIONS``).
12
+
13
+ issue #18 F5 — evidence-backed corroboration. A bare ``no_doc_delta`` is a
14
+ *self-attested literal*: the executor claims "the public surface is already
15
+ current, no docs change needed". On a **release/explicit-release** phase that
16
+ claim is exactly the one the docs-freshness incident proved unreliable. When a
17
+ runner-side freshness scan was threaded in (``ctx.docs_freshness``), we
18
+ corroborate the ``no_doc_delta`` against its enumerated-surface evidence: an
19
+ un-corroborated ``no_doc_delta`` on a release phase is downgraded to a recorded
20
+ **warn** finding (never a block — autonomy-first), surfacing the scan result
21
+ alongside. Ordinary phases, and any phase with no scan threaded in, are
22
+ unaffected (the literal still satisfies, as before).
23
+ """
24
+ from __future__ import annotations
25
+
26
+ from .closeout_validators import CloseoutContext, ReviewFinding, register_closeout_validator
27
+ from .docs_freshness import docs_freshness_evidence_backed
28
+ from .models import DOC_DELTA_DECISIONS, public_surface_touched
29
+
30
+
31
+ @register_closeout_validator
32
+ def doc_delta_validator(ctx: CloseoutContext) -> list[ReviewFinding]:
33
+ if not public_surface_touched(ctx.changed_paths):
34
+ return []
35
+ decision = str(ctx.terminal.get("doc_delta_decision") or "").strip()
36
+ if decision in DOC_DELTA_DECISIONS:
37
+ return _corroborate_no_doc_delta(ctx, decision) # may be [] or a warn
38
+ return [
39
+ ReviewFinding(
40
+ code="doc_delta_undecided",
41
+ reason=(
42
+ "changed a public surface (CLI/schema/contract docs/README/CHANGELOG) "
43
+ "but recorded no doc_delta decision; update the doc surface or record "
44
+ "doc_delta_decision=no_doc_delta with a justification"
45
+ ),
46
+ severity="block",
47
+ blocker_class="review_gate_block",
48
+ )
49
+ ]
50
+
51
+
52
+ def _corroborate_no_doc_delta(ctx: CloseoutContext, decision: str) -> list[ReviewFinding]:
53
+ """F5: corroborate a self-attested ``no_doc_delta`` on a release phase.
54
+
55
+ Default-safe by construction:
56
+
57
+ * Only ``no_doc_delta`` is corroborated — ``docs_updated`` /
58
+ ``docs_follow_up_filed`` already evidence a real change and pass through.
59
+ * No scan threaded in (``ctx.docs_freshness is None``) → no corroboration is
60
+ available → satisfied (return ``[]``). We never newly-fail because the
61
+ runner didn't wire the scan.
62
+ * Non-release phases are untouched: an ordinary phase legitimately records
63
+ ``no_doc_delta`` for an internal change, and the freshness scan reports
64
+ ``skipped`` there anyway.
65
+ * Freshness gate ``off`` (``PHASE_LOOP_DOCS_FRESHNESS=off``) means "assert
66
+ nothing about docs": no corroboration is performed (return ``[]``), so
67
+ disabling the gate never *adds* a finding.
68
+ * The downgrade is **warn**, never block — the existing block path is the
69
+ *undecided* case, which this branch never reaches.
70
+ """
71
+ if decision != "no_doc_delta":
72
+ return []
73
+ detail = ctx.docs_freshness
74
+ if not detail:
75
+ return [] # unwired scan — no corroboration available, satisfied
76
+ detail = dict(detail)
77
+ if str(detail.get("mode") or "") == "off":
78
+ return [] # gate disabled — assert nothing, never add a finding
79
+ if not detail.get("is_release_phase"):
80
+ return [] # ordinary phase — self-attested no_doc_delta is fine
81
+ if docs_freshness_evidence_backed(detail):
82
+ return [] # the path-keyed scan corroborates the claim — satisfied
83
+ status = str(detail.get("status") or "skipped")
84
+ surfaces = list(detail.get("surfaces_scanned") or ())
85
+ return [
86
+ ReviewFinding(
87
+ code="doc_delta_uncorroborated",
88
+ reason=(
89
+ "release phase recorded `no_doc_delta` but the path-keyed "
90
+ "docs-freshness scan does not corroborate it "
91
+ f"(docs_freshness={status}, surfaces_scanned={len(surfaces)}); "
92
+ "treat the no-doc-change claim as unverified until the freshness "
93
+ "scan enumerates the public-doc surfaces and reports `passed`."
94
+ ),
95
+ severity="warn",
96
+ blocker_class="review_gate_warn",
97
+ )
98
+ ]
@@ -0,0 +1,285 @@
1
+ """Pipeline-independent docs-freshness audit (docs-freshness v4 P1, Layer A).
2
+
3
+ `phase-loop docs-audit --base <ref>` runs on a git diff ALONE — no `.phase-loop/`
4
+ state — so it is immune to the three #18 evasion paths (under-scoped docs lane,
5
+ direct-`Agent()` pipeline bypass, absent runtime helper). It classifies the changed
6
+ paths against the canonical `docs_surfaces` taxonomy and enforces the per-surface,
7
+ relevance-bound decision contract, emitting a `docs_freshness: passed|skipped|blocked`
8
+ report and a non-zero exit when a public surface changed without a satisfying decision.
9
+
10
+ This is the only *non-bypassable* control; the in-pipeline `docs_freshness` closeout
11
+ gate (shipped in v0.1.6) is the advisory early-feedback / content-token layer. This
12
+ backstop adds the piece that gate structurally cannot catch: the *silent-absence* case
13
+ — a release surface changed in the diff but its required doc was never updated.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import subprocess
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from . import docs_surfaces as ds
25
+
26
+ #: git's well-known empty-tree object — the diff base for a first push / first tag,
27
+ #: where the whole tree is "new" and there is no prior ref to diff against.
28
+ _EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
29
+
30
+
31
+ class DiffUnavailable(RuntimeError):
32
+ """A git diff could not be computed — the audit fails CLOSED, never a silent pass."""
33
+
34
+ #: Decisions an operator/executor can record per surface. Release-class is held to
35
+ #: the relevance binding regardless (a token never substitutes for the real doc).
36
+ DECISION_TOKENS = ("docs_updated", "no_doc_delta", "docs_follow_up_filed")
37
+ #: Repo-visible decision artifact, recoverable WITHOUT `.phase-loop/` state.
38
+ DEFAULT_DECISIONS_PATH = ".doc-decisions.json"
39
+ DOC_DECISIONS_SCHEMA_VERSION = 1
40
+
41
+
42
+ def _git(repo: Path, *args: str) -> str:
43
+ try:
44
+ return subprocess.check_output(
45
+ ["git", "-C", str(repo), *args], text=True, stderr=subprocess.DEVNULL
46
+ ).strip()
47
+ except Exception:
48
+ return ""
49
+
50
+
51
+ def _rev_ok(repo: Path, ref: str) -> bool:
52
+ return bool(_git(repo, "rev-parse", "--verify", f"{ref}^{{commit}}"))
53
+
54
+
55
+ def _first_push_base(repo: Path) -> str:
56
+ """Base for a first push / first tag — HEAD~1 if it exists, else the empty tree."""
57
+ return "HEAD~1" if _rev_ok(repo, "HEAD~1") else _EMPTY_TREE
58
+
59
+
60
+ def resolve_base(repo: Path, base: str | None) -> tuple[str | None, str]:
61
+ """Resolve the diff base across the CI contexts. Returns (base, context).
62
+
63
+ Explicit `--base` wins. Otherwise: a PR (`GITHUB_BASE_REF`) → `origin/<base>`; a tag
64
+ push (`GITHUB_REF=refs/tags/...`) → the prior tag (first tag → fallback base); a
65
+ branch push → the push **before** SHA (`DOCS_AUDIT_PUSH_BEFORE`, set to
66
+ `github.event.before`) so the WHOLE batch is diffed, never just the tip. An all-zeros
67
+ `before` (first push to the ref) → fallback base.
68
+ """
69
+ if base:
70
+ return base, "explicit"
71
+ pr_base = os.environ.get("GITHUB_BASE_REF")
72
+ if pr_base:
73
+ return f"origin/{pr_base}", "pull_request"
74
+ ref = os.environ.get("GITHUB_REF", "")
75
+ if ref.startswith("refs/tags/"):
76
+ prior = _git(repo, "describe", "--tags", "--abbrev=0", "HEAD^")
77
+ if prior:
78
+ return prior, "push_tag"
79
+ return _first_push_base(repo), "push_tag_first"
80
+ before = os.environ.get("DOCS_AUDIT_PUSH_BEFORE", "").strip()
81
+ if before:
82
+ if set(before) <= {"0"}: # all-zeros sentinel → first push to this ref
83
+ return _first_push_base(repo), "push_first"
84
+ return before, "push"
85
+ return "HEAD~1", "push" # local / no-CI-env fallback
86
+
87
+
88
+ def _base_usable(repo: Path, base: str) -> bool:
89
+ return base == _EMPTY_TREE or _rev_ok(repo, base)
90
+
91
+
92
+ def _diff_op(context: str) -> str:
93
+ # PR / explicit: three-dot (changes vs the merge-base). push / tag: two-dot
94
+ # (exactly the pushed commits — the correct semantics for a batched push range).
95
+ return "..." if context in {"pull_request", "explicit"} else ".."
96
+
97
+
98
+ def changed_paths(repo: Path, base: str, op: str = "...", head: str = "HEAD") -> list[str]:
99
+ """`git diff --name-only base<op>head`. Raises `DiffUnavailable` on a git error so
100
+ the caller fails CLOSED — an empty result means "no changes", never "couldn't tell"."""
101
+ try:
102
+ completed = subprocess.run(
103
+ ["git", "-C", str(repo), "diff", "--name-only", f"{base}{op}{head}"],
104
+ stdout=subprocess.PIPE,
105
+ stderr=subprocess.DEVNULL,
106
+ text=True,
107
+ check=False,
108
+ )
109
+ except OSError as exc:
110
+ raise DiffUnavailable(str(exc)) from exc
111
+ if completed.returncode not in {0, 1}:
112
+ raise DiffUnavailable(f"git diff exited {completed.returncode} for {base}{op}{head}")
113
+ return [p for p in completed.stdout.splitlines() if p.strip()]
114
+
115
+
116
+ def load_decisions(repo: Path, path: str | None = None) -> dict[str, dict[str, Any]]:
117
+ """Read the repo-visible decision artifact. Missing file → no decisions.
118
+
119
+ Returns a mapping `surface-key -> {decision, reason, evidence}`. The key may be a
120
+ glob/path matching changed surfaces, or a class name (`general`/`release`/`*`).
121
+ """
122
+ decisions_path = repo / (path or DEFAULT_DECISIONS_PATH)
123
+ if not decisions_path.is_file():
124
+ return {}
125
+ try:
126
+ data = json.loads(decisions_path.read_text(encoding="utf-8"))
127
+ except (OSError, ValueError):
128
+ return {}
129
+ out: dict[str, dict[str, Any]] = {}
130
+ for entry in data.get("decisions", []) or []:
131
+ surface = str(entry.get("surface") or "").strip()
132
+ decision = str(entry.get("decision") or "").strip()
133
+ if surface and decision in DECISION_TOKENS:
134
+ out[surface] = {
135
+ "decision": decision,
136
+ "reason": str(entry.get("reason") or ""),
137
+ "evidence": tuple(entry.get("evidence") or ()),
138
+ }
139
+ return out
140
+
141
+
142
+ def _decision_for(path: str, klass: str, decisions: dict[str, dict[str, Any]]) -> dict[str, Any] | None:
143
+ for key in (path, klass, "*"):
144
+ if key in decisions:
145
+ return decisions[key]
146
+ for key, value in decisions.items():
147
+ if any(ch in key for ch in "*?[") and ds._match(path, key):
148
+ return value
149
+ return None
150
+
151
+
152
+ @dataclass
153
+ class AuditReport:
154
+ docs_freshness: str # "passed" | "skipped" | "blocked"
155
+ findings: list[dict[str, Any]] = field(default_factory=list)
156
+ surfaces: dict[str, list[str]] = field(default_factory=dict)
157
+ evidence: dict[str, Any] = field(default_factory=dict)
158
+
159
+ @property
160
+ def exit_code(self) -> int:
161
+ return 0 if self.docs_freshness in {"passed", "skipped"} else 1
162
+
163
+ def to_json(self) -> dict[str, Any]:
164
+ return {
165
+ "docs_freshness": self.docs_freshness,
166
+ "findings": self.findings,
167
+ "surfaces": self.surfaces,
168
+ "evidence": self.evidence,
169
+ }
170
+
171
+
172
+ def evaluate(changed: list[str], decisions: dict[str, dict[str, Any]]) -> AuditReport:
173
+ """Apply the per-surface, relevance-bound decision contract to a changed-path set."""
174
+ changed_docs = [p for p in changed if ds.is_doc_surface(p)]
175
+ release_surfaces = [p for p in changed if ds.classify_surface(p) == "release" and not ds.is_doc_surface(p)]
176
+ general_surfaces = [p for p in changed if ds.classify_surface(p) == "general" and not ds.is_doc_surface(p)]
177
+
178
+ findings: list[dict[str, Any]] = []
179
+
180
+ for surface in release_surfaces:
181
+ required = ds.required_docs_for(surface)
182
+ if required:
183
+ relevant_changed = any(ds._match(d, pat) for d in changed_docs for pat in required)
184
+ else:
185
+ relevant_changed = bool(changed_docs)
186
+ if not relevant_changed:
187
+ findings.append({
188
+ "surface": surface,
189
+ "klass": "release",
190
+ "code": "release_docs_unsatisfied",
191
+ "reason": (
192
+ f"release-class surface `{surface}` changed but its required doc "
193
+ f"surface(s) {list(required) or ['any doc']} did not — a token or an "
194
+ f"unrelated doc edit does not satisfy a release surface"
195
+ ),
196
+ })
197
+
198
+ for surface in general_surfaces:
199
+ decision = _decision_for(surface, "general", decisions)
200
+ if not (changed_docs or decision):
201
+ findings.append({
202
+ "surface": surface,
203
+ "klass": "general",
204
+ "code": "general_decision_missing",
205
+ "reason": (
206
+ f"public surface `{surface}` changed with no doc change and no recorded "
207
+ f"doc decision; update a doc or record a doc decision (no_doc_delta + reason)"
208
+ ),
209
+ })
210
+
211
+ surfaces = {
212
+ "release": release_surfaces,
213
+ "general": general_surfaces,
214
+ "docs": changed_docs,
215
+ }
216
+ if not release_surfaces and not general_surfaces:
217
+ return AuditReport("skipped", findings, surfaces, {"reason": "no public surfaces changed"})
218
+ if findings:
219
+ return AuditReport("blocked", findings, surfaces)
220
+ return AuditReport("passed", findings, surfaces)
221
+
222
+
223
+ def run_audit(repo: Path, base: str | None = None, decisions_path: str | None = None) -> AuditReport:
224
+ """Orchestrate the diff-driven audit. Un-evaluable input → `blocked` (never silent pass)."""
225
+ resolved, context = resolve_base(repo, base)
226
+ if not resolved or not _base_usable(repo, resolved):
227
+ return AuditReport(
228
+ "blocked",
229
+ [{
230
+ "surface": None,
231
+ "klass": "audit",
232
+ "code": "base_unresolved",
233
+ "reason": (
234
+ f"could not resolve the diff base (`{base or '<auto>'}`, context={context}); "
235
+ f"the audit cannot evaluate freshness — failing closed (never a silent pass)"
236
+ ),
237
+ }],
238
+ evidence={"base": resolved, "context": context},
239
+ )
240
+ try:
241
+ changed = changed_paths(repo, resolved, _diff_op(context))
242
+ except DiffUnavailable as exc:
243
+ return AuditReport(
244
+ "blocked",
245
+ [{
246
+ "surface": None,
247
+ "klass": "audit",
248
+ "code": "diff_unavailable",
249
+ "reason": (
250
+ f"git diff failed (base=`{resolved}`, context={context}): {exc} — the audit "
251
+ f"cannot evaluate freshness, failing closed (never a silent pass)"
252
+ ),
253
+ }],
254
+ evidence={"base": resolved, "context": context},
255
+ )
256
+ report = evaluate(changed, load_decisions(repo, decisions_path))
257
+ report.evidence = {"base": resolved, "context": context, "changed_count": len(changed)}
258
+ return report
259
+
260
+
261
+ def main(argv: list[str] | None = None) -> int:
262
+ import argparse
263
+
264
+ parser = argparse.ArgumentParser(prog="phase-loop docs-audit")
265
+ parser.add_argument("--base", help="Diff base ref (auto-resolved from CI env if omitted).")
266
+ parser.add_argument("--repo", default=".", help="Repository root (default: cwd).")
267
+ parser.add_argument("--decisions", help=f"Decision artifact path (default: {DEFAULT_DECISIONS_PATH}).")
268
+ parser.add_argument("--json", action="store_true", help="Emit the report as JSON.")
269
+ # tolerate the leading 'docs-audit' token when dispatched from the main CLI
270
+ args = parser.parse_args([a for a in (argv or []) if a != "docs-audit"])
271
+
272
+ report = run_audit(Path(args.repo), args.base, args.decisions)
273
+ if args.json:
274
+ print(json.dumps(report.to_json(), indent=2))
275
+ else:
276
+ print(f"docs_freshness: {report.docs_freshness}")
277
+ for f in report.findings:
278
+ print(f" [{f['klass']}] {f.get('surface') or '-'}: {f['reason']}")
279
+ if report.docs_freshness == "blocked":
280
+ print(
281
+ "\nRemediation: update the required doc surface(s), or record a doc decision in "
282
+ f"{args.decisions or DEFAULT_DECISIONS_PATH} (release-class needs a real, relevant "
283
+ "doc change — a token does not satisfy it)."
284
+ )
285
+ return report.exit_code