opencode-gbuild 0.4.0 → 0.5.0
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.
- package/README.md +25 -13
- package/agents/gbuild-auditor.md +1 -1
- package/agents/gbuild-reviewer.md +1 -1
- package/package.json +1 -1
- package/reference/graph-format.md +14 -3
- package/scripts/__pycache__/graph.cpython-314.pyc +0 -0
- package/scripts/__pycache__/test_graph.cpython-314.pyc +0 -0
- package/scripts/__pycache__/validate_format.cpython-314.pyc +0 -0
- package/scripts/graph.py +16 -8
- package/scripts/test_graph.py +36 -12
- package/scripts/validate_format.py +17 -8
- package/skills/plan/SKILL.md +10 -7
- package/skills/pr/SKILL.md +3 -3
- package/skills/review/SKILL.md +4 -4
- package/skills/run/SKILL.md +6 -5
- package/skills/status/SKILL.md +7 -6
- package/templates/graph.json.lock +0 -0
package/README.md
CHANGED
|
@@ -38,17 +38,17 @@ claude --plugin-dir ./plugins/gbuild
|
|
|
38
38
|
|
|
39
39
|
### OpenCode
|
|
40
40
|
|
|
41
|
-
The same directory is also an OpenCode (V2) plugin
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
From GitHub (subdirectory selector):
|
|
41
|
+
The same directory is also an OpenCode (V2) plugin, published to npm as
|
|
42
|
+
[`opencode-gbuild`](https://www.npmjs.com/package/opencode-gbuild) — `index.ts` registers the five
|
|
43
|
+
skills and five slash commands (`/gbuild-plan`, `/gbuild-run`, `/gbuild-status`, `/gbuild-review`,
|
|
44
|
+
`/gbuild-pr`) from the same markdown the Claude plugin uses.
|
|
46
45
|
|
|
47
46
|
```bash
|
|
48
|
-
opencode plugin add
|
|
47
|
+
opencode plugin add opencode-gbuild
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
`opencode plugin update opencode-gbuild` moves to the latest published version. For local
|
|
51
|
+
development, point any `opencode.json(c)` at a checkout instead:
|
|
52
52
|
|
|
53
53
|
```jsonc
|
|
54
54
|
{
|
|
@@ -56,6 +56,14 @@ Or point any `opencode.json(c)` at a local checkout:
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
#### Releasing
|
|
60
|
+
|
|
61
|
+
The npm package is published from this directory:
|
|
62
|
+
|
|
63
|
+
1. Bump `version` in `package.json` — keep `.claude-plugin/plugin.json`'s version in sync.
|
|
64
|
+
2. `npm publish` (2FA on the account; contents are whitelisted via `files` + `.npmignore`).
|
|
65
|
+
3. Users pick it up with `opencode plugin update opencode-gbuild`.
|
|
66
|
+
|
|
59
67
|
One translation note: OpenCode plugins can't register agents, so where the Claude plugin spawns its
|
|
60
68
|
bundled `gbuild-reviewer`/`gbuild-auditor` agents, the OpenCode skills dispatch the built-in `general`
|
|
61
69
|
subagent with the same `agents/gbuild-*.md` text inlined as its brief. The review is still a fresh,
|
|
@@ -83,7 +91,7 @@ OpenCode (same order, kebab-case names):
|
|
|
83
91
|
/gbuild-pr add-oauth-login-with-github
|
|
84
92
|
```
|
|
85
93
|
|
|
86
|
-
- **`plan`** decomposes a feature into `.gbuild/<feature>/graph.json` — a plain JSON graph file with
|
|
94
|
+
- **`plan`** decomposes a feature into `.gbuild/<feature>/db/graph.json` — a plain JSON graph file with
|
|
87
95
|
`Feature`, `Acceptance`, and `GbuildNode` nodes linked by `DEPENDS_ON`/`HAS_ACCEPTANCE`/`SATISFIES`
|
|
88
96
|
relationships. Every dependency edge passes the cut test (does the dependent actually read the
|
|
89
97
|
dependency's output?); every node has mandatory concrete acceptance criteria. Validated with
|
|
@@ -172,14 +180,17 @@ required. That's also why the graph file is `graph.json` rather than YAML: Pytho
|
|
|
172
180
|
parser, and adding PyYAML would defeat the zero-extra-dependencies goal. The file is written and read by
|
|
173
181
|
agents, not hand-edited, so losing comments costs little — `/gbuild:status` is the human-facing view.
|
|
174
182
|
|
|
183
|
+
The plan is a CypherLite GraphData file at `.gbuild/<feature>/db/graph.json`; because CypherLite opens a
|
|
184
|
+
*directory* store, it queries via the folder: `cypherlite .gbuild/<feature>/db "MATCH (n:GbuildNode) RETURN n.slug"`. Checkpoints stay in the sibling `.gbuild/<feature>/nodes/` dir, outside `db/`.
|
|
185
|
+
|
|
175
186
|
```bash
|
|
176
|
-
python3 plugins/gbuild/scripts/validate_format.py .gbuild/<feature>/graph.json # format gate (run after every change)
|
|
177
|
-
python3 plugins/gbuild/scripts/graph.py .gbuild/<feature>/graph.json --status # frontier/blocked/waves
|
|
187
|
+
python3 plugins/gbuild/scripts/validate_format.py .gbuild/<feature>/db/graph.json # format gate (run after every change)
|
|
188
|
+
python3 plugins/gbuild/scripts/graph.py .gbuild/<feature>/db/graph.json --status # frontier/blocked/waves
|
|
178
189
|
python3 -m unittest plugins/gbuild/scripts/test_graph.py
|
|
179
190
|
```
|
|
180
191
|
|
|
181
192
|
`validate_format.py` does stdlib JSON checks always; if the `cypherlite` binary is on PATH it *also*
|
|
182
|
-
opens the
|
|
193
|
+
opens the graph with CypherLite to confirm the real loader accepts it (catches serde issues the stdlib
|
|
183
194
|
walk can't). CypherLite is an optional consumer, not a dependency — absent binary → cross-check skipped
|
|
184
195
|
silently; `--no-cypherlite` forces the skip.
|
|
185
196
|
|
|
@@ -199,6 +210,7 @@ plugins/gbuild/
|
|
|
199
210
|
smoke.test.ts # node --test smoke test for the OpenCode translation (repo-root `npm test`)
|
|
200
211
|
```
|
|
201
212
|
|
|
202
|
-
State lives outside the plugin, in the project: `.gbuild/<feature>/graph.json` (a CypherLite GraphData
|
|
213
|
+
State lives outside the plugin, in the project: `.gbuild/<feature>/db/graph.json` (a CypherLite GraphData
|
|
203
214
|
file, written once by `plan`) and `.gbuild/<feature>/nodes/<slug>.json` (one checkpoint per node,
|
|
204
|
-
written by `run`).
|
|
215
|
+
written by `run`). The graph sits alone in `db/` so CypherLite can open that folder as a database;
|
|
216
|
+
`nodes/` is a sibling, outside the folder CypherLite opens.
|
package/agents/gbuild-auditor.md
CHANGED
|
@@ -7,7 +7,7 @@ description: Strict end-of-feature maintainability auditor for a gbuild feature
|
|
|
7
7
|
|
|
8
8
|
Unusually strict review of implementation quality, maintainability, and abstraction health. Push for **ambitious** restructurings — "code judo" moves that preserve behavior while making the code dramatically simpler, smaller, and more direct. Delete complexity; don't rearrange it.
|
|
9
9
|
|
|
10
|
-
The caller passes you the resolved feature slug and its graph path (`.gbuild/<slug>/graph.json`, a CypherLite GraphData file). Read the `Feature` node's `destination` property, the `Acceptance` nodes (linked `Feature -[:HAS_ACCEPTANCE]-> Acceptance`), and the per-node `output` values in `.gbuild/<slug>/nodes/*.json` (keyed by the GbuildNode `slug` property) for scoping context only — you are auditing the branch's code, not re-grading individual nodes (`gbuild-reviewer` already did that per node, against each node's own contract; your job is the thing no per-node pass can see — what the accumulated diff did to the codebase as a whole). Do NOT write any file — output your audit as your final message exclusively.
|
|
10
|
+
The caller passes you the resolved feature slug and its graph path (`.gbuild/<slug>/db/graph.json`, a CypherLite GraphData file). Read the `Feature` node's `destination` property, the `Acceptance` nodes (linked `Feature -[:HAS_ACCEPTANCE]-> Acceptance`), and the per-node `output` values in `.gbuild/<slug>/nodes/*.json` (keyed by the GbuildNode `slug` property) for scoping context only — you are auditing the branch's code, not re-grading individual nodes (`gbuild-reviewer` already did that per node, against each node's own contract; your job is the thing no per-node pass can see — what the accumulated diff did to the codebase as a whole). Do NOT write any file — output your audit as your final message exclusively.
|
|
11
11
|
|
|
12
12
|
## Core Prompt
|
|
13
13
|
|
|
@@ -9,7 +9,7 @@ declared bar.
|
|
|
9
9
|
|
|
10
10
|
## WHAT YOU'RE GIVEN
|
|
11
11
|
|
|
12
|
-
- The node's full definition from
|
|
12
|
+
- The node's full definition from `.gbuild/<slug>/db/graph.json` (a CypherLite GraphData file): its `slug`, `title`, `type`,
|
|
13
13
|
`contract.input`, `contract.output`, and `acceptance` — all read from the GbuildNode's `properties`.
|
|
14
14
|
- What the node's agent actually produced.
|
|
15
15
|
- For `code`/`test`/`chore` nodes: the git diff for this node's work (`git diff HEAD`, or
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-gbuild",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "OpenCode plugin for gbuild — graph-native task planning and execution with concurrent fan-out, per-node review, and durable checkpoints.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
This file is normative. Where a skill disagrees with this doc, this doc wins.
|
|
4
4
|
|
|
5
|
-
One file per feature: `.gbuild/<feature>/graph.json`, written once by `plan`, read-only during `run`.
|
|
5
|
+
One file per feature: `.gbuild/<feature>/db/graph.json`, written once by `plan`, read-only during `run`.
|
|
6
6
|
|
|
7
|
-
**`graph.json` is a CypherLite GraphData file.**
|
|
7
|
+
**`graph.json` is a CypherLite GraphData file.** It lives alone in its own `db/` folder so CypherLite can open that folder as a database and query it with Cypher — CypherLite opens a *directory* holding `graph.json`, not a bare `.json` file:
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
cypherlite .gbuild/<feature>/
|
|
10
|
+
cypherlite .gbuild/<feature>/db "MATCH (n:GbuildNode) RETURN n.slug, n.type"
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
The `db/` folder contains `graph.json` and nothing else that CypherLite cares about. Checkpoints live in the sibling `.gbuild/<feature>/nodes/` directory (see [State](#state-not-part-of-graphjson)) — deliberately *outside* `db/`, because CypherLite reads a directory named `nodes` as Parquet shards and would reject the store as "mixed snapshot codecs". Opening `db/` also makes CypherLite create a `db/wal/` directory; that is expected. **Only run read-only `MATCH … RETURN` Cypher against the plan** — a writing query could checkpoint and rewrite the write-once `graph.json`.
|
|
14
|
+
|
|
15
|
+
### Interoperability with CypherLite
|
|
16
|
+
|
|
17
|
+
`graph.json` is the *same* JSON snapshot CypherLite reads and writes — there is one store file, not a gbuild copy and a CypherLite copy. Two consequences:
|
|
18
|
+
|
|
19
|
+
- **The store must be the JSON codec.** gbuild always writes `graph.json`, so the store is JSON by construction. A Parquet store directory is not readable by gbuild's stdlib parsers.
|
|
20
|
+
- **A CypherLite checkpoint normalizes the file.** If a writing Cypher command is followed by `.checkpoint` (or the WAL rotates), CypherLite rewrites `graph.json` in full — adding `value_encoding` — and leaves `manifest.json` and `wal/` beside it. gbuild's parsers read only `nodes` / `relationships` / `next_node_id` / `next_rel_id` and ignore the rest, so the checkpointed snapshot remains valid and queryable by `graph.py` / `validate_format.py` unchanged.
|
|
21
|
+
|
|
22
|
+
CypherLite can *read* every property gbuild writes, including the `contract` map. It cannot **write** map property values via Cypher (`InvalidPropertyType`), so a node authored purely through Cypher cannot carry `contract` today — direct JSON authoring remains the way `plan` writes it.
|
|
23
|
+
|
|
13
24
|
JSON, not YAML — `scripts/graph.py` and `scripts/validate_format.py` are stdlib-only and Python's stdlib has no YAML parser. Comments are lost; that's fine, the file is agent-authored, not hand-typed. `/gbuild:status` is the human-facing view.
|
|
14
25
|
|
|
15
26
|
**Validate after every change.** `scripts/validate_format.py <path>` is the format gate — run it after the initial write, after any reopen edit, after any hand-edit, before doing anything else with the file (including committing). It exits 0 + `valid: <feature>` on success, 1 + every error joined on failure. Fix the file and re-run until it passes; never hand off or run an invalid graph.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/scripts/graph.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""gbuild graph utility: validate, topo-order, and query a graph.json feature graph.
|
|
3
3
|
|
|
4
|
-
graph.json is a CypherLite GraphData file
|
|
5
|
-
|
|
4
|
+
graph.json is a CypherLite GraphData file. It lives at
|
|
5
|
+
`.gbuild/<feature>/db/graph.json` so CypherLite can open the `db/` folder as a
|
|
6
|
+
JSON-store database (CypherLite opens a *directory*, not a bare `.json` file)
|
|
7
|
+
and query it with Cypher. This module is the query layer (topo waves, frontier,
|
|
6
8
|
status report) over that file. Format validation lives in validate_format.py
|
|
7
9
|
(the single source of truth for the contract); this module imports and re-exports
|
|
8
10
|
its validate() and enum sets.
|
|
@@ -10,11 +12,11 @@ its validate() and enum sets.
|
|
|
10
12
|
Stdlib only, no pip install required. Schema: ../reference/graph-format.md
|
|
11
13
|
|
|
12
14
|
CLI:
|
|
13
|
-
python3 graph.py <graph.json
|
|
14
|
-
python3 graph.py <graph.json
|
|
15
|
-
python3 graph.py <graph.json
|
|
15
|
+
python3 graph.py <feature>/db/graph.json --waves print the topological wave decomposition
|
|
16
|
+
python3 graph.py <feature>/db/graph.json --frontier print node ids ready to run right now
|
|
17
|
+
python3 graph.py <feature>/db/graph.json --status print the full status report (default)
|
|
16
18
|
|
|
17
|
-
Checkpoints are read from <
|
|
19
|
+
Checkpoints are read from <feature>/nodes/<slug>.json by default, or --state-dir.
|
|
18
20
|
"""
|
|
19
21
|
import argparse
|
|
20
22
|
import json
|
|
@@ -195,13 +197,19 @@ def compute_frontier(graph, state_dir):
|
|
|
195
197
|
|
|
196
198
|
|
|
197
199
|
def default_state_dir(graph_path):
|
|
198
|
-
|
|
200
|
+
"""Checkpoints live in the sibling `nodes/` dir, next to the `db/` graph folder.
|
|
201
|
+
|
|
202
|
+
The plan is `<feature>/db/graph.json` and state is `<feature>/nodes/`; the
|
|
203
|
+
two are siblings so CypherLite's `nodes/` Parquet-shard detection never sees
|
|
204
|
+
the checkpoint dir inside the folder it opens.
|
|
205
|
+
"""
|
|
206
|
+
return Path(graph_path).resolve().parent.parent / "nodes"
|
|
199
207
|
|
|
200
208
|
|
|
201
209
|
def main(argv=None):
|
|
202
210
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
203
211
|
parser.add_argument("graph_path")
|
|
204
|
-
parser.add_argument("--state-dir", default=None, help="checkpoint dir, default: <
|
|
212
|
+
parser.add_argument("--state-dir", default=None, help="checkpoint dir, default: <feature>/nodes")
|
|
205
213
|
group = parser.add_mutually_exclusive_group()
|
|
206
214
|
group.add_argument("--waves", action="store_true")
|
|
207
215
|
group.add_argument("--frontier", action="store_true")
|
package/scripts/test_graph.py
CHANGED
|
@@ -26,6 +26,20 @@ TEMPLATE = PLUGIN_ROOT / "templates" / "graph.json"
|
|
|
26
26
|
CYPHERLITE_BIN = shutil.which("cypherlite")
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def cypherlite_query(graph_path, query):
|
|
30
|
+
"""Run a Cypher query against a GraphData file the way gbuild does.
|
|
31
|
+
|
|
32
|
+
CypherLite opens a directory store holding `graph.json`, not a bare `.json`
|
|
33
|
+
file, so stage the file in a scratch dir as `graph.json` and open that.
|
|
34
|
+
"""
|
|
35
|
+
with tempfile.TemporaryDirectory() as d:
|
|
36
|
+
shutil.copyfile(graph_path, Path(d) / "graph.json")
|
|
37
|
+
return subprocess.run(
|
|
38
|
+
[CYPHERLITE_BIN, d, "-json", query],
|
|
39
|
+
capture_output=True, text=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
29
43
|
# --- canonical factories emitting GraphData ---------------------------------
|
|
30
44
|
|
|
31
45
|
def _gbuild_node(nid, slug, ntype="code", deps=None, satisfies_acc=None, **overrides):
|
|
@@ -338,10 +352,7 @@ class CypherLiteLoadTests(unittest.TestCase):
|
|
|
338
352
|
def test_template_loads_and_queries(self):
|
|
339
353
|
g = load_graph(str(TEMPLATE))
|
|
340
354
|
q = "MATCH (n:GbuildNode) RETURN n.slug AS slug ORDER BY slug"
|
|
341
|
-
res =
|
|
342
|
-
[CYPHERLITE_BIN, str(TEMPLATE), "-json", q],
|
|
343
|
-
capture_output=True, text=True,
|
|
344
|
-
)
|
|
355
|
+
res = cypherlite_query(TEMPLATE, q)
|
|
345
356
|
self.assertEqual(res.returncode, 0, res.stderr)
|
|
346
357
|
rows = json.loads(res.stdout)
|
|
347
358
|
slugs = [r["slug"] for r in rows]
|
|
@@ -350,13 +361,29 @@ class CypherLiteLoadTests(unittest.TestCase):
|
|
|
350
361
|
"c-consume-squared", "d-join-and-sum",
|
|
351
362
|
])
|
|
352
363
|
|
|
364
|
+
def test_sibling_nodes_dir_does_not_collide(self):
|
|
365
|
+
# The plan lives in <feature>/db/ and checkpoints in <feature>/nodes/.
|
|
366
|
+
# CypherLite treats a directory named `nodes` inside the opened store as
|
|
367
|
+
# Parquet shards, so the checkpoint dir must stay outside `db/`.
|
|
368
|
+
with tempfile.TemporaryDirectory() as root:
|
|
369
|
+
feature = Path(root) / ".gbuild" / "example-diamond"
|
|
370
|
+
db = feature / "db"
|
|
371
|
+
nodes = feature / "nodes"
|
|
372
|
+
db.mkdir(parents=True)
|
|
373
|
+
nodes.mkdir(parents=True)
|
|
374
|
+
shutil.copyfile(TEMPLATE, db / "graph.json")
|
|
375
|
+
(nodes / "a-produce-shared-value.json").write_text('{"status": "completed"}')
|
|
376
|
+
res = subprocess.run(
|
|
377
|
+
[CYPHERLITE_BIN, str(db), "-json", "MATCH (n) RETURN count(n) AS c"],
|
|
378
|
+
capture_output=True, text=True,
|
|
379
|
+
)
|
|
380
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
381
|
+
self.assertEqual(json.loads(res.stdout)[0]["c"], 7)
|
|
382
|
+
|
|
353
383
|
def test_template_transitive_deps(self):
|
|
354
384
|
q = ("MATCH (n {slug:'d-join-and-sum'})-[:DEPENDS_ON*]->(d) "
|
|
355
385
|
"RETURN DISTINCT d.slug AS slug ORDER BY slug")
|
|
356
|
-
res =
|
|
357
|
-
[CYPHERLITE_BIN, str(TEMPLATE), "-json", q],
|
|
358
|
-
capture_output=True, text=True,
|
|
359
|
-
)
|
|
386
|
+
res = cypherlite_query(TEMPLATE, q)
|
|
360
387
|
self.assertEqual(res.returncode, 0, res.stderr)
|
|
361
388
|
rows = json.loads(res.stdout)
|
|
362
389
|
self.assertEqual([r["slug"] for r in rows], [
|
|
@@ -366,10 +393,7 @@ class CypherLiteLoadTests(unittest.TestCase):
|
|
|
366
393
|
def test_template_acceptance_query(self):
|
|
367
394
|
q = ("MATCH (f:Feature)-[:HAS_ACCEPTANCE]->(a:Acceptance) "
|
|
368
395
|
"RETURN a.id AS id ORDER BY id")
|
|
369
|
-
res =
|
|
370
|
-
[CYPHERLITE_BIN, str(TEMPLATE), "-json", q],
|
|
371
|
-
capture_output=True, text=True,
|
|
372
|
-
)
|
|
396
|
+
res = cypherlite_query(TEMPLATE, q)
|
|
373
397
|
self.assertEqual(res.returncode, 0, res.stderr)
|
|
374
398
|
rows = json.loads(res.stdout)
|
|
375
399
|
self.assertEqual([r["id"] for r in rows], ["a-1", "a-2"])
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""gbuild graph format validator — the authoritative contract for graph.json.
|
|
3
3
|
|
|
4
|
-
graph.json is a CypherLite GraphData file
|
|
4
|
+
graph.json is a CypherLite GraphData file, living at
|
|
5
|
+
`.gbuild/<feature>/db/graph.json` so CypherLite can open the `db/` folder as a
|
|
6
|
+
JSON-store database (`cypherlite .gbuild/<feature>/db "MATCH ..."`).
|
|
5
7
|
This module checks it is both CypherLite-loadable and gbuild-correct: the
|
|
6
8
|
GraphData envelope, node shapes (Feature / Acceptance / GbuildNode), the three
|
|
7
9
|
relationship types (DEPENDS_ON / HAS_ACCEPTANCE / SATISFIES), id/counter
|
|
@@ -12,7 +14,7 @@ Single source of truth for the format: graph.py imports VALID_* and
|
|
|
12
14
|
SATISFIED_STATUSES from here, and its validate() calls validate() below.
|
|
13
15
|
|
|
14
16
|
CLI:
|
|
15
|
-
python3 validate_format.py <graph.json
|
|
17
|
+
python3 validate_format.py <feature>/db/graph.json
|
|
16
18
|
exit 0 + "valid: <feature>" on success
|
|
17
19
|
exit 1 + "invalid: <errors joined>" on failure
|
|
18
20
|
"""
|
|
@@ -20,6 +22,7 @@ import argparse
|
|
|
20
22
|
import json
|
|
21
23
|
import re
|
|
22
24
|
import sys
|
|
25
|
+
from pathlib import Path
|
|
23
26
|
|
|
24
27
|
# --- enum sets (single definition site; graph.py imports these) -------------
|
|
25
28
|
|
|
@@ -297,23 +300,29 @@ def _detect_cycle(node_ids, edges):
|
|
|
297
300
|
|
|
298
301
|
|
|
299
302
|
def _cypherlite_cross_check(graph_path):
|
|
300
|
-
"""If cypherlite is on PATH, confirm CypherLite itself loads the
|
|
303
|
+
"""If cypherlite is on PATH, confirm CypherLite itself loads the graph and
|
|
301
304
|
can run a trivial query. Catches serde/encoding issues the stdlib walk can't
|
|
302
305
|
(e.g. a top-level shape CypherLite's GraphData loader rejects). Returns an
|
|
303
306
|
error string on failure, None on success or when cypherlite is absent.
|
|
304
307
|
|
|
308
|
+
CypherLite opens a *directory* store, not a bare `.json` file. To avoid
|
|
309
|
+
writing a WAL into the plan's `db/` folder during validation, copy the file
|
|
310
|
+
into a throwaway dir as `graph.json` and open that.
|
|
311
|
+
|
|
305
312
|
Absence is silent — cypherlite is an optional consumer, not a dependency.
|
|
306
313
|
"""
|
|
307
314
|
import shutil
|
|
308
315
|
import subprocess
|
|
316
|
+
import tempfile
|
|
309
317
|
bin_path = shutil.which("cypherlite")
|
|
310
318
|
if bin_path is None:
|
|
311
319
|
return None # not installed — skip, not a failure
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
320
|
+
with tempfile.TemporaryDirectory() as scratch:
|
|
321
|
+
shutil.copyfile(graph_path, Path(scratch) / "graph.json")
|
|
322
|
+
res = subprocess.run(
|
|
323
|
+
[bin_path, scratch, "-json", "MATCH (n) RETURN count(n) AS c"],
|
|
324
|
+
capture_output=True, text=True, timeout=30,
|
|
325
|
+
)
|
|
317
326
|
# cypherlite returns exit 0 even on some load failures (it prints the error
|
|
318
327
|
# to stderr), so check both the exit code AND the output for failure markers.
|
|
319
328
|
out = (res.stderr + res.stdout).strip()
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Decomposes a feature into a real dependency graph in .gbuild/<feature>/graph.json — nodes with typed contracts, mandatory concrete acceptance criteria, and edges that pass the cut test. Use when starting new gbuild work, before /gbuild:run.
|
|
2
|
+
description: Decomposes a feature into a real dependency graph in .gbuild/<feature>/db/graph.json — nodes with typed contracts, mandatory concrete acceptance criteria, and edges that pass the cut test. Use when starting new gbuild work, before /gbuild:run.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
Plan the work described in `$ARGUMENTS` as a graph.
|
|
@@ -44,7 +44,7 @@ Report the branch. Everything below happens on it.
|
|
|
44
44
|
|
|
45
45
|
### 2. Analyse the codebase
|
|
46
46
|
|
|
47
|
-
Read the project manifest(s), directory structure, `README.md`, and any existing `.gbuild/*/graph.json`
|
|
47
|
+
Read the project manifest(s), directory structure, `README.md`, and any existing `.gbuild/*/db/graph.json`
|
|
48
48
|
for related in-flight work. Resolve file paths or URLs in `$ARGUMENTS`; if a reference fails to load,
|
|
49
49
|
say so and ask.
|
|
50
50
|
|
|
@@ -147,10 +147,12 @@ annotate around a failing item.
|
|
|
147
147
|
|
|
148
148
|
### 8. Write and validate
|
|
149
149
|
|
|
150
|
-
Write `.gbuild/<slug>/graph.json` (a CypherLite GraphData file).
|
|
150
|
+
Write `.gbuild/<slug>/db/graph.json` (a CypherLite GraphData file). It lives alone in the `db/` folder so
|
|
151
|
+
CypherLite can open that folder as a JSON-store database; checkpoints stay in the sibling
|
|
152
|
+
`.gbuild/<slug>/nodes/` directory. **Run the format validator first:**
|
|
151
153
|
|
|
152
154
|
```
|
|
153
|
-
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_format.py .gbuild/<slug>/graph.json
|
|
155
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_format.py .gbuild/<slug>/db/graph.json
|
|
154
156
|
```
|
|
155
157
|
|
|
156
158
|
If it prints `invalid: ...`, fix the file and re-run — never hand off or commit a file that fails
|
|
@@ -161,14 +163,15 @@ initial write, reopen, any hand-edit, any fix-up — before doing anything else
|
|
|
161
163
|
Then confirm run-readiness:
|
|
162
164
|
|
|
163
165
|
```
|
|
164
|
-
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<slug>/graph.json --status
|
|
166
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<slug>/db/graph.json --status
|
|
165
167
|
```
|
|
166
168
|
|
|
167
169
|
The graph is now queryable with Cypher **if the `cypherlite` binary is on PATH** (`command -v
|
|
168
|
-
cypherlite`) — in that case you can mention it to the user
|
|
170
|
+
cypherlite`) — in that case you can mention it to the user. Point CypherLite at the `db/` folder
|
|
171
|
+
(not the `.json` file), and keep it read-only:
|
|
169
172
|
|
|
170
173
|
```
|
|
171
|
-
cypherlite .gbuild/<slug>/
|
|
174
|
+
cypherlite .gbuild/<slug>/db "MATCH (n:GbuildNode) RETURN n.slug, n.type"
|
|
172
175
|
```
|
|
173
176
|
|
|
174
177
|
If the binary is absent, skip this without comment. CypherLite is an optional consumer, never a
|
package/skills/pr/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ This skill MUST run in the **main conversation context**. It MUST NOT be invoked
|
|
|
12
12
|
|
|
13
13
|
`$ARGUMENTS` is free-form text: an optional feature slug and an optional `--max-rounds N` flag.
|
|
14
14
|
|
|
15
|
-
- **slug**: the first token that isn't a flag. If absent, infer it — the current branch commonly ends in the slug (`plan` checks out `<prefix>/<slug>`), otherwise fall back to the most recently modified `.gbuild/*/graph.json`. If neither resolves, leave `<slug>` unresolved (the auto-fix step degrades gracefully — see Step 4).
|
|
15
|
+
- **slug**: the first token that isn't a flag. If absent, infer it — the current branch commonly ends in the slug (`plan` checks out `<prefix>/<slug>`), otherwise fall back to the most recently modified `.gbuild/*/db/graph.json`. If neither resolves, leave `<slug>` unresolved (the auto-fix step degrades gracefully — see Step 4).
|
|
16
16
|
- **max_rounds**: the value of `--max-rounds N` (also accept `--max-rounds=N`). Default `3` when absent.
|
|
17
17
|
|
|
18
18
|
## Step 1: Preconditions
|
|
@@ -25,7 +25,7 @@ Verify the environment before touching the remote. Stop with a clear, actionable
|
|
|
25
25
|
4. **Not on base.** Get the current branch (`git branch --show-current`). If it equals `<base>`, stop — there is nothing to open a PR for.
|
|
26
26
|
5. **Clean tree.** Check for uncommitted changes (`git status --porcelain`). `/gbuild:run` commits each node's work as it goes, so the tree should be clean. If there are uncommitted changes, warn the user and ask whether to proceed (they may want to commit first).
|
|
27
27
|
6. **Commits ahead of base.** Confirm the branch has commits the base lacks (`git log <base>..HEAD --oneline`). If empty, stop — nothing to push.
|
|
28
|
-
7. **Graph finished.** When `<slug>` resolved, run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<slug>/graph.json --status`. If the frontier is non-empty or nodes are still `in_progress`, warn that the graph isn't finished and ask whether to open the PR anyway. If any node is `failed`, name it — a PR built on an escalated node is usually premature.
|
|
28
|
+
7. **Graph finished.** When `<slug>` resolved, run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<slug>/db/graph.json --status`. If the frontier is non-empty or nodes are still `in_progress`, warn that the graph isn't finished and ask whether to open the PR anyway. If any node is `failed`, name it — a PR built on an escalated node is usually premature.
|
|
29
29
|
|
|
30
30
|
## Step 2: Determine the remote & push
|
|
31
31
|
|
|
@@ -45,7 +45,7 @@ Gather context from:
|
|
|
45
45
|
|
|
46
46
|
- Commit subjects: `git log <base>..HEAD --oneline`.
|
|
47
47
|
- File overview: `git diff <base>...HEAD --stat`.
|
|
48
|
-
- When present: the `Feature` node's `destination` property and the `Acceptance` nodes (linked `Feature -[:HAS_ACCEPTANCE]-> Acceptance`) from `.gbuild/<slug>/graph.json` (a CypherLite GraphData file), and the per-node `output` values in `.gbuild/<slug>/nodes/*.json` (keyed by GbuildNode `slug`).
|
|
48
|
+
- When present: the `Feature` node's `destination` property and the `Acceptance` nodes (linked `Feature -[:HAS_ACCEPTANCE]-> Acceptance`) from `.gbuild/<slug>/db/graph.json` (a CypherLite GraphData file), and the per-node `output` values in `.gbuild/<slug>/nodes/*.json` (keyed by GbuildNode `slug`).
|
|
49
49
|
|
|
50
50
|
Derive a concise, conventional-commit-style title from the feature/branch and the changes (e.g. `feat(auth): add OAuth login`).
|
|
51
51
|
|
package/skills/review/SKILL.md
CHANGED
|
@@ -12,10 +12,10 @@ missed abstractions that only exist *between* nodes, which no per-node review ca
|
|
|
12
12
|
|
|
13
13
|
`$ARGUMENTS` is optional. If present, treat it as the feature slug. If absent, infer it — the current
|
|
14
14
|
branch commonly ends in the slug (`plan` checks out `<prefix>/<slug>`), otherwise fall back to the most
|
|
15
|
-
recently modified `.gbuild/*/graph.json`. Do NOT write any file — this skill outputs to chat exclusively.
|
|
15
|
+
recently modified `.gbuild/*/db/graph.json`. Do NOT write any file — this skill outputs to chat exclusively.
|
|
16
16
|
|
|
17
|
-
Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_format.py .gbuild/<slug>/graph.json` then
|
|
18
|
-
`python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<slug>/graph.json --status` first. If the frontier is
|
|
17
|
+
Run `python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_format.py .gbuild/<slug>/db/graph.json` then
|
|
18
|
+
`python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<slug>/db/graph.json --status` first. If the frontier is
|
|
19
19
|
non-empty or nodes are still `in_progress`, say the graph isn't finished and ask whether to audit anyway
|
|
20
20
|
— auditing a half-built branch produces findings that the remaining nodes were going to address.
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ non-empty or nodes are still `in_progress`, say the graph isn't finished and ask
|
|
|
24
24
|
Launch the `gbuild-auditor` subagent (agents/gbuild-auditor.md — this plugin's own copy, gbuild does not
|
|
25
25
|
depend on hone-ai being installed). Pass it:
|
|
26
26
|
|
|
27
|
-
- The resolved slug and its `.gbuild/<slug>/graph.json` path.
|
|
27
|
+
- The resolved slug and its `.gbuild/<slug>/db/graph.json` path.
|
|
28
28
|
- Tell it to audit the current branch.
|
|
29
29
|
|
|
30
30
|
The sub-agent runs the full audit in its own fresh context and returns the audit as its final message.
|
package/skills/run/SKILL.md
CHANGED
|
@@ -2,26 +2,27 @@
|
|
|
2
2
|
description: Executes a gbuild graph — dispatches every ready-and-independent node in the current wave concurrently, reviews each node's output with gbuild-reviewer before checkpointing it complete, and resumes only the remaining frontier on re-invocation. Use after /gbuild:plan has written graph.json.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Run `.gbuild/$ARGUMENTS/graph.json` (the feature slug is `$ARGUMENTS`).
|
|
5
|
+
Run `.gbuild/$ARGUMENTS/db/graph.json` (the feature slug is `$ARGUMENTS`).
|
|
6
6
|
|
|
7
7
|
**CRITICAL: this skill must run in the main context, not inside a forked agent.** It dispatches its own
|
|
8
8
|
subagents per node; a subagent cannot itself fan out further subagents reliably.
|
|
9
9
|
|
|
10
10
|
## Step 0: Load and query
|
|
11
11
|
|
|
12
|
-
Load `.gbuild/<feature>/graph.json` (a CypherLite GraphData file — nodes keyed by integer `id`, with a
|
|
12
|
+
Load `.gbuild/<feature>/db/graph.json` (a CypherLite GraphData file — nodes keyed by integer `id`, with a
|
|
13
13
|
`slug` property; dependencies are `DEPENDS_ON` relationships, feature-level acceptance is `Acceptance`
|
|
14
|
-
nodes linked by `HAS_ACCEPTANCE`/`SATISFIES
|
|
14
|
+
nodes linked by `HAS_ACCEPTANCE`/`SATISFIES`; checkpoints live in the sibling `.gbuild/<feature>/nodes/`).
|
|
15
|
+
**Confirm it's well-formed first:**
|
|
15
16
|
|
|
16
17
|
```
|
|
17
|
-
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_format.py .gbuild/<feature>/graph.json
|
|
18
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_format.py .gbuild/<feature>/db/graph.json
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
If it prints `invalid: ...`, stop and report — do not attempt to run a graph `plan` didn't finish
|
|
21
22
|
correctly. Then take the frontier:
|
|
22
23
|
|
|
23
24
|
```
|
|
24
|
-
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<feature>/graph.json --status
|
|
25
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<feature>/db/graph.json --status
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
Take the `frontier` list (node **slugs**) from the status report. This is the set of nodes ready
|
package/skills/status/SKILL.md
CHANGED
|
@@ -15,15 +15,16 @@ Report status for `$ARGUMENTS`.
|
|
|
15
15
|
|
|
16
16
|
### 1. Load
|
|
17
17
|
|
|
18
|
-
`.gbuild/<feature>/graph.json` (a CypherLite GraphData file — GbuildNodes have a `slug` property and
|
|
19
|
-
linked by `DEPENDS_ON` relationships; feature-level acceptance is `Acceptance` nodes linked by
|
|
20
|
-
`HAS_ACCEPTANCE`/`SATISFIES`) and every `.gbuild/<feature>/nodes/*.json` checkpoint (keyed by slug
|
|
21
|
-
is a small, bounded read — status doesn't
|
|
18
|
+
`.gbuild/<feature>/db/graph.json` (a CypherLite GraphData file — GbuildNodes have a `slug` property and
|
|
19
|
+
are linked by `DEPENDS_ON` relationships; feature-level acceptance is `Acceptance` nodes linked by
|
|
20
|
+
`HAS_ACCEPTANCE`/`SATISFIES`) and every `.gbuild/<feature>/nodes/*.json` checkpoint (keyed by slug; the
|
|
21
|
+
`nodes/` dir is a sibling of the `db/` graph folder). This is a small, bounded read — status doesn't
|
|
22
|
+
scale with graph size the way loading full node prose would.
|
|
22
23
|
|
|
23
24
|
### 2. Compute
|
|
24
25
|
|
|
25
26
|
```
|
|
26
|
-
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<feature>/graph.json --status
|
|
27
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<feature>/db/graph.json --status
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
Gives `frontier` / `blocked` / `in_flight` / `completed` / `cancelled` / `failed` / `waves` (all as slug
|
|
@@ -80,5 +81,5 @@ If all five are clear, say so in one line — don't let silence be ambiguous bet
|
|
|
80
81
|
|
|
81
82
|
## All-features mode
|
|
82
83
|
|
|
83
|
-
For each `.gbuild/<slug>/graph.json` found, one line: slug, node counts by status, and whether the
|
|
84
|
+
For each `.gbuild/<slug>/db/graph.json` found, one line: slug, node counts by status, and whether the
|
|
84
85
|
frontier is empty (done or stuck) or has ready work. Point at `/gbuild:status <slug>` for detail.
|
|
File without changes
|