opencode-gbuild 0.7.1 → 0.9.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.
@@ -0,0 +1,39 @@
1
+ // One node's brief: everything its implementer or reviewer needs, read by the
2
+ // agent itself (never by the orchestrator). An input with a FROM edge carries
3
+ // the upstream output value and `from` = '<upstream-slug>.output.<name>'; an
4
+ // external input has a null `from` and `value`, and its `shape` says where to
5
+ // look. `failed_criteria` is the latest Review's when it failed (a retry), else
6
+ // []. `notes` holds every note written by a transitive upstream node plus setup
7
+ // and testing notes from any node, upstream first, then newest, at most 20.
8
+ // cypherlite query --read-only --database <git-common-dir>/gbuild/<slug>/db --mode jsonl --param 'slug="<node-slug>"' < cypher/brief.cypher
9
+ MATCH (n:GbuildNode {slug: $slug})
10
+ RETURN n.slug AS slug,
11
+ n.title AS title,
12
+ n.type AS type,
13
+ n.acceptance AS acceptance,
14
+ n.verify AS verify,
15
+ COLLECT {
16
+ MATCH (n)-[:INPUT]->(i:Field)
17
+ OPTIONAL MATCH (i)-[:FROM]->(o:Field)<-[:OUTPUT]-(u:GbuildNode)
18
+ WITH i, o, u ORDER BY i.name
19
+ RETURN {name: i.name, shape: i.shape, from: u.slug + '.output.' + o.name, value: o.value}
20
+ } AS inputs,
21
+ COLLECT {
22
+ MATCH (n)-[:OUTPUT]->(f:Field)
23
+ WITH f ORDER BY f.name
24
+ RETURN {name: f.name, shape: f.shape}
25
+ } AS outputs,
26
+ COLLECT {
27
+ MATCH (n)-[:REVIEWED]->(r:Review)
28
+ WITH r ORDER BY r.attempt DESC LIMIT 1
29
+ WITH r WHERE r.verdict = 'fail'
30
+ UNWIND r.failed_criteria AS c
31
+ RETURN c
32
+ } AS failed_criteria,
33
+ COLLECT {
34
+ MATCH (w:GbuildNode)-[:WROTE]->(nt:Note)
35
+ WITH nt, CASE WHEN EXISTS { MATCH (n)-[:DEPENDS_ON*]->(w) } THEN 0 ELSE 1 END AS rank
36
+ WHERE rank = 0 OR nt.topic IN ['setup', 'testing']
37
+ WITH nt, rank ORDER BY rank, nt.at DESC, nt.text LIMIT 20
38
+ RETURN {topic: nt.topic, text: nt.text, by: nt.by_node}
39
+ } AS notes;
@@ -0,0 +1,19 @@
1
+ // An implementer's self-check of its result file before it replies (and the
2
+ // reviewer's mechanical output check). Clean when missing_outputs,
3
+ // invalid_outputs and unknown_outputs are [] and has_times is true:
4
+ // missing_outputs declared outputs with no value (record would downgrade the pass)
5
+ // invalid_outputs values that are not a scalar or a single-type list of
6
+ // scalars (record would fail with InvalidPropertyType)
7
+ // unknown_outputs keys that are not declared outputs (likely typos; record ignores them)
8
+ // has_times started_at and completed_at are strings
9
+ // The missing-output expression is text-identical to record.cypher's
10
+ // (test-pinned).
11
+ // cypherlite query --read-only --database <git-common-dir>/gbuild/<slug>/db --mode jsonl --param 'slug="<node-slug>"' \
12
+ // --fetch-params result=<git-common-dir>/gbuild/<slug>/inbox/<node-slug>.json < cypher/check-result.cypher
13
+ MATCH (n:GbuildNode {slug: $slug})
14
+ WITH n, $result AS res, coalesce($result.outputs, {}) AS outputs
15
+ RETURN n.slug AS slug,
16
+ [k IN COLLECT { MATCH (n)-[:OUTPUT]->(f:Field) WITH f ORDER BY f.name RETURN f.name } WHERE res.outputs[k] IS NULL] AS missing_outputs,
17
+ [k IN keys(outputs) WHERE outputs[k] IS NOT NULL AND NOT (outputs[k] IS :: STRING | INTEGER | FLOAT | BOOLEAN | LIST<STRING> | LIST<INTEGER> | LIST<FLOAT> | LIST<BOOLEAN>)] AS invalid_outputs,
18
+ [k IN keys(outputs) WHERE NOT k IN COLLECT { MATCH (n)-[:OUTPUT]->(f:Field) RETURN f.name }] AS unknown_outputs,
19
+ $result.started_at IS :: STRING NOT NULL AND $result.completed_at IS :: STRING NOT NULL AS has_times;
@@ -1,31 +1,13 @@
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 query --database .gbuild/<slug>/db --mode jsonl < cypher/dispatch.cypher
1
+ // Claim every frontier node (pending, every DEPENDS_ON target completed or
2
+ // cancelled): move it to in_progress and return only what the orchestrator
3
+ // routes on. Each agent reads its own definition with brief.cypher, so no
4
+ // title, acceptance, inputs or outputs pass through the orchestrator. Run it
5
+ // at the start of a run and again after every record.cypher / set-status.cypher
6
+ // call. No rows = nothing newly ready (ask progress.cypher / attention.cypher
7
+ // whether the graph is done or stuck).
8
+ // cypherlite query --database <git-common-dir>/gbuild/<slug>/db --mode jsonl < cypher/dispatch.cypher
9
9
  MATCH (n:GbuildNode {status: 'pending'})
10
10
  WHERE NOT EXISTS { MATCH (n)-[:DEPENDS_ON]->(d:GbuildNode) WHERE NOT d.status IN ['completed', 'cancelled'] }
11
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
12
+ RETURN n.slug AS slug, n.type AS type, n.model_tier AS model_tier, n.failure_policy AS failure_policy
30
13
  ORDER BY slug;
31
- .checkpoint
@@ -2,7 +2,7 @@
2
2
  // nodes that cover each criterion. What review hands the auditor and what pr
3
3
  // builds the description from. Per-node output values are not included — get
4
4
  // one node's with node.cypher if you actually need it.
5
- // cypherlite query --database .gbuild/<slug>/db --mode jsonl < cypher/feature.cypher
5
+ // cypherlite query --read-only --database <git-common-dir>/gbuild/<slug>/db --mode jsonl < cypher/feature.cypher
6
6
  MATCH (f:Feature)
7
7
  RETURN f.feature AS feature,
8
8
  f.destination AS destination,
@@ -1,9 +1,10 @@
1
1
  // Everything needed to dispatch or review one node, with inputs resolved
2
2
  // through FROM edges to the upstream output values. An input with no FROM is
3
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 query --database .gbuild/<slug>/db --mode jsonl --param 'slug="<node-slug>"' < cypher/node.cypher
4
+ // Each review carries the reviewer's checklist ([] when it wrote none). Used
5
+ // for status, reopen and ad-hoc inspection; run's agents read their own
6
+ // definition with brief.cypher instead.
7
+ // cypherlite query --read-only --database <git-common-dir>/gbuild/<slug>/db --mode jsonl --param 'slug="<node-slug>"' < cypher/node.cypher
7
8
  MATCH (n:GbuildNode {slug: $slug})
8
9
  RETURN n.slug AS slug,
9
10
  n.title AS title,
@@ -37,5 +38,5 @@ RETURN n.slug AS slug,
37
38
  COLLECT {
38
39
  MATCH (n)-[:REVIEWED]->(r:Review)
39
40
  WITH r ORDER BY r.attempt
40
- RETURN {attempt: r.attempt, verdict: r.verdict, failed_criteria: r.failed_criteria, at: r.at}
41
+ RETURN {attempt: r.attempt, verdict: r.verdict, failed_criteria: r.failed_criteria, checklist: r.checklist, at: r.at}
41
42
  } AS reviews;
@@ -2,7 +2,7 @@
2
2
  // that wave not yet completed or cancelled. A wave with `open: []` is done;
3
3
  // the graph is done when every row's `open` is empty. `wave` is the longest
4
4
  // DEPENDS_ON chain below a node (0 = no dependencies).
5
- // cypherlite query --database .gbuild/<slug>/db --mode jsonl < cypher/progress.cypher
5
+ // cypherlite query --read-only --database <git-common-dir>/gbuild/<slug>/db --mode jsonl < cypher/progress.cypher
6
6
  MATCH (n:GbuildNode)
7
7
  OPTIONAL MATCH p = (n)-[:DEPENDS_ON*]->()
8
8
  WITH n, coalesce(max(length(p)), 0) AS wave
@@ -1,38 +1,90 @@
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 query --database .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,
1
+ // Record one node attempt per call. Every call adds a Review (attempt =
2
+ // previous reviews + 1) holding the verdict, its failed criteria and the
3
+ // reviewer's checklist. A pass with a value for every OUTPUT field also
4
+ // completes the node: output values stored on its fields, the implementing
5
+ // agent's own times on the node, and its notes and symbols stored as
6
+ // (n)-[:WROTE]->(:Note) and (n)-[:DEFINES]->(:Symbol). A pass missing outputs
7
+ // is recorded as a fail listing them, so the review history stays truthful. A
8
+ // fail stores the Review only: no outputs, notes or symbols.
9
+ // Three params, always all bound (the script can't tell --param from
10
+ // --fetch-params):
11
+ // verdict {"slug", "verdict": "pass"|"fail", "failed_criteria": [...]} — the
12
+ // only source of slug, verdict and failed criteria
13
+ // result <inbox>/<node-slug>.json (outputs, started_at, completed_at, and
14
+ // optional notes and symbols) on a pass whose file exists, else {};
15
+ // any slug/verdict key in it is ignored
16
+ // review <inbox>/<node-slug>.review.json ({checklist}) when it exists, else
17
+ // {}; string entries only, the first 40, each cut to 300 chars
18
+ // Notes keep only maps with a topic of setup|testing|conventions|gotcha and
19
+ // non-blank string text (trimmed, cut to 300 chars): the first 5 distinct ones,
20
+ // skipping any whose exact (topic, text) is already stored. Symbols keep only
21
+ // maps with string name, file and purpose and a non-blank name and file
22
+ // (trimmed, purpose cut to 160 chars): the first 10 distinct (name, file),
23
+ // skipping any whose (name, file) is already stored. The missing-output
24
+ // expression is text-identical to check-result.cypher's (test-pinned).
25
+ // No row unless the node is in_progress: compare the returned slug with the
26
+ // one sent. Output values must be scalars or lists of scalars — a nested value
27
+ // fails the whole call with InvalidPropertyType and writes nothing.
28
+ // `unselected`: for a completed decision node, the pending dependents reading
29
+ // one of its outputs whose string / list-of-strings value names some readers
30
+ // but not them — cancel them with set-status.cypher before the next dispatch.
31
+ // cypherlite query --database <git-common-dir>/gbuild/<slug>/db --mode jsonl \
32
+ // --param 'verdict={"slug":"<node-slug>","verdict":"pass","failed_criteria":[]}' \
33
+ // --fetch-params result=<git-common-dir>/gbuild/<slug>/inbox/<node-slug>.json \
34
+ // --fetch-params review=<git-common-dir>/gbuild/<slug>/inbox/<node-slug>.review.json < cypher/record.cypher
35
+ // cypherlite query --database <git-common-dir>/gbuild/<slug>/db --mode jsonl \
36
+ // --param 'verdict={"slug":"<node-slug>","verdict":"fail","failed_criteria":["implementer failed: <reason>"]}' \
37
+ // --param 'result={}' --param 'review={}' < cypher/record.cypher
38
+ WITH $verdict AS v, $result AS res, $review AS rev
39
+ MATCH (n:GbuildNode {slug: v.slug})
40
+ WHERE n.status = 'in_progress' AND v.verdict IN ['pass', 'fail']
41
+ WITH n, v, res, rev,
19
42
  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]
43
+ CASE WHEN v.verdict = 'pass'
44
+ THEN [k IN COLLECT { MATCH (n)-[:OUTPUT]->(f:Field) WITH f ORDER BY f.name RETURN f.name } WHERE res.outputs[k] IS NULL]
22
45
  ELSE [] END AS missing
23
- WITH n, r, attempt, missing, (r.verdict = 'pass' AND size(missing) = 0) AS ok
46
+ WITH n, v, res, rev, attempt, missing, (v.verdict = 'pass' AND size(missing) = 0) AS ok
47
+ WITH n, v, res, attempt, missing, ok,
48
+ [c IN coalesce(rev.checklist, []) WHERE (c IS :: STRING) | left(c, 300)][0..40] AS checklist,
49
+ CASE WHEN ok AND res.notes IS :: LIST<ANY> NOT NULL THEN reduce(acc = [], x IN [x IN res.notes WHERE CASE
50
+ WHEN NOT x IS :: MAP NOT NULL THEN false
51
+ WHEN NOT x.text IS :: STRING NOT NULL THEN false
52
+ WHEN NOT coalesce(x.topic, '') IN ['setup', 'testing', 'conventions', 'gotcha'] THEN false
53
+ ELSE trim(x.text) <> '' END
54
+ | {topic: x.topic, text: left(trim(x.text), 300)}] | CASE WHEN x IN acc THEN acc ELSE acc + [x] END)[0..5] ELSE [] END AS notes,
55
+ CASE WHEN ok AND res.symbols IS :: LIST<ANY> NOT NULL THEN reduce(acc = [], x IN [x IN res.symbols WHERE CASE
56
+ WHEN NOT x IS :: MAP NOT NULL THEN false
57
+ WHEN NOT (x.name IS :: STRING NOT NULL AND x.file IS :: STRING NOT NULL AND x.purpose IS :: STRING NOT NULL) THEN false
58
+ ELSE trim(x.name) <> '' AND trim(x.file) <> '' END
59
+ | {name: trim(x.name), file: trim(x.file), purpose: left(trim(x.purpose), 160)}]
60
+ | CASE WHEN any(y IN acc WHERE y.name = x.name AND y.file = x.file) THEN acc ELSE acc + [x] END)[0..10] ELSE [] END AS symbols
24
61
  CREATE (n)-[:REVIEWED]->(review:Review {
25
62
  attempt: attempt,
26
63
  verdict: CASE WHEN ok THEN 'pass' ELSE 'fail' END,
27
- failed_criteria: coalesce(r.failed_criteria, []) + [k IN missing | 'missing output value: ' + k],
64
+ failed_criteria: coalesce(v.failed_criteria, []) + [k IN missing | 'missing output value: ' + k],
65
+ checklist: checklist,
28
66
  at: toString(datetime())
29
67
  })
30
- WITH n, r, attempt, missing, ok, review
68
+ WITH n, res, attempt, missing, ok, review, notes, symbols
31
69
  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
70
+ SET f.value = res.outputs[f.name]
71
+ WITH DISTINCT n, res, attempt, missing, ok, review, notes, symbols
34
72
  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
73
+ SET n.status = 'completed', n.started_at = res.started_at, n.completed_at = res.completed_at)
74
+ WITH n, attempt, missing, ok, review,
75
+ [x IN notes WHERE NOT EXISTS { MATCH (o:Note) WHERE o.topic = x.topic AND o.text = x.text }] AS notes,
76
+ [x IN symbols WHERE NOT EXISTS { MATCH (o:Symbol) WHERE o.name = x.name AND o.file = x.file }] AS symbols
77
+ FOREACH (x IN notes |
78
+ CREATE (n)-[:WROTE]->(:Note {topic: x.topic, text: x.text, by_node: n.slug, at: review.at}))
79
+ FOREACH (x IN symbols |
80
+ CREATE (n)-[:DEFINES]->(:Symbol {name: x.name, file: x.file, purpose: x.purpose, by_node: n.slug, at: review.at}))
81
+ RETURN n.slug AS slug, attempt, review.verdict AS verdict, ok AS completed, missing AS missing_outputs,
82
+ size(notes) AS notes_added, size(symbols) AS symbols_added,
83
+ CASE WHEN ok AND n.type = 'decision' THEN COLLECT {
84
+ MATCH (n)-[:OUTPUT]->(f:Field)
85
+ WITH f, COLLECT { MATCH (f)<-[:FROM]-(:Field)<-[:INPUT]-(d:GbuildNode {status: 'pending'}) RETURN d.slug } AS readers,
86
+ CASE WHEN f.value IS :: STRING THEN [f.value] WHEN f.value IS :: LIST<STRING> THEN f.value ELSE [] END AS named
87
+ WHERE any(r IN readers WHERE r IN named)
88
+ UNWIND [r IN readers WHERE NOT r IN named] AS u
89
+ RETURN DISTINCT u ORDER BY u
90
+ } ELSE [] END AS unselected;
@@ -1,5 +1,5 @@
1
1
  // gbuild store schema. Run once, when plan creates the store:
2
- // cypherlite query --database .gbuild/<slug>/db --snapshot-format json < cypher/schema.cypher
2
+ // cypherlite query --database <git-common-dir>/gbuild/<slug>/db --snapshot-format json < cypher/schema.cypher
3
3
  // Everything the engine can enforce at write time lives here; the rest of the
4
4
  // contract is checked by validate.cypher.
5
5
  CREATE CONSTRAINT feature_name FOR (n:Feature) REQUIRE n.feature IS :: STRING;
@@ -42,4 +42,22 @@ CREATE CONSTRAINT review_verdict_exists FOR (n:Review) REQUIRE n.verdict IS NOT
42
42
  CREATE CONSTRAINT review_at FOR (n:Review) REQUIRE n.at IS :: STRING;
43
43
  CREATE CONSTRAINT review_at_exists FOR (n:Review) REQUIRE n.at IS NOT NULL;
44
44
  CREATE CONSTRAINT review_failed_criteria FOR (n:Review) REQUIRE n.failed_criteria IS :: LIST<STRING NOT NULL>;
45
- .checkpoint
45
+ CREATE CONSTRAINT review_checklist FOR (n:Review) REQUIRE n.checklist IS :: LIST<STRING NOT NULL>;
46
+ CREATE CONSTRAINT note_topic FOR (n:Note) REQUIRE n.topic IS :: STRING;
47
+ CREATE CONSTRAINT note_topic_exists FOR (n:Note) REQUIRE n.topic IS NOT NULL;
48
+ CREATE CONSTRAINT note_text FOR (n:Note) REQUIRE n.text IS :: STRING;
49
+ CREATE CONSTRAINT note_text_exists FOR (n:Note) REQUIRE n.text IS NOT NULL;
50
+ CREATE CONSTRAINT note_by_node FOR (n:Note) REQUIRE n.by_node IS :: STRING;
51
+ CREATE CONSTRAINT note_by_node_exists FOR (n:Note) REQUIRE n.by_node IS NOT NULL;
52
+ CREATE CONSTRAINT note_at FOR (n:Note) REQUIRE n.at IS :: STRING;
53
+ CREATE CONSTRAINT note_at_exists FOR (n:Note) REQUIRE n.at IS NOT NULL;
54
+ CREATE CONSTRAINT symbol_name FOR (n:Symbol) REQUIRE n.name IS :: STRING;
55
+ CREATE CONSTRAINT symbol_name_exists FOR (n:Symbol) REQUIRE n.name IS NOT NULL;
56
+ CREATE CONSTRAINT symbol_file FOR (n:Symbol) REQUIRE n.file IS :: STRING;
57
+ CREATE CONSTRAINT symbol_file_exists FOR (n:Symbol) REQUIRE n.file IS NOT NULL;
58
+ CREATE CONSTRAINT symbol_purpose FOR (n:Symbol) REQUIRE n.purpose IS :: STRING;
59
+ CREATE CONSTRAINT symbol_purpose_exists FOR (n:Symbol) REQUIRE n.purpose IS NOT NULL;
60
+ CREATE CONSTRAINT symbol_by_node FOR (n:Symbol) REQUIRE n.by_node IS :: STRING;
61
+ CREATE CONSTRAINT symbol_by_node_exists FOR (n:Symbol) REQUIRE n.by_node IS NOT NULL;
62
+ CREATE CONSTRAINT symbol_at FOR (n:Symbol) REQUIRE n.at IS :: STRING;
63
+ CREATE CONSTRAINT symbol_at_exists FOR (n:Symbol) REQUIRE n.at IS NOT NULL;
@@ -3,7 +3,7 @@
3
3
  // retry/repair, stop), or `pending` (reset nodes an interrupted run left
4
4
  // in_progress). `completed` is rejected — only record.cypher may set it, so it
5
5
  // can require a passing review and every output value.
6
- // cypherlite query --database .gbuild/<slug>/db --mode jsonl --param 'slugs=["a","b"]' --param 'status="cancelled"' \
6
+ // cypherlite query --database <git-common-dir>/gbuild/<slug>/db --mode jsonl --param 'slugs=["a","b"]' --param 'status="cancelled"' \
7
7
  // < cypher/set-status.cypher
8
8
  UNWIND $slugs AS s
9
9
  MATCH (n:GbuildNode {slug: s})
@@ -11,4 +11,3 @@ WHERE $status IN ['pending', 'in_progress', 'failed', 'cancelled']
11
11
  SET n.status = $status
12
12
  RETURN n.slug AS slug, n.status AS status
13
13
  ORDER BY slug;
14
- .checkpoint
@@ -4,16 +4,20 @@
4
4
  // otherwise = the stored status (in_progress, completed, failed, cancelled)
5
5
  // `wave` is the longest DEPENDS_ON chain below the node (0 = no dependencies),
6
6
  // so nodes sharing a wave are safe to run concurrently.
7
- // cypherlite query --database .gbuild/<slug>/db --mode jsonl < cypher/status.cypher
7
+ // `last_dependency_at` is the latest completed_at among the node's completed
8
+ // DEPENDS_ON targets (null without any): its gap to `started_at` is how long
9
+ // the node waited for dispatch once its last dependency landed.
10
+ // cypherlite query --read-only --database <git-common-dir>/gbuild/<slug>/db --mode jsonl < cypher/status.cypher
8
11
  MATCH (n:GbuildNode)
9
12
  OPTIONAL MATCH p = (n)-[:DEPENDS_ON*]->()
10
13
  WITH n, coalesce(max(length(p)), 0) AS wave
11
14
  OPTIONAL MATCH (n)-[:DEPENDS_ON]->(d:GbuildNode)
12
15
  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
16
+ WITH n, wave, [x IN collect(d) WHERE NOT x.status IN ['completed', 'cancelled'] | x.slug] AS waiting_on,
17
+ max(CASE WHEN d.status = 'completed' THEN d.completed_at END) AS last_dependency_at
14
18
  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
19
+ WITH n, wave, waiting_on, last_dependency_at, r ORDER BY r.attempt DESC
20
+ WITH n, wave, waiting_on, last_dependency_at, collect(r) AS reviews
17
21
  RETURN n.slug AS slug,
18
22
  n.title AS title,
19
23
  n.type AS type,
@@ -29,5 +33,6 @@ RETURN n.slug AS slug,
29
33
  reviews[0].verdict AS last_verdict,
30
34
  reviews[0].failed_criteria AS last_failed_criteria,
31
35
  n.started_at AS started_at,
32
- n.completed_at AS completed_at
36
+ n.completed_at AS completed_at,
37
+ last_dependency_at
33
38
  ORDER BY wave, slug;
@@ -0,0 +1,9 @@
1
+ // Helpers, types and utilities completed nodes added, for reuse: the reviewer
2
+ // reads it before grepping for duplication, the implementer before writing a
3
+ // new helper. Only completed (merged) nodes' symbols exist, so nodes running at
4
+ // the same time can't see each other's unmerged helpers. At most 100 rows.
5
+ // cypherlite query --read-only --database <git-common-dir>/gbuild/<slug>/db --mode jsonl < cypher/symbols.cypher
6
+ MATCH (:GbuildNode)-[:DEFINES]->(s:Symbol)
7
+ RETURN s.name AS name, s.file AS file, s.purpose AS purpose, s.by_node AS by
8
+ ORDER BY file, name
9
+ LIMIT 100;
@@ -1,18 +1,19 @@
1
1
  // gbuild format gate. Zero rows = valid; every row is one violation.
2
- // cypherlite query --database .gbuild/<slug>/db --mode jsonl < cypher/validate.cypher
2
+ // cypherlite query --read-only --database <git-common-dir>/gbuild/<slug>/db --mode jsonl < cypher/validate.cypher
3
3
  // Required properties, their types and slug/acceptance-id uniqueness are
4
4
  // engine constraints (schema.cypher) — a write that breaks them never lands.
5
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.
6
+ // edge shapes, coverage, acyclicity, the data-flow (FROM) cut test, and the
7
+ // topics and owners of agent notes and symbols.
7
8
  CALL {
8
9
  MATCH (f:Feature) WITH count(f) AS c WHERE c <> 1
9
10
  RETURN 'feature-count' AS check, 'expected exactly 1 Feature node, found ' + toString(c) AS detail
10
11
  UNION ALL
11
- MATCH (n) WHERE NOT (n:Feature OR n:Acceptance OR n:GbuildNode OR n:Field OR n:Review)
12
+ MATCH (n) WHERE NOT (n:Feature OR n:Acceptance OR n:GbuildNode OR n:Field OR n:Review OR n:Note OR n:Symbol)
12
13
  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
14
  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
15
+ MATCH (n:GbuildNode) WHERE NOT n.slug =~ '^[a-z0-9]+(-[a-z0-9]+)*$' OR n.slug IN ['tree', 'db', 'inbox']
16
+ RETURN 'bad-slug' AS check, n.slug + ': slug must be kebab-case and not tree, db or inbox' AS detail
16
17
  UNION ALL
17
18
  MATCH (n:GbuildNode) WHERE NOT n.type IN ['research', 'decision', 'code', 'test', 'verify', 'chore']
18
19
  RETURN 'bad-type' AS check, n.slug + ': invalid type ' + n.type AS detail
@@ -54,7 +55,9 @@ UNION ALL
54
55
  OR (type(r) = 'DEPENDS_ON' AND x:GbuildNode AND y:GbuildNode)
55
56
  OR (type(r) IN ['INPUT', 'OUTPUT'] AND x:GbuildNode AND y:Field)
56
57
  OR (type(r) = 'FROM' AND x:Field AND y:Field)
57
- OR (type(r) = 'REVIEWED' AND x:GbuildNode AND y:Review))
58
+ OR (type(r) = 'REVIEWED' AND x:GbuildNode AND y:Review)
59
+ OR (type(r) = 'WROTE' AND x:GbuildNode AND y:Note)
60
+ OR (type(r) = 'DEFINES' AND x:GbuildNode AND y:Symbol))
58
61
  RETURN 'bad-edge' AS check,
59
62
  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
63
  UNION ALL
@@ -100,6 +103,18 @@ UNION ALL
100
103
  UNION ALL
101
104
  MATCH (r:Review) WITH r, COUNT { (:GbuildNode)-[:REVIEWED]->(r) } AS c WHERE c <> 1
102
105
  RETURN 'review-owner' AS check, 'review attempt ' + toString(r.attempt) + ' has ' + toString(c) + ' owning nodes, want 1' AS detail
106
+ UNION ALL
107
+ MATCH (nt:Note) WHERE NOT nt.topic IN ['setup', 'testing', 'conventions', 'gotcha'] OR trim(nt.text) = ''
108
+ RETURN 'bad-note' AS check, nt.by_node + ': note topic ' + nt.topic + ' must be setup, testing, conventions or gotcha, with non-blank text' AS detail
109
+ UNION ALL
110
+ MATCH (nt:Note) WITH nt, COLLECT { MATCH (w:GbuildNode)-[:WROTE]->(nt) RETURN w.slug } AS by WHERE by <> [nt.by_node]
111
+ RETURN 'note-owner' AS check, nt.by_node + ': note must have exactly one WROTE edge, from the node named by by_node' AS detail
112
+ UNION ALL
113
+ MATCH (s:Symbol) WHERE trim(s.name) = '' OR trim(s.file) = ''
114
+ RETURN 'bad-symbol' AS check, s.by_node + ': symbol needs a non-blank name and file' AS detail
115
+ UNION ALL
116
+ MATCH (s:Symbol) WITH s, COLLECT { MATCH (w:GbuildNode)-[:DEFINES]->(s) RETURN w.slug } AS by WHERE by <> [s.by_node]
117
+ RETURN 'symbol-owner' AS check, s.by_node + '.' + s.name + ': symbol must have exactly one DEFINES edge, from the node named by by_node' AS detail
103
118
  }
104
119
  RETURN check, detail
105
120
  ORDER BY check, detail;
package/index.ts CHANGED
@@ -29,17 +29,25 @@ const OPENCODE_NOTES = `
29
29
  You are running under OpenCode, not Claude Code. Apply these mappings to the
30
30
  instructions above:
31
31
 
32
- - \`Agent\`/\`Task\` tool dispatches (including run's per-node fan-out) use the
33
- \`subagent\` tool. Fan-out stays real: one \`subagent\` call per frontier node,
34
- all in a single message.
35
- - \`gbuild-reviewer\` and \`gbuild-auditor\` are not registered agents here. When
36
- the instructions say to spawn one, dispatch the built-in \`general\` subagent
37
- and start its prompt with the full text of
38
- \`${join(root, "agents", "gbuild-reviewer.md")}\` (or
39
- \`${join(root, "agents", "gbuild-auditor.md")}\`, minus the frontmatter) as its
40
- instructions, followed by everything the instructions say to give it (node
41
- definition, diff locations, expected verdict format). It is still a fresh,
42
- isolated agent — never the one that implemented the node.
32
+ - \`Agent\`/\`Task\` tool dispatches (including run's per-node dispatch) use the
33
+ \`subagent\` tool: one call per ready node, with \`background: true\` where
34
+ Claude Code says \`run_in_background: true\`. Handle each completion as it
35
+ arrives instead of waiting for a whole wave. The auditor runs in the
36
+ foreground (no background flag).
37
+ - \`gbuild-implementer\`, \`gbuild-reviewer\` and \`gbuild-auditor\` are not
38
+ registered agents here. When the instructions say to spawn one, dispatch the
39
+ built-in \`general\` subagent instead:
40
+ - \`gbuild-implementer\` / \`gbuild-reviewer\`: the prompt is run's dispatch
41
+ template as written, prefixed with \`Read <Agent file> and follow it.\`,
42
+ where \`<Agent file>\` is the template's own \`Agent file:\` path
43
+ (\`${join(root, "agents", "gbuild-implementer.md")}\` /
44
+ \`${join(root, "agents", "gbuild-reviewer.md")}\`).
45
+ - \`gbuild-auditor\`: send
46
+ \`Read ${join(root, "agents", "gbuild-auditor.md")} and follow it.\`
47
+ followed by exactly what the review skill says to pass it.
48
+ Never paste the agent file's text into the prompt; the subagent reads it
49
+ itself. Each is still a fresh, isolated agent — a reviewer is never the one
50
+ that implemented the node.
43
51
  - \`$ARGUMENTS\` refers to the arguments this skill was invoked with in the
44
52
  conversation.
45
53
  - run's "Workflow tool" backend is Claude-specific; always use the fallback
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.7.1",
4
+ "version": "0.9.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",
@@ -1,6 +1,6 @@
1
1
  # Ship checklist
2
2
 
3
- `plan` runs this checklist against the graph it just wrote, before committing the store. Any item that
3
+ `plan` runs this checklist against the graph it just wrote, before handing off to `run`. Any item that
4
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