@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 +81 -15
- package/bin/zcode.js +126 -20
- package/lib/appserver.js +16 -1
- package/lib/bootstrap.js +2 -2
- package/lib/brief.js +69 -0
- package/lib/checks.js +3 -2
- package/lib/commands/doctor.js +3 -0
- package/lib/commands/exec.js +101 -35
- package/lib/commands/mcp-list.js +1 -1
- package/lib/commands/plan.js +301 -0
- package/lib/commands/sessions.js +60 -11
- package/lib/commands/skills.js +39 -0
- package/lib/contract.js +220 -17
- package/lib/executor.js +142 -34
- package/lib/isolation.js +36 -4
- package/lib/models.js +84 -0
- package/lib/plan.js +424 -0
- package/lib/ports.js +45 -0
- package/lib/skills.js +64 -0
- package/lib/store.js +50 -0
- package/lib/taskspec.js +107 -0
- package/lib/ui.js +7 -2
- package/lib/workspace.js +24 -7
- package/package.json +1 -2
- package/skills/README.md +9 -3
- package/skills/claude-code/zcode/SKILL.md +104 -74
- package/skills/codex/zcode/SKILL.md +104 -76
- package/lib/install-skills.js +0 -54
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>
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
|
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`, `
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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`
|
|
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
|
|
|
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
|
|
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
|
|
|
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`, `
|
|
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.
|
|
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]
|
|
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
|
-
--
|
|
34
|
-
|
|
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 |
|
|
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
|
|
52
|
-
|
|
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
|
|
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
|
|
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)
|
|
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('
|
|
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}"
|
|
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',
|