specfuse-loop 0.2.0__py3-none-any.whl
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/__init__.py +5 -0
- specfuse/loop/_miniyaml.py +466 -0
- specfuse/loop/adopt_feature.py +217 -0
- specfuse/loop/gate_eval.py +503 -0
- specfuse/loop/gh_backend.py +82 -0
- specfuse/loop/gh_features.py +98 -0
- specfuse/loop/lint_plan.py +616 -0
- specfuse/loop/loop.py +3504 -0
- specfuse/loop/validate_event.py +282 -0
- specfuse_loop-0.2.0.dist-info/METADATA +192 -0
- specfuse_loop-0.2.0.dist-info/RECORD +16 -0
- specfuse_loop-0.2.0.dist-info/WHEEL +5 -0
- specfuse_loop-0.2.0.dist-info/entry_points.txt +3 -0
- specfuse_loop-0.2.0.dist-info/licenses/LICENSE +201 -0
- specfuse_loop-0.2.0.dist-info/licenses/NOTICE +6 -0
- specfuse_loop-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Specfuse contributors
|
|
4
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
5
|
+
#
|
|
6
|
+
"""Validate component-repo event log entries against the orchestrator's event schema.
|
|
7
|
+
|
|
8
|
+
This is the component-repo copy of the orchestrator's scripts/validate-event.py.
|
|
9
|
+
Per binding rules (rules/never-touch.md), schema files are NOT duplicated into
|
|
10
|
+
component repos. Instead, this script resolves the schema root from the
|
|
11
|
+
orchestrator at runtime.
|
|
12
|
+
|
|
13
|
+
Schema-root resolution (first match wins):
|
|
14
|
+
1. SPECFUSE_SCHEMA_ROOT — absolute path to orchestrator/shared/schemas/
|
|
15
|
+
2. Relative sibling-repo fallback: this file at .specfuse/scripts/ →
|
|
16
|
+
<repo>/.specfuse/ → <repo> → <parent> → <parent>/orchestrator/shared/schemas/.
|
|
17
|
+
Works when the orchestrator and component repos sit as siblings under
|
|
18
|
+
the same parent directory (the standard local-dev layout).
|
|
19
|
+
|
|
20
|
+
Supported invocation patterns (only two):
|
|
21
|
+
|
|
22
|
+
# Stdin — pipe a single event or a JSONL file:
|
|
23
|
+
echo '{"timestamp": "...", ...}' | python3 .specfuse/scripts/validate-event.py
|
|
24
|
+
cat events/FEAT-2026-0001.jsonl | python3 .specfuse/scripts/validate-event.py
|
|
25
|
+
python3 .specfuse/scripts/validate-event.py --stdin # explicit alias
|
|
26
|
+
|
|
27
|
+
# File — pass a path to a .jsonl file:
|
|
28
|
+
python3 .specfuse/scripts/validate-event.py --file events/FEAT-2026-0001.jsonl
|
|
29
|
+
|
|
30
|
+
Exit codes:
|
|
31
|
+
0 — every event validated successfully
|
|
32
|
+
1 — at least one event failed validation (details on stderr)
|
|
33
|
+
2 — setup error (missing dependency, schema not found, bad input, etc.)
|
|
34
|
+
|
|
35
|
+
This script is the normative gate for appending to events/*.jsonl per
|
|
36
|
+
rules/verify-before-report.md §3 and the pr-submission / verification /
|
|
37
|
+
escalation component-agent skills.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import argparse
|
|
43
|
+
import json
|
|
44
|
+
import os
|
|
45
|
+
import sys
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from jsonschema import Draft202012Validator
|
|
50
|
+
except ImportError:
|
|
51
|
+
sys.stderr.write(
|
|
52
|
+
"error: the 'jsonschema' package is required.\n"
|
|
53
|
+
" install it with: pip install 'jsonschema>=4.0'\n"
|
|
54
|
+
)
|
|
55
|
+
sys.exit(2)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_schema_root() -> Path:
|
|
59
|
+
"""Return the orchestrator shared/schemas/ directory.
|
|
60
|
+
|
|
61
|
+
Order:
|
|
62
|
+
1. $SPECFUSE_SCHEMA_ROOT (absolute path)
|
|
63
|
+
2. Sibling-repo fallback relative to this file's location:
|
|
64
|
+
.specfuse/scripts/validate-event.py
|
|
65
|
+
.parent = .specfuse/scripts
|
|
66
|
+
.parent = .specfuse
|
|
67
|
+
.parent = <component-repo>
|
|
68
|
+
.parent = <workspace parent>
|
|
69
|
+
Then / "orchestrator" / "shared" / "schemas".
|
|
70
|
+
"""
|
|
71
|
+
env = os.environ.get("SPECFUSE_SCHEMA_ROOT")
|
|
72
|
+
if env:
|
|
73
|
+
return Path(env).expanduser().resolve()
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
Path(__file__).resolve().parent.parent.parent.parent
|
|
77
|
+
/ "orchestrator"
|
|
78
|
+
/ "shared"
|
|
79
|
+
/ "schemas"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
SCHEMA_ROOT = _resolve_schema_root()
|
|
84
|
+
SCHEMA_PATH = SCHEMA_ROOT / "event.schema.json"
|
|
85
|
+
PER_TYPE_SCHEMA_DIR = SCHEMA_ROOT / "events"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_validator() -> Draft202012Validator:
|
|
89
|
+
if not SCHEMA_PATH.is_file():
|
|
90
|
+
sys.stderr.write(
|
|
91
|
+
f"error: schema not found at {SCHEMA_PATH}\n"
|
|
92
|
+
" set SPECFUSE_SCHEMA_ROOT to the absolute path of\n"
|
|
93
|
+
" orchestrator/shared/schemas/ (or place the orchestrator\n"
|
|
94
|
+
" repo as a sibling of this component repo).\n"
|
|
95
|
+
)
|
|
96
|
+
sys.exit(2)
|
|
97
|
+
with SCHEMA_PATH.open("r", encoding="utf-8") as f:
|
|
98
|
+
schema = json.load(f)
|
|
99
|
+
Draft202012Validator.check_schema(schema)
|
|
100
|
+
return Draft202012Validator(schema)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_PER_TYPE_CACHE: dict[str, Draft202012Validator | None] = {}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_per_type_validator(event_type: str) -> Draft202012Validator | None:
|
|
107
|
+
"""Return a per-type payload validator if one exists, else None.
|
|
108
|
+
|
|
109
|
+
Per-type schemas are additive: an event type without a schema file in
|
|
110
|
+
PER_TYPE_SCHEMA_DIR validates against the top-level envelope alone,
|
|
111
|
+
preserving the Phase 1 freeze contract for component-agent emissions
|
|
112
|
+
whose per-type schemas have not been authored.
|
|
113
|
+
"""
|
|
114
|
+
if event_type in _PER_TYPE_CACHE:
|
|
115
|
+
return _PER_TYPE_CACHE[event_type]
|
|
116
|
+
|
|
117
|
+
schema_file = PER_TYPE_SCHEMA_DIR / f"{event_type}.schema.json"
|
|
118
|
+
if not schema_file.is_file():
|
|
119
|
+
_PER_TYPE_CACHE[event_type] = None
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
with schema_file.open("r", encoding="utf-8") as f:
|
|
124
|
+
schema = json.load(f)
|
|
125
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
126
|
+
sys.stderr.write(f"error: failed to read per-type schema {schema_file}: {exc}\n")
|
|
127
|
+
sys.exit(2)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
Draft202012Validator.check_schema(schema)
|
|
131
|
+
except Exception as exc: # jsonschema raises SchemaError, keep generic
|
|
132
|
+
sys.stderr.write(f"error: invalid per-type schema {schema_file}: {exc}\n")
|
|
133
|
+
sys.exit(2)
|
|
134
|
+
|
|
135
|
+
validator = Draft202012Validator(schema)
|
|
136
|
+
_PER_TYPE_CACHE[event_type] = validator
|
|
137
|
+
return validator
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_error(source: str, line_number: int, path: str, message: str) -> str:
|
|
141
|
+
location = f"{source}:{line_number}" if line_number else source
|
|
142
|
+
prefix = f"{location}"
|
|
143
|
+
if path:
|
|
144
|
+
prefix += f" at {path}"
|
|
145
|
+
return f"{prefix}: {message}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def validate_line(
|
|
149
|
+
validator: Draft202012Validator,
|
|
150
|
+
source: str,
|
|
151
|
+
line_number: int,
|
|
152
|
+
raw: str,
|
|
153
|
+
) -> list[str]:
|
|
154
|
+
try:
|
|
155
|
+
event = json.loads(raw)
|
|
156
|
+
except json.JSONDecodeError as exc:
|
|
157
|
+
return [format_error(source, line_number, "", f"invalid JSON — {exc.msg} (line {exc.lineno}, col {exc.colno})")]
|
|
158
|
+
|
|
159
|
+
errors: list[str] = []
|
|
160
|
+
for err in sorted(validator.iter_errors(event), key=lambda e: list(e.absolute_path)):
|
|
161
|
+
path = "/".join(str(p) for p in err.absolute_path) or "(root)"
|
|
162
|
+
errors.append(format_error(source, line_number, path, err.message))
|
|
163
|
+
|
|
164
|
+
# Per-type payload validation. Applied only when the top-level envelope
|
|
165
|
+
# is valid enough to name the event_type and the payload is a dict;
|
|
166
|
+
# otherwise the top-level errors above are the signal.
|
|
167
|
+
event_type = event.get("event_type") if isinstance(event, dict) else None
|
|
168
|
+
payload = event.get("payload") if isinstance(event, dict) else None
|
|
169
|
+
if isinstance(event_type, str) and isinstance(payload, dict):
|
|
170
|
+
per_type = load_per_type_validator(event_type)
|
|
171
|
+
if per_type is not None:
|
|
172
|
+
for err in sorted(per_type.iter_errors(payload), key=lambda e: list(e.absolute_path)):
|
|
173
|
+
sub_path = "/".join(str(p) for p in err.absolute_path) or "(root)"
|
|
174
|
+
path = f"payload/{sub_path}" if sub_path != "(root)" else "payload"
|
|
175
|
+
errors.append(format_error(source, line_number, path, err.message))
|
|
176
|
+
|
|
177
|
+
return errors
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def iter_lines_from_file(path: Path) -> list[tuple[int, str]]:
|
|
181
|
+
if not path.is_file():
|
|
182
|
+
sys.stderr.write(f"error: file not found: {path}\n")
|
|
183
|
+
sys.exit(2)
|
|
184
|
+
lines: list[tuple[int, str]] = []
|
|
185
|
+
with path.open("r", encoding="utf-8") as f:
|
|
186
|
+
for lineno, raw in enumerate(f, start=1):
|
|
187
|
+
stripped = raw.strip()
|
|
188
|
+
if stripped:
|
|
189
|
+
lines.append((lineno, stripped))
|
|
190
|
+
return lines
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def iter_lines_from_stdin() -> list[tuple[int, str]]:
|
|
194
|
+
data = sys.stdin.read()
|
|
195
|
+
if not data.strip():
|
|
196
|
+
sys.stderr.write("error: no input on stdin\n")
|
|
197
|
+
sys.exit(2)
|
|
198
|
+
lines: list[tuple[int, str]] = []
|
|
199
|
+
for lineno, raw in enumerate(data.splitlines(), start=1):
|
|
200
|
+
stripped = raw.strip()
|
|
201
|
+
if stripped:
|
|
202
|
+
lines.append((lineno, stripped))
|
|
203
|
+
return lines
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
_UNSUPPORTED_HINT = (
|
|
207
|
+
"Supported invocation patterns:\n"
|
|
208
|
+
" cat events/FEAT-XXXX-NNNN.jsonl | .specfuse/scripts/validate-event.py # stdin\n"
|
|
209
|
+
" .specfuse/scripts/validate-event.py --stdin # stdin (explicit)\n"
|
|
210
|
+
" .specfuse/scripts/validate-event.py --file events/FEAT-XXXX-NNNN.jsonl # file\n"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main() -> int:
|
|
215
|
+
parser = argparse.ArgumentParser(
|
|
216
|
+
description="Validate events against the orchestrator's event schema.",
|
|
217
|
+
epilog=_UNSUPPORTED_HINT,
|
|
218
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
219
|
+
)
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
"--file",
|
|
222
|
+
type=Path,
|
|
223
|
+
metavar="PATH",
|
|
224
|
+
help="Path to a .jsonl file of events to validate.",
|
|
225
|
+
)
|
|
226
|
+
parser.add_argument(
|
|
227
|
+
"--stdin",
|
|
228
|
+
action="store_true",
|
|
229
|
+
default=False,
|
|
230
|
+
help="Explicitly read events from stdin (same behaviour as omitting --file).",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Reject unsupported positional arguments before parsing flags.
|
|
234
|
+
# argparse would otherwise accept them silently.
|
|
235
|
+
known, unknown = parser.parse_known_args()
|
|
236
|
+
if unknown:
|
|
237
|
+
sys.stderr.write(
|
|
238
|
+
f"error: unsupported argument(s): {' '.join(unknown)}\n\n"
|
|
239
|
+
+ _UNSUPPORTED_HINT
|
|
240
|
+
)
|
|
241
|
+
return 2
|
|
242
|
+
|
|
243
|
+
args = known
|
|
244
|
+
|
|
245
|
+
if args.file is not None and args.stdin:
|
|
246
|
+
sys.stderr.write(
|
|
247
|
+
"error: --file and --stdin are mutually exclusive.\n\n"
|
|
248
|
+
+ _UNSUPPORTED_HINT
|
|
249
|
+
)
|
|
250
|
+
return 2
|
|
251
|
+
|
|
252
|
+
validator = load_validator()
|
|
253
|
+
|
|
254
|
+
if args.file is not None:
|
|
255
|
+
source = str(args.file)
|
|
256
|
+
entries = iter_lines_from_file(args.file)
|
|
257
|
+
else:
|
|
258
|
+
# Both explicit --stdin and the no-flag default route here.
|
|
259
|
+
source = "<stdin>"
|
|
260
|
+
entries = iter_lines_from_stdin()
|
|
261
|
+
|
|
262
|
+
all_errors: list[str] = []
|
|
263
|
+
for lineno, raw in entries:
|
|
264
|
+
all_errors.extend(validate_line(validator, source, lineno, raw))
|
|
265
|
+
|
|
266
|
+
if all_errors:
|
|
267
|
+
for msg in all_errors:
|
|
268
|
+
sys.stderr.write(msg + "\n")
|
|
269
|
+
sys.stderr.write(
|
|
270
|
+
f"\n{len(all_errors)} validation error(s) across {len(entries)} event(s).\n"
|
|
271
|
+
)
|
|
272
|
+
return 1
|
|
273
|
+
|
|
274
|
+
sys.stdout.write(
|
|
275
|
+
f"ok: {len(entries)} event(s) validated against {SCHEMA_PATH.name}"
|
|
276
|
+
f" (with per-type payload schemas under {PER_TYPE_SCHEMA_DIR.name}/ where present)\n"
|
|
277
|
+
)
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
sys.exit(main())
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: specfuse-loop
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology.
|
|
5
|
+
Author: Specfuse contributors
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/specfuse/loop
|
|
8
|
+
Project-URL: Source, https://github.com/specfuse/loop
|
|
9
|
+
Keywords: specfuse,agents,planning,ralph,gate-cycle
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Topic :: Software Development
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
License-File: NOTICE
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: PyYAML>=6.0; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
25
|
+
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
26
|
+
Requires-Dist: coverage>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: jsonschema>=4.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Specfuse Loop
|
|
31
|
+
|
|
32
|
+
**For engineers using AI coding agents** — a local-first driver that structures features as verified, work-unit sequences so each agent session runs focused on one task instead of accumulating context drift.
|
|
33
|
+
|
|
34
|
+
A small, local-first executor for the **Plan + Work Unit** pattern in a single
|
|
35
|
+
repository. You plan a feature as a sequence of *gates*, each a batch of
|
|
36
|
+
self-contained *work units* with explicit acceptance criteria and verification.
|
|
37
|
+
The loop dispatches each work unit to a fresh agent session, verifies the result
|
|
38
|
+
itself, commits one squashed commit per unit, and stops at each gate so you can
|
|
39
|
+
reflect — with the next gate already drafted and waiting for your review.
|
|
40
|
+
|
|
41
|
+
Specfuse Loop is one of three independently-adoptable projects under the Specfuse
|
|
42
|
+
methodology suite:
|
|
43
|
+
|
|
44
|
+
- **`specfuse/codegen`** — deterministic source code from OpenAPI / AsyncAPI /
|
|
45
|
+
Arazzo specifications.
|
|
46
|
+
- **`specfuse/loop`** — *this project*. Single-repo, spec-optional, lightweight.
|
|
47
|
+
You author the task graph directly; no specification and no agent-coordination
|
|
48
|
+
overhead are required.
|
|
49
|
+
- **`specfuse/orchestrator`** — multi-repo, spec-first, agent coordination across
|
|
50
|
+
many component repositories.
|
|
51
|
+
|
|
52
|
+
Use any one without the others. The loop and the orchestrator share the same
|
|
53
|
+
gate-cycle methodology (see [`docs/methodology.md`](docs/methodology.md)); the
|
|
54
|
+
loop is the lightweight surface for work that lives in one repo and may have no
|
|
55
|
+
formal specification.
|
|
56
|
+
|
|
57
|
+
## Why it exists
|
|
58
|
+
|
|
59
|
+
AI coding agents do well on narrow, well-scoped work and poorly on large, vague
|
|
60
|
+
work. The loop's bet is that the leverage is in the *planning*: if you remove
|
|
61
|
+
ambiguity up front — crisp work units with hard boundaries and machine-checkable
|
|
62
|
+
verification — then execution can run with a fresh agent per unit, re-grounding
|
|
63
|
+
from durable files each time rather than accumulating context drift. It is the
|
|
64
|
+
[Ralph loop](docs/concepts/ralph-lineage.md) idea applied at work-unit granularity, with
|
|
65
|
+
the planning rigor Ralph's bare task list lacks.
|
|
66
|
+
|
|
67
|
+
## How it works (in one minute)
|
|
68
|
+
|
|
69
|
+
- A **feature** lives in `.specfuse/features/FEAT-YYYY-NNNN-slug/`, with a
|
|
70
|
+
`PLAN.md` (the task graph: gate order, work-unit membership, dependencies),
|
|
71
|
+
one `GATE-NN.md` per gate, and one `WU-*.md` per work unit (frontmatter + the
|
|
72
|
+
prompt body a fresh session receives). The loop also handles *orchestrated*
|
|
73
|
+
features dispatched by the Specfuse Orchestrator, identified by
|
|
74
|
+
`INIT-YYYY-NNNN/FNN` IDs — the loop treats both namespaces identically; only
|
|
75
|
+
the ID root differs. Use `.specfuse/scripts/gh_features.py` to discover a
|
|
76
|
+
target repo's open `specfuse:feature` issues as feature candidates; use
|
|
77
|
+
`.specfuse/scripts/adopt_feature.py <repo> <issue-number>` (or the
|
|
78
|
+
interactive `/adopt-feature` skill) to scaffold a dispatchable feature
|
|
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
|
|
82
|
+
verification itself as the exit oracle, and commits one squashed,
|
|
83
|
+
trailer-carrying commit per unit. A failed gate is retried with a fresh
|
|
84
|
+
session carrying the failure evidence, up to three attempts, then escalated.
|
|
85
|
+
- Each gate ends with a **closing sequence** so reflection, a durable
|
|
86
|
+
cross-feature `LEARNINGS.md`, documentation, and *drafting the next gate* all
|
|
87
|
+
happen systematically. Non-terminal gates use a two-WU form
|
|
88
|
+
(`close-intermediate` + `plan-next`); the terminal gate uses a single `close`
|
|
89
|
+
WU. A legacy four-WU form (`retrospective → lessons → docs → plan-next`) is
|
|
90
|
+
accepted but emits a lint warning.
|
|
91
|
+
- The gate is the **human boundary.** The driver runs unattended within a gate
|
|
92
|
+
and stops at it; you review the next gate's draft and arm it. (Under automatic
|
|
93
|
+
mode, safe gates can self-arm; the dangerous edges always pull you back in —
|
|
94
|
+
see the methodology doc.)
|
|
95
|
+
|
|
96
|
+
## Quickstart
|
|
97
|
+
|
|
98
|
+
In a target single-repo project:
|
|
99
|
+
|
|
100
|
+
**Contributing to this repo?** Run `./scripts/install-hooks.sh` once after
|
|
101
|
+
cloning to enable the pre-push hook (runs `scripts/smoke-test.sh` — same
|
|
102
|
+
checks CI runs — before each `git push`). Bypass with `git push --no-verify`.
|
|
103
|
+
|
|
104
|
+
The driver installs from PyPI and the skills from the Claude Code marketplace:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install specfuse-loop # the driver: `specfuse-loop` on PATH
|
|
108
|
+
# in Claude Code: skills under the /specfuse: namespace
|
|
109
|
+
# /plugin marketplace add specfuse/specfuse
|
|
110
|
+
# /plugin install specfuse@specfuse
|
|
111
|
+
|
|
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
|
+
|
|
115
|
+
cd /path/to/your-project
|
|
116
|
+
$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
|
|
120
|
+
```
|
|
121
|
+
|
|
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.
|
|
127
|
+
|
|
128
|
+
> **One driver per working tree.** The driver holds an exclusive advisory lock on
|
|
129
|
+
> `.specfuse/.loop.lock` for the duration of a run; a second driver targeting the
|
|
130
|
+
> same checkout exits immediately with a clear error message. To run two features
|
|
131
|
+
> in parallel, use separate `git worktree` checkouts — each gets its own lock.
|
|
132
|
+
> `--dry-run` is exempt and may run alongside a live driver.
|
|
133
|
+
|
|
134
|
+
This repository is also a **self-demonstrating reference installation**: its own
|
|
135
|
+
`.specfuse/` contains a worked example feature
|
|
136
|
+
(`features/FEAT-2026-0001-health-endpoint/`). From the repo root you can run:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
python .specfuse/scripts/lint_plan.py .specfuse/features/FEAT-2026-0001-health-endpoint
|
|
140
|
+
python .specfuse/scripts/loop.py --dry-run
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Layout
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
specfuse-loop/
|
|
147
|
+
├── LICENSE NOTICE CONTRIBUTING.md README.md .gitignore
|
|
148
|
+
├── init.sh scaffold .specfuse/ into a target repo
|
|
149
|
+
├── docs/
|
|
150
|
+
│ ├── getting-started.md narrated first-feature + operator walkthrough
|
|
151
|
+
│ ├── methodology.md the gate-cycle contract (shared with the orchestrator)
|
|
152
|
+
│ ├── skills.md the skills catalog, ordered by lifecycle phase
|
|
153
|
+
│ ├── concepts/ why it exists; orchestrator mapping
|
|
154
|
+
│ │ ├── ralph-lineage.md the Ralph / Gas Town lineage
|
|
155
|
+
│ │ └── architecture-addendum-gates-and-iterative-planning.md
|
|
156
|
+
│ └── dev/ internal working notes (not user-facing)
|
|
157
|
+
└── .specfuse/ canonical scaffold + worked example
|
|
158
|
+
├── README.md
|
|
159
|
+
├── roadmap.template.md verification.yml.example LEARNINGS.md
|
|
160
|
+
├── rules/result-contract.md
|
|
161
|
+
├── skills/verification/SKILL.md
|
|
162
|
+
├── scripts/{loop.py, lint_plan.py, gh_features.py, adopt_feature.py, gh_backend.py}
|
|
163
|
+
├── templates/{PLAN,GATE,WU}.template.md
|
|
164
|
+
└── features/FEAT-2026-0001-health-endpoint/ (the worked example)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`init.sh` also ships the durable docs — `methodology.md`, `skills.md`, and
|
|
168
|
+
`concepts/` — into a target's `.specfuse/docs/`, so an initialized repo is
|
|
169
|
+
self-documenting without this checkout.
|
|
170
|
+
|
|
171
|
+
## Status
|
|
172
|
+
|
|
173
|
+
Early but exercised. The driver, linter, parsing, dependency ordering, draft/arm
|
|
174
|
+
gating, the deterministic auto-close predicate, and verification wiring are all
|
|
175
|
+
tested, and the loop dogfoods itself — its own `.specfuse/features/` holds 20+
|
|
176
|
+
features taken through the full gate cycle, including multi-gate features whose
|
|
177
|
+
forward-design model (each gate's `plan-next` drafts the next) has held across
|
|
178
|
+
four consecutive gates.
|
|
179
|
+
|
|
180
|
+
What works today: single-feature and orchestrator-dispatched features; adopting a
|
|
181
|
+
GitHub `specfuse:feature` issue into a dispatchable folder; GitHub issue-label
|
|
182
|
+
state transitions for adopted features; per-gate auto-close on clean runs with a
|
|
183
|
+
full-ceremony fallback when a gate goes off-plan; and a single-driver working-tree
|
|
184
|
+
lock so two drivers can't corrupt one checkout.
|
|
185
|
+
|
|
186
|
+
Expect rough edges. The interfaces (WU contract, RESULT block, correlation-ID
|
|
187
|
+
scheme, `verification.yml` shape) are stable; tooling around them is still
|
|
188
|
+
hardening.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
Apache License 2.0. See [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
specfuse/loop/__init__.py,sha256=frUlkSvjwLR3vbEefjM_F5v_xviBHWknIYzFR_3PzrI,164
|
|
2
|
+
specfuse/loop/_miniyaml.py,sha256=r9T7WIfMnlyBEz7hmpR9RylvXR3JWPoOJ2b8Cxr_hnY,18876
|
|
3
|
+
specfuse/loop/adopt_feature.py,sha256=5RBJh4clbqQqsZwW0f695-2O3FiEqC0m5ASo-HC0Lp0,6824
|
|
4
|
+
specfuse/loop/gate_eval.py,sha256=MvN1N_ovmBiyb8wU2liZrccAo3S8bwVIAepGck3m9_s,17742
|
|
5
|
+
specfuse/loop/gh_backend.py,sha256=VoqotYeXp8VoqQ26zpG8EpZF80nbPiuZeQPwd8g9AlQ,2938
|
|
6
|
+
specfuse/loop/gh_features.py,sha256=J4qX8HjINNriIU1SDClRySK6dZuoByOJ2MVidf6x6sk,2946
|
|
7
|
+
specfuse/loop/lint_plan.py,sha256=h-7zgbThcKXRSxE3vkFw8xcNv7mpRkeGptvKexYmZ8g,27120
|
|
8
|
+
specfuse/loop/loop.py,sha256=iKjt2kdFbCcdiVSjDoAY62VX4kKnCWcV9ZH-zt8nsCc,157705
|
|
9
|
+
specfuse/loop/validate_event.py,sha256=52j8nHeXPJfl7q7xvIPGgtjKAsg8-sdD3HFbXBEKp4M,9914
|
|
10
|
+
specfuse_loop-0.2.0.dist-info/licenses/LICENSE,sha256=de-gfE0q-xTYImzwC3dj3S7BxVhanf6RmIGjo_7y3aw,11357
|
|
11
|
+
specfuse_loop-0.2.0.dist-info/licenses/NOTICE,sha256=J_H84W_ngPiGIfdakJshaXWn0C24fjXFahUK0hHy_uw,232
|
|
12
|
+
specfuse_loop-0.2.0.dist-info/METADATA,sha256=pQt2cF3lGPBnTF1vGxcdxzFYEZyd1N-DFxob1DWoZtk,9742
|
|
13
|
+
specfuse_loop-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
specfuse_loop-0.2.0.dist-info/entry_points.txt,sha256=gXi5PRPgZAt9DQgNq4Hr4dwkiaM7EmRdpCuYaqgOMjk,103
|
|
15
|
+
specfuse_loop-0.2.0.dist-info/top_level.txt,sha256=fYXn5BMet9jdLKtleDZBPli5yrZByhMqabq0q9cO1qk,9
|
|
16
|
+
specfuse_loop-0.2.0.dist-info/RECORD,,
|