specfuse-loop 0.3.11__tar.gz → 0.3.12__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.
- {specfuse_loop-0.3.11/specfuse_loop.egg-info → specfuse_loop-0.3.12}/PKG-INFO +1 -1
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/pyproject.toml +1 -1
- specfuse_loop-0.3.12/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/loop.py +100 -3
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse_loop.egg-info/SOURCES.txt +1 -0
- specfuse_loop-0.3.12/tests/test_prepare_scaffold_sync.py +172 -0
- specfuse_loop-0.3.11/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/LICENSE +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/NOTICE +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/README.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/setup.cfg +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/lint_plan.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_backend.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_wu_execution_metadata.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfuse-loop"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.12"
|
|
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.12
|
|
@@ -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.
|
|
65
|
+
DRIVER_VERSION = "0.3.12"
|
|
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
|
-
|
|
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: "
|
|
@@ -3112,7 +3199,14 @@ def run(
|
|
|
3112
3199
|
# draft-feature can't take). --prepare-only stops after, so you can
|
|
3113
3200
|
# review the commit before the loop dispatches anything.
|
|
3114
3201
|
if prepare or prepare_only:
|
|
3115
|
-
|
|
3202
|
+
# Genuinely-unrelated dirty paths (not the /pick-feature flips, not
|
|
3203
|
+
# auto_sync's scaffold overlay) still stop --prepare — but with a
|
|
3204
|
+
# clean, actionable message and a non-zero exit, not a traceback.
|
|
3205
|
+
try:
|
|
3206
|
+
prepare_feature(feat_fm, feature_dir, feature_id)
|
|
3207
|
+
except FeatureBranchError as exc:
|
|
3208
|
+
print(f"--prepare cannot proceed:\n{exc}", file=sys.stderr)
|
|
3209
|
+
return 1
|
|
3116
3210
|
if prepare_only:
|
|
3117
3211
|
print("Prepared: feature is on its branch and committed. "
|
|
3118
3212
|
"Re-run `specfuse-loop` to start the gate.")
|
|
@@ -3998,6 +4092,7 @@ def auto_sync(
|
|
|
3998
4092
|
f"WARNING: auto_sync: plugin config drift corrected: {', '.join(changes)}",
|
|
3999
4093
|
file=sys.stderr,
|
|
4000
4094
|
)
|
|
4095
|
+
_persist_scaffold_sync(installed)
|
|
4001
4096
|
return
|
|
4002
4097
|
|
|
4003
4098
|
# Older with modified files.
|
|
@@ -4056,6 +4151,7 @@ def auto_sync(
|
|
|
4056
4151
|
f"WARNING: auto_sync: plugin config drift corrected: {', '.join(changes)}",
|
|
4057
4152
|
file=sys.stderr,
|
|
4058
4153
|
)
|
|
4154
|
+
_persist_scaffold_sync(installed)
|
|
4059
4155
|
else:
|
|
4060
4156
|
# Non-interactive (CI / claude -p): skip modified files + warn; never block.
|
|
4061
4157
|
print(
|
|
@@ -4092,6 +4188,7 @@ def auto_sync(
|
|
|
4092
4188
|
f"WARNING: auto_sync: plugin config drift corrected: {', '.join(changes)}",
|
|
4093
4189
|
file=sys.stderr,
|
|
4094
4190
|
)
|
|
4191
|
+
_persist_scaffold_sync(installed)
|
|
4095
4192
|
|
|
4096
4193
|
|
|
4097
4194
|
def main() -> int:
|
|
@@ -112,6 +112,7 @@ tests/test_miniyaml_equivalence.py
|
|
|
112
112
|
tests/test_miniyaml_negative.py
|
|
113
113
|
tests/test_planned_cost_lint.py
|
|
114
114
|
tests/test_prepare_feature.py
|
|
115
|
+
tests/test_prepare_scaffold_sync.py
|
|
115
116
|
tests/test_produces_field.py
|
|
116
117
|
tests/test_result_block.py
|
|
117
118
|
tests/test_roadmap_add_skill.py
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/docs/concepts/ralph-lineage.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/rules/security-boundaries.md
RENAMED
|
File without changes
|
{specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/specfuse/loop/data/rules/verification-discipline.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.11 → specfuse_loop-0.3.12}/tests/test_lint_correlation_id_close_intermediate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|