opencode-gbuild 0.8.0 → 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 +66 -27
- package/agents/gbuild-implementer.md +186 -0
- package/agents/gbuild-reviewer.md +113 -53
- package/cypher/brief.cypher +39 -0
- package/cypher/check-result.cypher +19 -0
- package/cypher/dispatch.cypher +8 -25
- package/cypher/node.cypher +4 -3
- package/cypher/record.cypher +81 -28
- package/cypher/schema.cypher +19 -0
- package/cypher/status.cypher +9 -4
- package/cypher/symbols.cypher +9 -0
- package/cypher/validate.cypher +20 -5
- package/index.ts +19 -11
- package/package.json +1 -1
- package/reference/cypher.md +29 -19
- package/reference/graph-format.md +54 -14
- package/reference/queries.md +4 -0
- package/skills/plan/SKILL.md +3 -3
- package/skills/run/SKILL.md +145 -108
- package/skills/status/SKILL.md +16 -8
package/cypher/dispatch.cypher
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
|
-
// Claim
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
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;
|
package/cypher/node.cypher
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
|
-
//
|
|
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.
|
|
6
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,
|
|
@@ -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/record.cypher
CHANGED
|
@@ -1,37 +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
|
-
|
|
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
|
@@ -42,3 +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
|
+
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/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
|
+
// `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.
|
|
7
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
|
@@ -3,16 +3,17 @@
|
|
|
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/cypher.md
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
Every gbuild skill reads and writes the feature store (modelled in `reference/graph-format.md`) with the
|
|
4
4
|
`cypherlite` CLI and nothing else. `DB` throughout is the store path:
|
|
5
5
|
`<git-common-dir>/gbuild/<feature>/db` — beside the feature worktree, machine-local, never committed.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
`INBOX` is `<git-common-dir>/gbuild/<feature>/inbox`, beside it: where `run`'s agents leave their result
|
|
7
|
+
files for `record.cypher` to read. This file is the full operating manual; the skills that only run
|
|
8
|
+
plugin scripts carry the short version of these rules inline.
|
|
8
9
|
|
|
9
10
|
## Requirement
|
|
10
11
|
|
|
@@ -24,15 +25,19 @@ require the explicit `query` verb. The old path-first form — database as the f
|
|
|
24
25
|
|
|
25
26
|
## One writer, one command at a time
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
record
|
|
33
|
-
- **
|
|
34
|
-
|
|
35
|
-
-
|
|
28
|
+
The store is a directory store. A writable open takes its exclusive write lock; a `--read-only` open
|
|
29
|
+
does not, so read-only queries open the store beside a writer. Only a second writer fails, with
|
|
30
|
+
`storage locked` (exit 5). So:
|
|
31
|
+
|
|
32
|
+
- **Only the skill running in the main context writes to the store.** It is the only writer: schema,
|
|
33
|
+
dispatch, record, set-status and authored writes run there and nowhere else.
|
|
34
|
+
- **Subagents may run read-only queries, never writes.** `run`'s implementers and reviewers read their
|
|
35
|
+
own brief (`brief.cypher`), the helper registry (`symbols.cypher`) and their result check
|
|
36
|
+
(`check-result.cypher`), always with `--read-only` — their agent files list exactly what they run.
|
|
37
|
+
They report through files in `INBOX` plus a one-line reply; the orchestrator records them.
|
|
38
|
+
- **One `cypherlite` command at a time from the main context** — no parallel tool calls on the store.
|
|
39
|
+
Fan-out is for agents, not for store writes.
|
|
40
|
+
- On `storage locked` (exit 5), another writer holds the store (another gbuild command, a REPL left
|
|
36
41
|
open). Wait a few seconds and retry, up to 3 times; then stop and report. Never delete
|
|
37
42
|
`.cypherlite.lock`.
|
|
38
43
|
|
|
@@ -40,9 +45,9 @@ or writer — fails with `storage locked` (exit 5). So:
|
|
|
40
45
|
|
|
41
46
|
Always pass `--mode jsonl`: one compact JSON object per row, one row per line, and no output at all when
|
|
42
47
|
there are no rows. Every call is `cypherlite query [STATEMENT] --database DB …`; with no STATEMENT the
|
|
43
|
-
piped stdin is the script list. Pure reads add `--read-only`: no exit flush,
|
|
44
|
-
fail-fast on a wrong path — a read-only call never creates a missing store.
|
|
45
|
-
dispatch, record, set-status, authored heredocs) go without.
|
|
48
|
+
piped stdin is the script list. Pure reads add `--read-only`: no exit flush, no lock (it opens beside
|
|
49
|
+
a writer, as above), and a fail-fast on a wrong path — a read-only call never creates a missing store.
|
|
50
|
+
Writes (schema creation, dispatch, record, set-status, authored heredocs) go without.
|
|
46
51
|
|
|
47
52
|
```
|
|
48
53
|
# a plugin read script
|
|
@@ -70,6 +75,8 @@ CYPHER
|
|
|
70
75
|
- **Parameters are JSON.** Every `--param` value is JSON, single-quoted for the shell: strings in double
|
|
71
76
|
quotes (`--param 'slug="01-scan-runners"'`), lists and objects as JSON (`--param 'slugs=["a","b"]'`).
|
|
72
77
|
Bare values that start with a digit — timestamps, `01-…` slugs — are rejected, so always quote.
|
|
78
|
+
`--fetch-params NAME=<file>` binds a JSON file as `$NAME` instead — how `record` and `check-result`
|
|
79
|
+
read inbox files without anyone retyping them.
|
|
73
80
|
- **String literals** in Cypher you author: single quotes by default; double quotes when the text
|
|
74
81
|
contains an apostrophe (`"gbuild's"`); backslash-escape (`\'`, `\"`) when it contains both.
|
|
75
82
|
- **Authored writes are atomic.** Wrap them in `.begin` / `.commit` — if any statement fails, the whole
|
|
@@ -119,12 +126,15 @@ All in `${CLAUDE_PLUGIN_ROOT}/cypher/`.
|
|
|
119
126
|
| `validate.cypher` | — | one row per violation (`check`, `detail`); no rows = valid | every skill |
|
|
120
127
|
| `progress.cypher` | — | one row per wave: `total`, counts per state, `open` (slugs not yet completed or cancelled) | every skill |
|
|
121
128
|
| `attention.cypher` | — | only nodes needing a decision: `slug`, `reason` (`failed` / `in_progress` / `blocked-by-failed`), `detail` | run, status |
|
|
122
|
-
| `dispatch.cypher` | — | claims
|
|
123
|
-
| `
|
|
129
|
+
| `dispatch.cypher` | — | claims every frontier node (→ `in_progress`); per claimed node only the routing columns `slug`, `type`, `model_tier`, `failure_policy` — agents read the rest with `brief` | run (at start, and after every record / set-status) |
|
|
130
|
+
| `brief.cypher` | `slug` | one node's brief: `slug`, `title`, `type`, `acceptance`, `verify`, `inputs` (`name`, `shape`, `from`, `value`), `outputs` (`name`, `shape`), `failed_criteria` (the latest review's when it failed, else `[]`), `notes` (`topic`, `text`, `by` — every transitive upstream node's notes plus setup and testing notes from any node, upstream first, then newest, at most 20) | run's implementer and reviewer (read-only) |
|
|
131
|
+
| `check-result.cypher` | `slug`; `result` (`--fetch-params result=INBOX/<slug>.json`) | `slug`, `missing_outputs`, `invalid_outputs`, `unknown_outputs`, `has_times` — clean when the three lists are `[]` and `has_times` is `true` | run's implementer and reviewer (read-only) |
|
|
132
|
+
| `record.cypher` | `verdict` — `{slug, verdict, failed_criteria}`; `result` — `--fetch-params result=INBOX/<slug>.json` on a pass whose file exists, else `{}`; `review` — `--fetch-params review=INBOX/<slug>.review.json` when it exists, else `{}` (all three always bound) | one call per node: `slug`, `attempt`, `verdict`, `completed`, `missing_outputs`, `notes_added`, `symbols_added`, `unselected`; the `Review` keeps the review file's `checklist`, and a completing pass stores the result file's outputs, times, `notes` (as `Note`) and `symbols` (as `Symbol`), skipping notes and symbols already stored | run |
|
|
133
|
+
| `symbols.cypher` | — | `name`, `file`, `purpose`, `by` — every completed node's registered helpers, by file and name, at most 100 | run's implementer and reviewer (read-only) |
|
|
124
134
|
| `set-status.cypher` | `slugs` (list), `status` (`pending` / `in_progress` / `failed` / `cancelled`) | `slug`, `status` per changed node | run |
|
|
125
|
-
| `status.cypher` | — | one row per node: `state`, `wave`, `waiting_on`, review summary, timestamps | status (human report) |
|
|
126
|
-
| `node.cypher` | `slug` | one node in full: properties, `inputs` (with `from`), `outputs`, `depends_on`, `satisfies`, `reviews` | plan (reopen), ad hoc |
|
|
135
|
+
| `status.cypher` | — | one row per node: `state`, `wave`, `waiting_on`, review summary, timestamps, `last_dependency_at` (latest `completed_at` among its completed dependencies, null without any) | status (human report) |
|
|
136
|
+
| `node.cypher` | `slug` | one node in full: properties, `inputs` (with `from`), `outputs`, `depends_on`, `satisfies`, `reviews` (`attempt`, `verdict`, `failed_criteria`, `checklist`, `at`) | plan (reopen), ad hoc |
|
|
127
137
|
| `feature.cypher` | — | `destination`, `context`, `out_of_scope`, `acceptance` (with `covered_by`) | review, pr |
|
|
128
138
|
|
|
129
|
-
Ready-made ad-hoc queries (lineage, critical path, repair history, …) are in
|
|
139
|
+
Ready-made ad-hoc queries (lineage, critical path, repair history, notes by topic, …) are in
|
|
130
140
|
`${CLAUDE_PLUGIN_ROOT}/reference/queries.md`.
|
|
@@ -4,9 +4,9 @@ This file is normative. Where a skill disagrees with this doc, this doc wins.
|
|
|
4
4
|
|
|
5
5
|
One CypherLite store per feature: `<git-common-dir>/gbuild/<feature>/db/`, beside the feature worktree
|
|
6
6
|
(`<git-common-dir>/gbuild/<feature>/tree/`, which `plan` creates) — untracked, machine-local, never
|
|
7
|
-
committed. `plan` creates and charts it, `run` records progress in it, and every skill reads it
|
|
8
|
-
through Cypher**. No skill reads or writes `graph.json` directly; it
|
|
9
|
-
the store, not a gbuild artifact. How to talk to the store (the single-writer rule, invocation, quoting,
|
|
7
|
+
committed. `plan` creates and charts it, `run` records progress in it, and every skill reads it, as do
|
|
8
|
+
`run`'s agents (read-only) — **all through Cypher**. No skill reads or writes `graph.json` directly; it
|
|
9
|
+
is CypherLite's local snapshot of the store, not a gbuild artifact. How to talk to the store (the single-writer rule, invocation, quoting,
|
|
10
10
|
the script catalog) is in `reference/cypher.md`.
|
|
11
11
|
|
|
12
12
|
`templates/example.cypher` is a complete worked example — a 4-node diamond — written exactly the way
|
|
@@ -15,13 +15,21 @@ the script catalog) is in `reference/cypher.md`.
|
|
|
15
15
|
## Store layout
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
<git-common-dir>/gbuild/<feature>/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
<git-common-dir>/gbuild/<feature>/
|
|
19
|
+
db/ the store
|
|
20
|
+
graph.json CypherLite JSON snapshot — the engine's local snapshot; gbuild never reads or writes it
|
|
21
|
+
manifest.json engine files
|
|
22
|
+
wal/
|
|
23
|
+
.cypherlite.lock
|
|
24
|
+
inbox/ run's hand-off files: <slug>.json (implementer result), <slug>.review.json (reviewer checklist)
|
|
25
|
+
tree/ the feature worktree
|
|
26
|
+
<slug>/ a file-touching node's worktree, during run
|
|
23
27
|
```
|
|
24
28
|
|
|
29
|
+
The inbox is not part of the store: `run` empties it when it starts, agents write their files there, and
|
|
30
|
+
`record.cypher` reads them with `--fetch-params` — a node's outputs, notes, symbols and checklist reach
|
|
31
|
+
the store only through that record.
|
|
32
|
+
|
|
25
33
|
The store lives outside every checkout: untracked, machine-local, never committed. It is created with
|
|
26
34
|
the JSON codec (`--snapshot-format json`, see `reference/cypher.md`) for cross-build compatibility: a
|
|
27
35
|
parquet-enabled CypherLite build defaults a new store to Parquet, and a JSON-pinned store opens in every
|
|
@@ -36,6 +44,8 @@ build.
|
|
|
36
44
|
| `GbuildNode` | one unit of work (always with a PascalCase type label) | one per node |
|
|
37
45
|
| `Field` | one named input or output of a GbuildNode's contract | one per field |
|
|
38
46
|
| `Review` | one `gbuild-reviewer` verdict on a GbuildNode | one per review attempt |
|
|
47
|
+
| `Note` | a fact an implementer left for later agents | 0–5 per completed node |
|
|
48
|
+
| `Symbol` | a reusable helper a completed node added | 0–10 per completed node |
|
|
39
49
|
|
|
40
50
|
### `Feature`
|
|
41
51
|
|
|
@@ -62,7 +72,7 @@ universal label is for `MATCH (n:GbuildNode)`; the type label enables `MATCH (n:
|
|
|
62
72
|
|
|
63
73
|
| property | type | meaning |
|
|
64
74
|
| ---------------- | -------------- | -------------------------------------------------------------- |
|
|
65
|
-
| `slug` | string | kebab-case, unique, immutable — the identity every skill keys on; `tree`
|
|
75
|
+
| `slug` | string | kebab-case, unique, immutable — the identity every skill keys on; `tree`, `db` and `inbox` are reserved (they name the directories beside the node worktrees) |
|
|
66
76
|
| `title` | string | what the user reads; refer to nodes by title in conversation |
|
|
67
77
|
| `type` | string | `research \| decision \| code \| test \| verify \| chore` |
|
|
68
78
|
| `acceptance` | list of string | node-level criteria — mandatory, non-empty, concrete |
|
|
@@ -107,8 +117,36 @@ map as a property value. JSON-encode anything nested into a string and say so in
|
|
|
107
117
|
### `Review` — one reviewer verdict
|
|
108
118
|
|
|
109
119
|
`attempt` (integer, 1-based, assigned by `record.cypher`), `verdict` (`pass | fail`),
|
|
110
|
-
`failed_criteria` (list of the acceptance criteria the reviewer failed; empty on a pass), `
|
|
111
|
-
|
|
120
|
+
`failed_criteria` (list of the acceptance criteria the reviewer failed; empty on a pass), `checklist`
|
|
121
|
+
(list of string — the reviewer's per-check lines from `<slug>.review.json`, at most 40, each cut to 300
|
|
122
|
+
chars; `[]` when it wrote none), `at` (string). The verdict comes from the reviewer's reply, never from
|
|
123
|
+
the checklist file. Reviews are never updated or deleted — the repair history stays visible to `status`.
|
|
124
|
+
|
|
125
|
+
### `Note` — a fact left for later agents
|
|
126
|
+
|
|
127
|
+
| property | type | meaning |
|
|
128
|
+
| --------- | ------ | -------------------------------------------------------------------- |
|
|
129
|
+
| `topic` | string | `setup \| testing \| conventions \| gotcha` |
|
|
130
|
+
| `text` | string | non-blank, trimmed, at most 300 chars |
|
|
131
|
+
| `by_node` | string | slug of the writing GbuildNode — the note's only `WROTE` source |
|
|
132
|
+
| `at` | string | the completing `Review`'s `at` |
|
|
133
|
+
|
|
134
|
+
### `Symbol` — a registered helper
|
|
135
|
+
|
|
136
|
+
| property | type | meaning |
|
|
137
|
+
| --------- | ------ | -------------------------------------------------------------------- |
|
|
138
|
+
| `name` | string | non-blank identifier, as written in code |
|
|
139
|
+
| `file` | string | non-blank repo-relative path |
|
|
140
|
+
| `purpose` | string | one line, at most 160 chars |
|
|
141
|
+
| `by_node` | string | slug of the defining GbuildNode — the symbol's only `DEFINES` source |
|
|
142
|
+
| `at` | string | the completing `Review`'s `at` |
|
|
143
|
+
|
|
144
|
+
Only `record.cypher` writes them, from the optional `notes` and `symbols` in a node's result file, and
|
|
145
|
+
only when it completes the node — so they exist only for completed nodes; a fail or a downgraded pass
|
|
146
|
+
stores none. A node keeps at most 5 notes (any whose exact `topic` and `text` is already stored is
|
|
147
|
+
skipped) and 10 symbols (the first of each `name` and `file`). `brief.cypher` gives a node its
|
|
148
|
+
transitive upstream nodes' notes plus every `setup` and `testing` note; `symbols.cypher` lists every
|
|
149
|
+
symbol.
|
|
112
150
|
|
|
113
151
|
## Relationships
|
|
114
152
|
|
|
@@ -121,6 +159,8 @@ Reviews are never updated or deleted — the repair history stays visible to `st
|
|
|
121
159
|
| `OUTPUT` | `GbuildNode` → `Field` | the node produces this field |
|
|
122
160
|
| `FROM` | `Field` → `Field` | an input field reads an upstream node's output field |
|
|
123
161
|
| `REVIEWED` | `GbuildNode` → `Review` | a verdict on the node |
|
|
162
|
+
| `WROTE` | `GbuildNode` → `Note` | the node left this note |
|
|
163
|
+
| `DEFINES` | `GbuildNode` → `Symbol` | the node added this helper |
|
|
124
164
|
|
|
125
165
|
No other edge shapes are valid. `MATCH (n)-[:DEPENDS_ON]->(d)` is "what does n need";
|
|
126
166
|
`MATCH (n)<-[:DEPENDS_ON]-(d)` is "what waits on n".
|
|
@@ -175,9 +215,9 @@ with a value for every `OUTPUT` — a pass missing outputs is recorded as a fail
|
|
|
175
215
|
|
|
176
216
|
| layer | enforces |
|
|
177
217
|
| ------------------------- | ------------------------------------------------------------------------ |
|
|
178
|
-
| engine constraints (`cypher/schema.cypher`) | required properties and their types; unique `slug` and `Acceptance.id` — a violating write never lands |
|
|
179
|
-
| `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 |
|
|
180
|
-
| write scripts (`cypher/*.cypher`) | `dispatch` claims only frontier nodes; `record` completes a node only on a pass with every output; `set-status` never sets `completed` |
|
|
218
|
+
| engine constraints (`cypher/schema.cypher`) | required properties and their types (including every `Note` and `Symbol` property, and `Review.checklist` as a list of strings); unique `slug` and `Acceptance.id` — a violating write never lands |
|
|
219
|
+
| `cypher/validate.cypher` | exactly one `Feature`; enums; kebab-case slugs other than `tree`, `db`, `inbox` (`bad-slug`); type label matches `type`; non-blank acceptance; `verify` target exists; acceptance linked and covered; allowed labels and edge shapes only (`WROTE`, `DEFINES` included); 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; note topic and non-blank text (`bad-note`); non-blank symbol name and file (`bad-symbol`); each note and symbol owned by exactly its `by_node` (`note-owner`, `symbol-owner`) |
|
|
220
|
+
| write scripts (`cypher/*.cypher`) | `dispatch` claims only frontier nodes; `record` completes a node only on a pass with every output, and stores notes and symbols only then; `set-status` never sets `completed` |
|
|
181
221
|
|
|
182
222
|
## Querying
|
|
183
223
|
|
package/reference/queries.md
CHANGED
|
@@ -35,4 +35,8 @@ MATCH (a:Acceptance)<-[:SATISFIES]-(n:GbuildNode) RETURN a.id, collect(n.slug)
|
|
|
35
35
|
|
|
36
36
|
-- nodes that needed a repair pass
|
|
37
37
|
MATCH (n:GbuildNode)-[:REVIEWED]->(r:Review {verdict: 'fail'}) RETURN n.slug, count(r) AS failed_reviews
|
|
38
|
+
|
|
39
|
+
-- notes by topic, newest first (topics: setup, testing, conventions, gotcha)
|
|
40
|
+
MATCH (nt:Note) WHERE nt.topic IN ['setup'] RETURN nt.topic AS topic, nt.text AS text, nt.by_node AS by
|
|
41
|
+
ORDER BY nt.at DESC, text LIMIT 20
|
|
38
42
|
```
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -7,9 +7,9 @@ Plan the work described in `$ARGUMENTS` as a graph.
|
|
|
7
7
|
## The loop
|
|
8
8
|
|
|
9
9
|
The intended workflow: plan in ordinary conversation first, then chart that plan here. After this
|
|
10
|
-
skill: `/gbuild:run <slug>` once — it
|
|
11
|
-
`/gbuild:review <slug>`; if it finds issues, reopen with
|
|
12
|
-
`/gbuild:run` again; if clean, `/gbuild:pr <slug>`.
|
|
10
|
+
skill: `/gbuild:run <slug>` once — it starts each node as its dependencies complete, until the graph
|
|
11
|
+
completes. Then `/gbuild:review <slug>`; if it finds issues, reopen with
|
|
12
|
+
`/gbuild:plan <slug> --add "<fix>"` and `/gbuild:run` again; if clean, `/gbuild:pr <slug>`.
|
|
13
13
|
|
|
14
14
|
Read `${CLAUDE_PLUGIN_ROOT}/reference/graph-format.md` (the normative graph model) and
|
|
15
15
|
`${CLAUDE_PLUGIN_ROOT}/reference/cypher.md` (how to talk to the store) before doing anything, and run
|