premanmcp 1.1.16 → 1.1.17

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.
Files changed (3) hide show
  1. package/bin/cli.js +5 -1
  2. package/bin/swarm.js +358 -0
  3. package/package.json +2 -2
package/bin/cli.js CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
37
37
  import { PREDICT_HELP, predictCommand } from "./predict.js";
38
38
  import { STATUS_HELP, statusCommand } from "./status.js";
39
+ import { SWARM_HELP, swarmCommand } from "./swarm.js";
39
40
  import { HOOK_HELP, hookCommand, installHook, scheduleHookRepair } from "./hook.js";
40
41
  import { EVAL_HELP } from "./eval.js";
41
42
  import { TARGET_HELP, targetCommand } from "./target.js";
@@ -142,6 +143,7 @@ function printHelp() {
142
143
  ["", "Chat with the PreMan agent in this terminal"],
143
144
  ["chat [message] [--continue]", "The same session, named explicitly"],
144
145
  ["status", "Healthy / failing / recently fixed endpoints"],
146
+ ["swarm list|launch|watch|stop|bank", "Send real coding agents to see if they pick your MCP tools"],
145
147
  ["verify [options]", "Test endpoints against your local app"],
146
148
  ["probe [options]", "Fire a population of simulated users at your local app"],
147
149
  ["hook install|uninstall|status", "Manage the git pre-push hook"],
@@ -178,7 +180,7 @@ function printHelp() {
178
180
 
179
181
  Usage:
180
182
  ${usageLines}
181
- ${AGENT_HELP}${INTEGRATIONS_HELP}${CONNECT_HELP}${DISPATCH_HELP}${STATUS_HELP}${VERIFY_HELP}${PROBE_HELP}${HOOK_HELP}${RUNNER_HELP}${EVAL_HELP}${TARGET_HELP}${ACCOUNT_HELP}${DESKTOP_HELP}${PREDICT_HELP}${ENDPOINTS_HELP}${TEST_HELP}${TESTS_HELP}
183
+ ${AGENT_HELP}${INTEGRATIONS_HELP}${CONNECT_HELP}${DISPATCH_HELP}${STATUS_HELP}${SWARM_HELP}${VERIFY_HELP}${PROBE_HELP}${HOOK_HELP}${RUNNER_HELP}${EVAL_HELP}${TARGET_HELP}${ACCOUNT_HELP}${DESKTOP_HELP}${PREDICT_HELP}${ENDPOINTS_HELP}${TEST_HELP}${TESTS_HELP}
182
184
  Login options:
183
185
  --email <email> Pre-fill the email prompt
184
186
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
@@ -375,6 +377,8 @@ async function main() {
375
377
  await loginCommand();
376
378
  } else if (command === "status") {
377
379
  await statusCommand(commandArgs);
380
+ } else if (command === "swarm") {
381
+ await swarmCommand(commandArgs);
378
382
  } else if (command === "verify") {
379
383
  await verifyCommand(commandArgs);
380
384
  } else if (command === "probe") {
package/bin/swarm.js ADDED
@@ -0,0 +1,358 @@
1
+ /**
2
+ * `preman swarm` — send real coding agents at a hosted MCP and see whether
3
+ * they pick its tools, from the terminal.
4
+ *
5
+ * A swarm is a harness matrix run, so every call here goes to `/matrix`; the
6
+ * dashboard's Swarms page reads the same runs. What this adds is the swarm's
7
+ * vocabulary: agents grouped by where developers run them (terminal, IDE,
8
+ * desktop app), each with how often it picked the tool, and the rival that
9
+ * won when it did not.
10
+ */
11
+
12
+ import {
13
+ backendUrl,
14
+ callBackendJson,
15
+ cliInvocation,
16
+ describeFailure,
17
+ frontendUrl,
18
+ makeArgs,
19
+ resolveApiKey,
20
+ truncate,
21
+ } from "./shared.js";
22
+
23
+ export const SWARM_HELP = `
24
+ Swarm options:
25
+ preman swarm list Recent swarms and what each found
26
+ preman swarm launch --server <slug|id> --ask "<what a developer asks>" [--ask ...]
27
+ [--expect tool,tool] [--agents claude_code,codex,...] [--rivals 0,5]
28
+ [--size 60] [--name <label>] [--dry-run]
29
+ Start a swarm (--dry-run only counts it)
30
+ preman swarm watch <id> [--once] Follow a swarm until it finishes
31
+ preman swarm stop <id> Stop a swarm; unfinished agents do not run
32
+ preman swarm bank [--server <slug|id>] Behaviors PreMan learned and you kept
33
+ --json Print the raw JSON instead
34
+ `;
35
+
36
+ const ENVIRONMENTS = [
37
+ { id: "terminal", label: "Terminal", harnesses: ["claude_code", "codex", "cursor_cli"] },
38
+ { id: "ide", label: "IDE", harnesses: ["cursor_ide"] },
39
+ { id: "desktop", label: "Desktop app", harnesses: ["claude_desktop", "codex_desktop"] },
40
+ ];
41
+
42
+ const AGENT_NAMES = {
43
+ claude_code: "Claude Code",
44
+ codex: "Codex CLI",
45
+ cursor_cli: "Cursor CLI",
46
+ cursor_ide: "Cursor",
47
+ claude_desktop: "Claude Desktop",
48
+ codex_desktop: "Codex Desktop",
49
+ };
50
+
51
+ const TERMINAL = new Set(["done", "failed", "cancelled"]);
52
+ const POLL_MS = 5000;
53
+
54
+ function environmentOf(harness) {
55
+ return ENVIRONMENTS.find((env) => env.harnesses.includes(harness))?.id || "other";
56
+ }
57
+
58
+ function paintFor(enabled) {
59
+ const wrap = (code) => (text) => (enabled ? `\u001b[${code}m${text}\u001b[0m` : String(text));
60
+ return { green: wrap("32"), red: wrap("31"), yellow: wrap("33"), dim: wrap("2"), bold: wrap("1") };
61
+ }
62
+
63
+ function colourEnabled(args) {
64
+ if (args.has("--no-color") || process.env.NO_COLOR) return false;
65
+ return Boolean(process.stdout.isTTY);
66
+ }
67
+
68
+ /** Every value of a repeatable flag, in order. */
69
+ export function allValues(raw, name) {
70
+ const out = [];
71
+ for (let i = 0; i < raw.length; i += 1) {
72
+ if (raw[i] === name && raw[i + 1] !== undefined) out.push(raw[i + 1]);
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /** Values not taken by a flag: `stop --backend <url> <id>` names the swarm, not the url. */
78
+ export function positionals(raw) {
79
+ const out = [];
80
+ for (let i = 0; i < raw.length; i += 1) {
81
+ if (raw[i].startsWith("--")) {
82
+ if (raw[i + 1] !== undefined && !raw[i + 1].startsWith("--") && !BARE_FLAGS.has(raw[i])) i += 1;
83
+ continue;
84
+ }
85
+ out.push(raw[i]);
86
+ }
87
+ return out;
88
+ }
89
+
90
+ const BARE_FLAGS = new Set(["--json", "--once", "--dry-run", "--no-color"]);
91
+
92
+ function listOf(value) {
93
+ return String(value || "")
94
+ .split(",")
95
+ .map((item) => item.trim())
96
+ .filter(Boolean);
97
+ }
98
+
99
+ /** A Wilson 95% interval, so a small swarm never reads as certain. */
100
+ export function likelyRange(hits, n) {
101
+ if (!n) return null;
102
+ const z = 1.96;
103
+ const p = hits / n;
104
+ const d = 1 + (z * z) / n;
105
+ const c = p + (z * z) / (2 * n);
106
+ const m = z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n));
107
+ return [Math.max(0, (c - m) / d), Math.min(1, (c + m) / d)];
108
+ }
109
+
110
+ const pct = (rate) => (rate === null || rate === undefined ? "—" : `${Math.round(rate * 100)}%`);
111
+
112
+ /**
113
+ * The swarm by environment and agent, from its cells.
114
+ *
115
+ * The same rules as the dashboard: a cell with no verdict is not a miss, and
116
+ * two arms of one harness are two agents whose rates are never pooled.
117
+ */
118
+ export function summarize(cells) {
119
+ const agents = new Map();
120
+ for (const cell of cells || []) {
121
+ const arm = cell.tool_visibility || "deferred";
122
+ const key = `${cell.harness}/${arm}`;
123
+ const agent = agents.get(key) || {
124
+ key,
125
+ harness: cell.harness,
126
+ arm,
127
+ environment: environmentOf(cell.harness),
128
+ picked: 0,
129
+ scored: 0,
130
+ running: 0,
131
+ queued: 0,
132
+ lostTo: new Map(),
133
+ };
134
+ if (cell.status === "running") agent.running += 1;
135
+ else if (cell.status === "queued") agent.queued += 1;
136
+ const unseen = cell.evidence?.tool_surfaced_by_search === false && arm === "deferred";
137
+ if ((cell.selected === true || cell.selected === false) && !unseen) {
138
+ agent.scored += 1;
139
+ if (cell.selected) agent.picked += 1;
140
+ else {
141
+ const expected = new Set([
142
+ ...(cell.evidence?.expected?.tools || []),
143
+ cell.evidence?.expected?.tool,
144
+ ...(cell.evidence?.expected?.alternates || []),
145
+ ].filter(Boolean));
146
+ for (const tool of cell.evidence?.tools_called || []) {
147
+ if (!expected.has(tool)) agent.lostTo.set(tool, (agent.lostTo.get(tool) || 0) + 1);
148
+ }
149
+ }
150
+ }
151
+ agents.set(key, agent);
152
+ }
153
+ const arms = new Map();
154
+ for (const agent of agents.values()) arms.set(agent.harness, (arms.get(agent.harness) || 0) + 1);
155
+ for (const agent of agents.values()) {
156
+ agent.name = AGENT_NAMES[agent.harness] || agent.harness;
157
+ if (arms.get(agent.harness) > 1) {
158
+ agent.name += agent.arm === "deferred" ? " (on demand)" : " (always loaded)";
159
+ }
160
+ agent.rate = agent.scored ? agent.picked / agent.scored : null;
161
+ }
162
+ const groups = [...ENVIRONMENTS, { id: "other", label: "Elsewhere" }]
163
+ .map((env) => ({ ...env, agents: [...agents.values()].filter((a) => a.environment === env.id) }))
164
+ .filter((env) => env.agents.length);
165
+ const picked = [...agents.values()].reduce((sum, a) => sum + a.picked, 0);
166
+ const scored = [...agents.values()].reduce((sum, a) => sum + a.scored, 0);
167
+ return { groups, picked, scored, total: (cells || []).length };
168
+ }
169
+
170
+ export function renderSwarm(run, cells, { colour = false } = {}) {
171
+ const paint = paintFor(colour);
172
+ const summary = summarize(cells);
173
+ const done = (cells || []).filter((cell) => cell.status === "done" || cell.status === "failed").length;
174
+ const lines = [];
175
+ lines.push(
176
+ `${paint.bold(run.release_label || run.id)} ${paint.dim(run.hosted_mcp_name || run.hosted_mcp_slug || "")} ${run.status}`
177
+ );
178
+ if (summary.scored) {
179
+ const range = likelyRange(summary.picked, summary.scored);
180
+ lines.push(
181
+ `${TERMINAL.has(run.status) ? "Agents" : "So far, agents"} picked your tool ${summary.picked} of ${summary.scored} times ` +
182
+ paint.dim(`(likely ${Math.round(range[0] * 100)}–${Math.round(range[1] * 100)}%)`)
183
+ );
184
+ } else {
185
+ lines.push(paint.dim(TERMINAL.has(run.status) ? "Nothing was scored." : "Agents are on their way."));
186
+ }
187
+ lines.push(paint.dim(`${done} of ${summary.total} attempts done`));
188
+ for (const group of summary.groups) {
189
+ lines.push("");
190
+ lines.push(paint.bold(group.label));
191
+ for (const agent of group.agents) {
192
+ 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));
193
+ const working = agent.running ? paint.dim(` · ${agent.running} working`) : "";
194
+ const rival = [...agent.lostTo.entries()].sort((a, b) => b[1] - a[1])[0];
195
+ const lost = rival ? paint.dim(` · lost to ${rival[0]} ×${rival[1]}`) : "";
196
+ lines.push(` ${truncate(agent.name, 30).padEnd(30)} ${String(rate).padStart(colour ? 13 : 4)} ${paint.dim(`${agent.picked}/${agent.scored}`)}${working}${lost}`);
197
+ }
198
+ }
199
+ return `${lines.join("\n")}\n`;
200
+ }
201
+
202
+ function headlineFromRollup(run) {
203
+ const bucket = run?.rollup?.by_harness_visibility || {};
204
+ let hits = 0;
205
+ let n = 0;
206
+ for (const value of Object.values(bucket)) {
207
+ hits += value?.selected?.hits || 0;
208
+ n += value?.selected?.n || 0;
209
+ }
210
+ return n ? `picked ${hits} of ${n}` : "";
211
+ }
212
+
213
+ function requireToken(args) {
214
+ const token = resolveApiKey(args);
215
+ if (!token) {
216
+ throw new Error(`no PreMan API key found. Run \`${cliInvocation()} login\` first, or pass --api-key.`);
217
+ }
218
+ return token;
219
+ }
220
+
221
+ async function call(args, token, method, path, options = {}) {
222
+ const result = await callBackendJson(args, method, path, { token, ...options });
223
+ if (!result.ok) {
224
+ throw new Error(`${method} ${path} on ${backendUrl(args)}: ${result.status_code} ${describeFailure(result, "request failed")}`);
225
+ }
226
+ return result;
227
+ }
228
+
229
+ async function findServer(args, token, ref) {
230
+ if (/^[0-9a-f-]{36}$/i.test(ref)) return { id: ref, name: ref };
231
+ const result = await call(args, token, "GET", "/hosted-mcps");
232
+ const servers = result.hosted_mcps || result.list || [];
233
+ const match = servers.find((server) => [server.slug, server.name, server.id].includes(ref));
234
+ if (!match) {
235
+ const names = servers.map((server) => server.slug || server.name).filter(Boolean).join(", ");
236
+ throw new Error(`no hosted MCP called "${ref}"${names ? `. Yours: ${names}` : ""}`);
237
+ }
238
+ return match;
239
+ }
240
+
241
+ async function readSwarm(args, token, id) {
242
+ const run = await call(args, token, "GET", `/matrix/runs/${encodeURIComponent(id)}`);
243
+ const cells = await call(args, token, "GET", `/matrix/runs/${encodeURIComponent(id)}/cells`);
244
+ const { status_code, ok, ...runBody } = run;
245
+ return { run: runBody, cells: cells.cells || cells.list || [] };
246
+ }
247
+
248
+ function swarmLink(args, id) {
249
+ return `${frontendUrl(args)}/workbench/swarms/swarms/${encodeURIComponent(id)}`;
250
+ }
251
+
252
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
253
+
254
+ export async function swarmCommand(commandArgs = []) {
255
+ const [sub = "list", ...rest] = commandArgs;
256
+ const args = makeArgs(rest);
257
+ const json = args.has("--json");
258
+ const colour = colourEnabled(args);
259
+ const token = requireToken(args);
260
+
261
+ if (sub === "list") {
262
+ const result = await call(args, token, "GET", "/matrix/runs");
263
+ const runs = result.runs || result.list || [];
264
+ if (json) return void process.stdout.write(`${JSON.stringify(runs, null, 2)}\n`);
265
+ if (!runs.length) {
266
+ process.stdout.write(`No swarms yet. Start one with \`${cliInvocation()} swarm launch\`.\n`);
267
+ return;
268
+ }
269
+ for (const run of runs.slice(0, 20)) {
270
+ process.stdout.write(
271
+ `${String(run.id).slice(0, 8)} ${truncate(run.release_label || "", 32).padEnd(32)} ` +
272
+ `${truncate(run.hosted_mcp_name || run.hosted_mcp_slug || "", 20).padEnd(20)} ${String(run.status).padEnd(9)} ${headlineFromRollup(run)}\n`
273
+ );
274
+ }
275
+ return;
276
+ }
277
+
278
+ if (sub === "launch") {
279
+ const asks = allValues(rest, "--ask");
280
+ const serverRef = args.value("--server");
281
+ if (!serverRef || !asks.length) {
282
+ throw new Error(`a swarm needs --server and at least one --ask.${SWARM_HELP}`);
283
+ }
284
+ const server = await findServer(args, token, serverRef);
285
+ const expect = listOf(args.value("--expect"));
286
+ const payload = {
287
+ hosted_mcp_id: server.id,
288
+ asks: asks.map((prompt) => ({ prompt, expected_tools: expect })),
289
+ distractor_levels: listOf(args.value("--rivals", "0,5")).map(Number).filter((n) => Number.isFinite(n)),
290
+ target_size: Number(args.value("--size", "60")),
291
+ release_label: args.value("--name") || `cli ${new Date().toISOString().slice(0, 16).replace("T", " ")}`,
292
+ };
293
+ if (!Number.isInteger(payload.target_size) || payload.target_size < 1) {
294
+ throw new Error("--size must be a whole number of attempts, 1 or more.");
295
+ }
296
+ const agents = listOf(args.value("--agents"));
297
+ if (agents.length) payload.harnesses = agents;
298
+ if (args.has("--dry-run")) payload.dry_run = true;
299
+ const result = await call(args, token, "POST", "/matrix/runs", { json: payload });
300
+ const { status_code, ok, ...body } = result;
301
+ if (json) return void process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
302
+ if (payload.dry_run) {
303
+ process.stdout.write(`Would send ${body.cell_count ?? "?"} attempts (${body.asks_used ?? asks.length} asks, ${body.replicates ?? 1}× each).\n`);
304
+ return;
305
+ }
306
+ process.stdout.write(`Swarm ${body.id} launched against ${server.name || server.slug}.\n`);
307
+ process.stdout.write(` Watch: ${cliInvocation()} swarm watch ${body.id}\n Open: ${swarmLink(args, body.id)}\n`);
308
+ return;
309
+ }
310
+
311
+ if (sub === "watch" || sub === "show") {
312
+ const [id] = positionals(rest);
313
+ if (!id) throw new Error(`which swarm? ${cliInvocation()} swarm watch <id>`);
314
+ for (;;) {
315
+ const { run, cells } = await readSwarm(args, token, id);
316
+ if (json) {
317
+ process.stdout.write(`${JSON.stringify({ run, summary: summarize(cells) }, (k, v) => (v instanceof Map ? Object.fromEntries(v) : v), 2)}\n`);
318
+ } else {
319
+ if (process.stdout.isTTY && sub === "watch") process.stdout.write("\u001b[2J\u001b[H");
320
+ process.stdout.write(renderSwarm(run, cells, { colour }));
321
+ }
322
+ if (sub === "show" || args.has("--once") || TERMINAL.has(run.status)) {
323
+ if (!json) process.stdout.write(`\n${swarmLink(args, id)}\n`);
324
+ return;
325
+ }
326
+ await sleep(POLL_MS);
327
+ }
328
+ }
329
+
330
+ if (sub === "stop") {
331
+ const [id] = positionals(rest);
332
+ if (!id) throw new Error(`which swarm? ${cliInvocation()} swarm stop <id>`);
333
+ await call(args, token, "POST", `/matrix/runs/${encodeURIComponent(id)}/cancel`);
334
+ process.stdout.write(`Stopped ${id}. Agents that had not finished will not run.\n`);
335
+ return;
336
+ }
337
+
338
+ if (sub === "bank") {
339
+ const serverRef = args.value("--server");
340
+ const server = serverRef ? await findServer(args, token, serverRef) : null;
341
+ const result = await call(args, token, "GET", "/swarms/behaviors", {
342
+ query: server ? { hosted_mcp_id: server.id } : undefined,
343
+ });
344
+ const kept = (result.behaviors || []).filter((row) => row.status === "kept");
345
+ if (json) return void process.stdout.write(`${JSON.stringify(kept, null, 2)}\n`);
346
+ if (!kept.length) {
347
+ process.stdout.write("Nothing kept yet. After a swarm, keep the patterns PreMan finds in its misses.\n");
348
+ return;
349
+ }
350
+ for (const row of kept) {
351
+ process.stdout.write(`${row.hosted_mcp_name || ""} ${row.title} (${row.evidence?.count || 0} attempts)\n`);
352
+ for (const ask of (row.seed?.asks || []).slice(0, 3)) process.stdout.write(` “${truncate(ask.prompt, 70)}”\n`);
353
+ }
354
+ return;
355
+ }
356
+
357
+ throw new Error(`unknown swarm command "${sub}".${SWARM_HELP}`);
358
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "1.1.16",
3
+ "version": "1.1.17",
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",
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",
20
20
  "test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
21
21
  },
22
22
  "devDependencies": {