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 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 — `index.ts` registers the five skills and five
42
- slash commands (`/gbuild-plan`, `/gbuild-run`, `/gbuild-status`, `/gbuild-review`, `/gbuild-pr`) from
43
- the same markdown the Claude plugin uses.
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 'github:oskarhane/oskars.ai#main::path:plugins/gbuild'
47
+ opencode plugin add opencode-gbuild
49
48
  ```
50
49
 
51
- Or point any `opencode.json(c)` at a local checkout:
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 file with CypherLite to confirm the real loader accepts it (catches serde issues the stdlib
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.
@@ -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 `graph.json` (a CypherLite GraphData file): its `slug`, `title`, `type`,
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.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.** Open it directly with CypherLite and query it with Cypher:
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>/graph.json "MATCH (n:GbuildNode) RETURN n.slug, n.type"
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.
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 — openable with `cypherlite <path>` and
5
- queryable with Cypher. This module is the query layer (topo waves, frontier,
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> --waves print the topological wave decomposition
14
- python3 graph.py <graph.json> --frontier print node ids ready to run right now
15
- python3 graph.py <graph.json> --status print the full status report (default)
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 <graph dir>/nodes/<slug>.json by default, or --state-dir.
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
- return Path(graph_path).resolve().parent / "nodes"
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: <graph dir>/nodes")
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")
@@ -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 = subprocess.run(
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 = subprocess.run(
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 = subprocess.run(
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 (openable with `cypherlite <path>`).
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 file and
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
- res = subprocess.run(
313
- [bin_path, str(graph_path), "-json",
314
- "MATCH (n) RETURN count(n) AS c"],
315
- capture_output=True, text=True, timeout=30,
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()
@@ -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). **Run the format validator first:**
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>/graph.json "MATCH (n:GbuildNode) RETURN n.slug, n.type"
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
@@ -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
 
@@ -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.
@@ -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`). **Confirm it's well-formed first:**
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
@@ -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 are
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). This
21
- is a small, bounded read — status doesn't scale with graph size the way loading full node prose would.
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