specfuse-loop 0.3.3__tar.gz → 0.3.5__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 (133) hide show
  1. {specfuse_loop-0.3.3/specfuse_loop.egg-info → specfuse_loop-0.3.5}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.5/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/loop.py +33 -10
  5. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5/specfuse_loop.egg-info}/PKG-INFO +1 -1
  6. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/SOURCES.txt +1 -0
  7. specfuse_loop-0.3.5/tests/test_lint_plan_relative_import.py +57 -0
  8. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_orchestration.py +2 -2
  9. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_prepare_feature.py +39 -1
  10. specfuse_loop-0.3.3/specfuse/loop/data/VERSION +0 -1
  11. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/LICENSE +0 -0
  12. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/NOTICE +0 -0
  13. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/README.md +0 -0
  14. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/setup.cfg +0 -0
  15. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/__init__.py +0 -0
  16. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/_miniyaml.py +0 -0
  17. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/adopt_feature.py +0 -0
  18. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  19. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  20. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  21. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/getting-started.md +0 -0
  22. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/methodology.md +0 -0
  23. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/skills.md +0 -0
  24. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/gitignore.snippet +0 -0
  25. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/roadmap.template.md +0 -0
  26. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  27. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/rules/never-touch.md +0 -0
  28. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/rules/result-contract.md +0 -0
  29. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  30. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/templates/GATE.template.md +0 -0
  31. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  32. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/templates/WU.template.md +0 -0
  33. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/verification.yml.example +0 -0
  34. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/gate_eval.py +0 -0
  35. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/gh_backend.py +0 -0
  36. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/gh_features.py +0 -0
  37. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/lint_plan.py +0 -0
  38. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/scaffold.py +0 -0
  39. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/validate_event.py +0 -0
  40. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  41. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/entry_points.txt +0 -0
  42. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/requires.txt +0 -0
  43. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/top_level.txt +0 -0
  44. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_adopt_feature.py +0 -0
  45. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  46. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_attempt_outcome_emission.py +0 -0
  47. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_autosync.py +0 -0
  48. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_autosync_consent.py +0 -0
  49. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_autosync_firstrun.py +0 -0
  50. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_autosync_plugin.py +0 -0
  51. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_backend.py +0 -0
  52. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  53. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  54. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_closing_deliverable_guard.py +0 -0
  55. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_cost_tracking.py +0 -0
  56. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_deliverable_presence_gate.py +0 -0
  57. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_doctor.py +0 -0
  58. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_driver_integration.py +0 -0
  59. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_driver_lock.py +0 -0
  60. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_duration_tracking.py +0 -0
  61. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_empty_files_escalation.py +0 -0
  62. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_ensure_feature_branch.py +0 -0
  63. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_extra_gates.py +0 -0
  64. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_force_full_close.py +0 -0
  65. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gate_eval.py +0 -0
  66. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gate_eval_calibration.py +0 -0
  67. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  68. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gate_eval_terminal_wiring.py +0 -0
  69. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gh_backend.py +0 -0
  70. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gh_features.py +0 -0
  71. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_git_env_isolation.py +0 -0
  72. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_hashed_denylist.py +0 -0
  73. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_hashed_denylist_ci.py +0 -0
  74. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_init_integration.py +0 -0
  75. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_leak_findings_redaction.py +0 -0
  76. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_leak_scan.py +0 -0
  77. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_leak_scan_content.py +0 -0
  78. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  79. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lifecycle_integration.py +0 -0
  80. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_bare_produces_path.py +0 -0
  81. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_close_intermediate.py +0 -0
  82. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_close_wu.py +0 -0
  83. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_correlation_id.py +0 -0
  84. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  85. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_oracle_env.py +0 -0
  86. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_plan_errors.py +0 -0
  87. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_plan_next_draft.py +0 -0
  88. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_produces_driver_helper.py +0 -0
  89. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_sections.py +0 -0
  90. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  91. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_auto_archive.py +0 -0
  92. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_caveman_preamble.py +0 -0
  93. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_close_intermediate.py +0 -0
  94. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_defaults_by_type.py +0 -0
  95. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_effort.py +0 -0
  96. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_failure_note_cap.py +0 -0
  97. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_files_changed_guard.py +0 -0
  98. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_gate_budget.py +0 -0
  99. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_model_alias.py +0 -0
  100. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_post_pass_invariant.py +0 -0
  101. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_reset_preserving_events.py +0 -0
  102. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_smoke_runner.py +0 -0
  103. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_unsandboxed.py +0 -0
  104. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_zero_token_guard.py +0 -0
  105. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_migrate_legacy.py +0 -0
  106. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_miniyaml_equivalence.py +0 -0
  107. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_miniyaml_negative.py +0 -0
  108. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_planned_cost_lint.py +0 -0
  109. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_produces_field.py +0 -0
  110. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_result_block.py +0 -0
  111. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_roadmap_add_skill.py +0 -0
  112. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_roadmap_archive_skill.py +0 -0
  113. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_roadmap_row_parser.py +0 -0
  114. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_data_in_sync.py +0 -0
  115. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_docs.py +0 -0
  116. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_init.py +0 -0
  117. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_manifest.py +0 -0
  118. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_resources.py +0 -0
  119. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_seed_sanity.py +0 -0
  120. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_upgrade.py +0 -0
  121. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_wiring.py +0 -0
  122. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_squash_commit_hook_crash.py +0 -0
  123. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_template_closing_shapes.py +0 -0
  124. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_terminal_flip_ownership.py +0 -0
  125. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_terminal_flips.py +0 -0
  126. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_untracked_feature_folder.py +0 -0
  127. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_upgrade_integration.py +0 -0
  128. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_validate_event.py +0 -0
  129. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_verdict_coupling.py +0 -0
  130. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_verify_empty_gate_set.py +0 -0
  131. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_version_consistency.py +0 -0
  132. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_version_skew.py +0 -0
  133. {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/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.5
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.5"
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.5
@@ -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.5"
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:
@@ -3517,7 +3531,12 @@ def run(
3517
3531
  # FEAT-2026-0018/T07 — plan-next-draft lint hook (warn-only v1)
3518
3532
  if wu.type == "plan-next":
3519
3533
  try:
3520
- from lint_plan import lint_plan_next_draft
3534
+ # Package-relative + function-local: lint_plan
3535
+ # imports `from .loop import VERDICT_VALUES`, so a
3536
+ # module-top import would be circular; the old
3537
+ # flat (top-level) form broke once the driver
3538
+ # ships as a pip package (#100).
3539
+ from .lint_plan import lint_plan_next_draft
3521
3540
  _warns = lint_plan_next_draft(feature_dir, gate.number)
3522
3541
  except Exception as _exc:
3523
3542
  _warns = [f"lint_plan_next_draft raised: {_exc}"]
@@ -4014,12 +4033,16 @@ def main() -> int:
4014
4033
  "'branch:') and commit its folder, then run — the do-it-for-me "
4015
4034
  "path after /draft-feature, which leaves the folder "
4016
4035
  "uncommitted on the current branch.")
4036
+ ap.add_argument("--prepare-only", action="store_true",
4037
+ help="Like --prepare (create branch + commit the folder) but "
4038
+ "STOP afterwards without dispatching — review the commit, then "
4039
+ "re-run `specfuse-loop` to start.")
4017
4040
  args = ap.parse_args()
4018
4041
  if not FEATURES_DIR.exists():
4019
4042
  sys.exit(f"No {FEATURES_DIR}. Run from your repo root.")
4020
4043
  auto_sync(dry_run=args.dry_run, no_autosync=args.no_autosync)
4021
4044
  return run(args.feature, args.dry_run, force_full_close=args.force_full_close,
4022
- prepare=args.prepare)
4045
+ prepare=args.prepare, prepare_only=args.prepare_only)
4023
4046
 
4024
4047
 
4025
4048
  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.5
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
@@ -79,6 +79,7 @@ tests/test_lint_correlation_id_close_intermediate.py
79
79
  tests/test_lint_oracle_env.py
80
80
  tests/test_lint_plan_errors.py
81
81
  tests/test_lint_plan_next_draft.py
82
+ tests/test_lint_plan_relative_import.py
82
83
  tests/test_lint_produces_driver_helper.py
83
84
  tests/test_lint_sections.py
84
85
  tests/test_lint_task_graph_yaml_selection.py
@@ -0,0 +1,57 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """Regression test for #100 — the plan-next-draft lint hook must use a
6
+ package-relative import.
7
+
8
+ `loop.py`'s plan-next hook imported `from lint_plan import lint_plan_next_draft`
9
+ (flat). That only resolved when the scripts were copied side-by-side (pre-pip);
10
+ in the installed package it raises `No module named 'lint_plan'`, so the
11
+ plan-next gate logs a warning and the draft lint never runs. The import is
12
+ function-local on purpose (lint_plan imports `from .loop import VERDICT_VALUES`,
13
+ so a module-top import would be circular) — the fix is to make the local import
14
+ package-relative, not to hoist it.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ import unittest
23
+ from pathlib import Path
24
+
25
+ REPO_ROOT = Path(__file__).resolve().parent.parent
26
+ LOOP_SRC = REPO_ROOT / "specfuse" / "loop" / "loop.py"
27
+
28
+
29
+ class TestLintPlanRelativeImport(unittest.TestCase):
30
+
31
+ def test_loop_uses_package_relative_lint_plan_import(self):
32
+ src = LOOP_SRC.read_text(encoding="utf-8")
33
+ self.assertNotIn(
34
+ "from lint_plan import", src,
35
+ "flat `from lint_plan import` breaks in the installed package (#100)",
36
+ )
37
+ self.assertIn(
38
+ "from .lint_plan import lint_plan_next_draft", src,
39
+ "the plan-next hook must import lint_plan_next_draft package-relative",
40
+ )
41
+
42
+ def test_lint_plan_next_draft_importable_outside_repo_root(self):
43
+ """Resolvable as a package member even when CWD is not the repo root
44
+ (so a flat `lint_plan.py` on the path can't mask the bug)."""
45
+ with tempfile.TemporaryDirectory() as tmp:
46
+ r = subprocess.run(
47
+ [sys.executable, "-c",
48
+ "from specfuse.loop.lint_plan import lint_plan_next_draft; "
49
+ "print('ok')"],
50
+ capture_output=True, text=True, cwd=tmp,
51
+ )
52
+ self.assertEqual(r.returncode, 0, r.stderr)
53
+ self.assertIn("ok", r.stdout)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ unittest.main()
@@ -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