specfuse-loop 0.3.11__tar.gz → 0.3.13__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 (147) hide show
  1. {specfuse_loop-0.3.11/specfuse_loop.egg-info → specfuse_loop-0.3.13}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.13/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/loop.py +128 -11
  5. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13/specfuse_loop.egg-info}/PKG-INFO +1 -1
  6. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/SOURCES.txt +2 -0
  7. specfuse_loop-0.3.13/tests/test_close_wu_self_reference.py +115 -0
  8. specfuse_loop-0.3.13/tests/test_prepare_scaffold_sync.py +172 -0
  9. specfuse_loop-0.3.11/specfuse/loop/data/VERSION +0 -1
  10. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/LICENSE +0 -0
  11. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/NOTICE +0 -0
  12. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/README.md +0 -0
  13. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/setup.cfg +0 -0
  14. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/__init__.py +0 -0
  15. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/_miniyaml.py +0 -0
  16. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/adopt_feature.py +0 -0
  17. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  18. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  19. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  20. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/getting-started.md +0 -0
  21. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/methodology.md +0 -0
  22. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/skills.md +0 -0
  23. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/gitignore.snippet +0 -0
  24. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/roadmap.template.md +0 -0
  25. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  26. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/never-touch.md +0 -0
  27. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/result-contract.md +0 -0
  28. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  29. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  30. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/event.schema.json +0 -0
  31. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  32. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  33. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  34. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  35. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/GATE.template.md +0 -0
  36. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  37. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/WU.template.md +0 -0
  38. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/data/verification.yml.example +0 -0
  39. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/gate_eval.py +0 -0
  40. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/gh_backend.py +0 -0
  41. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/gh_features.py +0 -0
  42. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/lint_plan.py +0 -0
  43. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/scaffold.py +0 -0
  44. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse/loop/validate_event.py +0 -0
  45. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  46. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/entry_points.txt +0 -0
  47. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/requires.txt +0 -0
  48. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/top_level.txt +0 -0
  49. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_adopt_feature.py +0 -0
  50. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  51. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_attempt_outcome_emission.py +0 -0
  52. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_autosync.py +0 -0
  53. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_autosync_consent.py +0 -0
  54. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_autosync_firstrun.py +0 -0
  55. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_autosync_plugin.py +0 -0
  56. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_backend.py +0 -0
  57. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  58. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  59. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_closing_deliverable_guard.py +0 -0
  60. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_cost_tracking.py +0 -0
  61. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_degraded_oracle.py +0 -0
  62. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_deliverable_presence_gate.py +0 -0
  63. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_doctor.py +0 -0
  64. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_driver_integration.py +0 -0
  65. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_driver_lock.py +0 -0
  66. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_duration_tracking.py +0 -0
  67. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_empty_files_escalation.py +0 -0
  68. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_ensure_feature_branch.py +0 -0
  69. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_events_redaction.py +0 -0
  70. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_extra_gates.py +0 -0
  71. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_force_full_close.py +0 -0
  72. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_gate_eval.py +0 -0
  73. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_gate_eval_calibration.py +0 -0
  74. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  75. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_gate_eval_terminal_wiring.py +0 -0
  76. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_gh_backend.py +0 -0
  77. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_gh_features.py +0 -0
  78. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_git_env_isolation.py +0 -0
  79. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_hashed_denylist.py +0 -0
  80. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_hashed_denylist_ci.py +0 -0
  81. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_init_integration.py +0 -0
  82. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_leak_findings_redaction.py +0 -0
  83. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_leak_scan.py +0 -0
  84. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_leak_scan_content.py +0 -0
  85. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_learnings_query.py +0 -0
  86. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  87. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lifecycle_integration.py +0 -0
  88. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_bare_produces_path.py +0 -0
  89. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_close_intermediate.py +0 -0
  90. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_close_wu.py +0 -0
  91. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_correlation_id.py +0 -0
  92. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  93. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_oracle_env.py +0 -0
  94. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_plan_errors.py +0 -0
  95. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_plan_next_draft.py +0 -0
  96. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_plan_relative_import.py +0 -0
  97. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_produces_driver_helper.py +0 -0
  98. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_sections.py +0 -0
  99. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  100. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_auto_archive.py +0 -0
  101. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_caveman_preamble.py +0 -0
  102. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_close_intermediate.py +0 -0
  103. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_defaults_by_type.py +0 -0
  104. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_effort.py +0 -0
  105. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_failure_note_cap.py +0 -0
  106. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_files_changed_guard.py +0 -0
  107. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_gate_budget.py +0 -0
  108. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_model_alias.py +0 -0
  109. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_orchestration.py +0 -0
  110. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_post_pass_invariant.py +0 -0
  111. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_reset_preserving_events.py +0 -0
  112. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_smoke_runner.py +0 -0
  113. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_unsandboxed.py +0 -0
  114. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_loop_zero_token_guard.py +0 -0
  115. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_migrate_legacy.py +0 -0
  116. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_miniyaml_equivalence.py +0 -0
  117. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_miniyaml_negative.py +0 -0
  118. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_planned_cost_lint.py +0 -0
  119. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_prepare_feature.py +0 -0
  120. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_produces_field.py +0 -0
  121. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_result_block.py +0 -0
  122. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_roadmap_add_skill.py +0 -0
  123. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_roadmap_archive_skill.py +0 -0
  124. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_roadmap_row_parser.py +0 -0
  125. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_data_in_sync.py +0 -0
  126. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_doc_hygiene.py +0 -0
  127. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_docs.py +0 -0
  128. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_init.py +0 -0
  129. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_manifest.py +0 -0
  130. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_resources.py +0 -0
  131. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_seed_sanity.py +0 -0
  132. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_upgrade.py +0 -0
  133. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_scaffold_wiring.py +0 -0
  134. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_skills_vendored_in_sync.py +0 -0
  135. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_squash_commit_hook_crash.py +0 -0
  136. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_template_closing_shapes.py +0 -0
  137. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_terminal_flip_ownership.py +0 -0
  138. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_terminal_flips.py +0 -0
  139. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_untracked_feature_folder.py +0 -0
  140. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_upgrade_integration.py +0 -0
  141. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_upgrade_merge_gate.py +0 -0
  142. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_validate_event.py +0 -0
  143. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_verdict_coupling.py +0 -0
  144. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_verify_empty_gate_set.py +0 -0
  145. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_version_consistency.py +0 -0
  146. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/tests/test_version_skew.py +0 -0
  147. {specfuse_loop-0.3.11 → specfuse_loop-0.3.13}/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.11
3
+ Version: 0.3.13
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.11"
7
+ version = "0.3.13"
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.13
@@ -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.11"
65
+ DRIVER_VERSION = "0.3.13"
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
@@ -766,6 +766,84 @@ def _current_branch() -> str:
766
766
  return git("branch", "--show-current")
767
767
 
768
768
 
769
+ def _default_branch() -> "str | None":
770
+ """The repo's default branch, or None if undeterminable.
771
+
772
+ Prefers origin's HEAD symref (`origin/main` -> `main`); falls back to a
773
+ local `main`/`master` when there is no remote. None when neither resolves —
774
+ callers then treat the current branch as non-default (auto_sync commits its
775
+ scaffold sync rather than leaving it dangling).
776
+ """
777
+ ref = subprocess.run(
778
+ ["git", "symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
779
+ capture_output=True, text=True,
780
+ )
781
+ if ref.returncode == 0 and ref.stdout.strip():
782
+ name = ref.stdout.strip()
783
+ return name[len("origin/"):] if name.startswith("origin/") else name
784
+ for cand in ("main", "master"):
785
+ if subprocess.run(
786
+ ["git", "rev-parse", "--verify", "--quiet", cand],
787
+ capture_output=True, text=True,
788
+ ).returncode == 0:
789
+ return cand
790
+ return None
791
+
792
+
793
+ # Paths the scaffold overlay owns — auto_sync writes these on upgrade. They are
794
+ # the driver's, not the user's edits: they may carry onto a feature branch and
795
+ # are folded into the --prepare scaffold commit. Mirrors scaffold.py's versioned
796
+ # overlay set (templates/, rules/, docs/, schemas/) plus the meta files.
797
+ _SCAFFOLD_MANAGED_PREFIXES: tuple[str, ...] = (
798
+ ".specfuse/templates/", ".specfuse/rules/",
799
+ ".specfuse/docs/", ".specfuse/schemas/",
800
+ )
801
+ _SCAFFOLD_MANAGED_EXACT: frozenset[str] = frozenset({
802
+ ".specfuse/VERSION", ".specfuse/.scaffold-manifest",
803
+ ".specfuse/verification.yml.example",
804
+ })
805
+
806
+
807
+ def _is_scaffold_managed(path: str) -> bool:
808
+ return (path in _SCAFFOLD_MANAGED_EXACT
809
+ or any(path.startswith(p) for p in _SCAFFOLD_MANAGED_PREFIXES))
810
+
811
+
812
+ def _scaffold_managed_dirty() -> set[str]:
813
+ """Tracked-dirty paths the scaffold overlay owns (subset of the dirty set)."""
814
+ return {p for p in _tracked_dirty_paths() if _is_scaffold_managed(p)}
815
+
816
+
817
+ def _persist_scaffold_sync(installed: str) -> None:
818
+ """After auto_sync overlays a newer scaffold, keep the tree clean for the
819
+ rest of the run. On a non-default branch, commit the scaffold-managed
820
+ changes (`chore(loop): sync scaffold to X.Y.Z`). On the DEFAULT branch, leave
821
+ them and print guidance — committing scaffold churn onto the default branch
822
+ is undesirable; --prepare (or the next ensure_feature_branch) carries them
823
+ onto the feature branch and folds them into its scaffold commit instead.
824
+ """
825
+ dirty = _scaffold_managed_dirty()
826
+ if not dirty:
827
+ return
828
+ branch = _current_branch()
829
+ on_default = (not branch) or branch == _default_branch()
830
+ if on_default:
831
+ print(
832
+ f"auto_sync: scaffold upgraded to {installed}; leaving "
833
+ f"{len(dirty)} scaffold file(s) uncommitted on default branch "
834
+ f"'{branch or '(detached)'}'. --prepare will carry them onto the "
835
+ f"feature branch, or commit them yourself.",
836
+ file=sys.stderr,
837
+ )
838
+ return
839
+ commit_bookkeeping(sorted(dirty), f"chore(loop): sync scaffold to {installed}")
840
+ print(
841
+ f"auto_sync: scaffold upgraded to {installed}; committed "
842
+ f"{len(dirty)} scaffold file(s) on '{branch}'.",
843
+ file=sys.stderr,
844
+ )
845
+
846
+
769
847
  def _branch_prep_hint(feature_dir: "Path", feat_fm: dict, feature_id: str) -> str:
770
848
  """Actionable next-step block for the two pre-flight guards.
771
849
 
@@ -920,6 +998,11 @@ def prepare_feature(feat_fm: dict, feature_dir: "Path", feature_id: str) -> None
920
998
  for rel in (".specfuse/roadmap.md", ".specfuse/roadmap-archive.md"):
921
999
  if (repo_root / rel).exists():
922
1000
  add_paths.append(str(repo_root / rel))
1001
+ # Fold auto_sync's scaffold overlay into this commit too. On the default
1002
+ # branch auto_sync leaves those uncommitted (see _persist_scaffold_sync);
1003
+ # committing them here lands the upgrade on the feature branch, clean.
1004
+ for rel in sorted(_scaffold_managed_dirty()):
1005
+ add_paths.append(str(repo_root / rel))
923
1006
  git("add", "--", *add_paths)
924
1007
  staged = git("status", "--porcelain", "--", *add_paths)
925
1008
  if staged:
@@ -1006,7 +1089,11 @@ def ensure_feature_branch(feat_fm: dict, feature_dir: "Path | None" = None) -> N
1006
1089
  # Create-from-HEAD carries the working tree onto the new branch. Only
1007
1090
  # the expected /pick-feature flips may ride along; anything else stops.
1008
1091
  dirty = _tracked_dirty_paths()
1009
- unexpected = dirty - _expected_flip_paths(feature_dir)
1092
+ # Scaffold-overlay files (auto_sync's own upgrade writes) ride along too:
1093
+ # they are driver-owned, and prepare_feature folds them into the scaffold
1094
+ # commit. Only genuinely-unrelated edits remain "unexpected" (#prepare).
1095
+ allowed = _expected_flip_paths(feature_dir) | _scaffold_managed_dirty()
1096
+ unexpected = dirty - allowed
1010
1097
  if unexpected:
1011
1098
  raise FeatureBranchError(
1012
1099
  "working tree has uncommitted changes to unexpected paths: "
@@ -2564,13 +2651,17 @@ _NO_FAILURES_SENTINEL = "### Failure-class breakdown\n\n(no non-passing attempts
2564
2651
  def summarize_attempt_failure_classes(
2565
2652
  feature_dir: Path,
2566
2653
  gate_n: int | None = None,
2654
+ exclude_correlation_id: str | None = None,
2567
2655
  ) -> str:
2568
2656
  """Render a '### Failure-class breakdown' markdown table from events.jsonl.
2569
2657
 
2570
2658
  Reads attempt_outcome events whose outcome != 'passed'. When gate_n is
2571
2659
  provided, restricts to events whose correlation_id belongs to that gate
2572
- (resolved via _gate_number_from_wu_id). Returns _NO_FAILURES_SENTINEL when
2573
- no non-passing attempts match the filter.
2660
+ (resolved via _gate_number_from_wu_id). When exclude_correlation_id is
2661
+ provided, drops events with that exact correlation_id — used to keep a
2662
+ close WU's OWN non-passing attempts out of the breakdown it authors, so the
2663
+ close's stumble does not arm the guard against itself (issue #145). Returns
2664
+ _NO_FAILURES_SENTINEL when no non-passing attempts match the filter.
2574
2665
 
2575
2666
  Pure function — reads events.jsonl; no writes, no side effects.
2576
2667
  Malformed JSONL lines are skipped (legacy-event tolerance, AC5).
@@ -2593,8 +2684,10 @@ def summarize_attempt_failure_classes(
2593
2684
  payload = evt.get("payload") or {}
2594
2685
  if payload.get("outcome") == "passed":
2595
2686
  continue
2687
+ cid = evt.get("correlation_id", "")
2688
+ if exclude_correlation_id is not None and cid == exclude_correlation_id:
2689
+ continue
2596
2690
  if gate_n is not None:
2597
- cid = evt.get("correlation_id", "")
2598
2691
  if _gate_number_from_wu_id(cid) != gate_n:
2599
2692
  continue
2600
2693
  non_passing.append(payload)
@@ -2658,7 +2751,14 @@ def assert_failure_class_breakdown_when_failures_present(
2658
2751
  return True, ""
2659
2752
 
2660
2753
  gate_n = _gate_number_from_wu_id(wu.wu_id)
2661
- summary = summarize_attempt_failure_classes(feature_dir, gate_n)
2754
+ # Exclude the close WU's OWN non-passing attempts: the breakdown documents
2755
+ # SUBSTANTIVE-WU failures, not the close's own stumble. Without this, a
2756
+ # failed first close attempt retroactively requires a new subsection in the
2757
+ # RETROSPECTIVE the close itself authors — and the between-attempt
2758
+ # `reset --hard` wipes the partial each retry → spin → blocked_human on
2759
+ # otherwise-done features (issue #145).
2760
+ summary = summarize_attempt_failure_classes(
2761
+ feature_dir, gate_n, exclude_correlation_id=wu.wu_id)
2662
2762
 
2663
2763
  if summary == _NO_FAILURES_SENTINEL:
2664
2764
  return True, ""
@@ -2666,7 +2766,7 @@ def assert_failure_class_breakdown_when_failures_present(
2666
2766
  if re.search(r"^#{3} Failure-class breakdown\b", retro.read_text(), re.MULTILINE):
2667
2767
  return True, ""
2668
2768
 
2669
- # Count non-passing attempts for the error message.
2769
+ # Count non-passing attempts (excluding the close's own) for the message.
2670
2770
  events_path = feature_dir / "events.jsonl"
2671
2771
  count = 0
2672
2772
  if events_path.exists():
@@ -2683,18 +2783,25 @@ def assert_failure_class_breakdown_when_failures_present(
2683
2783
  payload = evt.get("payload") or {}
2684
2784
  if payload.get("outcome") == "passed":
2685
2785
  continue
2786
+ cid = evt.get("correlation_id", "")
2787
+ if cid == wu.wu_id:
2788
+ continue
2686
2789
  if gate_n is not None:
2687
- cid = evt.get("correlation_id", "")
2688
2790
  if _gate_number_from_wu_id(cid) != gate_n:
2689
2791
  continue
2690
2792
  count += 1
2691
2793
 
2692
2794
  gate_label = f"gate {gate_n}" if gate_n is not None else "all gates"
2795
+ # Actionable, self-contained retry note (flows into the next attempt's prompt
2796
+ # via failure_note, issue #145 fix #2): name the moved bar AND embed the exact
2797
+ # table to paste, so the agent doesn't rediscover the requirement.
2693
2798
  return (
2694
2799
  False,
2695
2800
  f"assert_failure_class_breakdown_when_failures_present: {count} "
2696
- f"non-passing attempt(s) in {gate_label} but '### Failure-class breakdown' "
2697
- f"subsection absent from RETROSPECTIVE.md",
2801
+ f"substantive-WU attempt(s) in {gate_label} did not pass, so "
2802
+ f"RETROSPECTIVE.md MUST include a '### Failure-class breakdown' subsection "
2803
+ f"— it is absent. Add exactly this subsection to RETROSPECTIVE.md:\n\n"
2804
+ f"{summary}",
2698
2805
  )
2699
2806
 
2700
2807
 
@@ -3112,7 +3219,14 @@ def run(
3112
3219
  # draft-feature can't take). --prepare-only stops after, so you can
3113
3220
  # review the commit before the loop dispatches anything.
3114
3221
  if prepare or prepare_only:
3115
- prepare_feature(feat_fm, feature_dir, feature_id)
3222
+ # Genuinely-unrelated dirty paths (not the /pick-feature flips, not
3223
+ # auto_sync's scaffold overlay) still stop --prepare — but with a
3224
+ # clean, actionable message and a non-zero exit, not a traceback.
3225
+ try:
3226
+ prepare_feature(feat_fm, feature_dir, feature_id)
3227
+ except FeatureBranchError as exc:
3228
+ print(f"--prepare cannot proceed:\n{exc}", file=sys.stderr)
3229
+ return 1
3116
3230
  if prepare_only:
3117
3231
  print("Prepared: feature is on its branch and committed. "
3118
3232
  "Re-run `specfuse-loop` to start the gate.")
@@ -3998,6 +4112,7 @@ def auto_sync(
3998
4112
  f"WARNING: auto_sync: plugin config drift corrected: {', '.join(changes)}",
3999
4113
  file=sys.stderr,
4000
4114
  )
4115
+ _persist_scaffold_sync(installed)
4001
4116
  return
4002
4117
 
4003
4118
  # Older with modified files.
@@ -4056,6 +4171,7 @@ def auto_sync(
4056
4171
  f"WARNING: auto_sync: plugin config drift corrected: {', '.join(changes)}",
4057
4172
  file=sys.stderr,
4058
4173
  )
4174
+ _persist_scaffold_sync(installed)
4059
4175
  else:
4060
4176
  # Non-interactive (CI / claude -p): skip modified files + warn; never block.
4061
4177
  print(
@@ -4092,6 +4208,7 @@ def auto_sync(
4092
4208
  f"WARNING: auto_sync: plugin config drift corrected: {', '.join(changes)}",
4093
4209
  file=sys.stderr,
4094
4210
  )
4211
+ _persist_scaffold_sync(installed)
4095
4212
 
4096
4213
 
4097
4214
  def main() -> int:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.11
3
+ Version: 0.3.13
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
@@ -51,6 +51,7 @@ tests/test_autosync_plugin.py
51
51
  tests/test_backend.py
52
52
  tests/test_bookkeeping_commit_crash_run.py
53
53
  tests/test_bookkeeping_commit_hook_crash.py
54
+ tests/test_close_wu_self_reference.py
54
55
  tests/test_closing_deliverable_guard.py
55
56
  tests/test_cost_tracking.py
56
57
  tests/test_degraded_oracle.py
@@ -112,6 +113,7 @@ tests/test_miniyaml_equivalence.py
112
113
  tests/test_miniyaml_negative.py
113
114
  tests/test_planned_cost_lint.py
114
115
  tests/test_prepare_feature.py
116
+ tests/test_prepare_scaffold_sync.py
115
117
  tests/test_produces_field.py
116
118
  tests/test_result_block.py
117
119
  tests/test_roadmap_add_skill.py
@@ -0,0 +1,115 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """Issue #145 — a close WU's own failed attempt must not arm the
6
+ failure-class-breakdown guard against that same close WU.
7
+
8
+ `assert_failure_class_breakdown_when_failures_present` requires a
9
+ `### Failure-class breakdown` subsection in RETROSPECTIVE.md once non-passing
10
+ attempts exist in the gate. It counted ALL non-passing attempts — including the
11
+ close WU's OWN. Since the close WU is the one that authors the RETROSPECTIVE the
12
+ guard inspects, its malformed first attempt retroactively added a required
13
+ subsection to its own output, and the between-attempt `reset --hard` wiped the
14
+ partial each retry → spin → blocked_human on otherwise-done features.
15
+
16
+ Fix #3: exclude the close WU's own attempts (correlation_id == its wu_id) from
17
+ the failure set that arms the guard against it. Substantive-WU failures still
18
+ require the breakdown. Fix #2: when the guard does fire, its message names the
19
+ required subsection and embeds the exact expected table (it flows into the retry
20
+ prompt via failure_note), so the agent doesn't rediscover the moved bar.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import tempfile
27
+ import unittest
28
+ from pathlib import Path
29
+
30
+ from tests._loop_loader import load_loop
31
+
32
+ loop = load_loop()
33
+
34
+ CLOSE_ID = "FEAT-2026-0099/G1-CLOSE"
35
+ SUBSTANTIVE_ID = "FEAT-2026-0099/G1-T01"
36
+
37
+
38
+ def _close_wu(wu_file: Path) -> "loop.WorkUnit":
39
+ return loop.WorkUnit(
40
+ wu_id=CLOSE_ID, file=wu_file, depends_on=[], type="close",
41
+ model="opus", effort="high", status="pending", attempts=1,
42
+ title="Close gate 1", body="body",
43
+ )
44
+
45
+
46
+ def _attempt_event(correlation_id: str, outcome: str = "failed",
47
+ failure_class: str = "other", sig: str = "verdict_malformed") -> str:
48
+ return json.dumps({
49
+ "event_type": "attempt_outcome",
50
+ "correlation_id": correlation_id,
51
+ "timestamp": "2026-07-12T00:00:00Z",
52
+ "payload": {"outcome": outcome, "failure_class": failure_class,
53
+ "failure_signature": sig},
54
+ })
55
+
56
+
57
+ class TestCloseSelfReference(unittest.TestCase):
58
+ def _feature_dir(self, tmp: str, events: list[str], retro: str = "# Retro\n") -> Path:
59
+ d = Path(tmp)
60
+ (d / "events.jsonl").write_text("\n".join(events) + "\n")
61
+ (d / "RETROSPECTIVE.md").write_text(retro)
62
+ return d
63
+
64
+ def test_close_own_failure_does_not_arm_guard(self):
65
+ # Only the close WU's OWN attempts are non-passing (the observed run).
66
+ with tempfile.TemporaryDirectory() as tmp:
67
+ d = self._feature_dir(tmp, [_attempt_event(CLOSE_ID)])
68
+ wu = _close_wu(d / "WU.md")
69
+ ok, msg = loop.assert_failure_class_breakdown_when_failures_present(
70
+ wu, d, Path(tmp), "HEAD~1")
71
+ self.assertTrue(ok, f"close's own failure must not require the breakdown: {msg}")
72
+
73
+ def test_substantive_failure_still_requires_breakdown(self):
74
+ with tempfile.TemporaryDirectory() as tmp:
75
+ d = self._feature_dir(
76
+ tmp, [_attempt_event(SUBSTANTIVE_ID), _attempt_event(CLOSE_ID)])
77
+ wu = _close_wu(d / "WU.md")
78
+ ok, msg = loop.assert_failure_class_breakdown_when_failures_present(
79
+ wu, d, Path(tmp), "HEAD~1")
80
+ self.assertFalse(ok, "a substantive-WU failure still requires the breakdown")
81
+ # Fix #2 — message is actionable and embeds the exact table to include.
82
+ self.assertIn("### Failure-class breakdown", msg)
83
+ self.assertIn("| failure_class |", msg)
84
+
85
+ def test_breakdown_table_excludes_close_own_attempts(self):
86
+ # Two substantive failures + one close-own failure; the rendered table
87
+ # the guard offers must count only the two substantive ones.
88
+ with tempfile.TemporaryDirectory() as tmp:
89
+ d = self._feature_dir(
90
+ tmp,
91
+ [_attempt_event(SUBSTANTIVE_ID), _attempt_event(SUBSTANTIVE_ID),
92
+ _attempt_event(CLOSE_ID)])
93
+ table = loop.summarize_attempt_failure_classes(
94
+ d, gate_n=1, exclude_correlation_id=CLOSE_ID)
95
+ self.assertIn("**total** | **2**", table)
96
+
97
+ def test_summarize_all_self_returns_sentinel(self):
98
+ with tempfile.TemporaryDirectory() as tmp:
99
+ d = self._feature_dir(tmp, [_attempt_event(CLOSE_ID)])
100
+ table = loop.summarize_attempt_failure_classes(
101
+ d, gate_n=1, exclude_correlation_id=CLOSE_ID)
102
+ self.assertEqual(table, loop._NO_FAILURES_SENTINEL)
103
+
104
+ def test_breakdown_present_passes(self):
105
+ retro = "# Retro\n\n### Failure-class breakdown\n\n| x | 1 | y |\n"
106
+ with tempfile.TemporaryDirectory() as tmp:
107
+ d = self._feature_dir(tmp, [_attempt_event(SUBSTANTIVE_ID)], retro=retro)
108
+ wu = _close_wu(d / "WU.md")
109
+ ok, _ = loop.assert_failure_class_breakdown_when_failures_present(
110
+ wu, d, Path(tmp), "HEAD~1")
111
+ self.assertTrue(ok)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()
@@ -0,0 +1,172 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """auto_sync's scaffold upgrade must not poison the same run's --prepare.
6
+
7
+ Before this fix, `specfuse-loop --prepare` crashed with a traceback: auto_sync
8
+ overlays the new scaffold (dirtying .specfuse/VERSION, .scaffold-manifest,
9
+ docs/, templates/) and leaves it uncommitted, then ensure_feature_branch
10
+ refuses to carry those "unexpected" paths onto the new feature branch.
11
+
12
+ The graceful behavior pinned here:
13
+
14
+ 1. auto_sync COMMITS its own scaffold overlay on a non-default branch
15
+ (`chore(loop): sync scaffold to X.Y.Z`) so the tree is clean; on the
16
+ DEFAULT branch it leaves them + prints guidance (--prepare carries them).
17
+ 2. ensure_feature_branch CARRIES scaffold-managed dirty paths onto a new
18
+ branch instead of refusing — but still blocks genuinely-unrelated edits.
19
+ 3. prepare_feature FOLDS the scaffold-managed paths into its scaffold commit
20
+ (so the default-branch case lands them committed on the feature branch).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ import subprocess
27
+ import unittest
28
+ from contextlib import contextmanager
29
+ from pathlib import Path
30
+ from tempfile import TemporaryDirectory
31
+
32
+ from tests._loop_loader import load_loop
33
+
34
+ loop = load_loop()
35
+
36
+
37
+ def _git(root: Path, *args: str) -> str:
38
+ return subprocess.run(
39
+ ["git", "-C", str(root), *args],
40
+ capture_output=True, text=True, check=True,
41
+ ).stdout.strip()
42
+
43
+
44
+ @contextmanager
45
+ def _chdir(path: Path):
46
+ prev = Path.cwd()
47
+ os.chdir(path)
48
+ try:
49
+ yield
50
+ finally:
51
+ os.chdir(prev)
52
+
53
+
54
+ @contextmanager
55
+ def _repo():
56
+ """Temp git repo on `main`, one commit, a tracked scaffold template."""
57
+ with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
58
+ root = Path(tmp)
59
+ subprocess.run(["git", "init", "-q", "-b", "main", str(root)], check=True)
60
+ _git(root, "config", "user.email", "test@example.com")
61
+ _git(root, "config", "user.name", "Test")
62
+ _git(root, "config", "commit.gpgSign", "false")
63
+ tmpl = root / ".specfuse/templates/WU.template.md"
64
+ tmpl.parent.mkdir(parents=True)
65
+ tmpl.write_text("old template\n")
66
+ (root / ".specfuse/VERSION").write_text("0.3.0\n")
67
+ (root / "README.md").write_text("# fixture\n")
68
+ _git(root, "add", ".")
69
+ _git(root, "commit", "-q", "-m", "init")
70
+ yield root
71
+
72
+
73
+ def _dirty_scaffold(root: Path) -> None:
74
+ """Simulate auto_sync's overlay: modify tracked scaffold files."""
75
+ (root / ".specfuse/templates/WU.template.md").write_text("new template\n")
76
+ (root / ".specfuse/VERSION").write_text("0.3.11\n")
77
+
78
+
79
+ class TestDefaultBranch(unittest.TestCase):
80
+ def test_local_main_detected(self):
81
+ with _repo() as root, _chdir(root):
82
+ self.assertEqual(loop._default_branch(), "main")
83
+
84
+
85
+ class TestScaffoldManagedDirty(unittest.TestCase):
86
+ def test_classifies_scaffold_vs_user_paths(self):
87
+ with _repo() as root:
88
+ _dirty_scaffold(root)
89
+ (root / "README.md").write_text("# edited\n") # user edit
90
+ with _chdir(root):
91
+ managed = loop._scaffold_managed_dirty()
92
+ self.assertIn(".specfuse/templates/WU.template.md", managed)
93
+ self.assertIn(".specfuse/VERSION", managed)
94
+ self.assertNotIn("README.md", managed)
95
+
96
+
97
+ class TestPersistScaffoldSync(unittest.TestCase):
98
+ def test_commits_on_feature_branch(self):
99
+ with _repo() as root:
100
+ _git(root, "checkout", "-q", "-b", "feat/x")
101
+ _dirty_scaffold(root)
102
+ with _chdir(root):
103
+ loop._persist_scaffold_sync("0.3.11")
104
+ dirty = loop._tracked_dirty_paths()
105
+ self.assertEqual(dirty, set(), "scaffold sync must be committed")
106
+ self.assertIn("sync scaffold to 0.3.11",
107
+ _git(root, "log", "-1", "--pretty=%s"))
108
+
109
+ def test_guides_and_leaves_on_default_branch(self):
110
+ with _repo() as root: # on main == default
111
+ _dirty_scaffold(root)
112
+ with _chdir(root):
113
+ loop._persist_scaffold_sync("0.3.11")
114
+ dirty = loop._tracked_dirty_paths()
115
+ # left uncommitted (no surprise commit on the default branch)
116
+ self.assertIn(".specfuse/VERSION", dirty)
117
+ self.assertEqual("init", _git(root, "log", "-1", "--pretty=%s"))
118
+
119
+
120
+ class TestEnsureFeatureBranchCarriesScaffold(unittest.TestCase):
121
+ def test_scaffold_paths_carry_onto_new_branch(self):
122
+ with _repo() as root:
123
+ _dirty_scaffold(root)
124
+ with _chdir(root):
125
+ loop.ensure_feature_branch({"branch": "feat/y"})
126
+ current = _git(root, "branch", "--show-current")
127
+ self.assertEqual(current, "feat/y")
128
+ self.assertEqual(
129
+ (root / ".specfuse/templates/WU.template.md").read_text(),
130
+ "new template\n",
131
+ )
132
+
133
+ def test_unexpected_edit_still_blocks_even_with_scaffold_dirty(self):
134
+ with _repo() as root:
135
+ _commit = root / "src/app.py"
136
+ _commit.parent.mkdir()
137
+ _commit.write_text("x = 1\n")
138
+ _git(root, "add", ".")
139
+ _git(root, "commit", "-q", "-m", "app")
140
+ _dirty_scaffold(root)
141
+ (root / "src/app.py").write_text("x = 2\n") # unrelated edit
142
+ with _chdir(root):
143
+ with self.assertRaises(loop.FeatureBranchError) as ctx:
144
+ loop.ensure_feature_branch({"branch": "feat/z"})
145
+ self.assertIn("src/app.py", str(ctx.exception))
146
+
147
+
148
+ class TestPrepareFoldsScaffold(unittest.TestCase):
149
+ def test_default_branch_scaffold_folds_into_prepare_commit(self):
150
+ with _repo() as root: # on main == default
151
+ feat_dir = root / ".specfuse/features/FEAT-2026-0099-x"
152
+ feat_dir.mkdir(parents=True)
153
+ (feat_dir / "PLAN.md").write_text("branch: feat/FEAT-2026-0099-x\n")
154
+ _dirty_scaffold(root) # auto_sync left these on default branch
155
+ with _chdir(root):
156
+ loop.prepare_feature(
157
+ {"branch": "feat/FEAT-2026-0099-x"}, feat_dir,
158
+ "FEAT-2026-0099",
159
+ )
160
+ dirty = loop._tracked_dirty_paths()
161
+ current = _git(root, "branch", "--show-current")
162
+ self.assertEqual(current, "feat/FEAT-2026-0099-x")
163
+ self.assertEqual(dirty, set(), "scaffold + folder must be committed")
164
+ # scaffold file is committed on the feature branch
165
+ self.assertEqual(
166
+ _git(root, "show", "HEAD:.specfuse/templates/WU.template.md"),
167
+ "new template",
168
+ )
169
+
170
+
171
+ if __name__ == "__main__":
172
+ unittest.main()
@@ -1 +0,0 @@
1
- 0.3.11
File without changes
File without changes
File without changes
File without changes