premanmcp 1.1.18 → 1.1.19

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
@@ -83,8 +83,10 @@ account, backend, and workspace:
83
83
  /swarm stop <run-id>
84
84
  ```
85
85
 
86
- `/swarm help` lists every option. Add `--dry-run` to preview a launch without
87
- starting attempts. Ctrl+C while watching returns to the conversation and leaves
86
+ Launch follows automatically, showing queue and running counts, live tool
87
+ actions, and the final result. `/swarm help` lists every option. Add `--dry-run`
88
+ to preview a launch without starting attempts, or `--no-watch` to return
89
+ immediately. `--json` returns one JSON response. Ctrl+C while watching returns to the conversation and leaves
88
90
  the swarm running; `/swarm stop <run-id>` cancels it explicitly. The same commands
89
91
  work outside a session as `preman swarm list|launch|watch|stop|bank`. These run
90
92
  real agents against hosted MCPs; the app's sample swarms remain a separate demo.
package/bin/agent.js CHANGED
@@ -2086,7 +2086,7 @@ export async function runSwarm(rest, { args, token, workspaceId, say, paint, reg
2086
2086
  }
2087
2087
  await swarmCommand(argv, {
2088
2088
  sessionArgs: args, token, workspaceId, signal: controller.signal,
2089
- write: say, clearWatch: false,
2089
+ write: say,
2090
2090
  });
2091
2091
  say("\n");
2092
2092
  } catch (error) {
@@ -0,0 +1,127 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+
3
+ const FINISHED = new Set(["done", "failed", "cancelled"]);
4
+ const MAX_LINES = 24;
5
+ const MAX_BUFFER = 65536;
6
+
7
+ // Only action names and outcomes enter the transcript, never tool arguments,
8
+ // results, credentials, or the agent's private reasoning.
9
+ export function cleanLabel(value, limit = 120) {
10
+ return stripVTControlCharacters(String(value ?? ""))
11
+ .replace(/[\x00-\x1f\x7f-\x9f]/g, " ")
12
+ .replace(/(?:pm_live_|sk-)[A-Za-z0-9_-]+/g, "[redacted]")
13
+ .slice(0, limit);
14
+ }
15
+
16
+ export class SwarmProgress {
17
+ constructor({ write, colour = false }) {
18
+ this.write = write;
19
+ this.colour = colour;
20
+ this.cells = new Map();
21
+ this.lines = [];
22
+ this.omitted = 0;
23
+ }
24
+
25
+ state(cell) {
26
+ if (!this.cells.has(cell.id)) {
27
+ this.cells.set(cell.id, { seen: new Set(), offset: 0, pending: "", tools: new Map() });
28
+ }
29
+ return this.cells.get(cell.id);
30
+ }
31
+
32
+ note(cell, key, text, outcome = "") {
33
+ const state = this.state(cell);
34
+ if (state.seen.has(key)) return;
35
+ // Tool logs are untrusted and a single agent can emit indefinitely.
36
+ if (state.seen.size >= 500) return;
37
+ state.seen.add(key);
38
+ const mark = outcome === "failed" ? "✗" : outcome === "done" ? "✓" : "·";
39
+ const coloured = this.colour && outcome ? `\x1b[${outcome === "failed" ? 31 : 32}m${mark}\x1b[0m` : mark;
40
+ if (this.lines.length < MAX_LINES) this.lines.push(` ${coloured} ${cleanLabel(cell.harness, 24)} ${cleanLabel(cell.id, 8)} ${text}`);
41
+ else this.omitted += 1;
42
+ }
43
+
44
+ observe(cells, { results = false } = {}) {
45
+ for (const cell of cells) {
46
+ const evidence = cell.evidence || {};
47
+ if (!results) {
48
+ if (cell.status === "running") this.note(cell, "running", "Agent starting");
49
+ if (evidence.container_id) this.note(cell, "container", "Container started", "done");
50
+ if (cell.reached === true) this.note(cell, "connected", "Connected to the MCP", "done");
51
+ if (evidence.tools_listed === true) this.note(cell, "listed", "Tools loaded", "done");
52
+ continue;
53
+ }
54
+ for (const tool of (evidence.tools_called || []).slice(0, 100)) {
55
+ this.note(cell, `called:${tool}`, `Called ${cleanLabel(tool)}`, "done");
56
+ }
57
+ if (FINISHED.has(cell.status)) {
58
+ const failed = cell.status !== "done" || cell.selected === false;
59
+ const result = cell.selected === true ? "Expected tool selected" : cell.selected === false ? "Expected tool not selected" : "Not scored";
60
+ const reason = cell.verdict_reason ? ` · ${cleanLabel(cell.verdict_reason).replaceAll("_", " ")}` : "";
61
+ this.note(cell, `finished:${cell.status}`, `${cell.status === "cancelled" ? "Cancelled" : "Finished"} — ${result}${reason}`, failed ? "failed" : "done");
62
+ }
63
+ }
64
+ }
65
+
66
+ console(cell, chunk) {
67
+ const state = this.state(cell);
68
+ if (chunk.truncated) state.pending = "";
69
+ state.offset = Number.isFinite(chunk.total) ? chunk.total : state.offset;
70
+ let text = state.pending + String(chunk.text || "");
71
+ if (text.length > MAX_BUFFER) {
72
+ text = text.slice(-MAX_BUFFER);
73
+ text = text.slice(text.indexOf("\n") + 1);
74
+ }
75
+ const lines = text.split("\n");
76
+ state.pending = lines.pop() || "";
77
+ if (FINISHED.has(cell.status) && state.pending) {
78
+ lines.push(state.pending);
79
+ state.pending = "";
80
+ }
81
+ for (const line of lines) {
82
+ let event;
83
+ try { event = JSON.parse(line); } catch { continue; }
84
+ if (!event || typeof event !== "object") continue;
85
+ const item = event.item;
86
+ if (item?.type === "mcp_tool_call") {
87
+ const name = cleanLabel(item.tool || item.name || "MCP tool");
88
+ const finished = event.type === "item.completed";
89
+ const failed = Boolean(item.error) || item.status === "failed";
90
+ this.note(cell, `event:${item.id}:${name}:${event.type}`, `${finished ? failed ? "Tool failed" : "Tool completed" : "Calling"}: ${name}`, finished ? failed ? "failed" : "done" : "");
91
+ if (finished && !failed && state.seen.size < 500) state.seen.add(`called:${item.tool || item.name}`);
92
+ } else if (item?.type === "command_execution" && event.type === "item.started") {
93
+ this.note(cell, `command:${item.id}`, "Running a shell command");
94
+ }
95
+ const content = event.message?.content || event.content;
96
+ if (Array.isArray(content)) for (const block of content) {
97
+ if (block?.type === "tool_use") {
98
+ const name = cleanLabel(block.name || "tool");
99
+ if (state.tools.size < 100) state.tools.set(block.id, name);
100
+ this.note(cell, `use:${block.id}`, `Calling: ${name}`);
101
+ } else if (block?.type === "tool_result") {
102
+ const name = state.tools.get(block.tool_use_id) || "tool";
103
+ this.note(cell, `result:${block.tool_use_id}`, `Tool ${block.is_error ? "failed" : "completed"}: ${name}`, block.is_error ? "failed" : "done");
104
+ }
105
+ }
106
+ if (event.type === "turn.started") this.note(cell, "turn", "Agent working");
107
+ if (event.type === "turn.failed") this.note(cell, "turn-failed", "Agent turn failed", "failed");
108
+ }
109
+ }
110
+
111
+ flush() {
112
+ if (this.lines.length) this.write(`${this.lines.slice(0, MAX_LINES).join("\n")}\n`);
113
+ if (this.omitted) this.write(` … ${this.omitted} more updates; full details in the swarm page.\n`);
114
+ this.lines = [];
115
+ this.omitted = 0;
116
+ }
117
+ }
118
+
119
+ export function progressLine(run, cells, started) {
120
+ const counts = {};
121
+ for (const cell of cells) counts[cell.status] = (counts[cell.status] || 0) + 1;
122
+ const done = (counts.done || 0) + (counts.failed || 0) + (counts.cancelled || 0);
123
+ const elapsed = Math.max(0, Math.floor((Date.now() - started) / 1000));
124
+ const age = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
125
+ const state = run.status === "queued" ? "Waiting for a worker" : FINISHED.has(run.status) ? cleanLabel(run.status) : "Running";
126
+ return `${state} · ${counts.queued || 0} queued · ${counts.running || 0} running · ${done}/${cells.length} finished · watching ${age}\n`;
127
+ }
package/bin/swarm.js CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { setTimeout as wait } from "node:timers/promises";
13
+ import { SwarmProgress, cleanLabel, progressLine } from "./swarm-progress.js";
13
14
 
14
15
  import {
15
16
  backendUrl,
@@ -27,8 +28,9 @@ Swarm options:
27
28
  preman swarm list Recent swarms and what each found
28
29
  preman swarm launch --server <slug|id> --ask "<what a developer asks>" [--ask ...]
29
30
  [--expect tool,tool] [--agents claude_code,codex,...] [--rivals 0,5]
30
- [--size 60] [--name <label>] [--dry-run]
31
- Start a swarm (--dry-run only counts it)
31
+ [--size 60] [--name <label>] [--dry-run] [--no-watch]
32
+ Launch and follow live actions until done
33
+ --no-watch queues and returns immediately
32
34
  preman swarm watch <id> [--once] Follow a swarm until it finishes
33
35
  preman swarm stop <id> Stop a swarm; unfinished agents do not run
34
36
  preman swarm bank [--server <slug|id>] Behaviors PreMan learned and you kept
@@ -51,7 +53,7 @@ const AGENT_NAMES = {
51
53
  };
52
54
 
53
55
  const TERMINAL = new Set(["done", "failed", "cancelled"]);
54
- const POLL_MS = 5000;
56
+ const POLL_MS = 2000;
55
57
 
56
58
  function environmentOf(harness) {
57
59
  return ENVIRONMENTS.find((env) => env.harnesses.includes(harness))?.id || "other";
@@ -89,7 +91,7 @@ export function positionals(raw) {
89
91
  return out;
90
92
  }
91
93
 
92
- const BARE_FLAGS = new Set(["--json", "--once", "--dry-run", "--no-color"]);
94
+ const BARE_FLAGS = new Set(["--json", "--once", "--dry-run", "--no-color", "--no-watch"]);
93
95
 
94
96
  function listOf(value) {
95
97
  return String(value || "")
@@ -179,7 +181,7 @@ export function renderSwarm(run, cells, { colour = false } = {}) {
179
181
  const done = (cells || []).filter((cell) => cell.status === "done" || cell.status === "failed").length;
180
182
  const lines = [];
181
183
  lines.push(
182
- `${paint.bold(run.release_label || run.id)} ${paint.dim(run.hosted_mcp_name || run.hosted_mcp_slug || "")} ${run.status}`
184
+ `${paint.bold(cleanLabel(run.release_label || run.id))} ${paint.dim(cleanLabel(run.hosted_mcp_name || run.hosted_mcp_slug || ""))} ${cleanLabel(run.status)}`
183
185
  );
184
186
  if (summary.scored) {
185
187
  const range = likelyRange(summary.picked, summary.scored);
@@ -198,8 +200,8 @@ export function renderSwarm(run, cells, { colour = false } = {}) {
198
200
  const rate = agent.rate === null ? paint.dim("—") : agent.rate >= 0.75 ? paint.green(pct(agent.rate)) : agent.rate >= 0.4 ? paint.yellow(pct(agent.rate)) : paint.red(pct(agent.rate));
199
201
  const working = agent.running ? paint.dim(` · ${agent.running} working`) : "";
200
202
  const rival = [...agent.lostTo.entries()].sort((a, b) => b[1] - a[1])[0];
201
- const lost = rival ? paint.dim(` · lost to ${rival[0]} ×${rival[1]}`) : "";
202
- lines.push(` ${truncate(agent.name, 30).padEnd(30)} ${String(rate).padStart(colour ? 13 : 4)} ${paint.dim(`${agent.picked}/${agent.scored}`)}${working}${lost}`);
203
+ const lost = rival ? paint.dim(` · lost to ${cleanLabel(rival[0])} ×${rival[1]}`) : "";
204
+ lines.push(` ${truncate(cleanLabel(agent.name), 30).padEnd(30)} ${String(rate).padStart(colour ? 13 : 4)} ${paint.dim(`${agent.picked}/${agent.scored}`)}${working}${lost}`);
203
205
  }
204
206
  }
205
207
  return `${lines.join("\n")}\n`;
@@ -227,7 +229,9 @@ function requireToken(args) {
227
229
  async function call(args, token, method, path, options = {}) {
228
230
  const result = await callBackendJson(args, method, path, { token, ...options });
229
231
  if (!result.ok) {
230
- throw new Error(`${method} ${path} on ${backendUrl(args)}: ${result.status_code} ${describeFailure(result, "request failed")}`);
232
+ const error = new Error(`${method} ${path} on ${backendUrl(args)}: ${result.status_code} ${describeFailure(result, "request failed")}`);
233
+ error.status = result.status_code;
234
+ throw error;
231
235
  }
232
236
  return result;
233
237
  }
@@ -255,9 +259,92 @@ function swarmLink(args, id) {
255
259
  return `${frontendUrl(args)}/workbench/swarms/swarms/${encodeURIComponent(id)}`;
256
260
  }
257
261
 
262
+ async function followSwarm(id, { args, request, write, colour, signal, pollMs, once = false, json = false }) {
263
+ const controller = new AbortController();
264
+ const interrupt = () => controller.abort();
265
+ if (signal?.aborted) controller.abort();
266
+ signal?.addEventListener("abort", interrupt, { once: true });
267
+ if (!signal) process.on("SIGINT", interrupt);
268
+ const read = (method, path, options) => request(method, path, { ...options, signal: controller.signal });
269
+ const resume = `${cliInvocation()} swarm watch ${id}`;
270
+ const progress = new SwarmProgress({ write, colour });
271
+ const started = Date.now();
272
+ let cursor = 0;
273
+ let failures = 0;
274
+ let consoleWarned = false;
275
+ if (!once && !json) write("Following live actions. Ctrl+C stops watching; the swarm keeps running.\n");
276
+ try {
277
+ for (;;) {
278
+ let snapshot;
279
+ try {
280
+ snapshot = await readSwarm(read, id);
281
+ failures = 0;
282
+ } catch (error) {
283
+ if (controller.signal.aborted) throw error;
284
+ failures += 1;
285
+ const retryable = !error.status || error.status === 429 || error.status >= 500;
286
+ if (once || json || !retryable || failures >= 3) throw error;
287
+ write(`Connection interrupted; retrying status (${failures}/3). Swarm ${id} remains on the server.\n`);
288
+ await wait(pollMs, undefined, { signal: controller.signal });
289
+ continue;
290
+ }
291
+ const { run, cells } = snapshot;
292
+ if (json) {
293
+ write(`${JSON.stringify({ run, summary: summarize(cells) }, (k, v) => (v instanceof Map ? Object.fromEntries(v) : v), 2)}\n`);
294
+ return;
295
+ }
296
+ if (once) {
297
+ write(renderSwarm(run, cells, { colour }));
298
+ write(`\n${swarmLink(args, id)}\n`);
299
+ return;
300
+ }
301
+ write(progressLine(run, cells, started));
302
+ progress.observe(cells);
303
+ // At most four console reads per poll, rotated across larger swarms.
304
+ // Keep finished cells eligible for their final output after status changes.
305
+ const available = cells.filter((cell) => cell.id && cell.status !== "queued" && !progress.state(cell).consoleFinished);
306
+ const selected = available.length ? Array.from({ length: Math.min(4, available.length) }, (_, i) => available[(cursor + i) % available.length]) : [];
307
+ cursor += selected.length;
308
+ const logs = await Promise.allSettled(selected.map((cell) => read("GET", `/matrix/cells/${encodeURIComponent(cell.id)}/console`, { query: { after: progress.state(cell).offset } })));
309
+ for (let i = 0; i < logs.length; i += 1) {
310
+ const log = logs[i];
311
+ if (log.status === "fulfilled") {
312
+ progress.console(selected[i], log.value);
313
+ if (TERMINAL.has(selected[i].status)) progress.state(selected[i]).consoleFinished = true;
314
+ } else if (!controller.signal.aborted && !consoleWarned) {
315
+ write(" Live action output is unavailable; continuing to follow run status.\n");
316
+ consoleWarned = true;
317
+ }
318
+ }
319
+ if (controller.signal.aborted) throw new Error("Interrupted");
320
+ progress.observe(cells, { results: true });
321
+ progress.flush();
322
+ if (once || TERMINAL.has(run.status)) {
323
+ const eligible = cells.filter((cell) => cell.id && cell.status !== "queued");
324
+ const shown = eligible.filter((cell) => progress.state(cell).consoleFinished).length;
325
+ if (shown < eligible.length) write(`Action logs shown for ${shown} of ${eligible.length} attempts; open the swarm page for all logs.\n`);
326
+ write(renderSwarm(run, cells, { colour }));
327
+ if (run.stop_reason) write(`Stopped: ${cleanLabel(run.stop_reason)}\n`);
328
+ write(`\n${swarmLink(args, id)}\n`);
329
+ return;
330
+ }
331
+ await wait(pollMs, undefined, { signal: controller.signal });
332
+ }
333
+ } catch (error) {
334
+ if (controller.signal.aborted) {
335
+ write(`\nStopped watching. The swarm was not cancelled.\nResume: ${resume}\n`);
336
+ return;
337
+ }
338
+ throw new Error(`Could not follow swarm ${id}: ${cleanLabel(error.message, 240)}\nResume: ${resume}`);
339
+ } finally {
340
+ signal?.removeEventListener("abort", interrupt);
341
+ if (!signal) process.off("SIGINT", interrupt);
342
+ }
343
+ }
344
+
258
345
  export async function swarmCommand(commandArgs = [], {
259
346
  sessionArgs, token: sessionToken, workspaceId, signal,
260
- write = (text) => process.stdout.write(text), clearWatch = true,
347
+ write = (text) => process.stdout.write(text), pollMs = POLL_MS,
261
348
  } = {}) {
262
349
  const [sub = "list", ...rest] = commandArgs;
263
350
  // A session's credential stays with its backend even when slash-command
@@ -269,9 +356,23 @@ export async function swarmCommand(commandArgs = [], {
269
356
  const json = args.has("--json");
270
357
  const colour = colourEnabled(args);
271
358
  const token = sessionToken || requireToken(args);
272
- const request = (method, path, options = {}) => call(args, token, method, path, {
273
- ...options, signal, ...(workspaceId ? { headers: { "x-workspace-id": workspaceId } } : {}),
274
- });
359
+ const request = async (method, path, options = {}) => {
360
+ const parent = options.signal || signal;
361
+ const controller = new AbortController();
362
+ const abort = () => controller.abort();
363
+ if (parent?.aborted) abort();
364
+ parent?.addEventListener("abort", abort, { once: true });
365
+ const timeout = setTimeout(abort, 15000);
366
+ try {
367
+ return await call(args, token, method, path, {
368
+ ...options, signal: controller.signal,
369
+ ...(workspaceId ? { headers: { "x-workspace-id": workspaceId } } : {}),
370
+ });
371
+ } finally {
372
+ clearTimeout(timeout);
373
+ parent?.removeEventListener("abort", abort);
374
+ }
375
+ };
275
376
 
276
377
  if (sub === "list") {
277
378
  const result = await request("GET", "/matrix/runs");
@@ -296,6 +397,7 @@ export async function swarmCommand(commandArgs = [], {
296
397
  if (!serverRef || !asks.length) {
297
398
  throw new Error(`a swarm needs --server and at least one --ask.${SWARM_HELP}`);
298
399
  }
400
+ if (!json && !args.has("--dry-run")) write(`Resolving ${cleanLabel(serverRef)}…\n`);
299
401
  const server = await findServer(request, serverRef);
300
402
  const expect = listOf(args.value("--expect"));
301
403
  const payload = {
@@ -311,6 +413,7 @@ export async function swarmCommand(commandArgs = [], {
311
413
  const agents = listOf(args.value("--agents"));
312
414
  if (agents.length) payload.harnesses = agents;
313
415
  if (args.has("--dry-run")) payload.dry_run = true;
416
+ if (!json && !payload.dry_run) write("Queueing swarm…\n");
314
417
  const result = await request("POST", "/matrix/runs", { json: payload });
315
418
  const { status_code, ok, ...body } = result;
316
419
  if (json) return void write(`${JSON.stringify(body, null, 2)}\n`);
@@ -318,28 +421,17 @@ export async function swarmCommand(commandArgs = [], {
318
421
  write(`Would send ${body.cell_count ?? "?"} attempts (${body.asks_used ?? asks.length} asks, ${body.replicates ?? 1}× each).\n`);
319
422
  return;
320
423
  }
321
- write(`Swarm ${body.id} launched against ${server.name || server.slug}.\n`);
424
+ write(`Swarm ${cleanLabel(body.id)} launched against ${cleanLabel(server.name || server.slug)}.\n`);
322
425
  write(` Watch: ${cliInvocation()} swarm watch ${body.id}\n Open: ${swarmLink(args, body.id)}\n`);
426
+ if (!args.has("--no-watch")) await followSwarm(body.id, { args, request, write, colour, signal, pollMs });
323
427
  return;
324
428
  }
325
429
 
326
430
  if (sub === "watch" || sub === "show") {
327
431
  const [id] = positionals(rest);
328
432
  if (!id) throw new Error(`which swarm? ${cliInvocation()} swarm watch <id>`);
329
- for (;;) {
330
- const { run, cells } = await readSwarm(request, id);
331
- if (json) {
332
- write(`${JSON.stringify({ run, summary: summarize(cells) }, (k, v) => (v instanceof Map ? Object.fromEntries(v) : v), 2)}\n`);
333
- } else {
334
- if (clearWatch && process.stdout.isTTY && sub === "watch") write("\u001b[2J\u001b[H");
335
- write(renderSwarm(run, cells, { colour }));
336
- }
337
- if (sub === "show" || args.has("--once") || TERMINAL.has(run.status)) {
338
- if (!json) write(`\n${swarmLink(args, id)}\n`);
339
- return;
340
- }
341
- await wait(POLL_MS, undefined, { signal });
342
- }
433
+ await followSwarm(id, { args, request, write, colour, signal, pollMs, json, once: sub === "show" || args.has("--once") || json });
434
+ return;
343
435
  }
344
436
 
345
437
  if (sub === "stop") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "description": "PreMan CLI and stdio proxy for PreMan's hosted MCP server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "test:proxy": "node --test scripts/smoke-proxy.mjs",
17
17
  "test:connect": "node scripts/smoke-connect.mjs",
18
18
  "test:connect-heartbeat": "node scripts/smoke-connect-heartbeat.mjs",
19
- "test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-agent-session.mjs scripts/smoke-agent-dock.mjs scripts/smoke-terminal-lifecycle.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-cli-update.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-eval-start.mjs scripts/smoke-eval-run-watch.mjs scripts/smoke-eval-behaviors.mjs scripts/smoke-eval-runtime.mjs scripts/smoke-eval-entry.mjs scripts/smoke-eval-entry-placeholder.mjs scripts/smoke-eval-discover.mjs scripts/smoke-eval-templates.mjs scripts/smoke-eval-decorated-capture.mjs scripts/smoke-eval-decorate-approval.mjs scripts/smoke-eval-approval-record.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-agent-target.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs scripts/smoke-prompts.mjs scripts/smoke-process-title.mjs scripts/smoke-predict.mjs scripts/smoke-swarm.mjs",
19
+ "test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-agent-session.mjs scripts/smoke-agent-dock.mjs scripts/smoke-terminal-lifecycle.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-cli-update.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-eval-start.mjs scripts/smoke-eval-run-watch.mjs scripts/smoke-eval-behaviors.mjs scripts/smoke-eval-runtime.mjs scripts/smoke-eval-entry.mjs scripts/smoke-eval-entry-placeholder.mjs scripts/smoke-eval-discover.mjs scripts/smoke-eval-templates.mjs scripts/smoke-eval-decorated-capture.mjs scripts/smoke-eval-decorate-approval.mjs scripts/smoke-eval-approval-record.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-agent-target.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs scripts/smoke-prompts.mjs scripts/smoke-process-title.mjs scripts/smoke-predict.mjs scripts/smoke-swarm.mjs scripts/smoke-swarm-follow.mjs",
20
20
  "test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
21
21
  },
22
22
  "devDependencies": {