opencode-gbuild 0.5.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:
@@ -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,29 @@ 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:
182
180
 
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/`.
181
+ ```bash
182
+ curl -sSfL https://neo4j-labs.github.io/cypherlite/install.sh | bash
183
+ ```
184
+
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 -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
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
+ 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.
196
202
 
197
203
  ## Layout
198
204
 
@@ -200,17 +206,17 @@ silently; `--no-cypherlite` forces the skip.
200
206
  plugins/gbuild/
201
207
  agents/gbuild-reviewer.md # per-node review, ported from hone-ai's reviewer, never self-review
202
208
  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)
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
207
212
  skills/{plan,run,status,review,pr}/
208
213
  index.ts # OpenCode plugin entry: registers the skills + /gbuild-* commands, translating paths/names
209
214
  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`)
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
211
217
  ```
212
218
 
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.
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>/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`, 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 `.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,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.5.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": {
@@ -1,31 +1,31 @@
1
1
  # Ship checklist
2
2
 
3
- `plan` runs this checklist against the graph it just wrote, before committing `graph.json`. Any item
4
- that fails means revise the graph, not add a caveat in prose.
3
+ `plan` runs this checklist against the graph it just wrote, before committing the store. Any item that
4
+ fails means revise the graph (with Cypher), not add a caveat in prose.
5
5
 
6
6
  1. **Risky assumptions are resolved, not deferred.** Every assumption that would invalidate nodes or
7
7
  edges if wrong has either been confirmed against the codebase/docs, or is charted as a `research`
8
8
  node that answers it. An assumption left as "probably fine" is a hole.
9
- 2. **Every edge passes the cut test.** For each `DEPENDS_ON` relationship, the dependent node actually
10
- reads something the dependency produced. No edge exists just because one node was thought of after
11
- another.
12
- 3. **Independent nodes are actually in the same wave.** Run `python3 scripts/graph.py <path> --waves`
13
- and confirm nodes with no real edge between them land together, not in a chain.
14
- 4. **Every node has non-empty, concrete acceptance criteria.** `validate_format.py` enforces non-empty;
15
- `plan` itself is responsible for concrete — no `"looks correct"`, no criterion a fresh context
16
- couldn't check unaided.
17
- 5. **Every node's `contract.output` is structured, not free text.** A downstream node's `contract.input`
18
- should be able to reference it directly.
9
+ 2. **Every edge passes the cut test.** For each `DEPENDS_ON`, the dependent reads one of the
10
+ dependency's `OUTPUT` fields through a `FROM` edge. `validate.cypher` rejects an edge with no data
11
+ behind it and data with no edge behind it — but an agent can still invent a field just to satisfy it,
12
+ so ask honestly whether the dependent *needs* that value.
13
+ 3. **Independent nodes are actually in the same wave.** Read `wave` from `status.cypher` and confirm
14
+ nodes with no real edge between them land together, not in a chain.
15
+ 4. **Every node has non-empty, concrete acceptance criteria.** The schema requires a list and
16
+ `validate.cypher` rejects blanks; `plan` itself is responsible for concrete — no `"looks correct"`, no
17
+ criterion a fresh context couldn't check unaided.
18
+ 5. **Every `OUTPUT` field has a precise `shape`**, not free text. Downstream `INPUT` fields read it
19
+ through `FROM` rather than re-describing it.
19
20
  6. **Every `Acceptance` node is `SATISFIES`-covered** by at least one GbuildNode. An uncovered
20
- feature-level acceptance bullet means a node is missing. (`validate_format.py` enforces this.)
21
+ feature-level acceptance bullet means a node is missing. (`validate.cypher` enforces this.)
21
22
  7. **No node restates a global acceptance bullet verbatim as its own criterion.** Node-level acceptance
22
23
  is the local, observable slice; the `Acceptance` nodes keep the whole-feature bar.
23
24
  8. **Every controlled cycle has a hard round cap and a dedup rule**, stated in the node(s) that form the
24
25
  cycle — not left implicit.
25
- 9. **`DEPENDS_ON` is acyclic.** `validate_format.py` (and `graph.py --waves`) reject a cycle; confirm
26
- they don't.
26
+ 9. **`DEPENDS_ON` is acyclic.** `validate.cypher` rejects a cycle; confirm it doesn't.
27
27
  10. **Every `chore` node is depended on by at least one other node.** A chore nobody depends on isn't in
28
- scope — it's either wired wrong or shouldn't exist. (`validate_format.py` enforces this.)
28
+ scope — it's either wired wrong or shouldn't exist. (`validate.cypher` enforces this.)
29
29
  11. **`failure_policy` fits the node type**, per `reference/failure-policies.md` — not defaulted to
30
30
  `retry` everywhere without thinking about it.
31
31
  12. **`model_tier` is `strong` only where judgement, not throughput, is the bottleneck** — see