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.
Files changed (214) hide show
  1. {specfuse_loop-0.4.0/specfuse_loop.egg-info → specfuse_loop-0.6.0}/PKG-INFO +1 -1
  2. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/pyproject.toml +3 -1
  3. specfuse_loop-0.6.0/specfuse/loop/_wu_sections.py +44 -0
  4. specfuse_loop-0.6.0/specfuse/loop/data/VERSION +1 -0
  5. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/methodology.md +71 -4
  6. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/skills.md +25 -1
  7. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/gitignore.snippet +1 -0
  8. specfuse_loop-0.6.0/specfuse/loop/data/monitoring-secrets-checklist.md +32 -0
  9. specfuse_loop-0.6.0/specfuse/loop/data/monitoring.overrides.yml.example +70 -0
  10. specfuse_loop-0.6.0/specfuse/loop/data/monitoring.yml.example +170 -0
  11. specfuse_loop-0.6.0/specfuse/loop/data/rules/close-discipline.md +115 -0
  12. specfuse_loop-0.6.0/specfuse/loop/data/rules/design-for-diagnosis.md +90 -0
  13. specfuse_loop-0.6.0/specfuse/loop/data/rules/operator-escalation.md +70 -0
  14. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/planning-discipline.md +51 -9
  15. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/templates/WU.template.md +18 -6
  16. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/verification.yml.example +3 -0
  17. specfuse_loop-0.6.0/specfuse/loop/escalation.py +236 -0
  18. specfuse_loop-0.6.0/specfuse/loop/events_stats.py +288 -0
  19. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/gate_eval.py +3 -0
  20. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/gh_features.py +3 -1
  21. specfuse_loop-0.6.0/specfuse/loop/labels.py +159 -0
  22. specfuse_loop-0.6.0/specfuse/loop/lint_monitoring.py +339 -0
  23. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/lint_plan.py +258 -42
  24. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/loop.py +393 -20
  25. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/scaffold.py +47 -2
  26. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0/specfuse_loop.egg-info}/PKG-INFO +1 -1
  27. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse_loop.egg-info/SOURCES.txt +38 -0
  28. specfuse_loop-0.6.0/specfuse_loop.egg-info/entry_points.txt +5 -0
  29. specfuse_loop-0.6.0/tests/test_accept_hedged_close_skill.py +168 -0
  30. specfuse_loop-0.6.0/tests/test_attempt_outcome_contract.py +136 -0
  31. specfuse_loop-0.6.0/tests/test_attention_nonwriting_guard.py +83 -0
  32. specfuse_loop-0.6.0/tests/test_attention_skill_structure.py +75 -0
  33. specfuse_loop-0.6.0/tests/test_autoclose_deferral_visibility.py +244 -0
  34. specfuse_loop-0.6.0/tests/test_bats_suites_gated.py +89 -0
  35. specfuse_loop-0.6.0/tests/test_closing_guard_contracts.py +172 -0
  36. specfuse_loop-0.6.0/tests/test_closing_guard_prediction.py +235 -0
  37. specfuse_loop-0.6.0/tests/test_derive_monitoring_discovery.py +973 -0
  38. specfuse_loop-0.6.0/tests/test_derive_monitoring_skill_registration.py +211 -0
  39. specfuse_loop-0.6.0/tests/test_design_for_diagnosis_rule.py +76 -0
  40. specfuse_loop-0.6.0/tests/test_done_feature_gates.py +124 -0
  41. specfuse_loop-0.6.0/tests/test_escalation_contract.py +133 -0
  42. specfuse_loop-0.6.0/tests/test_escalation_emit.py +116 -0
  43. specfuse_loop-0.6.0/tests/test_events_stats.py +230 -0
  44. specfuse_loop-0.6.0/tests/test_feature_id_resolution.py +127 -0
  45. specfuse_loop-0.6.0/tests/test_gitleaks_pinning.py +258 -0
  46. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_init_integration.py +7 -0
  47. specfuse_loop-0.6.0/tests/test_label_registry.py +52 -0
  48. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_leak_scan.py +92 -11
  49. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lifecycle_integration.py +53 -1
  50. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_bare_produces_path.py +59 -1
  51. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_close_wu.py +4 -1
  52. specfuse_loop-0.6.0/tests/test_lint_monitoring.py +765 -0
  53. specfuse_loop-0.6.0/tests/test_lint_plan_verdict_exempt.py +92 -0
  54. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_post_pass_invariant.py +187 -0
  55. specfuse_loop-0.6.0/tests/test_monitoring_bootstrap_artifacts.py +123 -0
  56. specfuse_loop-0.6.0/tests/test_monitoring_example.py +202 -0
  57. specfuse_loop-0.6.0/tests/test_monitoring_fenced_blocks.py +179 -0
  58. specfuse_loop-0.6.0/tests/test_monitoring_seed.py +65 -0
  59. specfuse_loop-0.6.0/tests/test_operator_escalation_rule.py +165 -0
  60. specfuse_loop-0.6.0/tests/test_planning_cost_floor.py +198 -0
  61. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_produces_in_diff.py +36 -0
  62. specfuse_loop-0.6.0/tests/test_provision_labels.py +208 -0
  63. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_data_in_sync.py +5 -0
  64. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_init.py +6 -0
  65. specfuse_loop-0.6.0/tests/test_scaffold_label_provisioning.py +145 -0
  66. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_resources.py +5 -0
  67. specfuse_loop-0.6.0/tests/test_skill_discovery_links.py +137 -0
  68. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_terminal_flips.py +204 -0
  69. specfuse_loop-0.4.0/specfuse/loop/data/VERSION +0 -1
  70. specfuse_loop-0.4.0/specfuse/loop/data/rules/close-discipline.md +0 -74
  71. specfuse_loop-0.4.0/specfuse_loop.egg-info/entry_points.txt +0 -3
  72. specfuse_loop-0.4.0/tests/test_autoclose_deferral_visibility.py +0 -67
  73. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/LICENSE +0 -0
  74. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/NOTICE +0 -0
  75. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/README.md +0 -0
  76. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/setup.cfg +0 -0
  77. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/__init__.py +0 -0
  78. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/_filelock.py +0 -0
  79. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/_miniyaml.py +0 -0
  80. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/adopt_feature.py +0 -0
  81. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  82. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  83. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  84. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/docs/getting-started.md +0 -0
  85. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/roadmap.template.md +0 -0
  86. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  87. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/never-touch.md +0 -0
  88. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/result-contract.md +0 -0
  89. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  90. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  91. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/rules-local/README.md +0 -0
  92. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/event.schema.json +0 -0
  93. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  94. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  95. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  96. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  97. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/templates/GATE.template.md +0 -0
  98. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  99. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/gh_backend.py +0 -0
  100. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse/loop/validate_event.py +0 -0
  101. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  102. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse_loop.egg-info/requires.txt +0 -0
  103. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/specfuse_loop.egg-info/top_level.txt +0 -0
  104. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_adopt_feature.py +0 -0
  105. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  106. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_attempt_outcome_emission.py +0 -0
  107. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_autosync.py +0 -0
  108. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_autosync_consent.py +0 -0
  109. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_autosync_firstrun.py +0 -0
  110. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_autosync_plugin.py +0 -0
  111. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_backend.py +0 -0
  112. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_baseline_escalation_message.py +0 -0
  113. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_baseline_persistence.py +0 -0
  114. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_baseline_probe.py +0 -0
  115. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_bash_routing.py +0 -0
  116. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  117. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  118. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_bookkeeping_no_verify.py +0 -0
  119. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_claude_resolution.py +0 -0
  120. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_close_wu_self_reference.py +0 -0
  121. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_closing_deliverable_guard.py +0 -0
  122. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_console_encoding.py +0 -0
  123. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_cost_tracking.py +0 -0
  124. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_degraded_oracle.py +0 -0
  125. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_deliverable_presence_gate.py +0 -0
  126. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_doctor.py +0 -0
  127. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_driver_integration.py +0 -0
  128. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_driver_lock.py +0 -0
  129. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_duration_tracking.py +0 -0
  130. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_empty_files_escalation.py +0 -0
  131. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_ensure_feature_branch.py +0 -0
  132. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_ensure_feature_branch_base.py +0 -0
  133. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_events_redaction.py +0 -0
  134. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_extra_gates.py +0 -0
  135. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_filelock_portable.py +0 -0
  136. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_force_full_close.py +0 -0
  137. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gate_eval.py +0 -0
  138. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gate_eval_calibration.py +0 -0
  139. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  140. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gate_eval_terminal_wiring.py +0 -0
  141. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gh_backend.py +0 -0
  142. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_gh_features.py +0 -0
  143. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_git_env_isolation.py +0 -0
  144. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_hashed_denylist.py +0 -0
  145. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_hashed_denylist_ci.py +0 -0
  146. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_interpreter_normalization.py +0 -0
  147. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_leak_findings_redaction.py +0 -0
  148. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_leak_scan_content.py +0 -0
  149. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_learnings_query.py +0 -0
  150. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  151. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lifetime_accounting.py +0 -0
  152. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_close_intermediate.py +0 -0
  153. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_correlation_id.py +0 -0
  154. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  155. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_oracle_env.py +0 -0
  156. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_plan_errors.py +0 -0
  157. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_plan_next_draft.py +0 -0
  158. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_plan_relative_import.py +0 -0
  159. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_planning_sections.py +0 -0
  160. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_produces_driver_helper.py +0 -0
  161. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_sections.py +0 -0
  162. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  163. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_lint_verification_reference.py +0 -0
  164. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_attempt_evidence.py +0 -0
  165. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_auto_archive.py +0 -0
  166. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_caveman_preamble.py +0 -0
  167. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_close_intermediate.py +0 -0
  168. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_defaults_by_type.py +0 -0
  169. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_effort.py +0 -0
  170. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_failure_note_cap.py +0 -0
  171. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_files_changed_guard.py +0 -0
  172. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_gate_budget.py +0 -0
  173. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_model_alias.py +0 -0
  174. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_orchestration.py +0 -0
  175. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_reset_preserving_events.py +0 -0
  176. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_smoke_runner.py +0 -0
  177. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_unsandboxed.py +0 -0
  178. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_loop_zero_token_guard.py +0 -0
  179. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_migrate_legacy.py +0 -0
  180. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_miniyaml_equivalence.py +0 -0
  181. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_miniyaml_negative.py +0 -0
  182. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_planned_cost_lint.py +0 -0
  183. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_prepare_feature.py +0 -0
  184. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_prepare_scaffold_sync.py +0 -0
  185. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_produces_field.py +0 -0
  186. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_redaction_windows_home.py +0 -0
  187. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_resolve_base.py +0 -0
  188. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_result_block.py +0 -0
  189. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_roadmap_add_skill.py +0 -0
  190. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_roadmap_archive_skill.py +0 -0
  191. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_roadmap_row_parser.py +0 -0
  192. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_doc_hygiene.py +0 -0
  193. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_docs.py +0 -0
  194. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_manifest.py +0 -0
  195. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_seed_sanity.py +0 -0
  196. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_upgrade.py +0 -0
  197. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_scaffold_wiring.py +0 -0
  198. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_skills_vendored_in_sync.py +0 -0
  199. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_spinning_rearm_gate.py +0 -0
  200. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_squash_commit_hook_crash.py +0 -0
  201. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_template_closing_shapes.py +0 -0
  202. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_terminal_flip_ownership.py +0 -0
  203. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_timeout_kill_portable.py +0 -0
  204. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_untracked_feature_folder.py +0 -0
  205. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_untracked_file_absorption.py +0 -0
  206. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_upgrade_integration.py +0 -0
  207. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_upgrade_merge_gate.py +0 -0
  208. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_validate_event.py +0 -0
  209. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_verdict_coupling.py +0 -0
  210. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_verify_empty_gate_set.py +0 -0
  211. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_version_consistency.py +0 -0
  212. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_version_skew.py +0 -0
  213. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_windows_gate_exec.py +0 -0
  214. {specfuse_loop-0.4.0 → specfuse_loop-0.6.0}/tests/test_wu_execution_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology.
5
5
  Author: Specfuse contributors
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specfuse-loop"
7
- version = "0.4.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 is **locked at v1**: `passed | failed | blocked |
180
- zero_token | files_changed_mismatch | post_pass_invariant_failed |
181
- closing_deliverable_missing | smoke_import_failed`. Extending the
182
- taxonomy requires a deliberate versioning decision.
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
 
@@ -9,3 +9,4 @@
9
9
  .specfuse/.loop.lock
10
10
  .specfuse/.scratch-*
11
11
  .specfuse/scripts/__pycache__/
12
+ .specfuse/monitoring.overrides.yml
@@ -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.