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.
- package/README.md +72 -33
- package/agents/gbuild-auditor.md +1 -1
- package/agents/gbuild-implementer.md +186 -0
- package/agents/gbuild-reviewer.md +113 -53
- package/cypher/attention.cypher +1 -1
- package/cypher/brief.cypher +39 -0
- package/cypher/check-result.cypher +19 -0
- package/cypher/dispatch.cypher +9 -27
- package/cypher/feature.cypher +1 -1
- package/cypher/node.cypher +5 -4
- package/cypher/progress.cypher +1 -1
- package/cypher/record.cypher +81 -29
- package/cypher/schema.cypher +20 -2
- package/cypher/set-status.cypher +1 -2
- package/cypher/status.cypher +10 -5
- package/cypher/symbols.cypher +9 -0
- package/cypher/validate.cypher +21 -6
- package/index.ts +19 -11
- package/package.json +1 -1
- package/reference/checklist.md +1 -1
- package/reference/cypher.md +51 -36
- package/reference/graph-format.md +62 -22
- package/reference/queries.md +7 -3
- package/skills/plan/SKILL.md +26 -33
- package/skills/pr/SKILL.md +7 -5
- package/skills/review/SKILL.md +8 -7
- package/skills/run/SKILL.md +154 -117
- package/skills/status/SKILL.md +26 -17
- package/templates/example.cypher +1 -2
|
@@ -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;
|
package/cypher/dispatch.cypher
CHANGED
|
@@ -1,31 +1,13 @@
|
|
|
1
|
-
// Claim
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// cypherlite query --database
|
|
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
|
package/cypher/feature.cypher
CHANGED
|
@@ -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
|
|
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,
|
package/cypher/node.cypher
CHANGED
|
@@ -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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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;
|
package/cypher/progress.cypher
CHANGED
|
@@ -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
|
|
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
|
package/cypher/record.cypher
CHANGED
|
@@ -1,38 +1,90 @@
|
|
|
1
|
-
// Record
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
21
|
-
THEN [k IN COLLECT { MATCH (n)-[:OUTPUT]->(f:Field) RETURN f.name } WHERE
|
|
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,
|
|
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(
|
|
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,
|
|
68
|
+
WITH n, res, attempt, missing, ok, review, notes, symbols
|
|
31
69
|
OPTIONAL MATCH (n)-[:OUTPUT]->(f:Field) WHERE ok
|
|
32
|
-
SET f.value =
|
|
33
|
-
WITH DISTINCT n,
|
|
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 =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.
|
|
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;
|
package/cypher/schema.cypher
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// gbuild store schema. Run once, when plan creates the store:
|
|
2
|
-
// cypherlite query --database
|
|
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
|
-
.
|
|
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;
|
package/cypher/set-status.cypher
CHANGED
|
@@ -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
|
|
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
|
package/cypher/status.cypher
CHANGED
|
@@ -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
|
-
//
|
|
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;
|
package/cypher/validate.cypher
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
// gbuild format gate. Zero rows = valid; every row is one violation.
|
|
2
|
-
// cypherlite query --database
|
|
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,
|
|
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
|
|
33
|
-
\`subagent\` tool
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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",
|
package/reference/checklist.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ship checklist
|
|
2
2
|
|
|
3
|
-
`plan` runs this checklist against the graph it just wrote, before
|
|
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
|