@0xgeorgemathew/zcode 0.3.1 → 0.4.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 +179 -130
- package/bin/zcode.js +181 -140
- package/lib/appserver.js +190 -135
- package/lib/bootstrap.js +39 -60
- package/lib/checks.js +103 -0
- package/lib/commands/doctor.js +43 -64
- package/lib/commands/exec.js +327 -0
- package/lib/commands/login.js +2 -9
- package/lib/commands/mcp-list.js +6 -14
- package/lib/commands/sessions.js +181 -0
- package/lib/contract.js +301 -0
- package/lib/executor.js +617 -0
- package/lib/isolation.js +253 -193
- package/lib/key.js +6 -9
- package/lib/store.js +276 -0
- package/lib/ui.js +200 -71
- package/lib/workspace.js +336 -0
- package/package.json +9 -5
- package/skills/README.md +32 -0
- package/skills/claude-code/zcode/SKILL.md +112 -0
- package/skills/codex/zcode/SKILL.md +114 -0
- package/skills/codex/zcode/agents/openai.yaml +4 -0
- package/lib/commands/agent.js +0 -203
- package/lib/commands/run.js +0 -126
- package/lib/overlay.js +0 -138
package/lib/workspace.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Workspace boundaries for concurrent sessions:
|
|
3
|
+
// - ownership: `--owns` patterns (paths or globs, relative to --cwd) name
|
|
4
|
+
// what a session may modify; none = the whole workspace.
|
|
5
|
+
// - leases: a live session registers its ownership under
|
|
6
|
+
// <root>/leases/<workspace>/; a new session whose ownership overlaps a
|
|
7
|
+
// live one is refused (exit 7) unless --allow-overlap.
|
|
8
|
+
// - change tracking: a snapshot before and after the work yields the files
|
|
9
|
+
// the session changed (git status + content hashes in a git repo, a
|
|
10
|
+
// bounded mtime walk otherwise) and the ones outside its ownership.
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const crypto = require('node:crypto');
|
|
14
|
+
const { execFileSync } = require('node:child_process');
|
|
15
|
+
const { rootDir } = require('./isolation.js');
|
|
16
|
+
const { pidAlive } = require('./store.js');
|
|
17
|
+
|
|
18
|
+
class OwnershipError extends Error {}
|
|
19
|
+
|
|
20
|
+
const toPosix = (p) => p.split(path.sep).join('/');
|
|
21
|
+
|
|
22
|
+
function gitToplevel(cwd) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
27
|
+
timeout: 5000,
|
|
28
|
+
}).trim();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The lease/change-tracking root: the git toplevel when --cwd is inside a
|
|
35
|
+
// repo (so sessions in sibling subdirectories still see each other), else
|
|
36
|
+
// the real --cwd.
|
|
37
|
+
function workspaceRoot(cwd) {
|
|
38
|
+
const real = fs.realpathSync(cwd);
|
|
39
|
+
const top = gitToplevel(real);
|
|
40
|
+
return top ? fs.realpathSync(top) : real;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Ownership patterns arrive relative to --cwd; store them relative to the
|
|
44
|
+
// workspace root, posix-style, so every session compares like with like.
|
|
45
|
+
function normalizeOwns(patterns, cwd, root) {
|
|
46
|
+
const realCwd = fs.realpathSync(cwd);
|
|
47
|
+
return patterns.map((raw) => {
|
|
48
|
+
const pattern = String(raw).trim();
|
|
49
|
+
if (!pattern) throw new OwnershipError('--owns pattern must not be empty');
|
|
50
|
+
const abs = path.isAbsolute(pattern) ? pattern : path.join(realCwd, pattern);
|
|
51
|
+
const rel = toPosix(path.relative(root, abs));
|
|
52
|
+
if (rel === '' || rel === '.') return '**';
|
|
53
|
+
if (rel.startsWith('../') || rel === '..' || path.isAbsolute(rel)) {
|
|
54
|
+
throw new OwnershipError(`--owns "${pattern}" is outside the workspace ${root}`);
|
|
55
|
+
}
|
|
56
|
+
return /[/\\]$/.test(pattern) && !hasWildcard(rel) ? `${rel}/**` : rel;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function globToRegExp(glob) {
|
|
61
|
+
let re = '';
|
|
62
|
+
for (let i = 0; i < glob.length; i++) {
|
|
63
|
+
const c = glob[i];
|
|
64
|
+
if (c === '*') {
|
|
65
|
+
if (glob[i + 1] === '*') {
|
|
66
|
+
i++;
|
|
67
|
+
if (glob[i + 1] === '/') {
|
|
68
|
+
i++;
|
|
69
|
+
re += '(?:.*/)?';
|
|
70
|
+
} else {
|
|
71
|
+
re += '.*';
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
re += '[^/]*';
|
|
75
|
+
}
|
|
76
|
+
} else if (c === '?') {
|
|
77
|
+
re += '[^/]';
|
|
78
|
+
} else {
|
|
79
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return new RegExp(`^${re}$`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hasWildcard = (p) => /[*?]/.test(p);
|
|
86
|
+
|
|
87
|
+
// A plain path owns itself and everything beneath it; a glob owns its
|
|
88
|
+
// matches. Empty ownership = the whole workspace.
|
|
89
|
+
function isOwned(relPath, owns) {
|
|
90
|
+
if (!owns || owns.length === 0) return true;
|
|
91
|
+
return owns.some((pattern) => {
|
|
92
|
+
if (!hasWildcard(pattern)) return relPath === pattern || relPath.startsWith(`${pattern}/`);
|
|
93
|
+
return globToRegExp(pattern).test(relPath);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The literal directory part of a pattern (segments before the first
|
|
98
|
+
// wildcard). Overlap is decided conservatively on these prefixes: two
|
|
99
|
+
// patterns overlap when one prefix contains the other.
|
|
100
|
+
function staticSegments(pattern) {
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const seg of pattern.split('/')) {
|
|
103
|
+
if (hasWildcard(seg)) break;
|
|
104
|
+
out.push(seg);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function patternsOverlap(a, b) {
|
|
110
|
+
const sa = staticSegments(a);
|
|
111
|
+
const sb = staticSegments(b);
|
|
112
|
+
const n = Math.min(sa.length, sb.length);
|
|
113
|
+
for (let i = 0; i < n; i++) if (sa[i] !== sb[i]) return false;
|
|
114
|
+
// Same literal prefix. Two fully literal, distinct paths of equal depth
|
|
115
|
+
// were already ruled out above; anything else may share files.
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ownsOverlap(a, b) {
|
|
120
|
+
if (!a || !b || a.length === 0 || b.length === 0) return true;
|
|
121
|
+
return a.some((pa) => b.some((pb) => patternsOverlap(pa, pb)));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- leases -------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
function leaseDir(root, env = process.env) {
|
|
127
|
+
const key = crypto.createHash('sha256').update(root).digest('hex').slice(0, 16);
|
|
128
|
+
return path.join(rootDir(env), 'leases', key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readLeases(dir) {
|
|
132
|
+
let names = [];
|
|
133
|
+
try {
|
|
134
|
+
names = fs.readdirSync(dir).filter((n) => n.endsWith('.json'));
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
try {
|
|
141
|
+
out.push(JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')));
|
|
142
|
+
} catch {
|
|
143
|
+
/* half-written or removed concurrently */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Registers this session's lease, then compares against every other live
|
|
150
|
+
// lease. Writing first and reading second means two sessions starting at
|
|
151
|
+
// the same instant both see each other; the later (startedAt, id) backs
|
|
152
|
+
// off, so exactly one proceeds.
|
|
153
|
+
function acquireLease({ id, root, owns, allowOverlap = false, env = process.env }) {
|
|
154
|
+
const dir = leaseDir(root, env);
|
|
155
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
156
|
+
const mine = { id, pid: process.pid, root, owns, startedAt: Date.now() };
|
|
157
|
+
const file = path.join(dir, `${id}.json`);
|
|
158
|
+
fs.writeFileSync(file, JSON.stringify(mine) + '\n', { mode: 0o600 });
|
|
159
|
+
|
|
160
|
+
const others = [];
|
|
161
|
+
for (const lease of readLeases(dir)) {
|
|
162
|
+
if (!lease || lease.id === id) continue;
|
|
163
|
+
if (!pidAlive(lease.pid)) {
|
|
164
|
+
fs.rmSync(path.join(dir, `${lease.id}.json`), { force: true });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
others.push(lease);
|
|
168
|
+
}
|
|
169
|
+
const conflicts = others.filter((o) => ownsOverlap(owns, o.owns));
|
|
170
|
+
const blocking = conflicts.filter(
|
|
171
|
+
(o) => o.startedAt < mine.startedAt || (o.startedAt === mine.startedAt && o.id < id)
|
|
172
|
+
);
|
|
173
|
+
if (blocking.length > 0 && !allowOverlap) {
|
|
174
|
+
fs.rmSync(file, { force: true });
|
|
175
|
+
return { ok: false, conflicts: blocking, others };
|
|
176
|
+
}
|
|
177
|
+
return { ok: true, conflicts, others, release: () => fs.rmSync(file, { force: true }) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function liveLeases(root, env = process.env) {
|
|
181
|
+
return readLeases(leaseDir(root, env)).filter((l) => l && pidAlive(l.pid));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- change tracking ----------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
const WALK_SKIP = new Set(['.git', 'node_modules', '.zcode', '.DS_Store']);
|
|
187
|
+
const WALK_LIMIT = 20000;
|
|
188
|
+
const HASH_LIMIT = 8 * 1024 * 1024;
|
|
189
|
+
|
|
190
|
+
function fileSignature(abs) {
|
|
191
|
+
try {
|
|
192
|
+
const st = fs.statSync(abs);
|
|
193
|
+
if (!st.isFile()) return `other:${st.mtimeMs}`;
|
|
194
|
+
if (st.size > HASH_LIMIT) return `big:${st.size}:${st.mtimeMs}`;
|
|
195
|
+
return crypto.createHash('sha1').update(fs.readFileSync(abs)).digest('hex');
|
|
196
|
+
} catch {
|
|
197
|
+
return 'missing';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Parses `git status --porcelain=v1 -z`; rename/copy entries carry the
|
|
202
|
+
// source path as an extra NUL field.
|
|
203
|
+
function gitStatus(root) {
|
|
204
|
+
const raw = execFileSync(
|
|
205
|
+
'git',
|
|
206
|
+
['-C', root, 'status', '--porcelain=v1', '-z', '--untracked-files=all'],
|
|
207
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000, maxBuffer: 64 * 1024 * 1024 }
|
|
208
|
+
);
|
|
209
|
+
const fields = raw.split('\0');
|
|
210
|
+
const out = new Map();
|
|
211
|
+
for (let i = 0; i < fields.length; i++) {
|
|
212
|
+
const f = fields[i];
|
|
213
|
+
if (f.length < 4) continue;
|
|
214
|
+
const code = f.slice(0, 2);
|
|
215
|
+
out.set(f.slice(3), code);
|
|
216
|
+
if (code[0] === 'R' || code[0] === 'C') i++;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function gitHead(root) {
|
|
222
|
+
try {
|
|
223
|
+
return execFileSync('git', ['-C', root, 'rev-parse', '--verify', '-q', 'HEAD'], {
|
|
224
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000,
|
|
225
|
+
}).trim() || null;
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Files changed between two commits (the executor committed, stashed or
|
|
232
|
+
// checked out): without this, committed work would vanish from the diff.
|
|
233
|
+
function committedChanges(root, from, to) {
|
|
234
|
+
try {
|
|
235
|
+
const raw = execFileSync('git', ['-C', root, 'diff', '--name-status', '-z', '--no-renames', from, to], {
|
|
236
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000, maxBuffer: 64 * 1024 * 1024,
|
|
237
|
+
});
|
|
238
|
+
const f = raw.split('\0');
|
|
239
|
+
const out = [];
|
|
240
|
+
for (let i = 0; i + 1 < f.length; i += 2) {
|
|
241
|
+
if (!f[i]) break;
|
|
242
|
+
const change = f[i][0] === 'A' ? 'added' : f[i][0] === 'D' ? 'deleted' : 'modified';
|
|
243
|
+
out.push({ path: f[i + 1], change });
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
} catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function snapshot(root) {
|
|
252
|
+
if (gitToplevel(root)) {
|
|
253
|
+
try {
|
|
254
|
+
const entries = new Map();
|
|
255
|
+
for (const [rel, code] of gitStatus(root)) {
|
|
256
|
+
entries.set(rel, { code, sig: fileSignature(path.join(root, rel)) });
|
|
257
|
+
}
|
|
258
|
+
return { kind: 'git', root, head: gitHead(root), entries };
|
|
259
|
+
} catch {
|
|
260
|
+
/* fall back to the walk */
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const entries = new Map();
|
|
264
|
+
const stack = [''];
|
|
265
|
+
while (stack.length) {
|
|
266
|
+
const rel = stack.pop();
|
|
267
|
+
let dirents;
|
|
268
|
+
try {
|
|
269
|
+
dirents = fs.readdirSync(path.join(root, rel), { withFileTypes: true });
|
|
270
|
+
} catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
for (const d of dirents) {
|
|
274
|
+
if (WALK_SKIP.has(d.name)) continue;
|
|
275
|
+
const childRel = rel ? `${rel}/${d.name}` : d.name;
|
|
276
|
+
if (d.isDirectory()) stack.push(childRel);
|
|
277
|
+
else if (d.isFile()) {
|
|
278
|
+
if (entries.size >= WALK_LIMIT) return { kind: 'unavailable', entries: null };
|
|
279
|
+
try {
|
|
280
|
+
const st = fs.statSync(path.join(root, childRel));
|
|
281
|
+
entries.set(childRel, { code: 'fs', sig: `${st.size}:${st.mtimeMs}` });
|
|
282
|
+
} catch {
|
|
283
|
+
/* vanished mid-walk */
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { kind: 'fs', entries };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Files whose state differs between two snapshots. In git mode a path
|
|
292
|
+
// absent from `after` but present in `before` went back to clean — it was
|
|
293
|
+
// still changed by someone.
|
|
294
|
+
function diffSnapshots(before, after) {
|
|
295
|
+
if (!before || !after || !before.entries || !after.entries || before.kind !== after.kind) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const out = [];
|
|
299
|
+
const paths = new Set([...before.entries.keys(), ...after.entries.keys()]);
|
|
300
|
+
for (const rel of [...paths].sort()) {
|
|
301
|
+
const b = before.entries.get(rel);
|
|
302
|
+
const a = after.entries.get(rel);
|
|
303
|
+
if (b && a && b.sig === a.sig && b.code === a.code) continue;
|
|
304
|
+
let change = 'modified';
|
|
305
|
+
if (after.kind === 'fs') {
|
|
306
|
+
if (!b) change = 'added';
|
|
307
|
+
else if (!a) change = 'deleted';
|
|
308
|
+
} else if (a) {
|
|
309
|
+
if (a.code.includes('D') || a.sig === 'missing') change = 'deleted';
|
|
310
|
+
else if (!b && (a.code === '??' || a.code[0] === 'A')) change = 'added';
|
|
311
|
+
}
|
|
312
|
+
out.push({ path: rel, change });
|
|
313
|
+
}
|
|
314
|
+
if (after.kind === 'git' && before.head && after.head && before.head !== after.head) {
|
|
315
|
+
const seen = new Set(out.map((f) => f.path));
|
|
316
|
+
for (const f of committedChanges(after.root, before.head, after.head)) {
|
|
317
|
+
if (!seen.has(f.path)) out.push({ ...f, committed: true });
|
|
318
|
+
}
|
|
319
|
+
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
OwnershipError,
|
|
326
|
+
workspaceRoot,
|
|
327
|
+
normalizeOwns,
|
|
328
|
+
globToRegExp,
|
|
329
|
+
isOwned,
|
|
330
|
+
ownsOverlap,
|
|
331
|
+
acquireLease,
|
|
332
|
+
liveLeases,
|
|
333
|
+
leaseDir,
|
|
334
|
+
snapshot,
|
|
335
|
+
diffSnapshots,
|
|
336
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xgeorgemathew/zcode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Lean GLM-5.3 execution engine for orchestrator agents: task contracts, independent sessions, zcode-run verification, structured handbacks, subagents, MCP, skills. Wraps the ZCode engine. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": {
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"bin",
|
|
16
16
|
"lib",
|
|
17
|
+
"skills",
|
|
17
18
|
"README.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
|
-
"test": "node --test --test-concurrency=1 \"test
|
|
22
|
-
"
|
|
22
|
+
"test": "node --test --test-concurrency=1 \"test/unit/*.test.js\"",
|
|
23
|
+
"test:live": "node --test --test-concurrency=1 \"test/live/*.test.js\"",
|
|
24
|
+
"prepublishOnly": "npm test && npm run test:live",
|
|
23
25
|
"accept:flash": "node scripts/accept.js flash",
|
|
24
26
|
"accept:subagent": "node scripts/accept.js subagent",
|
|
25
27
|
"accept:53": "node scripts/accept.js glm53"
|
|
@@ -36,6 +38,8 @@
|
|
|
36
38
|
"mcp",
|
|
37
39
|
"subagent",
|
|
38
40
|
"headless",
|
|
39
|
-
"ndjson"
|
|
41
|
+
"ndjson",
|
|
42
|
+
"orchestrator",
|
|
43
|
+
"executor"
|
|
40
44
|
]
|
|
41
45
|
}
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Orchestrator skills
|
|
2
|
+
|
|
3
|
+
These skills teach an orchestrating agent how to delegate work to zcode, run sessions in parallel, and read the handbacks. Both describe the same CLI; they differ only in runtime notes (tool timeouts for Claude Code, sandbox and network escalation for Codex).
|
|
4
|
+
|
|
5
|
+
| platform | skill in this repo | format |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Claude Code | `skills/claude-code/zcode/SKILL.md` | `SKILL.md` with `name` + `description` frontmatter |
|
|
8
|
+
| Codex | `skills/codex/zcode/SKILL.md` + `agents/openai.yaml` | `SKILL.md` with `name` + `description` frontmatter; optional UI metadata in `agents/openai.yaml` |
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Run these from a checkout of this repo, or from the installed package (`$(npm root -g)/@0xgeorgemathew/zcode`), which ships `skills/`.
|
|
13
|
+
|
|
14
|
+
**Claude Code.** Personal skills, available in every project:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
mkdir -p ~/.claude/skills
|
|
18
|
+
cp -R skills/claude-code/zcode ~/.claude/skills/zcode
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For a single project, copy it to `<project>/.claude/skills/zcode` instead and commit it so the whole team gets it. Claude Code discovers the skill from its description. You can also invoke it directly with `/zcode`.
|
|
22
|
+
|
|
23
|
+
**Codex.** User skills live in `$CODEX_HOME/skills`, which defaults to `~/.codex/skills`:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
|
|
27
|
+
cp -R skills/codex/zcode "${CODEX_HOME:-$HOME/.codex}/skills/zcode"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Skills are discovered when a session starts, so open a new Codex session if `$zcode` doesn't appear. The skill is selected automatically from its description, or explicitly with `$zcode`.
|
|
31
|
+
|
|
32
|
+
Both skills assume `zcode` is on `PATH` (`npm install -g @0xgeorgemathew/zcode`) and that `zcode doctor` passes.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zcode
|
|
3
|
+
description: Delegate scoped implementation work to the zcode CLI (GLM-5.3 executor) and consume its verified handback. Use when you are orchestrating and want a page, component, fix or other bounded unit implemented and checked by an independent executor session — alone or several in parallel — while you keep product, design and planning decisions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Operating zcode as an executor
|
|
7
|
+
|
|
8
|
+
zcode is a CLI execution engine. You hand it a scoped task; it implements the task in its own session, runs your acceptance checks itself, repairs failures within a budget, and returns a structured handback. You stay the orchestrator. You decide what to build and how it should look, how to split the work, when to run sessions in parallel, and when to review. zcode decides routine implementation details within the scope you give it.
|
|
9
|
+
|
|
10
|
+
Check it is ready once per machine: `zcode doctor` (exit 3 means no key, so run `zcode login`; exit 4 means the engine is missing).
|
|
11
|
+
|
|
12
|
+
## When to use it
|
|
13
|
+
|
|
14
|
+
- Use it for bounded implementation work with a clear outcome: one page, a few components, a bug fix, a refactor of a known area, a test suite for a module.
|
|
15
|
+
- Don't use it for product or design decisions, open-ended exploration, or work you can't describe a finished state for. Decide those yourself first, then delegate the implementation.
|
|
16
|
+
- Size each session so you could review its result in one sitting. Several small sessions beat one huge one, because each returns its own evidence.
|
|
17
|
+
|
|
18
|
+
## Writing the handoff
|
|
19
|
+
|
|
20
|
+
Only the objective is required. Add what the executor cannot infer:
|
|
21
|
+
|
|
22
|
+
| field | flag | give it when |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| objective | positional text | always: the outcome, not the steps |
|
|
25
|
+
| acceptance | `--accept "…"` | you have criteria beyond "it works" |
|
|
26
|
+
| checks | `--check "cmd"` | always if possible: shell commands zcode runs itself (build, tests, lint, a grep) |
|
|
27
|
+
| owns | `--owns path/or/glob` | other sessions may touch the same repo |
|
|
28
|
+
| files | `--file path` | specific files to read first |
|
|
29
|
+
| context / design | `--context "…"` or `--context @notes.md` | design decisions, copy, references, constraints |
|
|
30
|
+
| everything | `--task contract.json` | larger handoffs (keys: objective, scope, constraints, design, acceptance, checks, files, references, context, owns, limits, model, effort) |
|
|
31
|
+
|
|
32
|
+
Rules that keep you in charge:
|
|
33
|
+
|
|
34
|
+
- Put design decisions in `design` (JSON) or `--context`, written as decisions ("Hero uses the dark palette; CTA right-aligned"). The executor is told to implement them exactly and to report `blocked` rather than invent product or design choices.
|
|
35
|
+
- Make checks mechanical and fast. A task with no `--check` can only come back `unverified`.
|
|
36
|
+
- Don't ask zcode to commit, push, or deploy unless you mean it. It won't by default.
|
|
37
|
+
|
|
38
|
+
## Running sessions
|
|
39
|
+
|
|
40
|
+
Your Bash tool times out (2 minutes by default, 10 at most), and real tasks usually take longer. Start them detached and wait in bounded slices:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
zcode task --detach --id <id> --cwd <repo> [flags] "<objective>" # prints the id, returns at once
|
|
44
|
+
zcode wait <id> --timeout-ms 540000 --json --quiet # exit 124 = still running: wait again
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You can also run a foreground `zcode task … --json --quiet` with `run_in_background: true` and read its single result line when you're notified. Quick tasks (well under 2 minutes) can run in the foreground directly.
|
|
48
|
+
|
|
49
|
+
- `zcode status` lists sessions. `zcode status <id> --json` shows the state (`queued|running|finished|interrupted`), phase (`starting|executing|checking|repairing|reporting`), activity, recent events, and the result once finished. Use it only when you need it. You don't have to watch progress, and nothing waits for your approval.
|
|
50
|
+
- `zcode cancel <id>` stops the turn and any running check; the session is recorded as `cancelled`.
|
|
51
|
+
- `zcode resume <id> "<message>"` continues the same session with its context intact. Use it to answer questions, request follow-up changes, or continue after `interrupted`, `failed`, or `partial`. It accepts `--detach`, `--model`, `--effort`, `--timeout-ms`, and `--max-repairs`. The scope (`--cwd`, `--owns`, checks) stays as it was.
|
|
52
|
+
- Pick your own `--id` values (`page-1`, `fix-auth`) so you can retry safely. A start with an existing id is refused (exit 2) instead of running the work twice.
|
|
53
|
+
|
|
54
|
+
## Parallel sessions
|
|
55
|
+
|
|
56
|
+
- Each session has its own engine context, so nothing leaks between them.
|
|
57
|
+
- Sessions in the same repo must declare disjoint `--owns`. An overlapping start is refused with exit 7, and the error names the conflicting session. Fix the split, wait, or give the session its own `git worktree` and point `--cwd` at it.
|
|
58
|
+
- After the work, each handback lists changed files. Anything outside the session's ownership appears in `scopeViolations` and makes the result `partial`.
|
|
59
|
+
- You integrate the results. zcode doesn't coordinate sessions with each other.
|
|
60
|
+
|
|
61
|
+
## Reading the handback
|
|
62
|
+
|
|
63
|
+
The result (`--json`: one `{"type":"result"}` line) has `status`, `summary`, `changes.files`, `checks` (zcode's own runs, with output tails), `issues`, `questions`, `unresolved`, `notVerified`, `scopeViolations`, `subagents`, and `handback` (the executor's own claims).
|
|
64
|
+
|
|
65
|
+
| status | exit | what it means | what you do |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `completed` | 0 | zcode's checks passed, executor reports done, scope respected | review on your own schedule; integrate |
|
|
68
|
+
| `unverified` | 9 | executor says done, but there were no checks (or no usable handback) | verify yourself, or resume with a check added to the message |
|
|
69
|
+
| `partial` | 8 | work remains, a criterion is unmet, or files changed outside scope | read `issues`/`unresolved`/`scopeViolations`; resume or re-scope |
|
|
70
|
+
| `blocked` | 10 | needs you: see `questions` | decide, then `zcode resume <id> "<answer>"` |
|
|
71
|
+
| `failed` | 8 / 5 / 124 | `error.kind`: `checks_failed` (still failing after repairs), turn/engine error, or `timeout` | read `checks[].output` / `error`; resume with guidance or re-scope |
|
|
72
|
+
| `cancelled` | 130 | stopped | resume if wanted |
|
|
73
|
+
|
|
74
|
+
Trust `status`, `checks`, and `changes`. They come from what zcode observed. Treat `handback` and `summary` as the executor's report. A subagent's `success` only means it returned. zcode already verified the integrated result through your checks, so you don't need to review per subagent or per step.
|
|
75
|
+
|
|
76
|
+
## Example: small task
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
zcode task --id fix-typo --cwd ~/site --owns README.md \
|
|
80
|
+
--check "grep -q '^# Acme Docs' README.md" \
|
|
81
|
+
"Fix the misspelled top-level heading in README.md (should read 'Acme Docs')" --json --quiet
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Exit 0 means it's done. The result line shows the single changed file and the passing check.
|
|
85
|
+
|
|
86
|
+
## Example: Page 1 and Page 2 in parallel
|
|
87
|
+
|
|
88
|
+
You've already decided the design. Each page gets its own session, its own scope, and its own checks:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
zcode task --detach --id page-1 --cwd ~/site --label "Home page" \
|
|
92
|
+
--owns src/pages/home/ --file src/theme.ts \
|
|
93
|
+
--context @specs/home.md \
|
|
94
|
+
--accept "Hero, three feature cards, footer CTA as specified" \
|
|
95
|
+
--check "npm run build" --check "npm test -- home" \
|
|
96
|
+
"Implement the Home page exactly as specified in the context"
|
|
97
|
+
|
|
98
|
+
zcode task --detach --id page-2 --cwd ~/site --label "Pricing page" \
|
|
99
|
+
--owns src/pages/pricing/ --file src/theme.ts \
|
|
100
|
+
--context @specs/pricing.md \
|
|
101
|
+
--accept "Three plan cards, middle one highlighted; monthly/yearly toggle" \
|
|
102
|
+
--check "npm run build" --check "npm test -- pricing" \
|
|
103
|
+
"Implement the Pricing page exactly as specified in the context"
|
|
104
|
+
|
|
105
|
+
zcode wait page-1 page-2 --timeout-ms 540000 --json --quiet # repeat while exit is 124
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If `page-2` comes back `blocked` with "Should yearly prices show the discount badge?", decide and run `zcode resume page-2 "Yes, show the 'Save 20%' badge on yearly." --detach`, then wait again. To run the pages one after another instead, start `page-2` after `page-1`'s handback. The shared `src/theme.ts` is passed with `--file` (read), not owned, so a change to it by either session would come back as a scope violation. If a page truly needs a shared change, make it yourself or give that file to exactly one session's `--owns`.
|
|
109
|
+
|
|
110
|
+
## Exit codes
|
|
111
|
+
|
|
112
|
+
0 completed · 2 usage (bad flag, contract, or duplicate id) · 3 no key · 4 no engine · 5 failed · 6 `agent` had no subagent · 7 workspace conflict / session busy · 8 partial or checks failing · 9 unverified · 10 blocked · 124 timeout (for `wait`: still running) · 130 cancelled.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zcode
|
|
3
|
+
description: Delegate scoped implementation work to the zcode CLI (GLM-5.3 executor) and consume its verified handback. Use when you are orchestrating and want a page, component, fix or other bounded unit implemented and checked by an independent executor session — alone or several in parallel — while you keep product, design and planning decisions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Operating zcode as an executor
|
|
7
|
+
|
|
8
|
+
zcode is a CLI execution engine. You hand it a scoped task; it implements the task in its own session, runs your acceptance checks itself, repairs failures within a budget, and returns a structured handback. You stay the orchestrator. You decide what to build and how it should look, how to split the work, when to run sessions in parallel, and when to review. zcode decides routine implementation details within the scope you give it.
|
|
9
|
+
|
|
10
|
+
Check it is ready once per machine: `zcode doctor` (exit 3 means no key, so run `zcode login`; exit 4 means the engine is missing).
|
|
11
|
+
|
|
12
|
+
## When to use it
|
|
13
|
+
|
|
14
|
+
- Use it for bounded implementation work with a clear outcome: one page, a few components, a bug fix, a refactor of a known area, a test suite for a module.
|
|
15
|
+
- Don't use it for product or design decisions, open-ended exploration, or work you can't describe a finished state for. Decide those yourself first, then delegate the implementation.
|
|
16
|
+
- Size each session so you could review its result in one sitting. Several small sessions beat one huge one, because each returns its own evidence.
|
|
17
|
+
|
|
18
|
+
## Writing the handoff
|
|
19
|
+
|
|
20
|
+
Only the objective is required. Add what the executor cannot infer:
|
|
21
|
+
|
|
22
|
+
| field | flag | give it when |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| objective | positional text | always: the outcome, not the steps |
|
|
25
|
+
| acceptance | `--accept "…"` | you have criteria beyond "it works" |
|
|
26
|
+
| checks | `--check "cmd"` | always if possible: shell commands zcode runs itself (build, tests, lint, a grep) |
|
|
27
|
+
| owns | `--owns path/or/glob` | other sessions may touch the same repo |
|
|
28
|
+
| files | `--file path` | specific files to read first |
|
|
29
|
+
| context / design | `--context "…"` or `--context @notes.md` | design decisions, copy, references, constraints |
|
|
30
|
+
| everything | `--task contract.json` | larger handoffs (keys: objective, scope, constraints, design, acceptance, checks, files, references, context, owns, limits, model, effort) |
|
|
31
|
+
|
|
32
|
+
Rules that keep you in charge:
|
|
33
|
+
|
|
34
|
+
- Put design decisions in `design` (JSON) or `--context`, written as decisions ("Hero uses the dark palette; CTA right-aligned"). The executor is told to implement them exactly and to report `blocked` rather than invent product or design choices.
|
|
35
|
+
- Make checks mechanical and fast. A task with no `--check` can only come back `unverified`.
|
|
36
|
+
- Don't ask zcode to commit, push, or deploy unless you mean it. It won't by default.
|
|
37
|
+
|
|
38
|
+
## Running sessions
|
|
39
|
+
|
|
40
|
+
zcode needs network access (the GLM API) and write access to `~/.zcode-cli` and the target repo. In a sandboxed Codex session, those commands may fail with sandbox or network errors. If they do, re-run the same command with escalated permissions instead of changing the command.
|
|
41
|
+
|
|
42
|
+
Real tasks usually outlast a single shell call's timeout. Start them detached and wait in bounded slices:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
zcode task --detach --id <id> --cwd <repo> [flags] "<objective>" # prints the id, returns at once
|
|
46
|
+
zcode wait <id> --timeout-ms 540000 --json --quiet # exit 124 = still running: wait again
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Quick tasks can run in the foreground: `zcode task … --json --quiet` prints exactly one result line.
|
|
50
|
+
|
|
51
|
+
- `zcode status` lists sessions. `zcode status <id> --json` shows the state (`queued|running|finished|interrupted`), phase (`starting|executing|checking|repairing|reporting`), activity, recent events, and the result once finished. Use it only when you need it. You don't have to watch progress, and nothing waits for your approval.
|
|
52
|
+
- `zcode cancel <id>` stops the turn and any running check; the session is recorded as `cancelled`.
|
|
53
|
+
- `zcode resume <id> "<message>"` continues the same session with its context intact. Use it to answer questions, request follow-up changes, or continue after `interrupted`, `failed`, or `partial`. It accepts `--detach`, `--model`, `--effort`, `--timeout-ms`, and `--max-repairs`. The scope (`--cwd`, `--owns`, checks) stays as it was.
|
|
54
|
+
- Pick your own `--id` values (`page-1`, `fix-auth`) so you can retry safely. A start with an existing id is refused (exit 2) instead of running the work twice.
|
|
55
|
+
|
|
56
|
+
## Parallel sessions
|
|
57
|
+
|
|
58
|
+
- Each session has its own engine context, so nothing leaks between them.
|
|
59
|
+
- Sessions in the same repo must declare disjoint `--owns`. An overlapping start is refused with exit 7, and the error names the conflicting session. Fix the split, wait, or give the session its own `git worktree` and point `--cwd` at it.
|
|
60
|
+
- After the work, each handback lists changed files. Anything outside the session's ownership appears in `scopeViolations` and makes the result `partial`.
|
|
61
|
+
- You integrate the results. zcode doesn't coordinate sessions with each other.
|
|
62
|
+
|
|
63
|
+
## Reading the handback
|
|
64
|
+
|
|
65
|
+
The result (`--json`: one `{"type":"result"}` line) has `status`, `summary`, `changes.files`, `checks` (zcode's own runs, with output tails), `issues`, `questions`, `unresolved`, `notVerified`, `scopeViolations`, `subagents`, and `handback` (the executor's own claims).
|
|
66
|
+
|
|
67
|
+
| status | exit | what it means | what you do |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| `completed` | 0 | zcode's checks passed, executor reports done, scope respected | review on your own schedule; integrate |
|
|
70
|
+
| `unverified` | 9 | executor says done, but there were no checks (or no usable handback) | verify yourself, or resume with a check added to the message |
|
|
71
|
+
| `partial` | 8 | work remains, a criterion is unmet, or files changed outside scope | read `issues`/`unresolved`/`scopeViolations`; resume or re-scope |
|
|
72
|
+
| `blocked` | 10 | needs you: see `questions` | decide, then `zcode resume <id> "<answer>"` |
|
|
73
|
+
| `failed` | 8 / 5 / 124 | `error.kind`: `checks_failed` (still failing after repairs), turn/engine error, or `timeout` | read `checks[].output` / `error`; resume with guidance or re-scope |
|
|
74
|
+
| `cancelled` | 130 | stopped | resume if wanted |
|
|
75
|
+
|
|
76
|
+
Trust `status`, `checks`, and `changes`. They come from what zcode observed. Treat `handback` and `summary` as the executor's report. A subagent's `success` only means it returned. zcode already verified the integrated result through your checks, so you don't need to review per subagent or per step.
|
|
77
|
+
|
|
78
|
+
## Example: small task
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
zcode task --id fix-typo --cwd ~/site --owns README.md \
|
|
82
|
+
--check "grep -q '^# Acme Docs' README.md" \
|
|
83
|
+
"Fix the misspelled top-level heading in README.md (should read 'Acme Docs')" --json --quiet
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Exit 0 means it's done. The result line shows the single changed file and the passing check.
|
|
87
|
+
|
|
88
|
+
## Example: Page 1 and Page 2 in parallel
|
|
89
|
+
|
|
90
|
+
You've already decided the design. Each page gets its own session, its own scope, and its own checks:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
zcode task --detach --id page-1 --cwd ~/site --label "Home page" \
|
|
94
|
+
--owns src/pages/home/ --file src/theme.ts \
|
|
95
|
+
--context @specs/home.md \
|
|
96
|
+
--accept "Hero, three feature cards, footer CTA as specified" \
|
|
97
|
+
--check "npm run build" --check "npm test -- home" \
|
|
98
|
+
"Implement the Home page exactly as specified in the context"
|
|
99
|
+
|
|
100
|
+
zcode task --detach --id page-2 --cwd ~/site --label "Pricing page" \
|
|
101
|
+
--owns src/pages/pricing/ --file src/theme.ts \
|
|
102
|
+
--context @specs/pricing.md \
|
|
103
|
+
--accept "Three plan cards, middle one highlighted; monthly/yearly toggle" \
|
|
104
|
+
--check "npm run build" --check "npm test -- pricing" \
|
|
105
|
+
"Implement the Pricing page exactly as specified in the context"
|
|
106
|
+
|
|
107
|
+
zcode wait page-1 page-2 --timeout-ms 540000 --json --quiet # repeat while exit is 124
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If `page-2` comes back `blocked` with "Should yearly prices show the discount badge?", decide and run `zcode resume page-2 "Yes, show the 'Save 20%' badge on yearly." --detach`, then wait again. To run the pages one after another instead, start `page-2` after `page-1`'s handback. The shared `src/theme.ts` is passed with `--file` (read), not owned, so a change to it by either session would come back as a scope violation. If a page truly needs a shared change, make it yourself or give that file to exactly one session's `--owns`.
|
|
111
|
+
|
|
112
|
+
## Exit codes
|
|
113
|
+
|
|
114
|
+
0 completed · 2 usage (bad flag, contract, or duplicate id) · 3 no key · 4 no engine · 5 failed · 6 `agent` had no subagent · 7 workspace conflict / session busy · 8 partial or checks failing · 9 unverified · 10 blocked · 124 timeout (for `wait`: still running) · 130 cancelled.
|