specfuse-loop 0.3.22__tar.gz → 0.3.24__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.22/specfuse_loop.egg-info → specfuse_loop-0.3.24}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.24/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/methodology.md +12 -0
  5. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/skills.md +5 -0
  6. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/roadmap.template.md +18 -2
  7. specfuse_loop-0.3.24/specfuse/loop/data/rules/close-discipline.md +74 -0
  8. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/PLAN.template.md +1 -1
  9. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/WU.template.md +20 -0
  10. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/lint_plan.py +53 -1
  11. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/loop.py +44 -10
  12. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/scaffold.py +75 -5
  13. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24/specfuse_loop.egg-info}/PKG-INFO +1 -1
  14. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/SOURCES.txt +2 -0
  15. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_init_integration.py +2 -0
  16. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_close_intermediate.py +8 -0
  17. specfuse_loop-0.3.24/tests/test_lint_planning_sections.py +117 -0
  18. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_auto_archive.py +40 -0
  19. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_gate_budget.py +28 -4
  20. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_orchestration.py +25 -0
  21. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_data_in_sync.py +1 -0
  22. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_docs.py +7 -0
  23. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_init.py +2 -0
  24. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_resources.py +1 -0
  25. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_upgrade.py +55 -1
  26. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_upgrade_integration.py +8 -0
  27. specfuse_loop-0.3.22/specfuse/loop/data/VERSION +0 -1
  28. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/LICENSE +0 -0
  29. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/NOTICE +0 -0
  30. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/README.md +0 -0
  31. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/setup.cfg +0 -0
  32. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/__init__.py +0 -0
  33. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/_filelock.py +0 -0
  34. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/_miniyaml.py +0 -0
  35. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/adopt_feature.py +0 -0
  36. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  37. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  38. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  39. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/getting-started.md +0 -0
  40. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/gitignore.snippet +0 -0
  41. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  42. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/never-touch.md +0 -0
  43. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/planning-discipline.md +0 -0
  44. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/result-contract.md +0 -0
  45. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  46. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  47. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/rules-local/README.md +0 -0
  48. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/event.schema.json +0 -0
  49. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  50. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  51. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  52. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  53. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/GATE.template.md +0 -0
  54. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/data/verification.yml.example +0 -0
  55. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/gate_eval.py +0 -0
  56. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/gh_backend.py +0 -0
  57. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/gh_features.py +0 -0
  58. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse/loop/validate_event.py +0 -0
  59. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  60. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/entry_points.txt +0 -0
  61. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/requires.txt +0 -0
  62. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/top_level.txt +0 -0
  63. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_adopt_feature.py +0 -0
  64. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  65. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_attempt_outcome_emission.py +0 -0
  66. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_autoclose_deferral_visibility.py +0 -0
  67. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_autosync.py +0 -0
  68. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_autosync_consent.py +0 -0
  69. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_autosync_firstrun.py +0 -0
  70. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_autosync_plugin.py +0 -0
  71. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_backend.py +0 -0
  72. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_bash_routing.py +0 -0
  73. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  74. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  75. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_bookkeeping_no_verify.py +0 -0
  76. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_claude_resolution.py +0 -0
  77. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_close_wu_self_reference.py +0 -0
  78. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_closing_deliverable_guard.py +0 -0
  79. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_console_encoding.py +0 -0
  80. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_cost_tracking.py +0 -0
  81. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_degraded_oracle.py +0 -0
  82. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_deliverable_presence_gate.py +0 -0
  83. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_doctor.py +0 -0
  84. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_driver_integration.py +0 -0
  85. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_driver_lock.py +0 -0
  86. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_duration_tracking.py +0 -0
  87. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_empty_files_escalation.py +0 -0
  88. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_ensure_feature_branch.py +0 -0
  89. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_ensure_feature_branch_base.py +0 -0
  90. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_events_redaction.py +0 -0
  91. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_extra_gates.py +0 -0
  92. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_filelock_portable.py +0 -0
  93. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_force_full_close.py +0 -0
  94. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_gate_eval.py +0 -0
  95. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_gate_eval_calibration.py +0 -0
  96. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  97. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_gate_eval_terminal_wiring.py +0 -0
  98. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_gh_backend.py +0 -0
  99. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_gh_features.py +0 -0
  100. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_git_env_isolation.py +0 -0
  101. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_hashed_denylist.py +0 -0
  102. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_hashed_denylist_ci.py +0 -0
  103. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_interpreter_normalization.py +0 -0
  104. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_leak_findings_redaction.py +0 -0
  105. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_leak_scan.py +0 -0
  106. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_leak_scan_content.py +0 -0
  107. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_learnings_query.py +0 -0
  108. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  109. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lifecycle_integration.py +0 -0
  110. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lifetime_accounting.py +0 -0
  111. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_bare_produces_path.py +0 -0
  112. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_close_wu.py +0 -0
  113. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_correlation_id.py +0 -0
  114. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  115. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_oracle_env.py +0 -0
  116. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_plan_errors.py +0 -0
  117. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_plan_next_draft.py +0 -0
  118. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_plan_relative_import.py +0 -0
  119. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_produces_driver_helper.py +0 -0
  120. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_sections.py +0 -0
  121. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  122. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_lint_verification_reference.py +0 -0
  123. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_attempt_evidence.py +0 -0
  124. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_caveman_preamble.py +0 -0
  125. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_close_intermediate.py +0 -0
  126. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_defaults_by_type.py +0 -0
  127. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_effort.py +0 -0
  128. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_failure_note_cap.py +0 -0
  129. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_files_changed_guard.py +0 -0
  130. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_model_alias.py +0 -0
  131. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_post_pass_invariant.py +0 -0
  132. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_reset_preserving_events.py +0 -0
  133. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_smoke_runner.py +0 -0
  134. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_unsandboxed.py +0 -0
  135. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_loop_zero_token_guard.py +0 -0
  136. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_migrate_legacy.py +0 -0
  137. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_miniyaml_equivalence.py +0 -0
  138. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_miniyaml_negative.py +0 -0
  139. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_planned_cost_lint.py +0 -0
  140. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_prepare_feature.py +0 -0
  141. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_prepare_scaffold_sync.py +0 -0
  142. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_produces_field.py +0 -0
  143. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_produces_in_diff.py +0 -0
  144. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_redaction_windows_home.py +0 -0
  145. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_resolve_base.py +0 -0
  146. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_result_block.py +0 -0
  147. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_roadmap_add_skill.py +0 -0
  148. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_roadmap_archive_skill.py +0 -0
  149. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_roadmap_row_parser.py +0 -0
  150. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_doc_hygiene.py +0 -0
  151. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_manifest.py +0 -0
  152. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_seed_sanity.py +0 -0
  153. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_scaffold_wiring.py +0 -0
  154. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_skills_vendored_in_sync.py +0 -0
  155. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_spinning_rearm_gate.py +0 -0
  156. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_squash_commit_hook_crash.py +0 -0
  157. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_template_closing_shapes.py +0 -0
  158. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_terminal_flip_ownership.py +0 -0
  159. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_terminal_flips.py +0 -0
  160. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_timeout_kill_portable.py +0 -0
  161. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_untracked_feature_folder.py +0 -0
  162. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_untracked_file_absorption.py +0 -0
  163. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_upgrade_merge_gate.py +0 -0
  164. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_validate_event.py +0 -0
  165. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_verdict_coupling.py +0 -0
  166. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_verify_empty_gate_set.py +0 -0
  167. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_version_consistency.py +0 -0
  168. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_version_skew.py +0 -0
  169. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/tests/test_windows_gate_exec.py +0 -0
  170. {specfuse_loop-0.3.22 → specfuse_loop-0.3.24}/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.22
3
+ Version: 0.3.24
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.22"
7
+ version = "0.3.24"
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.24
@@ -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
@@ -48,6 +48,11 @@ roadmap ──/pick-feature──▶ active ──/draft-feature──▶ gate 1
48
48
  `/draft-feature`.
49
49
  - **`/roadmap-add`** — add a new feature row to the roadmap before it's ready to
50
50
  pick.
51
+ - **`/block-feature`** — flip a roadmap feature to `blocked` (or clear it) with a
52
+ linked `**Blocked by.**` blocker — an ADR awaiting approval or an upstream
53
+ FEAT-ID that must complete first. Writes the row status, the detail block, and
54
+ PLAN frontmatter, keeping intra-page feature links resolvable.
55
+ `/block-feature FEAT-ID --unblock` clears the block and restores the status.
51
56
 
52
57
  ### 2. Draft — turn a picked feature into a runnable gate
53
58
 
@@ -23,8 +23,24 @@ the canonical column order below.
23
23
  |----------------|-------|--------|--------|--------|
24
24
 
25
25
  Status: `planned` → `active` → `done` (or `abandoned`). `deferred` = parked
26
- pending an external decision/dependency; resumable (a human flips it back to
27
- `active` when the blocker clears), distinct from `abandoned` (dead).
26
+ by choice pending an external decision/dependency; resumable (a human flips it
27
+ back to `active` when the blocker clears), distinct from `abandoned` (dead).
28
+ `blocked` = cannot proceed because a *named* dependency is unmet — an ADR
29
+ awaiting approval, or an upstream feature that must complete first. Resumable
30
+ like `deferred`, but where `deferred` is a voluntary park with no named blocker,
31
+ `blocked` always names its blocker so the roadmap shows what it waits on and
32
+ links to it.
33
+
34
+ A `blocked` feature's detail section carries a `**Blocked by.**` block — blank
35
+ line above it, one linked blocker per clause:
36
+
37
+ **Blocked by.** [ADR-0007: event-schema versioning](../docs/adr/0007-event-schema-versioning.md) — awaiting approval; [FEAT-2026-0011](#feat-2026-0011) — scoring data must land first.
38
+
39
+ Each blocker is a Markdown link: an ADR points at its `.md` file (kept under
40
+ `docs/adr/` by convention, so from `.specfuse/roadmap.md` the link is
41
+ `../docs/adr/…`); a feature dependency points at the blocking
42
+ feature's roadmap anchor (`#feat-yyyy-nnnn`). When every blocker clears, a human
43
+ flips `blocked` → `active` (or `planned`) and removes the `**Blocked by.**` block.
28
44
 
29
45
  ## Notes
30
46
 
@@ -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.
@@ -7,7 +7,7 @@ roadmap_goal: <one line copied from the roadmap — the north star this feature
7
7
  plan-next anchors every drafted gate to this and flags if a retrospective implies
8
8
  it should change>
9
9
  autonomy_default: review # auto | review | supervised
10
- status: active # active | deferred | done | abandoned
10
+ status: active # active | blocked | deferred | done | abandoned
11
11
  # deferred = parked pending an external decision/dependency; resumable, non-dispatchable
12
12
  # planned_cost_usd: 0.00 # OPTIONAL — sum of WU planned costs; lint warns when missing or when delta from WU sum > 10%
13
13
  ---
@@ -92,6 +92,26 @@ defect gates later.
92
92
  |---|---|---|
93
93
  | `<path/method>` | yes / no | <one line> |
94
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
+
95
115
  **Do not touch.** Generated directories (`_generated/`, `gen-src/`, or the repo's
96
116
  declared equivalent), files owned by other work units in this gate, secrets,
97
117
  `.git/`. The driver owns all git operations — you edit files only. See
@@ -46,7 +46,7 @@ VALID_STATUS = {"draft", "pending", "ready", "in_progress", "in_review", "done",
46
46
  # unrecognized status that passes lint and then behaves differently at dispatch
47
47
  # is the class #183 (deferred) and #185 exist to close. Keep in sync with
48
48
  # find_feature / the gate-status transitions in loop.py.
49
- VALID_FEATURE_STATUS = {"planned", "active", "deferred", "done", "abandoned"}
49
+ VALID_FEATURE_STATUS = {"planned", "active", "blocked", "deferred", "done", "abandoned"}
50
50
  VALID_GATE_STATUS = {"open", "awaiting_review", "passed"}
51
51
  CLOSING_SEQUENCE = ["retrospective", "lessons", "docs", "plan-next"]
52
52
  # New compact closing shapes (FEAT-2026-0015):
@@ -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.22"
67
+ DRIVER_VERSION = "0.3.24"
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
@@ -285,10 +285,22 @@ def find_feature(arg: str | None) -> Path:
285
285
  f"deferred features. Flip its PLAN.md status back to `active` "
286
286
  f"when the blocker clears, then re-run."
287
287
  )
288
+ # A `blocked` feature waits on a named unmet dependency (an ADR or an
289
+ # upstream feature — see its roadmap `**Blocked by.**` block). Like
290
+ # `deferred` it is non-dispatchable; refuse an explicit --feature
291
+ # targeting one rather than dispatching it anyway.
292
+ if _fm.get("status") == "blocked":
293
+ sys.exit(
294
+ f"{d.name} is blocked — the loop does not dispatch blocked "
295
+ f"features. Clear its blocker(s), then run "
296
+ f"`/block-feature {_fm.get('feature_id', d.name)} --unblock` "
297
+ f"(or flip its PLAN.md status back to `active`) and re-run."
298
+ )
288
299
  return d
289
300
  actives = []
290
301
  done_pending_wrap = []
291
302
  deferred = []
303
+ blocked = []
292
304
  for d in sorted(FEATURES_DIR.glob("*/")):
293
305
  plan = d / "PLAN.md"
294
306
  if plan.exists():
@@ -316,6 +328,11 @@ def find_feature(arg: str | None) -> Path:
316
328
  # a bare invocation won't pick it up; surfaced below so a lone
317
329
  # deferred feature reads as parked, not silently absent.
318
330
  deferred.append(d)
331
+ elif fm.get("status") == "blocked":
332
+ # Waiting on a named unmet dependency — non-dispatchable, same
333
+ # as deferred. Surfaced below so a lone blocked feature reads as
334
+ # parked-on-a-blocker, not silently absent.
335
+ blocked.append(d)
319
336
  if len(actives) == 1:
320
337
  return actives[0]
321
338
  if not actives:
@@ -328,6 +345,13 @@ def find_feature(arg: str | None) -> Path:
328
345
  f" Flip one's PLAN.md status to `active` to "
329
346
  f"resume it.\n"
330
347
  )
348
+ if blocked:
349
+ names = ", ".join(d.name for d in blocked[-3:])
350
+ msg += (
351
+ f" - {len(blocked)} blocked feature(s), skipped: {names}\n"
352
+ f" Clear the blocker, then /block-feature "
353
+ f"<id> --unblock to resume.\n"
354
+ )
331
355
  if done_pending_wrap:
332
356
  names = ", ".join(d.name for d in done_pending_wrap[-3:])
333
357
  msg += (
@@ -1537,15 +1561,20 @@ def gate_budget_usd(gate_file: Path) -> float | None:
1537
1561
 
1538
1562
 
1539
1563
  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).
1564
+ """Sum lifetime recorded cost across ALL of the gate's WUs.
1541
1565
 
1542
1566
  Reads each WU file's frontmatter from `gate["work_units"]` and adds
1543
1567
  `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.
1568
+ by fold_cumulative_on_rearm — #199) regardless of WU status (#219):
1569
+ write_cost_to_wu records cost on every outcome — pass, blocked, spinning —
1570
+ so cost on a non-done WU is real spend, not an estimate. The pre-#219
1571
+ done-only filter left a blocked WU's re-arm burn invisible to the budget
1572
+ brake until the WU finally passed (FEAT-2026-0049/WU-06: $30.29 across 9
1573
+ attempts, contributing $0 while blocked). WUs whose frontmatter omits both
1574
+ fields — cost tracking off, never dispatched, or the attempt didn't record
1575
+ a cost — contribute 0.0. `plan` is the feature frontmatter dict and is
1576
+ accepted for signature symmetry with the broader gate-budget helpers; the
1577
+ spent total is derived from WU files alone.
1549
1578
  """
1550
1579
  del plan # signature symmetry — sum is derived from WU files only
1551
1580
  total = 0.0
@@ -1557,8 +1586,6 @@ def gate_spent_usd(plan: dict, gate: dict, feature_dir: Path) -> float:
1557
1586
  if not wu_path.exists():
1558
1587
  continue
1559
1588
  fm, _ = read_frontmatter(wu_path)
1560
- if fm.get("status") != "done":
1561
- continue
1562
1589
  # Lifetime spend, not per-cycle (#199): fold_cumulative_on_rearm
1563
1590
  # moves each prior dispatch cycle's cost into cumulative_cost_usd and
1564
1591
  # zeroes cost_usd, so summing cost_usd alone undercounts every
@@ -2666,7 +2693,14 @@ def auto_archive_feature(feature_id: str, repo_root: Path) -> str:
2666
2693
  section_m2 = section_re.search(roadmap_text)
2667
2694
  if section_m2:
2668
2695
  roadmap_text = roadmap_text[:section_m2.start()] + roadmap_text[section_m2.end():]
2669
- roadmap_text = re.sub(r'\n{3,}', '\n\n', roadmap_text)
2696
+ # A feature that was ever `blocked` via /block-feature carries an explicit
2697
+ # `<a id="feat-…"></a>` line above its heading (so intra-page `#feat-…`
2698
+ # links resolve). The section regex above does not span it, so removing the
2699
+ # section would strand the anchor pointing at a section that has moved to the
2700
+ # archive. Strip the now-orphaned anchor line; Step 3 already re-emitted the
2701
+ # canonical anchor in roadmap-archive.md, so the link target travels with it.
2702
+ roadmap_text = roadmap_text.replace(f"{anchor}\n", "")
2703
+ roadmap_text = re.sub(r'\n{3,}', '\n\n', roadmap_text)
2670
2704
  roadmap_path.write_text(roadmap_text)
2671
2705
 
2672
2706
  return "archived"
@@ -70,6 +70,27 @@ def _write_manifest(specfuse_dir: Path, entries: dict[str, str]) -> None:
70
70
  )
71
71
 
72
72
 
73
+ def _read_manifest(specfuse_dir: Path) -> dict[str, str]:
74
+ """Return the on-disk .scaffold-manifest entries, {} when absent/unreadable.
75
+
76
+ The manifest is the ownership record: a versioned-dir file is provably
77
+ specfuse-written iff its relpath appears here (#214). Malformed content
78
+ degrades to {} — the callers then act as if ownership is unprovable,
79
+ which errs on the side of not deleting.
80
+ """
81
+ manifest_path = specfuse_dir / _MANIFEST_FILE
82
+ if not manifest_path.exists():
83
+ return {}
84
+ try:
85
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
86
+ except (ValueError, OSError):
87
+ return {}
88
+ if not isinstance(data, dict):
89
+ return {}
90
+ return {k: v for k, v in data.items()
91
+ if isinstance(k, str) and isinstance(v, str)}
92
+
93
+
73
94
  def detect_modified(target: str | Path) -> list[str]:
74
95
  """Return sorted relpaths of versioned .specfuse/ files whose sha256 differs from the manifest.
75
96
 
@@ -322,10 +343,12 @@ def upgrade_specfuse(
322
343
  """Overlay versioned seed onto an existing .specfuse/ tree.
323
344
 
324
345
  Overwrites versioned files (templates/, rules/, verification.yml.example, VERSION),
325
- prunes versioned files the seed no longer ships (scoped to templates/ and rules/),
326
- seeds missing user-authored files from templates, and refreshes .claude wiring.
327
- rules-local/ is project-owned: never overlaid, never pruned; only its README
328
- is seeded when absent.
346
+ prunes versioned files the seed no longer ships — but only those the prior
347
+ .scaffold-manifest proves specfuse wrote; unknown files are kept with a
348
+ warning (#214) — seeds missing user-authored files from templates, and
349
+ refreshes .claude wiring. Overwriting a file whose content is not what the
350
+ prior upgrade wrote warns to stderr. rules-local/ is project-owned: never
351
+ overlaid, never pruned; only its README is seeded when absent.
329
352
 
330
353
  Returns the sorted list of .specfuse/ relpaths written.
331
354
  Raises ScaffoldDowngradeError if the target VERSION is newer than the installed seed.
@@ -350,6 +373,11 @@ def upgrade_specfuse(
350
373
  written: list[str] = []
351
374
  versioned_relpaths: set[str] = set()
352
375
  manifest_entries: dict[str, str] = {}
376
+ # Ownership record from the PRIOR init/upgrade (#214): a file in a
377
+ # versioned dir is provably specfuse-written iff it appears here. Read
378
+ # before the overlay overwrites anything and before _write_manifest
379
+ # replaces it below.
380
+ old_manifest = _read_manifest(specfuse_dir)
353
381
 
354
382
  for relpath, content in iter_scaffold_files():
355
383
  if not (
@@ -358,6 +386,32 @@ def upgrade_specfuse(
358
386
  ):
359
387
  continue
360
388
  dest = specfuse_dir / relpath
389
+ # Clobber warning (#214): overwriting is the versioned-dir contract,
390
+ # but do it loudly when the on-disk content is not what the prior
391
+ # upgrade wrote — either a project-authored file whose name now
392
+ # collides with a shipped one, or a locally-modified shipped file.
393
+ # A missing manifest (legacy pre-manifest tree) stays silent: every
394
+ # file would warn and the signal would drown.
395
+ if old_manifest and dest.exists():
396
+ on_disk_sha = _sha256_hex(dest.read_bytes())
397
+ if on_disk_sha != _sha256_hex(content):
398
+ owned_sha = old_manifest.get(relpath)
399
+ if owned_sha is None:
400
+ print(
401
+ f"specfuse upgrade: WARNING — overwriting {relpath}: "
402
+ "not written by a prior init/upgrade (project-"
403
+ "authored?). Project-owned rules belong in "
404
+ ".specfuse/rules-local/, which upgrade never touches.",
405
+ file=sys.stderr,
406
+ )
407
+ elif on_disk_sha != owned_sha:
408
+ print(
409
+ f"specfuse upgrade: WARNING — overwriting locally-"
410
+ f"modified versioned file {relpath}. Upstream edits "
411
+ "belong in a scaffold PR; project-specific rules in "
412
+ ".specfuse/rules-local/.",
413
+ file=sys.stderr,
414
+ )
361
415
  dest.parent.mkdir(parents=True, exist_ok=True)
362
416
  dest.write_bytes(content)
363
417
  written.append(relpath)
@@ -370,8 +424,24 @@ def upgrade_specfuse(
370
424
  for existing in dir_path.rglob("*"):
371
425
  if existing.is_file():
372
426
  rel = existing.relative_to(specfuse_dir).as_posix()
373
- if rel not in versioned_relpaths:
427
+ if rel in versioned_relpaths:
428
+ continue
429
+ # Manifest-scoped prune (#214): delete only files the
430
+ # prior manifest proves specfuse wrote (shipped once,
431
+ # since removed from the seed). Anything else — a
432
+ # project-authored file, or any file on a legacy
433
+ # pre-manifest tree — is kept, loudly.
434
+ if rel in old_manifest:
374
435
  existing.unlink()
436
+ else:
437
+ print(
438
+ f"specfuse upgrade: WARNING — keeping {rel}: not "
439
+ "in the shipped scaffold and not written by a "
440
+ "prior init/upgrade. If it is project-authored, "
441
+ "move it to .specfuse/rules-local/ (never touched "
442
+ "by upgrade); if it is stale, delete it manually.",
443
+ file=sys.stderr,
444
+ )
375
445
 
376
446
  user_seeds = [
377
447
  ("LEARNINGS.md", "LEARNINGS.template.md"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.22
3
+ Version: 0.3.24
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
@@ -23,6 +23,7 @@ specfuse/loop/data/docs/methodology.md
23
23
  specfuse/loop/data/docs/skills.md
24
24
  specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md
25
25
  specfuse/loop/data/docs/concepts/ralph-lineage.md
26
+ specfuse/loop/data/rules/close-discipline.md
26
27
  specfuse/loop/data/rules/correlation-ids.md
27
28
  specfuse/loop/data/rules/never-touch.md
28
29
  specfuse/loop/data/rules/planning-discipline.md
@@ -102,6 +103,7 @@ tests/test_lint_oracle_env.py
102
103
  tests/test_lint_plan_errors.py
103
104
  tests/test_lint_plan_next_draft.py
104
105
  tests/test_lint_plan_relative_import.py
106
+ tests/test_lint_planning_sections.py
105
107
  tests/test_lint_produces_driver_helper.py
106
108
  tests/test_lint_sections.py
107
109
  tests/test_lint_task_graph_yaml_selection.py
@@ -34,6 +34,7 @@ _EXPECTED_SPECFUSE_TREE = {
34
34
  "templates/GATE.template.md",
35
35
  "templates/PLAN.template.md",
36
36
  "templates/WU.template.md",
37
+ "rules/close-discipline.md",
37
38
  "rules/correlation-ids.md",
38
39
  "rules/never-touch.md",
39
40
  "rules/planning-discipline.md",
@@ -117,6 +118,7 @@ class TestInitFullLayout(unittest.TestCase):
117
118
 
118
119
  def test_rules_byte_faithful(self):
119
120
  for rel in (
121
+ "rules/close-discipline.md",
120
122
  "rules/correlation-ids.md",
121
123
  "rules/never-touch.md",
122
124
  "rules/planning-discipline.md",
@@ -42,6 +42,14 @@ def _write_plan(feature_dir: Path, gates_yaml: str) -> None:
42
42
  "```yaml\n"
43
43
  f"{gates_yaml}\n"
44
44
  "```\n"
45
+ "\n"
46
+ "## Existing-mechanism search\n"
47
+ "\n"
48
+ "n/a — no enforcement or measurement designed\n"
49
+ "\n"
50
+ "## Escalation-predicate satisfiability\n"
51
+ "\n"
52
+ "n/a — no severity flip\n"
45
53
  )
46
54
 
47
55