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,553 @@
|
|
|
1
|
+
"""CS-0.10c -- local-file `LeaseStore` + soft leases.
|
|
2
|
+
|
|
3
|
+
Implements the `LeaseStore` side of the CS-0.10b coordination contract
|
|
4
|
+
(`consiliency_contract`'s `lease.schema.json` / `lease-event.schema.json` /
|
|
5
|
+
`lease-store-protocol.schema.json`, contract >=0.2.0) so a fleet of local
|
|
6
|
+
agents working the same repo(s) in parallel can claim path-sets without
|
|
7
|
+
stepping on each other. Full design: `COORDINATION-deconfliction.md` on
|
|
8
|
+
consiliency-portal (`plans/unification/consiliency-standardization/`).
|
|
9
|
+
|
|
10
|
+
Two layers, kept deliberately separate (mirrors the contract's own split):
|
|
11
|
+
|
|
12
|
+
* :func:`project` -- a PURE function folding an ordered append-only
|
|
13
|
+
lease-event stream down to the current-lease view at a given instant. It
|
|
14
|
+
takes events only, nothing else, by construction (the sole-truth guardrail
|
|
15
|
+
from `coordination-channel-protocol.schema.json`: a coordination message may
|
|
16
|
+
PROMPT an actor to call a store op, but never itself mutates lease state --
|
|
17
|
+
there is no code path here for a message to even reach the projection).
|
|
18
|
+
This is the function the contract's `lease-*`/`coordination-*` conformance
|
|
19
|
+
vectors are asserted against directly (see
|
|
20
|
+
`tests/test_lease_store_conformance.py`).
|
|
21
|
+
* :class:`LeaseStore` -- the local-file backend: appends schema-valid
|
|
22
|
+
lease-events to `.consiliency/leases/events.jsonl` and answers
|
|
23
|
+
`acquire`/`renew`/`release`/`query` by re-running `project`/`project_all`
|
|
24
|
+
over that log. SOFT MODE ONLY: this backend never declares an atomic
|
|
25
|
+
backend (`atomic_backend = False`, always), so a requested hard lease
|
|
26
|
+
degrades to soft here per the contract's
|
|
27
|
+
`atomicity.degrade_without_atomic_backend` -- a real atomic compare-and-set
|
|
28
|
+
hard lease needs an off-device/coordinator backend (CS-0.10d, deferred).
|
|
29
|
+
Give-way policy is REROUTE, not block: an `acquire()` that collides with an
|
|
30
|
+
active lease (same `lease_id`, or an overlapping `scope` held by someone
|
|
31
|
+
else) returns a conflict result carrying the blocking lease so the caller
|
|
32
|
+
can go work something else -- it never raises, blocks, or preempts. TTL is
|
|
33
|
+
authoritative: an unrenewed lease past `heartbeat_at + ttl_seconds`
|
|
34
|
+
(exclusive boundary) is already free, so a dead holder can never freeze a
|
|
35
|
+
path forever.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from datetime import datetime, timedelta
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any, Mapping, Sequence
|
|
44
|
+
|
|
45
|
+
from jsonschema import Draft202012Validator
|
|
46
|
+
from referencing import Registry, Resource
|
|
47
|
+
|
|
48
|
+
from consiliency_contract import load_schema
|
|
49
|
+
|
|
50
|
+
from .consiliency_layout import consiliency_root
|
|
51
|
+
from .models import utc_now
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
import fcntl
|
|
55
|
+
except ImportError: # pragma: no cover -- POSIX-only advisory lock
|
|
56
|
+
fcntl = None # type: ignore[assignment]
|
|
57
|
+
|
|
58
|
+
LEASES_DIRNAME = "leases"
|
|
59
|
+
EVENTS_FILENAME = "events.jsonl"
|
|
60
|
+
|
|
61
|
+
#: The failure modes the contract's operation_semantics pin (used to build
|
|
62
|
+
#: `protocol_descriptor()` and as the closed set every `reason` we return
|
|
63
|
+
#: comes from).
|
|
64
|
+
FAILURE_MODES: tuple[str, ...] = ("conflict", "not-holder", "not-found", "expired")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def leases_root(repo: Path) -> Path:
|
|
68
|
+
return consiliency_root(repo) / LEASES_DIRNAME
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def events_path(repo: Path) -> Path:
|
|
72
|
+
return leases_root(repo) / EVENTS_FILENAME
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Vendored schemas / validators (built lazily, never copied -- CS-0.5/0.11
|
|
77
|
+
# convention: all shapes come from the installed `consiliency_contract`).
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def lease_schema() -> dict[str, Any]:
|
|
82
|
+
return load_schema("lease")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def lease_event_schema() -> dict[str, Any]:
|
|
86
|
+
return load_schema("lease_event")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def lease_store_protocol_schema() -> dict[str, Any]:
|
|
90
|
+
return load_schema("lease_store_protocol")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def lease_validator() -> Draft202012Validator:
|
|
94
|
+
return Draft202012Validator(lease_schema())
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def lease_event_validator() -> Draft202012Validator:
|
|
98
|
+
"""`lease-event.schema.json`'s `lease` property `$ref`s `lease.schema.json`
|
|
99
|
+
by `$id` -- wire the two schemas together via a `referencing` registry so
|
|
100
|
+
an acquire event's nested lease snapshot is validated too."""
|
|
101
|
+
lease = lease_schema()
|
|
102
|
+
registry = Registry().with_resources([(lease["$id"], Resource.from_contents(lease))])
|
|
103
|
+
return Draft202012Validator(lease_event_schema(), registry=registry)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def protocol_descriptor() -> dict[str, Any]:
|
|
107
|
+
"""The contract 0.2.0-pinned `LeaseStore` protocol descriptor this module
|
|
108
|
+
implements. Every field `lease-store-protocol.schema.json` pins is a
|
|
109
|
+
`const` (or a closed enum) -- building this and validating it against
|
|
110
|
+
that schema is a conformance self-check that our declared semantics
|
|
111
|
+
haven't drifted from the contract."""
|
|
112
|
+
return {
|
|
113
|
+
"schema": "consiliency.lease_store_protocol.v1",
|
|
114
|
+
"operations": ["acquire", "renew", "release", "query"],
|
|
115
|
+
"operation_semantics": {
|
|
116
|
+
"acquire": {
|
|
117
|
+
"request": "lease_id, holder, ttl_seconds, mode, scope, phase",
|
|
118
|
+
"response": "granted-lease | conflict",
|
|
119
|
+
"idempotent": False,
|
|
120
|
+
"holder_only": False,
|
|
121
|
+
"rejects": list(FAILURE_MODES),
|
|
122
|
+
},
|
|
123
|
+
"renew": {
|
|
124
|
+
"request": "lease_id, holder",
|
|
125
|
+
"response": "renewed-lease | rejected",
|
|
126
|
+
"idempotent": True,
|
|
127
|
+
"holder_only": True,
|
|
128
|
+
"rejects": list(FAILURE_MODES),
|
|
129
|
+
},
|
|
130
|
+
"release": {
|
|
131
|
+
"request": "lease_id, holder",
|
|
132
|
+
"response": "released | rejected",
|
|
133
|
+
"idempotent": True,
|
|
134
|
+
"holder_only": True,
|
|
135
|
+
"rejects": list(FAILURE_MODES),
|
|
136
|
+
},
|
|
137
|
+
"query": {
|
|
138
|
+
"request": "lease_id | scope",
|
|
139
|
+
"response": "current-lease | empty",
|
|
140
|
+
"idempotent": True,
|
|
141
|
+
"holder_only": False,
|
|
142
|
+
"rejects": list(FAILURE_MODES),
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
"failure_modes": list(FAILURE_MODES),
|
|
146
|
+
"source_of_truth": "lease-store",
|
|
147
|
+
"atomicity": {"hard_requires_atomic_acquire": True, "degrade_without_atomic_backend": "soft"},
|
|
148
|
+
"expiry": {
|
|
149
|
+
"ttl_authoritative": True,
|
|
150
|
+
"heartbeat_renews": True,
|
|
151
|
+
"auto_expiry": True,
|
|
152
|
+
"expires_at_formula": "heartbeat_at + ttl_seconds",
|
|
153
|
+
"boundary": "exclusive",
|
|
154
|
+
},
|
|
155
|
+
"granularity_ladder": {"default": "path-set", "opt_in": "symbol", "out_of_scope": "line"},
|
|
156
|
+
"backends": ["local-file", "portal", "coordinator"],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Timestamps
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _parse_ts(value: str) -> datetime:
|
|
166
|
+
# matches the rest of the runtime's convention (state_degradation.py,
|
|
167
|
+
# observability.py, ...): plain `fromisoformat("...Z")` raises on 3.10.
|
|
168
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _expires_at(heartbeat_at: str, ttl_seconds: int) -> datetime:
|
|
172
|
+
return _parse_ts(heartbeat_at) + timedelta(seconds=ttl_seconds)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def is_expired(lease: Mapping[str, Any], now: str) -> bool:
|
|
176
|
+
"""Exclusive boundary (`lease-store-protocol.schema.json`'s
|
|
177
|
+
`expiry.boundary`): a lease is held over `[acquired_at, expires_at)`; at
|
|
178
|
+
`now == expires_at` it is ALREADY expired."""
|
|
179
|
+
return _parse_ts(now) >= _expires_at(str(lease["heartbeat_at"]), int(lease["ttl_seconds"]))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# The pure conformance core
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def resolve_effective_mode(mode: str, atomic_backend: bool) -> str:
|
|
188
|
+
"""Hard mode is granted only over an atomic backend
|
|
189
|
+
(`atomicity.hard_requires_atomic_acquire`); anything else degrades to
|
|
190
|
+
soft (`atomicity.degrade_without_atomic_backend`)."""
|
|
191
|
+
return "hard" if mode == "hard" and atomic_backend else "soft"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def project(
|
|
195
|
+
events: Sequence[Mapping[str, Any]],
|
|
196
|
+
lease_id: str,
|
|
197
|
+
now: str,
|
|
198
|
+
*,
|
|
199
|
+
atomic_backend: bool = False,
|
|
200
|
+
) -> dict[str, Any] | None:
|
|
201
|
+
"""Fold an ordered append-only lease-event stream (the flat
|
|
202
|
+
`coordination-scenario.schema.json` event shape -- `event/lease_id/holder/
|
|
203
|
+
at` plus `ttl_seconds/mode/scope/phase` on an `acquire`) down to the
|
|
204
|
+
current-lease view for one `lease_id`, evaluated at `now`.
|
|
205
|
+
|
|
206
|
+
Pure and events-only by construction: there is no parameter here a
|
|
207
|
+
coordination message could ever occupy, which is what makes the
|
|
208
|
+
sole-truth guardrail structural rather than a runtime check.
|
|
209
|
+
"""
|
|
210
|
+
state: dict[str, Any] | None = None
|
|
211
|
+
for event in events:
|
|
212
|
+
if event.get("lease_id") != lease_id:
|
|
213
|
+
continue
|
|
214
|
+
kind = event.get("event")
|
|
215
|
+
if kind == "acquire":
|
|
216
|
+
state = {
|
|
217
|
+
"schema": "consiliency.lease.v1",
|
|
218
|
+
"lease_id": lease_id,
|
|
219
|
+
"holder": event["holder"],
|
|
220
|
+
"acquired_at": event["at"],
|
|
221
|
+
"ttl_seconds": event["ttl_seconds"],
|
|
222
|
+
"heartbeat_at": event["at"],
|
|
223
|
+
"mode": resolve_effective_mode(event.get("mode", "soft"), atomic_backend),
|
|
224
|
+
"scope": dict(event["scope"]),
|
|
225
|
+
"phase": event["phase"],
|
|
226
|
+
}
|
|
227
|
+
elif kind == "renew":
|
|
228
|
+
if state is not None:
|
|
229
|
+
state = {**state, "heartbeat_at": event["at"]}
|
|
230
|
+
elif kind in ("release", "expire"):
|
|
231
|
+
state = None
|
|
232
|
+
if state is not None and is_expired(state, now):
|
|
233
|
+
state = None
|
|
234
|
+
return state
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def project_all(
|
|
238
|
+
events: Sequence[Mapping[str, Any]],
|
|
239
|
+
now: str,
|
|
240
|
+
*,
|
|
241
|
+
atomic_backend: bool = False,
|
|
242
|
+
) -> dict[str, dict[str, Any]]:
|
|
243
|
+
"""`project()` every `lease_id` the stream ever mentions; the result is
|
|
244
|
+
exactly the map of currently ACTIVE leases (an expired/released/never-
|
|
245
|
+
granted `lease_id` is simply absent, not present-with-null)."""
|
|
246
|
+
ordered_ids: list[str] = []
|
|
247
|
+
seen: set[str] = set()
|
|
248
|
+
for event in events:
|
|
249
|
+
lease_id = event.get("lease_id")
|
|
250
|
+
if isinstance(lease_id, str) and lease_id not in seen:
|
|
251
|
+
seen.add(lease_id)
|
|
252
|
+
ordered_ids.append(lease_id)
|
|
253
|
+
active: dict[str, dict[str, Any]] = {}
|
|
254
|
+
for lease_id in ordered_ids:
|
|
255
|
+
view = project(events, lease_id, now, atomic_backend=atomic_backend)
|
|
256
|
+
if view is not None:
|
|
257
|
+
active[lease_id] = view
|
|
258
|
+
return active
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _ever_acquired(events: Sequence[Mapping[str, Any]], lease_id: str) -> bool:
|
|
262
|
+
return any(event.get("event") == "acquire" and event.get("lease_id") == lease_id for event in events)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Scope overlap -- acquire-on-intent give-way over the path-set granularity
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _normalize_selector_prefix(selector: str) -> str:
|
|
271
|
+
"""Strip a trailing glob suffix (`/**`, `/*`, `*`) so two path-set
|
|
272
|
+
selectors can be compared as plain path prefixes."""
|
|
273
|
+
value = selector
|
|
274
|
+
while value.endswith("/**") or value.endswith("/*"):
|
|
275
|
+
value = value.rsplit("/", 1)[0]
|
|
276
|
+
return value.rstrip("*").rstrip("/")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _paths_overlap(a: str, b: str) -> bool:
|
|
280
|
+
prefix_a, prefix_b = _normalize_selector_prefix(a), _normalize_selector_prefix(b)
|
|
281
|
+
if prefix_a == prefix_b:
|
|
282
|
+
return True
|
|
283
|
+
return prefix_a.startswith(prefix_b + "/") or prefix_b.startswith(prefix_a + "/")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _scope_paths(scope: Mapping[str, Any]) -> list[str]:
|
|
287
|
+
selectors = [str(s) for s in (scope.get("selector") or [])]
|
|
288
|
+
if scope.get("granularity") == "symbol":
|
|
289
|
+
# a symbol selector anchors "<path>::<symbol>" -- only the path
|
|
290
|
+
# component participates in path-set overlap checks.
|
|
291
|
+
return [selector.split("::", 1)[0] for selector in selectors]
|
|
292
|
+
return selectors
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def scope_overlaps(a: Mapping[str, Any], b: Mapping[str, Any]) -> bool:
|
|
296
|
+
"""Whether two lease scopes claim any of the same ground. `repo`
|
|
297
|
+
granularity always overlaps (it claims everything); `symbol` only
|
|
298
|
+
overlaps on an exact symbol match; `path-set` (the default) overlaps on a
|
|
299
|
+
shared path or path-ancestor relationship."""
|
|
300
|
+
if a.get("granularity") == "repo" or b.get("granularity") == "repo":
|
|
301
|
+
return True
|
|
302
|
+
if a.get("granularity") == "symbol" and b.get("granularity") == "symbol":
|
|
303
|
+
return bool(set(a.get("selector") or []) & set(b.get("selector") or []))
|
|
304
|
+
return any(_paths_overlap(pa, pb) for pa in _scope_paths(a) for pb in _scope_paths(b))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
# Results
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass(frozen=True)
|
|
313
|
+
class AcquireResult:
|
|
314
|
+
granted: bool
|
|
315
|
+
lease: dict[str, Any] | None
|
|
316
|
+
give_way: str | None # "reroute" on conflict; matches COORDINATION-deconfliction.md
|
|
317
|
+
conflict: dict[str, Any] | None # the blocking lease, when granted is False
|
|
318
|
+
degraded: bool # requested hard, granted soft (no atomic backend)
|
|
319
|
+
reason: str | None # one of FAILURE_MODES, or None when granted
|
|
320
|
+
|
|
321
|
+
def to_json(self) -> dict[str, Any]:
|
|
322
|
+
return {
|
|
323
|
+
"granted": self.granted,
|
|
324
|
+
"lease": self.lease,
|
|
325
|
+
"give_way": self.give_way,
|
|
326
|
+
"conflict": self.conflict,
|
|
327
|
+
"degraded": self.degraded,
|
|
328
|
+
"reason": self.reason,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@dataclass(frozen=True)
|
|
333
|
+
class RenewResult:
|
|
334
|
+
renewed: bool
|
|
335
|
+
lease: dict[str, Any] | None
|
|
336
|
+
reason: str | None
|
|
337
|
+
|
|
338
|
+
def to_json(self) -> dict[str, Any]:
|
|
339
|
+
return {"renewed": self.renewed, "lease": self.lease, "reason": self.reason}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass(frozen=True)
|
|
343
|
+
class ReleaseResult:
|
|
344
|
+
released: bool
|
|
345
|
+
reason: str | None
|
|
346
|
+
|
|
347
|
+
def to_json(self) -> dict[str, Any]:
|
|
348
|
+
return {"released": self.released, "reason": self.reason}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# The local-file backend
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class LeaseStore:
|
|
357
|
+
"""Local-file `LeaseStore` backend (CS-0.10c). Single machine, cooperative
|
|
358
|
+
agents. SOFT MODE ONLY: `atomic_backend` is always `False` here, so a
|
|
359
|
+
requested hard lease always degrades to soft -- there is no cross-machine
|
|
360
|
+
(or even cross-process-race-free) atomic compare-and-set available
|
|
361
|
+
locally; that needs the off-device/Portal backend (CS-0.10d).
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
backend = "local-file"
|
|
365
|
+
atomic_backend = False
|
|
366
|
+
|
|
367
|
+
def __init__(self, repo: str | Path):
|
|
368
|
+
self.repo = Path(repo)
|
|
369
|
+
|
|
370
|
+
# -- storage -----------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
def _events_file(self) -> Path:
|
|
373
|
+
return events_path(self.repo)
|
|
374
|
+
|
|
375
|
+
def _read_raw_events(self) -> list[dict[str, Any]]:
|
|
376
|
+
path = self._events_file()
|
|
377
|
+
if not path.exists():
|
|
378
|
+
return []
|
|
379
|
+
events: list[dict[str, Any]] = []
|
|
380
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
381
|
+
line = line.strip()
|
|
382
|
+
if line:
|
|
383
|
+
events.append(json.loads(line))
|
|
384
|
+
return events
|
|
385
|
+
|
|
386
|
+
def _append_raw_event(self, event: Mapping[str, Any]) -> None:
|
|
387
|
+
path = self._events_file()
|
|
388
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
389
|
+
with open(path, "a", encoding="utf-8") as handle:
|
|
390
|
+
if fcntl is not None:
|
|
391
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
392
|
+
try:
|
|
393
|
+
handle.write(json.dumps(dict(event), sort_keys=True) + "\n")
|
|
394
|
+
finally:
|
|
395
|
+
if fcntl is not None:
|
|
396
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
397
|
+
|
|
398
|
+
@staticmethod
|
|
399
|
+
def _to_scenario_event(event: Mapping[str, Any]) -> dict[str, Any]:
|
|
400
|
+
"""Flatten a stored `lease-event.schema.json`-shaped event (an
|
|
401
|
+
`acquire` nests the full lease snapshot under `lease`) into the flat
|
|
402
|
+
shape `project()` folds over."""
|
|
403
|
+
flat: dict[str, Any] = {
|
|
404
|
+
"event": event["event"],
|
|
405
|
+
"lease_id": event["lease_id"],
|
|
406
|
+
"holder": event["holder"],
|
|
407
|
+
"at": event["at"],
|
|
408
|
+
}
|
|
409
|
+
lease = event.get("lease")
|
|
410
|
+
if lease is not None:
|
|
411
|
+
flat.update(
|
|
412
|
+
ttl_seconds=lease["ttl_seconds"],
|
|
413
|
+
mode=lease["mode"],
|
|
414
|
+
scope=lease["scope"],
|
|
415
|
+
phase=lease["phase"],
|
|
416
|
+
)
|
|
417
|
+
return flat
|
|
418
|
+
|
|
419
|
+
def _scenario_events(self) -> list[dict[str, Any]]:
|
|
420
|
+
return [self._to_scenario_event(event) for event in self._read_raw_events()]
|
|
421
|
+
|
|
422
|
+
# -- operations ----------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
def acquire(
|
|
425
|
+
self,
|
|
426
|
+
*,
|
|
427
|
+
lease_id: str,
|
|
428
|
+
holder: str,
|
|
429
|
+
ttl_seconds: int,
|
|
430
|
+
mode: str,
|
|
431
|
+
scope: Mapping[str, Any],
|
|
432
|
+
phase: str,
|
|
433
|
+
now: str | None = None,
|
|
434
|
+
) -> AcquireResult:
|
|
435
|
+
"""Acquire-on-intent over a path-set (default granularity). NOT
|
|
436
|
+
idempotent: re-issuing acquire against an already-active `lease_id`
|
|
437
|
+
always conflicts -- extend a held lease with `renew()` instead. On
|
|
438
|
+
any conflict (same `lease_id` already held, or an overlapping scope
|
|
439
|
+
held by someone else) this returns a result carrying the blocking
|
|
440
|
+
lease and `give_way="reroute"` -- it never blocks or raises."""
|
|
441
|
+
now = now or utc_now()
|
|
442
|
+
active = project_all(self._scenario_events(), now, atomic_backend=self.atomic_backend)
|
|
443
|
+
|
|
444
|
+
existing = active.get(lease_id)
|
|
445
|
+
if existing is not None:
|
|
446
|
+
return AcquireResult(
|
|
447
|
+
granted=False, lease=None, give_way="reroute", conflict=existing, degraded=False, reason="conflict"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
conflicting = next(
|
|
451
|
+
(
|
|
452
|
+
other
|
|
453
|
+
for other_id, other in active.items()
|
|
454
|
+
if other_id != lease_id and other["holder"] != holder and scope_overlaps(scope, other["scope"])
|
|
455
|
+
),
|
|
456
|
+
None,
|
|
457
|
+
)
|
|
458
|
+
if conflicting is not None:
|
|
459
|
+
return AcquireResult(
|
|
460
|
+
granted=False, lease=None, give_way="reroute", conflict=conflicting, degraded=False, reason="conflict"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
effective_mode = resolve_effective_mode(mode, self.atomic_backend)
|
|
464
|
+
degraded = mode == "hard" and effective_mode == "soft"
|
|
465
|
+
lease = {
|
|
466
|
+
"schema": "consiliency.lease.v1",
|
|
467
|
+
"lease_id": lease_id,
|
|
468
|
+
"holder": holder,
|
|
469
|
+
"acquired_at": now,
|
|
470
|
+
"ttl_seconds": ttl_seconds,
|
|
471
|
+
"heartbeat_at": now,
|
|
472
|
+
"mode": effective_mode,
|
|
473
|
+
"scope": dict(scope),
|
|
474
|
+
"phase": phase,
|
|
475
|
+
}
|
|
476
|
+
raw_event = {
|
|
477
|
+
"schema": "consiliency.lease_event.v1",
|
|
478
|
+
"event": "acquire",
|
|
479
|
+
"lease_id": lease_id,
|
|
480
|
+
"holder": holder,
|
|
481
|
+
"at": now,
|
|
482
|
+
"lease": lease,
|
|
483
|
+
}
|
|
484
|
+
lease_event_validator().validate(raw_event)
|
|
485
|
+
self._append_raw_event(raw_event)
|
|
486
|
+
return AcquireResult(granted=True, lease=lease, give_way=None, conflict=None, degraded=degraded, reason=None)
|
|
487
|
+
|
|
488
|
+
def renew(self, *, lease_id: str, holder: str, now: str | None = None) -> RenewResult:
|
|
489
|
+
"""Idempotent, holder-only: extends `heartbeat_at` so the lease stays
|
|
490
|
+
active past its original TTL. Rejects `not-holder` if someone else
|
|
491
|
+
holds it, `expired`/`not-found` if there's nothing to renew."""
|
|
492
|
+
now = now or utc_now()
|
|
493
|
+
raw_events = self._read_raw_events()
|
|
494
|
+
scenario_events = [self._to_scenario_event(event) for event in raw_events]
|
|
495
|
+
current = project(scenario_events, lease_id, now, atomic_backend=self.atomic_backend)
|
|
496
|
+
if current is None:
|
|
497
|
+
reason = "expired" if _ever_acquired(scenario_events, lease_id) else "not-found"
|
|
498
|
+
return RenewResult(renewed=False, lease=None, reason=reason)
|
|
499
|
+
if current["holder"] != holder:
|
|
500
|
+
return RenewResult(renewed=False, lease=None, reason="not-holder")
|
|
501
|
+
|
|
502
|
+
raw_event = {"schema": "consiliency.lease_event.v1", "event": "renew", "lease_id": lease_id, "holder": holder, "at": now}
|
|
503
|
+
lease_event_validator().validate(raw_event)
|
|
504
|
+
self._append_raw_event(raw_event)
|
|
505
|
+
return RenewResult(renewed=True, lease={**current, "heartbeat_at": now}, reason=None)
|
|
506
|
+
|
|
507
|
+
def release(self, *, lease_id: str, holder: str, now: str | None = None) -> ReleaseResult:
|
|
508
|
+
"""Idempotent, holder-only. Releasing an already-free lease (never
|
|
509
|
+
held, already released, or auto-expired) is a no-op success -- that's
|
|
510
|
+
what idempotent means here. Someone else's active lease rejects
|
|
511
|
+
`not-holder`."""
|
|
512
|
+
now = now or utc_now()
|
|
513
|
+
raw_events = self._read_raw_events()
|
|
514
|
+
scenario_events = [self._to_scenario_event(event) for event in raw_events]
|
|
515
|
+
current = project(scenario_events, lease_id, now, atomic_backend=self.atomic_backend)
|
|
516
|
+
if current is None:
|
|
517
|
+
return ReleaseResult(released=True, reason=None)
|
|
518
|
+
if current["holder"] != holder:
|
|
519
|
+
return ReleaseResult(released=False, reason="not-holder")
|
|
520
|
+
|
|
521
|
+
raw_event = {"schema": "consiliency.lease_event.v1", "event": "release", "lease_id": lease_id, "holder": holder, "at": now}
|
|
522
|
+
lease_event_validator().validate(raw_event)
|
|
523
|
+
self._append_raw_event(raw_event)
|
|
524
|
+
return ReleaseResult(released=True, reason=None)
|
|
525
|
+
|
|
526
|
+
def query(
|
|
527
|
+
self,
|
|
528
|
+
*,
|
|
529
|
+
lease_id: str | None = None,
|
|
530
|
+
path: str | None = None,
|
|
531
|
+
scope: Mapping[str, Any] | None = None,
|
|
532
|
+
now: str | None = None,
|
|
533
|
+
) -> dict[str, Any] | None:
|
|
534
|
+
"""The current-lease view, projected from the event log ONLY -- no
|
|
535
|
+
coordination-channel input exists in this signature to mutate it.
|
|
536
|
+
Query by exact `lease_id`, or by `path`/`scope` to find whatever
|
|
537
|
+
active lease (if any) claims that ground."""
|
|
538
|
+
now = now or utc_now()
|
|
539
|
+
scenario_events = self._scenario_events()
|
|
540
|
+
if lease_id is not None:
|
|
541
|
+
return project(scenario_events, lease_id, now, atomic_backend=self.atomic_backend)
|
|
542
|
+
|
|
543
|
+
query_scope = scope if scope is not None else ({"granularity": "path-set", "selector": [path]} if path else None)
|
|
544
|
+
if query_scope is None:
|
|
545
|
+
raise ValueError("query() requires lease_id, path, or scope")
|
|
546
|
+
for other in project_all(scenario_events, now, atomic_backend=self.atomic_backend).values():
|
|
547
|
+
if scope_overlaps(query_scope, other["scope"]):
|
|
548
|
+
return other
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
def list_active(self, *, now: str | None = None) -> dict[str, dict[str, Any]]:
|
|
552
|
+
now = now or utc_now()
|
|
553
|
+
return project_all(self._scenario_events(), now, atomic_backend=self.atomic_backend)
|