specfuse-loop 0.3.19__tar.gz → 0.3.20__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 (163) hide show
  1. {specfuse_loop-0.3.19/specfuse_loop.egg-info → specfuse_loop-0.3.20}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.20/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/docs/methodology.md +7 -1
  5. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/loop.py +96 -1
  6. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20/specfuse_loop.egg-info}/PKG-INFO +1 -1
  7. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_gate_eval_intermediate_wiring.py +11 -0
  8. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_gate_eval_terminal_wiring.py +111 -0
  9. specfuse_loop-0.3.19/specfuse/loop/data/VERSION +0 -1
  10. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/LICENSE +0 -0
  11. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/NOTICE +0 -0
  12. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/README.md +0 -0
  13. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/setup.cfg +0 -0
  14. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/__init__.py +0 -0
  15. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/_filelock.py +0 -0
  16. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/_miniyaml.py +0 -0
  17. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/adopt_feature.py +0 -0
  18. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  19. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  20. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  21. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/docs/getting-started.md +0 -0
  22. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/docs/skills.md +0 -0
  23. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/gitignore.snippet +0 -0
  24. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/roadmap.template.md +0 -0
  25. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  26. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/rules/never-touch.md +0 -0
  27. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/rules/result-contract.md +0 -0
  28. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  29. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  30. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/schemas/event.schema.json +0 -0
  31. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  32. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  33. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  34. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  35. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/templates/GATE.template.md +0 -0
  36. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  37. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/templates/WU.template.md +0 -0
  38. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/data/verification.yml.example +0 -0
  39. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/gate_eval.py +0 -0
  40. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/gh_backend.py +0 -0
  41. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/gh_features.py +0 -0
  42. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/lint_plan.py +0 -0
  43. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/scaffold.py +0 -0
  44. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse/loop/validate_event.py +0 -0
  45. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse_loop.egg-info/SOURCES.txt +0 -0
  46. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  47. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse_loop.egg-info/entry_points.txt +0 -0
  48. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse_loop.egg-info/requires.txt +0 -0
  49. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/specfuse_loop.egg-info/top_level.txt +0 -0
  50. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_adopt_feature.py +0 -0
  51. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  52. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_attempt_outcome_emission.py +0 -0
  53. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_autoclose_deferral_visibility.py +0 -0
  54. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_autosync.py +0 -0
  55. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_autosync_consent.py +0 -0
  56. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_autosync_firstrun.py +0 -0
  57. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_autosync_plugin.py +0 -0
  58. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_backend.py +0 -0
  59. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_bash_routing.py +0 -0
  60. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  61. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  62. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_bookkeeping_no_verify.py +0 -0
  63. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_claude_resolution.py +0 -0
  64. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_close_wu_self_reference.py +0 -0
  65. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_closing_deliverable_guard.py +0 -0
  66. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_console_encoding.py +0 -0
  67. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_cost_tracking.py +0 -0
  68. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_degraded_oracle.py +0 -0
  69. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_deliverable_presence_gate.py +0 -0
  70. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_doctor.py +0 -0
  71. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_driver_integration.py +0 -0
  72. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_driver_lock.py +0 -0
  73. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_duration_tracking.py +0 -0
  74. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_empty_files_escalation.py +0 -0
  75. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_ensure_feature_branch.py +0 -0
  76. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_ensure_feature_branch_base.py +0 -0
  77. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_events_redaction.py +0 -0
  78. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_extra_gates.py +0 -0
  79. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_filelock_portable.py +0 -0
  80. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_force_full_close.py +0 -0
  81. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_gate_eval.py +0 -0
  82. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_gate_eval_calibration.py +0 -0
  83. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_gh_backend.py +0 -0
  84. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_gh_features.py +0 -0
  85. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_git_env_isolation.py +0 -0
  86. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_hashed_denylist.py +0 -0
  87. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_hashed_denylist_ci.py +0 -0
  88. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_init_integration.py +0 -0
  89. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_interpreter_normalization.py +0 -0
  90. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_leak_findings_redaction.py +0 -0
  91. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_leak_scan.py +0 -0
  92. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_leak_scan_content.py +0 -0
  93. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_learnings_query.py +0 -0
  94. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  95. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lifecycle_integration.py +0 -0
  96. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_bare_produces_path.py +0 -0
  97. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_close_intermediate.py +0 -0
  98. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_close_wu.py +0 -0
  99. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_correlation_id.py +0 -0
  100. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  101. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_oracle_env.py +0 -0
  102. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_plan_errors.py +0 -0
  103. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_plan_next_draft.py +0 -0
  104. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_plan_relative_import.py +0 -0
  105. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_produces_driver_helper.py +0 -0
  106. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_sections.py +0 -0
  107. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  108. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_lint_verification_reference.py +0 -0
  109. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_attempt_evidence.py +0 -0
  110. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_auto_archive.py +0 -0
  111. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_caveman_preamble.py +0 -0
  112. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_close_intermediate.py +0 -0
  113. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_defaults_by_type.py +0 -0
  114. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_effort.py +0 -0
  115. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_failure_note_cap.py +0 -0
  116. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_files_changed_guard.py +0 -0
  117. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_gate_budget.py +0 -0
  118. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_model_alias.py +0 -0
  119. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_orchestration.py +0 -0
  120. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_post_pass_invariant.py +0 -0
  121. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_reset_preserving_events.py +0 -0
  122. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_smoke_runner.py +0 -0
  123. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_unsandboxed.py +0 -0
  124. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_loop_zero_token_guard.py +0 -0
  125. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_migrate_legacy.py +0 -0
  126. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_miniyaml_equivalence.py +0 -0
  127. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_miniyaml_negative.py +0 -0
  128. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_planned_cost_lint.py +0 -0
  129. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_prepare_feature.py +0 -0
  130. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_prepare_scaffold_sync.py +0 -0
  131. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_produces_field.py +0 -0
  132. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_redaction_windows_home.py +0 -0
  133. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_resolve_base.py +0 -0
  134. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_result_block.py +0 -0
  135. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_roadmap_add_skill.py +0 -0
  136. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_roadmap_archive_skill.py +0 -0
  137. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_roadmap_row_parser.py +0 -0
  138. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_data_in_sync.py +0 -0
  139. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_doc_hygiene.py +0 -0
  140. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_docs.py +0 -0
  141. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_init.py +0 -0
  142. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_manifest.py +0 -0
  143. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_resources.py +0 -0
  144. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_seed_sanity.py +0 -0
  145. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_upgrade.py +0 -0
  146. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_scaffold_wiring.py +0 -0
  147. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_skills_vendored_in_sync.py +0 -0
  148. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_squash_commit_hook_crash.py +0 -0
  149. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_template_closing_shapes.py +0 -0
  150. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_terminal_flip_ownership.py +0 -0
  151. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_terminal_flips.py +0 -0
  152. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_timeout_kill_portable.py +0 -0
  153. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_untracked_feature_folder.py +0 -0
  154. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_untracked_file_absorption.py +0 -0
  155. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_upgrade_integration.py +0 -0
  156. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_upgrade_merge_gate.py +0 -0
  157. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_validate_event.py +0 -0
  158. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_verdict_coupling.py +0 -0
  159. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_verify_empty_gate_set.py +0 -0
  160. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_version_consistency.py +0 -0
  161. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_version_skew.py +0 -0
  162. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/tests/test_windows_gate_exec.py +0 -0
  163. {specfuse_loop-0.3.19 → specfuse_loop-0.3.20}/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.19
3
+ Version: 0.3.20
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.19"
7
+ version = "0.3.20"
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.20
@@ -139,7 +139,13 @@ auto-close only eliminates the reflective overhead, not the forward-design step.
139
139
  full close ceremony regardless of predicate outcome. Alternatively, set
140
140
  `auto_close_disabled: true` in a feature's `PLAN.md` frontmatter to disable
141
141
  auto-close permanently for that feature (e.g., for features that are inherently
142
- exploratory and expect off-plan behavior).
142
+ exploratory and expect off-plan behavior). For a single **substantive close**
143
+ — a `close` (or `close-intermediate`) WU whose acceptance criteria assert on the
144
+ source tree, reconcile prior gates, or observe runtime behaviour rather than
145
+ just writing the retrospective stub — set `auto_close_disabled: true` in **that
146
+ WU's** frontmatter. The predicate keys on the implementation WUs' cost and
147
+ plan-conformance and cannot see what a close verifies, so a substantive close
148
+ must be marked must-run or its verification is silently skipped (#189).
143
149
 
144
150
  **Predicate-version transparency** — every `auto_close_decision` event in
145
151
  `events.jsonl` carries a `predicate_version` field (e.g., `predicate_version:
@@ -63,7 +63,7 @@ SPECFUSE_DIR = Path(".specfuse")
63
63
  REPO_ROOT = SPECFUSE_DIR.parent
64
64
  FEATURES_DIR = SPECFUSE_DIR / "features"
65
65
  VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
66
- DRIVER_VERSION = "0.3.19"
66
+ DRIVER_VERSION = "0.3.20"
67
67
  # Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
68
68
  # version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
69
69
  # the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
@@ -2778,6 +2778,56 @@ def _already_auto_closed(wu_file: Path) -> bool:
2778
2778
  return auto in (True, "true", "True")
2779
2779
 
2780
2780
 
2781
+ def _gate_impl_deliverables_present(
2782
+ feature_dir: Path, gate: "GateNode",
2783
+ ) -> tuple[bool, str]:
2784
+ """Return (True, "") iff every implementation WU in *gate* has its declared
2785
+ ``produces:`` deliverables present and non-empty on disk (#190).
2786
+
2787
+ Auto-close keys on WU cost/plan-conformance and trusts each WU's ``done``
2788
+ status; ``assert_declared_deliverables`` — the dispatch-time presence gate —
2789
+ never runs on an auto-closed gate, so a WU that reported ``done`` with its
2790
+ ``produces:`` paths absent still counts as a clean gate. Running the same
2791
+ check here stops the predicate auto-closing over a ``done`` that did not
2792
+ deliver. Non-implementation WUs and WUs with no ``produces:`` are skipped
2793
+ (the latter is assert_declared_deliverables's own opt-out). A missing or
2794
+ malformed WU file is left to the predicate / dispatch path that already
2795
+ handles it, not crashed on here.
2796
+ """
2797
+ for ref in gate.refs:
2798
+ try:
2799
+ wu = load_wu(feature_dir, ref)
2800
+ except (FileNotFoundError, KeyError, _miniyaml.MiniYAMLError):
2801
+ continue
2802
+ if wu.type != "implementation":
2803
+ continue
2804
+ ok, summary = assert_declared_deliverables(wu)
2805
+ if not ok:
2806
+ return False, f"{ref.get('id', wu.wu_id)}: {summary}"
2807
+ return True, ""
2808
+
2809
+
2810
+ def _close_wu_disables_auto_close(close_wu: "WorkUnit | None") -> bool:
2811
+ """True iff the close WU's frontmatter sets ``auto_close_disabled`` truthy.
2812
+
2813
+ Per-WU counterpart to PLAN.md's feature-level ``auto_close_disabled``
2814
+ (#189). The auto-close predicate keys on the implementation WUs' cost and
2815
+ plan-conformance; it has no signal for what the CLOSE WU itself is
2816
+ contracted to verify. A substantive close — one whose acceptance criteria
2817
+ assert on the source tree, reconcile prior gates, or observe runtime
2818
+ behaviour — must be dispatched and run, not optimized away. The plan author
2819
+ marks such a close with ``auto_close_disabled: true`` in its frontmatter;
2820
+ the predicate then always defers to dispatch for that gate.
2821
+
2822
+ Distinct from the ``auto_close`` key, which is the auto-close OUTPUT marker
2823
+ (set to ``true`` by mark_close_wu_auto_closed after a gate auto-closes).
2824
+ """
2825
+ if close_wu is None or not close_wu.file.is_file():
2826
+ return False
2827
+ fm, _ = read_frontmatter(close_wu.file)
2828
+ return fm.get("auto_close_disabled") in (True, "true", "True")
2829
+
2830
+
2781
2831
  def write_stub_retrospective_terminal(
2782
2832
  feature_dir: Path,
2783
2833
  gate_number: int,
@@ -2881,9 +2931,32 @@ def maybe_auto_close_terminal(
2881
2931
  feature_id=feature_id,
2882
2932
  predicate_version="v1",
2883
2933
  )
2934
+ if _close_wu_disables_auto_close(close_wu_for_terminal):
2935
+ # Substantive close marked must-run (#189): dispatch it, don't skip.
2936
+ return False, AutoCloseDecision(
2937
+ auto=False,
2938
+ reasons=["auto_close_disabled_per_wu"],
2939
+ metrics={},
2940
+ gate_id=gate.number,
2941
+ feature_id=feature_id,
2942
+ predicate_version="v1",
2943
+ )
2884
2944
  decision = evaluate_auto_close(feature_dir, gate.number)
2885
2945
  if not decision.auto:
2886
2946
  return False, decision
2947
+ present, reason = _gate_impl_deliverables_present(feature_dir, gate)
2948
+ if not present:
2949
+ # A gate WU reported done without its declared deliverable on disk;
2950
+ # do not auto-close over the discrepancy — dispatch the close, which is
2951
+ # where it surfaces (#190).
2952
+ return False, AutoCloseDecision(
2953
+ auto=False,
2954
+ reasons=[f"declared_deliverable_missing: {reason}"],
2955
+ metrics=decision.metrics,
2956
+ gate_id=gate.number,
2957
+ feature_id=feature_id,
2958
+ predicate_version=decision.predicate_version,
2959
+ )
2887
2960
  write_stub_retrospective_terminal(feature_dir, gate.number, decision)
2888
2961
  mark_close_wu_auto_closed(close_wu_for_terminal, decision)
2889
2962
  metrics = decision.metrics
@@ -2994,9 +3067,31 @@ def maybe_auto_close_intermediate(
2994
3067
  feature_id=feature_id,
2995
3068
  predicate_version="v1",
2996
3069
  )
3070
+ if _close_wu_disables_auto_close(close_intermediate_wu):
3071
+ # Substantive close-intermediate marked must-run (#189): dispatch it.
3072
+ return False, AutoCloseDecision(
3073
+ auto=False,
3074
+ reasons=["auto_close_disabled_per_wu"],
3075
+ metrics={},
3076
+ gate_id=gate.number,
3077
+ feature_id=feature_id,
3078
+ predicate_version="v1",
3079
+ )
2997
3080
  decision = evaluate_auto_close(feature_dir, gate.number)
2998
3081
  if not decision.auto:
2999
3082
  return False, decision
3083
+ present, reason = _gate_impl_deliverables_present(feature_dir, gate)
3084
+ if not present:
3085
+ # See maybe_auto_close_terminal: don't auto-close over a done WU whose
3086
+ # declared deliverable is absent — dispatch the close instead (#190).
3087
+ return False, AutoCloseDecision(
3088
+ auto=False,
3089
+ reasons=[f"declared_deliverable_missing: {reason}"],
3090
+ metrics=decision.metrics,
3091
+ gate_id=gate.number,
3092
+ feature_id=feature_id,
3093
+ predicate_version=decision.predicate_version,
3094
+ )
3000
3095
  append_stub_retrospective_intermediate(feature_dir, gate.number, decision)
3001
3096
  if close_intermediate_wu is not None:
3002
3097
  write_frontmatter_field(close_intermediate_wu.file, "status", "done")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.19
3
+ Version: 0.3.20
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
@@ -245,6 +245,17 @@ class TestAutoCloseIntermediateFiresOnCleanGate(unittest.TestCase):
245
245
  self.events_path, self.root, self.ci_wu, self.plan_next_wu,
246
246
  )
247
247
 
248
+ def test_per_wu_opt_out_refuses(self):
249
+ # #189: close-intermediate marked auto_close_disabled → not auto-closed.
250
+ self.ci_wu.file.write_text(
251
+ f"---\nid: {self.feature_id}/G1-CI\ntype: close-intermediate\n"
252
+ f"model: opus\nstatus: pending\nattempts: 0\n"
253
+ f"auto_close_disabled: true\n---\n\n# CI\n"
254
+ )
255
+ result, decision = self._call()
256
+ self.assertFalse(result)
257
+ self.assertIn("auto_close_disabled_per_wu", decision.reasons)
258
+
248
259
  def test_returns_true_and_decision(self):
249
260
  result, decision = self._call()
250
261
  self.assertTrue(result)
@@ -256,6 +256,117 @@ class TestMaybeAutoCloseTerminalAutoPath(unittest.TestCase):
256
256
  # ---------------------------------------------------------------------------
257
257
 
258
258
 
259
+ class TestMaybeAutoCloseTerminalPerWuOptOut(unittest.TestCase):
260
+ """#189: a close WU marked auto_close_disabled must NOT be auto-closed,
261
+ even on an otherwise clean/on-plan gate — it is dispatched instead."""
262
+
263
+ def setUp(self):
264
+ self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
265
+ self.root = Path(self._tmp.name)
266
+ self.feature_id = "FEAT-2026-9981"
267
+ self.gate_num = 1
268
+ fdir = self.root
269
+ self.fdir = fdir
270
+ _write_plan_md(fdir, self.feature_id, self.gate_num)
271
+ _write_wu_impl(fdir, f"{self.feature_id}/T01")
272
+ # Close WU carries the per-WU opt-out.
273
+ (fdir / "WU-close.md").write_text(
274
+ f"---\nid: {self.feature_id}/G{self.gate_num}-CLOSE\ntype: close\n"
275
+ f"model: opus\nstatus: pending\nattempts: 0\n"
276
+ f"auto_close_disabled: true\n---\n\n# Close\n"
277
+ )
278
+ _write_gate_file(fdir, self.gate_num)
279
+ _write_task_completed_event(fdir, f"{self.feature_id}/T01")
280
+ self.gate = _make_gate_node(fdir, self.feature_id, self.gate_num)
281
+ self.close_wu = _make_close_wu(
282
+ fdir, f"{self.feature_id}/G{self.gate_num}-CLOSE")
283
+ self.events_path = fdir / "events.jsonl"
284
+
285
+ def tearDown(self):
286
+ self._tmp.cleanup()
287
+
288
+ def test_refuses_and_names_reason(self):
289
+ result, decision = loop.maybe_auto_close_terminal(
290
+ self.fdir, self.feature_id, self.gate, [self.gate],
291
+ self.events_path, self.close_wu, repo_root=self.root,
292
+ )
293
+ self.assertFalse(result)
294
+ self.assertFalse(decision.auto)
295
+ self.assertIn("auto_close_disabled_per_wu", decision.reasons)
296
+
297
+ def test_close_wu_not_marked_auto_closed(self):
298
+ loop.maybe_auto_close_terminal(
299
+ self.fdir, self.feature_id, self.gate, [self.gate],
300
+ self.events_path, self.close_wu, repo_root=self.root,
301
+ )
302
+ fm, _ = loop.read_frontmatter(self.close_wu.file)
303
+ # Status untouched (still dispatchable); no auto_close output marker.
304
+ self.assertEqual(fm.get("status"), "pending")
305
+ self.assertNotEqual(fm.get("auto_close"), True)
306
+
307
+ def test_helper_reads_the_flag(self):
308
+ self.assertTrue(loop._close_wu_disables_auto_close(self.close_wu))
309
+
310
+
311
+ class TestMaybeAutoCloseTerminalDeliverableMissing(unittest.TestCase):
312
+ """#190: auto-close must refuse when an implementation WU reports done but
313
+ its declared `produces:` deliverable is absent — dispatch the close."""
314
+
315
+ def setUp(self):
316
+ self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
317
+ self.root = Path(self._tmp.name)
318
+ self.feature_id = "FEAT-2026-9982"
319
+ self.gate_num = 1
320
+ fdir = self.root
321
+ self.fdir = fdir
322
+ _write_plan_md(fdir, self.feature_id, self.gate_num)
323
+ # Implementation WU claims done + a produces path that does NOT exist.
324
+ missing = fdir / "src" / "never-created.py"
325
+ (fdir / "WU-01.md").write_text(
326
+ f"---\nid: {self.feature_id}/T01\ntype: implementation\n"
327
+ f"model: sonnet\nstatus: done\nattempts: 1\ncost_usd: 0.5\n"
328
+ f"planned_cost_usd: 0.5\nproduces: [{missing}]\n---\n\n# T01\n"
329
+ )
330
+ _write_wu_close(fdir, f"{self.feature_id}/G{self.gate_num}-CLOSE")
331
+ _write_gate_file(fdir, self.gate_num)
332
+ _write_task_completed_event(fdir, f"{self.feature_id}/T01")
333
+ self.gate = _make_gate_node(fdir, self.feature_id, self.gate_num)
334
+ self.close_wu = _make_close_wu(
335
+ fdir, f"{self.feature_id}/G{self.gate_num}-CLOSE")
336
+ self.events_path = fdir / "events.jsonl"
337
+
338
+ def tearDown(self):
339
+ self._tmp.cleanup()
340
+
341
+ def _call(self):
342
+ return loop.maybe_auto_close_terminal(
343
+ self.fdir, self.feature_id, self.gate, [self.gate],
344
+ self.events_path, self.close_wu, repo_root=self.root,
345
+ )
346
+
347
+ def test_refuses_on_missing_deliverable(self):
348
+ result, decision = self._call()
349
+ self.assertFalse(result)
350
+ self.assertFalse(decision.auto)
351
+ self.assertTrue(any("declared_deliverable_missing" in r
352
+ for r in decision.reasons),
353
+ f"reasons={decision.reasons}")
354
+
355
+ def test_present_deliverable_still_auto_closes(self):
356
+ # Create the declared deliverable → predicate proceeds to auto-close.
357
+ missing = self.fdir / "src" / "never-created.py"
358
+ missing.parent.mkdir(parents=True, exist_ok=True)
359
+ missing.write_text("x = 1\n")
360
+ result, decision = self._call()
361
+ self.assertTrue(result)
362
+ self.assertTrue(decision.auto)
363
+
364
+ def test_helper_reports_the_offender(self):
365
+ ok, reason = loop._gate_impl_deliverables_present(self.fdir, self.gate)
366
+ self.assertFalse(ok)
367
+ self.assertIn("never-created.py", reason)
368
+
369
+
259
370
  class TestMaybeAutoCloseTerminalNonAutoPath(unittest.TestCase):
260
371
 
261
372
  def setUp(self):
@@ -1 +0,0 @@
1
- 0.3.19
File without changes
File without changes
File without changes
File without changes