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,642 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate_plan_doc.py — mechanical lint for phase-plan docs.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
validate_plan_doc.py <plan-path>
|
|
7
|
+
|
|
8
|
+
Checks the plan doc produced by /claude-plan-phase before it's handed off to
|
|
9
|
+
/claude-execute-phase. Exit 0 on clean pass; non-zero with a human-readable
|
|
10
|
+
failure list on stderr.
|
|
11
|
+
|
|
12
|
+
Checks (in order; all run even if earlier ones fail so the author sees
|
|
13
|
+
every issue at once):
|
|
14
|
+
|
|
15
|
+
(A) Required top-level headings present:
|
|
16
|
+
# <anything>
|
|
17
|
+
## Context
|
|
18
|
+
## Interface Freeze Gates
|
|
19
|
+
## Lane Index & Dependencies
|
|
20
|
+
## Lanes
|
|
21
|
+
## Execution Notes
|
|
22
|
+
## Acceptance Criteria
|
|
23
|
+
## Verification
|
|
24
|
+
Optional: ## Cross-Repo Gates
|
|
25
|
+
|
|
26
|
+
(B) Lane Index stanzas parse cleanly: each SL-N has
|
|
27
|
+
Depends on: comma-separated SL-IDs or (none)
|
|
28
|
+
Blocks: comma-separated SL-IDs or (none)
|
|
29
|
+
Parallel-safe: yes | no | mixed
|
|
30
|
+
|
|
31
|
+
(C) DAG has no cycles (topological sort succeeds).
|
|
32
|
+
|
|
33
|
+
(D) `Owned files` globs across ### SL-N sections are disjoint.
|
|
34
|
+
If run inside a git repo, expand each glob via `git ls-files`
|
|
35
|
+
and check for overlap. Otherwise fall back to textual prefix
|
|
36
|
+
comparison — flag any exact glob duplication and warn about
|
|
37
|
+
unresolvable cases.
|
|
38
|
+
|
|
39
|
+
(E) Every `impl` task row in a lane's task table has a preceding
|
|
40
|
+
`test` task in the same lane.
|
|
41
|
+
|
|
42
|
+
(F) Every `Interfaces consumed` entry either appears in some upstream
|
|
43
|
+
lane's `Interfaces provided` or is tagged "(pre-existing)".
|
|
44
|
+
|
|
45
|
+
Design: zero external deps (stdlib only). Parses markdown by regex on
|
|
46
|
+
stable headings produced by the claude-plan-phase template — not a full
|
|
47
|
+
Markdown parser, which would be overkill.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import re
|
|
53
|
+
import subprocess
|
|
54
|
+
import sys
|
|
55
|
+
from dataclasses import dataclass, field
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Data model
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Lane:
|
|
65
|
+
sl_id: str # e.g. "SL-1"
|
|
66
|
+
name: str # free-text after —
|
|
67
|
+
depends_on: List[str] = field(default_factory=list)
|
|
68
|
+
blocks: List[str] = field(default_factory=list)
|
|
69
|
+
parallel_safe: str = "" # yes | no | mixed
|
|
70
|
+
owned_globs: List[str] = field(default_factory=list)
|
|
71
|
+
interfaces_provided: List[str] = field(default_factory=list)
|
|
72
|
+
interfaces_consumed: List[str] = field(default_factory=list)
|
|
73
|
+
task_rows: List[dict] = field(default_factory=list) # parsed from the markdown table
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Helpers
|
|
78
|
+
|
|
79
|
+
def _read(path: Path) -> str:
|
|
80
|
+
try:
|
|
81
|
+
return path.read_text(encoding="utf-8")
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
_fail(f"cannot read {path}: {exc}")
|
|
84
|
+
sys.exit(2)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _top_headings(src: str) -> Set[str]:
|
|
88
|
+
return {
|
|
89
|
+
line[3:].strip()
|
|
90
|
+
for line in src.splitlines()
|
|
91
|
+
if line.startswith("## ")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _has_h1(src: str) -> bool:
|
|
96
|
+
for line in src.splitlines():
|
|
97
|
+
if line.startswith("# ") and not line.startswith("## "):
|
|
98
|
+
return True
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_section(src: str, heading: str) -> str:
|
|
103
|
+
"""Return the body of a ## heading up to the next ## heading or EOF."""
|
|
104
|
+
lines = src.splitlines()
|
|
105
|
+
want = f"## {heading}"
|
|
106
|
+
in_section = False
|
|
107
|
+
out: List[str] = []
|
|
108
|
+
for line in lines:
|
|
109
|
+
if line.startswith("## ") and line.strip() == want:
|
|
110
|
+
in_section = True
|
|
111
|
+
continue
|
|
112
|
+
if in_section and line.startswith("## "):
|
|
113
|
+
break
|
|
114
|
+
if in_section:
|
|
115
|
+
out.append(line)
|
|
116
|
+
return "\n".join(out)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_SL_HEADER_RE = re.compile(r"^(SL-\d+)\s*—\s*(.+?)\s*$")
|
|
120
|
+
_KEY_VALUE_RE = re.compile(r"^\s*(Depends on|Blocks|Parallel-safe)\s*:\s*(.*?)\s*$")
|
|
121
|
+
_COMMA_SPLIT_RE = re.compile(r"[,\s]+")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _parse_lane_index(body: str) -> List[Lane]:
|
|
125
|
+
"""Parse stanzas of the form:
|
|
126
|
+
SL-1 — name
|
|
127
|
+
Depends on: (none)
|
|
128
|
+
Blocks: SL-3, SL-4
|
|
129
|
+
Parallel-safe: yes
|
|
130
|
+
"""
|
|
131
|
+
lanes: List[Lane] = []
|
|
132
|
+
current: Optional[Lane] = None
|
|
133
|
+
for line in body.splitlines():
|
|
134
|
+
if not line.strip():
|
|
135
|
+
continue
|
|
136
|
+
m_header = _SL_HEADER_RE.match(line)
|
|
137
|
+
if m_header:
|
|
138
|
+
if current is not None:
|
|
139
|
+
lanes.append(current)
|
|
140
|
+
current = Lane(sl_id=m_header.group(1), name=m_header.group(2))
|
|
141
|
+
continue
|
|
142
|
+
if current is None:
|
|
143
|
+
continue
|
|
144
|
+
m_kv = _KEY_VALUE_RE.match(line)
|
|
145
|
+
if not m_kv:
|
|
146
|
+
continue
|
|
147
|
+
key, value = m_kv.group(1), m_kv.group(2)
|
|
148
|
+
if key == "Depends on":
|
|
149
|
+
current.depends_on = _parse_sl_list(value)
|
|
150
|
+
elif key == "Blocks":
|
|
151
|
+
current.blocks = _parse_sl_list(value)
|
|
152
|
+
elif key == "Parallel-safe":
|
|
153
|
+
current.parallel_safe = value.split()[0] if value.strip() else ""
|
|
154
|
+
if current is not None:
|
|
155
|
+
lanes.append(current)
|
|
156
|
+
return lanes
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _parse_sl_list(value: str) -> List[str]:
|
|
160
|
+
value = value.strip()
|
|
161
|
+
if not value or value.lower() == "(none)":
|
|
162
|
+
return []
|
|
163
|
+
return [tok.strip() for tok in re.split(r",\s*", value) if tok.strip()]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Regex for per-lane ### SL-N sections inside ## Lanes
|
|
167
|
+
_LANE_SECTION_RE = re.compile(r"^###\s+(SL-\d+)\b.*$", re.MULTILINE)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _split_lane_sections(lanes_body: str) -> Dict[str, str]:
|
|
171
|
+
"""Map SL-N -> section body (text between this ### heading and the next)."""
|
|
172
|
+
matches = list(_LANE_SECTION_RE.finditer(lanes_body))
|
|
173
|
+
out: Dict[str, str] = {}
|
|
174
|
+
for idx, m in enumerate(matches):
|
|
175
|
+
sl_id = m.group(1)
|
|
176
|
+
start = m.end()
|
|
177
|
+
end = matches[idx + 1].start() if idx + 1 < len(matches) else len(lanes_body)
|
|
178
|
+
out[sl_id] = lanes_body[start:end]
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
_BULLET_VALUE_RE = re.compile(
|
|
183
|
+
r"^\s*-\s*\*\*(Scope|Owned files|Interfaces provided|Interfaces consumed)\*\*\s*:\s*(.*?)\s*$",
|
|
184
|
+
re.IGNORECASE,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse_lane_section(body: str) -> dict:
|
|
189
|
+
"""Extract Scope, Owned files, Interfaces provided, Interfaces consumed,
|
|
190
|
+
and task rows from a single ### SL-N section body."""
|
|
191
|
+
out = {
|
|
192
|
+
"owned_globs": [],
|
|
193
|
+
"interfaces_provided": [],
|
|
194
|
+
"interfaces_consumed": [],
|
|
195
|
+
"task_rows": [],
|
|
196
|
+
}
|
|
197
|
+
# Collect bullet values (may span multiple lines if formatted oddly;
|
|
198
|
+
# stick to single-line bullets per the template).
|
|
199
|
+
for line in body.splitlines():
|
|
200
|
+
m = _BULLET_VALUE_RE.match(line)
|
|
201
|
+
if not m:
|
|
202
|
+
continue
|
|
203
|
+
key = m.group(1).lower()
|
|
204
|
+
val = m.group(2)
|
|
205
|
+
items = _split_inline_items(val)
|
|
206
|
+
if key == "owned files":
|
|
207
|
+
out["owned_globs"] = items
|
|
208
|
+
elif key == "interfaces provided":
|
|
209
|
+
out["interfaces_provided"] = items
|
|
210
|
+
elif key == "interfaces consumed":
|
|
211
|
+
out["interfaces_consumed"] = items
|
|
212
|
+
# Parse task table rows. The plan template uses:
|
|
213
|
+
# | Task ID | Type | Depends on | Files in scope | Tests owned | Test command |
|
|
214
|
+
out["task_rows"] = _parse_task_table(body)
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
_CODE_TOKEN_RE = re.compile(r"`([^`]+)`")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _split_inline_items(raw: str) -> List[str]:
|
|
222
|
+
"""Extract items from an inline list. Prefer backtick-quoted tokens; else
|
|
223
|
+
fall back to comma-splitting. Normalizes '(none)' to [].
|
|
224
|
+
|
|
225
|
+
Propagates an inline '(pre-existing)' annotation onto EVERY extracted
|
|
226
|
+
token so downstream checks (e.g., _check_f_interfaces_trace) can see
|
|
227
|
+
the annotation even when the user wrote it outside the backticks,
|
|
228
|
+
e.g. ``- **Interfaces consumed**: `IPlugin` (pre-existing)``.
|
|
229
|
+
"""
|
|
230
|
+
s = raw.strip()
|
|
231
|
+
if not s or s.lower() in {"(none)", "none", "—"}:
|
|
232
|
+
return []
|
|
233
|
+
has_pre_existing = "(pre-existing)" in s.lower()
|
|
234
|
+
tokens = _CODE_TOKEN_RE.findall(s)
|
|
235
|
+
if tokens:
|
|
236
|
+
items = [t.strip() for t in tokens if t.strip()]
|
|
237
|
+
else:
|
|
238
|
+
items = [tok.strip().strip("`") for tok in re.split(r",\s*", s) if tok.strip()]
|
|
239
|
+
if has_pre_existing:
|
|
240
|
+
items = [f"{t} (pre-existing)" for t in items]
|
|
241
|
+
return items
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _parse_task_table(body: str) -> List[dict]:
|
|
245
|
+
"""Parse the markdown task table out of a lane body.
|
|
246
|
+
|
|
247
|
+
Expected header (case-insensitive):
|
|
248
|
+
| Task ID | Type | Depends on | Files in scope | Tests owned | Test command |
|
|
249
|
+
"""
|
|
250
|
+
rows: List[dict] = []
|
|
251
|
+
lines = body.splitlines()
|
|
252
|
+
header_idx: Optional[int] = None
|
|
253
|
+
for idx, line in enumerate(lines):
|
|
254
|
+
if (
|
|
255
|
+
line.startswith("|")
|
|
256
|
+
and "Task ID" in line
|
|
257
|
+
and "Type" in line
|
|
258
|
+
and "Depends" in line
|
|
259
|
+
):
|
|
260
|
+
header_idx = idx
|
|
261
|
+
break
|
|
262
|
+
if header_idx is None:
|
|
263
|
+
return rows
|
|
264
|
+
# Skip the separator row (---|---|).
|
|
265
|
+
for line in lines[header_idx + 2:]:
|
|
266
|
+
if not line.startswith("|"):
|
|
267
|
+
break
|
|
268
|
+
cells = [c.strip() for c in line.split("|")[1:-1]]
|
|
269
|
+
if len(cells) < 2:
|
|
270
|
+
continue
|
|
271
|
+
task_id = cells[0].strip("`")
|
|
272
|
+
task_type = cells[1].strip("`").lower()
|
|
273
|
+
rows.append({"task_id": task_id, "type": task_type})
|
|
274
|
+
return rows
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
# Checks
|
|
279
|
+
|
|
280
|
+
Findings = List[str] # human-readable error strings
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _check_a_required_headings(src: str) -> Findings:
|
|
284
|
+
out: Findings = []
|
|
285
|
+
if not _has_h1(src):
|
|
286
|
+
out.append("(A) missing top-level `# <PHASE_ID>:` heading")
|
|
287
|
+
required = [
|
|
288
|
+
"Context",
|
|
289
|
+
"Interface Freeze Gates",
|
|
290
|
+
"Lane Index & Dependencies",
|
|
291
|
+
"Lanes",
|
|
292
|
+
"Execution Notes",
|
|
293
|
+
"Acceptance Criteria",
|
|
294
|
+
"Verification",
|
|
295
|
+
]
|
|
296
|
+
found = _top_headings(src)
|
|
297
|
+
for heading in required:
|
|
298
|
+
if heading not in found:
|
|
299
|
+
out.append(f"(A) missing required heading: `## {heading}`")
|
|
300
|
+
return out
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _check_b_lane_index_parses(lanes: List[Lane]) -> Findings:
|
|
304
|
+
out: Findings = []
|
|
305
|
+
if not lanes:
|
|
306
|
+
out.append("(B) `## Lane Index & Dependencies` has no parseable `SL-N — name` stanzas")
|
|
307
|
+
return out
|
|
308
|
+
for lane in lanes:
|
|
309
|
+
if not lane.parallel_safe:
|
|
310
|
+
out.append(f"(B) {lane.sl_id}: missing `Parallel-safe:` line")
|
|
311
|
+
elif lane.parallel_safe not in {"yes", "no", "mixed"}:
|
|
312
|
+
out.append(
|
|
313
|
+
f"(B) {lane.sl_id}: `Parallel-safe: {lane.parallel_safe}` not in {{yes, no, mixed}}"
|
|
314
|
+
)
|
|
315
|
+
return out
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _check_c_dag_acyclic(lanes: List[Lane]) -> Findings:
|
|
319
|
+
"""Kahn's algorithm. Nodes with no incoming edges go first; remove them
|
|
320
|
+
and their outgoing edges; repeat. If we can't drain the graph, there's
|
|
321
|
+
a cycle."""
|
|
322
|
+
out: Findings = []
|
|
323
|
+
if not lanes:
|
|
324
|
+
return out
|
|
325
|
+
# Validate referenced IDs exist.
|
|
326
|
+
known_ids = {lane.sl_id for lane in lanes}
|
|
327
|
+
for lane in lanes:
|
|
328
|
+
for dep in lane.depends_on:
|
|
329
|
+
if dep not in known_ids:
|
|
330
|
+
out.append(f"(C) {lane.sl_id}: depends on unknown lane `{dep}`")
|
|
331
|
+
if out:
|
|
332
|
+
return out
|
|
333
|
+
in_deg: Dict[str, int] = {lane.sl_id: len(lane.depends_on) for lane in lanes}
|
|
334
|
+
reverse_edges: Dict[str, List[str]] = {lane.sl_id: [] for lane in lanes}
|
|
335
|
+
for lane in lanes:
|
|
336
|
+
for dep in lane.depends_on:
|
|
337
|
+
reverse_edges[dep].append(lane.sl_id)
|
|
338
|
+
queue = [sl for sl, d in in_deg.items() if d == 0]
|
|
339
|
+
visited = 0
|
|
340
|
+
while queue:
|
|
341
|
+
node = queue.pop(0)
|
|
342
|
+
visited += 1
|
|
343
|
+
for dependent in reverse_edges[node]:
|
|
344
|
+
in_deg[dependent] -= 1
|
|
345
|
+
if in_deg[dependent] == 0:
|
|
346
|
+
queue.append(dependent)
|
|
347
|
+
if visited != len(lanes):
|
|
348
|
+
unresolved = [sl for sl, d in in_deg.items() if d > 0]
|
|
349
|
+
out.append(f"(C) lane DAG has a cycle; unresolved after topological sort: {unresolved}")
|
|
350
|
+
return out
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _git_ls_files(repo_root: Path) -> Optional[List[str]]:
|
|
354
|
+
try:
|
|
355
|
+
r = subprocess.run(
|
|
356
|
+
["git", "ls-files"],
|
|
357
|
+
cwd=repo_root,
|
|
358
|
+
capture_output=True,
|
|
359
|
+
text=True,
|
|
360
|
+
check=True,
|
|
361
|
+
)
|
|
362
|
+
except Exception:
|
|
363
|
+
return None
|
|
364
|
+
return [line for line in r.stdout.splitlines() if line]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _glob_to_regex(pattern: str) -> re.Pattern:
|
|
368
|
+
"""Minimal glob to regex: ** → any depth; * → any chars not /; everything else literal."""
|
|
369
|
+
s = pattern.strip("`").strip()
|
|
370
|
+
# Escape regex specials, then un-escape our own glob metacharacters.
|
|
371
|
+
esc = re.escape(s)
|
|
372
|
+
esc = esc.replace(r"\*\*", ".*").replace(r"\*", "[^/]*").replace(r"\?", ".")
|
|
373
|
+
return re.compile("^" + esc + "$")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _check_d_owned_files_disjoint(
|
|
377
|
+
lane_sections: Dict[str, dict],
|
|
378
|
+
repo_root: Optional[Path],
|
|
379
|
+
) -> Findings:
|
|
380
|
+
out: Findings = []
|
|
381
|
+
tracked: Optional[List[str]] = None
|
|
382
|
+
if repo_root is not None:
|
|
383
|
+
tracked = _git_ls_files(repo_root)
|
|
384
|
+
|
|
385
|
+
# First: exact glob duplication check.
|
|
386
|
+
glob_origin: Dict[str, str] = {}
|
|
387
|
+
for sl_id, parsed in lane_sections.items():
|
|
388
|
+
for glob in parsed["owned_globs"]:
|
|
389
|
+
norm = glob.strip().strip("`")
|
|
390
|
+
if not norm:
|
|
391
|
+
continue
|
|
392
|
+
if norm in glob_origin and glob_origin[norm] != sl_id:
|
|
393
|
+
out.append(
|
|
394
|
+
f"(D) duplicate owned glob `{norm}` in {glob_origin[norm]} and {sl_id}"
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
glob_origin[norm] = sl_id
|
|
398
|
+
|
|
399
|
+
if tracked is None:
|
|
400
|
+
# No repo context — skip expansion, only the exact-duplicate check ran.
|
|
401
|
+
out.append(
|
|
402
|
+
"(D) WARN: not running inside a git repo; owned-file disjointness only checked for exact duplicates"
|
|
403
|
+
)
|
|
404
|
+
return out
|
|
405
|
+
|
|
406
|
+
# Expand each lane's globs to a concrete file set, then intersect.
|
|
407
|
+
lane_file_sets: Dict[str, Set[str]] = {}
|
|
408
|
+
for sl_id, parsed in lane_sections.items():
|
|
409
|
+
matched: Set[str] = set()
|
|
410
|
+
for glob in parsed["owned_globs"]:
|
|
411
|
+
norm = glob.strip().strip("`")
|
|
412
|
+
if not norm:
|
|
413
|
+
continue
|
|
414
|
+
rex = _glob_to_regex(norm)
|
|
415
|
+
matched |= {p for p in tracked if rex.match(p)}
|
|
416
|
+
lane_file_sets[sl_id] = matched
|
|
417
|
+
|
|
418
|
+
sl_ids = sorted(lane_file_sets.keys())
|
|
419
|
+
for i in range(len(sl_ids)):
|
|
420
|
+
for j in range(i + 1, len(sl_ids)):
|
|
421
|
+
a, b = sl_ids[i], sl_ids[j]
|
|
422
|
+
inter = lane_file_sets[a] & lane_file_sets[b]
|
|
423
|
+
if inter:
|
|
424
|
+
sample = sorted(inter)[:5]
|
|
425
|
+
out.append(
|
|
426
|
+
f"(D) file-ownership overlap between {a} and {b}: {len(inter)} files "
|
|
427
|
+
f"(first few: {sample})"
|
|
428
|
+
)
|
|
429
|
+
return out
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _check_e_test_before_impl(lane_sections: Dict[str, dict]) -> Findings:
|
|
433
|
+
out: Findings = []
|
|
434
|
+
for sl_id, parsed in lane_sections.items():
|
|
435
|
+
seen_test = False
|
|
436
|
+
for row in parsed["task_rows"]:
|
|
437
|
+
tt = row["type"]
|
|
438
|
+
if tt == "test":
|
|
439
|
+
seen_test = True
|
|
440
|
+
elif tt == "impl" and not seen_test:
|
|
441
|
+
out.append(
|
|
442
|
+
f"(E) {sl_id} task {row['task_id']}: `impl` appears before any `test` in the same lane"
|
|
443
|
+
)
|
|
444
|
+
return out
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _check_f_interfaces_trace(
|
|
448
|
+
lane_sections: Dict[str, dict],
|
|
449
|
+
lane_sections_raw: Optional[Dict[str, str]] = None,
|
|
450
|
+
) -> Findings:
|
|
451
|
+
out: Findings = []
|
|
452
|
+
provided: Set[str] = set()
|
|
453
|
+
for parsed in lane_sections.values():
|
|
454
|
+
for sym in parsed["interfaces_provided"]:
|
|
455
|
+
provided.add(_normalize_interface(sym))
|
|
456
|
+
for sl_id, parsed in lane_sections.items():
|
|
457
|
+
# Fall back to raw body scan when the token's annotation was
|
|
458
|
+
# separated from it during parsing (e.g. ``backtick`` (pre-existing)).
|
|
459
|
+
raw_body = (lane_sections_raw or {}).get(sl_id, "")
|
|
460
|
+
for sym in parsed["interfaces_consumed"]:
|
|
461
|
+
norm = _normalize_interface(sym)
|
|
462
|
+
if not norm:
|
|
463
|
+
continue
|
|
464
|
+
if "pre-existing" in sym.lower():
|
|
465
|
+
continue
|
|
466
|
+
# Raw-body fallback: the token itself appears with a (pre-existing)
|
|
467
|
+
# annotation somewhere in the lane body (maybe on a different line).
|
|
468
|
+
if raw_body:
|
|
469
|
+
pattern = rf"`?{re.escape(norm)}`?\s*\(pre-existing\)"
|
|
470
|
+
if re.search(pattern, raw_body, re.IGNORECASE):
|
|
471
|
+
continue
|
|
472
|
+
if norm not in provided:
|
|
473
|
+
out.append(
|
|
474
|
+
f"(F) WARN: {sl_id} consumes `{sym}` but no upstream lane provides it "
|
|
475
|
+
f"(mark '(pre-existing)' if it exists outside P-this-phase)"
|
|
476
|
+
)
|
|
477
|
+
return out
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _check_g_grep_paired_with_tests(src: str) -> Findings:
|
|
481
|
+
"""Every acceptance criterion that uses `rg`/`grep` as its sole assertion
|
|
482
|
+
must cite a test file in the same bullet. A bare grep is defeatable by
|
|
483
|
+
renaming a symbol to pass the regex — pair it with a behavioral test."""
|
|
484
|
+
out: Findings = []
|
|
485
|
+
body = _extract_section(src, "Acceptance Criteria")
|
|
486
|
+
if not body:
|
|
487
|
+
return out
|
|
488
|
+
# Iterate top-level bullets. A bullet is a line starting with "- [ ]" or "- [x]".
|
|
489
|
+
# Multi-line bullets continue until the next top-level bullet or blank line.
|
|
490
|
+
lines = body.splitlines()
|
|
491
|
+
current: List[str] = []
|
|
492
|
+
bullets: List[str] = []
|
|
493
|
+
for line in lines:
|
|
494
|
+
if line.lstrip().startswith("- ["):
|
|
495
|
+
if current:
|
|
496
|
+
bullets.append("\n".join(current))
|
|
497
|
+
current = [line]
|
|
498
|
+
elif current:
|
|
499
|
+
if not line.strip():
|
|
500
|
+
bullets.append("\n".join(current))
|
|
501
|
+
current = []
|
|
502
|
+
else:
|
|
503
|
+
current.append(line)
|
|
504
|
+
if current:
|
|
505
|
+
bullets.append("\n".join(current))
|
|
506
|
+
for bullet in bullets:
|
|
507
|
+
low = bullet.lower()
|
|
508
|
+
has_grep = (
|
|
509
|
+
re.search(r"\brg\s+-?\w*\b", bullet) is not None
|
|
510
|
+
or re.search(r"\bgrep\s+-?\w*\b", bullet) is not None
|
|
511
|
+
)
|
|
512
|
+
if not has_grep:
|
|
513
|
+
continue
|
|
514
|
+
# Cite a test file if anything in the bullet looks like a test path:
|
|
515
|
+
# `tests/` or `test_` or a `.py` with a test-ish stem.
|
|
516
|
+
cites_test = (
|
|
517
|
+
"tests/" in low
|
|
518
|
+
or re.search(r"\btest_\w+\.py\b", low) is not None
|
|
519
|
+
or re.search(r"\.py\b", low) is not None
|
|
520
|
+
and ("test" in low or "pytest" in low)
|
|
521
|
+
)
|
|
522
|
+
if not cites_test:
|
|
523
|
+
first_line = bullet.strip().splitlines()[0][:90]
|
|
524
|
+
out.append(
|
|
525
|
+
f"(G) WARN: acceptance criterion uses grep/rg without citing a paired test "
|
|
526
|
+
f"file — rename-defeat risk: {first_line!r}"
|
|
527
|
+
)
|
|
528
|
+
return out
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _check_h_eager_reexport(src: str) -> Findings:
|
|
532
|
+
"""If Execution Notes says SL-0 adds re-exports to a __init__.py, require
|
|
533
|
+
the doc to also specify the __getattr__ lazy pattern. Eager top-level
|
|
534
|
+
re-exports break package load when a later lane drops or renames the
|
|
535
|
+
exported symbol."""
|
|
536
|
+
out: Findings = []
|
|
537
|
+
body = _extract_section(src, "Execution Notes")
|
|
538
|
+
if not body:
|
|
539
|
+
return out
|
|
540
|
+
low = body.lower()
|
|
541
|
+
mentions_reexport = ("re-export" in low) or ("reexport" in low)
|
|
542
|
+
mentions_init_py = "__init__.py" in low
|
|
543
|
+
if mentions_reexport and mentions_init_py and "__getattr__" not in low:
|
|
544
|
+
out.append(
|
|
545
|
+
"(H) WARN: Execution Notes mentions re-exports in __init__.py but does not "
|
|
546
|
+
"specify the `__getattr__` lazy pattern — eager top-level imports break "
|
|
547
|
+
"package load when a later lane drops or renames the exported symbol"
|
|
548
|
+
)
|
|
549
|
+
return out
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _normalize_interface(sym: str) -> str:
|
|
553
|
+
"""Strip backticks, parens, and leading/trailing whitespace. Drop annotations like '(pre-existing)'."""
|
|
554
|
+
s = sym.strip().strip("`").strip()
|
|
555
|
+
# Drop any trailing parenthesized annotation.
|
|
556
|
+
s = re.sub(r"\s*\([^)]*\)\s*$", "", s)
|
|
557
|
+
return s.strip()
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
# Main
|
|
562
|
+
|
|
563
|
+
def _fail(msg: str) -> None:
|
|
564
|
+
print(f"validate_plan_doc: {msg}", file=sys.stderr)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def main(argv: List[str]) -> int:
|
|
568
|
+
if len(argv) != 2:
|
|
569
|
+
_fail("usage: validate_plan_doc.py <plan-path>")
|
|
570
|
+
return 2
|
|
571
|
+
path = Path(argv[1])
|
|
572
|
+
if not path.exists():
|
|
573
|
+
_fail(f"plan doc not found: {path}")
|
|
574
|
+
return 2
|
|
575
|
+
|
|
576
|
+
src = _read(path)
|
|
577
|
+
|
|
578
|
+
# Work out if we're inside a git repo for the disjointness expansion.
|
|
579
|
+
repo_root: Optional[Path] = None
|
|
580
|
+
try:
|
|
581
|
+
r = subprocess.run(
|
|
582
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
583
|
+
cwd=path.parent,
|
|
584
|
+
capture_output=True,
|
|
585
|
+
text=True,
|
|
586
|
+
check=True,
|
|
587
|
+
)
|
|
588
|
+
repo_root = Path(r.stdout.strip())
|
|
589
|
+
except Exception:
|
|
590
|
+
repo_root = None
|
|
591
|
+
|
|
592
|
+
findings: Findings = []
|
|
593
|
+
findings.extend(_check_a_required_headings(src))
|
|
594
|
+
|
|
595
|
+
lane_index_body = _extract_section(src, "Lane Index & Dependencies")
|
|
596
|
+
lanes = _parse_lane_index(lane_index_body)
|
|
597
|
+
findings.extend(_check_b_lane_index_parses(lanes))
|
|
598
|
+
findings.extend(_check_c_dag_acyclic(lanes))
|
|
599
|
+
|
|
600
|
+
lanes_body = _extract_section(src, "Lanes")
|
|
601
|
+
lane_sections_raw = _split_lane_sections(lanes_body)
|
|
602
|
+
lane_sections_parsed = {
|
|
603
|
+
sl_id: _parse_lane_section(body) for sl_id, body in lane_sections_raw.items()
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
# Every lane listed in the Lane Index should also have a ### section.
|
|
607
|
+
missing_sections = [
|
|
608
|
+
lane.sl_id for lane in lanes if lane.sl_id not in lane_sections_parsed
|
|
609
|
+
]
|
|
610
|
+
for sl_id in missing_sections:
|
|
611
|
+
findings.append(f"(B) {sl_id}: no matching `### {sl_id}` section under `## Lanes`")
|
|
612
|
+
|
|
613
|
+
findings.extend(_check_d_owned_files_disjoint(lane_sections_parsed, repo_root))
|
|
614
|
+
findings.extend(_check_e_test_before_impl(lane_sections_parsed))
|
|
615
|
+
findings.extend(_check_f_interfaces_trace(lane_sections_parsed, lane_sections_raw))
|
|
616
|
+
findings.extend(_check_g_grep_paired_with_tests(src))
|
|
617
|
+
findings.extend(_check_h_eager_reexport(src))
|
|
618
|
+
|
|
619
|
+
# Partition findings into errors vs warnings.
|
|
620
|
+
errors = [f for f in findings if "WARN" not in f]
|
|
621
|
+
warnings = [f for f in findings if "WARN" in f]
|
|
622
|
+
|
|
623
|
+
if warnings:
|
|
624
|
+
for w in warnings:
|
|
625
|
+
print(w, file=sys.stderr)
|
|
626
|
+
if errors:
|
|
627
|
+
for e in errors:
|
|
628
|
+
print(e, file=sys.stderr)
|
|
629
|
+
print(
|
|
630
|
+
f"validate_plan_doc: {len(errors)} error(s), {len(warnings)} warning(s)",
|
|
631
|
+
file=sys.stderr,
|
|
632
|
+
)
|
|
633
|
+
return 1
|
|
634
|
+
|
|
635
|
+
print(
|
|
636
|
+
f"validate_plan_doc: OK — {len(lanes)} lanes, {len(warnings)} warning(s)"
|
|
637
|
+
)
|
|
638
|
+
return 0
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
if __name__ == "__main__":
|
|
642
|
+
sys.exit(main(sys.argv))
|