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.
- {specfuse_loop-0.3.0/specfuse_loop.egg-info → specfuse_loop-0.3.1}/PKG-INFO +23 -18
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/README.md +22 -17
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/pyproject.toml +1 -1
- specfuse_loop-0.3.1/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/getting-started.md +55 -22
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/skills.md +10 -8
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/roadmap.template.md +6 -5
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/verification.yml.example +2 -2
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/lint_plan.py +1 -1
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/loop.py +2 -2
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1/specfuse_loop.egg-info}/PKG-INFO +23 -18
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/SOURCES.txt +2 -0
- specfuse_loop-0.3.1/tests/test_scaffold_seed_sanity.py +80 -0
- specfuse_loop-0.3.1/tests/test_version_consistency.py +120 -0
- specfuse_loop-0.3.0/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/LICENSE +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/NOTICE +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/setup.cfg +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_backend.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/tests/test_verify_empty_gate_set.py +0 -0
- {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.
|
|
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** (
|
|
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
|
|
104
|
+
The driver installs from PyPI and the skills ship as a Claude Code plugin:
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
|
|
108
|
-
#
|
|
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
|
|
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
|
|
118
|
-
specfuse-loop --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
|
|
123
|
-
> `
|
|
124
|
-
>
|
|
125
|
-
>
|
|
126
|
-
>
|
|
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
|
|
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
|
|
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** (
|
|
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
|
|
75
|
+
The driver installs from PyPI and the skills ship as a Claude Code plugin:
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
|
|
79
|
-
#
|
|
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
|
|
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
|
|
89
|
-
specfuse-loop --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
|
|
94
|
-
> `
|
|
95
|
-
>
|
|
96
|
-
>
|
|
97
|
-
>
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
10
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
24
|
+
pipx install specfuse # recommended
|
|
25
|
+
# or, inside a virtualenv you control:
|
|
26
|
+
python3 -m pip install specfuse
|
|
21
27
|
```
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
30
|
-
|
|
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
|
|
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:**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
66
|
-
fresh session, verifies, and commits. It is a
|
|
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 `
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
|
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: "
|
|
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: "
|
|
53
|
+
command: "specfuse-lint {feature_dir}"
|
|
@@ -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.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
|
|
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.
|
|
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** (
|
|
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
|
|
104
|
+
The driver installs from PyPI and the skills ship as a Claude Code plugin:
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
|
|
108
|
-
#
|
|
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
|
|
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
|
|
118
|
-
specfuse-loop --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
|
|
123
|
-
> `
|
|
124
|
-
>
|
|
125
|
-
>
|
|
126
|
-
>
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.0 → specfuse_loop-0.3.1}/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
|
|
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.0 → specfuse_loop-0.3.1}/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
|