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,415 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .discovery import PLAN_RE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SCHEMA_VERSION = 1
|
|
14
|
+
# Repo-relative default only; resolution always joins this against an explicit
|
|
15
|
+
# repo root supplied by the caller (see _manifest_path) -- never an implicit
|
|
16
|
+
# fleet-absolute hardcode (DECOUPLE SL-2).
|
|
17
|
+
MANIFEST_PATH = PurePosixPath("plans/manifest.json")
|
|
18
|
+
PLAN_TYPES = {"phase", "detailed"}
|
|
19
|
+
PLAN_STATUSES = {"imported", "committed", "executing", "completed", "failed", "orphaned"}
|
|
20
|
+
TRANSITIONS = {
|
|
21
|
+
"imported": {"executing", "orphaned"},
|
|
22
|
+
"committed": {"executing", "orphaned"},
|
|
23
|
+
"executing": {"completed", "failed", "orphaned"},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class DotfilesPlanRef:
|
|
29
|
+
slug: str
|
|
30
|
+
file: str
|
|
31
|
+
type: str
|
|
32
|
+
status: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class DotfilesPlanLifecycleEvent:
|
|
37
|
+
transition: str
|
|
38
|
+
by: str
|
|
39
|
+
at: str
|
|
40
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class DotfilesPlanEntry:
|
|
45
|
+
slug: str
|
|
46
|
+
file: str
|
|
47
|
+
type: str
|
|
48
|
+
status: str
|
|
49
|
+
created_at: str
|
|
50
|
+
updated_at: str
|
|
51
|
+
owner_skill: str
|
|
52
|
+
handoff_ref: str | None = None
|
|
53
|
+
reflection_ref: str | None = None
|
|
54
|
+
task_summary: str | None = None
|
|
55
|
+
acceptance_criteria_count: int | None = None
|
|
56
|
+
roadmap_ref: DotfilesPlanRef | None = None
|
|
57
|
+
phase_alias: str | None = None
|
|
58
|
+
if_gates_produced: tuple[str, ...] = ()
|
|
59
|
+
lanes: tuple[str, ...] = ()
|
|
60
|
+
lifecycle: tuple[DotfilesPlanLifecycleEvent, ...] = ()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class DotfilesPlanManifest:
|
|
65
|
+
schema_version: int = SCHEMA_VERSION
|
|
66
|
+
plans: tuple[DotfilesPlanEntry, ...] = ()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ValidationResult:
|
|
71
|
+
valid: bool
|
|
72
|
+
errors: tuple[str, ...] = ()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def read_manifest(repo: Path) -> DotfilesPlanManifest:
|
|
76
|
+
manifest_path = _manifest_path(repo)
|
|
77
|
+
if not manifest_path.exists():
|
|
78
|
+
return DotfilesPlanManifest()
|
|
79
|
+
try:
|
|
80
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
81
|
+
except json.JSONDecodeError as exc:
|
|
82
|
+
raise ValueError(f"manifest JSON is malformed at line {exc.lineno} column {exc.colno}") from exc
|
|
83
|
+
manifest = _manifest_from_json(data)
|
|
84
|
+
if manifest.schema_version != SCHEMA_VERSION:
|
|
85
|
+
raise ValueError(f"unsupported manifest schema_version: {manifest.schema_version}")
|
|
86
|
+
return manifest
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def append_entry(repo: Path, entry: DotfilesPlanEntry) -> None:
|
|
90
|
+
manifest = read_manifest(repo)
|
|
91
|
+
entries = {existing.slug: existing for existing in manifest.plans}
|
|
92
|
+
entries[entry.slug] = entry
|
|
93
|
+
_write_manifest(repo, DotfilesPlanManifest(plans=tuple(entries[slug] for slug in sorted(entries))))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def update_lifecycle(repo: Path, slug: str, transition: str, by: str, metadata: dict[str, Any]) -> None:
|
|
97
|
+
if not isinstance(metadata, dict):
|
|
98
|
+
raise ValueError("metadata must be an object")
|
|
99
|
+
manifest = read_manifest(repo)
|
|
100
|
+
now = _utc_now()
|
|
101
|
+
entries: list[DotfilesPlanEntry] = []
|
|
102
|
+
updated = False
|
|
103
|
+
for entry in manifest.plans:
|
|
104
|
+
if entry.slug != slug:
|
|
105
|
+
entries.append(entry)
|
|
106
|
+
continue
|
|
107
|
+
allowed = TRANSITIONS.get(entry.status, set())
|
|
108
|
+
if transition not in allowed:
|
|
109
|
+
raise ValueError(f"invalid lifecycle transition for {slug}: {entry.status} -> {transition}")
|
|
110
|
+
event = DotfilesPlanLifecycleEvent(transition=transition, by=by, at=now, metadata=metadata)
|
|
111
|
+
entries.append(
|
|
112
|
+
DotfilesPlanEntry(
|
|
113
|
+
slug=entry.slug,
|
|
114
|
+
file=entry.file,
|
|
115
|
+
type=entry.type,
|
|
116
|
+
status=transition,
|
|
117
|
+
created_at=entry.created_at,
|
|
118
|
+
updated_at=now,
|
|
119
|
+
owner_skill=entry.owner_skill,
|
|
120
|
+
handoff_ref=entry.handoff_ref,
|
|
121
|
+
reflection_ref=entry.reflection_ref,
|
|
122
|
+
task_summary=entry.task_summary,
|
|
123
|
+
acceptance_criteria_count=entry.acceptance_criteria_count,
|
|
124
|
+
roadmap_ref=entry.roadmap_ref,
|
|
125
|
+
phase_alias=entry.phase_alias,
|
|
126
|
+
if_gates_produced=entry.if_gates_produced,
|
|
127
|
+
lanes=entry.lanes,
|
|
128
|
+
lifecycle=(*entry.lifecycle, event),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
updated = True
|
|
132
|
+
if not updated:
|
|
133
|
+
raise ValueError(f"manifest entry not found: {slug}")
|
|
134
|
+
_write_manifest(repo, DotfilesPlanManifest(plans=tuple(entries)))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validate_manifest(manifest_path: Path) -> ValidationResult:
|
|
138
|
+
errors: list[str] = []
|
|
139
|
+
try:
|
|
140
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
141
|
+
except FileNotFoundError:
|
|
142
|
+
return ValidationResult(False, ("manifest file does not exist",))
|
|
143
|
+
except json.JSONDecodeError as exc:
|
|
144
|
+
return ValidationResult(False, (f"manifest JSON is malformed at line {exc.lineno} column {exc.colno}",))
|
|
145
|
+
if not isinstance(data, dict):
|
|
146
|
+
return ValidationResult(False, ("manifest must be an object",))
|
|
147
|
+
if data.get("schema_version") != SCHEMA_VERSION:
|
|
148
|
+
errors.append("schema_version must be 1")
|
|
149
|
+
plans = data.get("plans")
|
|
150
|
+
if not isinstance(plans, list):
|
|
151
|
+
errors.append("plans must be an array")
|
|
152
|
+
return ValidationResult(False, tuple(errors))
|
|
153
|
+
repo = manifest_path.parent.parent
|
|
154
|
+
seen: set[str] = set()
|
|
155
|
+
for index, raw_entry in enumerate(plans):
|
|
156
|
+
label = f"plans[{index}]"
|
|
157
|
+
if not isinstance(raw_entry, dict):
|
|
158
|
+
errors.append(f"{label} must be an object")
|
|
159
|
+
continue
|
|
160
|
+
slug = raw_entry.get("slug")
|
|
161
|
+
if not isinstance(slug, str) or not slug:
|
|
162
|
+
errors.append(f"{label}.slug is required")
|
|
163
|
+
elif slug in seen:
|
|
164
|
+
errors.append(f"{label}.slug duplicates {slug}")
|
|
165
|
+
else:
|
|
166
|
+
seen.add(slug)
|
|
167
|
+
_validate_common_entry(label, raw_entry, repo, errors)
|
|
168
|
+
if raw_entry.get("type") == "phase":
|
|
169
|
+
_validate_phase_entry(label, raw_entry, errors)
|
|
170
|
+
elif raw_entry.get("type") == "detailed":
|
|
171
|
+
_validate_detailed_entry(label, raw_entry, errors)
|
|
172
|
+
_validate_lifecycle(label, raw_entry.get("lifecycle"), errors)
|
|
173
|
+
return ValidationResult(not errors, tuple(errors))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def import_existing_phase_plans(repo: Path) -> DotfilesPlanManifest:
|
|
177
|
+
entries: list[DotfilesPlanEntry] = []
|
|
178
|
+
for plan_path in sorted((repo / "plans").glob("phase-plan-v*-*.md")):
|
|
179
|
+
match = PLAN_RE.search(plan_path.name)
|
|
180
|
+
if not match:
|
|
181
|
+
continue
|
|
182
|
+
version, phase_alias = match.groups()
|
|
183
|
+
rel_path = plan_path.relative_to(repo).as_posix()
|
|
184
|
+
slug = f"{version}-{phase_alias}"
|
|
185
|
+
timestamp = _timestamp_for_path(plan_path)
|
|
186
|
+
roadmap_file = _frontmatter_value(plan_path, "roadmap")
|
|
187
|
+
roadmap_ref = (
|
|
188
|
+
DotfilesPlanRef(
|
|
189
|
+
slug=Path(roadmap_file).stem,
|
|
190
|
+
file=roadmap_file,
|
|
191
|
+
type="phase",
|
|
192
|
+
status="imported",
|
|
193
|
+
)
|
|
194
|
+
if roadmap_file
|
|
195
|
+
else None
|
|
196
|
+
)
|
|
197
|
+
entries.append(
|
|
198
|
+
DotfilesPlanEntry(
|
|
199
|
+
slug=slug,
|
|
200
|
+
file=rel_path,
|
|
201
|
+
type="phase",
|
|
202
|
+
status="imported",
|
|
203
|
+
created_at=timestamp,
|
|
204
|
+
updated_at=timestamp,
|
|
205
|
+
owner_skill="codex-plan-phase",
|
|
206
|
+
roadmap_ref=roadmap_ref,
|
|
207
|
+
phase_alias=phase_alias,
|
|
208
|
+
if_gates_produced=_extract_if_gates(plan_path),
|
|
209
|
+
lanes=_extract_lanes(plan_path),
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
return DotfilesPlanManifest(plans=tuple(entries))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _manifest_path(repo: Path) -> Path:
|
|
216
|
+
return Path(repo) / MANIFEST_PATH
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _write_manifest(repo: Path, manifest: DotfilesPlanManifest) -> None:
|
|
220
|
+
manifest_path = _manifest_path(repo)
|
|
221
|
+
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
payload = json.dumps(_manifest_to_json(manifest), indent=2, sort_keys=True) + "\n"
|
|
223
|
+
if manifest_path.exists() and manifest_path.read_text(encoding="utf-8") == payload:
|
|
224
|
+
return
|
|
225
|
+
manifest_path.write_text(payload, encoding="utf-8")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _manifest_to_json(manifest: DotfilesPlanManifest) -> dict[str, Any]:
|
|
229
|
+
return {
|
|
230
|
+
"schema_version": manifest.schema_version,
|
|
231
|
+
"plans": [_entry_to_json(entry) for entry in manifest.plans],
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _entry_to_json(entry: DotfilesPlanEntry) -> dict[str, Any]:
|
|
236
|
+
return {
|
|
237
|
+
"acceptance_criteria_count": entry.acceptance_criteria_count,
|
|
238
|
+
"created_at": entry.created_at,
|
|
239
|
+
"file": entry.file,
|
|
240
|
+
"handoff_ref": entry.handoff_ref,
|
|
241
|
+
"if_gates_produced": list(entry.if_gates_produced),
|
|
242
|
+
"lanes": list(entry.lanes),
|
|
243
|
+
"lifecycle": [_event_to_json(event) for event in entry.lifecycle],
|
|
244
|
+
"owner_skill": entry.owner_skill,
|
|
245
|
+
"phase_alias": entry.phase_alias,
|
|
246
|
+
"reflection_ref": entry.reflection_ref,
|
|
247
|
+
"roadmap_ref": _ref_to_json(entry.roadmap_ref) if entry.roadmap_ref else None,
|
|
248
|
+
"slug": entry.slug,
|
|
249
|
+
"status": entry.status,
|
|
250
|
+
"task_summary": entry.task_summary,
|
|
251
|
+
"type": entry.type,
|
|
252
|
+
"updated_at": entry.updated_at,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _event_to_json(event: DotfilesPlanLifecycleEvent) -> dict[str, Any]:
|
|
257
|
+
return {
|
|
258
|
+
"at": event.at,
|
|
259
|
+
"by": event.by,
|
|
260
|
+
"metadata": event.metadata,
|
|
261
|
+
"transition": event.transition,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _ref_to_json(ref: DotfilesPlanRef) -> dict[str, Any]:
|
|
266
|
+
return {"file": ref.file, "slug": ref.slug, "status": ref.status, "type": ref.type}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _manifest_from_json(data: Any) -> DotfilesPlanManifest:
|
|
270
|
+
if not isinstance(data, dict):
|
|
271
|
+
raise ValueError("manifest must be an object")
|
|
272
|
+
plans = data.get("plans", [])
|
|
273
|
+
if not isinstance(plans, list):
|
|
274
|
+
raise ValueError("manifest plans must be an array")
|
|
275
|
+
return DotfilesPlanManifest(
|
|
276
|
+
schema_version=int(data.get("schema_version", 0)),
|
|
277
|
+
plans=tuple(_entry_from_json(entry) for entry in plans),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _entry_from_json(data: Any) -> DotfilesPlanEntry:
|
|
282
|
+
if not isinstance(data, dict):
|
|
283
|
+
raise ValueError("manifest entry must be an object")
|
|
284
|
+
roadmap_ref = data.get("roadmap_ref")
|
|
285
|
+
return DotfilesPlanEntry(
|
|
286
|
+
slug=str(data.get("slug", "")),
|
|
287
|
+
file=str(data.get("file", "")),
|
|
288
|
+
type=str(data.get("type", "")),
|
|
289
|
+
status=str(data.get("status", "")),
|
|
290
|
+
created_at=str(data.get("created_at", "")),
|
|
291
|
+
updated_at=str(data.get("updated_at", "")),
|
|
292
|
+
owner_skill=str(data.get("owner_skill", "")),
|
|
293
|
+
handoff_ref=data.get("handoff_ref"),
|
|
294
|
+
reflection_ref=data.get("reflection_ref"),
|
|
295
|
+
task_summary=data.get("task_summary"),
|
|
296
|
+
acceptance_criteria_count=data.get("acceptance_criteria_count"),
|
|
297
|
+
roadmap_ref=_ref_from_json(roadmap_ref) if roadmap_ref is not None else None,
|
|
298
|
+
phase_alias=data.get("phase_alias"),
|
|
299
|
+
if_gates_produced=tuple(data.get("if_gates_produced") or ()),
|
|
300
|
+
lanes=tuple(data.get("lanes") or ()),
|
|
301
|
+
lifecycle=tuple(_event_from_json(event) for event in data.get("lifecycle") or ()),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _event_from_json(data: Any) -> DotfilesPlanLifecycleEvent:
|
|
306
|
+
if not isinstance(data, dict):
|
|
307
|
+
raise ValueError("manifest lifecycle event must be an object")
|
|
308
|
+
metadata = data.get("metadata", {})
|
|
309
|
+
if not isinstance(metadata, dict):
|
|
310
|
+
raise ValueError("manifest lifecycle metadata must be an object")
|
|
311
|
+
return DotfilesPlanLifecycleEvent(
|
|
312
|
+
transition=str(data.get("transition", "")),
|
|
313
|
+
by=str(data.get("by", "")),
|
|
314
|
+
at=str(data.get("at", "")),
|
|
315
|
+
metadata=metadata,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _ref_from_json(data: Any) -> DotfilesPlanRef:
|
|
320
|
+
if not isinstance(data, dict):
|
|
321
|
+
raise ValueError("manifest plan ref must be an object")
|
|
322
|
+
return DotfilesPlanRef(
|
|
323
|
+
slug=str(data.get("slug", "")),
|
|
324
|
+
file=str(data.get("file", "")),
|
|
325
|
+
type=str(data.get("type", "")),
|
|
326
|
+
status=str(data.get("status", "")),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _validate_common_entry(label: str, entry: dict[str, Any], repo: Path, errors: list[str]) -> None:
|
|
331
|
+
for field_name in ("file", "type", "status", "created_at", "updated_at", "owner_skill"):
|
|
332
|
+
if not isinstance(entry.get(field_name), str) or not entry.get(field_name):
|
|
333
|
+
errors.append(f"{label}.{field_name} is required")
|
|
334
|
+
if entry.get("type") not in PLAN_TYPES:
|
|
335
|
+
errors.append(f"{label}.type must be phase or detailed")
|
|
336
|
+
if entry.get("status") not in PLAN_STATUSES:
|
|
337
|
+
errors.append(f"{label}.status is invalid")
|
|
338
|
+
file_value = entry.get("file")
|
|
339
|
+
if isinstance(file_value, str) and file_value:
|
|
340
|
+
if Path(file_value).is_absolute() or ".." in PurePosixPath(file_value).parts:
|
|
341
|
+
errors.append(f"{label}.file must be repo-relative")
|
|
342
|
+
elif not (repo / file_value).exists():
|
|
343
|
+
errors.append(f"{label}.file does not exist")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _validate_phase_entry(label: str, entry: dict[str, Any], errors: list[str]) -> None:
|
|
347
|
+
if not isinstance(entry.get("phase_alias"), str) or not entry.get("phase_alias"):
|
|
348
|
+
errors.append(f"{label}.phase_alias is required for phase entries")
|
|
349
|
+
if entry.get("task_summary") is not None or entry.get("acceptance_criteria_count") is not None:
|
|
350
|
+
errors.append(f"{label} mixes detailed-only fields into a phase entry")
|
|
351
|
+
if not isinstance(entry.get("if_gates_produced"), list):
|
|
352
|
+
errors.append(f"{label}.if_gates_produced must be an array")
|
|
353
|
+
if not isinstance(entry.get("lanes"), list):
|
|
354
|
+
errors.append(f"{label}.lanes must be an array")
|
|
355
|
+
roadmap_ref = entry.get("roadmap_ref")
|
|
356
|
+
if roadmap_ref is not None and not isinstance(roadmap_ref, dict):
|
|
357
|
+
errors.append(f"{label}.roadmap_ref must be an object or null")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _validate_detailed_entry(label: str, entry: dict[str, Any], errors: list[str]) -> None:
|
|
361
|
+
if not isinstance(entry.get("task_summary"), str) or not entry.get("task_summary"):
|
|
362
|
+
errors.append(f"{label}.task_summary is required for detailed entries")
|
|
363
|
+
if not isinstance(entry.get("acceptance_criteria_count"), int):
|
|
364
|
+
errors.append(f"{label}.acceptance_criteria_count is required for detailed entries")
|
|
365
|
+
if entry.get("roadmap_ref") is not None or entry.get("phase_alias") is not None:
|
|
366
|
+
errors.append(f"{label} mixes phase-only fields into a detailed entry")
|
|
367
|
+
if entry.get("if_gates_produced") not in (None, []):
|
|
368
|
+
errors.append(f"{label}.if_gates_produced must be empty for detailed entries")
|
|
369
|
+
if entry.get("lanes") not in (None, []):
|
|
370
|
+
errors.append(f"{label}.lanes must be empty for detailed entries")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _validate_lifecycle(label: str, lifecycle: Any, errors: list[str]) -> None:
|
|
374
|
+
if not isinstance(lifecycle, list):
|
|
375
|
+
errors.append(f"{label}.lifecycle must be an array")
|
|
376
|
+
return
|
|
377
|
+
for index, event in enumerate(lifecycle):
|
|
378
|
+
event_label = f"{label}.lifecycle[{index}]"
|
|
379
|
+
if not isinstance(event, dict):
|
|
380
|
+
errors.append(f"{event_label} must be an object")
|
|
381
|
+
continue
|
|
382
|
+
for field_name in ("transition", "by", "at"):
|
|
383
|
+
if not isinstance(event.get(field_name), str) or not event.get(field_name):
|
|
384
|
+
errors.append(f"{event_label}.{field_name} is required")
|
|
385
|
+
if not isinstance(event.get("metadata"), dict):
|
|
386
|
+
errors.append(f"{event_label}.metadata must be an object")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _utc_now() -> str:
|
|
390
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _timestamp_for_path(path: Path) -> str:
|
|
394
|
+
return datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _frontmatter_value(path: Path, field_name: str) -> str | None:
|
|
398
|
+
text = path.read_text(encoding="utf-8")
|
|
399
|
+
if not text.startswith("---\n"):
|
|
400
|
+
return None
|
|
401
|
+
end = text.find("\n---", 4)
|
|
402
|
+
if end == -1:
|
|
403
|
+
return None
|
|
404
|
+
match = re.search(rf"^{re.escape(field_name)}:\s*(.+?)\s*$", text[4:end], re.MULTILINE)
|
|
405
|
+
return match.group(1).strip() if match else None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _extract_if_gates(path: Path) -> tuple[str, ...]:
|
|
409
|
+
text = path.read_text(encoding="utf-8")
|
|
410
|
+
return tuple(dict.fromkeys(re.findall(r"\bIF-[A-Za-z0-9._-]+", text)))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _extract_lanes(path: Path) -> tuple[str, ...]:
|
|
414
|
+
text = path.read_text(encoding="utf-8")
|
|
415
|
+
return tuple(dict.fromkeys(re.findall(r"^###\s+(SL-\d+[A-Z]?)\b", text, re.MULTILINE)))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import re
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from .models import (
|
|
8
|
+
BLOCKER_CLASSES,
|
|
9
|
+
DISPATCH_CAPABILITIES,
|
|
10
|
+
EXECUTORS,
|
|
11
|
+
PHASE_STATUSES,
|
|
12
|
+
PRODUCT_LOOP_ACTIONS,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
VERIFICATION_STATUSES = ("not_run", "passed", "failed", "blocked")
|
|
17
|
+
EXECUTION_POLICY_DEFAULT_SELECTORS = ("work-unit defaults",)
|
|
18
|
+
|
|
19
|
+
DISPATCH_SECTION_RE = re.compile(
|
|
20
|
+
r"^#{2,3}\s+Dispatch Hints\s*$\n(?P<body>.*?)(?=^#{1,3}\s+\S|\Z)",
|
|
21
|
+
re.IGNORECASE | re.MULTILINE | re.DOTALL,
|
|
22
|
+
)
|
|
23
|
+
DISPATCH_SUBSECTION_RE = re.compile(
|
|
24
|
+
r"^#{3,4}\s+(?P<label>[^\n#]+?)\s*$\n(?P<body>.*?)(?=^#{3,4}\s+\S|\Z)",
|
|
25
|
+
re.IGNORECASE | re.MULTILINE | re.DOTALL,
|
|
26
|
+
)
|
|
27
|
+
DISPATCH_LINE_RE = re.compile(
|
|
28
|
+
r"^\s*-\s*(?:(?P<action>[A-Za-z][\w-]*)\s+)?"
|
|
29
|
+
r"(?P<label>preferred executors|allowed executors|fallback executors|disabled executors|required capabilities)"
|
|
30
|
+
r"\s*:\s*(?P<value>.+?)\s*$",
|
|
31
|
+
re.IGNORECASE | re.MULTILINE,
|
|
32
|
+
)
|
|
33
|
+
EXECUTION_POLICY_SECTION_RE = re.compile(
|
|
34
|
+
r"^#{2,3}\s+Execution Policy\s*$\n(?P<body>.*?)(?=^#{1,3}\s+\S|\Z)",
|
|
35
|
+
re.IGNORECASE | re.MULTILINE | re.DOTALL,
|
|
36
|
+
)
|
|
37
|
+
EXECUTION_POLICY_LINE_RE = re.compile(r"^\s*-\s*(?P<selector>[^:]+?)\s*:\s*(?P<value>.+?)\s*$", re.MULTILINE)
|
|
38
|
+
EXECUTOR_ASSIGNMENT_RE = re.compile(r"(?:^|[,;])\s*executor\s*=\s*(?P<value>`[^`]+`|[^,;]+)", re.IGNORECASE)
|
|
39
|
+
CLOSEOUT_LITERAL_RE = re.compile(
|
|
40
|
+
r"(?:^|[{,\n])\s*[\"']?(?P<field>terminal_status|verification_status|blocker_class)[\"']?\s*[:=]\s*"
|
|
41
|
+
r"(?P<value>`[^`]+`|\"[^\"]+\"|'[^']+'|[^,\n}]+)",
|
|
42
|
+
re.MULTILINE,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class ValidationFinding:
|
|
48
|
+
field_path: str
|
|
49
|
+
literal: str
|
|
50
|
+
allowed_values: tuple[str, ...]
|
|
51
|
+
suggested_fix: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_plan_dispatch_hints(
|
|
55
|
+
plan_text: str,
|
|
56
|
+
*,
|
|
57
|
+
dispatch_capabilities: Iterable[str] | None = None,
|
|
58
|
+
executors: Iterable[str] | None = None,
|
|
59
|
+
product_loop_actions: Iterable[str] | None = None,
|
|
60
|
+
) -> list[ValidationFinding]:
|
|
61
|
+
"""Validate planner-emitted protocol literals without mutating state."""
|
|
62
|
+
|
|
63
|
+
dispatch_capabilities = tuple(dispatch_capabilities or DISPATCH_CAPABILITIES)
|
|
64
|
+
executors = tuple(executors or EXECUTORS)
|
|
65
|
+
product_loop_actions = tuple(product_loop_actions or PRODUCT_LOOP_ACTIONS)
|
|
66
|
+
text = str(plan_text or "")
|
|
67
|
+
findings: list[ValidationFinding] = []
|
|
68
|
+
|
|
69
|
+
findings.extend(_validate_dispatch_hints(text, dispatch_capabilities, executors, product_loop_actions))
|
|
70
|
+
findings.extend(_validate_execution_policy(text, executors, product_loop_actions))
|
|
71
|
+
findings.extend(_validate_closeout_literals(text))
|
|
72
|
+
return findings
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _validate_dispatch_hints(
|
|
76
|
+
text: str,
|
|
77
|
+
dispatch_capabilities: tuple[str, ...],
|
|
78
|
+
executors: tuple[str, ...],
|
|
79
|
+
product_loop_actions: tuple[str, ...],
|
|
80
|
+
) -> list[ValidationFinding]:
|
|
81
|
+
findings: list[ValidationFinding] = []
|
|
82
|
+
for section in DISPATCH_SECTION_RE.finditer(text):
|
|
83
|
+
bodies = [("default", section.group("body"))]
|
|
84
|
+
bodies.extend((m.group("label").strip().lower(), m.group("body")) for m in DISPATCH_SUBSECTION_RE.finditer(section.group("body")))
|
|
85
|
+
for bucket, body in bodies:
|
|
86
|
+
if bucket not in {"default", "roadmap default", "plan default"} and bucket not in product_loop_actions:
|
|
87
|
+
findings.append(_finding("dispatch_hints.selector", bucket, product_loop_actions))
|
|
88
|
+
for line in DISPATCH_LINE_RE.finditer(body):
|
|
89
|
+
action = (line.group("action") or bucket).strip().lower()
|
|
90
|
+
if action not in {"default", "roadmap default", "plan default"} and action not in product_loop_actions:
|
|
91
|
+
findings.append(_finding("dispatch_hints.selector", action, product_loop_actions))
|
|
92
|
+
label = line.group("label").strip().lower().replace(" ", "_")
|
|
93
|
+
values = _split_literals(line.group("value"))
|
|
94
|
+
allowed = dispatch_capabilities if label == "required_capabilities" else executors
|
|
95
|
+
for index, literal in enumerate(values):
|
|
96
|
+
if literal not in allowed:
|
|
97
|
+
findings.append(_finding(f"dispatch_hints.{label}[{index}]", literal, allowed))
|
|
98
|
+
return _dedupe(findings)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_execution_policy(
|
|
102
|
+
text: str,
|
|
103
|
+
executors: tuple[str, ...],
|
|
104
|
+
product_loop_actions: tuple[str, ...],
|
|
105
|
+
) -> list[ValidationFinding]:
|
|
106
|
+
findings: list[ValidationFinding] = []
|
|
107
|
+
allowed_selectors = (*EXECUTION_POLICY_DEFAULT_SELECTORS, *product_loop_actions, "SL-<N>")
|
|
108
|
+
for section in EXECUTION_POLICY_SECTION_RE.finditer(text):
|
|
109
|
+
for line in EXECUTION_POLICY_LINE_RE.finditer(section.group("body")):
|
|
110
|
+
selector = _clean_literal(line.group("selector"))
|
|
111
|
+
selector_key = selector.lower()
|
|
112
|
+
if not (
|
|
113
|
+
selector_key in EXECUTION_POLICY_DEFAULT_SELECTORS
|
|
114
|
+
or selector_key in product_loop_actions
|
|
115
|
+
or re.fullmatch(r"SL-\d+[A-Z]?", selector, re.IGNORECASE)
|
|
116
|
+
):
|
|
117
|
+
findings.append(_finding("execution_policy.selector", selector, allowed_selectors))
|
|
118
|
+
for assignment in EXECUTOR_ASSIGNMENT_RE.finditer(line.group("value")):
|
|
119
|
+
literal = _clean_literal(assignment.group("value"))
|
|
120
|
+
if literal not in executors:
|
|
121
|
+
findings.append(_finding(f"execution_policy.{selector}.executor", literal, executors))
|
|
122
|
+
return _dedupe(findings)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _validate_closeout_literals(text: str) -> list[ValidationFinding]:
|
|
126
|
+
allowed_by_field = {
|
|
127
|
+
"terminal_status": PHASE_STATUSES,
|
|
128
|
+
"verification_status": VERIFICATION_STATUSES,
|
|
129
|
+
"blocker_class": (*BLOCKER_CLASSES, "none"),
|
|
130
|
+
}
|
|
131
|
+
findings: list[ValidationFinding] = []
|
|
132
|
+
for match in CLOSEOUT_LITERAL_RE.finditer(text):
|
|
133
|
+
field = match.group("field")
|
|
134
|
+
literal = _clean_literal(match.group("value"))
|
|
135
|
+
if literal in {"null", "None"}:
|
|
136
|
+
continue
|
|
137
|
+
allowed = allowed_by_field[field]
|
|
138
|
+
if literal not in allowed:
|
|
139
|
+
findings.append(_finding(f"closeout.{field}", literal, allowed))
|
|
140
|
+
return _dedupe(findings)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _split_literals(value: str) -> list[str]:
|
|
144
|
+
return [literal for literal in (_clean_literal(part) for part in value.split(",")) if literal]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _clean_literal(value: str) -> str:
|
|
148
|
+
literal = str(value or "").strip().rstrip(",")
|
|
149
|
+
literal = literal.split(" #", 1)[0].strip()
|
|
150
|
+
if (literal.startswith("`") and literal.endswith("`")) or (literal.startswith('"') and literal.endswith('"')) or (
|
|
151
|
+
literal.startswith("'") and literal.endswith("'")
|
|
152
|
+
):
|
|
153
|
+
literal = literal[1:-1].strip()
|
|
154
|
+
return literal
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _finding(field_path: str, literal: str, allowed_values: Iterable[str]) -> ValidationFinding:
|
|
158
|
+
allowed = tuple(allowed_values)
|
|
159
|
+
return ValidationFinding(
|
|
160
|
+
field_path=field_path,
|
|
161
|
+
literal=literal,
|
|
162
|
+
allowed_values=allowed,
|
|
163
|
+
suggested_fix=f"Use one of: {', '.join(allowed)}",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _dedupe(findings: Iterable[ValidationFinding]) -> list[ValidationFinding]:
|
|
168
|
+
seen: set[tuple[str, str]] = set()
|
|
169
|
+
deduped: list[ValidationFinding] = []
|
|
170
|
+
for finding in findings:
|
|
171
|
+
key = (finding.field_path, finding.literal)
|
|
172
|
+
if key in seen:
|
|
173
|
+
continue
|
|
174
|
+
seen.add(key)
|
|
175
|
+
deduped.append(finding)
|
|
176
|
+
return deduped
|