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.
- phase_loop_runtime/__init__.py +20 -0
- phase_loop_runtime/_contract_docs/phase-loop/extraction-readiness.md +90 -0
- phase_loop_runtime/_contract_docs/phase-loop/granular-execution-policy.md +137 -0
- phase_loop_runtime/_contract_docs/phase-loop/harness-capability-matrix.md +510 -0
- phase_loop_runtime/_contract_docs/phase-loop/harness-skill-matrix.md +125 -0
- phase_loop_runtime/_contract_docs/phase-loop/harness-substrate-manifest.md +232 -0
- phase_loop_runtime/_contract_docs/phase-loop/pi-loop-control.md +247 -0
- phase_loop_runtime/_contract_docs/phase-loop/protocol.md +2759 -0
- phase_loop_runtime/_contract_docs/phase-loop/runtime-boundary.md +289 -0
- phase_loop_runtime/_contract_docs/phase-loop/spec-discovery-roots.md +47 -0
- phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-governed-pipeline.md +42 -0
- phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-portal-projection.md +34 -0
- phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-regenesis.md +29 -0
- phase_loop_runtime/_contract_docs/phase-loop/substrate-soak-report.md +32 -0
- phase_loop_runtime/_contract_docs/runtime/verification-evidence-contract.md +52 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/commands.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-mixed-signals/mixed.json +5 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-provider-timeout/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-provider-timeout/timeout-note.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-redacted-summary/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-redacted-summary/redacted-summary.json +6 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-small-sample/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-small-sample/sample.json +7 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-threshold-edge/edge.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/borderline/borderline-threshold-edge/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-a.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-b.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-duplicate-png/shot-c.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-missing-references/claimed-results.json +5 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-missing-references/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-subtle-low-variance/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-subtle-low-variance/metrics.json +9 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-templated-prose/findings.md +10 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-templated-prose/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/scores.json +4 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-fake/fake-uniform-scores/summary.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/bundle-check.json +8 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-adoption-bundle/operator-note.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/events-sample.jsonl +3 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-handoff-ledger/receipt.txt +1 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/evidence.json +13 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-runner-diff/summary.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/matrix.json +9 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-test-matrix/notes.md +4 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/console-summary.txt +2 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/manifest.json +16 -0
- phase_loop_runtime/_test_fixtures/evidence-audit-calibration/known-real/real-ui-screenshot-standin/screenshot-standin.ppm +5 -0
- phase_loop_runtime/adoption_bundle.py +273 -0
- phase_loop_runtime/advisor_board/CONTRACTS.md +257 -0
- phase_loop_runtime/advisor_board/__init__.py +288 -0
- phase_loop_runtime/advisor_board/backing.py +137 -0
- phase_loop_runtime/advisor_board/backing_omnigent.py +498 -0
- phase_loop_runtime/advisor_board/config.py +225 -0
- phase_loop_runtime/advisor_board/events.py +115 -0
- phase_loop_runtime/advisor_board/fixtures/advisor-boards.example.toml +59 -0
- phase_loop_runtime/advisor_board/fixtures.py +91 -0
- phase_loop_runtime/advisor_board/harness_mapping.py +125 -0
- phase_loop_runtime/advisor_board/matrix.py +146 -0
- phase_loop_runtime/advisor_board/observability.py +481 -0
- phase_loop_runtime/advisor_board/presets.py +188 -0
- phase_loop_runtime/advisor_board/registries.py +309 -0
- phase_loop_runtime/advisor_board/resolver.py +253 -0
- phase_loop_runtime/advisor_board/schema.py +269 -0
- phase_loop_runtime/advisor_board/standin.py +166 -0
- phase_loop_runtime/advisor_board/validation.py +159 -0
- phase_loop_runtime/agent_runtime_provider.py +676 -0
- phase_loop_runtime/baml_modular.py +656 -0
- phase_loop_runtime/baml_src/dotfiles_adoption_manifest.baml +37 -0
- phase_loop_runtime/baml_src/dotfiles_c4_document.baml +14 -0
- phase_loop_runtime/baml_src/dotfiles_plan_manifest.baml +50 -0
- phase_loop_runtime/baml_src/dotfiles_runtime_projection.baml +16 -0
- phase_loop_runtime/baml_src/dotfiles_task_catalog.baml +20 -0
- phase_loop_runtime/baml_src/emit_phase_closeout.baml +86 -0
- phase_loop_runtime/baml_src/evaluate_suspected_fake_evidence.baml +36 -0
- phase_loop_runtime/baml_src/verification_evidence.baml +40 -0
- phase_loop_runtime/broker.py +181 -0
- phase_loop_runtime/build_bundle.py +294 -0
- phase_loop_runtime/capability_registry.py +835 -0
- phase_loop_runtime/classifier.py +87 -0
- phase_loop_runtime/claude_agent_view.py +625 -0
- phase_loop_runtime/claude_channel_sidecar.py +1086 -0
- phase_loop_runtime/cli.py +2599 -0
- phase_loop_runtime/closeout.py +865 -0
- phase_loop_runtime/closeout_classifier.py +120 -0
- phase_loop_runtime/closeout_evidence_audit.py +112 -0
- phase_loop_runtime/closeout_validation.py +144 -0
- phase_loop_runtime/closeout_validators.py +203 -0
- phase_loop_runtime/consiliency_gates.py +374 -0
- phase_loop_runtime/consiliency_ingest.py +399 -0
- phase_loop_runtime/consiliency_layout.py +176 -0
- phase_loop_runtime/consiliency_scaffold.py +305 -0
- phase_loop_runtime/cross_repo_channel.py +331 -0
- phase_loop_runtime/discovery.py +2368 -0
- phase_loop_runtime/dispatch_lock.py +167 -0
- phase_loop_runtime/doc_delta_validator.py +98 -0
- phase_loop_runtime/docs_audit.py +285 -0
- phase_loop_runtime/docs_freshness.py +468 -0
- phase_loop_runtime/docs_surfaces.py +150 -0
- phase_loop_runtime/dotfiles_profile_plugin.py +96 -0
- phase_loop_runtime/events.py +154 -0
- phase_loop_runtime/events_migration.py +96 -0
- phase_loop_runtime/evidence_audit.py +1082 -0
- phase_loop_runtime/evidence_audit_config.py +172 -0
- phase_loop_runtime/fleet_map.py +426 -0
- phase_loop_runtime/fleet_metrics.py +298 -0
- phase_loop_runtime/fleet_metrics_export.py +196 -0
- phase_loop_runtime/git_discipline.py +479 -0
- phase_loop_runtime/git_ops.py +122 -0
- phase_loop_runtime/git_topology.py +252 -0
- phase_loop_runtime/governed_bundle.py +137 -0
- phase_loop_runtime/governed_premerge.py +219 -0
- phase_loop_runtime/governed_review.py +264 -0
- phase_loop_runtime/handoff.py +932 -0
- phase_loop_runtime/handoff_path.py +14 -0
- phase_loop_runtime/injection.py +740 -0
- phase_loop_runtime/install_status.py +88 -0
- phase_loop_runtime/lane_scheduler.py +317 -0
- phase_loop_runtime/launcher.py +2760 -0
- phase_loop_runtime/lease_store.py +553 -0
- phase_loop_runtime/maintenance.py +401 -0
- phase_loop_runtime/migrate_handoffs.py +201 -0
- phase_loop_runtime/models.py +2144 -0
- phase_loop_runtime/observability.py +1290 -0
- phase_loop_runtime/panel_invoker.py +1638 -0
- phase_loop_runtime/phase_loop_drift_audit.py +388 -0
- phase_loop_runtime/phase_worktree_executor.py +433 -0
- phase_loop_runtime/pipeline_adapter/__init__.py +12 -0
- phase_loop_runtime/pipeline_adapter/branch_ops.py +274 -0
- phase_loop_runtime/pipeline_adapter/flag.py +55 -0
- phase_loop_runtime/pipeline_adapter/markers.py +13 -0
- phase_loop_runtime/pipeline_adapter/merge_policy.py +72 -0
- phase_loop_runtime/pipeline_adapter/ratification.py +100 -0
- phase_loop_runtime/pipeline_adapter/sibling_matcher.py +123 -0
- phase_loop_runtime/plan_ir.py +523 -0
- phase_loop_runtime/plan_manifest.py +415 -0
- phase_loop_runtime/planner_validation.py +176 -0
- phase_loop_runtime/profiles.py +462 -0
- phase_loop_runtime/prompts.py +365 -0
- phase_loop_runtime/provenance.py +165 -0
- phase_loop_runtime/publishing.py +339 -0
- phase_loop_runtime/py.typed +1 -0
- phase_loop_runtime/reconcile.py +1550 -0
- phase_loop_runtime/redaction.py +184 -0
- phase_loop_runtime/reflection_sync.py +293 -0
- phase_loop_runtime/release_guard.py +186 -0
- phase_loop_runtime/render.py +432 -0
- phase_loop_runtime/repo_validation.py +427 -0
- phase_loop_runtime/review_summary.py +150 -0
- phase_loop_runtime/roadmap_lint.py +425 -0
- phase_loop_runtime/route_log.py +59 -0
- phase_loop_runtime/route_policy/__init__.py +11 -0
- phase_loop_runtime/route_policy/fixtures/route_selection.golden.json +263 -0
- phase_loop_runtime/runner.py +8400 -0
- phase_loop_runtime/runtime_paths.py +225 -0
- phase_loop_runtime/runtime_projection.py +113 -0
- phase_loop_runtime/runtime_resources.py +36 -0
- phase_loop_runtime/schema_export.py +292 -0
- phase_loop_runtime/schemas/phase_loop_closeout.schema.json +366 -0
- phase_loop_runtime/skill_install.py +176 -0
- phase_loop_runtime/skill_inventory.py +662 -0
- phase_loop_runtime/skill_paths.py +42 -0
- phase_loop_runtime/skill_sources_plugin.py +37 -0
- phase_loop_runtime/skills_bundle/claude-advisor-board/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-advisor-board/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/claude-advisor-panel/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-advisor-panel/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/claude-execute-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-execute-detailed/SKILL.md +152 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/SKILL.md +745 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/allocate_worktree_name.sh +33 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/audit_lane_file_touches.py +197 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/state.py +274 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/team_teardown.sh +32 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/validate_plan_doc.py +642 -0
- phase_loop_runtime/skills_bundle/claude-execute-phase/scripts/verify_harness.sh +96 -0
- phase_loop_runtime/skills_bundle/claude-phase-loop/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-phase-loop/SKILL.md +115 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/SKILL.md +439 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/assets/review_prompt.md +19 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/references/roadmap-template.md +193 -0
- phase_loop_runtime/skills_bundle/claude-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
- phase_loop_runtime/skills_bundle/claude-plan-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-plan-detailed/SKILL.md +334 -0
- phase_loop_runtime/skills_bundle/claude-plan-detailed/assets/review_prompt.md +25 -0
- phase_loop_runtime/skills_bundle/claude-plan-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-plan-phase/SKILL.md +776 -0
- phase_loop_runtime/skills_bundle/claude-plan-phase/assets/review_prompt.md +17 -0
- phase_loop_runtime/skills_bundle/claude-plan-phase/scripts/validate_plan_doc.py +1092 -0
- phase_loop_runtime/skills_bundle/claude-run-train/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-run-train/SKILL.md +75 -0
- phase_loop_runtime/skills_bundle/claude-skill-editor/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-skill-editor/SKILL.md +243 -0
- phase_loop_runtime/skills_bundle/claude-skill-editor/assets/editor_prompt.md +72 -0
- phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/SKILL.md +249 -0
- phase_loop_runtime/skills_bundle/claude-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
- phase_loop_runtime/skills_bundle/claude-task-contextualizer/README.md +1 -0
- phase_loop_runtime/skills_bundle/claude-task-contextualizer/SKILL.md +74 -0
- phase_loop_runtime/skills_bundle/claude-task-contextualizer/references/task-templates.md +103 -0
- phase_loop_runtime/skills_bundle/codex-advisor-board/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-advisor-board/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/codex-advisor-panel/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-advisor-panel/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/codex-execute-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-execute-detailed/SKILL.md +154 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/SKILL.md +231 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/allocate_worktree_name.sh +33 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/audit_lane_file_touches.py +197 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/state.py +274 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/team_teardown.sh +32 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/validate_plan_doc.py +642 -0
- phase_loop_runtime/skills_bundle/codex-execute-phase/scripts/verify_harness.sh +96 -0
- phase_loop_runtime/skills_bundle/codex-phase-loop/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-phase-loop/SKILL.md +248 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/SKILL.md +150 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/assets/review_prompt.md +19 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/references/roadmap-template.md +193 -0
- phase_loop_runtime/skills_bundle/codex-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
- phase_loop_runtime/skills_bundle/codex-plan-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-plan-detailed/SKILL.md +119 -0
- phase_loop_runtime/skills_bundle/codex-plan-detailed/assets/review_prompt.md +25 -0
- phase_loop_runtime/skills_bundle/codex-plan-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-plan-phase/SKILL.md +307 -0
- phase_loop_runtime/skills_bundle/codex-plan-phase/assets/review_prompt.md +17 -0
- phase_loop_runtime/skills_bundle/codex-plan-phase/scripts/validate_plan_doc.py +1092 -0
- phase_loop_runtime/skills_bundle/codex-run-train/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-run-train/SKILL.md +75 -0
- phase_loop_runtime/skills_bundle/codex-skill-editor/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-skill-editor/SKILL.md +94 -0
- phase_loop_runtime/skills_bundle/codex-skill-editor/assets/editor_prompt.md +72 -0
- phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/SKILL.md +114 -0
- phase_loop_runtime/skills_bundle/codex-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
- phase_loop_runtime/skills_bundle/codex-task-contextualizer/README.md +1 -0
- phase_loop_runtime/skills_bundle/codex-task-contextualizer/SKILL.md +102 -0
- phase_loop_runtime/skills_bundle/codex-task-contextualizer/references/task-templates.md +103 -0
- phase_loop_runtime/skills_bundle/gemini-advisor-board/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-advisor-board/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/gemini-advisor-panel/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-advisor-panel/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/gemini-execute-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-execute-detailed/SKILL.md +144 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/SKILL.md +198 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/allocate_worktree_name.sh +33 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/audit_lane_file_touches.py +197 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/state.py +274 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/team_teardown.sh +32 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/validate_plan_doc.py +642 -0
- phase_loop_runtime/skills_bundle/gemini-execute-phase/scripts/verify_harness.sh +96 -0
- phase_loop_runtime/skills_bundle/gemini-phase-loop/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-phase-loop/SKILL.md +111 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/SKILL.md +147 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/assets/review_prompt.md +19 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/references/roadmap-template.md +193 -0
- phase_loop_runtime/skills_bundle/gemini-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
- phase_loop_runtime/skills_bundle/gemini-plan-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-plan-detailed/SKILL.md +119 -0
- phase_loop_runtime/skills_bundle/gemini-plan-detailed/assets/review_prompt.md +25 -0
- phase_loop_runtime/skills_bundle/gemini-plan-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-plan-phase/SKILL.md +217 -0
- phase_loop_runtime/skills_bundle/gemini-plan-phase/assets/review_prompt.md +17 -0
- phase_loop_runtime/skills_bundle/gemini-plan-phase/scripts/validate_plan_doc.py +1092 -0
- phase_loop_runtime/skills_bundle/gemini-run-train/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-run-train/SKILL.md +75 -0
- phase_loop_runtime/skills_bundle/gemini-skill-editor/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-skill-editor/SKILL.md +87 -0
- phase_loop_runtime/skills_bundle/gemini-skill-editor/assets/editor_prompt.md +72 -0
- phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/SKILL.md +109 -0
- phase_loop_runtime/skills_bundle/gemini-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
- phase_loop_runtime/skills_bundle/gemini-task-contextualizer/README.md +1 -0
- phase_loop_runtime/skills_bundle/gemini-task-contextualizer/SKILL.md +98 -0
- phase_loop_runtime/skills_bundle/gemini-task-contextualizer/references/task-templates.md +103 -0
- phase_loop_runtime/skills_bundle/opencode-advisor-board/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-advisor-board/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/opencode-advisor-panel/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-advisor-panel/SKILL.md +38 -0
- phase_loop_runtime/skills_bundle/opencode-execute-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-execute-detailed/SKILL.md +144 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/SKILL.md +196 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/allocate_worktree_name.sh +33 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/audit_lane_file_touches.py +197 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/cleanup_lane_worktrees.sh +103 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/parent_tree_leakage_check.sh +125 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/post_merge_import_smoke.sh +62 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/pre_merge_destructiveness_check.sh +127 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/resolve_branch_from_sha.sh +51 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/state.py +274 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/sweep_stale_worktrees.sh +62 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/team_teardown.sh +32 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/validate_plan_doc.py +642 -0
- phase_loop_runtime/skills_bundle/opencode-execute-phase/scripts/verify_harness.sh +96 -0
- phase_loop_runtime/skills_bundle/opencode-phase-loop/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-phase-loop/SKILL.md +113 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/SKILL.md +147 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/assets/review_prompt.md +19 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/references/parallelization-heuristics.md +111 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/references/roadmap-template.md +193 -0
- phase_loop_runtime/skills_bundle/opencode-phase-roadmap-builder/scripts/validate_roadmap.py +32 -0
- phase_loop_runtime/skills_bundle/opencode-plan-detailed/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-plan-detailed/SKILL.md +119 -0
- phase_loop_runtime/skills_bundle/opencode-plan-detailed/assets/review_prompt.md +25 -0
- phase_loop_runtime/skills_bundle/opencode-plan-phase/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-plan-phase/SKILL.md +211 -0
- phase_loop_runtime/skills_bundle/opencode-plan-phase/assets/review_prompt.md +17 -0
- phase_loop_runtime/skills_bundle/opencode-plan-phase/scripts/validate_plan_doc.py +1092 -0
- phase_loop_runtime/skills_bundle/opencode-run-train/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-run-train/SKILL.md +75 -0
- phase_loop_runtime/skills_bundle/opencode-skill-editor/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-skill-editor/SKILL.md +87 -0
- phase_loop_runtime/skills_bundle/opencode-skill-editor/assets/editor_prompt.md +72 -0
- phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/SKILL.md +109 -0
- phase_loop_runtime/skills_bundle/opencode-skill-improvement-planner/assets/aggregator_prompt.md +79 -0
- phase_loop_runtime/skills_bundle/opencode-task-contextualizer/README.md +1 -0
- phase_loop_runtime/skills_bundle/opencode-task-contextualizer/SKILL.md +98 -0
- phase_loop_runtime/skills_bundle/opencode-task-contextualizer/references/task-templates.md +103 -0
- phase_loop_runtime/state.py +156 -0
- phase_loop_runtime/state_degradation.py +136 -0
- phase_loop_runtime/state_ops.py +676 -0
- phase_loop_runtime/train_ledger.py +215 -0
- phase_loop_runtime/train_roadmap.py +339 -0
- phase_loop_runtime/train_runner.py +1197 -0
- phase_loop_runtime/verification_evidence.py +650 -0
- phase_loop_runtime/verification_evidence_validator.py +75 -0
- phase_loop_runtime/visual_evidence_validator.py +36 -0
- phase_loop_runtime/worker_pool.py +123 -0
- phase_loop_runtime/worktree_index.py +270 -0
- phase_loop_runtime-0.2.0.data/data/share/phase-loop-runtime/protocol/protocol.md +2475 -0
- phase_loop_runtime-0.2.0.dist-info/METADATA +106 -0
- phase_loop_runtime-0.2.0.dist-info/RECORD +370 -0
- phase_loop_runtime-0.2.0.dist-info/WHEEL +5 -0
- phase_loop_runtime-0.2.0.dist-info/entry_points.txt +9 -0
- 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
|