opencode-gbuild 0.4.0 → 0.6.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
@@ -21,6 +21,8 @@ verification never grades itself.
21
21
 
22
22
  ## Install
23
23
 
24
+ gbuild needs the `cypherlite` CLI on PATH — see [Runtime](#runtime).
25
+
24
26
  ### Claude Code
25
27
 
26
28
  From the marketplace:
@@ -38,17 +40,17 @@ claude --plugin-dir ./plugins/gbuild
38
40
 
39
41
  ### OpenCode
40
42
 
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):
43
+ The same directory is also an OpenCode (V2) plugin, published to npm as
44
+ [`opencode-gbuild`](https://www.npmjs.com/package/opencode-gbuild) — `index.ts` registers the five
45
+ skills and five slash commands (`/gbuild-plan`, `/gbuild-run`, `/gbuild-status`, `/gbuild-review`,
46
+ `/gbuild-pr`) from the same markdown the Claude plugin uses.
46
47
 
47
48
  ```bash
48
- opencode plugin add 'github:oskarhane/oskars.ai#main::path:plugins/gbuild'
49
+ opencode plugin add opencode-gbuild
49
50
  ```
50
51
 
51
- Or point any `opencode.json(c)` at a local checkout:
52
+ `opencode plugin update opencode-gbuild` moves to the latest published version. For local
53
+ development, point any `opencode.json(c)` at a checkout instead:
52
54
 
53
55
  ```jsonc
54
56
  {
@@ -56,6 +58,14 @@ Or point any `opencode.json(c)` at a local checkout:
56
58
  }
57
59
  ```
58
60
 
61
+ #### Releasing
62
+
63
+ The npm package is published from this directory:
64
+
65
+ 1. Bump `version` in `package.json` — keep `.claude-plugin/plugin.json`'s version in sync.
66
+ 2. `npm publish` (2FA on the account; contents are whitelisted via `files` + `.npmignore`).
67
+ 3. Users pick it up with `opencode plugin update opencode-gbuild`.
68
+
59
69
  One translation note: OpenCode plugins can't register agents, so where the Claude plugin spawns its
60
70
  bundled `gbuild-reviewer`/`gbuild-auditor` agents, the OpenCode skills dispatch the built-in `general`
61
71
  subagent with the same `agents/gbuild-*.md` text inlined as its brief. The review is still a fresh,
@@ -83,13 +93,13 @@ OpenCode (same order, kebab-case names):
83
93
  /gbuild-pr add-oauth-login-with-github
84
94
  ```
85
95
 
86
- - **`plan`** decomposes a feature into `.gbuild/<feature>/graph.json` — a plain JSON graph file with
87
- `Feature`, `Acceptance`, and `GbuildNode` nodes linked by `DEPENDS_ON`/`HAS_ACCEPTANCE`/`SATISFIES`
88
- relationships. Every dependency edge passes the cut test (does the dependent actually read the
89
- dependency's output?); every node has mandatory concrete acceptance criteria. Validated with
90
- `scripts/validate_format.py` after every change.
96
+ - **`plan`** charts a feature into a CypherLite graph store at `.gbuild/<feature>/db/` — `Feature`,
97
+ `Acceptance`, and `GbuildNode` nodes, each node's contract as `INPUT`/`OUTPUT` `Field` nodes, and
98
+ `FROM` edges carrying data from one node's output to the next node's input. Every dependency edge
99
+ passes the cut test — and because data flow is explicit, the validator checks it; every node has
100
+ mandatory concrete acceptance criteria. Written and validated entirely in Cypher.
91
101
  - **`run`** dispatches every node in the current ready wave concurrently — not one at a time — reviews
92
- each node's output with a dedicated `gbuild-reviewer` subagent before checkpointing it complete, and
102
+ each node's output with a dedicated `gbuild-reviewer` subagent before recording it complete, and
93
103
  applies the node's declared failure policy on a review failure. Re-invoking after an interruption
94
104
  resumes only the remaining frontier.
95
105
  - **`status`** reports the graph — frontier, blocked, in-flight, completed, each node's review verdict,
@@ -111,16 +121,15 @@ incoming edge, and a controlled cycle gets a hard round cap instead of an open-e
111
121
 
112
122
  ## Node statuses
113
123
 
114
- A node's status lives in its own checkpoint file, `.gbuild/<feature>/nodes/<slug>.json` (keyed by the
115
- GbuildNode's `slug` property — the stable identity skills and checkpoints key on, since the graph file's
116
- integer `id` is authoring convenience). There are five,
117
- and each exists to answer a different question `run` has to ask on every invocation:
124
+ A node's status is its `status` property in the store, next to its outputs and its review history
125
+ (`Review` nodes, one per attempt). There are five, and each exists to answer a different question `run`
126
+ has to ask on every invocation:
118
127
 
119
128
  | status | means | why it exists |
120
129
  | ------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
- | `pending` | not started | The default — a node with no checkpoint file *is* pending. Nothing has to be written to declare work undone, so an interrupted run leaves no cleanup behind. |
130
+ | `pending` | not started | Every node is charted `pending`. Interrupted work goes back to `pending` when the next run resets it, so nothing is left half-declared. |
122
131
  | `in_progress` | dispatched, no verdict yet | Distinguishes "running right now" from "never started", so a resumed run doesn't double-dispatch a node whose agent was killed mid-flight. |
123
- | `completed` | reviewed and passed | The only status that satisfies a dependent's edge on merit. Written only after `gbuild-reviewer` returns `VERDICT: pass` — an implementing agent cannot mark its own success. |
132
+ | `completed` | reviewed and passed | The only status that satisfies a dependent's edge on merit. The write refuses without a passing `Review` and a value for every output — an implementing agent cannot mark its own success. |
124
133
  | `cancelled` | deliberately abandoned, dependents may proceed | The `skip` failure policy's landing spot. Counts as satisfied, so abandoning an optional node doesn't permanently wedge everything downstream. |
125
134
  | `failed` | escalated, dependents held | Records that the node is genuinely stuck and needs a human. Does **not** satisfy dependents — they stay blocked rather than building on a broken foundation. |
126
135
 
@@ -130,7 +139,7 @@ without me*, `failed` says *stop and wait*. Collapsing them into one "didn't wor
130
139
  `run` to guess which one a given failure meant.
131
140
 
132
141
  Everything else the tooling reports — `frontier`, `blocked`, `in_flight`, `waves` — is **derived** from
133
- these five by `scripts/graph.py`, never stored. Nothing can drift out of sync with itself, and the
142
+ these five by `cypher/status.cypher`, never stored. Nothing can drift out of sync with itself, and the
134
143
  frontier is a query result rather than a judgement call:
135
144
 
136
145
  ```
@@ -143,12 +152,12 @@ in_flight = in_progress
143
152
 
144
153
  Subagents are forked, so they cannot talk to you directly — a subagent's final message is a tool result,
145
154
  not chat output. Anything it finds that needs a human decision therefore travels a specific path:
146
- the subagent reports it, `run` checkpoints it, and `run`'s report surfaces it. Nothing gets silently
155
+ the subagent reports it, `run` records it, and `run`'s report surfaces it. Nothing gets silently
147
156
  resolved by the agent that found it, and nothing gets routed around.
148
157
 
149
158
  Three things can trigger it:
150
159
 
151
- - **A node exhausts its failure policy.** `escalate` checkpoints the node `failed`, stops dispatching
160
+ - **A node exhausts its failure policy.** `escalate` sets the node `failed`, stops dispatching
152
161
  its dependents (siblings elsewhere in the graph keep going), and surfaces it as needing a decision.
153
162
  `repair` escalates the same way once its 2 attempts are spent; `retry` and `fallback` are capped at 2
154
163
  attempts too, so no policy can loop unbounded. See `reference/failure-policies.md`.
@@ -167,21 +176,29 @@ decision is wanted; neither guesses.
167
176
 
168
177
  ## Runtime
169
178
 
170
- `scripts/graph.py` and `scripts/validate_format.py` are Python 3, stdlib only — no `pip install`
171
- required. That's also why the graph file is `graph.json` rather than YAML: Python's stdlib has no YAML
172
- parser, and adding PyYAML would defeat the zero-extra-dependencies goal. The file is written and read by
173
- agents, not hand-edited, so losing comments costs little — `/gbuild:status` is the human-facing view.
179
+ gbuild **requires the [CypherLite](https://github.com/neo4j-labs/cypherlite) CLI**, 0.7.0 or newer:
174
180
 
175
181
  ```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
178
- python3 -m unittest plugins/gbuild/scripts/test_graph.py
182
+ curl -sSfL https://neo4j-labs.github.io/cypherlite/install.sh | bash
179
183
  ```
180
184
 
181
- `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
183
- walk can't). CypherLite is an optional consumer, not a dependency — absent binary → cross-check skipped
184
- silently; `--no-cypherlite` forces the skip.
185
+ Every skill reads and writes the graph with `cypherlite` and nothing else — there is no Python and no
186
+ direct JSON editing. The plugin ships the queries as `.cypher` files; skills pipe them into the store:
187
+
188
+ ```bash
189
+ cypherlite .gbuild/<feature>/db -json < plugins/gbuild/cypher/validate.cypher # format gate: [] = valid
190
+ cypherlite .gbuild/<feature>/db -json < plugins/gbuild/cypher/status.cypher # frontier/blocked/waves
191
+ cypherlite .gbuild/<feature>/db -json "MATCH (n:GbuildNode) RETURN n.slug, n.status" # anything else
192
+ ```
193
+
194
+ Required properties and their types are engine constraints, so an invalid write never lands; the rest of
195
+ the contract (enums, edge shapes, coverage, acyclicity, the cut test) is `validate.cypher`. CypherLite
196
+ locks a store while a process has it open, so only the main-context skill touches it, one command at a
197
+ time — subagents get their inputs in their prompt and report back. `reference/cypher.md` has the full
198
+ operating rules.
199
+
200
+ Tests (repo-root `npm test`) run the Cypher layer against the real binary: `cypher.test.ts` drives the
201
+ worked example through a full run and checks every validator rule and write guard.
185
202
 
186
203
  ## Layout
187
204
 
@@ -189,16 +206,17 @@ silently; `--no-cypherlite` forces the skip.
189
206
  plugins/gbuild/
190
207
  agents/gbuild-reviewer.md # per-node review, ported from hone-ai's reviewer, never self-review
191
208
  agents/gbuild-auditor.md # end-of-branch maintainability audit, ported from hone-ai's auditor
192
- reference/ # graph-format.md is normative; shapes/failure-policies/cost-model/checklist inform plan
193
- scripts/validate_format.py # the authoritative format contract — checks the GraphData file is CypherLite-loadable + gbuild-correct
194
- scripts/graph.py # query layer: topo-sort into waves, compute frontier/blocked/in-flight
195
- templates/graph.json # a worked 4-node diamond example (CypherLite GraphData)
209
+ reference/ # graph-format.md (normative model) + cypher.md (how to talk to the store); shapes/failure-policies/cost-model/checklist inform plan
210
+ cypher/ # schema, validate, status/node/feature reads, and run's write scripts
211
+ templates/example.cypher # a worked 4-node diamond, written the way plan writes a feature
196
212
  skills/{plan,run,status,review,pr}/
197
213
  index.ts # OpenCode plugin entry: registers the skills + /gbuild-* commands, translating paths/names
198
214
  package.json # OpenCode plugin manifest (npm package "opencode-gbuild")
199
- smoke.test.ts # node --test smoke test for the OpenCode translation (repo-root `npm test`)
215
+ cypher.test.ts # node --test contract tests for the Cypher layer (repo-root `npm test`)
216
+ smoke.test.ts # node --test smoke test for the OpenCode translation
200
217
  ```
201
218
 
202
- State lives outside the plugin, in the project: `.gbuild/<feature>/graph.json` (a CypherLite GraphData
203
- file, written once by `plan`) and `.gbuild/<feature>/nodes/<slug>.json` (one checkpoint per node,
204
- written by `run`).
219
+ State lives outside the plugin, in the project: one CypherLite store per feature at
220
+ `.gbuild/<feature>/db/`. It holds the plan, every node's status and output values, and the full review
221
+ history. Only `db/graph.json` (CypherLite's JSON snapshot, kept current by every write) and the store's
222
+ own `.gitignore` are committed.
@@ -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 the feature context it read from the gbuild store: the `destination`, the acceptance bar (each criterion with the nodes covering it), and every node's recorded output values. Use it 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 open `.gbuild/` or run `cypherlite` — the caller owns the store. Do NOT write any file — output your audit as your final message exclusively.
11
11
 
12
12
  ## Core Prompt
13
13
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gbuild-reviewer
3
- description: Reviews a gbuild node's output against its contract and acceptance criteria. Runs after every node's implement pass, before it can be checkpointed complete. Never the same agent that implemented the node.
3
+ description: Reviews a gbuild node's output against its contract and acceptance criteria. Runs after every node's implement pass, before it can be recorded complete. Never the same agent that implemented the node.
4
4
  ---
5
5
 
6
6
  You are reviewing one gbuild node's output. You did not implement it — do not defer to the implementer's
@@ -9,19 +9,25 @@ 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`,
13
- `contract.input`, `contract.output`, and `acceptance` — all read from the GbuildNode's `properties`.
14
- - What the node's agent actually produced.
12
+ - The node's definition, as the caller read it from the gbuild store: its `slug`, `title`, `type`,
13
+ `inputs` (each with `name`, `shape`, and — when read from another node — the upstream `value`),
14
+ `outputs` (each with `name` and `shape`), and `acceptance`. The contract is those input and output
15
+ fields.
16
+ - What the node's agent actually produced, including the `outputs` object it reported.
15
17
  - For `code`/`test`/`chore` nodes: the git diff for this node's work (`git diff HEAD`, or
16
18
  `git diff --staged`, or `git log -1 -p` if it already committed).
17
19
 
20
+ Everything you need is in your prompt — do not open `.gbuild/` or run `cypherlite`; the caller owns the
21
+ store and records your verdict.
22
+
18
23
  ## REVIEW OBJECTIVE
19
24
 
20
25
  Two checks, in order:
21
26
 
22
- 1. **Does the output match `contract.output`'s declared shape?** If the contract said this node
23
- produces a specific structure and the output doesn't match it, that's a fail regardless of the
24
- acceptance criteria — a downstream node depending on this output will break.
27
+ 1. **Does every output field have a value matching its declared `shape`?** A missing field, or a value
28
+ that doesn't match its shape, is a fail regardless of the acceptance criteria — a downstream node
29
+ reading it through `FROM` will break. Values must be scalars or lists of scalars; a nested value must
30
+ be a JSON-encoded string.
25
31
  2. **Does the output satisfy every bullet in `acceptance`, one at a time?** Not "does it seem fine
26
32
  overall" — go bullet by bullet, pass or fail each one individually, cite why.
27
33
 
@@ -89,6 +95,7 @@ Lead with the acceptance checklist, one line per bullet:
89
95
  Then, only if there are findings beyond acceptance: `Issue` or `Suggestion`, each with a `Priority`
90
96
  (`critical | high | medium | low`).
91
97
 
92
- Finish with one verdict line: `VERDICT: pass` only if `contract.output` matches shape and every
93
- acceptance bullet passed — otherwise `VERDICT: fail`. A single failed acceptance bullet or a
94
- `contract.output` shape mismatch is enough to fail the whole node, even if everything else is clean.
98
+ Finish with one verdict line: `VERDICT: pass` only if every output field has a value matching its shape
99
+ and every acceptance bullet passed — otherwise `VERDICT: fail`. A single failed acceptance bullet or an
100
+ output shape mismatch is enough to fail the whole node, even if everything else is clean. The caller
101
+ records each `[fail]` line's criterion as the review's failed criteria.
@@ -0,0 +1,23 @@
1
+ // Record a node as completed: store its output values on its OUTPUT fields
2
+ // and the implementing agent's own start/finish times. Guarded — it writes
3
+ // nothing unless the node is in_progress, has a passing Review, and $outputs
4
+ // has a value for every declared output. Check `completed` in the result;
5
+ // `missing_outputs` / `has_passing_review` say why when it is false.
6
+ // Output values must be scalars or lists of scalars; JSON-encode anything
7
+ // nested into a string.
8
+ // cypherlite .gbuild/<slug>/db -json --param slug=<node-slug> \
9
+ // --param 'outputs={"value": 7}' --param started_at=<iso> --param completed_at=<iso> \
10
+ // < cypher/complete.cypher
11
+ MATCH (n:GbuildNode {slug: $slug})
12
+ WITH n,
13
+ [k IN COLLECT { MATCH (n)-[:OUTPUT]->(f:Field) RETURN f.name } WHERE $outputs[k] IS NULL] AS missing,
14
+ EXISTS { (n)-[:REVIEWED]->(:Review {verdict: 'pass'}) } AS passed
15
+ WITH n, missing, passed, (n.status = 'in_progress' AND passed AND size(missing) = 0) AS ok
16
+ OPTIONAL MATCH (n)-[:OUTPUT]->(f:Field) WHERE ok
17
+ SET f.value = $outputs[f.name]
18
+ WITH DISTINCT n, missing, passed, ok
19
+ FOREACH (_ IN CASE WHEN ok THEN [1] ELSE [] END |
20
+ SET n.status = 'completed', n.started_at = $started_at, n.completed_at = $completed_at)
21
+ RETURN n.slug AS slug, ok AS completed, n.status AS status,
22
+ passed AS has_passing_review, missing AS missing_outputs;
23
+ .checkpoint
@@ -0,0 +1,13 @@
1
+ // Mark frontier nodes in_progress just before run fans them out. Only nodes
2
+ // that are pending AND whose every DEPENDS_ON target is completed or cancelled
3
+ // are moved; anything else is silently skipped, so compare `dispatched`
4
+ // against what you asked for.
5
+ // cypherlite .gbuild/<slug>/db -json --param 'slugs=["a","b"]' < cypher/dispatch.cypher
6
+ UNWIND $slugs AS s
7
+ MATCH (n:GbuildNode {slug: s})
8
+ WHERE n.status = 'pending'
9
+ AND NOT EXISTS { MATCH (n)-[:DEPENDS_ON]->(d:GbuildNode) WHERE NOT d.status IN ['completed', 'cancelled'] }
10
+ SET n.status = 'in_progress'
11
+ RETURN n.slug AS dispatched
12
+ ORDER BY dispatched;
13
+ .checkpoint
@@ -0,0 +1,21 @@
1
+ // Feature-level context: destination, the acceptance bar with the nodes that
2
+ // cover each criterion, and every node's recorded output values. What review
3
+ // hands the auditor and what pr builds the description from.
4
+ // cypherlite .gbuild/<slug>/db -json < cypher/feature.cypher
5
+ MATCH (f:Feature)
6
+ RETURN f.feature AS feature,
7
+ f.destination AS destination,
8
+ f.context AS context,
9
+ f.out_of_scope AS out_of_scope,
10
+ COLLECT {
11
+ MATCH (f)-[:HAS_ACCEPTANCE]->(a:Acceptance)
12
+ WITH a ORDER BY a.id
13
+ RETURN {id: a.id, text: a.text,
14
+ covered_by: COLLECT { MATCH (a)<-[:SATISFIES]-(n:GbuildNode) WITH n ORDER BY n.slug RETURN n.slug }}
15
+ } AS acceptance,
16
+ COLLECT {
17
+ MATCH (n:GbuildNode)
18
+ WITH n ORDER BY n.slug
19
+ RETURN {slug: n.slug, title: n.title, status: n.status,
20
+ outputs: COLLECT { MATCH (n)-[:OUTPUT]->(o:Field) WITH o ORDER BY o.name RETURN {name: o.name, value: o.value} }}
21
+ } AS nodes;
@@ -0,0 +1,39 @@
1
+ // Everything needed to dispatch or review one node, with inputs resolved
2
+ // through FROM edges to the upstream output values. An input with no FROM is
3
+ // external (from the codebase / environment): its `from` and `value` are null.
4
+ // cypherlite .gbuild/<slug>/db -json --param slug=<node-slug> < cypher/node.cypher
5
+ MATCH (n:GbuildNode {slug: $slug})
6
+ RETURN n.slug AS slug,
7
+ n.title AS title,
8
+ n.type AS type,
9
+ n.status AS status,
10
+ n.acceptance AS acceptance,
11
+ n.verify AS verify,
12
+ n.failure_policy AS failure_policy,
13
+ n.model_tier AS model_tier,
14
+ COLLECT {
15
+ MATCH (n)-[:INPUT]->(i:Field)
16
+ OPTIONAL MATCH (i)-[:FROM]->(o:Field)<-[:OUTPUT]-(u:GbuildNode)
17
+ WITH i, o, u ORDER BY i.name
18
+ RETURN {name: i.name, shape: i.shape, from: u.slug + '.output.' + o.name, value: o.value}
19
+ } AS inputs,
20
+ COLLECT {
21
+ MATCH (n)-[:OUTPUT]->(f:Field)
22
+ WITH f ORDER BY f.name
23
+ RETURN {name: f.name, shape: f.shape, value: f.value}
24
+ } AS outputs,
25
+ COLLECT {
26
+ MATCH (n)-[:DEPENDS_ON]->(d:GbuildNode)
27
+ WITH d ORDER BY d.slug
28
+ RETURN d.slug
29
+ } AS depends_on,
30
+ COLLECT {
31
+ MATCH (n)-[:SATISFIES]->(a:Acceptance)
32
+ WITH a ORDER BY a.id
33
+ RETURN {id: a.id, text: a.text}
34
+ } AS satisfies,
35
+ COLLECT {
36
+ MATCH (n)-[:REVIEWED]->(r:Review)
37
+ WITH r ORDER BY r.attempt
38
+ RETURN {attempt: r.attempt, verdict: r.verdict, failed_criteria: r.failed_criteria, at: r.at}
39
+ } AS reviews;
@@ -0,0 +1,17 @@
1
+ // Record one gbuild-reviewer verdict as a Review node. `attempt` is assigned
2
+ // here (previous reviews + 1), so repair history is never lost.
3
+ // failed_criteria is the list of acceptance criteria the reviewer failed
4
+ // (pass `[]` on a pass).
5
+ // cypherlite .gbuild/<slug>/db -json --param slug=<node-slug> --param verdict=pass \
6
+ // --param 'failed_criteria=[]' < cypher/record-review.cypher
7
+ MATCH (n:GbuildNode {slug: $slug})
8
+ WHERE $verdict IN ['pass', 'fail']
9
+ WITH n, COUNT { (n)-[:REVIEWED]->(:Review) } + 1 AS attempt
10
+ CREATE (n)-[:REVIEWED]->(r:Review {
11
+ attempt: attempt,
12
+ verdict: $verdict,
13
+ failed_criteria: $failed_criteria,
14
+ at: toString(datetime())
15
+ })
16
+ RETURN n.slug AS slug, r.attempt AS attempt, r.verdict AS verdict;
17
+ .checkpoint
@@ -0,0 +1,45 @@
1
+ // gbuild store schema. Run once, when plan creates the store:
2
+ // cypherlite .gbuild/<slug>/db --snapshot-format json < cypher/schema.cypher
3
+ // Everything the engine can enforce at write time lives here; the rest of the
4
+ // contract is checked by validate.cypher.
5
+ CREATE CONSTRAINT feature_name FOR (n:Feature) REQUIRE n.feature IS :: STRING;
6
+ CREATE CONSTRAINT feature_name_exists FOR (n:Feature) REQUIRE n.feature IS NOT NULL;
7
+ CREATE CONSTRAINT feature_destination FOR (n:Feature) REQUIRE n.destination IS :: STRING;
8
+ CREATE CONSTRAINT feature_destination_exists FOR (n:Feature) REQUIRE n.destination IS NOT NULL;
9
+ CREATE CONSTRAINT feature_context FOR (n:Feature) REQUIRE n.context IS :: STRING;
10
+ CREATE CONSTRAINT feature_context_exists FOR (n:Feature) REQUIRE n.context IS NOT NULL;
11
+ CREATE CONSTRAINT feature_out_of_scope FOR (n:Feature) REQUIRE n.out_of_scope IS :: LIST<STRING NOT NULL>;
12
+ CREATE CONSTRAINT feature_out_of_scope_exists FOR (n:Feature) REQUIRE n.out_of_scope IS NOT NULL;
13
+ CREATE CONSTRAINT acceptance_id FOR (n:Acceptance) REQUIRE n.id IS UNIQUE;
14
+ CREATE CONSTRAINT acceptance_id_exists FOR (n:Acceptance) REQUIRE n.id IS NOT NULL;
15
+ CREATE CONSTRAINT acceptance_text FOR (n:Acceptance) REQUIRE n.text IS :: STRING;
16
+ CREATE CONSTRAINT acceptance_text_exists FOR (n:Acceptance) REQUIRE n.text IS NOT NULL;
17
+ CREATE CONSTRAINT node_slug FOR (n:GbuildNode) REQUIRE n.slug IS UNIQUE;
18
+ CREATE CONSTRAINT node_slug_exists FOR (n:GbuildNode) REQUIRE n.slug IS NOT NULL;
19
+ CREATE CONSTRAINT node_title FOR (n:GbuildNode) REQUIRE n.title IS :: STRING;
20
+ CREATE CONSTRAINT node_title_exists FOR (n:GbuildNode) REQUIRE n.title IS NOT NULL;
21
+ CREATE CONSTRAINT node_type FOR (n:GbuildNode) REQUIRE n.type IS :: STRING;
22
+ CREATE CONSTRAINT node_type_exists FOR (n:GbuildNode) REQUIRE n.type IS NOT NULL;
23
+ CREATE CONSTRAINT node_acceptance FOR (n:GbuildNode) REQUIRE n.acceptance IS :: LIST<STRING NOT NULL>;
24
+ CREATE CONSTRAINT node_acceptance_exists FOR (n:GbuildNode) REQUIRE n.acceptance IS NOT NULL;
25
+ CREATE CONSTRAINT node_failure_policy FOR (n:GbuildNode) REQUIRE n.failure_policy IS :: STRING;
26
+ CREATE CONSTRAINT node_failure_policy_exists FOR (n:GbuildNode) REQUIRE n.failure_policy IS NOT NULL;
27
+ CREATE CONSTRAINT node_model_tier FOR (n:GbuildNode) REQUIRE n.model_tier IS :: STRING;
28
+ CREATE CONSTRAINT node_model_tier_exists FOR (n:GbuildNode) REQUIRE n.model_tier IS NOT NULL;
29
+ CREATE CONSTRAINT node_status FOR (n:GbuildNode) REQUIRE n.status IS :: STRING;
30
+ CREATE CONSTRAINT node_status_exists FOR (n:GbuildNode) REQUIRE n.status IS NOT NULL;
31
+ CREATE CONSTRAINT node_verify FOR (n:GbuildNode) REQUIRE n.verify IS :: STRING;
32
+ CREATE CONSTRAINT node_started_at FOR (n:GbuildNode) REQUIRE n.started_at IS :: STRING;
33
+ CREATE CONSTRAINT node_completed_at FOR (n:GbuildNode) REQUIRE n.completed_at IS :: STRING;
34
+ CREATE CONSTRAINT field_name FOR (n:Field) REQUIRE n.name IS :: STRING;
35
+ CREATE CONSTRAINT field_name_exists FOR (n:Field) REQUIRE n.name IS NOT NULL;
36
+ CREATE CONSTRAINT field_shape FOR (n:Field) REQUIRE n.shape IS :: STRING;
37
+ CREATE CONSTRAINT field_shape_exists FOR (n:Field) REQUIRE n.shape IS NOT NULL;
38
+ CREATE CONSTRAINT review_attempt FOR (n:Review) REQUIRE n.attempt IS :: INTEGER;
39
+ CREATE CONSTRAINT review_attempt_exists FOR (n:Review) REQUIRE n.attempt IS NOT NULL;
40
+ CREATE CONSTRAINT review_verdict FOR (n:Review) REQUIRE n.verdict IS :: STRING;
41
+ CREATE CONSTRAINT review_verdict_exists FOR (n:Review) REQUIRE n.verdict IS NOT NULL;
42
+ CREATE CONSTRAINT review_at FOR (n:Review) REQUIRE n.at IS :: STRING;
43
+ CREATE CONSTRAINT review_at_exists FOR (n:Review) REQUIRE n.at IS NOT NULL;
44
+ CREATE CONSTRAINT review_failed_criteria FOR (n:Review) REQUIRE n.failed_criteria IS :: LIST<STRING NOT NULL>;
45
+ .checkpoint
@@ -0,0 +1,11 @@
1
+ // Move a node to a non-completed status: `cancelled` (skip policy), `failed`
2
+ // (escalate, or an exhausted retry/repair), or `pending` (reset a node an
3
+ // interrupted run left in_progress). `completed` is rejected here — only
4
+ // complete.cypher may set it, so it can enforce the review and outputs.
5
+ // cypherlite .gbuild/<slug>/db -json --param slug=<node-slug> --param status=cancelled \
6
+ // < cypher/set-status.cypher
7
+ MATCH (n:GbuildNode {slug: $slug})
8
+ WHERE $status IN ['pending', 'in_progress', 'failed', 'cancelled']
9
+ SET n.status = $status
10
+ RETURN n.slug AS slug, n.status AS status;
11
+ .checkpoint
@@ -0,0 +1,33 @@
1
+ // One row per GbuildNode, in wave order. `state` is derived, never stored:
2
+ // frontier = pending, every DEPENDS_ON target completed or cancelled
3
+ // blocked = pending, some DEPENDS_ON target not completed or cancelled
4
+ // otherwise = the stored status (in_progress, completed, failed, cancelled)
5
+ // `wave` is the longest DEPENDS_ON chain below the node (0 = no dependencies),
6
+ // so nodes sharing a wave are safe to run concurrently.
7
+ // cypherlite .gbuild/<slug>/db -json < cypher/status.cypher
8
+ MATCH (n:GbuildNode)
9
+ OPTIONAL MATCH p = (n)-[:DEPENDS_ON*]->()
10
+ WITH n, coalesce(max(length(p)), 0) AS wave
11
+ OPTIONAL MATCH (n)-[:DEPENDS_ON]->(d:GbuildNode)
12
+ WITH n, wave, d ORDER BY d.slug
13
+ WITH n, wave, [x IN collect(d) WHERE NOT x.status IN ['completed', 'cancelled'] | x.slug] AS waiting_on
14
+ OPTIONAL MATCH (n)-[:REVIEWED]->(r:Review)
15
+ WITH n, wave, waiting_on, r ORDER BY r.attempt DESC
16
+ WITH n, wave, waiting_on, collect(r) AS reviews
17
+ RETURN n.slug AS slug,
18
+ n.title AS title,
19
+ n.type AS type,
20
+ n.status AS status,
21
+ CASE
22
+ WHEN n.status = 'pending' AND size(waiting_on) = 0 THEN 'frontier'
23
+ WHEN n.status = 'pending' THEN 'blocked'
24
+ ELSE n.status
25
+ END AS state,
26
+ wave,
27
+ waiting_on,
28
+ size(reviews) AS review_attempts,
29
+ reviews[0].verdict AS last_verdict,
30
+ reviews[0].failed_criteria AS last_failed_criteria,
31
+ n.started_at AS started_at,
32
+ n.completed_at AS completed_at
33
+ ORDER BY wave, slug;
@@ -0,0 +1,105 @@
1
+ // gbuild format gate. Zero rows = valid; every row is one violation.
2
+ // cypherlite .gbuild/<slug>/db -json < cypher/validate.cypher
3
+ // Required properties, their types and slug/acceptance-id uniqueness are
4
+ // engine constraints (schema.cypher) — a write that breaks them never lands.
5
+ // This query checks everything the engine can't: enums, label/type agreement,
6
+ // edge shapes, coverage, acyclicity, and the data-flow (FROM) cut test.
7
+ CALL {
8
+ MATCH (f:Feature) WITH count(f) AS c WHERE c <> 1
9
+ RETURN 'feature-count' AS check, 'expected exactly 1 Feature node, found ' + toString(c) AS detail
10
+ UNION ALL
11
+ MATCH (n) WHERE NOT (n:Feature OR n:Acceptance OR n:GbuildNode OR n:Field OR n:Review)
12
+ RETURN 'unknown-node' AS check, 'node with labels [' + reduce(s = '', l IN labels(n) | s + ' ' + l) + ' ] is not part of the gbuild model' AS detail
13
+ UNION ALL
14
+ MATCH (n:GbuildNode) WHERE NOT n.slug =~ '^[a-z0-9]+(-[a-z0-9]+)*$'
15
+ RETURN 'bad-slug' AS check, n.slug + ': slug must be kebab-case' AS detail
16
+ UNION ALL
17
+ MATCH (n:GbuildNode) WHERE NOT n.type IN ['research', 'decision', 'code', 'test', 'verify', 'chore']
18
+ RETURN 'bad-type' AS check, n.slug + ': invalid type ' + n.type AS detail
19
+ UNION ALL
20
+ MATCH (n:GbuildNode) WHERE NOT n.failure_policy IN ['retry', 'fallback', 'skip', 'repair', 'escalate', 'stop']
21
+ RETURN 'bad-failure-policy' AS check, n.slug + ': invalid failure_policy ' + n.failure_policy AS detail
22
+ UNION ALL
23
+ MATCH (n:GbuildNode) WHERE NOT n.model_tier IN ['cheap', 'strong']
24
+ RETURN 'bad-model-tier' AS check, n.slug + ': invalid model_tier ' + n.model_tier AS detail
25
+ UNION ALL
26
+ MATCH (n:GbuildNode) WHERE NOT n.status IN ['pending', 'in_progress', 'completed', 'failed', 'cancelled']
27
+ RETURN 'bad-status' AS check, n.slug + ': invalid status ' + n.status AS detail
28
+ UNION ALL
29
+ WITH {research: 'Research', decision: 'Decision', code: 'Code', test: 'Test', verify: 'Verify', chore: 'Chore'} AS label
30
+ MATCH (n:GbuildNode)
31
+ WITH n, label[n.type] AS want,
32
+ [l IN labels(n) WHERE l IN ['Research', 'Decision', 'Code', 'Test', 'Verify', 'Chore']] AS have
33
+ WHERE want IS NOT NULL AND have <> [want]
34
+ RETURN 'type-label' AS check, n.slug + ': type ' + n.type + ' needs exactly the type label ' + want AS detail
35
+ UNION ALL
36
+ MATCH (n:GbuildNode) WHERE size(n.acceptance) = 0 OR any(c IN n.acceptance WHERE trim(c) = '')
37
+ RETURN 'bad-acceptance' AS check, n.slug + ': acceptance must be a non-empty list of non-blank criteria' AS detail
38
+ UNION ALL
39
+ MATCH (n:GbuildNode) WHERE n.verify IS NOT NULL AND NOT EXISTS { MATCH (v:GbuildNode) WHERE v.slug = n.verify }
40
+ RETURN 'verify-target' AS check, n.slug + ': verify names unknown node ' + n.verify AS detail
41
+ UNION ALL
42
+ MATCH (a:Acceptance) WHERE NOT a.id =~ '^a-[0-9]+$' OR trim(a.text) = ''
43
+ RETURN 'bad-acceptance-node' AS check, a.id + ': id must look like a-1 and text must be non-blank' AS detail
44
+ UNION ALL
45
+ MATCH (a:Acceptance) WHERE NOT EXISTS { (:Feature)-[:HAS_ACCEPTANCE]->(a) }
46
+ RETURN 'acceptance-unlinked' AS check, a.id + ': no HAS_ACCEPTANCE from the Feature' AS detail
47
+ UNION ALL
48
+ MATCH (a:Acceptance) WHERE NOT EXISTS { (:GbuildNode)-[:SATISFIES]->(a) }
49
+ RETURN 'acceptance-uncovered' AS check, a.id + ': no node SATISFIES it' AS detail
50
+ UNION ALL
51
+ MATCH (x)-[r]->(y)
52
+ WHERE NOT ((type(r) = 'HAS_ACCEPTANCE' AND x:Feature AND y:Acceptance)
53
+ OR (type(r) = 'SATISFIES' AND x:GbuildNode AND y:Acceptance)
54
+ OR (type(r) = 'DEPENDS_ON' AND x:GbuildNode AND y:GbuildNode)
55
+ OR (type(r) IN ['INPUT', 'OUTPUT'] AND x:GbuildNode AND y:Field)
56
+ OR (type(r) = 'FROM' AND x:Field AND y:Field)
57
+ OR (type(r) = 'REVIEWED' AND x:GbuildNode AND y:Review))
58
+ RETURN 'bad-edge' AS check,
59
+ type(r) + ' from ' + coalesce(x.slug, x.id, x.name, '?') + ' to ' + coalesce(y.slug, y.id, y.name, '?') + ' is not an allowed edge' AS detail
60
+ UNION ALL
61
+ MATCH (n:GbuildNode) WHERE EXISTS { (n)-[:DEPENDS_ON*]->(n) }
62
+ RETURN 'cycle' AS check, n.slug + ': DEPENDS_ON cycle' AS detail
63
+ UNION ALL
64
+ MATCH (n:GbuildNode {type: 'chore'}) WHERE NOT EXISTS { ()-[:DEPENDS_ON]->(n) }
65
+ RETURN 'orphan-chore' AS check, n.slug + ': chore is not depended on by any node' AS detail
66
+ UNION ALL
67
+ MATCH (n:GbuildNode) WHERE NOT EXISTS { (n)-[:OUTPUT]->(:Field) }
68
+ RETURN 'no-output' AS check, n.slug + ': declares no OUTPUT field' AS detail
69
+ UNION ALL
70
+ MATCH (f:Field) WITH f, COUNT { ()-[:INPUT|OUTPUT]->(f) } AS owners WHERE owners <> 1
71
+ RETURN 'field-owner' AS check, f.name + ': field has ' + toString(owners) + ' owning nodes, want 1' AS detail
72
+ UNION ALL
73
+ MATCH (n:GbuildNode)-[r:INPUT|OUTPUT]->(f:Field)
74
+ WITH n, type(r) AS dir, f.name AS name, count(*) AS c WHERE c > 1
75
+ RETURN 'duplicate-field' AS check, n.slug + ': ' + dir + ' field ' + name + ' declared ' + toString(c) + ' times' AS detail
76
+ UNION ALL
77
+ MATCH (i:Field)-[:FROM]->(o:Field)
78
+ WHERE NOT EXISTS { (:GbuildNode)-[:INPUT]->(i) } OR NOT EXISTS { (:GbuildNode)-[:OUTPUT]->(o) }
79
+ RETURN 'bad-from' AS check, i.name + ' -> ' + o.name + ': FROM must run from an INPUT field to an OUTPUT field' AS detail
80
+ UNION ALL
81
+ MATCH (i:Field) WITH i, COUNT { (i)-[:FROM]->() } AS c WHERE c > 1
82
+ RETURN 'multi-from' AS check, i.name + ': input field has ' + toString(c) + ' FROM edges, want at most 1' AS detail
83
+ UNION ALL
84
+ MATCH (n:GbuildNode)-[:INPUT]->(i:Field)-[:FROM]->(:Field)<-[:OUTPUT]-(u:GbuildNode)
85
+ WHERE NOT EXISTS { (n)-[:DEPENDS_ON]->(u) }
86
+ RETURN 'from-without-dependency' AS check, n.slug + '.' + i.name + ' reads ' + u.slug + ' but does not DEPENDS_ON it' AS detail
87
+ UNION ALL
88
+ MATCH (n:GbuildNode)-[:DEPENDS_ON]->(u:GbuildNode)
89
+ WHERE NOT EXISTS { (n)-[:INPUT]->(:Field)-[:FROM]->(:Field)<-[:OUTPUT]-(u) }
90
+ RETURN 'dependency-without-from' AS check, n.slug + ' DEPENDS_ON ' + u.slug + ' but reads none of its outputs (cut test)' AS detail
91
+ UNION ALL
92
+ MATCH (n:GbuildNode {status: 'completed'}) WHERE NOT EXISTS { (n)-[:REVIEWED]->(:Review {verdict: 'pass'}) }
93
+ RETURN 'completed-without-pass' AS check, n.slug + ': completed without a passing Review' AS detail
94
+ UNION ALL
95
+ MATCH (n:GbuildNode {status: 'completed'})-[:OUTPUT]->(f:Field) WHERE f.value IS NULL
96
+ RETURN 'completed-missing-output' AS check, n.slug + '.' + f.name + ': completed node has no value for this output' AS detail
97
+ UNION ALL
98
+ MATCH (r:Review) WHERE NOT r.verdict IN ['pass', 'fail'] OR r.attempt < 1
99
+ RETURN 'bad-review' AS check, 'review attempt ' + toString(r.attempt) + ' has verdict ' + r.verdict AS detail
100
+ UNION ALL
101
+ MATCH (r:Review) WITH r, COUNT { (:GbuildNode)-[:REVIEWED]->(r) } AS c WHERE c <> 1
102
+ RETURN 'review-owner' AS check, 'review attempt ' + toString(r.attempt) + ' has ' + toString(c) + ' owning nodes, want 1' AS detail
103
+ }
104
+ RETURN check, detail
105
+ ORDER BY check, detail;
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.6.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",
@@ -24,7 +24,7 @@
24
24
  "skills",
25
25
  "agents",
26
26
  "reference",
27
- "scripts",
27
+ "cypher",
28
28
  "templates"
29
29
  ],
30
30
  "devDependencies": {