@wrongstack/acp 0.295.0 → 0.296.2

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 (49) hide show
  1. package/dist/agent/protocol-handler.d.ts +9 -0
  2. package/dist/agent/protocol-handler.d.ts.map +1 -1
  3. package/dist/agent/server-agent-turn.d.ts +83 -1
  4. package/dist/agent/server-agent-turn.d.ts.map +1 -1
  5. package/dist/agent/session-store.d.ts.map +1 -1
  6. package/dist/agent/stdio-transport.d.ts +18 -0
  7. package/dist/agent/stdio-transport.d.ts.map +1 -1
  8. package/dist/agent/wrongstack-acp-agent.d.ts +35 -0
  9. package/dist/agent/wrongstack-acp-agent.d.ts.map +1 -1
  10. package/dist/agent.js +30 -23
  11. package/dist/agent.js.map +3 -3
  12. package/dist/client/acp-message-routing.d.ts +2 -0
  13. package/dist/client/acp-message-routing.d.ts.map +1 -0
  14. package/dist/client/acp-session-callbacks.d.ts +12 -0
  15. package/dist/client/acp-session-callbacks.d.ts.map +1 -0
  16. package/dist/client/acp-session-content.d.ts +24 -0
  17. package/dist/client/acp-session-content.d.ts.map +1 -0
  18. package/dist/client/acp-session-errors.d.ts +13 -0
  19. package/dist/client/acp-session-errors.d.ts.map +1 -0
  20. package/dist/client/acp-session-types.d.ts +81 -0
  21. package/dist/client/acp-session-types.d.ts.map +1 -0
  22. package/dist/client/acp-session-updates.d.ts +18 -0
  23. package/dist/client/acp-session-updates.d.ts.map +1 -0
  24. package/dist/client/acp-session.d.ts +6 -171
  25. package/dist/client/acp-session.d.ts.map +1 -1
  26. package/dist/client/file-server.d.ts +19 -0
  27. package/dist/client/file-server.d.ts.map +1 -1
  28. package/dist/client/terminal-server.d.ts.map +1 -1
  29. package/dist/client/trust-boundary-permission.d.ts +16 -1
  30. package/dist/client/trust-boundary-permission.d.ts.map +1 -1
  31. package/dist/client/websocket-transport.d.ts.map +1 -1
  32. package/dist/client.js +358 -348
  33. package/dist/client.js.map +3 -3
  34. package/dist/index.js +409 -395
  35. package/dist/index.js.map +4 -4
  36. package/dist/integration/acp-bench.d.ts +32 -0
  37. package/dist/integration/acp-bench.d.ts.map +1 -1
  38. package/dist/integration/acp-subagent-runner.d.ts.map +1 -1
  39. package/dist/integration/ensemble-runner.d.ts +22 -1
  40. package/dist/integration/ensemble-runner.d.ts.map +1 -1
  41. package/dist/registry/acp-registry-fetch.d.ts +1 -1
  42. package/dist/registry/acp-registry-fetch.d.ts.map +1 -1
  43. package/dist/registry/ensemble-registry.d.ts +22 -0
  44. package/dist/registry/ensemble-registry.d.ts.map +1 -1
  45. package/dist/version.d.ts +8 -0
  46. package/dist/version.d.ts.map +1 -1
  47. package/dist/wrongstack-acp-agent.js +26 -20
  48. package/dist/wrongstack-acp-agent.js.map +2 -2
  49. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -9,9 +9,9 @@ function assertNeverSessionUpdate(x) {
9
9
  // src/version.ts
10
10
  import { createRequire } from "node:module";
11
11
  var require2 = createRequire(import.meta.url);
12
- function readPackageVersion() {
12
+ function readPackageVersion(load = () => require2("../package.json")) {
13
13
  try {
14
- const packageJson = require2("../package.json");
14
+ const packageJson = load();
15
15
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
16
16
  return packageJson.version;
17
17
  }
@@ -79,7 +79,7 @@ var ACPProtocolHandler = class {
79
79
  this.replayFor = opts.replayFor;
80
80
  this.seedFor = opts.seedFor;
81
81
  this.disposeFor = opts.disposeFor;
82
- this.maxSessions = Number.isFinite(opts.maxSessions) && (opts.maxSessions ?? 0) > 0 ? Math.floor(opts.maxSessions) : DEFAULT_MAX_SESSIONS;
82
+ this.maxSessions = Number.isFinite(opts.maxSessions) && opts.maxSessions > 0 ? Math.floor(opts.maxSessions) : DEFAULT_MAX_SESSIONS;
83
83
  this.store = opts.store;
84
84
  if (typeof this.transport.onMessage === "function") {
85
85
  this.transport.onMessage((m) => this.maybeResolvePending(m));
@@ -361,7 +361,7 @@ var ACPProtocolHandler = class {
361
361
  }
362
362
  if (existing) {
363
363
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
364
- const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
364
+ const replay = this.replayFor?.(sessionId);
365
365
  if (replay) {
366
366
  for (const update of replay) {
367
367
  await this.sendNotification({ sessionId, update });
@@ -426,10 +426,8 @@ var ACPProtocolHandler = class {
426
426
  return false;
427
427
  }
428
428
  session.abort.abort();
429
- if (sessionId) {
430
- this.sessions.delete(sessionId);
431
- this.disposeSession(sessionId);
432
- }
429
+ this.sessions.delete(sessionId);
430
+ this.disposeSession(sessionId);
433
431
  await this.transport.send(toWire({
434
432
  jsonrpc: "2.0",
435
433
  id,
@@ -738,6 +736,7 @@ function errorToJsonRpc(err) {
738
736
 
739
737
  // src/agent/stdio-transport.ts
740
738
  import { expectDefined, writeErr } from "@wrongstack/core/utils";
739
+ import { treeKill } from "@wrongstack/core/utils/tree-kill";
741
740
 
742
741
  // src/win32-cmd.ts
743
742
  var WIN32_CMD_META = /[&|<>"\r\n\0]/;
@@ -767,7 +766,7 @@ function quoteWin32CmdArg(arg) {
767
766
  var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
768
767
  var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
769
768
  function positiveLimit(value, fallback) {
770
- return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
769
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
771
770
  }
772
771
  var StdioTransport = class {
773
772
  stdin = process.stdin;
@@ -782,10 +781,7 @@ var StdioTransport = class {
782
781
  maxQueuedMessages;
783
782
  constructor(opts = {}) {
784
783
  this.maxFrameChars = positiveLimit(opts.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
785
- this.maxQueuedMessages = positiveLimit(
786
- opts.maxQueuedMessages,
787
- DEFAULT_MAX_QUEUED_MESSAGES
788
- );
784
+ this.maxQueuedMessages = positiveLimit(opts.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
789
785
  this.stdin.resume();
790
786
  this.stdin.setEncoding("utf8");
791
787
  this.stdin.on("data", (chunk) => this.onData(chunk));
@@ -806,7 +802,8 @@ var StdioTransport = class {
806
802
  this.stdout.write(chunk, "utf8");
807
803
  }
808
804
  read() {
809
- if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
805
+ if (this.messageQueue.length > 0)
806
+ return Promise.resolve(expectDefined(this.messageQueue.shift()));
810
807
  if (this.closed) return Promise.resolve(null);
811
808
  return new Promise((resolve3) => {
812
809
  this.resolveRead = resolve3;
@@ -908,10 +905,7 @@ var ClientTransport = class {
908
905
  ...options
909
906
  };
910
907
  this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
911
- this.maxQueuedMessages = positiveLimit(
912
- options.maxQueuedMessages,
913
- DEFAULT_MAX_QUEUED_MESSAGES
914
- );
908
+ this.maxQueuedMessages = positiveLimit(options.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
915
909
  }
916
910
  async start() {
917
911
  if (this.child) return;
@@ -930,13 +924,13 @@ var ClientTransport = class {
930
924
  const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
931
925
  try {
932
926
  const childArgs = this.opts.args ?? [];
933
- const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(this.opts.command, childArgs) : null;
934
- this.child = spawn3(shim?.command ?? this.opts.command, shim?.args ?? childArgs, {
927
+ const invocation = spawnInvocation(this.opts.command, childArgs, process.platform);
928
+ this.child = spawn3(invocation.command, invocation.args, {
935
929
  env: { ...buildChildEnv2(), ...this.opts.env },
936
930
  cwd: spawnCwd,
937
931
  stdio: ["pipe", "pipe", "pipe"],
938
932
  windowsHide: true,
939
- ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
933
+ ...verbatimOptions(invocation)
940
934
  });
941
935
  } catch (err) {
942
936
  clearTimeout(timeout);
@@ -980,6 +974,9 @@ var ClientTransport = class {
980
974
  };
981
975
  const waitForMarker = (chunk) => {
982
976
  this.buffer += chunk;
977
+ if (this.buffer.length > this.maxFrameChars) {
978
+ this.buffer = this.buffer.slice(-this.maxFrameChars);
979
+ }
983
980
  const idx = this.buffer.indexOf("[wstack-acp]\n");
984
981
  if (idx !== -1) {
985
982
  this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
@@ -1001,7 +998,8 @@ var ClientTransport = class {
1001
998
  });
1002
999
  }
1003
1000
  read() {
1004
- if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
1001
+ if (this.messageQueue.length > 0)
1002
+ return Promise.resolve(expectDefined(this.messageQueue.shift()));
1005
1003
  if (this.closed) return Promise.resolve(null);
1006
1004
  return new Promise((resolve3) => {
1007
1005
  this.resolveRead = resolve3;
@@ -1020,10 +1018,7 @@ var ClientTransport = class {
1020
1018
  this.handlers.clear();
1021
1019
  const child = this.child;
1022
1020
  if (!child) return;
1023
- try {
1024
- child.kill();
1025
- } catch {
1026
- }
1021
+ treeKill(child);
1027
1022
  this.child = null;
1028
1023
  }
1029
1024
  onChildData(chunk) {
@@ -1087,6 +1082,13 @@ var ClientTransport = class {
1087
1082
  }
1088
1083
  }
1089
1084
  };
1085
+ function spawnInvocation(command, args, platform) {
1086
+ if (platform !== "win32") return { command, args };
1087
+ return buildWin32CmdShimInvocation(command, args);
1088
+ }
1089
+ function verbatimOptions(invocation) {
1090
+ return invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: invocation.windowsVerbatimArguments } : {};
1091
+ }
1090
1092
 
1091
1093
  // src/agent/tools-registry.ts
1092
1094
  var ACPToolsRegistry = class {
@@ -1203,8 +1205,8 @@ function toolToPriority(tool) {
1203
1205
  }
1204
1206
 
1205
1207
  // src/agent/wrongstack-acp-agent.ts
1206
- import { fileURLToPath } from "node:url";
1207
1208
  import { createServer } from "node:http";
1209
+ import { fileURLToPath } from "node:url";
1208
1210
  import { writeErr as writeErr2 } from "@wrongstack/core/utils";
1209
1211
  var WrongStackACPServer = class {
1210
1212
  transport;
@@ -1265,10 +1267,9 @@ var WrongStackACPServer = class {
1265
1267
  let httpChain = Promise.resolve();
1266
1268
  this.httpServer = createServer(async (req, res) => {
1267
1269
  if (authToken) {
1268
- const url = new URL(req.url ?? "/", `http://${host}:${port}`);
1270
+ const url = new URL(requestPath(req.url), `http://${host}:${port}`);
1269
1271
  const queryToken = url.searchParams.get("token");
1270
- const authHeader = req.headers["authorization"];
1271
- const bearerToken = Array.isArray(authHeader) ? authHeader[0]?.replace(/^Bearer\s+/i, "") : authHeader?.replace(/^Bearer\s+/i, "");
1272
+ const bearerToken = headerValue(req.headers.authorization)?.replace(/^Bearer\s+/i, "");
1272
1273
  const supplied = queryToken ?? bearerToken ?? "";
1273
1274
  if (supplied !== authToken) {
1274
1275
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -1277,7 +1278,7 @@ var WrongStackACPServer = class {
1277
1278
  }
1278
1279
  }
1279
1280
  const selfOrigin = `http://${host}:${port}`;
1280
- const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
1281
+ const reqOrigin = headerValue(req.headers.origin);
1281
1282
  if (reqOrigin && reqOrigin !== selfOrigin) {
1282
1283
  res.writeHead(403);
1283
1284
  res.end(JSON.stringify({ error: "cross-origin request forbidden" }));
@@ -1357,6 +1358,8 @@ var WrongStackACPServer = class {
1357
1358
  try {
1358
1359
  await requestPromise;
1359
1360
  } catch {
1361
+ res.writeHead(500, { "Content-Type": "application/json" });
1362
+ res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
1360
1363
  }
1361
1364
  });
1362
1365
  return new Promise((resolve3) => {
@@ -1382,6 +1385,12 @@ var WrongStackACPServer = class {
1382
1385
  var defaultEchoRunTurn = async (_input, _emit) => {
1383
1386
  return { stopReason: "end_turn" };
1384
1387
  };
1388
+ function headerValue(value) {
1389
+ return Array.isArray(value) ? value[0] : value;
1390
+ }
1391
+ function requestPath(value) {
1392
+ return value ?? "/";
1393
+ }
1385
1394
  async function main() {
1386
1395
  const server = new WrongStackACPServer();
1387
1396
  await server.start();
@@ -1395,11 +1404,52 @@ if (isEntrypoint) {
1395
1404
  });
1396
1405
  }
1397
1406
 
1407
+ // src/client/acp-session-content.ts
1408
+ function textContent(text) {
1409
+ return { type: "text", text };
1410
+ }
1411
+ function imageContent(mimeType, data) {
1412
+ return { type: "image", mimeType, data };
1413
+ }
1414
+ function audioContent(mimeType, data) {
1415
+ return { type: "audio", mimeType, data };
1416
+ }
1417
+ function extractText(block) {
1418
+ if (typeof block !== "object" || block === null) return "";
1419
+ const b = block;
1420
+ if (b.type === "text" && typeof b.text === "string") return b.text;
1421
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
1422
+ return b.resource.text;
1423
+ }
1424
+ return "";
1425
+ }
1426
+ function isRecord(v) {
1427
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1428
+ }
1429
+ function emptyRunResult(stopReason) {
1430
+ return {
1431
+ text: "",
1432
+ stopReason,
1433
+ hasText: false,
1434
+ toolCalls: [],
1435
+ diffs: [],
1436
+ thoughts: ""
1437
+ };
1438
+ }
1439
+
1398
1440
  // src/client/file-server.ts
1399
1441
  import { randomBytes } from "node:crypto";
1400
1442
  import { realpathSync } from "node:fs";
1401
1443
  import * as fsp from "node:fs/promises";
1402
1444
  import * as path from "node:path";
1445
+ var DEFAULT_FILE_OPERATIONS = {
1446
+ stat: fsp.stat,
1447
+ readFile: fsp.readFile,
1448
+ writeFile: fsp.writeFile,
1449
+ realpath: fsp.realpath,
1450
+ rename: fsp.rename,
1451
+ unlink: fsp.unlink
1452
+ };
1403
1453
  var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
1404
1454
  var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
1405
1455
  var FsError = class extends Error {
@@ -1418,12 +1468,14 @@ var FileServer = class {
1418
1468
  timeoutMs;
1419
1469
  maxReadBytes;
1420
1470
  maxWriteBytes;
1471
+ operations;
1421
1472
  constructor(opts) {
1422
1473
  this.root = path.resolve(opts.projectRoot);
1423
1474
  this.realRoot = safeRealpathSync(this.root);
1424
1475
  this.timeoutMs = opts.timeoutMs ?? 3e4;
1425
1476
  this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
1426
1477
  this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
1478
+ this.operations = opts.operations ?? DEFAULT_FILE_OPERATIONS;
1427
1479
  }
1428
1480
  /** Read a text file. Returns the content as a string. */
1429
1481
  async readTextFile(params) {
@@ -1431,7 +1483,7 @@ var FileServer = class {
1431
1483
  const controller = new AbortController();
1432
1484
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1433
1485
  try {
1434
- const stat2 = await fsp.stat(safe).catch((err) => {
1486
+ const stat2 = await this.operations.stat(safe).catch((err) => {
1435
1487
  throw mapFsError(err, safe);
1436
1488
  });
1437
1489
  if (stat2.size > this.maxReadBytes) {
@@ -1441,7 +1493,7 @@ var FileServer = class {
1441
1493
  `file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
1442
1494
  );
1443
1495
  }
1444
- const content = await fsp.readFile(safe, {
1496
+ const content = await this.operations.readFile(safe, {
1445
1497
  encoding: "utf8",
1446
1498
  signal: controller.signal
1447
1499
  });
@@ -1471,20 +1523,20 @@ var FileServer = class {
1471
1523
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1472
1524
  const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
1473
1525
  try {
1474
- await fsp.writeFile(tmp, params.content, {
1526
+ await this.operations.writeFile(tmp, params.content, {
1475
1527
  encoding: "utf8",
1476
1528
  signal: controller.signal
1477
1529
  });
1478
1530
  await this.assertRealInside(tmp);
1479
1531
  await this.assertRealInside(path.dirname(safe));
1480
- await fsp.rename(tmp, safe);
1532
+ await this.operations.rename(tmp, safe);
1481
1533
  } catch (err) {
1482
1534
  if (err instanceof FsError) {
1483
- await fsp.unlink(tmp).catch(() => void 0);
1535
+ await this.operations.unlink(tmp).catch(() => void 0);
1484
1536
  throw err;
1485
1537
  }
1486
1538
  try {
1487
- await fsp.unlink(tmp);
1539
+ await this.operations.unlink(tmp);
1488
1540
  } catch {
1489
1541
  }
1490
1542
  if (controller.signal.aborted) {
@@ -1528,12 +1580,14 @@ var FileServer = class {
1528
1580
  for (; ; ) {
1529
1581
  let real;
1530
1582
  try {
1531
- real = await fsp.realpath(probe);
1583
+ real = await this.operations.realpath(probe);
1532
1584
  } catch (err) {
1533
1585
  const code = err.code;
1534
1586
  if (code === "ENOENT") {
1535
1587
  const parent = path.dirname(probe);
1536
- if (parent === probe) return;
1588
+ if (parent === probe) {
1589
+ throw new FsError("ENOENT", resolvedPath, `no existing ancestor: ${resolvedPath}`);
1590
+ }
1537
1591
  probe = parent;
1538
1592
  continue;
1539
1593
  }
@@ -1631,7 +1685,6 @@ var TerminalServer = class {
1631
1685
  this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
1632
1686
  this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
1633
1687
  this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
1634
- if (this.maxTerminals < 1) throw new RangeError("maxTerminals must be at least 1");
1635
1688
  this.abortSignal = opts.signal;
1636
1689
  if (opts.signal) {
1637
1690
  opts.signal.addEventListener("abort", this.abortHandler, { once: true });
@@ -1962,7 +2015,6 @@ var WebSocketClientTransport = class {
1962
2015
  const ws = new WS(this.opts.url, this.opts.protocols);
1963
2016
  this.ws = ws;
1964
2017
  const timer = setTimeout(() => {
1965
- if (settled) return;
1966
2018
  settled = true;
1967
2019
  try {
1968
2020
  ws.close();
@@ -2000,7 +2052,7 @@ var WebSocketClientTransport = class {
2000
2052
  }
2001
2053
  try {
2002
2054
  const serialized = JSON.stringify(msg);
2003
- const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount ?? 0 : 0;
2055
+ const buffered = Number.isFinite(this.ws.bufferedAmount) ? this.ws.bufferedAmount : 0;
2004
2056
  if (buffered + Buffer.byteLength(serialized, "utf8") > this.maxBufferedBytes) {
2005
2057
  this.stop();
2006
2058
  return Promise.reject(new Error("WebSocket transport send buffer limit exceeded"));
@@ -2057,10 +2109,10 @@ var WebSocketClientTransport = class {
2057
2109
  }
2058
2110
  };
2059
2111
  function finitePositiveLimit(value, fallback) {
2060
- return Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
2112
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
2061
2113
  }
2062
2114
 
2063
- // src/client/acp-session.ts
2115
+ // src/client/acp-session-errors.ts
2064
2116
  var ACPSessionError = class extends Error {
2065
2117
  kind;
2066
2118
  cause;
@@ -2074,6 +2126,264 @@ var ACPSessionError = class extends Error {
2074
2126
  function isJsonRpcError(v) {
2075
2127
  return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
2076
2128
  }
2129
+
2130
+ // src/client/acp-session-updates.ts
2131
+ function createSessionScratch() {
2132
+ return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2133
+ }
2134
+ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2135
+ const update = msg.params?.update;
2136
+ if (typeof update !== "object" || update === null) return;
2137
+ const u = update;
2138
+ emitProgress({ type: "raw", update: u });
2139
+ switch (u.sessionUpdate) {
2140
+ case "agent_message_chunk": {
2141
+ const text = extractText(u.content);
2142
+ if (text) {
2143
+ scratch.text += text;
2144
+ emitProgress({ type: "message", text });
2145
+ }
2146
+ return;
2147
+ }
2148
+ case "thought_chunk": {
2149
+ const text = extractText(u.content);
2150
+ if (text) {
2151
+ scratch.thoughts += text;
2152
+ emitProgress({ type: "thought", text });
2153
+ }
2154
+ return;
2155
+ }
2156
+ case "tool_call":
2157
+ case "tool_call_update":
2158
+ captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress);
2159
+ return;
2160
+ case "plan":
2161
+ if (Array.isArray(u.entries)) {
2162
+ scratch.plan = u.entries;
2163
+ emitProgress({ type: "plan", entries: u.entries });
2164
+ }
2165
+ return;
2166
+ case "usage_update":
2167
+ if (typeof u.used === "number" && typeof u.size === "number") {
2168
+ const usage = {
2169
+ used: u.used,
2170
+ size: u.size,
2171
+ ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2172
+ };
2173
+ scratch.usage = usage;
2174
+ emitProgress({ type: "usage", usage });
2175
+ }
2176
+ return;
2177
+ case "available_commands_update":
2178
+ case "current_mode_update":
2179
+ case "config_option_update":
2180
+ case "session_info_update":
2181
+ case "user_message_chunk":
2182
+ case "next_edit_suggestions":
2183
+ case "elicitation":
2184
+ return;
2185
+ default:
2186
+ return;
2187
+ }
2188
+ }
2189
+ function captureToolCall(u, isNew, scratch, emitProgress) {
2190
+ const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2191
+ if (!toolCallId) return;
2192
+ const prev = scratch.toolCalls.get(toolCallId);
2193
+ const record = {
2194
+ toolCallId,
2195
+ title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
2196
+ kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2197
+ status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2198
+ rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2199
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2200
+ };
2201
+ scratch.toolCalls.set(toolCallId, record);
2202
+ if (Array.isArray(u.content)) {
2203
+ for (const c of u.content) {
2204
+ if (c && typeof c === "object" && c.type === "diff") {
2205
+ const diff = {
2206
+ path: c.path,
2207
+ oldText: c.oldText,
2208
+ newText: c.newText
2209
+ };
2210
+ scratch.diffs.push(diff);
2211
+ emitProgress({ type: "diff", diff });
2212
+ }
2213
+ }
2214
+ }
2215
+ emitProgress({
2216
+ type: isNew ? "tool_call" : "tool_call_update",
2217
+ toolCall: record
2218
+ });
2219
+ }
2220
+
2221
+ // src/client/acp-session-callbacks.ts
2222
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender) {
2223
+ const id = msg.id;
2224
+ if (id === void 0) return;
2225
+ const params = msg.params;
2226
+ const toolCall = params?.toolCall;
2227
+ const options = Array.isArray(params?.options) ? params.options : [];
2228
+ if (!toolCall) {
2229
+ await sender.sendErrorResponse(id, -32602, "toolCall is required");
2230
+ return;
2231
+ }
2232
+ const policyAbort = new AbortController();
2233
+ try {
2234
+ const outcome = await permissionPolicy({
2235
+ toolCall,
2236
+ options,
2237
+ signal: policyAbort.signal
2238
+ });
2239
+ await sender.sendResult(id, { outcome });
2240
+ } catch (err) {
2241
+ const message = err instanceof Error ? err.message : String(err);
2242
+ await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2243
+ }
2244
+ }
2245
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
2246
+ const id = msg.id;
2247
+ if (id === void 0) return;
2248
+ const params = msg.params;
2249
+ if (!params?.path) {
2250
+ await sender.sendErrorResponse(id, -32602, "path is required");
2251
+ return;
2252
+ }
2253
+ if (msg.method === "fs/write_text_file") {
2254
+ const allowed = await authorizeAcpCallback(permissionPolicy, {
2255
+ toolCallId: `acp-fs-write-${id}`,
2256
+ title: `Write file: ${params.path}`,
2257
+ kind: "edit",
2258
+ rawInput: { path: params.path, sessionId: params.sessionId }
2259
+ });
2260
+ if (!allowed) {
2261
+ await sender.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
2262
+ return;
2263
+ }
2264
+ }
2265
+ try {
2266
+ if (msg.method === "fs/read_text_file") {
2267
+ const result = await fileServer.readTextFile({
2268
+ sessionId: params.sessionId ?? "",
2269
+ path: params.path
2270
+ });
2271
+ await sender.sendResult(id, result);
2272
+ } else {
2273
+ await fileServer.writeTextFile({
2274
+ sessionId: params.sessionId ?? "",
2275
+ path: params.path,
2276
+ content: params.content ?? ""
2277
+ });
2278
+ await sender.sendResult(id, {});
2279
+ }
2280
+ } catch (err) {
2281
+ const code = err instanceof FsError ? -32602 : -32603;
2282
+ const message = err instanceof Error ? err.message : String(err);
2283
+ await sender.sendErrorResponse(id, code, message);
2284
+ }
2285
+ }
2286
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender) {
2287
+ const id = msg.id;
2288
+ if (id === void 0) return;
2289
+ const params = msg.params ?? {};
2290
+ try {
2291
+ switch (msg.method) {
2292
+ case "terminal/create": {
2293
+ const allowed = await authorizeAcpCallback(permissionPolicy, {
2294
+ toolCallId: `acp-terminal-create-${id}`,
2295
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
2296
+ kind: "execute",
2297
+ rawInput: {
2298
+ command: params.command,
2299
+ args: params.args,
2300
+ cwd: params.cwd,
2301
+ sessionId: params.sessionId
2302
+ }
2303
+ });
2304
+ if (!allowed) {
2305
+ await sender.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
2306
+ return;
2307
+ }
2308
+ const createOpts = {
2309
+ sessionId: String(params.sessionId ?? ""),
2310
+ command: String(params.command ?? ""),
2311
+ args: Array.isArray(params.args) ? params.args : []
2312
+ };
2313
+ if (Array.isArray(params.env)) {
2314
+ createOpts.env = params.env;
2315
+ }
2316
+ if (typeof params.cwd === "string") {
2317
+ createOpts.cwd = params.cwd;
2318
+ }
2319
+ if (typeof params.outputByteLimit === "number") {
2320
+ createOpts.outputByteLimit = params.outputByteLimit;
2321
+ }
2322
+ const result = terminalServer.create(createOpts);
2323
+ await sender.sendResult(id, result);
2324
+ return;
2325
+ }
2326
+ case "terminal/output": {
2327
+ const terminalId = String(params.terminalId ?? "");
2328
+ const out = terminalServer.output(terminalId);
2329
+ await sender.sendResult(id, out);
2330
+ return;
2331
+ }
2332
+ case "terminal/wait_for_exit": {
2333
+ const terminalId = String(params.terminalId ?? "");
2334
+ const exit = await terminalServer.waitForExit(terminalId);
2335
+ await sender.sendResult(id, exit);
2336
+ return;
2337
+ }
2338
+ case "terminal/kill": {
2339
+ const terminalId = String(params.terminalId ?? "");
2340
+ terminalServer.kill(terminalId);
2341
+ await sender.sendResult(id, {});
2342
+ return;
2343
+ }
2344
+ case "terminal/release": {
2345
+ const terminalId = String(params.terminalId ?? "");
2346
+ terminalServer.release(terminalId);
2347
+ await sender.sendResult(id, {});
2348
+ return;
2349
+ }
2350
+ default:
2351
+ await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
2352
+ }
2353
+ } catch (err) {
2354
+ const message = err instanceof Error ? err.message : String(err);
2355
+ await sender.sendErrorResponse(id, -32603, message);
2356
+ }
2357
+ }
2358
+ async function authorizeAcpCallback(permissionPolicy, partial) {
2359
+ try {
2360
+ const outcome = await permissionPolicy({
2361
+ toolCall: {
2362
+ sessionUpdate: "tool_call_update",
2363
+ toolCallId: partial.toolCallId,
2364
+ title: partial.title,
2365
+ kind: partial.kind,
2366
+ status: "pending",
2367
+ ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2368
+ },
2369
+ options: [
2370
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
2371
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
2372
+ ],
2373
+ signal: new AbortController().signal
2374
+ });
2375
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
2376
+ } catch {
2377
+ return false;
2378
+ }
2379
+ }
2380
+
2381
+ // src/client/acp-message-routing.ts
2382
+ function isBestEffortAckMethod(method) {
2383
+ return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
2384
+ }
2385
+
2386
+ // src/client/acp-session.ts
2077
2387
  var ACPSession = class _ACPSession {
2078
2388
  transport;
2079
2389
  fileServer;
@@ -2767,6 +3077,12 @@ var ACPSession = class _ACPSession {
2767
3077
  error: { code, message }
2768
3078
  });
2769
3079
  }
3080
+ responseSender() {
3081
+ return {
3082
+ sendResult: (id, result) => this.sendResult(id, result),
3083
+ sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
3084
+ };
3085
+ }
2770
3086
  handleMessage(msg) {
2771
3087
  if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
2772
3088
  const pending = this.pending.get(msg.id);
@@ -2781,29 +3097,32 @@ var ACPSession = class _ACPSession {
2781
3097
  return;
2782
3098
  }
2783
3099
  if (msg.method === "session/update") {
2784
- this.handleUpdate(msg);
3100
+ handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
2785
3101
  return;
2786
3102
  }
2787
3103
  if (msg.method === "session/request_permission") {
2788
- void this.handlePermissionRequest(msg);
3104
+ void handleAcpPermissionRequest(msg, this.permissionPolicy, this.responseSender());
2789
3105
  return;
2790
3106
  }
2791
3107
  if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
2792
- void this.handleFsRequest(msg);
3108
+ void handleAcpFsRequest(
3109
+ msg,
3110
+ this.fileServer,
3111
+ this.permissionPolicy,
3112
+ this.responseSender()
3113
+ );
2793
3114
  return;
2794
3115
  }
2795
3116
  if (msg.method?.startsWith("terminal/")) {
2796
- void this.handleTerminalRequest(msg);
2797
- return;
2798
- }
2799
- if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
2800
- if (msg.id !== void 0) {
2801
- this.sendResult(msg.id, {}).catch(() => {
2802
- });
2803
- }
3117
+ void handleAcpTerminalRequest(
3118
+ msg,
3119
+ this.terminalServer,
3120
+ this.permissionPolicy,
3121
+ this.responseSender()
3122
+ );
2804
3123
  return;
2805
3124
  }
2806
- if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
3125
+ if (isBestEffortAckMethod(msg.method)) {
2807
3126
  if (msg.id !== void 0) {
2808
3127
  this.sendResult(msg.id, {}).catch(() => {
2809
3128
  });
@@ -2824,98 +3143,6 @@ var ACPSession = class _ACPSession {
2824
3143
  );
2825
3144
  }
2826
3145
  }
2827
- handleUpdate(msg) {
2828
- const update = msg.params?.update;
2829
- if (typeof update !== "object" || update === null) return;
2830
- const u = update;
2831
- this.emitProgress({ type: "raw", update: u });
2832
- switch (u.sessionUpdate) {
2833
- case "agent_message_chunk": {
2834
- const text = extractText(u.content);
2835
- if (text) {
2836
- this.scratch.text += text;
2837
- this.emitProgress({ type: "message", text });
2838
- }
2839
- return;
2840
- }
2841
- case "thought_chunk": {
2842
- const text = extractText(u.content);
2843
- if (text) {
2844
- this.scratch.thoughts += text;
2845
- this.emitProgress({ type: "thought", text });
2846
- }
2847
- return;
2848
- }
2849
- case "tool_call":
2850
- case "tool_call_update": {
2851
- this.captureToolCall(u, u.sessionUpdate === "tool_call");
2852
- return;
2853
- }
2854
- case "plan":
2855
- if (Array.isArray(u.entries)) {
2856
- this.scratch.plan = u.entries;
2857
- this.emitProgress({ type: "plan", entries: u.entries });
2858
- }
2859
- return;
2860
- case "usage_update":
2861
- if (typeof u.used === "number" && typeof u.size === "number") {
2862
- const usage = {
2863
- used: u.used,
2864
- size: u.size,
2865
- ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2866
- };
2867
- this.scratch.usage = usage;
2868
- this.emitProgress({ type: "usage", usage });
2869
- }
2870
- return;
2871
- case "available_commands_update":
2872
- case "current_mode_update":
2873
- case "config_option_update":
2874
- case "session_info_update":
2875
- case "user_message_chunk":
2876
- case "next_edit_suggestions":
2877
- case "elicitation":
2878
- return;
2879
- default:
2880
- return;
2881
- }
2882
- }
2883
- /**
2884
- * Fold a `tool_call` / `tool_call_update` notification into the scratch
2885
- * tool-call map (deduped by toolCallId), extract any `diff` content into
2886
- * the diffs list, and emit live progress.
2887
- */
2888
- captureToolCall(u, isNew) {
2889
- const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2890
- if (!toolCallId) return;
2891
- const prev = this.scratch.toolCalls.get(toolCallId);
2892
- const record = {
2893
- toolCallId,
2894
- title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
2895
- kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2896
- status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2897
- rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2898
- rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2899
- };
2900
- this.scratch.toolCalls.set(toolCallId, record);
2901
- if (Array.isArray(u.content)) {
2902
- for (const c of u.content) {
2903
- if (c && typeof c === "object" && c.type === "diff") {
2904
- const diff = {
2905
- path: c.path,
2906
- oldText: c.oldText,
2907
- newText: c.newText
2908
- };
2909
- this.scratch.diffs.push(diff);
2910
- this.emitProgress({ type: "diff", diff });
2911
- }
2912
- }
2913
- }
2914
- this.emitProgress({
2915
- type: isNew ? "tool_call" : "tool_call_update",
2916
- toolCall: record
2917
- });
2918
- }
2919
3146
  emitProgress(event) {
2920
3147
  if (!this.progressHandler) return;
2921
3148
  try {
@@ -2926,217 +3153,11 @@ var ACPSession = class _ACPSession {
2926
3153
  /** Live progress handler installed for the duration of a `prompt()` turn. */
2927
3154
  progressHandler = null;
2928
3155
  // Per-prompt scratch state
2929
- scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
3156
+ scratch = createSessionScratch();
2930
3157
  resetScratch() {
2931
- this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2932
- }
2933
- async handlePermissionRequest(msg) {
2934
- const id = msg.id;
2935
- if (id === void 0) return;
2936
- const params = msg.params;
2937
- const toolCall = params?.toolCall;
2938
- const options = Array.isArray(params?.options) ? params.options : [];
2939
- if (!toolCall) {
2940
- await this.sendErrorResponse(id, -32602, "toolCall is required");
2941
- return;
2942
- }
2943
- const policyAbort = new AbortController();
2944
- try {
2945
- const outcome = await this.permissionPolicy({
2946
- toolCall,
2947
- options,
2948
- signal: policyAbort.signal
2949
- });
2950
- await this.sendResult(id, { outcome });
2951
- } catch (err) {
2952
- const message = err instanceof Error ? err.message : String(err);
2953
- await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2954
- }
2955
- }
2956
- /**
2957
- * Enforce authorization at privileged callback sinks (fs/write,
2958
- * terminal/create). Unlike `handlePermissionRequest` which responds to
2959
- * agent-initiated `session/request_permission` messages, this method is
2960
- * called by the handler BEFORE dispatching to FileServer/TerminalServer,
2961
- * closing the gap where the agent simply skips the voluntary permission
2962
- * request and sends the privileged callback directly.
2963
- *
2964
- * Uses the session's permission policy. The default
2965
- * (`readOnlyPermissionPolicy`) auto-approves only side-effect-free tool
2966
- * calls (read/search/fetch/think) and rejects everything else — this is
2967
- * the safe-by-default posture. For trusted local agents (CLI `acp spawn`,
2968
- * Director fan-out), inject `defaultPermissionPolicy` to grant
2969
- * write/execute access.
2970
- *
2971
- * Returns true if the callback is authorized, false if denied.
2972
- */
2973
- async authorizeCallback(partial) {
2974
- try {
2975
- const outcome = await this.permissionPolicy({
2976
- toolCall: {
2977
- sessionUpdate: "tool_call_update",
2978
- toolCallId: partial.toolCallId,
2979
- title: partial.title,
2980
- kind: partial.kind,
2981
- status: "pending",
2982
- ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2983
- },
2984
- options: [
2985
- { optionId: "allow", name: "Allow", kind: "allow_once" },
2986
- { optionId: "reject", name: "Reject", kind: "reject_once" }
2987
- ],
2988
- signal: new AbortController().signal
2989
- });
2990
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
2991
- } catch {
2992
- return false;
2993
- }
2994
- }
2995
- async handleFsRequest(msg) {
2996
- const id = msg.id;
2997
- if (id === void 0) return;
2998
- const params = msg.params;
2999
- if (!params?.path) {
3000
- await this.sendErrorResponse(id, -32602, "path is required");
3001
- return;
3002
- }
3003
- if (msg.method === "fs/write_text_file") {
3004
- const allowed = await this.authorizeCallback({
3005
- toolCallId: `acp-fs-write-${id}`,
3006
- title: `Write file: ${params.path}`,
3007
- kind: "edit",
3008
- rawInput: { path: params.path, sessionId: params.sessionId }
3009
- });
3010
- if (!allowed) {
3011
- await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
3012
- return;
3013
- }
3014
- }
3015
- try {
3016
- if (msg.method === "fs/read_text_file") {
3017
- const result = await this.fileServer.readTextFile({
3018
- sessionId: params.sessionId ?? "",
3019
- path: params.path
3020
- });
3021
- await this.sendResult(id, result);
3022
- } else {
3023
- await this.fileServer.writeTextFile({
3024
- sessionId: params.sessionId ?? "",
3025
- path: params.path,
3026
- content: params.content ?? ""
3027
- });
3028
- await this.sendResult(id, {});
3029
- }
3030
- } catch (err) {
3031
- const code = err instanceof FsError ? -32602 : -32603;
3032
- const message = err instanceof Error ? err.message : String(err);
3033
- await this.sendErrorResponse(id, code, message);
3034
- }
3035
- }
3036
- async handleTerminalRequest(msg) {
3037
- const id = msg.id;
3038
- if (id === void 0) return;
3039
- const params = msg.params ?? {};
3040
- try {
3041
- switch (msg.method) {
3042
- case "terminal/create": {
3043
- const allowed = await this.authorizeCallback({
3044
- toolCallId: `acp-terminal-create-${id}`,
3045
- title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
3046
- kind: "execute",
3047
- rawInput: {
3048
- command: params.command,
3049
- args: params.args,
3050
- cwd: params.cwd,
3051
- sessionId: params.sessionId
3052
- }
3053
- });
3054
- if (!allowed) {
3055
- await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
3056
- return;
3057
- }
3058
- const createOpts = {
3059
- sessionId: String(params.sessionId ?? ""),
3060
- command: String(params.command ?? ""),
3061
- args: Array.isArray(params.args) ? params.args : []
3062
- };
3063
- if (Array.isArray(params.env)) {
3064
- createOpts.env = params.env;
3065
- }
3066
- if (typeof params.cwd === "string") {
3067
- createOpts.cwd = params.cwd;
3068
- }
3069
- if (typeof params.outputByteLimit === "number") {
3070
- createOpts.outputByteLimit = params.outputByteLimit;
3071
- }
3072
- const result = this.terminalServer.create(createOpts);
3073
- await this.sendResult(id, result);
3074
- return;
3075
- }
3076
- case "terminal/output": {
3077
- const terminalId = String(params.terminalId ?? "");
3078
- const out = this.terminalServer.output(terminalId);
3079
- await this.sendResult(id, out);
3080
- return;
3081
- }
3082
- case "terminal/wait_for_exit": {
3083
- const terminalId = String(params.terminalId ?? "");
3084
- const exit = await this.terminalServer.waitForExit(terminalId);
3085
- await this.sendResult(id, exit);
3086
- return;
3087
- }
3088
- case "terminal/kill": {
3089
- const terminalId = String(params.terminalId ?? "");
3090
- this.terminalServer.kill(terminalId);
3091
- await this.sendResult(id, {});
3092
- return;
3093
- }
3094
- case "terminal/release": {
3095
- const terminalId = String(params.terminalId ?? "");
3096
- this.terminalServer.release(terminalId);
3097
- await this.sendResult(id, {});
3098
- return;
3099
- }
3100
- default:
3101
- await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
3102
- }
3103
- } catch (err) {
3104
- const message = err instanceof Error ? err.message : String(err);
3105
- await this.sendErrorResponse(id, -32603, message);
3106
- }
3158
+ this.scratch = createSessionScratch();
3107
3159
  }
3108
3160
  };
3109
- function textContent(text) {
3110
- return { type: "text", text };
3111
- }
3112
- function imageContent(mimeType, data) {
3113
- return { type: "image", mimeType, data };
3114
- }
3115
- function audioContent(mimeType, data) {
3116
- return { type: "audio", mimeType, data };
3117
- }
3118
- function extractText(block) {
3119
- if (typeof block !== "object" || block === null) return "";
3120
- const b = block;
3121
- if (b.type === "text" && typeof b.text === "string") return b.text;
3122
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
3123
- return b.resource.text;
3124
- }
3125
- return "";
3126
- }
3127
- function isRecord(v) {
3128
- return typeof v === "object" && v !== null && !Array.isArray(v);
3129
- }
3130
- function emptyRunResult(stopReason) {
3131
- return {
3132
- text: "",
3133
- stopReason,
3134
- hasText: false,
3135
- toolCalls: [],
3136
- diffs: [],
3137
- thoughts: ""
3138
- };
3139
- }
3140
3161
 
3141
3162
  // src/client/tool-translator.ts
3142
3163
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
@@ -3286,12 +3307,12 @@ async function benchOne(agentId, cmd, opts) {
3286
3307
  signal
3287
3308
  );
3288
3309
  fsOk = fsRes.text.includes(fileToken);
3289
- if (!fsOk) fsDetail = "agent did not return the file contents (may not have used a read tool)";
3310
+ if (!fsOk)
3311
+ fsDetail = "agent did not return the file contents (may not have used a read tool)";
3290
3312
  } catch (err) {
3291
3313
  fsDetail = err instanceof Error ? err.message : String(err);
3292
3314
  } finally {
3293
- await fsp2.rm(filePath, { force: true }).catch(() => {
3294
- });
3315
+ await removeBenchFile(filePath);
3295
3316
  }
3296
3317
  checks.push({ name: "fs", ok: fsOk, detail: fsDetail });
3297
3318
  }
@@ -3306,8 +3327,7 @@ async function benchOne(agentId, cmd, opts) {
3306
3327
  }
3307
3328
  const required = checks.filter((c) => c.name !== "fs" || opts.checkFs);
3308
3329
  const allReq = required.every((c) => c.ok);
3309
- const handshakeOk = checks.find((c) => c.name === "handshake")?.ok === true;
3310
- const status = allReq ? "pass" : handshakeOk ? "partial" : "fail";
3330
+ const status = allReq ? "pass" : "partial";
3311
3331
  return {
3312
3332
  agentId,
3313
3333
  status,
@@ -3399,7 +3419,9 @@ function renderAcpBenchText(result) {
3399
3419
  for (const r of result.results) {
3400
3420
  const checks = r.checks.map((c) => `${c.ok ? "\u2713" : "\u2717"}${c.name}`).join(" ");
3401
3421
  const timing = r.handshakeMs !== void 0 ? ` hs=${r.handshakeMs}ms${r.promptMs !== void 0 ? ` prompt=${r.promptMs}ms` : ""}` : "";
3402
- lines.push(` ${icon(r.status)} ${r.agentId.padEnd(16)} ${r.status.toUpperCase().padEnd(7)} ${checks}${timing}`);
3422
+ lines.push(
3423
+ ` ${icon(r.status)} ${r.agentId.padEnd(16)} ${r.status.toUpperCase().padEnd(7)} ${checks}${timing}`
3424
+ );
3403
3425
  if (r.agentInfo) lines.push(` agent: ${r.agentInfo.name} ${r.agentInfo.version}`);
3404
3426
  if (r.sample) lines.push(` reply: ${r.sample}`);
3405
3427
  if (r.reason) lines.push(` reason: ${r.reason}`);
@@ -3411,6 +3433,9 @@ function renderAcpBenchText(result) {
3411
3433
  );
3412
3434
  return lines.join("\n");
3413
3435
  }
3436
+ async function removeBenchFile(filePath, remove = fsp2.rm) {
3437
+ await remove(filePath, { force: true }).catch(() => void 0);
3438
+ }
3414
3439
 
3415
3440
  // src/registry/agents.catalog.ts
3416
3441
  var AGENTS_CATALOG = [
@@ -3799,16 +3824,8 @@ function mapACPKind(acpKind) {
3799
3824
  }
3800
3825
  }
3801
3826
  function isRetryable(kind) {
3802
- switch (kind) {
3803
- case "provider_5xx":
3804
- case "provider_rate_limit":
3805
- case "provider_timeout":
3806
- case "tool_threw":
3807
- case "budget_timeout":
3808
- return true;
3809
- default:
3810
- return false;
3811
- }
3827
+ void kind;
3828
+ return false;
3812
3829
  }
3813
3830
  var REGISTRY_ID_ALIASES = {
3814
3831
  "claude-code": "claude-acp",
@@ -3948,11 +3965,15 @@ async function probeAcpAgents(opts) {
3948
3965
  }
3949
3966
  await runPhase(local, opts.concurrency ?? 4, localTimeout);
3950
3967
  await runPhase(pkg, 2, pkgTimeout);
3951
- return ids.map((id) => byId.get(id) ?? { id, ok: false, ms: 0, error: "not probed" });
3968
+ return ids.map((id) => byId.get(id));
3952
3969
  }
3953
3970
 
3971
+ // src/integration/ensemble-runner.ts
3972
+ import { SubagentBudget } from "@wrongstack/core/coordination";
3973
+
3954
3974
  // src/registry/ensemble-registry.ts
3955
3975
  import { spawn as spawn2 } from "node:child_process";
3976
+ import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
3956
3977
  var PROBE_TIMEOUT_MS = 5e3;
3957
3978
  var PROBE_CACHE_MS = 5e3;
3958
3979
  var MAX_PARALLEL_PROBES = 4;
@@ -3976,7 +3997,7 @@ async function probeWithBound(items, worker, limit) {
3976
3997
  await Promise.all(runners);
3977
3998
  return results;
3978
3999
  }
3979
- async function defaultProbe(desc, timeoutMs) {
4000
+ async function defaultProbe(desc, timeoutMs, spawnProcess = spawn2, platform = process.platform) {
3980
4001
  const start = Date.now();
3981
4002
  return new Promise((resolve3) => {
3982
4003
  let settled = false;
@@ -3986,7 +4007,9 @@ async function defaultProbe(desc, timeoutMs) {
3986
4007
  if (settled) return;
3987
4008
  settled = true;
3988
4009
  try {
3989
- child.kill();
4010
+ if (child.exitCode === null && child.signalCode === null) {
4011
+ treeKill2(child);
4012
+ }
3990
4013
  } catch {
3991
4014
  }
3992
4015
  resolve3(result);
@@ -3994,8 +4017,8 @@ async function defaultProbe(desc, timeoutMs) {
3994
4017
  let child;
3995
4018
  try {
3996
4019
  const probeArgs = [...desc.probe.args ?? []];
3997
- const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(desc.probe.command, probeArgs) : null;
3998
- child = spawn2(shim?.command ?? desc.probe.command, shim?.args ?? probeArgs, {
4020
+ const shim = platform === "win32" ? buildWin32CmdShimInvocation(desc.probe.command, probeArgs) : null;
4021
+ child = spawnProcess(shim?.command ?? desc.probe.command, shim?.args ?? probeArgs, {
3999
4022
  stdio: ["ignore", "pipe", "pipe"],
4000
4023
  windowsHide: true,
4001
4024
  ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
@@ -4028,7 +4051,7 @@ async function defaultProbe(desc, timeoutMs) {
4028
4051
  clearTimeout(timer);
4029
4052
  const durationMs = Date.now() - start;
4030
4053
  const out = (stdout + stderr).trim();
4031
- const isWindowsShellMiss = process.platform === "win32" && out.toLowerCase().includes("is not recognized");
4054
+ const isWindowsShellMiss = platform === "win32" && out.toLowerCase().includes("is not recognized");
4032
4055
  if (isWindowsShellMiss) {
4033
4056
  finish({
4034
4057
  ok: false,
@@ -4040,7 +4063,7 @@ async function defaultProbe(desc, timeoutMs) {
4040
4063
  if (out.length > 0) {
4041
4064
  finish({
4042
4065
  ok: true,
4043
- version: out.split("\n")[0]?.trim() ?? "",
4066
+ version: out.split("\n")[0].trim(),
4044
4067
  path: desc.probe.command,
4045
4068
  durationMs
4046
4069
  });
@@ -4116,7 +4139,6 @@ var EnsembleRegistry = class {
4116
4139
  };
4117
4140
 
4118
4141
  // src/integration/ensemble-runner.ts
4119
- import { SubagentBudget } from "@wrongstack/core/coordination";
4120
4142
  var DEFAULT_MAX_CONCURRENCY = 4;
4121
4143
  async function mapBound(items, worker, limit) {
4122
4144
  const results = new Array(items.length);
@@ -4259,10 +4281,7 @@ async function runEnsemble(opts) {
4259
4281
  }
4260
4282
  runnable.push({ id, cmd });
4261
4283
  }
4262
- const concurrency = Math.max(
4263
- 1,
4264
- opts.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY
4265
- );
4284
+ const concurrency = Math.max(1, opts.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
4266
4285
  await mapBound(
4267
4286
  runnable,
4268
4287
  async ({ id, cmd }) => {
@@ -4311,15 +4330,11 @@ function renderEnsembleText(result) {
4311
4330
  );
4312
4331
  break;
4313
4332
  case "failed":
4314
- lines.push(
4315
- `[${r.error?.kind ?? "unknown"}] ${r.error?.message ?? "failed"}`
4316
- );
4333
+ lines.push(`[${r.error?.kind ?? "unknown"}] ${r.error?.message ?? "failed"}`);
4317
4334
  lines.push(`[${r.agentId}] failed ${r.durationMs}ms`);
4318
4335
  break;
4319
4336
  case "cancelled":
4320
- lines.push(
4321
- `[${r.error?.kind ?? "aborted"}] ${r.error?.message ?? "cancelled"}`
4322
- );
4337
+ lines.push(`[${r.error?.kind ?? "aborted"}] ${r.error?.message ?? "cancelled"}`);
4323
4338
  lines.push(`[${r.agentId}] cancelled ${r.durationMs}ms`);
4324
4339
  break;
4325
4340
  case "skipped":
@@ -4381,15 +4396,14 @@ async function runOneAcpTask(opts) {
4381
4396
 
4382
4397
  // src/registry/acp-registry-fetch.ts
4383
4398
  var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
4384
- function currentPlatformKey() {
4385
- const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux";
4386
- const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch;
4399
+ function currentPlatformKey(platform = process.platform, architecture = process.arch) {
4400
+ const os = platform === "win32" ? "windows" : platform === "darwin" ? "darwin" : "linux";
4401
+ const arch = architecture === "arm64" ? "aarch64" : architecture === "x64" ? "x86_64" : architecture;
4387
4402
  return `${os}-${arch}`;
4388
4403
  }
4389
4404
  function basename(cmd) {
4390
4405
  const cleaned = cmd.replace(/^\.\//, "").replace(/\\/g, "/");
4391
- const parts = cleaned.split("/");
4392
- return parts[parts.length - 1] || cleaned;
4406
+ return cleaned.slice(cleaned.lastIndexOf("/") + 1);
4393
4407
  }
4394
4408
  function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
4395
4409
  if (!entry || typeof entry.id !== "string" || entry.id.length === 0) return null;