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,2144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import fnmatch
|
|
6
|
+
import hashlib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Iterable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PHASE_STATUSES = (
|
|
12
|
+
"unplanned",
|
|
13
|
+
"planned",
|
|
14
|
+
"executing",
|
|
15
|
+
"executed",
|
|
16
|
+
"awaiting_phase_closeout",
|
|
17
|
+
"complete",
|
|
18
|
+
"blocked",
|
|
19
|
+
"unknown",
|
|
20
|
+
)
|
|
21
|
+
EVENT_STATUSES = PHASE_STATUSES + ("plan_skipped",)
|
|
22
|
+
|
|
23
|
+
COMMANDS = ("run", "resume", "status", "dry-run", "maintain-skills", "monitor", "hotfix")
|
|
24
|
+
MODEL_PROFILES = ("roadmap", "plan", "execute", "repair", "review", "skill-maintenance")
|
|
25
|
+
EXECUTORS = ("codex", "claude", "gemini", "opencode", "pi", "command", "manual")
|
|
26
|
+
# Vendor-agnostic model roles (model-routing-v1). "class" not "tier" — tier
|
|
27
|
+
# already denotes evidence-audit budgets (--tier-2/--tier-3).
|
|
28
|
+
MODEL_CLASSES = ("planner", "implementer", "worker")
|
|
29
|
+
WORK_UNIT_KINDS = (
|
|
30
|
+
"roadmap_build",
|
|
31
|
+
"phase_plan",
|
|
32
|
+
"lane_execute",
|
|
33
|
+
"lane_review",
|
|
34
|
+
"phase_reducer",
|
|
35
|
+
"phase_verify",
|
|
36
|
+
"repair",
|
|
37
|
+
"closeout",
|
|
38
|
+
)
|
|
39
|
+
HOTFIX_WORK_UNITS = ("hotfix",)
|
|
40
|
+
HARNESS_WORK_UNIT_PROMPT_KINDS = (
|
|
41
|
+
"implementation",
|
|
42
|
+
"review",
|
|
43
|
+
"reducer",
|
|
44
|
+
"verify",
|
|
45
|
+
"closeout",
|
|
46
|
+
)
|
|
47
|
+
WORK_UNIT_STATUSES = (
|
|
48
|
+
"pending",
|
|
49
|
+
"running",
|
|
50
|
+
"complete",
|
|
51
|
+
"blocked",
|
|
52
|
+
"skipped",
|
|
53
|
+
"superseded",
|
|
54
|
+
"awaiting-closeout",
|
|
55
|
+
)
|
|
56
|
+
WORK_UNIT_METRIC_SCHEMA_VERSION = "work_unit_metric.v1"
|
|
57
|
+
NORMALIZED_EFFORT_LEVELS = ("minimal", "low", "medium", "high", "xhigh", "max")
|
|
58
|
+
UNSUPPORTED_POLICY_BEHAVIORS = ("block", "fallback", "inherit_default")
|
|
59
|
+
CLOSEOUT_MODES = ("manual", "commit", "push")
|
|
60
|
+
# v45 IF-0-FOUND-1: cross-phase scheduler surface. "off"/"serialized" preserve
|
|
61
|
+
# today's one-phase-at-a-time behavior; "concurrent" dispatches the full ready
|
|
62
|
+
# wave. Wiring into the runner main-loop lands in v45 Phase 3 (SCHED).
|
|
63
|
+
PHASE_SCHEDULER_MODES = ("off", "serialized", "concurrent")
|
|
64
|
+
# Closeout-exception vocabulary (roadmap v40 PROTO freeze). GATE/BREAKGLASS import
|
|
65
|
+
# these; they do not redefine them. Sensitivity classes drive the graduated gate:
|
|
66
|
+
# SAFE classes may auto-pass as a recorded soft exception; UNSAFE classes require an
|
|
67
|
+
# explicit break-glass reason. Any path that matches no class is treated as UNSAFE
|
|
68
|
+
# (deny-by-default).
|
|
69
|
+
SAFE_SENSITIVITY_CLASSES = ("docs", "plans", "handoffs", "config_nonsource")
|
|
70
|
+
UNSAFE_SENSITIVITY_CLASSES = ("source", "ci", "secrets", "lockfile")
|
|
71
|
+
SENSITIVITY_CLASSES = SAFE_SENSITIVITY_CLASSES + UNSAFE_SENSITIVITY_CLASSES
|
|
72
|
+
CLOSEOUT_EXCEPTION_KINDS = ("soft", "break_glass")
|
|
73
|
+
CLOSEOUT_EXCEPTIONS_METADATA_KEY = "closeout_exceptions"
|
|
74
|
+
PERMISSION_POSTURES = ("explicit", "permissive", "manual", "unknown")
|
|
75
|
+
SUBAGENT_POSTURES = ("native", "limited", "none", "unknown")
|
|
76
|
+
CLAUDE_EXECUTION_MODES = ("solo", "subagent", "agent_team")
|
|
77
|
+
CLAUDE_WORKTREE_POSTURES = ("phase_loop_managed", "manual_only")
|
|
78
|
+
DISPATCH_HINT_FIELDS = (
|
|
79
|
+
"preferred_executors",
|
|
80
|
+
"allowed_executors",
|
|
81
|
+
"fallback_executors",
|
|
82
|
+
"disabled_executors",
|
|
83
|
+
"required_capabilities",
|
|
84
|
+
)
|
|
85
|
+
DISPATCH_CAPABILITIES = (
|
|
86
|
+
"live_launch",
|
|
87
|
+
"dry_run",
|
|
88
|
+
"skill_bundle_injection",
|
|
89
|
+
"inline_instructions",
|
|
90
|
+
"context_file_instructions",
|
|
91
|
+
"manual_handoff",
|
|
92
|
+
"subagents",
|
|
93
|
+
"explicit_approval_controls",
|
|
94
|
+
"structured_output",
|
|
95
|
+
"browser_automation",
|
|
96
|
+
)
|
|
97
|
+
DISPATCH_SELECTION_PATHS = ("fixed_action_policy", "preferred", "fallback")
|
|
98
|
+
DELEGATION_PRIORITIES = ("low", "normal", "high", "urgent")
|
|
99
|
+
DELEGATION_STATUSES = ("approved", "denied")
|
|
100
|
+
LIVE_PROOF_GATES = ("none", "disposable_proof_required", "disposable_proof_recorded")
|
|
101
|
+
AUTH_PREFLIGHT_MODES = ("none", "metadata_only")
|
|
102
|
+
TIMEOUT_POSTURES = ("runner_managed", "executor_managed", "unknown")
|
|
103
|
+
OUTPUT_CAPTURE_FORMATS = ("combined_output", "json_stream", "terminal_summary")
|
|
104
|
+
PROMOTION_STATUSES = ("live", "proof_gated", "manual_only")
|
|
105
|
+
FAILURE_KINDS = ("adapter_failure", "phase_failure")
|
|
106
|
+
BLOCKER_POSTURES = ("human_required", "repairable_non_human")
|
|
107
|
+
OPERATOR_MATURITY_LABELS = ("live_supported", "proof_blocked", "experimental", "manual_only")
|
|
108
|
+
|
|
109
|
+
BLOCKER_CLASSES = (
|
|
110
|
+
"missing_secret",
|
|
111
|
+
"account_or_billing_setup",
|
|
112
|
+
"admin_approval",
|
|
113
|
+
"destructive_operation",
|
|
114
|
+
"ambiguous_roadmap_selection",
|
|
115
|
+
"product_decision_missing",
|
|
116
|
+
"dirty_worktree_conflict",
|
|
117
|
+
"branch_sync_conflict",
|
|
118
|
+
"stalled_child_observation",
|
|
119
|
+
"repeated_verification_failure",
|
|
120
|
+
"sandbox_command_restriction",
|
|
121
|
+
"upstream_phase_unmet",
|
|
122
|
+
"contract_bug",
|
|
123
|
+
"gold_record_amendment",
|
|
124
|
+
"closeout_evidence_drift",
|
|
125
|
+
"closeout_scope_violation",
|
|
126
|
+
"unretryable_external_outage",
|
|
127
|
+
"stuck_loop",
|
|
128
|
+
"merge_conflict",
|
|
129
|
+
"operator_override_missing_reason",
|
|
130
|
+
"concurrent_dispatch",
|
|
131
|
+
"verification_evidence_missing",
|
|
132
|
+
"review_gate_block",
|
|
133
|
+
"docs_freshness_stale",
|
|
134
|
+
"consiliency_gate_blocked",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
LANE_IR_DIAGNOSTIC_KINDS = (
|
|
138
|
+
"cycle",
|
|
139
|
+
"overlapping_write_ownership",
|
|
140
|
+
"unsafe_concurrent_lane",
|
|
141
|
+
"stale_worktree_assignment",
|
|
142
|
+
"active_work_unit",
|
|
143
|
+
"human_required_blocked_work_unit",
|
|
144
|
+
"missing_producer_dependency",
|
|
145
|
+
"missing_owned_files",
|
|
146
|
+
"malformed_owned_files",
|
|
147
|
+
"malformed_dependencies",
|
|
148
|
+
"unsupported_lane_policy",
|
|
149
|
+
"missing_lane_sections",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
LANE_REDUCER_KINDS = (
|
|
153
|
+
"none",
|
|
154
|
+
"acceptance_reducer",
|
|
155
|
+
"compatibility_reducer",
|
|
156
|
+
"verification_reducer",
|
|
157
|
+
"summary_reducer",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
LANE_SCHEDULER_MODES = ("off", "serialized", "concurrent")
|
|
161
|
+
LANE_WAVE_STATUSES = ("ready", "blocked", "empty")
|
|
162
|
+
WORKTREE_ISOLATION_MODES = ("main_worktree", "git_worktree")
|
|
163
|
+
DIRTY_PATH_CLASSIFICATIONS = (
|
|
164
|
+
"pre_existing",
|
|
165
|
+
"lane_owned",
|
|
166
|
+
"peer_owned",
|
|
167
|
+
"reducer_owned",
|
|
168
|
+
"unowned",
|
|
169
|
+
)
|
|
170
|
+
PIPELINE_MODE_LITERALS = ("standalone", "pipeline_optional", "pipeline_required")
|
|
171
|
+
PIPELINE_PROTECTED_SOURCE_CATEGORIES = (
|
|
172
|
+
"specs",
|
|
173
|
+
"diagrams",
|
|
174
|
+
"adapter_config",
|
|
175
|
+
"definition_files",
|
|
176
|
+
"portal_contracts",
|
|
177
|
+
"phase_artifacts",
|
|
178
|
+
)
|
|
179
|
+
PIPELINE_PROTECTED_SOURCE_ROLES = (
|
|
180
|
+
"seed_spec",
|
|
181
|
+
"predecessor_spec",
|
|
182
|
+
"active_canonical_spec",
|
|
183
|
+
"archived_spec",
|
|
184
|
+
"managed_mirror_file",
|
|
185
|
+
"unmanaged_spec_input",
|
|
186
|
+
"legacy_specs_bundle",
|
|
187
|
+
"root_specs_intake",
|
|
188
|
+
"pipeline_specs_canonical",
|
|
189
|
+
"adapter_configured_intake_root",
|
|
190
|
+
"mirror_manifest",
|
|
191
|
+
"archive_manifest",
|
|
192
|
+
)
|
|
193
|
+
PIPELINE_PROTECTED_SOURCE_LEGACY_ROLES = ("protected",)
|
|
194
|
+
PIPELINE_METADATA_DIAGNOSTIC_KINDS = (
|
|
195
|
+
"invalid_pipeline_mode",
|
|
196
|
+
"missing_source_bundle",
|
|
197
|
+
"missing_source_bundle_sha256",
|
|
198
|
+
"missing_source_bundle_file",
|
|
199
|
+
"mismatched_source_bundle_sha256",
|
|
200
|
+
"malformed_source_bundle",
|
|
201
|
+
"unknown_phase_id",
|
|
202
|
+
"missing_protected_source_entries",
|
|
203
|
+
"missing_protected_source_file",
|
|
204
|
+
"mismatched_protected_source_sha256",
|
|
205
|
+
)
|
|
206
|
+
PHASE_SOURCE_BUNDLE_SCHEMA = "phase-source-bundle.v1"
|
|
207
|
+
PIPELINE_CLOSEOUT_SCHEMA = "phase_loop_closeout.v1"
|
|
208
|
+
PIPELINE_CLOSEOUT_OUTCOMES = (
|
|
209
|
+
"complete",
|
|
210
|
+
"blocked",
|
|
211
|
+
"stale_input",
|
|
212
|
+
"failed_verification",
|
|
213
|
+
"human_required",
|
|
214
|
+
)
|
|
215
|
+
CHANGED_PATH_CATEGORIES = (
|
|
216
|
+
"code",
|
|
217
|
+
"tests",
|
|
218
|
+
"docs",
|
|
219
|
+
"specs",
|
|
220
|
+
"active_canonical_spec",
|
|
221
|
+
"managed_root_mirror_spec",
|
|
222
|
+
"mirror_manifest",
|
|
223
|
+
"archive_manifest",
|
|
224
|
+
"archived_spec",
|
|
225
|
+
"unmanaged_spec",
|
|
226
|
+
"pipeline_sources",
|
|
227
|
+
"portal_contract_refs",
|
|
228
|
+
"greenfield_authority_refs",
|
|
229
|
+
"unknown",
|
|
230
|
+
)
|
|
231
|
+
CANONICAL_REFRESH_REASON_CODES = (
|
|
232
|
+
"docs_source_truth_touched",
|
|
233
|
+
"specs_source_truth_touched",
|
|
234
|
+
"active_specs_touched",
|
|
235
|
+
"managed_mirror_specs_touched",
|
|
236
|
+
"mirror_manifests_touched",
|
|
237
|
+
"archive_manifests_touched",
|
|
238
|
+
"archived_specs_touched",
|
|
239
|
+
"unmanaged_specs_touched",
|
|
240
|
+
"adoption_contracts_touched",
|
|
241
|
+
"contract_refs_touched",
|
|
242
|
+
"pipeline_sources_touched",
|
|
243
|
+
"portal_contract_refs_touched",
|
|
244
|
+
"greenfield_authority_refs_touched",
|
|
245
|
+
)
|
|
246
|
+
REDACTION_POSTURES = ("metadata_only", "rejected_forbidden_metadata")
|
|
247
|
+
SPEC_DELTA_CLOSEOUT_SCHEMA = "spec_delta_closeout.v1"
|
|
248
|
+
SPEC_DELTA_DECISIONS = (
|
|
249
|
+
"no_spec_delta",
|
|
250
|
+
"roadmap_amendment",
|
|
251
|
+
"canonical_spec_update",
|
|
252
|
+
"governed_pipeline_refresh",
|
|
253
|
+
"mirror_cutover_required",
|
|
254
|
+
"dotfiles_skill_source_update",
|
|
255
|
+
"human_source_judgment_required",
|
|
256
|
+
)
|
|
257
|
+
SPEC_DELTA_TARGET_SURFACES = (
|
|
258
|
+
"shared/phase-loop/protocol.md",
|
|
259
|
+
"vendor/phase-loop-runtime/protocol/protocol.md",
|
|
260
|
+
"vendor/phase-loop-runtime/src/phase_loop_runtime/baml_src/emit_phase_closeout.baml",
|
|
261
|
+
"codex-config/skills/**",
|
|
262
|
+
"claude-config/claude-skills/**",
|
|
263
|
+
"gemini-config/skills/**",
|
|
264
|
+
"opencode-config/skills/**",
|
|
265
|
+
"vendor/phase-loop-skills/**",
|
|
266
|
+
"vendor/phase-loop-runtime/tests/**",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# --- rigor-v1 P1: shared review-gate contracts -----------------------------
|
|
270
|
+
# One canonical term for a phase's definition of done, replacing the fragmented
|
|
271
|
+
# "Acceptance criteria" / "Exit criteria" vocabulary across skills/validators.
|
|
272
|
+
DEFINITION_OF_DONE_TERM = "acceptance_criteria"
|
|
273
|
+
|
|
274
|
+
# doc_delta_closeout.v1 — code↔doc currency decision, mirroring spec_delta.
|
|
275
|
+
DOC_DELTA_CLOSEOUT_SCHEMA = "doc_delta_closeout.v1"
|
|
276
|
+
DOC_DELTA_DECISIONS = (
|
|
277
|
+
"no_doc_delta", # public surface unchanged, or change needs no docs
|
|
278
|
+
"docs_updated", # docs updated to match the change
|
|
279
|
+
"docs_follow_up_filed", # tracked for a later phase
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Globs whose change implies a user-visible public surface that docs may track.
|
|
283
|
+
PUBLIC_SURFACE_GLOBS = (
|
|
284
|
+
"**/cli.py",
|
|
285
|
+
"**/*.proto",
|
|
286
|
+
"**/openapi*.json",
|
|
287
|
+
"**/openapi*.yaml",
|
|
288
|
+
"**/*.openapi.*",
|
|
289
|
+
"**/schema*.json",
|
|
290
|
+
"README.md",
|
|
291
|
+
"**/README.md",
|
|
292
|
+
"CHANGELOG.md",
|
|
293
|
+
"CHANGELOG*",
|
|
294
|
+
"**/CHANGELOG.md",
|
|
295
|
+
"RELEASE_NOTES*",
|
|
296
|
+
"RELEASES*",
|
|
297
|
+
"**/_contract_docs/**",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Verification-evidence policy: OFF by default (warn-only) to preserve autonomy;
|
|
301
|
+
# opt-in promotes a missing-evidence finding to `block`. Declining evidence when
|
|
302
|
+
# opted in records one of these typed reason codes.
|
|
303
|
+
VERIFICATION_EVIDENCE_REQUIRED_DEFAULT = False
|
|
304
|
+
VERIFICATION_EVIDENCE_OPT_OUT_REASONS = (
|
|
305
|
+
"no_executable_verification",
|
|
306
|
+
"verification_deferred_to_later_phase",
|
|
307
|
+
"operator_attested_manual",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# UI/visual surfaces: a change here means a screenshot/visual check is *expected*
|
|
311
|
+
# (absence is a `warn` finding by default).
|
|
312
|
+
UI_GLOBS = (
|
|
313
|
+
"**/*.tsx",
|
|
314
|
+
"**/*.jsx",
|
|
315
|
+
"**/*.vue",
|
|
316
|
+
"**/*.svelte",
|
|
317
|
+
"**/*.css",
|
|
318
|
+
"**/*.scss",
|
|
319
|
+
"**/components/**",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
TERMINAL_SUMMARY_FIELDS = (
|
|
323
|
+
"terminal_status",
|
|
324
|
+
"terminal_blocker",
|
|
325
|
+
"verification_status",
|
|
326
|
+
"next_action",
|
|
327
|
+
"dirty_paths",
|
|
328
|
+
"phase_owned_dirty",
|
|
329
|
+
"phase_owned_dirty_paths",
|
|
330
|
+
"previous_phase_owned_paths",
|
|
331
|
+
"unowned_dirty_paths",
|
|
332
|
+
"pre_existing_dirty_paths",
|
|
333
|
+
"artifact_paths",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
from .baml_modular import export_function_schema
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
CLOSEOUT_SCHEMA: dict[str, Any] = export_function_schema("EmitPhaseCloseout")
|
|
340
|
+
|
|
341
|
+
INJECTION_MODES = ("prompt_only", "inline", "stdin", "context_file", "manual")
|
|
342
|
+
PRODUCT_LOOP_ACTIONS = ("roadmap", "plan", "execute", "repair", "review", "maintain-skills")
|
|
343
|
+
COMMAND_ADAPTER_SUPPORTED_ACTIONS = ("roadmap", "plan", "execute", "repair", "review")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def utc_now() -> str:
|
|
347
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def require_literal(value: str, allowed: tuple[str, ...], label: str) -> str:
|
|
351
|
+
if value not in allowed:
|
|
352
|
+
raise ValueError(f"invalid {label}: {value}")
|
|
353
|
+
return value
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@dataclass(frozen=True)
|
|
357
|
+
class ModelSelection:
|
|
358
|
+
profile: str
|
|
359
|
+
model: str
|
|
360
|
+
effort: str
|
|
361
|
+
source: str = "default"
|
|
362
|
+
override_reason: str | None = None
|
|
363
|
+
# model-routing-v1 P4: the resolved role (planner/implementer/worker) when a
|
|
364
|
+
# model_class policy chose the model; None on the legacy/empty-policy path.
|
|
365
|
+
model_class: str | None = None
|
|
366
|
+
|
|
367
|
+
def __post_init__(self) -> None:
|
|
368
|
+
require_literal(self.profile, MODEL_PROFILES, "model profile")
|
|
369
|
+
|
|
370
|
+
def to_json(self) -> dict[str, Any]:
|
|
371
|
+
return clean_dict(asdict(self))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@dataclass(frozen=True)
|
|
375
|
+
class CloseoutException:
|
|
376
|
+
"""A recorded, visible exception to the ownership gate (roadmap v40).
|
|
377
|
+
|
|
378
|
+
Frozen by PROTO so GATE (soft) and BREAKGLASS (break_glass) construct the same
|
|
379
|
+
record. Recorded under the ``closeout.<CLOSEOUT_EXCEPTIONS_METADATA_KEY>``
|
|
380
|
+
metadata key, never counted as a clean pass.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
paths: tuple[str, ...]
|
|
384
|
+
exception_kind: str
|
|
385
|
+
sensitivity_class: str
|
|
386
|
+
reason: str | None = None
|
|
387
|
+
verification_status: str = "passed"
|
|
388
|
+
|
|
389
|
+
def __post_init__(self) -> None:
|
|
390
|
+
require_literal(self.exception_kind, CLOSEOUT_EXCEPTION_KINDS, "closeout exception kind")
|
|
391
|
+
require_literal(self.sensitivity_class, SENSITIVITY_CLASSES, "sensitivity class")
|
|
392
|
+
|
|
393
|
+
def to_json(self) -> dict[str, Any]:
|
|
394
|
+
return clean_dict(asdict(self))
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@dataclass(frozen=True)
|
|
398
|
+
class PromptBundle:
|
|
399
|
+
workflow_command: str
|
|
400
|
+
body: str
|
|
401
|
+
injection_mode: str
|
|
402
|
+
context_body: str | None = None
|
|
403
|
+
expected_skill_pack: tuple[str, ...] = ()
|
|
404
|
+
product_action: str | None = None
|
|
405
|
+
skill_bundle_id: str | None = None
|
|
406
|
+
skill_bundle_sha256: str | None = None
|
|
407
|
+
fallback_mode: str | None = None
|
|
408
|
+
context_path: str | None = None
|
|
409
|
+
recommended_installed_roots: tuple[str, ...] = ()
|
|
410
|
+
installed_skill_roots: tuple[str, ...] = ()
|
|
411
|
+
installed_skill_warnings: tuple[str, ...] = ()
|
|
412
|
+
bridge_skill_inventory: tuple[dict[str, Any], ...] = ()
|
|
413
|
+
|
|
414
|
+
def __post_init__(self) -> None:
|
|
415
|
+
require_literal(self.injection_mode, INJECTION_MODES, "injection mode")
|
|
416
|
+
if self.product_action is not None:
|
|
417
|
+
require_literal(self.product_action, PRODUCT_LOOP_ACTIONS, "product action")
|
|
418
|
+
|
|
419
|
+
def render_prompt(self) -> str:
|
|
420
|
+
body = self.body.strip()
|
|
421
|
+
if not body:
|
|
422
|
+
return self.workflow_command
|
|
423
|
+
return f"{self.workflow_command}\n\n{body}"
|
|
424
|
+
|
|
425
|
+
def render_context(self) -> str:
|
|
426
|
+
body = (self.context_body if self.context_body is not None else self.body).strip()
|
|
427
|
+
if not body:
|
|
428
|
+
return self.workflow_command
|
|
429
|
+
return f"{self.workflow_command}\n\n{body}"
|
|
430
|
+
|
|
431
|
+
def body_sha256(self) -> str:
|
|
432
|
+
return hashlib.sha256(self.body.encode("utf-8")).hexdigest()
|
|
433
|
+
|
|
434
|
+
def context_sha256(self) -> str:
|
|
435
|
+
return hashlib.sha256(self.render_context().encode("utf-8")).hexdigest()
|
|
436
|
+
|
|
437
|
+
def body_line_count(self) -> int:
|
|
438
|
+
return len(self.body.splitlines())
|
|
439
|
+
|
|
440
|
+
def body_char_count(self) -> int:
|
|
441
|
+
return len(self.body)
|
|
442
|
+
|
|
443
|
+
def context_line_count(self) -> int:
|
|
444
|
+
return len(self.render_context().splitlines())
|
|
445
|
+
|
|
446
|
+
def context_char_count(self) -> int:
|
|
447
|
+
return len(self.render_context())
|
|
448
|
+
|
|
449
|
+
def to_json(self) -> dict[str, Any]:
|
|
450
|
+
return clean_dict(
|
|
451
|
+
{
|
|
452
|
+
"workflow_command": self.workflow_command,
|
|
453
|
+
"injection_mode": self.injection_mode,
|
|
454
|
+
"context_sha256": self.context_sha256(),
|
|
455
|
+
"expected_skill_pack": self.expected_skill_pack,
|
|
456
|
+
"product_action": self.product_action,
|
|
457
|
+
"skill_bundle_id": self.skill_bundle_id,
|
|
458
|
+
"skill_bundle_sha256": self.skill_bundle_sha256,
|
|
459
|
+
"fallback_mode": self.fallback_mode,
|
|
460
|
+
"context_path": self.context_path,
|
|
461
|
+
"recommended_installed_roots": self.recommended_installed_roots,
|
|
462
|
+
"installed_skill_roots": self.installed_skill_roots,
|
|
463
|
+
"installed_skill_warnings": self.installed_skill_warnings,
|
|
464
|
+
"bridge_skill_inventory": self.bridge_skill_inventory,
|
|
465
|
+
"body_sha256": self.body_sha256(),
|
|
466
|
+
"body_line_count": self.body_line_count(),
|
|
467
|
+
"body_char_count": self.body_char_count(),
|
|
468
|
+
"context_line_count": self.context_line_count(),
|
|
469
|
+
"context_char_count": self.context_char_count(),
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@dataclass(frozen=True)
|
|
475
|
+
class InjectionMetadata:
|
|
476
|
+
harness_target: str
|
|
477
|
+
injection_mode: str
|
|
478
|
+
context_sha256: str | None = None
|
|
479
|
+
context_line_count: int | None = None
|
|
480
|
+
context_char_count: int | None = None
|
|
481
|
+
expected_skill_pack: tuple[str, ...] = ()
|
|
482
|
+
skill_bundle_id: str | None = None
|
|
483
|
+
skill_bundle_sha256: str | None = None
|
|
484
|
+
context_path: str | None = None
|
|
485
|
+
fallback_mode: str | None = None
|
|
486
|
+
recommended_installed_roots: tuple[str, ...] = ()
|
|
487
|
+
installed_skill_roots: tuple[str, ...] = ()
|
|
488
|
+
installed_skill_warnings: tuple[str, ...] = ()
|
|
489
|
+
bridge_skill_inventory: tuple[dict[str, Any], ...] = ()
|
|
490
|
+
|
|
491
|
+
def __post_init__(self) -> None:
|
|
492
|
+
require_literal(self.harness_target, EXECUTORS, "harness target")
|
|
493
|
+
require_literal(self.injection_mode, INJECTION_MODES, "injection mode")
|
|
494
|
+
|
|
495
|
+
def to_json(self) -> dict[str, Any]:
|
|
496
|
+
return clean_dict(asdict(self))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@dataclass(frozen=True)
|
|
500
|
+
class DispatchHints:
|
|
501
|
+
preferred_executors: tuple[str, ...] = ()
|
|
502
|
+
allowed_executors: tuple[str, ...] = ()
|
|
503
|
+
fallback_executors: tuple[str, ...] = ()
|
|
504
|
+
disabled_executors: tuple[str, ...] = ()
|
|
505
|
+
required_capabilities: tuple[str, ...] = ()
|
|
506
|
+
source: str = "default"
|
|
507
|
+
action: str | None = None
|
|
508
|
+
|
|
509
|
+
def __post_init__(self) -> None:
|
|
510
|
+
for field_name in (
|
|
511
|
+
"preferred_executors",
|
|
512
|
+
"allowed_executors",
|
|
513
|
+
"fallback_executors",
|
|
514
|
+
"disabled_executors",
|
|
515
|
+
):
|
|
516
|
+
values = getattr(self, field_name)
|
|
517
|
+
for value in values:
|
|
518
|
+
require_literal(value, EXECUTORS, field_name)
|
|
519
|
+
for capability in self.required_capabilities:
|
|
520
|
+
require_literal(capability, DISPATCH_CAPABILITIES, "required capability")
|
|
521
|
+
if self.action is not None:
|
|
522
|
+
require_literal(self.action, PRODUCT_LOOP_ACTIONS, "product action")
|
|
523
|
+
|
|
524
|
+
def to_json(self) -> dict[str, Any]:
|
|
525
|
+
return clean_dict(asdict(self))
|
|
526
|
+
|
|
527
|
+
def is_empty(self) -> bool:
|
|
528
|
+
return not any(getattr(self, field_name) for field_name in DISPATCH_HINT_FIELDS)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@dataclass(frozen=True)
|
|
532
|
+
class ProviderPolicyCapability:
|
|
533
|
+
provider: str
|
|
534
|
+
executor: str
|
|
535
|
+
supported_work_units: tuple[str, ...]
|
|
536
|
+
supported_efforts: tuple[str, ...]
|
|
537
|
+
unsupported_policy_behavior: str = "block"
|
|
538
|
+
named_fallback: str | None = None
|
|
539
|
+
default_effort: str | None = None
|
|
540
|
+
effort_map: dict[str, str] = field(default_factory=dict)
|
|
541
|
+
model_aliases: dict[str, str] = field(default_factory=dict)
|
|
542
|
+
requires_run_local_user_scope: bool = False
|
|
543
|
+
notes: tuple[str, ...] = ()
|
|
544
|
+
|
|
545
|
+
def __post_init__(self) -> None:
|
|
546
|
+
require_literal(self.executor, EXECUTORS, "executor")
|
|
547
|
+
for work_unit in self.supported_work_units:
|
|
548
|
+
require_literal(work_unit, WORK_UNIT_KINDS, "work-unit kind")
|
|
549
|
+
for effort in self.supported_efforts:
|
|
550
|
+
require_literal(effort, NORMALIZED_EFFORT_LEVELS, "normalized effort")
|
|
551
|
+
require_literal(self.unsupported_policy_behavior, UNSUPPORTED_POLICY_BEHAVIORS, "unsupported policy behavior")
|
|
552
|
+
if self.default_effort is not None:
|
|
553
|
+
require_literal(self.default_effort, NORMALIZED_EFFORT_LEVELS, "default effort")
|
|
554
|
+
for effort in self.effort_map:
|
|
555
|
+
require_literal(effort, NORMALIZED_EFFORT_LEVELS, "effort map key")
|
|
556
|
+
require_literal(self.effort_map[effort], NORMALIZED_EFFORT_LEVELS, "effort map value")
|
|
557
|
+
for work_unit in self.model_aliases:
|
|
558
|
+
require_literal(work_unit, WORK_UNIT_KINDS, "model alias work-unit kind")
|
|
559
|
+
if self.unsupported_policy_behavior == "fallback" and not self.named_fallback:
|
|
560
|
+
raise ValueError("unsupported fallback policy requires named_fallback")
|
|
561
|
+
|
|
562
|
+
def to_json(self) -> dict[str, Any]:
|
|
563
|
+
return clean_dict(asdict(self))
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@dataclass(frozen=True)
|
|
567
|
+
class WorkUnitPolicy:
|
|
568
|
+
work_unit_kind: str
|
|
569
|
+
effort: str | None = None
|
|
570
|
+
unsupported_policy_behavior: str = "block"
|
|
571
|
+
fallback: str | None = None
|
|
572
|
+
inherit_default: bool = False
|
|
573
|
+
|
|
574
|
+
def __post_init__(self) -> None:
|
|
575
|
+
require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "work-unit kind")
|
|
576
|
+
if self.effort is not None:
|
|
577
|
+
require_literal(self.effort, NORMALIZED_EFFORT_LEVELS, "normalized effort")
|
|
578
|
+
require_literal(self.unsupported_policy_behavior, UNSUPPORTED_POLICY_BEHAVIORS, "unsupported policy behavior")
|
|
579
|
+
if self.unsupported_policy_behavior == "fallback" and not self.fallback:
|
|
580
|
+
raise ValueError("unsupported fallback policy requires fallback")
|
|
581
|
+
if self.unsupported_policy_behavior == "inherit_default" and not self.inherit_default:
|
|
582
|
+
raise ValueError("inherit_default policy must set inherit_default")
|
|
583
|
+
|
|
584
|
+
def to_json(self) -> dict[str, Any]:
|
|
585
|
+
return clean_dict(asdict(self))
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@dataclass(frozen=True)
|
|
589
|
+
class ExecutionPolicyRule:
|
|
590
|
+
selector: str = "default"
|
|
591
|
+
action: str | None = None
|
|
592
|
+
lane: str | None = None
|
|
593
|
+
executor: str | None = None
|
|
594
|
+
model: str | None = None
|
|
595
|
+
model_class: str | None = None
|
|
596
|
+
effort: str | None = None
|
|
597
|
+
work_unit_kind: str | None = None
|
|
598
|
+
unsupported_policy_behavior: str = "block"
|
|
599
|
+
fallback: str | None = None
|
|
600
|
+
inherit_default: bool = False
|
|
601
|
+
source: str = "default"
|
|
602
|
+
override_reason: str | None = None
|
|
603
|
+
|
|
604
|
+
def __post_init__(self) -> None:
|
|
605
|
+
if self.action is not None:
|
|
606
|
+
require_literal(self.action, PRODUCT_LOOP_ACTIONS, "execution policy action")
|
|
607
|
+
if self.executor is not None:
|
|
608
|
+
require_literal(self.executor, EXECUTORS, "execution policy executor")
|
|
609
|
+
if self.model_class is not None:
|
|
610
|
+
require_literal(self.model_class, MODEL_CLASSES, "execution policy model class")
|
|
611
|
+
if self.effort is not None:
|
|
612
|
+
require_literal(self.effort, NORMALIZED_EFFORT_LEVELS, "execution policy effort")
|
|
613
|
+
if self.work_unit_kind is not None:
|
|
614
|
+
require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "execution policy work-unit kind")
|
|
615
|
+
require_literal(self.unsupported_policy_behavior, UNSUPPORTED_POLICY_BEHAVIORS, "unsupported policy behavior")
|
|
616
|
+
if self.model is not None and not self.model.strip():
|
|
617
|
+
raise ValueError("execution policy model must not be empty")
|
|
618
|
+
if self.unsupported_policy_behavior == "fallback" and not self.fallback:
|
|
619
|
+
raise ValueError("execution policy fallback requires fallback")
|
|
620
|
+
if self.unsupported_policy_behavior == "inherit_default" and not self.inherit_default:
|
|
621
|
+
raise ValueError("execution policy inherit_default requires inherit_default")
|
|
622
|
+
|
|
623
|
+
def work_unit_policy(self) -> WorkUnitPolicy | None:
|
|
624
|
+
if self.work_unit_kind is None:
|
|
625
|
+
return None
|
|
626
|
+
return WorkUnitPolicy(
|
|
627
|
+
work_unit_kind=self.work_unit_kind,
|
|
628
|
+
effort=self.effort,
|
|
629
|
+
unsupported_policy_behavior=self.unsupported_policy_behavior,
|
|
630
|
+
fallback=self.fallback,
|
|
631
|
+
inherit_default=self.inherit_default,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def to_json(self) -> dict[str, Any]:
|
|
635
|
+
return clean_dict(asdict(self))
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
@dataclass(frozen=True)
|
|
639
|
+
class ExecutionPolicyParseError:
|
|
640
|
+
path: str
|
|
641
|
+
line_number: int
|
|
642
|
+
raw_line: str
|
|
643
|
+
detail: str
|
|
644
|
+
|
|
645
|
+
def to_json(self) -> dict[str, Any]:
|
|
646
|
+
return {"path": self.path, "line_number": self.line_number, "raw_line": self.raw_line, "detail": self.detail}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@dataclass(frozen=True)
|
|
650
|
+
class ExecutionPolicyDocument:
|
|
651
|
+
rules: tuple[ExecutionPolicyRule, ...] = ()
|
|
652
|
+
source: str = "default"
|
|
653
|
+
parse_error: ExecutionPolicyParseError | None = None
|
|
654
|
+
|
|
655
|
+
def to_json(self) -> dict[str, Any]:
|
|
656
|
+
payload: dict[str, Any] = {"source": self.source, "rules": tuple(rule.to_json() for rule in self.rules)}
|
|
657
|
+
if self.parse_error is not None:
|
|
658
|
+
payload["parse_error"] = self.parse_error.to_json()
|
|
659
|
+
return clean_dict(payload)
|
|
660
|
+
|
|
661
|
+
def is_empty(self) -> bool:
|
|
662
|
+
return not self.rules
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
@dataclass(frozen=True)
|
|
666
|
+
class PipelinePlanMetadata:
|
|
667
|
+
source_bundle: str | None = None
|
|
668
|
+
source_bundle_sha256: str | None = None
|
|
669
|
+
pipeline_phase_id: str | None = None
|
|
670
|
+
pipeline_mode: str | None = None
|
|
671
|
+
|
|
672
|
+
def __post_init__(self) -> None:
|
|
673
|
+
if self.pipeline_mode is not None:
|
|
674
|
+
require_literal(self.pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
|
|
675
|
+
|
|
676
|
+
@property
|
|
677
|
+
def required(self) -> bool:
|
|
678
|
+
return self.pipeline_mode == "pipeline_required"
|
|
679
|
+
|
|
680
|
+
@property
|
|
681
|
+
def empty(self) -> bool:
|
|
682
|
+
return not any((self.source_bundle, self.source_bundle_sha256, self.pipeline_phase_id, self.pipeline_mode))
|
|
683
|
+
|
|
684
|
+
def to_json(self) -> dict[str, Any]:
|
|
685
|
+
return clean_dict(asdict(self))
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
@dataclass(frozen=True)
|
|
689
|
+
class PipelineProtectedSource:
|
|
690
|
+
path: str
|
|
691
|
+
category: str
|
|
692
|
+
sha256: str | None = None
|
|
693
|
+
role: str | None = None
|
|
694
|
+
|
|
695
|
+
def __post_init__(self) -> None:
|
|
696
|
+
if not self.path.strip():
|
|
697
|
+
raise ValueError("protected source path must not be empty")
|
|
698
|
+
require_literal(self.category, PIPELINE_PROTECTED_SOURCE_CATEGORIES, "protected source category")
|
|
699
|
+
if self.role is not None and self.role not in PIPELINE_PROTECTED_SOURCE_LEGACY_ROLES:
|
|
700
|
+
require_literal(self.role, PIPELINE_PROTECTED_SOURCE_ROLES, "protected source role")
|
|
701
|
+
|
|
702
|
+
def to_json(self) -> dict[str, Any]:
|
|
703
|
+
return clean_dict(asdict(self))
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
@dataclass(frozen=True)
|
|
707
|
+
class PipelineMetadataDiagnostic:
|
|
708
|
+
kind: str
|
|
709
|
+
message: str
|
|
710
|
+
metadata: PipelinePlanMetadata | None = None
|
|
711
|
+
expected_sha256: str | None = None
|
|
712
|
+
actual_sha256: str | None = None
|
|
713
|
+
blocker_class: str = "contract_bug"
|
|
714
|
+
human_required: bool = False
|
|
715
|
+
|
|
716
|
+
def __post_init__(self) -> None:
|
|
717
|
+
require_literal(self.kind, PIPELINE_METADATA_DIAGNOSTIC_KINDS, "pipeline metadata diagnostic")
|
|
718
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
|
|
719
|
+
|
|
720
|
+
def to_json(self) -> dict[str, Any]:
|
|
721
|
+
return clean_dict(
|
|
722
|
+
{
|
|
723
|
+
"kind": self.kind,
|
|
724
|
+
"message": self.message,
|
|
725
|
+
"metadata": self.metadata.to_json() if self.metadata else None,
|
|
726
|
+
"expected_sha256": self.expected_sha256,
|
|
727
|
+
"actual_sha256": self.actual_sha256,
|
|
728
|
+
"blocker_class": self.blocker_class,
|
|
729
|
+
"human_required": self.human_required,
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
@dataclass(frozen=True)
|
|
735
|
+
class PhaseSourceBundle:
|
|
736
|
+
path: str
|
|
737
|
+
sha256: str
|
|
738
|
+
phase_id: str
|
|
739
|
+
phase_alias: str
|
|
740
|
+
phase_plan_path: str
|
|
741
|
+
roadmap_path: str
|
|
742
|
+
roadmap_sha256: str
|
|
743
|
+
protected_sources: tuple[PipelineProtectedSource, ...]
|
|
744
|
+
delegated_write_policy: dict[str, Any] = field(default_factory=dict)
|
|
745
|
+
source_files: tuple[dict[str, Any], ...] = ()
|
|
746
|
+
artifact_target_root: str | None = None
|
|
747
|
+
freshness: dict[str, Any] = field(default_factory=dict)
|
|
748
|
+
pipeline_mode: str = "pipeline_optional"
|
|
749
|
+
|
|
750
|
+
def __post_init__(self) -> None:
|
|
751
|
+
if not self.path.strip():
|
|
752
|
+
raise ValueError("source bundle path must not be empty")
|
|
753
|
+
if not self.sha256.strip():
|
|
754
|
+
raise ValueError("source bundle sha256 must not be empty")
|
|
755
|
+
if not self.phase_id.strip():
|
|
756
|
+
raise ValueError("source bundle phase_id must not be empty")
|
|
757
|
+
if not self.phase_alias.strip():
|
|
758
|
+
raise ValueError("source bundle phase_alias must not be empty")
|
|
759
|
+
require_literal(self.pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
|
|
760
|
+
|
|
761
|
+
def plan_metadata(self) -> PipelinePlanMetadata:
|
|
762
|
+
return PipelinePlanMetadata(
|
|
763
|
+
source_bundle=self.path,
|
|
764
|
+
source_bundle_sha256=self.sha256,
|
|
765
|
+
pipeline_phase_id=self.phase_id,
|
|
766
|
+
pipeline_mode=self.pipeline_mode,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
def to_json(self) -> dict[str, Any]:
|
|
770
|
+
return clean_dict(
|
|
771
|
+
{
|
|
772
|
+
"path": self.path,
|
|
773
|
+
"sha256": self.sha256,
|
|
774
|
+
"phase_id": self.phase_id,
|
|
775
|
+
"phase_alias": self.phase_alias,
|
|
776
|
+
"phase_plan_path": self.phase_plan_path,
|
|
777
|
+
"roadmap_path": self.roadmap_path,
|
|
778
|
+
"roadmap_sha256": self.roadmap_sha256,
|
|
779
|
+
"protected_sources": tuple(source.to_json() for source in self.protected_sources),
|
|
780
|
+
"delegated_write_policy": self.delegated_write_policy,
|
|
781
|
+
"source_files": self.source_files,
|
|
782
|
+
"artifact_target_root": self.artifact_target_root,
|
|
783
|
+
"freshness": self.freshness,
|
|
784
|
+
"pipeline_mode": self.pipeline_mode,
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
@dataclass(frozen=True)
|
|
790
|
+
class PhaseLoopAutomation:
|
|
791
|
+
status: str
|
|
792
|
+
next_skill: str | None = None
|
|
793
|
+
next_command: str | None = None
|
|
794
|
+
next_model_hint: str | None = None
|
|
795
|
+
next_effort_hint: str | None = None
|
|
796
|
+
human_required: bool = False
|
|
797
|
+
blocker_class: str | None = None
|
|
798
|
+
blocker_summary: str | None = None
|
|
799
|
+
required_human_inputs: tuple[str, ...] = ()
|
|
800
|
+
verification_status: str = "not_run"
|
|
801
|
+
artifact: str | None = None
|
|
802
|
+
artifact_state: str | None = None
|
|
803
|
+
|
|
804
|
+
def __post_init__(self) -> None:
|
|
805
|
+
require_literal(self.status, PHASE_STATUSES, "automation status")
|
|
806
|
+
if self.blocker_class is not None:
|
|
807
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "automation blocker class")
|
|
808
|
+
if self.next_effort_hint is not None:
|
|
809
|
+
require_literal(self.next_effort_hint, NORMALIZED_EFFORT_LEVELS, "automation effort hint")
|
|
810
|
+
|
|
811
|
+
def to_json(self) -> dict[str, Any]:
|
|
812
|
+
return clean_dict(asdict(self))
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@dataclass(frozen=True)
|
|
816
|
+
class PhaseLoopArtifacts:
|
|
817
|
+
plan_path: str
|
|
818
|
+
plan_sha256: str
|
|
819
|
+
artifact_paths: dict[str, str] = field(default_factory=dict)
|
|
820
|
+
changed_paths: tuple[str, ...] = ()
|
|
821
|
+
evidence_refs: tuple[dict[str, Any], ...] = ()
|
|
822
|
+
|
|
823
|
+
def to_json(self) -> dict[str, Any]:
|
|
824
|
+
return clean_dict(asdict(self))
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
@dataclass(frozen=True)
|
|
828
|
+
class PhaseLoopVerification:
|
|
829
|
+
status: str
|
|
830
|
+
commands: tuple[str, ...] = ()
|
|
831
|
+
results: tuple[dict[str, Any], ...] = ()
|
|
832
|
+
agent_reported_verification_status: str | None = None
|
|
833
|
+
|
|
834
|
+
def to_json(self) -> dict[str, Any]:
|
|
835
|
+
return clean_dict(asdict(self))
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
@dataclass(frozen=True)
|
|
839
|
+
class PhaseLoopBlocker:
|
|
840
|
+
human_required: bool = False
|
|
841
|
+
blocker_class: str | None = None
|
|
842
|
+
blocker_summary: str | None = None
|
|
843
|
+
required_human_inputs: tuple[str, ...] = ()
|
|
844
|
+
access_attempts: tuple[dict[str, Any], ...] = ()
|
|
845
|
+
|
|
846
|
+
def __post_init__(self) -> None:
|
|
847
|
+
if self.blocker_class is not None:
|
|
848
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
|
|
849
|
+
|
|
850
|
+
def to_json(self) -> dict[str, Any]:
|
|
851
|
+
return clean_dict(asdict(self))
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
@dataclass(frozen=True)
|
|
855
|
+
class PhaseLoopSourceBundle:
|
|
856
|
+
path: str | None = None
|
|
857
|
+
sha256: str | None = None
|
|
858
|
+
phase_id: str | None = None
|
|
859
|
+
pipeline_mode: str = "standalone"
|
|
860
|
+
protected_sources: tuple[dict[str, Any], ...] = ()
|
|
861
|
+
|
|
862
|
+
def __post_init__(self) -> None:
|
|
863
|
+
require_literal(self.pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
|
|
864
|
+
|
|
865
|
+
def to_json(self) -> dict[str, Any]:
|
|
866
|
+
data = {
|
|
867
|
+
"path": self.path,
|
|
868
|
+
"sha256": self.sha256,
|
|
869
|
+
"phase_id": self.phase_id,
|
|
870
|
+
"pipeline_mode": self.pipeline_mode,
|
|
871
|
+
"protected_sources": self.protected_sources or None,
|
|
872
|
+
}
|
|
873
|
+
return clean_dict(data)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
@dataclass(frozen=True)
|
|
877
|
+
class SourceTruthImpact:
|
|
878
|
+
changed_path_boundaries: tuple[dict[str, str], ...] = ()
|
|
879
|
+
canonical_refresh_recommended: bool = False
|
|
880
|
+
canonical_refresh_reason_codes: tuple[str, ...] = ()
|
|
881
|
+
redaction_posture: str = "metadata_only"
|
|
882
|
+
|
|
883
|
+
def __post_init__(self) -> None:
|
|
884
|
+
require_literal(self.redaction_posture, REDACTION_POSTURES, "redaction posture")
|
|
885
|
+
for boundary in self.changed_path_boundaries:
|
|
886
|
+
require_literal(str(boundary.get("category")), CHANGED_PATH_CATEGORIES, "changed path category")
|
|
887
|
+
for reason in self.canonical_refresh_reason_codes:
|
|
888
|
+
require_literal(reason, CANONICAL_REFRESH_REASON_CODES, "canonical refresh reason code")
|
|
889
|
+
|
|
890
|
+
def to_json(self) -> dict[str, Any]:
|
|
891
|
+
return clean_dict(asdict(self))
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
@dataclass(frozen=True)
|
|
895
|
+
class SpecDeltaCloseout:
|
|
896
|
+
decision: str
|
|
897
|
+
target_surfaces: tuple[str, ...] = ()
|
|
898
|
+
evidence_paths: tuple[str, ...] = ()
|
|
899
|
+
redaction_posture: str = "metadata_only"
|
|
900
|
+
blocker_class: str | None = None
|
|
901
|
+
schema: str = SPEC_DELTA_CLOSEOUT_SCHEMA
|
|
902
|
+
|
|
903
|
+
def __post_init__(self) -> None:
|
|
904
|
+
if self.schema != SPEC_DELTA_CLOSEOUT_SCHEMA:
|
|
905
|
+
raise ValueError(f"invalid spec delta closeout schema: {self.schema}")
|
|
906
|
+
require_literal(self.decision, SPEC_DELTA_DECISIONS, "spec delta decision")
|
|
907
|
+
require_literal(self.redaction_posture, REDACTION_POSTURES, "redaction posture")
|
|
908
|
+
if self.blocker_class is not None:
|
|
909
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
|
|
910
|
+
|
|
911
|
+
def to_json(self) -> dict[str, Any]:
|
|
912
|
+
return clean_dict(asdict(self))
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _glob_touched(paths: Iterable[str], globs: Iterable[str]) -> bool:
|
|
916
|
+
"""True if any path matches any glob (POSIX, case-sensitive)."""
|
|
917
|
+
pats = tuple(globs)
|
|
918
|
+
for raw in paths:
|
|
919
|
+
path = str(raw).strip()
|
|
920
|
+
if not path:
|
|
921
|
+
continue
|
|
922
|
+
for pattern in pats:
|
|
923
|
+
if fnmatch.fnmatchcase(path, pattern):
|
|
924
|
+
return True
|
|
925
|
+
# `**/x` should also match a top-level `x`.
|
|
926
|
+
if pattern.startswith("**/") and fnmatch.fnmatchcase(path, pattern[3:]):
|
|
927
|
+
return True
|
|
928
|
+
return False
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def public_surface_touched(changed_paths: Iterable[str]) -> bool:
|
|
932
|
+
"""True if a changed path looks like a user-visible public surface (rigor-v1 P1/P2)."""
|
|
933
|
+
return _glob_touched(changed_paths, PUBLIC_SURFACE_GLOBS)
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def ui_change_detected(changed_paths: Iterable[str]) -> bool:
|
|
937
|
+
"""True if a changed path is a UI/visual surface (rigor-v1 P1/P6)."""
|
|
938
|
+
return _glob_touched(changed_paths, UI_GLOBS)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
@dataclass(frozen=True)
|
|
942
|
+
class DocDeltaCloseout:
|
|
943
|
+
"""doc_delta_closeout.v1 — records the code↔doc currency decision for a phase."""
|
|
944
|
+
|
|
945
|
+
decision: str
|
|
946
|
+
target_surfaces: tuple[str, ...] = ()
|
|
947
|
+
evidence_paths: tuple[str, ...] = ()
|
|
948
|
+
justification: str | None = None
|
|
949
|
+
schema: str = DOC_DELTA_CLOSEOUT_SCHEMA
|
|
950
|
+
|
|
951
|
+
def __post_init__(self) -> None:
|
|
952
|
+
if self.schema != DOC_DELTA_CLOSEOUT_SCHEMA:
|
|
953
|
+
raise ValueError(f"invalid doc delta closeout schema: {self.schema}")
|
|
954
|
+
require_literal(self.decision, DOC_DELTA_DECISIONS, "doc delta decision")
|
|
955
|
+
|
|
956
|
+
def to_json(self) -> dict[str, Any]:
|
|
957
|
+
return clean_dict(asdict(self))
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
@dataclass(frozen=True)
|
|
961
|
+
class VisualEvidence:
|
|
962
|
+
"""Screenshot/artifact evidence that a UI change was visually verified (rigor-v1 P1/P6)."""
|
|
963
|
+
|
|
964
|
+
artifact_paths: tuple[str, ...] = ()
|
|
965
|
+
observed: str | None = None
|
|
966
|
+
|
|
967
|
+
def to_json(self) -> dict[str, Any]:
|
|
968
|
+
return clean_dict(asdict(self))
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
@dataclass(frozen=True)
|
|
972
|
+
class PhaseLoopCloseout:
|
|
973
|
+
phase: str
|
|
974
|
+
terminal_status: str
|
|
975
|
+
automation: PhaseLoopAutomation
|
|
976
|
+
artifacts: PhaseLoopArtifacts
|
|
977
|
+
verification: PhaseLoopVerification
|
|
978
|
+
blocker: PhaseLoopBlocker
|
|
979
|
+
source_bundle: PhaseLoopSourceBundle
|
|
980
|
+
source_truth_impact: SourceTruthImpact | None = None
|
|
981
|
+
spec_delta_closeout: SpecDeltaCloseout | None = None
|
|
982
|
+
doc_delta_closeout: DocDeltaCloseout | None = None
|
|
983
|
+
schema: str = PIPELINE_CLOSEOUT_SCHEMA
|
|
984
|
+
|
|
985
|
+
def __post_init__(self) -> None:
|
|
986
|
+
if self.schema != PIPELINE_CLOSEOUT_SCHEMA:
|
|
987
|
+
raise ValueError(f"invalid closeout schema: {self.schema}")
|
|
988
|
+
require_literal(self.terminal_status, PIPELINE_CLOSEOUT_OUTCOMES, "terminal status")
|
|
989
|
+
|
|
990
|
+
def to_json(self) -> dict[str, Any]:
|
|
991
|
+
return clean_dict(
|
|
992
|
+
{
|
|
993
|
+
"schema": self.schema,
|
|
994
|
+
"phase": self.phase,
|
|
995
|
+
"terminal_status": self.terminal_status,
|
|
996
|
+
"automation": self.automation.to_json(),
|
|
997
|
+
"artifacts": self.artifacts.to_json(),
|
|
998
|
+
"verification": self.verification.to_json(),
|
|
999
|
+
"blocker": self.blocker.to_json(),
|
|
1000
|
+
"source_bundle": self.source_bundle.to_json(),
|
|
1001
|
+
"source_truth_impact": self.source_truth_impact.to_json() if self.source_truth_impact else None,
|
|
1002
|
+
"spec_delta_closeout": self.spec_delta_closeout.to_json() if self.spec_delta_closeout else None,
|
|
1003
|
+
"doc_delta_closeout": self.doc_delta_closeout.to_json() if self.doc_delta_closeout else None,
|
|
1004
|
+
}
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
@dataclass(frozen=True)
|
|
1009
|
+
class WorkUnitIdentity:
|
|
1010
|
+
phase: str
|
|
1011
|
+
kind: str
|
|
1012
|
+
lane_id: str
|
|
1013
|
+
attempt: int
|
|
1014
|
+
|
|
1015
|
+
def __post_init__(self) -> None:
|
|
1016
|
+
require_literal(self.kind, WORK_UNIT_KINDS, "work-unit kind")
|
|
1017
|
+
if not self.phase.strip():
|
|
1018
|
+
raise ValueError("work-unit phase must not be empty")
|
|
1019
|
+
if not self.lane_id.strip():
|
|
1020
|
+
raise ValueError("work-unit lane id must not be empty")
|
|
1021
|
+
if self.attempt < 1:
|
|
1022
|
+
raise ValueError("work-unit attempt must be positive")
|
|
1023
|
+
|
|
1024
|
+
@property
|
|
1025
|
+
def work_unit_id(self) -> str:
|
|
1026
|
+
return f"{self.phase}.{self.kind}.{self.lane_id}.{self.attempt}"
|
|
1027
|
+
|
|
1028
|
+
@classmethod
|
|
1029
|
+
def from_id(cls, work_unit_id: str) -> "WorkUnitIdentity":
|
|
1030
|
+
parts = work_unit_id.split(".")
|
|
1031
|
+
if len(parts) < 4:
|
|
1032
|
+
raise ValueError(f"invalid work-unit id: {work_unit_id}")
|
|
1033
|
+
phase = parts[0]
|
|
1034
|
+
kind = parts[1]
|
|
1035
|
+
lane_id = ".".join(parts[2:-1])
|
|
1036
|
+
try:
|
|
1037
|
+
attempt = int(parts[-1])
|
|
1038
|
+
except ValueError as exc:
|
|
1039
|
+
raise ValueError(f"invalid work-unit attempt: {parts[-1]}") from exc
|
|
1040
|
+
return cls(phase=phase, kind=kind, lane_id=lane_id, attempt=attempt)
|
|
1041
|
+
|
|
1042
|
+
@classmethod
|
|
1043
|
+
def from_json(cls, data: dict[str, Any]) -> "WorkUnitIdentity":
|
|
1044
|
+
if data.get("work_unit_id") and not all(key in data for key in ("phase", "kind", "lane_id", "attempt")):
|
|
1045
|
+
return cls.from_id(str(data["work_unit_id"]))
|
|
1046
|
+
return cls(
|
|
1047
|
+
phase=str(data["phase"]),
|
|
1048
|
+
kind=str(data["kind"]),
|
|
1049
|
+
lane_id=str(data["lane_id"]),
|
|
1050
|
+
attempt=int(data["attempt"]),
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
def to_json(self) -> dict[str, Any]:
|
|
1054
|
+
return clean_dict({**asdict(self), "work_unit_id": self.work_unit_id})
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
@dataclass(frozen=True)
|
|
1058
|
+
class WorkUnitAttempt:
|
|
1059
|
+
identity: WorkUnitIdentity
|
|
1060
|
+
status: str = "pending"
|
|
1061
|
+
parent_phase_event_id: str | None = None
|
|
1062
|
+
policy: dict[str, Any] = field(default_factory=dict)
|
|
1063
|
+
artifacts: dict[str, str] = field(default_factory=dict)
|
|
1064
|
+
started_at: str | None = None
|
|
1065
|
+
finished_at: str | None = None
|
|
1066
|
+
heartbeat_path: str | None = None
|
|
1067
|
+
terminal_summary_path: str | None = None
|
|
1068
|
+
retry_of: str | None = None
|
|
1069
|
+
superseded_by: str | None = None
|
|
1070
|
+
|
|
1071
|
+
def __post_init__(self) -> None:
|
|
1072
|
+
require_literal(self.status, WORK_UNIT_STATUSES, "work-unit status")
|
|
1073
|
+
|
|
1074
|
+
def to_json(self) -> dict[str, Any]:
|
|
1075
|
+
return clean_dict(
|
|
1076
|
+
{
|
|
1077
|
+
"identity": self.identity.to_json(),
|
|
1078
|
+
"status": self.status,
|
|
1079
|
+
"parent_phase_event_id": self.parent_phase_event_id,
|
|
1080
|
+
"policy": self.policy,
|
|
1081
|
+
"artifacts": self.artifacts,
|
|
1082
|
+
"started_at": self.started_at,
|
|
1083
|
+
"finished_at": self.finished_at,
|
|
1084
|
+
"heartbeat_path": self.heartbeat_path,
|
|
1085
|
+
"terminal_summary_path": self.terminal_summary_path,
|
|
1086
|
+
"retry_of": self.retry_of,
|
|
1087
|
+
"superseded_by": self.superseded_by,
|
|
1088
|
+
}
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
@dataclass(frozen=True)
|
|
1093
|
+
class WorkUnitCloseout:
|
|
1094
|
+
identity: WorkUnitIdentity
|
|
1095
|
+
status: str
|
|
1096
|
+
automation: dict[str, Any] = field(default_factory=dict)
|
|
1097
|
+
terminal_summary: dict[str, Any] = field(default_factory=dict)
|
|
1098
|
+
closeout_summary: dict[str, Any] = field(default_factory=dict)
|
|
1099
|
+
wave_id: str | None = None
|
|
1100
|
+
worktree_path: str | None = None
|
|
1101
|
+
changed_paths: tuple[str, ...] = ()
|
|
1102
|
+
verification_status: str | None = None
|
|
1103
|
+
evidence_refs: tuple[dict[str, Any], ...] = ()
|
|
1104
|
+
human_required: bool = False
|
|
1105
|
+
blocker_class: str | None = None
|
|
1106
|
+
blocker_summary: str | None = None
|
|
1107
|
+
required_human_inputs: tuple[str, ...] = ()
|
|
1108
|
+
|
|
1109
|
+
def __post_init__(self) -> None:
|
|
1110
|
+
require_literal(self.status, WORK_UNIT_STATUSES, "work-unit status")
|
|
1111
|
+
if self.blocker_class is not None:
|
|
1112
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
|
|
1113
|
+
|
|
1114
|
+
def to_json(self) -> dict[str, Any]:
|
|
1115
|
+
return clean_dict(
|
|
1116
|
+
{
|
|
1117
|
+
"identity": self.identity.to_json(),
|
|
1118
|
+
"status": self.status,
|
|
1119
|
+
"automation": self.automation,
|
|
1120
|
+
"terminal_summary": self.terminal_summary,
|
|
1121
|
+
"closeout_summary": self.closeout_summary,
|
|
1122
|
+
"wave_id": self.wave_id,
|
|
1123
|
+
"worktree_path": self.worktree_path,
|
|
1124
|
+
"changed_paths": self.changed_paths,
|
|
1125
|
+
"verification_status": self.verification_status,
|
|
1126
|
+
"evidence_refs": self.evidence_refs,
|
|
1127
|
+
"human_required": self.human_required,
|
|
1128
|
+
"blocker_class": self.blocker_class,
|
|
1129
|
+
"blocker_summary": self.blocker_summary,
|
|
1130
|
+
"required_human_inputs": self.required_human_inputs,
|
|
1131
|
+
}
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
@dataclass(frozen=True)
|
|
1136
|
+
class WorkUnitEventMetadata:
|
|
1137
|
+
identity: WorkUnitIdentity
|
|
1138
|
+
status: str
|
|
1139
|
+
event_type: str = "status"
|
|
1140
|
+
timestamp: str = field(default_factory=utc_now)
|
|
1141
|
+
launch_metadata: dict[str, Any] = field(default_factory=dict)
|
|
1142
|
+
heartbeat_path: str | None = None
|
|
1143
|
+
terminal_summary_path: str | None = None
|
|
1144
|
+
closeout_summary: dict[str, Any] = field(default_factory=dict)
|
|
1145
|
+
retry_of: str | None = None
|
|
1146
|
+
superseded_by: str | None = None
|
|
1147
|
+
blocker: dict[str, Any] | None = None
|
|
1148
|
+
|
|
1149
|
+
def __post_init__(self) -> None:
|
|
1150
|
+
require_literal(self.status, WORK_UNIT_STATUSES, "work-unit status")
|
|
1151
|
+
|
|
1152
|
+
def to_json(self) -> dict[str, Any]:
|
|
1153
|
+
return clean_dict(
|
|
1154
|
+
{
|
|
1155
|
+
"event_kind": "work_unit",
|
|
1156
|
+
"event_type": self.event_type,
|
|
1157
|
+
"timestamp": self.timestamp,
|
|
1158
|
+
"work_unit": {
|
|
1159
|
+
**self.identity.to_json(),
|
|
1160
|
+
"status": self.status,
|
|
1161
|
+
"launch_metadata": self.launch_metadata,
|
|
1162
|
+
"heartbeat_path": self.heartbeat_path,
|
|
1163
|
+
"terminal_summary_path": self.terminal_summary_path,
|
|
1164
|
+
"closeout_summary": self.closeout_summary,
|
|
1165
|
+
"retry_of": self.retry_of,
|
|
1166
|
+
"superseded_by": self.superseded_by,
|
|
1167
|
+
"blocker": self.blocker,
|
|
1168
|
+
},
|
|
1169
|
+
}
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
@dataclass(frozen=True)
|
|
1174
|
+
class WorkUnitState:
|
|
1175
|
+
identity: WorkUnitIdentity
|
|
1176
|
+
status: str = "pending"
|
|
1177
|
+
created_at: str = field(default_factory=utc_now)
|
|
1178
|
+
updated_at: str = field(default_factory=utc_now)
|
|
1179
|
+
parent_phase_event_id: str | None = None
|
|
1180
|
+
policy: dict[str, Any] = field(default_factory=dict)
|
|
1181
|
+
artifacts: dict[str, str] = field(default_factory=dict)
|
|
1182
|
+
heartbeat_path: str | None = None
|
|
1183
|
+
terminal_summary_path: str | None = None
|
|
1184
|
+
closeout_summary: dict[str, Any] = field(default_factory=dict)
|
|
1185
|
+
retry_of: str | None = None
|
|
1186
|
+
superseded_by: str | None = None
|
|
1187
|
+
blocker: dict[str, Any] | None = None
|
|
1188
|
+
human_required: bool = False
|
|
1189
|
+
|
|
1190
|
+
def __post_init__(self) -> None:
|
|
1191
|
+
require_literal(self.status, WORK_UNIT_STATUSES, "work-unit status")
|
|
1192
|
+
|
|
1193
|
+
@property
|
|
1194
|
+
def work_unit_id(self) -> str:
|
|
1195
|
+
return self.identity.work_unit_id
|
|
1196
|
+
|
|
1197
|
+
@classmethod
|
|
1198
|
+
def from_json(cls, data: dict[str, Any]) -> "WorkUnitState":
|
|
1199
|
+
identity_data = data.get("identity") if isinstance(data.get("identity"), dict) else data
|
|
1200
|
+
identity = WorkUnitIdentity.from_json(identity_data)
|
|
1201
|
+
return cls(
|
|
1202
|
+
identity=identity,
|
|
1203
|
+
status=str(data.get("status", "pending")),
|
|
1204
|
+
created_at=str(data.get("created_at") or utc_now()),
|
|
1205
|
+
updated_at=str(data.get("updated_at") or data.get("created_at") or utc_now()),
|
|
1206
|
+
parent_phase_event_id=data.get("parent_phase_event_id"),
|
|
1207
|
+
policy=dict(data.get("policy", {})) if isinstance(data.get("policy"), dict) else {},
|
|
1208
|
+
artifacts={str(k): str(v) for k, v in dict(data.get("artifacts", {})).items()} if isinstance(data.get("artifacts"), dict) else {},
|
|
1209
|
+
heartbeat_path=data.get("heartbeat_path"),
|
|
1210
|
+
terminal_summary_path=data.get("terminal_summary_path"),
|
|
1211
|
+
closeout_summary=dict(data.get("closeout_summary", {})) if isinstance(data.get("closeout_summary"), dict) else {},
|
|
1212
|
+
retry_of=data.get("retry_of"),
|
|
1213
|
+
superseded_by=data.get("superseded_by"),
|
|
1214
|
+
blocker=dict(data.get("blocker")) if isinstance(data.get("blocker"), dict) else None,
|
|
1215
|
+
human_required=bool(data.get("human_required", False)),
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
def with_status(self, status: str, **updates: Any) -> "WorkUnitState":
|
|
1219
|
+
require_literal(status, WORK_UNIT_STATUSES, "work-unit status")
|
|
1220
|
+
data = {**asdict(self), **updates, "status": status, "updated_at": updates.get("updated_at", utc_now())}
|
|
1221
|
+
data["identity"] = self.identity
|
|
1222
|
+
return WorkUnitState(**data)
|
|
1223
|
+
|
|
1224
|
+
def to_json(self) -> dict[str, Any]:
|
|
1225
|
+
return clean_dict(
|
|
1226
|
+
{
|
|
1227
|
+
"identity": self.identity.to_json(),
|
|
1228
|
+
"work_unit_id": self.work_unit_id,
|
|
1229
|
+
"status": self.status,
|
|
1230
|
+
"created_at": self.created_at,
|
|
1231
|
+
"updated_at": self.updated_at,
|
|
1232
|
+
"parent_phase_event_id": self.parent_phase_event_id,
|
|
1233
|
+
"policy": self.policy,
|
|
1234
|
+
"artifacts": self.artifacts,
|
|
1235
|
+
"heartbeat_path": self.heartbeat_path,
|
|
1236
|
+
"terminal_summary_path": self.terminal_summary_path,
|
|
1237
|
+
"closeout_summary": self.closeout_summary,
|
|
1238
|
+
"retry_of": self.retry_of,
|
|
1239
|
+
"superseded_by": self.superseded_by,
|
|
1240
|
+
"blocker": self.blocker,
|
|
1241
|
+
"human_required": self.human_required,
|
|
1242
|
+
}
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
@dataclass(frozen=True)
|
|
1247
|
+
class LaneDependency:
|
|
1248
|
+
source_lane_id: str
|
|
1249
|
+
target_lane_id: str
|
|
1250
|
+
relation: str = "depends_on"
|
|
1251
|
+
|
|
1252
|
+
def to_json(self) -> dict[str, Any]:
|
|
1253
|
+
return clean_dict(asdict(self))
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
@dataclass(frozen=True)
|
|
1257
|
+
class LaneTaskSet:
|
|
1258
|
+
test: tuple[str, ...] = ()
|
|
1259
|
+
impl: tuple[str, ...] = ()
|
|
1260
|
+
verify: tuple[str, ...] = ()
|
|
1261
|
+
other: tuple[str, ...] = ()
|
|
1262
|
+
|
|
1263
|
+
def to_json(self) -> dict[str, Any]:
|
|
1264
|
+
return clean_dict(asdict(self))
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
@dataclass(frozen=True)
|
|
1268
|
+
class LaneIRDiagnostic(Exception):
|
|
1269
|
+
kind: str
|
|
1270
|
+
message: str
|
|
1271
|
+
lane_id: str | None = None
|
|
1272
|
+
blocker_class: str = "contract_bug"
|
|
1273
|
+
human_required: bool = False
|
|
1274
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
1275
|
+
|
|
1276
|
+
def __post_init__(self) -> None:
|
|
1277
|
+
require_literal(self.kind, LANE_IR_DIAGNOSTIC_KINDS, "lane IR diagnostic kind")
|
|
1278
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "lane IR blocker class")
|
|
1279
|
+
|
|
1280
|
+
def to_json(self) -> dict[str, Any]:
|
|
1281
|
+
return clean_dict(asdict(self))
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
@dataclass(frozen=True)
|
|
1285
|
+
class PhasePlanLane:
|
|
1286
|
+
lane_id: str
|
|
1287
|
+
name: str
|
|
1288
|
+
heading: str
|
|
1289
|
+
owned_files: tuple[str, ...] = ()
|
|
1290
|
+
read_only: bool = False
|
|
1291
|
+
depends_on: tuple[str, ...] = ()
|
|
1292
|
+
blocks: tuple[str, ...] = ()
|
|
1293
|
+
interfaces_provided: tuple[str, ...] = ()
|
|
1294
|
+
interfaces_consumed: tuple[str, ...] = ()
|
|
1295
|
+
tasks: LaneTaskSet = field(default_factory=LaneTaskSet)
|
|
1296
|
+
verification_commands: tuple[str, ...] = ()
|
|
1297
|
+
parallel_safe: bool = False
|
|
1298
|
+
reducer_kind: str = "none"
|
|
1299
|
+
execution_policy: ExecutionPolicyRule | None = None
|
|
1300
|
+
|
|
1301
|
+
def __post_init__(self) -> None:
|
|
1302
|
+
require_literal(self.reducer_kind, LANE_REDUCER_KINDS, "lane reducer kind")
|
|
1303
|
+
|
|
1304
|
+
def to_json(self) -> dict[str, Any]:
|
|
1305
|
+
data = asdict(self)
|
|
1306
|
+
if self.execution_policy is not None:
|
|
1307
|
+
data["execution_policy"] = self.execution_policy.to_json()
|
|
1308
|
+
return clean_dict(data)
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
@dataclass(frozen=True)
|
|
1312
|
+
class PhasePlanIR:
|
|
1313
|
+
plan_path: str
|
|
1314
|
+
metadata: dict[str, str]
|
|
1315
|
+
lanes: tuple[PhasePlanLane, ...] = ()
|
|
1316
|
+
dependencies: tuple[LaneDependency, ...] = ()
|
|
1317
|
+
diagnostics: tuple[LaneIRDiagnostic, ...] = ()
|
|
1318
|
+
execution_policy: ExecutionPolicyDocument | None = None
|
|
1319
|
+
dispatch_hints: dict[str, DispatchHints] = field(default_factory=dict)
|
|
1320
|
+
merge_policy: Any | None = None
|
|
1321
|
+
|
|
1322
|
+
@property
|
|
1323
|
+
def valid(self) -> bool:
|
|
1324
|
+
return not self.diagnostics
|
|
1325
|
+
|
|
1326
|
+
def to_json(self) -> dict[str, Any]:
|
|
1327
|
+
return clean_dict(
|
|
1328
|
+
{
|
|
1329
|
+
"plan_path": self.plan_path,
|
|
1330
|
+
"metadata": self.metadata,
|
|
1331
|
+
"lanes": tuple(lane.to_json() for lane in self.lanes),
|
|
1332
|
+
"dependencies": tuple(dependency.to_json() for dependency in self.dependencies),
|
|
1333
|
+
"diagnostics": tuple(diagnostic.to_json() for diagnostic in self.diagnostics),
|
|
1334
|
+
"execution_policy": self.execution_policy.to_json() if self.execution_policy else None,
|
|
1335
|
+
"dispatch_hints": {
|
|
1336
|
+
key: value.to_json() for key, value in self.dispatch_hints.items()
|
|
1337
|
+
},
|
|
1338
|
+
"merge_policy": self.merge_policy.to_json() if hasattr(self.merge_policy, "to_json") else self.merge_policy,
|
|
1339
|
+
}
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
@dataclass(frozen=True)
|
|
1344
|
+
class LaneWorktreeAssignment:
|
|
1345
|
+
lane_id: str
|
|
1346
|
+
worktree_path: str
|
|
1347
|
+
isolation_mode: str = "main_worktree"
|
|
1348
|
+
branch: str | None = None
|
|
1349
|
+
base_sha: str | None = None
|
|
1350
|
+
|
|
1351
|
+
def __post_init__(self) -> None:
|
|
1352
|
+
if not self.lane_id.strip():
|
|
1353
|
+
raise ValueError("lane worktree assignment lane_id must not be empty")
|
|
1354
|
+
if not self.worktree_path.strip():
|
|
1355
|
+
raise ValueError("lane worktree assignment worktree_path must not be empty")
|
|
1356
|
+
require_literal(self.isolation_mode, WORKTREE_ISOLATION_MODES, "worktree isolation mode")
|
|
1357
|
+
|
|
1358
|
+
def to_json(self) -> dict[str, Any]:
|
|
1359
|
+
return clean_dict(asdict(self))
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
@dataclass(frozen=True)
|
|
1363
|
+
class LaneWave:
|
|
1364
|
+
wave_id: str
|
|
1365
|
+
lane_ids: tuple[str, ...]
|
|
1366
|
+
mode: str = "serialized"
|
|
1367
|
+
assignments: tuple[LaneWorktreeAssignment, ...] = ()
|
|
1368
|
+
|
|
1369
|
+
def __post_init__(self) -> None:
|
|
1370
|
+
if not self.wave_id.strip():
|
|
1371
|
+
raise ValueError("lane wave id must not be empty")
|
|
1372
|
+
require_literal(self.mode, LANE_SCHEDULER_MODES, "lane scheduler mode")
|
|
1373
|
+
if self.mode == "off":
|
|
1374
|
+
raise ValueError("lane wave mode cannot be off")
|
|
1375
|
+
if not self.lane_ids:
|
|
1376
|
+
raise ValueError("lane wave must include at least one lane")
|
|
1377
|
+
|
|
1378
|
+
def to_json(self) -> dict[str, Any]:
|
|
1379
|
+
return clean_dict(
|
|
1380
|
+
{
|
|
1381
|
+
"wave_id": self.wave_id,
|
|
1382
|
+
"lane_ids": self.lane_ids,
|
|
1383
|
+
"mode": self.mode,
|
|
1384
|
+
"assignments": tuple(assignment.to_json() for assignment in self.assignments),
|
|
1385
|
+
}
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
@dataclass(frozen=True)
|
|
1390
|
+
class LaneWaveDecision:
|
|
1391
|
+
status: str
|
|
1392
|
+
mode: str
|
|
1393
|
+
ready_wave: LaneWave | None = None
|
|
1394
|
+
pending_lane_ids: tuple[str, ...] = ()
|
|
1395
|
+
completed_lane_ids: tuple[str, ...] = ()
|
|
1396
|
+
blocked_lane_ids: tuple[str, ...] = ()
|
|
1397
|
+
diagnostics: tuple[LaneIRDiagnostic, ...] = ()
|
|
1398
|
+
|
|
1399
|
+
def __post_init__(self) -> None:
|
|
1400
|
+
require_literal(self.status, LANE_WAVE_STATUSES, "lane wave status")
|
|
1401
|
+
require_literal(self.mode, LANE_SCHEDULER_MODES, "lane scheduler mode")
|
|
1402
|
+
if self.status == "ready" and self.ready_wave is None:
|
|
1403
|
+
raise ValueError("ready lane wave decision requires ready_wave")
|
|
1404
|
+
|
|
1405
|
+
def to_json(self) -> dict[str, Any]:
|
|
1406
|
+
return clean_dict(
|
|
1407
|
+
{
|
|
1408
|
+
"status": self.status,
|
|
1409
|
+
"mode": self.mode,
|
|
1410
|
+
"ready_wave": self.ready_wave.to_json() if self.ready_wave else None,
|
|
1411
|
+
"pending_lane_ids": self.pending_lane_ids,
|
|
1412
|
+
"completed_lane_ids": self.completed_lane_ids,
|
|
1413
|
+
"blocked_lane_ids": self.blocked_lane_ids,
|
|
1414
|
+
"diagnostics": tuple(diagnostic.to_json() for diagnostic in self.diagnostics),
|
|
1415
|
+
}
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
HARNESS_CLOSEOUT_REQUIRED_FIELDS = (
|
|
1420
|
+
"automation.status",
|
|
1421
|
+
"automation.next_skill",
|
|
1422
|
+
"automation.next_command",
|
|
1423
|
+
"automation.human_required",
|
|
1424
|
+
"automation.blocker_class",
|
|
1425
|
+
"automation.verification_status",
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
@dataclass(frozen=True)
|
|
1430
|
+
class HarnessLaneAssignment:
|
|
1431
|
+
phase: str
|
|
1432
|
+
lane_id: str
|
|
1433
|
+
work_unit_kind: str
|
|
1434
|
+
prompt_kind: str = "implementation"
|
|
1435
|
+
wave_id: str | None = None
|
|
1436
|
+
owned_files: tuple[str, ...] = ()
|
|
1437
|
+
read_only_refs: tuple[str, ...] = ()
|
|
1438
|
+
consumed_interfaces: tuple[str, ...] = ()
|
|
1439
|
+
depends_on: tuple[str, ...] = ()
|
|
1440
|
+
execution_policy: dict[str, Any] = field(default_factory=dict)
|
|
1441
|
+
worktree_assignment: LaneWorktreeAssignment | None = None
|
|
1442
|
+
harness_route: str | None = None
|
|
1443
|
+
model: str | None = None
|
|
1444
|
+
effort: str | None = None
|
|
1445
|
+
fallback_reason: str | None = None
|
|
1446
|
+
closeout_schema_required: tuple[str, ...] = HARNESS_CLOSEOUT_REQUIRED_FIELDS
|
|
1447
|
+
reducer_kind: str = "none"
|
|
1448
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
1449
|
+
|
|
1450
|
+
def __post_init__(self) -> None:
|
|
1451
|
+
if not self.phase.strip():
|
|
1452
|
+
raise ValueError("harness lane assignment phase must not be empty")
|
|
1453
|
+
if not self.lane_id.strip():
|
|
1454
|
+
raise ValueError("harness lane assignment lane_id must not be empty")
|
|
1455
|
+
require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "harness work-unit kind")
|
|
1456
|
+
require_literal(self.prompt_kind, HARNESS_WORK_UNIT_PROMPT_KINDS, "harness prompt kind")
|
|
1457
|
+
require_literal(self.reducer_kind, LANE_REDUCER_KINDS, "lane reducer kind")
|
|
1458
|
+
if self.work_unit_kind in {"lane_execute", "lane_review"} and not self.owned_files:
|
|
1459
|
+
raise ValueError("harness lane assignment requires owned_files for lane work")
|
|
1460
|
+
|
|
1461
|
+
@classmethod
|
|
1462
|
+
def from_lane(
|
|
1463
|
+
cls,
|
|
1464
|
+
*,
|
|
1465
|
+
phase: str,
|
|
1466
|
+
lane: PhasePlanLane,
|
|
1467
|
+
work_unit_kind: str = "lane_execute",
|
|
1468
|
+
prompt_kind: str = "implementation",
|
|
1469
|
+
worktree_assignment: LaneWorktreeAssignment | None = None,
|
|
1470
|
+
metadata: dict[str, Any] | None = None,
|
|
1471
|
+
) -> "HarnessLaneAssignment":
|
|
1472
|
+
return cls(
|
|
1473
|
+
phase=phase,
|
|
1474
|
+
lane_id=lane.lane_id,
|
|
1475
|
+
work_unit_kind=work_unit_kind,
|
|
1476
|
+
prompt_kind=prompt_kind,
|
|
1477
|
+
owned_files=lane.owned_files,
|
|
1478
|
+
read_only_refs=lane.interfaces_consumed,
|
|
1479
|
+
consumed_interfaces=lane.interfaces_consumed,
|
|
1480
|
+
depends_on=lane.depends_on,
|
|
1481
|
+
execution_policy=lane.execution_policy.to_json() if lane.execution_policy else {},
|
|
1482
|
+
worktree_assignment=worktree_assignment,
|
|
1483
|
+
reducer_kind=lane.reducer_kind,
|
|
1484
|
+
metadata=dict(metadata or {}),
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
@classmethod
|
|
1488
|
+
def from_json(cls, data: dict[str, Any]) -> "HarnessLaneAssignment":
|
|
1489
|
+
assignment_data = data.get("worktree_assignment")
|
|
1490
|
+
return cls(
|
|
1491
|
+
phase=str(data.get("phase") or ""),
|
|
1492
|
+
lane_id=str(data.get("lane_id") or ""),
|
|
1493
|
+
work_unit_kind=str(data.get("work_unit_kind") or ""),
|
|
1494
|
+
prompt_kind=str(data.get("prompt_kind") or "implementation"),
|
|
1495
|
+
wave_id=data.get("wave_id"),
|
|
1496
|
+
owned_files=tuple(str(item) for item in data.get("owned_files") or ()),
|
|
1497
|
+
read_only_refs=tuple(str(item) for item in data.get("read_only_refs") or ()),
|
|
1498
|
+
consumed_interfaces=tuple(str(item) for item in data.get("consumed_interfaces") or ()),
|
|
1499
|
+
depends_on=tuple(str(item) for item in data.get("depends_on") or ()),
|
|
1500
|
+
execution_policy=dict(data.get("execution_policy") or {}),
|
|
1501
|
+
worktree_assignment=(
|
|
1502
|
+
LaneWorktreeAssignment(
|
|
1503
|
+
lane_id=str(assignment_data.get("lane_id") or ""),
|
|
1504
|
+
worktree_path=str(assignment_data.get("worktree_path") or ""),
|
|
1505
|
+
isolation_mode=str(assignment_data.get("isolation_mode") or "main_worktree"),
|
|
1506
|
+
branch=assignment_data.get("branch"),
|
|
1507
|
+
base_sha=assignment_data.get("base_sha"),
|
|
1508
|
+
)
|
|
1509
|
+
if isinstance(assignment_data, dict)
|
|
1510
|
+
else None
|
|
1511
|
+
),
|
|
1512
|
+
harness_route=data.get("harness_route"),
|
|
1513
|
+
model=data.get("model"),
|
|
1514
|
+
effort=data.get("effort"),
|
|
1515
|
+
fallback_reason=data.get("fallback_reason"),
|
|
1516
|
+
closeout_schema_required=tuple(str(item) for item in data.get("closeout_schema_required") or HARNESS_CLOSEOUT_REQUIRED_FIELDS),
|
|
1517
|
+
reducer_kind=str(data.get("reducer_kind") or "none"),
|
|
1518
|
+
metadata=dict(data.get("metadata") or {}),
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
def to_json(self) -> dict[str, Any]:
|
|
1522
|
+
return clean_dict(
|
|
1523
|
+
{
|
|
1524
|
+
"phase": self.phase,
|
|
1525
|
+
"lane_id": self.lane_id,
|
|
1526
|
+
"work_unit_kind": self.work_unit_kind,
|
|
1527
|
+
"prompt_kind": self.prompt_kind,
|
|
1528
|
+
"wave_id": self.wave_id,
|
|
1529
|
+
"owned_files": self.owned_files,
|
|
1530
|
+
"read_only_refs": self.read_only_refs,
|
|
1531
|
+
"consumed_interfaces": self.consumed_interfaces,
|
|
1532
|
+
"depends_on": self.depends_on,
|
|
1533
|
+
"execution_policy": self.execution_policy,
|
|
1534
|
+
"worktree_assignment": self.worktree_assignment.to_json() if self.worktree_assignment else None,
|
|
1535
|
+
"harness_route": self.harness_route,
|
|
1536
|
+
"model": self.model,
|
|
1537
|
+
"effort": self.effort,
|
|
1538
|
+
"fallback_reason": self.fallback_reason,
|
|
1539
|
+
"closeout_schema_required": self.closeout_schema_required,
|
|
1540
|
+
"reducer_kind": self.reducer_kind,
|
|
1541
|
+
"metadata": self.metadata,
|
|
1542
|
+
}
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
@dataclass(frozen=True)
|
|
1547
|
+
class DirtyPathClassification:
|
|
1548
|
+
path: str
|
|
1549
|
+
classification: str
|
|
1550
|
+
lane_id: str | None = None
|
|
1551
|
+
|
|
1552
|
+
def __post_init__(self) -> None:
|
|
1553
|
+
if not self.path.strip():
|
|
1554
|
+
raise ValueError("dirty path classification path must not be empty")
|
|
1555
|
+
require_literal(self.classification, DIRTY_PATH_CLASSIFICATIONS, "dirty path classification")
|
|
1556
|
+
|
|
1557
|
+
def to_json(self) -> dict[str, Any]:
|
|
1558
|
+
return clean_dict(asdict(self))
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
@dataclass(frozen=True)
|
|
1562
|
+
class ResolvedExecutionPolicy:
|
|
1563
|
+
action: str
|
|
1564
|
+
lane: str | None
|
|
1565
|
+
executor: str
|
|
1566
|
+
model: str
|
|
1567
|
+
effort: str
|
|
1568
|
+
work_unit_kind: str
|
|
1569
|
+
fallback: str | None = None
|
|
1570
|
+
unsupported_policy_behavior: str = "block"
|
|
1571
|
+
execution_policy_source: str = "registry defaults"
|
|
1572
|
+
execution_policy_override_reason: str | None = None
|
|
1573
|
+
executor_source: str = "registry defaults"
|
|
1574
|
+
model_source: str = "registry defaults"
|
|
1575
|
+
effort_source: str = "registry defaults"
|
|
1576
|
+
fallback_source: str = "registry defaults"
|
|
1577
|
+
fallback_applied: bool = False
|
|
1578
|
+
# model-routing-v1 P4: the role the winning policy requested, for route logs.
|
|
1579
|
+
model_class: str | None = None
|
|
1580
|
+
|
|
1581
|
+
def __post_init__(self) -> None:
|
|
1582
|
+
require_literal(self.action, PRODUCT_LOOP_ACTIONS, "execution policy action")
|
|
1583
|
+
require_literal(self.executor, EXECUTORS, "execution policy executor")
|
|
1584
|
+
require_literal(self.effort, NORMALIZED_EFFORT_LEVELS, "execution policy effort")
|
|
1585
|
+
require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "execution policy work-unit kind")
|
|
1586
|
+
require_literal(self.unsupported_policy_behavior, UNSUPPORTED_POLICY_BEHAVIORS, "unsupported policy behavior")
|
|
1587
|
+
|
|
1588
|
+
def to_json(self) -> dict[str, Any]:
|
|
1589
|
+
return clean_dict(asdict(self))
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
@dataclass(frozen=True)
|
|
1593
|
+
class WorkUnitMetric:
|
|
1594
|
+
metric_id: str
|
|
1595
|
+
schema_version: str
|
|
1596
|
+
timestamp: str
|
|
1597
|
+
work_unit_id: str | None
|
|
1598
|
+
work_unit_kind: str
|
|
1599
|
+
phase: str
|
|
1600
|
+
action: str
|
|
1601
|
+
executor: str
|
|
1602
|
+
provider: str
|
|
1603
|
+
model: str
|
|
1604
|
+
effort: str | None = None
|
|
1605
|
+
thinking_level: str | None = None
|
|
1606
|
+
lane_id: str | None = None
|
|
1607
|
+
wave_id: str | None = None
|
|
1608
|
+
policy_source: str | None = None
|
|
1609
|
+
policy_override_reason: str | None = None
|
|
1610
|
+
profile_source: str | None = None
|
|
1611
|
+
fallback_applied: bool = False
|
|
1612
|
+
fallback: str | None = None
|
|
1613
|
+
fallback_reason: str | None = None
|
|
1614
|
+
duration_seconds: float | None = None
|
|
1615
|
+
returncode: int | None = None
|
|
1616
|
+
terminal_status: str | None = None
|
|
1617
|
+
verification_status: str | None = None
|
|
1618
|
+
blocker_class: str | None = None
|
|
1619
|
+
artifact_paths: dict[str, str] = field(default_factory=dict)
|
|
1620
|
+
|
|
1621
|
+
def __post_init__(self) -> None:
|
|
1622
|
+
if self.schema_version != WORK_UNIT_METRIC_SCHEMA_VERSION:
|
|
1623
|
+
raise ValueError(f"invalid work-unit metric schema_version: {self.schema_version}")
|
|
1624
|
+
require_literal(self.work_unit_kind, WORK_UNIT_KINDS, "work-unit kind")
|
|
1625
|
+
require_literal(self.action, PRODUCT_LOOP_ACTIONS, "metric action")
|
|
1626
|
+
require_literal(self.executor, EXECUTORS, "metric executor")
|
|
1627
|
+
if self.effort is not None:
|
|
1628
|
+
require_literal(self.effort, NORMALIZED_EFFORT_LEVELS, "metric effort")
|
|
1629
|
+
if self.blocker_class is not None:
|
|
1630
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "metric blocker class")
|
|
1631
|
+
|
|
1632
|
+
def to_json(self) -> dict[str, Any]:
|
|
1633
|
+
return clean_dict(asdict(self))
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
@dataclass(frozen=True)
|
|
1637
|
+
class ExecutorCapabilityRecord:
|
|
1638
|
+
executor: str
|
|
1639
|
+
supported_actions: tuple[str, ...]
|
|
1640
|
+
capabilities: tuple[str, ...]
|
|
1641
|
+
strengths: tuple[str, ...] = ()
|
|
1642
|
+
limits: tuple[str, ...] = ()
|
|
1643
|
+
injection_mode: str = "manual"
|
|
1644
|
+
permission_posture: str = "unknown"
|
|
1645
|
+
subagent_posture: str = "unknown"
|
|
1646
|
+
live_available: bool = False
|
|
1647
|
+
dry_run_available: bool = True
|
|
1648
|
+
live_proof_gate: str = "disposable_proof_required"
|
|
1649
|
+
promotion_status: str = "proof_gated"
|
|
1650
|
+
promotion_requirements: tuple[str, ...] = ()
|
|
1651
|
+
auth_preflight_mode: str = "none"
|
|
1652
|
+
auth_preflight_probes: tuple[str, ...] = ()
|
|
1653
|
+
timeout_posture: str = "runner_managed"
|
|
1654
|
+
output_capture_format: str = "combined_output"
|
|
1655
|
+
terminal_summary_artifact: str = "terminal-summary.json"
|
|
1656
|
+
default_model_profiles: dict[str, str] = field(default_factory=dict)
|
|
1657
|
+
known_failure_cases: tuple[str, ...] = ()
|
|
1658
|
+
default_claude_execution_mode: str | None = None
|
|
1659
|
+
claude_execution_policies: tuple["ClaudeTeamPolicy", ...] = ()
|
|
1660
|
+
|
|
1661
|
+
def __post_init__(self) -> None:
|
|
1662
|
+
require_literal(self.executor, EXECUTORS, "executor")
|
|
1663
|
+
require_literal(self.injection_mode, INJECTION_MODES, "injection mode")
|
|
1664
|
+
require_literal(self.permission_posture, PERMISSION_POSTURES, "permission posture")
|
|
1665
|
+
require_literal(self.subagent_posture, SUBAGENT_POSTURES, "subagent posture")
|
|
1666
|
+
require_literal(self.live_proof_gate, LIVE_PROOF_GATES, "live proof gate")
|
|
1667
|
+
require_literal(self.promotion_status, PROMOTION_STATUSES, "promotion status")
|
|
1668
|
+
require_literal(self.auth_preflight_mode, AUTH_PREFLIGHT_MODES, "auth preflight mode")
|
|
1669
|
+
require_literal(self.timeout_posture, TIMEOUT_POSTURES, "timeout posture")
|
|
1670
|
+
require_literal(self.output_capture_format, OUTPUT_CAPTURE_FORMATS, "output capture format")
|
|
1671
|
+
if self.default_claude_execution_mode is not None:
|
|
1672
|
+
require_literal(self.default_claude_execution_mode, CLAUDE_EXECUTION_MODES, "Claude execution mode")
|
|
1673
|
+
for action in self.supported_actions:
|
|
1674
|
+
require_literal(action, PRODUCT_LOOP_ACTIONS, "supported action")
|
|
1675
|
+
for capability in self.capabilities:
|
|
1676
|
+
require_literal(capability, DISPATCH_CAPABILITIES, "capability")
|
|
1677
|
+
for action, profile in self.default_model_profiles.items():
|
|
1678
|
+
require_literal(action, PRODUCT_LOOP_ACTIONS, "default model profile action")
|
|
1679
|
+
require_literal(profile, MODEL_PROFILES, "default model profile")
|
|
1680
|
+
|
|
1681
|
+
def to_json(self) -> dict[str, Any]:
|
|
1682
|
+
return clean_dict(asdict(self))
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
@dataclass(frozen=True)
|
|
1686
|
+
class ClaudeTeamPolicy:
|
|
1687
|
+
execution_mode: str
|
|
1688
|
+
maturity_label: str
|
|
1689
|
+
live_proof_gate: str
|
|
1690
|
+
promotion_status: str
|
|
1691
|
+
launch_default: bool = False
|
|
1692
|
+
max_teammates: int = 0
|
|
1693
|
+
max_native_tasks: int = 0
|
|
1694
|
+
max_delegation_depth: int = 0
|
|
1695
|
+
max_fanout: int = 0
|
|
1696
|
+
default_model: str | None = None
|
|
1697
|
+
default_effort: str | None = None
|
|
1698
|
+
budget_guidance: dict[str, Any] = field(default_factory=dict)
|
|
1699
|
+
allowed_actions: tuple[str, ...] = ()
|
|
1700
|
+
disallowed_actions: tuple[str, ...] = ()
|
|
1701
|
+
allowed_tools: tuple[str, ...] = ()
|
|
1702
|
+
disallowed_tools: tuple[str, ...] = ()
|
|
1703
|
+
worktree_posture: str = "phase_loop_managed"
|
|
1704
|
+
requires_disjoint_owned_files: bool = False
|
|
1705
|
+
allows_read_only_lanes: bool = True
|
|
1706
|
+
direct_teammate_messaging_allowed: bool = False
|
|
1707
|
+
task_lifecycle_supported: bool = False
|
|
1708
|
+
|
|
1709
|
+
def __post_init__(self) -> None:
|
|
1710
|
+
require_literal(self.execution_mode, CLAUDE_EXECUTION_MODES, "Claude execution mode")
|
|
1711
|
+
require_literal(self.maturity_label, OPERATOR_MATURITY_LABELS, "operator maturity label")
|
|
1712
|
+
require_literal(self.live_proof_gate, LIVE_PROOF_GATES, "live proof gate")
|
|
1713
|
+
require_literal(self.promotion_status, PROMOTION_STATUSES, "promotion status")
|
|
1714
|
+
require_literal(self.worktree_posture, CLAUDE_WORKTREE_POSTURES, "Claude worktree posture")
|
|
1715
|
+
for action in self.allowed_actions:
|
|
1716
|
+
require_literal(action, PRODUCT_LOOP_ACTIONS, "allowed action")
|
|
1717
|
+
for action in self.disallowed_actions:
|
|
1718
|
+
require_literal(action, PRODUCT_LOOP_ACTIONS, "disallowed action")
|
|
1719
|
+
|
|
1720
|
+
def to_json(self) -> dict[str, Any]:
|
|
1721
|
+
return clean_dict(asdict(self))
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
@dataclass(frozen=True)
|
|
1725
|
+
class PhaseTeamEligibility:
|
|
1726
|
+
allowed_execution_modes: tuple[str, ...]
|
|
1727
|
+
default_execution_mode: str
|
|
1728
|
+
eligible_for_native_team: bool
|
|
1729
|
+
has_disjoint_write_lanes: bool
|
|
1730
|
+
has_only_read_only_lanes: bool
|
|
1731
|
+
unmanaged_write_risk: bool
|
|
1732
|
+
reason: str
|
|
1733
|
+
invalid_reasons: tuple[str, ...] = ()
|
|
1734
|
+
lane_summaries: tuple[dict[str, Any], ...] = ()
|
|
1735
|
+
|
|
1736
|
+
def __post_init__(self) -> None:
|
|
1737
|
+
require_literal(self.default_execution_mode, CLAUDE_EXECUTION_MODES, "default Claude execution mode")
|
|
1738
|
+
for mode in self.allowed_execution_modes:
|
|
1739
|
+
require_literal(mode, CLAUDE_EXECUTION_MODES, "allowed Claude execution mode")
|
|
1740
|
+
|
|
1741
|
+
def to_json(self) -> dict[str, Any]:
|
|
1742
|
+
return clean_dict(asdict(self))
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
@dataclass(frozen=True)
|
|
1746
|
+
class DispatchDecision:
|
|
1747
|
+
action: str
|
|
1748
|
+
selected_executor: str | None
|
|
1749
|
+
source: str
|
|
1750
|
+
preferred_executors: tuple[str, ...] = ()
|
|
1751
|
+
allowed_executors: tuple[str, ...] = ()
|
|
1752
|
+
fallback_executors: tuple[str, ...] = ()
|
|
1753
|
+
disabled_executors: tuple[str, ...] = ()
|
|
1754
|
+
required_capabilities: tuple[str, ...] = ()
|
|
1755
|
+
considered_executors: tuple[str, ...] = ()
|
|
1756
|
+
blocked_reason: str | None = None
|
|
1757
|
+
blocked_summary: str | None = None
|
|
1758
|
+
fallback_applied: bool = False
|
|
1759
|
+
selected_via: str | None = None
|
|
1760
|
+
|
|
1761
|
+
def __post_init__(self) -> None:
|
|
1762
|
+
require_literal(self.action, PRODUCT_LOOP_ACTIONS, "product action")
|
|
1763
|
+
if self.selected_executor is not None:
|
|
1764
|
+
require_literal(self.selected_executor, EXECUTORS, "selected executor")
|
|
1765
|
+
if self.selected_via is not None:
|
|
1766
|
+
require_literal(self.selected_via, DISPATCH_SELECTION_PATHS, "dispatch selection path")
|
|
1767
|
+
for field_name in (
|
|
1768
|
+
"preferred_executors",
|
|
1769
|
+
"allowed_executors",
|
|
1770
|
+
"fallback_executors",
|
|
1771
|
+
"disabled_executors",
|
|
1772
|
+
"considered_executors",
|
|
1773
|
+
):
|
|
1774
|
+
values = getattr(self, field_name)
|
|
1775
|
+
for value in values:
|
|
1776
|
+
require_literal(value, EXECUTORS, field_name)
|
|
1777
|
+
for capability in self.required_capabilities:
|
|
1778
|
+
require_literal(capability, DISPATCH_CAPABILITIES, "required capability")
|
|
1779
|
+
|
|
1780
|
+
@property
|
|
1781
|
+
def blocked(self) -> bool:
|
|
1782
|
+
return self.selected_executor is None
|
|
1783
|
+
|
|
1784
|
+
def to_json(self) -> dict[str, Any]:
|
|
1785
|
+
return clean_dict(asdict(self))
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
@dataclass(frozen=True)
|
|
1789
|
+
class PermissionPolicy:
|
|
1790
|
+
sandbox_mode: str
|
|
1791
|
+
bypass_approvals: bool = False
|
|
1792
|
+
|
|
1793
|
+
def to_json(self) -> dict[str, Any]:
|
|
1794
|
+
return clean_dict(asdict(self))
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
@dataclass(frozen=True)
|
|
1798
|
+
class CommandAdapterConfig:
|
|
1799
|
+
name: str
|
|
1800
|
+
template: str
|
|
1801
|
+
delivery_mode: str = "context_file"
|
|
1802
|
+
supported_actions: tuple[str, ...] = COMMAND_ADAPTER_SUPPORTED_ACTIONS
|
|
1803
|
+
|
|
1804
|
+
def __post_init__(self) -> None:
|
|
1805
|
+
require_literal(self.delivery_mode, INJECTION_MODES, "command adapter delivery mode")
|
|
1806
|
+
for action in self.supported_actions:
|
|
1807
|
+
require_literal(action, PRODUCT_LOOP_ACTIONS, "command adapter supported action")
|
|
1808
|
+
|
|
1809
|
+
def to_json(self) -> dict[str, Any]:
|
|
1810
|
+
return clean_dict(asdict(self))
|
|
1811
|
+
|
|
1812
|
+
|
|
1813
|
+
@dataclass(frozen=True)
|
|
1814
|
+
class LaunchRequest:
|
|
1815
|
+
executor: str
|
|
1816
|
+
action: str
|
|
1817
|
+
repo: Path
|
|
1818
|
+
roadmap: Path
|
|
1819
|
+
phase: str | None
|
|
1820
|
+
plan: Path | None
|
|
1821
|
+
model_selection: ModelSelection
|
|
1822
|
+
prompt_bundle: PromptBundle
|
|
1823
|
+
injection_metadata: InjectionMetadata
|
|
1824
|
+
permission_policy: PermissionPolicy
|
|
1825
|
+
command_adapter: CommandAdapterConfig | None = None
|
|
1826
|
+
dispatch_decision: DispatchDecision | None = None
|
|
1827
|
+
harness_lane_assignment: HarnessLaneAssignment | None = None
|
|
1828
|
+
delegation_request: "DelegationRequest | None" = None
|
|
1829
|
+
parent_child_metadata: "ParentChildRunMetadata | None" = None
|
|
1830
|
+
claude_execution_mode: str | None = None
|
|
1831
|
+
claude_team_policy: ClaudeTeamPolicy | None = None
|
|
1832
|
+
phase_team_eligibility: PhaseTeamEligibility | None = None
|
|
1833
|
+
json_output: bool = False
|
|
1834
|
+
bypass_approvals: bool = False
|
|
1835
|
+
launch_timeout_seconds: int | None = None
|
|
1836
|
+
|
|
1837
|
+
def __post_init__(self) -> None:
|
|
1838
|
+
require_literal(self.executor, EXECUTORS, "executor")
|
|
1839
|
+
if self.claude_execution_mode is not None:
|
|
1840
|
+
require_literal(self.claude_execution_mode, CLAUDE_EXECUTION_MODES, "Claude execution mode")
|
|
1841
|
+
|
|
1842
|
+
def to_json(self) -> dict[str, Any]:
|
|
1843
|
+
return {
|
|
1844
|
+
"executor": self.executor,
|
|
1845
|
+
"action": self.action,
|
|
1846
|
+
"repo": str(self.repo),
|
|
1847
|
+
"roadmap": str(self.roadmap),
|
|
1848
|
+
"phase": self.phase,
|
|
1849
|
+
"plan": str(self.plan) if self.plan is not None else None,
|
|
1850
|
+
"model_selection": self.model_selection.to_json(),
|
|
1851
|
+
"prompt_bundle": self.prompt_bundle.to_json(),
|
|
1852
|
+
"injection_metadata": self.injection_metadata.to_json(),
|
|
1853
|
+
"permission_policy": self.permission_policy.to_json(),
|
|
1854
|
+
"command_adapter": self.command_adapter.to_json() if self.command_adapter else None,
|
|
1855
|
+
"dispatch_decision": self.dispatch_decision.to_json() if self.dispatch_decision else None,
|
|
1856
|
+
"harness_lane_assignment": self.harness_lane_assignment.to_json() if self.harness_lane_assignment else None,
|
|
1857
|
+
"delegation_request": self.delegation_request.to_json() if self.delegation_request else None,
|
|
1858
|
+
"parent_child_metadata": self.parent_child_metadata.to_json() if self.parent_child_metadata else None,
|
|
1859
|
+
"claude_execution_mode": self.claude_execution_mode,
|
|
1860
|
+
"claude_team_policy": self.claude_team_policy.to_json() if self.claude_team_policy else None,
|
|
1861
|
+
"phase_team_eligibility": self.phase_team_eligibility.to_json() if self.phase_team_eligibility else None,
|
|
1862
|
+
"json_output": self.json_output,
|
|
1863
|
+
"bypass_approvals": self.bypass_approvals,
|
|
1864
|
+
"launch_timeout_seconds": self.launch_timeout_seconds,
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
@dataclass(frozen=True)
|
|
1869
|
+
class DelegationBudget:
|
|
1870
|
+
max_tokens: int | None = None
|
|
1871
|
+
max_seconds: int | None = None
|
|
1872
|
+
max_cost_usd: float | None = None
|
|
1873
|
+
notes: str | None = None
|
|
1874
|
+
|
|
1875
|
+
def to_json(self) -> dict[str, Any]:
|
|
1876
|
+
return clean_dict(asdict(self))
|
|
1877
|
+
|
|
1878
|
+
def is_defined(self) -> bool:
|
|
1879
|
+
return any(value is not None for value in (self.max_tokens, self.max_seconds, self.max_cost_usd)) or bool(self.notes)
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
@dataclass(frozen=True)
|
|
1883
|
+
class DelegationRequest:
|
|
1884
|
+
request_id: str
|
|
1885
|
+
product_action: str
|
|
1886
|
+
target_executor: str
|
|
1887
|
+
reason: str
|
|
1888
|
+
owned_files: tuple[str, ...]
|
|
1889
|
+
expected_output: str
|
|
1890
|
+
priority: str = "normal"
|
|
1891
|
+
review_context: str | None = None
|
|
1892
|
+
repair_context: str | None = None
|
|
1893
|
+
budget: DelegationBudget | None = None
|
|
1894
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
1895
|
+
|
|
1896
|
+
def __post_init__(self) -> None:
|
|
1897
|
+
require_literal(self.product_action, PRODUCT_LOOP_ACTIONS, "product action")
|
|
1898
|
+
require_literal(self.target_executor, EXECUTORS, "target executor")
|
|
1899
|
+
require_literal(self.priority, DELEGATION_PRIORITIES, "delegation priority")
|
|
1900
|
+
|
|
1901
|
+
def to_json(self) -> dict[str, Any]:
|
|
1902
|
+
return clean_dict(
|
|
1903
|
+
{
|
|
1904
|
+
"request_id": self.request_id,
|
|
1905
|
+
"product_action": self.product_action,
|
|
1906
|
+
"target_executor": self.target_executor,
|
|
1907
|
+
"reason": self.reason,
|
|
1908
|
+
"owned_files": self.owned_files,
|
|
1909
|
+
"expected_output": self.expected_output,
|
|
1910
|
+
"priority": self.priority,
|
|
1911
|
+
"review_context": self.review_context,
|
|
1912
|
+
"repair_context": self.repair_context,
|
|
1913
|
+
"budget": self.budget.to_json() if self.budget else None,
|
|
1914
|
+
"metadata": self.metadata,
|
|
1915
|
+
}
|
|
1916
|
+
)
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
@dataclass(frozen=True)
|
|
1920
|
+
class DelegationDecision:
|
|
1921
|
+
request_id: str
|
|
1922
|
+
status: str
|
|
1923
|
+
reason_code: str
|
|
1924
|
+
summary: str
|
|
1925
|
+
selected_executor: str | None = None
|
|
1926
|
+
dispatch_decision: dict[str, Any] | None = None
|
|
1927
|
+
human_required: bool = False
|
|
1928
|
+
blocker_class: str | None = None
|
|
1929
|
+
observed_depth: int | None = None
|
|
1930
|
+
observed_fanout: int | None = None
|
|
1931
|
+
validation_order: tuple[str, ...] = (
|
|
1932
|
+
"active_loop_mode",
|
|
1933
|
+
"ownership_boundaries",
|
|
1934
|
+
"depth_limit",
|
|
1935
|
+
"fanout_limit",
|
|
1936
|
+
"budget_metadata",
|
|
1937
|
+
"dispatch_policy",
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
def __post_init__(self) -> None:
|
|
1941
|
+
require_literal(self.status, DELEGATION_STATUSES, "delegation status")
|
|
1942
|
+
if self.selected_executor is not None:
|
|
1943
|
+
require_literal(self.selected_executor, EXECUTORS, "selected executor")
|
|
1944
|
+
if self.blocker_class is not None:
|
|
1945
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
|
|
1946
|
+
|
|
1947
|
+
@property
|
|
1948
|
+
def approved(self) -> bool:
|
|
1949
|
+
return self.status == "approved"
|
|
1950
|
+
|
|
1951
|
+
def to_json(self) -> dict[str, Any]:
|
|
1952
|
+
return clean_dict(asdict(self))
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
@dataclass(frozen=True)
|
|
1956
|
+
class ParentChildRunMetadata:
|
|
1957
|
+
parent_phase: str
|
|
1958
|
+
parent_action: str
|
|
1959
|
+
parent_executor: str | None = None
|
|
1960
|
+
parent_run_id: str | None = None
|
|
1961
|
+
child_phase: str | None = None
|
|
1962
|
+
child_action: str | None = None
|
|
1963
|
+
child_run_id: str | None = None
|
|
1964
|
+
request_id: str | None = None
|
|
1965
|
+
child_executor: str | None = None
|
|
1966
|
+
observed_launch_path: str | None = None
|
|
1967
|
+
child_artifact_root: str | None = None
|
|
1968
|
+
child_worktree_root: str | None = None
|
|
1969
|
+
child_closeout_result: dict[str, Any] | None = None
|
|
1970
|
+
|
|
1971
|
+
def __post_init__(self) -> None:
|
|
1972
|
+
require_literal(self.parent_action, PRODUCT_LOOP_ACTIONS, "parent action")
|
|
1973
|
+
if self.parent_executor is not None:
|
|
1974
|
+
require_literal(self.parent_executor, EXECUTORS, "parent executor")
|
|
1975
|
+
if self.child_action is not None:
|
|
1976
|
+
require_literal(self.child_action, PRODUCT_LOOP_ACTIONS, "child action")
|
|
1977
|
+
if self.child_executor is not None:
|
|
1978
|
+
require_literal(self.child_executor, EXECUTORS, "child executor")
|
|
1979
|
+
|
|
1980
|
+
def to_json(self) -> dict[str, Any]:
|
|
1981
|
+
return clean_dict(asdict(self))
|
|
1982
|
+
|
|
1983
|
+
|
|
1984
|
+
@dataclass(frozen=True)
|
|
1985
|
+
class Blocker:
|
|
1986
|
+
human_required: bool = False
|
|
1987
|
+
blocker_class: str | None = None
|
|
1988
|
+
blocker_summary: str | None = None
|
|
1989
|
+
required_human_inputs: tuple[str, ...] = ()
|
|
1990
|
+
|
|
1991
|
+
def __post_init__(self) -> None:
|
|
1992
|
+
if self.blocker_class is not None:
|
|
1993
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
|
|
1994
|
+
|
|
1995
|
+
def to_json(self) -> dict[str, Any]:
|
|
1996
|
+
return clean_dict(asdict(self))
|
|
1997
|
+
|
|
1998
|
+
|
|
1999
|
+
# CS-2.1 SA — fleet-metric ledger event types. These populate the three
|
|
2000
|
+
# LEDGER-FAITHFUL named fleet-trend metrics (velocity / burn_down /
|
|
2001
|
+
# promise_broken_duration) reserved by the CS-2.1 spine. They are emitted to a
|
|
2002
|
+
# SEPARATE append-only sibling ledger (``.phase-loop/fleet-metrics.jsonl``) so
|
|
2003
|
+
# the existing ``events.jsonl`` bytes and reconcile semantics stay untouched.
|
|
2004
|
+
FLEET_METRIC_SCHEMA = "fleet_metric.v1"
|
|
2005
|
+
FLEET_METRIC_KINDS = (
|
|
2006
|
+
"velocity", # throughput: cumulative completed phases + timestamp
|
|
2007
|
+
"burn_down", # remaining-vs-completed scope trajectory
|
|
2008
|
+
"promise_broken", # an IF-gate promise declared-but-not-produced (break)
|
|
2009
|
+
"promise_repaired", # a previously-broken IF-gate promise later produced
|
|
2010
|
+
)
|
|
2011
|
+
|
|
2012
|
+
|
|
2013
|
+
@dataclass(frozen=True)
|
|
2014
|
+
class FleetMetricEvent:
|
|
2015
|
+
"""One append to the fleet-metric ledger (CS-2.1 SA).
|
|
2016
|
+
|
|
2017
|
+
``event_kind`` is fixed to ``fleet_metric`` and ``metric_kind`` selects the
|
|
2018
|
+
series. ``payload`` carries only the small numeric/label fields that series
|
|
2019
|
+
needs (never a filesystem path, secret, or session internal — the export
|
|
2020
|
+
bridge re-asserts that before anything leaves the enforcement side).
|
|
2021
|
+
"""
|
|
2022
|
+
|
|
2023
|
+
metric_kind: str
|
|
2024
|
+
timestamp: str = field(default_factory=utc_now)
|
|
2025
|
+
phase: str | None = None
|
|
2026
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
2027
|
+
schema: str = FLEET_METRIC_SCHEMA
|
|
2028
|
+
|
|
2029
|
+
def __post_init__(self) -> None:
|
|
2030
|
+
require_literal(self.metric_kind, FLEET_METRIC_KINDS, "fleet metric kind")
|
|
2031
|
+
if self.schema != FLEET_METRIC_SCHEMA:
|
|
2032
|
+
raise ValueError(f"invalid fleet metric schema: {self.schema}")
|
|
2033
|
+
|
|
2034
|
+
def to_json(self) -> dict[str, Any]:
|
|
2035
|
+
return clean_dict(
|
|
2036
|
+
{
|
|
2037
|
+
"event_kind": "fleet_metric",
|
|
2038
|
+
"schema": self.schema,
|
|
2039
|
+
"metric_kind": self.metric_kind,
|
|
2040
|
+
"timestamp": self.timestamp,
|
|
2041
|
+
"phase": self.phase,
|
|
2042
|
+
"payload": self.payload,
|
|
2043
|
+
}
|
|
2044
|
+
)
|
|
2045
|
+
|
|
2046
|
+
|
|
2047
|
+
@dataclass(frozen=True)
|
|
2048
|
+
class LoopEvent:
|
|
2049
|
+
timestamp: str
|
|
2050
|
+
repo: str
|
|
2051
|
+
roadmap: str
|
|
2052
|
+
phase: str
|
|
2053
|
+
action: str
|
|
2054
|
+
status: str
|
|
2055
|
+
model: str
|
|
2056
|
+
reasoning_effort: str
|
|
2057
|
+
source: str
|
|
2058
|
+
override_reason: str | None = None
|
|
2059
|
+
command: list[str] | None = None
|
|
2060
|
+
blocker: dict[str, Any] | None = None
|
|
2061
|
+
metadata: dict[str, Any] | None = None
|
|
2062
|
+
git_topology: dict[str, Any] | None = None
|
|
2063
|
+
selected_executor: str | None = None
|
|
2064
|
+
schema_version: int = 2
|
|
2065
|
+
roadmap_sha256: str | None = None
|
|
2066
|
+
phase_sha256: str | None = None
|
|
2067
|
+
|
|
2068
|
+
def __post_init__(self) -> None:
|
|
2069
|
+
require_literal(self.status, EVENT_STATUSES, "event status")
|
|
2070
|
+
if self.selected_executor is not None:
|
|
2071
|
+
require_literal(self.selected_executor, EXECUTORS, "selected executor")
|
|
2072
|
+
|
|
2073
|
+
def to_json(self) -> dict[str, Any]:
|
|
2074
|
+
return clean_dict(asdict(self))
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
@dataclass(frozen=True)
|
|
2078
|
+
class StateSnapshot:
|
|
2079
|
+
timestamp: str
|
|
2080
|
+
repo: str
|
|
2081
|
+
roadmap: str
|
|
2082
|
+
phases: dict[str, str] = field(default_factory=dict)
|
|
2083
|
+
current_phase: str | None = None
|
|
2084
|
+
last_action: str | None = None
|
|
2085
|
+
model: str | None = None
|
|
2086
|
+
reasoning_effort: str | None = None
|
|
2087
|
+
source: str | None = None
|
|
2088
|
+
override_reason: str | None = None
|
|
2089
|
+
human_required: bool = False
|
|
2090
|
+
blocker_class: str | None = None
|
|
2091
|
+
blocker_summary: str | None = None
|
|
2092
|
+
required_human_inputs: tuple[str, ...] = ()
|
|
2093
|
+
access_attempts: tuple[dict[str, Any], ...] = ()
|
|
2094
|
+
dirty_paths: tuple[str, ...] = ()
|
|
2095
|
+
phase_owned_dirty_paths: tuple[str, ...] = ()
|
|
2096
|
+
previous_phase_owned_paths: tuple[str, ...] = ()
|
|
2097
|
+
unowned_dirty_paths: tuple[str, ...] = ()
|
|
2098
|
+
pre_existing_dirty_paths: tuple[str, ...] = ()
|
|
2099
|
+
phase_owned_dirty: bool = False
|
|
2100
|
+
terminal_summary: dict[str, Any] | None = None
|
|
2101
|
+
latest_metric: dict[str, Any] | None = None
|
|
2102
|
+
metrics_summary: dict[str, Any] | None = None
|
|
2103
|
+
closeout_terminal_status: str | None = None
|
|
2104
|
+
closeout_summary: dict[str, Any] | None = None
|
|
2105
|
+
work_units: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
2106
|
+
latest_work_unit: dict[str, Any] | None = None
|
|
2107
|
+
pipeline_mode: str = "standalone"
|
|
2108
|
+
schema_version: int = 2
|
|
2109
|
+
roadmap_sha256: str | None = None
|
|
2110
|
+
phase_sha256: dict[str, str] = field(default_factory=dict)
|
|
2111
|
+
ledger_warnings: tuple[dict[str, Any], ...] = ()
|
|
2112
|
+
ledger_duplicates_skipped: tuple[dict[str, Any], ...] = ()
|
|
2113
|
+
git_topology: dict[str, Any] | None = None
|
|
2114
|
+
|
|
2115
|
+
def __post_init__(self) -> None:
|
|
2116
|
+
for status in self.phases.values():
|
|
2117
|
+
require_literal(status, PHASE_STATUSES, "phase status")
|
|
2118
|
+
if self.blocker_class is not None:
|
|
2119
|
+
require_literal(self.blocker_class, BLOCKER_CLASSES, "blocker class")
|
|
2120
|
+
require_literal(self.pipeline_mode, PIPELINE_MODE_LITERALS, "pipeline mode")
|
|
2121
|
+
for key, value in self.work_units.items():
|
|
2122
|
+
if not isinstance(value, dict):
|
|
2123
|
+
raise ValueError(f"invalid work-unit state record: {key}")
|
|
2124
|
+
WorkUnitState.from_json(value)
|
|
2125
|
+
|
|
2126
|
+
def to_json(self) -> dict[str, Any]:
|
|
2127
|
+
return clean_dict(asdict(self))
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
def clean_dict(value: Any) -> Any:
|
|
2131
|
+
if isinstance(value, dict):
|
|
2132
|
+
return {k: clean_dict(v) for k, v in value.items() if v is not None}
|
|
2133
|
+
if isinstance(value, list):
|
|
2134
|
+
return [clean_dict(v) for v in value]
|
|
2135
|
+
if isinstance(value, tuple):
|
|
2136
|
+
return [clean_dict(v) for v in value]
|
|
2137
|
+
return value
|
|
2138
|
+
|
|
2139
|
+
|
|
2140
|
+
def metadata_command(command: list[str], prompt: str | None = None) -> list[str]:
|
|
2141
|
+
redacted = list(command)
|
|
2142
|
+
if prompt is not None and redacted and redacted[-1] == prompt:
|
|
2143
|
+
redacted[-1] = f"<prompt redacted sha256={hashlib.sha256(prompt.encode('utf-8')).hexdigest()}>"
|
|
2144
|
+
return redacted
|