specfuse-loop 0.7.0__tar.gz → 0.8.0__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 (264) hide show
  1. {specfuse_loop-0.7.0/specfuse_loop.egg-info → specfuse_loop-0.8.0}/PKG-INFO +1 -1
  2. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/pyproject.toml +1 -1
  3. specfuse_loop-0.8.0/specfuse/loop/_wu_sections.py +51 -0
  4. specfuse_loop-0.8.0/specfuse/loop/arm_eval.py +405 -0
  5. specfuse_loop-0.8.0/specfuse/loop/arm_txn.py +276 -0
  6. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/closing_requirements.py +21 -0
  7. specfuse_loop-0.8.0/specfuse/loop/data/VERSION +1 -0
  8. specfuse_loop-0.8.0/specfuse/loop/data/docs/concepts/adopting-auto-mode.md +151 -0
  9. specfuse_loop-0.8.0/specfuse/loop/data/docs/concepts/autonomy-stop-classes.md +212 -0
  10. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/methodology.md +53 -11
  11. specfuse_loop-0.8.0/specfuse/loop/data/templates/LEARNINGS-pending.template.md +37 -0
  12. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/templates/WU.template.md +13 -1
  13. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/lint_plan.py +327 -0
  14. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/loop.py +224 -23
  15. specfuse_loop-0.8.0/specfuse/loop/plan_baseline.py +102 -0
  16. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0/specfuse_loop.egg-info}/PKG-INFO +1 -1
  17. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/SOURCES.txt +19 -0
  18. specfuse_loop-0.8.0/tests/test_arm_eval.py +411 -0
  19. specfuse_loop-0.8.0/tests/test_arm_eval_lint_class.py +103 -0
  20. specfuse_loop-0.8.0/tests/test_arm_eval_wiring.py +236 -0
  21. specfuse_loop-0.8.0/tests/test_arm_txn.py +215 -0
  22. specfuse_loop-0.8.0/tests/test_arm_wiring.py +481 -0
  23. specfuse_loop-0.8.0/tests/test_feature_review_accumulation.py +268 -0
  24. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_init_integration.py +4 -0
  25. specfuse_loop-0.8.0/tests/test_learnings_staging.py +262 -0
  26. specfuse_loop-0.8.0/tests/test_lint_boundary_consistency.py +194 -0
  27. specfuse_loop-0.8.0/tests/test_lint_boundary_extraction.py +228 -0
  28. specfuse_loop-0.8.0/tests/test_lint_plan_contract_fields.py +164 -0
  29. specfuse_loop-0.8.0/tests/test_lint_produces_satisfiability.py +209 -0
  30. specfuse_loop-0.8.0/tests/test_plan_baseline.py +144 -0
  31. specfuse_loop-0.8.0/tests/test_produces_semantics_unified.py +190 -0
  32. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_data_in_sync.py +3 -0
  33. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_init.py +3 -0
  34. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_resources.py +3 -0
  35. specfuse_loop-0.7.0/specfuse/loop/_wu_sections.py +0 -44
  36. specfuse_loop-0.7.0/specfuse/loop/data/VERSION +0 -1
  37. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/LICENSE +0 -0
  38. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/NOTICE +0 -0
  39. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/README.md +0 -0
  40. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/setup.cfg +0 -0
  41. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/__init__.py +0 -0
  42. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/_filelock.py +0 -0
  43. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/_miniyaml.py +0 -0
  44. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/adopt_feature.py +0 -0
  45. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  46. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  47. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  48. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/getting-started.md +0 -0
  49. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/docs/skills.md +0 -0
  50. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/gitignore.snippet +0 -0
  51. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
  52. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
  53. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/monitoring.yml.example +0 -0
  54. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/roadmap.template.md +0 -0
  55. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/close-discipline.md +0 -0
  56. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  57. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
  58. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/never-touch.md +0 -0
  59. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/operator-escalation.md +0 -0
  60. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/planning-discipline.md +0 -0
  61. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/result-contract.md +0 -0
  62. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  63. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  64. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/rules-local/README.md +0 -0
  65. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/event.schema.json +0 -0
  66. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  67. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  68. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  69. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  70. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/templates/GATE.template.md +0 -0
  71. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  72. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/verification.yml.example +0 -0
  73. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
  74. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/escalation.py +0 -0
  75. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/events_stats.py +0 -0
  76. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/gate_eval.py +0 -0
  77. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/gh_backend.py +0 -0
  78. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/gh_features.py +0 -0
  79. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/labels.py +0 -0
  80. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/lint_closing.py +0 -0
  81. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/lint_monitoring.py +0 -0
  82. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/scaffold.py +0 -0
  83. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/loop/validate_event.py +0 -0
  84. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/__init__.py +0 -0
  85. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/adapters.py +0 -0
  86. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/artifact.py +0 -0
  87. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/cli.py +0 -0
  88. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/fingerprint.py +0 -0
  89. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/issues.py +0 -0
  90. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/providers/__init__.py +0 -0
  91. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/providers/_azure_auth.py +0 -0
  92. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/providers/azure_app_insights.py +0 -0
  93. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/providers/azure_service_bus.py +0 -0
  94. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/redaction.py +0 -0
  95. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse/monitor/schedule.py +0 -0
  96. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  97. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/entry_points.txt +0 -0
  98. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/requires.txt +0 -0
  99. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/specfuse_loop.egg-info/top_level.txt +0 -0
  100. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_accept_hedged_close_skill.py +0 -0
  101. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_adopt_feature.py +0 -0
  102. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_app_insights_adapters.py +0 -0
  103. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  104. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_artifact_redaction.py +0 -0
  105. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_attempt_outcome_contract.py +0 -0
  106. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_attempt_outcome_emission.py +0 -0
  107. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_attention_nonwriting_guard.py +0 -0
  108. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_attention_skill_structure.py +0 -0
  109. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autoclose_deferral_visibility.py +0 -0
  110. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync.py +0 -0
  111. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync_consent.py +0 -0
  112. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync_firstrun.py +0 -0
  113. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync_no_cwd_leak.py +0 -0
  114. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_autosync_plugin.py +0 -0
  115. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_azure_credential_resolution.py +0 -0
  116. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_backend.py +0 -0
  117. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_baseline_escalation_message.py +0 -0
  118. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_baseline_persistence.py +0 -0
  119. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_baseline_probe.py +0 -0
  120. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bash_routing.py +0 -0
  121. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bats_suites_gated.py +0 -0
  122. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  123. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  124. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_bookkeeping_no_verify.py +0 -0
  125. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_claude_resolution.py +0 -0
  126. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_close_wu_self_reference.py +0 -0
  127. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_closing_deliverable_guard.py +0 -0
  128. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_closing_guard_contracts.py +0 -0
  129. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_closing_guard_prediction.py +0 -0
  130. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_closing_requirements.py +0 -0
  131. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_console_encoding.py +0 -0
  132. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_cost_tracking.py +0 -0
  133. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_degraded_oracle.py +0 -0
  134. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_deliverable_presence_gate.py +0 -0
  135. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_derive_monitoring_discovery.py +0 -0
  136. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_derive_monitoring_skill_registration.py +0 -0
  137. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_design_for_diagnosis_rule.py +0 -0
  138. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_dispatch_skeleton.py +0 -0
  139. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_doctor.py +0 -0
  140. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_done_feature_gates.py +0 -0
  141. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_driver_integration.py +0 -0
  142. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_driver_lock.py +0 -0
  143. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_duration_tracking.py +0 -0
  144. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_empty_files_escalation.py +0 -0
  145. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_ensure_feature_branch.py +0 -0
  146. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_ensure_feature_branch_base.py +0 -0
  147. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_escalation_contract.py +0 -0
  148. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_escalation_emit.py +0 -0
  149. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_events_redaction.py +0 -0
  150. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_events_stats.py +0 -0
  151. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_extra_gates.py +0 -0
  152. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_failure_artifact_model.py +0 -0
  153. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_feature_id_resolution.py +0 -0
  154. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_filelock_portable.py +0 -0
  155. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_fingerprint.py +0 -0
  156. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_force_full_close.py +0 -0
  157. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gate_eval.py +0 -0
  158. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gate_eval_calibration.py +0 -0
  159. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  160. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gate_eval_terminal_wiring.py +0 -0
  161. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gh_backend.py +0 -0
  162. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gh_features.py +0 -0
  163. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_git_env_isolation.py +0 -0
  164. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_gitleaks_pinning.py +0 -0
  165. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_hashed_denylist.py +0 -0
  166. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_hashed_denylist_ci.py +0 -0
  167. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_heartbeat_adapter.py +0 -0
  168. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_interpreter_normalization.py +0 -0
  169. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_label_registry.py +0 -0
  170. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_label_registry_covers_consumers.py +0 -0
  171. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_leak_findings_redaction.py +0 -0
  172. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_leak_scan.py +0 -0
  173. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_leak_scan_content.py +0 -0
  174. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_learnings_query.py +0 -0
  175. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  176. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lifecycle_integration.py +0 -0
  177. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lifetime_accounting.py +0 -0
  178. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_bare_produces_path.py +0 -0
  179. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_close_intermediate.py +0 -0
  180. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_close_wu.py +0 -0
  181. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_closing.py +0 -0
  182. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_correlation_id.py +0 -0
  183. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  184. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_monitoring.py +0 -0
  185. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_oracle_env.py +0 -0
  186. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_plan_errors.py +0 -0
  187. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_plan_next_draft.py +0 -0
  188. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_plan_relative_import.py +0 -0
  189. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_plan_verdict_exempt.py +0 -0
  190. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_planning_sections.py +0 -0
  191. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_produces_driver_helper.py +0 -0
  192. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_sections.py +0 -0
  193. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  194. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_lint_verification_reference.py +0 -0
  195. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_attempt_evidence.py +0 -0
  196. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_auto_archive.py +0 -0
  197. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_caveman_preamble.py +0 -0
  198. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_close_intermediate.py +0 -0
  199. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_defaults_by_type.py +0 -0
  200. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_effort.py +0 -0
  201. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_failure_note_cap.py +0 -0
  202. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_files_changed_guard.py +0 -0
  203. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_gate_budget.py +0 -0
  204. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_model_alias.py +0 -0
  205. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_orchestration.py +0 -0
  206. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_post_pass_invariant.py +0 -0
  207. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_reset_preserving_events.py +0 -0
  208. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_smoke_runner.py +0 -0
  209. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_unsandboxed.py +0 -0
  210. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_loop_zero_token_guard.py +0 -0
  211. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_migrate_legacy.py +0 -0
  212. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_miniyaml_equivalence.py +0 -0
  213. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_miniyaml_negative.py +0 -0
  214. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitor_cli.py +0 -0
  215. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitor_issue_lifecycle.py +0 -0
  216. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitor_runner_surfaces.py +0 -0
  217. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
  218. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_cron_dialect.py +0 -0
  219. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_example.py +0 -0
  220. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_fenced_blocks.py +0 -0
  221. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_monitoring_seed.py +0 -0
  222. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_operator_escalation_rule.py +0 -0
  223. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_planned_cost_lint.py +0 -0
  224. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_planning_cost_floor.py +0 -0
  225. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_prepare_feature.py +0 -0
  226. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_prepare_scaffold_sync.py +0 -0
  227. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_produces_field.py +0 -0
  228. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_produces_in_diff.py +0 -0
  229. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_provision_labels.py +0 -0
  230. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_queue_stalled_adapter.py +0 -0
  231. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_redaction_windows_home.py +0 -0
  232. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_resolve_base.py +0 -0
  233. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_result_block.py +0 -0
  234. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_roadmap_add_skill.py +0 -0
  235. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_roadmap_archive_skill.py +0 -0
  236. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_roadmap_row_parser.py +0 -0
  237. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_doc_hygiene.py +0 -0
  238. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_docs.py +0 -0
  239. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_label_provisioning.py +0 -0
  240. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_manifest.py +0 -0
  241. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_seed_sanity.py +0 -0
  242. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_upgrade.py +0 -0
  243. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_scaffold_wiring.py +0 -0
  244. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_schedule_dialect.py +0 -0
  245. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_service_bus_dlq_adapter.py +0 -0
  246. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_skill_discovery_links.py +0 -0
  247. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_skills_vendored_in_sync.py +0 -0
  248. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_spinning_rearm_gate.py +0 -0
  249. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_squash_commit_hook_crash.py +0 -0
  250. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_template_closing_shapes.py +0 -0
  251. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_terminal_flip_ownership.py +0 -0
  252. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_terminal_flips.py +0 -0
  253. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_timeout_kill_portable.py +0 -0
  254. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_untracked_feature_folder.py +0 -0
  255. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_untracked_file_absorption.py +0 -0
  256. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_upgrade_integration.py +0 -0
  257. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_upgrade_merge_gate.py +0 -0
  258. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_validate_event.py +0 -0
  259. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_verdict_coupling.py +0 -0
  260. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_verify_empty_gate_set.py +0 -0
  261. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_version_consistency.py +0 -0
  262. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_version_skew.py +0 -0
  263. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/tests/test_windows_gate_exec.py +0 -0
  264. {specfuse_loop-0.7.0 → specfuse_loop-0.8.0}/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.7.0
3
+ Version: 0.8.0
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.7.0"
7
+ version = "0.8.0"
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,51 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright 2026 Specfuse contributors
4
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
5
+ #
6
+ """
7
+ Dependency-free slicers for work-unit body sections.
8
+
9
+ Shared by `loop.py` and `lint_plan.py`, which cannot import each other
10
+ (`lint_plan.py` imports `VERDICT_VALUES` from `loop.py`, so the reverse
11
+ import would be a cycle). This module imports nothing from either.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ _AC_END_RE = re.compile(r"(?m)^(?:\*\*|#{1,6}\s)")
19
+
20
+
21
+ def slice_wu_section(body: str, section_name: str) -> str:
22
+ """Return content between a named section heading and the next heading.
23
+
24
+ Handles both section-heading shapes a WU body uses: the canonical
25
+ bold-preamble form (`**Section name.** content starting on the same
26
+ line`, what `.specfuse/templates/WU.template.md` prescribes and what
27
+ 327/327 real WU bodies in this repo use) and the ATX form (`## Section
28
+ name`, content starting on the next line). For the bold form, content
29
+ immediately following the closing `**` — on the label line itself — is
30
+ part of the section; discarding it (as a next-line-only slice would)
31
+ silently drops whatever a WU author wrote inline after the label.
32
+ """
33
+ escaped = re.escape(section_name)
34
+ heading_re = re.compile(
35
+ rf"(?mi)^#{{1,6}}\s*{escaped}\b|^\*\*{escaped}[^\n*]*\*\*\.?"
36
+ )
37
+ m = heading_re.search(body)
38
+ if not m:
39
+ return ""
40
+ if m.group(0).startswith("#"):
41
+ nl = body.find("\n", m.end())
42
+ after = body[nl + 1:] if nl != -1 else ""
43
+ else:
44
+ after = body[m.end():]
45
+ em = _AC_END_RE.search(after)
46
+ return after[:em.start()] if em else after
47
+
48
+
49
+ def slice_acceptance_criteria(body: str) -> str:
50
+ """Return the text of the Acceptance criteria section only (bold-preamble or ATX)."""
51
+ return slice_wu_section(body, "Acceptance criteria")
@@ -0,0 +1,405 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """Deterministic gate-arm predicate — pure, side-effect-free module.
6
+
7
+ Given a feature directory and the gate id that just closed, evaluates whether
8
+ the drafted successor gate may be armed automatically, and returns a
9
+ structured per-class verdict. Mirrors the shape of `gate_eval.py`'s
10
+ `evaluate_auto_close`: pure evaluation over files, a structured decision
11
+ object, a formatter for the event trail, no side effects. Arming and closing
12
+ are deliberately separate decisions with different inputs — this module does
13
+ not fold into `gate_eval.py`, does not import from it, and does not import
14
+ from `loop.py` (the dependency points the other way: `loop.py` will call
15
+ into this module, T04).
16
+
17
+ The organizing principle: model-authored signals may only veto; only
18
+ mechanical facts and human-authored constants may approve. Of the eight
19
+ classes below, "missing_provenance", "open_questions_human_only", and
20
+ "plan_next_lint" (FEAT-2026-0053/T07) are veto channels — a clean verdict
21
+ there withholds nothing, it just declines to block; every other class both
22
+ approves (clean) and blocks (fired) on mechanical grounds alone. `would_arm`
23
+ is True only when all eight classes are clean, so the veto classes can only
24
+ ever pull the decision from True to False, never the reverse.
25
+
26
+ Honest v1 limit: a draft that weakens an *existing* test's assertions is
27
+ undetectable here. The judge-editing class catches `produces:` paths, not
28
+ prose or assertion semantics — it is a path-surface check, nothing more.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import re
34
+ from dataclasses import dataclass
35
+ from pathlib import Path
36
+
37
+ from . import _miniyaml
38
+ from .closing_requirements import gate_review_filename
39
+ from .plan_baseline import load_baseline, load_plan_graph
40
+
41
+ PREDICATE_VERSION = "v1"
42
+
43
+ BUDGET_PROJECTION_MULTIPLIER = 2.0
44
+ DRIFT_CAP_RATIO = 0.5
45
+ ADDED_GATE_CAP = 1
46
+
47
+ # Path-prefix surfaces a drafted WU must not touch (class 2). `pyproject.toml`
48
+ # is matched whole-file — this predicate cannot distinguish the coverage/lint
49
+ # config sections from the rest of the file without a diff, so any produced
50
+ # `pyproject.toml` fires both this class and decision_class_paths below. That
51
+ # double-fire is an accepted v1 approximation, not a bug.
52
+ JUDGE_PATHS = (
53
+ ".specfuse/verification.yml",
54
+ ".specfuse/hooks/",
55
+ ".specfuse/rules/",
56
+ ".github/workflows/",
57
+ "specfuse/loop/",
58
+ "pyproject.toml",
59
+ )
60
+
61
+ # Dependency-manifest surfaces (class 3). Same `pyproject.toml` caveat as above.
62
+ _DEPENDENCY_MANIFEST_EXACT = ("pyproject.toml", "package.json")
63
+ _REQUIREMENTS_RE = re.compile(r"(^|/)requirements[^/]*\.txt$")
64
+
65
+ _FM_DELIM = re.compile(r"^---\s*$")
66
+ _TITLE_RE = re.compile(r"^#\s+(.*)$", re.MULTILINE)
67
+
68
+ CLASS_NAMES = (
69
+ "budget_projection",
70
+ "judge_editing",
71
+ "decision_class_paths",
72
+ "retroactive_edits",
73
+ "drift_caps",
74
+ "missing_provenance",
75
+ "open_questions_human_only",
76
+ "plan_next_lint",
77
+ )
78
+
79
+ VETO_CLASSES = frozenset(
80
+ {"missing_provenance", "open_questions_human_only", "plan_next_lint"}
81
+ )
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ClassVerdict:
86
+ status: str # "fired" | "clean" | "not_evaluable"
87
+ reason: str
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class ArmDecision:
92
+ would_arm: bool
93
+ classes: dict # class name -> ClassVerdict, one entry per CLASS_NAMES
94
+ feature_id: str
95
+ gate_id: int # the just-closed gate
96
+ predicate_version: str
97
+
98
+
99
+ def _parse_frontmatter(text: str) -> tuple[dict, str]:
100
+ """Return (frontmatter_dict, body_text) from ---\n...\n--- delimited text."""
101
+ lines = text.splitlines()
102
+ if not lines or not _FM_DELIM.match(lines[0]):
103
+ return {}, text
104
+ end = next((i for i, ln in enumerate(lines[1:], 1) if _FM_DELIM.match(ln)), None)
105
+ if end is None:
106
+ return {}, text
107
+ fm = _miniyaml.parse("\n".join(lines[1:end])) or {}
108
+ body = "\n".join(lines[end + 1:])
109
+ return fm, body
110
+
111
+
112
+ def _read_wu(feature_dir: Path, ref: dict) -> "dict | None":
113
+ """Return a normalized WU record, or None when the referenced file is absent."""
114
+ path = feature_dir / ref["file"]
115
+ if not path.exists():
116
+ return None
117
+ fm, body = _parse_frontmatter(path.read_text())
118
+ title_m = _TITLE_RE.search(body)
119
+ goal = title_m.group(1).strip() if title_m else ref["id"]
120
+ return {
121
+ "id": ref["id"],
122
+ "type": fm.get("type", "implementation"),
123
+ "status": fm.get("status", "pending"),
124
+ "cost_usd": fm.get("cost_usd", 0.0) or 0.0,
125
+ "planned_cost_usd": fm.get("planned_cost_usd"),
126
+ "produces": fm.get("produces") or [],
127
+ "produces_driver_helper": fm.get("produces_driver_helper") or [],
128
+ "human_only": bool(fm.get("human_only", False)),
129
+ "provenance": fm.get("provenance"),
130
+ "goal": goal,
131
+ }
132
+
133
+
134
+ def _matches_judge_path(path: str) -> bool:
135
+ return any(path == jp or path.startswith(jp) for jp in JUDGE_PATHS)
136
+
137
+
138
+ def _matches_dependency_manifest(path: str) -> bool:
139
+ if _REQUIREMENTS_RE.search(path):
140
+ return True
141
+ return any(path == p or path.endswith("/" + p) for p in _DEPENDENCY_MANIFEST_EXACT)
142
+
143
+
144
+ def evaluate_arm_predicate(feature_dir: Path, just_closed_gate: int) -> ArmDecision:
145
+ """Return an ArmDecision for the gate that just closed in `feature_dir`.
146
+
147
+ `just_closed_gate` is the gate number that just passed; the predicate
148
+ evaluates whether gate `just_closed_gate + 1`'s drafted WUs may be armed.
149
+ """
150
+ feature_dir = Path(feature_dir)
151
+ plan_fm, _ = _parse_frontmatter((feature_dir / "PLAN.md").read_text())
152
+ feature_id = plan_fm.get("feature_id", feature_dir.name)
153
+
154
+ baseline = load_baseline(feature_dir)
155
+ if baseline is None:
156
+ classes = {name: ClassVerdict("not_evaluable", "no_baseline") for name in CLASS_NAMES}
157
+ return ArmDecision(
158
+ would_arm=False,
159
+ classes=classes,
160
+ feature_id=feature_id,
161
+ gate_id=just_closed_gate,
162
+ predicate_version=PREDICATE_VERSION,
163
+ )
164
+
165
+ plan = load_plan_graph(feature_dir)
166
+ gates = plan.get("gates", []) or []
167
+
168
+ # Baseline indices
169
+ baseline_wus: dict[str, dict] = {}
170
+ baseline_gate_wu_ids: dict[int, set] = {}
171
+ for g in baseline.get("gates", []) or []:
172
+ ids = set()
173
+ for wu in g.get("work_units", []) or []:
174
+ baseline_wus[wu["id"]] = wu
175
+ ids.add(wu["id"])
176
+ baseline_gate_wu_ids[g["gate"]] = ids
177
+ baseline_gate_nums = set(baseline_gate_wu_ids.keys())
178
+ baseline_total_planned = sum(
179
+ (wu.get("planned_cost_usd") or 0.0) for wu in baseline_wus.values()
180
+ )
181
+ baseline_total_count = len(baseline_wus)
182
+
183
+ # Current plan indices
184
+ current_by_gate: dict[int, list] = {}
185
+ current_all: dict[str, dict] = {}
186
+ for g in gates:
187
+ gnum = g.get("gate")
188
+ wus = []
189
+ for ref in g.get("work_units", []) or []:
190
+ wu = _read_wu(feature_dir, ref)
191
+ if wu is None:
192
+ continue
193
+ wus.append(wu)
194
+ current_all[wu["id"]] = wu
195
+ current_by_gate[gnum] = wus
196
+
197
+ added_ids = set(current_all) - set(baseline_wus)
198
+
199
+ classes: dict[str, ClassVerdict] = {}
200
+
201
+ # --- Class 1: budget projection ---
202
+ lifetime_spend = sum(wu["cost_usd"] for wu in current_all.values())
203
+ planned_remaining = sum(
204
+ (wu["planned_cost_usd"] or 0.0)
205
+ for wu in current_all.values()
206
+ if wu["status"] != "done"
207
+ )
208
+ projection = lifetime_spend + planned_remaining
209
+ ceiling = BUDGET_PROJECTION_MULTIPLIER * baseline_total_planned
210
+ if projection > ceiling:
211
+ classes["budget_projection"] = ClassVerdict(
212
+ "fired",
213
+ f"projected spend ${projection:.2f} (spend ${lifetime_spend:.2f} + "
214
+ f"remaining ${planned_remaining:.2f}) exceeds "
215
+ f"{BUDGET_PROJECTION_MULTIPLIER:.1f}x baseline planned total "
216
+ f"${baseline_total_planned:.2f} (cap ${ceiling:.2f})",
217
+ )
218
+ else:
219
+ classes["budget_projection"] = ClassVerdict(
220
+ "clean",
221
+ f"projected spend ${projection:.2f} within "
222
+ f"{BUDGET_PROJECTION_MULTIPLIER:.1f}x baseline planned total "
223
+ f"${baseline_total_planned:.2f} (cap ${ceiling:.2f})",
224
+ )
225
+
226
+ next_gate_num = just_closed_gate + 1
227
+ drafted = current_by_gate.get(next_gate_num, [])
228
+
229
+ # --- Class 2: judge-editing ---
230
+ judge_hits: list[str] = []
231
+ for wu in drafted:
232
+ for p in wu["produces"]:
233
+ if _matches_judge_path(p):
234
+ judge_hits.append(f"{wu['id']} produces {p}")
235
+ if wu["produces_driver_helper"]:
236
+ judge_hits.append(
237
+ f"{wu['id']} declares produces_driver_helper "
238
+ f"{wu['produces_driver_helper']!r}"
239
+ )
240
+ if judge_hits:
241
+ classes["judge_editing"] = ClassVerdict("fired", "; ".join(judge_hits))
242
+ else:
243
+ classes["judge_editing"] = ClassVerdict(
244
+ "clean", "no drafted WU touches judge-path surfaces"
245
+ )
246
+
247
+ # --- Class 3: decision-class paths ---
248
+ dep_hits: list[str] = []
249
+ for wu in drafted:
250
+ for p in wu["produces"]:
251
+ if _matches_dependency_manifest(p):
252
+ dep_hits.append(f"{wu['id']} produces {p}")
253
+ if dep_hits:
254
+ classes["decision_class_paths"] = ClassVerdict("fired", "; ".join(dep_hits))
255
+ else:
256
+ classes["decision_class_paths"] = ClassVerdict(
257
+ "clean", "no drafted WU touches dependency manifests"
258
+ )
259
+
260
+ # --- Class 4: retroactive edits (baseline WUs of already-passed gates) ---
261
+ retro_hits: list[str] = []
262
+ for gnum, ids in baseline_gate_wu_ids.items():
263
+ gate_entry = next((g for g in gates if g.get("gate") == gnum), None)
264
+ gate_file = gate_entry.get("file") if gate_entry else f"GATE-{gnum:02d}.md"
265
+ gate_path = feature_dir / gate_file
266
+ if not gate_path.exists():
267
+ continue
268
+ gate_fm, _ = _parse_frontmatter(gate_path.read_text())
269
+ if gate_fm.get("status") != "passed":
270
+ continue
271
+ for wu_id in sorted(ids):
272
+ baseline_wu = baseline_wus[wu_id]
273
+ current_wu = current_all.get(wu_id)
274
+ if current_wu is None:
275
+ retro_hits.append(f"{wu_id} left the graph")
276
+ continue
277
+ if current_wu["type"] != baseline_wu.get("type"):
278
+ retro_hits.append(
279
+ f"{wu_id} type changed {baseline_wu.get('type')!r} -> "
280
+ f"{current_wu['type']!r}"
281
+ )
282
+ if current_wu["goal"] != baseline_wu.get("goal"):
283
+ retro_hits.append(
284
+ f"{wu_id} goal changed {baseline_wu.get('goal')!r} -> "
285
+ f"{current_wu['goal']!r}"
286
+ )
287
+ if retro_hits:
288
+ classes["retroactive_edits"] = ClassVerdict("fired", "; ".join(retro_hits))
289
+ else:
290
+ classes["retroactive_edits"] = ClassVerdict(
291
+ "clean", "no passed-gate baseline WU altered or removed"
292
+ )
293
+
294
+ # --- Class 5: drift caps ---
295
+ added_count = len(added_ids)
296
+ added_sum = sum((current_all[i]["planned_cost_usd"] or 0.0) for i in added_ids)
297
+ current_gate_nums = {g.get("gate") for g in gates}
298
+ added_gate_nums = current_gate_nums - baseline_gate_nums
299
+ count_ceiling = DRIFT_CAP_RATIO * baseline_total_count
300
+ sum_ceiling = DRIFT_CAP_RATIO * baseline_total_planned
301
+ drift_hits: list[str] = []
302
+ if added_count > count_ceiling:
303
+ drift_hits.append(
304
+ f"added WU count {added_count} exceeds {DRIFT_CAP_RATIO:.1f}x baseline "
305
+ f"count {baseline_total_count} (cap {count_ceiling:.1f})"
306
+ )
307
+ if added_sum > sum_ceiling:
308
+ drift_hits.append(
309
+ f"added planned cost ${added_sum:.2f} exceeds {DRIFT_CAP_RATIO:.1f}x "
310
+ f"baseline planned total ${baseline_total_planned:.2f} "
311
+ f"(cap ${sum_ceiling:.2f})"
312
+ )
313
+ if len(added_gate_nums) > ADDED_GATE_CAP:
314
+ drift_hits.append(
315
+ f"added gate count {len(added_gate_nums)} exceeds cap {ADDED_GATE_CAP}: "
316
+ f"{sorted(added_gate_nums)}"
317
+ )
318
+ if drift_hits:
319
+ classes["drift_caps"] = ClassVerdict("fired", "; ".join(drift_hits))
320
+ else:
321
+ classes["drift_caps"] = ClassVerdict(
322
+ "clean", "added WUs and gates within drift caps"
323
+ )
324
+
325
+ # --- Class 6: missing provenance (veto channel) ---
326
+ missing_prov = sorted(
327
+ wu_id for wu_id in added_ids if not current_all[wu_id]["provenance"]
328
+ )
329
+ if missing_prov:
330
+ classes["missing_provenance"] = ClassVerdict(
331
+ "fired", f"added WU(s) missing provenance: {', '.join(missing_prov)}"
332
+ )
333
+ else:
334
+ classes["missing_provenance"] = ClassVerdict(
335
+ "clean",
336
+ "every added WU carries a provenance field" if added_ids else "no added WUs",
337
+ )
338
+
339
+ # --- Class 7: open questions / human-only (veto channel) ---
340
+ reasons7: list[str] = []
341
+ if next_gate_num in current_by_gate:
342
+ review_path = feature_dir / gate_review_filename(next_gate_num)
343
+ if not review_path.exists():
344
+ reasons7.append(f"{review_path.name} missing")
345
+ else:
346
+ review_fm, _ = _parse_frontmatter(review_path.read_text())
347
+ if "open_questions" not in review_fm:
348
+ reasons7.append(f"{review_path.name} missing open_questions field")
349
+ else:
350
+ oq = review_fm.get("open_questions") or []
351
+ if oq:
352
+ reasons7.append(f"{review_path.name} open_questions non-empty: {oq!r}")
353
+ human_only_hits = [wu["id"] for wu in drafted if wu["human_only"]]
354
+ if human_only_hits:
355
+ reasons7.append(f"human_only flagged: {', '.join(human_only_hits)}")
356
+ clean_reason = "open_questions present and empty, no human_only flags"
357
+ else:
358
+ clean_reason = "terminal gate: no drafted successor"
359
+ if reasons7:
360
+ classes["open_questions_human_only"] = ClassVerdict("fired", "; ".join(reasons7))
361
+ else:
362
+ classes["open_questions_human_only"] = ClassVerdict("clean", clean_reason)
363
+
364
+ # --- Class 8: plan-next contract lint (veto channel, FEAT-2026-0053/T07) ---
365
+ # lint_plan_next_draft is WARN-only for its own CLI caller; here a
366
+ # non-empty finding list vetoes the arm. A parse failure inside the lint
367
+ # (malformed frontmatter) must degrade to a fired verdict, not crash the
368
+ # gate close — same precedent as build_arm_predicate_event's outer catch.
369
+ try:
370
+ from .lint_plan import lint_plan_next_draft # deferred: lint_plan imports loop.py
371
+
372
+ lint_warns = lint_plan_next_draft(feature_dir, just_closed_gate)
373
+ except Exception as exc: # noqa: BLE001 - a raise is not a verdict
374
+ classes["plan_next_lint"] = ClassVerdict(
375
+ "fired",
376
+ f"plan-next lint raised {type(exc).__name__}: {exc}",
377
+ )
378
+ else:
379
+ if lint_warns:
380
+ classes["plan_next_lint"] = ClassVerdict("fired", "; ".join(lint_warns))
381
+ else:
382
+ classes["plan_next_lint"] = ClassVerdict("clean", "plan-next lint: no findings")
383
+
384
+ would_arm = all(classes[name].status != "fired" for name in CLASS_NAMES)
385
+
386
+ return ArmDecision(
387
+ would_arm=would_arm,
388
+ classes=classes,
389
+ feature_id=feature_id,
390
+ gate_id=just_closed_gate,
391
+ predicate_version=PREDICATE_VERSION,
392
+ )
393
+
394
+
395
+ def _format_decision(decision: ArmDecision) -> str:
396
+ """Render an ArmDecision to the canonical event-trail block shape."""
397
+ lines: list[str] = [
398
+ f" arm_predicate gate={decision.gate_id} would_arm={decision.would_arm}"
399
+ ]
400
+ for name in CLASS_NAMES:
401
+ v = decision.classes.get(name)
402
+ if v is None:
403
+ continue
404
+ lines.append(f" {name}: {v.status} — {v.reason}")
405
+ return "\n".join(lines)