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,75 @@
|
|
|
1
|
+
"""Verification-evidence closeout validator (rigor-v1 P5).
|
|
2
|
+
|
|
3
|
+
Closes the generic-phase hole: today only `RG` phases or plans opting in via
|
|
4
|
+
`IF-0-RG-1`/`--verification-log` require an evidence artifact, so any other phase
|
|
5
|
+
can self-report `verification_status=passed` with nothing backing it. This
|
|
6
|
+
validator raises a finding for those generic phases too.
|
|
7
|
+
|
|
8
|
+
Autonomy-first: ``block``-severity, so under the default ``PHASE_LOOP_REVIEW=warn``
|
|
9
|
+
it is recorded and the loop continues; it blocks only on opt-in. The agent
|
|
10
|
+
self-satisfies by attaching a verification artifact or recording a typed opt-out
|
|
11
|
+
reason (``verification_evidence_opt_out``) — no human required.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Mapping
|
|
17
|
+
|
|
18
|
+
from .closeout_validators import CloseoutContext, ReviewFinding, register_closeout_validator
|
|
19
|
+
from .models import VERIFICATION_EVIDENCE_OPT_OUT_REASONS
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _has_verification_artifact(terminal: Mapping) -> bool:
|
|
23
|
+
"""Detect a verification artifact in the same places the upstream gate reads
|
|
24
|
+
(`closeout._verification_artifact_path`): the nested `artifact_paths` map, or
|
|
25
|
+
the top-level convenience key the skills tell executors to write."""
|
|
26
|
+
if terminal.get("verification_artifact_path"):
|
|
27
|
+
return True
|
|
28
|
+
artifact_paths = terminal.get("artifact_paths")
|
|
29
|
+
if isinstance(artifact_paths, Mapping):
|
|
30
|
+
return bool(
|
|
31
|
+
artifact_paths.get("verification")
|
|
32
|
+
or artifact_paths.get("verification_artifact")
|
|
33
|
+
or artifact_paths.get("verification_log")
|
|
34
|
+
or artifact_paths.get("root")
|
|
35
|
+
)
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _legacy_evidence_gate_owns(phase_alias: str, plan_path: str) -> bool:
|
|
40
|
+
"""True when the existing verification-evidence gate already governs this phase
|
|
41
|
+
(RG, or a plan opting into IF-0-RG-1/--verification-log). Those are handled
|
|
42
|
+
upstream in build_phase_loop_closeout, so this validator stays out of them."""
|
|
43
|
+
if phase_alias.upper() == "RG":
|
|
44
|
+
return True
|
|
45
|
+
try:
|
|
46
|
+
text = Path(plan_path).read_text(encoding="utf-8")
|
|
47
|
+
except OSError:
|
|
48
|
+
return False
|
|
49
|
+
return "IF-0-RG-1" in text or "--verification-log" in text
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@register_closeout_validator
|
|
53
|
+
def verification_evidence_validator(ctx: CloseoutContext) -> list[ReviewFinding]:
|
|
54
|
+
reported = str(ctx.terminal.get("verification_status") or ctx.automation.get("verification_status") or "")
|
|
55
|
+
if reported != "passed":
|
|
56
|
+
return []
|
|
57
|
+
if _legacy_evidence_gate_owns(ctx.phase_alias, ctx.plan_path):
|
|
58
|
+
return [] # owned by the upstream evidence gate
|
|
59
|
+
if _has_verification_artifact(ctx.terminal):
|
|
60
|
+
return [] # evidence attached
|
|
61
|
+
opt_out = str(ctx.terminal.get("verification_evidence_opt_out") or "").strip()
|
|
62
|
+
if opt_out in VERIFICATION_EVIDENCE_OPT_OUT_REASONS:
|
|
63
|
+
return [] # declined with a typed reason
|
|
64
|
+
return [
|
|
65
|
+
ReviewFinding(
|
|
66
|
+
code="verification_evidence_missing_generic",
|
|
67
|
+
reason=(
|
|
68
|
+
"reported verification_status=passed with no verification_artifact_path; "
|
|
69
|
+
"attach the runner verification log or record a verification_evidence_opt_out "
|
|
70
|
+
f"reason ({', '.join(VERIFICATION_EVIDENCE_OPT_OUT_REASONS)})"
|
|
71
|
+
),
|
|
72
|
+
severity="block",
|
|
73
|
+
blocker_class="review_gate_block",
|
|
74
|
+
)
|
|
75
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Visual-evidence closeout validator (rigor-v1 P6).
|
|
2
|
+
|
|
3
|
+
When a phase changes UI/visual surfaces but attaches no visual evidence
|
|
4
|
+
(screenshot path + observed outcome), raise a finding. Autonomy-first:
|
|
5
|
+
``block``-severity, so under the default ``PHASE_LOOP_REVIEW=warn`` it is
|
|
6
|
+
recorded and the loop continues; it blocks only on opt-in. The agent
|
|
7
|
+
self-satisfies by capturing a screenshot (claude-in-chrome / Playwright-via-PMCP)
|
|
8
|
+
and recording its path — no human eyeball is required to pass.
|
|
9
|
+
|
|
10
|
+
The executor records ``visual_evidence_path`` (and optionally
|
|
11
|
+
``visual_evidence_observed``) in the terminal summary.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .closeout_validators import CloseoutContext, ReviewFinding, register_closeout_validator
|
|
16
|
+
from .models import ui_change_detected
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register_closeout_validator
|
|
20
|
+
def visual_evidence_validator(ctx: CloseoutContext) -> list[ReviewFinding]:
|
|
21
|
+
if not ui_change_detected(ctx.changed_paths):
|
|
22
|
+
return []
|
|
23
|
+
if ctx.terminal.get("visual_evidence_path") or ctx.terminal.get("visual_evidence"):
|
|
24
|
+
return [] # evidence attached
|
|
25
|
+
return [
|
|
26
|
+
ReviewFinding(
|
|
27
|
+
code="visual_evidence_missing",
|
|
28
|
+
reason=(
|
|
29
|
+
"changed a UI/visual surface (*.tsx/jsx/vue/svelte/css or components/**) "
|
|
30
|
+
"but attached no visual_evidence_path; capture a screenshot "
|
|
31
|
+
"(claude-in-chrome or Playwright-via-PMCP) and record its path"
|
|
32
|
+
),
|
|
33
|
+
severity="block",
|
|
34
|
+
blocker_class="review_gate_block",
|
|
35
|
+
)
|
|
36
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from time import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .launcher import LaunchResult, LaunchSpec, launch_with_spec
|
|
12
|
+
from .observability import write_terminal_summary
|
|
13
|
+
from .runtime_paths import phase_loop_dir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PhaseWorkerJob:
|
|
18
|
+
phase: str
|
|
19
|
+
spec: LaunchSpec
|
|
20
|
+
log_path: Path | None = None
|
|
21
|
+
heartbeat_path: Path | None = None
|
|
22
|
+
dry_run: bool = False
|
|
23
|
+
stream_output: bool = False
|
|
24
|
+
heartbeat_interval_seconds: float = 30.0
|
|
25
|
+
quiet_warning_seconds: float = 300.0
|
|
26
|
+
quiet_blocker_seconds: float = 900.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class PhaseWorkerResult:
|
|
31
|
+
phase: str
|
|
32
|
+
result: LaunchResult
|
|
33
|
+
summary_path: Path
|
|
34
|
+
terminal_summary: dict[str, object]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def worker_summary_path(repo: Path, roadmap: Path, phase: str) -> Path:
|
|
38
|
+
return phase_loop_dir(repo) / _roadmap_slug(repo, roadmap) / "workers" / f"{phase}.summary.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def write_worker_summary(repo: Path, roadmap: Path, phase: str, summary: dict[str, Any]) -> Path:
|
|
42
|
+
path = worker_summary_path(repo, roadmap, phase)
|
|
43
|
+
write_terminal_summary(path, {"phase": phase, **summary})
|
|
44
|
+
return path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_worker_summary(
|
|
48
|
+
repo: Path,
|
|
49
|
+
roadmap: Path,
|
|
50
|
+
phase: str,
|
|
51
|
+
*,
|
|
52
|
+
stale_after_seconds: float = 1.0,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
path = worker_summary_path(repo, roadmap, phase)
|
|
55
|
+
if not path.exists():
|
|
56
|
+
return {"status": "missing", "path": str(path)}
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
59
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
60
|
+
stat = path.stat()
|
|
61
|
+
age_seconds = max(0.0, time() - stat.st_mtime)
|
|
62
|
+
status = "torn" if age_seconds >= stale_after_seconds and stat.st_size > 0 else "pending"
|
|
63
|
+
return {
|
|
64
|
+
"status": status,
|
|
65
|
+
"path": str(path),
|
|
66
|
+
"size": stat.st_size,
|
|
67
|
+
"age_seconds": age_seconds,
|
|
68
|
+
"error": str(exc),
|
|
69
|
+
}
|
|
70
|
+
if not isinstance(data, dict):
|
|
71
|
+
return {"status": "malformed", "path": str(path), "summary": data}
|
|
72
|
+
return {"status": "ok", "path": str(path), "summary": data}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_phase_worker_pool(
|
|
76
|
+
repo: Path,
|
|
77
|
+
roadmap: Path,
|
|
78
|
+
jobs: tuple[PhaseWorkerJob, ...] | list[PhaseWorkerJob],
|
|
79
|
+
*,
|
|
80
|
+
max_workers: int | None = None,
|
|
81
|
+
) -> tuple[PhaseWorkerResult, ...]:
|
|
82
|
+
if not jobs:
|
|
83
|
+
return ()
|
|
84
|
+
worker_count = max_workers or min(len(jobs), os.cpu_count() or 1)
|
|
85
|
+
worker_count = max(1, min(worker_count, len(jobs)))
|
|
86
|
+
by_phase = {job.phase: index for index, job in enumerate(jobs)}
|
|
87
|
+
results: list[PhaseWorkerResult] = []
|
|
88
|
+
with ThreadPoolExecutor(max_workers=worker_count) as executor:
|
|
89
|
+
futures = {executor.submit(_run_one, repo, roadmap, job): job for job in jobs}
|
|
90
|
+
for future in as_completed(futures):
|
|
91
|
+
results.append(future.result())
|
|
92
|
+
return tuple(sorted(results, key=lambda item: by_phase[item.phase]))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _run_one(repo: Path, roadmap: Path, job: PhaseWorkerJob) -> PhaseWorkerResult:
|
|
96
|
+
result = launch_with_spec(
|
|
97
|
+
job.spec,
|
|
98
|
+
dry_run=job.dry_run,
|
|
99
|
+
log_path=job.log_path,
|
|
100
|
+
heartbeat_path=job.heartbeat_path,
|
|
101
|
+
stream_output=job.stream_output,
|
|
102
|
+
heartbeat_interval_seconds=job.heartbeat_interval_seconds,
|
|
103
|
+
quiet_warning_seconds=job.quiet_warning_seconds,
|
|
104
|
+
quiet_blocker_seconds=job.quiet_blocker_seconds,
|
|
105
|
+
)
|
|
106
|
+
summary = {
|
|
107
|
+
"phase": job.phase,
|
|
108
|
+
"terminal_status": "blocked" if result.failed else "complete",
|
|
109
|
+
"verification_status": "blocked" if result.failed else "passed",
|
|
110
|
+
"returncode": result.returncode,
|
|
111
|
+
"executor": result.executor,
|
|
112
|
+
"log_path": result.log_path,
|
|
113
|
+
}
|
|
114
|
+
path = write_worker_summary(repo, roadmap, job.phase, summary)
|
|
115
|
+
return PhaseWorkerResult(phase=job.phase, result=result, summary_path=path, terminal_summary=summary)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _roadmap_slug(repo: Path, roadmap: Path) -> str:
|
|
119
|
+
try:
|
|
120
|
+
value = str(roadmap.relative_to(repo))
|
|
121
|
+
except ValueError:
|
|
122
|
+
value = roadmap.name
|
|
123
|
+
return "".join(char if char.isalnum() or char in "._-" else "-" for char in value).strip("-") or "roadmap"
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Git-derived worktree freshness index (CS-0.10a).
|
|
2
|
+
|
|
3
|
+
Answers "where is the freshest working copy of a path, and who's touching it" by
|
|
4
|
+
reading `git worktree list` and diffing each worktree's branch against the base
|
|
5
|
+
ref (default: `origin/<default-branch>`, falling back to `origin/main`). Purely
|
|
6
|
+
git-derived and READ-ONLY: it never writes repo state, never mutates the working
|
|
7
|
+
tree, and holds no persistent index of its own — every call re-derives the answer
|
|
8
|
+
from `git` at invocation time, so it can't drift from reality the way a cached
|
|
9
|
+
index could.
|
|
10
|
+
|
|
11
|
+
If a repo has no worktrees beyond the primary one, or none of them touch the
|
|
12
|
+
queried path, the answer collapses to the `origin/main` baseline entry alone —
|
|
13
|
+
there is nothing else to report.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import subprocess
|
|
18
|
+
from dataclasses import asdict, dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class WorktreeRef:
|
|
25
|
+
"""One entry from `git worktree list --porcelain`."""
|
|
26
|
+
|
|
27
|
+
path: str
|
|
28
|
+
branch: str | None
|
|
29
|
+
head_sha: str | None
|
|
30
|
+
bare: bool = False
|
|
31
|
+
detached: bool = False
|
|
32
|
+
|
|
33
|
+
def to_json(self) -> dict[str, Any]:
|
|
34
|
+
return asdict(self)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Holder:
|
|
39
|
+
"""A working copy (a worktree, or the `origin/main` baseline) that carries a
|
|
40
|
+
commit touching the queried path."""
|
|
41
|
+
|
|
42
|
+
worktree: str
|
|
43
|
+
branch: str
|
|
44
|
+
last_commit_sha: str
|
|
45
|
+
last_commit_time: str
|
|
46
|
+
|
|
47
|
+
def to_json(self) -> dict[str, Any]:
|
|
48
|
+
return asdict(self)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class PathFreshness:
|
|
53
|
+
path: str
|
|
54
|
+
holders: tuple[Holder, ...]
|
|
55
|
+
main_behind: bool
|
|
56
|
+
|
|
57
|
+
def to_json(self) -> dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
"path": self.path,
|
|
60
|
+
"holders": [holder.to_json() for holder in self.holders],
|
|
61
|
+
"main_behind": self.main_behind,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class WorktreeIndexReport:
|
|
67
|
+
base_ref: str
|
|
68
|
+
worktrees: tuple[WorktreeRef, ...]
|
|
69
|
+
paths: tuple[PathFreshness, ...]
|
|
70
|
+
|
|
71
|
+
def to_json(self) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"base_ref": self.base_ref,
|
|
74
|
+
"worktrees": [wt.to_json() for wt in self.worktrees],
|
|
75
|
+
"paths": [pf.to_json() for pf in self.paths],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _git(repo: Path, *args: str) -> str | None:
|
|
80
|
+
try:
|
|
81
|
+
completed = subprocess.run(
|
|
82
|
+
["git", "-C", str(repo), *args],
|
|
83
|
+
check=False,
|
|
84
|
+
stdout=subprocess.PIPE,
|
|
85
|
+
stderr=subprocess.DEVNULL,
|
|
86
|
+
text=True,
|
|
87
|
+
timeout=10,
|
|
88
|
+
)
|
|
89
|
+
except Exception:
|
|
90
|
+
return None
|
|
91
|
+
if completed.returncode != 0:
|
|
92
|
+
return None
|
|
93
|
+
return completed.stdout.strip() or None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_ancestor(repo: Path, sha: str, ref: str) -> bool:
|
|
97
|
+
try:
|
|
98
|
+
completed = subprocess.run(
|
|
99
|
+
["git", "-C", str(repo), "merge-base", "--is-ancestor", sha, ref],
|
|
100
|
+
check=False,
|
|
101
|
+
stdout=subprocess.DEVNULL,
|
|
102
|
+
stderr=subprocess.DEVNULL,
|
|
103
|
+
timeout=10,
|
|
104
|
+
)
|
|
105
|
+
except Exception:
|
|
106
|
+
return False
|
|
107
|
+
return completed.returncode == 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def default_base_ref(repo: Path) -> str:
|
|
111
|
+
"""`origin/<default-branch>` from the remote's HEAD symref, or `origin/main`."""
|
|
112
|
+
default_remote = _git(repo, "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD")
|
|
113
|
+
return default_remote or "origin/main"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def list_worktrees(repo: Path) -> tuple[WorktreeRef, ...]:
|
|
117
|
+
"""Parse `git worktree list --porcelain` into `WorktreeRef` entries."""
|
|
118
|
+
output = _git(repo, "worktree", "list", "--porcelain")
|
|
119
|
+
if not output:
|
|
120
|
+
return ()
|
|
121
|
+
|
|
122
|
+
entries: list[WorktreeRef] = []
|
|
123
|
+
path: str | None = None
|
|
124
|
+
head: str | None = None
|
|
125
|
+
branch: str | None = None
|
|
126
|
+
bare = False
|
|
127
|
+
detached = False
|
|
128
|
+
|
|
129
|
+
def flush() -> None:
|
|
130
|
+
nonlocal path, head, branch, bare, detached
|
|
131
|
+
if path is not None:
|
|
132
|
+
entries.append(WorktreeRef(path=path, branch=branch, head_sha=head, bare=bare, detached=detached))
|
|
133
|
+
path, head, branch, bare, detached = None, None, None, False, False
|
|
134
|
+
|
|
135
|
+
for line in output.splitlines():
|
|
136
|
+
if not line.strip():
|
|
137
|
+
flush()
|
|
138
|
+
elif line.startswith("worktree "):
|
|
139
|
+
flush()
|
|
140
|
+
path = line[len("worktree "):].strip()
|
|
141
|
+
elif line.startswith("HEAD "):
|
|
142
|
+
head = line[len("HEAD "):].strip()
|
|
143
|
+
elif line.startswith("branch "):
|
|
144
|
+
branch = line[len("branch "):].strip().removeprefix("refs/heads/")
|
|
145
|
+
elif line == "bare":
|
|
146
|
+
bare = True
|
|
147
|
+
elif line == "detached":
|
|
148
|
+
detached = True
|
|
149
|
+
flush()
|
|
150
|
+
return tuple(entries)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def branch_touched_paths(repo: Path, branch: str, base_ref: str) -> tuple[str, ...]:
|
|
154
|
+
"""Repo-relative paths `branch` touches relative to `base_ref` (`git diff --name-only`)."""
|
|
155
|
+
output = _git(repo, "diff", f"{base_ref}...{branch}", "--name-only")
|
|
156
|
+
if not output:
|
|
157
|
+
return ()
|
|
158
|
+
return tuple(line.strip() for line in output.splitlines() if line.strip())
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _last_commit(repo: Path, rev: str, path: str) -> tuple[str, str] | None:
|
|
162
|
+
output = _git(repo, "log", "-1", "--format=%H\x1f%cI", rev, "--", path)
|
|
163
|
+
if not output:
|
|
164
|
+
return None
|
|
165
|
+
parts = output.split("\x1f")
|
|
166
|
+
if len(parts) != 2:
|
|
167
|
+
return None
|
|
168
|
+
sha, when = parts
|
|
169
|
+
return sha, when
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _path_freshness(
|
|
173
|
+
repo: Path,
|
|
174
|
+
path: str,
|
|
175
|
+
base_ref: str,
|
|
176
|
+
touched_by_worktree: dict[WorktreeRef, tuple[str, ...]],
|
|
177
|
+
) -> PathFreshness:
|
|
178
|
+
holders: list[Holder] = []
|
|
179
|
+
|
|
180
|
+
baseline = _last_commit(repo, base_ref, path)
|
|
181
|
+
if baseline is not None:
|
|
182
|
+
sha, when = baseline
|
|
183
|
+
holders.append(Holder(worktree="origin/main", branch=base_ref, last_commit_sha=sha, last_commit_time=when))
|
|
184
|
+
|
|
185
|
+
main_behind = False
|
|
186
|
+
for worktree, touched in touched_by_worktree.items():
|
|
187
|
+
if path not in touched:
|
|
188
|
+
continue
|
|
189
|
+
commit = _last_commit(repo, worktree.branch or worktree.head_sha or "HEAD", path)
|
|
190
|
+
if commit is None:
|
|
191
|
+
continue
|
|
192
|
+
sha, when = commit
|
|
193
|
+
holders.append(
|
|
194
|
+
Holder(worktree=worktree.path, branch=worktree.branch or "detached", last_commit_sha=sha, last_commit_time=when)
|
|
195
|
+
)
|
|
196
|
+
if not _is_ancestor(repo, sha, base_ref):
|
|
197
|
+
main_behind = True
|
|
198
|
+
|
|
199
|
+
return PathFreshness(path=path, holders=tuple(holders), main_behind=main_behind)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def build_index(repo: Path, *, base_ref: str | None = None, path: str | None = None) -> WorktreeIndexReport:
|
|
203
|
+
"""Build the freshness index. `path=None` reports every path any active
|
|
204
|
+
worktree touches relative to `base_ref`; a repo with no diverging worktrees
|
|
205
|
+
yields an empty `paths` tuple — the answer is `origin/main`."""
|
|
206
|
+
resolved_base = base_ref or default_base_ref(repo)
|
|
207
|
+
worktrees = list_worktrees(repo)
|
|
208
|
+
|
|
209
|
+
touched_by_worktree: dict[WorktreeRef, tuple[str, ...]] = {}
|
|
210
|
+
for worktree in worktrees:
|
|
211
|
+
if worktree.bare or not worktree.branch:
|
|
212
|
+
continue
|
|
213
|
+
touched_by_worktree[worktree] = branch_touched_paths(repo, worktree.branch, resolved_base)
|
|
214
|
+
|
|
215
|
+
if path is not None:
|
|
216
|
+
candidate_paths: tuple[str, ...] = (path,)
|
|
217
|
+
else:
|
|
218
|
+
seen: dict[str, None] = {}
|
|
219
|
+
for touched in touched_by_worktree.values():
|
|
220
|
+
for touched_path in touched:
|
|
221
|
+
seen.setdefault(touched_path, None)
|
|
222
|
+
candidate_paths = tuple(sorted(seen))
|
|
223
|
+
|
|
224
|
+
paths = tuple(_path_freshness(repo, p, resolved_base, touched_by_worktree) for p in candidate_paths)
|
|
225
|
+
return WorktreeIndexReport(base_ref=resolved_base, worktrees=worktrees, paths=paths)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def render_human(report: WorktreeIndexReport) -> str:
|
|
229
|
+
lines = [f"base_ref: {report.base_ref}", f"worktrees ({len(report.worktrees)}):"]
|
|
230
|
+
for worktree in report.worktrees:
|
|
231
|
+
tag = " [detached]" if worktree.detached else ""
|
|
232
|
+
lines.append(f" {worktree.path} branch={worktree.branch or '-'}{tag} head={worktree.head_sha or '-'}")
|
|
233
|
+
|
|
234
|
+
if not report.paths:
|
|
235
|
+
lines.append("")
|
|
236
|
+
lines.append("no divergent paths relative to base_ref — origin/main is the answer")
|
|
237
|
+
return "\n".join(lines)
|
|
238
|
+
|
|
239
|
+
lines.append("")
|
|
240
|
+
for path_freshness in report.paths:
|
|
241
|
+
behind = " [main behind]" if path_freshness.main_behind else ""
|
|
242
|
+
lines.append(f"{path_freshness.path}{behind}")
|
|
243
|
+
if not path_freshness.holders:
|
|
244
|
+
lines.append(" (no holders)")
|
|
245
|
+
continue
|
|
246
|
+
for holder in path_freshness.holders:
|
|
247
|
+
lines.append(
|
|
248
|
+
f" {holder.branch:<30} {holder.worktree} {holder.last_commit_sha[:12]} {holder.last_commit_time}"
|
|
249
|
+
)
|
|
250
|
+
return "\n".join(lines)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def main(argv: list[str] | None = None) -> int:
|
|
254
|
+
import argparse
|
|
255
|
+
import json
|
|
256
|
+
|
|
257
|
+
parser = argparse.ArgumentParser(prog="phase-loop worktree-index")
|
|
258
|
+
parser.add_argument("--repo", default=".", help="Repository root (default: cwd).")
|
|
259
|
+
parser.add_argument("--path", help="Report freshness holders for a single repo-relative path.")
|
|
260
|
+
parser.add_argument("--base", help="Diff base ref (default: origin/<default-branch>, falling back to origin/main).")
|
|
261
|
+
parser.add_argument("--json", action="store_true", help="Emit the report as JSON.")
|
|
262
|
+
# tolerate the leading 'worktree-index' token when dispatched from the main CLI
|
|
263
|
+
args = parser.parse_args([a for a in (argv or []) if a != "worktree-index"])
|
|
264
|
+
|
|
265
|
+
report = build_index(Path(args.repo), base_ref=args.base, path=args.path)
|
|
266
|
+
if args.json:
|
|
267
|
+
print(json.dumps(report.to_json(), indent=2))
|
|
268
|
+
else:
|
|
269
|
+
print(render_human(report))
|
|
270
|
+
return 0
|