@0xgeorgemathew/zcode 0.4.4 → 0.5.1

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 CHANGED
@@ -18,8 +18,8 @@ The orchestrator keeps product decisions, design direction, and coordination. zc
18
18
  <tr><td><b>Checks run by zcode</b></td><td>zcode runs each <code>--check</code> after the work. A failing check sends its output back to the model for up to 2 repair turns.</td></tr>
19
19
  <tr><td><b>Status from evidence</b></td><td><code>completed</code> requires passing checks and no edits outside <code>--owns</code>. The model's own report is kept as a claim.</td></tr>
20
20
  <tr><td><b>Sessions</b></td><td>Stable ids, <code>--detach</code>, <code>status</code>, <code>wait</code>, <code>resume</code>, <code>cancel</code>. A session whose process died shows as interrupted and can be resumed.</td></tr>
21
- <tr><td><b>Parallel work</b></td><td>Two sessions with overlapping <code>--owns</code> in one repo are refused with exit 7.</td></tr>
22
- <tr><td><b>Output</b></td><td>One <code>zcode.result/1</code> JSON line with <code>--json</code>. No colour codes when stdout is not a terminal.</td></tr>
21
+ <tr><td><b>Parallel work</b></td><td>Two sessions with overlapping <code>--owns</code> in one repo are refused with exit 7. Every session gets its own port, output dir and tmp dir (<code>ZCODE_PORT</code>, <code>ZCODE_OUT</code>, <code>ZCODE_TMP</code>).</td></tr>
22
+ <tr><td><b>Output</b></td><td>With <code>--json</code>, one compact <code>zcode.brief/1</code> result line (<code>--full</code> for the whole <code>zcode.result/1</code> record). Timeouts and usage errors are one line. No colour codes when stdout is not a terminal.</td></tr>
23
23
  </table>
24
24
 
25
25
  &nbsp;
@@ -56,13 +56,13 @@ Every command runs on GLM-5.3-Flash at effort `max` unless you pass a flag.
56
56
  | setting | flag | values | default |
57
57
  |---|---|---|---|
58
58
  | model | `--model` | `glm-5.3-flash` (aliases `flash`, `5.3-flash`, `53f`), `glm-5.3` (aliases `5.3`, `53`) | `glm-5.3-flash` |
59
- | effort | `--effort` | `low`, `medium`, `high`, `xhigh`, `max` | `max` |
59
+ | effort | `--effort` | `low`, `high`, `max` (the engine's levels for both models; `zcode doctor` lists them) | `max` |
60
60
 
61
61
  ```sh
62
62
  zcode task "..." # GLM-5.3-Flash, effort max
63
63
  zcode task --effort high "..." # Flash with less reasoning
64
64
  zcode task --model glm-5.3 "..." # GLM-5.3, effort max
65
- zcode task --model glm-5.3 --effort medium "..." # GLM-5.3, effort medium
65
+ zcode task --model glm-5.3 --effort high "..." # GLM-5.3, effort high
66
66
  ```
67
67
 
68
68
  A task contract can set `"model"` and `"effort"`. Flags override the contract. A resumed session keeps its original model and effort unless you pass the flags again.
@@ -96,7 +96,7 @@ not verified
96
96
  - prose acceptance criteria are confirmed only by the executor's own report
97
97
  ```
98
98
 
99
- That block is the format of the summary printed to stdout. The live feed (session header, phases, tools, subagents, check results) goes to stderr. With `--json` you get the same data as one machine-readable result line.
99
+ That block is the format of the summary printed to stdout. The live feed (session header, phases, tools, subagents, check results) goes to stderr. With `--json` you get the compact brief as one machine-readable result line (see Machine interface).
100
100
 
101
101
  For small work, the objective alone is enough: `zcode task "Fix the typo in the README heading"`.
102
102
 
@@ -109,7 +109,7 @@ Flags cover the common fields. For richer handoffs, pass `--task contract.json`,
109
109
  "objective": "Implement page 2 (Team) per the design spec",
110
110
  "scope": ["Team page route and its components"],
111
111
  "constraints": ["No new dependencies", "Keep existing routing untouched"],
112
- "design": ["Grid of 3 columns ≥1024px, 1 column below", "Use tokens from src/theme.ts only"],
112
+ "decisions": ["Grid of 3 columns ≥1024px, 1 column below", "Use tokens from src/theme.ts only"],
113
113
  "acceptance": ["Every member card shows name, role, avatar"],
114
114
  "checks": ["npm run build", "npm test -- team"],
115
115
  "files": ["src/theme.ts", "src/pages/home/index.tsx"],
@@ -122,7 +122,15 @@ Flags cover the common fields. For richer handoffs, pass `--task contract.json`,
122
122
  }
123
123
  ```
124
124
 
125
- `design` entries are presented to the model as binding decisions it must implement, not revisit. If the model hits a consequential product or design question the contract does not answer, it reports `blocked` with its questions instead of guessing.
125
+ `decisions` (flags `--decision "…"`, repeatable, and `--decisions @file`; `design` is an alias) are presented to the model as binding decisions it must implement, not revisit. `@file` entries inside contract `decisions`/`context` lists are inlined (relative to the contract file). If something user-visible or behavioural is left open, the executor does not guess: when it blocks the work it reports `blocked` with a structured question `{"q", "options": [...], "recommend"}` (plain strings still work); when it does not block, it takes the most conservative choice and lists it in the handback `decisions: [{what, why}]`, which the brief carries so the orchestrator can overrule it.
126
+
127
+ A file passed with `--file` and also inlined with `--context @file` reaches the executor once (as context); identical context and acceptance entries are deduped.
128
+
129
+ **Preflight.** `--preflight` runs a read-only turn over the same contract: the executor lists every open question it would otherwise guess and makes no edits; zcode runs no checks. The result is `blocked` with `questions`, or `completed` with summary `no open questions`; any changed file fails it (`preflight_wrote`). The session is recorded as a preflight; `zcode resume <id> "<answers>"` then switches it to execution: the answers plus the full task protocol go to the executor, and checks and visual verification run as usual.
130
+
131
+ **Units.** `--unit "name: objective [owns a,b]"` (repeatable; contract `units: [{name, objective, owns?}]`) makes the executor dispatch each unit to its own subagent inside this one session, in parallel where independent, then integrate and verify the whole. Unit `owns` must lie within the session's `--owns` and be disjoint from each other (exit 2 otherwise). The handback `units: [{name, status, note}]` is listed under `notVerified` as executor claims; zcode's checks still verify the integrated result.
132
+
133
+ **Visual verification.** `--visual "criterion"` (repeatable; contract `visual`), `--viewport WxH` (repeatable, default `390x844` and `1440x900`; contract `viewports`) and `--serve "cmd"` (contract `serve`; must honour `$ZCODE_PORT`, default: the executor reads `package.json`) have the executor serve the app on `$ZCODE_PORT`, capture Playwright screenshots into `$ZCODE_OUT/shots` at each viewport, look at them, fix and re-capture, and report `visual: [{criterion, viewport, met, note, shot}]`. zcode runs no pixel check: results are executor judgement (`notVerified`). Unmet criteria spend the repair budget like failing checks (one combined repair turn when both occur); still unmet afterwards, the result is `partial` with the issue `visual criteria unmet: …`. Without `--visual` nothing changes.
126
134
 
127
135
  ### Status: what a result means
128
136
 
@@ -149,6 +157,8 @@ zcode task --detach --id page-2 --owns src/pages/team/ --check "npm run build" "
149
157
  zcode status # all sessions: id, state/status, kind, updated, label
150
158
  zcode status page-1 --json # one session: state, phase, activity, recent events, result
151
159
  zcode wait page-1 page-2 # block until both finish; print each handback
160
+ zcode wait page-1 page-2 --any # return as soon as the first one finishes
161
+ zcode result page-1 # a finished session's handback, without waiting
152
162
  zcode resume page-2 "Use the 2-column variant below 1024px." # answer / follow up / retry
153
163
  zcode cancel page-1 # stop the turn and any running check; recorded as cancelled
154
164
  ```
@@ -166,6 +176,34 @@ zcode cancel page-1 # stop the turn and any running check; recorded as
166
176
  - After the work, zcode diffs the workspace and reports each changed file. Changes outside the session's ownership are listed in `scopeViolations`, and for task sessions they make the result `partial`. Files that fall in a concurrent session's ownership are attributed to that session, not counted against this one.
167
177
  - For overlapping or risky parallel work, give each session its own git worktree via `--cwd`.
168
178
  - `ZAI_CODE_MAX_ACTIVE` (default 8) is a soft machine-wide cap. Sessions beyond it wait in `queued`.
179
+ - **Session environment.** Every session's executor and every check run with `ZCODE_SESSION_ID`, `ZCODE_PORT` (a free TCP port from 41000-48999, reserved per session and unique among live sessions, also recorded in the session), `ZCODE_OUT` (`<cwd>/.zcode/out/<id>`, created) and `ZCODE_TMP` (a per-session scratch dir). The task prompt tells the executor to use them for dev and preview servers, build output dirs and test artifacts, so parallel sessions cannot collide on a port or a `dist/`. Files under `.zcode/` never count as changes or scope violations; add `.zcode/` to the `.gitignore` of repos you run zcode in.
180
+ - **`--touch <path|glob>`** (repeatable; contract key `touch`) allows minimal edits outside `--owns` without declaring ownership. Touched files are listed in `touched` and are not scope violations, so one stray line in a shared stylesheet no longer blocks a session. Starting is refused with exit 7 if a running session owns a matching path; a touch reserves nothing, so it never blocks another session's `--owns`.
181
+
182
+ ### Plans: several sessions with dependencies
183
+
184
+ `zcode plan` runs a set of task sessions as one unit, in dependency order, and returns a single summary.
185
+
186
+ ```json
187
+ { "decisions": ["@decisions.md"], "context": ["@brief.md"], "effort": "high",
188
+ "sessions": [
189
+ { "id": "tokens", "objective": "Design tokens", "owns": ["src/tokens/**"], "checks": ["npm run lint"] },
190
+ { "id": "home", "objective": "Home page", "after": ["tokens"], "owns": ["src/home/**"] },
191
+ { "id": "team", "objective": "Team page", "after": ["tokens"], "owns": ["src/team/**"] }
192
+ ] }
193
+ ```
194
+
195
+ Optional top-level keys: `cwd` (relative to the plan file), `decisions`, `context`, `files` (prepended to every session; `@file` entries are read relative to the plan file), `model`, `effort`. Each session has an `id` (its zcode session id), optional `after` (ids) and any task contract keys.
196
+
197
+ ```sh
198
+ zcode plan check plan.json # validate: exit 2, one line per problem
199
+ zcode plan run plan.json --preflight # read-only questions per session (<id>.pre); exit 10 if any
200
+ zcode plan run plan.json --detach # supervise in the background
201
+ zcode plan wait plan --json --timeout-ms 600000 # one line; exit 124 on timeout
202
+ zcode plan status plan # one line per session
203
+ zcode plan resume plan "answers…" # resume failed/partial/blocked sessions, relaunch skipped ones
204
+ ```
205
+
206
+ A session starts once everything it is `after` has completed; sessions that can run together must own disjoint paths (`plan check` enforces it). A session that ends without completing marks its downstream `skipped` while the others continue. The summary is `{"type":"plan","name","status","sessions":[{"id","status"}],"briefs":[…]}` with a brief for each finished session that did not complete. Exit codes: 0 completed, 10 blocked, 8 partial or failed, 124 timeout. Plan status is derived from the sessions, so it stays accurate even if the supervisor process dies; `plan resume` restarts it. The plan name is the file's basename (`--id` overrides); ids that already exist as sessions are refused, so re-run through `plan resume`.
169
207
 
170
208
  &nbsp;
171
209
 
@@ -189,6 +227,18 @@ Inside `zcode task`, subagents are optional. The executor decides when to use th
189
227
 
190
228
  `--json`: stdout carries NDJSON events, then exactly one final `{"type":"result",…}` line. With `--quiet`, the result line is the only output. For task sessions, text/reasoning deltas are left out of the events unless you pass `--stream`, so an orchestrator reading the output pays only for lifecycle events.
191
229
 
230
+ The final line is the compact **brief** (`zcode.brief/1`), sized for an orchestrator that spends tokens on decisions. Keys with empty values are omitted:
231
+
232
+ ```json
233
+ {"type":"result","schema":"zcode.brief/1","status":"failed","ok":false,"id":"page-1","label":"home",
234
+ "summary":"…(≤600 chars)…","files":["src/pages/home/index.tsx"],
235
+ "checks":[{"command":"npm run lint","ok":true},{"command":"npm test","ok":false,"exitCode":1,"tail":"…last 1500 chars…"}],
236
+ "questions":[],"issues":["…"],"unresolved":[],"scopeViolations":[],"error":{"kind":"checks_failed"},
237
+ "result":"/…/sessions/page-1/result.json"}
238
+ ```
239
+
240
+ `run` and `agent` briefs also carry `response`, because the text answer is the product there. Passing checks are reduced to `{command, ok:true}`; a failing check carries `exitCode`, `timedOut` and the last 1500 characters of its output as `tail`. `result` is the path to the full record: `result.json` on disk is always the full `zcode.result/1` record, and `--full` prints that same record instead of the brief (on `task`/`run`/`agent`/`resume`/`wait`/`result`):
241
+
192
242
  ```json
193
243
  {"type":"result","schema":"zcode.result/1","ok":true,"status":"completed","id":"page-1","kind":"task",
194
244
  "summary":"…","response":"…executor report…","issues":[],"questions":[],"unresolved":[],"notVerified":[],
@@ -201,7 +251,7 @@ Inside `zcode task`, subagents are optional. The executor decides when to use th
201
251
  "toolCalls":14,"durationMs":142301,"files":{"session":"…/sessions/page-1","result":"…/result.json","events":"…/events.ndjson"}}
202
252
  ```
203
253
 
204
- `model` and `effort` are engine-reported (from the engine's `model_request_started` event), not echoes. `sessionId` is the engine's session id; `id` is the zcode session id you pass to `status`, `wait`, `resume`, and `cancel`. A run that fails still ends with a parseable result line.
254
+ `model` and `effort` are engine-reported (from the engine's `model_request_started` event), not echoes. `sessionId` is the engine's session id; `id` is the zcode session id you pass to `status`, `wait`, `result`, `resume`, and `cancel`. A run that fails still ends with a parseable result line.
205
255
 
206
256
  | event | meaning |
207
257
  |---|---|
@@ -213,7 +263,7 @@ Inside `zcode task`, subagents are optional. The executor decides when to use th
213
263
  | `{"type":"mcp","name","status"}` · `{"type":"retry",…}` · `{"type":"model_request_started",…}` | MCP settling, start-up retry, engine model evidence |
214
264
  | `{"type":"stream","kind":"text_delta\|reasoning_delta","delta"}` | live text (run/agent; task with `--stream`) |
215
265
 
216
- `zcode status <id> --json` returns `{"type":"status", state, status, phase, pid, live, activity, recent:[…], result?}`. `zcode wait --json` prints one result line per session as each finishes.
266
+ `zcode status <id> --json` returns `{"type":"status", state, status, phase, pid, live, activity, recent:[…], result?}`. `zcode wait --json` prints one brief per session as each finishes (`--full`: full records; `--any`: stop at the first). When `--timeout-ms` elapses first it prints exactly one line per session still running, `{"type":"pending","id","state","phase","elapsedMs"}` (human mode: `<id> <state> <phase> 12m`), and exits 124. `zcode result <id> [--json] [--full]` prints a finished session's handback without waiting (exit 7 if it is still running). `zcode status` without an id is one line per session. Usage errors are always a single line (`zcode: unknown flag (did you mean …?)` for a mistyped option), never the help text.
217
267
 
218
268
  &nbsp;
219
269
 
@@ -225,24 +275,30 @@ Inside `zcode task`, subagents are optional. The executor decides when to use th
225
275
  | `--detach` | run, task, agent, resume | off | background worker; prints the id |
226
276
  | `--cwd` | run, task, agent | current dir | the workspace and blast radius |
227
277
  | `--owns` | run, task, agent | whole workspace | repeatable path or glob |
278
+ | `--touch` | task | | repeatable; minimal edits outside `--owns`, reported as `touched` |
228
279
  | `--allow-overlap` | run, task, agent | off | start despite overlapping ownership |
229
280
  | `--task` | task | | JSON contract, markdown brief, or `-` |
230
- | `--accept`, `--check`, `--file`, `--context` | task | | repeatable; `--context @file` reads a file |
281
+ | `--accept`, `--check`, `--file`, `--context` | task | | repeatable; `--context @file` reads a file (a `--file` also inlined this way is listed once) |
282
+ | `--decision`, `--decisions` | task | | repeatable binding decisions; `--decisions @file` reads a file |
283
+ | `--preflight` | task | off | read-only turn that returns the open questions; `resume <id> "<answers>"` then executes |
284
+ | `--unit` | task | | repeatable `"name: objective [owns a,b]"`; one subagent per unit |
285
+ | `--visual`, `--viewport`, `--serve` | task | viewports `390x844`, `1440x900` | executor screenshots the app with Playwright and judges each criterion |
231
286
  | `--max-repairs` | task, resume | `2` | repair turns after failing checks |
232
287
  | `--check-timeout-ms` | task, resume | `300000` | per check; checks get `CI=1` when unset |
233
288
  | `--model` | all turns | `glm-5.3-flash` | `glm-5.3` plus aliases `5.3`, `53`, `flash`, `5.3-flash`, `53f` |
234
- | `--effort` | all turns | `max` | `low`, `medium`, `high`, `xhigh`, `max` |
289
+ | `--effort` | all turns | `max` | the model's levels: `low`, `high`, `max` |
235
290
  | `--timeout-ms` | all turns; wait | run 600000, task 1800000 | the whole invocation incl. checks and repairs |
236
291
  | `--session` | run | | 0.3 compat: continue by zcode id or engine `sess_` id |
237
292
  | `--profile` | agent | `general-purpose` | subagent profile |
238
293
  | `--mcp` / `--mcp-inherit` / `--mcp-list` | run, task, agent | hermetic, plus the desktop's `playwright` | MCP servers (inline JSON or `@file`) |
239
294
  | `--no-browser` | run, task, agent | off | Drop the desktop's `playwright` MCP server |
240
295
  | `--skills` / `--no-skills` | run, task, agent | desktop skills | add skills; or run with none |
241
- | `--json`, `--quiet` | all | | see Machine interface |
296
+ | `--json`, `--full`, `--quiet` | all | | see Machine interface |
297
+ | `--any` | wait | off | return when the first listed session finishes |
242
298
  | `--thinking` / `--no-thinking` | turns | run on, task off | mirror reasoning to stderr |
243
299
  | `--stream`, `--include-reasoning` | turns | off | deltas in task events; full reasoning in the result |
244
300
 
245
- Only `glm-5.3` and `glm-5.3-flash` are accepted; anything else exits 2 before any spawn. Effort levels `low`, `high`, and `max` have been observed live on Flash. `medium` and `xhigh` may be rejected server-side.
301
+ Only `glm-5.3` and `glm-5.3-flash` are accepted; anything else exits 2 before any spawn. The effort levels each model accepts are read from the engine's builtin provider config (`zcode doctor --json` lists them as `models`; a built-in table is the fallback). An unsupported model and effort pair, whether from `--effort`, the task contract, or a resumed session's stored setting, exits 2 with one line naming the supported values, before any engine is spawned.
246
302
 
247
303
  ## Exit codes
248
304
 
package/bin/zcode.js CHANGED
@@ -10,8 +10,16 @@ const USAGE = `usage:
10
10
  zcode agent [options] <instructions…> turn that MUST dispatch a subagent
11
11
  zcode resume <id> [message…] continue a session (answer, follow-up, retry)
12
12
  zcode status [<id>] [--json] one session in detail, or all sessions
13
- zcode wait <id…> [--json] block until sessions finish; print handbacks
13
+ zcode wait <id…> [--any] [--json] block until sessions finish; print handbacks
14
+ zcode result <id> [--full] [--json] print a finished session's handback (brief by default)
14
15
  zcode cancel <id> [--json] stop a running session
16
+ zcode plan check <file> validate a plan file (ids, after, cycles, owns, effort)
17
+ zcode plan run <file> [--preflight] [--detach] [--timeout-ms n] [--id name] [--json]
18
+ run a DAG of task sessions (see "plan" below)
19
+ zcode plan wait|status <name> [--timeout-ms n] [--json]
20
+ one summary line: {type:"plan",name,status,sessions,briefs}
21
+ zcode plan resume <name> [message…] [--session <id>]
22
+ resume failed/partial/blocked sessions, restart supervision
15
23
  zcode login read API key from stdin → credentials
16
24
  zcode doctor [--json] engine + isolation + key report
17
25
  zcode skills [install|uninstall] [claude|codex]
@@ -32,14 +40,47 @@ task contract (task only):
32
40
  --task <file|-> JSON contract or markdown brief (- = stdin)
33
41
  --accept <text> repeatable acceptance criterion
34
42
  --check <command> repeatable shell check zcode runs after the work
35
- --file <path> repeatable relevant file to read first
36
- --context <text|@f> repeatable extra context (design decisions, references)
43
+ --touch <path|glob> repeatable: minimal edits allowed outside --owns; reported
44
+ as "touched", not a scope violation. Refused (exit 7) if a
45
+ running session owns a matching path; reserves nothing
46
+ --file <path> repeatable relevant file to read first (a file also given
47
+ as --context @file is inlined once, not listed twice)
48
+ --context <text|@f> repeatable extra context (references, background)
49
+ --decision <text> repeatable binding decision the orchestrator made (the
50
+ executor implements it exactly and must not guess others)
51
+ --decisions <@f|text> binding decisions from a file (same list as --decision)
52
+ --preflight read-only turn: the executor lists the open questions it
53
+ would otherwise guess (blocked + questions, or completed
54
+ "no open questions"); no edits, no checks. Then
55
+ zcode resume <id> "<answers>" executes the task
56
+ --unit "n: goal [owns a,b]" repeatable: one subagent per unit inside this
57
+ session; owns must lie within --owns and be disjoint
58
+ --visual <criterion> repeatable: the executor serves the app on $ZCODE_PORT,
59
+ screenshots it with Playwright and judges each criterion
60
+ --viewport <WxH> repeatable viewport for --visual (default 390x844, 1440x900)
61
+ --serve <cmd> command that serves the app for --visual (honours $ZCODE_PORT)
37
62
  --max-repairs <n> repair turns after failing checks (default 2)
38
63
  --check-timeout-ms <n> per-check timeout (default 300000)
39
64
 
65
+ plan (zcode plan run <plan.json>):
66
+ plan.json {cwd?, decisions?, context?, files?, model?, effort?,
67
+ sessions:[{id, after?:[ids], ...task contract keys}]}; the
68
+ top-level decisions/context/files are prepended to every
69
+ session. A session starts when everything it is "after" has
70
+ completed; sessions that can run together must own disjoint
71
+ paths. A session ending non-completed marks its downstream
72
+ "skipped"; the others continue
73
+ --preflight plan run: read-only preflight of every session ("<id>.pre"),
74
+ prints the questions as one brief per session (exit 10 if any)
75
+ --detach plan run/resume: supervise in a background process
76
+ --id <name> plan name (default: the file's basename)
77
+ --session <id> plan resume: only this session
78
+
40
79
  execution:
41
80
  --model <id> glm-5.3 | glm-5.3-flash (+aliases). default: glm-5.3-flash
42
- --effort <level> low | medium | high | xhigh | max. default: max
81
+ --effort <level> low | high | max for both models (zcode doctor lists the
82
+ engine's levels per model); an unsupported level exits 2
83
+ before any spawn. default: max
43
84
  --timeout-ms <n> whole invocation incl. checks/repairs (default 600000;
44
85
  task default 1800000)
45
86
  --profile <name> subagent profile (agent command; default general-purpose)
@@ -51,8 +92,13 @@ execution:
51
92
  --no-skills empty skill set
52
93
 
53
94
  output:
54
- --json NDJSON events on stdout; final line {"type":"result",…}
55
- --quiet no progress: no stderr feed; with --json only the result line
95
+ --json NDJSON events on stdout; final line is the compact brief
96
+ {"type":"result","schema":"zcode.brief/1",…}
97
+ --full with --json (and result/wait): the full record instead
98
+ (schema zcode.result/1; result.json on disk is always full)
99
+ --any wait: return as soon as the FIRST id finishes
100
+ --quiet no progress: no stderr feed; with --json only the result line;
101
+ usage errors are always a single line
56
102
  --thinking / --no-thinking mirror reasoning to stderr (run: on, task: off)
57
103
  --stream task --json: include text/reasoning deltas in the events
58
104
  --include-reasoning full reasoning text in the result
@@ -97,9 +143,10 @@ function parseMcpSpec(value) {
97
143
  return parsed;
98
144
  }
99
145
 
100
- const COMMANDS = new Set(['run', 'task', 'agent', 'resume', 'status', 'list', 'wait', 'cancel', 'login', 'doctor', 'skills', '__exec']);
101
- const TASK_ONLY = new Set(['task', 'accept', 'check', 'file', 'context', 'maxRepairs', 'checkTimeoutMs']);
102
- const START_ONLY = new Set(['id', 'label', 'owns', 'allowOverlap', 'mcpSpecs', 'mcpInherit', 'noBrowser', 'skillsDirs', 'noSkills', 'profile', 'cwd']);
146
+ const COMMANDS = new Set(['run', 'task', 'agent', 'resume', 'status', 'list', 'wait', 'cancel', 'login', 'doctor', 'skills', 'result', 'plan', '__plan', '__exec']);
147
+ const TASK_ONLY = new Set(['task', 'accept', 'check', 'file', 'context', 'touch', 'maxRepairs', 'checkTimeoutMs', 'decisions', 'units', 'visual', 'viewports', 'serve', 'preflight']);
148
+ const FULL_COMMANDS = new Set(['run', 'task', 'agent', 'resume', 'wait', 'result', '__exec']);
149
+ const START_ONLY = new Set(['id', 'label', 'owns', 'touch', 'preflight', 'allowOverlap', 'mcpSpecs', 'mcpInherit', 'noBrowser', 'skillsDirs', 'noSkills', 'profile', 'cwd']);
103
150
 
104
151
  function parseArgv(argv) {
105
152
  const out = {
@@ -125,6 +172,13 @@ function parseArgv(argv) {
125
172
  file: [],
126
173
  context: [],
127
174
  owns: [],
175
+ touch: [],
176
+ decisions: [],
177
+ units: [],
178
+ visual: [],
179
+ viewports: [],
180
+ serve: undefined,
181
+ preflight: false,
128
182
  allowOverlap: false,
129
183
  maxRepairs: undefined,
130
184
  checkTimeoutMs: undefined,
@@ -138,17 +192,21 @@ function parseArgv(argv) {
138
192
  quiet: false,
139
193
  thinking: undefined,
140
194
  stream: false,
195
+ full: false,
196
+ any: false,
141
197
  },
142
198
  };
143
199
  const valueFlags = new Map([
144
200
  ['--model', 'model'], ['--effort', 'effort'], ['--cwd', 'cwd'], ['--profile', 'profile'],
145
201
  ['--timeout-ms', 'timeoutMs'], ['-p', 'prompt'], ['--prompt', 'prompt'], ['--session', 'session'],
146
202
  ['--id', 'id'], ['--label', 'label'], ['--task', 'task'], ['--max-repairs', 'maxRepairs'],
147
- ['--check-timeout-ms', 'checkTimeoutMs'],
203
+ ['--check-timeout-ms', 'checkTimeoutMs'], ['--serve', 'serve'],
148
204
  ]);
149
205
  const listFlags = new Map([
150
206
  ['--mcp', 'mcpSpecs'], ['--skills', 'skillsDirs'], ['--accept', 'accept'], ['--check', 'check'],
151
- ['--file', 'file'], ['--context', 'context'], ['--owns', 'owns'],
207
+ ['--file', 'file'], ['--context', 'context'], ['--owns', 'owns'], ['--touch', 'touch'],
208
+ ['--decision', 'decisions'], ['--decisions', 'decisions'], ['--unit', 'units'], ['--visual', 'visual'],
209
+ ['--viewport', 'viewports'],
152
210
  ]);
153
211
  const boolFlags = new Map([
154
212
  ['--json', ['json', true]], ['--fresh', ['fresh', true]], ['--mcp-inherit', ['mcpInherit', true]],
@@ -156,6 +214,7 @@ function parseArgv(argv) {
156
214
  ['--include-reasoning', ['includeReasoning', true]], ['--quiet', ['quiet', true]],
157
215
  ['--detach', ['detach', true]], ['--allow-overlap', ['allowOverlap', true]],
158
216
  ['--thinking', ['thinking', true]], ['--no-thinking', ['thinking', false]], ['--stream', ['stream', true]],
217
+ ['--full', ['full', true]], ['--any', ['any', true]], ['--preflight', ['preflight', true]],
159
218
  ]);
160
219
  const numeric = (flag, value, { min = 1 } = {}) => {
161
220
  const n = Number(value);
@@ -168,7 +227,7 @@ function parseArgv(argv) {
168
227
  if (valueFlags.has(arg) || listFlags.has(arg)) {
169
228
  const key = valueFlags.get(arg) || listFlags.get(arg);
170
229
  const value = argv[++i];
171
- if (value === undefined) fail(2, `option ${arg} requires a value.\n${USAGE}`);
230
+ if (value === undefined) fail(2, `option ${arg} requires a value.`);
172
231
  out.given.add(key);
173
232
  if (key === 'timeoutMs' || key === 'checkTimeoutMs') out.flags[key] = numeric(arg, value);
174
233
  else if (key === 'maxRepairs') out.flags[key] = numeric(arg, value, { min: 0 });
@@ -176,8 +235,6 @@ function parseArgv(argv) {
176
235
  else if (key === 'skillsDirs') {
177
236
  if (!isDirectory(value)) fail(2, `--skills requires an existing directory, got "${value}".`);
178
237
  out.flags.skillsDirs.push(value);
179
- } else if (key === 'context') {
180
- out.flags.context.push(value.startsWith('@') ? readFileOrFail(value.slice(1), '--context') : value);
181
238
  } else if (listFlags.has(arg)) out.flags[key].push(value);
182
239
  else if (!value.trim()) fail(2, `${arg} requires a non-empty value.`);
183
240
  else out.flags[key] = value.trim();
@@ -201,7 +258,10 @@ function parseArgv(argv) {
201
258
  out.prompts.push(...argv.slice(i + 1));
202
259
  break;
203
260
  }
204
- if (arg.startsWith('--') && arg.length > 2) fail(2, `unknown option "${arg}".\n${USAGE}`);
261
+ if (arg.startsWith('--') && arg.length > 2) {
262
+ const hint = suggestFlag(arg, [...valueFlags.keys(), ...listFlags.keys(), ...boolFlags.keys()]);
263
+ fail(2, `unknown flag ${arg} (${hint ? `did you mean ${hint}?` : 'see zcode --help'})`);
264
+ }
205
265
  if (out.prompts.length === 0 && out.command === 'run' && !out.commandSet && COMMANDS.has(arg)) {
206
266
  out.command = arg === 'list' ? 'status' : arg;
207
267
  out.commandSet = true;
@@ -214,17 +274,25 @@ function parseArgv(argv) {
214
274
  if (out.flags.prompt !== undefined) out.prompts.unshift(out.flags.prompt);
215
275
  if (cmd !== 'task') {
216
276
  for (const key of TASK_ONLY) {
277
+ if (cmd === 'plan' && key === 'preflight') continue;
217
278
  if (out.given.has(key) && !(cmd === 'resume' && (key === 'maxRepairs' || key === 'checkTimeoutMs'))) {
218
279
  fail(2, `--${flagName(key)} is only valid with "zcode task".`);
219
280
  }
220
281
  }
221
282
  }
283
+ if (cmd === 'plan' || cmd === '__plan') {
284
+ for (const key of ['label', 'owns', 'touch', 'allowOverlap', 'model', 'effort', 'profile']) {
285
+ if (out.given.has(key)) fail(2, `--${flagName(key)} is not valid with "zcode plan"; set it in the plan file.`);
286
+ }
287
+ }
222
288
  if (cmd === 'resume' || cmd === '__exec') {
223
289
  for (const key of START_ONLY) {
224
290
  if (out.given.has(key)) fail(2, `--${flagName(key)} cannot change on resume; it is fixed when the session starts.`);
225
291
  }
226
292
  }
227
- if (out.given.has('session') && cmd !== 'run') fail(2, '--session is only valid with "zcode run"; use "zcode resume <id>".');
293
+ if (out.given.has('any') && cmd !== 'wait') fail(2, '--any is only valid with "zcode wait".');
294
+ if (out.given.has('full') && !FULL_COMMANDS.has(cmd)) fail(2, `--full is not valid with "zcode ${cmd}".`);
295
+ if (out.given.has('session') && cmd !== 'run' && cmd !== 'plan') fail(2, '--session is only valid with "zcode run"; use "zcode resume <id>".');
228
296
  if (out.given.has('id') && !require('../lib/store.js').validId(out.flags.id)) {
229
297
  fail(2, `--id "${out.flags.id}" must be 1-64 chars of letters, digits, ".", "_" or "-", starting with a letter or digit.`);
230
298
  }
@@ -239,8 +307,34 @@ function parseArgv(argv) {
239
307
  return out;
240
308
  }
241
309
 
310
+ // Closest known flag by edit distance (<= 2), or null.
311
+ function suggestFlag(arg, known) {
312
+ const dist = (a, b) => {
313
+ const row = Array.from({ length: b.length + 1 }, (_, j) => j);
314
+ for (let i = 1; i <= a.length; i++) {
315
+ let prev = row[0];
316
+ row[0] = i;
317
+ for (let j = 1; j <= b.length; j++) {
318
+ const tmp = row[j];
319
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
320
+ prev = tmp;
321
+ }
322
+ }
323
+ return row[b.length];
324
+ };
325
+ let best = null;
326
+ let bestD = Infinity;
327
+ for (const k of known) {
328
+ if (!k.startsWith('--')) continue;
329
+ const d = dist(arg, k);
330
+ if (d < bestD) { best = k; bestD = d; }
331
+ }
332
+ return best && bestD <= 2 ? best : null;
333
+ }
334
+
242
335
  function flagName(key) {
243
- return key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`).replace(/^mcp-specs$/, 'mcp').replace(/^skills-dirs$/, 'skills');
336
+ return key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`).replace(/^mcp-specs$/, 'mcp').replace(/^skills-dirs$/, 'skills')
337
+ .replace(/^(unit|viewport)s$/, '$1');
244
338
  }
245
339
 
246
340
  function isDirectory(p) {
@@ -261,8 +355,12 @@ async function main() {
261
355
  case 'resume':
262
356
  case '__exec':
263
357
  return require('../lib/commands/exec.js')(parsed);
358
+ case 'plan':
359
+ case '__plan':
360
+ return require('../lib/commands/plan.js')(parsed);
264
361
  case 'status':
265
362
  case 'wait':
363
+ case 'result':
266
364
  case 'cancel':
267
365
  return require('../lib/commands/sessions.js')(parsed);
268
366
  case 'login':
@@ -272,10 +370,13 @@ async function main() {
272
370
  case 'skills':
273
371
  return require('../lib/commands/skills.js')(parsed);
274
372
  default:
275
- return fail(2, `unknown command "${parsed.command}".\n${USAGE}`);
373
+ return fail(2, `unknown command "${parsed.command}" (see zcode --help).`);
276
374
  }
277
375
  }
278
376
 
377
+ // Exported before main() runs: the plan supervisor reuses parseArgv.
378
+ module.exports = { parseArgv, USAGE };
379
+
279
380
  if (require.main === module) {
280
381
  main().catch((err) => {
281
382
  process.stderr.write(`zcode: internal error: ${err && err.stack ? err.stack : err}\n`);
@@ -283,4 +384,3 @@ if (require.main === module) {
283
384
  });
284
385
  }
285
386
 
286
- module.exports = { parseArgv, USAGE };
package/lib/bootstrap.js CHANGED
@@ -55,11 +55,11 @@ function resolveRuntime(env = process.env) {
55
55
  };
56
56
  }
57
57
 
58
- function makeClient(runtime, { home, cwd }) {
58
+ function makeClient(runtime, { home, cwd, sessionEnv = {} }) {
59
59
  return new AppServerClient({
60
60
  nodeBin: runtime.nodeBin,
61
61
  enginePath: runtime.enginePath,
62
- env: engineEnv({ home, builtinProviderConfig: runtime.builtinConfig }),
62
+ env: engineEnv({ home, builtinProviderConfig: runtime.builtinConfig, sessionEnv }),
63
63
  cwd,
64
64
  scrub: runtime.scrub,
65
65
  });
package/lib/brief.js ADDED
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+ // The compact handback: what an orchestrator reads by default (`--json`,
3
+ // `zcode result`, `zcode wait --json`). A projection of the full result
4
+ // record (schema zcode.result/1, still what result.json holds on disk and
5
+ // what `--full` prints): decision-relevant fields only, empty keys omitted,
6
+ // long text bounded, passing checks reduced to {command, ok:true}, and a
7
+ // pointer (`result`) to the full record for anything else.
8
+ const BRIEF_SCHEMA = 'zcode.brief/1';
9
+ const SUMMARY_MAX = 600;
10
+ const ERROR_MAX = 500;
11
+ const TAIL_MAX = 1500;
12
+
13
+ function clip(text, max) {
14
+ const s = String(text ?? '');
15
+ return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
16
+ }
17
+
18
+ const empty = (v) =>
19
+ v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0);
20
+
21
+ function briefCheck(c) {
22
+ if (c.ok) return { command: c.command, ok: true };
23
+ const out = { command: c.command, ok: false, exitCode: c.exitCode ?? null };
24
+ if (c.timedOut) out.timedOut = true;
25
+ out.tail = String(c.output || '').slice(-TAIL_MAX);
26
+ return out;
27
+ }
28
+
29
+ const ALWAYS = new Set(['type', 'schema', 'status', 'ok', 'id']);
30
+
31
+ function buildBrief(result) {
32
+ // checks_failed restates the failing `checks` entries below: kind only.
33
+ const error = result.error
34
+ ? result.error.kind === 'checks_failed'
35
+ ? { kind: 'checks_failed' }
36
+ : { kind: result.error.kind, message: clip(result.error.message, ERROR_MAX) }
37
+ : null;
38
+ // The failure message usually repeats as an issue; say it once.
39
+ const issues = (result.issues || []).filter((i) => !(error && i === result.error.message));
40
+ const raw = {
41
+ type: 'result',
42
+ schema: BRIEF_SCHEMA,
43
+ status: result.status,
44
+ ok: Boolean(result.ok),
45
+ id: result.id,
46
+ label: result.label,
47
+ preflight: result.preflight,
48
+ summary: clip(result.summary, SUMMARY_MAX),
49
+ // run/agent: the answer is the product. task: the handback is.
50
+ response: result.kind === 'task' ? undefined : result.response,
51
+ files: ((result.changes && result.changes.files) || []).map((f) => f.path),
52
+ checks: (result.checks || []).map(briefCheck),
53
+ questions: result.questions,
54
+ issues,
55
+ unresolved: result.unresolved,
56
+ scopeViolations: result.scopeViolations,
57
+ touched: result.touched,
58
+ decisions: result.decisions,
59
+ visual: result.visual,
60
+ units: result.units,
61
+ error,
62
+ result: result.files && result.files.result,
63
+ };
64
+ const out = {};
65
+ for (const [k, v] of Object.entries(raw)) if (ALWAYS.has(k) || !empty(v)) out[k] = v;
66
+ return out;
67
+ }
68
+
69
+ module.exports = { buildBrief, BRIEF_SCHEMA };
package/lib/checks.js CHANGED
@@ -22,6 +22,7 @@ function killGroup(child) {
22
22
  }
23
23
 
24
24
  function runCheck(command, { cwd, timeoutMs = 300000, signal, env = process.env } = {}) {
25
+ env = env || process.env;
25
26
  const started = Date.now();
26
27
  return new Promise((resolve) => {
27
28
  let output = '';
@@ -80,12 +81,12 @@ function runCheck(command, { cwd, timeoutMs = 300000, signal, env = process.env
80
81
 
81
82
  // Runs every check in order (later checks still run when an earlier one
82
83
  // fails, so the handback carries complete evidence); stops on cancel.
83
- async function runChecks(commands, { cwd, timeoutMs, signal, onEvent = () => {} } = {}) {
84
+ async function runChecks(commands, { cwd, timeoutMs, signal, env, onEvent = () => {} } = {}) {
84
85
  const results = [];
85
86
  for (const command of commands) {
86
87
  if (signal && signal.aborted) break;
87
88
  onEvent({ type: 'check', phase: 'started', command });
88
- const r = await runCheck(command, { cwd, timeoutMs, signal });
89
+ const r = await runCheck(command, { cwd, timeoutMs, signal, env });
89
90
  results.push(r);
90
91
  onEvent({
91
92
  type: 'check',
@@ -8,6 +8,7 @@ const { sha256File, engineVersion } = require('../engine.js');
8
8
  const { resolveRuntime, ConfigError } = require('../bootstrap.js');
9
9
  const { buildEngineHome, engineEnv, rootDir, liveDir, effectiveMcpServers } = require('../isolation.js');
10
10
  const store = require('../store.js');
11
+ const { modelLevels } = require('../models.js');
11
12
 
12
13
  module.exports = async function doctor(parsed) {
13
14
  const flags = parsed.flags;
@@ -51,6 +52,7 @@ module.exports = async function doctor(parsed) {
51
52
  version,
52
53
  builtinProviderConfig: builtinConfig,
53
54
  },
55
+ models: modelLevels({ builtinConfig }),
54
56
  isolation: {
55
57
  root,
56
58
  home: built.home,
@@ -83,6 +85,7 @@ module.exports = async function doctor(parsed) {
83
85
  ` sha256 ${report.engine.sha256}`,
84
86
  ` version ${version ?? 'unknown'}`,
85
87
  ` builtin provider config ${builtinConfig}`,
88
+ ` models ${Object.entries(report.models).map(([id, l]) => `${id} (effort ${l.join('|')})`).join(', ')}`,
86
89
  ` isolation root ${root} (one engine HOME per session; config mode: ${built.mode})`,
87
90
  ` skills inherited ${built.skillsCopied}, mcp servers available with --mcp-inherit ${report.isolation.mcpServersInherited}`,
88
91
  ` sessions ${report.sessions.total} (${report.sessions.running} running, ${report.sessions.interrupted} interrupted)`,