specfuse-loop 0.3.23__tar.gz → 0.3.24__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 (170) hide show
  1. {specfuse_loop-0.3.23/specfuse_loop.egg-info → specfuse_loop-0.3.24}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.24/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/skills.md +5 -0
  5. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/roadmap.template.md +18 -2
  6. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/PLAN.template.md +1 -1
  7. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/lint_plan.py +1 -1
  8. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/loop.py +33 -2
  9. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24/specfuse_loop.egg-info}/PKG-INFO +1 -1
  10. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_auto_archive.py +40 -0
  11. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_orchestration.py +25 -0
  12. specfuse_loop-0.3.23/specfuse/loop/data/VERSION +0 -1
  13. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/LICENSE +0 -0
  14. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/NOTICE +0 -0
  15. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/README.md +0 -0
  16. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/setup.cfg +0 -0
  17. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/__init__.py +0 -0
  18. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/_filelock.py +0 -0
  19. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/_miniyaml.py +0 -0
  20. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/adopt_feature.py +0 -0
  21. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  22. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  23. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  24. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/getting-started.md +0 -0
  25. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/methodology.md +0 -0
  26. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/gitignore.snippet +0 -0
  27. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/close-discipline.md +0 -0
  28. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  29. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/never-touch.md +0 -0
  30. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/planning-discipline.md +0 -0
  31. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/result-contract.md +0 -0
  32. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  33. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  34. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules-local/README.md +0 -0
  35. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/event.schema.json +0 -0
  36. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  37. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  38. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  39. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  40. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/GATE.template.md +0 -0
  41. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/WU.template.md +0 -0
  42. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/verification.yml.example +0 -0
  43. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/gate_eval.py +0 -0
  44. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/gh_backend.py +0 -0
  45. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/gh_features.py +0 -0
  46. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/scaffold.py +0 -0
  47. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/validate_event.py +0 -0
  48. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/SOURCES.txt +0 -0
  49. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  50. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/entry_points.txt +0 -0
  51. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/requires.txt +0 -0
  52. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/top_level.txt +0 -0
  53. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_adopt_feature.py +0 -0
  54. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  55. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_attempt_outcome_emission.py +0 -0
  56. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autoclose_deferral_visibility.py +0 -0
  57. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autosync.py +0 -0
  58. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autosync_consent.py +0 -0
  59. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autosync_firstrun.py +0 -0
  60. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autosync_plugin.py +0 -0
  61. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_backend.py +0 -0
  62. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_bash_routing.py +0 -0
  63. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  64. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  65. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_bookkeeping_no_verify.py +0 -0
  66. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_claude_resolution.py +0 -0
  67. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_close_wu_self_reference.py +0 -0
  68. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_closing_deliverable_guard.py +0 -0
  69. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_console_encoding.py +0 -0
  70. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_cost_tracking.py +0 -0
  71. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_degraded_oracle.py +0 -0
  72. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_deliverable_presence_gate.py +0 -0
  73. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_doctor.py +0 -0
  74. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_driver_integration.py +0 -0
  75. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_driver_lock.py +0 -0
  76. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_duration_tracking.py +0 -0
  77. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_empty_files_escalation.py +0 -0
  78. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_ensure_feature_branch.py +0 -0
  79. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_ensure_feature_branch_base.py +0 -0
  80. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_events_redaction.py +0 -0
  81. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_extra_gates.py +0 -0
  82. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_filelock_portable.py +0 -0
  83. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_force_full_close.py +0 -0
  84. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gate_eval.py +0 -0
  85. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gate_eval_calibration.py +0 -0
  86. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  87. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gate_eval_terminal_wiring.py +0 -0
  88. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gh_backend.py +0 -0
  89. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gh_features.py +0 -0
  90. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_git_env_isolation.py +0 -0
  91. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_hashed_denylist.py +0 -0
  92. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_hashed_denylist_ci.py +0 -0
  93. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_init_integration.py +0 -0
  94. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_interpreter_normalization.py +0 -0
  95. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_leak_findings_redaction.py +0 -0
  96. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_leak_scan.py +0 -0
  97. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_leak_scan_content.py +0 -0
  98. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_learnings_query.py +0 -0
  99. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  100. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lifecycle_integration.py +0 -0
  101. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lifetime_accounting.py +0 -0
  102. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_bare_produces_path.py +0 -0
  103. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_close_intermediate.py +0 -0
  104. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_close_wu.py +0 -0
  105. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_correlation_id.py +0 -0
  106. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  107. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_oracle_env.py +0 -0
  108. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_plan_errors.py +0 -0
  109. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_plan_next_draft.py +0 -0
  110. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_plan_relative_import.py +0 -0
  111. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_planning_sections.py +0 -0
  112. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_produces_driver_helper.py +0 -0
  113. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_sections.py +0 -0
  114. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  115. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_verification_reference.py +0 -0
  116. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_attempt_evidence.py +0 -0
  117. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_caveman_preamble.py +0 -0
  118. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_close_intermediate.py +0 -0
  119. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_defaults_by_type.py +0 -0
  120. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_effort.py +0 -0
  121. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_failure_note_cap.py +0 -0
  122. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_files_changed_guard.py +0 -0
  123. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_gate_budget.py +0 -0
  124. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_model_alias.py +0 -0
  125. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_post_pass_invariant.py +0 -0
  126. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_reset_preserving_events.py +0 -0
  127. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_smoke_runner.py +0 -0
  128. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_unsandboxed.py +0 -0
  129. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_zero_token_guard.py +0 -0
  130. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_migrate_legacy.py +0 -0
  131. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_miniyaml_equivalence.py +0 -0
  132. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_miniyaml_negative.py +0 -0
  133. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_planned_cost_lint.py +0 -0
  134. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_prepare_feature.py +0 -0
  135. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_prepare_scaffold_sync.py +0 -0
  136. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_produces_field.py +0 -0
  137. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_produces_in_diff.py +0 -0
  138. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_redaction_windows_home.py +0 -0
  139. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_resolve_base.py +0 -0
  140. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_result_block.py +0 -0
  141. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_roadmap_add_skill.py +0 -0
  142. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_roadmap_archive_skill.py +0 -0
  143. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_roadmap_row_parser.py +0 -0
  144. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_data_in_sync.py +0 -0
  145. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_doc_hygiene.py +0 -0
  146. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_docs.py +0 -0
  147. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_init.py +0 -0
  148. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_manifest.py +0 -0
  149. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_resources.py +0 -0
  150. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_seed_sanity.py +0 -0
  151. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_upgrade.py +0 -0
  152. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_wiring.py +0 -0
  153. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_skills_vendored_in_sync.py +0 -0
  154. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_spinning_rearm_gate.py +0 -0
  155. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_squash_commit_hook_crash.py +0 -0
  156. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_template_closing_shapes.py +0 -0
  157. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_terminal_flip_ownership.py +0 -0
  158. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_terminal_flips.py +0 -0
  159. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_timeout_kill_portable.py +0 -0
  160. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_untracked_feature_folder.py +0 -0
  161. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_untracked_file_absorption.py +0 -0
  162. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_upgrade_integration.py +0 -0
  163. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_upgrade_merge_gate.py +0 -0
  164. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_validate_event.py +0 -0
  165. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_verdict_coupling.py +0 -0
  166. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_verify_empty_gate_set.py +0 -0
  167. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_version_consistency.py +0 -0
  168. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_version_skew.py +0 -0
  169. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_windows_gate_exec.py +0 -0
  170. {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_wu_execution_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.23
3
+ Version: 0.3.24
4
4
  Summary: Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology.
5
5
  Author: Specfuse contributors
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specfuse-loop"
7
- version = "0.3.23"
7
+ version = "0.3.24"
8
8
  description = "Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ 0.3.24
@@ -48,6 +48,11 @@ roadmap ──/pick-feature──▶ active ──/draft-feature──▶ gate 1
48
48
  `/draft-feature`.
49
49
  - **`/roadmap-add`** — add a new feature row to the roadmap before it's ready to
50
50
  pick.
51
+ - **`/block-feature`** — flip a roadmap feature to `blocked` (or clear it) with a
52
+ linked `**Blocked by.**` blocker — an ADR awaiting approval or an upstream
53
+ FEAT-ID that must complete first. Writes the row status, the detail block, and
54
+ PLAN frontmatter, keeping intra-page feature links resolvable.
55
+ `/block-feature FEAT-ID --unblock` clears the block and restores the status.
51
56
 
52
57
  ### 2. Draft — turn a picked feature into a runnable gate
53
58
 
@@ -23,8 +23,24 @@ the canonical column order below.
23
23
  |----------------|-------|--------|--------|--------|
24
24
 
25
25
  Status: `planned` → `active` → `done` (or `abandoned`). `deferred` = parked
26
- pending an external decision/dependency; resumable (a human flips it back to
27
- `active` when the blocker clears), distinct from `abandoned` (dead).
26
+ by choice pending an external decision/dependency; resumable (a human flips it
27
+ back to `active` when the blocker clears), distinct from `abandoned` (dead).
28
+ `blocked` = cannot proceed because a *named* dependency is unmet — an ADR
29
+ awaiting approval, or an upstream feature that must complete first. Resumable
30
+ like `deferred`, but where `deferred` is a voluntary park with no named blocker,
31
+ `blocked` always names its blocker so the roadmap shows what it waits on and
32
+ links to it.
33
+
34
+ A `blocked` feature's detail section carries a `**Blocked by.**` block — blank
35
+ line above it, one linked blocker per clause:
36
+
37
+ **Blocked by.** [ADR-0007: event-schema versioning](../docs/adr/0007-event-schema-versioning.md) — awaiting approval; [FEAT-2026-0011](#feat-2026-0011) — scoring data must land first.
38
+
39
+ Each blocker is a Markdown link: an ADR points at its `.md` file (kept under
40
+ `docs/adr/` by convention, so from `.specfuse/roadmap.md` the link is
41
+ `../docs/adr/…`); a feature dependency points at the blocking
42
+ feature's roadmap anchor (`#feat-yyyy-nnnn`). When every blocker clears, a human
43
+ flips `blocked` → `active` (or `planned`) and removes the `**Blocked by.**` block.
28
44
 
29
45
  ## Notes
30
46
 
@@ -7,7 +7,7 @@ roadmap_goal: <one line copied from the roadmap — the north star this feature
7
7
  plan-next anchors every drafted gate to this and flags if a retrospective implies
8
8
  it should change>
9
9
  autonomy_default: review # auto | review | supervised
10
- status: active # active | deferred | done | abandoned
10
+ status: active # active | blocked | deferred | done | abandoned
11
11
  # deferred = parked pending an external decision/dependency; resumable, non-dispatchable
12
12
  # planned_cost_usd: 0.00 # OPTIONAL — sum of WU planned costs; lint warns when missing or when delta from WU sum > 10%
13
13
  ---
@@ -46,7 +46,7 @@ VALID_STATUS = {"draft", "pending", "ready", "in_progress", "in_review", "done",
46
46
  # unrecognized status that passes lint and then behaves differently at dispatch
47
47
  # is the class #183 (deferred) and #185 exist to close. Keep in sync with
48
48
  # find_feature / the gate-status transitions in loop.py.
49
- VALID_FEATURE_STATUS = {"planned", "active", "deferred", "done", "abandoned"}
49
+ VALID_FEATURE_STATUS = {"planned", "active", "blocked", "deferred", "done", "abandoned"}
50
50
  VALID_GATE_STATUS = {"open", "awaiting_review", "passed"}
51
51
  CLOSING_SEQUENCE = ["retrospective", "lessons", "docs", "plan-next"]
52
52
  # New compact closing shapes (FEAT-2026-0015):
@@ -64,7 +64,7 @@ SPECFUSE_DIR = Path(".specfuse")
64
64
  REPO_ROOT = SPECFUSE_DIR.parent
65
65
  FEATURES_DIR = SPECFUSE_DIR / "features"
66
66
  VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
67
- DRIVER_VERSION = "0.3.23"
67
+ DRIVER_VERSION = "0.3.24"
68
68
  # Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
69
69
  # version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
70
70
  # the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
@@ -285,10 +285,22 @@ def find_feature(arg: str | None) -> Path:
285
285
  f"deferred features. Flip its PLAN.md status back to `active` "
286
286
  f"when the blocker clears, then re-run."
287
287
  )
288
+ # A `blocked` feature waits on a named unmet dependency (an ADR or an
289
+ # upstream feature — see its roadmap `**Blocked by.**` block). Like
290
+ # `deferred` it is non-dispatchable; refuse an explicit --feature
291
+ # targeting one rather than dispatching it anyway.
292
+ if _fm.get("status") == "blocked":
293
+ sys.exit(
294
+ f"{d.name} is blocked — the loop does not dispatch blocked "
295
+ f"features. Clear its blocker(s), then run "
296
+ f"`/block-feature {_fm.get('feature_id', d.name)} --unblock` "
297
+ f"(or flip its PLAN.md status back to `active`) and re-run."
298
+ )
288
299
  return d
289
300
  actives = []
290
301
  done_pending_wrap = []
291
302
  deferred = []
303
+ blocked = []
292
304
  for d in sorted(FEATURES_DIR.glob("*/")):
293
305
  plan = d / "PLAN.md"
294
306
  if plan.exists():
@@ -316,6 +328,11 @@ def find_feature(arg: str | None) -> Path:
316
328
  # a bare invocation won't pick it up; surfaced below so a lone
317
329
  # deferred feature reads as parked, not silently absent.
318
330
  deferred.append(d)
331
+ elif fm.get("status") == "blocked":
332
+ # Waiting on a named unmet dependency — non-dispatchable, same
333
+ # as deferred. Surfaced below so a lone blocked feature reads as
334
+ # parked-on-a-blocker, not silently absent.
335
+ blocked.append(d)
319
336
  if len(actives) == 1:
320
337
  return actives[0]
321
338
  if not actives:
@@ -328,6 +345,13 @@ def find_feature(arg: str | None) -> Path:
328
345
  f" Flip one's PLAN.md status to `active` to "
329
346
  f"resume it.\n"
330
347
  )
348
+ if blocked:
349
+ names = ", ".join(d.name for d in blocked[-3:])
350
+ msg += (
351
+ f" - {len(blocked)} blocked feature(s), skipped: {names}\n"
352
+ f" Clear the blocker, then /block-feature "
353
+ f"<id> --unblock to resume.\n"
354
+ )
331
355
  if done_pending_wrap:
332
356
  names = ", ".join(d.name for d in done_pending_wrap[-3:])
333
357
  msg += (
@@ -2669,7 +2693,14 @@ def auto_archive_feature(feature_id: str, repo_root: Path) -> str:
2669
2693
  section_m2 = section_re.search(roadmap_text)
2670
2694
  if section_m2:
2671
2695
  roadmap_text = roadmap_text[:section_m2.start()] + roadmap_text[section_m2.end():]
2672
- roadmap_text = re.sub(r'\n{3,}', '\n\n', roadmap_text)
2696
+ # A feature that was ever `blocked` via /block-feature carries an explicit
2697
+ # `<a id="feat-…"></a>` line above its heading (so intra-page `#feat-…`
2698
+ # links resolve). The section regex above does not span it, so removing the
2699
+ # section would strand the anchor pointing at a section that has moved to the
2700
+ # archive. Strip the now-orphaned anchor line; Step 3 already re-emitted the
2701
+ # canonical anchor in roadmap-archive.md, so the link target travels with it.
2702
+ roadmap_text = roadmap_text.replace(f"{anchor}\n", "")
2703
+ roadmap_text = re.sub(r'\n{3,}', '\n\n', roadmap_text)
2673
2704
  roadmap_path.write_text(roadmap_text)
2674
2705
 
2675
2706
  return "archived"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.23
3
+ Version: 0.3.24
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
@@ -42,6 +42,27 @@ _ROADMAP_DONE = textwrap.dedent("""\
42
42
  Some content here.
43
43
  """)
44
44
 
45
+ _ROADMAP_DONE_WITH_ANCHOR = textwrap.dedent("""\
46
+ ---
47
+ project: test
48
+ ---
49
+
50
+ # Roadmap
51
+
52
+ | Feature ID | Title | Status | Folder | Detail |
53
+ |------------|-------|--------|--------|--------|
54
+ | FEAT-2026-9999 | Test feature | done | — | — |
55
+
56
+ <a id="feat-2026-9999"></a>
57
+ ## FEAT-2026-9999 — Test feature
58
+
59
+ Some content here.
60
+
61
+ ## Notes
62
+
63
+ Trailing content.
64
+ """)
65
+
45
66
  _ROADMAP_DONE_NO_SECTION = textwrap.dedent("""\
46
67
  ---
47
68
  project: test
@@ -122,6 +143,25 @@ class TestAutoArchiveFeature(unittest.TestCase):
122
143
  "anchor must appear before section heading in archive")
123
144
  self.assertIn('Some content here.', archive_text)
124
145
 
146
+ def test_preexisting_anchor_not_orphaned(self):
147
+ """A feature blocked via /block-feature carries an explicit <a id> above
148
+ its heading; archiving must remove it, not strand it in roadmap.md."""
149
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
150
+ repo = _make_repo(tmp, roadmap=_ROADMAP_DONE_WITH_ANCHOR)
151
+ result = loop.auto_archive_feature("FEAT-2026-9999", repo)
152
+ self.assertEqual(result, "archived")
153
+
154
+ roadmap_text = (repo / ".specfuse" / "roadmap.md").read_text()
155
+ archive_text = (repo / ".specfuse" / "roadmap-archive.md").read_text()
156
+
157
+ # No orphan anchor left behind in roadmap.md.
158
+ self.assertNotIn('<a id="feat-2026-9999"></a>', roadmap_text)
159
+ self.assertNotIn('## FEAT-2026-9999 — ', roadmap_text)
160
+ # Unrelated content survives.
161
+ self.assertIn('## Notes', roadmap_text)
162
+ # Archive carries exactly one anchor for the feature.
163
+ self.assertEqual(archive_text.count('<a id="feat-2026-9999"></a>'), 1)
164
+
125
165
  def test_idempotent_second_call(self):
126
166
  """Second call returns 'already archived' and makes zero file edits."""
127
167
  with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
@@ -368,6 +368,31 @@ class TestFindFeatureSelection(unittest.TestCase):
368
368
  good = self._write_plan("FEAT-2026-9502-live", "active")
369
369
  self.assertEqual(loop.find_feature(None), good)
370
370
 
371
+ def test_explicit_blocked_feature_is_refused(self):
372
+ """`--feature` on a blocked feature must refuse, not dispatch."""
373
+ self._write_plan("FEAT-2026-9502-stuck", "blocked")
374
+ with self.assertRaises(SystemExit) as ctx:
375
+ loop.find_feature("FEAT-2026-9502-stuck")
376
+ msg = str(ctx.exception.code).lower()
377
+ self.assertIn("blocked", msg)
378
+ self.assertIn("unblock", msg) # points at the fix
379
+
380
+ def test_blocked_not_treated_as_active_in_scan(self):
381
+ """A lone blocked feature yields 'no active', named as blocked."""
382
+ self._write_plan("FEAT-2026-9502-stuck", "blocked")
383
+ with self.assertRaises(SystemExit) as ctx:
384
+ loop.find_feature(None)
385
+ msg = str(ctx.exception.code)
386
+ self.assertIn("No active feature", msg)
387
+ self.assertIn("blocked", msg.lower())
388
+ self.assertIn("FEAT-2026-9502-stuck", msg)
389
+
390
+ def test_blocked_does_not_block_a_real_active(self):
391
+ """A blocked feature alongside one active resolves to the active."""
392
+ self._write_plan("FEAT-2026-9502-stuck", "blocked")
393
+ good = self._write_plan("FEAT-2026-9502-live", "active")
394
+ self.assertEqual(loop.find_feature(None), good)
395
+
371
396
  def test_read_frontmatter_error_names_the_file(self):
372
397
  """#177 fix 1: a parse error carries the file path, not just a
373
398
  block-relative line number."""
@@ -1 +0,0 @@
1
- 0.3.23
File without changes
File without changes
File without changes
File without changes