specfuse-loop 0.9.2__tar.gz → 0.9.3__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 (313) hide show
  1. {specfuse_loop-0.9.2/specfuse_loop.egg-info → specfuse_loop-0.9.3}/PKG-INFO +1 -1
  2. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/pyproject.toml +1 -1
  3. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/cost.py +21 -16
  4. specfuse_loop-0.9.3/specfuse/loop/data/VERSION +1 -0
  5. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/templates/WU.template.md +9 -1
  6. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/loop.py +38 -9
  7. specfuse_loop-0.9.3/specfuse/loop/rearm_migration.py +267 -0
  8. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3/specfuse_loop.egg-info}/PKG-INFO +1 -1
  9. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/SOURCES.txt +4 -0
  10. specfuse_loop-0.9.3/tests/test_fold_contract_documented.py +127 -0
  11. specfuse_loop-0.9.3/tests/test_rearm_fold_marker.py +143 -0
  12. specfuse_loop-0.9.3/tests/test_rearm_migration.py +306 -0
  13. specfuse_loop-0.9.2/specfuse/loop/data/VERSION +0 -1
  14. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/LICENSE +0 -0
  15. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/NOTICE +0 -0
  16. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/README.md +0 -0
  17. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/setup.cfg +0 -0
  18. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/__init__.py +0 -0
  19. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/_filelock.py +0 -0
  20. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/_miniyaml.py +0 -0
  21. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/_wu_sections.py +0 -0
  22. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/adopt_feature.py +0 -0
  23. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/arm_eval.py +0 -0
  24. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/arm_sweep.py +0 -0
  25. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/arm_txn.py +0 -0
  26. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/changelog.py +0 -0
  27. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/closing_requirements.py +0 -0
  28. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/CHANGELOG.seed.md +0 -0
  29. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  30. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/concepts/adopting-auto-mode.md +0 -0
  31. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  32. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/concepts/autonomy-stop-classes.md +0 -0
  33. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  34. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/getting-started.md +0 -0
  35. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/methodology.md +0 -0
  36. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/skills.md +0 -0
  37. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/gitignore.snippet +0 -0
  38. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
  39. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
  40. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/monitoring.yml.example +0 -0
  41. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/roadmap.template.md +0 -0
  42. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/close-discipline.md +0 -0
  43. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  44. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
  45. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/never-touch.md +0 -0
  46. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/operator-escalation.md +0 -0
  47. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/planning-discipline.md +0 -0
  48. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/result-contract.md +0 -0
  49. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  50. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  51. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules-local/README.md +0 -0
  52. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/driver-event.schema.json +0 -0
  53. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/event.schema.json +0 -0
  54. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  55. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  56. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  57. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  58. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/templates/GATE.template.md +0 -0
  59. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/templates/LEARNINGS-pending.template.md +0 -0
  60. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  61. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/verification.yml.example +0 -0
  62. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
  63. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/escalation.py +0 -0
  64. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/events_stats.py +0 -0
  65. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/gate_commands.py +0 -0
  66. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/gate_eval.py +0 -0
  67. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/gh_backend.py +0 -0
  68. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/gh_features.py +0 -0
  69. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/labels.py +0 -0
  70. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/lint_closing.py +0 -0
  71. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/lint_monitoring.py +0 -0
  72. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/lint_plan.py +0 -0
  73. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/lint_roadmap.py +0 -0
  74. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/plan_baseline.py +0 -0
  75. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/scaffold.py +0 -0
  76. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/validate_event.py +0 -0
  77. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/__init__.py +0 -0
  78. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/adapters.py +0 -0
  79. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/artifact.py +0 -0
  80. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/autofix.py +0 -0
  81. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/autofix_invoke.py +0 -0
  82. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/autofix_run.py +0 -0
  83. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/autofix_state.py +0 -0
  84. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/cli.py +0 -0
  85. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/diagnose_cli.py +0 -0
  86. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/diagnosis.py +0 -0
  87. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/fingerprint.py +0 -0
  88. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/issues.py +0 -0
  89. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/providers/__init__.py +0 -0
  90. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/providers/_azure_auth.py +0 -0
  91. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/providers/azure_app_insights.py +0 -0
  92. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/providers/azure_service_bus.py +0 -0
  93. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/redaction.py +0 -0
  94. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/schedule.py +0 -0
  95. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  96. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/entry_points.txt +0 -0
  97. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/requires.txt +0 -0
  98. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/top_level.txt +0 -0
  99. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_accept_hedged_close_headline.py +0 -0
  100. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_accept_hedged_close_skill.py +0 -0
  101. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_adopt_feature.py +0 -0
  102. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_app_insights_adapters.py +0 -0
  103. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_eval.py +0 -0
  104. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_eval_lint_class.py +0 -0
  105. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_eval_wiring.py +0 -0
  106. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  107. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_sweep.py +0 -0
  108. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_sweep_gate.py +0 -0
  109. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_txn.py +0 -0
  110. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_wiring.py +0 -0
  111. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_artifact_redaction.py +0 -0
  112. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_attempt_outcome_contract.py +0 -0
  113. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_attempt_outcome_emission.py +0 -0
  114. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_attention_nonwriting_guard.py +0 -0
  115. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_attention_skill_structure.py +0 -0
  116. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autoclose_deferral_visibility.py +0 -0
  117. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_invoke.py +0 -0
  118. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_live.py +0 -0
  119. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_predicate.py +0 -0
  120. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_run.py +0 -0
  121. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_state.py +0 -0
  122. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync.py +0 -0
  123. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync_consent.py +0 -0
  124. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync_firstrun.py +0 -0
  125. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync_no_cwd_leak.py +0 -0
  126. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync_plugin.py +0 -0
  127. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_azure_credential_resolution.py +0 -0
  128. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_backend.py +0 -0
  129. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_baseline_escalation_message.py +0 -0
  130. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_baseline_persistence.py +0 -0
  131. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_baseline_probe.py +0 -0
  132. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bash_routing.py +0 -0
  133. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bats_suites_gated.py +0 -0
  134. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  135. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  136. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bookkeeping_no_verify.py +0 -0
  137. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_changelog_collection.py +0 -0
  138. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_changelog_release_wiring.py +0 -0
  139. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_changelog_schema.py +0 -0
  140. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_ci_runs_every_gate.py +0 -0
  141. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_claude_resolution.py +0 -0
  142. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_close_wu_self_reference.py +0 -0
  143. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_closing_deliverable_guard.py +0 -0
  144. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_closing_guard_contracts.py +0 -0
  145. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_closing_guard_prediction.py +0 -0
  146. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_closing_requirements.py +0 -0
  147. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_console_encoding.py +0 -0
  148. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_correlation_id_override.py +0 -0
  149. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_correlation_id_two_surfaces.py +0 -0
  150. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_cost_lifetime.py +0 -0
  151. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_cost_tracking.py +0 -0
  152. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_degraded_oracle.py +0 -0
  153. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_deliverable_presence_gate.py +0 -0
  154. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_derive_monitoring_discovery.py +0 -0
  155. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_derive_monitoring_skill_registration.py +0 -0
  156. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_design_for_diagnosis_rule.py +0 -0
  157. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_diagnose_headless.py +0 -0
  158. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_diagnose_issue_skill.py +0 -0
  159. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_diagnosis.py +0 -0
  160. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_diagnosis_roundtrip_live.py +0 -0
  161. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_dispatch_skeleton.py +0 -0
  162. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_doctor.py +0 -0
  163. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_done_feature_gates.py +0 -0
  164. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_driver_event_registry_covers_emitters.py +0 -0
  165. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_driver_integration.py +0 -0
  166. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_driver_lock.py +0 -0
  167. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_duration_tracking.py +0 -0
  168. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_empty_files_escalation.py +0 -0
  169. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_ensure_feature_branch.py +0 -0
  170. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_ensure_feature_branch_base.py +0 -0
  171. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_escalation_contract.py +0 -0
  172. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_escalation_emit.py +0 -0
  173. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_event_gate_full_envelope.py +0 -0
  174. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_events_redaction.py +0 -0
  175. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_events_stats.py +0 -0
  176. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_extra_gates.py +0 -0
  177. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_failure_artifact_model.py +0 -0
  178. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_feature_id_resolution.py +0 -0
  179. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_feature_review_accumulation.py +0 -0
  180. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_filelock_portable.py +0 -0
  181. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_fingerprint.py +0 -0
  182. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_fix_bug_headless.py +0 -0
  183. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_force_full_close.py +0 -0
  184. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_budget.py +0 -0
  185. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_budget_post_dispatch.py +0 -0
  186. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_eval.py +0 -0
  187. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_eval_calibration.py +0 -0
  188. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  189. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_eval_terminal_wiring.py +0 -0
  190. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_report_verdict.py +0 -0
  191. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gh_backend.py +0 -0
  192. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gh_features.py +0 -0
  193. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_git_env_isolation.py +0 -0
  194. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gitleaks_pinning.py +0 -0
  195. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_hashed_denylist.py +0 -0
  196. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_hashed_denylist_ci.py +0 -0
  197. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_heartbeat_adapter.py +0 -0
  198. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_hedged_kind_contract.py +0 -0
  199. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_init_integration.py +0 -0
  200. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_interpreter_normalization.py +0 -0
  201. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_label_registry.py +0 -0
  202. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_label_registry_covers_consumers.py +0 -0
  203. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_leak_findings_redaction.py +0 -0
  204. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_leak_scan.py +0 -0
  205. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_leak_scan_content.py +0 -0
  206. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_learnings_query.py +0 -0
  207. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_learnings_staging.py +0 -0
  208. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  209. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lifecycle_integration.py +0 -0
  210. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lifetime_accounting.py +0 -0
  211. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_bare_produces_path.py +0 -0
  212. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_boundary_consistency.py +0 -0
  213. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_boundary_extraction.py +0 -0
  214. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_close_intermediate.py +0 -0
  215. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_close_wu.py +0 -0
  216. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_closing.py +0 -0
  217. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_correlation_id.py +0 -0
  218. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  219. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_monitoring.py +0 -0
  220. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_oracle_env.py +0 -0
  221. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_contract_fields.py +0 -0
  222. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_errors.py +0 -0
  223. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_next_draft.py +0 -0
  224. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_relative_import.py +0 -0
  225. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_verdict_exempt.py +0 -0
  226. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_planning_sections.py +0 -0
  227. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_produces_driver_helper.py +0 -0
  228. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_produces_satisfiability.py +0 -0
  229. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_roadmap.py +0 -0
  230. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_sections.py +0 -0
  231. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  232. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_verification_reference.py +0 -0
  233. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_attempt_evidence.py +0 -0
  234. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_auto_archive.py +0 -0
  235. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_caveman_preamble.py +0 -0
  236. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_close_intermediate.py +0 -0
  237. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_defaults_by_type.py +0 -0
  238. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_effort.py +0 -0
  239. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_failure_note_cap.py +0 -0
  240. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_files_changed_guard.py +0 -0
  241. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_gate_budget.py +0 -0
  242. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_model_alias.py +0 -0
  243. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_orchestration.py +0 -0
  244. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_post_pass_invariant.py +0 -0
  245. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_reset_preserving_events.py +0 -0
  246. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_smoke_runner.py +0 -0
  247. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_unsandboxed.py +0 -0
  248. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_zero_token_guard.py +0 -0
  249. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_migrate_legacy.py +0 -0
  250. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_miniyaml_equivalence.py +0 -0
  251. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_miniyaml_negative.py +0 -0
  252. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitor_cli.py +0 -0
  253. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitor_issue_lifecycle.py +0 -0
  254. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitor_runner_surfaces.py +0 -0
  255. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
  256. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_cron_dialect.py +0 -0
  257. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_example.py +0 -0
  258. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_fenced_blocks.py +0 -0
  259. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_seed.py +0 -0
  260. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_no_pytest_subprocess.py +0 -0
  261. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_operator_escalation_rule.py +0 -0
  262. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_plan_baseline.py +0 -0
  263. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_planned_cost_lint.py +0 -0
  264. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_planning_cost_floor.py +0 -0
  265. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_prepare_feature.py +0 -0
  266. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_prepare_scaffold_sync.py +0 -0
  267. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_produces_field.py +0 -0
  268. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_produces_in_diff.py +0 -0
  269. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_produces_semantics_unified.py +0 -0
  270. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_produces_shape.py +0 -0
  271. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_provision_labels.py +0 -0
  272. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_queue_stalled_adapter.py +0 -0
  273. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_redaction_windows_home.py +0 -0
  274. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_resolve_base.py +0 -0
  275. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_result_block.py +0 -0
  276. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_roadmap_add_skill.py +0 -0
  277. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_roadmap_archive_skill.py +0 -0
  278. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_roadmap_link_gate.py +0 -0
  279. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_roadmap_row_parser.py +0 -0
  280. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_routed_finding_tracking.py +0 -0
  281. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_data_in_sync.py +0 -0
  282. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_doc_hygiene.py +0 -0
  283. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_docs.py +0 -0
  284. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_init.py +0 -0
  285. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_label_provisioning.py +0 -0
  286. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_manifest.py +0 -0
  287. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_resources.py +0 -0
  288. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_seed_sanity.py +0 -0
  289. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_upgrade.py +0 -0
  290. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_wiring.py +0 -0
  291. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_schedule_dialect.py +0 -0
  292. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_service_bus_dlq_adapter.py +0 -0
  293. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_skill_discovery_links.py +0 -0
  294. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_skills_vendored_in_sync.py +0 -0
  295. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_spinning_rearm_gate.py +0 -0
  296. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_squash_commit_hook_crash.py +0 -0
  297. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_template_closing_shapes.py +0 -0
  298. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_terminal_flip_ownership.py +0 -0
  299. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_terminal_flips.py +0 -0
  300. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_timeout_kill_portable.py +0 -0
  301. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_unterminated_frontmatter.py +0 -0
  302. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_untracked_feature_folder.py +0 -0
  303. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_untracked_file_absorption.py +0 -0
  304. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_upgrade_integration.py +0 -0
  305. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_upgrade_merge_gate.py +0 -0
  306. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_validate_event.py +0 -0
  307. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_validate_event_driver_types.py +0 -0
  308. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_verdict_coupling.py +0 -0
  309. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_verify_empty_gate_set.py +0 -0
  310. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_version_consistency.py +0 -0
  311. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_version_skew.py +0 -0
  312. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_windows_gate_exec.py +0 -0
  313. {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/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.9.2
3
+ Version: 0.9.3
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.9.2"
7
+ version = "0.9.3"
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"
@@ -5,22 +5,27 @@
5
5
  """One lifetime-cost reader shared by both cost-gated consumers.
6
6
 
7
7
  `wu_lifetime_cost_usd` answers "what has this work unit cost over its whole
8
- life," correctly on both re-arm shapes recorded in FEAT-2026-0062/PLAN.md:
9
-
10
- * fold-ran — a re-arm folded the prior cycle's spend into
11
- `cumulative_cost_usd`, and `re_arm_history[].prior_cost_usd`
12
- names the same money.
13
- * fold-never-ran — the prior cycle's spend survives only in
14
- `re_arm_history[].prior_cost_usd`; `cumulative_cost_usd` is
15
- absent.
16
-
17
- Summing frontmatter fields cannot distinguish these shapes without replaying
18
- fold history, and naive summation double-counts the fold-ran shape. Instead
19
- this module sums `payload.cost_usd` across every `attempt_outcome` event for
20
- the work unit in `events.jsonl` — a source that already carries every
21
- dispatch cycle's cost exactly once, independent of fold shape. Frontmatter is
22
- used only as a fallback for the units this repo's corpus shows have no usable
23
- event history (see FEAT-2026-0062 criterion 9's measurement).
8
+ life." FEAT-2026-0067 converged the fold onto one path: every re-arm now
9
+ unconditionally folds the prior cycle's spend into `cumulative_cost_usd`
10
+ (driven by an explicit `folded_through_re_arm` marker, not by inferring state
11
+ from a value), so `cumulative_cost_usd` is the lifetime accumulator and
12
+ `re_arm_history[].prior_cost_usd` is a pure audit record of what each cycle
13
+ cost — not a second place the fold might or might not have run.
14
+
15
+ Before that migration, some already-re-armed WUs had a prior cycle's spend
16
+ survive only in `re_arm_history[].prior_cost_usd` with `cumulative_cost_usd`
17
+ absent — a pre-migration legacy shape that this module's frontmatter fallback
18
+ still tolerates for projects that have not run the FEAT-2026-0067 migration,
19
+ not an ongoing design a reader needs to branch on.
20
+
21
+ Summing frontmatter fields cannot distinguish "already folded" from "never
22
+ folded" without replaying fold history, and naive summation double-counts a
23
+ folded record. Instead this module sums `payload.cost_usd` across every
24
+ `attempt_outcome` event for the work unit in `events.jsonl` — a source that
25
+ already carries every dispatch cycle's cost exactly once, independent of fold
26
+ shape. Frontmatter is used only as a fallback for the units this repo's
27
+ corpus shows have no usable event history (see FEAT-2026-0062 criterion 9's
28
+ measurement).
24
29
 
25
30
  Deliberately dependency-light: this module must not import `loop.py` or
26
31
  `arm_eval.py` (see `arm_eval.py`'s module docstring on the `loop -> arm_eval`
@@ -0,0 +1 @@
1
+ 0.9.3
@@ -62,10 +62,18 @@ AUTHOR-SET FIELDS — fill or override these at draft/arm time:
62
62
  DRIVER-OWNED FIELDS - the driver writes these at outcome time; authors leave them absent:
63
63
  <!-- driver-owned: attempts, cost_usd, input_tokens, output_tokens, duration_seconds,
64
64
  cumulative_cost_usd, cumulative_duration_seconds, cumulative_input_tokens,
65
- cumulative_output_tokens, re_arm_count, re_arm_history -->
65
+ cumulative_output_tokens, re_arm_count, re_arm_history, folded_through_re_arm -->
66
66
  <!-- driver-stamped at dispatch (resolved execution metadata, visible in this .md):
67
67
  model, effort (override or type default), gate_set (the verification.yml set
68
68
  that is this WU's exit oracle), driver_version, started_at (UTC ISO). -->
69
+ <!-- `folded_through_re_arm` (FEAT-2026-0067): the `re_arm_count` value already
70
+ folded into the `cumulative_*` accumulators. On every re-arm the driver folds
71
+ the prior cycle's cost/duration/token spend into `cumulative_cost_usd`,
72
+ `cumulative_duration_seconds`, `cumulative_input_tokens`, and
73
+ `cumulative_output_tokens` — unconditionally, one fold path, including a
74
+ re-arm whose prior cycle cost nothing. `cumulative_*` is the lifetime
75
+ accumulator across every re-arm; `re_arm_history[].prior_*` is a pure audit
76
+ record of what each cycle cost, not a second place the fold might live. -->
69
77
  <!-- Full field semantics in docs/methodology.md §2 and events.jsonl outcome payloads. -->
70
78
 
71
79
  Dependencies live in PLAN.md's `gates[].work_units[].depends_on` graph, not
@@ -105,7 +105,7 @@ SPECFUSE_DIR = Path(".specfuse")
105
105
  REPO_ROOT = SPECFUSE_DIR.parent
106
106
  FEATURES_DIR = SPECFUSE_DIR / "features"
107
107
  VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
108
- DRIVER_VERSION = "0.9.2"
108
+ DRIVER_VERSION = "0.9.3"
109
109
  # Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
110
110
  # version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
111
111
  # the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
@@ -1672,20 +1672,35 @@ def write_cost_to_wu(backend, wu: WorkUnit, cum_usage: dict) -> None:
1672
1672
 
1673
1673
 
1674
1674
  def detect_rearm_dispatch(wu: WorkUnit) -> bool:
1675
- """Return True when wu is a re-arm dispatch whose prior cycle's cost has
1676
- not yet been folded into the cumulative accumulators.
1675
+ """Return True when wu is a re-arm dispatch whose prior cycle has not yet
1676
+ been folded into the cumulative accumulators.
1677
+
1678
+ Reads re_arm_count and folded_through_re_arm from the WU's on-disk
1679
+ frontmatter because load_wu does not load those fields into the
1680
+ WorkUnit object. A fold is owed when re_arm_count > folded_through_re_arm;
1681
+ fold_cumulative_on_rearm stamps folded_through_re_arm = re_arm_count as
1682
+ part of the same write set it uses for the accumulators, so this is a
1683
+ marker comparison, not an inference from a value that a fold can also
1684
+ produce as a side effect (FEAT-2026-0067/T01).
1677
1685
 
1678
- Reads re_arm_count and cost_usd from the WU's on-disk frontmatter because
1679
- load_wu does not load those fields into the WorkUnit object.
1680
1686
  Returns False for first-time dispatches (re_arm_count absent or 0) and for
1681
- re-arms where cost was already folded (cost_usd == 0 after a prior fold).
1687
+ re-arms already folded (folded_through_re_arm == re_arm_count).
1688
+
1689
+ Exposure left for FEAT-2026-0067/T02: a WU re-armed under the prior
1690
+ cost_usd-based guard has no folded_through_re_arm field at all, so it
1691
+ reads as 0 here — indistinguishable from "never folded" even when it was.
1692
+ This WU does not add a value-inferring fallback to paper over that; T02
1693
+ owns the migration of existing WU shapes.
1682
1694
  """
1683
1695
  fm, _ = read_frontmatter(wu.file)
1684
1696
  re_arm_count = fm.get("re_arm_count", 0)
1685
- if not isinstance(re_arm_count, int) or re_arm_count <= 0:
1697
+ if not isinstance(re_arm_count, int) or isinstance(re_arm_count, bool) \
1698
+ or re_arm_count <= 0:
1686
1699
  return False
1687
- cost_usd = fm.get("cost_usd", 0)
1688
- return isinstance(cost_usd, (int, float)) and float(cost_usd) > 0
1700
+ folded_through = fm.get("folded_through_re_arm", 0)
1701
+ if not isinstance(folded_through, int) or isinstance(folded_through, bool):
1702
+ folded_through = 0
1703
+ return re_arm_count > folded_through
1689
1704
 
1690
1705
 
1691
1706
  def rearm_reproduction_gate(wu: WorkUnit) -> tuple[bool, str]:
@@ -1744,6 +1759,12 @@ def fold_cumulative_on_rearm(wu: WorkUnit, backend: Backend) -> None:
1744
1759
 
1745
1760
  Backward-compatible: existing WUs with no cumulative_* fields initialise
1746
1761
  from 0 — no KeyError on first re-arm of a WU that pre-dates this contract.
1762
+
1763
+ Idempotent: a second call for the same re-arm reads cost_usd/duration_seconds/
1764
+ input_tokens/output_tokens back as 0 (this call already reset them), so it
1765
+ adds 0 to each cumulative_* accumulator and re-stamps the same
1766
+ folded_through_re_arm value. Two calls for one re-arm produce the same
1767
+ accumulators as one (FEAT-2026-0067/T01).
1747
1768
  """
1748
1769
  fm, _ = read_frontmatter(wu.file)
1749
1770
  prior_cost = float(fm.get("cost_usd") or 0)
@@ -1783,6 +1804,14 @@ def fold_cumulative_on_rearm(wu: WorkUnit, backend: Backend) -> None:
1783
1804
  backend.set_wu(wu, "input_tokens", 0)
1784
1805
  backend.set_wu(wu, "output_tokens", 0)
1785
1806
 
1807
+ # Explicit fold marker (FEAT-2026-0067/T01): stamped in the same write set
1808
+ # as the accumulators above so detect_rearm_dispatch can tell "folded" from
1809
+ # "cost was zero" without inferring it from cost_usd's value.
1810
+ re_arm_count = fm.get("re_arm_count", 0)
1811
+ if not isinstance(re_arm_count, int) or isinstance(re_arm_count, bool):
1812
+ re_arm_count = 0
1813
+ backend.set_wu(wu, "folded_through_re_arm", re_arm_count)
1814
+
1786
1815
 
1787
1816
  def gate_budget_usd(gate_file: Path) -> float | None:
1788
1817
  """Return the optional cumulative-cost ceiling declared on a GATE.md.
@@ -0,0 +1,267 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """Stamp `folded_through_re_arm` onto every already-re-armed work unit
6
+ (FEAT-2026-0067/T02).
7
+
8
+ T01 (`detect_rearm_dispatch`, `specfuse/loop/loop.py`) reads an absent marker
9
+ as `0`. Six WUs in this repository were folded under the pre-T01 `cost_usd >
10
+ 0` guard and carry no marker at all — indistinguishable, under T01's rule,
11
+ from "never folded". This module closes that gap by giving every re-armed WU
12
+ an explicit marker, without re-running the fold logic T01 owns.
13
+
14
+ Two shapes, two treatments:
15
+
16
+ * **fold-ran** (`cumulative_cost_usd` present) — the fold already moved the
17
+ prior cycle's spend. Stamping `folded_through_re_arm = re_arm_count` is
18
+ the whole fix; `cumulative_*` is read, never written.
19
+ * **fold-never-ran** (`cumulative_cost_usd` absent, `re_arm_history[]`
20
+ populated) — the prior cycle's spend survives only as
21
+ `re_arm_history[].prior_cost_usd` / `prior_duration_seconds`. This module
22
+ folds those two recorded fields into `cumulative_cost_usd` /
23
+ `cumulative_duration_seconds` before stamping the marker, so every
24
+ re-armed WU ends up the same shape (PLAN.md's "converge" decision,
25
+ applied retroactively). No number is invented: only figures already
26
+ present in `re_arm_history` are summed. `prior_input_tokens` /
27
+ `prior_output_tokens` were never recorded on either fold-never-ran WU in
28
+ this repository, so `cumulative_input_tokens` / `cumulative_output_tokens`
29
+ are left unset rather than backfilled from `events.jsonl` — that would be
30
+ a second, undeclared source of truth for the same accumulator.
31
+
32
+ **A value with two meanings, read as if it had one.** `cost_usd` may be
33
+ folded safely by `fold_cumulative_on_rearm` because that fold runs *at
34
+ dispatch*, where `cost_usd` provably holds the prior cycle's spend. This
35
+ module runs *offline*, where `cost_usd` means "the prior cycle" **or**
36
+ "the current cycle" depending on whether a re-dispatch ever happened —
37
+ the exact defect class this feature exists to remove, reproduced by its
38
+ own fix. When a WU was re-armed and never re-dispatched, `cost_usd`
39
+ still holds the prior cycle's spend, and folding `re_arm_history`'s copy
40
+ of that same spend into `cumulative_cost_usd` without resetting
41
+ `cost_usd` double-counts it. The fold-never-ran branch below detects this
42
+ case — `cost_usd` agrees with the `re_arm_history` prior-cost sum within
43
+ `_COST_TOLERANCE_USD` — and resets `cost_usd` / `duration_seconds` to
44
+ `0.0` in the same write set, rather than adding a second copy of money
45
+ that is already accounted for in `cumulative_*`. Anyone adding a new
46
+ offline reader of `cost_usd` should assume it needs the same treatment.
47
+
48
+ A WU never re-armed (`re_arm_count` absent or `0`) is untouched: no read,
49
+ no write, not even a no-op frontmatter round-trip.
50
+
51
+ Writes go through `specfuse.loop.loop.write_frontmatter_field`, the same
52
+ single-key, no-reflow writer `fold_cumulative_on_rearm` uses — every other
53
+ line in the file, marker or not, keeps its original text.
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import json
59
+ from dataclasses import dataclass, field
60
+ from pathlib import Path
61
+
62
+ from . import loop as _loop
63
+
64
+ _COST_TOLERANCE_USD = 0.02
65
+
66
+
67
+ def _int_or_zero(value: object) -> int:
68
+ if isinstance(value, bool) or not isinstance(value, int):
69
+ return 0
70
+ return value
71
+
72
+
73
+ def _classify(fm: dict) -> str:
74
+ """Return "not_rearmed", "fold_ran", or "fold_never_ran"."""
75
+ re_arm_count = fm.get("re_arm_count", 0)
76
+ if not isinstance(re_arm_count, int) or isinstance(re_arm_count, bool) or re_arm_count <= 0:
77
+ return "not_rearmed"
78
+ if "cumulative_cost_usd" in fm:
79
+ return "fold_ran"
80
+ return "fold_never_ran"
81
+
82
+
83
+ def _history_sums(fm: dict) -> tuple[float, float]:
84
+ """Sum `prior_cost_usd` and `prior_duration_seconds` across every
85
+ `re_arm_history` entry. Non-numeric or missing fields contribute 0."""
86
+ history = fm.get("re_arm_history") or []
87
+ cost = 0.0
88
+ duration = 0.0
89
+ if isinstance(history, list):
90
+ for entry in history:
91
+ if not isinstance(entry, dict):
92
+ continue
93
+ pc = entry.get("prior_cost_usd")
94
+ if isinstance(pc, (int, float)) and not isinstance(pc, bool):
95
+ cost += float(pc)
96
+ pd = entry.get("prior_duration_seconds")
97
+ if isinstance(pd, (int, float)) and not isinstance(pd, bool):
98
+ duration += float(pd)
99
+ return cost, duration
100
+
101
+
102
+ def _events_cost_before(
103
+ events_path: Path, wu_id: str, cutoff_iso: str
104
+ ) -> tuple[float, bool]:
105
+ """Sum `attempt_outcome` `cost_usd` for `wu_id` at or before `cutoff_iso`.
106
+
107
+ Returns `(total, seen)`; `seen` is False when the events file is
108
+ missing/unreadable or carries no matching `attempt_outcome` for this WU
109
+ — that means "nothing to cross-check against", not "prior cost was
110
+ zero", so the caller must not treat it as a disagreement. ISO-8601
111
+ timestamps with a fixed-offset suffix compare correctly as strings, so
112
+ this avoids a datetime dependency.
113
+ """
114
+ try:
115
+ lines = events_path.read_text().splitlines()
116
+ except OSError:
117
+ return 0.0, False
118
+ total = 0.0
119
+ seen = False
120
+ for line in lines:
121
+ line = line.strip()
122
+ if not line:
123
+ continue
124
+ try:
125
+ event = json.loads(line)
126
+ except (json.JSONDecodeError, ValueError):
127
+ continue
128
+ if not isinstance(event, dict):
129
+ continue
130
+ if event.get("event_type") != "attempt_outcome":
131
+ continue
132
+ if event.get("correlation_id") != wu_id:
133
+ continue
134
+ timestamp = event.get("timestamp")
135
+ if not isinstance(timestamp, str) or timestamp > cutoff_iso:
136
+ continue
137
+ seen = True
138
+ payload = event.get("payload")
139
+ if not isinstance(payload, dict):
140
+ continue
141
+ cost = payload.get("cost_usd")
142
+ if isinstance(cost, (int, float)) and not isinstance(cost, bool):
143
+ total += float(cost)
144
+ return total, seen
145
+
146
+
147
+ @dataclass
148
+ class CensusResult:
149
+ fold_ran: list[Path] = field(default_factory=list)
150
+ fold_never_ran: list[Path] = field(default_factory=list)
151
+
152
+ def __str__(self) -> str:
153
+ return (
154
+ f"re-armed WUs: fold ran = {len(self.fold_ran)}"
155
+ f" fold never ran = {len(self.fold_never_ran)}"
156
+ )
157
+
158
+
159
+ def census(root: Path) -> CensusResult:
160
+ """Classify every re-armed WU under `root` by fold shape.
161
+
162
+ `root` is a directory searched recursively for `WU-*.md` files (the
163
+ repository's `.specfuse/features/` tree, or a fixture directory in
164
+ tests).
165
+ """
166
+ result = CensusResult()
167
+ for wu_path in sorted(Path(root).glob("**/WU-*.md")):
168
+ fm, _ = _loop.read_frontmatter(wu_path)
169
+ shape = _classify(fm)
170
+ if shape == "fold_ran":
171
+ result.fold_ran.append(wu_path)
172
+ elif shape == "fold_never_ran":
173
+ result.fold_never_ran.append(wu_path)
174
+ return result
175
+
176
+
177
+ class PriorCostDisagreement(RuntimeError):
178
+ """Raised when a fold-never-ran WU's recorded prior cost disagrees with
179
+ its own `events.jsonl` total by more than a rounding tolerance."""
180
+
181
+
182
+ @dataclass
183
+ class MigrationOutcome:
184
+ path: Path
185
+ action: str # "not_rearmed" | "fold_ran_stamped" | "fold_never_ran_migrated" | "already_migrated"
186
+
187
+
188
+ def migrate_file(wu_path: Path, *, events_path: Path | None = None) -> MigrationOutcome:
189
+ """Migrate one WU file in place. Idempotent: a second call for an
190
+ already-migrated file is a no-op read, no writes.
191
+
192
+ `events_path` defaults to `events.jsonl` next to `wu_path` (the feature
193
+ folder layout `correlation-ids.md` documents); pass explicitly for a
194
+ fixture that does not follow that layout.
195
+ """
196
+ wu_path = Path(wu_path)
197
+ fm, _ = _loop.read_frontmatter(wu_path)
198
+ shape = _classify(fm)
199
+
200
+ if shape == "not_rearmed":
201
+ return MigrationOutcome(wu_path, "not_rearmed")
202
+
203
+ re_arm_count = _int_or_zero(fm.get("re_arm_count", 0))
204
+ folded_through = fm.get("folded_through_re_arm", 0)
205
+ if not isinstance(folded_through, int) or isinstance(folded_through, bool):
206
+ folded_through = 0
207
+
208
+ if shape == "fold_ran":
209
+ if folded_through == re_arm_count:
210
+ return MigrationOutcome(wu_path, "already_migrated")
211
+ _loop.write_frontmatter_field(wu_path, "folded_through_re_arm", re_arm_count)
212
+ return MigrationOutcome(wu_path, "fold_ran_stamped")
213
+
214
+ # fold_never_ran
215
+ if folded_through == re_arm_count and "cumulative_cost_usd" in fm:
216
+ return MigrationOutcome(wu_path, "already_migrated")
217
+
218
+ prior_cost, prior_duration = _history_sums(fm)
219
+
220
+ wu_id = fm.get("id")
221
+ history = fm.get("re_arm_history") or []
222
+ last_entry_ts = None
223
+ if isinstance(history, list):
224
+ for entry in history:
225
+ if isinstance(entry, dict) and isinstance(entry.get("timestamp"), str):
226
+ last_entry_ts = entry["timestamp"]
227
+ if isinstance(wu_id, str) and wu_id and last_entry_ts:
228
+ resolved_events_path = events_path or (wu_path.parent / "events.jsonl")
229
+ events_total, seen = _events_cost_before(
230
+ resolved_events_path, wu_id, last_entry_ts
231
+ )
232
+ if seen and abs(events_total - prior_cost) > _COST_TOLERANCE_USD:
233
+ raise PriorCostDisagreement(
234
+ f"{wu_path}: re_arm_history prior_cost_usd sums to "
235
+ f"{prior_cost:.6f} but events.jsonl attempt_outcome total "
236
+ f"through {last_entry_ts} is {events_total:.6f} "
237
+ f"(tolerance {_COST_TOLERANCE_USD})"
238
+ )
239
+
240
+ cost_usd = fm.get("cost_usd")
241
+ money_already_present = (
242
+ isinstance(cost_usd, (int, float))
243
+ and not isinstance(cost_usd, bool)
244
+ and abs(float(cost_usd) - prior_cost) <= _COST_TOLERANCE_USD
245
+ )
246
+
247
+ _loop.write_frontmatter_field(
248
+ wu_path, "cumulative_cost_usd", round(prior_cost, 6)
249
+ )
250
+ _loop.write_frontmatter_field(
251
+ wu_path, "cumulative_duration_seconds", round(prior_duration, 3)
252
+ )
253
+ if money_already_present:
254
+ # Never re-dispatched: `cost_usd` still holds the same spend now
255
+ # folded into `cumulative_cost_usd`. Reset it in this write set so
256
+ # the two fields don't both carry a copy of one cycle's spend.
257
+ _loop.write_frontmatter_field(wu_path, "cost_usd", 0.0)
258
+ _loop.write_frontmatter_field(wu_path, "duration_seconds", 0.0)
259
+ _loop.write_frontmatter_field(wu_path, "folded_through_re_arm", re_arm_count)
260
+ return MigrationOutcome(wu_path, "fold_never_ran_migrated")
261
+
262
+
263
+ def migrate_repo(root: Path) -> list[MigrationOutcome]:
264
+ """Migrate every `WU-*.md` file under `root`. Returns one outcome per
265
+ file that was inspected (including untouched `not_rearmed` files, so
266
+ callers can audit that nothing else was written)."""
267
+ return [migrate_file(wu_path) for wu_path in sorted(Path(root).glob("**/WU-*.md"))]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.9.2
3
+ Version: 0.9.3
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
@@ -26,6 +26,7 @@ specfuse/loop/lint_plan.py
26
26
  specfuse/loop/lint_roadmap.py
27
27
  specfuse/loop/loop.py
28
28
  specfuse/loop/plan_baseline.py
29
+ specfuse/loop/rearm_migration.py
29
30
  specfuse/loop/scaffold.py
30
31
  specfuse/loop/validate_event.py
31
32
  specfuse/loop/data/CHANGELOG.seed.md
@@ -173,6 +174,7 @@ tests/test_feature_review_accumulation.py
173
174
  tests/test_filelock_portable.py
174
175
  tests/test_fingerprint.py
175
176
  tests/test_fix_bug_headless.py
177
+ tests/test_fold_contract_documented.py
176
178
  tests/test_force_full_close.py
177
179
  tests/test_gate_budget.py
178
180
  tests/test_gate_budget_post_dispatch.py
@@ -263,6 +265,8 @@ tests/test_produces_semantics_unified.py
263
265
  tests/test_produces_shape.py
264
266
  tests/test_provision_labels.py
265
267
  tests/test_queue_stalled_adapter.py
268
+ tests/test_rearm_fold_marker.py
269
+ tests/test_rearm_migration.py
266
270
  tests/test_redaction_windows_home.py
267
271
  tests/test_resolve_base.py
268
272
  tests/test_result_block.py
@@ -0,0 +1,127 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """FEAT-2026-0067/T03 — the written contract agrees with the converged fold.
6
+
7
+ Asserts the WU template documents `folded_through_re_arm` and the
8
+ unconditional lifetime-accumulator contract, that both template copies stay
9
+ byte-identical, that `cost.py`'s module docstring names the pre-migration
10
+ fallback as legacy rather than an ongoing second shape, and that
11
+ `wu_lifetime_cost_usd`'s behaviour is unchanged by this documentation-only WU.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import tempfile
18
+ import unittest
19
+ from pathlib import Path
20
+
21
+ from specfuse.loop import cost
22
+ from specfuse.loop.cost import wu_lifetime_cost_usd
23
+
24
+ _REPO_ROOT = Path(__file__).resolve().parent.parent
25
+ _CANONICAL_TEMPLATE = _REPO_ROOT / "specfuse/loop/data/templates/WU.template.md"
26
+ _VENDORED_TEMPLATE = _REPO_ROOT / ".specfuse/templates/WU.template.md"
27
+
28
+
29
+ def _write(path: Path, text: str) -> None:
30
+ path.parent.mkdir(parents=True, exist_ok=True)
31
+ path.write_text(text)
32
+
33
+
34
+ def _write_events(path: Path, events: list[dict]) -> None:
35
+ path.parent.mkdir(parents=True, exist_ok=True)
36
+ path.write_text("\n".join(json.dumps(e) for e in events) + "\n")
37
+
38
+
39
+ def _attempt_outcome(correlation_id: str, cost_usd) -> dict:
40
+ return {
41
+ "correlation_id": correlation_id,
42
+ "event_type": "attempt_outcome",
43
+ "payload": {"cost_usd": cost_usd},
44
+ }
45
+
46
+
47
+ class TestFoldContract(unittest.TestCase):
48
+ def test_template_documents_cumulative_as_lifetime(self):
49
+ text = _CANONICAL_TEMPLATE.read_text()
50
+ self.assertIn("folded_through_re_arm", text)
51
+ self.assertIn("every re-arm", text)
52
+ self.assertIn(
53
+ "whose prior cycle cost nothing",
54
+ text,
55
+ "template must state cumulative_* folds even a zero-cost prior cycle",
56
+ )
57
+
58
+ def test_vendored_template_documents_cumulative_as_lifetime(self):
59
+ text = _VENDORED_TEMPLATE.read_text()
60
+ self.assertIn("folded_through_re_arm", text)
61
+ self.assertIn("every re-arm", text)
62
+ self.assertIn("whose prior cycle cost nothing", text)
63
+
64
+ def test_template_copies_are_byte_identical(self):
65
+ self.assertEqual(
66
+ _CANONICAL_TEMPLATE.read_bytes(),
67
+ _VENDORED_TEMPLATE.read_bytes(),
68
+ )
69
+
70
+ def test_cost_docstring_names_migration_not_two_live_shapes(self):
71
+ doc = cost.__doc__ or ""
72
+ self.assertIn("FEAT-2026-0067", doc)
73
+ self.assertIn("pre-migration", doc)
74
+ self.assertNotIn("fold-never-ran", doc)
75
+ self.assertNotIn("fold-ran", doc)
76
+
77
+
78
+ class TestLifetimeCostUnchanged(unittest.TestCase):
79
+ """Criterion 5: same reader behaviour before and after this WU's edits."""
80
+
81
+ def setUp(self):
82
+ self._tmp = tempfile.TemporaryDirectory()
83
+ self.addCleanup(self._tmp.cleanup)
84
+ self.root = Path(self._tmp.name)
85
+ self.wu_path = self.root / "WU-07-x.md"
86
+ self.events_path = self.root / "events.jsonl"
87
+
88
+ def test_events_bearing_wu_unchanged(self):
89
+ _write(
90
+ self.wu_path,
91
+ """---
92
+ id: FEAT-2026-0053/WU-07
93
+ type: implementation
94
+ status: done
95
+ cost_usd: 4.281823
96
+ ---
97
+ body
98
+ """,
99
+ )
100
+ _write_events(
101
+ self.events_path,
102
+ [
103
+ _attempt_outcome("FEAT-2026-0053/WU-07", 1.0),
104
+ _attempt_outcome("FEAT-2026-0053/WU-07", 2.0),
105
+ ],
106
+ )
107
+ result = wu_lifetime_cost_usd(self.wu_path, self.events_path)
108
+ self.assertAlmostEqual(result, 3.0, delta=0.01)
109
+
110
+ def test_frontmatter_only_wu_unchanged(self):
111
+ _write(
112
+ self.wu_path,
113
+ """---
114
+ id: FEAT-2026-0099/WU-01
115
+ cost_usd: 1.5
116
+ cumulative_cost_usd: 2.5
117
+ ---
118
+ body
119
+ """,
120
+ )
121
+ _write_events(self.events_path, [])
122
+ result = wu_lifetime_cost_usd(self.wu_path, self.events_path)
123
+ self.assertAlmostEqual(result, 4.0, delta=0.01)
124
+
125
+
126
+ if __name__ == "__main__":
127
+ unittest.main()