@0xgeorgemathew/zcode 0.5.1 → 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 +152 -34
- 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/contract.js
CHANGED
|
@@ -189,21 +189,35 @@ function unitsSection(units) {
|
|
|
189
189
|
return `## Units\nDispatch each unit below to its own subagent (Agent tool) with that unit's objective, the binding decisions above and its owns. Run independent units in parallel, then integrate and verify the whole. Report each in handback \`units\`: [{"name", "status", "note"}].\n${bullets(lines)}\n`;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
// Only rendered when the contract declares visual criteria.
|
|
193
|
-
|
|
192
|
+
// Only rendered when the contract declares visual criteria. zcode itself
|
|
193
|
+
// decides `visual`: an independent image-capable verifier judges the result
|
|
194
|
+
// from its own screenshots after the work (lib/verify.js).
|
|
195
|
+
function visualSection(contract, { canSee = true } = {}) {
|
|
194
196
|
if (!contract.visual || !contract.visual.length) return '';
|
|
195
197
|
const viewports = contract.viewports && contract.viewports.length ? contract.viewports : DEFAULT_VIEWPORTS;
|
|
198
|
+
const serve = contract.serve
|
|
199
|
+
? `- Make sure the app serves with \`${contract.serve}\` on $ZCODE_PORT (it must honour that port).`
|
|
200
|
+
: '- Make sure the app serves on $ZCODE_PORT (pick the dev/preview command from package.json; no other port).';
|
|
201
|
+
if (!canSee) {
|
|
202
|
+
return [
|
|
203
|
+
'## Visual verification',
|
|
204
|
+
'You cannot see images. Do not judge screenshots or report `visual`; an independent vision verifier will.',
|
|
205
|
+
serve,
|
|
206
|
+
'- Self-check visibility in the DOM: key text must have a non-zero box inside the viewport, not clipped by an overflow ancestor, computed opacity > 0, visibility visible, after animations finish.',
|
|
207
|
+
`Viewports: ${viewports.join(', ')}. Criteria the verifier will judge:`,
|
|
208
|
+
bullets(contract.visual),
|
|
209
|
+
'',
|
|
210
|
+
].join('\n');
|
|
211
|
+
}
|
|
196
212
|
return [
|
|
197
213
|
'## Visual verification',
|
|
198
|
-
'Verify the rendered result yourself with Playwright; you can look at screenshots.',
|
|
199
|
-
|
|
200
|
-
? `- Serve the app with \`${contract.serve}\` on $ZCODE_PORT (it must honour that port).`
|
|
201
|
-
: '- Serve the app on $ZCODE_PORT (pick the dev/preview command from package.json; no other port).',
|
|
214
|
+
'Verify the rendered result yourself with Playwright; you can look at screenshots. An independent verifier will re-judge it from its own screenshots afterwards.',
|
|
215
|
+
serve,
|
|
202
216
|
`- At each viewport (${viewports.join(', ')}) capture screenshots into $ZCODE_OUT/shots: the full page plus every key state a criterion names.`,
|
|
203
217
|
'- Look at the screenshots and judge each criterion. Fix failures, re-capture, judge again. Stop the server when done.',
|
|
204
218
|
'Criteria:',
|
|
205
219
|
bullets(contract.visual),
|
|
206
|
-
'
|
|
220
|
+
'You may report handback `visual`: [{"criterion", "viewport", "met", "note", "shot"}]; it is not required.',
|
|
207
221
|
'',
|
|
208
222
|
].join('\n');
|
|
209
223
|
}
|
|
@@ -215,7 +229,7 @@ const PREFLIGHT_SECTION = [
|
|
|
215
229
|
].join('\n');
|
|
216
230
|
|
|
217
231
|
// `ownsDisplay` are the patterns as the caller wrote them (relative to cwd).
|
|
218
|
-
function buildTaskPrompt(contract, { ownsDisplay = contract.owns, session = null, preflight = false } = {}) {
|
|
232
|
+
function buildTaskPrompt(contract, { ownsDisplay = contract.owns, session = null, preflight = false, canSeeImages = true } = {}) {
|
|
219
233
|
const parts = [
|
|
220
234
|
'You are Zcode, an execution engine invoked by an orchestrator agent. The orchestrator owns product decisions, design direction, planning and task boundaries. You own implementation and verification inside the scope of this task. Work autonomously: there is no human watching and nobody reviews your intermediate steps.',
|
|
221
235
|
'',
|
|
@@ -235,7 +249,7 @@ function buildTaskPrompt(contract, { ownsDisplay = contract.owns, session = null
|
|
|
235
249
|
? `## Also allowed\nYou may also make minimal edits to:\n${bullets(contract.touch)}\nReport each such edit in your handback \`changes\`. Do not use this to widen the task.\n`
|
|
236
250
|
: '',
|
|
237
251
|
sessionSection(session),
|
|
238
|
-
visualSection(contract),
|
|
252
|
+
visualSection(contract, { canSee: canSeeImages }),
|
|
239
253
|
preflight ? PREFLIGHT_SECTION : '',
|
|
240
254
|
contract.checks.length && !preflight
|
|
241
255
|
? `## Acceptance checks\nAfter you finish, Zcode will run these commands in the working directory and treat any non-zero exit as a failure. Run them yourself before you finish:\n${bullets(contract.checks.map((c) => `\`${c}\``))}\n`
|
|
@@ -251,11 +265,12 @@ function buildTaskPrompt(contract, { ownsDisplay = contract.owns, session = null
|
|
|
251
265
|
'- Report "blocked" only when you lack information or authority that this task does not provide (for example a missing credential, a required change outside your ownership, or a consequential ambiguity).',
|
|
252
266
|
'',
|
|
253
267
|
].join('\n'),
|
|
254
|
-
`## Handback\n${HANDBACK_SPEC}
|
|
268
|
+
`## Handback\n${HANDBACK_SPEC}`,
|
|
255
269
|
];
|
|
256
270
|
return parts.filter(Boolean).join('\n');
|
|
257
271
|
}
|
|
258
272
|
|
|
273
|
+
// `visual`: the verifier's unmet entries {criterion, viewport, seen, note, shot}.
|
|
259
274
|
function buildRepairPrompt(failures, { attempt, maxRepairs, visual = [] }) {
|
|
260
275
|
const blocks = failures.map((f) => {
|
|
261
276
|
const why = f.timedOut ? 'timed out' : `exit ${f.exitCode}`;
|
|
@@ -263,14 +278,21 @@ function buildRepairPrompt(failures, { attempt, maxRepairs, visual = [] }) {
|
|
|
263
278
|
});
|
|
264
279
|
const head = failures.length
|
|
265
280
|
? `Zcode ran the acceptance checks and ${failures.length === 1 ? 'one failed' : `${failures.length} failed`} (repair attempt ${attempt} of ${maxRepairs}):`
|
|
266
|
-
: `
|
|
281
|
+
: `An independent visual verifier, looking at screenshots of the running app, found criteria unmet (repair attempt ${attempt} of ${maxRepairs}).`;
|
|
267
282
|
const lines = [head, '', ...blocks];
|
|
268
283
|
if (visual.length) {
|
|
269
284
|
lines.push(
|
|
270
285
|
failures.length ? '' : null,
|
|
271
|
-
'Visual criteria
|
|
272
|
-
...visual.map((v) =>
|
|
273
|
-
|
|
286
|
+
'Visual criteria the verifier found unmet:',
|
|
287
|
+
...visual.map((v) => {
|
|
288
|
+
const text = [
|
|
289
|
+
v.seen ? `saw: ${JSON.stringify(String(v.seen).slice(0, 300))}` : null,
|
|
290
|
+
v.note ? String(v.note).slice(0, 300) : null,
|
|
291
|
+
v.shot ? `shot: ${v.shot}` : null,
|
|
292
|
+
].filter(Boolean).join(' | ');
|
|
293
|
+
return `- ${v.criterion || '(unnamed)'}${v.viewport ? ` @${v.viewport}` : ''}${text ? ` - ${text}` : ''}`;
|
|
294
|
+
}),
|
|
295
|
+
'Fix the cause. Zcode will re-run the checks and the verifier will look again; its screenshots are the evidence, not DOM presence.'
|
|
274
296
|
);
|
|
275
297
|
}
|
|
276
298
|
lines.push(
|
|
@@ -286,12 +308,6 @@ const HANDBACK_REQUEST =
|
|
|
286
308
|
'Your previous message did not end with a valid zcode-handback block. Do not redo any work. Reply with only the handback block describing the current state.\n\n' +
|
|
287
309
|
HANDBACK_SPEC;
|
|
288
310
|
|
|
289
|
-
// Cheap report-only follow-up when declared visual criteria were left out of
|
|
290
|
-
// the handback (no redo, no repair budget).
|
|
291
|
-
function buildVisualReportRequest(missing) {
|
|
292
|
-
return `Do not redo any work. Reply with only the handback block, including \`visual\` for every criterion: ${missing.join('; ')}\n\n` + HANDBACK_SPEC;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
311
|
function buildResumePrompt(message, { kind, portChange = null }) {
|
|
296
312
|
const note =
|
|
297
313
|
'Work from earlier in this session is already applied to the workspace. Inspect the current state before repeating any step, and do not repeat external side effects.';
|
|
@@ -336,27 +352,29 @@ function decision(d) {
|
|
|
336
352
|
return isObject(d) ? { what: String(d.what ?? '').trim(), why: String(d.why ?? '').trim() } : { what: String(d ?? '').trim(), why: '' };
|
|
337
353
|
}
|
|
338
354
|
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
355
|
+
// Visual verdicts (zcode's verifier entries) against the declared criteria.
|
|
356
|
+
// `failed`: entries judged unmet (met:false); `unverified`: entries the
|
|
357
|
+
// verifier could not judge (met:null); `missing`: declared criteria no entry
|
|
358
|
+
// covers. Entries not met:true include the built-in readability criterion.
|
|
359
|
+
function visualGaps(entries, declared) {
|
|
360
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
344
361
|
const norm = (x) => String(x || '').trim().toLowerCase();
|
|
345
|
-
const covered = (c) =>
|
|
362
|
+
const covered = (c) => list.some((v) => {
|
|
346
363
|
const a = norm(v.criterion);
|
|
347
364
|
return a && (a === norm(c) || a.includes(norm(c)) || norm(c).includes(a));
|
|
348
365
|
});
|
|
366
|
+
const label = (v) => `${v.criterion || '(unnamed)'}${v.viewport ? ` @${v.viewport}` : ''}`;
|
|
349
367
|
return {
|
|
350
|
-
missing: declared.filter((c) => !covered(c)),
|
|
351
|
-
failed:
|
|
352
|
-
|
|
353
|
-
.map((v) => `${v.criterion || '(unnamed)'}${v.viewport ? ` @${v.viewport}` : ''}`),
|
|
368
|
+
missing: (declared || []).filter((c) => !covered(c)),
|
|
369
|
+
failed: list.filter((v) => v.met === false),
|
|
370
|
+
unverified: list.filter((v) => v.met === null).map(label),
|
|
354
371
|
};
|
|
355
372
|
}
|
|
356
373
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
374
|
+
// Strings for what is unmet: failed entries and declared criteria nobody judged.
|
|
375
|
+
function unmetVisual(entries, declared) {
|
|
376
|
+
const { missing, failed } = visualGaps(entries, declared);
|
|
377
|
+
return [...failed.map((v) => `${v.criterion || '(unnamed)'}${v.viewport ? ` @${v.viewport}` : ''}`), ...missing.map((c) => `${c} (not reported)`)];
|
|
360
378
|
}
|
|
361
379
|
|
|
362
380
|
// Returns {handback, report, error}: the LAST block wins; `report` is the
|
|
@@ -422,6 +440,7 @@ function deriveStatus({
|
|
|
422
440
|
preflight = false,
|
|
423
441
|
preflightWrote = [],
|
|
424
442
|
visualDeclared = [],
|
|
443
|
+
visual = [],
|
|
425
444
|
}) {
|
|
426
445
|
const issues = [];
|
|
427
446
|
if (cancelled) return { status: 'cancelled', issues: ['cancelled before completion'] };
|
|
@@ -461,10 +480,17 @@ function deriveStatus({
|
|
|
461
480
|
partial = true;
|
|
462
481
|
issues.push(`unmet acceptance criteria: ${unmet.map((c) => c.criterion).join('; ')}`);
|
|
463
482
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
483
|
+
if (handback && visualDeclared.length) {
|
|
484
|
+
const gaps = visualGaps(visual, visualDeclared);
|
|
485
|
+
const unmet = unmetVisual(visual, visualDeclared);
|
|
486
|
+
if (unmet.length) {
|
|
487
|
+
partial = true;
|
|
488
|
+
issues.push(`visual criteria unmet: ${unmet.join('; ')}`);
|
|
489
|
+
}
|
|
490
|
+
if (gaps.unverified.length) {
|
|
491
|
+
partial = true;
|
|
492
|
+
issues.push(`visual not verified: ${gaps.unverified.join('; ')}`);
|
|
493
|
+
}
|
|
468
494
|
}
|
|
469
495
|
if (scopeViolations.length) {
|
|
470
496
|
partial = true;
|
|
@@ -493,7 +519,6 @@ module.exports = {
|
|
|
493
519
|
questionText,
|
|
494
520
|
unmetVisual,
|
|
495
521
|
visualGaps,
|
|
496
|
-
buildVisualReportRequest,
|
|
497
522
|
normalizeContract,
|
|
498
523
|
buildTaskPrompt,
|
|
499
524
|
buildRepairPrompt,
|
package/lib/executor.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
liveLeases,
|
|
29
29
|
snapshot,
|
|
30
30
|
diffSnapshots,
|
|
31
|
+
ensureLocalExclude,
|
|
31
32
|
} = require('./workspace.js');
|
|
32
33
|
const {
|
|
33
34
|
buildTaskPrompt,
|
|
@@ -36,10 +37,10 @@ const {
|
|
|
36
37
|
HANDBACK_REQUEST,
|
|
37
38
|
parseHandback,
|
|
38
39
|
deriveStatus,
|
|
39
|
-
unmetVisual,
|
|
40
40
|
visualGaps,
|
|
41
|
-
buildVisualReportRequest,
|
|
42
41
|
} = require('./contract.js');
|
|
42
|
+
const { buildVerifierPrompt, parseVisualBlock, notVerified: notVerifiedEntries } = require('./verify.js');
|
|
43
|
+
const { visionModel, supportsImage } = require('./models.js');
|
|
43
44
|
|
|
44
45
|
const RESULT_SCHEMA = 'zcode.result/1';
|
|
45
46
|
const SUBAGENT_TOOLS = new Set(['Agent', 'Task']);
|
|
@@ -131,6 +132,31 @@ function sumUsage(turns) {
|
|
|
131
132
|
return input === null && output === null ? null : { inputTokens: input, outputTokens: output };
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// Model requests and tokens over every turn of an invocation, failed ones
|
|
136
|
+
// included (their usage never arrived: `incomplete`). null = nothing ran.
|
|
137
|
+
function usageOf(turns, failedTurns) {
|
|
138
|
+
const all = [...turns, ...failedTurns];
|
|
139
|
+
if (!all.length) return null;
|
|
140
|
+
const base = sumUsage(turns) || { inputTokens: null, outputTokens: null };
|
|
141
|
+
const incomplete = all.some((t) => !t.usage);
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
modelRequests: all.reduce((n, t) => n + (t.modelRequests || 0), 0),
|
|
145
|
+
...(incomplete ? { incomplete: true } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Running totals across invocations (resumes) kept on the session record.
|
|
150
|
+
function addUsageTotal(prior, usage) {
|
|
151
|
+
const p = prior || {};
|
|
152
|
+
const u = usage || {};
|
|
153
|
+
return {
|
|
154
|
+
inputTokens: (p.inputTokens || 0) + (u.inputTokens || 0),
|
|
155
|
+
outputTokens: (p.outputTokens || 0) + (u.outputTokens || 0),
|
|
156
|
+
modelRequests: (p.modelRequests || 0) + (u.modelRequests || 0),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
function exitCodeFor(result) {
|
|
135
161
|
switch (result.status) {
|
|
136
162
|
case 'completed':
|
|
@@ -231,31 +257,42 @@ async function execute(inv, opts = {}) {
|
|
|
231
257
|
let lastWrite = 0;
|
|
232
258
|
let toolCalls = record.activity ? record.activity.toolCalls || 0 : 0;
|
|
233
259
|
let subagentsStarted = record.activity ? record.activity.subagentsStarted || 0 : 0;
|
|
260
|
+
let modelRequests = record.activity ? record.activity.modelRequests || 0 : 0;
|
|
261
|
+
let subagentsRunning = 0;
|
|
234
262
|
let lastTool = null;
|
|
263
|
+
let lastToolAt = null;
|
|
264
|
+
let lastEventAt = null;
|
|
265
|
+
const activity = () => ({
|
|
266
|
+
toolCalls, subagentsStarted, subagentsRunning, modelRequests, lastTool, lastToolAt,
|
|
267
|
+
lastEventAt: lastEventAt || new Date().toISOString(),
|
|
268
|
+
});
|
|
235
269
|
const relay = (ev) => {
|
|
236
270
|
emit(ev);
|
|
237
|
-
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
lastEventAt = new Date(now).toISOString(); // any engine event counts as life
|
|
273
|
+
if (ev.type === 'model_request_started') modelRequests++;
|
|
238
274
|
if (ev.type === 'tool') {
|
|
239
275
|
if (ev.phase === 'started') {
|
|
240
276
|
toolCalls++;
|
|
241
277
|
lastTool = ev.name;
|
|
278
|
+
lastToolAt = new Date(now).toISOString();
|
|
242
279
|
if (SUBAGENT_TOOLS.has(ev.name)) {
|
|
243
280
|
subagentsStarted++;
|
|
281
|
+
subagentsRunning++;
|
|
244
282
|
emit({ type: 'subagent', phase: 'started', toolCallId: ev.id });
|
|
245
283
|
}
|
|
246
284
|
} else if ((ev.phase === 'result' || ev.phase === 'error') && SUBAGENT_TOOLS.has(ev.name)) {
|
|
285
|
+
subagentsRunning = Math.max(0, subagentsRunning - 1);
|
|
247
286
|
emit({ type: 'subagent', phase: ev.phase === 'result' ? 'returned' : 'error', toolCallId: ev.id });
|
|
248
287
|
}
|
|
249
288
|
}
|
|
250
|
-
if (ev.type === 'model_request_started' || ev.type === 'state' || ev.type === 'turn') return;
|
|
251
289
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
290
|
+
if (!(ev.type === 'stream' || ev.type === 'model_request_started' || ev.type === 'state' || ev.type === 'turn')) {
|
|
291
|
+
store.appendEvent(id, ev, env);
|
|
292
|
+
}
|
|
254
293
|
if (now - lastWrite > 1000) {
|
|
255
294
|
lastWrite = now;
|
|
256
|
-
update({
|
|
257
|
-
activity: { toolCalls, subagentsStarted, lastTool, lastEventAt: new Date(now).toISOString() },
|
|
258
|
-
});
|
|
295
|
+
update({ activity: activity() });
|
|
259
296
|
}
|
|
260
297
|
} catch {
|
|
261
298
|
/* activity is best-effort; never fail the run over it */
|
|
@@ -266,7 +303,8 @@ async function execute(inv, opts = {}) {
|
|
|
266
303
|
emit(ev);
|
|
267
304
|
try {
|
|
268
305
|
store.appendEvent(id, ev, env);
|
|
269
|
-
|
|
306
|
+
lastEventAt = new Date().toISOString();
|
|
307
|
+
update({ phase, phaseDetail: Object.keys(detail).length ? detail : null, activity: activity() });
|
|
270
308
|
} catch {
|
|
271
309
|
/* best-effort */
|
|
272
310
|
}
|
|
@@ -328,10 +366,13 @@ async function execute(inv, opts = {}) {
|
|
|
328
366
|
}
|
|
329
367
|
|
|
330
368
|
update({ state: 'running', phase: 'starting' });
|
|
369
|
+
ensureLocalExclude(record.cwd);
|
|
331
370
|
|
|
332
371
|
let client = null;
|
|
333
372
|
let session = null;
|
|
334
373
|
const turns = [];
|
|
374
|
+
const vturns = []; // independent verifier turns (never the implementer's)
|
|
375
|
+
const failedTurns = []; // turns that threw: only their partial facts survive
|
|
335
376
|
let checks = [];
|
|
336
377
|
let repairs = 0;
|
|
337
378
|
let parsed = { handback: null, report: '', error: 'missing' };
|
|
@@ -345,7 +386,11 @@ async function execute(inv, opts = {}) {
|
|
|
345
386
|
let sessionEnv = null;
|
|
346
387
|
const visualDeclared = (inv.contract && inv.contract.visual) || [];
|
|
347
388
|
const concurrent = new Map(lease.others.map((o) => [o.id, o.owns || []]));
|
|
348
|
-
const checkTouched = new Set(); // files written by zcode's own check runs
|
|
389
|
+
const checkTouched = new Set(); // files written by zcode's own check runs and the verifier
|
|
390
|
+
const verifierEdits = new Set();
|
|
391
|
+
let verifierModel = null;
|
|
392
|
+
let verifyRounds = 0;
|
|
393
|
+
let visualEntries = null; // the verifier's verdicts: authoritative `visual`
|
|
349
394
|
const invocationStart = new Date(started).toISOString();
|
|
350
395
|
|
|
351
396
|
try {
|
|
@@ -379,6 +424,7 @@ async function execute(inv, opts = {}) {
|
|
|
379
424
|
browser: !inv.noBrowser,
|
|
380
425
|
skillsDirs: inv.skillsDirs || [],
|
|
381
426
|
noSkills: inv.noSkills,
|
|
427
|
+
model: inv.model,
|
|
382
428
|
});
|
|
383
429
|
before = snapshot(root);
|
|
384
430
|
|
|
@@ -415,6 +461,7 @@ async function execute(inv, opts = {}) {
|
|
|
415
461
|
ownsDisplay: inv.ownsDisplay || [],
|
|
416
462
|
session: { id, port: sessionEnv.ZCODE_PORT, out: sessionEnv.ZCODE_OUT, tmp: sessionEnv.ZCODE_TMP },
|
|
417
463
|
preflight,
|
|
464
|
+
canSeeImages: supportsImage(inv.model, { env, builtinConfig: runtime.builtinConfig }),
|
|
418
465
|
});
|
|
419
466
|
prompt = leavingPreflight
|
|
420
467
|
? `${taskPrompt()}\n\n## Answers to your preflight questions\nThe preflight is over: implement the task now (edits are allowed). The orchestrator's answers are binding:\n${(inv.resume.message || '').trim() || '(none: take the most conservative choice for anything open and list it in handback `decisions`)'}`
|
|
@@ -435,8 +482,67 @@ async function execute(inv, opts = {}) {
|
|
|
435
482
|
prompt = inv.resume ? buildResumePrompt(inv.resume.message, { kind: 'run' }) : inv.prompt;
|
|
436
483
|
}
|
|
437
484
|
|
|
485
|
+
// One verifier round: a NEW engine session (own client: fresh context,
|
|
486
|
+
// separate from the implementer's) on the first image-capable model.
|
|
487
|
+
// Returns an entry per criterion; failures become met:null, never false.
|
|
488
|
+
const verifyVisual = async (round) => {
|
|
489
|
+
verifierModel = visionModel({ env, builtinConfig: runtime.builtinConfig });
|
|
490
|
+
verifyRounds = round;
|
|
491
|
+
setPhase('verifying', { round });
|
|
492
|
+
const vprompt = buildVerifierPrompt(inv.contract, {
|
|
493
|
+
session: { id, port: sessionEnv.ZCODE_PORT, out: sessionEnv.ZCODE_OUT, tmp: sessionEnv.ZCODE_TMP },
|
|
494
|
+
round,
|
|
495
|
+
});
|
|
496
|
+
const preSnap = before ? snapshot(root) : null;
|
|
497
|
+
let entries = null;
|
|
498
|
+
let why = 'verifier did not run';
|
|
499
|
+
for (let attempt = 1; attempt <= 2 && !entries; attempt++) {
|
|
500
|
+
if ((signal && signal.aborted) || Date.now() >= deadline) break;
|
|
501
|
+
let vclient = null;
|
|
502
|
+
try {
|
|
503
|
+
vclient = makeClient(runtime, { home, cwd: record.cwd, sessionEnv });
|
|
504
|
+
vclient.start();
|
|
505
|
+
const vsession = await openSession(vclient, {
|
|
506
|
+
workspacePath: record.cwd,
|
|
507
|
+
modelId: verifierModel,
|
|
508
|
+
effort: 'high',
|
|
509
|
+
requiredMcp: built.mcp.added,
|
|
510
|
+
onEvent: relay,
|
|
511
|
+
});
|
|
512
|
+
const vturn = await runTurn(vclient, vsession, {
|
|
513
|
+
modelId: verifierModel,
|
|
514
|
+
effort: 'high',
|
|
515
|
+
prompt: vprompt,
|
|
516
|
+
deadline,
|
|
517
|
+
signal,
|
|
518
|
+
onEvent: relay,
|
|
519
|
+
});
|
|
520
|
+
vturns.push(vturn);
|
|
521
|
+
const p = parseVisualBlock(vturn.response);
|
|
522
|
+
if (p.error) why = `the verifier gave no usable zcode-visual block (${p.error})`;
|
|
523
|
+
else entries = p.entries;
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (err && err.turn) failedTurns.push(err.turn);
|
|
526
|
+
if (err && (err.kind === 'timeout' || err.kind === 'cancelled')) throw err;
|
|
527
|
+
why = `verifier failed: ${scrub(err && err.message ? err.message : String(err)).slice(0, 200)}`;
|
|
528
|
+
} finally {
|
|
529
|
+
if (vclient) vclient.kill();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (preSnap) {
|
|
533
|
+
const owned = (p) => [...concurrent.values()].some((o) => o.length && isOwned(p, o));
|
|
534
|
+
for (const f of diffSnapshots(preSnap, snapshot(root)) || []) {
|
|
535
|
+
if (isZcodePath(f.path) || owned(f.path)) continue;
|
|
536
|
+
verifierEdits.add(f.path);
|
|
537
|
+
checkTouched.add(f.path); // verifier edits are an issue, not a task scope violation
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const got = entries || [];
|
|
541
|
+
const gaps = visualGaps(got, visualDeclared);
|
|
542
|
+
return [...got, ...notVerifiedEntries(gaps.missing, entries ? 'the verifier did not report it' : why)];
|
|
543
|
+
};
|
|
544
|
+
|
|
438
545
|
let askedForHandback = false;
|
|
439
|
-
let askedForVisual = false;
|
|
440
546
|
let phase = 'executing';
|
|
441
547
|
for (;;) {
|
|
442
548
|
setPhase(phase, phase === 'repairing' ? { attempt: repairs, of: inv.maxRepairs } : {});
|
|
@@ -454,10 +560,6 @@ async function execute(inv, opts = {}) {
|
|
|
454
560
|
if (inv.kind !== 'task') break;
|
|
455
561
|
|
|
456
562
|
const next = parseHandback(turn.response);
|
|
457
|
-
// A report-only visual follow-up that comes back without a valid block
|
|
458
|
-
// keeps the earlier (valid) handback instead of losing it.
|
|
459
|
-
const visualReport = phase === 'reporting' && askedForVisual;
|
|
460
|
-
if (visualReport && !next.handback && parsed.handback) break;
|
|
461
563
|
// A handback-only follow-up keeps the substantive report from before.
|
|
462
564
|
parsed = { ...next, report: next.report || parsed.report };
|
|
463
565
|
if (parsed.error && !askedForHandback && Date.now() < deadline) {
|
|
@@ -470,8 +572,7 @@ async function execute(inv, opts = {}) {
|
|
|
470
572
|
if (parsed.handback && parsed.handback.status === 'blocked') break;
|
|
471
573
|
if (!inv.checks.length && !visualDeclared.length) break;
|
|
472
574
|
|
|
473
|
-
|
|
474
|
-
if (inv.checks.length && !visualReport) {
|
|
575
|
+
if (inv.checks.length) {
|
|
475
576
|
setPhase('checking', { count: inv.checks.length });
|
|
476
577
|
const preChecks = before ? snapshot(root) : null;
|
|
477
578
|
checks = await runChecks(inv.checks, {
|
|
@@ -489,18 +590,15 @@ async function execute(inv, opts = {}) {
|
|
|
489
590
|
}
|
|
490
591
|
}
|
|
491
592
|
const failures = checks.filter((c) => !c.ok);
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
if (
|
|
593
|
+
// zcode, not the implementer, judges the visual criteria. Unmet ones
|
|
594
|
+
// share the repair budget with failing checks; a verifier that could
|
|
595
|
+
// not run (met:null) is reported, not repaired.
|
|
596
|
+
let visualUnmet = [];
|
|
597
|
+
if (visualDeclared.length && !failures.length && parsed.handback
|
|
497
598
|
&& !(signal && signal.aborted) && Date.now() < deadline) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
phase = 'reporting';
|
|
501
|
-
continue;
|
|
599
|
+
visualEntries = await verifyVisual(verifyRounds + 1);
|
|
600
|
+
visualUnmet = visualEntries.filter((v) => v.met === false);
|
|
502
601
|
}
|
|
503
|
-
const visualUnmet = unmetVisual(parsed.handback, visualDeclared);
|
|
504
602
|
if ((!failures.length && !visualUnmet.length) || repairs >= inv.maxRepairs) break;
|
|
505
603
|
if ((signal && signal.aborted) || Date.now() >= deadline) break;
|
|
506
604
|
repairs++;
|
|
@@ -520,6 +618,7 @@ async function execute(inv, opts = {}) {
|
|
|
520
618
|
}
|
|
521
619
|
} catch (err) {
|
|
522
620
|
error = err;
|
|
621
|
+
if (err && err.turn) failedTurns.push(err.turn);
|
|
523
622
|
} finally {
|
|
524
623
|
if (client) client.kill();
|
|
525
624
|
if (releasePort) releasePort();
|
|
@@ -591,11 +690,20 @@ async function execute(inv, opts = {}) {
|
|
|
591
690
|
preflight,
|
|
592
691
|
preflightWrote,
|
|
593
692
|
visualDeclared,
|
|
693
|
+
visual: visualEntries || [],
|
|
594
694
|
});
|
|
595
695
|
|
|
596
696
|
let errorOut = null;
|
|
597
697
|
if (status === 'failed') {
|
|
598
|
-
if (error
|
|
698
|
+
if (error && error.kind === 'timeout') {
|
|
699
|
+
// The work already on disk is kept; resuming continues from it.
|
|
700
|
+
const plan = /^plan:(\S+)/.exec(inv.label || record.label || '');
|
|
701
|
+
const cmd = plan ? `zcode plan resume ${plan[1]} "continue"` : `zcode resume ${id} "continue"`;
|
|
702
|
+
errorOut = {
|
|
703
|
+
kind: 'timeout',
|
|
704
|
+
message: `timed out after ${Math.round(inv.timeoutMs / 60000)} min; "${cmd}" picks up from the current files`,
|
|
705
|
+
};
|
|
706
|
+
} else if (error) errorOut = { kind: error.kind || 'error', message: scrub(error.message) };
|
|
599
707
|
else if (preflightWrote.length) errorOut = { kind: 'preflight_wrote', message: issues[0] };
|
|
600
708
|
else if (checks.some((c) => !c.ok)) errorOut = { kind: 'checks_failed', message: issues[0] };
|
|
601
709
|
} else if (status === 'cancelled') {
|
|
@@ -624,18 +732,23 @@ async function execute(inv, opts = {}) {
|
|
|
624
732
|
notVerified.push(`unit results are executor claims: ${handback.units.map((u) => `${u.name} ${u.status || '?'}`).join(', ')}; only the integrated result is verified`);
|
|
625
733
|
}
|
|
626
734
|
if (visualDeclared.length) {
|
|
627
|
-
const
|
|
628
|
-
notVerified.push(
|
|
735
|
+
const judged = visualEntries || [];
|
|
736
|
+
notVerified.push(visualEntries
|
|
737
|
+
? `visual criteria judged by an independent ${verifierModel} verifier from its own screenshots (${judged.filter((v) => v.met === true).length} of ${judged.length} met)`
|
|
738
|
+
: 'visual criteria were not judged: the independent verifier did not run');
|
|
629
739
|
}
|
|
630
740
|
}
|
|
631
741
|
if (before && !diff) notVerified.push('changed files could not be tracked in this workspace');
|
|
632
742
|
if (subagentNote) issues.push(subagentNote);
|
|
743
|
+
if (verifierEdits.size) issues.push(`verifier edited files: ${[...verifierEdits].join(', ')}`);
|
|
633
744
|
|
|
634
745
|
const last = turns[turns.length - 1] || null;
|
|
635
746
|
const response = last ? last.response : '';
|
|
636
747
|
const questions = [...(handback ? handback.questions : [])];
|
|
637
748
|
if (userInput) questions.push(userInput);
|
|
638
749
|
|
|
750
|
+
const usage = usageOf([...turns, ...vturns], failedTurns);
|
|
751
|
+
const usageTotal = addUsageTotal(record.usageTotal, usage);
|
|
639
752
|
const result = {
|
|
640
753
|
type: 'result',
|
|
641
754
|
schema: RESULT_SCHEMA,
|
|
@@ -660,7 +773,9 @@ async function execute(inv, opts = {}) {
|
|
|
660
773
|
touched,
|
|
661
774
|
...(preflight ? { preflight: true } : {}),
|
|
662
775
|
decisions: handback ? handback.decisions : [],
|
|
663
|
-
visual:
|
|
776
|
+
visual: visualEntries || [],
|
|
777
|
+
executorVisual: handback ? handback.visual : [],
|
|
778
|
+
...(visualDeclared.length ? { verifier: { model: verifierModel, rounds: verifyRounds } } : {}),
|
|
664
779
|
units: handback ? handback.units : [],
|
|
665
780
|
sessionEnv: sessionEnv
|
|
666
781
|
? { port: Number(sessionEnv.ZCODE_PORT), out: sessionEnv.ZCODE_OUT, tmp: sessionEnv.ZCODE_TMP }
|
|
@@ -689,7 +804,8 @@ async function execute(inv, opts = {}) {
|
|
|
689
804
|
effort: (last && last.engineReasoningLevel) || inv.effort,
|
|
690
805
|
effortRequested: inv.effort,
|
|
691
806
|
mode: session ? session.mode : null,
|
|
692
|
-
usage
|
|
807
|
+
usage,
|
|
808
|
+
usageTotal,
|
|
693
809
|
reasoningChars: turns.reduce((n, t) => n + t.reasoningChars, 0),
|
|
694
810
|
toolCalls: turns.some((t) => typeof t.toolCallCount === 'number')
|
|
695
811
|
? turns.reduce((n, t) => n + (t.toolCallCount || 0), 0)
|
|
@@ -714,7 +830,9 @@ async function execute(inv, opts = {}) {
|
|
|
714
830
|
state: 'finished',
|
|
715
831
|
phase: 'done',
|
|
716
832
|
status,
|
|
717
|
-
|
|
833
|
+
usageTotal,
|
|
834
|
+
phaseDetail: null,
|
|
835
|
+
activity: activity(),
|
|
718
836
|
outcome: {
|
|
719
837
|
status,
|
|
720
838
|
summary: result.summary,
|
package/lib/help.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Per-command help: `zcode <command> --help` prints the command's usage and
|
|
3
|
+
// only the flags that apply to it (each text <= 2000 bytes). The full text
|
|
4
|
+
// (bin/zcode.js USAGE) is for `zcode --help` with no command.
|
|
5
|
+
const SESSION = ` --id <id> name the session (existing id is never re-run: use resume)
|
|
6
|
+
--label <text> free-form label shown in status
|
|
7
|
+
--detach start in the background, print the id, exit 0
|
|
8
|
+
--cwd <dir> workspace (default: current dir)
|
|
9
|
+
--owns <path> repeatable: paths this session may change (default: whole workspace)
|
|
10
|
+
--allow-overlap start despite overlapping ownership`;
|
|
11
|
+
const EXEC = ` --model <id> glm-5.3 | glm-5.3-flash (default glm-5.3-flash)
|
|
12
|
+
--effort <lvl> low | high | max (default max)
|
|
13
|
+
--mcp <json|@f> add MCP servers (repeatable) --mcp-inherit --no-browser
|
|
14
|
+
--skills <dir> add skills (repeatable) --no-skills
|
|
15
|
+
--json NDJSON events; last line is the compact brief (--full: whole record)
|
|
16
|
+
--quiet no progress feed; with --json only the result line`;
|
|
17
|
+
|
|
18
|
+
const HELP = {
|
|
19
|
+
task: `usage: zcode task [options] <objective...>
|
|
20
|
+
Execute a task contract, verify it with zcode-run checks, hand back.
|
|
21
|
+
|
|
22
|
+
contract:
|
|
23
|
+
--task <file|-> JSON contract or markdown brief (- = stdin)
|
|
24
|
+
--accept <text> repeatable acceptance criterion
|
|
25
|
+
--check <command> repeatable shell check zcode runs after the work
|
|
26
|
+
--file <path> repeatable file to read first
|
|
27
|
+
--context <text|@f> repeatable context --decision <text> / --decisions <@f>
|
|
28
|
+
--touch <path> repeatable minimal edits allowed outside --owns
|
|
29
|
+
--unit "n: goal [owns a,b]" repeatable subagent unit
|
|
30
|
+
--visual <criterion> repeatable --viewport <WxH> --serve <cmd>
|
|
31
|
+
--preflight read-only: list open questions, no edits
|
|
32
|
+
--max-repairs <n> default 2 --check-timeout-ms <n> default 300000
|
|
33
|
+
session:
|
|
34
|
+
${SESSION}
|
|
35
|
+
--timeout-ms <n> whole invocation (default 7200000 = 2 h)
|
|
36
|
+
${EXEC}
|
|
37
|
+
Full reference: zcode --help`,
|
|
38
|
+
run: `usage: zcode [run] [options] <prompt...>
|
|
39
|
+
One-shot turn (prompt via stdin when piped).
|
|
40
|
+
--session <id> continue a session
|
|
41
|
+
--timeout-ms <n> default 600000
|
|
42
|
+
--thinking | --no-thinking mirror reasoning to stderr
|
|
43
|
+
${SESSION}
|
|
44
|
+
${EXEC}
|
|
45
|
+
Full reference: zcode --help`,
|
|
46
|
+
agent: `usage: zcode agent [options] <instructions...>
|
|
47
|
+
Turn that MUST dispatch a subagent.
|
|
48
|
+
--profile <name> subagent profile (default general-purpose)
|
|
49
|
+
--timeout-ms <n> default 600000
|
|
50
|
+
${SESSION}
|
|
51
|
+
${EXEC}
|
|
52
|
+
Full reference: zcode --help`,
|
|
53
|
+
resume: `usage: zcode resume <id> [message...]
|
|
54
|
+
Continue a session: answer questions, follow up, or retry after a timeout.
|
|
55
|
+
Pass "-" as the message to read it from stdin.
|
|
56
|
+
--timeout-ms <n> --max-repairs <n> --check-timeout-ms <n>
|
|
57
|
+
--model <id> --effort <lvl>
|
|
58
|
+
--json --full --quiet --include-reasoning
|
|
59
|
+
Start-time flags (--id, --owns, --cwd, ...) cannot change on resume.`,
|
|
60
|
+
status: `usage: zcode status [<id>] [--all] [--json]
|
|
61
|
+
One session in detail, or the 15 most recent (--all: every session).
|
|
62
|
+
--json machine-readable list or detail`,
|
|
63
|
+
wait: `usage: zcode wait <id...> [--any] [--json] [--full] [--timeout-ms n]
|
|
64
|
+
Block until sessions finish; print each handback.
|
|
65
|
+
--any return when the FIRST id finishes
|
|
66
|
+
--timeout-ms <n> one line per pending session, exit 124
|
|
67
|
+
--json briefs --full full records`,
|
|
68
|
+
watch: `usage: zcode watch [<id>… | --plan <name>] [--stall-min n]
|
|
69
|
+
Live view of sessions, no model tokens. One line each:
|
|
70
|
+
id state phase[ n/m] elapsed last: <tool> <age> ago req <n> sub <running>
|
|
71
|
+
STALLED? is appended when a running session has had no engine event for
|
|
72
|
+
--stall-min minutes (default 8). Nothing is done automatically.
|
|
73
|
+
<id…> the sessions to watch
|
|
74
|
+
--plan name every session of a plan
|
|
75
|
+
(no args) live sessions and those finished in the last 10 minutes
|
|
76
|
+
TTY: redraws every 2 s. Piped: a line is printed only when a session changes.
|
|
77
|
+
Exits 0 when every watched session has finished; Ctrl-C exits 0.`,
|
|
78
|
+
result: `usage: zcode result <id> [--json] [--full]
|
|
79
|
+
Print a finished session's handback without waiting (exit 7 if running).
|
|
80
|
+
--json compact brief --full the whole record`,
|
|
81
|
+
cancel: `usage: zcode cancel <id> [--json]
|
|
82
|
+
Stop a running session (SIGTERM, SIGKILL after 15 s). No-op when finished.`,
|
|
83
|
+
plan: `usage: zcode plan <subcommand>
|
|
84
|
+
zcode plan check <file> validate (ids, after, cycles, owns, effort)
|
|
85
|
+
zcode plan run <file> [--preflight] [--detach] [--timeout-ms n] [--id name] [--json]
|
|
86
|
+
zcode plan wait|status <name> [--timeout-ms n] [--json]
|
|
87
|
+
zcode plan resume <name> [message...] [--session <id>]
|
|
88
|
+
plan file: {cwd?, decisions?, context?, files?, model?, effort?, limits?,
|
|
89
|
+
sessions:[{id, after?:[ids], label?, ...task contract keys}]}
|
|
90
|
+
top-level decisions/context/files are prepended to every session; limits
|
|
91
|
+
({timeoutMs,maxRepairs,checkTimeoutMs}) default every session; "@file"
|
|
92
|
+
resolves next to the plan file, then against its cwd.
|
|
93
|
+
--preflight read-only preflight of every session (sessions with "after"
|
|
94
|
+
are told to assume their prerequisites)
|
|
95
|
+
--detach supervise in a background process
|
|
96
|
+
--id <name> plan name (default: file basename)
|
|
97
|
+
--cwd <dir> workspace override
|
|
98
|
+
Exit: 0 completed, 10 blocked, 8 partial/failed, 124 timeout.`,
|
|
99
|
+
login: `usage: zcode login
|
|
100
|
+
Read the API key from stdin and store it in the credentials file.`,
|
|
101
|
+
doctor: `usage: zcode doctor [--json]
|
|
102
|
+
Engine, isolation and key report.`,
|
|
103
|
+
skills: `usage: zcode skills [install|uninstall] [claude|codex]
|
|
104
|
+
Install the orchestrator skills (default: show status).`,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function commandHelp(command) {
|
|
108
|
+
return HELP[command] || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { commandHelp };
|