specfuse-loop 0.7.0__tar.gz → 0.8.0__tar.gz
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.
- {specfuse_loop-0.7.0/specfuse_loop.egg-info → specfuse_loop-0.8.0}/PKG-INFO +1 -1
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/pyproject.toml +1 -1
- specfuse_loop-0.8.0/specfuse/loop/_wu_sections.py +51 -0
- specfuse_loop-0.8.0/specfuse/loop/arm_eval.py +405 -0
- specfuse_loop-0.8.0/specfuse/loop/arm_txn.py +276 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/closing_requirements.py +21 -0
- specfuse_loop-0.8.0/specfuse/loop/data/VERSION +1 -0
- specfuse_loop-0.8.0/specfuse/loop/data/docs/concepts/adopting-auto-mode.md +151 -0
- specfuse_loop-0.8.0/specfuse/loop/data/docs/concepts/autonomy-stop-classes.md +212 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/methodology.md +53 -11
- specfuse_loop-0.8.0/specfuse/loop/data/templates/LEARNINGS-pending.template.md +37 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/templates/WU.template.md +13 -1
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/lint_plan.py +327 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/loop.py +224 -23
- specfuse_loop-0.8.0/specfuse/loop/plan_baseline.py +102 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/SOURCES.txt +19 -0
- specfuse_loop-0.8.0/tests/test_arm_eval.py +411 -0
- specfuse_loop-0.8.0/tests/test_arm_eval_lint_class.py +103 -0
- specfuse_loop-0.8.0/tests/test_arm_eval_wiring.py +236 -0
- specfuse_loop-0.8.0/tests/test_arm_txn.py +215 -0
- specfuse_loop-0.8.0/tests/test_arm_wiring.py +481 -0
- specfuse_loop-0.8.0/tests/test_feature_review_accumulation.py +268 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_init_integration.py +4 -0
- specfuse_loop-0.8.0/tests/test_learnings_staging.py +262 -0
- specfuse_loop-0.8.0/tests/test_lint_boundary_consistency.py +194 -0
- specfuse_loop-0.8.0/tests/test_lint_boundary_extraction.py +228 -0
- specfuse_loop-0.8.0/tests/test_lint_plan_contract_fields.py +164 -0
- specfuse_loop-0.8.0/tests/test_lint_produces_satisfiability.py +209 -0
- specfuse_loop-0.8.0/tests/test_plan_baseline.py +144 -0
- specfuse_loop-0.8.0/tests/test_produces_semantics_unified.py +190 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_data_in_sync.py +3 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_init.py +3 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_resources.py +3 -0
- specfuse_loop-0.7.0/specfuse/loop/_wu_sections.py +0 -44
- specfuse_loop-0.7.0/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/LICENSE +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/NOTICE +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/README.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/setup.cfg +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/_filelock.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/monitoring.yml.example +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/close-discipline.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/operator-escalation.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/planning-discipline.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules-local/README.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/escalation.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/events_stats.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/labels.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/lint_closing.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/lint_monitoring.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/__init__.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/adapters.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/artifact.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/cli.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/fingerprint.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/issues.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/providers/__init__.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/providers/_azure_auth.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/providers/azure_app_insights.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/providers/azure_service_bus.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/redaction.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/schedule.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_accept_hedged_close_skill.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_app_insights_adapters.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_artifact_redaction.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_attempt_outcome_contract.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_attention_nonwriting_guard.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_attention_skill_structure.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autoclose_deferral_visibility.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync_no_cwd_leak.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_azure_credential_resolution.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_backend.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_baseline_escalation_message.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_baseline_persistence.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_baseline_probe.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bash_routing.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bats_suites_gated.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bookkeeping_no_verify.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_claude_resolution.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_close_wu_self_reference.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_closing_guard_contracts.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_closing_guard_prediction.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_closing_requirements.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_console_encoding.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_derive_monitoring_discovery.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_derive_monitoring_skill_registration.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_design_for_diagnosis_rule.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_dispatch_skeleton.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_done_feature_gates.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_ensure_feature_branch_base.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_escalation_contract.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_escalation_emit.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_events_stats.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_failure_artifact_model.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_feature_id_resolution.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_filelock_portable.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_fingerprint.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gitleaks_pinning.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_heartbeat_adapter.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_interpreter_normalization.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_label_registry.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_label_registry_covers_consumers.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lifetime_accounting.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_closing.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_monitoring.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_plan_verdict_exempt.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_planning_sections.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_verification_reference.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_attempt_evidence.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitor_cli.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitor_issue_lifecycle.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitor_runner_surfaces.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_cron_dialect.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_example.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_fenced_blocks.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_seed.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_operator_escalation_rule.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_planning_cost_floor.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_produces_in_diff.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_provision_labels.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_queue_stalled_adapter.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_redaction_windows_home.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_resolve_base.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_label_provisioning.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_schedule_dialect.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_service_bus_dlq_adapter.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_skill_discovery_links.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_spinning_rearm_gate.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_timeout_kill_portable.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_untracked_file_absorption.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_windows_gate_exec.py +0 -0
- {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_wu_execution_metadata.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfuse-loop"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Specfuse contributors
|
|
4
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
5
|
+
#
|
|
6
|
+
"""
|
|
7
|
+
Dependency-free slicers for work-unit body sections.
|
|
8
|
+
|
|
9
|
+
Shared by `loop.py` and `lint_plan.py`, which cannot import each other
|
|
10
|
+
(`lint_plan.py` imports `VERDICT_VALUES` from `loop.py`, so the reverse
|
|
11
|
+
import would be a cycle). This module imports nothing from either.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
_AC_END_RE = re.compile(r"(?m)^(?:\*\*|#{1,6}\s)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def slice_wu_section(body: str, section_name: str) -> str:
|
|
22
|
+
"""Return content between a named section heading and the next heading.
|
|
23
|
+
|
|
24
|
+
Handles both section-heading shapes a WU body uses: the canonical
|
|
25
|
+
bold-preamble form (`**Section name.** content starting on the same
|
|
26
|
+
line`, what `.specfuse/templates/WU.template.md` prescribes and what
|
|
27
|
+
327/327 real WU bodies in this repo use) and the ATX form (`## Section
|
|
28
|
+
name`, content starting on the next line). For the bold form, content
|
|
29
|
+
immediately following the closing `**` — on the label line itself — is
|
|
30
|
+
part of the section; discarding it (as a next-line-only slice would)
|
|
31
|
+
silently drops whatever a WU author wrote inline after the label.
|
|
32
|
+
"""
|
|
33
|
+
escaped = re.escape(section_name)
|
|
34
|
+
heading_re = re.compile(
|
|
35
|
+
rf"(?mi)^#{{1,6}}\s*{escaped}\b|^\*\*{escaped}[^\n*]*\*\*\.?"
|
|
36
|
+
)
|
|
37
|
+
m = heading_re.search(body)
|
|
38
|
+
if not m:
|
|
39
|
+
return ""
|
|
40
|
+
if m.group(0).startswith("#"):
|
|
41
|
+
nl = body.find("\n", m.end())
|
|
42
|
+
after = body[nl + 1:] if nl != -1 else ""
|
|
43
|
+
else:
|
|
44
|
+
after = body[m.end():]
|
|
45
|
+
em = _AC_END_RE.search(after)
|
|
46
|
+
return after[:em.start()] if em else after
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def slice_acceptance_criteria(body: str) -> str:
|
|
50
|
+
"""Return the text of the Acceptance criteria section only (bold-preamble or ATX)."""
|
|
51
|
+
return slice_wu_section(body, "Acceptance criteria")
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Specfuse contributors
|
|
3
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
#
|
|
5
|
+
"""Deterministic gate-arm predicate — pure, side-effect-free module.
|
|
6
|
+
|
|
7
|
+
Given a feature directory and the gate id that just closed, evaluates whether
|
|
8
|
+
the drafted successor gate may be armed automatically, and returns a
|
|
9
|
+
structured per-class verdict. Mirrors the shape of `gate_eval.py`'s
|
|
10
|
+
`evaluate_auto_close`: pure evaluation over files, a structured decision
|
|
11
|
+
object, a formatter for the event trail, no side effects. Arming and closing
|
|
12
|
+
are deliberately separate decisions with different inputs — this module does
|
|
13
|
+
not fold into `gate_eval.py`, does not import from it, and does not import
|
|
14
|
+
from `loop.py` (the dependency points the other way: `loop.py` will call
|
|
15
|
+
into this module, T04).
|
|
16
|
+
|
|
17
|
+
The organizing principle: model-authored signals may only veto; only
|
|
18
|
+
mechanical facts and human-authored constants may approve. Of the eight
|
|
19
|
+
classes below, "missing_provenance", "open_questions_human_only", and
|
|
20
|
+
"plan_next_lint" (FEAT-2026-0053/T07) are veto channels — a clean verdict
|
|
21
|
+
there withholds nothing, it just declines to block; every other class both
|
|
22
|
+
approves (clean) and blocks (fired) on mechanical grounds alone. `would_arm`
|
|
23
|
+
is True only when all eight classes are clean, so the veto classes can only
|
|
24
|
+
ever pull the decision from True to False, never the reverse.
|
|
25
|
+
|
|
26
|
+
Honest v1 limit: a draft that weakens an *existing* test's assertions is
|
|
27
|
+
undetectable here. The judge-editing class catches `produces:` paths, not
|
|
28
|
+
prose or assertion semantics — it is a path-surface check, nothing more.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import re
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from . import _miniyaml
|
|
38
|
+
from .closing_requirements import gate_review_filename
|
|
39
|
+
from .plan_baseline import load_baseline, load_plan_graph
|
|
40
|
+
|
|
41
|
+
PREDICATE_VERSION = "v1"
|
|
42
|
+
|
|
43
|
+
BUDGET_PROJECTION_MULTIPLIER = 2.0
|
|
44
|
+
DRIFT_CAP_RATIO = 0.5
|
|
45
|
+
ADDED_GATE_CAP = 1
|
|
46
|
+
|
|
47
|
+
# Path-prefix surfaces a drafted WU must not touch (class 2). `pyproject.toml`
|
|
48
|
+
# is matched whole-file — this predicate cannot distinguish the coverage/lint
|
|
49
|
+
# config sections from the rest of the file without a diff, so any produced
|
|
50
|
+
# `pyproject.toml` fires both this class and decision_class_paths below. That
|
|
51
|
+
# double-fire is an accepted v1 approximation, not a bug.
|
|
52
|
+
JUDGE_PATHS = (
|
|
53
|
+
".specfuse/verification.yml",
|
|
54
|
+
".specfuse/hooks/",
|
|
55
|
+
".specfuse/rules/",
|
|
56
|
+
".github/workflows/",
|
|
57
|
+
"specfuse/loop/",
|
|
58
|
+
"pyproject.toml",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Dependency-manifest surfaces (class 3). Same `pyproject.toml` caveat as above.
|
|
62
|
+
_DEPENDENCY_MANIFEST_EXACT = ("pyproject.toml", "package.json")
|
|
63
|
+
_REQUIREMENTS_RE = re.compile(r"(^|/)requirements[^/]*\.txt$")
|
|
64
|
+
|
|
65
|
+
_FM_DELIM = re.compile(r"^---\s*$")
|
|
66
|
+
_TITLE_RE = re.compile(r"^#\s+(.*)$", re.MULTILINE)
|
|
67
|
+
|
|
68
|
+
CLASS_NAMES = (
|
|
69
|
+
"budget_projection",
|
|
70
|
+
"judge_editing",
|
|
71
|
+
"decision_class_paths",
|
|
72
|
+
"retroactive_edits",
|
|
73
|
+
"drift_caps",
|
|
74
|
+
"missing_provenance",
|
|
75
|
+
"open_questions_human_only",
|
|
76
|
+
"plan_next_lint",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
VETO_CLASSES = frozenset(
|
|
80
|
+
{"missing_provenance", "open_questions_human_only", "plan_next_lint"}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class ClassVerdict:
|
|
86
|
+
status: str # "fired" | "clean" | "not_evaluable"
|
|
87
|
+
reason: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class ArmDecision:
|
|
92
|
+
would_arm: bool
|
|
93
|
+
classes: dict # class name -> ClassVerdict, one entry per CLASS_NAMES
|
|
94
|
+
feature_id: str
|
|
95
|
+
gate_id: int # the just-closed gate
|
|
96
|
+
predicate_version: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
|
100
|
+
"""Return (frontmatter_dict, body_text) from ---\n...\n--- delimited text."""
|
|
101
|
+
lines = text.splitlines()
|
|
102
|
+
if not lines or not _FM_DELIM.match(lines[0]):
|
|
103
|
+
return {}, text
|
|
104
|
+
end = next((i for i, ln in enumerate(lines[1:], 1) if _FM_DELIM.match(ln)), None)
|
|
105
|
+
if end is None:
|
|
106
|
+
return {}, text
|
|
107
|
+
fm = _miniyaml.parse("\n".join(lines[1:end])) or {}
|
|
108
|
+
body = "\n".join(lines[end + 1:])
|
|
109
|
+
return fm, body
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _read_wu(feature_dir: Path, ref: dict) -> "dict | None":
|
|
113
|
+
"""Return a normalized WU record, or None when the referenced file is absent."""
|
|
114
|
+
path = feature_dir / ref["file"]
|
|
115
|
+
if not path.exists():
|
|
116
|
+
return None
|
|
117
|
+
fm, body = _parse_frontmatter(path.read_text())
|
|
118
|
+
title_m = _TITLE_RE.search(body)
|
|
119
|
+
goal = title_m.group(1).strip() if title_m else ref["id"]
|
|
120
|
+
return {
|
|
121
|
+
"id": ref["id"],
|
|
122
|
+
"type": fm.get("type", "implementation"),
|
|
123
|
+
"status": fm.get("status", "pending"),
|
|
124
|
+
"cost_usd": fm.get("cost_usd", 0.0) or 0.0,
|
|
125
|
+
"planned_cost_usd": fm.get("planned_cost_usd"),
|
|
126
|
+
"produces": fm.get("produces") or [],
|
|
127
|
+
"produces_driver_helper": fm.get("produces_driver_helper") or [],
|
|
128
|
+
"human_only": bool(fm.get("human_only", False)),
|
|
129
|
+
"provenance": fm.get("provenance"),
|
|
130
|
+
"goal": goal,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _matches_judge_path(path: str) -> bool:
|
|
135
|
+
return any(path == jp or path.startswith(jp) for jp in JUDGE_PATHS)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _matches_dependency_manifest(path: str) -> bool:
|
|
139
|
+
if _REQUIREMENTS_RE.search(path):
|
|
140
|
+
return True
|
|
141
|
+
return any(path == p or path.endswith("/" + p) for p in _DEPENDENCY_MANIFEST_EXACT)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def evaluate_arm_predicate(feature_dir: Path, just_closed_gate: int) -> ArmDecision:
|
|
145
|
+
"""Return an ArmDecision for the gate that just closed in `feature_dir`.
|
|
146
|
+
|
|
147
|
+
`just_closed_gate` is the gate number that just passed; the predicate
|
|
148
|
+
evaluates whether gate `just_closed_gate + 1`'s drafted WUs may be armed.
|
|
149
|
+
"""
|
|
150
|
+
feature_dir = Path(feature_dir)
|
|
151
|
+
plan_fm, _ = _parse_frontmatter((feature_dir / "PLAN.md").read_text())
|
|
152
|
+
feature_id = plan_fm.get("feature_id", feature_dir.name)
|
|
153
|
+
|
|
154
|
+
baseline = load_baseline(feature_dir)
|
|
155
|
+
if baseline is None:
|
|
156
|
+
classes = {name: ClassVerdict("not_evaluable", "no_baseline") for name in CLASS_NAMES}
|
|
157
|
+
return ArmDecision(
|
|
158
|
+
would_arm=False,
|
|
159
|
+
classes=classes,
|
|
160
|
+
feature_id=feature_id,
|
|
161
|
+
gate_id=just_closed_gate,
|
|
162
|
+
predicate_version=PREDICATE_VERSION,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
plan = load_plan_graph(feature_dir)
|
|
166
|
+
gates = plan.get("gates", []) or []
|
|
167
|
+
|
|
168
|
+
# Baseline indices
|
|
169
|
+
baseline_wus: dict[str, dict] = {}
|
|
170
|
+
baseline_gate_wu_ids: dict[int, set] = {}
|
|
171
|
+
for g in baseline.get("gates", []) or []:
|
|
172
|
+
ids = set()
|
|
173
|
+
for wu in g.get("work_units", []) or []:
|
|
174
|
+
baseline_wus[wu["id"]] = wu
|
|
175
|
+
ids.add(wu["id"])
|
|
176
|
+
baseline_gate_wu_ids[g["gate"]] = ids
|
|
177
|
+
baseline_gate_nums = set(baseline_gate_wu_ids.keys())
|
|
178
|
+
baseline_total_planned = sum(
|
|
179
|
+
(wu.get("planned_cost_usd") or 0.0) for wu in baseline_wus.values()
|
|
180
|
+
)
|
|
181
|
+
baseline_total_count = len(baseline_wus)
|
|
182
|
+
|
|
183
|
+
# Current plan indices
|
|
184
|
+
current_by_gate: dict[int, list] = {}
|
|
185
|
+
current_all: dict[str, dict] = {}
|
|
186
|
+
for g in gates:
|
|
187
|
+
gnum = g.get("gate")
|
|
188
|
+
wus = []
|
|
189
|
+
for ref in g.get("work_units", []) or []:
|
|
190
|
+
wu = _read_wu(feature_dir, ref)
|
|
191
|
+
if wu is None:
|
|
192
|
+
continue
|
|
193
|
+
wus.append(wu)
|
|
194
|
+
current_all[wu["id"]] = wu
|
|
195
|
+
current_by_gate[gnum] = wus
|
|
196
|
+
|
|
197
|
+
added_ids = set(current_all) - set(baseline_wus)
|
|
198
|
+
|
|
199
|
+
classes: dict[str, ClassVerdict] = {}
|
|
200
|
+
|
|
201
|
+
# --- Class 1: budget projection ---
|
|
202
|
+
lifetime_spend = sum(wu["cost_usd"] for wu in current_all.values())
|
|
203
|
+
planned_remaining = sum(
|
|
204
|
+
(wu["planned_cost_usd"] or 0.0)
|
|
205
|
+
for wu in current_all.values()
|
|
206
|
+
if wu["status"] != "done"
|
|
207
|
+
)
|
|
208
|
+
projection = lifetime_spend + planned_remaining
|
|
209
|
+
ceiling = BUDGET_PROJECTION_MULTIPLIER * baseline_total_planned
|
|
210
|
+
if projection > ceiling:
|
|
211
|
+
classes["budget_projection"] = ClassVerdict(
|
|
212
|
+
"fired",
|
|
213
|
+
f"projected spend ${projection:.2f} (spend ${lifetime_spend:.2f} + "
|
|
214
|
+
f"remaining ${planned_remaining:.2f}) exceeds "
|
|
215
|
+
f"{BUDGET_PROJECTION_MULTIPLIER:.1f}x baseline planned total "
|
|
216
|
+
f"${baseline_total_planned:.2f} (cap ${ceiling:.2f})",
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
classes["budget_projection"] = ClassVerdict(
|
|
220
|
+
"clean",
|
|
221
|
+
f"projected spend ${projection:.2f} within "
|
|
222
|
+
f"{BUDGET_PROJECTION_MULTIPLIER:.1f}x baseline planned total "
|
|
223
|
+
f"${baseline_total_planned:.2f} (cap ${ceiling:.2f})",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
next_gate_num = just_closed_gate + 1
|
|
227
|
+
drafted = current_by_gate.get(next_gate_num, [])
|
|
228
|
+
|
|
229
|
+
# --- Class 2: judge-editing ---
|
|
230
|
+
judge_hits: list[str] = []
|
|
231
|
+
for wu in drafted:
|
|
232
|
+
for p in wu["produces"]:
|
|
233
|
+
if _matches_judge_path(p):
|
|
234
|
+
judge_hits.append(f"{wu['id']} produces {p}")
|
|
235
|
+
if wu["produces_driver_helper"]:
|
|
236
|
+
judge_hits.append(
|
|
237
|
+
f"{wu['id']} declares produces_driver_helper "
|
|
238
|
+
f"{wu['produces_driver_helper']!r}"
|
|
239
|
+
)
|
|
240
|
+
if judge_hits:
|
|
241
|
+
classes["judge_editing"] = ClassVerdict("fired", "; ".join(judge_hits))
|
|
242
|
+
else:
|
|
243
|
+
classes["judge_editing"] = ClassVerdict(
|
|
244
|
+
"clean", "no drafted WU touches judge-path surfaces"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# --- Class 3: decision-class paths ---
|
|
248
|
+
dep_hits: list[str] = []
|
|
249
|
+
for wu in drafted:
|
|
250
|
+
for p in wu["produces"]:
|
|
251
|
+
if _matches_dependency_manifest(p):
|
|
252
|
+
dep_hits.append(f"{wu['id']} produces {p}")
|
|
253
|
+
if dep_hits:
|
|
254
|
+
classes["decision_class_paths"] = ClassVerdict("fired", "; ".join(dep_hits))
|
|
255
|
+
else:
|
|
256
|
+
classes["decision_class_paths"] = ClassVerdict(
|
|
257
|
+
"clean", "no drafted WU touches dependency manifests"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# --- Class 4: retroactive edits (baseline WUs of already-passed gates) ---
|
|
261
|
+
retro_hits: list[str] = []
|
|
262
|
+
for gnum, ids in baseline_gate_wu_ids.items():
|
|
263
|
+
gate_entry = next((g for g in gates if g.get("gate") == gnum), None)
|
|
264
|
+
gate_file = gate_entry.get("file") if gate_entry else f"GATE-{gnum:02d}.md"
|
|
265
|
+
gate_path = feature_dir / gate_file
|
|
266
|
+
if not gate_path.exists():
|
|
267
|
+
continue
|
|
268
|
+
gate_fm, _ = _parse_frontmatter(gate_path.read_text())
|
|
269
|
+
if gate_fm.get("status") != "passed":
|
|
270
|
+
continue
|
|
271
|
+
for wu_id in sorted(ids):
|
|
272
|
+
baseline_wu = baseline_wus[wu_id]
|
|
273
|
+
current_wu = current_all.get(wu_id)
|
|
274
|
+
if current_wu is None:
|
|
275
|
+
retro_hits.append(f"{wu_id} left the graph")
|
|
276
|
+
continue
|
|
277
|
+
if current_wu["type"] != baseline_wu.get("type"):
|
|
278
|
+
retro_hits.append(
|
|
279
|
+
f"{wu_id} type changed {baseline_wu.get('type')!r} -> "
|
|
280
|
+
f"{current_wu['type']!r}"
|
|
281
|
+
)
|
|
282
|
+
if current_wu["goal"] != baseline_wu.get("goal"):
|
|
283
|
+
retro_hits.append(
|
|
284
|
+
f"{wu_id} goal changed {baseline_wu.get('goal')!r} -> "
|
|
285
|
+
f"{current_wu['goal']!r}"
|
|
286
|
+
)
|
|
287
|
+
if retro_hits:
|
|
288
|
+
classes["retroactive_edits"] = ClassVerdict("fired", "; ".join(retro_hits))
|
|
289
|
+
else:
|
|
290
|
+
classes["retroactive_edits"] = ClassVerdict(
|
|
291
|
+
"clean", "no passed-gate baseline WU altered or removed"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# --- Class 5: drift caps ---
|
|
295
|
+
added_count = len(added_ids)
|
|
296
|
+
added_sum = sum((current_all[i]["planned_cost_usd"] or 0.0) for i in added_ids)
|
|
297
|
+
current_gate_nums = {g.get("gate") for g in gates}
|
|
298
|
+
added_gate_nums = current_gate_nums - baseline_gate_nums
|
|
299
|
+
count_ceiling = DRIFT_CAP_RATIO * baseline_total_count
|
|
300
|
+
sum_ceiling = DRIFT_CAP_RATIO * baseline_total_planned
|
|
301
|
+
drift_hits: list[str] = []
|
|
302
|
+
if added_count > count_ceiling:
|
|
303
|
+
drift_hits.append(
|
|
304
|
+
f"added WU count {added_count} exceeds {DRIFT_CAP_RATIO:.1f}x baseline "
|
|
305
|
+
f"count {baseline_total_count} (cap {count_ceiling:.1f})"
|
|
306
|
+
)
|
|
307
|
+
if added_sum > sum_ceiling:
|
|
308
|
+
drift_hits.append(
|
|
309
|
+
f"added planned cost ${added_sum:.2f} exceeds {DRIFT_CAP_RATIO:.1f}x "
|
|
310
|
+
f"baseline planned total ${baseline_total_planned:.2f} "
|
|
311
|
+
f"(cap ${sum_ceiling:.2f})"
|
|
312
|
+
)
|
|
313
|
+
if len(added_gate_nums) > ADDED_GATE_CAP:
|
|
314
|
+
drift_hits.append(
|
|
315
|
+
f"added gate count {len(added_gate_nums)} exceeds cap {ADDED_GATE_CAP}: "
|
|
316
|
+
f"{sorted(added_gate_nums)}"
|
|
317
|
+
)
|
|
318
|
+
if drift_hits:
|
|
319
|
+
classes["drift_caps"] = ClassVerdict("fired", "; ".join(drift_hits))
|
|
320
|
+
else:
|
|
321
|
+
classes["drift_caps"] = ClassVerdict(
|
|
322
|
+
"clean", "added WUs and gates within drift caps"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# --- Class 6: missing provenance (veto channel) ---
|
|
326
|
+
missing_prov = sorted(
|
|
327
|
+
wu_id for wu_id in added_ids if not current_all[wu_id]["provenance"]
|
|
328
|
+
)
|
|
329
|
+
if missing_prov:
|
|
330
|
+
classes["missing_provenance"] = ClassVerdict(
|
|
331
|
+
"fired", f"added WU(s) missing provenance: {', '.join(missing_prov)}"
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
classes["missing_provenance"] = ClassVerdict(
|
|
335
|
+
"clean",
|
|
336
|
+
"every added WU carries a provenance field" if added_ids else "no added WUs",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# --- Class 7: open questions / human-only (veto channel) ---
|
|
340
|
+
reasons7: list[str] = []
|
|
341
|
+
if next_gate_num in current_by_gate:
|
|
342
|
+
review_path = feature_dir / gate_review_filename(next_gate_num)
|
|
343
|
+
if not review_path.exists():
|
|
344
|
+
reasons7.append(f"{review_path.name} missing")
|
|
345
|
+
else:
|
|
346
|
+
review_fm, _ = _parse_frontmatter(review_path.read_text())
|
|
347
|
+
if "open_questions" not in review_fm:
|
|
348
|
+
reasons7.append(f"{review_path.name} missing open_questions field")
|
|
349
|
+
else:
|
|
350
|
+
oq = review_fm.get("open_questions") or []
|
|
351
|
+
if oq:
|
|
352
|
+
reasons7.append(f"{review_path.name} open_questions non-empty: {oq!r}")
|
|
353
|
+
human_only_hits = [wu["id"] for wu in drafted if wu["human_only"]]
|
|
354
|
+
if human_only_hits:
|
|
355
|
+
reasons7.append(f"human_only flagged: {', '.join(human_only_hits)}")
|
|
356
|
+
clean_reason = "open_questions present and empty, no human_only flags"
|
|
357
|
+
else:
|
|
358
|
+
clean_reason = "terminal gate: no drafted successor"
|
|
359
|
+
if reasons7:
|
|
360
|
+
classes["open_questions_human_only"] = ClassVerdict("fired", "; ".join(reasons7))
|
|
361
|
+
else:
|
|
362
|
+
classes["open_questions_human_only"] = ClassVerdict("clean", clean_reason)
|
|
363
|
+
|
|
364
|
+
# --- Class 8: plan-next contract lint (veto channel, FEAT-2026-0053/T07) ---
|
|
365
|
+
# lint_plan_next_draft is WARN-only for its own CLI caller; here a
|
|
366
|
+
# non-empty finding list vetoes the arm. A parse failure inside the lint
|
|
367
|
+
# (malformed frontmatter) must degrade to a fired verdict, not crash the
|
|
368
|
+
# gate close — same precedent as build_arm_predicate_event's outer catch.
|
|
369
|
+
try:
|
|
370
|
+
from .lint_plan import lint_plan_next_draft # deferred: lint_plan imports loop.py
|
|
371
|
+
|
|
372
|
+
lint_warns = lint_plan_next_draft(feature_dir, just_closed_gate)
|
|
373
|
+
except Exception as exc: # noqa: BLE001 - a raise is not a verdict
|
|
374
|
+
classes["plan_next_lint"] = ClassVerdict(
|
|
375
|
+
"fired",
|
|
376
|
+
f"plan-next lint raised {type(exc).__name__}: {exc}",
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
if lint_warns:
|
|
380
|
+
classes["plan_next_lint"] = ClassVerdict("fired", "; ".join(lint_warns))
|
|
381
|
+
else:
|
|
382
|
+
classes["plan_next_lint"] = ClassVerdict("clean", "plan-next lint: no findings")
|
|
383
|
+
|
|
384
|
+
would_arm = all(classes[name].status != "fired" for name in CLASS_NAMES)
|
|
385
|
+
|
|
386
|
+
return ArmDecision(
|
|
387
|
+
would_arm=would_arm,
|
|
388
|
+
classes=classes,
|
|
389
|
+
feature_id=feature_id,
|
|
390
|
+
gate_id=just_closed_gate,
|
|
391
|
+
predicate_version=PREDICATE_VERSION,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _format_decision(decision: ArmDecision) -> str:
|
|
396
|
+
"""Render an ArmDecision to the canonical event-trail block shape."""
|
|
397
|
+
lines: list[str] = [
|
|
398
|
+
f" arm_predicate gate={decision.gate_id} would_arm={decision.would_arm}"
|
|
399
|
+
]
|
|
400
|
+
for name in CLASS_NAMES:
|
|
401
|
+
v = decision.classes.get(name)
|
|
402
|
+
if v is None:
|
|
403
|
+
continue
|
|
404
|
+
lines.append(f" {name}: {v.status} — {v.reason}")
|
|
405
|
+
return "\n".join(lines)
|