specfuse-loop 0.3.3__tar.gz → 0.3.4__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 (132) hide show
  1. {specfuse_loop-0.3.3/specfuse_loop.egg-info → specfuse_loop-0.3.4}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.4/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/loop.py +27 -9
  5. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4/specfuse_loop.egg-info}/PKG-INFO +1 -1
  6. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_orchestration.py +2 -2
  7. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_prepare_feature.py +39 -1
  8. specfuse_loop-0.3.3/specfuse/loop/data/VERSION +0 -1
  9. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/LICENSE +0 -0
  10. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/NOTICE +0 -0
  11. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/README.md +0 -0
  12. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/setup.cfg +0 -0
  13. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/__init__.py +0 -0
  14. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/_miniyaml.py +0 -0
  15. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/adopt_feature.py +0 -0
  16. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  17. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  18. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  19. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/docs/getting-started.md +0 -0
  20. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/docs/methodology.md +0 -0
  21. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/docs/skills.md +0 -0
  22. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/gitignore.snippet +0 -0
  23. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/roadmap.template.md +0 -0
  24. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  25. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/rules/never-touch.md +0 -0
  26. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/rules/result-contract.md +0 -0
  27. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  28. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/templates/GATE.template.md +0 -0
  29. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  30. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/templates/WU.template.md +0 -0
  31. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/data/verification.yml.example +0 -0
  32. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/gate_eval.py +0 -0
  33. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/gh_backend.py +0 -0
  34. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/gh_features.py +0 -0
  35. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/lint_plan.py +0 -0
  36. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/scaffold.py +0 -0
  37. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse/loop/validate_event.py +0 -0
  38. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse_loop.egg-info/SOURCES.txt +0 -0
  39. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  40. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse_loop.egg-info/entry_points.txt +0 -0
  41. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse_loop.egg-info/requires.txt +0 -0
  42. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/specfuse_loop.egg-info/top_level.txt +0 -0
  43. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_adopt_feature.py +0 -0
  44. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  45. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_attempt_outcome_emission.py +0 -0
  46. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_autosync.py +0 -0
  47. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_autosync_consent.py +0 -0
  48. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_autosync_firstrun.py +0 -0
  49. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_autosync_plugin.py +0 -0
  50. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_backend.py +0 -0
  51. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  52. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  53. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_closing_deliverable_guard.py +0 -0
  54. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_cost_tracking.py +0 -0
  55. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_deliverable_presence_gate.py +0 -0
  56. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_doctor.py +0 -0
  57. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_driver_integration.py +0 -0
  58. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_driver_lock.py +0 -0
  59. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_duration_tracking.py +0 -0
  60. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_empty_files_escalation.py +0 -0
  61. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_ensure_feature_branch.py +0 -0
  62. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_extra_gates.py +0 -0
  63. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_force_full_close.py +0 -0
  64. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_gate_eval.py +0 -0
  65. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_gate_eval_calibration.py +0 -0
  66. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  67. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_gate_eval_terminal_wiring.py +0 -0
  68. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_gh_backend.py +0 -0
  69. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_gh_features.py +0 -0
  70. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_git_env_isolation.py +0 -0
  71. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_hashed_denylist.py +0 -0
  72. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_hashed_denylist_ci.py +0 -0
  73. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_init_integration.py +0 -0
  74. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_leak_findings_redaction.py +0 -0
  75. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_leak_scan.py +0 -0
  76. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_leak_scan_content.py +0 -0
  77. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  78. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lifecycle_integration.py +0 -0
  79. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_bare_produces_path.py +0 -0
  80. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_close_intermediate.py +0 -0
  81. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_close_wu.py +0 -0
  82. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_correlation_id.py +0 -0
  83. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  84. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_oracle_env.py +0 -0
  85. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_plan_errors.py +0 -0
  86. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_plan_next_draft.py +0 -0
  87. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_produces_driver_helper.py +0 -0
  88. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_sections.py +0 -0
  89. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  90. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_auto_archive.py +0 -0
  91. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_caveman_preamble.py +0 -0
  92. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_close_intermediate.py +0 -0
  93. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_defaults_by_type.py +0 -0
  94. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_effort.py +0 -0
  95. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_failure_note_cap.py +0 -0
  96. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_files_changed_guard.py +0 -0
  97. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_gate_budget.py +0 -0
  98. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_model_alias.py +0 -0
  99. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_post_pass_invariant.py +0 -0
  100. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_reset_preserving_events.py +0 -0
  101. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_smoke_runner.py +0 -0
  102. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_unsandboxed.py +0 -0
  103. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_loop_zero_token_guard.py +0 -0
  104. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_migrate_legacy.py +0 -0
  105. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_miniyaml_equivalence.py +0 -0
  106. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_miniyaml_negative.py +0 -0
  107. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_planned_cost_lint.py +0 -0
  108. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_produces_field.py +0 -0
  109. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_result_block.py +0 -0
  110. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_roadmap_add_skill.py +0 -0
  111. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_roadmap_archive_skill.py +0 -0
  112. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_roadmap_row_parser.py +0 -0
  113. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_scaffold_data_in_sync.py +0 -0
  114. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_scaffold_docs.py +0 -0
  115. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_scaffold_init.py +0 -0
  116. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_scaffold_manifest.py +0 -0
  117. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_scaffold_resources.py +0 -0
  118. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_scaffold_seed_sanity.py +0 -0
  119. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_scaffold_upgrade.py +0 -0
  120. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_scaffold_wiring.py +0 -0
  121. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_squash_commit_hook_crash.py +0 -0
  122. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_template_closing_shapes.py +0 -0
  123. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_terminal_flip_ownership.py +0 -0
  124. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_terminal_flips.py +0 -0
  125. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_untracked_feature_folder.py +0 -0
  126. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_upgrade_integration.py +0 -0
  127. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_validate_event.py +0 -0
  128. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_verdict_coupling.py +0 -0
  129. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_verify_empty_gate_set.py +0 -0
  130. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_version_consistency.py +0 -0
  131. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/tests/test_version_skew.py +0 -0
  132. {specfuse_loop-0.3.3 → specfuse_loop-0.3.4}/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.3
3
+ Version: 0.3.4
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.3"
7
+ version = "0.3.4"
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.4
@@ -62,7 +62,7 @@ SPECFUSE_DIR = Path(".specfuse")
62
62
  REPO_ROOT = SPECFUSE_DIR.parent
63
63
  FEATURES_DIR = SPECFUSE_DIR / "features"
64
64
  VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
65
- DRIVER_VERSION = "0.3.3"
65
+ DRIVER_VERSION = "0.3.4"
66
66
  # Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
67
67
  # version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
68
68
  # the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
@@ -728,7 +728,8 @@ def _branch_prep_hint(feature_dir: "Path", feat_fm: dict, feature_id: str) -> st
728
728
  lines = [
729
729
  "",
730
730
  "Easiest — let the loop create the branch and commit for you:",
731
- " specfuse-loop --prepare",
731
+ " specfuse-loop --prepare # …then run",
732
+ " specfuse-loop --prepare-only # …then stop, so you can review first",
732
733
  "",
733
734
  "Or do it manually:",
734
735
  ]
@@ -926,9 +927,15 @@ def ensure_feature_branch(feat_fm: dict, feature_dir: "Path | None" = None) -> N
926
927
  ).returncode == 0
927
928
  if not is_ancestor:
928
929
  raise FeatureBranchError(
929
- f"branch '{branch}' exists and diverges from HEAD (not an "
930
- f"ancestor). Refusing to silently check out a stale branch; "
931
- f"resolve manually (rebase, recreate, or delete it) and re-run."
930
+ f"branch '{branch}' already exists and has diverged from your "
931
+ f"current branch '{current}' (it carries commits HEAD does not). "
932
+ f"Refusing to silently check it out that could build on a stale "
933
+ f"base or lose work. Pick one, then re-run:\n"
934
+ f" - resume that branch's work: git checkout {branch}\n"
935
+ f" - rebase it onto here: git checkout {branch} && "
936
+ f"git rebase {current}\n"
937
+ f" - discard it (work not needed): git branch -D {branch} "
938
+ f"(the loop recreates it from HEAD)"
932
939
  )
933
940
  _checked_checkout(["checkout", branch], f"checkout of existing branch '{branch}'")
934
941
  print(f"Switched to feature branch '{branch}' (was on '{current}').")
@@ -2974,6 +2981,7 @@ def run(
2974
2981
  dry_run: bool,
2975
2982
  force_full_close: str | None = None,
2976
2983
  prepare: bool = False,
2984
+ prepare_only: bool = False,
2977
2985
  ) -> int:
2978
2986
  # Fail-fast on a malformed verification.yml BEFORE we touch any WU state.
2979
2987
  # The per-gate `verify()` call lazy-loads the same file; if it's malformed,
@@ -3027,10 +3035,16 @@ def run(
3027
3035
  )
3028
3036
  return 1
3029
3037
  require_git_ready()
3030
- # --prepare: create the feature branch + commit the folder up front so
3031
- # the guards below pass (the do-it-for-me path draft-feature can't take).
3032
- if prepare:
3038
+ # --prepare / --prepare-only: create the feature branch + commit the
3039
+ # folder up front so the guards below pass (the do-it-for-me path
3040
+ # draft-feature can't take). --prepare-only stops after, so you can
3041
+ # review the commit before the loop dispatches anything.
3042
+ if prepare or prepare_only:
3033
3043
  prepare_feature(feat_fm, feature_dir, feature_id)
3044
+ if prepare_only:
3045
+ print("Prepared: feature is on its branch and committed. "
3046
+ "Re-run `specfuse-loop` to start the gate.")
3047
+ return 0
3034
3048
  # Pre-flight guards — both run BEFORE ensure_feature_branch so the
3035
3049
  # refusal happens before any branch mutation, and both protect against
3036
3050
  # the per-attempt `git reset --hard` destroying uncommitted state:
@@ -4014,12 +4028,16 @@ def main() -> int:
4014
4028
  "'branch:') and commit its folder, then run — the do-it-for-me "
4015
4029
  "path after /draft-feature, which leaves the folder "
4016
4030
  "uncommitted on the current branch.")
4031
+ ap.add_argument("--prepare-only", action="store_true",
4032
+ help="Like --prepare (create branch + commit the folder) but "
4033
+ "STOP afterwards without dispatching — review the commit, then "
4034
+ "re-run `specfuse-loop` to start.")
4017
4035
  args = ap.parse_args()
4018
4036
  if not FEATURES_DIR.exists():
4019
4037
  sys.exit(f"No {FEATURES_DIR}. Run from your repo root.")
4020
4038
  auto_sync(dry_run=args.dry_run, no_autosync=args.no_autosync)
4021
4039
  return run(args.feature, args.dry_run, force_full_close=args.force_full_close,
4022
- prepare=args.prepare)
4040
+ prepare=args.prepare, prepare_only=args.prepare_only)
4023
4041
 
4024
4042
 
4025
4043
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.3
3
+ Version: 0.3.4
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
@@ -706,7 +706,7 @@ class TestMainArgparse(unittest.TestCase):
706
706
  with patch.object(sys, "argv", ["loop", "--feature", "my-feature"]):
707
707
  result = loop.main()
708
708
  self.assertEqual(result, 0)
709
- mock_run.assert_called_once_with("my-feature", False, force_full_close=None, prepare=False)
709
+ mock_run.assert_called_once_with("my-feature", False, force_full_close=None, prepare=False, prepare_only=False)
710
710
 
711
711
  def test_dry_run_flag_forwarded_to_run(self):
712
712
  """--dry-run is forwarded as dry_run=True to run()."""
@@ -718,7 +718,7 @@ class TestMainArgparse(unittest.TestCase):
718
718
  with patch.object(loop, "run", return_value=0) as mock_run:
719
719
  with patch.object(sys, "argv", ["loop", "--dry-run"]):
720
720
  loop.main()
721
- mock_run.assert_called_once_with(None, True, force_full_close=None, prepare=False)
721
+ mock_run.assert_called_once_with(None, True, force_full_close=None, prepare=False, prepare_only=False)
722
722
 
723
723
  def test_multi_active_error_propagates_from_run(self):
724
724
  """main() with no args propagates the find_feature multi-active SystemExit."""
@@ -36,6 +36,13 @@ def _init_git(root: Path) -> None:
36
36
  subprocess.run(["git", "-C", str(root), "config", "commit.gpgSign", "false"],
37
37
  check=True)
38
38
  (root / "README.md").write_text("# fixture\n")
39
+ specfuse = root / ".specfuse"
40
+ specfuse.mkdir()
41
+ (specfuse / "verification.yml").write_text(
42
+ "code:\n - name: noop\n command: \"true\"\n"
43
+ "doc:\n - name: noop\n command: \"true\"\n"
44
+ "plannext:\n - name: noop\n command: \"true\"\n"
45
+ )
39
46
  subprocess.run(["git", "-C", str(root), "add", "."], check=True)
40
47
  subprocess.run(["git", "-C", str(root), "commit", "-q", "-m", "init"], check=True)
41
48
 
@@ -46,7 +53,10 @@ def _write_uncommitted_feature(root: Path) -> Path:
46
53
  fdir.mkdir(parents=True)
47
54
  (fdir / "PLAN.md").write_text(
48
55
  f"---\nfeature_id: {_FEAT_ID}\ntitle: T\nbranch: {_BRANCH}\n"
49
- f"roadmap_goal: t\nstatus: active\n---\n\n# Plan\n"
56
+ f"roadmap_goal: t\nstatus: active\n---\n\n# Plan\n\n```yaml\n"
57
+ f"gates:\n - gate: 1\n file: GATE-01.md\n work_units:\n"
58
+ f" - id: {_FEAT_ID}/T01\n file: WU-01.md\n"
59
+ f" depends_on: []\n```\n"
50
60
  )
51
61
  (fdir / "GATE-01.md").write_text("---\ngate: 1\nstatus: open\n---\n\n# Gate 1\n")
52
62
  (fdir / "WU-01.md").write_text(
@@ -85,6 +95,34 @@ class TestPrepareFeature(unittest.TestCase):
85
95
  capture_output=True, text=True).stdout
86
96
  self.assertIn("PLAN.md", tracked)
87
97
 
98
+ def test_prepare_only_prepares_then_stops(self):
99
+ """--prepare-only branches + commits but does NOT dispatch (rc=0)."""
100
+ with tempfile.TemporaryDirectory() as tmp:
101
+ root = Path(tmp)
102
+ _init_git(root)
103
+ _write_uncommitted_feature(root)
104
+ os.chdir(root)
105
+
106
+ called = {"dispatch": False}
107
+
108
+ def _no_dispatch(*a, **k):
109
+ called["dispatch"] = True
110
+ raise AssertionError("must not dispatch under --prepare-only")
111
+
112
+ orig = loop.dispatch
113
+ loop.dispatch = _no_dispatch
114
+ try:
115
+ rc = loop.run(None, dry_run=False, prepare_only=True)
116
+ finally:
117
+ loop.dispatch = orig
118
+
119
+ self.assertEqual(rc, 0)
120
+ self.assertFalse(called["dispatch"])
121
+ self.assertEqual(loop._current_branch(), _BRANCH)
122
+ # folder committed (tree clean)
123
+ rel = Path(".specfuse/features") / f"{_FEAT_ID}-thing"
124
+ self.assertEqual(loop.untracked_feature_files(rel), [])
125
+
88
126
  def test_committed_guard_message_suggests_prepare_and_branch(self):
89
127
  """On the default branch, the untracked-folder refusal names --prepare
90
128
  AND the `git checkout -b <branch>` step."""
@@ -1 +0,0 @@
1
- 0.3.3
File without changes
File without changes
File without changes
File without changes