specfuse-loop 0.9.1__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.1/specfuse_loop.egg-info → specfuse_loop-0.9.2}/PKG-INFO +1 -1
  2. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/pyproject.toml +1 -1
  3. specfuse_loop-0.9.2/specfuse/loop/data/VERSION +1 -0
  4. specfuse_loop-0.9.2/specfuse/loop/gate_commands.py +94 -0
  5. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/lint_plan.py +79 -0
  6. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/loop.py +91 -9
  7. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2/specfuse_loop.egg-info}/PKG-INFO +1 -1
  8. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/SOURCES.txt +4 -0
  9. specfuse_loop-0.9.2/tests/test_ci_runs_every_gate.py +74 -0
  10. specfuse_loop-0.9.2/tests/test_produces_shape.py +160 -0
  11. specfuse_loop-0.9.2/tests/test_unterminated_frontmatter.py +139 -0
  12. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_upgrade_merge_gate.py +103 -0
  13. specfuse_loop-0.9.1/specfuse/loop/data/VERSION +0 -1
  14. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/LICENSE +0 -0
  15. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/NOTICE +0 -0
  16. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/README.md +0 -0
  17. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/setup.cfg +0 -0
  18. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/__init__.py +0 -0
  19. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/_filelock.py +0 -0
  20. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/_miniyaml.py +0 -0
  21. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/_wu_sections.py +0 -0
  22. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/adopt_feature.py +0 -0
  23. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/arm_eval.py +0 -0
  24. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/arm_sweep.py +0 -0
  25. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/arm_txn.py +0 -0
  26. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/changelog.py +0 -0
  27. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/closing_requirements.py +0 -0
  28. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/cost.py +0 -0
  29. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/CHANGELOG.seed.md +0 -0
  30. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  31. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/adopting-auto-mode.md +0 -0
  32. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  33. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/autonomy-stop-classes.md +0 -0
  34. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  35. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/getting-started.md +0 -0
  36. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/methodology.md +0 -0
  37. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/skills.md +0 -0
  38. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/gitignore.snippet +0 -0
  39. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
  40. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
  41. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring.yml.example +0 -0
  42. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/roadmap.template.md +0 -0
  43. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/close-discipline.md +0 -0
  44. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  45. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
  46. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/never-touch.md +0 -0
  47. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/operator-escalation.md +0 -0
  48. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/planning-discipline.md +0 -0
  49. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/result-contract.md +0 -0
  50. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  51. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  52. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/rules-local/README.md +0 -0
  53. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/driver-event.schema.json +0 -0
  54. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/event.schema.json +0 -0
  55. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  56. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  57. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  58. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  59. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/GATE.template.md +0 -0
  60. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/LEARNINGS-pending.template.md +0 -0
  61. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  62. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/WU.template.md +0 -0
  63. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/verification.yml.example +0 -0
  64. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
  65. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/escalation.py +0 -0
  66. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/events_stats.py +0 -0
  67. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/gate_eval.py +0 -0
  68. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/gh_backend.py +0 -0
  69. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/gh_features.py +0 -0
  70. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/labels.py +0 -0
  71. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/lint_closing.py +0 -0
  72. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/lint_monitoring.py +0 -0
  73. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/lint_roadmap.py +0 -0
  74. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/plan_baseline.py +0 -0
  75. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/scaffold.py +0 -0
  76. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/loop/validate_event.py +0 -0
  77. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/__init__.py +0 -0
  78. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/adapters.py +0 -0
  79. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/artifact.py +0 -0
  80. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/autofix.py +0 -0
  81. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_invoke.py +0 -0
  82. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_run.py +0 -0
  83. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_state.py +0 -0
  84. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/cli.py +0 -0
  85. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/diagnose_cli.py +0 -0
  86. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/diagnosis.py +0 -0
  87. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/fingerprint.py +0 -0
  88. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/issues.py +0 -0
  89. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/providers/__init__.py +0 -0
  90. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/providers/_azure_auth.py +0 -0
  91. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/providers/azure_app_insights.py +0 -0
  92. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/providers/azure_service_bus.py +0 -0
  93. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/redaction.py +0 -0
  94. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse/monitor/schedule.py +0 -0
  95. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  96. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/entry_points.txt +0 -0
  97. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/requires.txt +0 -0
  98. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/top_level.txt +0 -0
  99. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_accept_hedged_close_headline.py +0 -0
  100. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_accept_hedged_close_skill.py +0 -0
  101. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_adopt_feature.py +0 -0
  102. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_app_insights_adapters.py +0 -0
  103. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_arm_eval.py +0 -0
  104. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_arm_eval_lint_class.py +0 -0
  105. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_arm_eval_wiring.py +0 -0
  106. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  107. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_arm_sweep.py +0 -0
  108. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_arm_sweep_gate.py +0 -0
  109. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_arm_txn.py +0 -0
  110. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_arm_wiring.py +0 -0
  111. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_artifact_redaction.py +0 -0
  112. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_attempt_outcome_contract.py +0 -0
  113. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_attempt_outcome_emission.py +0 -0
  114. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_attention_nonwriting_guard.py +0 -0
  115. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_attention_skill_structure.py +0 -0
  116. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autoclose_deferral_visibility.py +0 -0
  117. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autofix_invoke.py +0 -0
  118. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autofix_live.py +0 -0
  119. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autofix_predicate.py +0 -0
  120. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autofix_run.py +0 -0
  121. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autofix_state.py +0 -0
  122. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autosync.py +0 -0
  123. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autosync_consent.py +0 -0
  124. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autosync_firstrun.py +0 -0
  125. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autosync_no_cwd_leak.py +0 -0
  126. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_autosync_plugin.py +0 -0
  127. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_azure_credential_resolution.py +0 -0
  128. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_backend.py +0 -0
  129. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_baseline_escalation_message.py +0 -0
  130. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_baseline_persistence.py +0 -0
  131. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_baseline_probe.py +0 -0
  132. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_bash_routing.py +0 -0
  133. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_bats_suites_gated.py +0 -0
  134. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  135. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  136. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_bookkeeping_no_verify.py +0 -0
  137. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_changelog_collection.py +0 -0
  138. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_changelog_release_wiring.py +0 -0
  139. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_changelog_schema.py +0 -0
  140. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_claude_resolution.py +0 -0
  141. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_close_wu_self_reference.py +0 -0
  142. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_closing_deliverable_guard.py +0 -0
  143. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_closing_guard_contracts.py +0 -0
  144. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_closing_guard_prediction.py +0 -0
  145. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_closing_requirements.py +0 -0
  146. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_console_encoding.py +0 -0
  147. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_correlation_id_override.py +0 -0
  148. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_correlation_id_two_surfaces.py +0 -0
  149. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_cost_lifetime.py +0 -0
  150. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_cost_tracking.py +0 -0
  151. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_degraded_oracle.py +0 -0
  152. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_deliverable_presence_gate.py +0 -0
  153. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_derive_monitoring_discovery.py +0 -0
  154. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_derive_monitoring_skill_registration.py +0 -0
  155. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_design_for_diagnosis_rule.py +0 -0
  156. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_diagnose_headless.py +0 -0
  157. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_diagnose_issue_skill.py +0 -0
  158. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_diagnosis.py +0 -0
  159. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_diagnosis_roundtrip_live.py +0 -0
  160. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_dispatch_skeleton.py +0 -0
  161. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_doctor.py +0 -0
  162. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_done_feature_gates.py +0 -0
  163. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_driver_event_registry_covers_emitters.py +0 -0
  164. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_driver_integration.py +0 -0
  165. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_driver_lock.py +0 -0
  166. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_duration_tracking.py +0 -0
  167. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_empty_files_escalation.py +0 -0
  168. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_ensure_feature_branch.py +0 -0
  169. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_ensure_feature_branch_base.py +0 -0
  170. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_escalation_contract.py +0 -0
  171. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_escalation_emit.py +0 -0
  172. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_event_gate_full_envelope.py +0 -0
  173. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_events_redaction.py +0 -0
  174. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_events_stats.py +0 -0
  175. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_extra_gates.py +0 -0
  176. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_failure_artifact_model.py +0 -0
  177. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_feature_id_resolution.py +0 -0
  178. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_feature_review_accumulation.py +0 -0
  179. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_filelock_portable.py +0 -0
  180. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_fingerprint.py +0 -0
  181. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_fix_bug_headless.py +0 -0
  182. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_force_full_close.py +0 -0
  183. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gate_budget.py +0 -0
  184. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gate_budget_post_dispatch.py +0 -0
  185. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gate_eval.py +0 -0
  186. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gate_eval_calibration.py +0 -0
  187. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  188. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gate_eval_terminal_wiring.py +0 -0
  189. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gate_report_verdict.py +0 -0
  190. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gh_backend.py +0 -0
  191. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gh_features.py +0 -0
  192. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_git_env_isolation.py +0 -0
  193. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_gitleaks_pinning.py +0 -0
  194. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_hashed_denylist.py +0 -0
  195. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_hashed_denylist_ci.py +0 -0
  196. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_heartbeat_adapter.py +0 -0
  197. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_hedged_kind_contract.py +0 -0
  198. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_init_integration.py +0 -0
  199. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_interpreter_normalization.py +0 -0
  200. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_label_registry.py +0 -0
  201. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_label_registry_covers_consumers.py +0 -0
  202. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_leak_findings_redaction.py +0 -0
  203. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_leak_scan.py +0 -0
  204. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_leak_scan_content.py +0 -0
  205. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_learnings_query.py +0 -0
  206. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_learnings_staging.py +0 -0
  207. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  208. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lifecycle_integration.py +0 -0
  209. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lifetime_accounting.py +0 -0
  210. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_bare_produces_path.py +0 -0
  211. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_boundary_consistency.py +0 -0
  212. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_boundary_extraction.py +0 -0
  213. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_close_intermediate.py +0 -0
  214. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_close_wu.py +0 -0
  215. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_closing.py +0 -0
  216. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_correlation_id.py +0 -0
  217. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  218. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_monitoring.py +0 -0
  219. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_oracle_env.py +0 -0
  220. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_plan_contract_fields.py +0 -0
  221. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_plan_errors.py +0 -0
  222. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_plan_next_draft.py +0 -0
  223. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_plan_relative_import.py +0 -0
  224. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_plan_verdict_exempt.py +0 -0
  225. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_planning_sections.py +0 -0
  226. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_produces_driver_helper.py +0 -0
  227. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_produces_satisfiability.py +0 -0
  228. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_roadmap.py +0 -0
  229. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_sections.py +0 -0
  230. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  231. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_lint_verification_reference.py +0 -0
  232. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_attempt_evidence.py +0 -0
  233. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_auto_archive.py +0 -0
  234. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_caveman_preamble.py +0 -0
  235. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_close_intermediate.py +0 -0
  236. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_defaults_by_type.py +0 -0
  237. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_effort.py +0 -0
  238. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_failure_note_cap.py +0 -0
  239. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_files_changed_guard.py +0 -0
  240. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_gate_budget.py +0 -0
  241. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_model_alias.py +0 -0
  242. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_orchestration.py +0 -0
  243. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_post_pass_invariant.py +0 -0
  244. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_reset_preserving_events.py +0 -0
  245. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_smoke_runner.py +0 -0
  246. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_unsandboxed.py +0 -0
  247. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_loop_zero_token_guard.py +0 -0
  248. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_migrate_legacy.py +0 -0
  249. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_miniyaml_equivalence.py +0 -0
  250. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_miniyaml_negative.py +0 -0
  251. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_monitor_cli.py +0 -0
  252. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_monitor_issue_lifecycle.py +0 -0
  253. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_monitor_runner_surfaces.py +0 -0
  254. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
  255. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_monitoring_cron_dialect.py +0 -0
  256. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_monitoring_example.py +0 -0
  257. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_monitoring_fenced_blocks.py +0 -0
  258. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_monitoring_seed.py +0 -0
  259. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_no_pytest_subprocess.py +0 -0
  260. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_operator_escalation_rule.py +0 -0
  261. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_plan_baseline.py +0 -0
  262. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_planned_cost_lint.py +0 -0
  263. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_planning_cost_floor.py +0 -0
  264. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_prepare_feature.py +0 -0
  265. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_prepare_scaffold_sync.py +0 -0
  266. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_produces_field.py +0 -0
  267. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_produces_in_diff.py +0 -0
  268. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_produces_semantics_unified.py +0 -0
  269. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_provision_labels.py +0 -0
  270. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_queue_stalled_adapter.py +0 -0
  271. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_redaction_windows_home.py +0 -0
  272. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_resolve_base.py +0 -0
  273. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_result_block.py +0 -0
  274. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_roadmap_add_skill.py +0 -0
  275. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_roadmap_archive_skill.py +0 -0
  276. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_roadmap_link_gate.py +0 -0
  277. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_roadmap_row_parser.py +0 -0
  278. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_routed_finding_tracking.py +0 -0
  279. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_data_in_sync.py +0 -0
  280. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_doc_hygiene.py +0 -0
  281. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_docs.py +0 -0
  282. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_init.py +0 -0
  283. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_label_provisioning.py +0 -0
  284. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_manifest.py +0 -0
  285. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_resources.py +0 -0
  286. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_seed_sanity.py +0 -0
  287. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_upgrade.py +0 -0
  288. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_scaffold_wiring.py +0 -0
  289. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_schedule_dialect.py +0 -0
  290. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_service_bus_dlq_adapter.py +0 -0
  291. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_skill_discovery_links.py +0 -0
  292. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_skills_vendored_in_sync.py +0 -0
  293. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_spinning_rearm_gate.py +0 -0
  294. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_squash_commit_hook_crash.py +0 -0
  295. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_template_closing_shapes.py +0 -0
  296. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_terminal_flip_ownership.py +0 -0
  297. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_terminal_flips.py +0 -0
  298. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_timeout_kill_portable.py +0 -0
  299. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_untracked_feature_folder.py +0 -0
  300. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_untracked_file_absorption.py +0 -0
  301. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_upgrade_integration.py +0 -0
  302. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_validate_event.py +0 -0
  303. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_validate_event_driver_types.py +0 -0
  304. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_verdict_coupling.py +0 -0
  305. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_verify_empty_gate_set.py +0 -0
  306. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_version_consistency.py +0 -0
  307. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_version_skew.py +0 -0
  308. {specfuse_loop-0.9.1 → specfuse_loop-0.9.2}/tests/test_windows_gate_exec.py +0 -0
  309. {specfuse_loop-0.9.1 → 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.1
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.1"
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 @@
1
+ 0.9.2
@@ -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())
@@ -240,6 +240,15 @@ def read_frontmatter(path: Path) -> tuple[dict, str]:
240
240
  j = 1
241
241
  while j < len(lines) and not FM.match(lines[j]):
242
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
+ )
243
252
  return _miniyaml.parse("\n".join(lines[1:j])) or {}, "\n".join(lines[j + 1:])
244
253
 
245
254
 
@@ -659,6 +668,60 @@ def _match_dnt_boundary(
659
668
  return None
660
669
 
661
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
+
662
725
  def check_produces_boundary(feature_dir: Path, gates: list) -> list[str]:
663
726
  """ERROR when a WU's own `produces:` path (or `produces_driver_helper`
664
727
  surface) falls inside its own Do-not-touch section's paths.
@@ -893,6 +956,21 @@ def check_planning_sections(
893
956
 
894
957
 
895
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]:
896
974
  errs: list[str] = []
897
975
  plan = feature_dir / "PLAN.md"
898
976
  if not plan.exists():
@@ -1212,6 +1290,7 @@ def lint(feature_dir: Path) -> list[str]:
1212
1290
  check_closing_guard_literals(feature_dir, gates)
1213
1291
  check_autoclose_debt_prediction(feature_dir, gates)
1214
1292
  check_produces_satisfiability(feature_dir, gates)
1293
+ errs.extend(check_produces_shape(feature_dir, gates))
1215
1294
  errs.extend(check_produces_boundary(feature_dir, gates))
1216
1295
  errs.extend(check_done_feature_gates(feature_dir, fm))
1217
1296
 
@@ -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.1"
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:
@@ -4800,6 +4811,52 @@ def assert_implementation_touched_files(
4800
4811
  )
4801
4812
 
4802
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
+
4803
4860
  def assert_declared_deliverables(wu: WorkUnit) -> tuple[bool, str]:
4804
4861
  """Deliverable-presence gate (FEAT-2026-0022/T02), unified with
4805
4862
  ``assert_produces_in_diff``'s literal/glob contract (FEAT-2026-0055/T03).
@@ -4831,14 +4888,11 @@ def assert_declared_deliverables(wu: WorkUnit) -> tuple[bool, str]:
4831
4888
  for raw in wu.produces:
4832
4889
  path = str(raw)
4833
4890
  p = Path(path)
4834
- if path.endswith("/") or p.is_dir():
4835
- return False, (
4836
- f"declared deliverable is a directory: {path} — directories "
4837
- "are not valid produces: entries under the unified "
4838
- "literal/glob contract (a directory always passed presence "
4839
- "while failing assert_produces_in_diff's diff match); declare "
4840
- "the specific file(s) or a glob instead"
4841
- )
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
4842
4896
  if any(ch in path for ch in "*?["):
4843
4897
  matches = [
4844
4898
  m for m in glob.glob(path)
@@ -5516,6 +5570,34 @@ def run(
5516
5570
  blocked = True
5517
5571
  continue
5518
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
+
5519
5601
  head_before = git("rev-parse", "HEAD")
5520
5602
  # Snapshot the operator's untracked files alongside head_before:
5521
5603
  # squash_commit must not absorb WIP that pre-dates this dispatch
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.9.1
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
@@ -129,6 +130,7 @@ tests/test_bookkeeping_no_verify.py
129
130
  tests/test_changelog_collection.py
130
131
  tests/test_changelog_release_wiring.py
131
132
  tests/test_changelog_schema.py
133
+ tests/test_ci_runs_every_gate.py
132
134
  tests/test_claude_resolution.py
133
135
  tests/test_close_wu_self_reference.py
134
136
  tests/test_closing_deliverable_guard.py
@@ -258,6 +260,7 @@ tests/test_prepare_scaffold_sync.py
258
260
  tests/test_produces_field.py
259
261
  tests/test_produces_in_diff.py
260
262
  tests/test_produces_semantics_unified.py
263
+ tests/test_produces_shape.py
261
264
  tests/test_provision_labels.py
262
265
  tests/test_queue_stalled_adapter.py
263
266
  tests/test_redaction_windows_home.py
@@ -288,6 +291,7 @@ tests/test_template_closing_shapes.py
288
291
  tests/test_terminal_flip_ownership.py
289
292
  tests/test_terminal_flips.py
290
293
  tests/test_timeout_kill_portable.py
294
+ tests/test_unterminated_frontmatter.py
291
295
  tests/test_untracked_feature_folder.py
292
296
  tests/test_untracked_file_absorption.py
293
297
  tests/test_upgrade_integration.py
@@ -0,0 +1,74 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """#592 — CI must run every gate `verification.yml` declares.
6
+
7
+ `scripts/smoke-test.sh` carried a hand-maintained copy of the `code` gate set
8
+ under the comment "Keep in sync". It drifted by six: `roadmap-link-gate`,
9
+ `arm-sweep-gate`, `monitoring-example-lint`, `sync-scaffold-symlinks-bats`,
10
+ `init-sh-shim-bats`, and `hookspath-conflict-bats` were declared and never
11
+ executed.
12
+
13
+ A declared-but-unrun gate is worse than no gate: `verification.yml` is the
14
+ source of truth for what gets verified, so an unrun entry reads as coverage.
15
+ `sync_scaffold_symlinks.bats` sat red through two merges because of this, and
16
+ its silence made a one-PR-old regression look environmental.
17
+
18
+ `init-skills-bats` already carried the comment "Was silently red + un-run
19
+ before #121" — the same problem, fixed once for one gate.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import unittest
25
+ from pathlib import Path
26
+
27
+ from specfuse.loop.gate_commands import code_gate_names, iter_code_gates
28
+
29
+ REPO_ROOT = Path(__file__).resolve().parents[1]
30
+ SMOKE = REPO_ROOT / "scripts" / "smoke-test.sh"
31
+ VERIFICATION = REPO_ROOT / ".specfuse" / "verification.yml"
32
+
33
+
34
+ class TestGateParity(unittest.TestCase):
35
+ def test_declared_gates_were_found(self):
36
+ """Guard the guard: an empty parse would make every check vacuous."""
37
+ names = code_gate_names(VERIFICATION)
38
+ self.assertGreaterEqual(len(names), 10, names)
39
+ self.assertIn("tests", names)
40
+ self.assertIn("roadmap-link-gate", names)
41
+
42
+ def test_every_declared_gate_has_a_command(self):
43
+ for name, command in iter_code_gates(VERIFICATION):
44
+ with self.subTest(gate=name):
45
+ self.assertTrue(command.strip(), f"{name} declares no command")
46
+
47
+ def test_smoke_test_derives_gates_rather_than_listing_them(self):
48
+ """The drift is structural: any hand-copied list can fall behind.
49
+
50
+ Asserts the runner iterates the declared set instead of naming gates
51
+ one by one, so a gate added to verification.yml is run without anyone
52
+ remembering to touch the shell script.
53
+ """
54
+ sh = SMOKE.read_text()
55
+ self.assertIn("gate_commands", sh,
56
+ "smoke-test.sh does not derive its gates from "
57
+ "verification.yml; a seventh drift is a matter of time")
58
+
59
+ def test_no_declared_gate_is_hardcoded_out_of_the_run(self):
60
+ """Every declared gate must be reachable by the runner's iteration.
61
+
62
+ Catches the specific failure this issue is about: a gate present in
63
+ verification.yml that the CI runner never executes.
64
+ """
65
+ declared = set(code_gate_names(VERIFICATION))
66
+ emitted = {name for name, _ in iter_code_gates(VERIFICATION)}
67
+ self.assertEqual(
68
+ declared - emitted, set(),
69
+ "declared gates the runner would skip",
70
+ )
71
+
72
+
73
+ if __name__ == "__main__":
74
+ unittest.main()
@@ -0,0 +1,160 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """#593 — a directory in `produces:` must be caught before a session runs.
6
+
7
+ `assert_declared_deliverables` refuses a `produces:` entry that resolves to a
8
+ directory. The verdict is right; the timing was not. It ran after a full
9
+ `claude -p` session, three times, for a fact that is a `Path.is_dir()` call on
10
+ static frontmatter: $6.42 and 20.6 minutes observed on a real feature, with
11
+ `specfuse-lint` reporting `OK — structurally valid` throughout.
12
+
13
+ Two surfaces, one contract:
14
+ - `lint_plan` must ERROR at draft/arm/conformance time, before the loop runs;
15
+ - the driver must refuse at dispatch, before a token is spent.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import contextlib
21
+ import io
22
+ import tempfile
23
+ import unittest
24
+ from pathlib import Path
25
+
26
+ from specfuse.loop.loop import produces_shape_error
27
+ from tests._loop_loader import load_lint
28
+ from tests.test_lint_boundary_consistency import _build_feature
29
+
30
+ lint_plan = load_lint()
31
+
32
+
33
+ class TestProducesShapeError(unittest.TestCase):
34
+ """The shared predicate both surfaces call — pure, no session input."""
35
+
36
+ def test_trailing_slash_is_refused_without_touching_the_filesystem(self):
37
+ err = produces_shape_error("src/main/java/dev/example/regression/")
38
+ self.assertIsNotNone(err)
39
+ self.assertIn("directory", err)
40
+
41
+ def test_existing_directory_is_refused(self):
42
+ with tempfile.TemporaryDirectory() as tmp:
43
+ d = Path(tmp) / "pkg"
44
+ d.mkdir()
45
+ self.assertIsNotNone(produces_shape_error(str(d)))
46
+
47
+ def test_plain_file_path_is_accepted(self):
48
+ self.assertIsNone(produces_shape_error("specfuse/loop/loop.py"))
49
+
50
+ def test_glob_is_accepted(self):
51
+ self.assertIsNone(produces_shape_error("src/**/Regression*.java"))
52
+
53
+ def test_nonexistent_file_path_is_accepted(self):
54
+ """Shape only. Presence is the dispatch gate's job, not this one."""
55
+ self.assertIsNone(produces_shape_error("does/not/exist/yet.py"))
56
+
57
+
58
+ class TestLintCatchesProducesShape(unittest.TestCase):
59
+ """Surface 1: the pre-dispatch checklist must not pass a doomed WU."""
60
+
61
+ def _lint(self, produces: str) -> list[str]:
62
+ with tempfile.TemporaryDirectory() as tmp:
63
+ feat = _build_feature(
64
+ Path(tmp),
65
+ [{
66
+ "id": "FEAT-2026-0099/T01", "file": "WU-01.md",
67
+ "type": "implementation", "status": "pending",
68
+ "do_not_touch": "- nothing relevant.",
69
+ "produces": [produces],
70
+ }],
71
+ )
72
+ buf = io.StringIO()
73
+ with contextlib.redirect_stdout(buf):
74
+ errs = lint_plan.lint(feat)
75
+ return [str(e) for e in errs] + buf.getvalue().splitlines()
76
+
77
+ def test_directory_produces_is_an_error_finding(self):
78
+ found = [f for f in self._lint("src/regression/") if "directory" in f]
79
+ self.assertTrue(
80
+ found,
81
+ "lint passed a produces: entry the driver is guaranteed to refuse "
82
+ "- that is the $6.42 in #593",
83
+ )
84
+ self.assertTrue(
85
+ any(f.startswith("ERROR") for f in found),
86
+ f"directory produces: must be ERROR, not WARN: {found}",
87
+ )
88
+
89
+ def test_the_error_names_the_guard_it_preempts(self):
90
+ """Earlier-enforcer-names-the-later-one, per FEAT-2026-0070."""
91
+ found = [f for f in self._lint("src/regression/") if "directory" in f]
92
+ self.assertTrue(any("assert_declared_deliverables" in f for f in found))
93
+
94
+ def test_file_produces_is_not_flagged(self):
95
+ self.assertEqual(
96
+ [f for f in self._lint("src/Regression.java") if "directory" in f], [],
97
+ )
98
+
99
+ def test_glob_produces_is_not_flagged(self):
100
+ self.assertEqual(
101
+ [f for f in self._lint("src/**/*.java") if "directory" in f], [],
102
+ )
103
+
104
+
105
+
106
+ class TestDispatchRefusesBeforeSpending(unittest.TestCase):
107
+ """Surface 2: the driver must refuse without opening a session."""
108
+
109
+ def test_shape_gate_is_wired_before_the_attempt_loop(self):
110
+ """The guard is worthless if it is defined but never reached.
111
+
112
+ Asserts the call site sits BEFORE `head_before = git("rev-parse",
113
+ "HEAD")`, which is where dispatch state starts changing. A guard
114
+ placed after that point would still cost the session it exists to
115
+ prevent.
116
+ """
117
+ src = (
118
+ Path(__file__).resolve().parents[1]
119
+ / "specfuse" / "loop" / "loop.py"
120
+ ).read_text()
121
+ call = src.index("shape_ok, shape_reason = assert_produces_shape(wu)")
122
+ head = src.index('head_before = git("rev-parse", "HEAD")')
123
+ attempt_loop = src.index("for attempt in range(1, MAX_ATTEMPTS + 1):")
124
+ self.assertLess(call, head, "shape gate runs after dispatch begins")
125
+ self.assertLess(call, attempt_loop, "shape gate runs inside the attempt loop")
126
+
127
+ def test_refusal_escalates_immediately_rather_than_retrying(self):
128
+ """A retry cannot help: the agent cannot edit its own frontmatter."""
129
+ src = (
130
+ Path(__file__).resolve().parents[1]
131
+ / "specfuse" / "loop" / "loop.py"
132
+ ).read_text()
133
+ block = src[src.index("shape_ok, shape_reason = assert_produces_shape(wu)"):][:1200]
134
+ self.assertIn('backend.set_wu(wu, "status", "blocked_human")', block)
135
+ self.assertIn("produces_shape_invalid", block)
136
+
137
+ def test_refusal_carries_a_failure_class_so_it_clusters(self):
138
+ """#593's smaller finding: guard refusals were invisible to
139
+ learnings-suggest because failure_class/failure_signature were null."""
140
+ src = (
141
+ Path(__file__).resolve().parents[1]
142
+ / "specfuse" / "loop" / "loop.py"
143
+ ).read_text()
144
+ block = src[src.index("shape_ok, shape_reason = assert_produces_shape(wu)"):][:1200]
145
+ self.assertIn('"failure_class": "guard_refusal"', block)
146
+ self.assertIn('"failure_signature": "assert_produces_shape"', block)
147
+
148
+ def test_shape_guard_and_dispatch_guard_render_one_message(self):
149
+ """Three surfaces, one wording -- they must not drift apart."""
150
+ from specfuse.loop.loop import WorkUnit, assert_produces_shape
151
+ wu = WorkUnit.__new__(WorkUnit)
152
+ object.__setattr__(wu, "produces", ["src/regression/"]) if hasattr(
153
+ WorkUnit, "__slots__") else setattr(wu, "produces", ["src/regression/"])
154
+ ok, reason = assert_produces_shape(wu)
155
+ self.assertFalse(ok)
156
+ self.assertEqual(reason, produces_shape_error("src/regression/"))
157
+
158
+
159
+ if __name__ == "__main__":
160
+ unittest.main()