specfuse-loop 0.9.0__tar.gz → 0.9.2__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 (309) hide show
  1. {specfuse_loop-0.9.0/specfuse_loop.egg-info → specfuse_loop-0.9.2}/PKG-INFO +1 -1
  2. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/pyproject.toml +1 -1
  3. specfuse_loop-0.9.2/specfuse/loop/data/CHANGELOG.seed.md +27 -0
  4. specfuse_loop-0.9.2/specfuse/loop/data/VERSION +1 -0
  5. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/correlation-ids.md +0 -11
  6. specfuse_loop-0.9.2/specfuse/loop/gate_commands.py +94 -0
  7. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/lint_plan.py +93 -0
  8. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/loop.py +98 -11
  9. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/scaffold.py +27 -0
  10. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/validate_event.py +5 -0
  11. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2/specfuse_loop.egg-info}/PKG-INFO +1 -1
  12. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/SOURCES.txt +6 -0
  13. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_changelog_collection.py +40 -0
  14. specfuse_loop-0.9.2/tests/test_ci_runs_every_gate.py +74 -0
  15. specfuse_loop-0.9.2/tests/test_correlation_id_two_surfaces.py +79 -0
  16. specfuse_loop-0.9.2/tests/test_produces_shape.py +160 -0
  17. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_data_in_sync.py +1 -0
  18. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_resources.py +1 -0
  19. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_wiring.py +81 -0
  20. specfuse_loop-0.9.2/tests/test_unterminated_frontmatter.py +139 -0
  21. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_upgrade_merge_gate.py +103 -0
  22. specfuse_loop-0.9.0/specfuse/loop/data/VERSION +0 -1
  23. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/LICENSE +0 -0
  24. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/NOTICE +0 -0
  25. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/README.md +0 -0
  26. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/setup.cfg +0 -0
  27. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/__init__.py +0 -0
  28. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/_filelock.py +0 -0
  29. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/_miniyaml.py +0 -0
  30. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/_wu_sections.py +0 -0
  31. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/adopt_feature.py +0 -0
  32. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/arm_eval.py +0 -0
  33. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/arm_sweep.py +0 -0
  34. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/arm_txn.py +0 -0
  35. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/changelog.py +0 -0
  36. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/closing_requirements.py +0 -0
  37. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/cost.py +0 -0
  38. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  39. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/adopting-auto-mode.md +0 -0
  40. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  41. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/autonomy-stop-classes.md +0 -0
  42. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  43. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/getting-started.md +0 -0
  44. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/methodology.md +0 -0
  45. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/skills.md +0 -0
  46. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/gitignore.snippet +0 -0
  47. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
  48. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
  49. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring.yml.example +0 -0
  50. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/roadmap.template.md +0 -0
  51. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/close-discipline.md +0 -0
  52. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
  53. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/never-touch.md +0 -0
  54. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/operator-escalation.md +0 -0
  55. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/planning-discipline.md +0 -0
  56. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/result-contract.md +0 -0
  57. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  58. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  59. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules-local/README.md +0 -0
  60. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/driver-event.schema.json +0 -0
  61. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/event.schema.json +0 -0
  62. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  63. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  64. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  65. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  66. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/GATE.template.md +0 -0
  67. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/LEARNINGS-pending.template.md +0 -0
  68. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  69. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/WU.template.md +0 -0
  70. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/verification.yml.example +0 -0
  71. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
  72. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/escalation.py +0 -0
  73. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/events_stats.py +0 -0
  74. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/gate_eval.py +0 -0
  75. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/gh_backend.py +0 -0
  76. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/gh_features.py +0 -0
  77. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/labels.py +0 -0
  78. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/lint_closing.py +0 -0
  79. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/lint_monitoring.py +0 -0
  80. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/lint_roadmap.py +0 -0
  81. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/plan_baseline.py +0 -0
  82. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/__init__.py +0 -0
  83. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/adapters.py +0 -0
  84. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/artifact.py +0 -0
  85. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/autofix.py +0 -0
  86. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_invoke.py +0 -0
  87. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_run.py +0 -0
  88. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_state.py +0 -0
  89. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/cli.py +0 -0
  90. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/diagnose_cli.py +0 -0
  91. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/diagnosis.py +0 -0
  92. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/fingerprint.py +0 -0
  93. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/issues.py +0 -0
  94. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/providers/__init__.py +0 -0
  95. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/providers/_azure_auth.py +0 -0
  96. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/providers/azure_app_insights.py +0 -0
  97. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/providers/azure_service_bus.py +0 -0
  98. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/redaction.py +0 -0
  99. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/schedule.py +0 -0
  100. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  101. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/entry_points.txt +0 -0
  102. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/requires.txt +0 -0
  103. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/top_level.txt +0 -0
  104. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_accept_hedged_close_headline.py +0 -0
  105. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_accept_hedged_close_skill.py +0 -0
  106. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_adopt_feature.py +0 -0
  107. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_app_insights_adapters.py +0 -0
  108. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_eval.py +0 -0
  109. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_eval_lint_class.py +0 -0
  110. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_eval_wiring.py +0 -0
  111. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  112. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_sweep.py +0 -0
  113. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_sweep_gate.py +0 -0
  114. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_txn.py +0 -0
  115. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_wiring.py +0 -0
  116. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_artifact_redaction.py +0 -0
  117. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_attempt_outcome_contract.py +0 -0
  118. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_attempt_outcome_emission.py +0 -0
  119. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_attention_nonwriting_guard.py +0 -0
  120. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_attention_skill_structure.py +0 -0
  121. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autoclose_deferral_visibility.py +0 -0
  122. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_invoke.py +0 -0
  123. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_live.py +0 -0
  124. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_predicate.py +0 -0
  125. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_run.py +0 -0
  126. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_state.py +0 -0
  127. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync.py +0 -0
  128. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync_consent.py +0 -0
  129. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync_firstrun.py +0 -0
  130. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync_no_cwd_leak.py +0 -0
  131. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync_plugin.py +0 -0
  132. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_azure_credential_resolution.py +0 -0
  133. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_backend.py +0 -0
  134. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_baseline_escalation_message.py +0 -0
  135. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_baseline_persistence.py +0 -0
  136. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_baseline_probe.py +0 -0
  137. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bash_routing.py +0 -0
  138. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bats_suites_gated.py +0 -0
  139. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  140. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  141. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bookkeeping_no_verify.py +0 -0
  142. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_changelog_release_wiring.py +0 -0
  143. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_changelog_schema.py +0 -0
  144. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_claude_resolution.py +0 -0
  145. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_close_wu_self_reference.py +0 -0
  146. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_closing_deliverable_guard.py +0 -0
  147. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_closing_guard_contracts.py +0 -0
  148. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_closing_guard_prediction.py +0 -0
  149. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_closing_requirements.py +0 -0
  150. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_console_encoding.py +0 -0
  151. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_correlation_id_override.py +0 -0
  152. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_cost_lifetime.py +0 -0
  153. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_cost_tracking.py +0 -0
  154. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_degraded_oracle.py +0 -0
  155. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_deliverable_presence_gate.py +0 -0
  156. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_derive_monitoring_discovery.py +0 -0
  157. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_derive_monitoring_skill_registration.py +0 -0
  158. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_design_for_diagnosis_rule.py +0 -0
  159. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_diagnose_headless.py +0 -0
  160. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_diagnose_issue_skill.py +0 -0
  161. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_diagnosis.py +0 -0
  162. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_diagnosis_roundtrip_live.py +0 -0
  163. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_dispatch_skeleton.py +0 -0
  164. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_doctor.py +0 -0
  165. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_done_feature_gates.py +0 -0
  166. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_driver_event_registry_covers_emitters.py +0 -0
  167. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_driver_integration.py +0 -0
  168. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_driver_lock.py +0 -0
  169. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_duration_tracking.py +0 -0
  170. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_empty_files_escalation.py +0 -0
  171. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_ensure_feature_branch.py +0 -0
  172. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_ensure_feature_branch_base.py +0 -0
  173. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_escalation_contract.py +0 -0
  174. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_escalation_emit.py +0 -0
  175. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_event_gate_full_envelope.py +0 -0
  176. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_events_redaction.py +0 -0
  177. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_events_stats.py +0 -0
  178. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_extra_gates.py +0 -0
  179. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_failure_artifact_model.py +0 -0
  180. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_feature_id_resolution.py +0 -0
  181. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_feature_review_accumulation.py +0 -0
  182. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_filelock_portable.py +0 -0
  183. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_fingerprint.py +0 -0
  184. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_fix_bug_headless.py +0 -0
  185. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_force_full_close.py +0 -0
  186. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_budget.py +0 -0
  187. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_budget_post_dispatch.py +0 -0
  188. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_eval.py +0 -0
  189. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_eval_calibration.py +0 -0
  190. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  191. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_eval_terminal_wiring.py +0 -0
  192. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_report_verdict.py +0 -0
  193. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gh_backend.py +0 -0
  194. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gh_features.py +0 -0
  195. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_git_env_isolation.py +0 -0
  196. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gitleaks_pinning.py +0 -0
  197. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_hashed_denylist.py +0 -0
  198. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_hashed_denylist_ci.py +0 -0
  199. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_heartbeat_adapter.py +0 -0
  200. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_hedged_kind_contract.py +0 -0
  201. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_init_integration.py +0 -0
  202. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_interpreter_normalization.py +0 -0
  203. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_label_registry.py +0 -0
  204. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_label_registry_covers_consumers.py +0 -0
  205. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_leak_findings_redaction.py +0 -0
  206. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_leak_scan.py +0 -0
  207. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_leak_scan_content.py +0 -0
  208. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_learnings_query.py +0 -0
  209. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_learnings_staging.py +0 -0
  210. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  211. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lifecycle_integration.py +0 -0
  212. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lifetime_accounting.py +0 -0
  213. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_bare_produces_path.py +0 -0
  214. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_boundary_consistency.py +0 -0
  215. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_boundary_extraction.py +0 -0
  216. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_close_intermediate.py +0 -0
  217. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_close_wu.py +0 -0
  218. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_closing.py +0 -0
  219. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_correlation_id.py +0 -0
  220. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  221. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_monitoring.py +0 -0
  222. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_oracle_env.py +0 -0
  223. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_contract_fields.py +0 -0
  224. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_errors.py +0 -0
  225. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_next_draft.py +0 -0
  226. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_relative_import.py +0 -0
  227. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_verdict_exempt.py +0 -0
  228. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_planning_sections.py +0 -0
  229. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_produces_driver_helper.py +0 -0
  230. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_produces_satisfiability.py +0 -0
  231. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_roadmap.py +0 -0
  232. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_sections.py +0 -0
  233. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  234. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_verification_reference.py +0 -0
  235. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_attempt_evidence.py +0 -0
  236. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_auto_archive.py +0 -0
  237. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_caveman_preamble.py +0 -0
  238. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_close_intermediate.py +0 -0
  239. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_defaults_by_type.py +0 -0
  240. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_effort.py +0 -0
  241. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_failure_note_cap.py +0 -0
  242. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_files_changed_guard.py +0 -0
  243. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_gate_budget.py +0 -0
  244. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_model_alias.py +0 -0
  245. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_orchestration.py +0 -0
  246. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_post_pass_invariant.py +0 -0
  247. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_reset_preserving_events.py +0 -0
  248. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_smoke_runner.py +0 -0
  249. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_unsandboxed.py +0 -0
  250. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_zero_token_guard.py +0 -0
  251. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_migrate_legacy.py +0 -0
  252. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_miniyaml_equivalence.py +0 -0
  253. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_miniyaml_negative.py +0 -0
  254. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitor_cli.py +0 -0
  255. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitor_issue_lifecycle.py +0 -0
  256. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitor_runner_surfaces.py +0 -0
  257. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
  258. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_cron_dialect.py +0 -0
  259. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_example.py +0 -0
  260. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_fenced_blocks.py +0 -0
  261. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_seed.py +0 -0
  262. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_no_pytest_subprocess.py +0 -0
  263. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_operator_escalation_rule.py +0 -0
  264. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_plan_baseline.py +0 -0
  265. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_planned_cost_lint.py +0 -0
  266. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_planning_cost_floor.py +0 -0
  267. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_prepare_feature.py +0 -0
  268. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_prepare_scaffold_sync.py +0 -0
  269. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_produces_field.py +0 -0
  270. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_produces_in_diff.py +0 -0
  271. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_produces_semantics_unified.py +0 -0
  272. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_provision_labels.py +0 -0
  273. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_queue_stalled_adapter.py +0 -0
  274. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_redaction_windows_home.py +0 -0
  275. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_resolve_base.py +0 -0
  276. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_result_block.py +0 -0
  277. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_roadmap_add_skill.py +0 -0
  278. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_roadmap_archive_skill.py +0 -0
  279. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_roadmap_link_gate.py +0 -0
  280. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_roadmap_row_parser.py +0 -0
  281. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_routed_finding_tracking.py +0 -0
  282. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_doc_hygiene.py +0 -0
  283. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_docs.py +0 -0
  284. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_init.py +0 -0
  285. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_label_provisioning.py +0 -0
  286. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_manifest.py +0 -0
  287. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_seed_sanity.py +0 -0
  288. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_upgrade.py +0 -0
  289. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_schedule_dialect.py +0 -0
  290. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_service_bus_dlq_adapter.py +0 -0
  291. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_skill_discovery_links.py +0 -0
  292. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_skills_vendored_in_sync.py +0 -0
  293. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_spinning_rearm_gate.py +0 -0
  294. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_squash_commit_hook_crash.py +0 -0
  295. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_template_closing_shapes.py +0 -0
  296. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_terminal_flip_ownership.py +0 -0
  297. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_terminal_flips.py +0 -0
  298. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_timeout_kill_portable.py +0 -0
  299. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_untracked_feature_folder.py +0 -0
  300. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_untracked_file_absorption.py +0 -0
  301. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_upgrade_integration.py +0 -0
  302. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_validate_event.py +0 -0
  303. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_validate_event_driver_types.py +0 -0
  304. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_verdict_coupling.py +0 -0
  305. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_verify_empty_gate_set.py +0 -0
  306. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_version_consistency.py +0 -0
  307. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_version_skew.py +0 -0
  308. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_windows_gate_exec.py +0 -0
  309. {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/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.0
3
+ Version: 0.9.2
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.0"
7
+ version = "0.9.2"
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,27 @@
1
+ # Changelog
2
+
3
+ Consumer-visible changes to this project, newest first.
4
+
5
+ <!--
6
+ How entries get here. Two collection points, because bugs have no close
7
+ ceremony:
8
+
9
+ - a feature's close appends its "Consumer-visible contract changes"
10
+ enumeration (close-discipline.md §3) to Unreleased, one entry per change;
11
+ - `fix-bug` appends one line carrying the issue number before opening its PR.
12
+
13
+ Every entry carries a `FEAT-YYYY-NNNN` or `#<issue>` trace in trailing
14
+ parentheses, and sits under one of four class headings: Breaking, Added,
15
+ Changed, Fixed. An entry without a trace is a lint finding — a changelog line
16
+ nobody can trace back to the work that produced it is a line nobody can act on.
17
+
18
+ No backfill. This file starts empty on purpose. Reconstructing history from
19
+ commit subjects produces entries that read plausibly and describe the wrong
20
+ thing; the enumeration a close already wrote is the source, and work that
21
+ predates this file has none to lift.
22
+
23
+ Cutting a release stamps Unreleased with the version and freezes it. Appends
24
+ only ever go to Unreleased.
25
+ -->
26
+
27
+ ## [Unreleased]
@@ -0,0 +1 @@
1
+ 0.9.2
@@ -145,17 +145,6 @@ The session running `plan-next` mints the next gate's substantive IDs as drafts;
145
145
  the human arms them. When adding a new closing WU type to `lint_plan.py`, also
146
146
  add its `<NAME>` segment to `CORRELATION_ID_RE` so the new IDs pass validation.
147
147
 
148
- **Two enforcement surfaces, one contract.** `lint_plan.py`'s `CORRELATION_ID_RE`
149
- enforces this format on `PLAN.md` graphs and WU frontmatter. The event envelope
150
- (`specfuse/loop/data/schemas/event.schema.json`) enforces a narrower vendored
151
- `correlation_id` pattern that does not yet know the closing-sequence or hygiene
152
- shapes; the loop widens it without editing the vendored file, via a driver-local
153
- registry (`specfuse/loop/data/schemas/driver-event.schema.json`'s `correlation_id`
154
- key, read by `validate_event.py`'s `load_validator`) — the same deep-copy
155
- fall-through FEAT-2026-0060 established for `event_type` (FEAT-2026-0073). When
156
- adding a new `<NAME>` segment, update `CORRELATION_ID_RE` **and** that registry's
157
- `closing_names` so both surfaces admit the new shape together.
158
-
159
148
  Do not reuse an ordinal even after a unit is abandoned. Once an ID has appeared in
160
149
  the event log, it is spent. Reusing it would make history ambiguous.
161
150
 
@@ -0,0 +1,94 @@
1
+ #
2
+ # Copyright 2026 Specfuse Contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """Read the gate sets out of `.specfuse/verification.yml` (#592).
6
+
7
+ `verification.yml` is the source of truth for what gets verified. Anything
8
+ that runs gates should derive its list from here rather than keeping a copy.
9
+
10
+ `scripts/smoke-test.sh` used to carry a hand-maintained mirror under the
11
+ comment "Keep in sync". It drifted by six gates, and two of them
12
+ (`roadmap-link-gate`, `arm-sweep-gate`) shipped with features that week and
13
+ were never once executed by CI. A declared-but-unrun gate is worse than an
14
+ absent one, because the declaration reads as coverage.
15
+
16
+ Deliberately not a YAML dependency: `_miniyaml` already parses this project's
17
+ files, and adding a third-party parser to a path CI depends on would trade one
18
+ drift risk for an install risk.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from pathlib import Path
25
+
26
+ _SET_RE = re.compile(r"^([a-z_][a-z0-9_]*):\s*$", re.M)
27
+ _NAME_RE = re.compile(r"^ - name:\s*(\S+)\s*$", re.M)
28
+ _COMMAND_RE = re.compile(r'^\s+command:\s*"(.*?)"\s*$', re.M | re.S)
29
+
30
+
31
+ def _set_block(text: str, set_name: str) -> str:
32
+ """Return the raw text of one top-level gate set, without its siblings."""
33
+ starts = [(m.group(1), m.start()) for m in _SET_RE.finditer(text)]
34
+ for i, (name, pos) in enumerate(starts):
35
+ if name != set_name:
36
+ continue
37
+ end = starts[i + 1][1] if i + 1 < len(starts) else len(text)
38
+ return text[pos:end]
39
+ return ""
40
+
41
+
42
+ def iter_code_gates(
43
+ verification_yml: "Path | str", set_name: str = "code"
44
+ ) -> list[tuple[str, str]]:
45
+ """Yield `(name, command)` for every gate in *set_name*, in declared order.
46
+
47
+ Order is preserved because gate order is a real signal: the cheap,
48
+ fast-failing gates are declared first so a broken build reports in seconds
49
+ rather than after the full suite.
50
+ """
51
+ text = Path(verification_yml).read_text(encoding="utf-8")
52
+ block = _set_block(text, set_name)
53
+ if not block:
54
+ return []
55
+
56
+ gates: list[tuple[str, str]] = []
57
+ entries = re.split(r"^ - name:\s*", block, flags=re.M)[1:]
58
+ for entry in entries:
59
+ name = entry.split("\n", 1)[0].strip()
60
+ m = _COMMAND_RE.search(entry)
61
+ command = m.group(1).strip() if m else ""
62
+ gates.append((name, command))
63
+ return gates
64
+
65
+
66
+ def code_gate_names(
67
+ verification_yml: "Path | str", set_name: str = "code"
68
+ ) -> list[str]:
69
+ """Names only, for parity checks."""
70
+ text = Path(verification_yml).read_text(encoding="utf-8")
71
+ return _NAME_RE.findall(_set_block(text, set_name))
72
+
73
+
74
+ def main() -> int:
75
+ """Print `name<TAB>command` per gate, for shell consumption.
76
+
77
+ Used by `scripts/smoke-test.sh`, which substitutes its own interpreter for
78
+ a leading `python3`: the declared commands name `python3` because that is
79
+ what a target project's driver invokes, while the smoke test must use the
80
+ virtualenv interpreter that actually has the dependencies installed.
81
+ """
82
+ import argparse
83
+
84
+ ap = argparse.ArgumentParser(description=__doc__)
85
+ ap.add_argument("verification_yml")
86
+ ap.add_argument("--set", default="code", dest="set_name")
87
+ args = ap.parse_args()
88
+ for name, command in iter_code_gates(args.verification_yml, args.set_name):
89
+ print(f"{name}\t{command}")
90
+ return 0
91
+
92
+
93
+ if __name__ == "__main__":
94
+ raise SystemExit(main())
@@ -84,6 +84,20 @@ MODEL_ALIASES = frozenset({"sonnet", "opus", "haiku"})
84
84
  VALID_EFFORT = frozenset({"low", "medium", "high", "xhigh", "max"})
85
85
  FULL_MODEL_ID_RE = re.compile(r"^claude-\w[\w.-]*$")
86
86
 
87
+ # Two enforcement surfaces, one contract. This pattern governs PLAN.md graphs
88
+ # and WU frontmatter. The event envelope enforces its own, narrower
89
+ # `correlation_id` pattern (`data/schemas/event.schema.json`, vendored from the
90
+ # methodology core and never edited here); the driver widens it via the
91
+ # driver-local registry `data/schemas/driver-event.schema.json`, read by
92
+ # `validate_event.load_validator` on a deep copy — the fall-through
93
+ # FEAT-2026-0060 established for `event_type` and FEAT-2026-0073 extended to
94
+ # correlation IDs.
95
+ #
96
+ # ADDING A NEW `<NAME>` SEGMENT: update this pattern AND that registry's
97
+ # `closing_names`, or IDs the linter accepts will be rejected by envelope
98
+ # validation. This note lives here, at the change site, rather than in
99
+ # `rules/correlation-ids.md` — that file is vendored from core, so a loop-local
100
+ # addition to it is reverted by the next `sync-scaffold.sh` run (#581).
87
101
  CORRELATION_ID_RE = re.compile(
88
102
  r"^(FEAT-\d{4}-\d{4}(/(T\d{2}(H\d*)?|G\d+-(RETRO|LESSONS|DOCS|PLAN|CLOSE-INTERMEDIATE|CLOSE)))?|"
89
103
  r"INIT-\d{4}-\d{4}/F\d{2}(/(T\d{2}(H\d*)?|G\d+-(RETRO|LESSONS|DOCS|PLAN|CLOSE-INTERMEDIATE|CLOSE)))?)$"
@@ -226,6 +240,15 @@ def read_frontmatter(path: Path) -> tuple[dict, str]:
226
240
  j = 1
227
241
  while j < len(lines) and not FM.match(lines[j]):
228
242
  j += 1
243
+ if j >= len(lines):
244
+ # See loop.read_frontmatter: an opening `---` with no closing one made
245
+ # the scan run off the end and parse the document as frontmatter,
246
+ # reporting the failure against an arbitrary body line (#306).
247
+ raise _miniyaml.MiniYAMLError(
248
+ f"{path}: frontmatter block opened with `---` on line 1 but is "
249
+ f"never closed — add a `---` line to end it. Every line after it "
250
+ f"was read as frontmatter, so no check could evaluate this file."
251
+ )
229
252
  return _miniyaml.parse("\n".join(lines[1:j])) or {}, "\n".join(lines[j + 1:])
230
253
 
231
254
 
@@ -645,6 +668,60 @@ def _match_dnt_boundary(
645
668
  return None
646
669
 
647
670
 
671
+ def check_produces_shape(feature_dir: Path, gates: list) -> list[str]:
672
+ """ERROR when a WU's `produces:` entry is a directory rather than a file
673
+ or glob.
674
+
675
+ Same family as `check_produces_boundary` below, and the same argument: the
676
+ driver refuses this outright, but only after a full `claude -p` session.
677
+ Since the agent cannot edit its own frontmatter, every retry is
678
+ byte-identical -- a real feature paid $6.42 and 20.6 minutes across three
679
+ of them before `spinning_detected` fired, while `specfuse-lint` reported
680
+ `OK - structurally valid` throughout (#593).
681
+
682
+ Every input is static, so this belongs in the pre-dispatch checklist. The
683
+ message is rendered by `loop.produces_shape_error`, the same function the
684
+ driver's guards call, so all three surfaces read identically rather than
685
+ drifting into three descriptions of one rule.
686
+
687
+ Applies to every WU with a `produces:` entry regardless of status: a `done`
688
+ WU with a directory entry is a folder authored against the pre-0.9.0
689
+ contract, and reporting it is how a migrating project finds them all at
690
+ once instead of one dispatch at a time.
691
+ """
692
+ from .loop import produces_shape_error
693
+
694
+ found: list[str] = []
695
+ for gate in gates:
696
+ for entry in gate.get("work_units") or []:
697
+ wid, wfile = entry.get("id"), entry.get("file")
698
+ if not wid or not wfile:
699
+ continue
700
+ wpath = feature_dir / wfile
701
+ if not wpath.exists():
702
+ continue
703
+ wfm, _ = read_frontmatter(wpath)
704
+ produces_raw = wfm.get("produces")
705
+ if not produces_raw:
706
+ continue
707
+ raw_entries = (
708
+ produces_raw if isinstance(produces_raw, list) else [produces_raw]
709
+ )
710
+ for p in raw_entries:
711
+ p_s = str(p).strip()
712
+ if not p_s:
713
+ continue
714
+ err = produces_shape_error(p_s)
715
+ if err:
716
+ found.append(
717
+ f"ERROR: {wfile}: {wid} {err}. assert_declared_"
718
+ f"deliverables refuses this after a full dispatch "
719
+ f"attempt, and a retry cannot help — the agent cannot "
720
+ f"edit its own frontmatter. See #593."
721
+ )
722
+ return found
723
+
724
+
648
725
  def check_produces_boundary(feature_dir: Path, gates: list) -> list[str]:
649
726
  """ERROR when a WU's own `produces:` path (or `produces_driver_helper`
650
727
  surface) falls inside its own Do-not-touch section's paths.
@@ -879,6 +956,21 @@ def check_planning_sections(
879
956
 
880
957
 
881
958
  def lint(feature_dir: Path) -> list[str]:
959
+ """Lint a feature folder, converting an unparseable file into a finding.
960
+
961
+ A malformed WU used to abort the whole run with a traceback, which made
962
+ the folder unevaluable by *every* check rather than one (#306). The lint's
963
+ job is to report what is wrong with a folder; a file it cannot parse is
964
+ the most basic thing that can be wrong with one, so it is a finding like
965
+ any other. Callers still get a non-empty error list and a non-zero exit.
966
+ """
967
+ try:
968
+ return _lint_impl(feature_dir)
969
+ except _miniyaml.MiniYAMLError as exc:
970
+ return [f"ERROR: unparseable frontmatter — {exc}"]
971
+
972
+
973
+ def _lint_impl(feature_dir: Path) -> list[str]:
882
974
  errs: list[str] = []
883
975
  plan = feature_dir / "PLAN.md"
884
976
  if not plan.exists():
@@ -1198,6 +1290,7 @@ def lint(feature_dir: Path) -> list[str]:
1198
1290
  check_closing_guard_literals(feature_dir, gates)
1199
1291
  check_autoclose_debt_prediction(feature_dir, gates)
1200
1292
  check_produces_satisfiability(feature_dir, gates)
1293
+ errs.extend(check_produces_shape(feature_dir, gates))
1201
1294
  errs.extend(check_produces_boundary(feature_dir, gates))
1202
1295
  errs.extend(check_done_feature_gates(feature_dir, fm))
1203
1296
 
@@ -60,7 +60,7 @@ from . import _filelock
60
60
  from . import _miniyaml
61
61
  from . import _wu_sections
62
62
  from . import scaffold as _scaffold
63
- from .changelog import parse_changelog
63
+ from .changelog import ENTRY_CLASSES, parse_changelog
64
64
  from .closing_requirements import (
65
65
  AUTOCLOSE_DEBT_MARKER_RE,
66
66
  CHANGELOG_PATH,
@@ -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.0"
108
+ DRIVER_VERSION = "0.9.2"
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
@@ -245,6 +245,17 @@ def read_frontmatter(path: Path) -> tuple[dict, str]:
245
245
  j = 1
246
246
  while j < len(lines) and not FM.match(lines[j]):
247
247
  j += 1
248
+ if j >= len(lines):
249
+ # Unterminated block: an opening `---` with no closing one. Without
250
+ # this check the scan runs off the end, the whole document is parsed
251
+ # as frontmatter, and the failure is reported against whatever body
252
+ # line happens to be first — which sent #306 chasing an HTML comment
253
+ # that was never the problem. Name the actual defect instead.
254
+ raise _miniyaml.MiniYAMLError(
255
+ f"{path}: frontmatter block opened with `---` on line 1 but is "
256
+ f"never closed — add a `---` line to end it. Every line after it "
257
+ f"was read as frontmatter, so no check could evaluate this file."
258
+ )
248
259
  try:
249
260
  fm = _miniyaml.parse("\n".join(lines[1:j])) or {}
250
261
  except _miniyaml.MiniYAMLError as exc:
@@ -4674,7 +4685,12 @@ def assert_changelog_entry_for_contract_changes(
4674
4685
  return (
4675
4686
  False,
4676
4687
  f"assert_changelog_entry_for_contract_changes: '{CONSUMER_VISIBLE_HEADING}' "
4677
- f"names a real change but {CHANGELOG_PATH} does not exist",
4688
+ f"names a real change but {CHANGELOG_PATH} does not exist. Create it at "
4689
+ f"the repo root with a '## [Unreleased]' heading, then append this "
4690
+ f"feature's enumeration under one of: "
4691
+ f"{', '.join(sorted(ENTRY_CLASSES))}. Projects scaffolded before "
4692
+ f"{CHANGELOG_PATH} was seeded will not have one; `specfuse upgrade` "
4693
+ f"creates it without touching an existing file (#575)",
4678
4694
  )
4679
4695
  result = parse_changelog(changelog.read_text())
4680
4696
  unreleased = result.unreleased()
@@ -4795,6 +4811,52 @@ def assert_implementation_touched_files(
4795
4811
  )
4796
4812
 
4797
4813
 
4814
+ def produces_shape_error(path: str) -> "str | None":
4815
+ """Return why *path* is an invalid ``produces:`` entry, or None if it is fine.
4816
+
4817
+ Shape only -- never presence. A path that does not exist yet is perfectly
4818
+ valid at draft time; whether it was actually produced is the dispatch
4819
+ gate's question, asked after the session.
4820
+
4821
+ Split out of ``assert_declared_deliverables`` so the one fact that needs no
4822
+ session output can be checked by every surface that has the frontmatter:
4823
+ ``lint_plan`` at draft/arm/conformance time, and the driver before it
4824
+ spends a token (#593). A real feature paid $6.42 across three byte-identical
4825
+ refusals for a ``Path.is_dir()`` call, with ``specfuse-lint`` reporting
4826
+ ``OK - structurally valid`` the whole time.
4827
+
4828
+ The three surfaces must read identically, so all render this one string.
4829
+ """
4830
+ if path.endswith("/") or Path(path).is_dir():
4831
+ return (
4832
+ f"declared deliverable is a directory: {path} — directories "
4833
+ "are not valid produces: entries under the unified "
4834
+ "literal/glob contract (a directory always passed presence "
4835
+ "while failing assert_produces_in_diff's diff match); declare "
4836
+ "the specific file(s) or a glob instead"
4837
+ )
4838
+ return None
4839
+
4840
+
4841
+ def assert_produces_shape(wu: WorkUnit) -> tuple[bool, str]:
4842
+ """Pre-dispatch half of ``assert_declared_deliverables`` (#593).
4843
+
4844
+ Every input is static: the WU's own frontmatter and the filesystem. None
4845
+ of it depends on what a session did, so refusing here converts a $2.46
4846
+ attempt into a $0.00 refusal -- the lever ``planning-discipline.md`` §5
4847
+ already names, where ``assert_verdict_well_formed`` fired 10 times for
4848
+ nothing because it is checked before the agent spends anything.
4849
+
4850
+ The agent cannot edit its own frontmatter, so a WU that fails this cannot
4851
+ be fixed by retrying it; escalating immediately is the only honest move.
4852
+ """
4853
+ for raw in wu.produces or []:
4854
+ err = produces_shape_error(str(raw))
4855
+ if err:
4856
+ return False, err
4857
+ return True, ""
4858
+
4859
+
4798
4860
  def assert_declared_deliverables(wu: WorkUnit) -> tuple[bool, str]:
4799
4861
  """Deliverable-presence gate (FEAT-2026-0022/T02), unified with
4800
4862
  ``assert_produces_in_diff``'s literal/glob contract (FEAT-2026-0055/T03).
@@ -4826,14 +4888,11 @@ def assert_declared_deliverables(wu: WorkUnit) -> tuple[bool, str]:
4826
4888
  for raw in wu.produces:
4827
4889
  path = str(raw)
4828
4890
  p = Path(path)
4829
- if path.endswith("/") or p.is_dir():
4830
- return False, (
4831
- f"declared deliverable is a directory: {path} — directories "
4832
- "are not valid produces: entries under the unified "
4833
- "literal/glob contract (a directory always passed presence "
4834
- "while failing assert_produces_in_diff's diff match); declare "
4835
- "the specific file(s) or a glob instead"
4836
- )
4891
+ # Shape refusal is shared with the pre-dispatch guard and lint_plan so
4892
+ # all three surfaces render one message (#593).
4893
+ shape_err = produces_shape_error(path)
4894
+ if shape_err:
4895
+ return False, shape_err
4837
4896
  if any(ch in path for ch in "*?["):
4838
4897
  matches = [
4839
4898
  m for m in glob.glob(path)
@@ -5511,6 +5570,34 @@ def run(
5511
5570
  blocked = True
5512
5571
  continue
5513
5572
 
5573
+ # produces: shape gate (#593). Same posture as the re-arm gate
5574
+ # above: fires BEFORE any dispatch state change, so a WU the
5575
+ # driver is guaranteed to refuse costs $0.00 instead of a full
5576
+ # session. Every input is static frontmatter plus the
5577
+ # filesystem, and the agent cannot edit its own frontmatter --
5578
+ # so retrying is provably useless. A real feature paid $6.42
5579
+ # across three byte-identical refusals before spinning fired.
5580
+ shape_ok, shape_reason = assert_produces_shape(wu)
5581
+ if not shape_ok:
5582
+ backend.set_wu(wu, "status", "blocked_human")
5583
+ flush_events(events_path, [build_event(
5584
+ "human_escalation", wu.wu_id, {
5585
+ "reason": "produces_shape_invalid",
5586
+ "blocked_reason": shape_reason,
5587
+ "attempts": 0,
5588
+ "failure_class": "guard_refusal",
5589
+ "failure_signature": "assert_produces_shape",
5590
+ })])
5591
+ commit_bookkeeping(
5592
+ [wu.file, events_path],
5593
+ f"chore(loop): {wu.wu_id} blocked "
5594
+ f"(produces: shape invalid)"
5595
+ f"\n\nFeature: {wu.wu_id}",
5596
+ )
5597
+ print(f" BLOCKED before dispatch (no cost) — {shape_reason}")
5598
+ blocked = True
5599
+ continue
5600
+
5514
5601
  head_before = git("rev-parse", "HEAD")
5515
5602
  # Snapshot the operator's untracked files alongside head_before:
5516
5603
  # squash_commit must not absorb WIP that pre-dates this dispatch
@@ -13,6 +13,9 @@ from pathlib import Path
13
13
 
14
14
  from . import _miniyaml
15
15
  from . import labels as _labels
16
+ # The one name for the file close-k reads; scaffold seeds what that guard
17
+ # requires, so both must agree on the path (#575).
18
+ from .closing_requirements import CHANGELOG_PATH
16
19
 
17
20
  try:
18
21
  from importlib.resources.abc import Traversable # Python 3.11+
@@ -163,8 +166,11 @@ _SEED_RENAME: dict[str, str] = {
163
166
 
164
167
  # Seed files handled by other work units; skip during init.
165
168
  # - gitignore.snippet: handled by T05 (wire_claude)
169
+ # - CHANGELOG.seed.md: a repo-root file, not a .specfuse/ one; wire_claude
170
+ # writes it to the project root (#575)
166
171
  _SKIP_SEEDS: frozenset[str] = frozenset({
167
172
  "gitignore.snippet",
173
+ "CHANGELOG.seed.md",
168
174
  })
169
175
 
170
176
 
@@ -260,6 +266,26 @@ def _write_gitignore(target_path: Path) -> None:
260
266
  gitignore.write_text(existing + snippet, encoding="utf-8")
261
267
 
262
268
 
269
+ def _write_changelog(target_path: Path) -> None:
270
+ """Seed a root CHANGELOG.md when the project has none (#575).
271
+
272
+ `close-k` fails a close whose §3 enumeration names a real consumer-visible
273
+ change unless `CHANGELOG.md`'s Unreleased gained a matching entry. Before
274
+ this, `specfuse init` never created the file, so a fresh project's first
275
+ such feature failed at its terminal close — the most expensive point in a
276
+ feature to be blocked — on a file the scaffold owed it.
277
+
278
+ Create-only. An existing CHANGELOG.md is a project's own document and is
279
+ never read, merged, or overwritten: `upgrade` routes through `wire_claude`
280
+ too, and silently rewriting a consumer-facing file on upgrade is worse than
281
+ the gap this closes.
282
+ """
283
+ changelog = target_path / CHANGELOG_PATH
284
+ if changelog.exists():
285
+ return
286
+ changelog.write_bytes(read_scaffold("CHANGELOG.seed.md"))
287
+
288
+
263
289
  def _write_claude_md(target_path: Path) -> None:
264
290
  claude_dir = target_path / ".claude"
265
291
  claude_dir.mkdir(parents=True, exist_ok=True)
@@ -355,6 +381,7 @@ def wire_claude(target: str | Path) -> None:
355
381
  """
356
382
  target_path = Path(target)
357
383
  _write_gitignore(target_path)
384
+ _write_changelog(target_path)
358
385
  _write_claude_md(target_path)
359
386
  _write_settings_json(target_path)
360
387
  refresh_claude_plugin_config(target_path)
@@ -146,6 +146,11 @@ def load_driver_correlation_patterns() -> dict | None:
146
146
  A missing, unreadable, or structurally incomplete registry file degrades
147
147
  to None (no widening — vendored-only validation) rather than raising,
148
148
  matching ``load_driver_event_types``'s existing contract.
149
+
150
+ ADDING A NEW ``<NAME>`` SEGMENT: update ``closing_names`` here AND
151
+ ``lint_plan.CORRELATION_ID_RE``, or the linter will accept IDs that
152
+ envelope validation rejects. The two surfaces enforce one contract and
153
+ must move together (#581).
149
154
  """
150
155
  global _DRIVER_CORRELATION_PATTERNS_CACHE
151
156
  if _DRIVER_CORRELATION_PATTERNS_CACHE is not _UNSET:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.9.0
3
+ Version: 0.9.2
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
@@ -15,6 +15,7 @@ specfuse/loop/closing_requirements.py
15
15
  specfuse/loop/cost.py
16
16
  specfuse/loop/escalation.py
17
17
  specfuse/loop/events_stats.py
18
+ specfuse/loop/gate_commands.py
18
19
  specfuse/loop/gate_eval.py
19
20
  specfuse/loop/gh_backend.py
20
21
  specfuse/loop/gh_features.py
@@ -27,6 +28,7 @@ specfuse/loop/loop.py
27
28
  specfuse/loop/plan_baseline.py
28
29
  specfuse/loop/scaffold.py
29
30
  specfuse/loop/validate_event.py
31
+ specfuse/loop/data/CHANGELOG.seed.md
30
32
  specfuse/loop/data/LEARNINGS.template.md
31
33
  specfuse/loop/data/VERSION
32
34
  specfuse/loop/data/gitignore.snippet
@@ -128,6 +130,7 @@ tests/test_bookkeeping_no_verify.py
128
130
  tests/test_changelog_collection.py
129
131
  tests/test_changelog_release_wiring.py
130
132
  tests/test_changelog_schema.py
133
+ tests/test_ci_runs_every_gate.py
131
134
  tests/test_claude_resolution.py
132
135
  tests/test_close_wu_self_reference.py
133
136
  tests/test_closing_deliverable_guard.py
@@ -136,6 +139,7 @@ tests/test_closing_guard_prediction.py
136
139
  tests/test_closing_requirements.py
137
140
  tests/test_console_encoding.py
138
141
  tests/test_correlation_id_override.py
142
+ tests/test_correlation_id_two_surfaces.py
139
143
  tests/test_cost_lifetime.py
140
144
  tests/test_cost_tracking.py
141
145
  tests/test_degraded_oracle.py
@@ -256,6 +260,7 @@ tests/test_prepare_scaffold_sync.py
256
260
  tests/test_produces_field.py
257
261
  tests/test_produces_in_diff.py
258
262
  tests/test_produces_semantics_unified.py
263
+ tests/test_produces_shape.py
259
264
  tests/test_provision_labels.py
260
265
  tests/test_queue_stalled_adapter.py
261
266
  tests/test_redaction_windows_home.py
@@ -286,6 +291,7 @@ tests/test_template_closing_shapes.py
286
291
  tests/test_terminal_flip_ownership.py
287
292
  tests/test_terminal_flips.py
288
293
  tests/test_timeout_kill_portable.py
294
+ tests/test_unterminated_frontmatter.py
289
295
  tests/test_untracked_feature_folder.py
290
296
  tests/test_untracked_file_absorption.py
291
297
  tests/test_upgrade_integration.py
@@ -216,3 +216,43 @@ class TestChangelogCollection(unittest.TestCase):
216
216
 
217
217
  if __name__ == "__main__":
218
218
  unittest.main()
219
+
220
+
221
+ class TestMissingChangelogMessage(unittest.TestCase):
222
+ """#575 — the absent-file halt must name the fix, not just the absence.
223
+
224
+ The bare message reported that CHANGELOG.md does not exist without saying
225
+ the operator should create one or that the scaffold now seeds it, so a
226
+ first-time user read a terminal-close halt as a broken install.
227
+ """
228
+
229
+ def _reason_for_missing_changelog(self, root: Path) -> str:
230
+ fdir = root / "feature"
231
+ fdir.mkdir()
232
+ (fdir / "RETROSPECTIVE.md").write_text(
233
+ "# Retrospective\n\n"
234
+ "## Consumer-visible contract changes\n\n"
235
+ "1. **New CLI flag `--foo`.** Additive, no removal.\n\n"
236
+ "## Cost analysis\n\nAll cheap.\n"
237
+ )
238
+ wu = _make_wu(file=fdir / "WU-close.md", wu_id="FEAT-2026-9999/G1-CLOSE")
239
+ ok, reason = loop.assert_changelog_entry_for_contract_changes(
240
+ wu, fdir, root, DUMMY_HEAD,
241
+ )
242
+ self.assertFalse(ok, "a missing CHANGELOG.md must still fail close-k")
243
+ return reason
244
+
245
+ def test_message_names_the_remedy_and_the_legal_classes(self):
246
+ with tempfile.TemporaryDirectory() as tmp:
247
+ reason = self._reason_for_missing_changelog(Path(tmp))
248
+ self.assertIn("[Unreleased]", reason)
249
+ self.assertIn("specfuse upgrade", reason)
250
+ self.assertIn("#575", reason)
251
+ for cls in ("added", "changed", "fixed", "breaking"):
252
+ self.assertIn(cls, reason, f"message omits legal class {cls!r}")
253
+
254
+ def test_message_says_upgrade_preserves_an_existing_file(self):
255
+ """Telling an operator to run `upgrade` is only safe if it cannot clobber."""
256
+ with tempfile.TemporaryDirectory() as tmp:
257
+ reason = self._reason_for_missing_changelog(Path(tmp))
258
+ self.assertIn("without touching an existing file", reason)