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.
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ specfuse-lint = specfuse.loop.lint_plan:main
3
+ specfuse-loop = specfuse.loop.loop:main