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,215 @@
|
|
|
1
|
+
"""Durable append-only train ledger.
|
|
2
|
+
|
|
3
|
+
Records per-node status transitions for a cross-repo release train. State
|
|
4
|
+
lives **entirely outside any repo's ``.phase-loop/``** — it is coordinator-
|
|
5
|
+
owned.
|
|
6
|
+
|
|
7
|
+
Durability model (self-consistent):
|
|
8
|
+
- Each append is a single ``os.write(fd, encoded)`` on a file opened with
|
|
9
|
+
``O_APPEND``. The OS guarantees atomicity for writes ≤ PIPE_BUF on
|
|
10
|
+
local filesystems (typically 4 KiB); our records are far smaller.
|
|
11
|
+
- The resume reader is **tolerant**: if the final line of the file is
|
|
12
|
+
malformed (e.g. the coordinator crashed mid-write), that trailing line is
|
|
13
|
+
**silently dropped** — it is not yet committed state. A malformed line
|
|
14
|
+
at any position OTHER than the last fails loud (unexpected corruption).
|
|
15
|
+
- No temp-rename. ``events.py:read_events`` does a bare ``json.loads`` and
|
|
16
|
+
crashes on a truncated final line — this reader is **net-new**, not a
|
|
17
|
+
mirror of that function.
|
|
18
|
+
|
|
19
|
+
Record shape (one JSON object per line)::
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
"node_id": "<repo>/<roadmap>",
|
|
23
|
+
"status": "pending|running|pr_open|approved|merged|blocked",
|
|
24
|
+
"branch": "<branch-name or null>",
|
|
25
|
+
"pr_url": "<PR URL or null>",
|
|
26
|
+
"head_sha": "<draft branch HEAD SHA or null>",
|
|
27
|
+
"upstream_merge_sha": "<merged SHA or null — P4 only>",
|
|
28
|
+
"merge_order": <int or null>,
|
|
29
|
+
"ts": "<ISO-8601 UTC timestamp>"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Current state = last-record-wins per ``node_id`` (fold over append log).
|
|
33
|
+
``pr_open`` records store the draft branch HEAD SHA in ``head_sha`` (set by P3).
|
|
34
|
+
``merged`` records carry the actual merge-commit SHA in ``upstream_merge_sha``
|
|
35
|
+
(set by P4 after the merge lands). Do NOT put a draft head in
|
|
36
|
+
``upstream_merge_sha`` — that field is reserved for the real merged SHA so P4
|
|
37
|
+
can gate-on-merge without ambiguity.
|
|
38
|
+
|
|
39
|
+
Zero external deps (stdlib only).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import datetime
|
|
45
|
+
import json
|
|
46
|
+
import os
|
|
47
|
+
from dataclasses import asdict, dataclass
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import Dict, List, Optional
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Status vocabulary (IF-0-P2-1)
|
|
54
|
+
|
|
55
|
+
VALID_STATUSES = frozenset(
|
|
56
|
+
{"pending", "running", "pr_open", "approved", "merged", "blocked"}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Record shape
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class LedgerRecord:
|
|
65
|
+
"""One append to the train ledger."""
|
|
66
|
+
|
|
67
|
+
node_id: str
|
|
68
|
+
status: str
|
|
69
|
+
branch: Optional[str] = None
|
|
70
|
+
pr_url: Optional[str] = None
|
|
71
|
+
head_sha: Optional[str] = None # draft branch HEAD SHA (P3, pr_open records)
|
|
72
|
+
upstream_merge_sha: Optional[str] = None # merged-commit SHA (P4 only)
|
|
73
|
+
merge_order: Optional[int] = None
|
|
74
|
+
ts: str = "" # ISO-8601 UTC; auto-set on append if blank
|
|
75
|
+
|
|
76
|
+
def __post_init__(self) -> None:
|
|
77
|
+
if self.status not in VALID_STATUSES:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"invalid ledger status {self.status!r}; "
|
|
80
|
+
f"expected one of: {', '.join(sorted(VALID_STATUSES))}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict:
|
|
84
|
+
return {k: v for k, v in asdict(self).items()}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Ledger path convention
|
|
89
|
+
|
|
90
|
+
def default_ledger_path(coordinator_dir: Path, train_name: str) -> Path:
|
|
91
|
+
"""Return the default ledger file path for a named train.
|
|
92
|
+
|
|
93
|
+
The path is inside ``coordinator_dir`` which must **not** be under any
|
|
94
|
+
repo's ``.phase-loop/``.
|
|
95
|
+
"""
|
|
96
|
+
return coordinator_dir / f"train-{train_name}.ledger.jsonl"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Append
|
|
101
|
+
|
|
102
|
+
def append_record(path: Path, record: LedgerRecord) -> None:
|
|
103
|
+
"""Atomically append ``record`` to the ledger at ``path``.
|
|
104
|
+
|
|
105
|
+
Uses ``O_APPEND`` with a single ``os.write`` call for durability. The
|
|
106
|
+
parent directory is created if absent.
|
|
107
|
+
|
|
108
|
+
The caller is responsible for ensuring ``path`` is outside any repo's
|
|
109
|
+
``.phase-loop/`` directory.
|
|
110
|
+
"""
|
|
111
|
+
if not record.ts:
|
|
112
|
+
record = LedgerRecord(
|
|
113
|
+
node_id=record.node_id,
|
|
114
|
+
status=record.status,
|
|
115
|
+
branch=record.branch,
|
|
116
|
+
pr_url=record.pr_url,
|
|
117
|
+
head_sha=record.head_sha,
|
|
118
|
+
upstream_merge_sha=record.upstream_merge_sha,
|
|
119
|
+
merge_order=record.merge_order,
|
|
120
|
+
ts=_utc_now(),
|
|
121
|
+
)
|
|
122
|
+
_assert_not_phase_loop(path)
|
|
123
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
encoded = (json.dumps(record.to_dict(), sort_keys=True) + "\n").encode("utf-8")
|
|
125
|
+
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o666)
|
|
126
|
+
try:
|
|
127
|
+
os.write(fd, encoded)
|
|
128
|
+
finally:
|
|
129
|
+
os.close(fd)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Resume reader (tolerant — drops malformed trailing line)
|
|
134
|
+
|
|
135
|
+
def read_ledger(path: Path) -> Dict[str, LedgerRecord]:
|
|
136
|
+
"""Read the ledger, returning the **current state** per node (last-wins fold).
|
|
137
|
+
|
|
138
|
+
Tolerant resume reader:
|
|
139
|
+
- An empty file returns ``{}``.
|
|
140
|
+
- A malformed **final** line (crashed mid-write) is silently dropped.
|
|
141
|
+
- A malformed line at any other position raises :exc:`ValueError` (unexpected
|
|
142
|
+
corruption; don't silently paper over mid-file damage).
|
|
143
|
+
"""
|
|
144
|
+
if not path.exists():
|
|
145
|
+
return {}
|
|
146
|
+
|
|
147
|
+
raw_lines = path.read_bytes().decode("utf-8").splitlines(keepends=True)
|
|
148
|
+
non_empty = [ln for ln in raw_lines if ln.strip()]
|
|
149
|
+
if not non_empty:
|
|
150
|
+
return {}
|
|
151
|
+
|
|
152
|
+
state: Dict[str, LedgerRecord] = {}
|
|
153
|
+
for i, line in enumerate(non_empty):
|
|
154
|
+
stripped = line.strip()
|
|
155
|
+
is_last = i == len(non_empty) - 1
|
|
156
|
+
try:
|
|
157
|
+
obj = json.loads(stripped)
|
|
158
|
+
except json.JSONDecodeError:
|
|
159
|
+
if is_last:
|
|
160
|
+
# Tolerant: crashed mid-write — drop the trailing line
|
|
161
|
+
break
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"malformed ledger line {i + 1} (not the final line — unexpected "
|
|
164
|
+
f"corruption): {stripped!r}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
record = _dict_to_record(obj)
|
|
169
|
+
except (KeyError, ValueError) as exc:
|
|
170
|
+
if is_last:
|
|
171
|
+
break
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"invalid ledger record at line {i + 1}: {exc}"
|
|
174
|
+
) from exc
|
|
175
|
+
|
|
176
|
+
state[record.node_id] = record
|
|
177
|
+
|
|
178
|
+
return state
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def resume_state(path: Path) -> Dict[str, LedgerRecord]:
|
|
182
|
+
"""Alias for :func:`read_ledger` — returns the current per-node state."""
|
|
183
|
+
return read_ledger(path)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Helpers
|
|
188
|
+
|
|
189
|
+
def _dict_to_record(obj: dict) -> LedgerRecord:
|
|
190
|
+
return LedgerRecord(
|
|
191
|
+
node_id=obj["node_id"],
|
|
192
|
+
status=obj["status"],
|
|
193
|
+
branch=obj.get("branch"),
|
|
194
|
+
pr_url=obj.get("pr_url"),
|
|
195
|
+
head_sha=obj.get("head_sha"),
|
|
196
|
+
upstream_merge_sha=obj.get("upstream_merge_sha"),
|
|
197
|
+
merge_order=obj.get("merge_order"),
|
|
198
|
+
ts=obj.get("ts", ""),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _utc_now() -> str:
|
|
203
|
+
return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _assert_not_phase_loop(path: Path) -> None:
|
|
207
|
+
"""Fail loud if the ledger path is inside any repo's ``.phase-loop/``."""
|
|
208
|
+
parts = path.parts
|
|
209
|
+
for part in parts:
|
|
210
|
+
if part in {".phase-loop", "phase-loop"}:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"train ledger path '{path}' is inside a '.phase-loop/' directory — "
|
|
213
|
+
f"train state must never touch any repo's .phase-loop/; "
|
|
214
|
+
f"use a coordinator-owned directory instead"
|
|
215
|
+
)
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Cross-repo release-train roadmap schema, parser, and gate identity.
|
|
2
|
+
|
|
3
|
+
IF-0-P2-1 contract: train roadmap nodes = ``(repo, roadmap)``, edges =
|
|
4
|
+
``Depends on: <upstream node>`` plus a per-edge consumption-channel
|
|
5
|
+
descriptor, and a cross-repo gate identity pinned to an upstream merge SHA.
|
|
6
|
+
|
|
7
|
+
The gate identity uses the ``XGATE:`` namespace — **not** the in-repo
|
|
8
|
+
``IF-0-<alias>-<n>`` token (which is hardwired to a phase alias and has no
|
|
9
|
+
cross-repo consume side). Any attempt to reuse an ``IF-0-...`` token as a
|
|
10
|
+
node identifier or dependency reference is rejected by ``validate_train``.
|
|
11
|
+
|
|
12
|
+
This is a **new parser**, separate from the ``### Phase N (ALIAS)`` regex in
|
|
13
|
+
``roadmap_lint``. The topo-sort / cycle-check algorithm is extracted from
|
|
14
|
+
``roadmap_lint.check_dag_acyclic`` and reused unchanged.
|
|
15
|
+
|
|
16
|
+
Train roadmap Markdown format (minimal)::
|
|
17
|
+
|
|
18
|
+
# Release Train: <title>
|
|
19
|
+
|
|
20
|
+
## Nodes
|
|
21
|
+
|
|
22
|
+
### Node: <repo> / <roadmap-path>
|
|
23
|
+
|
|
24
|
+
**Depends on:** (none)
|
|
25
|
+
**Channel:** (none)
|
|
26
|
+
|
|
27
|
+
### Node: <repo2> / <roadmap2-path>
|
|
28
|
+
|
|
29
|
+
**Depends on:** <repo> / <roadmap-path>
|
|
30
|
+
**Channel:** submodule path=vendor/repo
|
|
31
|
+
|
|
32
|
+
Zero external deps (stdlib only).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Dict, List, Optional, Tuple
|
|
41
|
+
|
|
42
|
+
from .cross_repo_channel import ChannelDescriptor, parse_channel_line
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Data model — IF-0-P2-1
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class TrainNode:
|
|
50
|
+
"""Identity of one node in the release train: a ``(repo, roadmap)`` pair."""
|
|
51
|
+
|
|
52
|
+
repo: str
|
|
53
|
+
roadmap: str
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def node_id(self) -> str:
|
|
57
|
+
"""Canonical string identifier: ``<repo>/<roadmap>``."""
|
|
58
|
+
return f"{self.repo}/{self.roadmap}"
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
return f"{self.repo} / {self.roadmap}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class TrainEdge:
|
|
66
|
+
"""A directed dependency edge (upstream → downstream) with a channel.
|
|
67
|
+
|
|
68
|
+
This is the static declaration. ``gate_id(sha)`` produces the runtime
|
|
69
|
+
cross-repo gate identity once the upstream merges.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
upstream: TrainNode
|
|
73
|
+
downstream: TrainNode
|
|
74
|
+
channel: ChannelDescriptor
|
|
75
|
+
|
|
76
|
+
def gate_id(self, merge_sha: str) -> str:
|
|
77
|
+
"""Return the cross-repo gate identity token for a given upstream merge SHA.
|
|
78
|
+
|
|
79
|
+
The ``XGATE:`` namespace is distinct from ``IF-0-<alias>-<n>`` — it
|
|
80
|
+
carries a full node reference and is pinned to a content-addressed
|
|
81
|
+
merge commit rather than a phase alias.
|
|
82
|
+
"""
|
|
83
|
+
return f"XGATE:{self.upstream.node_id}@{merge_sha}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class TrainRoadmap:
|
|
88
|
+
"""Parsed cross-repo release-train roadmap (IF-0-P2-1)."""
|
|
89
|
+
|
|
90
|
+
title: str
|
|
91
|
+
nodes: List[TrainNode] = field(default_factory=list)
|
|
92
|
+
edges: List[TrainEdge] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
def topo_order(self) -> List[TrainNode]:
|
|
95
|
+
"""Return nodes in a valid serial execution order (Kahn's algorithm).
|
|
96
|
+
|
|
97
|
+
Raises ``ValueError`` if the train is not serially orderable (cycle).
|
|
98
|
+
"""
|
|
99
|
+
order = _topo_sort(self.nodes, self.edges)
|
|
100
|
+
return order
|
|
101
|
+
|
|
102
|
+
def node_by_id(self, node_id: str) -> Optional[TrainNode]:
|
|
103
|
+
for n in self.nodes:
|
|
104
|
+
if n.node_id == node_id:
|
|
105
|
+
return n
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def edges_for_downstream(self, node: TrainNode) -> List[TrainEdge]:
|
|
109
|
+
return [e for e in self.edges if e.downstream == node]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Parser
|
|
114
|
+
|
|
115
|
+
# Regex for the train-roadmap headings (new format — NOT the phase-plan regex)
|
|
116
|
+
_TITLE_RE = re.compile(r"^#\s+Release Train:\s*(.+?)\s*$", re.MULTILINE)
|
|
117
|
+
_NODE_HEADING_RE = re.compile(r"^###\s+Node:\s*(.+?)\s*$", re.MULTILINE)
|
|
118
|
+
_FIELD_RE = re.compile(
|
|
119
|
+
r"^\*\*(?P<label>Depends on|Channel):\*\*\s*(?P<value>.+)$",
|
|
120
|
+
re.MULTILINE | re.IGNORECASE,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Reject in-repo IF-gate tokens used as node / dep references
|
|
124
|
+
_IF_GATE_TOKEN_RE = re.compile(r"\bIF-0-[A-Za-z0-9]+-\d+\b")
|
|
125
|
+
|
|
126
|
+
_NONE_VALUES = frozenset({"(none)", "none", ""})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _parse_node_id(raw: str) -> TrainNode:
|
|
130
|
+
"""Parse ``<repo> / <roadmap>`` or ``<repo>/<roadmap>`` into a TrainNode."""
|
|
131
|
+
raw = raw.strip()
|
|
132
|
+
if " / " in raw:
|
|
133
|
+
repo, roadmap = raw.split(" / ", 1)
|
|
134
|
+
elif "/" in raw:
|
|
135
|
+
repo, roadmap = raw.split("/", 1)
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(f"invalid node identifier (expected '<repo> / <roadmap>'): {raw!r}")
|
|
138
|
+
return TrainNode(repo=repo.strip(), roadmap=roadmap.strip())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _extract_node_bodies(text: str) -> List[Tuple[str, str]]:
|
|
142
|
+
"""Return list of (raw_node_id, body_text) for each ``### Node:`` block."""
|
|
143
|
+
matches = list(_NODE_HEADING_RE.finditer(text))
|
|
144
|
+
result = []
|
|
145
|
+
for i, m in enumerate(matches):
|
|
146
|
+
raw_id = m.group(1)
|
|
147
|
+
body_start = m.end()
|
|
148
|
+
body_end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
149
|
+
result.append((raw_id, text[body_start:body_end]))
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def parse_train_roadmap(text: str) -> TrainRoadmap:
|
|
154
|
+
"""Parse train-roadmap markdown text into a :class:`TrainRoadmap`.
|
|
155
|
+
|
|
156
|
+
Raises :exc:`ValueError` on malformed input (not a validation function —
|
|
157
|
+
use :func:`validate_train` for full semantic checks).
|
|
158
|
+
"""
|
|
159
|
+
title_m = _TITLE_RE.search(text)
|
|
160
|
+
if not title_m:
|
|
161
|
+
raise ValueError("missing '# Release Train: <title>' heading")
|
|
162
|
+
title = title_m.group(1)
|
|
163
|
+
|
|
164
|
+
node_bodies = _extract_node_bodies(text)
|
|
165
|
+
if not node_bodies:
|
|
166
|
+
raise ValueError("no '### Node: <repo> / <roadmap>' blocks found")
|
|
167
|
+
|
|
168
|
+
nodes: List[TrainNode] = []
|
|
169
|
+
raw_edges: List[Tuple[TrainNode, str, str]] = [] # (downstream, dep_id, channel_raw)
|
|
170
|
+
|
|
171
|
+
for raw_id, body in node_bodies:
|
|
172
|
+
node = _parse_node_id(raw_id)
|
|
173
|
+
nodes.append(node)
|
|
174
|
+
|
|
175
|
+
fields: Dict[str, str] = {}
|
|
176
|
+
for m in _FIELD_RE.finditer(body):
|
|
177
|
+
fields[m.group("label").lower()] = m.group("value").strip()
|
|
178
|
+
|
|
179
|
+
dep_raw = fields.get("depends on", "(none)")
|
|
180
|
+
channel_raw = fields.get("channel", "(none)")
|
|
181
|
+
|
|
182
|
+
if dep_raw.strip().lower() not in _NONE_VALUES:
|
|
183
|
+
raw_edges.append((node, dep_raw.strip(), channel_raw.strip()))
|
|
184
|
+
|
|
185
|
+
# Resolve edges
|
|
186
|
+
node_by_id: Dict[str, TrainNode] = {n.node_id: n for n in nodes}
|
|
187
|
+
# Also index by "repo / roadmap" string representation for lookup
|
|
188
|
+
node_by_str: Dict[str, TrainNode] = {str(n): n for n in nodes}
|
|
189
|
+
|
|
190
|
+
edges: List[TrainEdge] = []
|
|
191
|
+
for downstream, dep_id, channel_raw in raw_edges:
|
|
192
|
+
# Distinct check: reject in-repo IF-gate tokens before the node lookup so
|
|
193
|
+
# "invalid in-repo token" and "missing node" produce distinguishable errors.
|
|
194
|
+
if _IF_GATE_TOKEN_RE.search(dep_id):
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"node '{downstream.node_id}' references in-repo IF-gate token "
|
|
197
|
+
f"'{dep_id}' as a dependency; cross-repo edges use '<repo>/<roadmap>' "
|
|
198
|
+
f"node identifiers (XGATE: namespace), not IF-0-... tokens"
|
|
199
|
+
)
|
|
200
|
+
upstream = node_by_id.get(dep_id) or node_by_str.get(dep_id)
|
|
201
|
+
if upstream is None:
|
|
202
|
+
# Try normalising " / " vs "/"
|
|
203
|
+
alt = dep_id.replace(" / ", "/")
|
|
204
|
+
upstream = node_by_id.get(alt)
|
|
205
|
+
if upstream is None:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"node '{downstream.node_id}' depends on unknown node '{dep_id}'"
|
|
208
|
+
)
|
|
209
|
+
channel = parse_channel_line(channel_raw)
|
|
210
|
+
edges.append(TrainEdge(upstream=upstream, downstream=downstream, channel=channel))
|
|
211
|
+
|
|
212
|
+
return TrainRoadmap(title=title, nodes=nodes, edges=edges)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def load_train_roadmap(path: Path | str) -> TrainRoadmap:
|
|
216
|
+
"""Load and parse a train roadmap from disk."""
|
|
217
|
+
return parse_train_roadmap(Path(path).read_text(encoding="utf-8"))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Validation — extends roadmap_lint's topo algorithm with train-specific rules
|
|
222
|
+
|
|
223
|
+
def validate_train(roadmap: TrainRoadmap) -> List[str]:
|
|
224
|
+
"""Return human-readable validation issues for a parsed :class:`TrainRoadmap`.
|
|
225
|
+
|
|
226
|
+
Checks:
|
|
227
|
+
(T-A) No in-repo ``IF-0-<alias>-<n>`` tokens used as node IDs or deps.
|
|
228
|
+
(T-B) Every edge's upstream node exists in the node list.
|
|
229
|
+
(T-C) Every edge with a dependency carries a non-none channel descriptor.
|
|
230
|
+
(T-D) The cross-repo DAG is acyclic (topo-sort succeeds).
|
|
231
|
+
(T-E) Every dependency edge uses a supported channel kind: ``pin`` (with
|
|
232
|
+
``file=``) or ``submodule``. ``workspace`` channels are not
|
|
233
|
+
implemented for real consumption and are rejected here so that a
|
|
234
|
+
train with a workspace edge fails the preflight gate — opening zero
|
|
235
|
+
PRs — rather than running until injection fails mid-train.
|
|
236
|
+
"""
|
|
237
|
+
errors: List[str] = []
|
|
238
|
+
|
|
239
|
+
# (T-A) Reject in-repo IF-gate tokens
|
|
240
|
+
for node in roadmap.nodes:
|
|
241
|
+
if _IF_GATE_TOKEN_RE.search(node.node_id):
|
|
242
|
+
errors.append(
|
|
243
|
+
f"(T-A) node id '{node.node_id}' contains an in-repo IF-gate token "
|
|
244
|
+
f"(IF-0-...) — use '<repo>/<roadmap>' node identifiers instead"
|
|
245
|
+
)
|
|
246
|
+
for edge in roadmap.edges:
|
|
247
|
+
if _IF_GATE_TOKEN_RE.search(edge.upstream.node_id):
|
|
248
|
+
errors.append(
|
|
249
|
+
f"(T-A) edge depends on '{edge.upstream.node_id}' which contains an "
|
|
250
|
+
f"in-repo IF-gate token — use node identifiers instead"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# (T-B) Every upstream in an edge is a declared node
|
|
254
|
+
declared = {n.node_id for n in roadmap.nodes}
|
|
255
|
+
for edge in roadmap.edges:
|
|
256
|
+
if edge.upstream.node_id not in declared:
|
|
257
|
+
errors.append(
|
|
258
|
+
f"(T-B) edge '{edge.downstream.node_id}' depends on "
|
|
259
|
+
f"'{edge.upstream.node_id}' which is not a declared node"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# (T-C) Every dependency edge must declare a channel — a bare `(none)` is
|
|
263
|
+
# ambiguous (forgotten channel?). #47: an intentional ORDER-ONLY dependency
|
|
264
|
+
# (B must merge after A but does not consume A's artifact) is declared
|
|
265
|
+
# explicitly with `Channel: order-only`, which is NOT `none` and passes here.
|
|
266
|
+
for edge in roadmap.edges:
|
|
267
|
+
if edge.channel.kind == "none":
|
|
268
|
+
errors.append(
|
|
269
|
+
f"(T-C) edge '{edge.downstream.node_id}' → '{edge.upstream.node_id}' "
|
|
270
|
+
f"has no consumption-channel descriptor; declare a channel "
|
|
271
|
+
f"(pin/submodule) if the downstream consumes the upstream, or "
|
|
272
|
+
f"'order-only' for a merge-order-only (freeze) dependency"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# (T-E) Supported channel kinds: pin (with file=), submodule, or order-only
|
|
276
|
+
# (order-only carries no injection — it enforces merge order only, #47).
|
|
277
|
+
_SUPPORTED_KINDS = frozenset({"pin", "submodule", "order-only"})
|
|
278
|
+
for edge in roadmap.edges:
|
|
279
|
+
kind = edge.channel.kind
|
|
280
|
+
if kind == "none":
|
|
281
|
+
continue # T-C already reported this
|
|
282
|
+
if kind not in _SUPPORTED_KINDS:
|
|
283
|
+
errors.append(
|
|
284
|
+
f"(T-E) edge '{edge.downstream.node_id}' → '{edge.upstream.node_id}' "
|
|
285
|
+
f"uses unsupported channel kind '{kind}'; only 'pin' (with file=), "
|
|
286
|
+
f"'submodule', and 'order-only' are supported. "
|
|
287
|
+
f"'workspace' channels are rejected at preflight to prevent hollow injection."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# (T-D) Acyclic + serially orderable
|
|
291
|
+
if not errors:
|
|
292
|
+
try:
|
|
293
|
+
_topo_sort(roadmap.nodes, roadmap.edges)
|
|
294
|
+
except ValueError as exc:
|
|
295
|
+
errors.append(f"(T-D) {exc}")
|
|
296
|
+
|
|
297
|
+
return errors
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def validate_train_loud(roadmap: TrainRoadmap) -> None:
|
|
301
|
+
"""Raise :exc:`ValueError` if the train roadmap has any validation issues."""
|
|
302
|
+
issues = validate_train(roadmap)
|
|
303
|
+
if issues:
|
|
304
|
+
msg = "\n".join(f" • {e}" for e in issues)
|
|
305
|
+
raise ValueError(f"train roadmap has {len(issues)} issue(s):\n{msg}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Topo-sort (reused from roadmap_lint.check_dag_acyclic — Kahn's algorithm)
|
|
310
|
+
|
|
311
|
+
def _topo_sort(nodes: List[TrainNode], edges: List[TrainEdge]) -> List[TrainNode]:
|
|
312
|
+
"""Return nodes in topological order. Raises :exc:`ValueError` on a cycle."""
|
|
313
|
+
adj: Dict[str, List[str]] = {n.node_id: [] for n in nodes}
|
|
314
|
+
indeg: Dict[str, int] = {n.node_id: 0 for n in nodes}
|
|
315
|
+
for edge in edges:
|
|
316
|
+
up = edge.upstream.node_id
|
|
317
|
+
dn = edge.downstream.node_id
|
|
318
|
+
if up in adj and dn in indeg:
|
|
319
|
+
adj[up].append(dn)
|
|
320
|
+
indeg[dn] += 1
|
|
321
|
+
|
|
322
|
+
queue = [nid for nid, d in indeg.items() if d == 0]
|
|
323
|
+
order: List[str] = []
|
|
324
|
+
while queue:
|
|
325
|
+
cur = queue.pop(0)
|
|
326
|
+
order.append(cur)
|
|
327
|
+
for nb in adj[cur]:
|
|
328
|
+
indeg[nb] -= 1
|
|
329
|
+
if indeg[nb] == 0:
|
|
330
|
+
queue.append(nb)
|
|
331
|
+
|
|
332
|
+
if len(order) != len(nodes):
|
|
333
|
+
unresolved = [nid for nid, d in indeg.items() if d > 0]
|
|
334
|
+
raise ValueError(
|
|
335
|
+
f"cycle detected in train dependency DAG; unresolved nodes: {', '.join(unresolved)}"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
node_map = {n.node_id: n for n in nodes}
|
|
339
|
+
return [node_map[nid] for nid in order]
|