specfuse-loop 0.3.19__tar.gz → 0.3.21__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 (166) hide show
  1. {specfuse_loop-0.3.19/specfuse_loop.egg-info → specfuse_loop-0.3.21}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.21/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/docs/methodology.md +7 -1
  5. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/lint_plan.py +27 -1
  6. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/loop.py +416 -34
  7. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21/specfuse_loop.egg-info}/PKG-INFO +1 -1
  8. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse_loop.egg-info/SOURCES.txt +3 -0
  9. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_attempt_outcome_emission.py +50 -0
  10. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_gate_eval_intermediate_wiring.py +11 -0
  11. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_gate_eval_terminal_wiring.py +111 -0
  12. specfuse_loop-0.3.21/tests/test_lifetime_accounting.py +240 -0
  13. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_planned_cost_lint.py +72 -0
  14. specfuse_loop-0.3.21/tests/test_produces_in_diff.py +264 -0
  15. specfuse_loop-0.3.21/tests/test_spinning_rearm_gate.py +308 -0
  16. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_terminal_flips.py +85 -0
  17. specfuse_loop-0.3.19/specfuse/loop/data/VERSION +0 -1
  18. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/LICENSE +0 -0
  19. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/NOTICE +0 -0
  20. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/README.md +0 -0
  21. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/setup.cfg +0 -0
  22. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/__init__.py +0 -0
  23. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/_filelock.py +0 -0
  24. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/_miniyaml.py +0 -0
  25. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/adopt_feature.py +0 -0
  26. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  27. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  28. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  29. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/docs/getting-started.md +0 -0
  30. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/docs/skills.md +0 -0
  31. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/gitignore.snippet +0 -0
  32. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/roadmap.template.md +0 -0
  33. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  34. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/rules/never-touch.md +0 -0
  35. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/rules/result-contract.md +0 -0
  36. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  37. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  38. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/schemas/event.schema.json +0 -0
  39. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  40. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  41. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  42. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  43. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/templates/GATE.template.md +0 -0
  44. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  45. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/templates/WU.template.md +0 -0
  46. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/data/verification.yml.example +0 -0
  47. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/gate_eval.py +0 -0
  48. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/gh_backend.py +0 -0
  49. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/gh_features.py +0 -0
  50. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/scaffold.py +0 -0
  51. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse/loop/validate_event.py +0 -0
  52. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  53. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse_loop.egg-info/entry_points.txt +0 -0
  54. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse_loop.egg-info/requires.txt +0 -0
  55. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/specfuse_loop.egg-info/top_level.txt +0 -0
  56. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_adopt_feature.py +0 -0
  57. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  58. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_autoclose_deferral_visibility.py +0 -0
  59. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_autosync.py +0 -0
  60. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_autosync_consent.py +0 -0
  61. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_autosync_firstrun.py +0 -0
  62. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_autosync_plugin.py +0 -0
  63. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_backend.py +0 -0
  64. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_bash_routing.py +0 -0
  65. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  66. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  67. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_bookkeeping_no_verify.py +0 -0
  68. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_claude_resolution.py +0 -0
  69. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_close_wu_self_reference.py +0 -0
  70. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_closing_deliverable_guard.py +0 -0
  71. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_console_encoding.py +0 -0
  72. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_cost_tracking.py +0 -0
  73. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_degraded_oracle.py +0 -0
  74. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_deliverable_presence_gate.py +0 -0
  75. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_doctor.py +0 -0
  76. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_driver_integration.py +0 -0
  77. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_driver_lock.py +0 -0
  78. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_duration_tracking.py +0 -0
  79. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_empty_files_escalation.py +0 -0
  80. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_ensure_feature_branch.py +0 -0
  81. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_ensure_feature_branch_base.py +0 -0
  82. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_events_redaction.py +0 -0
  83. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_extra_gates.py +0 -0
  84. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_filelock_portable.py +0 -0
  85. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_force_full_close.py +0 -0
  86. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_gate_eval.py +0 -0
  87. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_gate_eval_calibration.py +0 -0
  88. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_gh_backend.py +0 -0
  89. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_gh_features.py +0 -0
  90. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_git_env_isolation.py +0 -0
  91. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_hashed_denylist.py +0 -0
  92. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_hashed_denylist_ci.py +0 -0
  93. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_init_integration.py +0 -0
  94. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_interpreter_normalization.py +0 -0
  95. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_leak_findings_redaction.py +0 -0
  96. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_leak_scan.py +0 -0
  97. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_leak_scan_content.py +0 -0
  98. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_learnings_query.py +0 -0
  99. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  100. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lifecycle_integration.py +0 -0
  101. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_bare_produces_path.py +0 -0
  102. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_close_intermediate.py +0 -0
  103. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_close_wu.py +0 -0
  104. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_correlation_id.py +0 -0
  105. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  106. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_oracle_env.py +0 -0
  107. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_plan_errors.py +0 -0
  108. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_plan_next_draft.py +0 -0
  109. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_plan_relative_import.py +0 -0
  110. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_produces_driver_helper.py +0 -0
  111. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_sections.py +0 -0
  112. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  113. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_lint_verification_reference.py +0 -0
  114. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_attempt_evidence.py +0 -0
  115. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_auto_archive.py +0 -0
  116. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_caveman_preamble.py +0 -0
  117. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_close_intermediate.py +0 -0
  118. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_defaults_by_type.py +0 -0
  119. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_effort.py +0 -0
  120. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_failure_note_cap.py +0 -0
  121. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_files_changed_guard.py +0 -0
  122. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_gate_budget.py +0 -0
  123. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_model_alias.py +0 -0
  124. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_orchestration.py +0 -0
  125. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_post_pass_invariant.py +0 -0
  126. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_reset_preserving_events.py +0 -0
  127. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_smoke_runner.py +0 -0
  128. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_unsandboxed.py +0 -0
  129. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_loop_zero_token_guard.py +0 -0
  130. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_migrate_legacy.py +0 -0
  131. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_miniyaml_equivalence.py +0 -0
  132. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_miniyaml_negative.py +0 -0
  133. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_prepare_feature.py +0 -0
  134. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_prepare_scaffold_sync.py +0 -0
  135. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_produces_field.py +0 -0
  136. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_redaction_windows_home.py +0 -0
  137. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_resolve_base.py +0 -0
  138. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_result_block.py +0 -0
  139. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_roadmap_add_skill.py +0 -0
  140. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_roadmap_archive_skill.py +0 -0
  141. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_roadmap_row_parser.py +0 -0
  142. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_data_in_sync.py +0 -0
  143. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_doc_hygiene.py +0 -0
  144. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_docs.py +0 -0
  145. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_init.py +0 -0
  146. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_manifest.py +0 -0
  147. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_resources.py +0 -0
  148. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_seed_sanity.py +0 -0
  149. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_upgrade.py +0 -0
  150. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_scaffold_wiring.py +0 -0
  151. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_skills_vendored_in_sync.py +0 -0
  152. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_squash_commit_hook_crash.py +0 -0
  153. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_template_closing_shapes.py +0 -0
  154. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_terminal_flip_ownership.py +0 -0
  155. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_timeout_kill_portable.py +0 -0
  156. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_untracked_feature_folder.py +0 -0
  157. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_untracked_file_absorption.py +0 -0
  158. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_upgrade_integration.py +0 -0
  159. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_upgrade_merge_gate.py +0 -0
  160. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_validate_event.py +0 -0
  161. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_verdict_coupling.py +0 -0
  162. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_verify_empty_gate_set.py +0 -0
  163. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_version_consistency.py +0 -0
  164. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_version_skew.py +0 -0
  165. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/tests/test_windows_gate_exec.py +0 -0
  166. {specfuse_loop-0.3.19 → specfuse_loop-0.3.21}/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.21
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.21"
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.21
@@ -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:
@@ -248,12 +248,23 @@ def _find_task_graph_block(body: str) -> dict | None:
248
248
  return None
249
249
 
250
250
 
251
+ # Ceremony WU types systematically under-estimated in the field: close and
252
+ # plan-next WUs ran 2.8-5.2x over on $2-3 estimates across FEAT-2026-0049
253
+ # (clabonte/generator), poisoning every calibration built on the plans (#201).
254
+ # The floor is a WARN threshold, not a cap — estimates below it are almost
255
+ # certainly wishful, not cheap.
256
+ CEREMONY_COST_FLOOR_USD = 5.0
257
+ _CEREMONY_TYPES = frozenset({"close", "close-intermediate", "plan-next"})
258
+
259
+
251
260
  def check_planned_cost(feature_dir: Path, plan_fm: dict, gates: list) -> None:
252
261
  """Emit WARN for missing planned_cost_usd on WUs and PLAN.md.
253
262
 
254
263
  Sealed WUs (wu status=done AND plan status=done) are skipped silently —
255
264
  backfilling cost estimates on history is pointless. Active or draft WUs
256
- get the WARN. PLAN.md is compared against the sum of WU planned costs;
265
+ get the WARN. Ceremony-type WUs (close/close-intermediate/plan-next)
266
+ with a planned cost below CEREMONY_COST_FLOOR_USD get a floor WARN
267
+ (#201). PLAN.md is compared against the sum of WU planned costs;
257
268
  delta > 10% emits a separate WARN naming the delta. Never raises or
258
269
  appends to an errors list — all findings are WARN-only (exit code 0).
259
270
  """
@@ -283,6 +294,21 @@ def check_planned_cost(feature_dir: Path, plan_fm: dict, gates: list) -> None:
283
294
  )
284
295
  if planned is not None:
285
296
  wu_sum += float(planned)
297
+ # Floor applies to POSITIVE estimates only: the observed
298
+ # failure is a real-but-wishful $2-3 estimate anchoring
299
+ # calibration, not an explicit 0.00 (the scaffold's
300
+ # "unestimated" placeholder, already visible as such).
301
+ if (not is_sealed
302
+ and wfm.get("type") in _CEREMONY_TYPES
303
+ and 0 < float(planned) < CEREMONY_COST_FLOOR_USD):
304
+ print(
305
+ f"WARN: {wfile}: planned_cost_usd "
306
+ f"${float(planned):.2f} is below the "
307
+ f"${CEREMONY_COST_FLOOR_USD:.2f} floor for "
308
+ f"'{wfm.get('type')}' WUs. Ceremony WUs ran 2.8-5.2x "
309
+ f"over on $2-3 estimates (FEAT-2026-0049); raise the "
310
+ f"estimate rather than anchoring calibration on it."
311
+ )
286
312
 
287
313
  wu_sum = round(wu_sum, 2)
288
314