opencode-gbuild 0.5.0 → 0.6.1

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:
@@ -91,13 +93,13 @@ OpenCode (same order, kebab-case names):
91
93
  /gbuild-pr add-oauth-login-with-github
92
94
  ```
93
95
 
94
- - **`plan`** decomposes a feature into `.gbuild/<feature>/db/graph.json` — a plain JSON graph file with
95
- `Feature`, `Acceptance`, and `GbuildNode` nodes linked by `DEPENDS_ON`/`HAS_ACCEPTANCE`/`SATISFIES`
96
- relationships. Every dependency edge passes the cut test (does the dependent actually read the
97
- dependency's output?); every node has mandatory concrete acceptance criteria. Validated with
98
- `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.
99
101
  - **`run`** dispatches every node in the current ready wave concurrently — not one at a time — reviews
100
- 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
101
103
  applies the node's declared failure policy on a review failure. Re-invoking after an interruption
102
104
  resumes only the remaining frontier.
103
105
  - **`status`** reports the graph — frontier, blocked, in-flight, completed, each node's review verdict,
@@ -119,16 +121,15 @@ incoming edge, and a controlled cycle gets a hard round cap instead of an open-e
119
121
 
120
122
  ## Node statuses
121
123
 
122
- A node's status lives in its own checkpoint file, `.gbuild/<feature>/nodes/<slug>.json` (keyed by the
123
- GbuildNode's `slug` property — the stable identity skills and checkpoints key on, since the graph file's
124
- integer `id` is authoring convenience). There are five,
125
- 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:
126
127
 
127
128
  | status | means | why it exists |
128
129
  | ------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
129
- | `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. |
130
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. |
131
- | `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. |
132
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. |
133
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. |
134
135
 
@@ -138,7 +139,7 @@ without me*, `failed` says *stop and wait*. Collapsing them into one "didn't wor
138
139
  `run` to guess which one a given failure meant.
139
140
 
140
141
  Everything else the tooling reports — `frontier`, `blocked`, `in_flight`, `waves` — is **derived** from
141
- 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
142
143
  frontier is a query result rather than a judgement call:
143
144
 
144
145
  ```
@@ -151,12 +152,12 @@ in_flight = in_progress
151
152
 
152
153
  Subagents are forked, so they cannot talk to you directly — a subagent's final message is a tool result,
153
154
  not chat output. Anything it finds that needs a human decision therefore travels a specific path:
154
- 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
155
156
  resolved by the agent that found it, and nothing gets routed around.
156
157
 
157
158
  Three things can trigger it:
158
159
 
159
- - **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
160
161
  its dependents (siblings elsewhere in the graph keep going), and surfaces it as needing a decision.
161
162
  `repair` escalates the same way once its 2 attempts are spent; `retry` and `fallback` are capped at 2
162
163
  attempts too, so no policy can loop unbounded. See `reference/failure-policies.md`.
@@ -175,24 +176,35 @@ decision is wanted; neither guesses.
175
176
 
176
177
  ## Runtime
177
178
 
178
- `scripts/graph.py` and `scripts/validate_format.py` are Python 3, stdlib only — no `pip install`
179
- required. That's also why the graph file is `graph.json` rather than YAML: Python's stdlib has no YAML
180
- parser, and adding PyYAML would defeat the zero-extra-dependencies goal. The file is written and read by
181
- 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:
180
+
181
+ ```bash
182
+ curl -sSfL https://neo4j-labs.github.io/cypherlite/install.sh | bash
183
+ ```
182
184
 
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
+ 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:
185
187
 
186
188
  ```bash
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
189
- python3 -m unittest plugins/gbuild/scripts/test_graph.py
189
+ cypherlite .gbuild/<feature>/db --mode jsonl < plugins/gbuild/cypher/validate.cypher # format gate: no output = valid
190
+ cypherlite .gbuild/<feature>/db --mode jsonl < plugins/gbuild/cypher/progress.cypher # one row per wave: counts + open nodes
191
+ cypherlite .gbuild/<feature>/db --mode jsonl "MATCH (n:GbuildNode {status: 'failed'}) RETURN n.slug" # anything else
190
192
  ```
191
193
 
192
- `validate_format.py` does stdlib JSON checks always; if the `cypherlite` binary is on PATH it *also*
193
- opens the graph with CypherLite to confirm the real loader accepts it (catches serde issues the stdlib
194
- walk can't). CypherLite is an optional consumer, not a dependency — absent binary → cross-check skipped
195
- silently; `--no-cypherlite` forces the skip.
194
+ Reads are deliberately narrow so a run doesn't pay for the whole graph on every step: `run` claims a
195
+ wave (`dispatch.cypher` returns only that wave's nodes, with their inputs resolved) and records it
196
+ (`record.cypher`, one batched call) — two store calls per wave, regardless of graph size. The full
197
+ per-node view (`status.cypher`) is only for the human-facing `/gbuild:status`.
198
+
199
+ Required properties and their types are engine constraints, so an invalid write never lands; the rest of
200
+ the contract (enums, edge shapes, coverage, acyclicity, the cut test) is `validate.cypher`. CypherLite
201
+ locks a store while a process has it open, so only the main-context skill touches it, one command at a
202
+ time — subagents get their inputs in their prompt and report back. `reference/cypher.md` has the full
203
+ operating rules.
204
+
205
+ Tests (repo-root `npm test`) run the Cypher layer against the real binary: `cypher.test.ts` drives the
206
+ worked example through a full run, checks every validator rule and write guard, and holds a token budget
207
+ — a 40-node run must take two store calls per wave and stay under a fixed number of bytes read.
196
208
 
197
209
  ## Layout
198
210
 
@@ -200,17 +212,17 @@ silently; `--no-cypherlite` forces the skip.
200
212
  plugins/gbuild/
201
213
  agents/gbuild-reviewer.md # per-node review, ported from hone-ai's reviewer, never self-review
202
214
  agents/gbuild-auditor.md # end-of-branch maintainability audit, ported from hone-ai's auditor
203
- reference/ # graph-format.md is normative; shapes/failure-policies/cost-model/checklist inform plan
204
- scripts/validate_format.py # the authoritative format contract — checks the GraphData file is CypherLite-loadable + gbuild-correct
205
- scripts/graph.py # query layer: topo-sort into waves, compute frontier/blocked/in-flight
206
- templates/graph.json # a worked 4-node diamond example (CypherLite GraphData)
215
+ reference/ # graph-format.md (normative model), cypher.md (how to talk to the store), queries.md (ad-hoc recipes); shapes/failure-policies/cost-model/checklist inform plan
216
+ cypher/ # schema, validate, progress/attention/status/node/feature reads, and run's dispatch/record/set-status
217
+ templates/example.cypher # a worked 4-node diamond, written the way plan writes a feature
207
218
  skills/{plan,run,status,review,pr}/
208
219
  index.ts # OpenCode plugin entry: registers the skills + /gbuild-* commands, translating paths/names
209
220
  package.json # OpenCode plugin manifest (npm package "opencode-gbuild")
210
- smoke.test.ts # node --test smoke test for the OpenCode translation (repo-root `npm test`)
221
+ cypher.test.ts # node --test contract tests for the Cypher layer (repo-root `npm test`)
222
+ smoke.test.ts # node --test smoke test for the OpenCode translation
211
223
  ```
212
224
 
213
- State lives outside the plugin, in the project: `.gbuild/<feature>/db/graph.json` (a CypherLite GraphData
214
- file, written once by `plan`) and `.gbuild/<feature>/nodes/<slug>.json` (one checkpoint per node,
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.
225
+ State lives outside the plugin, in the project: one CypherLite store per feature at
226
+ `.gbuild/<feature>/db/`. It holds the plan, every node's status and output values, and the full review
227
+ history. Only `db/graph.json` (CypherLite's JSON snapshot, kept current by every write) and the store's
228
+ 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>/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.
10
+ The caller passes you the resolved feature slug and the feature context it read from the gbuild store: the `destination` and the acceptance bar (each criterion with the nodes covering it). 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 `.gbuild/<slug>/db/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,20 @@
1
+ // Only the nodes that need a decision — no rows means nothing does.
2
+ // failed escalated or exhausted; `detail` = the last review's failed criteria
3
+ // in_progress claimed but not recorded; if no run is active, an interrupted wave left it
4
+ // blocked-by-failed pending behind a failed node, so it can never unblock on its own;
5
+ // `detail` = the failed nodes it waits on
6
+ // cypherlite .gbuild/<slug>/db --mode jsonl < cypher/attention.cypher
7
+ CALL {
8
+ MATCH (n:GbuildNode {status: 'failed'})
9
+ RETURN n.slug AS slug, 'failed' AS reason,
10
+ COLLECT { MATCH (n)-[:REVIEWED]->(r:Review) WITH r ORDER BY r.attempt DESC LIMIT 1 UNWIND r.failed_criteria AS c RETURN c } AS detail
11
+ UNION ALL
12
+ MATCH (n:GbuildNode {status: 'in_progress'})
13
+ RETURN n.slug AS slug, 'in_progress' AS reason, [] AS detail
14
+ UNION ALL
15
+ MATCH (n:GbuildNode {status: 'pending'})-[:DEPENDS_ON*]->(f:GbuildNode {status: 'failed'})
16
+ WITH n, f ORDER BY f.slug
17
+ RETURN n.slug AS slug, 'blocked-by-failed' AS reason, collect(DISTINCT f.slug) AS detail
18
+ }
19
+ RETURN slug, reason, detail
20
+ ORDER BY reason, slug;
@@ -0,0 +1,31 @@
1
+ // Claim the next wave: move every frontier node (pending, every DEPENDS_ON
2
+ // target completed or cancelled) to in_progress, and return exactly what its
3
+ // implementing agent needs — nothing about the rest of the graph. An input
4
+ // with a `value` was read from an upstream node; one with a null value is
5
+ // external, and its `shape` says where to find it. No rows = nothing is
6
+ // ready (the graph is done or stuck — ask progress.cypher / attention.cypher).
7
+ // For a router, cancel the unselected branches with set-status.cypher first.
8
+ // cypherlite .gbuild/<slug>/db --mode jsonl < cypher/dispatch.cypher
9
+ MATCH (n:GbuildNode {status: 'pending'})
10
+ WHERE NOT EXISTS { MATCH (n)-[:DEPENDS_ON]->(d:GbuildNode) WHERE NOT d.status IN ['completed', 'cancelled'] }
11
+ SET n.status = 'in_progress'
12
+ RETURN n.slug AS slug,
13
+ n.title AS title,
14
+ n.type AS type,
15
+ n.acceptance AS acceptance,
16
+ n.failure_policy AS failure_policy,
17
+ n.model_tier AS model_tier,
18
+ n.verify AS verify,
19
+ COLLECT {
20
+ MATCH (n)-[:INPUT]->(i:Field)
21
+ OPTIONAL MATCH (i)-[:FROM]->(o:Field)
22
+ WITH i, o ORDER BY i.name
23
+ RETURN {name: i.name, shape: i.shape, value: o.value}
24
+ } AS inputs,
25
+ COLLECT {
26
+ MATCH (n)-[:OUTPUT]->(f:Field)
27
+ WITH f ORDER BY f.name
28
+ RETURN {name: f.name, shape: f.shape}
29
+ } AS outputs
30
+ ORDER BY slug;
31
+ .checkpoint
@@ -0,0 +1,16 @@
1
+ // Feature-level context: the destination and the acceptance bar, with the
2
+ // nodes that cover each criterion. What review hands the auditor and what pr
3
+ // builds the description from. Per-node output values are not included — get
4
+ // one node's with node.cypher if you actually need it.
5
+ // cypherlite .gbuild/<slug>/db --mode jsonl < cypher/feature.cypher
6
+ MATCH (f:Feature)
7
+ RETURN f.feature AS feature,
8
+ f.destination AS destination,
9
+ f.context AS context,
10
+ f.out_of_scope AS out_of_scope,
11
+ COLLECT {
12
+ MATCH (f)-[:HAS_ACCEPTANCE]->(a:Acceptance)
13
+ WITH a ORDER BY a.id
14
+ RETURN {id: a.id, text: a.text,
15
+ covered_by: COLLECT { MATCH (a)<-[:SATISFIES]-(n:GbuildNode) WITH n ORDER BY n.slug RETURN n.slug }}
16
+ } AS acceptance;
@@ -0,0 +1,41 @@
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
+ // Used for reopen and ad-hoc inspection; run gets its definitions from
5
+ // dispatch.cypher instead.
6
+ // cypherlite .gbuild/<slug>/db --mode jsonl --param 'slug="<node-slug>"' < cypher/node.cypher
7
+ MATCH (n:GbuildNode {slug: $slug})
8
+ RETURN n.slug AS slug,
9
+ n.title AS title,
10
+ n.type AS type,
11
+ n.status AS status,
12
+ n.acceptance AS acceptance,
13
+ n.verify AS verify,
14
+ n.failure_policy AS failure_policy,
15
+ n.model_tier AS model_tier,
16
+ COLLECT {
17
+ MATCH (n)-[:INPUT]->(i:Field)
18
+ OPTIONAL MATCH (i)-[:FROM]->(o:Field)<-[:OUTPUT]-(u:GbuildNode)
19
+ WITH i, o, u ORDER BY i.name
20
+ RETURN {name: i.name, shape: i.shape, from: u.slug + '.output.' + o.name, value: o.value}
21
+ } AS inputs,
22
+ COLLECT {
23
+ MATCH (n)-[:OUTPUT]->(f:Field)
24
+ WITH f ORDER BY f.name
25
+ RETURN {name: f.name, shape: f.shape, value: f.value}
26
+ } AS outputs,
27
+ COLLECT {
28
+ MATCH (n)-[:DEPENDS_ON]->(d:GbuildNode)
29
+ WITH d ORDER BY d.slug
30
+ RETURN d.slug
31
+ } AS depends_on,
32
+ COLLECT {
33
+ MATCH (n)-[:SATISFIES]->(a:Acceptance)
34
+ WITH a ORDER BY a.id
35
+ RETURN {id: a.id, text: a.text}
36
+ } AS satisfies,
37
+ COLLECT {
38
+ MATCH (n)-[:REVIEWED]->(r:Review)
39
+ WITH r ORDER BY r.attempt
40
+ RETURN {attempt: r.attempt, verdict: r.verdict, failed_criteria: r.failed_criteria, at: r.at}
41
+ } AS reviews;
@@ -0,0 +1,25 @@
1
+ // One compact row per wave: node counts by state, and `open` — the slugs in
2
+ // that wave not yet completed or cancelled. A wave with `open: []` is done;
3
+ // the graph is done when every row's `open` is empty. `wave` is the longest
4
+ // DEPENDS_ON chain below a node (0 = no dependencies).
5
+ // cypherlite .gbuild/<slug>/db --mode jsonl < cypher/progress.cypher
6
+ MATCH (n:GbuildNode)
7
+ OPTIONAL MATCH p = (n)-[:DEPENDS_ON*]->()
8
+ WITH n, coalesce(max(length(p)), 0) AS wave
9
+ WITH n, wave,
10
+ CASE
11
+ WHEN n.status <> 'pending' THEN n.status
12
+ WHEN EXISTS { MATCH (n)-[:DEPENDS_ON]->(d:GbuildNode) WHERE NOT d.status IN ['completed', 'cancelled'] } THEN 'blocked'
13
+ ELSE 'frontier'
14
+ END AS state
15
+ ORDER BY n.slug
16
+ RETURN wave,
17
+ count(*) AS total,
18
+ sum(CASE WHEN state = 'completed' THEN 1 ELSE 0 END) AS completed,
19
+ sum(CASE WHEN state = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
20
+ sum(CASE WHEN state = 'in_progress' THEN 1 ELSE 0 END) AS in_progress,
21
+ sum(CASE WHEN state = 'failed' THEN 1 ELSE 0 END) AS failed,
22
+ sum(CASE WHEN state = 'frontier' THEN 1 ELSE 0 END) AS frontier,
23
+ sum(CASE WHEN state = 'blocked' THEN 1 ELSE 0 END) AS blocked,
24
+ [s IN collect(CASE WHEN NOT state IN ['completed', 'cancelled'] THEN n.slug END) WHERE s IS NOT NULL] AS open
25
+ ORDER BY wave;
@@ -0,0 +1,38 @@
1
+ // Record a whole wave's review verdicts in one atomic call. Each result adds a
2
+ // Review (attempt = previous reviews + 1). A pass with a value for every
3
+ // OUTPUT field also completes the node: output values stored on its fields,
4
+ // the implementing agent's own times on the node. A pass missing outputs is
5
+ // recorded as a fail listing them, so the review history stays truthful.
6
+ // Only in_progress nodes are recorded: compare the returned slugs with what
7
+ // you sent. Output values must be scalars or lists of scalars — one nested
8
+ // value rejects the whole batch (InvalidPropertyType); JSON-encode it and
9
+ // resend.
10
+ // cypherlite .gbuild/<slug>/db --mode jsonl --param 'results=[
11
+ // {"slug": "a", "verdict": "pass", "failed_criteria": [], "outputs": {"value": 7},
12
+ // "started_at": "<iso>", "completed_at": "<iso>"},
13
+ // {"slug": "b", "verdict": "fail", "failed_criteria": ["<criterion>"]}
14
+ // ]' < cypher/record.cypher
15
+ UNWIND $results AS r
16
+ MATCH (n:GbuildNode {slug: r.slug})
17
+ WHERE n.status = 'in_progress' AND r.verdict IN ['pass', 'fail']
18
+ WITH n, r,
19
+ COUNT { (n)-[:REVIEWED]->(:Review) } + 1 AS attempt,
20
+ CASE WHEN r.verdict = 'pass'
21
+ THEN [k IN COLLECT { MATCH (n)-[:OUTPUT]->(f:Field) RETURN f.name } WHERE r.outputs[k] IS NULL]
22
+ ELSE [] END AS missing
23
+ WITH n, r, attempt, missing, (r.verdict = 'pass' AND size(missing) = 0) AS ok
24
+ CREATE (n)-[:REVIEWED]->(review:Review {
25
+ attempt: attempt,
26
+ verdict: CASE WHEN ok THEN 'pass' ELSE 'fail' END,
27
+ failed_criteria: coalesce(r.failed_criteria, []) + [k IN missing | 'missing output value: ' + k],
28
+ at: toString(datetime())
29
+ })
30
+ WITH n, r, attempt, missing, ok, review
31
+ OPTIONAL MATCH (n)-[:OUTPUT]->(f:Field) WHERE ok
32
+ SET f.value = r.outputs[f.name]
33
+ WITH DISTINCT n, r, attempt, missing, ok, review
34
+ FOREACH (_ IN CASE WHEN ok THEN [1] ELSE [] END |
35
+ SET n.status = 'completed', n.started_at = r.started_at, n.completed_at = r.completed_at)
36
+ RETURN n.slug AS slug, attempt, review.verdict AS verdict, ok AS completed, missing AS missing_outputs
37
+ ORDER BY slug;
38
+ .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,14 @@
1
+ // Move nodes to a non-completed status in one call: `cancelled` (skip policy,
2
+ // or a router's unselected branches), `failed` (escalate, an exhausted
3
+ // retry/repair, stop), or `pending` (reset nodes an interrupted run left
4
+ // in_progress). `completed` is rejected — only record.cypher may set it, so it
5
+ // can require a passing review and every output value.
6
+ // cypherlite .gbuild/<slug>/db --mode jsonl --param 'slugs=["a","b"]' --param 'status="cancelled"' \
7
+ // < cypher/set-status.cypher
8
+ UNWIND $slugs AS s
9
+ MATCH (n:GbuildNode {slug: s})
10
+ WHERE $status IN ['pending', 'in_progress', 'failed', 'cancelled']
11
+ SET n.status = $status
12
+ RETURN n.slug AS slug, n.status AS status
13
+ ORDER BY slug;
14
+ .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 --mode jsonl < 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 --mode jsonl < 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.5.0",
4
+ "version": "0.6.1",
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": {