specfuse-loop 0.3.0__tar.gz → 0.3.1__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 (130) hide show
  1. {specfuse_loop-0.3.0/specfuse_loop.egg-info → specfuse_loop-0.3.1}/PKG-INFO +23 -18
  2. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/README.md +22 -17
  3. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/pyproject.toml +1 -1
  4. specfuse_loop-0.3.1/specfuse/loop/data/VERSION +1 -0
  5. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/getting-started.md +55 -22
  6. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/skills.md +10 -8
  7. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/roadmap.template.md +6 -5
  8. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/verification.yml.example +2 -2
  9. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/lint_plan.py +1 -1
  10. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/loop.py +2 -2
  11. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1/specfuse_loop.egg-info}/PKG-INFO +23 -18
  12. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/SOURCES.txt +2 -0
  13. specfuse_loop-0.3.1/tests/test_scaffold_seed_sanity.py +80 -0
  14. specfuse_loop-0.3.1/tests/test_version_consistency.py +120 -0
  15. specfuse_loop-0.3.0/specfuse/loop/data/VERSION +0 -1
  16. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/LICENSE +0 -0
  17. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/NOTICE +0 -0
  18. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/setup.cfg +0 -0
  19. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/__init__.py +0 -0
  20. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/_miniyaml.py +0 -0
  21. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/adopt_feature.py +0 -0
  22. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  23. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  24. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  25. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/methodology.md +0 -0
  26. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/gitignore.snippet +0 -0
  27. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  28. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/rules/never-touch.md +0 -0
  29. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/rules/result-contract.md +0 -0
  30. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  31. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/templates/GATE.template.md +0 -0
  32. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  33. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/templates/WU.template.md +0 -0
  34. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/gate_eval.py +0 -0
  35. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/gh_backend.py +0 -0
  36. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/gh_features.py +0 -0
  37. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/scaffold.py +0 -0
  38. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/validate_event.py +0 -0
  39. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  40. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/entry_points.txt +0 -0
  41. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/requires.txt +0 -0
  42. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/top_level.txt +0 -0
  43. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_adopt_feature.py +0 -0
  44. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  45. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_attempt_outcome_emission.py +0 -0
  46. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_autosync.py +0 -0
  47. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_autosync_consent.py +0 -0
  48. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_autosync_firstrun.py +0 -0
  49. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_autosync_plugin.py +0 -0
  50. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_backend.py +0 -0
  51. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  52. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  53. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_closing_deliverable_guard.py +0 -0
  54. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_cost_tracking.py +0 -0
  55. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_deliverable_presence_gate.py +0 -0
  56. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_doctor.py +0 -0
  57. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_driver_integration.py +0 -0
  58. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_driver_lock.py +0 -0
  59. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_duration_tracking.py +0 -0
  60. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_empty_files_escalation.py +0 -0
  61. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_ensure_feature_branch.py +0 -0
  62. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_extra_gates.py +0 -0
  63. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_force_full_close.py +0 -0
  64. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gate_eval.py +0 -0
  65. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gate_eval_calibration.py +0 -0
  66. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  67. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gate_eval_terminal_wiring.py +0 -0
  68. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gh_backend.py +0 -0
  69. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gh_features.py +0 -0
  70. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_git_env_isolation.py +0 -0
  71. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_hashed_denylist.py +0 -0
  72. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_hashed_denylist_ci.py +0 -0
  73. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_init_integration.py +0 -0
  74. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_leak_findings_redaction.py +0 -0
  75. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_leak_scan.py +0 -0
  76. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_leak_scan_content.py +0 -0
  77. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  78. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lifecycle_integration.py +0 -0
  79. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_bare_produces_path.py +0 -0
  80. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_close_intermediate.py +0 -0
  81. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_close_wu.py +0 -0
  82. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_correlation_id.py +0 -0
  83. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  84. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_oracle_env.py +0 -0
  85. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_plan_errors.py +0 -0
  86. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_plan_next_draft.py +0 -0
  87. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_produces_driver_helper.py +0 -0
  88. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_sections.py +0 -0
  89. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  90. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_auto_archive.py +0 -0
  91. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_caveman_preamble.py +0 -0
  92. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_close_intermediate.py +0 -0
  93. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_defaults_by_type.py +0 -0
  94. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_effort.py +0 -0
  95. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_failure_note_cap.py +0 -0
  96. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_files_changed_guard.py +0 -0
  97. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_gate_budget.py +0 -0
  98. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_model_alias.py +0 -0
  99. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_orchestration.py +0 -0
  100. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_post_pass_invariant.py +0 -0
  101. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_reset_preserving_events.py +0 -0
  102. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_smoke_runner.py +0 -0
  103. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_unsandboxed.py +0 -0
  104. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_zero_token_guard.py +0 -0
  105. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_migrate_legacy.py +0 -0
  106. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_miniyaml_equivalence.py +0 -0
  107. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_miniyaml_negative.py +0 -0
  108. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_planned_cost_lint.py +0 -0
  109. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_produces_field.py +0 -0
  110. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_result_block.py +0 -0
  111. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_roadmap_add_skill.py +0 -0
  112. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_roadmap_archive_skill.py +0 -0
  113. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_roadmap_row_parser.py +0 -0
  114. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_data_in_sync.py +0 -0
  115. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_docs.py +0 -0
  116. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_init.py +0 -0
  117. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_manifest.py +0 -0
  118. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_resources.py +0 -0
  119. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_upgrade.py +0 -0
  120. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_wiring.py +0 -0
  121. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_squash_commit_hook_crash.py +0 -0
  122. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_template_closing_shapes.py +0 -0
  123. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_terminal_flip_ownership.py +0 -0
  124. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_terminal_flips.py +0 -0
  125. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_untracked_feature_folder.py +0 -0
  126. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_upgrade_integration.py +0 -0
  127. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_validate_event.py +0 -0
  128. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_verdict_coupling.py +0 -0
  129. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_verify_empty_gate_set.py +0 -0
  130. {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_version_skew.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
@@ -77,8 +77,8 @@ the planning rigor Ralph's bare task list lacks.
77
77
  `.specfuse/scripts/adopt_feature.py <repo> <issue-number>` (or the
78
78
  interactive `/adopt-feature` skill) to scaffold a dispatchable feature
79
79
  folder from a picked issue.
80
- - The **driver** (`.specfuse/scripts/loop.py`) walks the current gate's ready
81
- work units, dispatches each as a fresh `claude -p` session, runs the unit's
80
+ - The **driver** (`specfuse-loop`, from the pip package) walks the current gate's
81
+ ready work units, dispatches each as a fresh `claude -p` session, runs the unit's
82
82
  verification itself as the exit oracle, and commits one squashed,
83
83
  trailer-carrying commit per unit. A failed gate is retried with a fresh
84
84
  session carrying the failure evidence, up to three attempts, then escalated.
@@ -101,29 +101,34 @@ In a target single-repo project:
101
101
  cloning to enable the pre-push hook (runs `scripts/smoke-test.sh` — same
102
102
  checks CI runs — before each `git push`). Bypass with `git push --no-verify`.
103
103
 
104
- The driver installs from PyPI and the skills from the Claude Code marketplace:
104
+ The driver installs from PyPI and the skills ship as a Claude Code plugin:
105
105
 
106
106
  ```bash
107
- pip install specfuse-loop # the driver: `specfuse-loop` on PATH
108
- # in Claude Code: skills under the /specfuse: namespace
107
+ pipx install specfuse # umbrella CLI; pulls specfuse-loop>=0.3.0
108
+ # gives you: specfuse, specfuse-loop, specfuse-lint (or: python3 -m pip install specfuse, in a venv)
109
+
110
+ # in Claude Code, enable the skills plugin (one-time):
109
111
  # /plugin marketplace add specfuse/specfuse
110
112
  # /plugin install specfuse@specfuse
111
113
 
112
- # scaffold a target repo's .specfuse/ state (templates, rules, verification.yml)
113
- ./init.sh /path/to/your-project # legacy installer (v1.0; removed in v1.1)
114
+ specfuse init /path/to/your-project # scaffold .specfuse/ + wire .claude/ (--dry-run previews)
114
115
 
115
116
  cd /path/to/your-project
116
117
  $EDITOR .specfuse/verification.yml # match the `code` gates to your stack
117
- # author your first feature folder under .specfuse/features/ from .specfuse/templates/
118
- specfuse-loop --dry-run # or: python .specfuse/scripts/loop.py --dry-run
119
- specfuse-loop
118
+ # author your first feature (in Claude Code: /draft-feature)
119
+ specfuse-loop --dry-run # show the gate walk, no dispatch
120
+ specfuse-loop # the real run
120
121
  ```
121
122
 
122
- > **Distribution (FEAT-2026-0019).** Code ships via pip (`specfuse-loop`), the
123
- > `specfuse` umbrella CLI bridges pip plugin (`specfuse upgrade`), and Claude
124
- > assets ship via the [`specfuse/specfuse`](https://github.com/specfuse/specfuse)
125
- > marketplace. `init.sh` remains the scaffold bootstrap (laying down `.specfuse/`
126
- > state) until pip-native scaffolding lands; it prints a deprecation banner.
123
+ > **Distribution.** Code ships via pip `specfuse` (umbrella CLI: `init` /
124
+ > `upgrade`) pulls `specfuse-loop` (the driver); Claude assets ship via the
125
+ > [`specfuse/specfuse`](https://github.com/specfuse/specfuse) plugin marketplace.
126
+ > `specfuse init` lays down `.specfuse/` and wires `.claude/`; `specfuse upgrade`
127
+ > overlays a newer scaffold and pip-upgrades both packages. Every `specfuse-loop`
128
+ > run self-provisions (version-syncs `.specfuse/` from the installed package), so
129
+ > an upgrade reaches existing projects on their next run. (`./init.sh` is a
130
+ > deprecated v1.0 shim that delegates to `specfuse init`/`upgrade`; slated for
131
+ > removal.)
127
132
 
128
133
  > **One driver per working tree.** The driver holds an exclusive advisory lock on
129
134
  > `.specfuse/.loop.lock` for the duration of a run; a second driver targeting the
@@ -145,7 +150,7 @@ python .specfuse/scripts/loop.py --dry-run
145
150
  ```
146
151
  specfuse-loop/
147
152
  ├── LICENSE NOTICE CONTRIBUTING.md README.md .gitignore
148
- ├── init.sh scaffold .specfuse/ into a target repo
153
+ ├── init.sh deprecated v1.0 shim delegates to `specfuse init`/`upgrade`
149
154
  ├── docs/
150
155
  │ ├── getting-started.md narrated first-feature + operator walkthrough
151
156
  │ ├── methodology.md the gate-cycle contract (shared with the orchestrator)
@@ -164,7 +169,7 @@ specfuse-loop/
164
169
  └── features/FEAT-2026-0001-health-endpoint/ (the worked example)
165
170
  ```
166
171
 
167
- `init.sh` also ships the durable docs — `methodology.md`, `skills.md`, and
172
+ `specfuse init` also ships the durable docs — `methodology.md`, `skills.md`, and
168
173
  `concepts/` — into a target's `.specfuse/docs/`, so an initialized repo is
169
174
  self-documenting without this checkout.
170
175
 
@@ -48,8 +48,8 @@ the planning rigor Ralph's bare task list lacks.
48
48
  `.specfuse/scripts/adopt_feature.py <repo> <issue-number>` (or the
49
49
  interactive `/adopt-feature` skill) to scaffold a dispatchable feature
50
50
  folder from a picked issue.
51
- - The **driver** (`.specfuse/scripts/loop.py`) walks the current gate's ready
52
- work units, dispatches each as a fresh `claude -p` session, runs the unit's
51
+ - The **driver** (`specfuse-loop`, from the pip package) walks the current gate's
52
+ ready work units, dispatches each as a fresh `claude -p` session, runs the unit's
53
53
  verification itself as the exit oracle, and commits one squashed,
54
54
  trailer-carrying commit per unit. A failed gate is retried with a fresh
55
55
  session carrying the failure evidence, up to three attempts, then escalated.
@@ -72,29 +72,34 @@ In a target single-repo project:
72
72
  cloning to enable the pre-push hook (runs `scripts/smoke-test.sh` — same
73
73
  checks CI runs — before each `git push`). Bypass with `git push --no-verify`.
74
74
 
75
- The driver installs from PyPI and the skills from the Claude Code marketplace:
75
+ The driver installs from PyPI and the skills ship as a Claude Code plugin:
76
76
 
77
77
  ```bash
78
- pip install specfuse-loop # the driver: `specfuse-loop` on PATH
79
- # in Claude Code: skills under the /specfuse: namespace
78
+ pipx install specfuse # umbrella CLI; pulls specfuse-loop>=0.3.0
79
+ # gives you: specfuse, specfuse-loop, specfuse-lint (or: python3 -m pip install specfuse, in a venv)
80
+
81
+ # in Claude Code, enable the skills plugin (one-time):
80
82
  # /plugin marketplace add specfuse/specfuse
81
83
  # /plugin install specfuse@specfuse
82
84
 
83
- # scaffold a target repo's .specfuse/ state (templates, rules, verification.yml)
84
- ./init.sh /path/to/your-project # legacy installer (v1.0; removed in v1.1)
85
+ specfuse init /path/to/your-project # scaffold .specfuse/ + wire .claude/ (--dry-run previews)
85
86
 
86
87
  cd /path/to/your-project
87
88
  $EDITOR .specfuse/verification.yml # match the `code` gates to your stack
88
- # author your first feature folder under .specfuse/features/ from .specfuse/templates/
89
- specfuse-loop --dry-run # or: python .specfuse/scripts/loop.py --dry-run
90
- specfuse-loop
89
+ # author your first feature (in Claude Code: /draft-feature)
90
+ specfuse-loop --dry-run # show the gate walk, no dispatch
91
+ specfuse-loop # the real run
91
92
  ```
92
93
 
93
- > **Distribution (FEAT-2026-0019).** Code ships via pip (`specfuse-loop`), the
94
- > `specfuse` umbrella CLI bridges pip plugin (`specfuse upgrade`), and Claude
95
- > assets ship via the [`specfuse/specfuse`](https://github.com/specfuse/specfuse)
96
- > marketplace. `init.sh` remains the scaffold bootstrap (laying down `.specfuse/`
97
- > state) until pip-native scaffolding lands; it prints a deprecation banner.
94
+ > **Distribution.** Code ships via pip `specfuse` (umbrella CLI: `init` /
95
+ > `upgrade`) pulls `specfuse-loop` (the driver); Claude assets ship via the
96
+ > [`specfuse/specfuse`](https://github.com/specfuse/specfuse) plugin marketplace.
97
+ > `specfuse init` lays down `.specfuse/` and wires `.claude/`; `specfuse upgrade`
98
+ > overlays a newer scaffold and pip-upgrades both packages. Every `specfuse-loop`
99
+ > run self-provisions (version-syncs `.specfuse/` from the installed package), so
100
+ > an upgrade reaches existing projects on their next run. (`./init.sh` is a
101
+ > deprecated v1.0 shim that delegates to `specfuse init`/`upgrade`; slated for
102
+ > removal.)
98
103
 
99
104
  > **One driver per working tree.** The driver holds an exclusive advisory lock on
100
105
  > `.specfuse/.loop.lock` for the duration of a run; a second driver targeting the
@@ -116,7 +121,7 @@ python .specfuse/scripts/loop.py --dry-run
116
121
  ```
117
122
  specfuse-loop/
118
123
  ├── LICENSE NOTICE CONTRIBUTING.md README.md .gitignore
119
- ├── init.sh scaffold .specfuse/ into a target repo
124
+ ├── init.sh deprecated v1.0 shim delegates to `specfuse init`/`upgrade`
120
125
  ├── docs/
121
126
  │ ├── getting-started.md narrated first-feature + operator walkthrough
122
127
  │ ├── methodology.md the gate-cycle contract (shared with the orchestrator)
@@ -135,7 +140,7 @@ specfuse-loop/
135
140
  └── features/FEAT-2026-0001-health-endpoint/ (the worked example)
136
141
  ```
137
142
 
138
- `init.sh` also ships the durable docs — `methodology.md`, `skills.md`, and
143
+ `specfuse init` also ships the durable docs — `methodology.md`, `skills.md`, and
139
144
  `concepts/` — into a target's `.specfuse/docs/`, so an initialized repo is
140
145
  self-documenting without this checkout.
141
146
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specfuse-loop"
7
- version = "0.3.0"
7
+ version = "0.3.1"
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.1
@@ -6,33 +6,65 @@ the [README](../README.md); for the full contracts see
6
6
  [`methodology.md`](methodology.md) and for the interactive operations see
7
7
  [`skills.md`](skills.md).
8
8
 
9
- The loop is stdlib-only Python plus Claude Code. There is no install step in your
10
- target repo `init.sh` copies a self-contained scaffold in.
9
+ The driver is pure-stdlib Python; it installs from PyPI, and the interactive
10
+ skills ship as a Claude Code plugin. You install the tooling once, then scaffold
11
+ each project you want to drive with one command.
11
12
 
12
13
  ---
13
14
 
14
- ## 1. Install the scaffold
15
+ ## 1. Install the tooling and scaffold your project
15
16
 
16
- From your checkout of `specfuse/loop`, point `init.sh` at the repo you want to
17
- drive:
17
+ Install the umbrella package it pulls the driver (`specfuse-loop>=0.3.0`) as a
18
+ dependency and puts the `specfuse`, `specfuse-loop`, and `specfuse-lint` commands
19
+ on your PATH. It's a command-line app, so **pipx** is the recommended installer
20
+ (isolated environment, no `--break-system-packages` on PEP 668 / externally-
21
+ managed Pythons):
18
22
 
19
23
  ```bash
20
- ./init.sh /path/to/your-project
24
+ pipx install specfuse # recommended
25
+ # or, inside a virtualenv you control:
26
+ python3 -m pip install specfuse
21
27
  ```
22
28
 
23
- This writes `.specfuse/` (templates, rules, skills, the driver, and the durable
24
- docs) into your project and wires `.claude/` so Claude Code discovers the skills.
25
- It refuses if `.specfuse/` already exists use `./init.sh --upgrade` to update an
26
- existing install in place without touching your authored files.
29
+ > On Debian/Ubuntu/macOS-Homebrew Pythons a bare `pip install` into the system
30
+ > interpreter is blocked (`externally-managed-environment`). Use `pipx` (or a
31
+ > venv) that's what puts `specfuse-loop` / `specfuse-lint` on PATH for the gate
32
+ > commands to find.
33
+
34
+ Enable the skills plugin in Claude Code (one-time):
35
+
36
+ ```
37
+ /plugin marketplace add specfuse/specfuse
38
+ /plugin install specfuse@specfuse
39
+ ```
40
+
41
+ Then scaffold the repo you want to drive:
42
+
43
+ ```bash
44
+ specfuse init /path/to/your-project # add --dry-run to preview, writes nothing
45
+ ```
46
+
47
+ This writes `.specfuse/` (templates, rules, the durable docs, `verification.yml`,
48
+ and an empty `features/`) into your project and merge-safely wires `.claude/`
49
+ (`CLAUDE.md`, `settings.json` enabling the `specfuse@specfuse` plugin) plus a
50
+ `.gitignore` snippet. It refuses if `.specfuse/` already exists — use
51
+ `specfuse upgrade /path/to/your-project` to overlay a newer scaffold in place
52
+ without touching your authored files. (The skills come from the plugin, not from
53
+ files copied into your repo.)
27
54
 
28
55
  > **Don't gitignore `.specfuse/`.** The loop's durable state lives there and must
29
- > be committed for the loop to work. `init.sh` warns if it detects the directory
30
- > is ignored.
56
+ > be committed for the loop to work.
57
+
58
+ > **Self-provisioning.** Every `specfuse-loop` run first version-syncs `.specfuse/`
59
+ > from the installed package (missing → scaffold, older → overlay, equal → no-op,
60
+ > never downgrades). So `pip install -U specfuse` followed by a run keeps the
61
+ > scaffold current — `specfuse upgrade` is the explicit equivalent. Disable with
62
+ > `--no-autosync` or `autosync: false` in `.specfuse/config`.
31
63
 
32
64
  ## 2. Match verification to your stack
33
65
 
34
- `init.sh` seeds `.specfuse/verification.yml`. Open it and make the `code` gate set
35
- run *your* project's checks:
66
+ `specfuse init` seeds `.specfuse/verification.yml`. Open it and make the `code`
67
+ gate set run *your* project's checks:
36
68
 
37
69
  ```yaml
38
70
  code:
@@ -63,10 +95,11 @@ Two ways to create a feature folder under `.specfuse/features/`:
63
95
  - **Interactively (recommended):** run **`/pick-feature`** to choose from your
64
96
  roadmap, then **`/draft-feature`**. Draft-feature asks framing questions, then
65
97
  proposes a gate skeleton and gate 1's work units, writing only on your accept.
66
- - **By hand:** copy the worked example,
67
- `.specfuse/features/FEAT-2026-0001-health-endpoint/`, and adapt it. It's a
68
- deliberately small two-unit feature that exercises the whole loop. Or start from
69
- the bare templates in `.specfuse/templates/`.
98
+ - **By hand:** start from the bare templates in `.specfuse/templates/`
99
+ (`PLAN`, `GATE`, `WU`) and fill in a small first feature. (The
100
+ `specfuse/loop` source repo also carries a worked example,
101
+ `FEAT-2026-0001-health-endpoint`, if you want a complete reference to copy
102
+ from.)
70
103
 
71
104
  A feature folder holds:
72
105
 
@@ -81,7 +114,7 @@ Then create the branch named in `PLAN.md`'s frontmatter (`branch:`).
81
114
  ## 4. Validate before running
82
115
 
83
116
  ```bash
84
- python .specfuse/scripts/lint_plan.py .specfuse/features/FEAT-2026-0001-health-endpoint
117
+ specfuse-lint .specfuse/features/FEAT-YYYY-NNNN-your-feature
85
118
  ```
86
119
 
87
120
  The linter checks structure: every WU has the five mandatory sections, the closing
@@ -92,8 +125,8 @@ dispatch.
92
125
  ## 5. Dry-run, then run
93
126
 
94
127
  ```bash
95
- python .specfuse/scripts/loop.py --dry-run # show the gate walked, in dep order, no dispatch
96
- python .specfuse/scripts/loop.py # the real thing
128
+ specfuse-loop --dry-run # show the gate walked, in dep order, no dispatch
129
+ specfuse-loop # the real thing
97
130
  ```
98
131
 
99
132
  With no `--feature` flag the driver picks the single `active` feature. For each
@@ -136,7 +169,7 @@ the ones you accept to `pending`, marks the finished gate `passed`, and prints t
136
169
  resume command. Read the `GATE-NN-REVIEW.md` the planner wrote first: it's
137
170
  weighted toward where the planner was *least* certain.
138
171
 
139
- Then re-run `loop.py`. Repeat until the terminal gate is `done`.
172
+ Then re-run `specfuse-loop`. Repeat until the terminal gate is `done`.
140
173
 
141
174
  ## 7. Wrap up
142
175
 
@@ -10,9 +10,11 @@ to do, and writes only on your explicit go-ahead. None of them dispatch agent
10
10
  sessions or run the loop — they manipulate the durable files (`roadmap.md`,
11
11
  `PLAN.md`, `GATE-NN.md`, `WU-*.md`, `LEARNINGS.md`) that the driver then acts on.
12
12
 
13
- All skills below ship to a target repo via `init.sh` and appear under
14
- `.specfuse/skills/` (symlinked into `.claude/skills/` so Claude Code discovers
15
- them).
13
+ All skills below ship as the `specfuse@specfuse` Claude Code plugin (enable once
14
+ with `/plugin marketplace add specfuse/specfuse` then `/plugin install
15
+ specfuse@specfuse`; `specfuse init` wires the plugin into the repo's
16
+ `.claude/settings.json`). They appear under the `/specfuse:` namespace — no files
17
+ are copied into your repo.
16
18
 
17
19
  ## The lifecycle, in order
18
20
 
@@ -21,7 +23,7 @@ A feature moves through these phases. The skill for each phase is named.
21
23
  ```
22
24
  roadmap ──/pick-feature──▶ active ──/draft-feature──▶ gate 1 detailed
23
25
 
24
- python loop.py
26
+ specfuse-loop
25
27
 
26
28
  ┌────────────────────────┴───────────────┐
27
29
  ▼ ▼
@@ -62,9 +64,9 @@ roadmap ──/pick-feature──▶ active ──/draft-feature──▶ gate 1
62
64
 
63
65
  ### 3. Run — the driver (not a skill)
64
66
 
65
- `python .specfuse/scripts/loop.py` walks the active gate, dispatches each WU as a
66
- fresh session, verifies, and commits. It is a script, not a skill. It either
67
- auto-closes a clean gate or halts at the gate boundary for review.
67
+ `specfuse-loop` (the pip-installed driver) walks the active gate, dispatches each
68
+ WU as a fresh session, verifies, and commits. It is a command, not a skill. It
69
+ either auto-closes a clean gate or halts at the gate boundary for review.
68
70
 
69
71
  ### 4. Arm — the human checkpoint at each gate
70
72
 
@@ -106,7 +108,7 @@ auto-closes a clean gate or halts at the gate boundary for review.
106
108
  methodology: 1 bug = 1 branch = 1 PR, test-first. Refuses and proposes
107
109
  promoting to a feature if the work is large or risky.
108
110
  - **`/feature-conversion`** — bring an existing feature folder into conformance
109
- with the current scaffold's structural contract. Runs after `init.sh --upgrade`
111
+ with the current scaffold's structural contract. Runs after `specfuse upgrade`
110
112
  flags a feature as `FAIL`. Interactive, lint-driven.
111
113
  - **`/learnings-suggest`** — scan `attempt_outcome` events across features,
112
114
  cluster non-passing attempts, and surface recurring patterns as candidate
@@ -15,11 +15,12 @@ Detail a feature's first-gate work units when you are ready to start it; the gat
15
15
  after that is drafted for you by the prior gate's plan-next. Until you start a
16
16
  feature, a one-line entry here is enough.
17
17
 
18
- | Feature ID | Title | Status | Folder |
19
- |----------------|----------------------|----------|--------|
20
- | FEAT-2026-0001 | Health-check endpoint| active | `features/FEAT-2026-0001-health-endpoint/` |
21
- | FEAT-2026-0002 | <title> | planned | — |
22
- | FEAT-2026-0003 | <title> | planned | |
18
+ Add your first feature with **`/roadmap-add`** (it auto-picks the next
19
+ `FEAT-YYYY-NNNN` ID and writes the row + detail section), or add a row by hand in
20
+ the canonical column order below.
21
+
22
+ | Feature ID | Title | Status | Folder | Detail |
23
+ |----------------|-------|--------|--------|--------|
23
24
 
24
25
  Status: `planned` → `active` → `done` (or `abandoned`).
25
26
 
@@ -34,7 +34,7 @@ code:
34
34
  - name: coverage
35
35
  command: "coverage report --fail-under=90"
36
36
  - name: warnings
37
- command: "python -W error -c 'pass'" # replace with your build's -Werror equivalent
37
+ command: "python3 -W error -c 'pass'" # replace with your build's -Werror equivalent
38
38
  - name: lint
39
39
  command: "ruff check ."
40
40
  - name: security
@@ -50,4 +50,4 @@ doc:
50
50
  # a malformed next-gate draft fails here, where you are already reviewing.
51
51
  plannext:
52
52
  - name: plan-lint
53
- command: "python .specfuse/scripts/lint_plan.py {feature_dir}"
53
+ command: "specfuse-lint {feature_dir}"
@@ -23,7 +23,7 @@ Two jobs:
23
23
 
24
24
  Exit 0 = clean, 1 = problems (printed).
25
25
 
26
- Usage: python .specfuse/scripts/lint_plan.py .specfuse/features/FEAT-XXXX-slug
26
+ Usage: specfuse-lint .specfuse/features/FEAT-XXXX-slug
27
27
  """
28
28
 
29
29
  from __future__ import annotations
@@ -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.0"
65
+ DRIVER_VERSION = "0.3.1"
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
@@ -3666,7 +3666,7 @@ def run(
3666
3666
  f"flip accepted WUs to `pending`,\n"
3667
3667
  f" mark this gate `passed`. "
3668
3668
  f"Reads {review.name} for planner findings.\n"
3669
- f" - Resume python3 .specfuse/scripts/loop.py"
3669
+ f" - Resume specfuse-loop"
3670
3670
  )
3671
3671
  return 0
3672
3672
  except BookkeepingCommitError as exc:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
@@ -77,8 +77,8 @@ the planning rigor Ralph's bare task list lacks.
77
77
  `.specfuse/scripts/adopt_feature.py <repo> <issue-number>` (or the
78
78
  interactive `/adopt-feature` skill) to scaffold a dispatchable feature
79
79
  folder from a picked issue.
80
- - The **driver** (`.specfuse/scripts/loop.py`) walks the current gate's ready
81
- work units, dispatches each as a fresh `claude -p` session, runs the unit's
80
+ - The **driver** (`specfuse-loop`, from the pip package) walks the current gate's
81
+ ready work units, dispatches each as a fresh `claude -p` session, runs the unit's
82
82
  verification itself as the exit oracle, and commits one squashed,
83
83
  trailer-carrying commit per unit. A failed gate is retried with a fresh
84
84
  session carrying the failure evidence, up to three attempts, then escalated.
@@ -101,29 +101,34 @@ In a target single-repo project:
101
101
  cloning to enable the pre-push hook (runs `scripts/smoke-test.sh` — same
102
102
  checks CI runs — before each `git push`). Bypass with `git push --no-verify`.
103
103
 
104
- The driver installs from PyPI and the skills from the Claude Code marketplace:
104
+ The driver installs from PyPI and the skills ship as a Claude Code plugin:
105
105
 
106
106
  ```bash
107
- pip install specfuse-loop # the driver: `specfuse-loop` on PATH
108
- # in Claude Code: skills under the /specfuse: namespace
107
+ pipx install specfuse # umbrella CLI; pulls specfuse-loop>=0.3.0
108
+ # gives you: specfuse, specfuse-loop, specfuse-lint (or: python3 -m pip install specfuse, in a venv)
109
+
110
+ # in Claude Code, enable the skills plugin (one-time):
109
111
  # /plugin marketplace add specfuse/specfuse
110
112
  # /plugin install specfuse@specfuse
111
113
 
112
- # scaffold a target repo's .specfuse/ state (templates, rules, verification.yml)
113
- ./init.sh /path/to/your-project # legacy installer (v1.0; removed in v1.1)
114
+ specfuse init /path/to/your-project # scaffold .specfuse/ + wire .claude/ (--dry-run previews)
114
115
 
115
116
  cd /path/to/your-project
116
117
  $EDITOR .specfuse/verification.yml # match the `code` gates to your stack
117
- # author your first feature folder under .specfuse/features/ from .specfuse/templates/
118
- specfuse-loop --dry-run # or: python .specfuse/scripts/loop.py --dry-run
119
- specfuse-loop
118
+ # author your first feature (in Claude Code: /draft-feature)
119
+ specfuse-loop --dry-run # show the gate walk, no dispatch
120
+ specfuse-loop # the real run
120
121
  ```
121
122
 
122
- > **Distribution (FEAT-2026-0019).** Code ships via pip (`specfuse-loop`), the
123
- > `specfuse` umbrella CLI bridges pip plugin (`specfuse upgrade`), and Claude
124
- > assets ship via the [`specfuse/specfuse`](https://github.com/specfuse/specfuse)
125
- > marketplace. `init.sh` remains the scaffold bootstrap (laying down `.specfuse/`
126
- > state) until pip-native scaffolding lands; it prints a deprecation banner.
123
+ > **Distribution.** Code ships via pip `specfuse` (umbrella CLI: `init` /
124
+ > `upgrade`) pulls `specfuse-loop` (the driver); Claude assets ship via the
125
+ > [`specfuse/specfuse`](https://github.com/specfuse/specfuse) plugin marketplace.
126
+ > `specfuse init` lays down `.specfuse/` and wires `.claude/`; `specfuse upgrade`
127
+ > overlays a newer scaffold and pip-upgrades both packages. Every `specfuse-loop`
128
+ > run self-provisions (version-syncs `.specfuse/` from the installed package), so
129
+ > an upgrade reaches existing projects on their next run. (`./init.sh` is a
130
+ > deprecated v1.0 shim that delegates to `specfuse init`/`upgrade`; slated for
131
+ > removal.)
127
132
 
128
133
  > **One driver per working tree.** The driver holds an exclusive advisory lock on
129
134
  > `.specfuse/.loop.lock` for the duration of a run; a second driver targeting the
@@ -145,7 +150,7 @@ python .specfuse/scripts/loop.py --dry-run
145
150
  ```
146
151
  specfuse-loop/
147
152
  ├── LICENSE NOTICE CONTRIBUTING.md README.md .gitignore
148
- ├── init.sh scaffold .specfuse/ into a target repo
153
+ ├── init.sh deprecated v1.0 shim delegates to `specfuse init`/`upgrade`
149
154
  ├── docs/
150
155
  │ ├── getting-started.md narrated first-feature + operator walkthrough
151
156
  │ ├── methodology.md the gate-cycle contract (shared with the orchestrator)
@@ -164,7 +169,7 @@ specfuse-loop/
164
169
  └── features/FEAT-2026-0001-health-endpoint/ (the worked example)
165
170
  ```
166
171
 
167
- `init.sh` also ships the durable docs — `methodology.md`, `skills.md`, and
172
+ `specfuse init` also ships the durable docs — `methodology.md`, `skills.md`, and
168
173
  `concepts/` — into a target's `.specfuse/docs/`, so an initialized repo is
169
174
  self-documenting without this checkout.
170
175
 
@@ -111,6 +111,7 @@ tests/test_scaffold_docs.py
111
111
  tests/test_scaffold_init.py
112
112
  tests/test_scaffold_manifest.py
113
113
  tests/test_scaffold_resources.py
114
+ tests/test_scaffold_seed_sanity.py
114
115
  tests/test_scaffold_upgrade.py
115
116
  tests/test_scaffold_wiring.py
116
117
  tests/test_squash_commit_hook_crash.py
@@ -122,4 +123,5 @@ tests/test_upgrade_integration.py
122
123
  tests/test_validate_event.py
123
124
  tests/test_verdict_coupling.py
124
125
  tests/test_verify_empty_gate_set.py
126
+ tests/test_version_consistency.py
125
127
  tests/test_version_skew.py
@@ -0,0 +1,80 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """Sanity guards on the scaffold seed a fresh `specfuse init` writes.
6
+
7
+ These lock the fixes for the brand-new-project install bugs:
8
+ - the shipped verification.yml gate commands must be runnable on a
9
+ pip-installed project (console scripts, not `python .specfuse/scripts/...`
10
+ which does not exist there);
11
+ - the roadmap template must carry the 5-column header `roadmap-add` requires
12
+ (Detail column), and must NOT pre-populate demo features.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ import unittest
19
+ from pathlib import Path
20
+
21
+ REPO_ROOT = Path(__file__).resolve().parent.parent
22
+ DATA = REPO_ROOT / "specfuse" / "loop" / "data"
23
+
24
+
25
+ class TestVerificationSeed(unittest.TestCase):
26
+
27
+ def test_gate_commands_reference_no_missing_scripts(self):
28
+ """No shipped gate command may invoke `.specfuse/scripts/...` — that path
29
+ is absent in a pip-installed project; use the console scripts."""
30
+ text = (DATA / "verification.yml.example").read_text(encoding="utf-8")
31
+ offenders = [
32
+ ln.strip() for ln in text.splitlines()
33
+ if "command:" in ln and ".specfuse/scripts/" in ln
34
+ ]
35
+ self.assertEqual(
36
+ offenders, [],
37
+ f"gate commands must not call .specfuse/scripts/* (absent under pip); "
38
+ f"use specfuse-lint / specfuse-loop. Offenders: {offenders}",
39
+ )
40
+
41
+ def test_plan_lint_uses_console_script(self):
42
+ text = (DATA / "verification.yml.example").read_text(encoding="utf-8")
43
+ self.assertIn("specfuse-lint", text,
44
+ "the plan-next lint gate should call `specfuse-lint`")
45
+
46
+
47
+ class TestRoadmapSeed(unittest.TestCase):
48
+
49
+ def _table_header(self) -> str:
50
+ text = (DATA / "roadmap.template.md").read_text(encoding="utf-8")
51
+ for ln in text.splitlines():
52
+ if ln.startswith("| Feature ID"):
53
+ return ln
54
+ self.fail("roadmap.template.md has no `| Feature ID` table header")
55
+
56
+ def test_header_has_detail_column(self):
57
+ """roadmap-add requires the 5-column order incl. Detail."""
58
+ cols = [c.strip() for c in self._table_header().strip("|").split("|")]
59
+ self.assertEqual(
60
+ cols, ["Feature ID", "Title", "Status", "Folder", "Detail"],
61
+ f"roadmap template header must match roadmap-add's expected columns; "
62
+ f"got {cols}",
63
+ )
64
+
65
+ def test_no_demo_feature_rows(self):
66
+ """A fresh project's roadmap must not ship example features."""
67
+ text = (DATA / "roadmap.template.md").read_text(encoding="utf-8")
68
+ feat_rows = [
69
+ ln for ln in text.splitlines()
70
+ if re.match(r"\|\s*FEAT-\d{4}-\d{4}", ln)
71
+ ]
72
+ self.assertEqual(
73
+ feat_rows, [],
74
+ f"roadmap template must start with an empty table; demo rows found: "
75
+ f"{feat_rows}",
76
+ )
77
+
78
+
79
+ if __name__ == "__main__":
80
+ unittest.main()
@@ -0,0 +1,120 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """The four package-version sources must agree — enforced every CI run.
6
+
7
+ specfuse-loop's version lives in four places (pyproject, DRIVER_VERSION, the
8
+ canonical .specfuse/VERSION, and the synced specfuse/loop/data/VERSION seed).
9
+ The release.yml tag/version-agreement check covers pyproject + DRIVER_VERSION
10
+ but (a) omits the two scaffold VERSION files and (b) only runs at TAG time. A
11
+ half-bump then sits undetected until release. This test closes both gaps: it
12
+ runs on every PR and asserts all four are equal, plus MIN_SCAFFOLD_VERSION is
13
+ not ahead of the driver. It also exercises scripts/bump_version.py, the helper
14
+ that sets all four in lockstep.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import importlib.util
20
+ import re
21
+ import tempfile
22
+ import unittest
23
+ from pathlib import Path
24
+
25
+ from tests._loop_loader import load_loop
26
+
27
+ loop = load_loop()
28
+ REPO_ROOT = Path(__file__).resolve().parent.parent
29
+
30
+
31
+ def _pyproject_version(root: Path) -> str:
32
+ text = (root / "pyproject.toml").read_text(encoding="utf-8")
33
+ m = re.search(r'(?m)^version\s*=\s*"([^"]+)"', text)
34
+ assert m, "no version line in pyproject.toml"
35
+ return m.group(1)
36
+
37
+
38
+ def _load_bump():
39
+ path = REPO_ROOT / "scripts" / "bump_version.py"
40
+ spec = importlib.util.spec_from_file_location("bump_version", path)
41
+ mod = importlib.util.module_from_spec(spec)
42
+ spec.loader.exec_module(mod)
43
+ return mod
44
+
45
+
46
+ bump_version = _load_bump()
47
+
48
+
49
+ class TestVersionConsistency(unittest.TestCase):
50
+
51
+ def test_all_four_sources_agree(self):
52
+ pkg = _pyproject_version(REPO_ROOT)
53
+ driver = loop.DRIVER_VERSION
54
+ specfuse_ver = (REPO_ROOT / ".specfuse" / "VERSION").read_text().strip()
55
+ data_ver = (REPO_ROOT / "specfuse" / "loop" / "data" / "VERSION").read_text().strip()
56
+ self.assertEqual(
57
+ {pkg, driver, specfuse_ver, data_ver}, {pkg},
58
+ f"version sources disagree: pyproject={pkg} DRIVER_VERSION={driver} "
59
+ f".specfuse/VERSION={specfuse_ver} data/VERSION={data_ver}. "
60
+ f"Run `python3 scripts/bump_version.py {pkg}` to re-sync.",
61
+ )
62
+
63
+ def test_min_scaffold_not_ahead_of_driver(self):
64
+ self.assertLessEqual(
65
+ loop._parse_version(loop.MIN_SCAFFOLD_VERSION),
66
+ loop._parse_version(loop.DRIVER_VERSION),
67
+ "MIN_SCAFFOLD_VERSION must not exceed DRIVER_VERSION",
68
+ )
69
+
70
+
71
+ class TestBumpVersionHelper(unittest.TestCase):
72
+
73
+ def _make_tree(self, root: Path, version: str) -> None:
74
+ (root / "pyproject.toml").write_text(
75
+ f'[project]\nname = "specfuse-loop"\nversion = "{version}"\n',
76
+ encoding="utf-8",
77
+ )
78
+ loop_dir = root / "specfuse" / "loop"
79
+ (loop_dir / "data").mkdir(parents=True)
80
+ (loop_dir / "loop.py").write_text(
81
+ f'DRIVER_VERSION = "{version}"\nMIN_SCAFFOLD_VERSION = "0.2.0"\n',
82
+ encoding="utf-8",
83
+ )
84
+ (root / ".specfuse").mkdir()
85
+ (root / ".specfuse" / "VERSION").write_text(version + "\n", encoding="utf-8")
86
+ (loop_dir / "data" / "VERSION").write_text(version + "\n", encoding="utf-8")
87
+
88
+ def test_set_version_updates_all_four(self):
89
+ with tempfile.TemporaryDirectory() as tmp:
90
+ root = Path(tmp)
91
+ self._make_tree(root, "0.2.0")
92
+ changed = bump_version.set_version(root, "0.9.0")
93
+ self.assertEqual(
94
+ set(changed),
95
+ {"pyproject.toml", "specfuse/loop/loop.py",
96
+ ".specfuse/VERSION", "specfuse/loop/data/VERSION"},
97
+ )
98
+ self.assertIn('version = "0.9.0"', (root / "pyproject.toml").read_text())
99
+ self.assertIn('DRIVER_VERSION = "0.9.0"',
100
+ (root / "specfuse/loop/loop.py").read_text())
101
+ self.assertEqual((root / ".specfuse/VERSION").read_text().strip(), "0.9.0")
102
+ self.assertEqual(
103
+ (root / "specfuse/loop/data/VERSION").read_text().strip(), "0.9.0")
104
+ # MIN_SCAFFOLD_VERSION must be left untouched.
105
+ self.assertIn('MIN_SCAFFOLD_VERSION = "0.2.0"',
106
+ (root / "specfuse/loop/loop.py").read_text())
107
+
108
+ def test_set_version_idempotent(self):
109
+ with tempfile.TemporaryDirectory() as tmp:
110
+ root = Path(tmp)
111
+ self._make_tree(root, "0.5.0")
112
+ self.assertEqual(bump_version.set_version(root, "0.5.0"), [])
113
+
114
+ def test_main_rejects_bad_version(self):
115
+ self.assertEqual(bump_version.main(["not-a-version"]), 2)
116
+ self.assertEqual(bump_version.main([]), 2)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ unittest.main()
@@ -1 +0,0 @@
1
- 0.3.0
File without changes
File without changes
File without changes