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,298 @@
1
+ """CS-2.1 SA — fleet-metric ledger emission + pure derivation.
2
+
3
+ This is the enforcement-side PUSH source for the three LEDGER-FAITHFUL named
4
+ fleet-trend metrics the CS-2.1 spine reserved (velocity / burn_down /
5
+ promise_broken_duration). It is additive observability on the harness's OWN
6
+ ledger:
7
+
8
+ * Events are appended to a SEPARATE sibling ledger,
9
+ ``.phase-loop/fleet-metrics.jsonl`` — ``events.jsonl`` is never touched, so
10
+ its bytes, digests, and reconcile/fold semantics are provably unaffected.
11
+ * Every append is a single ``os.write`` on an ``O_APPEND`` fd (mirrors
12
+ ``events._append_jsonl`` and ``train_ledger.append_record``).
13
+ * Derivation is pure (no I/O) so the velocity/burn_down/promise-broken-duration
14
+ series are unit-testable without a runner or a database.
15
+
16
+ The load-bearing boundary: this file lives on the ENFORCEMENT side of the
17
+ ``.phase-loop`` wall. Portal never reads it. Only the sanitized aggregate
18
+ series (see ``fleet_metrics_export``) crosses to Portal, one-way.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+ from typing import Any, Iterable
28
+
29
+ from .models import FleetMetricEvent
30
+ from .runtime_paths import (
31
+ ensure_phase_loop_excluded,
32
+ phase_loop_fleet_metrics_file,
33
+ phase_loop_fleet_metrics_read_files,
34
+ )
35
+
36
+
37
+ # ── append + read (the sibling ledger) ─────────────────────────────────────
38
+
39
+ def fleet_metrics_path(repo: Path) -> Path:
40
+ return phase_loop_fleet_metrics_file(repo)
41
+
42
+
43
+ def append_fleet_metric(repo: Path, event: FleetMetricEvent) -> None:
44
+ """Atomically append one fleet-metric event to the sibling ledger."""
45
+ ensure_phase_loop_excluded(repo)
46
+ path = fleet_metrics_path(repo)
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ encoded = (json.dumps(event.to_json(), sort_keys=True) + "\n").encode("utf-8")
49
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o666)
50
+ try:
51
+ os.write(fd, encoded)
52
+ finally:
53
+ os.close(fd)
54
+
55
+
56
+ def read_fleet_metrics(repo: Path) -> list[dict[str, Any]]:
57
+ """Tolerant reader: skips blank/malformed lines, keeps only fleet_metric records."""
58
+ records: list[dict[str, Any]] = []
59
+ for path in phase_loop_fleet_metrics_read_files(repo):
60
+ if not path.exists():
61
+ continue
62
+ with path.open(encoding="utf-8") as handle:
63
+ for line in handle:
64
+ line = line.strip()
65
+ if not line:
66
+ continue
67
+ try:
68
+ record = json.loads(line)
69
+ except json.JSONDecodeError:
70
+ continue
71
+ if isinstance(record, dict) and record.get("event_kind") == "fleet_metric":
72
+ records.append(record)
73
+ return records
74
+
75
+
76
+ # ── runner hook — emit at closeout ─────────────────────────────────────────
77
+
78
+ def record_phase_fleet_metrics(
79
+ repo: Path,
80
+ *,
81
+ phase: str,
82
+ completed: bool,
83
+ total_scope: int,
84
+ completed_count: int,
85
+ missing_gates: Iterable[str] = (),
86
+ produced_gates: Iterable[str] = (),
87
+ timestamp: str | None = None,
88
+ ) -> list[FleetMetricEvent]:
89
+ """Emit the fleet-metric events implied by one phase closeout.
90
+
91
+ Truth sources (all already known at the closeout site, runner.py):
92
+ * ``completed`` / ``completed_count`` / ``total_scope`` — velocity + burn_down.
93
+ * ``missing_gates`` (declared-but-not-produced, from ``validate_produced_gates``)
94
+ — a BROKEN promise per gate.
95
+ * ``produced_gates`` that had an earlier unmatched ``promise_broken`` — a
96
+ REPAIRED promise (break-anchored, so duration is broken→repaired, never
97
+ mere fulfillment lead-time).
98
+
99
+ Best-effort: callers wrap this so observability can never break the loop.
100
+ """
101
+ ts = timestamp or _utc_now()
102
+ emitted: list[FleetMetricEvent] = []
103
+
104
+ if completed:
105
+ emitted.append(
106
+ FleetMetricEvent(
107
+ metric_kind="velocity",
108
+ timestamp=ts,
109
+ phase=phase,
110
+ payload={"completed_total": int(completed_count)},
111
+ )
112
+ )
113
+ remaining = max(0, int(total_scope) - int(completed_count))
114
+ emitted.append(
115
+ FleetMetricEvent(
116
+ metric_kind="burn_down",
117
+ timestamp=ts,
118
+ phase=phase,
119
+ payload={
120
+ "total_scope": int(total_scope),
121
+ "completed": int(completed_count),
122
+ "remaining": remaining,
123
+ },
124
+ )
125
+ )
126
+
127
+ # Which gates are currently open breaks (declared-but-not-produced without a
128
+ # later repair) — computed from the ledger so repairs are break-anchored.
129
+ open_breaks = _open_promise_breaks(read_fleet_metrics(repo))
130
+ missing = [str(g) for g in missing_gates if str(g).strip()]
131
+ produced = {str(g) for g in produced_gates if str(g).strip()}
132
+
133
+ for gate in missing:
134
+ if gate in open_breaks:
135
+ continue # already an open break — don't double-count
136
+ emitted.append(
137
+ FleetMetricEvent(
138
+ metric_kind="promise_broken",
139
+ timestamp=ts,
140
+ phase=phase,
141
+ payload={"gate": gate},
142
+ )
143
+ )
144
+ open_breaks[gate] = ts
145
+
146
+ for gate in sorted(produced):
147
+ if gate in open_breaks:
148
+ emitted.append(
149
+ FleetMetricEvent(
150
+ metric_kind="promise_repaired",
151
+ timestamp=ts,
152
+ phase=phase,
153
+ payload={"gate": gate},
154
+ )
155
+ )
156
+ open_breaks.pop(gate, None)
157
+
158
+ for event in emitted:
159
+ append_fleet_metric(repo, event)
160
+ return emitted
161
+
162
+
163
+ def _open_promise_breaks(records: Iterable[dict[str, Any]]) -> dict[str, str]:
164
+ """Fold the ledger to gates with an open break (break with no later repair).
165
+
166
+ Returns ``{gate: broken_at_ts}``. Records are in append order.
167
+ """
168
+ open_at: dict[str, str] = {}
169
+ for record in records:
170
+ kind = record.get("metric_kind")
171
+ gate = str((record.get("payload") or {}).get("gate") or "").strip()
172
+ if not gate:
173
+ continue
174
+ if kind == "promise_broken":
175
+ open_at.setdefault(gate, str(record.get("timestamp") or ""))
176
+ elif kind == "promise_repaired":
177
+ open_at.pop(gate, None)
178
+ return open_at
179
+
180
+
181
+ # ── pure derivation — the sanitizable aggregate series ─────────────────────
182
+
183
+ def derive_fleet_metric_series(
184
+ records: Iterable[dict[str, Any]],
185
+ *,
186
+ now: str | None = None,
187
+ ) -> dict[str, Any]:
188
+ """Fold fleet-metric records into ledger-faithful aggregate time-series.
189
+
190
+ Pure: no I/O. Output contains ONLY numeric aggregates + ISO timestamps +
191
+ fixed enum labels — no gate identifiers, paths, secrets, or payloads. This
192
+ is exactly what the export bridge is allowed to push to Portal.
193
+ """
194
+ records = list(records)
195
+ now_ts = now or _utc_now()
196
+
197
+ velocity_points: list[dict[str, Any]] = []
198
+ burndown_points: list[dict[str, Any]] = []
199
+ # Break/repair pairing keyed by gate (gate id stays LOCAL — only durations leave).
200
+ break_open: dict[str, str] = {}
201
+ repaired_durations: list[float] = []
202
+ open_break_started: dict[str, str] = {}
203
+
204
+ for record in records:
205
+ kind = record.get("metric_kind")
206
+ ts = str(record.get("timestamp") or "")
207
+ payload = record.get("payload") or {}
208
+ if kind == "velocity":
209
+ velocity_points.append(
210
+ {"t": ts, "completed_total": int(payload.get("completed_total") or 0)}
211
+ )
212
+ elif kind == "burn_down":
213
+ burndown_points.append(
214
+ {
215
+ "t": ts,
216
+ "total": int(payload.get("total_scope") or 0),
217
+ "completed": int(payload.get("completed") or 0),
218
+ "remaining": int(payload.get("remaining") or 0),
219
+ }
220
+ )
221
+ elif kind == "promise_broken":
222
+ gate = str(payload.get("gate") or "")
223
+ if gate:
224
+ break_open.setdefault(gate, ts)
225
+ open_break_started.setdefault(gate, ts)
226
+ elif kind == "promise_repaired":
227
+ gate = str(payload.get("gate") or "")
228
+ started = break_open.pop(gate, None)
229
+ open_break_started.pop(gate, None)
230
+ if started:
231
+ repaired_durations.append(_seconds_between(started, ts))
232
+
233
+ # promise_broken_duration series: one point per broken promise carrying only
234
+ # its duration (seconds) + whether it was repaired — never the gate id.
235
+ broken_points: list[dict[str, Any]] = []
236
+ # Re-walk to preserve order + emit repaired ones with their measured duration.
237
+ replay_open: dict[str, str] = {}
238
+ for record in records:
239
+ kind = record.get("metric_kind")
240
+ ts = str(record.get("timestamp") or "")
241
+ gate = str((record.get("payload") or {}).get("gate") or "")
242
+ if kind == "promise_broken" and gate:
243
+ replay_open.setdefault(gate, ts)
244
+ elif kind == "promise_repaired" and gate and gate in replay_open:
245
+ started = replay_open.pop(gate)
246
+ broken_points.append(
247
+ {"t": ts, "broken_seconds": _seconds_between(started, ts), "repaired": True}
248
+ )
249
+ # Still-open breaks: duration measured to `now`.
250
+ for gate, started in sorted(open_break_started.items()):
251
+ broken_points.append(
252
+ {"t": now_ts, "broken_seconds": _seconds_between(started, now_ts), "repaired": False}
253
+ )
254
+
255
+ open_count = len(open_break_started)
256
+ open_seconds = [
257
+ _seconds_between(started, now_ts) for started in open_break_started.values()
258
+ ]
259
+ mean_repaired = (
260
+ round(sum(repaired_durations) / len(repaired_durations), 3)
261
+ if repaired_durations
262
+ else None
263
+ )
264
+
265
+ return {
266
+ "velocity": {"points": velocity_points},
267
+ "burn_down": {"points": burndown_points},
268
+ "promise_broken_duration": {
269
+ "points": broken_points,
270
+ "aggregate": {
271
+ "open_count": open_count,
272
+ "max_open_seconds": round(max(open_seconds), 3) if open_seconds else 0,
273
+ "mean_repaired_seconds": mean_repaired,
274
+ "repaired_count": len(repaired_durations),
275
+ },
276
+ },
277
+ }
278
+
279
+
280
+ def _seconds_between(start_ts: str, end_ts: str) -> float:
281
+ try:
282
+ start = _parse_ts(start_ts)
283
+ end = _parse_ts(end_ts)
284
+ except ValueError:
285
+ return 0.0
286
+ return max(0.0, round((end - start).total_seconds(), 3))
287
+
288
+
289
+ def _parse_ts(value: str) -> datetime:
290
+ text = value.strip().replace("Z", "+00:00")
291
+ parsed = datetime.fromisoformat(text)
292
+ if parsed.tzinfo is None:
293
+ parsed = parsed.replace(tzinfo=timezone.utc)
294
+ return parsed
295
+
296
+
297
+ def _utc_now() -> str:
298
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
@@ -0,0 +1,196 @@
1
+ """CS-2.1 SA — the one-way SANITIZED export bridge (enforcement → Portal).
2
+
3
+ This is the PUSH side of the ``.phase-loop`` wall. Enforcement computes the
4
+ ledger-faithful aggregate series (``fleet_metrics.derive_fleet_metric_series``)
5
+ and pushes ONLY sanitized aggregates + timestamps to Portal's control-plane
6
+ ingest. Portal never reaches back into ``.phase-loop`` — the direction is
7
+ strictly enforcement → Portal.
8
+
9
+ Sanitization is PROVABLE, not incidental: :func:`build_sanitized_export` runs
10
+ every payload through :func:`assert_sanitized`, which fails closed if any leaf
11
+ value looks like a filesystem path, a ``.phase-loop`` / ``events.jsonl``
12
+ reference, a secret, or a raw session payload. The named metrics only ever carry
13
+ numbers, ISO-8601 timestamps, and a fixed set of enum labels.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import re
20
+ import urllib.request
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from .fleet_metrics import derive_fleet_metric_series, read_fleet_metrics
25
+
26
+
27
+ EXPORT_SCHEMA = "fleet_metrics_export.v1"
28
+
29
+ # The complete set of NAMED ledger-faithful series this bridge is allowed to
30
+ # push. Matches the series_kinds the CS-2.1 spine reserved for `ledger-faithful`.
31
+ NAMED_SERIES = ("velocity", "burn_down", "promise_broken_duration")
32
+
33
+ # A repo id is a control-plane LABEL, never a path — enforce a slug shape.
34
+ _REPO_SLUG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
35
+
36
+ # ISO-8601 UTC timestamps are the only strings allowed as leaf *values* (besides
37
+ # the fixed enum labels), so a stray path can never masquerade as a timestamp.
38
+ _ISO_TS_RE = re.compile(
39
+ r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$"
40
+ )
41
+
42
+ # Substrings that must NEVER appear anywhere in the payload — the wall markers
43
+ # plus common secret/path tells. A hit is a hard sanitization failure.
44
+ _FORBIDDEN_SUBSTRINGS = (
45
+ "/",
46
+ "\\",
47
+ ".phase-loop",
48
+ "events.jsonl",
49
+ "state.json",
50
+ "secret",
51
+ "token",
52
+ "api_key",
53
+ "authorization",
54
+ "password",
55
+ "session",
56
+ "prompt",
57
+ )
58
+
59
+ # Keys allowed to appear in the payload tree. Anything else (e.g. a raw "path",
60
+ # "gate", "payload", "command" field) is a sanitization failure — this is what
61
+ # stops raw ledger internals from leaking.
62
+ _ALLOWED_KEYS = frozenset(
63
+ {
64
+ "schema",
65
+ "repo",
66
+ "captured_at",
67
+ "series",
68
+ "series_kind",
69
+ "provenance",
70
+ "points",
71
+ "aggregate",
72
+ # numeric/label leaves inside points + aggregate:
73
+ "t",
74
+ "completed_total",
75
+ "total",
76
+ "completed",
77
+ "remaining",
78
+ "broken_seconds",
79
+ "repaired",
80
+ "open_count",
81
+ "max_open_seconds",
82
+ "mean_repaired_seconds",
83
+ "repaired_count",
84
+ }
85
+ )
86
+
87
+
88
+ class SanitizationError(ValueError):
89
+ """Raised when an export payload carries a forbidden path/secret/payload field."""
90
+
91
+
92
+ def build_export_payload(repo_id: str, series: dict[str, Any], captured_at: str) -> dict[str, Any]:
93
+ """Shape the derived series into the ``fleet_metrics_export.v1`` wire payload.
94
+
95
+ Every pushed series is ``provenance: ledger-faithful`` — the DB CHECK on the
96
+ Portal side binds these three series_kinds to exactly that provenance.
97
+ """
98
+ if not _REPO_SLUG_RE.match(repo_id):
99
+ raise SanitizationError(f"repo id is not a control-plane slug: {repo_id!r}")
100
+ return {
101
+ "schema": EXPORT_SCHEMA,
102
+ "repo": repo_id,
103
+ "captured_at": captured_at,
104
+ "series": [
105
+ {
106
+ "series_kind": name,
107
+ "provenance": "ledger-faithful",
108
+ **series.get(name, {}),
109
+ }
110
+ for name in NAMED_SERIES
111
+ ],
112
+ }
113
+
114
+
115
+ def build_sanitized_export(repo: Path, *, repo_id: str, captured_at: str) -> dict[str, Any]:
116
+ """Read the enforcement ledger, derive the series, and return a proven-sanitized payload."""
117
+ series = derive_fleet_metric_series(read_fleet_metrics(repo), now=captured_at)
118
+ payload = build_export_payload(repo_id, series, captured_at)
119
+ assert_sanitized(payload)
120
+ return payload
121
+
122
+
123
+ def assert_sanitized(payload: Any, *, _path: str = "$") -> None:
124
+ """Fail closed unless every node is a numeric aggregate, ISO timestamp, or fixed label.
125
+
126
+ Walks the whole tree:
127
+ * dict keys must be in ``_ALLOWED_KEYS``;
128
+ * every string value must be an allowed enum label or a strict ISO-8601
129
+ timestamp — and must contain no forbidden substring;
130
+ * numbers/bools/None pass; nested lists/dicts recurse.
131
+ """
132
+ if isinstance(payload, dict):
133
+ for key, value in payload.items():
134
+ if key not in _ALLOWED_KEYS:
135
+ raise SanitizationError(f"forbidden key at {_path}.{key}")
136
+ assert_sanitized(value, _path=f"{_path}.{key}")
137
+ return
138
+ if isinstance(payload, (list, tuple)):
139
+ for i, item in enumerate(payload):
140
+ assert_sanitized(item, _path=f"{_path}[{i}]")
141
+ return
142
+ if isinstance(payload, bool) or payload is None:
143
+ return
144
+ if isinstance(payload, (int, float)):
145
+ return
146
+ if isinstance(payload, str):
147
+ lowered = payload.lower()
148
+ for bad in _FORBIDDEN_SUBSTRINGS:
149
+ if bad in lowered:
150
+ raise SanitizationError(f"forbidden substring {bad!r} at {_path}: {payload!r}")
151
+ if payload in _ALLOWED_LABELS or _ISO_TS_RE.match(payload):
152
+ return
153
+ # The one free-form string is the control-plane repo LABEL, allowed only
154
+ # under the `$.repo` key and only if it is a slug (no path/secret shape).
155
+ if _path == "$.repo" and _REPO_SLUG_RE.match(payload):
156
+ return
157
+ raise SanitizationError(f"non-timestamp, non-label string at {_path}: {payload!r}")
158
+ raise SanitizationError(f"unsupported leaf type {type(payload).__name__} at {_path}")
159
+
160
+
161
+ # Fixed enum labels permitted as string leaf VALUES (schema/provenance/kind/repo).
162
+ # Note: repo ids are validated separately by shape; the wall markers can never be
163
+ # a repo id because a slug forbids "/".
164
+ _ALLOWED_LABELS = frozenset({EXPORT_SCHEMA, "ledger-faithful", *NAMED_SERIES})
165
+
166
+
167
+ def push_export(
168
+ url: str,
169
+ payload: dict[str, Any],
170
+ *,
171
+ ingest_key: str,
172
+ timeout: float = 15.0,
173
+ ) -> int:
174
+ """One-way HTTP POST of a proven-sanitized payload to Portal's ingest route.
175
+
176
+ Fails closed on missing url/key. Re-asserts sanitization immediately before
177
+ the wire write so nothing unsanitized can ever leave, even if a caller built
178
+ the payload by hand. Returns the HTTP status code.
179
+ """
180
+ if not url:
181
+ raise ValueError("fleet-metrics export url is not configured")
182
+ if not ingest_key:
183
+ raise ValueError("fleet-metrics export ingest key is not configured")
184
+ assert_sanitized(payload)
185
+ body = json.dumps(payload, sort_keys=True).encode("utf-8")
186
+ request = urllib.request.Request(
187
+ url,
188
+ data=body,
189
+ method="POST",
190
+ headers={
191
+ "content-type": "application/json",
192
+ "x-ledger-export-key": ingest_key,
193
+ },
194
+ )
195
+ with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 (trusted control-plane url)
196
+ return int(response.status)