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.
@@ -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
@@ -0,0 +1,102 @@
1
+ # Talking to the gbuild store
2
+
3
+ Every gbuild skill reads and writes the feature store (`.gbuild/<feature>/db/`, modelled in
4
+ `reference/graph-format.md`) with the `cypherlite` CLI and nothing else. This file is the operating
5
+ manual; follow it exactly.
6
+
7
+ ## Requirement
8
+
9
+ `cypherlite` 0.7.0 or newer must be on PATH. Check first, in every skill:
10
+
11
+ ```
12
+ cypherlite --version
13
+ ```
14
+
15
+ If the command is missing, stop and tell the user gbuild needs it:
16
+ `curl -sSfL https://neo4j-labs.github.io/cypherlite/install.sh | bash`. Never fall back to reading or
17
+ editing `graph.json` by hand.
18
+
19
+ ## One writer, one command at a time
20
+
21
+ CypherLite takes an exclusive lock on the store while a process has it open. A second process — reader
22
+ or writer — fails with `storage locked` (exit 1). So:
23
+
24
+ - **Only the skill running in the main context runs `cypherlite` against the store.** Subagents never
25
+ do: give them what they need in their prompt, have them report results in their final message, and
26
+ record those results yourself.
27
+ - **Never run two `cypherlite` commands against the same store at once** — no parallel tool calls on it.
28
+ Fan-out is for agents, not for store access.
29
+ - On `storage locked`, another process holds the store (another gbuild command, a REPL left open). Wait a
30
+ few seconds and retry, up to 3 times; then stop and report. Never delete `.cypherlite.lock`.
31
+
32
+ ## Invocation
33
+
34
+ Always pass `-json` when you need the result. `DB` below is `.gbuild/<feature>/db`.
35
+
36
+ ```
37
+ # a plugin script
38
+ cypherlite DB -json < ${CLAUDE_PLUGIN_ROOT}/cypher/status.cypher
39
+
40
+ # a plugin script with parameters
41
+ cypherlite DB -json --param 'slug="b-consume-doubled"' < ${CLAUDE_PLUGIN_ROOT}/cypher/node.cypher
42
+
43
+ # an ad-hoc read
44
+ cypherlite DB -json "MATCH (n:GbuildNode) RETURN n.slug, n.status ORDER BY n.slug LIMIT 100"
45
+
46
+ # a write you author (plan, reopen, fixes) — quoted heredoc delimiter, so the shell expands nothing
47
+ cypherlite DB <<'CYPHER'
48
+ .begin
49
+ MATCH (n:GbuildNode {slug: 'b-consume-doubled'}) SET n.title = 'Consume the value, double it';
50
+ .commit
51
+ .checkpoint
52
+ CYPHER
53
+ ```
54
+
55
+ ## Rules
56
+
57
+ - **Parameters are JSON.** Every `--param` value is JSON, single-quoted for the shell: strings in double
58
+ quotes (`--param 'slug="01-scan-runners"'`), lists and objects as JSON (`--param 'slugs=["a","b"]'`,
59
+ `--param 'outputs={"value": 7}'`). Bare values that start with a digit — timestamps, `01-…` slugs —
60
+ are rejected, so always quote.
61
+ - **String literals** in Cypher you author: single quotes by default; double quotes when the text
62
+ contains an apostrophe (`"gbuild's"`); backslash-escape (`\'`, `\"`) when it contains both.
63
+ - **Authored writes are atomic and checkpointed.** Wrap them in `.begin` / `.commit` — if any statement
64
+ fails, the whole transaction is discarded — and end with `.checkpoint` so `graph.json` on disk (what
65
+ git commits) is always current. The plugin write scripts already do both.
66
+ - **Values are scalars or lists of scalars.** CypherLite cannot store a map as a property value;
67
+ JSON-encode anything nested into a string.
68
+ - **Read the result.** The plugin write scripts are guarded: a write that did nothing returns `[]` (or
69
+ `completed: false` with the reason). Never assume a write landed.
70
+ - **Exit code 1 is a failure** — lock, constraint violation (`ConstraintValidationFailed`), syntax. Read
71
+ stderr, fix, retry the whole transaction.
72
+ - A one-shot write without `.checkpoint` still lands (reads replay the WAL), but `graph.json` stays stale
73
+ until the next checkpoint — so always end writes with it.
74
+
75
+ ## Creating a store
76
+
77
+ Only `plan` does this, once per feature:
78
+
79
+ ```
80
+ mkdir -p .gbuild/<feature>/db
81
+ cypherlite .gbuild/<feature>/db --snapshot-format json < ${CLAUDE_PLUGIN_ROOT}/cypher/schema.cypher
82
+ printf '*\n!.gitignore\n!graph.json\n' > .gbuild/<feature>/db/.gitignore
83
+ ```
84
+
85
+ `--snapshot-format json` belongs on this first call only (it applies to an empty directory). It installs
86
+ the schema constraints and pins the JSON codec, so `graph.json` exists from the start.
87
+
88
+ ## Script catalog
89
+
90
+ All in `${CLAUDE_PLUGIN_ROOT}/cypher/`. Reads print one JSON array; writes end with `.checkpoint`.
91
+
92
+ | script | params | returns | used by |
93
+ | -------------------- | ---------------------------------------- | -------------------------------------------------------------- | ------- |
94
+ | `schema.cypher` | — | nothing (installs constraints) | plan |
95
+ | `validate.cypher` | — | one row per violation (`check`, `detail`); `[]` = valid | every skill |
96
+ | `status.cypher` | — | one row per node: `slug`, `title`, `type`, `status`, `state` (`frontier`/`blocked`/stored status), `wave`, `waiting_on`, `review_attempts`, `last_verdict`, `last_failed_criteria`, `started_at`, `completed_at` | every skill |
97
+ | `node.cypher` | `slug` | one node: properties, `inputs` (resolved through `FROM`: `name`, `shape`, `from`, `value`), `outputs`, `depends_on`, `satisfies`, `reviews` | run |
98
+ | `feature.cypher` | — | `destination`, `context`, `out_of_scope`, `acceptance` (with `covered_by`), every node's `outputs` | review, pr |
99
+ | `dispatch.cypher` | `slugs` (list) | `dispatched` — only pending nodes whose dependencies are satisfied | run |
100
+ | `record-review.cypher` | `slug`, `verdict` (`pass`/`fail`), `failed_criteria` (list) | `attempt`, `verdict` | run |
101
+ | `complete.cypher` | `slug`, `outputs` (object), `started_at`, `completed_at` | `completed` (bool), `status`, `has_passing_review`, `missing_outputs` | run |
102
+ | `set-status.cypher` | `slug`, `status` (`pending`/`in_progress`/`failed`/`cancelled`) | `slug`, `status` | run |
@@ -6,10 +6,10 @@ returns `fail`, or the node's own agent errors out.
6
6
  | policy | `run` does |
7
7
  | ---------- | ----------------------------------------------------------------------------------------------------- |
8
8
  | `retry` | Re-dispatch the same node, same contract, up to 2 more attempts. Same agent role, fresh context. |
9
- | `fallback` | On failure, dispatch a named alternate approach (`plan` records it in the node's `contract.input`) instead of retrying the same approach. |
10
- | `skip` | Mark `cancelled`, not `failed`. Downstream nodes treat it as satisfied (see derived queries). Use only when the node is genuinely optional to the destination. |
11
- | `repair` | Feed the reviewer's `acceptance_results` failures back into a repair pass targeting just the failed criteria, then re-review. Bounded: 2 repair attempts, then escalate. |
12
- | `escalate` | Stop dispatching this node's dependents, checkpoint it `failed`, and surface it to the user in `/gbuild:status` as needing a decision. `run` does not guess a fix. |
9
+ | `fallback` | On failure, dispatch a named alternate approach (`plan` records it as an external `INPUT` field named `fallback`, whose `shape` describes the approach) instead of retrying the same approach. |
10
+ | `skip` | Mark `cancelled` (`set-status.cypher`), not `failed`. Downstream nodes treat it as satisfied (see the status lifecycle in `graph-format.md`). Use only when the node is genuinely optional to the destination. |
11
+ | `repair` | Feed the reviewer's failed criteria (the latest `Review`'s `failed_criteria`) back into a repair pass targeting just those criteria, then re-review. Bounded: 2 repair attempts, then escalate. |
12
+ | `escalate` | Stop dispatching this node's dependents, set it `failed` (`set-status.cypher`), and surface it to the user in `/gbuild:status` as needing a decision. `run` does not guess a fix. |
13
13
  | `stop` | Halt the entire run — not just this node's downstream. Reserved for failures that put the whole graph's premise in doubt (e.g. a `decision` node's answer contradicts an already-completed node). |
14
14
 
15
15
  ## Choosing a policy at plan time
@@ -25,7 +25,7 @@ returns `fail`, or the node's own agent errors out.
25
25
 
26
26
  ## Bounds are mandatory
27
27
 
28
- `retry`, `repair`, and `fallback` all have hard caps enforced by `run` (2 attempts unless the node
29
- overrides it in `contract.input`). No policy allows an unbounded loop — that's what turns a bounded
28
+ `retry`, `repair`, and `fallback` all have hard caps enforced by `run`: 2 attempts, counted from the
29
+ node's `Review` nodes. No policy allows an unbounded loop — that's what turns a bounded
30
30
  retry into the "controlled cycle" shape's job (`reference/shapes.md`), which has its own explicit round
31
31
  cap and dedup rule.
@@ -1,120 +1,78 @@
1
- # `graph.json` — normative format
1
+ # gbuild graph model — normative
2
2
 
3
3
  This file is normative. Where a skill disagrees with this doc, this doc wins.
4
4
 
5
- One file per feature: `.gbuild/<feature>/graph.json`, written once by `plan`, read-only during `run`.
5
+ One CypherLite store per feature: `.gbuild/<feature>/db/`. `plan` creates and charts it, `run` records
6
+ progress in it, and every skill reads it — **all through Cypher**. No skill reads or writes `graph.json`
7
+ directly; it is CypherLite's snapshot of the store. How to talk to the store (the single-writer rule,
8
+ invocation, quoting, the script catalog) is in `reference/cypher.md`.
6
9
 
7
- **`graph.json` is a CypherLite GraphData file.** Open it directly with CypherLite and query it with Cypher:
10
+ `templates/example.cypher` is a complete worked example — a 4-node diamond — written exactly the way
11
+ `plan` writes a new feature.
8
12
 
9
- ```
10
- cypherlite .gbuild/<feature>/graph.json "MATCH (n:GbuildNode) RETURN n.slug, n.type"
11
- ```
12
-
13
- JSON, not YAML — `scripts/graph.py` and `scripts/validate_format.py` are stdlib-only and Python's stdlib has no YAML parser. Comments are lost; that's fine, the file is agent-authored, not hand-typed. `/gbuild:status` is the human-facing view.
14
-
15
- **Validate after every change.** `scripts/validate_format.py <path>` is the format gate — run it after the initial write, after any reopen edit, after any hand-edit, before doing anything else with the file (including committing). It exits 0 + `valid: <feature>` on success, 1 + every error joined on failure. Fix the file and re-run until it passes; never hand off or run an invalid graph.
16
-
17
- The validator does its stdlib JSON checks always. If the `cypherlite` binary is on PATH, it *also* opens the file with CypherLite and runs a trivial `MATCH` — confirming CypherLite itself loads it, not just that the stdlib walk approves the shape. CypherLite is an optional consumer, not a dependency: if the binary is absent the cross-check is skipped silently (`--no-cypherlite` forces the skip too).
13
+ ## Store layout
18
14
 
19
- ## Top level — the GraphData envelope
20
-
21
- ```jsonc
22
- {
23
- "nodes": [ /* Feature, Acceptance, GbuildNode — see below */ ],
24
- "relationships": [ /* DEPENDS_ON, HAS_ACCEPTANCE, SATISFIES */ ],
25
- "indexes": [],
26
- "constraints": [],
27
- "next_node_id": 8, // max(node.id) + 1 (1 if empty) — CypherLite trusts this
28
- "next_rel_id": 10 // max(rel.id) + 1 (1 if empty)
29
- }
30
15
  ```
31
-
32
- `indexes` and `constraints` are empty for gbuild graphs — kept only because CypherLite's loader expects them. `next_node_id`/`next_rel_id` are the id allocator counters; they must equal `max(id)+1` for each kind, and the validator enforces this.
33
-
34
- ## Node labels
35
-
36
- Three node kinds, distinguished by label:
37
-
38
- | label | meaning | count per graph |
39
- | ------------ | ---------------------------------------- | --------------- |
40
- | `Feature` | the one feature this graph is for | exactly 1 |
41
- | `Acceptance` | one feature-level acceptance bullet | one per bullet |
42
- | `GbuildNode` | one unit of work in the dependency graph | one per node |
43
-
44
- ### `Feature` node (id 1)
45
-
46
- ```jsonc
47
- {
48
- "id": 1,
49
- "labels": ["Feature"],
50
- "properties": {
51
- "feature": "gbuild-mvp", // slug, matches the .gbuild/<feature>/ dir
52
- "destination": "<one line — what \"done\" looks like>",
53
- "context": "<why this exists, one or two lines>",
54
- "out_of_scope": ["<bullet>"]
55
- }
56
- }
57
- ```
58
-
59
- Keep `destination`/`context`/`out_of_scope` short — bullets, not paragraphs. Detail lives on the nodes, not here. Same discipline as hone's 150-line `spec.md` budget, for the same reason: this block gets re-read every invocation. **Do not put an `acceptance` property here** — feature-level acceptance lives in `Acceptance` nodes.
60
-
61
- ### `Acceptance` nodes
62
-
63
- One per feature-level acceptance bullet:
64
-
65
- ```jsonc
66
- {
67
- "id": 2,
68
- "labels": ["Acceptance"],
69
- "properties": { "id": "a-1", "text": "<bullet>" }
70
- }
16
+ .gbuild/<feature>/db/
17
+ graph.json CypherLite JSON snapshot — committed; always current (every write ends with .checkpoint)
18
+ .gitignore "*", "!.gitignore", "!graph.json" — committed
19
+ manifest.json engine files — ignored
20
+ wal/
21
+ .cypherlite.lock
71
22
  ```
72
23
 
73
- Each is linked `Feature -[:HAS_ACCEPTANCE]-> Acceptance`. A node that serves a criterion gets `GbuildNode -[:SATISFIES]-> Acceptance` (see below). Normalizing the bar into nodes makes it queryable on its own and joinable to the nodes that cover it.
24
+ The store is created with the JSON codec (`--snapshot-format json`, see `reference/cypher.md`) so
25
+ `graph.json` diffs are reviewable in a PR, and a clone holding only `graph.json` is a complete store —
26
+ data **and** constraints.
74
27
 
75
- ### `GbuildNode` — a unit of work
76
-
77
- ```jsonc
78
- {
79
- "id": 4,
80
- "labels": ["GbuildNode", "Code"], // PascalCase type label, always alongside GbuildNode
81
- "properties": {
82
- "slug": "01-scan-existing-runners", // filename-safe kebab-case, immutable, the stable identity
83
- "title": "<string>",
84
- "type": "code", // lowercase enum — kept as a property too (matches the label)
85
- "contract": { "input": {}, "output": {} }, // structured shapes, not prose
86
- "acceptance": [ // node-level local criteria (mandatory, concrete)
87
- "<concrete, checkable-by-a-fresh-context criterion>"
88
- ],
89
- "verify": null, // slug of an *additional* verify-type node, or null
90
- "failure_policy": "retry | fallback | skip | repair | escalate | stop",
91
- "model_tier": "cheap | strong"
92
- }
93
- }
94
- ```
28
+ ## Nodes
95
29
 
96
- - `id` — integer, sequential, unique, never reused even if a node is removed. Authoring convenience, not semantic identity — `slug` is the stable identity skills and checkpoints key on.
97
- - `labels` — `["GbuildNode", <PascalCase type>]`. The universal `GbuildNode` label is for `MATCH (n:GbuildNode)`; the type label (`Research`, `Decision`, `Code`, `Test`, `Verify`, `Chore`) enables `MATCH (n:Code)`.
98
- - `slug` — the human/inter-skill identifier. Checkpoint files are named `<slug>.json`; skills refer to nodes by slug in anything the user reads. Immutable once created.
99
- - `type` — lowercase enum, duplicated as a property alongside the PascalCase label so `n.type` works for sort/filter and survives any label-casing edge. The validator checks the label matches the type.
100
- - `contract.input` / `contract.output` — structured shapes, not prose. A downstream node's `input` should reference an upstream node's `output` field directly (by slug path in the doc string, e.g. `"<integer, from a-produce-shared-value.output.value>"`).
101
- - `acceptance` — **mandatory, non-empty, concrete.** The validator rejects empty or all-blank lists. Concrete means checkable by a fresh context with no other information — not `"looks correct"` or `"works well"`.
102
- - `verify` — only set when this node needs something *beyond* the standard `gbuild-reviewer` pass. Usually `null`.
103
- - **no `satisfies` property** — which global acceptance ids this node serves is expressed as `SATISFIES` edges, not an array. The validator rejects a stray `satisfies` property.
30
+ | label | meaning | count |
31
+ | ------------ | -------------------------------------------------- | ----------------- |
32
+ | `Feature` | the one feature this store is for | exactly 1 |
33
+ | `Acceptance` | one feature-level acceptance bullet | one per bullet |
34
+ | `GbuildNode` | one unit of work (always with a PascalCase type label) | one per node |
35
+ | `Field` | one named input or output of a GbuildNode's contract | one per field |
36
+ | `Review` | one `gbuild-reviewer` verdict on a GbuildNode | one per review attempt |
104
37
 
105
- ### The cut test
38
+ ### `Feature`
106
39
 
107
- Before adding an edge `B -[:DEPENDS_ON]-> A`, ask: does B actually read something A produced? If B would run exactly the same way with A deleted, it's not a dependency — it's just the order you thought of them in. Sequence ≠ dependency. Nodes with no real edge between them belong in the same wave, not a chain.
40
+ | property | type | meaning |
41
+ | -------------- | -------------- | ---------------------------------------------------- |
42
+ | `feature` | string | the slug — matches the `.gbuild/<feature>/` dir, immutable |
43
+ | `destination` | string | one line: what is true once this is done |
44
+ | `context` | string | why this exists, one or two lines |
45
+ | `out_of_scope` | list of string | bullets, may be empty |
108
46
 
109
- ### `acceptance` is mandatory and must be concrete
47
+ Keep these short — bullets, not paragraphs. Detail lives on the nodes. There is no `acceptance` property:
48
+ the feature-level bar is `Acceptance` nodes.
110
49
 
111
- `validate_format.py` rejects the file if any GbuildNode has an empty or all-blank `acceptance` list (same rule as hone's invariant: every build task has ≥1 acceptance criterion). Concrete means checkable by a fresh context with no other information — not `"looks correct"` or `"works well"`. Prefer criteria that name a file, a command's exit code, a specific behavior, or a literal output shape.
50
+ ### `Acceptance`
112
51
 
113
- ### `verify` is not the review
52
+ `id` (string, `a-1`, `a-2`, …, unique) and `text` (string, non-blank). Each is linked
53
+ `(:Feature)-[:HAS_ACCEPTANCE]->(:Acceptance)` and covered by at least one
54
+ `(:GbuildNode)-[:SATISFIES]->(:Acceptance)`.
114
55
 
115
- Every node — regardless of `type` — goes through `gbuild-reviewer` before it's checkpointed complete (see `agents/gbuild-reviewer.md`). `verify` is for something *beyond* that default: a dedicated fact-checker, an adversarial multi-vote, a human-approval gate. Most nodes have `verify: null` and are still reviewed.
56
+ ### `GbuildNode` — a unit of work
116
57
 
117
- ### `type`
58
+ Labels: `GbuildNode` plus exactly one type label matching `type` — `(:GbuildNode:Code {...})`. The
59
+ universal label is for `MATCH (n:GbuildNode)`; the type label enables `MATCH (n:Code)`.
60
+
61
+ | property | type | meaning |
62
+ | ---------------- | -------------- | -------------------------------------------------------------- |
63
+ | `slug` | string | kebab-case, unique, immutable — the identity every skill keys on |
64
+ | `title` | string | what the user reads; refer to nodes by title in conversation |
65
+ | `type` | string | `research \| decision \| code \| test \| verify \| chore` |
66
+ | `acceptance` | list of string | node-level criteria — mandatory, non-empty, concrete |
67
+ | `verify` | string, optional | slug of an *additional* verify-type node; usually absent |
68
+ | `failure_policy` | string | `retry \| fallback \| skip \| repair \| escalate \| stop` — `reference/failure-policies.md` |
69
+ | `model_tier` | string | `cheap \| strong` — `reference/cost-model.md` |
70
+ | `status` | string | `pending` at creation — see [Status lifecycle](#status-lifecycle) |
71
+ | `started_at` | string, optional | implementing agent's own UTC start time, set on completion |
72
+ | `completed_at` | string, optional | implementing agent's own UTC finish time, set on completion |
73
+
74
+ The contract is not a property — it is the node's `Field`s (below). Which feature criteria the node
75
+ serves is `SATISFIES` edges, not a property.
118
76
 
119
77
  | type | label | produces |
120
78
  | ---------- | ---------- | -------------------------------------------------------------------- |
@@ -125,116 +83,134 @@ Every node — regardless of `type` — goes through `gbuild-reviewer` before it
125
83
  | `verify` | `Verify` | a check on another node's output — never the same agent, never self |
126
84
  | `chore` | `Chore` | mechanical work with no behavior change |
127
85
 
128
- ### `failure_policy`
86
+ **`acceptance` must be concrete** — checkable by a fresh context with no other information. Not
87
+ "looks correct" or "works well": name a file, a command's exit code, a specific behavior, or a literal
88
+ output value.
129
89
 
130
- See `reference/failure-policies.md` for how `run` applies each value.
90
+ **`verify` is not the review.** Every node goes through `gbuild-reviewer` before it can be completed
91
+ (`agents/gbuild-reviewer.md`). `verify` is for something *beyond* that: a fact-checker, an adversarial
92
+ multi-vote, a human-approval gate.
131
93
 
132
- ### `model_tier`
94
+ ### `Field` — one contract field
133
95
 
134
- See `reference/cost-model.md` for the cheap/strong split and when a cluster should collapse to one agent.
96
+ | property | type | meaning |
97
+ | -------- | ------ | ----------------------------------------------------------------------- |
98
+ | `name` | string | unique per node and direction |
99
+ | `shape` | string | what the value is — `<integer>`, `<list of changed file paths>`, `<commit sha>` |
100
+ | `value` | scalar or list of scalars, optional | outputs only — recorded when the node completes |
135
101
 
136
- ## Relationships
102
+ Output values must be scalars (string, number, boolean) or lists of scalars; CypherLite cannot store a
103
+ map as a property value. JSON-encode anything nested into a string and say so in `shape`.
137
104
 
138
- Three types. Each gets a sequential integer `id` (never reused), `start_node`/`end_node` reference node ids, `properties` is an object (empty for all gbuild edges today).
105
+ ### `Review` — one reviewer verdict
139
106
 
140
- ### `DEPENDS_ON` — the dependency edge
107
+ `attempt` (integer, 1-based, assigned by `record-review.cypher`), `verdict` (`pass | fail`),
108
+ `failed_criteria` (list of the acceptance criteria the reviewer failed; empty on a pass), `at` (string).
109
+ Reviews are never updated or deleted — the repair history stays visible to `status`.
141
110
 
142
- ```jsonc
143
- { "id": 6, "type": "DEPENDS_ON", "start_node": 5, "end_node": 4, "properties": {} }
144
- ```
145
-
146
- Direction: **dependent → dependency** (`start_node` depends on `end_node`). So `MATCH (n)-[:DEPENDS_ON]->(d)` is "what does n need", and `MATCH (n)<-[:DEPENDS_ON]-(d)` is "what waits on n". Both endpoints must be `GbuildNode`s — you can't depend on the Feature or an Acceptance. The `DEPENDS_ON` graph must be acyclic (validator enforces).
111
+ ## Relationships
147
112
 
148
- ### `HAS_ACCEPTANCE` — feature → acceptance
113
+ | type | from → to | meaning |
114
+ | ---------------- | -------------------------- | ------------------------------------------------- |
115
+ | `HAS_ACCEPTANCE` | `Feature` → `Acceptance` | the criterion belongs to the feature |
116
+ | `SATISFIES` | `GbuildNode` → `Acceptance`| the node covers the criterion |
117
+ | `DEPENDS_ON` | `GbuildNode` → `GbuildNode`| dependent → dependency; must be acyclic |
118
+ | `INPUT` | `GbuildNode` → `Field` | the node reads this field |
119
+ | `OUTPUT` | `GbuildNode` → `Field` | the node produces this field |
120
+ | `FROM` | `Field` → `Field` | an input field reads an upstream node's output field |
121
+ | `REVIEWED` | `GbuildNode` → `Review` | a verdict on the node |
149
122
 
150
- ```jsonc
151
- { "id": 1, "type": "HAS_ACCEPTANCE", "start_node": 1, "end_node": 2, "properties": {} }
152
- ```
123
+ No other edge shapes are valid. `MATCH (n)-[:DEPENDS_ON]->(d)` is "what does n need";
124
+ `MATCH (n)<-[:DEPENDS_ON]-(d)` is "what waits on n".
153
125
 
154
- Ties each `Acceptance` node to its `Feature`. `start_node` must be the Feature node, `end_node` an Acceptance node. One per acceptance bullet.
126
+ ### The contract: `INPUT`, `OUTPUT`, `FROM`
155
127
 
156
- ### `SATISFIES` — node → acceptance
128
+ A node's contract is its fields. Every node declares at least one `OUTPUT` — a node that produces
129
+ nothing structured gives `gbuild-reviewer` nothing to check. An `INPUT` field with a `FROM` edge reads
130
+ that upstream output: when the upstream completes, the value flows to the consumer through the edge
131
+ (`node.cypher` resolves it). An `INPUT` without `FROM` is external — the codebase, the environment, the
132
+ user — and its `shape` says where it comes from.
157
133
 
158
- ```jsonc
159
- { "id": 3, "type": "SATISFIES", "start_node": 5, "end_node": 2, "properties": {} }
134
+ ```cypher
135
+ (b:GbuildNode)-[:DEPENDS_ON]->(a:GbuildNode)
136
+ (a)-[:OUTPUT]->(v:Field {name: 'value', shape: '<integer>'})
137
+ (b)-[:INPUT]->(:Field {name: 'value', shape: '<integer>'})-[:FROM]->(v)
160
138
  ```
161
139
 
162
- Ties a `GbuildNode` to a feature-level criterion it covers (replaces the old `satisfies` array property). `start_node` a GbuildNode, `end_node` an Acceptance. Every `Acceptance` must be the target of at least one `SATISFIES` — an uncovered criterion means a node is missing (validator enforces).
140
+ ### The cut test, machine-checked
163
141
 
164
- ## ID authoring rules
142
+ Before adding `B -[:DEPENDS_ON]-> A`, ask: does B actually read something A produced? If B would run
143
+ exactly the same way with A deleted, it is not a dependency — it is the order you thought of them in.
144
+ Sequence ≠ dependency; nodes with no real edge between them belong in the same wave, not a chain.
165
145
 
166
- - The skill authors integer ids directly in the file, sequential: `Feature` = 1, then `Acceptance` nodes, then `GbuildNode`s in any stable order. Relationship ids are sequential across all three types.
167
- - `next_node_id` / `next_rel_id` must equal `max(id)+1` for each kind (or 1 if empty). The validator enforces this because CypherLite trusts these counters on load — a stale counter can collide with a live id.
168
- - Never reuse an id, even if a node is removed. Slugs are the stable identity; ids are authoring convenience.
169
- - On reopen: append new `Acceptance`/`GbuildNode` nodes with the next available ids, add their edges, bump the counters, then re-run `validate_format.py`.
146
+ In this model the question has a checkable answer, and `validate.cypher` enforces both directions:
170
147
 
171
- ## State (not part of `graph.json`)
148
+ - every `DEPENDS_ON` carries at least one `FROM` from B's inputs to A's outputs — no edge without data
149
+ (`dependency-without-from`);
150
+ - every `FROM` points at an output of a node B `DEPENDS_ON` — no data without an edge
151
+ (`from-without-dependency`).
172
152
 
173
- `.gbuild/<feature>/nodes/<slug>.json`, one file per GbuildNode, written by `run`:
153
+ ## Status lifecycle
174
154
 
175
- ```jsonc
176
- {
177
- "status": "pending | in_progress | completed | failed | cancelled",
178
- "output": {}, // matches the node's contract.output shape
179
- "review": {
180
- "verdict": "pass | fail",
181
- "acceptance_results": [{ "criterion": "<text>", "passed": true }]
182
- },
183
- "started_at": "<ISO8601>",
184
- "completed_at": "<ISO8601>"
185
- }
186
- ```
155
+ `status` is stored on the node; `frontier` and `blocked` are derived by `status.cypher`, never stored.
187
156
 
188
- A missing checkpoint file means `pending`. Each node owns its own file, so parallel writers never collide. Checkpoints stay as sidecar files by design: `graph.json` is write-once (plan), checkpoints are write-many (run), and only the plan needs to be CypherLite-queryable. Runtime status is joined in by `graph.py --status`, not stored in the graph.
157
+ ```
158
+ pending ──dispatch──▶ in_progress ──complete──▶ completed
159
+ │
160
+ ├──set-status──▶ failed (escalate, exhausted retry/repair, stop)
161
+ └──set-status──▶ cancelled (skip)
189
162
 
190
- ## Derived queries
163
+ frontier = pending ∧ every DEPENDS_ON target completed-or-cancelled
164
+ blocked = pending ∧ some DEPENDS_ON target not completed-or-cancelled
165
+ ```
191
166
 
192
- `scripts/graph.py` computes all of this (joining the GraphData plan with the sidecar checkpoints):
167
+ A `cancelled` dependency counts as satisfied — abandoning an optional node must not wedge its
168
+ dependents. A `failed` one does not — dependents stay blocked rather than build on a broken foundation.
169
+ `completed` can only be set by `complete.cypher`, which refuses unless the node is `in_progress`, has a
170
+ passing `Review`, and every `OUTPUT` has a value.
193
171
 
194
- ```
195
- frontier = status pending ∧ every DEPENDS_ON target completed-or-cancelled
196
- blocked = status pending ∧ some DEPENDS_ON target not completed-or-cancelled
197
- in_flight = status in_progress
198
- waves = topological layering — wave N holds every node whose deps all resolved in wave < N
199
- ```
172
+ ## Who enforces what
200
173
 
201
- A `cancelled` dependency counts as satisfied — cancelling a node must not permanently block its dependents.
174
+ | layer | enforces |
175
+ | ------------------------- | ------------------------------------------------------------------------ |
176
+ | engine constraints (`cypher/schema.cypher`) | required properties and their types; unique `slug` and `Acceptance.id` — a violating write never lands |
177
+ | `cypher/validate.cypher` | exactly one `Feature`; enums; type label matches `type`; non-blank acceptance; `verify` target exists; acceptance linked and covered; allowed edge shapes only; acyclic `DEPENDS_ON`; every chore depended on; every node has an output; field ownership and uniqueness; the `FROM` cut test; completed nodes have a passing review and all output values |
178
+ | write scripts (`cypher/*.cypher`) | `dispatch` moves only frontier nodes; `complete` needs a passing review and every output; `set-status` never sets `completed` |
202
179
 
203
- ## Querying with Cypher
180
+ ## Querying
204
181
 
205
- Because `graph.json` is a CypherLite GraphData file, the *plan structure* is queryable directly with Cypher (no conversion). Runtime status is not — it lives in sidecar checkpoint files, so `frontier`/`blocked`/`completed` come from `graph.py --status`, not pure Cypher. Dependency-derived questions are pure Cypher:
182
+ The plan and its progress are one graph, so any question is a Cypher query. Run ad-hoc reads with
183
+ `cypherlite .gbuild/<feature>/db -json "<query>"` (see `reference/cypher.md`); keep a `LIMIT` on anything
184
+ that could grow.
206
185
 
207
186
  ```cypher
208
- -- the whole plan
209
- MATCH (n:GbuildNode) RETURN n.slug, n.type, n.title
187
+ -- the whole plan with progress
188
+ MATCH (n:GbuildNode) RETURN n.slug, n.type, n.status, n.title ORDER BY n.slug
210
189
 
211
190
  -- nodes by type
212
191
  MATCH (n:Code) RETURN n.slug, n.title
213
192
 
214
- -- what does node d depend on (direct)
215
- MATCH (n {slug:'d-join-and-sum'})-[:DEPENDS_ON]->(d) RETURN d.slug
193
+ -- full transitive dependency chain of a node
194
+ MATCH (n:GbuildNode {slug: 'd-join-and-sum'})-[:DEPENDS_ON*]->(d) RETURN DISTINCT d.slug
216
195
 
217
- -- full transitive dependency chain
218
- MATCH (n {slug:'d-join-and-sum'})-[:DEPENDS_ON*]->(d) RETURN DISTINCT d.slug
196
+ -- what waits on a node
197
+ MATCH (n:GbuildNode {slug: 'a-produce-shared-value'})<-[:DEPENDS_ON]-(w) RETURN w.slug
219
198
 
220
- -- what waits on a given node
221
- MATCH (n)<-[:DEPENDS_ON]-(dependent) WHERE n.slug='a-produce-shared-value' RETURN dependent.slug
199
+ -- where a node's inputs come from
200
+ MATCH (:GbuildNode {slug: 'd-join-and-sum'})-[:INPUT]->(i)-[:FROM]->(o)<-[:OUTPUT]-(u)
201
+ RETURN i.name, u.slug, o.name, o.value
222
202
 
223
- -- nodes with no dependencies (first wave / roots)
224
- MATCH (n:GbuildNode) WHERE NOT (n)-[:DEPENDS_ON]->() RETURN n.slug
225
-
226
- -- nodes nobody depends on (final joins / leaves)
227
- MATCH (n:GbuildNode) WHERE NOT ()-[:DEPENDS_ON]->(n) RETURN n.slug
203
+ -- everything downstream of a node's output field (data lineage)
204
+ MATCH (:GbuildNode {slug: 'a-produce-shared-value'})-[:OUTPUT]->(o)<-[:FROM]-(i)<-[:INPUT]-(c)
205
+ RETURN o.name, c.slug, i.name
228
206
 
229
207
  -- critical path (longest dependency chain)
230
- MATCH p=(n:GbuildNode)-[:DEPENDS_ON*]->(m:GbuildNode) RETURN length(p) AS d, p ORDER BY d DESC LIMIT 1
231
-
232
- -- feature-level bar
233
- MATCH (f:Feature)-[:HAS_ACCEPTANCE]->(a:Acceptance) RETURN a.id, a.text
208
+ MATCH p = (n:GbuildNode)-[:DEPENDS_ON*]->(m:GbuildNode) RETURN [x IN nodes(p) | x.slug] AS path
209
+ ORDER BY length(p) DESC LIMIT 1
234
210
 
235
- -- which nodes cover each criterion
236
- MATCH (a:Acceptance)<-[:SATISFIES]-(n:GbuildNode) RETURN a.id, n.slug
211
+ -- which nodes cover each feature criterion
212
+ MATCH (a:Acceptance)<-[:SATISFIES]-(n:GbuildNode) RETURN a.id, collect(n.slug)
237
213
 
238
- -- uncovered criteria (a node is missing)
239
- MATCH (a:Acceptance) WHERE NOT ()-[:SATISFIES]->(a) RETURN a.id, a.text
214
+ -- nodes that needed a repair pass
215
+ MATCH (n:GbuildNode)-[:REVIEWED]->(r:Review {verdict: 'fail'}) RETURN n.slug, count(r) AS failed_reviews
240
216
  ```
@@ -1,8 +1,8 @@
1
1
  # Graph shapes
2
2
 
3
3
  `plan` classifies each cluster of nodes it creates into one of these. `run` uses the classification to
4
- decide how to schedule it. This is a lens for judgement, not a field stored on the node — the shape
5
- falls out of `dependencies`.
4
+ decide how to schedule it. This is a lens for judgement, not a property stored on the node — the shape
5
+ falls out of the `DEPENDS_ON` edges.
6
6
 
7
7
  ## Chain
8
8
 
@@ -24,10 +24,11 @@ though nothing requires it; the whole point of a real dependency graph is that `
24
24
  ## Router
25
25
 
26
26
  One node whose output selects which of several downstream nodes actually runs — not all of them, not
27
- none, exactly the branch the output implies. Model this as a `decision`-type node whose `contract.output`
28
- names the chosen branch, with the downstream nodes each depending on it; `run`'s job is to only dispatch
29
- the node(s) the router's output actually selected, and mark the un-selected siblings `cancelled` (which,
30
- per the derived-queries rule, still satisfies anything waiting on "the router resolved").
27
+ none, exactly the branch the output implies. Model this as a `decision`-type node with an `OUTPUT`
28
+ field naming the chosen branch, and each downstream node depending on it with an `INPUT` field `FROM`
29
+ that output; `run`'s job is to only dispatch the node(s) the router's output actually selected, and mark
30
+ the un-selected siblings `cancelled` (which, per the status lifecycle, still satisfies anything waiting
31
+ on "the router resolved").
31
32
 
32
33
  ## Controlled cycle
33
34
 
@@ -40,9 +41,9 @@ in a graph must have, at plan time:
40
41
  - an explicit **exit node** reached either by the verify passing or by hitting the cap and escalating
41
42
 
42
43
  `run` implements this as a bounded loop (see `skills/run/SKILL.md`), never as extra graph edges that
43
- loop back on themselves — `scripts/graph.py` rejects literal cycles in `dependencies` (that's how it
44
- catches a malformed graph). A controlled cycle is a *behavior* `run` executes around a small
45
- sub-cluster, not a topology `graph.py` has to tolerate.
44
+ loop back on themselves — `validate.cypher` rejects literal `DEPENDS_ON` cycles (that's how it catches a
45
+ malformed graph). A controlled cycle is a *behavior* `run` executes around a small sub-cluster, not a
46
+ topology the graph has to tolerate.
46
47
 
47
48
  ## Choosing a shape
48
49