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,468 @@
|
|
|
1
|
+
"""Docs-freshness closeout gate (issue #18).
|
|
2
|
+
|
|
3
|
+
A phase-loop roadmap can close **green** — clean tree, pushed ``main``, release
|
|
4
|
+
workflow passed — while its public docs are stale or absent. The existing
|
|
5
|
+
``doc_delta_validator`` cannot catch this: it is *diff-keyed* (it fires only
|
|
6
|
+
when a public surface is already in ``changed_paths``), so files that *should*
|
|
7
|
+
have changed but didn't are doubly invisible. And under the default
|
|
8
|
+
``PHASE_LOOP_REVIEW=warn`` every registry finding is forced to ``warn`` and
|
|
9
|
+
never blocks (see ``closeout_validators``).
|
|
10
|
+
|
|
11
|
+
This module is the *path-keyed* complement: it enumerates public-doc surfaces
|
|
12
|
+
**from the filesystem** (and ``.claude/docs-catalog.json`` when present), scans
|
|
13
|
+
their *contents* for stale/placeholder tokens, and — for **release/package
|
|
14
|
+
phases only** — blocks ``complete`` independent of ``PHASE_LOOP_REVIEW``,
|
|
15
|
+
mirroring ``_apply_verification_evidence_gate`` (a hard, non-registry gate
|
|
16
|
+
gated on its own ``PHASE_LOOP_DOCS_FRESHNESS`` knob, default ``hard``).
|
|
17
|
+
|
|
18
|
+
Design seam (research open-question 1, option a): validators stay pure. The
|
|
19
|
+
**runner pre-scans** the repo via :func:`scan_docs_freshness` and threads the
|
|
20
|
+
resulting dict into ``build_phase_loop_closeout``; the gate consumes the dict
|
|
21
|
+
and never touches the filesystem from inside the closeout-validator contract.
|
|
22
|
+
|
|
23
|
+
Default-safe posture:
|
|
24
|
+
|
|
25
|
+
* Ordinary (non-release) phases are **unaffected** — the scan reports
|
|
26
|
+
``skipped`` and never blocks, preserving autonomy-first elsewhere.
|
|
27
|
+
* No pre-scan data (unwired call site) → ``skipped``, never blocked.
|
|
28
|
+
* False-positive control: only *unambiguous* placeholder tokens block; fuzzy
|
|
29
|
+
signals (stale package-count claims, unlabeled versions) are warn-tier. An
|
|
30
|
+
explicit ``<!-- freshness-ok -->`` marker on a line suppresses that line.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
from dataclasses import dataclass, field, replace
|
|
37
|
+
from fnmatch import fnmatchcase
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any, Iterable, Mapping
|
|
40
|
+
|
|
41
|
+
# Enforcement knob — like PHASE_LOOP_VERIFY_ENFORCE, this gate has its OWN
|
|
42
|
+
# control so the hard block is independent of the global PHASE_LOOP_REVIEW
|
|
43
|
+
# warn-forcing default. "hard" (default) blocks release phases; "warn" records
|
|
44
|
+
# only; "off" disables the scan entirely.
|
|
45
|
+
DOCS_FRESHNESS_ENV = "PHASE_LOOP_DOCS_FRESHNESS"
|
|
46
|
+
DOCS_FRESHNESS_MODES: tuple[str, ...] = ("off", "warn", "hard")
|
|
47
|
+
DEFAULT_DOCS_FRESHNESS_MODE = "hard"
|
|
48
|
+
|
|
49
|
+
# An inline marker that suppresses freshness flagging for the line it appears on
|
|
50
|
+
# (operator escape hatch for legitimate uses of an otherwise-suspicious token).
|
|
51
|
+
FRESHNESS_OK_MARKER = "freshness-ok"
|
|
52
|
+
|
|
53
|
+
# Public-doc surfaces to enumerate from the filesystem. Kept aligned with
|
|
54
|
+
# models.PUBLIC_SURFACE_GLOBS' doc subset but extended to package-level READMEs
|
|
55
|
+
# and release notes — the exact surfaces issue #18 found stale.
|
|
56
|
+
PUBLIC_DOC_GLOBS: tuple[str, ...] = (
|
|
57
|
+
"README.md",
|
|
58
|
+
"**/README.md",
|
|
59
|
+
"CHANGELOG.md",
|
|
60
|
+
"CHANGELOG*",
|
|
61
|
+
"**/CHANGELOG.md",
|
|
62
|
+
"RELEASE_NOTES*",
|
|
63
|
+
"RELEASES*",
|
|
64
|
+
"docs/RELEASE*.md",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Directories never worth scanning (vendored / generated / VCS).
|
|
68
|
+
_SKIP_DIRS: frozenset[str] = frozenset(
|
|
69
|
+
{".git", "node_modules", "vendor", "dist", "build", "__pycache__", ".venv", "venv"}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Unambiguous placeholders — a release doc carrying one of these (outside an
|
|
73
|
+
# inline-code span) is stale by construction. Kept DELIBERATELY TIGHT: a hard
|
|
74
|
+
# gate that fires on a token that legitimately lives in shipped docs would block
|
|
75
|
+
# a real release fleet-wide (worse than the bug for normal operation). The
|
|
76
|
+
# issue's smoking gun was `recovery commit pending`. TODO/FIXME/XXX are common
|
|
77
|
+
# enough in real prose docs to be WARN-tier, not block.
|
|
78
|
+
BLOCK_TOKENS: tuple[str, ...] = (
|
|
79
|
+
"recovery commit pending",
|
|
80
|
+
"commit pending",
|
|
81
|
+
"TBD",
|
|
82
|
+
"<placeholder>",
|
|
83
|
+
)
|
|
84
|
+
# `pending` alone is too common in prose; only the qualified forms above block.
|
|
85
|
+
# Bare-word block tokens that need word-boundary care are handled in _line_hits.
|
|
86
|
+
_BARE_BLOCK_WORDS: tuple[str, ...] = ("TBD",)
|
|
87
|
+
|
|
88
|
+
# Fuzzy / advisory signals — likely-stale but false-positive-prone. WARN only.
|
|
89
|
+
WARN_TOKENS: tuple[str, ...] = (
|
|
90
|
+
"publishes three",
|
|
91
|
+
"publishes two",
|
|
92
|
+
"is a skeleton",
|
|
93
|
+
"still a skeleton",
|
|
94
|
+
"TODO",
|
|
95
|
+
"FIXME",
|
|
96
|
+
"XXX",
|
|
97
|
+
)
|
|
98
|
+
# Warn-tier bare words needing word-boundary care (so "TODO" ≠ "METHODOLOGY").
|
|
99
|
+
_BARE_WARN_WORDS: tuple[str, ...] = ("TODO", "FIXME", "XXX")
|
|
100
|
+
|
|
101
|
+
# Release-artifact globs — if a phase's changed paths touch any of these, it is
|
|
102
|
+
# release/package-shaped. Used as the heuristic fallback when the plan carries
|
|
103
|
+
# no explicit release declaration.
|
|
104
|
+
RELEASE_ARTIFACT_GLOBS: tuple[str, ...] = (
|
|
105
|
+
"**/package.json",
|
|
106
|
+
"package.json",
|
|
107
|
+
"CHANGELOG.md",
|
|
108
|
+
"**/CHANGELOG.md",
|
|
109
|
+
"RELEASE_NOTES*",
|
|
110
|
+
"**/Cargo.toml",
|
|
111
|
+
"**/pyproject.toml",
|
|
112
|
+
"**/setup.cfg",
|
|
113
|
+
"**/version.txt",
|
|
114
|
+
"**/VERSION",
|
|
115
|
+
"**/__version__.py",
|
|
116
|
+
".github/workflows/release*.yml",
|
|
117
|
+
".github/workflows/release*.yaml",
|
|
118
|
+
".github/workflows/publish*.yml",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_docs_freshness_mode(env: Mapping[str, str] | None = None) -> str:
|
|
123
|
+
env = os.environ if env is None else env
|
|
124
|
+
value = str(env.get(DOCS_FRESHNESS_ENV) or "").strip().lower()
|
|
125
|
+
return value if value in DOCS_FRESHNESS_MODES else DEFAULT_DOCS_FRESHNESS_MODE
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def docs_freshness_evidence_backed(detail: Mapping[str, Any] | None) -> bool:
|
|
129
|
+
"""True iff a ``passed`` freshness claim is *provable* from the scan evidence (F5).
|
|
130
|
+
|
|
131
|
+
A ``passed`` status is only trustworthy when the scan actually ran and
|
|
132
|
+
enumerated a per-surface evidence list — i.e. ``surfaces_scanned`` is
|
|
133
|
+
non-empty — and recorded no blocking hit. A bare/empty detail (no surfaces
|
|
134
|
+
enumerated) must NOT read as a corroborated pass: there is nothing to back
|
|
135
|
+
the claim. ``skipped``/``blocked`` are never "evidence-backed passes".
|
|
136
|
+
|
|
137
|
+
Used in two places (single source of truth): :func:`scan_docs_freshness`
|
|
138
|
+
(so it cannot *emit* ``passed`` without enumerated-surface evidence) and
|
|
139
|
+
``doc_delta_validator`` (to corroborate a self-attested ``no_doc_delta``).
|
|
140
|
+
"""
|
|
141
|
+
if not detail:
|
|
142
|
+
return False
|
|
143
|
+
d = dict(detail)
|
|
144
|
+
if str(d.get("status") or "") != "passed":
|
|
145
|
+
return False
|
|
146
|
+
if d.get("blocking_hits"):
|
|
147
|
+
return False
|
|
148
|
+
return bool(d.get("surfaces_scanned"))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _glob_match(path: str, patterns: Iterable[str]) -> bool:
|
|
152
|
+
path = path.strip()
|
|
153
|
+
if not path:
|
|
154
|
+
return False
|
|
155
|
+
for pattern in patterns:
|
|
156
|
+
if fnmatchcase(path, pattern):
|
|
157
|
+
return True
|
|
158
|
+
if pattern.startswith("**/") and fnmatchcase(path, pattern[3:]):
|
|
159
|
+
return True
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def is_explicit_release_phase(
|
|
164
|
+
plan_frontmatter: Mapping[str, Any] | None = None,
|
|
165
|
+
) -> bool:
|
|
166
|
+
"""True only when the plan *explicitly* declares itself a release phase.
|
|
167
|
+
|
|
168
|
+
This is the **authoritative** release marker — ``phase_loop_mutation:
|
|
169
|
+
release_dispatch`` (emitted by the plan-phase skill) or a release-shaped
|
|
170
|
+
``phase_type``. It is the ONLY signal eligible to drive the **hard block**
|
|
171
|
+
in :func:`scan_docs_freshness`: hard-blocking an ordinary phase fleet-wide
|
|
172
|
+
on a stale token in any public doc is a worse failure than the bug it
|
|
173
|
+
catches, so the hard gate must be opt-in via frontmatter.
|
|
174
|
+
|
|
175
|
+
The artifact-glob heuristic (see :func:`is_release_phase`) deliberately
|
|
176
|
+
does NOT satisfy this predicate — it only governs whether the scan runs and
|
|
177
|
+
can emit warn-tier evidence.
|
|
178
|
+
"""
|
|
179
|
+
if not plan_frontmatter:
|
|
180
|
+
return False
|
|
181
|
+
mutation = str(plan_frontmatter.get("phase_loop_mutation") or "").strip().lower()
|
|
182
|
+
if mutation == "release_dispatch":
|
|
183
|
+
return True
|
|
184
|
+
ptype = str(plan_frontmatter.get("phase_type") or "").strip().lower()
|
|
185
|
+
return ptype in ("release", "package", "roadmap_completion", "roadmap-completion")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def is_release_phase(
|
|
189
|
+
*,
|
|
190
|
+
plan_frontmatter: Mapping[str, Any] | None = None,
|
|
191
|
+
changed_paths: Iterable[str] = (),
|
|
192
|
+
) -> bool:
|
|
193
|
+
"""True if this phase is release/package/roadmap-completion shaped.
|
|
194
|
+
|
|
195
|
+
Layered, default-safe signal used to decide **whether the freshness scan
|
|
196
|
+
runs at all** (not whether it may hard-block — see
|
|
197
|
+
:func:`is_explicit_release_phase` for that):
|
|
198
|
+
|
|
199
|
+
1. Explicit: the plan frontmatter declares ``phase_loop_mutation:
|
|
200
|
+
release_dispatch`` (the authoritative release marker the plan-phase skill
|
|
201
|
+
already emits).
|
|
202
|
+
2. Heuristic fallback: the phase's changed paths touch a release artifact
|
|
203
|
+
(manifests / version files / changelog / release workflows). Keying
|
|
204
|
+
release *detection* off changed paths is sound — release phases do bump
|
|
205
|
+
versions — and is distinct from keying *freshness* off changed paths
|
|
206
|
+
(which this module deliberately does NOT do).
|
|
207
|
+
|
|
208
|
+
No signal → not a release phase → the gate is inert. A heuristic-only match
|
|
209
|
+
(no explicit frontmatter) lets the scan run and record **warn-tier**
|
|
210
|
+
evidence, but can NEVER produce a ``blocked`` status.
|
|
211
|
+
"""
|
|
212
|
+
if is_explicit_release_phase(plan_frontmatter):
|
|
213
|
+
return True
|
|
214
|
+
return _glob_match_any(changed_paths, RELEASE_ARTIFACT_GLOBS)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _glob_match_any(paths: Iterable[str], patterns: tuple[str, ...]) -> bool:
|
|
218
|
+
return any(_glob_match(str(p), patterns) for p in paths)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def parse_plan_frontmatter(plan_path: str | Path | None) -> dict[str, str]:
|
|
222
|
+
if not plan_path:
|
|
223
|
+
return {}
|
|
224
|
+
try:
|
|
225
|
+
text = Path(plan_path).read_text(encoding="utf-8")
|
|
226
|
+
except OSError:
|
|
227
|
+
return {}
|
|
228
|
+
if not text.startswith("---\n"):
|
|
229
|
+
return {}
|
|
230
|
+
end = text.find("\n---", 4)
|
|
231
|
+
if end == -1:
|
|
232
|
+
return {}
|
|
233
|
+
data: dict[str, str] = {}
|
|
234
|
+
for line in text[4:end].splitlines():
|
|
235
|
+
if ":" not in line:
|
|
236
|
+
continue
|
|
237
|
+
key, value = line.split(":", 1)
|
|
238
|
+
data[key.strip()] = value.strip().strip("'\"")
|
|
239
|
+
return data
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _catalog_surfaces(repo: Path) -> list[str]:
|
|
243
|
+
"""Repo-relative doc paths declared in ``.claude/docs-catalog.json`` (if any).
|
|
244
|
+
|
|
245
|
+
The catalog is a list of objects (``{"path": ...}``) or bare strings. We are
|
|
246
|
+
permissive: anything that yields a path string is included. Absent/malformed
|
|
247
|
+
catalog → empty list (filesystem glob enumeration is the fallback).
|
|
248
|
+
"""
|
|
249
|
+
catalog = repo / ".claude" / "docs-catalog.json"
|
|
250
|
+
try:
|
|
251
|
+
data = json.loads(catalog.read_text(encoding="utf-8"))
|
|
252
|
+
except (OSError, ValueError):
|
|
253
|
+
return []
|
|
254
|
+
surfaces: list[str] = []
|
|
255
|
+
items = data if isinstance(data, list) else data.get("docs", []) if isinstance(data, Mapping) else []
|
|
256
|
+
for item in items or []:
|
|
257
|
+
if isinstance(item, str):
|
|
258
|
+
surfaces.append(item)
|
|
259
|
+
elif isinstance(item, Mapping):
|
|
260
|
+
path = item.get("path") or item.get("file")
|
|
261
|
+
if isinstance(path, str):
|
|
262
|
+
surfaces.append(path)
|
|
263
|
+
return surfaces
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def enumerate_public_docs(repo: Path) -> list[str]:
|
|
267
|
+
"""Path-keyed enumeration of public-doc surfaces (repo-relative, sorted).
|
|
268
|
+
|
|
269
|
+
Filesystem walk matched against :data:`PUBLIC_DOC_GLOBS`, unioned with any
|
|
270
|
+
surfaces declared in ``.claude/docs-catalog.json``. NOT keyed off
|
|
271
|
+
``changed_paths`` — that is the entire point (issue #18's blind spot).
|
|
272
|
+
"""
|
|
273
|
+
found: set[str] = set()
|
|
274
|
+
if repo.is_dir():
|
|
275
|
+
for dirpath, dirnames, filenames in os.walk(repo):
|
|
276
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
277
|
+
rel_dir = Path(dirpath).relative_to(repo)
|
|
278
|
+
for name in filenames:
|
|
279
|
+
rel = (rel_dir / name).as_posix()
|
|
280
|
+
if _glob_match(rel, PUBLIC_DOC_GLOBS):
|
|
281
|
+
found.add(rel)
|
|
282
|
+
for rel in _catalog_surfaces(repo):
|
|
283
|
+
candidate = repo / rel
|
|
284
|
+
if candidate.is_file():
|
|
285
|
+
found.add(Path(rel).as_posix())
|
|
286
|
+
return sorted(found)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass(frozen=True)
|
|
290
|
+
class DocsFreshnessHit:
|
|
291
|
+
path: str
|
|
292
|
+
line: int
|
|
293
|
+
token: str
|
|
294
|
+
severity: str # "block" | "warn"
|
|
295
|
+
excerpt: str
|
|
296
|
+
|
|
297
|
+
def to_json(self) -> dict[str, Any]:
|
|
298
|
+
return {
|
|
299
|
+
"path": self.path,
|
|
300
|
+
"line": self.line,
|
|
301
|
+
"token": self.token,
|
|
302
|
+
"severity": self.severity,
|
|
303
|
+
"excerpt": self.excerpt,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _strip_inline_code(line: str) -> str:
|
|
308
|
+
"""Blank out inline-code spans (`...`) so a doc that *documents* a stale
|
|
309
|
+
token (always backticked) is not flagged as itself being stale. Replaces
|
|
310
|
+
span contents with spaces to preserve column alignment cheaply."""
|
|
311
|
+
out: list[str] = []
|
|
312
|
+
in_code = False
|
|
313
|
+
for ch in line:
|
|
314
|
+
if ch == "`":
|
|
315
|
+
in_code = not in_code
|
|
316
|
+
out.append(" ")
|
|
317
|
+
else:
|
|
318
|
+
out.append(" " if in_code else ch)
|
|
319
|
+
return "".join(out)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _line_hits(line: str) -> list[tuple[str, str]]:
|
|
323
|
+
"""Return (token, severity) hits in a single line. Empty if suppressed."""
|
|
324
|
+
if FRESHNESS_OK_MARKER in line:
|
|
325
|
+
return []
|
|
326
|
+
# Tokens inside inline-code spans are documentation, not stale content.
|
|
327
|
+
line = _strip_inline_code(line)
|
|
328
|
+
hits: list[tuple[str, str]] = []
|
|
329
|
+
lowered = line.lower()
|
|
330
|
+
block_matched = False
|
|
331
|
+
for token in BLOCK_TOKENS:
|
|
332
|
+
if token in _BARE_BLOCK_WORDS:
|
|
333
|
+
# Bare words: require them to stand as a token, not a substring
|
|
334
|
+
# (avoid e.g. "TBD" inside a longer identifier). Word-boundary test.
|
|
335
|
+
if _word_present(line, token):
|
|
336
|
+
hits.append((token, "block"))
|
|
337
|
+
block_matched = True
|
|
338
|
+
elif token.lower() in lowered:
|
|
339
|
+
hits.append((token, "block"))
|
|
340
|
+
block_matched = True
|
|
341
|
+
# Dedupe block substrings (e.g. "recovery commit pending" also contains
|
|
342
|
+
# "commit pending") — one block hit per line is enough to gate.
|
|
343
|
+
if block_matched:
|
|
344
|
+
hits = [h for h in hits if h[1] == "block"][:1]
|
|
345
|
+
return hits
|
|
346
|
+
for token in WARN_TOKENS:
|
|
347
|
+
if token in _BARE_WARN_WORDS:
|
|
348
|
+
if _word_present(line, token):
|
|
349
|
+
hits.append((token, "warn"))
|
|
350
|
+
elif token.lower() in lowered:
|
|
351
|
+
hits.append((token, "warn"))
|
|
352
|
+
return hits
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _word_present(line: str, word: str) -> bool:
|
|
356
|
+
import re
|
|
357
|
+
|
|
358
|
+
return re.search(rf"(?<![A-Za-z0-9_]){re.escape(word)}(?![A-Za-z0-9_])", line) is not None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def scan_docs_freshness(
|
|
362
|
+
repo: str | Path | None,
|
|
363
|
+
*,
|
|
364
|
+
plan_path: str | Path | None = None,
|
|
365
|
+
changed_paths: Iterable[str] = (),
|
|
366
|
+
plan_frontmatter: Mapping[str, Any] | None = None,
|
|
367
|
+
env: Mapping[str, str] | None = None,
|
|
368
|
+
) -> dict[str, Any]:
|
|
369
|
+
"""Runner-side pre-scan. Pure of closeout state; returns a JSON-able dict.
|
|
370
|
+
|
|
371
|
+
Result shape (threaded into ``build_phase_loop_closeout`` as
|
|
372
|
+
``docs_freshness=...`` and surfaced in the closeout payload):
|
|
373
|
+
|
|
374
|
+
{
|
|
375
|
+
"status": "passed" | "skipped" | "blocked",
|
|
376
|
+
"mode": "off" | "warn" | "hard",
|
|
377
|
+
"is_release_phase": bool,
|
|
378
|
+
"surfaces_scanned": [<repo-relative paths>],
|
|
379
|
+
"hits": [DocsFreshnessHit, ...],
|
|
380
|
+
"blocking_hits": [<subset that are severity=block>],
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
``status``:
|
|
384
|
+
* ``skipped`` — not a release phase, or mode ``off``, or no repo.
|
|
385
|
+
* ``blocked`` — release phase, mode ``hard``, and ≥1 block-severity hit.
|
|
386
|
+
* ``passed`` — release phase, scanned, no blocking hit (warn hits may
|
|
387
|
+
still be recorded as evidence).
|
|
388
|
+
"""
|
|
389
|
+
mode = resolve_docs_freshness_mode(env)
|
|
390
|
+
fm = dict(plan_frontmatter or {})
|
|
391
|
+
if not fm and plan_path:
|
|
392
|
+
fm = parse_plan_frontmatter(plan_path)
|
|
393
|
+
explicit = is_explicit_release_phase(fm)
|
|
394
|
+
release = is_release_phase(plan_frontmatter=fm, changed_paths=changed_paths)
|
|
395
|
+
|
|
396
|
+
base: dict[str, Any] = {
|
|
397
|
+
"status": "skipped",
|
|
398
|
+
"mode": mode,
|
|
399
|
+
"is_release_phase": release,
|
|
400
|
+
"explicit_release": explicit,
|
|
401
|
+
"surfaces_scanned": [],
|
|
402
|
+
"hits": [],
|
|
403
|
+
"blocking_hits": [],
|
|
404
|
+
# F5: `skipped` is never an evidence-backed pass — keep the flag present
|
|
405
|
+
# so every result has a consistent shape for downstream corroboration.
|
|
406
|
+
"evidence_backed": False,
|
|
407
|
+
}
|
|
408
|
+
if mode == "off" or not release or not repo:
|
|
409
|
+
return base
|
|
410
|
+
repo_path = Path(repo)
|
|
411
|
+
if not repo_path.is_dir():
|
|
412
|
+
return base
|
|
413
|
+
|
|
414
|
+
surfaces = enumerate_public_docs(repo_path)
|
|
415
|
+
hits: list[DocsFreshnessHit] = []
|
|
416
|
+
for rel in surfaces:
|
|
417
|
+
try:
|
|
418
|
+
text = (repo_path / rel).read_text(encoding="utf-8", errors="replace")
|
|
419
|
+
except OSError:
|
|
420
|
+
continue
|
|
421
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
422
|
+
for token, severity in _line_hits(line):
|
|
423
|
+
hits.append(
|
|
424
|
+
DocsFreshnessHit(
|
|
425
|
+
path=rel,
|
|
426
|
+
line=lineno,
|
|
427
|
+
token=token,
|
|
428
|
+
severity=severity,
|
|
429
|
+
excerpt=line.strip()[:200],
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
# The HARD block is opt-in via explicit release frontmatter. A heuristic-only
|
|
433
|
+
# release phase (artifact-glob match, no frontmatter) scans and records
|
|
434
|
+
# evidence, but block-severity hits are downgraded to warn so an ordinary
|
|
435
|
+
# changelog/dep bump can NEVER hard-block the fleet.
|
|
436
|
+
if not explicit:
|
|
437
|
+
hits = [
|
|
438
|
+
h if h.severity == "warn" else replace(h, severity="warn")
|
|
439
|
+
for h in hits
|
|
440
|
+
]
|
|
441
|
+
blocking = [h for h in hits if h.severity == "block"]
|
|
442
|
+
# In "warn" mode the gate records but never blocks. Only explicit-release
|
|
443
|
+
# phases in "hard" mode with a block-severity hit are blocked.
|
|
444
|
+
if blocking and mode == "hard" and explicit:
|
|
445
|
+
status = "blocked"
|
|
446
|
+
elif not surfaces:
|
|
447
|
+
# F5: a `passed` claim must be PROVABLE from enumerated-surface evidence.
|
|
448
|
+
# A release phase whose scan ran but found NO public-doc surfaces has
|
|
449
|
+
# nothing to back a pass — record it as `skipped` (no assertion made)
|
|
450
|
+
# rather than letting a bare/empty detail read as `passed`. The honest
|
|
451
|
+
# outcome here is "could not verify", which `skipped` already denotes.
|
|
452
|
+
status = "skipped"
|
|
453
|
+
else:
|
|
454
|
+
status = "passed"
|
|
455
|
+
result = {
|
|
456
|
+
"status": status,
|
|
457
|
+
"mode": mode,
|
|
458
|
+
"is_release_phase": release,
|
|
459
|
+
"explicit_release": explicit,
|
|
460
|
+
"surfaces_scanned": surfaces,
|
|
461
|
+
"hits": [h.to_json() for h in hits],
|
|
462
|
+
"blocking_hits": [h.to_json() for h in blocking],
|
|
463
|
+
}
|
|
464
|
+
# F5: thread the corroboration verdict alongside the raw status so a
|
|
465
|
+
# downstream consumer (and doc_delta corroboration) never has to re-derive
|
|
466
|
+
# whether a `passed` is backed by enumerated-surface evidence.
|
|
467
|
+
result["evidence_backed"] = docs_freshness_evidence_backed(result)
|
|
468
|
+
return result
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Public/release surface taxonomy for the standalone `docs-audit` backstop.
|
|
2
|
+
|
|
3
|
+
This is the **audit's own** surface map — deliberately standalone (stdlib only, no
|
|
4
|
+
intra-package imports). It does NOT re-export through `models.py` / `release_guard.py`:
|
|
5
|
+
the decision-panel consensus kept the shipped controls untouched (unifying the taxonomy
|
|
6
|
+
there regressed `release_guard`/`doc_delta_validator`). A single canonical taxonomy is a
|
|
7
|
+
later, separately-tested PR; for now a third copy here is the accepted trade — drift in
|
|
8
|
+
new, additive code beats regressing shipped, released controls.
|
|
9
|
+
|
|
10
|
+
Two tiers:
|
|
11
|
+
- RELEASE_AFFECTING_PATTERNS — release-class (version/manifest/install-posture/
|
|
12
|
+
release-workflow). A change here is held to the strict, relevance-bound
|
|
13
|
+
decision contract.
|
|
14
|
+
- GENERAL_PUBLIC_GLOBS — general user-visible surfaces. A change here must carry
|
|
15
|
+
at least a *recorded* decision (closing the silent-absence leak).
|
|
16
|
+
|
|
17
|
+
`RELEASE_DOC_REQUIREMENTS` is the relevance binding (anti-rubber-stamp): a changed
|
|
18
|
+
release surface is only satisfied when its *required* doc surface(s) also changed,
|
|
19
|
+
so a README whitespace edit cannot stand in for a missing CHANGELOG entry.
|
|
20
|
+
|
|
21
|
+
This module depends on stdlib only (no intra-package imports) to stay cycle-free.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import fnmatch
|
|
26
|
+
from typing import Iterable
|
|
27
|
+
|
|
28
|
+
# Release-class surfaces: a change here affects what/how the project ships.
|
|
29
|
+
RELEASE_AFFECTING_PATTERNS = (
|
|
30
|
+
".github/workflows/**",
|
|
31
|
+
"CHANGELOG*",
|
|
32
|
+
"RELEASE*",
|
|
33
|
+
"VERSION",
|
|
34
|
+
"pyproject.toml",
|
|
35
|
+
"setup.cfg",
|
|
36
|
+
"setup.py",
|
|
37
|
+
"uv.lock",
|
|
38
|
+
"poetry.lock",
|
|
39
|
+
"requirements*.txt",
|
|
40
|
+
"docs/release/**",
|
|
41
|
+
"docs/releases/**",
|
|
42
|
+
"docs/release*.md",
|
|
43
|
+
"scripts/*release*",
|
|
44
|
+
"scripts/download-release.py",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# General public surfaces (user-visible; docs may track). Mirrors the historical
|
|
48
|
+
# models.PUBLIC_SURFACE_GLOBS set.
|
|
49
|
+
GENERAL_PUBLIC_GLOBS = (
|
|
50
|
+
"**/cli.py",
|
|
51
|
+
"**/*.proto",
|
|
52
|
+
"**/openapi*.json",
|
|
53
|
+
"**/openapi*.yaml",
|
|
54
|
+
"**/*.openapi.*",
|
|
55
|
+
"**/schema*.json",
|
|
56
|
+
"README.md",
|
|
57
|
+
"CHANGELOG.md",
|
|
58
|
+
"**/_contract_docs/**",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Durable doc surfaces — what a "doc change" can be.
|
|
62
|
+
DOC_SURFACE_GLOBS = (
|
|
63
|
+
"README.md",
|
|
64
|
+
"**/README.md",
|
|
65
|
+
"CHANGELOG.md",
|
|
66
|
+
"**/CHANGELOG*",
|
|
67
|
+
"RELEASE*",
|
|
68
|
+
"docs/**",
|
|
69
|
+
"**/_contract_docs/**",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Relevance binding (IF-0-P1-1): a changed release surface requires its mapped doc
|
|
73
|
+
# surface(s) to ALSO have changed. (release-surface pattern -> required doc patterns)
|
|
74
|
+
RELEASE_DOC_REQUIREMENTS = (
|
|
75
|
+
("VERSION", ("CHANGELOG*",)),
|
|
76
|
+
("pyproject.toml", ("CHANGELOG*",)),
|
|
77
|
+
("setup.cfg", ("CHANGELOG*",)),
|
|
78
|
+
("setup.py", ("CHANGELOG*",)),
|
|
79
|
+
("uv.lock", ("CHANGELOG*",)),
|
|
80
|
+
("poetry.lock", ("CHANGELOG*",)),
|
|
81
|
+
("requirements*.txt", ("CHANGELOG*",)),
|
|
82
|
+
(".github/workflows/**", ("CHANGELOG*", "docs/release*")),
|
|
83
|
+
("scripts/*release*", ("CHANGELOG*", "docs/release*")),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _match(path: str, pattern: str) -> bool:
|
|
88
|
+
"""fnmatch with the historical `**/` prefix special-case (matches at root too)."""
|
|
89
|
+
p = path.replace("\\", "/")
|
|
90
|
+
if fnmatch.fnmatchcase(p, pattern):
|
|
91
|
+
return True
|
|
92
|
+
if pattern.startswith("**/") and fnmatch.fnmatchcase(p, pattern[3:]):
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _any(path: str, patterns: Iterable[str]) -> bool:
|
|
98
|
+
return any(_match(path, pat) for pat in patterns)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_release_surface(path: str) -> bool:
|
|
102
|
+
return _any(path, RELEASE_AFFECTING_PATTERNS)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_general_public_surface(path: str) -> bool:
|
|
106
|
+
return _any(path, GENERAL_PUBLIC_GLOBS)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_doc_surface(path: str) -> bool:
|
|
110
|
+
return _any(path, DOC_SURFACE_GLOBS)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def classify_surface(path: str) -> str | None:
|
|
114
|
+
"""`"release"` | `"general"` | None. Release-class wins (it's the stricter tier)."""
|
|
115
|
+
if is_release_surface(path):
|
|
116
|
+
return "release"
|
|
117
|
+
if is_general_public_surface(path):
|
|
118
|
+
return "general"
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def required_docs_for(path: str) -> tuple[str, ...]:
|
|
123
|
+
"""The doc surface pattern(s) a changed release surface must be matched by."""
|
|
124
|
+
reqs: list[str] = []
|
|
125
|
+
for pat, docs in RELEASE_DOC_REQUIREMENTS:
|
|
126
|
+
if _match(path, pat):
|
|
127
|
+
reqs.extend(docs)
|
|
128
|
+
return tuple(dict.fromkeys(reqs))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
#: Doc-delta validator finding codes that mean docs-freshness is unsatisfied.
|
|
132
|
+
DOC_FRESHNESS_FINDING_CODES: tuple[str, ...] = ("doc_delta_undecided", "release_doc_missing")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def docs_freshness_verdict(
|
|
136
|
+
changed_paths: Iterable[str],
|
|
137
|
+
finding_codes: Iterable[str] = (),
|
|
138
|
+
) -> str:
|
|
139
|
+
"""`"passed" | "skipped" | "blocked"` — the shared Layer-A/Layer-B verdict.
|
|
140
|
+
|
|
141
|
+
``skipped`` when no public surface changed; ``blocked`` when a doc-freshness
|
|
142
|
+
finding fired; ``passed`` when a public surface changed and was satisfied.
|
|
143
|
+
"""
|
|
144
|
+
public = any(classify_surface(p) is not None for p in changed_paths)
|
|
145
|
+
if not public:
|
|
146
|
+
return "skipped"
|
|
147
|
+
codes = set(finding_codes)
|
|
148
|
+
if any(code in codes for code in DOC_FRESHNESS_FINDING_CODES):
|
|
149
|
+
return "blocked"
|
|
150
|
+
return "passed"
|