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,865 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ from fnmatch import fnmatchcase
6
+ from pathlib import Path
7
+ from typing import Any, Mapping
8
+
9
+ from .closeout_validation import verification_enforcement_mode
10
+ from .closeout_validators import (
11
+ CloseoutContext,
12
+ apply_review_findings,
13
+ run_closeout_validators,
14
+ )
15
+ from .models import (
16
+ PIPELINE_CLOSEOUT_OUTCOMES,
17
+ PIPELINE_CLOSEOUT_SCHEMA,
18
+ PIPELINE_PROTECTED_SOURCE_CATEGORIES,
19
+ PIPELINE_PROTECTED_SOURCE_LEGACY_ROLES,
20
+ PIPELINE_PROTECTED_SOURCE_ROLES,
21
+ SPEC_DELTA_CLOSEOUT_SCHEMA,
22
+ SPEC_DELTA_DECISIONS,
23
+ DirtyPathClassification,
24
+ PhaseLoopArtifacts,
25
+ PhaseLoopAutomation,
26
+ PhaseLoopBlocker,
27
+ PhaseLoopCloseout,
28
+ PhaseLoopSourceBundle,
29
+ SpecDeltaCloseout,
30
+ PhaseLoopVerification,
31
+ PhasePlanLane,
32
+ PhaseSourceBundle,
33
+ PipelineMetadataDiagnostic,
34
+ PipelinePlanMetadata,
35
+ SourceTruthImpact,
36
+ WorkUnitCloseout,
37
+ )
38
+ from .redaction import build_source_truth_impact, metadata_redaction_diagnostic
39
+ from .verification_evidence import ARTIFACT_NAME, validate_verification_artifact
40
+
41
+
42
+ def reduce_lane_dirty_paths(
43
+ dirty_paths: tuple[str, ...] | list[str],
44
+ lanes: tuple[PhasePlanLane, ...] | list[PhasePlanLane],
45
+ *,
46
+ active_lane_id: str | None = None,
47
+ pre_existing_paths: tuple[str, ...] | list[str] = (),
48
+ reducer_paths: tuple[str, ...] | list[str] = (),
49
+ ) -> tuple[DirtyPathClassification, ...]:
50
+ pre_existing = set(pre_existing_paths)
51
+ reducers = set(reducer_paths)
52
+ reduced: list[DirtyPathClassification] = []
53
+ for path in dirty_paths:
54
+ if path in pre_existing:
55
+ reduced.append(DirtyPathClassification(path=path, classification="pre_existing"))
56
+ continue
57
+ if path in reducers:
58
+ reduced.append(DirtyPathClassification(path=path, classification="reducer_owned"))
59
+ continue
60
+ owner = _owning_lane(path, lanes)
61
+ if owner is None:
62
+ reduced.append(DirtyPathClassification(path=path, classification="unowned"))
63
+ elif active_lane_id is None or owner.lane_id == active_lane_id:
64
+ reduced.append(DirtyPathClassification(path=path, classification="lane_owned", lane_id=owner.lane_id))
65
+ else:
66
+ reduced.append(DirtyPathClassification(path=path, classification="peer_owned", lane_id=owner.lane_id))
67
+ return tuple(reduced)
68
+
69
+
70
+ def _owning_lane(path: str, lanes: tuple[PhasePlanLane, ...] | list[PhasePlanLane]) -> PhasePlanLane | None:
71
+ for lane in lanes:
72
+ if any(_path_matches(path, pattern) for pattern in lane.owned_files):
73
+ return lane
74
+ return None
75
+
76
+
77
+ def _path_matches(path: str, pattern: str) -> bool:
78
+ if path == pattern:
79
+ return True
80
+ if any(token in pattern for token in ("*", "?", "[")):
81
+ return fnmatchcase(path, pattern)
82
+ return path.startswith(pattern.rstrip("/") + "/")
83
+
84
+
85
+ def build_phase_loop_closeout(
86
+ *,
87
+ phase_alias: str,
88
+ plan_path: str | Path,
89
+ source_bundle: PhaseSourceBundle | None = None,
90
+ plan_metadata: PipelinePlanMetadata | None = None,
91
+ pipeline_diagnostic: PipelineMetadataDiagnostic | None = None,
92
+ terminal_summary: Mapping[str, Any] | None = None,
93
+ automation: Mapping[str, Any] | None = None,
94
+ blocker: Mapping[str, Any] | None = None,
95
+ access_attempts: tuple[Mapping[str, Any], ...] | list[Mapping[str, Any]] = (),
96
+ changed_paths: tuple[str, ...] | list[str] = (),
97
+ artifact_paths: Mapping[str, str] | None = None,
98
+ evidence_refs: tuple[Mapping[str, Any], ...] | list[Mapping[str, Any]] = (),
99
+ work_unit_closeout: WorkUnitCloseout | Mapping[str, Any] | None = None,
100
+ spec_delta_closeout: Mapping[str, Any] | SpecDeltaCloseout | None = None,
101
+ run_mode: str = "autonomous",
102
+ docs_freshness: Mapping[str, Any] | None = None,
103
+ consiliency_gates: Mapping[str, Any] | None = None,
104
+ ) -> dict[str, Any]:
105
+ terminal = dict(terminal_summary or {})
106
+ normalized_automation = _automation_fields(dict(automation or {}))
107
+ blocker_data = dict(blocker or terminal.get("terminal_blocker") or {})
108
+ verification_results: list[dict[str, Any]] = []
109
+ agent_reported_verification_status = str(
110
+ terminal.get("verification_status") or normalized_automation.get("verification_status") or "unknown"
111
+ )
112
+ evidence_update = _apply_verification_evidence_gate(
113
+ phase_alias=phase_alias,
114
+ plan_path=Path(plan_path),
115
+ terminal=terminal,
116
+ automation=normalized_automation,
117
+ blocker=blocker_data,
118
+ )
119
+ if evidence_update:
120
+ terminal = evidence_update["terminal"]
121
+ normalized_automation = evidence_update["automation"]
122
+ blocker_data = evidence_update["blocker"]
123
+ verification_results = evidence_update["results"]
124
+
125
+ # Docs-freshness gate (issue #18). A HARD, non-registry gate — like the
126
+ # verification-evidence gate above — so a release/package phase cannot close
127
+ # `complete` while its public docs carry stale placeholders, independent of
128
+ # the warn-forcing `PHASE_LOOP_REVIEW` default. Inert for non-release phases
129
+ # and when no pre-scan was threaded in (status `skipped`).
130
+ docs_freshness_result = _apply_docs_freshness_gate(
131
+ docs_freshness=docs_freshness,
132
+ terminal=terminal,
133
+ automation=normalized_automation,
134
+ blocker=blocker_data,
135
+ )
136
+ if docs_freshness_result:
137
+ terminal = docs_freshness_result["terminal"]
138
+ normalized_automation = docs_freshness_result["automation"]
139
+ blocker_data = docs_freshness_result["blocker"]
140
+ verification_results = list(verification_results) + docs_freshness_result["results"]
141
+
142
+ # CS-0.6 L0 `.consiliency/` gates (presence, local-integrity, layout-validity,
143
+ # version-skew). SOFT by construction: only ``PHASE_LOOP_CONSILIENCY_GATES=hard``
144
+ # (opt-in) turns a finding into a block, and it never sets human_required.
145
+ # A repo with no `.consiliency/manifest` (no consent) or no pre-scan threaded
146
+ # in is inert (`status: "skipped"`).
147
+ consiliency_gates_result = _apply_consiliency_gates_gate(
148
+ consiliency_gates=consiliency_gates,
149
+ terminal=terminal,
150
+ automation=normalized_automation,
151
+ blocker=blocker_data,
152
+ )
153
+ if consiliency_gates_result:
154
+ terminal = consiliency_gates_result["terminal"]
155
+ normalized_automation = consiliency_gates_result["automation"]
156
+ blocker_data = consiliency_gates_result["blocker"]
157
+ verification_results = list(verification_results) + consiliency_gates_result["results"]
158
+
159
+ # Pluggable review gates (rigor-v1 P1). With zero validators registered this
160
+ # is a no-op; gates default to `warn` (record + continue) and never set
161
+ # human_required. See closeout_validators for the severity model.
162
+ review_findings = run_closeout_validators(
163
+ CloseoutContext(
164
+ phase_alias=phase_alias,
165
+ plan_path=str(plan_path),
166
+ terminal=terminal,
167
+ automation=normalized_automation,
168
+ blocker=blocker_data,
169
+ changed_paths=tuple(
170
+ changed_paths
171
+ or terminal.get("dirty_paths")
172
+ or terminal.get("phase_owned_dirty_paths")
173
+ or terminal.get("previous_phase_owned_paths")
174
+ or ()
175
+ ),
176
+ run_mode=run_mode,
177
+ docs_freshness=docs_freshness,
178
+ )
179
+ )
180
+ if review_findings:
181
+ review_update = apply_review_findings(
182
+ findings=review_findings,
183
+ terminal=terminal,
184
+ automation=normalized_automation,
185
+ blocker=blocker_data,
186
+ )
187
+ terminal = review_update["terminal"]
188
+ normalized_automation = review_update["automation"]
189
+ blocker_data = review_update["blocker"]
190
+ verification_results = list(verification_results) + review_update["results"]
191
+ metadata = plan_metadata or (pipeline_diagnostic.metadata if pipeline_diagnostic else None)
192
+
193
+ source_bundle_path = source_bundle.path if source_bundle else (metadata.source_bundle if metadata else None)
194
+ if source_bundle is not None:
195
+ source_bundle_sha256 = source_bundle.sha256
196
+ elif pipeline_diagnostic is not None:
197
+ source_bundle_sha256 = pipeline_diagnostic.actual_sha256 or pipeline_diagnostic.expected_sha256
198
+ else:
199
+ source_bundle_sha256 = metadata.source_bundle_sha256 if metadata else None
200
+ pipeline_phase_id = source_bundle.phase_id if source_bundle else (metadata.pipeline_phase_id if metadata else None)
201
+ plan_path_text = str(plan_path)
202
+
203
+ outcome = _closeout_outcome(
204
+ terminal_summary=terminal,
205
+ automation=normalized_automation,
206
+ blocker=blocker_data,
207
+ pipeline_diagnostic=pipeline_diagnostic,
208
+ work_unit_closeout=work_unit_closeout,
209
+ )
210
+
211
+ # Build nested model
212
+ auto = PhaseLoopAutomation(
213
+ status=str(normalized_automation.get("status") or "unknown"),
214
+ next_skill=normalized_automation.get("next_skill"),
215
+ next_command=normalized_automation.get("next_command"),
216
+ next_model_hint=normalized_automation.get("next_model_hint"),
217
+ next_effort_hint=normalized_automation.get("next_effort_hint"),
218
+ human_required=bool(normalized_automation.get("human_required", False)),
219
+ blocker_class=normalized_automation.get("blocker_class"),
220
+ blocker_summary=normalized_automation.get("blocker_summary"),
221
+ required_human_inputs=tuple(normalized_automation.get("required_human_inputs") or ()),
222
+ verification_status=str(normalized_automation.get("verification_status") or "not_run"),
223
+ artifact=normalized_automation.get("artifact"),
224
+ artifact_state=normalized_automation.get("artifact_state"),
225
+ )
226
+
227
+ arts = PhaseLoopArtifacts(
228
+ plan_path=plan_path_text,
229
+ plan_sha256=_file_sha256(plan_path),
230
+ artifact_paths=dict(artifact_paths or terminal.get("artifact_paths") or {}),
231
+ changed_paths=tuple(_stable_paths(
232
+ changed_paths
233
+ or terminal.get("dirty_paths")
234
+ or terminal.get("phase_owned_dirty_paths")
235
+ or terminal.get("previous_phase_owned_paths")
236
+ or ()
237
+ )),
238
+ evidence_refs=tuple(_evidence_refs(evidence_refs, terminal, work_unit_closeout)),
239
+ )
240
+ source_truth_impact = build_source_truth_impact(
241
+ arts.changed_paths,
242
+ _protected_source_roles(source_bundle),
243
+ )
244
+
245
+ verification = PhaseLoopVerification(
246
+ status=str(terminal.get("verification_status") or normalized_automation.get("verification_status") or "unknown"),
247
+ commands=tuple(_verification_commands(terminal)),
248
+ results=tuple(verification_results),
249
+ agent_reported_verification_status=agent_reported_verification_status,
250
+ )
251
+
252
+ blkr = PhaseLoopBlocker(
253
+ human_required=bool(blocker_data.get("human_required", normalized_automation.get("human_required", False))),
254
+ blocker_class=blocker_data.get("blocker_class") or normalized_automation.get("blocker_class"),
255
+ blocker_summary=blocker_data.get("blocker_summary") or normalized_automation.get("blocker_summary"),
256
+ required_human_inputs=tuple(blocker_data.get("required_human_inputs") or normalized_automation.get("required_human_inputs") or ()),
257
+ access_attempts=tuple(access_attempts or blocker_data.get("access_attempts") or ()),
258
+ )
259
+
260
+ sb = PhaseLoopSourceBundle(
261
+ path=source_bundle_path,
262
+ sha256=source_bundle_sha256,
263
+ phase_id=pipeline_phase_id,
264
+ pipeline_mode=source_bundle.pipeline_mode if source_bundle else ((metadata.pipeline_mode if metadata else None) or "standalone"),
265
+ protected_sources=tuple(source.to_json() for source in source_bundle.protected_sources) if source_bundle else (),
266
+ )
267
+
268
+ closeout = PhaseLoopCloseout(
269
+ phase=phase_alias,
270
+ terminal_status=outcome,
271
+ automation=auto,
272
+ artifacts=arts,
273
+ verification=verification,
274
+ blocker=blkr,
275
+ source_bundle=sb,
276
+ source_truth_impact=source_truth_impact,
277
+ spec_delta_closeout=_spec_delta_closeout(spec_delta_closeout, terminal, normalized_automation),
278
+ )
279
+
280
+ payload = closeout.to_json()
281
+
282
+ # issue #18 behavior #5: surface docs_freshness so a clean worktree alone
283
+ # cannot imply public docs are current.
284
+ if docs_freshness is not None:
285
+ status = str(dict(docs_freshness).get("status") or "skipped")
286
+ payload["docs_freshness"] = status
287
+ payload["docs_freshness_detail"] = dict(docs_freshness)
288
+
289
+ # CS-0.6: surface the `.consiliency/` gate scan the same way, so a closeout
290
+ # record shows adoption posture (or its absence) without re-scanning.
291
+ if consiliency_gates is not None:
292
+ status = str(dict(consiliency_gates).get("status") or "skipped")
293
+ payload["consiliency_gates"] = status
294
+ payload["consiliency_gates_detail"] = dict(consiliency_gates)
295
+
296
+ if source_bundle is not None:
297
+ payload["source_bundle"]["artifact_target_root"] = source_bundle.artifact_target_root
298
+ if pipeline_diagnostic is not None:
299
+ payload["pipeline_execution_preflight"] = {
300
+ "status": "blocked",
301
+ "diagnostic": pipeline_diagnostic.to_json(),
302
+ }
303
+ if work_unit_closeout is not None:
304
+ payload["work_unit"] = _work_unit_fields(work_unit_closeout)
305
+ payload["lane"] = _lane_closeout_fields(work_unit_closeout)
306
+ return _clean(payload)
307
+
308
+
309
+ def _spec_delta_closeout(
310
+ explicit: Mapping[str, Any] | SpecDeltaCloseout | None,
311
+ terminal: Mapping[str, Any],
312
+ automation: Mapping[str, Any],
313
+ ) -> SpecDeltaCloseout | None:
314
+ value = explicit or terminal.get("spec_delta_closeout") or automation.get("spec_delta_closeout")
315
+ if value is None:
316
+ return None
317
+ if isinstance(value, SpecDeltaCloseout):
318
+ return value
319
+ data = dict(value)
320
+ schema = data.get("schema") or SPEC_DELTA_CLOSEOUT_SCHEMA
321
+ if schema != SPEC_DELTA_CLOSEOUT_SCHEMA:
322
+ raise ValueError(f"invalid spec delta closeout schema: {schema}")
323
+ decision = str(data.get("decision") or "")
324
+ if decision not in SPEC_DELTA_DECISIONS:
325
+ raise ValueError(f"invalid spec delta decision: {decision}")
326
+ return SpecDeltaCloseout(
327
+ schema=schema,
328
+ decision=decision,
329
+ target_surfaces=tuple(str(item) for item in data.get("target_surfaces") or ()),
330
+ evidence_paths=tuple(str(item) for item in data.get("evidence_paths") or ()),
331
+ redaction_posture=str(data.get("redaction_posture") or "metadata_only"),
332
+ blocker_class=data.get("blocker_class"),
333
+ )
334
+
335
+
336
+ def _apply_verification_evidence_gate(
337
+ *,
338
+ phase_alias: str,
339
+ plan_path: Path,
340
+ terminal: dict[str, Any],
341
+ automation: dict[str, Any],
342
+ blocker: dict[str, Any],
343
+ ) -> dict[str, Any] | None:
344
+ reported = str(terminal.get("verification_status") or automation.get("verification_status") or "")
345
+ if reported != "passed":
346
+ return None
347
+ artifact_path = _verification_artifact_path(terminal)
348
+ if artifact_path is None:
349
+ if _verification_evidence_required(phase_alias, plan_path):
350
+ validation_payload = {
351
+ "ok": False,
352
+ "code": "missing_verification_artifact",
353
+ "artifact_path": None,
354
+ "log_path": None,
355
+ "exit_summary": {},
356
+ "findings": ["passed closeout requires IF-0-VC-1 verification evidence"],
357
+ "enforcement": verification_enforcement_mode(os.environ),
358
+ }
359
+ return _verification_evidence_block_or_warn(
360
+ validation_payload=validation_payload,
361
+ terminal=terminal,
362
+ automation=automation,
363
+ blocker=blocker,
364
+ )
365
+ return None
366
+ validation = validate_verification_artifact(artifact_path)
367
+ validation_payload = validation.to_json()
368
+ mode = verification_enforcement_mode(os.environ)
369
+ validation_payload["enforcement"] = mode
370
+ return _verification_evidence_block_or_warn(
371
+ validation_payload=validation_payload,
372
+ terminal=terminal,
373
+ automation=automation,
374
+ blocker=blocker,
375
+ )
376
+
377
+
378
+ def _verification_evidence_block_or_warn(
379
+ *,
380
+ validation_payload: dict[str, Any],
381
+ terminal: dict[str, Any],
382
+ automation: dict[str, Any],
383
+ blocker: dict[str, Any],
384
+ ) -> dict[str, Any]:
385
+ updated_terminal = dict(terminal)
386
+ updated_automation = dict(automation)
387
+ updated_blocker = dict(blocker)
388
+ if validation_payload.get("ok"):
389
+ return {
390
+ "terminal": updated_terminal,
391
+ "automation": updated_automation,
392
+ "blocker": updated_blocker,
393
+ "results": [validation_payload],
394
+ }
395
+ mode = str(validation_payload.get("enforcement") or verification_enforcement_mode(os.environ))
396
+ if mode == "warn":
397
+ validation_payload["warning"] = "verification evidence failed validation under PHASE_LOOP_VERIFY_ENFORCE=warn"
398
+ return {
399
+ "terminal": updated_terminal,
400
+ "automation": updated_automation,
401
+ "blocker": updated_blocker,
402
+ "results": [validation_payload],
403
+ }
404
+ updated_terminal["terminal_status"] = "blocked"
405
+ updated_terminal["verification_status"] = "blocked"
406
+ updated_automation["status"] = "blocked"
407
+ updated_automation["verification_status"] = "blocked"
408
+ updated_automation["blocker_class"] = "verification_evidence_missing"
409
+ updated_automation["blocker_summary"] = f"Verification evidence invalid: {validation_payload.get('code')}"
410
+ updated_automation["human_required"] = False
411
+ updated_blocker.update(
412
+ {
413
+ "human_required": False,
414
+ "blocker_class": "verification_evidence_missing",
415
+ "blocker_summary": f"Verification evidence invalid: {validation_payload.get('code')}",
416
+ "required_human_inputs": (),
417
+ }
418
+ )
419
+ return {
420
+ "terminal": updated_terminal,
421
+ "automation": updated_automation,
422
+ "blocker": updated_blocker,
423
+ "results": [validation_payload],
424
+ }
425
+
426
+
427
+ def _apply_docs_freshness_gate(
428
+ *,
429
+ docs_freshness: Mapping[str, Any] | None,
430
+ terminal: dict[str, Any],
431
+ automation: dict[str, Any],
432
+ blocker: dict[str, Any],
433
+ ) -> dict[str, Any] | None:
434
+ """Fold a runner-side docs-freshness pre-scan into the closeout.
435
+
436
+ Hard gate (issue #18), modeled on ``_apply_verification_evidence_gate``: a
437
+ ``blocked`` scan result turns the closeout into a non-human ``blocked``
438
+ outcome independent of ``PHASE_LOOP_REVIEW``. ``skipped``/``passed`` results
439
+ are recorded as evidence but never change the outcome. Returns ``None`` when
440
+ there is nothing to record (no pre-scan threaded in).
441
+ """
442
+ if not docs_freshness:
443
+ return None
444
+ result = dict(docs_freshness)
445
+ status = str(result.get("status") or "skipped")
446
+ updated_terminal = dict(terminal)
447
+ updated_automation = dict(automation)
448
+ updated_blocker = dict(blocker)
449
+ record = {"kind": "docs_freshness", **result}
450
+ if status != "blocked":
451
+ return {
452
+ "terminal": updated_terminal,
453
+ "automation": updated_automation,
454
+ "blocker": updated_blocker,
455
+ "results": [record],
456
+ }
457
+ blocking = result.get("blocking_hits") or []
458
+ paths = sorted({str(h.get("path")) for h in blocking if isinstance(h, Mapping)})
459
+ summary = (
460
+ "Public docs stale: placeholder/stale tokens in release docs "
461
+ f"({', '.join(paths) if paths else 'see docs_freshness_detail'})"
462
+ )
463
+ updated_terminal["terminal_status"] = "blocked"
464
+ updated_terminal["verification_status"] = "blocked"
465
+ updated_automation["status"] = "blocked"
466
+ updated_automation["verification_status"] = "blocked"
467
+ updated_automation["blocker_class"] = "docs_freshness_stale"
468
+ updated_automation["blocker_summary"] = summary
469
+ updated_automation["human_required"] = False
470
+ updated_blocker.update(
471
+ {
472
+ "human_required": False,
473
+ "blocker_class": "docs_freshness_stale",
474
+ "blocker_summary": summary,
475
+ "required_human_inputs": (),
476
+ }
477
+ )
478
+ return {
479
+ "terminal": updated_terminal,
480
+ "automation": updated_automation,
481
+ "blocker": updated_blocker,
482
+ "results": [record],
483
+ }
484
+
485
+
486
+ def _apply_consiliency_gates_gate(
487
+ *,
488
+ consiliency_gates: Mapping[str, Any] | None,
489
+ terminal: dict[str, Any],
490
+ automation: dict[str, Any],
491
+ blocker: dict[str, Any],
492
+ ) -> dict[str, Any] | None:
493
+ """Fold a runner-side CS-0.6 `.consiliency/` gate pre-scan into the closeout.
494
+
495
+ Modeled on :func:`_apply_docs_freshness_gate`, but soft by default: only a
496
+ ``blocked`` result -- which the scan itself only ever emits under the
497
+ opt-in ``PHASE_LOOP_CONSILIENCY_GATES=hard`` -- turns the closeout into a
498
+ non-human ``blocked`` outcome. ``skipped``/``passed``/``warn`` are recorded
499
+ as evidence but never change the outcome. Returns ``None`` when there is
500
+ nothing to record (no pre-scan threaded in).
501
+ """
502
+ if not consiliency_gates:
503
+ return None
504
+ result = dict(consiliency_gates)
505
+ status = str(result.get("status") or "skipped")
506
+ updated_terminal = dict(terminal)
507
+ updated_automation = dict(automation)
508
+ updated_blocker = dict(blocker)
509
+ record = {"kind": "consiliency_gates", **result}
510
+ if status != "blocked":
511
+ return {
512
+ "terminal": updated_terminal,
513
+ "automation": updated_automation,
514
+ "blocker": updated_blocker,
515
+ "results": [record],
516
+ }
517
+ blocking_gates = sorted(
518
+ name for name, gate in dict(result.get("gates") or {}).items() if isinstance(gate, Mapping) and gate.get("status") == "blocked"
519
+ )
520
+ summary = (
521
+ "Consiliency L0 gate(s) blocked under opt-in PHASE_LOOP_CONSILIENCY_GATES=hard: "
522
+ f"({', '.join(blocking_gates) if blocking_gates else 'see consiliency_gates_detail'})"
523
+ )
524
+ updated_terminal["terminal_status"] = "blocked"
525
+ updated_terminal["verification_status"] = "blocked"
526
+ updated_automation["status"] = "blocked"
527
+ updated_automation["verification_status"] = "blocked"
528
+ updated_automation["blocker_class"] = "consiliency_gate_blocked"
529
+ updated_automation["blocker_summary"] = summary
530
+ updated_automation["human_required"] = False
531
+ updated_blocker.update(
532
+ {
533
+ "human_required": False,
534
+ "blocker_class": "consiliency_gate_blocked",
535
+ "blocker_summary": summary,
536
+ "required_human_inputs": (),
537
+ }
538
+ )
539
+ return {
540
+ "terminal": updated_terminal,
541
+ "automation": updated_automation,
542
+ "blocker": updated_blocker,
543
+ "results": [record],
544
+ }
545
+
546
+
547
+ def _verification_evidence_required(phase_alias: str, plan_path: Path) -> bool:
548
+ if phase_alias.upper() == "RG":
549
+ return True
550
+ try:
551
+ text = plan_path.read_text(encoding="utf-8")
552
+ except OSError:
553
+ return False
554
+ return "IF-0-RG-1" in text or "--verification-log" in text
555
+
556
+
557
+ def _verification_artifact_path(terminal: Mapping[str, Any]) -> Path | None:
558
+ artifact_paths = terminal.get("artifact_paths")
559
+ if not isinstance(artifact_paths, Mapping):
560
+ return None
561
+ explicit = artifact_paths.get("verification") or artifact_paths.get("verification_artifact") or artifact_paths.get("verification_log")
562
+ if explicit:
563
+ candidate = Path(str(explicit))
564
+ if candidate.name == "verification.log":
565
+ return candidate.parent / ARTIFACT_NAME
566
+ return candidate
567
+ root = artifact_paths.get("root")
568
+ if root:
569
+ return Path(str(root)) / ARTIFACT_NAME
570
+ return None
571
+
572
+
573
+ def phase_loop_closeout_diagnostic(payload: Mapping[str, Any] | None) -> dict[str, str] | None:
574
+ if not isinstance(payload, Mapping):
575
+ return {"kind": "malformed_closeout", "message": "closeout payload must be an object"}
576
+ if payload.get("schema") != PIPELINE_CLOSEOUT_SCHEMA:
577
+ return {"kind": "malformed_closeout", "message": f"closeout schema must be {PIPELINE_CLOSEOUT_SCHEMA}"}
578
+ deprecated_root_fields = (
579
+ "status",
580
+ "next_skill",
581
+ "next_command",
582
+ "verification_status",
583
+ "artifact",
584
+ "artifact_state",
585
+ )
586
+ for field in deprecated_root_fields:
587
+ if field in payload:
588
+ return {"kind": "malformed_closeout", "message": f"native v1 closeout contains deprecated root field {field}"}
589
+
590
+ # Single source of truth: the export-schema generator owns the canonical
591
+ # required-nested-field set, so the schema artifact and this validator can
592
+ # never drift apart (SCHEMA SL-1 / CR #4).
593
+ from .schema_export import required_closeout_fields
594
+
595
+ for field in required_closeout_fields():
596
+ if field not in payload or payload.get(field) is None:
597
+ return {"kind": "malformed_closeout", "message": f"closeout payload is missing nested field {field}"}
598
+
599
+ if payload.get("terminal_status") not in PIPELINE_CLOSEOUT_OUTCOMES:
600
+ return {"kind": "malformed_closeout", "message": f"invalid terminal status: {payload.get('terminal_status')}"}
601
+ if payload.get("outcome") is not None and payload.get("outcome") not in PIPELINE_CLOSEOUT_OUTCOMES:
602
+ return {"kind": "malformed_closeout", "message": f"invalid closeout outcome: {payload.get('outcome')}"}
603
+ if not isinstance(payload.get("automation"), Mapping):
604
+ return {"kind": "malformed_closeout", "message": "closeout automation must be an object"}
605
+ artifacts = payload.get("artifacts")
606
+ verification = payload.get("verification")
607
+ blocker = payload.get("blocker")
608
+ source_bundle = payload.get("source_bundle")
609
+ if not isinstance(artifacts, Mapping):
610
+ return {"kind": "malformed_closeout", "message": "closeout artifacts must be an object"}
611
+ if not isinstance(verification, Mapping):
612
+ return {"kind": "malformed_closeout", "message": "closeout verification must be an object"}
613
+ if not isinstance(blocker, Mapping):
614
+ return {"kind": "malformed_closeout", "message": "closeout blocker must be an object"}
615
+ if not isinstance(source_bundle, Mapping):
616
+ return {"kind": "malformed_closeout", "message": "closeout source_bundle must be an object"}
617
+ if source_bundle.get("pipeline_mode") not in ("standalone", "pipeline_optional", "pipeline_required"):
618
+ return {"kind": "malformed_closeout", "message": "closeout source_bundle has invalid pipeline_mode"}
619
+ protected_sources = source_bundle.get("protected_sources")
620
+ if protected_sources is not None:
621
+ if not isinstance(protected_sources, list):
622
+ return {"kind": "malformed_closeout", "message": "closeout source_bundle.protected_sources must be a list"}
623
+ for item in protected_sources:
624
+ if not isinstance(item, Mapping):
625
+ return {"kind": "malformed_closeout", "message": "closeout source_bundle.protected_sources entries must be objects"}
626
+ category = item.get("category")
627
+ role = item.get("role")
628
+ if category not in PIPELINE_PROTECTED_SOURCE_CATEGORIES:
629
+ return {"kind": "malformed_closeout", "message": "closeout source_bundle protected source has invalid category"}
630
+ if role is not None and role not in PIPELINE_PROTECTED_SOURCE_ROLES and role not in PIPELINE_PROTECTED_SOURCE_LEGACY_ROLES:
631
+ return {"kind": "malformed_closeout", "message": "closeout source_bundle protected source has invalid role"}
632
+ if source_bundle.get("pipeline_mode") == "pipeline_required":
633
+ for field in ("path", "phase_id"):
634
+ if not source_bundle.get(field):
635
+ return {"kind": "malformed_closeout", "message": f"pipeline_required closeout missing source_bundle.{field}"}
636
+ if not source_bundle.get("sha256"):
637
+ return {"kind": "missing_source_bundle_sha256", "message": "pipeline_required closeout missing source_bundle.sha256"}
638
+ if not artifacts.get("plan_path") or not artifacts.get("plan_sha256"):
639
+ return {"kind": "malformed_closeout", "message": "closeout artifacts must include plan_path and plan_sha256"}
640
+ if not verification.get("status"):
641
+ return {"kind": "malformed_closeout", "message": "closeout verification must include status"}
642
+ impact = payload.get("source_truth_impact")
643
+ if not isinstance(impact, Mapping):
644
+ return {"kind": "malformed_closeout", "message": "closeout source_truth_impact must be an object"}
645
+ impact_diagnostic = _source_truth_impact_diagnostic(impact)
646
+ if impact_diagnostic is not None:
647
+ return impact_diagnostic
648
+ redaction = metadata_redaction_diagnostic(payload)
649
+ if redaction is not None:
650
+ return redaction
651
+ return None
652
+
653
+
654
+ def _automation_fields(raw: dict[str, Any]) -> dict[str, Any]:
655
+ if not raw:
656
+ return {}
657
+ mapping = {
658
+ "automation_status": "status",
659
+ "automation_next_skill": "next_skill",
660
+ "automation_next_command": "next_command",
661
+ "automation_human_required": "human_required",
662
+ "automation_blocker_class": "blocker_class",
663
+ "automation_blocker_summary": "blocker_summary",
664
+ "automation_required_human_inputs": "required_human_inputs",
665
+ "automation_verification_status": "verification_status",
666
+ }
667
+ normalized = {mapping.get(key, key): value for key, value in raw.items()}
668
+ if isinstance(normalized.get("human_required"), str):
669
+ normalized["human_required"] = normalized["human_required"].lower() == "true"
670
+ if normalized.get("blocker_class") in ("", "none"):
671
+ normalized["blocker_class"] = None
672
+ if normalized.get("blocker_summary") == "none":
673
+ normalized["blocker_summary"] = None
674
+ return normalized
675
+
676
+
677
+ def _blocker_fields(blocker: dict[str, Any], automation: dict[str, Any]) -> dict[str, Any]:
678
+ return _clean(
679
+ {
680
+ "human_required": bool(blocker.get("human_required", automation.get("human_required", False))),
681
+ "blocker_class": blocker.get("blocker_class") or automation.get("blocker_class"),
682
+ "blocker_summary": blocker.get("blocker_summary") or automation.get("blocker_summary"),
683
+ "required_human_inputs": blocker.get("required_human_inputs") or automation.get("required_human_inputs") or (),
684
+ }
685
+ )
686
+
687
+
688
+ def _closeout_outcome(
689
+ *,
690
+ terminal_summary: dict[str, Any],
691
+ automation: dict[str, Any],
692
+ blocker: dict[str, Any],
693
+ pipeline_diagnostic: PipelineMetadataDiagnostic | None,
694
+ work_unit_closeout: WorkUnitCloseout | Mapping[str, Any] | None,
695
+ ) -> str:
696
+ if bool(blocker.get("human_required")) or bool(automation.get("human_required")):
697
+ return "human_required"
698
+ if pipeline_diagnostic is not None or _is_stale_blocker(blocker):
699
+ return "stale_input"
700
+ verification = str(terminal_summary.get("verification_status") or automation.get("verification_status") or "")
701
+ blocker_class = str(blocker.get("blocker_class") or automation.get("blocker_class") or "")
702
+ work_status = _work_unit_status(work_unit_closeout)
703
+ if verification == "failed" or blocker_class == "repeated_verification_failure" or work_status == "blocked":
704
+ return "failed_verification"
705
+ if terminal_summary.get("terminal_status") == "complete" and verification == "passed" and not blocker:
706
+ return "complete"
707
+ return "blocked"
708
+
709
+
710
+ def _is_stale_blocker(blocker: dict[str, Any]) -> bool:
711
+ summary = str(blocker.get("blocker_summary") or "")
712
+ return "Pipeline execution freshness validation failed" in summary or "source_bundle" in summary
713
+
714
+
715
+ def _work_unit_status(work_unit_closeout: WorkUnitCloseout | Mapping[str, Any] | None) -> str | None:
716
+ if work_unit_closeout is None:
717
+ return None
718
+ if isinstance(work_unit_closeout, WorkUnitCloseout):
719
+ return work_unit_closeout.status
720
+ return str(work_unit_closeout.get("status")) if work_unit_closeout.get("status") else None
721
+
722
+
723
+ def _work_unit_fields(work_unit_closeout: WorkUnitCloseout | Mapping[str, Any]) -> dict[str, Any]:
724
+ data = work_unit_closeout.to_json() if isinstance(work_unit_closeout, WorkUnitCloseout) else dict(work_unit_closeout)
725
+ identity = data.get("identity") if isinstance(data.get("identity"), dict) else {}
726
+ return _clean(
727
+ {
728
+ "work_unit_id": identity.get("work_unit_id"),
729
+ "phase": identity.get("phase"),
730
+ "kind": identity.get("kind"),
731
+ "lane_id": identity.get("lane_id"),
732
+ "status": data.get("status"),
733
+ }
734
+ )
735
+
736
+
737
+ def _lane_closeout_fields(work_unit_closeout: WorkUnitCloseout | Mapping[str, Any]) -> dict[str, Any]:
738
+ data = work_unit_closeout.to_json() if isinstance(work_unit_closeout, WorkUnitCloseout) else dict(work_unit_closeout)
739
+ identity = data.get("identity") if isinstance(data.get("identity"), dict) else {}
740
+ terminal_summary = data.get("terminal_summary") if isinstance(data.get("terminal_summary"), Mapping) else {}
741
+ closeout_summary = data.get("closeout_summary") if isinstance(data.get("closeout_summary"), Mapping) else {}
742
+ assignment = closeout_summary.get("harness_lane_assignment") if isinstance(closeout_summary.get("harness_lane_assignment"), Mapping) else {}
743
+ worktree_assignment = assignment.get("worktree_assignment") if isinstance(assignment.get("worktree_assignment"), Mapping) else {}
744
+ return _clean(
745
+ {
746
+ "lane_id": identity.get("lane_id"),
747
+ "wave_id": data.get("wave_id") or terminal_summary.get("wave_id"),
748
+ "worktree_path": data.get("worktree_path") or terminal_summary.get("worktree_path"),
749
+ "worktree_isolation_mode": data.get("worktree_isolation_mode") or terminal_summary.get("worktree_isolation_mode") or worktree_assignment.get("isolation_mode"),
750
+ "base_sha": data.get("base_sha") or terminal_summary.get("base_sha") or worktree_assignment.get("base_sha"),
751
+ "harness_route": data.get("harness_route") or terminal_summary.get("harness_route") or assignment.get("harness_route"),
752
+ "work_unit_kind": identity.get("kind"),
753
+ "model": data.get("model") or terminal_summary.get("model") or assignment.get("model"),
754
+ "effort": data.get("effort") or terminal_summary.get("effort") or assignment.get("effort"),
755
+ "policy_source": data.get("policy_source") or terminal_summary.get("policy_source") or _policy_source(assignment),
756
+ "fallback_reason": data.get("fallback_reason") or terminal_summary.get("fallback_reason") or assignment.get("fallback_reason"),
757
+ "verification_status": data.get("verification_status") or terminal_summary.get("verification_status"),
758
+ "changed_paths": data.get("changed_paths") or terminal_summary.get("changed_paths"),
759
+ "evidence_refs": data.get("evidence_refs") or terminal_summary.get("evidence_refs"),
760
+ }
761
+ )
762
+
763
+
764
+ def _policy_source(assignment: Mapping[str, Any]) -> str | None:
765
+ policy = assignment.get("execution_policy") if isinstance(assignment.get("execution_policy"), Mapping) else {}
766
+ return policy.get("execution_policy_source") or policy.get("source")
767
+
768
+
769
+ def _file_sha256(path: str | Path) -> str:
770
+ candidate = Path(path)
771
+ try:
772
+ return hashlib.sha256(candidate.read_bytes()).hexdigest()
773
+ except OSError:
774
+ return hashlib.sha256(str(path).encode("utf-8")).hexdigest()
775
+
776
+
777
+ def _stable_paths(paths: tuple[str, ...] | list[str] | Any) -> list[str]:
778
+ if not isinstance(paths, (tuple, list)):
779
+ return []
780
+ return sorted(dict.fromkeys(str(path) for path in paths))
781
+
782
+
783
+ def _verification_commands(terminal: Mapping[str, Any]) -> list[str]:
784
+ commands = terminal.get("verification_commands") or ()
785
+ if not isinstance(commands, (tuple, list)):
786
+ return []
787
+ reduced: list[str] = []
788
+ for command in commands:
789
+ if isinstance(command, Mapping):
790
+ value = command.get("command")
791
+ else:
792
+ value = command
793
+ if value:
794
+ reduced.append(str(value))
795
+ return reduced
796
+
797
+
798
+ def _evidence_refs(
799
+ explicit_refs: tuple[Mapping[str, Any], ...] | list[Mapping[str, Any]],
800
+ terminal: Mapping[str, Any],
801
+ work_unit_closeout: WorkUnitCloseout | Mapping[str, Any] | None,
802
+ ) -> list[dict[str, Any]]:
803
+ refs: list[dict[str, Any]] = []
804
+ for source in (explicit_refs, terminal.get("evidence_refs") or (), _work_unit_evidence_refs(work_unit_closeout)):
805
+ if not isinstance(source, (tuple, list)):
806
+ continue
807
+ for item in source:
808
+ if isinstance(item, Mapping):
809
+ refs.append(dict(item))
810
+ return refs
811
+
812
+
813
+ def _work_unit_evidence_refs(work_unit_closeout: WorkUnitCloseout | Mapping[str, Any] | None) -> tuple[dict[str, Any], ...]:
814
+ if work_unit_closeout is None:
815
+ return ()
816
+ data = work_unit_closeout.to_json() if isinstance(work_unit_closeout, WorkUnitCloseout) else dict(work_unit_closeout)
817
+ refs = data.get("evidence_refs")
818
+ if not isinstance(refs, (tuple, list)):
819
+ return ()
820
+ return tuple(dict(ref) for ref in refs if isinstance(ref, Mapping))
821
+
822
+
823
+ def _source_truth_impact_diagnostic(impact: Mapping[str, Any]) -> dict[str, str] | None:
824
+ try:
825
+ SourceTruthImpact(
826
+ changed_path_boundaries=tuple(
827
+ dict(boundary)
828
+ for boundary in impact.get("changed_path_boundaries", ())
829
+ if isinstance(boundary, Mapping)
830
+ ),
831
+ canonical_refresh_recommended=bool(impact.get("canonical_refresh_recommended", False)),
832
+ canonical_refresh_reason_codes=tuple(str(reason) for reason in impact.get("canonical_refresh_reason_codes", ())),
833
+ redaction_posture=str(impact.get("redaction_posture") or "metadata_only"),
834
+ )
835
+ except (TypeError, ValueError) as exc:
836
+ return {"kind": "malformed_closeout", "message": f"invalid source_truth_impact: {exc}"}
837
+ return None
838
+
839
+
840
+ def _protected_source_roles(source_bundle: PhaseSourceBundle | None) -> dict[str, str]:
841
+ if source_bundle is None:
842
+ return {}
843
+ roles: dict[str, str] = {}
844
+ for source in source_bundle.protected_sources:
845
+ if source.role:
846
+ role = _classification_role(source.role)
847
+ roles[source.path] = role
848
+ roles[source.path.lower()] = role
849
+ return roles
850
+
851
+
852
+ def _classification_role(role: str) -> str:
853
+ if role in {"legacy_specs_bundle", "adapter_configured_intake_root"}:
854
+ return "unmanaged_spec_input"
855
+ return role
856
+
857
+
858
+ def _clean(value: Any) -> Any:
859
+ if isinstance(value, dict):
860
+ return {key: _clean(item) for key, item in value.items() if item is not None and item != ()}
861
+ if isinstance(value, tuple):
862
+ return [_clean(item) for item in value]
863
+ if isinstance(value, list):
864
+ return [_clean(item) for item in value]
865
+ return value