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,468 @@
1
+ """Docs-freshness closeout gate (issue #18).
2
+
3
+ A phase-loop roadmap can close **green** — clean tree, pushed ``main``, release
4
+ workflow passed — while its public docs are stale or absent. The existing
5
+ ``doc_delta_validator`` cannot catch this: it is *diff-keyed* (it fires only
6
+ when a public surface is already in ``changed_paths``), so files that *should*
7
+ have changed but didn't are doubly invisible. And under the default
8
+ ``PHASE_LOOP_REVIEW=warn`` every registry finding is forced to ``warn`` and
9
+ never blocks (see ``closeout_validators``).
10
+
11
+ This module is the *path-keyed* complement: it enumerates public-doc surfaces
12
+ **from the filesystem** (and ``.claude/docs-catalog.json`` when present), scans
13
+ their *contents* for stale/placeholder tokens, and — for **release/package
14
+ phases only** — blocks ``complete`` independent of ``PHASE_LOOP_REVIEW``,
15
+ mirroring ``_apply_verification_evidence_gate`` (a hard, non-registry gate
16
+ gated on its own ``PHASE_LOOP_DOCS_FRESHNESS`` knob, default ``hard``).
17
+
18
+ Design seam (research open-question 1, option a): validators stay pure. The
19
+ **runner pre-scans** the repo via :func:`scan_docs_freshness` and threads the
20
+ resulting dict into ``build_phase_loop_closeout``; the gate consumes the dict
21
+ and never touches the filesystem from inside the closeout-validator contract.
22
+
23
+ Default-safe posture:
24
+
25
+ * Ordinary (non-release) phases are **unaffected** — the scan reports
26
+ ``skipped`` and never blocks, preserving autonomy-first elsewhere.
27
+ * No pre-scan data (unwired call site) → ``skipped``, never blocked.
28
+ * False-positive control: only *unambiguous* placeholder tokens block; fuzzy
29
+ signals (stale package-count claims, unlabeled versions) are warn-tier. An
30
+ explicit ``<!-- freshness-ok -->`` marker on a line suppresses that line.
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import json
35
+ import os
36
+ from dataclasses import dataclass, field, replace
37
+ from fnmatch import fnmatchcase
38
+ from pathlib import Path
39
+ from typing import Any, Iterable, Mapping
40
+
41
+ # Enforcement knob — like PHASE_LOOP_VERIFY_ENFORCE, this gate has its OWN
42
+ # control so the hard block is independent of the global PHASE_LOOP_REVIEW
43
+ # warn-forcing default. "hard" (default) blocks release phases; "warn" records
44
+ # only; "off" disables the scan entirely.
45
+ DOCS_FRESHNESS_ENV = "PHASE_LOOP_DOCS_FRESHNESS"
46
+ DOCS_FRESHNESS_MODES: tuple[str, ...] = ("off", "warn", "hard")
47
+ DEFAULT_DOCS_FRESHNESS_MODE = "hard"
48
+
49
+ # An inline marker that suppresses freshness flagging for the line it appears on
50
+ # (operator escape hatch for legitimate uses of an otherwise-suspicious token).
51
+ FRESHNESS_OK_MARKER = "freshness-ok"
52
+
53
+ # Public-doc surfaces to enumerate from the filesystem. Kept aligned with
54
+ # models.PUBLIC_SURFACE_GLOBS' doc subset but extended to package-level READMEs
55
+ # and release notes — the exact surfaces issue #18 found stale.
56
+ PUBLIC_DOC_GLOBS: tuple[str, ...] = (
57
+ "README.md",
58
+ "**/README.md",
59
+ "CHANGELOG.md",
60
+ "CHANGELOG*",
61
+ "**/CHANGELOG.md",
62
+ "RELEASE_NOTES*",
63
+ "RELEASES*",
64
+ "docs/RELEASE*.md",
65
+ )
66
+
67
+ # Directories never worth scanning (vendored / generated / VCS).
68
+ _SKIP_DIRS: frozenset[str] = frozenset(
69
+ {".git", "node_modules", "vendor", "dist", "build", "__pycache__", ".venv", "venv"}
70
+ )
71
+
72
+ # Unambiguous placeholders — a release doc carrying one of these (outside an
73
+ # inline-code span) is stale by construction. Kept DELIBERATELY TIGHT: a hard
74
+ # gate that fires on a token that legitimately lives in shipped docs would block
75
+ # a real release fleet-wide (worse than the bug for normal operation). The
76
+ # issue's smoking gun was `recovery commit pending`. TODO/FIXME/XXX are common
77
+ # enough in real prose docs to be WARN-tier, not block.
78
+ BLOCK_TOKENS: tuple[str, ...] = (
79
+ "recovery commit pending",
80
+ "commit pending",
81
+ "TBD",
82
+ "<placeholder>",
83
+ )
84
+ # `pending` alone is too common in prose; only the qualified forms above block.
85
+ # Bare-word block tokens that need word-boundary care are handled in _line_hits.
86
+ _BARE_BLOCK_WORDS: tuple[str, ...] = ("TBD",)
87
+
88
+ # Fuzzy / advisory signals — likely-stale but false-positive-prone. WARN only.
89
+ WARN_TOKENS: tuple[str, ...] = (
90
+ "publishes three",
91
+ "publishes two",
92
+ "is a skeleton",
93
+ "still a skeleton",
94
+ "TODO",
95
+ "FIXME",
96
+ "XXX",
97
+ )
98
+ # Warn-tier bare words needing word-boundary care (so "TODO" ≠ "METHODOLOGY").
99
+ _BARE_WARN_WORDS: tuple[str, ...] = ("TODO", "FIXME", "XXX")
100
+
101
+ # Release-artifact globs — if a phase's changed paths touch any of these, it is
102
+ # release/package-shaped. Used as the heuristic fallback when the plan carries
103
+ # no explicit release declaration.
104
+ RELEASE_ARTIFACT_GLOBS: tuple[str, ...] = (
105
+ "**/package.json",
106
+ "package.json",
107
+ "CHANGELOG.md",
108
+ "**/CHANGELOG.md",
109
+ "RELEASE_NOTES*",
110
+ "**/Cargo.toml",
111
+ "**/pyproject.toml",
112
+ "**/setup.cfg",
113
+ "**/version.txt",
114
+ "**/VERSION",
115
+ "**/__version__.py",
116
+ ".github/workflows/release*.yml",
117
+ ".github/workflows/release*.yaml",
118
+ ".github/workflows/publish*.yml",
119
+ )
120
+
121
+
122
+ def resolve_docs_freshness_mode(env: Mapping[str, str] | None = None) -> str:
123
+ env = os.environ if env is None else env
124
+ value = str(env.get(DOCS_FRESHNESS_ENV) or "").strip().lower()
125
+ return value if value in DOCS_FRESHNESS_MODES else DEFAULT_DOCS_FRESHNESS_MODE
126
+
127
+
128
+ def docs_freshness_evidence_backed(detail: Mapping[str, Any] | None) -> bool:
129
+ """True iff a ``passed`` freshness claim is *provable* from the scan evidence (F5).
130
+
131
+ A ``passed`` status is only trustworthy when the scan actually ran and
132
+ enumerated a per-surface evidence list — i.e. ``surfaces_scanned`` is
133
+ non-empty — and recorded no blocking hit. A bare/empty detail (no surfaces
134
+ enumerated) must NOT read as a corroborated pass: there is nothing to back
135
+ the claim. ``skipped``/``blocked`` are never "evidence-backed passes".
136
+
137
+ Used in two places (single source of truth): :func:`scan_docs_freshness`
138
+ (so it cannot *emit* ``passed`` without enumerated-surface evidence) and
139
+ ``doc_delta_validator`` (to corroborate a self-attested ``no_doc_delta``).
140
+ """
141
+ if not detail:
142
+ return False
143
+ d = dict(detail)
144
+ if str(d.get("status") or "") != "passed":
145
+ return False
146
+ if d.get("blocking_hits"):
147
+ return False
148
+ return bool(d.get("surfaces_scanned"))
149
+
150
+
151
+ def _glob_match(path: str, patterns: Iterable[str]) -> bool:
152
+ path = path.strip()
153
+ if not path:
154
+ return False
155
+ for pattern in patterns:
156
+ if fnmatchcase(path, pattern):
157
+ return True
158
+ if pattern.startswith("**/") and fnmatchcase(path, pattern[3:]):
159
+ return True
160
+ return False
161
+
162
+
163
+ def is_explicit_release_phase(
164
+ plan_frontmatter: Mapping[str, Any] | None = None,
165
+ ) -> bool:
166
+ """True only when the plan *explicitly* declares itself a release phase.
167
+
168
+ This is the **authoritative** release marker — ``phase_loop_mutation:
169
+ release_dispatch`` (emitted by the plan-phase skill) or a release-shaped
170
+ ``phase_type``. It is the ONLY signal eligible to drive the **hard block**
171
+ in :func:`scan_docs_freshness`: hard-blocking an ordinary phase fleet-wide
172
+ on a stale token in any public doc is a worse failure than the bug it
173
+ catches, so the hard gate must be opt-in via frontmatter.
174
+
175
+ The artifact-glob heuristic (see :func:`is_release_phase`) deliberately
176
+ does NOT satisfy this predicate — it only governs whether the scan runs and
177
+ can emit warn-tier evidence.
178
+ """
179
+ if not plan_frontmatter:
180
+ return False
181
+ mutation = str(plan_frontmatter.get("phase_loop_mutation") or "").strip().lower()
182
+ if mutation == "release_dispatch":
183
+ return True
184
+ ptype = str(plan_frontmatter.get("phase_type") or "").strip().lower()
185
+ return ptype in ("release", "package", "roadmap_completion", "roadmap-completion")
186
+
187
+
188
+ def is_release_phase(
189
+ *,
190
+ plan_frontmatter: Mapping[str, Any] | None = None,
191
+ changed_paths: Iterable[str] = (),
192
+ ) -> bool:
193
+ """True if this phase is release/package/roadmap-completion shaped.
194
+
195
+ Layered, default-safe signal used to decide **whether the freshness scan
196
+ runs at all** (not whether it may hard-block — see
197
+ :func:`is_explicit_release_phase` for that):
198
+
199
+ 1. Explicit: the plan frontmatter declares ``phase_loop_mutation:
200
+ release_dispatch`` (the authoritative release marker the plan-phase skill
201
+ already emits).
202
+ 2. Heuristic fallback: the phase's changed paths touch a release artifact
203
+ (manifests / version files / changelog / release workflows). Keying
204
+ release *detection* off changed paths is sound — release phases do bump
205
+ versions — and is distinct from keying *freshness* off changed paths
206
+ (which this module deliberately does NOT do).
207
+
208
+ No signal → not a release phase → the gate is inert. A heuristic-only match
209
+ (no explicit frontmatter) lets the scan run and record **warn-tier**
210
+ evidence, but can NEVER produce a ``blocked`` status.
211
+ """
212
+ if is_explicit_release_phase(plan_frontmatter):
213
+ return True
214
+ return _glob_match_any(changed_paths, RELEASE_ARTIFACT_GLOBS)
215
+
216
+
217
+ def _glob_match_any(paths: Iterable[str], patterns: tuple[str, ...]) -> bool:
218
+ return any(_glob_match(str(p), patterns) for p in paths)
219
+
220
+
221
+ def parse_plan_frontmatter(plan_path: str | Path | None) -> dict[str, str]:
222
+ if not plan_path:
223
+ return {}
224
+ try:
225
+ text = Path(plan_path).read_text(encoding="utf-8")
226
+ except OSError:
227
+ return {}
228
+ if not text.startswith("---\n"):
229
+ return {}
230
+ end = text.find("\n---", 4)
231
+ if end == -1:
232
+ return {}
233
+ data: dict[str, str] = {}
234
+ for line in text[4:end].splitlines():
235
+ if ":" not in line:
236
+ continue
237
+ key, value = line.split(":", 1)
238
+ data[key.strip()] = value.strip().strip("'\"")
239
+ return data
240
+
241
+
242
+ def _catalog_surfaces(repo: Path) -> list[str]:
243
+ """Repo-relative doc paths declared in ``.claude/docs-catalog.json`` (if any).
244
+
245
+ The catalog is a list of objects (``{"path": ...}``) or bare strings. We are
246
+ permissive: anything that yields a path string is included. Absent/malformed
247
+ catalog → empty list (filesystem glob enumeration is the fallback).
248
+ """
249
+ catalog = repo / ".claude" / "docs-catalog.json"
250
+ try:
251
+ data = json.loads(catalog.read_text(encoding="utf-8"))
252
+ except (OSError, ValueError):
253
+ return []
254
+ surfaces: list[str] = []
255
+ items = data if isinstance(data, list) else data.get("docs", []) if isinstance(data, Mapping) else []
256
+ for item in items or []:
257
+ if isinstance(item, str):
258
+ surfaces.append(item)
259
+ elif isinstance(item, Mapping):
260
+ path = item.get("path") or item.get("file")
261
+ if isinstance(path, str):
262
+ surfaces.append(path)
263
+ return surfaces
264
+
265
+
266
+ def enumerate_public_docs(repo: Path) -> list[str]:
267
+ """Path-keyed enumeration of public-doc surfaces (repo-relative, sorted).
268
+
269
+ Filesystem walk matched against :data:`PUBLIC_DOC_GLOBS`, unioned with any
270
+ surfaces declared in ``.claude/docs-catalog.json``. NOT keyed off
271
+ ``changed_paths`` — that is the entire point (issue #18's blind spot).
272
+ """
273
+ found: set[str] = set()
274
+ if repo.is_dir():
275
+ for dirpath, dirnames, filenames in os.walk(repo):
276
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
277
+ rel_dir = Path(dirpath).relative_to(repo)
278
+ for name in filenames:
279
+ rel = (rel_dir / name).as_posix()
280
+ if _glob_match(rel, PUBLIC_DOC_GLOBS):
281
+ found.add(rel)
282
+ for rel in _catalog_surfaces(repo):
283
+ candidate = repo / rel
284
+ if candidate.is_file():
285
+ found.add(Path(rel).as_posix())
286
+ return sorted(found)
287
+
288
+
289
+ @dataclass(frozen=True)
290
+ class DocsFreshnessHit:
291
+ path: str
292
+ line: int
293
+ token: str
294
+ severity: str # "block" | "warn"
295
+ excerpt: str
296
+
297
+ def to_json(self) -> dict[str, Any]:
298
+ return {
299
+ "path": self.path,
300
+ "line": self.line,
301
+ "token": self.token,
302
+ "severity": self.severity,
303
+ "excerpt": self.excerpt,
304
+ }
305
+
306
+
307
+ def _strip_inline_code(line: str) -> str:
308
+ """Blank out inline-code spans (`...`) so a doc that *documents* a stale
309
+ token (always backticked) is not flagged as itself being stale. Replaces
310
+ span contents with spaces to preserve column alignment cheaply."""
311
+ out: list[str] = []
312
+ in_code = False
313
+ for ch in line:
314
+ if ch == "`":
315
+ in_code = not in_code
316
+ out.append(" ")
317
+ else:
318
+ out.append(" " if in_code else ch)
319
+ return "".join(out)
320
+
321
+
322
+ def _line_hits(line: str) -> list[tuple[str, str]]:
323
+ """Return (token, severity) hits in a single line. Empty if suppressed."""
324
+ if FRESHNESS_OK_MARKER in line:
325
+ return []
326
+ # Tokens inside inline-code spans are documentation, not stale content.
327
+ line = _strip_inline_code(line)
328
+ hits: list[tuple[str, str]] = []
329
+ lowered = line.lower()
330
+ block_matched = False
331
+ for token in BLOCK_TOKENS:
332
+ if token in _BARE_BLOCK_WORDS:
333
+ # Bare words: require them to stand as a token, not a substring
334
+ # (avoid e.g. "TBD" inside a longer identifier). Word-boundary test.
335
+ if _word_present(line, token):
336
+ hits.append((token, "block"))
337
+ block_matched = True
338
+ elif token.lower() in lowered:
339
+ hits.append((token, "block"))
340
+ block_matched = True
341
+ # Dedupe block substrings (e.g. "recovery commit pending" also contains
342
+ # "commit pending") — one block hit per line is enough to gate.
343
+ if block_matched:
344
+ hits = [h for h in hits if h[1] == "block"][:1]
345
+ return hits
346
+ for token in WARN_TOKENS:
347
+ if token in _BARE_WARN_WORDS:
348
+ if _word_present(line, token):
349
+ hits.append((token, "warn"))
350
+ elif token.lower() in lowered:
351
+ hits.append((token, "warn"))
352
+ return hits
353
+
354
+
355
+ def _word_present(line: str, word: str) -> bool:
356
+ import re
357
+
358
+ return re.search(rf"(?<![A-Za-z0-9_]){re.escape(word)}(?![A-Za-z0-9_])", line) is not None
359
+
360
+
361
+ def scan_docs_freshness(
362
+ repo: str | Path | None,
363
+ *,
364
+ plan_path: str | Path | None = None,
365
+ changed_paths: Iterable[str] = (),
366
+ plan_frontmatter: Mapping[str, Any] | None = None,
367
+ env: Mapping[str, str] | None = None,
368
+ ) -> dict[str, Any]:
369
+ """Runner-side pre-scan. Pure of closeout state; returns a JSON-able dict.
370
+
371
+ Result shape (threaded into ``build_phase_loop_closeout`` as
372
+ ``docs_freshness=...`` and surfaced in the closeout payload):
373
+
374
+ {
375
+ "status": "passed" | "skipped" | "blocked",
376
+ "mode": "off" | "warn" | "hard",
377
+ "is_release_phase": bool,
378
+ "surfaces_scanned": [<repo-relative paths>],
379
+ "hits": [DocsFreshnessHit, ...],
380
+ "blocking_hits": [<subset that are severity=block>],
381
+ }
382
+
383
+ ``status``:
384
+ * ``skipped`` — not a release phase, or mode ``off``, or no repo.
385
+ * ``blocked`` — release phase, mode ``hard``, and ≥1 block-severity hit.
386
+ * ``passed`` — release phase, scanned, no blocking hit (warn hits may
387
+ still be recorded as evidence).
388
+ """
389
+ mode = resolve_docs_freshness_mode(env)
390
+ fm = dict(plan_frontmatter or {})
391
+ if not fm and plan_path:
392
+ fm = parse_plan_frontmatter(plan_path)
393
+ explicit = is_explicit_release_phase(fm)
394
+ release = is_release_phase(plan_frontmatter=fm, changed_paths=changed_paths)
395
+
396
+ base: dict[str, Any] = {
397
+ "status": "skipped",
398
+ "mode": mode,
399
+ "is_release_phase": release,
400
+ "explicit_release": explicit,
401
+ "surfaces_scanned": [],
402
+ "hits": [],
403
+ "blocking_hits": [],
404
+ # F5: `skipped` is never an evidence-backed pass — keep the flag present
405
+ # so every result has a consistent shape for downstream corroboration.
406
+ "evidence_backed": False,
407
+ }
408
+ if mode == "off" or not release or not repo:
409
+ return base
410
+ repo_path = Path(repo)
411
+ if not repo_path.is_dir():
412
+ return base
413
+
414
+ surfaces = enumerate_public_docs(repo_path)
415
+ hits: list[DocsFreshnessHit] = []
416
+ for rel in surfaces:
417
+ try:
418
+ text = (repo_path / rel).read_text(encoding="utf-8", errors="replace")
419
+ except OSError:
420
+ continue
421
+ for lineno, line in enumerate(text.splitlines(), start=1):
422
+ for token, severity in _line_hits(line):
423
+ hits.append(
424
+ DocsFreshnessHit(
425
+ path=rel,
426
+ line=lineno,
427
+ token=token,
428
+ severity=severity,
429
+ excerpt=line.strip()[:200],
430
+ )
431
+ )
432
+ # The HARD block is opt-in via explicit release frontmatter. A heuristic-only
433
+ # release phase (artifact-glob match, no frontmatter) scans and records
434
+ # evidence, but block-severity hits are downgraded to warn so an ordinary
435
+ # changelog/dep bump can NEVER hard-block the fleet.
436
+ if not explicit:
437
+ hits = [
438
+ h if h.severity == "warn" else replace(h, severity="warn")
439
+ for h in hits
440
+ ]
441
+ blocking = [h for h in hits if h.severity == "block"]
442
+ # In "warn" mode the gate records but never blocks. Only explicit-release
443
+ # phases in "hard" mode with a block-severity hit are blocked.
444
+ if blocking and mode == "hard" and explicit:
445
+ status = "blocked"
446
+ elif not surfaces:
447
+ # F5: a `passed` claim must be PROVABLE from enumerated-surface evidence.
448
+ # A release phase whose scan ran but found NO public-doc surfaces has
449
+ # nothing to back a pass — record it as `skipped` (no assertion made)
450
+ # rather than letting a bare/empty detail read as `passed`. The honest
451
+ # outcome here is "could not verify", which `skipped` already denotes.
452
+ status = "skipped"
453
+ else:
454
+ status = "passed"
455
+ result = {
456
+ "status": status,
457
+ "mode": mode,
458
+ "is_release_phase": release,
459
+ "explicit_release": explicit,
460
+ "surfaces_scanned": surfaces,
461
+ "hits": [h.to_json() for h in hits],
462
+ "blocking_hits": [h.to_json() for h in blocking],
463
+ }
464
+ # F5: thread the corroboration verdict alongside the raw status so a
465
+ # downstream consumer (and doc_delta corroboration) never has to re-derive
466
+ # whether a `passed` is backed by enumerated-surface evidence.
467
+ result["evidence_backed"] = docs_freshness_evidence_backed(result)
468
+ return result
@@ -0,0 +1,150 @@
1
+ """Public/release surface taxonomy for the standalone `docs-audit` backstop.
2
+
3
+ This is the **audit's own** surface map — deliberately standalone (stdlib only, no
4
+ intra-package imports). It does NOT re-export through `models.py` / `release_guard.py`:
5
+ the decision-panel consensus kept the shipped controls untouched (unifying the taxonomy
6
+ there regressed `release_guard`/`doc_delta_validator`). A single canonical taxonomy is a
7
+ later, separately-tested PR; for now a third copy here is the accepted trade — drift in
8
+ new, additive code beats regressing shipped, released controls.
9
+
10
+ Two tiers:
11
+ - RELEASE_AFFECTING_PATTERNS — release-class (version/manifest/install-posture/
12
+ release-workflow). A change here is held to the strict, relevance-bound
13
+ decision contract.
14
+ - GENERAL_PUBLIC_GLOBS — general user-visible surfaces. A change here must carry
15
+ at least a *recorded* decision (closing the silent-absence leak).
16
+
17
+ `RELEASE_DOC_REQUIREMENTS` is the relevance binding (anti-rubber-stamp): a changed
18
+ release surface is only satisfied when its *required* doc surface(s) also changed,
19
+ so a README whitespace edit cannot stand in for a missing CHANGELOG entry.
20
+
21
+ This module depends on stdlib only (no intra-package imports) to stay cycle-free.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import fnmatch
26
+ from typing import Iterable
27
+
28
+ # Release-class surfaces: a change here affects what/how the project ships.
29
+ RELEASE_AFFECTING_PATTERNS = (
30
+ ".github/workflows/**",
31
+ "CHANGELOG*",
32
+ "RELEASE*",
33
+ "VERSION",
34
+ "pyproject.toml",
35
+ "setup.cfg",
36
+ "setup.py",
37
+ "uv.lock",
38
+ "poetry.lock",
39
+ "requirements*.txt",
40
+ "docs/release/**",
41
+ "docs/releases/**",
42
+ "docs/release*.md",
43
+ "scripts/*release*",
44
+ "scripts/download-release.py",
45
+ )
46
+
47
+ # General public surfaces (user-visible; docs may track). Mirrors the historical
48
+ # models.PUBLIC_SURFACE_GLOBS set.
49
+ GENERAL_PUBLIC_GLOBS = (
50
+ "**/cli.py",
51
+ "**/*.proto",
52
+ "**/openapi*.json",
53
+ "**/openapi*.yaml",
54
+ "**/*.openapi.*",
55
+ "**/schema*.json",
56
+ "README.md",
57
+ "CHANGELOG.md",
58
+ "**/_contract_docs/**",
59
+ )
60
+
61
+ # Durable doc surfaces — what a "doc change" can be.
62
+ DOC_SURFACE_GLOBS = (
63
+ "README.md",
64
+ "**/README.md",
65
+ "CHANGELOG.md",
66
+ "**/CHANGELOG*",
67
+ "RELEASE*",
68
+ "docs/**",
69
+ "**/_contract_docs/**",
70
+ )
71
+
72
+ # Relevance binding (IF-0-P1-1): a changed release surface requires its mapped doc
73
+ # surface(s) to ALSO have changed. (release-surface pattern -> required doc patterns)
74
+ RELEASE_DOC_REQUIREMENTS = (
75
+ ("VERSION", ("CHANGELOG*",)),
76
+ ("pyproject.toml", ("CHANGELOG*",)),
77
+ ("setup.cfg", ("CHANGELOG*",)),
78
+ ("setup.py", ("CHANGELOG*",)),
79
+ ("uv.lock", ("CHANGELOG*",)),
80
+ ("poetry.lock", ("CHANGELOG*",)),
81
+ ("requirements*.txt", ("CHANGELOG*",)),
82
+ (".github/workflows/**", ("CHANGELOG*", "docs/release*")),
83
+ ("scripts/*release*", ("CHANGELOG*", "docs/release*")),
84
+ )
85
+
86
+
87
+ def _match(path: str, pattern: str) -> bool:
88
+ """fnmatch with the historical `**/` prefix special-case (matches at root too)."""
89
+ p = path.replace("\\", "/")
90
+ if fnmatch.fnmatchcase(p, pattern):
91
+ return True
92
+ if pattern.startswith("**/") and fnmatch.fnmatchcase(p, pattern[3:]):
93
+ return True
94
+ return False
95
+
96
+
97
+ def _any(path: str, patterns: Iterable[str]) -> bool:
98
+ return any(_match(path, pat) for pat in patterns)
99
+
100
+
101
+ def is_release_surface(path: str) -> bool:
102
+ return _any(path, RELEASE_AFFECTING_PATTERNS)
103
+
104
+
105
+ def is_general_public_surface(path: str) -> bool:
106
+ return _any(path, GENERAL_PUBLIC_GLOBS)
107
+
108
+
109
+ def is_doc_surface(path: str) -> bool:
110
+ return _any(path, DOC_SURFACE_GLOBS)
111
+
112
+
113
+ def classify_surface(path: str) -> str | None:
114
+ """`"release"` | `"general"` | None. Release-class wins (it's the stricter tier)."""
115
+ if is_release_surface(path):
116
+ return "release"
117
+ if is_general_public_surface(path):
118
+ return "general"
119
+ return None
120
+
121
+
122
+ def required_docs_for(path: str) -> tuple[str, ...]:
123
+ """The doc surface pattern(s) a changed release surface must be matched by."""
124
+ reqs: list[str] = []
125
+ for pat, docs in RELEASE_DOC_REQUIREMENTS:
126
+ if _match(path, pat):
127
+ reqs.extend(docs)
128
+ return tuple(dict.fromkeys(reqs))
129
+
130
+
131
+ #: Doc-delta validator finding codes that mean docs-freshness is unsatisfied.
132
+ DOC_FRESHNESS_FINDING_CODES: tuple[str, ...] = ("doc_delta_undecided", "release_doc_missing")
133
+
134
+
135
+ def docs_freshness_verdict(
136
+ changed_paths: Iterable[str],
137
+ finding_codes: Iterable[str] = (),
138
+ ) -> str:
139
+ """`"passed" | "skipped" | "blocked"` — the shared Layer-A/Layer-B verdict.
140
+
141
+ ``skipped`` when no public surface changed; ``blocked`` when a doc-freshness
142
+ finding fired; ``passed`` when a public surface changed and was satisfied.
143
+ """
144
+ public = any(classify_surface(p) is not None for p in changed_paths)
145
+ if not public:
146
+ return "skipped"
147
+ codes = set(finding_codes)
148
+ if any(code in codes for code in DOC_FRESHNESS_FINDING_CODES):
149
+ return "blocked"
150
+ return "passed"