@0xgeorgemathew/zcode 0.4.1 → 0.5.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 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;
@@ -30,13 +30,22 @@ The orchestrator keeps product decisions, design direction, and coordination. zc
30
30
  npm install -g @0xgeorgemathew/zcode
31
31
  echo "$ZAI_API_KEY" | zcode login # stores your Z.AI key with mode 600; zcode never prints it
32
32
  zcode doctor # checks the engine, isolation and key
33
+ zcode skills install # optional: teach Claude Code and Codex to delegate to zcode
33
34
 
34
35
  zcode task --check "npm test" "Fix the off-by-one in src/paginate.ts"
35
36
  ```
36
37
 
37
38
  The CLI needs node 20 or later. The engine child runs best on node 26. zcode also needs the ZCode engine, which today ships inside the ZCode desktop app; zcode finds it automatically.
38
39
 
39
- Installing the package also installs the Claude Code skill to `~/.claude/skills/zcode` and the Codex skill to `${CODEX_HOME:-~/.codex}/skills/zcode`. These skills teach the orchestrator how to delegate to zcode. Set `ZCODE_SKIP_SKILLS=1` to skip this step. Details are in [`skills/`](skills/README.md).
40
+ The package ships skills that teach Claude Code and Codex how to delegate to zcode. Install them with one command:
41
+
42
+ ```sh
43
+ zcode skills install # both; or: zcode skills install claude | codex
44
+ zcode skills # show what is installed and where
45
+ zcode skills uninstall # remove them
46
+ ```
47
+
48
+ The Claude Code skill goes to `~/.claude/skills/zcode` and the Codex skill to `${CODEX_HOME:-~/.codex}/skills/zcode`. The package runs no install scripts. Details are in [`skills/`](skills/README.md).
40
49
 
41
50
  &nbsp;
42
51
 
@@ -47,13 +56,13 @@ Every command runs on GLM-5.3-Flash at effort `max` unless you pass a flag.
47
56
  | setting | flag | values | default |
48
57
  |---|---|---|---|
49
58
  | model | `--model` | `glm-5.3-flash` (aliases `flash`, `5.3-flash`, `53f`), `glm-5.3` (aliases `5.3`, `53`) | `glm-5.3-flash` |
50
- | 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` |
51
60
 
52
61
  ```sh
53
62
  zcode task "..." # GLM-5.3-Flash, effort max
54
63
  zcode task --effort high "..." # Flash with less reasoning
55
64
  zcode task --model glm-5.3 "..." # GLM-5.3, effort max
56
- 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
57
66
  ```
58
67
 
59
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.
@@ -87,7 +96,7 @@ not verified
87
96
  - prose acceptance criteria are confirmed only by the executor's own report
88
97
  ```
89
98
 
90
- 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).
91
100
 
92
101
  For small work, the objective alone is enough: `zcode task "Fix the typo in the README heading"`.
93
102
 
@@ -100,7 +109,7 @@ Flags cover the common fields. For richer handoffs, pass `--task contract.json`,
100
109
  "objective": "Implement page 2 (Team) per the design spec",
101
110
  "scope": ["Team page route and its components"],
102
111
  "constraints": ["No new dependencies", "Keep existing routing untouched"],
103
- "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"],
104
113
  "acceptance": ["Every member card shows name, role, avatar"],
105
114
  "checks": ["npm run build", "npm test -- team"],
106
115
  "files": ["src/theme.ts", "src/pages/home/index.tsx"],
@@ -113,7 +122,15 @@ Flags cover the common fields. For richer handoffs, pass `--task contract.json`,
113
122
  }
114
123
  ```
115
124
 
116
- `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.
117
134
 
118
135
  ### Status: what a result means
119
136
 
@@ -140,6 +157,8 @@ zcode task --detach --id page-2 --owns src/pages/team/ --check "npm run build" "
140
157
  zcode status # all sessions: id, state/status, kind, updated, label
141
158
  zcode status page-1 --json # one session: state, phase, activity, recent events, result
142
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
143
162
  zcode resume page-2 "Use the 2-column variant below 1024px." # answer / follow up / retry
144
163
  zcode cancel page-1 # stop the turn and any running check; recorded as cancelled
145
164
  ```
@@ -157,6 +176,34 @@ zcode cancel page-1 # stop the turn and any running check; recorded as
157
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.
158
177
  - For overlapping or risky parallel work, give each session its own git worktree via `--cwd`.
159
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`.
160
207
 
161
208
  &nbsp;
162
209
 
@@ -180,6 +227,18 @@ Inside `zcode task`, subagents are optional. The executor decides when to use th
180
227
 
181
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.
182
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
+
183
242
  ```json
184
243
  {"type":"result","schema":"zcode.result/1","ok":true,"status":"completed","id":"page-1","kind":"task",
185
244
  "summary":"…","response":"…executor report…","issues":[],"questions":[],"unresolved":[],"notVerified":[],
@@ -192,7 +251,7 @@ Inside `zcode task`, subagents are optional. The executor decides when to use th
192
251
  "toolCalls":14,"durationMs":142301,"files":{"session":"…/sessions/page-1","result":"…/result.json","events":"…/events.ndjson"}}
193
252
  ```
194
253
 
195
- `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.
196
255
 
197
256
  | event | meaning |
198
257
  |---|---|
@@ -204,7 +263,7 @@ Inside `zcode task`, subagents are optional. The executor decides when to use th
204
263
  | `{"type":"mcp","name","status"}` · `{"type":"retry",…}` · `{"type":"model_request_started",…}` | MCP settling, start-up retry, engine model evidence |
205
264
  | `{"type":"stream","kind":"text_delta\|reasoning_delta","delta"}` | live text (run/agent; task with `--stream`) |
206
265
 
207
- `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.
208
267
 
209
268
  &nbsp;
210
269
 
@@ -216,23 +275,30 @@ Inside `zcode task`, subagents are optional. The executor decides when to use th
216
275
  | `--detach` | run, task, agent, resume | off | background worker; prints the id |
217
276
  | `--cwd` | run, task, agent | current dir | the workspace and blast radius |
218
277
  | `--owns` | run, task, agent | whole workspace | repeatable path or glob |
278
+ | `--touch` | task | | repeatable; minimal edits outside `--owns`, reported as `touched` |
219
279
  | `--allow-overlap` | run, task, agent | off | start despite overlapping ownership |
220
280
  | `--task` | task | | JSON contract, markdown brief, or `-` |
221
- | `--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 |
222
286
  | `--max-repairs` | task, resume | `2` | repair turns after failing checks |
223
287
  | `--check-timeout-ms` | task, resume | `300000` | per check; checks get `CI=1` when unset |
224
288
  | `--model` | all turns | `glm-5.3-flash` | `glm-5.3` plus aliases `5.3`, `53`, `flash`, `5.3-flash`, `53f` |
225
- | `--effort` | all turns | `max` | `low`, `medium`, `high`, `xhigh`, `max` |
289
+ | `--effort` | all turns | `max` | the model's levels: `low`, `high`, `max` |
226
290
  | `--timeout-ms` | all turns; wait | run 600000, task 1800000 | the whole invocation incl. checks and repairs |
227
291
  | `--session` | run | | 0.3 compat: continue by zcode id or engine `sess_` id |
228
292
  | `--profile` | agent | `general-purpose` | subagent profile |
229
- | `--mcp` / `--mcp-inherit` / `--mcp-list` | run, task, agent | hermetic | MCP servers (inline JSON or `@file`) |
293
+ | `--mcp` / `--mcp-inherit` / `--mcp-list` | run, task, agent | hermetic, plus the desktop's `playwright` | MCP servers (inline JSON or `@file`) |
294
+ | `--no-browser` | run, task, agent | off | Drop the desktop's `playwright` MCP server |
230
295
  | `--skills` / `--no-skills` | run, task, agent | desktop skills | add skills; or run with none |
231
- | `--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 |
232
298
  | `--thinking` / `--no-thinking` | turns | run on, task off | mirror reasoning to stderr |
233
299
  | `--stream`, `--include-reasoning` | turns | off | deltas in task events; full reasoning in the result |
234
300
 
235
- 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.
236
302
 
237
303
  ## Exit codes
238
304
 
package/bin/zcode.js CHANGED
@@ -10,10 +10,20 @@ 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
25
+ zcode skills [install|uninstall] [claude|codex]
26
+ install the orchestrator skills (default: status)
17
27
 
18
28
  session:
19
29
  --id <id> name the session (letters, digits, . _ -); default zc-<random>.
@@ -30,26 +40,65 @@ task contract (task only):
30
40
  --task <file|-> JSON contract or markdown brief (- = stdin)
31
41
  --accept <text> repeatable acceptance criterion
32
42
  --check <command> repeatable shell check zcode runs after the work
33
- --file <path> repeatable relevant file to read first
34
- --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)
35
62
  --max-repairs <n> repair turns after failing checks (default 2)
36
63
  --check-timeout-ms <n> per-check timeout (default 300000)
37
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
+
38
79
  execution:
39
80
  --model <id> glm-5.3 | glm-5.3-flash (+aliases). default: glm-5.3-flash
40
- --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
41
84
  --timeout-ms <n> whole invocation incl. checks/repairs (default 600000;
42
85
  task default 1800000)
43
86
  --profile <name> subagent profile (agent command; default general-purpose)
44
87
  --mcp <json|@f> add MCP servers (repeatable); hermetic by default
45
88
  --mcp-inherit also inherit the desktop's enabled MCP servers
89
+ --no-browser drop the desktop's playwright MCP server (on by default)
46
90
  --mcp-list print the effective MCP server set as JSON and exit
47
91
  --skills <dir> add skills (repeatable): one skill dir or a root of them
48
92
  --no-skills empty skill set
49
93
 
50
94
  output:
51
- --json NDJSON events on stdout; final line {"type":"result",…}
52
- --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
53
102
  --thinking / --no-thinking mirror reasoning to stderr (run: on, task: off)
54
103
  --stream task --json: include text/reasoning deltas in the events
55
104
  --include-reasoning full reasoning text in the result
@@ -94,9 +143,10 @@ function parseMcpSpec(value) {
94
143
  return parsed;
95
144
  }
96
145
 
97
- const COMMANDS = new Set(['run', 'task', 'agent', 'resume', 'status', 'list', 'wait', 'cancel', 'login', 'doctor', '__exec']);
98
- const TASK_ONLY = new Set(['task', 'accept', 'check', 'file', 'context', 'maxRepairs', 'checkTimeoutMs']);
99
- const START_ONLY = new Set(['id', 'label', 'owns', 'allowOverlap', 'mcpSpecs', 'mcpInherit', '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']);
100
150
 
101
151
  function parseArgv(argv) {
102
152
  const out = {
@@ -122,11 +172,19 @@ function parseArgv(argv) {
122
172
  file: [],
123
173
  context: [],
124
174
  owns: [],
175
+ touch: [],
176
+ decisions: [],
177
+ units: [],
178
+ visual: [],
179
+ viewports: [],
180
+ serve: undefined,
181
+ preflight: false,
125
182
  allowOverlap: false,
126
183
  maxRepairs: undefined,
127
184
  checkTimeoutMs: undefined,
128
185
  mcpSpecs: [],
129
186
  mcpInherit: false,
187
+ noBrowser: false,
130
188
  mcpList: false,
131
189
  skillsDirs: [],
132
190
  noSkills: false,
@@ -134,24 +192,29 @@ function parseArgv(argv) {
134
192
  quiet: false,
135
193
  thinking: undefined,
136
194
  stream: false,
195
+ full: false,
196
+ any: false,
137
197
  },
138
198
  };
139
199
  const valueFlags = new Map([
140
200
  ['--model', 'model'], ['--effort', 'effort'], ['--cwd', 'cwd'], ['--profile', 'profile'],
141
201
  ['--timeout-ms', 'timeoutMs'], ['-p', 'prompt'], ['--prompt', 'prompt'], ['--session', 'session'],
142
202
  ['--id', 'id'], ['--label', 'label'], ['--task', 'task'], ['--max-repairs', 'maxRepairs'],
143
- ['--check-timeout-ms', 'checkTimeoutMs'],
203
+ ['--check-timeout-ms', 'checkTimeoutMs'], ['--serve', 'serve'],
144
204
  ]);
145
205
  const listFlags = new Map([
146
206
  ['--mcp', 'mcpSpecs'], ['--skills', 'skillsDirs'], ['--accept', 'accept'], ['--check', 'check'],
147
- ['--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'],
148
210
  ]);
149
211
  const boolFlags = new Map([
150
212
  ['--json', ['json', true]], ['--fresh', ['fresh', true]], ['--mcp-inherit', ['mcpInherit', true]],
151
- ['--mcp-list', ['mcpList', true]], ['--no-skills', ['noSkills', true]],
213
+ ['--mcp-list', ['mcpList', true]], ['--no-skills', ['noSkills', true]], ['--no-browser', ['noBrowser', true]],
152
214
  ['--include-reasoning', ['includeReasoning', true]], ['--quiet', ['quiet', true]],
153
215
  ['--detach', ['detach', true]], ['--allow-overlap', ['allowOverlap', true]],
154
216
  ['--thinking', ['thinking', true]], ['--no-thinking', ['thinking', false]], ['--stream', ['stream', true]],
217
+ ['--full', ['full', true]], ['--any', ['any', true]], ['--preflight', ['preflight', true]],
155
218
  ]);
156
219
  const numeric = (flag, value, { min = 1 } = {}) => {
157
220
  const n = Number(value);
@@ -164,7 +227,7 @@ function parseArgv(argv) {
164
227
  if (valueFlags.has(arg) || listFlags.has(arg)) {
165
228
  const key = valueFlags.get(arg) || listFlags.get(arg);
166
229
  const value = argv[++i];
167
- if (value === undefined) fail(2, `option ${arg} requires a value.\n${USAGE}`);
230
+ if (value === undefined) fail(2, `option ${arg} requires a value.`);
168
231
  out.given.add(key);
169
232
  if (key === 'timeoutMs' || key === 'checkTimeoutMs') out.flags[key] = numeric(arg, value);
170
233
  else if (key === 'maxRepairs') out.flags[key] = numeric(arg, value, { min: 0 });
@@ -172,8 +235,6 @@ function parseArgv(argv) {
172
235
  else if (key === 'skillsDirs') {
173
236
  if (!isDirectory(value)) fail(2, `--skills requires an existing directory, got "${value}".`);
174
237
  out.flags.skillsDirs.push(value);
175
- } else if (key === 'context') {
176
- out.flags.context.push(value.startsWith('@') ? readFileOrFail(value.slice(1), '--context') : value);
177
238
  } else if (listFlags.has(arg)) out.flags[key].push(value);
178
239
  else if (!value.trim()) fail(2, `${arg} requires a non-empty value.`);
179
240
  else out.flags[key] = value.trim();
@@ -197,7 +258,10 @@ function parseArgv(argv) {
197
258
  out.prompts.push(...argv.slice(i + 1));
198
259
  break;
199
260
  }
200
- 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
+ }
201
265
  if (out.prompts.length === 0 && out.command === 'run' && !out.commandSet && COMMANDS.has(arg)) {
202
266
  out.command = arg === 'list' ? 'status' : arg;
203
267
  out.commandSet = true;
@@ -210,17 +274,25 @@ function parseArgv(argv) {
210
274
  if (out.flags.prompt !== undefined) out.prompts.unshift(out.flags.prompt);
211
275
  if (cmd !== 'task') {
212
276
  for (const key of TASK_ONLY) {
277
+ if (cmd === 'plan' && key === 'preflight') continue;
213
278
  if (out.given.has(key) && !(cmd === 'resume' && (key === 'maxRepairs' || key === 'checkTimeoutMs'))) {
214
279
  fail(2, `--${flagName(key)} is only valid with "zcode task".`);
215
280
  }
216
281
  }
217
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
+ }
218
288
  if (cmd === 'resume' || cmd === '__exec') {
219
289
  for (const key of START_ONLY) {
220
290
  if (out.given.has(key)) fail(2, `--${flagName(key)} cannot change on resume; it is fixed when the session starts.`);
221
291
  }
222
292
  }
223
- 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>".');
224
296
  if (out.given.has('id') && !require('../lib/store.js').validId(out.flags.id)) {
225
297
  fail(2, `--id "${out.flags.id}" must be 1-64 chars of letters, digits, ".", "_" or "-", starting with a letter or digit.`);
226
298
  }
@@ -235,8 +307,34 @@ function parseArgv(argv) {
235
307
  return out;
236
308
  }
237
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
+
238
335
  function flagName(key) {
239
- 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');
240
338
  }
241
339
 
242
340
  function isDirectory(p) {
@@ -257,19 +355,28 @@ async function main() {
257
355
  case 'resume':
258
356
  case '__exec':
259
357
  return require('../lib/commands/exec.js')(parsed);
358
+ case 'plan':
359
+ case '__plan':
360
+ return require('../lib/commands/plan.js')(parsed);
260
361
  case 'status':
261
362
  case 'wait':
363
+ case 'result':
262
364
  case 'cancel':
263
365
  return require('../lib/commands/sessions.js')(parsed);
264
366
  case 'login':
265
367
  return require('../lib/commands/login.js')(parsed);
266
368
  case 'doctor':
267
369
  return require('../lib/commands/doctor.js')(parsed);
370
+ case 'skills':
371
+ return require('../lib/commands/skills.js')(parsed);
268
372
  default:
269
- return fail(2, `unknown command "${parsed.command}".\n${USAGE}`);
373
+ return fail(2, `unknown command "${parsed.command}" (see zcode --help).`);
270
374
  }
271
375
  }
272
376
 
377
+ // Exported before main() runs: the plan supervisor reuses parseArgv.
378
+ module.exports = { parseArgv, USAGE };
379
+
273
380
  if (require.main === module) {
274
381
  main().catch((err) => {
275
382
  process.stderr.write(`zcode: internal error: ${err && err.stack ? err.stack : err}\n`);
@@ -277,4 +384,3 @@ if (require.main === module) {
277
384
  });
278
385
  }
279
386
 
280
- module.exports = { parseArgv, USAGE };
package/lib/appserver.js CHANGED
@@ -150,6 +150,17 @@ class AppServerClient {
150
150
  if (this.onUserInputRequest) this.onUserInputRequest(frame);
151
151
  return;
152
152
  }
153
+ if (method === 'interaction/browserList') {
154
+ // The desktop app answers with its built-in browser panes; the CLI has
155
+ // none. An empty list lets the agent fall back to the user's own tools
156
+ // (Playwright, MCP browsers) instead of failing on a policy error.
157
+ reply({ browsers: [] });
158
+ return;
159
+ }
160
+ if (method === 'interaction/browserExecute') {
161
+ replyError(-32000, 'zcode: no built-in browser in the CLI; use Playwright or a browser MCP server');
162
+ return;
163
+ }
153
164
  // Everything else — incl. interaction/requestOfficialMcpAuthHeaders,
154
165
  // proven non-fatal with -32601 — gets method-not-found and the run
155
166
  // continues.
@@ -220,8 +231,12 @@ function workspaceKeyFor(workspacePath) {
220
231
  // turn always sees the caller's --mcp servers. Inherited servers are
221
232
  // best-effort and never block the send.
222
233
  async function waitForMcp(client, { workspacePath, required, timeoutMs = 20000, onEvent = () => {} }) {
223
- if (!required || required.length === 0) return;
224
234
  const ws = { workspace: { workspacePath, workspaceKey: workspaceKeyFor(workspacePath) } };
235
+ if (!required || required.length === 0) {
236
+ // Still start inherited servers (the browser server); never block on them.
237
+ client.request('mcp/list', { ...ws, mode: 'connect' }, { timeoutMs: 15000 }).catch(() => {});
238
+ return;
239
+ }
225
240
  try {
226
241
  await client.request('mcp/list', { ...ws, mode: 'connect' }, { timeoutMs: 15000 });
227
242
  } catch {
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',