@0xgeorgemathew/zcode 0.5.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.
- package/README.md +10 -11
- package/bin/zcode.js +26 -10
- package/lib/appserver.js +5 -0
- package/lib/brief.js +47 -7
- package/lib/commands/doctor.js +3 -2
- package/lib/commands/exec.js +1 -1
- package/lib/commands/plan.js +3 -2
- package/lib/commands/sessions.js +95 -8
- package/lib/contract.js +63 -38
- package/lib/executor.js +151 -28
- package/lib/help.js +111 -0
- package/lib/isolation.js +20 -1
- package/lib/models.js +42 -0
- package/lib/plan.js +44 -9
- package/lib/store.js +14 -0
- package/lib/taskspec.js +19 -12
- package/lib/ui.js +13 -3
- package/lib/verify.js +98 -0
- package/lib/workspace.js +30 -0
- package/package.json +1 -1
- package/skills/claude-code/zcode/SKILL.md +17 -13
- package/skills/codex/zcode/SKILL.md +17 -13
package/lib/isolation.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const os = require('node:os');
|
|
22
|
+
const { DEFAULT_MODEL } = require('./models.js');
|
|
22
23
|
|
|
23
24
|
// The desktop app's live directory. ZAI_CODE_LIVE_DIR points elsewhere
|
|
24
25
|
// (e.g. a second desktop profile, or a fixture in tests).
|
|
@@ -215,6 +216,19 @@ function isDanglingLink(p) {
|
|
|
215
216
|
// Playwright into a dead end.
|
|
216
217
|
const HOST_ONLY_SKILLS = new Set(['control-browser']);
|
|
217
218
|
|
|
219
|
+
// Rewrites the frontmatter `model:` of an agent profile when it names
|
|
220
|
+
// GLM-5.3 / GLM-5.3-Flash (any prefix: "account:x/GLM-5.3",
|
|
221
|
+
// "custom:account%3Ax:GLM-5.3") so its model part is `model`.
|
|
222
|
+
function pinAgentModel(text, model) {
|
|
223
|
+
const m = /^(---\r?\n)([\s\S]*?)(\r?\n---)/.exec(text);
|
|
224
|
+
if (!m) return text;
|
|
225
|
+
const front = m[2].replace(/^(\s*model\s*:\s*["']?)([^\n"']*?)(?:\/|:)GLM-5\.3(?:-Flash)?(["']?\s*)$/im, (all, a, prefix, tail) => {
|
|
226
|
+
const sep = all.slice(a.length + prefix.length, a.length + prefix.length + 1);
|
|
227
|
+
return `${a}${prefix}${sep}${model}${tail}`;
|
|
228
|
+
});
|
|
229
|
+
return m[1] + front + text.slice(m[0].length - m[3].length);
|
|
230
|
+
}
|
|
231
|
+
|
|
218
232
|
function buildEngineHome({
|
|
219
233
|
home,
|
|
220
234
|
root,
|
|
@@ -225,6 +239,7 @@ function buildEngineHome({
|
|
|
225
239
|
browser = true,
|
|
226
240
|
skillsDirs = [],
|
|
227
241
|
noSkills = false,
|
|
242
|
+
model = DEFAULT_MODEL,
|
|
228
243
|
}) {
|
|
229
244
|
const zdir = path.join(home, '.zcode');
|
|
230
245
|
const v2 = path.join(zdir, 'v2');
|
|
@@ -263,7 +278,10 @@ function buildEngineHome({
|
|
|
263
278
|
for (const entry of listDir(path.join(live, 'agents'))) {
|
|
264
279
|
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
265
280
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
266
|
-
|
|
281
|
+
// Profiles pinning a GLM-5.3 id would silently run another model than
|
|
282
|
+
// the session's: the copy follows the session (the live file is never edited).
|
|
283
|
+
const text = fs.readFileSync(path.join(live, 'agents', entry.name), 'utf8');
|
|
284
|
+
fs.writeFileSync(path.join(agentsDir, entry.name), pinAgentModel(text, model));
|
|
267
285
|
}
|
|
268
286
|
const agentsMd = path.join(zdir, 'AGENTS.md');
|
|
269
287
|
if (fs.existsSync(path.join(live, 'AGENTS.md'))) {
|
|
@@ -393,6 +411,7 @@ function engineEnv({ home, builtinProviderConfig, parentEnv = process.env, sessi
|
|
|
393
411
|
|
|
394
412
|
module.exports = {
|
|
395
413
|
buildEngineHome,
|
|
414
|
+
pinAgentModel,
|
|
396
415
|
engineEnv,
|
|
397
416
|
rootDir,
|
|
398
417
|
liveDir,
|
package/lib/models.js
CHANGED
|
@@ -115,6 +115,45 @@ function modelLevels(opts) {
|
|
|
115
115
|
return Object.fromEntries(OFFICIAL_IDS.map((id) => [id, supportedEfforts(id, opts)]));
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// --- image input per model ---------------------------------------------------
|
|
119
|
+
//
|
|
120
|
+
// Same resolution as reasoning levels: modelRules[].config.properties
|
|
121
|
+
// .inputFormat.supportsImage, last matching rule that sets it wins (the
|
|
122
|
+
// generic `glm-5.3(-flash)?` rule says false, the later `glm-5.3-flash` rule
|
|
123
|
+
// true). GLM-5.3 cannot see images: reading a PNG returns only an upload
|
|
124
|
+
// confirmation, so it must never be the judge of a screenshot.
|
|
125
|
+
const FALLBACK_IMAGES = { 'GLM-5.3': false, 'GLM-5.3-Flash': true };
|
|
126
|
+
|
|
127
|
+
function supportsImage(model, { env = process.env, builtinConfig } = {}) {
|
|
128
|
+
const file = builtinConfig || builtinConfigPath(env);
|
|
129
|
+
let value = null;
|
|
130
|
+
if (file) {
|
|
131
|
+
for (const rule of readRules(file)) {
|
|
132
|
+
let match = false;
|
|
133
|
+
try {
|
|
134
|
+
match = new RegExp(`^(?:${rule.modelMatch})$`, 'i').test(model);
|
|
135
|
+
} catch {
|
|
136
|
+
/* a malformed pattern matches nothing */
|
|
137
|
+
}
|
|
138
|
+
if (!match) continue;
|
|
139
|
+
const v = rule.config && rule.config.properties && rule.config.properties.inputFormat
|
|
140
|
+
&& rule.config.properties.inputFormat.supportsImage;
|
|
141
|
+
if (typeof v === 'boolean') value = v;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return value === null ? FALLBACK_IMAGES[model] === true : value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// {modelId: bool} for every supported model (doctor).
|
|
148
|
+
function modelVision(opts) {
|
|
149
|
+
return Object.fromEntries(OFFICIAL_IDS.map((id) => [id, supportsImage(id, opts)]));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The first image-capable model: judges screenshots for the visual verifier.
|
|
153
|
+
function visionModel(opts) {
|
|
154
|
+
return OFFICIAL_IDS.find((id) => supportsImage(id, opts)) || DEFAULT_MODEL;
|
|
155
|
+
}
|
|
156
|
+
|
|
118
157
|
// null when `effort` is usable with `model`, else the one-line refusal.
|
|
119
158
|
// `source` names where the value came from ("--effort", "contract effort").
|
|
120
159
|
function effortProblem(model, effort, { source = '--effort', ...opts } = {}) {
|
|
@@ -124,6 +163,9 @@ function effortProblem(model, effort, { source = '--effort', ...opts } = {}) {
|
|
|
124
163
|
}
|
|
125
164
|
|
|
126
165
|
module.exports = {
|
|
166
|
+
supportsImage,
|
|
167
|
+
modelVision,
|
|
168
|
+
visionModel,
|
|
127
169
|
supportedEfforts,
|
|
128
170
|
modelLevels,
|
|
129
171
|
effortProblem,
|
package/lib/plan.js
CHANGED
|
@@ -22,7 +22,7 @@ const { normalizeModel, normalizeEffort, effortProblem, unknownModelMessage, DEF
|
|
|
22
22
|
const { buildBrief } = require('./brief.js');
|
|
23
23
|
|
|
24
24
|
const BIN = path.join(__dirname, '..', 'bin', 'zcode.js');
|
|
25
|
-
const PLAN_KEYS = ['cwd', 'decisions', 'context', 'files', 'model', 'effort', 'sessions'];
|
|
25
|
+
const PLAN_KEYS = ['cwd', 'decisions', 'context', 'files', 'model', 'effort', 'limits', 'sessions'];
|
|
26
26
|
const POLL_MS = 500;
|
|
27
27
|
|
|
28
28
|
class PlanError extends Error {}
|
|
@@ -65,10 +65,10 @@ const sessionList = (plan) => (Array.isArray(plan.sessions) ? plan.sessions.filt
|
|
|
65
65
|
const afterOf = (s) => toArray(s.after).map(String);
|
|
66
66
|
|
|
67
67
|
// The session's contract object: top-level decisions/context/files first,
|
|
68
|
-
// the plan's model/effort as defaults
|
|
69
|
-
// contract keys.
|
|
68
|
+
// the plan's model/effort/limits as defaults (a session's own limits win per
|
|
69
|
+
// key). `id`, `after` and `label` are plan keys, not contract keys.
|
|
70
70
|
function sessionBase(r, s) {
|
|
71
|
-
const { id, after, ...rest } = s;
|
|
71
|
+
const { id, after, label, ...rest } = s;
|
|
72
72
|
const top = r.plan;
|
|
73
73
|
const base = { ...rest };
|
|
74
74
|
for (const key of ['decisions', 'context', 'files']) {
|
|
@@ -78,6 +78,9 @@ function sessionBase(r, s) {
|
|
|
78
78
|
for (const key of ['model', 'effort']) {
|
|
79
79
|
if (base[key] === undefined && top[key] !== undefined) base[key] = top[key];
|
|
80
80
|
}
|
|
81
|
+
if (top.limits && typeof top.limits === 'object' && !Array.isArray(top.limits)) {
|
|
82
|
+
base.limits = { ...top.limits, ...(rest.limits && typeof rest.limits === 'object' ? rest.limits : {}) };
|
|
83
|
+
}
|
|
81
84
|
return base;
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -88,6 +91,9 @@ function checkPlan(r, { name, state } = {}) {
|
|
|
88
91
|
const { plan } = r;
|
|
89
92
|
const unknown = Object.keys(plan).filter((k) => !PLAN_KEYS.includes(k));
|
|
90
93
|
if (unknown.length) problems.push(`unknown plan key(s): ${unknown.join(', ')}. Accepted: ${PLAN_KEYS.join(', ')}`);
|
|
94
|
+
if (plan.limits !== undefined && (!plan.limits || typeof plan.limits !== 'object' || Array.isArray(plan.limits))) {
|
|
95
|
+
problems.push('plan "limits" must be an object (timeoutMs, maxRepairs, checkTimeoutMs).');
|
|
96
|
+
}
|
|
91
97
|
if (!Array.isArray(plan.sessions) || plan.sessions.length === 0) {
|
|
92
98
|
problems.push('plan needs a non-empty "sessions" array.');
|
|
93
99
|
return problems;
|
|
@@ -115,6 +121,7 @@ function checkPlan(r, { name, state } = {}) {
|
|
|
115
121
|
problems.push(`duplicate session id "${s.id}".`);
|
|
116
122
|
}
|
|
117
123
|
if (typeof s.id === 'string') ids.push(s.id);
|
|
124
|
+
if (s.label !== undefined && typeof s.label !== 'string') problems.push(`${label}: "label" must be a string.`);
|
|
118
125
|
if (s.after !== undefined && !toArray(s.after).every((a) => typeof a === 'string')) {
|
|
119
126
|
problems.push(`${label}: "after" must be a list of session ids.`);
|
|
120
127
|
}
|
|
@@ -212,11 +219,16 @@ function checkPlan(r, { name, state } = {}) {
|
|
|
212
219
|
|
|
213
220
|
// Ids already taken by sessions that are not this plan's.
|
|
214
221
|
const mine = new Set(state && Array.isArray(state.sessions) ? state.sessions : []);
|
|
222
|
+
let taken = false;
|
|
215
223
|
for (const id of known) {
|
|
216
224
|
if (store.readSession(id) && !mine.has(id)) {
|
|
217
|
-
|
|
225
|
+
taken = true;
|
|
226
|
+
problems.push(`session "${id}" already exists (zcode status ${id}).`);
|
|
218
227
|
}
|
|
219
228
|
}
|
|
229
|
+
if (taken) {
|
|
230
|
+
problems.push(`hint: use a fresh id prefix for a new run (e.g. "<prefix>-<id>"), or "zcode plan resume ${name || '<name>'}" to continue that plan's sessions.`);
|
|
231
|
+
}
|
|
220
232
|
return problems;
|
|
221
233
|
}
|
|
222
234
|
|
|
@@ -289,7 +301,10 @@ function planReport(name, r, state, { briefs = true } = {}) {
|
|
|
289
301
|
type: 'plan',
|
|
290
302
|
name,
|
|
291
303
|
status,
|
|
292
|
-
sessions: sessionList(r.plan).map((s) =>
|
|
304
|
+
sessions: sessionList(r.plan).map((s) => {
|
|
305
|
+
const e = statuses.get(s.id);
|
|
306
|
+
return { id: s.id, status: e.status, ...(e.view ? store.idleInfo(e.view) : null) };
|
|
307
|
+
}),
|
|
293
308
|
};
|
|
294
309
|
if (briefs) {
|
|
295
310
|
payload.briefs = [];
|
|
@@ -309,13 +324,32 @@ function exec() {
|
|
|
309
324
|
return require('./commands/exec.js');
|
|
310
325
|
}
|
|
311
326
|
|
|
327
|
+
// Preflights run all at once on the current workspace, so a session with
|
|
328
|
+
// `after` would see none of its prerequisites' files. This paragraph tells it
|
|
329
|
+
// to assume them (ids + clipped objectives) instead of asking about them.
|
|
330
|
+
function preflightContext(r, s) {
|
|
331
|
+
const upstream = afterOf(s).map((a) => sessionList(r.plan).find((x) => x.id === a)).filter(Boolean);
|
|
332
|
+
if (!upstream.length) return null;
|
|
333
|
+
const clip = (t) => {
|
|
334
|
+
const x = String(t || '').replace(/\s+/g, ' ').trim();
|
|
335
|
+
return x.length <= 200 ? x : `${x.slice(0, 199)}…`;
|
|
336
|
+
};
|
|
337
|
+
const list = upstream.map((u) => `- ${u.id}: ${clip(sessionBase(r, u).objective) || '(no objective)'}`).join('\n');
|
|
338
|
+
return `This is a plan preflight. Sessions ${upstream.map((u) => u.id).join(', ')} run before you and have not run yet; their files will exist when you execute. Assume they deliver what the binding decisions and their objectives say:\n${list}\nDo not ask about their absence; ask only about what the decisions leave open for YOUR scope.`;
|
|
339
|
+
}
|
|
340
|
+
|
|
312
341
|
// The detached-start invocation for one plan session (id overrides the plan
|
|
313
|
-
// id: preflight uses "<id>.pre").
|
|
342
|
+
// id: preflight uses "<id>.pre"). The label is `plan:<name>` plus the
|
|
343
|
+
// session's own `label`, if any.
|
|
314
344
|
function sessionInvocation(r, name, s, { id = s.id, preflight = false } = {}) {
|
|
315
345
|
const { parseArgv } = require('../bin/zcode.js');
|
|
316
|
-
const
|
|
346
|
+
const label = `plan:${name}${typeof s.label === 'string' && s.label.trim() ? ` ${s.label.trim()}` : ''}`;
|
|
347
|
+
const argv = ['task', '--id', id, '--cwd', r.cwd, '--label', label];
|
|
317
348
|
if (preflight) argv.push('--preflight');
|
|
318
|
-
|
|
349
|
+
const base = sessionBase(r, s);
|
|
350
|
+
const note = preflight ? preflightContext(r, s) : null;
|
|
351
|
+
if (note) base.context = [...toArray(base.context), note];
|
|
352
|
+
return exec().buildInvocation(parseArgv(argv), { base, dir: r.dir });
|
|
319
353
|
}
|
|
320
354
|
|
|
321
355
|
function launchSession(r, name, s) {
|
|
@@ -415,6 +449,7 @@ module.exports = {
|
|
|
415
449
|
planReport,
|
|
416
450
|
launchFailure,
|
|
417
451
|
sessionInvocation,
|
|
452
|
+
preflightContext,
|
|
418
453
|
launchSession,
|
|
419
454
|
resumeSession,
|
|
420
455
|
supervise,
|
package/lib/store.js
CHANGED
|
@@ -175,6 +175,18 @@ function viewSession(record) {
|
|
|
175
175
|
return { ...record, live: ACTIVE_STATES.has(record.state) };
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// How long a running session has gone without any engine event
|
|
179
|
+
// ({idleMs, stalled?}); null unless it is running. Informational only: zcode
|
|
180
|
+
// never acts on it. The same rule everywhere (status, wait, watch).
|
|
181
|
+
const STALL_MIN = 8;
|
|
182
|
+
function idleInfo(view, { stallMin = STALL_MIN, now = Date.now() } = {}) {
|
|
183
|
+
if (!view || view.state !== 'running') return null;
|
|
184
|
+
const t = Date.parse((view.activity && view.activity.lastEventAt) || view.updatedAt);
|
|
185
|
+
if (!Number.isFinite(t)) return null;
|
|
186
|
+
const idleMs = Math.max(0, now - t);
|
|
187
|
+
return idleMs >= stallMin * 60000 ? { idleMs, stalled: true } : { idleMs };
|
|
188
|
+
}
|
|
189
|
+
|
|
178
190
|
function listSessions(env = process.env) {
|
|
179
191
|
let names = [];
|
|
180
192
|
try {
|
|
@@ -312,6 +324,8 @@ module.exports = {
|
|
|
312
324
|
readEvents,
|
|
313
325
|
pidAlive,
|
|
314
326
|
viewSession,
|
|
327
|
+
idleInfo,
|
|
328
|
+
STALL_MIN,
|
|
315
329
|
listSessions,
|
|
316
330
|
resolveSessionId,
|
|
317
331
|
claimSession,
|
package/lib/taskspec.js
CHANGED
|
@@ -61,38 +61,45 @@ function validateUnits(contract, cwd) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// base: parsed contract object; baseDir: where its "@file" entries resolve
|
|
64
|
-
// (the contract file's directory, else cwd). additions: the CLI flags
|
|
64
|
+
// (the contract file's directory, else cwd), then `cwd` when not found there. additions: the CLI flags
|
|
65
65
|
// (strings; "@file" resolves against the process cwd). cwd: --cwd.
|
|
66
66
|
function buildContract({ base = {}, baseDir = process.cwd(), objective = '', additions = {}, cwd = process.cwd() }) {
|
|
67
67
|
const inlined = new Set();
|
|
68
|
-
|
|
68
|
+
// dirs are tried in order; the error names every path tried.
|
|
69
|
+
const inline = (list, dirs, label) =>
|
|
69
70
|
toArray(list).map((item) => {
|
|
70
71
|
const text = String(item).trim();
|
|
71
72
|
const m = FILE_REF.exec(text);
|
|
72
73
|
if (!m) return text;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
const tried = [];
|
|
75
|
+
for (const dir of dirs) {
|
|
76
|
+
const file = path.resolve(dir, m[1]);
|
|
77
|
+
if (tried.includes(file)) continue;
|
|
78
|
+
tried.push(file);
|
|
79
|
+
try {
|
|
80
|
+
const body = fs.readFileSync(file, 'utf8');
|
|
81
|
+
inlined.add(canon(file));
|
|
82
|
+
return body;
|
|
83
|
+
} catch {
|
|
84
|
+
/* try the next directory */
|
|
85
|
+
}
|
|
80
86
|
}
|
|
87
|
+
throw new ContractError(`${label} file "${m[1]}" could not be read (tried ${tried.join(', ')}).`);
|
|
81
88
|
});
|
|
82
89
|
|
|
83
90
|
let b = base;
|
|
84
91
|
if (b && typeof b === 'object' && !Array.isArray(b)) {
|
|
85
92
|
b = { ...b };
|
|
86
93
|
for (const key of ['context', 'decisions']) {
|
|
87
|
-
if (b[key] !== undefined) b[key] = inline(b[key], baseDir, `contract ${key}`);
|
|
94
|
+
if (b[key] !== undefined) b[key] = inline(b[key], [baseDir, cwd], `contract ${key}`);
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
97
|
const flagDir = process.cwd();
|
|
91
98
|
const contract = normalizeContract(b, {
|
|
92
99
|
...additions,
|
|
93
100
|
objective,
|
|
94
|
-
context: inline(additions.context, flagDir, '--context'),
|
|
95
|
-
decisions: inline(additions.decisions, flagDir, '--decisions'),
|
|
101
|
+
context: inline(additions.context, [flagDir], '--context'),
|
|
102
|
+
decisions: inline(additions.decisions, [flagDir], '--decisions'),
|
|
96
103
|
});
|
|
97
104
|
// A file the executor already gets inlined is not also listed to read.
|
|
98
105
|
if (inlined.size) {
|
package/lib/ui.js
CHANGED
|
@@ -84,6 +84,7 @@ function makeReporter({ quiet = false, thinking = true, stream = process.stderr,
|
|
|
84
84
|
const extra =
|
|
85
85
|
ev.phase === 'repairing' ? ` ${ev.attempt}/${ev.of}`
|
|
86
86
|
: ev.phase === 'checking' ? ` ${ev.count}`
|
|
87
|
+
: ev.phase === 'verifying' ? ` round ${ev.round}`
|
|
87
88
|
: ev.reason ? ` (${ev.reason})` : '';
|
|
88
89
|
line(`${c.cyan('●')} ${ev.phase}${extra}`);
|
|
89
90
|
return;
|
|
@@ -186,7 +187,7 @@ function formatResult(r, { color = false } = {}) {
|
|
|
186
187
|
['questions', (r.questions || []).map(questionText)],
|
|
187
188
|
['decisions', (r.decisions || []).map((d) => `${d.what}${d.why ? ` (${d.why})` : ''}`)],
|
|
188
189
|
['units', (r.units || []).map((u) => `${u.name}: ${u.status || '?'}${u.note ? ` - ${u.note}` : ''}`)],
|
|
189
|
-
['visual', (r.visual || []).map((v) => `${v.met === true ? 'met' : 'UNMET'} ${v.criterion || ''}${v.viewport ? ` @${v.viewport}` : ''}${v.note ? ` - ${v.note}` : ''}`)],
|
|
190
|
+
['visual', (r.visual || []).map((v) => `${v.met === true ? 'met' : v.met === null ? 'NOT VERIFIED' : 'UNMET'} ${v.criterion || ''}${v.viewport ? ` @${v.viewport}` : ''}${v.note ? ` - ${v.note}` : ''}`)],
|
|
190
191
|
['unresolved', r.unresolved],
|
|
191
192
|
['not verified', r.notVerified],
|
|
192
193
|
]) {
|
|
@@ -211,17 +212,26 @@ function formatSessionRow(s) {
|
|
|
211
212
|
return `${s.id.padEnd(18)} ${String(state).padEnd(11)} ${String(s.kind || '').padEnd(5)} ${when} ${s.label || s.cwd || ''}`;
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
// `repairing 1/2`, `verifying round 2`, or the bare phase.
|
|
216
|
+
function phaseText(v) {
|
|
217
|
+
const d = v.phaseDetail;
|
|
218
|
+
if (d && d.attempt !== undefined) return `${v.phase} ${d.attempt}/${d.of}`;
|
|
219
|
+
if (d && d.round !== undefined) return `${v.phase} round ${d.round}`;
|
|
220
|
+
return v.phase;
|
|
221
|
+
}
|
|
222
|
+
|
|
214
223
|
function formatSessionView(v, { events = [] } = {}) {
|
|
215
224
|
const L = [];
|
|
216
225
|
const state = v.state === 'finished' ? `finished (${v.status})` : v.state;
|
|
217
226
|
L.push(`${v.id} ${v.kind}${v.label ? ` ${v.label}` : ''}`);
|
|
218
|
-
L.push(` state ${state}${v.live ? ` · phase ${v
|
|
227
|
+
L.push(` state ${state}${v.live ? ` · phase ${phaseText(v)} · pid ${v.pid}` : ''}`);
|
|
219
228
|
L.push(` cwd ${v.cwd}`);
|
|
220
229
|
if (v.owns && v.owns.length) L.push(` owns ${v.owns.join(', ')}`);
|
|
221
230
|
L.push(` model ${v.model} · effort ${v.effort} · turns ${v.turns || 0}`);
|
|
222
231
|
if (v.activity) {
|
|
223
232
|
L.push(` activity ${v.activity.toolCalls || 0} tool calls · ${v.activity.subagentsStarted || 0} subagents${v.activity.lastTool ? ` · last ${v.activity.lastTool}` : ''} · ${v.activity.lastEventAt || ''}`);
|
|
224
233
|
}
|
|
234
|
+
if (v.idleMs !== undefined) L.push(` idle ${Math.round(v.idleMs / 1000)}s since the last engine event${v.stalled ? ' STALLED?' : ''}`);
|
|
225
235
|
if (v.state === 'interrupted') L.push(` the owning process is gone; continue with: zcode resume ${v.id}`);
|
|
226
236
|
if (events.length) {
|
|
227
237
|
L.push(' recent');
|
|
@@ -236,4 +246,4 @@ function formatSessionView(v, { events = [] } = {}) {
|
|
|
236
246
|
return L.join('\n') + '\n';
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
module.exports = { makeReporter, formatResult, formatSessionRow, formatSessionView, useColor };
|
|
249
|
+
module.exports = { phaseText, makeReporter, formatResult, formatSessionRow, formatSessionView, useColor };
|
package/lib/verify.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The independent visual verifier (v0.6). The implementer never decides
|
|
3
|
+
// `visual`: after its turn zcode opens a fresh engine session on an
|
|
4
|
+
// image-capable model that did not build the result, has it screenshot the
|
|
5
|
+
// running app and LOOK at the pixels, and parses its `zcode-visual` block.
|
|
6
|
+
// This file is the prompt and the (lenient) parser; the orchestration is in
|
|
7
|
+
// lib/executor.js.
|
|
8
|
+
const { DEFAULT_VIEWPORTS } = require('./contract.js');
|
|
9
|
+
|
|
10
|
+
// Always judged in addition to the declared criteria.
|
|
11
|
+
const BUILTIN_CRITERION =
|
|
12
|
+
'All text the page means to show is readable after animations finish: not clipped, hidden, transparent, off-screen or overlapped.';
|
|
13
|
+
|
|
14
|
+
const bullets = (items) => items.map((i) => `- ${i}`).join('\n');
|
|
15
|
+
|
|
16
|
+
const clip = (text, max) => {
|
|
17
|
+
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
18
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const WAIT_SCRIPT =
|
|
22
|
+
'await document.fonts.ready; await Promise.all(document.getAnimations().map(a => a.finished.catch(() => {})))';
|
|
23
|
+
|
|
24
|
+
// contract: the task contract; session: {id, port, out, tmp}; round: 1-based.
|
|
25
|
+
function buildVerifierPrompt(contract, { session, round = 1 }) {
|
|
26
|
+
const viewports = contract.viewports && contract.viewports.length ? contract.viewports : DEFAULT_VIEWPORTS;
|
|
27
|
+
const dir = `${session.out}/verify/${round}`;
|
|
28
|
+
return [
|
|
29
|
+
'You are an independent visual verifier. You did not build this and have not seen it. Assume it is broken until the pixels prove otherwise. DOM or text presence is NOT evidence: only what you can see in a screenshot counts.',
|
|
30
|
+
'',
|
|
31
|
+
`## What was built\n${clip(contract.objective, 500)}`,
|
|
32
|
+
'',
|
|
33
|
+
'## Criteria',
|
|
34
|
+
bullets([...contract.visual, BUILTIN_CRITERION]),
|
|
35
|
+
'',
|
|
36
|
+
'## How',
|
|
37
|
+
contract.serve
|
|
38
|
+
? `- Serve the app with \`${contract.serve}\` on $ZCODE_PORT=${session.port} (build first if it needs one; put build output under $ZCODE_OUT=${session.out}, not in the workspace).`
|
|
39
|
+
: `- Serve the app on $ZCODE_PORT=${session.port} (pick the dev/preview command from package.json, build first if needed; put build output under $ZCODE_OUT=${session.out}, not in the workspace).`,
|
|
40
|
+
`- For every viewport (${viewports.join(', ')}): navigate, wait for fonts and for all animations to finish (\`${WAIT_SCRIPT}\`, capped at about 8 s) plus 500 ms, then capture a viewport screenshot and a full-page screenshot into ${dir}/ (names <viewport>-<n>.png, plus a named screenshot for every state a criterion requires).`,
|
|
41
|
+
'- Then LOOK at each image (read the PNG file). Judge every criterion at every viewport. For each verdict QUOTE the text you can actually read in the relevant region (`seen`); if you read none, say so.',
|
|
42
|
+
`- Make NO edits: write only files under ${session.out}. Stop any server you started.`,
|
|
43
|
+
'',
|
|
44
|
+
'## Reply',
|
|
45
|
+
'End with exactly one fenced block, a JSON array with one entry per criterion per viewport:',
|
|
46
|
+
'```zcode-visual',
|
|
47
|
+
'[{"criterion": "text of the criterion", "viewport": "1440x900", "met": true, "seen": "text you read there", "note": "what is wrong, if anything", "shot": "path of the screenshot"}]',
|
|
48
|
+
'```',
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const BLOCK_RE = /```zcode-visual[^\n]*\n([\s\S]*?)```/g;
|
|
53
|
+
|
|
54
|
+
function normMet(v) {
|
|
55
|
+
if (v === true || v === false) return v;
|
|
56
|
+
if (typeof v === 'string') {
|
|
57
|
+
const t = v.trim().toLowerCase();
|
|
58
|
+
if (['true', 'yes', 'met', 'pass'].includes(t)) return true;
|
|
59
|
+
if (['false', 'no', 'unmet', 'not met', 'fail'].includes(t)) return false;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// {entries: [{criterion, viewport?, met: true|false|null, seen, note, shot}],
|
|
65
|
+
// error}. The LAST block wins; an unparsable or empty block is an error.
|
|
66
|
+
function parseVisualBlock(text) {
|
|
67
|
+
const matches = [...String(text || '').matchAll(BLOCK_RE)];
|
|
68
|
+
if (!matches.length) return { entries: [], error: 'no zcode-visual block' };
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = JSON.parse(matches[matches.length - 1][1]);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return { entries: [], error: `invalid JSON: ${err.message}` };
|
|
74
|
+
}
|
|
75
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
76
|
+
raw = Array.isArray(raw.visual) ? raw.visual : Array.isArray(raw.entries) ? raw.entries : [raw];
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(raw)) return { entries: [], error: 'not a JSON array' };
|
|
79
|
+
const entries = raw
|
|
80
|
+
.filter((e) => e && typeof e === 'object' && !Array.isArray(e) && e.criterion !== undefined)
|
|
81
|
+
.map((e) => ({
|
|
82
|
+
criterion: String(e.criterion),
|
|
83
|
+
...(e.viewport ? { viewport: String(e.viewport) } : {}),
|
|
84
|
+
met: normMet(e.met),
|
|
85
|
+
seen: e.seen === undefined || e.seen === null ? '' : String(e.seen),
|
|
86
|
+
note: e.note === undefined || e.note === null ? '' : String(e.note),
|
|
87
|
+
...(e.shot ? { shot: String(e.shot) } : {}),
|
|
88
|
+
}));
|
|
89
|
+
return entries.length ? { entries, error: null } : { entries: [], error: 'no usable entries' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Entries for criteria that could not be judged: met:null, never met:false
|
|
93
|
+
// (a verifier failure is not the implementer's fault).
|
|
94
|
+
function notVerified(criteria, why) {
|
|
95
|
+
return criteria.map((criterion) => ({ criterion, met: null, seen: '', note: `not verified: ${why}` }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { buildVerifierPrompt, parseVisualBlock, notVerified, BUILTIN_CRITERION };
|
package/lib/workspace.js
CHANGED
|
@@ -337,7 +337,37 @@ function diffSnapshots(before, after) {
|
|
|
337
337
|
return out;
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
// Keeps zcode's own scratch dirs out of the target repo's `git status`:
|
|
341
|
+
// appends missing entries to the repo-local exclude file (never .gitignore;
|
|
342
|
+
// `--git-path info/exclude` also resolves for worktrees). Idempotent,
|
|
343
|
+
// append-only, silent: every error is ignored.
|
|
344
|
+
const LOCAL_EXCLUDES = ['.zcode/', '.playwright-mcp/'];
|
|
345
|
+
function ensureLocalExclude(cwd) {
|
|
346
|
+
try {
|
|
347
|
+
const rel = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-path', 'info/exclude'], {
|
|
348
|
+
encoding: 'utf8',
|
|
349
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
350
|
+
}).trim();
|
|
351
|
+
if (!rel) return;
|
|
352
|
+
const file = path.resolve(cwd, rel);
|
|
353
|
+
let text = '';
|
|
354
|
+
try {
|
|
355
|
+
text = fs.readFileSync(file, 'utf8');
|
|
356
|
+
} catch {
|
|
357
|
+
/* no exclude file yet */
|
|
358
|
+
}
|
|
359
|
+
const have = new Set(text.split(/\r?\n/).map((l) => l.trim().replace(/^\//, '').replace(/\/$/, '')));
|
|
360
|
+
const missing = LOCAL_EXCLUDES.filter((e) => !have.has(e.replace(/\/$/, '')));
|
|
361
|
+
if (!missing.length) return;
|
|
362
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
363
|
+
fs.appendFileSync(file, `${text && !text.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`);
|
|
364
|
+
} catch {
|
|
365
|
+
/* hygiene is best-effort */
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
340
369
|
module.exports = {
|
|
370
|
+
ensureLocalExclude,
|
|
341
371
|
OwnershipError,
|
|
342
372
|
workspaceRoot,
|
|
343
373
|
normalizeOwns,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xgeorgemathew/zcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
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,
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: zcode
|
|
3
|
-
description: Delegate scoped implementation work to the zcode CLI (GLM-5.3 executor
|
|
3
|
+
description: Delegate scoped implementation work to the zcode CLI (GLM-5.3-Flash executor: sees images, has Playwright; an independent vision verifier judges UI) and consume its compact verified handback. Use when you are orchestrating: you make the product, design and logic decisions once, then zcode implements, tests, visually verifies and repairs — in one session with subagent units, several parallel sessions, or a dependency plan — while you spend tokens only on decisions and exceptions.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Operating zcode as an executor
|
|
7
7
|
|
|
8
|
-
zcode is a CLI execution engine. You decide; it builds. It implements a scoped task in its own session, runs your checks itself,
|
|
8
|
+
zcode is a CLI execution engine. You decide; it builds. It implements a scoped task in its own session, runs your checks itself, has a fresh, independent vision verifier screenshot and judge UI when you ask, repairs failures within a budget, and returns a small structured handback.
|
|
9
9
|
|
|
10
10
|
**The point is token economy.** Your tokens go to decisions and exceptions. Everything mechanical — writing code, running builds and tests, reading diffs, looking at screenshots, fixing small failures — is zcode's. If you are doing any of that after dispatch, you are paying twice.
|
|
11
11
|
|
|
12
|
-
Check readiness once per machine: `zcode doctor` (exit 3 → `zcode login`; exit 4 → engine missing). Don't run `--help`: everything you need is here.
|
|
12
|
+
Check readiness once per machine: `zcode doctor` (exit 3 → `zcode login`; exit 4 → engine missing). Don't run `--help` or a bare `zcode status`: everything you need is here.
|
|
13
13
|
|
|
14
14
|
## The workflow
|
|
15
15
|
|
|
16
16
|
1. **Read** the codebase as much as you need to plan. This is the one phase where reading files is expected.
|
|
17
17
|
2. **Decide** everything a user could see or a system depends on (checklists below). Write the decisions down once, as decisions — a file such as `docs/decisions.md` that sessions receive with `--decisions @file`.
|
|
18
18
|
3. **Shape** the work: one session with units, several sessions, or a plan (next section).
|
|
19
|
-
4. **Preflight** (optional
|
|
19
|
+
4. **Preflight** (optional): `--preflight` makes zcode list every question it would otherwise guess; answer them all in one edit to the decisions file. It costs you a round trip, so use it only when your decisions have known gaps or the work is large and novel; skip it when the checklists below are fully covered.
|
|
20
20
|
5. **Dispatch and wait** with `--detach` + `zcode wait`, or `zcode plan run` + `zcode plan wait`.
|
|
21
21
|
6. **Handle exceptions only.** `completed` → move on. Anything else → read only what the brief shows, answer or adjust in one line with `zcode resume`.
|
|
22
22
|
|
|
@@ -30,7 +30,7 @@ The executor implements binding decisions exactly and is told not to invent anyt
|
|
|
30
30
|
|
|
31
31
|
**Boundaries:** what the executor may choose freely (internal names, file structure, helpers) and what it must ask about.
|
|
32
32
|
|
|
33
|
-
Write decisions as statements, not wishes: "Reveal: opacity 0→1 and translateY 16px→0, 400 ms, cubic-bezier(.2,.7,.2,1), 60 ms stagger, at 20% visibility; reduced motion: opacity only."
|
|
33
|
+
Keep the decisions file dense: tables and one-line statements, no prose, no rationale the executor doesn't need (aim for a few thousand tokens for a one-page site). Write decisions as statements, not wishes: "Reveal: opacity 0→1 and translateY 16px→0, 400 ms, cubic-bezier(.2,.7,.2,1), 60 ms stagger, at 20% visibility; reduced motion: opacity only."
|
|
34
34
|
|
|
35
35
|
## Shaping the work
|
|
36
36
|
|
|
@@ -40,7 +40,7 @@ Pick the cheapest shape that keeps each session finishable without you.
|
|
|
40
40
|
- **Several sessions** when pieces are independent and own disjoint paths. Launch them `--detach` in parallel; each may have its own units.
|
|
41
41
|
- **A plan** when there are dependencies (scaffold → parallel features → integration). `zcode plan run plan.json` sequences and parallelises for you; you wait once.
|
|
42
42
|
|
|
43
|
-
Parallel safety is automatic: each session gets `ZCODE_PORT`, `ZCODE_OUT`, `ZCODE_TMP` and `ZCODE_SESSION_ID`. Tell the first (scaffold) session to make dev-server ports, build output dirs and test artifacts honour them.
|
|
43
|
+
Parallel safety is automatic: each session gets `ZCODE_PORT`, `ZCODE_OUT`, `ZCODE_TMP` and `ZCODE_SESSION_ID`. Tell the first (scaffold) session to make dev-server ports, build output dirs and test artifacts honour them. zcode keeps `.zcode/` and `.playwright-mcp/` out of git via the repo's local `.git/info/exclude`; you don't need to touch `.gitignore` for them.
|
|
44
44
|
|
|
45
45
|
## The handoff
|
|
46
46
|
|
|
@@ -49,7 +49,7 @@ Parallel safety is automatic: each session gets `ZCODE_PORT`, `ZCODE_OUT`, `ZCOD
|
|
|
49
49
|
| objective | positional text | always: the outcome, not the steps |
|
|
50
50
|
| decisions | `--decision "…"`, `--decisions @file` | always for anything user-visible or behavioural |
|
|
51
51
|
| checks | `--check "cmd"` | always: build, typecheck, tests, lint, greps — zcode runs them itself |
|
|
52
|
-
| visual | `--visual "criterion"`, `--viewport WxH`, `--serve "cmd"` | any UI work:
|
|
52
|
+
| visual | `--visual "criterion"`, `--viewport WxH`, `--serve "cmd"` | any UI work: after the build, a fresh vision verifier serves on $ZCODE_PORT, waits for animations to finish, screenshots each viewport, quotes the text it can actually see, judges each criterion (plus "all intended text is readable"), and unmet ones go back to the implementer for repair. Phrase criteria as what must be visible: "hero headline 'Your money, simplified' is fully readable at 390 and 1440" |
|
|
53
53
|
| units | `--unit "name: objective [owns a,b]"` | one session should fan out to subagents |
|
|
54
54
|
| owns | `--owns path/glob` | always when other sessions may run in the same repo |
|
|
55
55
|
| touch | `--touch path` | a small edit to a shared file no running session owns |
|
|
@@ -58,7 +58,7 @@ Parallel safety is automatic: each session gets `ZCODE_PORT`, `ZCODE_OUT`, `ZCOD
|
|
|
58
58
|
| acceptance | `--accept "…"` | criteria beyond checks and visuals |
|
|
59
59
|
| everything | `--task contract.json` | keys: objective, scope, constraints, decisions, design, acceptance, checks, visual, viewports, units, files, references, context, owns, touch, limits, model, effort |
|
|
60
60
|
|
|
61
|
-
Model and effort: `--model glm-5.3`
|
|
61
|
+
Model and effort: don't pass `--model`. Everything runs on GLM-5.3-Flash by default (it sees images): sessions, units, plans, the verifier. Use `--model glm-5.3` only when the user asks for it; it can't see images. `--effort low|high|max` (default `max`). Other values are refused before anything starts.
|
|
62
62
|
|
|
63
63
|
## Running
|
|
64
64
|
|
|
@@ -81,13 +81,17 @@ zcode plan resume <name> ["<message>"] # resume failed/partial, rel
|
|
|
81
81
|
zcode plan resume <name> --session <id> "<answer>" # answer one blocked session
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
`plan.json`: `{"cwd"?, "decisions"?, "context"?, "files"?, "model"?, "effort"?, "sessions": [{"id", "after"?: [ids], ...contract keys}]}`. Top-level decisions/context/files go to every session; `@` entries
|
|
84
|
+
`plan.json`: `{"cwd"?, "decisions"?, "context"?, "files"?, "model"?, "effort"?, "limits"?, "sessions": [{"id", "after"?: [ids], "label"?, ...contract keys}]}`. Top-level decisions/context/files/limits go to every session; `@` entries resolve against the plan file's directory, then the plan's cwd. Don't also list the decisions file under `files`: it is already inlined. The plan name is the file basename (or `--id`). Sessions that can run at the same time must own disjoint paths; `plan check` enforces it. When an upstream session ends non-completed, its dependants are `skipped` until you `plan resume`.
|
|
85
|
+
|
|
86
|
+
Session ids are global and permanent: give every new run a fresh prefix (`wc3-scaffold`, …) so `plan check` passes first time. A task session may run 2 h by default; a big session (a whole page, a whole flow) can set `"limits": {"timeoutMs": 10800000}`. If one still times out, `zcode plan resume <name> "continue"` picks up from the files it already wrote.
|
|
87
|
+
|
|
88
|
+
While sessions run, the user can watch them with `zcode watch` (live phase, last tool, model requests, `STALLED?` after 8 idle minutes); you never run it. `wait` timeout lines carry `idleMs`/`stalled`: a stalled session is the one case to `cancel` + `resume` early.
|
|
85
89
|
|
|
86
90
|
Your Bash tool times out at 10 minutes; always wait in bounded slices. Run the wait with `run_in_background` if you have other planning to do.
|
|
87
91
|
|
|
88
92
|
## Reading the brief
|
|
89
93
|
|
|
90
|
-
`--json` prints one compact line: `status`, `summary`, `files`, failing
|
|
94
|
+
`--json` prints one compact line: `status`, `summary`, `files`, failing `checks` with tails (`checksPassed` counts the rest), `questions`, `issues`, `unresolved`, `scopeViolations`, `touched`, the first `decisions` (`decisionsMore` counts the rest), unmet `visual` with what the verifier actually `seen` (`visualMet` is "met/total"), `units`, and `result` (path to the full record). Read it straight from the command output; don't pipe it through jq, python or tail.
|
|
91
95
|
|
|
92
96
|
| status | exit | do |
|
|
93
97
|
|---|---|---|
|
|
@@ -101,7 +105,7 @@ Your Bash tool times out at 10 minutes; always wait in bounded slices. Run the w
|
|
|
101
105
|
## Hard rules after dispatch
|
|
102
106
|
|
|
103
107
|
- Don't open files a `completed` session changed. Don't re-run checks it ran. Don't look at its screenshots.
|
|
104
|
-
- Don't poll `status`; use `wait
|
|
108
|
+
- Don't poll `status`; use `wait` (or `plan wait`) once, in the background if you like, and act on its exit.
|
|
105
109
|
- Don't fix code yourself; `resume` the session with one line.
|
|
106
110
|
- Need to know something about the result? `zcode result <id> --full` first; read code only as a last resort.
|
|
107
111
|
- Don't ask zcode to commit, push, publish or deploy unless the user asked; when they did, make it its own final session gated on everything else being `completed`.
|
|
@@ -113,7 +117,7 @@ You read the empty repo, decide the design system, copy, motion, form logic and
|
|
|
113
117
|
```json
|
|
114
118
|
{
|
|
115
119
|
"decisions": ["@docs/decisions.md"],
|
|
116
|
-
"
|
|
120
|
+
"effort": "high",
|
|
117
121
|
"sessions": [
|
|
118
122
|
{"id": "scaffold", "owns": ["package.json", "package-lock.json", "index.html", "*.config.*", "tsconfig*.json", "src/app/**", "src/styles/**", "src/components/**", "e2e/smoke.spec.ts", ".gitignore"],
|
|
119
123
|
"objective": "Vite + React + TS scaffold, tokens, shared shell, stubs for src/landing and src/apply; dev/preview/e2e honour ZCODE_PORT and ZCODE_OUT",
|
|
@@ -135,7 +139,7 @@ You read the empty repo, decide the design system, copy, motion, form logic and
|
|
|
135
139
|
}
|
|
136
140
|
```
|
|
137
141
|
|
|
138
|
-
`zcode plan check plan.json` → `zcode plan run plan.json --preflight` (read-only `<id>.pre` sessions; prints only the questions) → answer them once in the decisions file → `zcode plan run plan.json --detach` → `zcode plan wait plan --timeout-ms 540000 --json`. Your total involvement: the decisions, one answer pass, one summary.
|
|
142
|
+
`zcode plan check plan.json` → `zcode plan run plan.json --preflight` (read-only `<id>.pre` sessions, all at once; downstream sessions are told their upstream hasn't run yet; prints only the questions) → answer them once in the decisions file → `zcode plan run plan.json --detach` → `zcode plan wait plan --timeout-ms 540000 --json`. Your total involvement: the decisions, one answer pass, one summary.
|
|
139
143
|
|
|
140
144
|
## Exit codes
|
|
141
145
|
|