specfuse-loop 0.3.21__tar.gz → 0.3.23__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 (170) hide show
  1. {specfuse_loop-0.3.21/specfuse_loop.egg-info → specfuse_loop-0.3.23}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.23/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/methodology.md +12 -0
  5. specfuse_loop-0.3.23/specfuse/loop/data/rules/close-discipline.md +74 -0
  6. specfuse_loop-0.3.23/specfuse/loop/data/rules/planning-discipline.md +119 -0
  7. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/result-contract.md +27 -0
  8. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/verification-discipline.md +6 -0
  9. specfuse_loop-0.3.23/specfuse/loop/data/rules-local/README.md +43 -0
  10. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/templates/GATE.template.md +17 -0
  11. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/templates/PLAN.template.md +26 -0
  12. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/templates/WU.template.md +35 -0
  13. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/lint_plan.py +52 -0
  14. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/loop.py +12 -9
  15. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/scaffold.py +84 -3
  16. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23/specfuse_loop.egg-info}/PKG-INFO +1 -1
  17. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/SOURCES.txt +4 -0
  18. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_init_integration.py +5 -0
  19. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_close_intermediate.py +8 -0
  20. specfuse_loop-0.3.23/tests/test_lint_planning_sections.py +117 -0
  21. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_gate_budget.py +28 -4
  22. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_data_in_sync.py +3 -0
  23. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_docs.py +7 -0
  24. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_init.py +5 -0
  25. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_resources.py +3 -0
  26. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_upgrade.py +89 -1
  27. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_upgrade_integration.py +8 -0
  28. specfuse_loop-0.3.21/specfuse/loop/data/VERSION +0 -1
  29. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/LICENSE +0 -0
  30. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/NOTICE +0 -0
  31. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/README.md +0 -0
  32. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/setup.cfg +0 -0
  33. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/__init__.py +0 -0
  34. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/_filelock.py +0 -0
  35. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/_miniyaml.py +0 -0
  36. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/adopt_feature.py +0 -0
  37. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  38. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  39. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  40. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/getting-started.md +0 -0
  41. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/skills.md +0 -0
  42. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/gitignore.snippet +0 -0
  43. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/roadmap.template.md +0 -0
  44. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  45. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/never-touch.md +0 -0
  46. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  47. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/event.schema.json +0 -0
  48. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  49. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  50. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  51. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  52. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/verification.yml.example +0 -0
  53. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/gate_eval.py +0 -0
  54. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/gh_backend.py +0 -0
  55. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/gh_features.py +0 -0
  56. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/validate_event.py +0 -0
  57. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  58. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/entry_points.txt +0 -0
  59. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/requires.txt +0 -0
  60. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/top_level.txt +0 -0
  61. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_adopt_feature.py +0 -0
  62. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  63. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_attempt_outcome_emission.py +0 -0
  64. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autoclose_deferral_visibility.py +0 -0
  65. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autosync.py +0 -0
  66. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autosync_consent.py +0 -0
  67. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autosync_firstrun.py +0 -0
  68. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autosync_plugin.py +0 -0
  69. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_backend.py +0 -0
  70. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_bash_routing.py +0 -0
  71. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  72. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  73. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_bookkeeping_no_verify.py +0 -0
  74. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_claude_resolution.py +0 -0
  75. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_close_wu_self_reference.py +0 -0
  76. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_closing_deliverable_guard.py +0 -0
  77. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_console_encoding.py +0 -0
  78. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_cost_tracking.py +0 -0
  79. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_degraded_oracle.py +0 -0
  80. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_deliverable_presence_gate.py +0 -0
  81. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_doctor.py +0 -0
  82. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_driver_integration.py +0 -0
  83. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_driver_lock.py +0 -0
  84. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_duration_tracking.py +0 -0
  85. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_empty_files_escalation.py +0 -0
  86. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_ensure_feature_branch.py +0 -0
  87. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_ensure_feature_branch_base.py +0 -0
  88. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_events_redaction.py +0 -0
  89. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_extra_gates.py +0 -0
  90. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_filelock_portable.py +0 -0
  91. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_force_full_close.py +0 -0
  92. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gate_eval.py +0 -0
  93. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gate_eval_calibration.py +0 -0
  94. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  95. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gate_eval_terminal_wiring.py +0 -0
  96. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gh_backend.py +0 -0
  97. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gh_features.py +0 -0
  98. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_git_env_isolation.py +0 -0
  99. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_hashed_denylist.py +0 -0
  100. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_hashed_denylist_ci.py +0 -0
  101. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_interpreter_normalization.py +0 -0
  102. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_leak_findings_redaction.py +0 -0
  103. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_leak_scan.py +0 -0
  104. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_leak_scan_content.py +0 -0
  105. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_learnings_query.py +0 -0
  106. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  107. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lifecycle_integration.py +0 -0
  108. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lifetime_accounting.py +0 -0
  109. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_bare_produces_path.py +0 -0
  110. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_close_wu.py +0 -0
  111. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_correlation_id.py +0 -0
  112. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  113. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_oracle_env.py +0 -0
  114. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_plan_errors.py +0 -0
  115. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_plan_next_draft.py +0 -0
  116. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_plan_relative_import.py +0 -0
  117. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_produces_driver_helper.py +0 -0
  118. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_sections.py +0 -0
  119. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  120. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_verification_reference.py +0 -0
  121. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_attempt_evidence.py +0 -0
  122. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_auto_archive.py +0 -0
  123. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_caveman_preamble.py +0 -0
  124. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_close_intermediate.py +0 -0
  125. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_defaults_by_type.py +0 -0
  126. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_effort.py +0 -0
  127. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_failure_note_cap.py +0 -0
  128. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_files_changed_guard.py +0 -0
  129. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_model_alias.py +0 -0
  130. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_orchestration.py +0 -0
  131. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_post_pass_invariant.py +0 -0
  132. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_reset_preserving_events.py +0 -0
  133. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_smoke_runner.py +0 -0
  134. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_unsandboxed.py +0 -0
  135. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_zero_token_guard.py +0 -0
  136. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_migrate_legacy.py +0 -0
  137. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_miniyaml_equivalence.py +0 -0
  138. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_miniyaml_negative.py +0 -0
  139. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_planned_cost_lint.py +0 -0
  140. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_prepare_feature.py +0 -0
  141. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_prepare_scaffold_sync.py +0 -0
  142. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_produces_field.py +0 -0
  143. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_produces_in_diff.py +0 -0
  144. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_redaction_windows_home.py +0 -0
  145. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_resolve_base.py +0 -0
  146. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_result_block.py +0 -0
  147. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_roadmap_add_skill.py +0 -0
  148. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_roadmap_archive_skill.py +0 -0
  149. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_roadmap_row_parser.py +0 -0
  150. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_doc_hygiene.py +0 -0
  151. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_manifest.py +0 -0
  152. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_seed_sanity.py +0 -0
  153. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_wiring.py +0 -0
  154. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_skills_vendored_in_sync.py +0 -0
  155. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_spinning_rearm_gate.py +0 -0
  156. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_squash_commit_hook_crash.py +0 -0
  157. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_template_closing_shapes.py +0 -0
  158. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_terminal_flip_ownership.py +0 -0
  159. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_terminal_flips.py +0 -0
  160. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_timeout_kill_portable.py +0 -0
  161. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_untracked_feature_folder.py +0 -0
  162. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_untracked_file_absorption.py +0 -0
  163. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_upgrade_merge_gate.py +0 -0
  164. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_validate_event.py +0 -0
  165. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_verdict_coupling.py +0 -0
  166. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_verify_empty_gate_set.py +0 -0
  167. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_version_consistency.py +0 -0
  168. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_version_skew.py +0 -0
  169. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_windows_gate_exec.py +0 -0
  170. {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/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.3.21
3
+ Version: 0.3.23
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.3.21"
7
+ version = "0.3.23"
8
8
  description = "Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ 0.3.23
@@ -147,6 +147,18 @@ WU's** frontmatter. The predicate keys on the implementation WUs' cost and
147
147
  plan-conformance and cannot see what a close verifies, so a substantive close
148
148
  must be marked must-run or its verification is silently skipped (#189).
149
149
 
150
+ **Hedged verdicts leave a trail** — a close that ends `met_locally` (everything
151
+ locally verifiable passed; something structurally could not be verified here)
152
+ leaves every terminal surface un-flipped (driver >= 0.3.21: gate
153
+ `awaiting_review`, roadmap row `active`, PLAN.md `active`) AND must produce a
154
+ follow-up record — in the gate review or `RETROSPECTIVE.md` — naming each
155
+ unmet criterion, why it was unverifiable in this environment, and the exact
156
+ re-run condition that would upgrade the verdict to `met`. Without the record,
157
+ `met_locally` is a dead end; with it, the feature is resumable by anyone. See
158
+ `.specfuse/rules/close-discipline.md` §2 for the record's shape and the other
159
+ close-time obligations (fresh oracle re-runs, consumer-visible-change
160
+ enumeration).
161
+
150
162
  **Predicate-version transparency** — every `auto_close_decision` event in
151
163
  `events.jsonl` carries a `predicate_version` field (e.g., `predicate_version:
152
164
  v1`). Future revisions to the predicate constants increment this version, so
@@ -0,0 +1,74 @@
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
+ ## Split with project-local rules
70
+
71
+ These are the generic obligations. The concrete grounding — which command is
72
+ the oracle, which surface is "the API", project-specific regeneration
73
+ hazards — is per-project and belongs in that project's
74
+ `.specfuse/rules-local/` (never touched by upgrade), referencing this rule.
@@ -0,0 +1,119 @@
1
+ <!--
2
+ Copyright 2026 Specfuse Contributors
3
+ Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ -->
5
+
6
+ # Rule: planning discipline
7
+
8
+ Encodes hard-won discipline from feature retrospectives — the checks that, applied
9
+ at design and arming time, would have prevented the most expensive re-plans. Each
10
+ check below is cheap (a grep, a sentence, a table, one local run) and guards a
11
+ failure mode that cost multiple gates when skipped. Provenance is named per check so
12
+ the reasoning survives the rule (the FEAT-2026-0049 / F1–F4 references resolve in the
13
+ retrospective of the specfuse-generator dogfood feature that motivated these checks).
14
+
15
+ These are **binding at draft, plan-next, and arm time**. A gate armed in violation
16
+ of any check is not ready, regardless of what a WU's frontmatter says.
17
+
18
+ ## 1. Existing-mechanism search — before designing any enforcement or measurement
19
+
20
+ Before an ADR designs a validation rule, a severity level, an enforcement gate, or a
21
+ measurement, it must first establish that the mechanism does not already exist. The
22
+ ADR is **incomplete** without this section.
23
+
24
+ - **Grep the inventory.** Search the directory that holds the mechanism's
25
+ siblings — for a validation rule, the project's rules directory (e.g.
26
+ `grep -rl <concept> <path/to/validation/rules/>`); for an artifact, the
27
+ tool's own inventory command (e.g. `<tool> templates`); for any other
28
+ check, the directory that holds its siblings.
29
+ - **Record the grep command and its verdict** in the ADR — the exact command, and
30
+ either "no existing mechanism, building new" or "found `<X>`, reusing / extending
31
+ it". A verdict of "reusing" that then builds new anyway must say why.
32
+ - **Read the javadoc/description of anything the grep surfaces.** A rule that already
33
+ covers the property may say so in its own words.
34
+
35
+ > **Provenance (FEAT-2026-0049 / F3).** The feature spent three gates building a
36
+ > `WARNING→ERROR` ladder on a new rule while the enforcement it needed already existed
37
+ > in `RelationshipSymmetryValidationRule` — whose javadoc (F-029, 2026-05-29) literally
38
+ > said the debt "surfaces as a Saturday-night surprise when the inference rule changes."
39
+ > A one-line grep of the rules directory at ADR time would have found it. Cost: two
40
+ > gates and a full pivot.
41
+
42
+ ## 2. Escalation-predicate satisfiability — for any severity flip
43
+
44
+ Any plan that raises a check to `ERROR`, flips a `WARNING` to blocking, or asserts a
45
+ "zero issues" close predicate must answer, in writing, one question:
46
+
47
+ > **What does this rule report on a spec/input already in its intended final state?**
48
+
49
+ If the answer is anything other than **zero**, the predicate is **unsatisfiable** —
50
+ the rule fires on correct inputs, so "zero issues" can never be reached and the
51
+ severity flip forces wrong changes. **Stop and redesign** before arming: fix the rule
52
+ so a correct input reports zero, or route enforcement to a different mechanism (see §1).
53
+
54
+ > **Provenance (FEAT-2026-0049 / F3).** ADR D3 ("omission = intent — a non-owning
55
+ > reference is declared by omission") and D6 ("`ERROR` when the rule reports zero
56
+ > issues") contradicted each other inside the same document: post-flip, every correct
57
+ > omission still tripped the rule, so "zero" was unreachable. The contradiction was
58
+ > answerable by reading the ADR against itself. Separately, the *shipped* rule
59
+ > false-positived ~35 of 49 on real specs (#821) — a severity flip forcing wrong
60
+ > changes, exactly what this check prevents.
61
+
62
+ ## 3. Flag-scope table — for any WU introducing or flipping a behavior flag
63
+
64
+ A WU that introduces, gates on, or flips a behavior flag must contain a **flag-scope
65
+ table**: every code path the flag is claimed to affect, marked *gated* / *not gated*,
66
+ with a one-line *why* per row. The arming review checks the feature's headline claim
67
+ ("the flip retires inference", "the flag makes X strict") against the table — a claim
68
+ the table does not support is a scope mismatch, and scope mismatches surface gates
69
+ later as defects, not at arming.
70
+
71
+ | Code path | Gated by flag? | Why |
72
+ |---|---|---|
73
+ | `<path/method>` | yes / no | <one line> |
74
+
75
+ > **Provenance (FEAT-2026-0049 / F1).** The plan enumerated both inference paths
76
+ > (bare-FK and `$ref`) on day one, but no document ever crossed "which paths does the
77
+ > `explicitRelationshipsOnly` flag gate" against "the flip retires inference." The flag
78
+ > was built FK-path-only, deliberately and correctly — but the mismatch with the
79
+ > headline claim surfaced two gates later (F1), and again as a runtime defect (F4).
80
+
81
+ ## 4. Runtime probe before arming a default/severity flip
82
+
83
+ A gate whose WUs flip a **default value** or a **severity** may **not** be armed on
84
+ "mechanical, nothing design-open." Before arming, apply the change locally and run the
85
+ **exact command the WU's tests gate will run** (the full oracle, not a subset), and
86
+ paste the resulting failure list into the gate review. The probe is the arming
87
+ evidence; "nothing design-open" without it is a claim, not a fact.
88
+
89
+ - Run the *full* oracle. A subset hides failures in the paths the change touches that
90
+ the subset does not exercise.
91
+ - The probe's failure list becomes the WU's enumerated test surface — the WU no longer
92
+ discovers its own breakage attempt by attempt.
93
+
94
+ > **Provenance (FEAT-2026-0049 / F4).** Gate 4 was armed on "nothing is design-open"
95
+ > without ever applying the default flip and running the full suite once. The WU then
96
+ > spun three times (~$14) on a real defect that one local run exposed in seconds — and
97
+ > the first two diagnoses were made against a *subset*, missing failures the full suite
98
+ > showed. Contrast: an earlier gate's "measure before drafting" probe (a `WARNING` run
99
+ > over the real specs) worked and correctly shrank the next WU's scope.
100
+
101
+ ## 5. Planning-WU cost floor
102
+
103
+ `plan-next` and `close` / `close-intermediate` WUs are systematically under-estimated.
104
+ When drafting their `planned_cost_usd`, use a **floor of $5.00** — never the $2–3 that
105
+ "it's just bookkeeping" suggests. A gate `cost_budget_usd` set to the sum of $2–3
106
+ planning estimates is a brake that fires by construction on the first real close.
107
+
108
+ > **Provenance (FEAT-2026-0049 + LEARNINGS [FEAT-2026-0044]).** Planning/close WUs ran
109
+ > 2.8–5.2× their $2–3 estimates: `plan-next` at $5.90 and $15.65, `close-intermediate`
110
+ > at $5.67, against $2–3 drafts. LEARNINGS `[FEAT-2026-0044]` had already predicted this
111
+ > systematic under-estimation; the floor makes the prediction binding.
112
+
113
+ ## Why these live together
114
+
115
+ Each is a one-time check that trades a minute at design/arm time against a gate (or
116
+ three) of rework. They share a root: **automation and authors both trust a claim
117
+ without the cheap check that would test it** — a grep not run (§1), a contradiction not
118
+ read (§2), a scope not tabled (§3), a suite not run (§4), an estimate not calibrated
119
+ (§5). State the check, run it, record the verdict. Every plan, every arm.
@@ -107,6 +107,33 @@ blocked_reason: <present only when status is blocked>
107
107
  5. **No secret-looking values in evidence.** The RESULT block is read by the driver
108
108
  and may be archived. See [`security-boundaries.md`](security-boundaries.md).
109
109
 
110
+ ## Closing obligations for implementation WUs (FEAT-2026-0049)
111
+
112
+ Four binding obligations before `status: complete`. Each guards a way a prior WU
113
+ closed on nothing:
114
+
115
+ 1. **Diff against `produces:` first.** Every path in the WU's `produces:` list must
116
+ show a working-tree change, or the RESULT must justify each unchanged path
117
+ explicitly: "no change needed because X; here is the evidence the deliverable
118
+ already holds: `<command + output>`." Silence on an unchanged deliverable is not
119
+ a valid close. (The driver enforces this mechanically since specfuse-loop 0.3.21
120
+ — #198, outcome `produces_not_in_diff`; the obligation binds regardless of
121
+ driver version.)
122
+ 2. **A plan-level contradiction is `blocked`, not `complete`.** If mid-WU you find
123
+ the plan cannot be delivered as written (a decision contradicts another, a
124
+ deliverable cannot exist), emit `status: blocked` with the finding in
125
+ `blocked_reason`. Never write the finding into a gate document and close
126
+ `complete`. `complete` means the deliverables exist; `blocked` means they cannot
127
+ or should not.
128
+ 3. **Every `evidence:` cites an executed command** — its observed exit code/output.
129
+ Reading source, grepping for a string, or citing another WU's RESULT is not
130
+ verification. For a validation-rule or severity claim the evidence must include a
131
+ negative observation (the rule seen rejecting a purpose-built bad input); see
132
+ [`verification-discipline.md`](verification-discipline.md) §3.
133
+ 4. **Analysis without edits is not a silent attempt.** If an attempt produces
134
+ analysis but no file edits, say so explicitly and end `blocked` (or with a
135
+ concrete next-edit plan). Do not exhaust the attempt on prose.
136
+
110
137
  ## Why this matters
111
138
 
112
139
  The driver's trust model is that the RESULT block is an honest claim about what
@@ -39,6 +39,12 @@ Two generic meanings, both apply unless the unit narrows the scope:
39
39
  decide done. Run them yourself first, in declared order, with full output.
40
40
  Generic verifications — "I assume the tests still pass" — are not verifications.
41
41
  The commands are the verification.
42
+ - **Observe behavior, not source.** A claim that code does X is verified by making
43
+ X happen and reading the output — never by reading the source that looks like it
44
+ does X. A rule read as `ERROR` in source may be gated off and never fire; only a
45
+ run proves it. For any rule or severity claim, include a **negative observation**:
46
+ the rule seen rejecting a purpose-built bad input. A rule that fires on nothing
47
+ passes every test.
42
48
 
43
49
  **Verification is the exit oracle.** The agent's own report is advisory; done is
44
50
  decided by the checks passing, not by the agent's belief. If a check fails you are
@@ -0,0 +1,43 @@
1
+ <!--
2
+ Copyright 2026 Specfuse Contributors
3
+ Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ -->
5
+
6
+ # Project-authored rules (`rules-local/`)
7
+
8
+ This directory belongs to **your project**, not to Specfuse. Put binding rules
9
+ here that are specific to this repository — its tools, its paths, its failure
10
+ modes — and that should survive every scaffold upgrade.
11
+
12
+ **The contract:** `specfuse upgrade` never writes, overwrites, or prunes
13
+ anything in `rules-local/`. That is the opposite of the sibling
14
+ `.specfuse/rules/` directory, which is versioned: upgrades overwrite its files
15
+ with the shipped versions and delete files the shipped scaffold no longer
16
+ carries. A project rule placed in `rules/` will be lost on upgrade; the same
17
+ rule placed here is permanent.
18
+
19
+ ## When to write a rule here vs. upstream
20
+
21
+ - **Here:** the rule names your project's directories, commands, tools, or
22
+ conventions ("grep `src/main/java/.../validation/rules/` before designing a
23
+ rule", "the generator jar's `templates` command lists artifacts"). Upstream
24
+ would have to genericize away exactly the specifics that make it useful to
25
+ you.
26
+ - **Upstream (a PR to the scaffold's `rules/`):** the failure mode is
27
+ project-agnostic and other Specfuse projects would hit it too. Keep the
28
+ provenance; genericize the examples.
29
+
30
+ ## Wiring a rule into the loop
31
+
32
+ Rules load via the `@`-references in your `.claude/CLAUDE.md` Specfuse rules
33
+ block. Add one line per rule you author:
34
+
35
+ ```markdown
36
+ @.specfuse/rules-local/<your-rule>.md
37
+ ```
38
+
39
+ Follow the shipped rules' format: one failure mode per rule, the check stated
40
+ imperatively, provenance recorded so the reasoning survives the rule (see
41
+ `.specfuse/rules/planning-discipline.md` for the model).
42
+
43
+ This README is seeded once and never overwritten — edit it freely.
@@ -30,6 +30,23 @@ here for human review-and-arm: read the review artifact, accept or edit the draf
30
30
  next-gate work units, flip the accepted ones to `pending`, set this gate's status to
31
31
  `passed`, and re-run.
32
32
 
33
+ ## Arming discipline (see `.specfuse/rules/planning-discipline.md`)
34
+
35
+ Before flipping this gate's next-gate WUs to `pending`:
36
+
37
+ - **Runtime probe for a default/severity flip (§4).** If any WU in the gate being armed
38
+ flips a **default value** or a **severity**, it may **not** be armed on "mechanical,
39
+ nothing design-open." Apply the change locally, run the **exact command the WU's tests
40
+ gate will run** (the full oracle, not a subset), and paste the failure list into
41
+ `GATE-NN-REVIEW.md`. That list becomes the WU's enumerated test surface. Arming such a
42
+ gate without a probe is what makes an implementation WU spin on a defect one local run
43
+ would have shown.
44
+ - **Flag-scope table (§3).** If a WU introduces or flips a behavior flag, confirm it
45
+ carries a flag-scope table and that the feature's headline claim matches it.
46
+ - **Escalation-predicate satisfiability (§2).** If a WU raises a check to `ERROR` or
47
+ asserts a "zero issues" predicate, confirm PLAN.md answered "what does the rule report
48
+ on a correct input" and that the answer is zero.
49
+
33
50
  ## Reflection notes
34
51
 
35
52
  <Written by the human at review time. What surprised you, what you changed in the
@@ -23,6 +23,32 @@ each WU file owns its own status, and each GATE file owns its gate's status. Det
23
23
  only as far as the next gate; plan-next drafts the gate after that from the
24
24
  retrospective and lessons.
25
25
 
26
+ ## Existing-mechanism search (mandatory — see `.specfuse/rules/planning-discipline.md` §1)
27
+
28
+ Before any ADR or gate designs a validation rule, a severity level, an enforcement
29
+ gate, or a measurement, establish that it does not already exist. **An ADR without
30
+ this section is incomplete.** If this feature designs no such mechanism, write
31
+ exactly `n/a — no enforcement or measurement designed` and stop — do not fabricate
32
+ a grep to fill the section.
33
+
34
+ - **Grep command run:** `<the exact command — e.g. grep -rl <concept>
35
+ src/main/java/.../validation/rules/>`
36
+ - **Verdict:** `<"no existing mechanism, building new" | "found <X>, reusing/extending">`
37
+ - **If reusing:** name `<X>` and quote the line of its javadoc/description that
38
+ confirms it covers the property. If building new despite a hit, say why the hit does
39
+ not suffice.
40
+
41
+ ## Escalation-predicate satisfiability (mandatory for any severity flip — §2)
42
+
43
+ If this feature raises a check to `ERROR`, flips a `WARNING` to blocking, or asserts a
44
+ "zero issues" close predicate, answer here:
45
+
46
+ - **What does the rule report on an input already in its intended final state?**
47
+ `<answer>`
48
+ - If that answer is **not zero**, the predicate is unsatisfiable — the rule fires on
49
+ correct inputs. **Redesign before drafting gates:** fix the rule so a correct input
50
+ reports zero, or route enforcement to the mechanism found in the search above.
51
+
26
52
  ## Task graph
27
53
 
28
54
  ```yaml
@@ -28,6 +28,10 @@ AUTHOR-SET FIELDS — fill or override these at draft/arm time:
28
28
  `xhigh` | `max`. Absent → type-keyed default. `low`/`medium` add a terseness directive.
29
29
  - `planned_cost_usd` — OPTIONAL. Estimated USD at draft time. Compared against actual in close
30
30
  WU's cost analysis. Lint WARN when absent on active/draft WUs (non-blocking).
31
+ **Planning-WU cost floor (`.specfuse/rules/planning-discipline.md` §5):** for `plan-next`,
32
+ `close`, and `close-intermediate` WUs, draft `planned_cost_usd` at a **floor of $5.00** — these
33
+ run 2.8–5.2× the $2–3 that "it's just bookkeeping" suggests. A gate `cost_budget_usd` set to
34
+ the sum of $2–3 planning estimates is a brake that fires by construction on the first real close.
31
35
  - `generated_surfaces` — OPTIONAL. Paths to generated files this unit's acceptance depends on.
32
36
  - `oracle_env` — OPTIONAL. Environment the verifying oracle runs in: `macos_local`,
33
37
  `linux_docker`, `github_actions_ci`, or an operator-named string. Lint WARN when AC mentions
@@ -77,6 +81,37 @@ cheapest hollow-pass guard; see `/authoring-work-units` §12 for the contract an
77
81
  the carve-outs (refactor, migration, pure-data → explicit `Red-test exempt:
78
82
  <reason>` line in the WU body).
79
83
 
84
+ **Flag-scope table** (REQUIRED only when this WU introduces, gates on, or flips a
85
+ behavior flag; omit otherwise — see `.specfuse/rules/planning-discipline.md` §3). Every
86
+ code path the flag is claimed to affect, marked gated / not gated, with a one-line why.
87
+ The arming review checks the feature's headline claim ("the flip retires X") against
88
+ this table; a claim the table does not support is a scope mismatch that surfaces as a
89
+ defect gates later.
90
+
91
+ | Code path | Gated by flag? | Why |
92
+ |---|---|---|
93
+ | `<path/method>` | yes / no | <one line> |
94
+
95
+ **Close obligations** (REQUIRED only for `close` / `close-intermediate` WUs; omit
96
+ otherwise — see `.specfuse/rules/close-discipline.md`). The close's acceptance
97
+ criteria must cover:
98
+
99
+ 1. **Oracles re-run fresh** (§1): every oracle the feature's criteria name,
100
+ full command(s), exit codes read directly — never a producing WU's
101
+ self-report; regenerate into a clean dir before asserting on generated
102
+ artifacts.
103
+ 2. **Hedged follow-up record** (§2): on `met_locally`, a named record per
104
+ unmet criterion — criterion, why unverifiable here, exact re-run condition
105
+ that upgrades it to `met`.
106
+ 3. **Consumer-visible contract changes** (§3): enumerate every addition /
107
+ removal / rename across the feature's producing WUs and block on human
108
+ acknowledgment, or write exactly `n/a — no consumer-visible contract
109
+ change`.
110
+
111
+ A close whose criteria include any of these is load-bearing — set
112
+ `auto_close_disabled: true` in this WU's frontmatter so the auto-close
113
+ predicate cannot skip it.
114
+
80
115
  **Do not touch.** Generated directories (`_generated/`, `gen-src/`, or the repo's
81
116
  declared equivalent), files owned by other work units in this gate, secrets,
82
117
  `.git/`. The driver owns all git operations — you edit files only. See
@@ -335,6 +335,56 @@ def check_planned_cost(feature_dir: Path, plan_fm: dict, gates: list) -> None:
335
335
  )
336
336
 
337
337
 
338
+ # Planning-discipline section names (#201, second half). Canonical since the
339
+ # FEAT-2026-0049 planning-discipline drop (PR #211): PLAN.md carries the two
340
+ # ADR sections; each gate file carries the arming-discipline section. Names
341
+ # are shared vocabulary with downstream planning tooling — do not rename here
342
+ # without a template/coordination change.
343
+ PLAN_DISCIPLINE_SECTIONS = (
344
+ "Existing-mechanism search",
345
+ "Escalation-predicate satisfiability",
346
+ )
347
+ GATE_ARMING_SECTION = "Arming discipline"
348
+
349
+
350
+ def check_planning_sections(
351
+ feature_dir: Path, plan_fm: dict, plan_body: str, gates: list,
352
+ ) -> None:
353
+ """WARN when the planning-discipline sections are absent (#201).
354
+
355
+ Presence-only: the PLAN template's sections allow an explicit n/a line, so
356
+ content is the arming reviewer's job — this check only catches the section
357
+ never being written at all (pre-0.3.22 drafts, or a plan-next that dropped
358
+ it). Sealed features (plan status done/abandoned) are history and skipped.
359
+ WARN-only, exit code 0, like the rest of the planning-lint family.
360
+ """
361
+ if plan_fm.get("status") in ("done", "abandoned"):
362
+ return
363
+ for section in PLAN_DISCIPLINE_SECTIONS:
364
+ if not _slice_section(plan_body, section).strip():
365
+ print(
366
+ f"WARN: {feature_dir}/PLAN.md: missing '{section}' section "
367
+ f"(planning-discipline; write it or an explicit 'n/a' line). "
368
+ f"See .specfuse/rules/planning-discipline.md."
369
+ )
370
+ for g in gates:
371
+ gate_file = g.get("file")
372
+ if not gate_file:
373
+ continue
374
+ gate_path = feature_dir / gate_file
375
+ if not gate_path.exists():
376
+ continue
377
+ gfm, gbody = read_frontmatter(gate_path)
378
+ if gfm.get("status") == "passed":
379
+ continue
380
+ if not _slice_section(gbody, GATE_ARMING_SECTION).strip():
381
+ print(
382
+ f"WARN: {gate_file}: missing '{GATE_ARMING_SECTION}' section "
383
+ f"(runtime-probe / flag-scope / predicate checks before "
384
+ f"arming). See .specfuse/rules/planning-discipline.md."
385
+ )
386
+
387
+
338
388
  def lint(feature_dir: Path) -> list[str]:
339
389
  errs: list[str] = []
340
390
  plan = feature_dir / "PLAN.md"
@@ -623,6 +673,8 @@ def lint(feature_dir: Path) -> list[str]:
623
673
 
624
674
  # Planned-cost capture: WARN on missing/divergent planned_cost_usd fields.
625
675
  check_planned_cost(feature_dir, fm, gates)
676
+ # Planning-discipline section presence (#201): WARN-only.
677
+ check_planning_sections(feature_dir, fm, body, gates)
626
678
 
627
679
  # Cross-gate mixed-shape check. Two directions of mix:
628
680
  #
@@ -64,7 +64,7 @@ SPECFUSE_DIR = Path(".specfuse")
64
64
  REPO_ROOT = SPECFUSE_DIR.parent
65
65
  FEATURES_DIR = SPECFUSE_DIR / "features"
66
66
  VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
67
- DRIVER_VERSION = "0.3.21"
67
+ DRIVER_VERSION = "0.3.23"
68
68
  # Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
69
69
  # version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
70
70
  # the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
@@ -1537,15 +1537,20 @@ def gate_budget_usd(gate_file: Path) -> float | None:
1537
1537
 
1538
1538
 
1539
1539
  def gate_spent_usd(plan: dict, gate: dict, feature_dir: Path) -> float:
1540
- """Sum lifetime cost across the gate's done WUs (closing-sequence included).
1540
+ """Sum lifetime recorded cost across ALL of the gate's WUs.
1541
1541
 
1542
1542
  Reads each WU file's frontmatter from `gate["work_units"]` and adds
1543
1543
  `cost_usd` PLUS `cumulative_cost_usd` (prior re-arm cycles' spend, folded
1544
- by fold_cumulative_on_rearm — #199) when the WU's status is "done". WUs
1545
- whose frontmatter omits both cost tracking off, or the attempt didn't
1546
- record a cost contribute 0.0. `plan` is the feature frontmatter dict and
1547
- is accepted for signature symmetry with the broader gate-budget helpers;
1548
- the spent total is derived from WU files alone.
1544
+ by fold_cumulative_on_rearm — #199) regardless of WU status (#219):
1545
+ write_cost_to_wu records cost on every outcome pass, blocked, spinning
1546
+ so cost on a non-done WU is real spend, not an estimate. The pre-#219
1547
+ done-only filter left a blocked WU's re-arm burn invisible to the budget
1548
+ brake until the WU finally passed (FEAT-2026-0049/WU-06: $30.29 across 9
1549
+ attempts, contributing $0 while blocked). WUs whose frontmatter omits both
1550
+ fields — cost tracking off, never dispatched, or the attempt didn't record
1551
+ a cost — contribute 0.0. `plan` is the feature frontmatter dict and is
1552
+ accepted for signature symmetry with the broader gate-budget helpers; the
1553
+ spent total is derived from WU files alone.
1549
1554
  """
1550
1555
  del plan # signature symmetry — sum is derived from WU files only
1551
1556
  total = 0.0
@@ -1557,8 +1562,6 @@ def gate_spent_usd(plan: dict, gate: dict, feature_dir: Path) -> float:
1557
1562
  if not wu_path.exists():
1558
1563
  continue
1559
1564
  fm, _ = read_frontmatter(wu_path)
1560
- if fm.get("status") != "done":
1561
- continue
1562
1565
  # Lifetime spend, not per-cycle (#199): fold_cumulative_on_rearm
1563
1566
  # moves each prior dispatch cycle's cost into cumulative_cost_usd and
1564
1567
  # zeroes cost_usd, so summing cost_usd alone undercounts every