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,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .runtime_paths import ensure_phase_loop_excluded, phase_loop_dir
|
|
11
|
+
from .runtime_paths import _path_slug
|
|
12
|
+
|
|
13
|
+
# Platform-conditional locking primitives (issue #16).
|
|
14
|
+
# POSIX uses fcntl.flock; Windows uses msvcrt.locking on a 1-byte region at
|
|
15
|
+
# the file's current seek position. Both surface non-blocking exclusive lock
|
|
16
|
+
# acquisition + release through the same _acquire_exclusive / _release_exclusive
|
|
17
|
+
# helpers so the rest of this module stays platform-agnostic.
|
|
18
|
+
if sys.platform == "win32":
|
|
19
|
+
import msvcrt
|
|
20
|
+
|
|
21
|
+
_LOCK_REGION_BYTES = 1
|
|
22
|
+
|
|
23
|
+
def _acquire_exclusive(handle) -> None:
|
|
24
|
+
# msvcrt.locking locks a region of length _LOCK_REGION_BYTES starting
|
|
25
|
+
# at the file's current seek position. Seek to start so concurrent
|
|
26
|
+
# holders contend on the same byte. LK_NBLCK = non-blocking exclusive.
|
|
27
|
+
# Raises OSError (subclass of BlockingIOError on Py3.10+? actually
|
|
28
|
+
# raises OSError with errno EACCES) when contended; normalize to
|
|
29
|
+
# BlockingIOError to match POSIX flock semantics.
|
|
30
|
+
handle.seek(0)
|
|
31
|
+
try:
|
|
32
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, _LOCK_REGION_BYTES)
|
|
33
|
+
except OSError as exc:
|
|
34
|
+
raise BlockingIOError(str(exc)) from exc
|
|
35
|
+
|
|
36
|
+
def _release_exclusive(handle) -> None:
|
|
37
|
+
handle.seek(0)
|
|
38
|
+
try:
|
|
39
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, _LOCK_REGION_BYTES)
|
|
40
|
+
except OSError:
|
|
41
|
+
# Already released or process is exiting — let cleanup proceed.
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
else:
|
|
45
|
+
import fcntl
|
|
46
|
+
|
|
47
|
+
def _acquire_exclusive(handle) -> None:
|
|
48
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
49
|
+
|
|
50
|
+
def _release_exclusive(handle) -> None:
|
|
51
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class DispatchLockContention(RuntimeError):
|
|
56
|
+
lock_path: Path
|
|
57
|
+
holder_pid: int | None
|
|
58
|
+
holder_started_at: float | None
|
|
59
|
+
roadmap: str | None
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def elapsed_seconds(self) -> int | None:
|
|
63
|
+
if self.holder_started_at is None:
|
|
64
|
+
return None
|
|
65
|
+
return max(0, int(time.time() - self.holder_started_at))
|
|
66
|
+
|
|
67
|
+
def blocker_summary(self, roadmap: Path) -> str:
|
|
68
|
+
pid = self.holder_pid if self.holder_pid is not None else "unknown"
|
|
69
|
+
elapsed = self.elapsed_seconds
|
|
70
|
+
elapsed_text = f"{elapsed}s" if elapsed is not None else "unknown seconds"
|
|
71
|
+
holder_roadmap = self.roadmap or str(roadmap)
|
|
72
|
+
return (
|
|
73
|
+
"Concurrent dispatch refused: roadmap "
|
|
74
|
+
f"{holder_roadmap} is locked by PID {pid} for {elapsed_text} "
|
|
75
|
+
f"(lock: {self.lock_path})."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DispatchLock:
|
|
80
|
+
def __init__(self, repo: Path, roadmap: Path) -> None:
|
|
81
|
+
self.repo = repo
|
|
82
|
+
self.roadmap = roadmap
|
|
83
|
+
self.path = dispatch_lock_path(repo, roadmap)
|
|
84
|
+
self._handle = None
|
|
85
|
+
|
|
86
|
+
def acquire(self) -> "DispatchLock":
|
|
87
|
+
ensure_phase_loop_excluded(self.repo)
|
|
88
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
handle = self.path.open("a+", encoding="utf-8")
|
|
90
|
+
try:
|
|
91
|
+
_acquire_exclusive(handle)
|
|
92
|
+
except BlockingIOError as exc:
|
|
93
|
+
holder = _read_holder_metadata(handle)
|
|
94
|
+
handle.close()
|
|
95
|
+
raise DispatchLockContention(
|
|
96
|
+
lock_path=self.path,
|
|
97
|
+
holder_pid=_int_or_none(holder.get("pid")),
|
|
98
|
+
holder_started_at=_float_or_none(holder.get("started_at")),
|
|
99
|
+
roadmap=str(holder.get("roadmap") or "") or None,
|
|
100
|
+
) from exc
|
|
101
|
+
|
|
102
|
+
handle.seek(0)
|
|
103
|
+
handle.truncate()
|
|
104
|
+
json.dump(
|
|
105
|
+
{
|
|
106
|
+
"pid": os.getpid(),
|
|
107
|
+
"started_at": time.time(),
|
|
108
|
+
"repo": str(self.repo),
|
|
109
|
+
"roadmap": str(self.roadmap),
|
|
110
|
+
},
|
|
111
|
+
handle,
|
|
112
|
+
sort_keys=True,
|
|
113
|
+
)
|
|
114
|
+
handle.write("\n")
|
|
115
|
+
handle.flush()
|
|
116
|
+
os.fsync(handle.fileno())
|
|
117
|
+
self._handle = handle
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def release(self) -> None:
|
|
121
|
+
if self._handle is None:
|
|
122
|
+
return
|
|
123
|
+
try:
|
|
124
|
+
_release_exclusive(self._handle)
|
|
125
|
+
finally:
|
|
126
|
+
self._handle.close()
|
|
127
|
+
self._handle = None
|
|
128
|
+
|
|
129
|
+
def __enter__(self) -> "DispatchLock":
|
|
130
|
+
if self._handle is not None:
|
|
131
|
+
return self
|
|
132
|
+
return self.acquire()
|
|
133
|
+
|
|
134
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
135
|
+
self.release()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def dispatch_lock_path(repo: Path, roadmap: Path) -> Path:
|
|
139
|
+
try:
|
|
140
|
+
roadmap_key = str(roadmap.resolve().relative_to(repo.resolve()))
|
|
141
|
+
except ValueError:
|
|
142
|
+
roadmap_key = str(roadmap.resolve())
|
|
143
|
+
return phase_loop_dir(repo) / _path_slug(roadmap_key) / "dispatch.lock"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _read_holder_metadata(handle) -> dict[str, object]:
|
|
147
|
+
try:
|
|
148
|
+
handle.seek(0)
|
|
149
|
+
raw = handle.read().strip()
|
|
150
|
+
payload = json.loads(raw) if raw else {}
|
|
151
|
+
except (OSError, json.JSONDecodeError):
|
|
152
|
+
return {}
|
|
153
|
+
return payload if isinstance(payload, dict) else {}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _int_or_none(value: object) -> int | None:
|
|
157
|
+
try:
|
|
158
|
+
return int(value)
|
|
159
|
+
except (TypeError, ValueError):
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _float_or_none(value: object) -> float | None:
|
|
164
|
+
try:
|
|
165
|
+
return float(value)
|
|
166
|
+
except (TypeError, ValueError):
|
|
167
|
+
return None
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Doc-delta closeout validator (rigor-v1 P2).
|
|
2
|
+
|
|
3
|
+
When a phase's diff touches a user-visible public surface but the closeout
|
|
4
|
+
records no doc-delta decision, raise a finding. Autonomy-first: the finding is
|
|
5
|
+
``block``-severity, so under the default ``PHASE_LOOP_REVIEW=warn`` it is merely
|
|
6
|
+
recorded (the loop continues) and only blocks when an operator opts in. The
|
|
7
|
+
agent self-satisfies it by updating docs or recording a ``no_doc_delta``
|
|
8
|
+
decision in the terminal summary — no human required.
|
|
9
|
+
|
|
10
|
+
The executor records its decision as ``doc_delta_decision: <literal>`` in the
|
|
11
|
+
terminal summary (one of ``models.DOC_DELTA_DECISIONS``).
|
|
12
|
+
|
|
13
|
+
issue #18 F5 — evidence-backed corroboration. A bare ``no_doc_delta`` is a
|
|
14
|
+
*self-attested literal*: the executor claims "the public surface is already
|
|
15
|
+
current, no docs change needed". On a **release/explicit-release** phase that
|
|
16
|
+
claim is exactly the one the docs-freshness incident proved unreliable. When a
|
|
17
|
+
runner-side freshness scan was threaded in (``ctx.docs_freshness``), we
|
|
18
|
+
corroborate the ``no_doc_delta`` against its enumerated-surface evidence: an
|
|
19
|
+
un-corroborated ``no_doc_delta`` on a release phase is downgraded to a recorded
|
|
20
|
+
**warn** finding (never a block — autonomy-first), surfacing the scan result
|
|
21
|
+
alongside. Ordinary phases, and any phase with no scan threaded in, are
|
|
22
|
+
unaffected (the literal still satisfies, as before).
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from .closeout_validators import CloseoutContext, ReviewFinding, register_closeout_validator
|
|
27
|
+
from .docs_freshness import docs_freshness_evidence_backed
|
|
28
|
+
from .models import DOC_DELTA_DECISIONS, public_surface_touched
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register_closeout_validator
|
|
32
|
+
def doc_delta_validator(ctx: CloseoutContext) -> list[ReviewFinding]:
|
|
33
|
+
if not public_surface_touched(ctx.changed_paths):
|
|
34
|
+
return []
|
|
35
|
+
decision = str(ctx.terminal.get("doc_delta_decision") or "").strip()
|
|
36
|
+
if decision in DOC_DELTA_DECISIONS:
|
|
37
|
+
return _corroborate_no_doc_delta(ctx, decision) # may be [] or a warn
|
|
38
|
+
return [
|
|
39
|
+
ReviewFinding(
|
|
40
|
+
code="doc_delta_undecided",
|
|
41
|
+
reason=(
|
|
42
|
+
"changed a public surface (CLI/schema/contract docs/README/CHANGELOG) "
|
|
43
|
+
"but recorded no doc_delta decision; update the doc surface or record "
|
|
44
|
+
"doc_delta_decision=no_doc_delta with a justification"
|
|
45
|
+
),
|
|
46
|
+
severity="block",
|
|
47
|
+
blocker_class="review_gate_block",
|
|
48
|
+
)
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _corroborate_no_doc_delta(ctx: CloseoutContext, decision: str) -> list[ReviewFinding]:
|
|
53
|
+
"""F5: corroborate a self-attested ``no_doc_delta`` on a release phase.
|
|
54
|
+
|
|
55
|
+
Default-safe by construction:
|
|
56
|
+
|
|
57
|
+
* Only ``no_doc_delta`` is corroborated — ``docs_updated`` /
|
|
58
|
+
``docs_follow_up_filed`` already evidence a real change and pass through.
|
|
59
|
+
* No scan threaded in (``ctx.docs_freshness is None``) → no corroboration is
|
|
60
|
+
available → satisfied (return ``[]``). We never newly-fail because the
|
|
61
|
+
runner didn't wire the scan.
|
|
62
|
+
* Non-release phases are untouched: an ordinary phase legitimately records
|
|
63
|
+
``no_doc_delta`` for an internal change, and the freshness scan reports
|
|
64
|
+
``skipped`` there anyway.
|
|
65
|
+
* Freshness gate ``off`` (``PHASE_LOOP_DOCS_FRESHNESS=off``) means "assert
|
|
66
|
+
nothing about docs": no corroboration is performed (return ``[]``), so
|
|
67
|
+
disabling the gate never *adds* a finding.
|
|
68
|
+
* The downgrade is **warn**, never block — the existing block path is the
|
|
69
|
+
*undecided* case, which this branch never reaches.
|
|
70
|
+
"""
|
|
71
|
+
if decision != "no_doc_delta":
|
|
72
|
+
return []
|
|
73
|
+
detail = ctx.docs_freshness
|
|
74
|
+
if not detail:
|
|
75
|
+
return [] # unwired scan — no corroboration available, satisfied
|
|
76
|
+
detail = dict(detail)
|
|
77
|
+
if str(detail.get("mode") or "") == "off":
|
|
78
|
+
return [] # gate disabled — assert nothing, never add a finding
|
|
79
|
+
if not detail.get("is_release_phase"):
|
|
80
|
+
return [] # ordinary phase — self-attested no_doc_delta is fine
|
|
81
|
+
if docs_freshness_evidence_backed(detail):
|
|
82
|
+
return [] # the path-keyed scan corroborates the claim — satisfied
|
|
83
|
+
status = str(detail.get("status") or "skipped")
|
|
84
|
+
surfaces = list(detail.get("surfaces_scanned") or ())
|
|
85
|
+
return [
|
|
86
|
+
ReviewFinding(
|
|
87
|
+
code="doc_delta_uncorroborated",
|
|
88
|
+
reason=(
|
|
89
|
+
"release phase recorded `no_doc_delta` but the path-keyed "
|
|
90
|
+
"docs-freshness scan does not corroborate it "
|
|
91
|
+
f"(docs_freshness={status}, surfaces_scanned={len(surfaces)}); "
|
|
92
|
+
"treat the no-doc-change claim as unverified until the freshness "
|
|
93
|
+
"scan enumerates the public-doc surfaces and reports `passed`."
|
|
94
|
+
),
|
|
95
|
+
severity="warn",
|
|
96
|
+
blocker_class="review_gate_warn",
|
|
97
|
+
)
|
|
98
|
+
]
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Pipeline-independent docs-freshness audit (docs-freshness v4 P1, Layer A).
|
|
2
|
+
|
|
3
|
+
`phase-loop docs-audit --base <ref>` runs on a git diff ALONE — no `.phase-loop/`
|
|
4
|
+
state — so it is immune to the three #18 evasion paths (under-scoped docs lane,
|
|
5
|
+
direct-`Agent()` pipeline bypass, absent runtime helper). It classifies the changed
|
|
6
|
+
paths against the canonical `docs_surfaces` taxonomy and enforces the per-surface,
|
|
7
|
+
relevance-bound decision contract, emitting a `docs_freshness: passed|skipped|blocked`
|
|
8
|
+
report and a non-zero exit when a public surface changed without a satisfying decision.
|
|
9
|
+
|
|
10
|
+
This is the only *non-bypassable* control; the in-pipeline `docs_freshness` closeout
|
|
11
|
+
gate (shipped in v0.1.6) is the advisory early-feedback / content-token layer. This
|
|
12
|
+
backstop adds the piece that gate structurally cannot catch: the *silent-absence* case
|
|
13
|
+
— a release surface changed in the diff but its required doc was never updated.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from . import docs_surfaces as ds
|
|
25
|
+
|
|
26
|
+
#: git's well-known empty-tree object — the diff base for a first push / first tag,
|
|
27
|
+
#: where the whole tree is "new" and there is no prior ref to diff against.
|
|
28
|
+
_EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DiffUnavailable(RuntimeError):
|
|
32
|
+
"""A git diff could not be computed — the audit fails CLOSED, never a silent pass."""
|
|
33
|
+
|
|
34
|
+
#: Decisions an operator/executor can record per surface. Release-class is held to
|
|
35
|
+
#: the relevance binding regardless (a token never substitutes for the real doc).
|
|
36
|
+
DECISION_TOKENS = ("docs_updated", "no_doc_delta", "docs_follow_up_filed")
|
|
37
|
+
#: Repo-visible decision artifact, recoverable WITHOUT `.phase-loop/` state.
|
|
38
|
+
DEFAULT_DECISIONS_PATH = ".doc-decisions.json"
|
|
39
|
+
DOC_DECISIONS_SCHEMA_VERSION = 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _git(repo: Path, *args: str) -> str:
|
|
43
|
+
try:
|
|
44
|
+
return subprocess.check_output(
|
|
45
|
+
["git", "-C", str(repo), *args], text=True, stderr=subprocess.DEVNULL
|
|
46
|
+
).strip()
|
|
47
|
+
except Exception:
|
|
48
|
+
return ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _rev_ok(repo: Path, ref: str) -> bool:
|
|
52
|
+
return bool(_git(repo, "rev-parse", "--verify", f"{ref}^{{commit}}"))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _first_push_base(repo: Path) -> str:
|
|
56
|
+
"""Base for a first push / first tag — HEAD~1 if it exists, else the empty tree."""
|
|
57
|
+
return "HEAD~1" if _rev_ok(repo, "HEAD~1") else _EMPTY_TREE
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_base(repo: Path, base: str | None) -> tuple[str | None, str]:
|
|
61
|
+
"""Resolve the diff base across the CI contexts. Returns (base, context).
|
|
62
|
+
|
|
63
|
+
Explicit `--base` wins. Otherwise: a PR (`GITHUB_BASE_REF`) → `origin/<base>`; a tag
|
|
64
|
+
push (`GITHUB_REF=refs/tags/...`) → the prior tag (first tag → fallback base); a
|
|
65
|
+
branch push → the push **before** SHA (`DOCS_AUDIT_PUSH_BEFORE`, set to
|
|
66
|
+
`github.event.before`) so the WHOLE batch is diffed, never just the tip. An all-zeros
|
|
67
|
+
`before` (first push to the ref) → fallback base.
|
|
68
|
+
"""
|
|
69
|
+
if base:
|
|
70
|
+
return base, "explicit"
|
|
71
|
+
pr_base = os.environ.get("GITHUB_BASE_REF")
|
|
72
|
+
if pr_base:
|
|
73
|
+
return f"origin/{pr_base}", "pull_request"
|
|
74
|
+
ref = os.environ.get("GITHUB_REF", "")
|
|
75
|
+
if ref.startswith("refs/tags/"):
|
|
76
|
+
prior = _git(repo, "describe", "--tags", "--abbrev=0", "HEAD^")
|
|
77
|
+
if prior:
|
|
78
|
+
return prior, "push_tag"
|
|
79
|
+
return _first_push_base(repo), "push_tag_first"
|
|
80
|
+
before = os.environ.get("DOCS_AUDIT_PUSH_BEFORE", "").strip()
|
|
81
|
+
if before:
|
|
82
|
+
if set(before) <= {"0"}: # all-zeros sentinel → first push to this ref
|
|
83
|
+
return _first_push_base(repo), "push_first"
|
|
84
|
+
return before, "push"
|
|
85
|
+
return "HEAD~1", "push" # local / no-CI-env fallback
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _base_usable(repo: Path, base: str) -> bool:
|
|
89
|
+
return base == _EMPTY_TREE or _rev_ok(repo, base)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _diff_op(context: str) -> str:
|
|
93
|
+
# PR / explicit: three-dot (changes vs the merge-base). push / tag: two-dot
|
|
94
|
+
# (exactly the pushed commits — the correct semantics for a batched push range).
|
|
95
|
+
return "..." if context in {"pull_request", "explicit"} else ".."
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def changed_paths(repo: Path, base: str, op: str = "...", head: str = "HEAD") -> list[str]:
|
|
99
|
+
"""`git diff --name-only base<op>head`. Raises `DiffUnavailable` on a git error so
|
|
100
|
+
the caller fails CLOSED — an empty result means "no changes", never "couldn't tell"."""
|
|
101
|
+
try:
|
|
102
|
+
completed = subprocess.run(
|
|
103
|
+
["git", "-C", str(repo), "diff", "--name-only", f"{base}{op}{head}"],
|
|
104
|
+
stdout=subprocess.PIPE,
|
|
105
|
+
stderr=subprocess.DEVNULL,
|
|
106
|
+
text=True,
|
|
107
|
+
check=False,
|
|
108
|
+
)
|
|
109
|
+
except OSError as exc:
|
|
110
|
+
raise DiffUnavailable(str(exc)) from exc
|
|
111
|
+
if completed.returncode not in {0, 1}:
|
|
112
|
+
raise DiffUnavailable(f"git diff exited {completed.returncode} for {base}{op}{head}")
|
|
113
|
+
return [p for p in completed.stdout.splitlines() if p.strip()]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load_decisions(repo: Path, path: str | None = None) -> dict[str, dict[str, Any]]:
|
|
117
|
+
"""Read the repo-visible decision artifact. Missing file → no decisions.
|
|
118
|
+
|
|
119
|
+
Returns a mapping `surface-key -> {decision, reason, evidence}`. The key may be a
|
|
120
|
+
glob/path matching changed surfaces, or a class name (`general`/`release`/`*`).
|
|
121
|
+
"""
|
|
122
|
+
decisions_path = repo / (path or DEFAULT_DECISIONS_PATH)
|
|
123
|
+
if not decisions_path.is_file():
|
|
124
|
+
return {}
|
|
125
|
+
try:
|
|
126
|
+
data = json.loads(decisions_path.read_text(encoding="utf-8"))
|
|
127
|
+
except (OSError, ValueError):
|
|
128
|
+
return {}
|
|
129
|
+
out: dict[str, dict[str, Any]] = {}
|
|
130
|
+
for entry in data.get("decisions", []) or []:
|
|
131
|
+
surface = str(entry.get("surface") or "").strip()
|
|
132
|
+
decision = str(entry.get("decision") or "").strip()
|
|
133
|
+
if surface and decision in DECISION_TOKENS:
|
|
134
|
+
out[surface] = {
|
|
135
|
+
"decision": decision,
|
|
136
|
+
"reason": str(entry.get("reason") or ""),
|
|
137
|
+
"evidence": tuple(entry.get("evidence") or ()),
|
|
138
|
+
}
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _decision_for(path: str, klass: str, decisions: dict[str, dict[str, Any]]) -> dict[str, Any] | None:
|
|
143
|
+
for key in (path, klass, "*"):
|
|
144
|
+
if key in decisions:
|
|
145
|
+
return decisions[key]
|
|
146
|
+
for key, value in decisions.items():
|
|
147
|
+
if any(ch in key for ch in "*?[") and ds._match(path, key):
|
|
148
|
+
return value
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class AuditReport:
|
|
154
|
+
docs_freshness: str # "passed" | "skipped" | "blocked"
|
|
155
|
+
findings: list[dict[str, Any]] = field(default_factory=list)
|
|
156
|
+
surfaces: dict[str, list[str]] = field(default_factory=dict)
|
|
157
|
+
evidence: dict[str, Any] = field(default_factory=dict)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def exit_code(self) -> int:
|
|
161
|
+
return 0 if self.docs_freshness in {"passed", "skipped"} else 1
|
|
162
|
+
|
|
163
|
+
def to_json(self) -> dict[str, Any]:
|
|
164
|
+
return {
|
|
165
|
+
"docs_freshness": self.docs_freshness,
|
|
166
|
+
"findings": self.findings,
|
|
167
|
+
"surfaces": self.surfaces,
|
|
168
|
+
"evidence": self.evidence,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def evaluate(changed: list[str], decisions: dict[str, dict[str, Any]]) -> AuditReport:
|
|
173
|
+
"""Apply the per-surface, relevance-bound decision contract to a changed-path set."""
|
|
174
|
+
changed_docs = [p for p in changed if ds.is_doc_surface(p)]
|
|
175
|
+
release_surfaces = [p for p in changed if ds.classify_surface(p) == "release" and not ds.is_doc_surface(p)]
|
|
176
|
+
general_surfaces = [p for p in changed if ds.classify_surface(p) == "general" and not ds.is_doc_surface(p)]
|
|
177
|
+
|
|
178
|
+
findings: list[dict[str, Any]] = []
|
|
179
|
+
|
|
180
|
+
for surface in release_surfaces:
|
|
181
|
+
required = ds.required_docs_for(surface)
|
|
182
|
+
if required:
|
|
183
|
+
relevant_changed = any(ds._match(d, pat) for d in changed_docs for pat in required)
|
|
184
|
+
else:
|
|
185
|
+
relevant_changed = bool(changed_docs)
|
|
186
|
+
if not relevant_changed:
|
|
187
|
+
findings.append({
|
|
188
|
+
"surface": surface,
|
|
189
|
+
"klass": "release",
|
|
190
|
+
"code": "release_docs_unsatisfied",
|
|
191
|
+
"reason": (
|
|
192
|
+
f"release-class surface `{surface}` changed but its required doc "
|
|
193
|
+
f"surface(s) {list(required) or ['any doc']} did not — a token or an "
|
|
194
|
+
f"unrelated doc edit does not satisfy a release surface"
|
|
195
|
+
),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
for surface in general_surfaces:
|
|
199
|
+
decision = _decision_for(surface, "general", decisions)
|
|
200
|
+
if not (changed_docs or decision):
|
|
201
|
+
findings.append({
|
|
202
|
+
"surface": surface,
|
|
203
|
+
"klass": "general",
|
|
204
|
+
"code": "general_decision_missing",
|
|
205
|
+
"reason": (
|
|
206
|
+
f"public surface `{surface}` changed with no doc change and no recorded "
|
|
207
|
+
f"doc decision; update a doc or record a doc decision (no_doc_delta + reason)"
|
|
208
|
+
),
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
surfaces = {
|
|
212
|
+
"release": release_surfaces,
|
|
213
|
+
"general": general_surfaces,
|
|
214
|
+
"docs": changed_docs,
|
|
215
|
+
}
|
|
216
|
+
if not release_surfaces and not general_surfaces:
|
|
217
|
+
return AuditReport("skipped", findings, surfaces, {"reason": "no public surfaces changed"})
|
|
218
|
+
if findings:
|
|
219
|
+
return AuditReport("blocked", findings, surfaces)
|
|
220
|
+
return AuditReport("passed", findings, surfaces)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def run_audit(repo: Path, base: str | None = None, decisions_path: str | None = None) -> AuditReport:
|
|
224
|
+
"""Orchestrate the diff-driven audit. Un-evaluable input → `blocked` (never silent pass)."""
|
|
225
|
+
resolved, context = resolve_base(repo, base)
|
|
226
|
+
if not resolved or not _base_usable(repo, resolved):
|
|
227
|
+
return AuditReport(
|
|
228
|
+
"blocked",
|
|
229
|
+
[{
|
|
230
|
+
"surface": None,
|
|
231
|
+
"klass": "audit",
|
|
232
|
+
"code": "base_unresolved",
|
|
233
|
+
"reason": (
|
|
234
|
+
f"could not resolve the diff base (`{base or '<auto>'}`, context={context}); "
|
|
235
|
+
f"the audit cannot evaluate freshness — failing closed (never a silent pass)"
|
|
236
|
+
),
|
|
237
|
+
}],
|
|
238
|
+
evidence={"base": resolved, "context": context},
|
|
239
|
+
)
|
|
240
|
+
try:
|
|
241
|
+
changed = changed_paths(repo, resolved, _diff_op(context))
|
|
242
|
+
except DiffUnavailable as exc:
|
|
243
|
+
return AuditReport(
|
|
244
|
+
"blocked",
|
|
245
|
+
[{
|
|
246
|
+
"surface": None,
|
|
247
|
+
"klass": "audit",
|
|
248
|
+
"code": "diff_unavailable",
|
|
249
|
+
"reason": (
|
|
250
|
+
f"git diff failed (base=`{resolved}`, context={context}): {exc} — the audit "
|
|
251
|
+
f"cannot evaluate freshness, failing closed (never a silent pass)"
|
|
252
|
+
),
|
|
253
|
+
}],
|
|
254
|
+
evidence={"base": resolved, "context": context},
|
|
255
|
+
)
|
|
256
|
+
report = evaluate(changed, load_decisions(repo, decisions_path))
|
|
257
|
+
report.evidence = {"base": resolved, "context": context, "changed_count": len(changed)}
|
|
258
|
+
return report
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def main(argv: list[str] | None = None) -> int:
|
|
262
|
+
import argparse
|
|
263
|
+
|
|
264
|
+
parser = argparse.ArgumentParser(prog="phase-loop docs-audit")
|
|
265
|
+
parser.add_argument("--base", help="Diff base ref (auto-resolved from CI env if omitted).")
|
|
266
|
+
parser.add_argument("--repo", default=".", help="Repository root (default: cwd).")
|
|
267
|
+
parser.add_argument("--decisions", help=f"Decision artifact path (default: {DEFAULT_DECISIONS_PATH}).")
|
|
268
|
+
parser.add_argument("--json", action="store_true", help="Emit the report as JSON.")
|
|
269
|
+
# tolerate the leading 'docs-audit' token when dispatched from the main CLI
|
|
270
|
+
args = parser.parse_args([a for a in (argv or []) if a != "docs-audit"])
|
|
271
|
+
|
|
272
|
+
report = run_audit(Path(args.repo), args.base, args.decisions)
|
|
273
|
+
if args.json:
|
|
274
|
+
print(json.dumps(report.to_json(), indent=2))
|
|
275
|
+
else:
|
|
276
|
+
print(f"docs_freshness: {report.docs_freshness}")
|
|
277
|
+
for f in report.findings:
|
|
278
|
+
print(f" [{f['klass']}] {f.get('surface') or '-'}: {f['reason']}")
|
|
279
|
+
if report.docs_freshness == "blocked":
|
|
280
|
+
print(
|
|
281
|
+
"\nRemediation: update the required doc surface(s), or record a doc decision in "
|
|
282
|
+
f"{args.decisions or DEFAULT_DECISIONS_PATH} (release-class needs a real, relevant "
|
|
283
|
+
"doc change — a token does not satisfy it)."
|
|
284
|
+
)
|
|
285
|
+
return report.exit_code
|