specfuse-loop 0.4.0__tar.gz → 0.6.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.4.0/specfuse_loop.egg-info → specfuse_loop-0.6.0}/PKG-INFO +1 -1
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/pyproject.toml +3 -1
- specfuse_loop-0.6.0/specfuse/loop/_wu_sections.py +44 -0
- specfuse_loop-0.6.0/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/methodology.md +71 -4
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/skills.md +25 -1
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/gitignore.snippet +1 -0
- specfuse_loop-0.6.0/specfuse/loop/data/monitoring-secrets-checklist.md +32 -0
- specfuse_loop-0.6.0/specfuse/loop/data/monitoring.overrides.yml.example +70 -0
- specfuse_loop-0.6.0/specfuse/loop/data/monitoring.yml.example +170 -0
- specfuse_loop-0.6.0/specfuse/loop/data/rules/close-discipline.md +115 -0
- specfuse_loop-0.6.0/specfuse/loop/data/rules/design-for-diagnosis.md +90 -0
- specfuse_loop-0.6.0/specfuse/loop/data/rules/operator-escalation.md +70 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/planning-discipline.md +51 -9
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/templates/WU.template.md +18 -6
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/verification.yml.example +3 -0
- specfuse_loop-0.6.0/specfuse/loop/escalation.py +236 -0
- specfuse_loop-0.6.0/specfuse/loop/events_stats.py +288 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/gate_eval.py +3 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/gh_features.py +3 -1
- specfuse_loop-0.6.0/specfuse/loop/labels.py +159 -0
- specfuse_loop-0.6.0/specfuse/loop/lint_monitoring.py +339 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/lint_plan.py +258 -42
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/loop.py +393 -20
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/scaffold.py +47 -2
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse_loop.egg-info/SOURCES.txt +38 -0
- specfuse_loop-0.6.0/specfuse_loop.egg-info/entry_points.txt +5 -0
- specfuse_loop-0.6.0/tests/test_accept_hedged_close_skill.py +168 -0
- specfuse_loop-0.6.0/tests/test_attempt_outcome_contract.py +136 -0
- specfuse_loop-0.6.0/tests/test_attention_nonwriting_guard.py +83 -0
- specfuse_loop-0.6.0/tests/test_attention_skill_structure.py +75 -0
- specfuse_loop-0.6.0/tests/test_autoclose_deferral_visibility.py +244 -0
- specfuse_loop-0.6.0/tests/test_bats_suites_gated.py +89 -0
- specfuse_loop-0.6.0/tests/test_closing_guard_contracts.py +172 -0
- specfuse_loop-0.6.0/tests/test_closing_guard_prediction.py +235 -0
- specfuse_loop-0.6.0/tests/test_derive_monitoring_discovery.py +973 -0
- specfuse_loop-0.6.0/tests/test_derive_monitoring_skill_registration.py +211 -0
- specfuse_loop-0.6.0/tests/test_design_for_diagnosis_rule.py +76 -0
- specfuse_loop-0.6.0/tests/test_done_feature_gates.py +124 -0
- specfuse_loop-0.6.0/tests/test_escalation_contract.py +133 -0
- specfuse_loop-0.6.0/tests/test_escalation_emit.py +116 -0
- specfuse_loop-0.6.0/tests/test_events_stats.py +230 -0
- specfuse_loop-0.6.0/tests/test_feature_id_resolution.py +127 -0
- specfuse_loop-0.6.0/tests/test_gitleaks_pinning.py +258 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_init_integration.py +7 -0
- specfuse_loop-0.6.0/tests/test_label_registry.py +52 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_leak_scan.py +92 -11
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lifecycle_integration.py +53 -1
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_bare_produces_path.py +59 -1
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_close_wu.py +4 -1
- specfuse_loop-0.6.0/tests/test_lint_monitoring.py +765 -0
- specfuse_loop-0.6.0/tests/test_lint_plan_verdict_exempt.py +92 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_post_pass_invariant.py +187 -0
- specfuse_loop-0.6.0/tests/test_monitoring_bootstrap_artifacts.py +123 -0
- specfuse_loop-0.6.0/tests/test_monitoring_example.py +202 -0
- specfuse_loop-0.6.0/tests/test_monitoring_fenced_blocks.py +179 -0
- specfuse_loop-0.6.0/tests/test_monitoring_seed.py +65 -0
- specfuse_loop-0.6.0/tests/test_operator_escalation_rule.py +165 -0
- specfuse_loop-0.6.0/tests/test_planning_cost_floor.py +198 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_produces_in_diff.py +36 -0
- specfuse_loop-0.6.0/tests/test_provision_labels.py +208 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_data_in_sync.py +5 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_init.py +6 -0
- specfuse_loop-0.6.0/tests/test_scaffold_label_provisioning.py +145 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_resources.py +5 -0
- specfuse_loop-0.6.0/tests/test_skill_discovery_links.py +137 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_terminal_flips.py +204 -0
- specfuse_loop-0.4.0/specfuse/loop/data/VERSION +0 -1
- specfuse_loop-0.4.0/specfuse/loop/data/rules/close-discipline.md +0 -74
- specfuse_loop-0.4.0/specfuse_loop.egg-info/entry_points.txt +0 -3
- specfuse_loop-0.4.0/tests/test_autoclose_deferral_visibility.py +0 -67
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/LICENSE +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/NOTICE +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/README.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/setup.cfg +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/_filelock.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules-local/README.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_backend.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_baseline_escalation_message.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_baseline_persistence.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_baseline_probe.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_bash_routing.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_bookkeeping_no_verify.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_claude_resolution.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_close_wu_self_reference.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_console_encoding.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_ensure_feature_branch_base.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_filelock_portable.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_interpreter_normalization.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lifetime_accounting.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_planning_sections.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_verification_reference.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_attempt_evidence.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_redaction_windows_home.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_resolve_base.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_spinning_rearm_gate.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_timeout_kill_portable.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_untracked_file_absorption.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_windows_gate_exec.py +0 -0
- {specfuse_loop-0.4.0 → specfuse_loop-0.6.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.6.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"
|
|
@@ -63,6 +63,8 @@ select = ["E4", "E7", "E9", "F", "PLW1510", "B", "BLE001", "S110", "TRY004"]
|
|
|
63
63
|
[project.scripts]
|
|
64
64
|
specfuse-loop = "specfuse.loop.loop:main"
|
|
65
65
|
specfuse-lint = "specfuse.loop.lint_plan:main"
|
|
66
|
+
specfuse-monitor-lint = "specfuse.loop.lint_monitoring:main"
|
|
67
|
+
specfuse-stats = "specfuse.loop.events_stats:main"
|
|
66
68
|
|
|
67
69
|
[tool.setuptools.packages.find]
|
|
68
70
|
# Ship ONLY the specfuse namespace package. Without this scope, setuptools'
|
|
@@ -0,0 +1,44 @@
|
|
|
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_START_RE = re.compile(
|
|
19
|
+
r"(?mi)^\*\*Acceptance criteria[^\n*]*\*\*\.?|^#{1,6}\s+Acceptance criteria"
|
|
20
|
+
)
|
|
21
|
+
_AC_END_RE = re.compile(r"(?m)^(?:\*\*|#{1,6}\s)")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def slice_acceptance_criteria(body: str) -> str:
|
|
25
|
+
"""Return the text of the Acceptance criteria section only (bold-preamble or ATX)."""
|
|
26
|
+
m = _AC_START_RE.search(body)
|
|
27
|
+
if not m:
|
|
28
|
+
return ""
|
|
29
|
+
nl = body.find("\n", m.end())
|
|
30
|
+
after = body[nl + 1:] if nl != -1 else ""
|
|
31
|
+
em = _AC_END_RE.search(after)
|
|
32
|
+
return after[:em.start()] if em else after
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def slice_wu_section(body: str, section_name: str) -> str:
|
|
36
|
+
"""Return content between a named section heading and the next heading."""
|
|
37
|
+
start_re = re.compile(rf"(?mi)^(?:#+\s*|\**){re.escape(section_name)}")
|
|
38
|
+
m = start_re.search(body)
|
|
39
|
+
if not m:
|
|
40
|
+
return ""
|
|
41
|
+
nl = body.find("\n", m.end())
|
|
42
|
+
after = body[nl + 1:] if nl != -1 else ""
|
|
43
|
+
em = _AC_END_RE.search(after)
|
|
44
|
+
return after[:em.start()] if em else after
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.6.0
|
|
@@ -159,6 +159,39 @@ re-run condition that would upgrade the verdict to `met`. Without the record,
|
|
|
159
159
|
close-time obligations (fresh oracle re-runs, consumer-visible-change
|
|
160
160
|
enumeration).
|
|
161
161
|
|
|
162
|
+
**Exiting a hedged verdict** (FEAT-2026-0070) — a hedge left the feature with no
|
|
163
|
+
supported path to `done`, because `fire_terminal_flips` runs at close-WU-*outcome*
|
|
164
|
+
time and the driver never re-dispatches a `done` close WU. Two surfaces now close
|
|
165
|
+
that gap, and **neither writes terminal state itself** — both route through the
|
|
166
|
+
one owner:
|
|
167
|
+
|
|
168
|
+
- `specfuse-loop --recheck-verdict <FEATURE_ID>` re-reads the terminal close WU's
|
|
169
|
+
verdict from disk and fires the flips if it now permits them, without
|
|
170
|
+
re-dispatching the WU. Use it when follow-ups were genuinely discharged and the
|
|
171
|
+
verdict was honestly upgraded to `met`. It is a no-op (exit `0`, printing why)
|
|
172
|
+
when the feature is already `done` or the verdict is still hedged.
|
|
173
|
+
- `/accept-hedged-close` is the operator path for a hedge that is the ceiling **by
|
|
174
|
+
construction** — the criterion's oracle lives outside the repo and no amount of
|
|
175
|
+
gate work will close it. It surfaces the §2 follow-up record, requires a one-line
|
|
176
|
+
reason and explicit acknowledgment of the standing follow-ups, writes an
|
|
177
|
+
acceptance record, and then invokes `--recheck-verdict`. It carries the
|
|
178
|
+
follow-ups forward; it does not discharge them.
|
|
179
|
+
|
|
180
|
+
**The row flips from any non-`done` status** (FEAT-2026-0070) — the terminal
|
|
181
|
+
roadmap-row flip previously fired only on `active → done`, so an `autonomy: auto`
|
|
182
|
+
feature that self-dispatched from a `planned` row stayed `planned` through a
|
|
183
|
+
correct close and escalated `roadmap_row_not_done`. Any non-`done` row now flips.
|
|
184
|
+
|
|
185
|
+
**Terminal state has exactly one writer.** `fire_terminal_flips` in
|
|
186
|
+
`specfuse/loop/loop.py` owns the gate → `passed`, roadmap row → `done`, PLAN.md →
|
|
187
|
+
`done`, and auto-archive transitions. Every close path — dispatched, auto-closed,
|
|
188
|
+
and out-of-band re-check — calls that one function; no skill and no agent writes
|
|
189
|
+
those surfaces. Issue #49 exists because two paths once diverged, and
|
|
190
|
+
`[FEAT-2026-0023/G1-CLOSE]` is the rule that came out of it: do **not** add a "flip
|
|
191
|
+
PLAN.md to done" acceptance criterion to a close WU, and do not let a skill
|
|
192
|
+
hand-edit a terminal surface — if a new path needs the flip, give the driver an
|
|
193
|
+
entry point and call it.
|
|
194
|
+
|
|
162
195
|
**Predicate-version transparency** — every `auto_close_decision` event in
|
|
163
196
|
`events.jsonl` carries a `predicate_version` field (e.g., `predicate_version:
|
|
164
197
|
v1`). Future revisions to the predicate constants increment this version, so
|
|
@@ -176,16 +209,50 @@ the field-by-field schema see
|
|
|
176
209
|
§ "Event payload shape — `attempt_outcome` v1". The full payload is
|
|
177
210
|
not restated here (one fact, one home).
|
|
178
211
|
|
|
179
|
-
`outcome` taxonomy
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
212
|
+
`outcome` taxonomy, as `loop.py` actually emits it — eleven values,
|
|
213
|
+
bound to the emitter by `tests/test_attempt_outcome_contract.py`:
|
|
214
|
+
|
|
215
|
+
| outcome | meaning |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `passed` | verify + all driver-side guards clean |
|
|
218
|
+
| `failed` | a verification gate failed |
|
|
219
|
+
| `blocked` | the agent reported `status: blocked` |
|
|
220
|
+
| `zero_token_skip` | the session produced no tokens; nothing ran |
|
|
221
|
+
| `files_changed_mismatch` | RESULT declared paths that show no diff |
|
|
222
|
+
| `closing_deliverable_missing` | a closing-WU guard refused (see `close-discipline.md` §4) |
|
|
223
|
+
| `deliverable_missing` | a declared `produces:` path is absent |
|
|
224
|
+
| `no_deliverable_files` | the squash names only the WU file / events |
|
|
225
|
+
| `produces_not_in_diff` | `produces:` path exists but is not in the squash |
|
|
226
|
+
| `squash_commit_failed` | `git commit` for the squash was rejected |
|
|
227
|
+
| `smoke_import_failed` | a declared smoke-import line failed post-squash |
|
|
228
|
+
|
|
229
|
+
Extending it is a breaking change for every consumer below and requires a
|
|
230
|
+
deliberate versioning decision — **and an update here in the same commit.**
|
|
231
|
+
Five of the values above shipped without that step, and the drift was
|
|
232
|
+
found only when someone mined the corpus (#270).
|
|
183
233
|
|
|
184
234
|
`failure_class` taxonomy is **locked at v1**: `tests | lint |
|
|
185
235
|
security | coverage | symbol_existence | bandit | other | null`.
|
|
186
236
|
`failure_class: other` is the explicit catch-all for paths not yet
|
|
187
237
|
classified; `null` means the outcome was `passed` (no failure).
|
|
188
238
|
|
|
239
|
+
**Where the diagnostic lives depends on the outcome.** This is the part that
|
|
240
|
+
misleads readers of the "standardized set" above: `failure_class` /
|
|
241
|
+
`failure_excerpt` are not populated on every non-`passed` outcome, and a
|
|
242
|
+
consumer that queries only those concludes the record is empty when it is not.
|
|
243
|
+
|
|
244
|
+
| outcome | carries its reason in |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `failed`, `files_changed_mismatch`, `produces_not_in_diff` | `failure_class` + `failure_signature` + `failure_excerpt` |
|
|
247
|
+
| `blocked` | **`agent_blocked_reason`** (plus a sibling `human_escalation` event) |
|
|
248
|
+
| `closing_deliverable_missing`, `no_deliverable_files`, `deliverable_missing`, `squash_commit_failed` | **`summary`** |
|
|
249
|
+
| `files_changed_mismatch` (pre-0.3.23) | **`unchanged_paths`** only — `failure_*` was added by #182 |
|
|
250
|
+
| `zero_token_skip` | nothing, correctly — no attempt ran |
|
|
251
|
+
|
|
252
|
+
**Read every field in that table before concluding a record is undiagnosable.**
|
|
253
|
+
A cross-repo audit reported three separate "missing diagnostic" findings that
|
|
254
|
+
were all query errors against this contract, none of which existed (#270).
|
|
255
|
+
|
|
189
256
|
Consumers that read `attempt_outcome` events (the auto-close
|
|
190
257
|
predicate, `/gate-status`, the spinning-detector hook, close-ceremony
|
|
191
258
|
cost analysis) treat the `outcome` and `failure_class` values as an
|
|
@@ -66,6 +66,11 @@ roadmap ──/pick-feature──▶ active ──/draft-feature──▶ gate 1
|
|
|
66
66
|
- **`/derive-verification`** — draft a `.specfuse/verification.yml` for a project
|
|
67
67
|
by inspecting its CI, tooling manifests, and code. Run this once when
|
|
68
68
|
bootstrapping the loop in a repo that already has CI worth deriving gates from.
|
|
69
|
+
- **`/derive-monitoring`** — draft a `.specfuse/monitoring.yml` (plus a local
|
|
70
|
+
overrides file and a filled secrets checklist) for a project by discovering
|
|
71
|
+
deployed components from repo evidence and auditing them against the
|
|
72
|
+
design-for-diagnosis rule. Run this once a project has real components to
|
|
73
|
+
monitor.
|
|
69
74
|
|
|
70
75
|
### 3. Run — the driver (not a skill)
|
|
71
76
|
|
|
@@ -73,6 +78,13 @@ roadmap ──/pick-feature──▶ active ──/draft-feature──▶ gate 1
|
|
|
73
78
|
WU as a fresh session, verifies, and commits. It is a command, not a skill. It
|
|
74
79
|
either auto-closes a clean gate or halts at the gate boundary for review.
|
|
75
80
|
|
|
81
|
+
It also owns every terminal flip (gate → `passed`, roadmap row → `done`, PLAN.md →
|
|
82
|
+
`done`, auto-archive) — no skill writes those surfaces. When a verdict is upgraded
|
|
83
|
+
*after* its close WU is already `done`, re-fire them with
|
|
84
|
+
`specfuse-loop --recheck-verdict <FEATURE_ID>`: it re-reads the terminal close WU's
|
|
85
|
+
verdict from disk and flips only if it now permits, printing why when it does not.
|
|
86
|
+
Safe to run on an already-`done` or still-hedged feature — it writes nothing.
|
|
87
|
+
|
|
76
88
|
### 4. Arm — the human checkpoint at each gate
|
|
77
89
|
|
|
78
90
|
- **`/arm-gate`** — at a gate boundary (driver halted with `awaiting_review`, next
|
|
@@ -96,9 +108,21 @@ either auto-closes a clean gate or halts at the gate boundary for review.
|
|
|
96
108
|
|
|
97
109
|
### 6. Wrap — finish a done feature
|
|
98
110
|
|
|
111
|
+
- **`/accept-hedged-close`** — the path out of `/wrap-feature`'s refusal. A close
|
|
112
|
+
that legitimately ends `verdict: met_locally` (or `partially_met`) leaves every
|
|
113
|
+
terminal surface un-flipped by design, and for some features that hedge is the
|
|
114
|
+
ceiling *by construction* — the criterion's oracle lives outside the repo. This
|
|
115
|
+
skill quotes the standing follow-up record, requires a one-line reason and
|
|
116
|
+
explicit acknowledgment of every open item, writes an acceptance record into
|
|
117
|
+
`RETROSPECTIVE.md`, and then fires the flips through the driver's
|
|
118
|
+
`--recheck-verdict` primitive. It carries the follow-ups forward — accepting a
|
|
119
|
+
hedge is shipping with known-open items, not closing them. Refuses on `met`
|
|
120
|
+
(nothing to accept), on `not_met`, and on a close WU that isn't `done`. It never
|
|
121
|
+
writes PLAN.md's status, a gate's status, or the roadmap row.
|
|
99
122
|
- **`/wrap-feature`** — after the terminal gate is `done`, push the feature
|
|
100
123
|
branch, open a PR, optionally watch CI, and point at the next pick. Refuses if
|
|
101
|
-
PLAN.md isn't `done` yet
|
|
124
|
+
PLAN.md isn't `done` yet — run `/accept-hedged-close` first if the block is a
|
|
125
|
+
standing hedged verdict.
|
|
102
126
|
- **`/roadmap-archive`** — move a done or abandoned feature's detail section from
|
|
103
127
|
`roadmap.md` to `roadmap-archive.md`, leaving a back-link.
|
|
104
128
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Monitoring secrets checklist
|
|
2
|
+
|
|
3
|
+
Merge and execution semantics land in FEAT-2026-0040 (`specfuse-monitor run`).
|
|
4
|
+
This file is a shape declaration only: the list of environment-variable
|
|
5
|
+
**names** an operator must export before running monitoring checks locally.
|
|
6
|
+
It names variables, never values — it is safe to commit and safe to read.
|
|
7
|
+
|
|
8
|
+
Pair this checklist with `.specfuse/monitoring.overrides.yml` (copied from
|
|
9
|
+
`monitoring.overrides.yml.example`): every `credentials.*` value in that file
|
|
10
|
+
is one of the names below.
|
|
11
|
+
|
|
12
|
+
## Required environment variables
|
|
13
|
+
|
|
14
|
+
- `ACME_TELEMETRY_STAGING_API_KEY` — Acme Telemetry console, staging
|
|
15
|
+
workspace, API keys page.
|
|
16
|
+
- `ACME_BROKER_STAGING_CONNECTION_STRING` — Acme Broker console, staging
|
|
17
|
+
namespace, connection strings page (read-only credential).
|
|
18
|
+
- `ACME_TELEMETRY_PROD_API_KEY` — Acme Telemetry console, production
|
|
19
|
+
workspace, API keys page.
|
|
20
|
+
- `ACME_BROKER_PROD_CONNECTION_STRING` — Acme Broker console, production
|
|
21
|
+
namespace, connection strings page (read-only credential).
|
|
22
|
+
|
|
23
|
+
## How to use this checklist
|
|
24
|
+
|
|
25
|
+
1. Export each name above on the machine that will run
|
|
26
|
+
`specfuse-monitor run --dry-run` (FEAT-2026-0040), using whatever secret
|
|
27
|
+
store your shell profile or process manager already uses.
|
|
28
|
+
2. Never write a value for any of these names into a tracked file, a commit
|
|
29
|
+
message, or this checklist itself — only the name belongs here.
|
|
30
|
+
3. If a name above is missing on your machine, obtain the value from the
|
|
31
|
+
console page listed next to it; do not invent or reuse a value from
|
|
32
|
+
another environment.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# .specfuse/monitoring.overrides.yml.example
|
|
2
|
+
#
|
|
3
|
+
# Machine-local override slice for monitoring.yml — which environment a local
|
|
4
|
+
# run targets, the credential ENV-VAR NAMES available on this machine, and
|
|
5
|
+
# `runner: local` forced on every component. Copy to
|
|
6
|
+
# `.specfuse/monitoring.overrides.yml` (gitignored — see
|
|
7
|
+
# .specfuse/gitignore.snippet) and edit for your own machine.
|
|
8
|
+
#
|
|
9
|
+
# MERGE AND EXECUTION SEMANTICS LAND IN FEAT-2026-0040. This file, like
|
|
10
|
+
# monitoring.yml.example, is a shape declaration only: valid, inspectable, and
|
|
11
|
+
# machine-checkable standing alone. It does not itself merge into
|
|
12
|
+
# monitoring.yml or invoke anything — that is `specfuse-monitor run`'s job,
|
|
13
|
+
# a CLI FEAT-2026-0040 ships.
|
|
14
|
+
#
|
|
15
|
+
# SAME SCHEMA AS monitoring.yml.example, unchanged — validated by
|
|
16
|
+
# `specfuse/loop/lint_monitoring.py` (`validate_monitoring`). One monitoring
|
|
17
|
+
# schema, not two. See docs/concepts/monitoring-schema.md.
|
|
18
|
+
#
|
|
19
|
+
# SECURITY POSTURE: identical to monitoring.yml.example. Every credential
|
|
20
|
+
# value below is an ENVIRONMENT-VARIABLE NAME (UPPER_SNAKE_CASE), never a
|
|
21
|
+
# literal secret — see .specfuse/monitoring-secrets-checklist.md for where to
|
|
22
|
+
# obtain each value. Every organization name, host, workspace ID, and queue
|
|
23
|
+
# name here is an obvious placeholder.
|
|
24
|
+
|
|
25
|
+
# --------------------------------------------------------------------------
|
|
26
|
+
# environments
|
|
27
|
+
#
|
|
28
|
+
# The machine-local slice usually names only the one environment this
|
|
29
|
+
# machine runs checks against locally (here: staging).
|
|
30
|
+
# --------------------------------------------------------------------------
|
|
31
|
+
environments:
|
|
32
|
+
staging:
|
|
33
|
+
telemetry:
|
|
34
|
+
provider: acme-telemetry
|
|
35
|
+
credentials:
|
|
36
|
+
api_key: ACME_TELEMETRY_STAGING_API_KEY
|
|
37
|
+
broker:
|
|
38
|
+
provider: acme-broker
|
|
39
|
+
credentials:
|
|
40
|
+
connection_string: ACME_BROKER_STAGING_CONNECTION_STRING
|
|
41
|
+
|
|
42
|
+
# --------------------------------------------------------------------------
|
|
43
|
+
# components
|
|
44
|
+
#
|
|
45
|
+
# Every component's `runner` is forced to `local` in this override slice —
|
|
46
|
+
# that is the property that makes it a *local-runner* bootstrap artifact.
|
|
47
|
+
# `diagnose`/`autofix` stay at the same conservative defaults as
|
|
48
|
+
# monitoring.yml.example.
|
|
49
|
+
# --------------------------------------------------------------------------
|
|
50
|
+
components:
|
|
51
|
+
- name: web-api
|
|
52
|
+
type: http-service
|
|
53
|
+
runner: local
|
|
54
|
+
diagnose: manual
|
|
55
|
+
autofix: "off"
|
|
56
|
+
checks:
|
|
57
|
+
- type: heartbeat
|
|
58
|
+
- type: error-logs
|
|
59
|
+
|
|
60
|
+
- name: order-worker
|
|
61
|
+
type: queue-consumer
|
|
62
|
+
runner: local
|
|
63
|
+
diagnose: manual
|
|
64
|
+
autofix: "off"
|
|
65
|
+
checks:
|
|
66
|
+
- type: dlq
|
|
67
|
+
harvest_mode: peek
|
|
68
|
+
targets:
|
|
69
|
+
- subscription: acme-orders-dlq-sub
|
|
70
|
+
function: ProcessOrder
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# .specfuse/monitoring.yml.example
|
|
2
|
+
#
|
|
3
|
+
# Declares how THIS project notices that a deployed component is misbehaving,
|
|
4
|
+
# the way verification.yml declares how a change is proven correct before it
|
|
5
|
+
# merges. Copy this file to `.specfuse/monitoring.yml` and edit it for your
|
|
6
|
+
# real environments and components to opt in.
|
|
7
|
+
#
|
|
8
|
+
# MONITORING IS OPT-IN. An absent `.specfuse/monitoring.yml` is a correct,
|
|
9
|
+
# valid final state — it is not an error, and the shipped gate does not fail
|
|
10
|
+
# on a missing file. Only add this file once you have real components to
|
|
11
|
+
# watch.
|
|
12
|
+
#
|
|
13
|
+
# This file is validated structurally by `specfuse/loop/lint_monitoring.py`
|
|
14
|
+
# (`validate_monitoring`). Schema reference: docs/concepts/monitoring-schema.md
|
|
15
|
+
#
|
|
16
|
+
# SECURITY POSTURE: every credential-bearing value below is an
|
|
17
|
+
# ENVIRONMENT-VARIABLE NAME (UPPER_SNAKE_CASE or the Section__Key form used by
|
|
18
|
+
# .NET, Spring, and other hierarchical-config stacks), never a literal secret. The
|
|
19
|
+
# validator rejects inline literals. Every organization name, host, workspace
|
|
20
|
+
# ID, and queue name here is an obvious placeholder — replace them with your
|
|
21
|
+
# own before use, but never with a literal credential value.
|
|
22
|
+
|
|
23
|
+
# --------------------------------------------------------------------------
|
|
24
|
+
# environments
|
|
25
|
+
#
|
|
26
|
+
# One entry per deploy target (staging, production, ...). Each environment
|
|
27
|
+
# names its telemetry and broker provider bindings. `provider` is an OPAQUE
|
|
28
|
+
# STRING this layer does not interpret — FEAT-2026-0040's harvester CLI reads
|
|
29
|
+
# it and dispatches to a provider-specific adapter. Adding a new provider
|
|
30
|
+
# never requires a change here or in the validator.
|
|
31
|
+
# --------------------------------------------------------------------------
|
|
32
|
+
environments:
|
|
33
|
+
staging:
|
|
34
|
+
telemetry:
|
|
35
|
+
provider: acme-telemetry
|
|
36
|
+
credentials:
|
|
37
|
+
api_key: ACME_TELEMETRY_STAGING_API_KEY
|
|
38
|
+
broker:
|
|
39
|
+
provider: acme-broker
|
|
40
|
+
credentials:
|
|
41
|
+
connection_string: ACME_BROKER_STAGING_CONNECTION_STRING
|
|
42
|
+
|
|
43
|
+
production:
|
|
44
|
+
telemetry:
|
|
45
|
+
provider: acme-telemetry
|
|
46
|
+
credentials:
|
|
47
|
+
api_key: ACME_TELEMETRY_PROD_API_KEY
|
|
48
|
+
broker:
|
|
49
|
+
provider: acme-broker
|
|
50
|
+
credentials:
|
|
51
|
+
connection_string: ACME_BROKER_PROD_CONNECTION_STRING
|
|
52
|
+
|
|
53
|
+
# --------------------------------------------------------------------------
|
|
54
|
+
# components
|
|
55
|
+
#
|
|
56
|
+
# One entry per deployable unit. Two are shown here so the per-component
|
|
57
|
+
# dial model is visible rather than implied: `web-api` is HTTP-serving,
|
|
58
|
+
# `order-worker` is message-consuming. Each component carries the same five
|
|
59
|
+
# required fields and the same three dials, regardless of its shape.
|
|
60
|
+
#
|
|
61
|
+
# Dials, all per component:
|
|
62
|
+
# runner — where diagnosis/autofix executes for this component.
|
|
63
|
+
# one of: local | gh-actions | in-cluster
|
|
64
|
+
# diagnose — whether a harvested finding is investigated automatically.
|
|
65
|
+
# one of: manual | auto
|
|
66
|
+
# autofix — whether a diagnosed finding may trigger an automated fix.
|
|
67
|
+
# one of: "off" | "on" (quoted: bare `off` is not valid YAML
|
|
68
|
+
# here — see _miniyaml's rejection of the "off"/"on" spellings)
|
|
69
|
+
#
|
|
70
|
+
# Start every component at runner: local, diagnose: manual, autofix: "off".
|
|
71
|
+
# Loosen one dial at a time as you build confidence in the checks below.
|
|
72
|
+
# --------------------------------------------------------------------------
|
|
73
|
+
components:
|
|
74
|
+
- name: web-api
|
|
75
|
+
type: http-service
|
|
76
|
+
runner: local
|
|
77
|
+
diagnose: manual
|
|
78
|
+
autofix: "off"
|
|
79
|
+
checks:
|
|
80
|
+
# http-5xx: alerts when the rolling 5xx rate crosses a threshold.
|
|
81
|
+
# Use for any component that serves HTTP traffic directly.
|
|
82
|
+
- type: http-5xx
|
|
83
|
+
|
|
84
|
+
# heartbeat: alerts when the component stops reporting in at all —
|
|
85
|
+
# the check that catches "it didn't error, it just went silent."
|
|
86
|
+
- type: heartbeat
|
|
87
|
+
|
|
88
|
+
# error-logs: scans structured application logs for error-level
|
|
89
|
+
# entries. Cheapest check to add; pairs with the design-for-diagnosis
|
|
90
|
+
# rule's structured-logging requirement.
|
|
91
|
+
- type: error-logs
|
|
92
|
+
|
|
93
|
+
- name: order-worker
|
|
94
|
+
type: queue-consumer
|
|
95
|
+
runner: local
|
|
96
|
+
diagnose: manual
|
|
97
|
+
autofix: "off"
|
|
98
|
+
checks:
|
|
99
|
+
# dlq: alerts when messages land in this component's dead-letter
|
|
100
|
+
# queue. harvest_mode controls whether the harvester CLI (0040) only
|
|
101
|
+
# reads DLQ entries (peek) or removes them from the queue after
|
|
102
|
+
# reading (quarantine) — start with peek until you trust the pipeline.
|
|
103
|
+
- type: dlq
|
|
104
|
+
harvest_mode: peek
|
|
105
|
+
targets:
|
|
106
|
+
- subscription: acme-orders-dlq-sub
|
|
107
|
+
function: ProcessOrder
|
|
108
|
+
|
|
109
|
+
# invariant: runs an operator-supplied query against this component's
|
|
110
|
+
# data and alerts when it returns unexpected rows. `query` is opaque
|
|
111
|
+
# to this schema (it is not parsed or executed here); `fingerprint_by`
|
|
112
|
+
# names the column used to dedupe repeat findings into one issue.
|
|
113
|
+
- type: invariant
|
|
114
|
+
query: "select count(*) from orders where fulfilled_at is null and created_at < now() - interval '1 day'"
|
|
115
|
+
fingerprint_by: order_id
|
|
116
|
+
|
|
117
|
+
- type: heartbeat
|
|
118
|
+
|
|
119
|
+
# acme-functions-host: the motivating shape this feature exists for. One
|
|
120
|
+
# deployable, many triggers — 3 queue subscriptions and 2 timer schedules,
|
|
121
|
+
# all running inside one process. `cloud_RoleName` is per-process, so this
|
|
122
|
+
# is ONE component (one role name, one set of dials), not five. Each
|
|
123
|
+
# trigger gets its own `targets[]` entry instead of its own component: a
|
|
124
|
+
# `dlq` check enumerates per subscription, a `heartbeat` check enumerates
|
|
125
|
+
# per schedule, and both stay attributable without duplicating the
|
|
126
|
+
# component (and its role-name-keyed error-logs/heartbeat query) once per
|
|
127
|
+
# trigger. See docs/concepts/monitoring-schema.md#check-targets.
|
|
128
|
+
- name: acme-functions-host
|
|
129
|
+
type: multi-trigger-host
|
|
130
|
+
runner: local
|
|
131
|
+
diagnose: manual
|
|
132
|
+
autofix: "off"
|
|
133
|
+
checks:
|
|
134
|
+
# One dlq check, many targets — not one dlq check per subscription.
|
|
135
|
+
# `subscription` is what the harvester queries; `function` is what a
|
|
136
|
+
# human diagnoses by, since a subscription name alone rarely tells an
|
|
137
|
+
# on-call engineer which handler failed.
|
|
138
|
+
- type: dlq
|
|
139
|
+
harvest_mode: peek
|
|
140
|
+
targets:
|
|
141
|
+
- subscription: acme-orders-created-sub
|
|
142
|
+
function: ProcessOrderCreated
|
|
143
|
+
- subscription: acme-orders-cancelled-sub
|
|
144
|
+
function: ProcessOrderCancelled
|
|
145
|
+
- subscription: acme-inventory-sync-sub
|
|
146
|
+
function: SyncInventoryLevels
|
|
147
|
+
|
|
148
|
+
# One heartbeat check, many targets — one per schedule, so a single
|
|
149
|
+
# silent timer among several is still individually visible instead of
|
|
150
|
+
# being averaged away by a single host-wide heartbeat.
|
|
151
|
+
- type: heartbeat
|
|
152
|
+
targets:
|
|
153
|
+
- name: nightly-reconciliation
|
|
154
|
+
cron: "0 2 * * *"
|
|
155
|
+
timezone: Etc/UTC
|
|
156
|
+
- name: hourly-cache-warm
|
|
157
|
+
cron: "0 * * * *"
|
|
158
|
+
timezone: Etc/UTC
|
|
159
|
+
|
|
160
|
+
# queue-stalled: catches a consumer that has stopped consuming — no
|
|
161
|
+
# message failed (so `dlq` sees nothing), the host is still alive (so
|
|
162
|
+
# `heartbeat` sees nothing), and queue depth is a broker coordinate, not
|
|
163
|
+
# a telemetry query (so `invariant` can't see it either). Same
|
|
164
|
+
# subscription/function coordinates as `dlq`; the stall-threshold value
|
|
165
|
+
# is opaque here, exactly like `invariant.query`.
|
|
166
|
+
- type: queue-stalled
|
|
167
|
+
targets:
|
|
168
|
+
- subscription: acme-orders-created-sub
|
|
169
|
+
function: ProcessOrderCreated
|
|
170
|
+
stall_after: 15m
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright 2026 Specfuse Contributors
|
|
3
|
+
Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Rule: close discipline
|
|
7
|
+
|
|
8
|
+
Three obligations for `close` and `close-intermediate` WUs, binding at close
|
|
9
|
+
time. Sibling of [`planning-discipline.md`](planning-discipline.md) (plan/arm
|
|
10
|
+
time) and of the per-WU closing obligations in
|
|
11
|
+
[`result-contract.md`](result-contract.md). Provenance: FEAT-2026-0049
|
|
12
|
+
(the specfuse-generator dogfood), where three gates auto-closed at
|
|
13
|
+
`attempts: 0` and the one close forced to run caught two false-`done` WUs and
|
|
14
|
+
an unsatisfiable predicate — 48% of the feature's spend landed after the
|
|
15
|
+
terminal gate had already "auto-closed". A close that only writes prose
|
|
16
|
+
verifies nothing.
|
|
17
|
+
|
|
18
|
+
A close whose acceptance criteria include ANY obligation below is
|
|
19
|
+
**load-bearing**: the plan author sets `auto_close_disabled: true` in that
|
|
20
|
+
WU's frontmatter so the auto-close predicate cannot optimize it away (#189).
|
|
21
|
+
|
|
22
|
+
## 1. Oracles re-run fresh
|
|
23
|
+
|
|
24
|
+
The close re-runs every oracle the feature's acceptance criteria name — the
|
|
25
|
+
full test command(s), plus any compile/execution gates — fresh, in this
|
|
26
|
+
session, exit codes read directly. Never inherit a producing WU's
|
|
27
|
+
self-report: `done` is a claim, the re-run is the verification. When the
|
|
28
|
+
feature's criteria assert on generated artifacts, regenerate into a clean
|
|
29
|
+
output directory before asserting — stale output satisfies any assertion.
|
|
30
|
+
|
|
31
|
+
> **Provenance.** A WU reported `done` while its source was untouched and its
|
|
32
|
+
> oracle never ran; the driver-side produces-vs-diff guard now refuses that
|
|
33
|
+
> pass (specfuse-loop >= 0.3.21), but only the close's own fresh re-run
|
|
34
|
+
> catches the composite: all WUs individually green while the feature-level
|
|
35
|
+
> oracle fails.
|
|
36
|
+
|
|
37
|
+
## 2. Hedged-verdict follow-up record
|
|
38
|
+
|
|
39
|
+
On `met_locally`, the close must produce a named record — in the gate review
|
|
40
|
+
or `RETROSPECTIVE.md` — with one entry per unmet criterion:
|
|
41
|
+
|
|
42
|
+
- the criterion, verbatim;
|
|
43
|
+
- why it is unverifiable in this environment;
|
|
44
|
+
- the exact re-run condition that would upgrade the verdict to `met`.
|
|
45
|
+
|
|
46
|
+
The driver (>= 0.3.21) already guarantees the surfaces stay un-flipped on a
|
|
47
|
+
hedged verdict (gate `awaiting_review`, roadmap `active`, PLAN `active`);
|
|
48
|
+
this record is the other half: without it, `met_locally` is a dead end —
|
|
49
|
+
no artifact says what would make it `met`, and the honest hedged verdict
|
|
50
|
+
degrades into a polite synonym for "unknown".
|
|
51
|
+
|
|
52
|
+
## 3. Consumer-visible contract changes enumerated, human-acknowledged
|
|
53
|
+
|
|
54
|
+
The close enumerates every consumer-visible addition, removal, or rename the
|
|
55
|
+
feature makes across ALL its producing WUs — API surface, generated models,
|
|
56
|
+
published schemas, CLI flags, whatever contract consumers depend on — and
|
|
57
|
+
blocks on explicit human acknowledgment of the list. A feature with no such
|
|
58
|
+
changes writes exactly: `n/a — no consumer-visible contract change` (do not
|
|
59
|
+
fabricate an empty enumeration; the n/a line is the reviewed claim).
|
|
60
|
+
|
|
61
|
+
A close carrying this obligation is always load-bearing
|
|
62
|
+
(`auto_close_disabled: true`).
|
|
63
|
+
|
|
64
|
+
> **Provenance.** A breaking removal from a generated model survived four
|
|
65
|
+
> gates unreviewed because no close surface owned the breaking-change list.
|
|
66
|
+
> A silent breaking change is the most expensive false-done, and it is not
|
|
67
|
+
> language-specific — any generated or published contract has this surface.
|
|
68
|
+
|
|
69
|
+
## 4. What the driver checks — the exact strings
|
|
70
|
+
|
|
71
|
+
Sections 1–3 are obligations you reason about. **This section is a format
|
|
72
|
+
contract you must match literally.** The driver refuses a closing WU that
|
|
73
|
+
produces correct content in the wrong shape, and it refuses it *after* the WU
|
|
74
|
+
has run — so a mismatch costs a full re-dispatch, not a re-arm.
|
|
75
|
+
|
|
76
|
+
That is not hypothetical. Across 158 closing WUs in 9 repositories, **28% of all
|
|
77
|
+
closing-WU spend was burned on attempts the driver refused**, and three guards
|
|
78
|
+
whose requirements appeared in no authoring surface accounted for **45% of that
|
|
79
|
+
waste**. This table exists so that number goes down.
|
|
80
|
+
|
|
81
|
+
| Guard | Applies to | What it requires, exactly |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `assert_retrospective_exists` | `close`, `close-intermediate` | `RETROSPECTIVE.md` exists in the feature dir and is non-empty |
|
|
84
|
+
| `assert_retrospective_gate_section` | `close-intermediate` | A heading matching `^#{1,3} Gate <N>` — i.e. `## Gate 1`, for the gate being closed. Not "Gate one", not a bare bold line |
|
|
85
|
+
| `assert_cost_analysis_section_when_met` | `close` with `verdict: met` | A heading matching `^##+ Cost analysis` (case-insensitive) — `## Cost analysis` is the conventional spelling |
|
|
86
|
+
| `assert_failure_class_breakdown_when_failures_present` | `close`, `close-intermediate`, when the gate had ≥1 failed attempt | A literal `### Failure-class breakdown` heading — three hashes, not two |
|
|
87
|
+
| `assert_learnings_appended_or_noop` | `close`, `close-intermediate` | Either ≥1 added line to `.specfuse/LEARNINGS.md` in this WU's squash, **or** the exact phrase `nothing generalizes` (case-insensitive) somewhere in `RETROSPECTIVE.md` |
|
|
88
|
+
| `assert_doc_or_roadmap_diff` | `close`, `close-intermediate` | The squash touches at least one of: `docs/*`, `.specfuse/roadmap.md`, `.specfuse/LEARNINGS.md`, or any `RETROSPECTIVE.md` |
|
|
89
|
+
| `assert_verdict_well_formed` | `close` | A `verdict:` **frontmatter** field on the WU file, one of `met`, `met_locally`, `partially_met`, `not_met`. Not in the body |
|
|
90
|
+
| `assert_gate_review_exists` | `plan-next` | **`GATE-{N+1}-REVIEW.md`** — named for the gate being *drafted*, not the gate being closed. A gate-1 `plan-next` writes `GATE-02-REVIEW.md` |
|
|
91
|
+
| `assert_next_gate_drafted_or_terminal` | `plan-next` | The next gate has ≥1 WU at `status: draft` in `PLAN.md`, or the feature is terminal |
|
|
92
|
+
| `assert_declared_deliverables` | any WU with `produces:` | Every path listed in `produces:` appears in the squash diff |
|
|
93
|
+
| `assert_autoclose_debt_reconciled` | `close` | On a `close` WU, if `RETROSPECTIVE.md` carries T06's `<!-- specfuse:autoclose-debt gate=N ... -->` marker for a gate earlier than the terminal gate, the terminal close's `## What the loop did NOT verify` section must name that gate literally as `gate N`. Marker-gated (fires on none of this repo's pre-FEAT-2026-0070 closes); short-circuits `(True, "")` when the terminal close WU is itself `auto_close: true` |
|
|
94
|
+
|
|
95
|
+
**The `GATE-{N+1}` row is the single most expensive guard in the system**
|
|
96
|
+
($53.11 of measured waste across 15 refusals) and the one most likely to
|
|
97
|
+
surprise: the review artifact is named for the gate it *arms*, because that is
|
|
98
|
+
the gate a human reads it to review. See issue #261.
|
|
99
|
+
|
|
100
|
+
**If a closing WU retries, read this table before assuming the work was hard.**
|
|
101
|
+
By measured cost, format mismatches are the more likely explanation.
|
|
102
|
+
|
|
103
|
+
> **Provenance (issue #265).** Guard requirements are enforced in
|
|
104
|
+
> `specfuse/loop/loop.py`'s `assert_*` functions and were, until this section
|
|
105
|
+
> existed, discoverable only by reading them or by paying for a refusal.
|
|
106
|
+
> `tests/test_closing_guard_contracts.py` binds this table to those functions'
|
|
107
|
+
> own source, so a guard that changes its literal fails a test rather than
|
|
108
|
+
> silently invalidating this documentation.
|
|
109
|
+
|
|
110
|
+
## Split with project-local rules
|
|
111
|
+
|
|
112
|
+
These are the generic obligations. The concrete grounding — which command is
|
|
113
|
+
the oracle, which surface is "the API", project-specific regeneration
|
|
114
|
+
hazards — is per-project and belongs in that project's
|
|
115
|
+
`.specfuse/rules-local/` (never touched by upgrade), referencing this rule.
|