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,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
PHASE_LOOP_DIRNAME = ".phase-loop"
|
|
9
|
+
LEGACY_PHASE_LOOP_PARENT = ".codex"
|
|
10
|
+
LEGACY_PHASE_LOOP_DIRNAME = "phase-loop"
|
|
11
|
+
EXCLUDE_ENTRIES = (".phase-loop/", ".codex/phase-loop/")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def phase_loop_dir(repo: Path) -> Path:
|
|
15
|
+
return repo / PHASE_LOOP_DIRNAME
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def legacy_phase_loop_dir(repo: Path) -> Path:
|
|
19
|
+
return repo / LEGACY_PHASE_LOOP_PARENT / LEGACY_PHASE_LOOP_DIRNAME
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def phase_loop_read_dir(repo: Path) -> Path:
|
|
23
|
+
canonical = phase_loop_dir(repo)
|
|
24
|
+
if canonical.exists():
|
|
25
|
+
return canonical
|
|
26
|
+
legacy = legacy_phase_loop_dir(repo)
|
|
27
|
+
return legacy if legacy.exists() else canonical
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def phase_loop_read_dirs(repo: Path) -> tuple[Path, ...]:
|
|
31
|
+
canonical = phase_loop_dir(repo)
|
|
32
|
+
legacy = legacy_phase_loop_dir(repo)
|
|
33
|
+
dirs = []
|
|
34
|
+
if canonical.exists():
|
|
35
|
+
dirs.append(canonical)
|
|
36
|
+
if legacy.exists():
|
|
37
|
+
dirs.append(legacy)
|
|
38
|
+
return tuple(dirs or (canonical,))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def phase_loop_runs_dir(repo: Path) -> Path:
|
|
42
|
+
return phase_loop_dir(repo) / "runs"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def lane_worktree_root(repo: Path, *, workspace_mount: Path | None = None) -> Path:
|
|
46
|
+
mount = workspace_mount or Path("/mnt/workspace")
|
|
47
|
+
if mount.exists():
|
|
48
|
+
return mount / "worktrees"
|
|
49
|
+
return repo.parent
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def lane_worktree_path(
|
|
53
|
+
repo: Path,
|
|
54
|
+
*,
|
|
55
|
+
branch: str,
|
|
56
|
+
lane_id: str,
|
|
57
|
+
project: str | None = None,
|
|
58
|
+
workspace_mount: Path | None = None,
|
|
59
|
+
) -> Path:
|
|
60
|
+
project_slug = _path_slug(project or repo.name)
|
|
61
|
+
branch_slug = _path_slug(branch)
|
|
62
|
+
lane_slug = _path_slug(lane_id)
|
|
63
|
+
return lane_worktree_root(repo, workspace_mount=workspace_mount) / f"{project_slug}-{branch_slug}-{lane_slug}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def phase_loop_runs_dirs(repo: Path) -> tuple[Path, ...]:
|
|
67
|
+
dirs = tuple(path / "runs" for path in phase_loop_read_dirs(repo))
|
|
68
|
+
existing = tuple(path for path in dirs if path.exists())
|
|
69
|
+
return existing or (phase_loop_runs_dir(repo),)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def phase_loop_run_context_file(run_root: Path) -> Path:
|
|
73
|
+
return run_root / "context.md"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def phase_loop_claude_bundle_root(run_root: Path) -> Path:
|
|
77
|
+
return run_root / "claude-bundle"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def phase_loop_claude_plugin_dir(run_root: Path) -> Path:
|
|
81
|
+
return phase_loop_claude_bundle_root(run_root) / "plugin"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def phase_loop_claude_settings_file(run_root: Path) -> Path:
|
|
85
|
+
return phase_loop_claude_bundle_root(run_root) / "settings.json"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def phase_loop_claude_agents_file(run_root: Path) -> Path:
|
|
89
|
+
return phase_loop_claude_bundle_root(run_root) / "agents.json"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def phase_loop_claude_mcp_config_file(run_root: Path) -> Path:
|
|
93
|
+
return phase_loop_claude_bundle_root(run_root) / "mcp.json"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def phase_loop_stop_file(repo: Path) -> Path:
|
|
97
|
+
return phase_loop_dir(repo) / "stop"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def phase_loop_stop_files(repo: Path) -> tuple[Path, ...]:
|
|
101
|
+
canonical = phase_loop_stop_file(repo)
|
|
102
|
+
legacy = legacy_phase_loop_dir(repo) / "stop"
|
|
103
|
+
files = [canonical]
|
|
104
|
+
if legacy.exists():
|
|
105
|
+
files.append(legacy)
|
|
106
|
+
return tuple(files)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def phase_loop_tui_handoff_file(repo: Path) -> Path:
|
|
110
|
+
return phase_loop_dir(repo) / "tui-handoff.md"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def phase_loop_tui_handoff_read_file(repo: Path) -> Path:
|
|
114
|
+
canonical = phase_loop_tui_handoff_file(repo)
|
|
115
|
+
if canonical.exists():
|
|
116
|
+
return canonical
|
|
117
|
+
legacy = legacy_phase_loop_dir(repo) / "tui-handoff.md"
|
|
118
|
+
return legacy if legacy.exists() else canonical
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def phase_loop_state_file(repo: Path) -> Path:
|
|
122
|
+
return phase_loop_dir(repo) / "state.json"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def phase_loop_executor_degradation_file(repo: Path) -> Path:
|
|
126
|
+
return phase_loop_dir(repo) / "executor-degradation.json"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def phase_loop_state_read_file(repo: Path) -> Path:
|
|
130
|
+
canonical = phase_loop_state_file(repo)
|
|
131
|
+
if canonical.exists():
|
|
132
|
+
return canonical
|
|
133
|
+
legacy = legacy_phase_loop_dir(repo) / "state.json"
|
|
134
|
+
return legacy if legacy.exists() else canonical
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def phase_loop_event_file(repo: Path) -> Path:
|
|
138
|
+
return phase_loop_dir(repo) / "events.jsonl"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def phase_loop_event_read_files(repo: Path) -> tuple[Path, ...]:
|
|
142
|
+
legacy = legacy_phase_loop_dir(repo) / "events.jsonl"
|
|
143
|
+
canonical = phase_loop_event_file(repo)
|
|
144
|
+
if canonical.exists():
|
|
145
|
+
return (canonical,)
|
|
146
|
+
if legacy.exists():
|
|
147
|
+
return (legacy,)
|
|
148
|
+
return (canonical,)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def phase_loop_fleet_metrics_file(repo: Path) -> Path:
|
|
152
|
+
"""Append-only sibling ledger for CS-2.1 SA fleet-metric events.
|
|
153
|
+
|
|
154
|
+
Kept as a SEPARATE file from ``events.jsonl`` so the existing event ledger's
|
|
155
|
+
bytes and reconcile/fold semantics are provably untouched by this additive
|
|
156
|
+
observability slice. Enforcement-owned (inside ``.phase-loop/``); the Portal
|
|
157
|
+
wall forbids Portal from ever reading it — the sanitized aggregates are
|
|
158
|
+
PUSHED to Portal, never pulled.
|
|
159
|
+
"""
|
|
160
|
+
return phase_loop_dir(repo) / "fleet-metrics.jsonl"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def phase_loop_fleet_metrics_read_files(repo: Path) -> tuple[Path, ...]:
|
|
164
|
+
canonical = phase_loop_fleet_metrics_file(repo)
|
|
165
|
+
legacy = legacy_phase_loop_dir(repo) / "fleet-metrics.jsonl"
|
|
166
|
+
if canonical.exists():
|
|
167
|
+
return (canonical,)
|
|
168
|
+
if legacy.exists():
|
|
169
|
+
return (legacy,)
|
|
170
|
+
return (canonical,)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def phase_loop_active_loop_file(repo: Path) -> Path:
|
|
174
|
+
return phase_loop_dir(repo) / "active-loop.json"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def phase_loop_active_loop_read_files(repo: Path) -> tuple[Path, ...]:
|
|
178
|
+
canonical = phase_loop_active_loop_file(repo)
|
|
179
|
+
legacy = legacy_phase_loop_dir(repo) / "active-loop.json"
|
|
180
|
+
if canonical.exists():
|
|
181
|
+
return (canonical,)
|
|
182
|
+
if legacy.exists():
|
|
183
|
+
return (legacy,)
|
|
184
|
+
return (canonical,)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def git_info_exclude(repo: Path) -> Path | None:
|
|
188
|
+
try:
|
|
189
|
+
path_text = subprocess.check_output(
|
|
190
|
+
["git", "-C", str(repo), "rev-parse", "--git-path", "info/exclude"],
|
|
191
|
+
text=True,
|
|
192
|
+
stderr=subprocess.DEVNULL,
|
|
193
|
+
).strip()
|
|
194
|
+
except Exception:
|
|
195
|
+
return None
|
|
196
|
+
if not path_text:
|
|
197
|
+
return None
|
|
198
|
+
path = Path(path_text)
|
|
199
|
+
if not path.is_absolute():
|
|
200
|
+
path = repo / path
|
|
201
|
+
return path
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def ensure_phase_loop_excluded(repo: Path) -> None:
|
|
205
|
+
exclude = git_info_exclude(repo)
|
|
206
|
+
if exclude is None:
|
|
207
|
+
return
|
|
208
|
+
try:
|
|
209
|
+
exclude.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
text = exclude.read_text() if exclude.exists() else ""
|
|
211
|
+
existing = {line.strip() for line in text.splitlines()}
|
|
212
|
+
missing = [entry for entry in EXCLUDE_ENTRIES if entry not in existing]
|
|
213
|
+
if not missing:
|
|
214
|
+
return
|
|
215
|
+
prefix = "" if not text or text.endswith("\n") else "\n"
|
|
216
|
+
with exclude.open("a", encoding="utf-8") as handle:
|
|
217
|
+
handle.write(prefix + "\n".join(missing) + "\n")
|
|
218
|
+
except OSError:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _path_slug(value: str) -> str:
|
|
223
|
+
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip())
|
|
224
|
+
slug = re.sub(r"-+", "-", slug).strip("-._")
|
|
225
|
+
return slug or "unknown"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .baml_modular import parse_baml_response
|
|
9
|
+
from .events import read_events
|
|
10
|
+
from .install_status import build_install_status
|
|
11
|
+
from .models import PIPELINE_MODE_LITERALS, StateSnapshot, require_literal, utc_now
|
|
12
|
+
from .plan_manifest import read_manifest
|
|
13
|
+
from .reconcile import reconcile
|
|
14
|
+
from .skill_paths import current_harness
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_runtime_projection(
|
|
18
|
+
repo: Path,
|
|
19
|
+
roadmap: Path,
|
|
20
|
+
snapshot: StateSnapshot | None = None,
|
|
21
|
+
pipeline_mode: str = "standalone",
|
|
22
|
+
) -> dict[str, object]:
|
|
23
|
+
require_literal(pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
|
|
24
|
+
resolved = snapshot or reconcile(repo, roadmap)
|
|
25
|
+
install = build_install_status(repo)
|
|
26
|
+
plan_activity = _plan_activity(repo)
|
|
27
|
+
payload: dict[str, object] = {
|
|
28
|
+
"runtime_version": __version__,
|
|
29
|
+
"protocol_version": "phase-loop-protocol-v1",
|
|
30
|
+
"harness": current_harness(),
|
|
31
|
+
"source_bundle_digest": _source_bundle_digest(repo),
|
|
32
|
+
"closeout_status": _closeout_status(resolved),
|
|
33
|
+
"handoff_status": "written" if (repo / ".phase-loop" / "tui-handoff.md").is_file() else "missing",
|
|
34
|
+
"current_phase_boundary": resolved.current_phase or "none",
|
|
35
|
+
"last_event_iso": _last_event_iso(repo) or resolved.timestamp or utc_now(),
|
|
36
|
+
"plans_in_flight": plan_activity["plans_in_flight"],
|
|
37
|
+
"plans_executing": plan_activity["plans_executing"],
|
|
38
|
+
"last_plan_event_iso": plan_activity["last_plan_event_iso"],
|
|
39
|
+
"install_status": str(install.get("summary") or "unknown"),
|
|
40
|
+
"gitignore_init_status": _gitignore_init_status(repo),
|
|
41
|
+
"operating_mode": pipeline_mode,
|
|
42
|
+
}
|
|
43
|
+
_assert_redacted(payload)
|
|
44
|
+
parse_baml_response("DotfilesRuntimeProjection", json.dumps(payload))
|
|
45
|
+
return payload
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _source_bundle_digest(repo: Path) -> str:
|
|
49
|
+
for event in reversed(read_events(repo)):
|
|
50
|
+
metadata = event.get("metadata")
|
|
51
|
+
if not isinstance(metadata, dict):
|
|
52
|
+
continue
|
|
53
|
+
bundle = metadata.get("pipeline_source_bundle")
|
|
54
|
+
if isinstance(bundle, dict) and isinstance(bundle.get("sha256"), str) and bundle["sha256"]:
|
|
55
|
+
return f"sha256:{bundle['sha256']}"
|
|
56
|
+
return "none"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _closeout_status(snapshot: StateSnapshot) -> str:
|
|
60
|
+
if snapshot.closeout_terminal_status:
|
|
61
|
+
return snapshot.closeout_terminal_status
|
|
62
|
+
if isinstance(snapshot.terminal_summary, dict):
|
|
63
|
+
status = snapshot.terminal_summary.get("terminal_status")
|
|
64
|
+
if isinstance(status, str) and status:
|
|
65
|
+
return status
|
|
66
|
+
if snapshot.current_phase and snapshot.current_phase in snapshot.phases:
|
|
67
|
+
return snapshot.phases[snapshot.current_phase]
|
|
68
|
+
return "unknown"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _last_event_iso(repo: Path) -> str | None:
|
|
72
|
+
events = read_events(repo)
|
|
73
|
+
if not events:
|
|
74
|
+
return None
|
|
75
|
+
timestamp = events[-1].get("timestamp")
|
|
76
|
+
return str(timestamp) if timestamp else None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _plan_activity(repo: Path) -> dict[str, object]:
|
|
80
|
+
try:
|
|
81
|
+
manifest = read_manifest(repo)
|
|
82
|
+
except (OSError, ValueError):
|
|
83
|
+
return {"plans_in_flight": 0, "plans_executing": [], "last_plan_event_iso": "none"}
|
|
84
|
+
in_flight_statuses = {"committed", "executing"}
|
|
85
|
+
plans_in_flight = sum(1 for entry in manifest.plans if entry.status in in_flight_statuses)
|
|
86
|
+
plans_executing = sorted(entry.slug for entry in manifest.plans if entry.status == "executing")
|
|
87
|
+
lifecycle_timestamps: list[str] = []
|
|
88
|
+
updated_timestamps: list[str] = []
|
|
89
|
+
for entry in manifest.plans:
|
|
90
|
+
lifecycle_timestamps.extend(event.at for event in entry.lifecycle if event.at)
|
|
91
|
+
if entry.updated_at:
|
|
92
|
+
updated_timestamps.append(entry.updated_at)
|
|
93
|
+
return {
|
|
94
|
+
"plans_in_flight": plans_in_flight,
|
|
95
|
+
"plans_executing": plans_executing,
|
|
96
|
+
"last_plan_event_iso": max(lifecycle_timestamps) if lifecycle_timestamps else max(updated_timestamps, default="none"),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _gitignore_init_status(repo: Path) -> str:
|
|
101
|
+
gitignore = repo / ".gitignore"
|
|
102
|
+
if not gitignore.is_file():
|
|
103
|
+
return "missing"
|
|
104
|
+
lines = gitignore.read_text(encoding="utf-8").splitlines()
|
|
105
|
+
return "present" if "/.dev-skills/" in lines or ".dev-skills/" in lines else "missing"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _assert_redacted(payload: dict[str, Any]) -> None:
|
|
109
|
+
serialized = json.dumps(payload, sort_keys=True)
|
|
110
|
+
forbidden = ("/home/", "/Users/", "/mnt/", "op://", "sk-", "AKIA", "ghp_")
|
|
111
|
+
leaked = [token for token in forbidden if token in serialized]
|
|
112
|
+
if leaked:
|
|
113
|
+
raise ValueError(f"runtime projection contains forbidden metadata token: {leaked[0]}")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Locate package-data shipped inside the ``phase_loop_runtime`` wheel.
|
|
2
|
+
|
|
3
|
+
Single home for the ``importlib.resources`` wheel-anchor idiom that was triplicated
|
|
4
|
+
across ``baml_modular``/``schema_export``/``skill_inventory`` (#12 CR). Returns a real
|
|
5
|
+
on-disk ``Path``.
|
|
6
|
+
|
|
7
|
+
Limitation (consistent with the prior per-module helpers): under a zip-imported app
|
|
8
|
+
(PEX/shiv/zipapp) ``importlib.resources.files`` yields a non-filesystem ``Traversable``
|
|
9
|
+
whose ``str()`` is not a real directory, so these return ``None``. ``pip install``
|
|
10
|
+
(extracted ``site-packages``) is the supported mode.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def package_root() -> Path | None:
|
|
18
|
+
"""The extracted on-disk root of the installed ``phase_loop_runtime`` package, or
|
|
19
|
+
``None`` when it can't be resolved to a real directory."""
|
|
20
|
+
try:
|
|
21
|
+
import importlib.resources
|
|
22
|
+
|
|
23
|
+
root = Path(str(importlib.resources.files("phase_loop_runtime")))
|
|
24
|
+
except (ModuleNotFoundError, TypeError, FileNotFoundError):
|
|
25
|
+
return None
|
|
26
|
+
return root if root.is_dir() else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def packaged_resource_dir(name: str) -> Path | None:
|
|
30
|
+
"""A package-data subdirectory shipped in the wheel (e.g. ``"skills_bundle"``), or
|
|
31
|
+
``None`` if the package root or the subdirectory is unavailable."""
|
|
32
|
+
root = package_root()
|
|
33
|
+
if root is None:
|
|
34
|
+
return None
|
|
35
|
+
candidate = root / name
|
|
36
|
+
return candidate if candidate.is_dir() else None
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Canonical phase-loop closeout schema export (SCHEMA SL-1).
|
|
2
|
+
|
|
3
|
+
The ONE canonical closeout shape is the nested :class:`PhaseLoopCloseout`
|
|
4
|
+
dataclass (``models.py``). This module *derives* a declared JSON-Schema and a flat
|
|
5
|
+
field-list FROM that dataclass — walking ``dataclasses.fields`` +
|
|
6
|
+
``typing.get_type_hints`` and RECURSING into the nested sub-dataclasses
|
|
7
|
+
(``PhaseLoopAutomation``/``Artifacts``/``Verification``/``Blocker``/``SourceBundle``/
|
|
8
|
+
``SourceTruthImpact``/``SpecDeltaCloseout``). Scalar types, nested required-sets,
|
|
9
|
+
nullable unions, and enums all come from the dataclass, so "single source of
|
|
10
|
+
truth" is real: a dataclass change that isn't regenerated fails ``--check`` and the
|
|
11
|
+
committed-artifact drift test.
|
|
12
|
+
|
|
13
|
+
The generated artifact is committed at ``schemas/phase_loop_closeout.schema.json``
|
|
14
|
+
and bundled as package-data; :func:`canonical_schema` resolves it via
|
|
15
|
+
``importlib.resources`` (the post-DECOUPLE pattern, mirroring ``baml_modular``)
|
|
16
|
+
so the bundled-artifact path works from a wheel-installed venv. :func:`check`
|
|
17
|
+
builds the canonical schema *fresh* from the live dataclass, so a stale committed
|
|
18
|
+
artifact cannot mask drift.
|
|
19
|
+
|
|
20
|
+
``required`` semantics: a field is optional iff its resolved annotation is a union
|
|
21
|
+
containing ``None`` AND it is not part of the closeout validator's mandatory set.
|
|
22
|
+
The runtime's malformed-closeout validator (``closeout.py``) is the ground truth for
|
|
23
|
+
the top-level required set; this module exposes that set as the single source the
|
|
24
|
+
validator consumes, killing the duplicate hand-maintained tuple.
|
|
25
|
+
|
|
26
|
+
Generation is deterministic (sorted keys, explicitly sorted lists, no volatile
|
|
27
|
+
content) so the byte-stable artifact is a meaningful parity baseline.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import dataclasses
|
|
33
|
+
import importlib.resources
|
|
34
|
+
import json
|
|
35
|
+
import types
|
|
36
|
+
import typing
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
from .models import (
|
|
41
|
+
PIPELINE_CLOSEOUT_OUTCOMES,
|
|
42
|
+
PIPELINE_CLOSEOUT_SCHEMA,
|
|
43
|
+
PhaseLoopCloseout,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
#: Repo-relative location of the committed/bundled artifact (resolved via
|
|
47
|
+
#: importlib.resources at runtime; this constant is for docs/diagnostics).
|
|
48
|
+
SCHEMA_RESOURCE = "schemas/phase_loop_closeout.schema.json"
|
|
49
|
+
|
|
50
|
+
#: Static provenance recorded INSIDE the JSON (never a // comment — the artifact
|
|
51
|
+
#: must remain valid JSON for json.load + --check). No timestamps/abs paths so it
|
|
52
|
+
#: stays byte-stable and metadata_only.
|
|
53
|
+
_PROVENANCE = (
|
|
54
|
+
"GENERATED from phase_loop_runtime.models.PhaseLoopCloseout by "
|
|
55
|
+
"`phase-loop export-schema`. Do not hand-edit; regenerate instead."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
#: The top-level closeout fields the runtime's malformed-closeout validator
|
|
59
|
+
#: requires present-and-non-null (closeout.py:phase_loop_closeout_diagnostic).
|
|
60
|
+
#: `schema` is validated separately (it is a fixed literal), and
|
|
61
|
+
#: `spec_delta_closeout` is genuinely optional, so neither appears here. This is
|
|
62
|
+
#: the SINGLE SOURCE the validator consumes — do not maintain a second copy.
|
|
63
|
+
CLOSEOUT_REQUIRED_NESTED_FIELDS: tuple[str, ...] = (
|
|
64
|
+
"phase",
|
|
65
|
+
"terminal_status",
|
|
66
|
+
"automation",
|
|
67
|
+
"artifacts",
|
|
68
|
+
"verification",
|
|
69
|
+
"blocker",
|
|
70
|
+
"source_bundle",
|
|
71
|
+
"source_truth_impact",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _strip_optional(annotation: Any) -> tuple[Any, bool]:
|
|
76
|
+
"""Return ``(inner_type, is_optional)`` for a possibly-``X | None`` annotation."""
|
|
77
|
+
origin = typing.get_origin(annotation)
|
|
78
|
+
if origin in (typing.Union, types.UnionType):
|
|
79
|
+
args = tuple(a for a in typing.get_args(annotation) if a is not type(None))
|
|
80
|
+
is_optional = len(args) != len(typing.get_args(annotation))
|
|
81
|
+
inner = args[0] if len(args) == 1 else annotation
|
|
82
|
+
return inner, is_optional
|
|
83
|
+
return annotation, False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _scalar_schema(tp: Any) -> dict[str, Any] | None:
|
|
87
|
+
"""Map a scalar Python type to its JSON-Schema type, else ``None``."""
|
|
88
|
+
# bool is a subclass of int — check it first.
|
|
89
|
+
if tp is bool:
|
|
90
|
+
return {"type": "boolean"}
|
|
91
|
+
if tp is int:
|
|
92
|
+
return {"type": "integer"}
|
|
93
|
+
if tp is float:
|
|
94
|
+
return {"type": "number"}
|
|
95
|
+
if tp is str:
|
|
96
|
+
return {"type": "string"}
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _annotation_schema(annotation: Any) -> dict[str, Any]:
|
|
101
|
+
"""Derive a JSON-Schema fragment from a (non-optional) resolved annotation."""
|
|
102
|
+
inner, _ = _strip_optional(annotation)
|
|
103
|
+
origin = typing.get_origin(inner)
|
|
104
|
+
scalar = _scalar_schema(inner)
|
|
105
|
+
if scalar is not None:
|
|
106
|
+
return scalar
|
|
107
|
+
if dataclasses.is_dataclass(inner):
|
|
108
|
+
return _dataclass_schema(inner)
|
|
109
|
+
if origin in (tuple, list):
|
|
110
|
+
args = typing.get_args(inner)
|
|
111
|
+
# tuple[X, ...] / list[X]; freeform `dict[..., Any]` items collapse to object.
|
|
112
|
+
item = args[0] if args else Any
|
|
113
|
+
item_inner, _ = _strip_optional(item)
|
|
114
|
+
if dataclasses.is_dataclass(item_inner):
|
|
115
|
+
return {"type": "array", "items": _dataclass_schema(item_inner)}
|
|
116
|
+
item_scalar = _scalar_schema(item_inner)
|
|
117
|
+
if item_scalar is not None:
|
|
118
|
+
return {"type": "array", "items": item_scalar}
|
|
119
|
+
return {"type": "array", "items": {"type": "object"}}
|
|
120
|
+
if origin is dict:
|
|
121
|
+
return {"type": "object"}
|
|
122
|
+
# Any / unrecognised -> permissive object (don't over-specify freeform shapes).
|
|
123
|
+
return {"type": "object"}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _dataclass_schema(cls: Any) -> dict[str, Any]:
|
|
127
|
+
"""Recursively build a JSON-Schema `object` for a dataclass.
|
|
128
|
+
|
|
129
|
+
A nested field is treated as required iff its annotation is NOT a
|
|
130
|
+
union-with-None — a conservative contract convention. (It is not a guarantee of
|
|
131
|
+
presence in every emitted instance: a few fields with custom `to_json` `or None`
|
|
132
|
+
emit logic, e.g. an empty `protected_sources` tuple, may be absent in practice.
|
|
133
|
+
Nothing validates live instances against this artifact, so the convention is the
|
|
134
|
+
declared shape, not an instance invariant.) Nullable fields get a
|
|
135
|
+
`["...","null"]` union type and are excluded from `required`.
|
|
136
|
+
"""
|
|
137
|
+
hints = typing.get_type_hints(cls)
|
|
138
|
+
properties: dict[str, dict[str, Any]] = {}
|
|
139
|
+
required: list[str] = []
|
|
140
|
+
for fld in dataclasses.fields(cls):
|
|
141
|
+
annotation = hints[fld.name]
|
|
142
|
+
_, is_optional = _strip_optional(annotation)
|
|
143
|
+
prop = _annotation_schema(annotation)
|
|
144
|
+
if is_optional:
|
|
145
|
+
base_type = prop.get("type", "object")
|
|
146
|
+
if isinstance(base_type, str):
|
|
147
|
+
prop = {**prop, "type": [base_type, "null"]}
|
|
148
|
+
else:
|
|
149
|
+
required.append(fld.name)
|
|
150
|
+
properties[fld.name] = prop
|
|
151
|
+
return {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"additionalProperties": False,
|
|
154
|
+
"required": sorted(required),
|
|
155
|
+
"properties": {name: properties[name] for name in sorted(properties)},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def field_names() -> tuple[str, ...]:
|
|
160
|
+
"""The declared field set of :class:`PhaseLoopCloseout`, in declaration order."""
|
|
161
|
+
return tuple(f.name for f in dataclasses.fields(PhaseLoopCloseout))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def required_closeout_fields() -> tuple[str, ...]:
|
|
165
|
+
"""Single source for the closeout validator's mandatory nested fields.
|
|
166
|
+
|
|
167
|
+
Consumed by ``closeout.py``'s malformed-closeout diagnostic so the required
|
|
168
|
+
set is not maintained in two places.
|
|
169
|
+
"""
|
|
170
|
+
return CLOSEOUT_REQUIRED_NESTED_FIELDS
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _top_level_property(name: str, annotation: Any) -> dict[str, Any]:
|
|
174
|
+
"""Property entry for a top-level closeout field (with closeout-specific consts).
|
|
175
|
+
|
|
176
|
+
A top-level field is null-allowed only when it is BOTH annotated nullable AND
|
|
177
|
+
not in the validator's mandatory set. ``source_truth_impact`` is annotated
|
|
178
|
+
``... | None`` (the builder always populates it) but the validator requires it
|
|
179
|
+
present-and-non-null, so it stays non-null here — matching what consumers
|
|
180
|
+
actually enforce.
|
|
181
|
+
"""
|
|
182
|
+
if name == "schema":
|
|
183
|
+
return {"type": "string", "const": PIPELINE_CLOSEOUT_SCHEMA}
|
|
184
|
+
if name == "terminal_status":
|
|
185
|
+
return {"type": "string", "enum": sorted(PIPELINE_CLOSEOUT_OUTCOMES)}
|
|
186
|
+
_, is_optional = _strip_optional(annotation)
|
|
187
|
+
prop = _annotation_schema(annotation)
|
|
188
|
+
null_allowed = is_optional and name not in CLOSEOUT_REQUIRED_NESTED_FIELDS
|
|
189
|
+
if null_allowed:
|
|
190
|
+
base_type = prop.get("type", "object")
|
|
191
|
+
if isinstance(base_type, str):
|
|
192
|
+
prop = {**prop, "type": [base_type, "null"]}
|
|
193
|
+
return prop
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_schema() -> dict[str, Any]:
|
|
197
|
+
"""Build the canonical JSON-Schema FROM the dataclass (single source of truth)."""
|
|
198
|
+
hints = typing.get_type_hints(PhaseLoopCloseout)
|
|
199
|
+
fields = dataclasses.fields(PhaseLoopCloseout)
|
|
200
|
+
properties = {f.name: _top_level_property(f.name, hints[f.name]) for f in fields}
|
|
201
|
+
# Required = the validator's mandatory nested fields + the `schema` literal.
|
|
202
|
+
# `spec_delta_closeout` is the only genuinely optional top-level field.
|
|
203
|
+
required = sorted({*CLOSEOUT_REQUIRED_NESTED_FIELDS, "schema"})
|
|
204
|
+
return {
|
|
205
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
206
|
+
"$id": PIPELINE_CLOSEOUT_SCHEMA,
|
|
207
|
+
"title": "phase_loop_closeout",
|
|
208
|
+
"$comment": _PROVENANCE,
|
|
209
|
+
"type": "object",
|
|
210
|
+
"required": required,
|
|
211
|
+
"additionalProperties": False,
|
|
212
|
+
"properties": {name: properties[name] for name in sorted(properties)},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def build_field_list() -> dict[str, Any]:
|
|
217
|
+
"""Build the flat field-list payload gp consumes (all declared fields)."""
|
|
218
|
+
return {
|
|
219
|
+
"schema_id": PIPELINE_CLOSEOUT_SCHEMA,
|
|
220
|
+
"source": "phase_loop_runtime.models.PhaseLoopCloseout",
|
|
221
|
+
"fields": sorted(field_names()),
|
|
222
|
+
"required": sorted({*CLOSEOUT_REQUIRED_NESTED_FIELDS, "schema"}),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def render(schema: dict[str, Any]) -> str:
|
|
227
|
+
"""Deterministic serialization: sorted keys, trailing newline, no volatile data."""
|
|
228
|
+
return json.dumps(schema, indent=2, sort_keys=True) + "\n"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _resource_path() -> Path:
|
|
232
|
+
"""Resolve the bundled artifact via importlib.resources (wheel-safe)."""
|
|
233
|
+
from .runtime_resources import package_root
|
|
234
|
+
|
|
235
|
+
root = package_root()
|
|
236
|
+
if root is not None:
|
|
237
|
+
packaged = root / SCHEMA_RESOURCE
|
|
238
|
+
if packaged.is_file():
|
|
239
|
+
return packaged
|
|
240
|
+
# Editable/source-layout fallback (same dir importlib.resources resolves once
|
|
241
|
+
# installed from a wheel).
|
|
242
|
+
fallback = Path(__file__).resolve().parent / SCHEMA_RESOURCE
|
|
243
|
+
if fallback.is_file():
|
|
244
|
+
return fallback
|
|
245
|
+
raise FileNotFoundError(
|
|
246
|
+
f"bundled closeout schema not found ({SCHEMA_RESOURCE}); regenerate via "
|
|
247
|
+
"`phase-loop export-schema --output <artifact>`"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def canonical_schema() -> dict[str, Any]:
|
|
252
|
+
"""Load the bundled canonical schema artifact (the in-package, wheel-resolved copy)."""
|
|
253
|
+
return json.loads(_resource_path().read_text(encoding="utf-8"))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _diff_json(canonical: Any, supplied: Any, path: str = "") -> list[str]:
|
|
257
|
+
"""Recursively diff two JSON values, naming the actual differing paths."""
|
|
258
|
+
if isinstance(canonical, dict) and isinstance(supplied, dict):
|
|
259
|
+
diffs: list[str] = []
|
|
260
|
+
for key in sorted(set(canonical) | set(supplied)):
|
|
261
|
+
here = f"{path}.{key}" if path else key
|
|
262
|
+
if key not in supplied:
|
|
263
|
+
diffs.append(f"{here}: missing (canonical={canonical[key]!r})")
|
|
264
|
+
elif key not in canonical:
|
|
265
|
+
diffs.append(f"{here}: unexpected (supplied={supplied[key]!r})")
|
|
266
|
+
else:
|
|
267
|
+
diffs.extend(_diff_json(canonical[key], supplied[key], here))
|
|
268
|
+
return diffs
|
|
269
|
+
if isinstance(canonical, list) and isinstance(supplied, list):
|
|
270
|
+
if canonical != supplied:
|
|
271
|
+
return [f"{path or '<root>'}: list differs (canonical={canonical!r} supplied={supplied!r})"]
|
|
272
|
+
return []
|
|
273
|
+
if canonical != supplied:
|
|
274
|
+
return [f"{path or '<root>'}: canonical={canonical!r} supplied={supplied!r}"]
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def check(path: Path) -> list[str]:
|
|
279
|
+
"""Compare a supplied artifact against the canonical schema built FRESH from the
|
|
280
|
+
live dataclass.
|
|
281
|
+
|
|
282
|
+
Building fresh (not loading the committed artifact) means a stale committed
|
|
283
|
+
artifact cannot mask a dataclass change — ``--check`` fails on any divergence
|
|
284
|
+
between the supplied artifact and the current dataclass-derived shape. Returns a
|
|
285
|
+
list of human-readable diffs naming the differing fields; empty means parity.
|
|
286
|
+
"""
|
|
287
|
+
canonical = build_schema()
|
|
288
|
+
try:
|
|
289
|
+
supplied = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
290
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
291
|
+
return [f"could not read/parse supplied artifact: {exc}"]
|
|
292
|
+
return _diff_json(canonical, supplied)
|