opencode-gbuild 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,9 @@ description: Reports a gbuild feature's graph — frontier, blocked, in-flight,
4
4
 
5
5
  Report status for `$ARGUMENTS`.
6
6
 
7
+ Read `${CLAUDE_PLUGIN_ROOT}/reference/cypher.md` first and run its version check. This skill only reads
8
+ the store; `DB` below is `.gbuild/<feature>/db`.
9
+
7
10
  ## Mode
8
11
 
9
12
  | arguments | mode |
@@ -15,70 +18,57 @@ Report status for `$ARGUMENTS`.
15
18
 
16
19
  ### 1. Load
17
20
 
18
- `.gbuild/<feature>/graph.json` (a CypherLite GraphData file — GbuildNodes have a `slug` property and are
19
- linked by `DEPENDS_ON` relationships; feature-level acceptance is `Acceptance` nodes linked by
20
- `HAS_ACCEPTANCE`/`SATISFIES`) and every `.gbuild/<feature>/nodes/*.json` checkpoint (keyed by slug). This
21
- is a small, bounded read — status doesn't scale with graph size the way loading full node prose would.
22
-
23
- ### 2. Compute
24
-
25
21
  ```
26
- python3 ${CLAUDE_PLUGIN_ROOT}/scripts/graph.py .gbuild/<feature>/graph.json --status
22
+ cypherlite DB -json < ${CLAUDE_PLUGIN_ROOT}/cypher/status.cypher
23
+ cypherlite DB -json < ${CLAUDE_PLUGIN_ROOT}/cypher/validate.cypher
27
24
  ```
28
25
 
29
- Gives `frontier` / `blocked` / `in_flight` / `completed` / `cancelled` / `failed` / `waves` (all as slug
30
- lists) directly — don't re-derive these by hand.
26
+ One call after the other. `status.cypher` gives one row per node with its derived `state`
27
+ (`frontier` / `blocked` / `in_progress` / `completed` / `cancelled` / `failed`), `wave`, `waiting_on`,
28
+ review summary and timestamps — don't re-derive any of these by hand. This is a small, bounded read —
29
+ status doesn't scale with graph size the way loading full node prose would.
31
30
 
32
- ### 3. Render the graph
31
+ ### 2. Render the graph
33
32
 
34
- Draw the DAG with a marker per node status:
33
+ Draw the DAG with a marker per node state:
35
34
 
36
35
  ```
37
- ✓ completed ◐ in_progress ○ pending (frontier) ● pending (blocked) ✗ failed
36
+ ✓ completed ◐ in_progress ○ frontier ● blocked ✗ failed – cancelled
38
37
  ```
39
38
 
40
- Annotate blocked nodes with what they're waiting on: `● d-join-and-sum ← b-consume-doubled, c-consume-squared`.
39
+ Annotate blocked nodes with what they're waiting on (`waiting_on`):
40
+ `● d-join-and-sum ← b-consume-doubled, c-consume-squared`.
41
41
 
42
- Group by wave (from `--waves`) so fan-out is visible in the layout, not just in the dependency list.
42
+ Group by `wave` so fan-out is visible in the layout, not just in the dependency list.
43
43
 
44
- ### 4. Review verdicts
44
+ ### 3. Review verdicts
45
45
 
46
- For every `completed` node, one line: pass outright, or passed after N repair attempts, from its
47
- checkpoint's `review` field. For `failed` nodes, the last review's failed acceptance bullets verbatim —
48
- this is what a human needs to make the `escalate`/`stop` call.
46
+ For every `completed` node, one line: passed outright (`review_attempts: 1`), or passed after N repair
47
+ attempts. For `failed` nodes, `last_failed_criteria` verbatim — this is what a human needs to make the
48
+ `escalate`/`stop` call.
49
49
 
50
- ### 5. Concurrency evidence
50
+ ### 4. Concurrency evidence
51
51
 
52
- For each wave with more than one node, compare `started_at` timestamps across that wave's checkpoints.
53
- Report whether they actually overlapped (real concurrent dispatch) or ran sequentially despite being in
54
- the same wave (a sign the fallback backend was invoked one node at a time — a bug in how `run` was
55
- driven, not a graph problem).
52
+ For each wave with more than one completed node, compare the implementing agents' `started_at` /
53
+ `completed_at` intervals. Report whether they actually overlapped (real concurrent dispatch) or ran
54
+ back-to-back despite being in the same wave (a sign the fallback backend was invoked one node at a time —
55
+ a bug in how `run` was driven, not a graph problem).
56
56
 
57
- ### 6. Next action
57
+ ### 5. Next action
58
58
 
59
59
  One priority-ordered list: what `/gbuild:run <feature>` would do next (the current frontier), and
60
- anything needing a human decision first (`failed`/`escalate`d nodes, an empty frontier with incomplete
61
- nodes remaining).
62
-
63
- ### 7. Invariant checks
64
-
65
- Numbered, only reporting violations (silence on an item means it passed, not that it was skipped):
60
+ anything needing a human decision first (`failed` nodes, an empty frontier with incomplete nodes
61
+ remaining, `in_progress` nodes when no run is active — an interrupted wave `run` will reset).
66
62
 
67
- 1. Every GbuildNode has non-empty `acceptance` (should already be caught by `validate_format.py`'s own
68
- validation — report if a node manages to lack it anyway, since that means the file was hand-edited or
69
- written by something other than `plan`).
70
- 2. Every `Acceptance` node is `SATISFIES`-covered by at least one GbuildNode (enforced by
71
- `validate_format.py` — report if one slips through anyway).
72
- 3. `DEPENDS_ON` is acyclic (validated by `validate_format.py`/`graph.py` already; report if it somehow
73
- wasn't checked).
74
- 4. No `completed` node's checkpoint is missing a `review` field with `verdict: pass` — a node marked
75
- complete without a passing review means `run` skipped the review step.
76
- 5. Every `chore`-type GbuildNode is the target of at least one `DEPENDS_ON` edge.
63
+ ### 6. Invariant checks
77
64
 
78
- If all five are clear, say so in one line — don't let silence be ambiguous between "checked, clear" and
79
- "not checked."
65
+ `validate.cypher` is the invariant check — it covers non-empty acceptance, acceptance coverage,
66
+ acyclicity, completed nodes without a passing review or missing an output value, orphan chores, and the
67
+ `FROM` cut test. Report every row it returned, numbered. If it returned `[]`, say so in one line — don't
68
+ let silence be ambiguous between "checked, clear" and "not checked."
80
69
 
81
70
  ## All-features mode
82
71
 
83
- For each `.gbuild/<slug>/graph.json` found, one line: slug, node counts by status, and whether the
84
- frontier is empty (done or stuck) or has ready work. Point at `/gbuild:status <slug>` for detail.
72
+ For each `.gbuild/<slug>/db` found, run `status.cypher` — one store at a time — and print one line: slug,
73
+ node counts by state, and whether the frontier is empty (done or stuck) or has ready work. Point at
74
+ `/gbuild:status <slug>` for detail.
@@ -0,0 +1,57 @@
1
+ // A worked 4-node diamond: A produces a value, B and C each consume it
2
+ // independently, D joins both. This is exactly the script plan writes for a
3
+ // new feature — one atomic statement, piped into the store:
4
+ // cypherlite .gbuild/<slug>/db < plan.cypher
5
+ // Variables (f, a1, a, a_value, ...) are only scoped to this one statement, so
6
+ // the whole graph is created in a single CREATE chain.
7
+ .begin
8
+ CREATE (f:Feature {
9
+ feature: 'example-diamond',
10
+ destination: 'A tiny worked example: A produces a shared value, B and C each consume it independently, D joins both.',
11
+ context: "Ships with the plugin as the toy graph used in gbuild's own tests. Not a real feature.",
12
+ out_of_scope: ['Anything beyond demonstrating fan-out and join scheduling.']
13
+ })
14
+ CREATE (a1:Acceptance {id: 'a-1', text: 'run dispatches B and C concurrently, not one after the other'})
15
+ CREATE (a2:Acceptance {id: 'a-2', text: 'D does not start until both B and C are recorded completed'})
16
+ CREATE (f)-[:HAS_ACCEPTANCE]->(a1), (f)-[:HAS_ACCEPTANCE]->(a2)
17
+
18
+ CREATE (a:GbuildNode:Chore {
19
+ slug: 'a-produce-shared-value', title: 'Produce the shared value B and C both need', type: 'chore',
20
+ acceptance: ['output.value is a single integer'],
21
+ failure_policy: 'retry', model_tier: 'cheap', status: 'pending'
22
+ })
23
+ CREATE (a)-[:OUTPUT]->(a_value:Field {name: 'value', shape: '<integer>'})
24
+
25
+ CREATE (b:GbuildNode:Code {
26
+ slug: 'b-consume-doubled', title: 'Consume the shared value, produce its double', type: 'code',
27
+ acceptance: ['output.doubled equals input.value multiplied by 2'],
28
+ failure_policy: 'repair', model_tier: 'cheap', status: 'pending'
29
+ })
30
+ CREATE (b)-[:DEPENDS_ON]->(a)
31
+ CREATE (b)-[:INPUT]->(:Field {name: 'value', shape: '<integer>'})-[:FROM]->(a_value)
32
+ CREATE (b)-[:OUTPUT]->(b_doubled:Field {name: 'doubled', shape: '<integer>'})
33
+ CREATE (b)-[:SATISFIES]->(a1)
34
+
35
+ CREATE (c:GbuildNode:Code {
36
+ slug: 'c-consume-squared', title: 'Consume the shared value, produce its square', type: 'code',
37
+ acceptance: ['output.squared equals input.value raised to the power of 2'],
38
+ failure_policy: 'repair', model_tier: 'cheap', status: 'pending'
39
+ })
40
+ CREATE (c)-[:DEPENDS_ON]->(a)
41
+ CREATE (c)-[:INPUT]->(:Field {name: 'value', shape: '<integer>'})-[:FROM]->(a_value)
42
+ CREATE (c)-[:OUTPUT]->(c_squared:Field {name: 'squared', shape: '<integer>'})
43
+ CREATE (c)-[:SATISFIES]->(a1)
44
+
45
+ CREATE (d:GbuildNode:Code {
46
+ slug: 'd-join-and-sum', title: 'Join B and C, produce their sum', type: 'code',
47
+ acceptance: ['output.sum equals input.doubled plus input.squared',
48
+ 'only runs after both b-consume-doubled and c-consume-squared are recorded completed'],
49
+ failure_policy: 'repair', model_tier: 'cheap', status: 'pending'
50
+ })
51
+ CREATE (d)-[:DEPENDS_ON]->(b), (d)-[:DEPENDS_ON]->(c)
52
+ CREATE (d)-[:INPUT]->(:Field {name: 'doubled', shape: '<integer>'})-[:FROM]->(b_doubled)
53
+ CREATE (d)-[:INPUT]->(:Field {name: 'squared', shape: '<integer>'})-[:FROM]->(c_squared)
54
+ CREATE (d)-[:OUTPUT]->(:Field {name: 'sum', shape: '<integer>'})
55
+ CREATE (d)-[:SATISFIES]->(a2);
56
+ .commit
57
+ .checkpoint
File without changes
package/scripts/graph.py DELETED
@@ -1,231 +0,0 @@
1
- #!/usr/bin/env python3
2
- """gbuild graph utility: validate, topo-order, and query a graph.json feature graph.
3
-
4
- graph.json is a CypherLite GraphData file — openable with `cypherlite <path>` and
5
- queryable with Cypher. This module is the query layer (topo waves, frontier,
6
- status report) over that file. Format validation lives in validate_format.py
7
- (the single source of truth for the contract); this module imports and re-exports
8
- its validate() and enum sets.
9
-
10
- Stdlib only, no pip install required. Schema: ../reference/graph-format.md
11
-
12
- CLI:
13
- python3 graph.py <graph.json> --waves print the topological wave decomposition
14
- python3 graph.py <graph.json> --frontier print node ids ready to run right now
15
- python3 graph.py <graph.json> --status print the full status report (default)
16
-
17
- Checkpoints are read from <graph dir>/nodes/<slug>.json by default, or --state-dir.
18
- """
19
- import argparse
20
- import json
21
- import sys
22
- from pathlib import Path
23
-
24
- # Format contract + enum sets live in validate_format (single definition site).
25
- from validate_format import (
26
- GraphError,
27
- SATISFIED_STATUSES,
28
- VALID_FAILURE_POLICIES,
29
- VALID_MODEL_TIERS,
30
- VALID_NODE_TYPES,
31
- VALID_TYPE_LABELS,
32
- validate,
33
- )
34
-
35
-
36
- # --- helpers over the GraphData shape ---------------------------------------
37
-
38
- def _labels(node):
39
- return set(node.get("labels", []))
40
-
41
-
42
- def _props(node):
43
- return node.get("properties", {}) if isinstance(node.get("properties"), dict) else {}
44
-
45
-
46
- def gbuild_nodes(graph):
47
- """GbuildNode-labelled nodes, as a list."""
48
- return [n for n in graph.get("nodes", []) if "GbuildNode" in _labels(n)]
49
-
50
-
51
- def node_by_id(graph, nid):
52
- for n in graph.get("nodes", []):
53
- if n.get("id") == nid:
54
- return n
55
- return None
56
-
57
-
58
- def node_by_slug(graph, slug):
59
- for n in gbuild_nodes(graph):
60
- if _props(n).get("slug") == slug:
61
- return n
62
- return None
63
-
64
-
65
- def slug_of(graph, nid):
66
- n = node_by_id(graph, nid)
67
- return _props(n).get("slug") if n else None
68
-
69
-
70
- def id_of_slug(graph, slug):
71
- n = node_by_slug(graph, slug)
72
- return n.get("id") if n else None
73
-
74
-
75
- def deps(graph, node_id):
76
- """node_ids this node depends on (DEPENDS_ON end_nodes)."""
77
- return [
78
- r["end_node"]
79
- for r in graph.get("relationships", [])
80
- if r.get("type") == "DEPENDS_ON" and r.get("start_node") == node_id
81
- ]
82
-
83
-
84
- def satisfies(graph, node_id):
85
- """acceptance node_ids this node covers (SATISFIES end_nodes)."""
86
- return [
87
- r["end_node"]
88
- for r in graph.get("relationships", [])
89
- if r.get("type") == "SATISFIES" and r.get("start_node") == node_id
90
- ]
91
-
92
-
93
- def feature(graph):
94
- """the single Feature node, or None."""
95
- for n in graph.get("nodes", []):
96
- if "Feature" in _labels(n):
97
- return n
98
- return None
99
-
100
-
101
- def acceptance_nodes(graph):
102
- return [n for n in graph.get("nodes", []) if "Acceptance" in _labels(n)]
103
-
104
-
105
- # --- core query layer --------------------------------------------------------
106
-
107
- def load_graph(path):
108
- with open(path) as f:
109
- return json.load(f)
110
-
111
-
112
- def topo_waves(graph):
113
- """Layer GbuildNodes into parallel-safe waves by slug.
114
-
115
- wave N contains every node whose DEPENDS_ON targets all resolved in <N.
116
- Returns a list of lists of slugs.
117
- """
118
- gn = gbuild_nodes(graph)
119
- # map node id -> slug, and slug -> dep slugs
120
- id_to_slug = {n["id"]: _props(n).get("slug") for n in gn}
121
- remaining = {id_to_slug[n["id"]]: n for n in gn}
122
- resolved = set()
123
- waves = []
124
-
125
- while remaining:
126
- wave = []
127
- for slug, node in remaining.items():
128
- dep_ids = deps(graph, node["id"])
129
- dep_slugs = {id_to_slug.get(d) for d in dep_ids}
130
- if dep_slugs.issubset(resolved):
131
- wave.append(slug)
132
- if not wave:
133
- raise GraphError(f"cycle detected among: {', '.join(sorted(remaining))}")
134
- wave.sort()
135
- waves.append(wave)
136
- for slug in wave:
137
- resolved.add(slug)
138
- del remaining[slug]
139
-
140
- return waves
141
-
142
-
143
- def read_checkpoint(state_dir, slug):
144
- path = Path(state_dir) / f"{slug}.json"
145
- if not path.exists():
146
- return {"status": "pending"}
147
- with open(path) as f:
148
- return json.load(f)
149
-
150
-
151
- def node_statuses(graph, state_dir):
152
- """slug -> status, for every GbuildNode."""
153
- return {
154
- _props(n).get("slug"): read_checkpoint(state_dir, _props(n).get("slug")).get("status", "pending")
155
- for n in gbuild_nodes(graph)
156
- }
157
-
158
-
159
- def compute_report(graph, state_dir):
160
- statuses = node_statuses(graph, state_dir)
161
- id_slug = {n["id"]: _props(n).get("slug") for n in gbuild_nodes(graph)}
162
- frontier, blocked, in_flight, completed, cancelled, failed = [], [], [], [], [], []
163
-
164
- for node in gbuild_nodes(graph):
165
- slug = _props(node).get("slug")
166
- status = statuses.get(slug, "pending")
167
- dep_ids = deps(graph, node["id"])
168
- dep_statuses = [statuses.get(id_slug.get(d), "pending") for d in dep_ids]
169
- if status == "completed":
170
- completed.append(slug)
171
- elif status == "cancelled":
172
- cancelled.append(slug)
173
- elif status == "in_progress":
174
- in_flight.append(slug)
175
- elif status == "failed":
176
- failed.append(slug)
177
- elif all(ds in SATISFIED_STATUSES for ds in dep_statuses):
178
- frontier.append(slug)
179
- else:
180
- blocked.append(slug)
181
-
182
- return {
183
- "frontier": sorted(frontier),
184
- "blocked": sorted(blocked),
185
- "in_flight": sorted(in_flight),
186
- "completed": sorted(completed),
187
- "cancelled": sorted(cancelled),
188
- "failed": sorted(failed),
189
- "waves": topo_waves(graph),
190
- }
191
-
192
-
193
- def compute_frontier(graph, state_dir):
194
- return compute_report(graph, state_dir)["frontier"]
195
-
196
-
197
- def default_state_dir(graph_path):
198
- return Path(graph_path).resolve().parent / "nodes"
199
-
200
-
201
- def main(argv=None):
202
- parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
203
- parser.add_argument("graph_path")
204
- parser.add_argument("--state-dir", default=None, help="checkpoint dir, default: <graph dir>/nodes")
205
- group = parser.add_mutually_exclusive_group()
206
- group.add_argument("--waves", action="store_true")
207
- group.add_argument("--frontier", action="store_true")
208
- group.add_argument("--status", action="store_true")
209
- args = parser.parse_args(argv)
210
-
211
- graph = load_graph(args.graph_path)
212
- try:
213
- validate(graph)
214
- except GraphError as e:
215
- print(f"invalid graph: {e}", file=sys.stderr)
216
- return 1
217
-
218
- state_dir = args.state_dir or default_state_dir(args.graph_path)
219
-
220
- if args.waves:
221
- print(json.dumps(topo_waves(graph)))
222
- elif args.frontier:
223
- print(json.dumps(compute_frontier(graph, state_dir)))
224
- else:
225
- print(json.dumps(compute_report(graph, state_dir), indent=2))
226
-
227
- return 0
228
-
229
-
230
- if __name__ == "__main__":
231
- sys.exit(main())