@cabane/companion 0.6.0 → 0.6.1

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/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  } from "fs";
21
21
  import { homedir, userInfo } from "os";
22
22
  import { dirname, join } from "path";
23
- import { z as z3 } from "zod";
23
+ import { z as z2 } from "zod";
24
24
 
25
25
  // src/errors.ts
26
26
  var CompanionError = class extends Error {
@@ -53,9 +53,6 @@ var PrepareHookError = class extends CompanionError {
53
53
  };
54
54
 
55
55
  // src/pairing.ts
56
- import { z } from "zod";
57
- var PAIRING_VERSION = 1;
58
- var DEVICE_TOKEN_PREFIX = "cabdev_";
59
56
  function isAllowedBaseUrl(raw) {
60
57
  let url;
61
58
  try {
@@ -70,78 +67,24 @@ function isAllowedBaseUrl(raw) {
70
67
  }
71
68
  return false;
72
69
  }
73
- var pairingSchema = z.object({
74
- // Bumped if the wire shape changes incompatibly. We only accept v1.
75
- v: z.literal(PAIRING_VERSION),
76
- baseUrl: z.string().url().refine(isAllowedBaseUrl, {
77
- message: "baseUrl must be https (loopback http is allowed for local dev only)"
78
- }),
79
- // The `cabdev_` device token plaintext — the companion's one durable credential.
80
- deviceToken: z.string().min(1).startsWith(DEVICE_TOKEN_PREFIX, {
81
- message: 'deviceToken must be a cabane device token (starts with "cabdev_")'
82
- }),
83
- // Optional identity hints the app may include for nicer local display. The
84
- // companion also learns these from the first assignments pull, so they're not
85
- // required.
86
- deviceId: z.string().min(1).optional(),
87
- deviceLabel: z.string().min(1).optional()
88
- });
89
- function decodePairing(raw) {
90
- const cleaned = raw.replace(/\s+/g, "");
91
- if (cleaned.length === 0) {
92
- throw new CompanionError("empty pairing string. Copy it from the cabane app and try again.");
93
- }
94
- const bytes = Buffer.from(cleaned, "base64url");
95
- const json = bytes.toString("utf8");
96
- let parsed;
97
- try {
98
- parsed = JSON.parse(json);
99
- } catch {
100
- throw new CompanionError(
101
- `that doesn't look like a complete pairing string \u2014 received ${cleaned.length} characters that decoded to ${bytes.length} bytes, but they weren't valid JSON. Copy the WHOLE block from the cabane app (a partial or truncated copy is the usual cause).`
102
- );
103
- }
104
- if (parsed && typeof parsed === "object" && "v" in parsed && parsed.v !== PAIRING_VERSION) {
105
- throw new CompanionError(
106
- `this pairing string is version ${String(parsed.v)}, but this companion only understands version ${PAIRING_VERSION}. Update cabane-companion (\`git pull\` + rebuild) and try again.`
107
- );
108
- }
109
- const result = pairingSchema.safeParse(parsed);
110
- if (!result.success) {
111
- if (result.error.issues.some((i) => i.path[0] === "baseUrl")) {
112
- throw new CompanionError(
113
- "this pairing string points at a non-https cabane URL. The companion runs the server's prompt with permissions bypassed and sends its credentials over the same channel, so it refuses plaintext http (except localhost for local dev). Use an https base URL."
114
- );
115
- }
116
- throw new CompanionError(
117
- `the pairing string is missing or malformed fields: ${result.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")}. Re-copy it from the cabane app.`
118
- );
119
- }
120
- return result.data;
121
- }
122
70
 
123
71
  // src/prepare-hook.ts
124
72
  import { spawn } from "child_process";
125
- import { z as z2 } from "zod";
126
- var prepareHookSchema = z2.object({
127
- command: z2.string().min(1),
128
- args: z2.array(z2.string()).optional(),
73
+ import { z } from "zod";
74
+ var prepareHookSchema = z.object({
75
+ command: z.string().min(1),
76
+ args: z.array(z.string()).optional(),
129
77
  // Extra env handed to the hook process itself (merged over process.env).
130
- env: z2.record(z2.string(), z2.string()).optional(),
78
+ env: z.record(z.string(), z.string()).optional(),
131
79
  // Wall-clock cap for the hook. Provisioning is slow (minutes), so the
132
80
  // default is generous; a hook that hangs past this is killed and the turn
133
81
  // fails with a clear timeout message rather than pinning the companion.
134
- timeoutMs: z2.number().int().positive().optional()
82
+ timeoutMs: z.number().int().positive().optional()
135
83
  }).strict();
136
84
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
137
- var prepareResultSchema = z2.object({
138
- cwd: z2.string().min(1),
139
- env: z2.record(z2.string(), z2.string()).optional(),
140
- nativeWorkAssignment: z2.object({
141
- itemId: z2.string(),
142
- executionId: z2.string(),
143
- activationEpoch: z2.number().int().nonnegative()
144
- }).strict().optional()
85
+ var prepareResultSchema = z.object({
86
+ cwd: z.string().min(1),
87
+ env: z.record(z.string(), z.string()).optional()
145
88
  });
146
89
  function parsePrepareOutput(stdout) {
147
90
  const last = stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).at(-1);
@@ -160,13 +103,12 @@ function parsePrepareOutput(stdout) {
160
103
  const r = prepareResultSchema.safeParse(parsed);
161
104
  if (!r.success) {
162
105
  throw new PrepareHookError(
163
- 'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env" or "nativeWorkAssignment")'
106
+ 'prepare hook JSON must carry a non-empty string "cwd" (plus optional "env")'
164
107
  );
165
108
  }
166
109
  return {
167
110
  cwd: r.data.cwd,
168
- ...r.data.env ? { env: r.data.env } : {},
169
- ...r.data.nativeWorkAssignment ? { nativeWorkAssignment: r.data.nativeWorkAssignment } : {}
111
+ ...r.data.env ? { env: r.data.env } : {}
170
112
  };
171
113
  }
172
114
  return { cwd: last };
@@ -204,10 +146,10 @@ var runPrepareHook = (hook, input) => {
204
146
  CABANE_CONVERSATION_TITLE: input.title ?? ""
205
147
  }
206
148
  });
207
- } catch (err2) {
149
+ } catch (err) {
208
150
  reject(
209
151
  new PrepareHookError(
210
- `prepare hook failed to start: ${err2 instanceof Error ? err2.message : String(err2)}`
152
+ `prepare hook failed to start: ${err instanceof Error ? err.message : String(err)}`
211
153
  )
212
154
  );
213
155
  return;
@@ -227,8 +169,8 @@ var runPrepareHook = (hook, input) => {
227
169
  child.stderr?.on("data", (d) => {
228
170
  stderr += d.toString();
229
171
  });
230
- child.on("error", (err2) => {
231
- finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${err2.message}`)));
172
+ child.on("error", (err) => {
173
+ finish(() => reject(new PrepareHookError(`prepare hook failed to start: ${err.message}`)));
232
174
  });
233
175
  child.on("close", (code) => {
234
176
  finish(() => {
@@ -241,8 +183,8 @@ var runPrepareHook = (hook, input) => {
241
183
  }
242
184
  try {
243
185
  resolve(parsePrepareOutput(stdout));
244
- } catch (err2) {
245
- reject(err2 instanceof PrepareHookError ? err2 : new PrepareHookError(String(err2)));
186
+ } catch (err) {
187
+ reject(err instanceof PrepareHookError ? err : new PrepareHookError(String(err)));
246
188
  }
247
189
  });
248
190
  });
@@ -281,8 +223,8 @@ function cabaneDir() {
281
223
  function configPath() {
282
224
  return join(cabaneDir(), "config.json");
283
225
  }
284
- var localAgentConfigSchema = z3.object({
285
- cwd: z3.string().optional(),
226
+ var localAgentConfigSchema = z2.object({
227
+ cwd: z2.string().optional(),
286
228
  prepareHook: prepareHookSchema.optional(),
287
229
  // CT289: the Claude Code auto-memory escape hatch. Auto-memory is forced OFF
288
230
  // by default on every companion (memory belongs in the Cabane workspace, and a
@@ -291,13 +233,13 @@ var localAgentConfigSchema = z3.object({
291
233
  // auto-memory back to your own `~/.claude/settings.json` — Cabane then stops
292
234
  // injecting the off switch and your normal Claude Code memory workflow applies
293
235
  // (in coding mode, where the checkout's project settings are read).
294
- claudeCode: z3.object({ autoMemory: z3.boolean().optional() }).strict().optional()
236
+ claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
295
237
  }).strict();
296
- var companionConfigSchema = z3.object({
238
+ var companionConfigSchema = z2.object({
297
239
  // The cabane instance this device is paired with. SJ515: https-enforced
298
240
  // (loopback exempt) so a hand-edited config can't smuggle a plaintext-http
299
241
  // base URL onto the MITM-able channel the device token + prompt ride.
300
- baseUrl: z3.string().url().refine(isAllowedBaseUrl, {
242
+ baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
301
243
  message: "baseUrl must be https (loopback http is allowed for local dev only)"
302
244
  }),
303
245
  // The `cabdev_` device token plaintext — the companion's one durable credential,
@@ -305,22 +247,22 @@ var companionConfigSchema = z3.object({
305
247
  // heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
306
248
  // "paired but logged out" state the supervisor refuses to run) while keeping
307
249
  // the rest of the config; `pair` always writes one.
308
- deviceToken: z3.string().optional(),
309
- // Identity hints, learned from the pairing string and refreshed on the first
250
+ deviceToken: z2.string().optional(),
251
+ // Identity hints, learned from the device flow and refreshed on the first
310
252
  // assignments pull. Cosmetic — used for `status`/dashboard display only.
311
- deviceId: z3.string().optional(),
312
- deviceLabel: z3.string().optional(),
253
+ deviceId: z2.string().optional(),
254
+ deviceLabel: z2.string().optional(),
313
255
  // Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
314
256
  // agentId / username / `slug/username`. Hand-added by the operator; the companion
315
257
  // never writes this (it only persists credentials + the device, elsewhere).
316
- agents: z3.record(z3.string(), localAgentConfigSchema).optional(),
258
+ agents: z2.record(z2.string(), localAgentConfigSchema).optional(),
317
259
  // Dashboard settings (all optional). dashboardPort: preferred bind port (next
318
260
  // free one if taken); autoOpen: whether `start` opens the browser (the
319
261
  // `--no-open` flag / `COMPANION_NO_OPEN=1` override per-run); logLevel: pino
320
262
  // level, live-editable from the dashboard settings panel.
321
- dashboardPort: z3.number().int().min(1).max(65535).optional(),
322
- autoOpen: z3.boolean().optional(),
323
- logLevel: z3.enum(["warn", "info", "debug"]).optional(),
263
+ dashboardPort: z2.number().int().min(1).max(65535).optional(),
264
+ autoOpen: z2.boolean().optional(),
265
+ logLevel: z2.enum(["warn", "info", "debug"]).optional(),
324
266
  // CT270: the opencode runtime, when the operator runs one on this machine. The
325
267
  // operator installs opencode, starts `opencode serve` (auth via opencode's own
326
268
  // `/connect` — Cabane never sees provider keys), and points the companion at it
@@ -328,8 +270,8 @@ var companionConfigSchema = z3.object({
328
270
  // heartbeat manifest (so the server offers DeepSeek/opencode models here and
329
271
  // routes those turns to this device) AND registers the opencode adapter in the
330
272
  // dispatcher. Absent → the device is claude-code-only, exactly as before.
331
- opencode: z3.object({
332
- serverUrl: z3.string().url()
273
+ opencode: z2.object({
274
+ serverUrl: z2.string().url()
333
275
  }).strict().optional(),
334
276
  // CT481: the codex runtime, when the operator runs Codex on this machine. Unlike
335
277
  // opencode (a long-lived `opencode serve` addressed by URL), Codex is a local CLI
@@ -340,20 +282,13 @@ var companionConfigSchema = z3.object({
340
282
  // keeps the block but turns it off). Enabling makes the device advertise the
341
283
  // `codex` runtime on its heartbeat manifest AND registers the codex adapter in
342
284
  // the dispatcher. Absent → the device doesn't offer codex, exactly as before.
343
- codex: z3.object({
344
- enabled: z3.boolean().optional()
285
+ codex: z2.object({
286
+ enabled: z2.boolean().optional()
345
287
  }).strict().optional()
346
288
  });
347
289
  function isCodexEnabled(cfg) {
348
290
  return !!cfg.codex && cfg.codex.enabled !== false;
349
291
  }
350
- function cabaneNativeApiKey() {
351
- const key = process.env.OPENROUTER_API_KEY?.trim();
352
- return key ? key : void 0;
353
- }
354
- function isCabaneNativeEnabled() {
355
- return cabaneNativeApiKey() !== void 0;
356
- }
357
292
  function localAgentConfig(cfg, agent) {
358
293
  const map = cfg.agents ?? {};
359
294
  return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
@@ -364,18 +299,18 @@ function loadConfig() {
364
299
  let raw;
365
300
  try {
366
301
  raw = readFileSync(path3, "utf8");
367
- } catch (err2) {
302
+ } catch (err) {
368
303
  throw new ConfigError(
369
- `couldn't read ${path3}: ${err2 instanceof Error ? err2.message : String(err2)}`
304
+ `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
370
305
  );
371
306
  }
372
307
  if (raw.trim().length === 0) return null;
373
308
  let parsed;
374
309
  try {
375
310
  parsed = JSON.parse(raw);
376
- } catch (err2) {
311
+ } catch (err) {
377
312
  throw new ConfigError(
378
- `${path3} is not valid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
313
+ `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
379
314
  );
380
315
  }
381
316
  const result = companionConfigSchema.safeParse(parsed);
@@ -424,7 +359,7 @@ function loadConfigTolerant() {
424
359
  }
425
360
  const obj = parsed && typeof parsed === "object" ? parsed : {};
426
361
  const local = {};
427
- const agents = z3.record(z3.string(), localAgentConfigSchema).safeParse(obj.agents);
362
+ const agents = z2.record(z2.string(), localAgentConfigSchema).safeParse(obj.agents);
428
363
  if (agents.success) local.agents = agents.data;
429
364
  if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
430
365
  if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
@@ -451,12 +386,12 @@ function saveConfig(cfg) {
451
386
  } catch {
452
387
  }
453
388
  renameSync(tmp, path3);
454
- } catch (err2) {
389
+ } catch (err) {
455
390
  try {
456
391
  rmSync(tmp, { force: true });
457
392
  } catch {
458
393
  }
459
- throw err2;
394
+ throw err;
460
395
  }
461
396
  }
462
397
  function requireConfig() {
@@ -631,8 +566,8 @@ async function verifyRuntime(state, fetchImpl = fetch) {
631
566
  let res;
632
567
  try {
633
568
  res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
634
- } catch (err2) {
635
- return isConnRefused(err2) ? "stale" : "unknown";
569
+ } catch (err) {
570
+ return isConnRefused(err) ? "stale" : "unknown";
636
571
  }
637
572
  if (!res.ok) return "unknown";
638
573
  let body;
@@ -644,9 +579,9 @@ async function verifyRuntime(state, fetchImpl = fetch) {
644
579
  if (typeof body.instance_id !== "string") return "unknown";
645
580
  return body.instance_id === state.instanceId ? "ours" : "stale";
646
581
  }
647
- function isConnRefused(err2) {
648
- if (!err2 || typeof err2 !== "object") return false;
649
- const cause = err2.cause;
582
+ function isConnRefused(err) {
583
+ if (!err || typeof err !== "object") return false;
584
+ const cause = err.cause;
650
585
  return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
651
586
  }
652
587
  function trimSlash(s) {
@@ -737,11 +672,11 @@ import {
737
672
  writeFileSync as writeFileSync3
738
673
  } from "fs";
739
674
  import { dirname as dirname3, join as join4 } from "path";
740
- import { z as z4 } from "zod";
675
+ import { z as z3 } from "zod";
741
676
  function credentialsPath() {
742
677
  return join4(cabaneDir(), "credentials.json");
743
678
  }
744
- var credentialStoreSchema = z4.record(z4.string(), z4.string());
679
+ var credentialStoreSchema = z3.record(z3.string(), z3.string());
745
680
  function load() {
746
681
  const path3 = credentialsPath();
747
682
  if (!existsSync3(path3)) return {};
@@ -774,12 +709,12 @@ function save(map) {
774
709
  } catch {
775
710
  }
776
711
  renameSync2(tmp, path3);
777
- } catch (err2) {
712
+ } catch (err) {
778
713
  try {
779
714
  rmSync3(tmp, { force: true });
780
715
  } catch {
781
716
  }
782
- throw err2;
717
+ throw err;
783
718
  }
784
719
  }
785
720
  function getCredential(agentId) {
@@ -818,13 +753,13 @@ async function logout(opts = {}) {
818
753
  }
819
754
  if (!opts.yes) {
820
755
  const message = opts.purge ? "Purge the entire local companion config (device + agent overrides + settings)?" : "Log out this device (removes the device token + cached agent credentials; keeps your config)?";
821
- const ok2 = await confirm({ message, default: false });
822
- if (!ok2) {
756
+ const ok = await confirm({ message, default: false });
757
+ if (!ok) {
823
758
  process.stdout.write("cancelled\n");
824
759
  return;
825
760
  }
826
761
  }
827
- const serverNote = `the device remains registered server-side \u2014 remove it in the cabane app (Settings \u2192 Agents \u2192 Companions) if you want it gone there too.`;
762
+ const serverNote = `the device remains registered server-side \u2014 remove it in the cabane app (Settings \u2192 Connectors) if you want it gone there too.`;
828
763
  if (opts.purge) {
829
764
  deleteConfig();
830
765
  clearCredentials();
@@ -841,10 +776,6 @@ async function logout(opts = {}) {
841
776
  );
842
777
  }
843
778
 
844
- // src/commands/pair.ts
845
- import { readFileSync as readFileSync4 } from "fs";
846
- import { password } from "@inquirer/prompts";
847
-
848
779
  // src/enrollment.ts
849
780
  function trimBase(baseUrl) {
850
781
  return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
@@ -857,9 +788,9 @@ async function postJson(baseUrl, path3, body) {
857
788
  headers: { "content-type": "application/json", accept: "application/json" },
858
789
  body: JSON.stringify(body)
859
790
  });
860
- } catch (err2) {
791
+ } catch (err) {
861
792
  throw new CompanionError(
862
- `couldn't reach cabane at ${baseUrl}: ${err2 instanceof Error ? err2.message : String(err2)}. Check the server URL (pass --server <url>) and your connection.`
793
+ `couldn't reach cabane at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check the server URL (pass --server <url>) and your connection.`
863
794
  );
864
795
  }
865
796
  const raw = await res.text();
@@ -874,7 +805,7 @@ async function postJson(baseUrl, path3, body) {
874
805
  if (res.status >= 400) {
875
806
  if (res.status === 404 && path3.endsWith("/code")) {
876
807
  throw new CompanionError(
877
- `this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server, or use \`cabane-companion pair --legacy\` with a pairing string from the app.`
808
+ `this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server to a version that supports \`cabane-companion pair\`.`
878
809
  );
879
810
  }
880
811
  const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
@@ -957,34 +888,6 @@ function writePairedConfig(paired) {
957
888
  }
958
889
 
959
890
  // src/commands/pair.ts
960
- async function readStdin() {
961
- const chunks = [];
962
- for await (const chunk of process.stdin) {
963
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
964
- }
965
- return Buffer.concat(chunks).toString("utf8");
966
- }
967
- async function resolvePairingString(opts) {
968
- if (opts.file !== void 0) {
969
- try {
970
- return readFileSync4(opts.file, "utf8");
971
- } catch (err2) {
972
- throw new CompanionError(
973
- `couldn't read the pairing string from ${opts.file}: ${err2 instanceof Error ? err2.message : String(err2)}`
974
- );
975
- }
976
- }
977
- if (opts.arg !== void 0) {
978
- process.stderr.write(
979
- "warning: passing the pairing string as an argument leaves it in your shell history and is readable by other processes while `pair` runs. Prefer `cabane-companion pair --legacy` (you'll be prompted to paste it) or `cabane-companion pair --legacy --file <path>`.\n"
980
- );
981
- return opts.arg;
982
- }
983
- if (!process.stdin.isTTY) {
984
- return await readStdin();
985
- }
986
- return await password({ message: "Paste the pairing string from cabane:" });
987
- }
988
891
  function writePairedConfigCli(paired) {
989
892
  const { note } = writePairedConfig(paired);
990
893
  if (note) process.stderr.write(`note: ${note}
@@ -996,25 +899,28 @@ Run \`cabane-companion start\` \u2014 it will pull the agents assigned to this d
996
899
  `
997
900
  );
998
901
  }
999
- async function pair(opts = {}) {
1000
- const useLegacy = opts.legacy === true || opts.arg !== void 0 || opts.file !== void 0;
1001
- if (useLegacy) {
1002
- const rawString = await resolvePairingString(opts);
1003
- const pairing = decodePairing(rawString);
1004
- writePairedConfigCli({
1005
- baseUrl: pairing.baseUrl,
1006
- deviceToken: pairing.deviceToken,
1007
- ...pairing.deviceId ? { deviceId: pairing.deviceId } : {},
1008
- ...pairing.deviceLabel ? { deviceLabel: pairing.deviceLabel } : {}
1009
- });
1010
- return;
902
+ function writeCompletedPairing(raw) {
903
+ let paired;
904
+ try {
905
+ paired = JSON.parse(raw);
906
+ } catch {
907
+ throw new Error("invalid completed pairing payload: expected JSON on stdin.");
908
+ }
909
+ if (!paired || typeof paired !== "object" || typeof paired.baseUrl !== "string" || typeof paired.deviceToken !== "string") {
910
+ throw new Error("invalid completed pairing payload: baseUrl and deviceToken are required.");
1011
911
  }
912
+ writePairedConfigCli(paired);
913
+ }
914
+ async function pair(opts = {}) {
1012
915
  const baseUrl = resolvePairBaseUrl(opts.server);
1013
916
  const paired = await runDeviceFlow(baseUrl, (line) => process.stdout.write(`${line}
1014
917
  `));
1015
918
  writePairedConfigCli(paired);
1016
919
  }
1017
920
 
921
+ // src/cli.ts
922
+ import { readFileSync as readFileSync11 } from "fs";
923
+
1018
924
  // src/browser.ts
1019
925
  import { spawn as spawn4 } from "child_process";
1020
926
  import { platform } from "process";
@@ -1463,15 +1369,15 @@ var DEFAULT_PORT = 7474;
1463
1369
  var PORT_FALLBACK_SPAN = 10;
1464
1370
  function buildDashboardApp(deps) {
1465
1371
  const app = new Hono();
1466
- app.onError((err2, c) => {
1467
- if (err2 instanceof ApiError) {
1468
- const status2 = err2.status >= 400 && err2.status < 600 ? err2.status : 502;
1469
- return c.json({ error: err2.message }, status2);
1372
+ app.onError((err, c) => {
1373
+ if (err instanceof ApiError) {
1374
+ const status2 = err.status >= 400 && err.status < 600 ? err.status : 502;
1375
+ return c.json({ error: err.message }, status2);
1470
1376
  }
1471
- if (err2 instanceof CompanionError) {
1472
- return c.json({ error: err2.message }, 400);
1377
+ if (err instanceof CompanionError) {
1378
+ return c.json({ error: err.message }, 400);
1473
1379
  }
1474
- return c.json({ error: err2 instanceof Error ? err2.message : "internal error" }, 500);
1380
+ return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
1475
1381
  });
1476
1382
  registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
1477
1383
  return app;
@@ -1492,12 +1398,12 @@ async function startDashboard(opts) {
1492
1398
  server.closeAllConnections?.();
1493
1399
  })
1494
1400
  };
1495
- } catch (err2) {
1496
- if (isAddrInUse(err2)) {
1497
- lastErr = err2;
1401
+ } catch (err) {
1402
+ if (isAddrInUse(err)) {
1403
+ lastErr = err;
1498
1404
  continue;
1499
1405
  }
1500
- throw err2;
1406
+ throw err;
1501
1407
  }
1502
1408
  }
1503
1409
  throw new CompanionError(
@@ -1513,16 +1419,16 @@ function listen(app, port) {
1513
1419
  resolve(server);
1514
1420
  }
1515
1421
  });
1516
- server.on("error", (err2) => {
1422
+ server.on("error", (err) => {
1517
1423
  if (!settled) {
1518
1424
  settled = true;
1519
- reject(err2);
1425
+ reject(err);
1520
1426
  }
1521
1427
  });
1522
1428
  });
1523
1429
  }
1524
- function isAddrInUse(err2) {
1525
- return Boolean(err2 && typeof err2 === "object" && "code" in err2 && err2.code === "EADDRINUSE");
1430
+ function isAddrInUse(err) {
1431
+ return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
1526
1432
  }
1527
1433
  function resolveStaticDir() {
1528
1434
  return join6(dirname4(fileURLToPath2(import.meta.url)), "static");
@@ -1665,10 +1571,10 @@ var CabaneApi = class {
1665
1571
  for (let attempt = 1; ; attempt++) {
1666
1572
  try {
1667
1573
  return await this.attempt(method, path3, body, signal);
1668
- } catch (err2) {
1669
- if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err2)) throw err2;
1574
+ } catch (err) {
1575
+ if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
1670
1576
  await sleep2(RETRY_BACKOFF_MS[attempt - 1], signal);
1671
- if (signal?.aborted) throw err2;
1577
+ if (signal?.aborted) throw err;
1672
1578
  }
1673
1579
  }
1674
1580
  }
@@ -1696,14 +1602,14 @@ var CabaneApi = class {
1696
1602
  retry: true,
1697
1603
  ...signal ? { signal } : {}
1698
1604
  });
1699
- } catch (err2) {
1605
+ } catch (err) {
1700
1606
  const outbox = this.opts.outbox;
1701
- if (!outbox) throw err2;
1702
- if (signal?.aborted || isAbortError(err2)) throw err2;
1703
- if (!isRetryable(err2)) throw err2;
1607
+ if (!outbox) throw err;
1608
+ if (signal?.aborted || isAbortError(err)) throw err;
1609
+ if (!isRetryable(err)) throw err;
1704
1610
  outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
1705
1611
  this.opts.log?.warn(
1706
- { kind, turnId, seq, err: err2 instanceof Error ? err2.message : String(err2) },
1612
+ { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1707
1613
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
1708
1614
  );
1709
1615
  }
@@ -1727,10 +1633,10 @@ var CabaneApi = class {
1727
1633
  await this.request(entry.method, entry.path, entry.body, { retry: true });
1728
1634
  outbox.remove(entry.turnId, entry.seq);
1729
1635
  progressed = true;
1730
- } catch (err2) {
1731
- if (err2 instanceof ApiError && err2.status >= 400 && err2.status < 500) {
1636
+ } catch (err) {
1637
+ if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
1732
1638
  this.opts.log?.warn(
1733
- { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err2.status },
1639
+ { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err.status },
1734
1640
  "companion outbox: discarding entry on terminal 4xx (will never land)"
1735
1641
  );
1736
1642
  outbox.remove(entry.turnId, entry.seq);
@@ -1878,11 +1784,11 @@ var CabaneApi = class {
1878
1784
  try {
1879
1785
  await this.request("PATCH", path3, body, { retry: true });
1880
1786
  outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1881
- } catch (err2) {
1882
- if (!outbox) throw err2;
1883
- if (!isRetryable(err2)) {
1787
+ } catch (err) {
1788
+ if (!outbox) throw err;
1789
+ if (!isRetryable(err)) {
1884
1790
  outbox.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1885
- throw err2;
1791
+ throw err;
1886
1792
  }
1887
1793
  outbox.persist({
1888
1794
  enqueuedAt: Date.now(),
@@ -1894,7 +1800,7 @@ var CabaneApi = class {
1894
1800
  kind: "active-run"
1895
1801
  });
1896
1802
  this.opts.log?.warn(
1897
- { conversationId, agentId, err: err2 instanceof Error ? err2.message : String(err2) },
1803
+ { conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
1898
1804
  "companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
1899
1805
  );
1900
1806
  }
@@ -1993,13 +1899,13 @@ var CabaneApi = class {
1993
1899
  return res.messages.find((m) => m.id === messageId2) ?? null;
1994
1900
  }
1995
1901
  };
1996
- function isRetryable(err2) {
1997
- if (err2 instanceof ApiError) return err2.status >= 500;
1998
- if (isAbortError(err2)) return false;
1902
+ function isRetryable(err) {
1903
+ if (err instanceof ApiError) return err.status >= 500;
1904
+ if (isAbortError(err)) return false;
1999
1905
  return true;
2000
1906
  }
2001
- function isAbortError(err2) {
2002
- return err2 instanceof Error && err2.name === "AbortError";
1907
+ function isAbortError(err) {
1908
+ return err instanceof Error && err.name === "AbortError";
2003
1909
  }
2004
1910
  function sleep2(ms, signal) {
2005
1911
  return new Promise((resolve) => {
@@ -2017,8 +1923,8 @@ function sleep2(ms, signal) {
2017
1923
  }
2018
1924
  function errorMessage(status2, body) {
2019
1925
  if (body && typeof body === "object" && "error" in body) {
2020
- const err2 = body.error;
2021
- if (typeof err2 === "string") return `${status2} ${err2}`;
1926
+ const err = body.error;
1927
+ if (typeof err === "string") return `${status2} ${err}`;
2022
1928
  }
2023
1929
  if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
2024
1930
  return `${status2} error`;
@@ -2074,15 +1980,15 @@ var DeviceApi = class {
2074
1980
  };
2075
1981
  function errorMessage2(status2, body) {
2076
1982
  if (body && typeof body === "object" && "error" in body) {
2077
- const err2 = body.error;
2078
- if (typeof err2 === "string") return `${status2} ${err2}`;
1983
+ const err = body.error;
1984
+ if (typeof err === "string") return `${status2} ${err}`;
2079
1985
  }
2080
1986
  if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
2081
1987
  return `${status2} error`;
2082
1988
  }
2083
1989
 
2084
1990
  // src/cursor.ts
2085
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
1991
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2086
1992
  import { join as join7 } from "path";
2087
1993
  function pathFor(workspaceId) {
2088
1994
  return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
@@ -2090,7 +1996,7 @@ function pathFor(workspaceId) {
2090
1996
  function readCursor(workspaceId) {
2091
1997
  const path3 = pathFor(workspaceId);
2092
1998
  if (!existsSync5(path3)) return null;
2093
- const raw = readFileSync5(path3, "utf8").trim();
1999
+ const raw = readFileSync4(path3, "utf8").trim();
2094
2000
  return raw.length > 0 ? raw : null;
2095
2001
  }
2096
2002
  function writeCursor(workspaceId, eventId) {
@@ -2139,7 +2045,7 @@ var CursorTracker = class {
2139
2045
  };
2140
2046
 
2141
2047
  // src/dispatch-dedupe.ts
2142
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
2048
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
2143
2049
  import { join as join8 } from "path";
2144
2050
  var MAX_IDS = 256;
2145
2051
  function dir(log) {
@@ -2152,7 +2058,7 @@ function readIds(log, workspaceId) {
2152
2058
  const path3 = pathFor2(log, workspaceId);
2153
2059
  if (!existsSync6(path3)) return [];
2154
2060
  try {
2155
- return readFileSync6(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2061
+ return readFileSync5(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2156
2062
  } catch {
2157
2063
  return [];
2158
2064
  }
@@ -2192,7 +2098,7 @@ function readResumeCounts(workspaceId) {
2192
2098
  const path3 = resumePathFor(workspaceId);
2193
2099
  if (!existsSync6(path3)) return out;
2194
2100
  try {
2195
- for (const line of readFileSync6(path3, "utf8").split("\n")) {
2101
+ for (const line of readFileSync5(path3, "utf8").split("\n")) {
2196
2102
  const trimmed = line.trim();
2197
2103
  if (!trimmed) continue;
2198
2104
  const tab = trimmed.lastIndexOf(" ");
@@ -2228,44 +2134,44 @@ function noResume() {
2228
2134
  var TURN_PROTOCOL_VERSION = 1;
2229
2135
 
2230
2136
  // packages/agent-runtime/src/host-policy.ts
2231
- import { z as z5 } from "zod";
2232
- var hostPolicySchema = z5.object({
2137
+ import { z as z4 } from "zod";
2138
+ var hostPolicySchema = z4.object({
2233
2139
  // Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
2234
2140
  // notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
2235
2141
  // tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
2236
2142
  // on under `coding` mode.
2237
- hostFs: z5.boolean(),
2143
+ hostFs: z4.boolean(),
2238
2144
  // Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
2239
2145
  // web, not host reach — granted by default today, but expressible as a grant.
2240
- web: z5.boolean(),
2146
+ web: z4.boolean(),
2241
2147
  // Browser automation (the Playwright MCP surface). Varies by host: a companion has
2242
2148
  // it, the house executor does not (CT230).
2243
- browser: z5.boolean(),
2149
+ browser: z4.boolean(),
2244
2150
  // User-configured MCP servers permitted. False for the house executor
2245
2151
  // (CT227: Cabane agents run no user MCP servers), true for a personal companion.
2246
- userMcp: z5.boolean(),
2152
+ userMcp: z4.boolean(),
2247
2153
  // Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
2248
2154
  // amendment above): `false` on the locked assistant/house surface (banned via
2249
2155
  // `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
2250
2156
  // the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
2251
2157
  // allowlist. The subagent completes within the turn, so
2252
2158
  // it's not the turn-model invariant `scheduling` is.
2253
- subagents: z5.boolean(),
2159
+ subagents: z4.boolean(),
2254
2160
  // ── Hard platform invariants — always denied, never granted ────────────────
2255
2161
  // Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
2256
2162
  // families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
2257
2163
  // `result` fires; a scheduled callback fires after the reply window has closed
2258
2164
  // and strands the agent (the CT155/CT156 rule).
2259
- scheduling: z5.literal("never"),
2165
+ scheduling: z4.literal("never"),
2260
2166
  // Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
2261
2167
  // handler to answer a structured prompt, so the call hangs the turn
2262
2168
  // (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
2263
- uiPrompts: z5.literal("never")
2169
+ uiPrompts: z4.literal("never")
2264
2170
  });
2265
2171
 
2266
2172
  // packages/agent-runtime/src/turn-event.ts
2267
- import { z as z6 } from "zod";
2268
- var turnEventSchema = z6.discriminatedUnion("type", [
2173
+ import { z as z5 } from "zod";
2174
+ var turnEventSchema = z5.discriminatedUnion("type", [
2269
2175
  // The runtime's opaque session state, emitted when the adapter learns it (e.g.
2270
2176
  // the SDK `system/init` frame). The platform stores `state` verbatim per
2271
2177
  // (conversation, agent) and hands it back on the next turn; only the adapter
@@ -2285,19 +2191,19 @@ var turnEventSchema = z6.discriminatedUnion("type", [
2285
2191
  // on the companion, after the server committed the manifest). Runtime-neutral: a
2286
2192
  // plain boolean, not a runtime-specific reason string (that stays in the
2287
2193
  // adapter's `onWarn` log). Additive + optional — an old receiver ignores it.
2288
- z6.object({
2289
- type: z6.literal("session"),
2290
- state: z6.string(),
2291
- degraded: z6.boolean().optional()
2194
+ z5.object({
2195
+ type: z5.literal("session"),
2196
+ state: z5.string(),
2197
+ degraded: z5.boolean().optional()
2292
2198
  }),
2293
2199
  // One readable thinking summary. Maps `onThinking({ text })`. Transient —
2294
2200
  // surfaced live, never persisted as durable content.
2295
- z6.object({ type: z6.literal("thinking"), text: z6.string() }),
2201
+ z5.object({ type: z5.literal("thinking"), text: z5.string() }),
2296
2202
  // Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
2297
2203
  // `final`→`terminal`. `terminal: false` is interim narration (commits as a
2298
2204
  // `progress` row); `terminal: true` is the turn's closing reply (commits as
2299
2205
  // the `final` row).
2300
- z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
2206
+ z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
2301
2207
  // A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
2302
2208
  // `toolName`→`name` (already prefix-stripped: `cabane_read`, not
2303
2209
  // `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
@@ -2312,15 +2218,15 @@ var turnEventSchema = z6.discriminatedUnion("type", [
2312
2218
  // dropped the prefix; null for a host / built-in tool. The client tags Cabane
2313
2219
  // MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
2314
2220
  // pre-CT496 producer that never sets it is unaffected (treated as null).
2315
- z6.object({
2316
- type: z6.literal("tool"),
2317
- id: z6.string(),
2318
- name: z6.string(),
2319
- phase: z6.enum(["start", "done", "error"]),
2320
- summary: z6.string(),
2321
- input: z6.unknown().optional(),
2322
- result: z6.unknown().optional(),
2323
- mcpServer: z6.string().nullable().optional()
2221
+ z5.object({
2222
+ type: z5.literal("tool"),
2223
+ id: z5.string(),
2224
+ name: z5.string(),
2225
+ phase: z5.enum(["start", "done", "error"]),
2226
+ summary: z5.string(),
2227
+ input: z5.unknown().optional(),
2228
+ result: z5.unknown().optional(),
2229
+ mcpServer: z5.string().nullable().optional()
2324
2230
  }),
2325
2231
  // The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
2326
2232
  // inline. `ok:false` carries a machine reason (`no_session`, an error code);
@@ -2364,34 +2270,34 @@ var turnEventSchema = z6.discriminatedUnion("type", [
2364
2270
  // `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
2365
2271
  // backward-compatible: an old adapter/companion omits them, a cancel has no result
2366
2272
  // event at all, and the columns stay null → the UI shows `—`.
2367
- z6.object({
2368
- type: z6.literal("result"),
2369
- ok: z6.boolean(),
2370
- reason: z6.string().optional(),
2371
- usage: z6.object({
2372
- inputTokens: z6.number(),
2373
- outputTokens: z6.number(),
2374
- cacheReadTokens: z6.number().optional(),
2375
- cacheCreationTokens: z6.number().optional(),
2376
- contextTokens: z6.number().optional(),
2377
- contextWindow: z6.number().optional()
2273
+ z5.object({
2274
+ type: z5.literal("result"),
2275
+ ok: z5.boolean(),
2276
+ reason: z5.string().optional(),
2277
+ usage: z5.object({
2278
+ inputTokens: z5.number(),
2279
+ outputTokens: z5.number(),
2280
+ cacheReadTokens: z5.number().optional(),
2281
+ cacheCreationTokens: z5.number().optional(),
2282
+ contextTokens: z5.number().optional(),
2283
+ contextWindow: z5.number().optional()
2378
2284
  }).optional(),
2379
- resolvedModel: z6.string().optional(),
2380
- resolvedConfig: z6.object({
2381
- effort: z6.string().optional(),
2382
- thinking: z6.string().optional(),
2383
- reasoningEffort: z6.string().optional()
2285
+ resolvedModel: z5.string().optional(),
2286
+ resolvedConfig: z5.object({
2287
+ effort: z5.string().optional(),
2288
+ thinking: z5.string().optional(),
2289
+ reasoningEffort: z5.string().optional()
2384
2290
  }).optional()
2385
2291
  })
2386
2292
  ]);
2387
2293
 
2388
2294
  // packages/agent-runtime/src/failure.ts
2389
- import { z as z7 } from "zod";
2390
- var turnFailureSchema = z7.discriminatedUnion("kind", [
2391
- z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
2392
- z7.object({ kind: z7.literal("rate_limited") }),
2393
- z7.object({ kind: z7.literal("server_error") }),
2394
- z7.object({ kind: z7.literal("auth_expired") })
2295
+ import { z as z6 } from "zod";
2296
+ var turnFailureSchema = z6.discriminatedUnion("kind", [
2297
+ z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
2298
+ z6.object({ kind: z6.literal("rate_limited") }),
2299
+ z6.object({ kind: z6.literal("server_error") }),
2300
+ z6.object({ kind: z6.literal("auth_expired") })
2395
2301
  ]);
2396
2302
  var USAGE_CAPPED = "usage_capped";
2397
2303
  var RATE_LIMITED = "rate_limited";
@@ -2497,63 +2403,63 @@ var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
2497
2403
  var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
2498
2404
 
2499
2405
  // packages/agent-runtime/src/turn-request.ts
2500
- import { z as z8 } from "zod";
2501
- var contentBlockSchema = z8.discriminatedUnion("type", [
2502
- z8.object({ type: z8.literal("text"), text: z8.string() }),
2503
- z8.object({
2504
- type: z8.literal("image"),
2505
- source: z8.object({ type: z8.literal("url"), url: z8.string() })
2406
+ import { z as z7 } from "zod";
2407
+ var contentBlockSchema = z7.discriminatedUnion("type", [
2408
+ z7.object({ type: z7.literal("text"), text: z7.string() }),
2409
+ z7.object({
2410
+ type: z7.literal("image"),
2411
+ source: z7.object({ type: z7.literal("url"), url: z7.string() })
2506
2412
  }),
2507
- z8.object({
2508
- type: z8.literal("document"),
2509
- source: z8.object({ type: z8.literal("url"), url: z8.string() })
2413
+ z7.object({
2414
+ type: z7.literal("document"),
2415
+ source: z7.object({ type: z7.literal("url"), url: z7.string() })
2510
2416
  })
2511
2417
  ]);
2512
- var effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
2513
- var resolvedRunConfigSchema = z8.object({
2514
- model: z8.string().nullable(),
2418
+ var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
2419
+ var resolvedRunConfigSchema = z7.object({
2420
+ model: z7.string().nullable(),
2515
2421
  effort: effortLevelSchema.optional(),
2516
- runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
2422
+ runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
2517
2423
  });
2518
- var resolvedMcpServerSchema = z8.union([
2519
- z8.object({
2520
- type: z8.literal("stdio").optional(),
2521
- command: z8.string(),
2522
- args: z8.array(z8.string()).optional(),
2523
- env: z8.record(z8.string(), z8.string()).optional()
2424
+ var resolvedMcpServerSchema = z7.union([
2425
+ z7.object({
2426
+ type: z7.literal("stdio").optional(),
2427
+ command: z7.string(),
2428
+ args: z7.array(z7.string()).optional(),
2429
+ env: z7.record(z7.string(), z7.string()).optional()
2524
2430
  }),
2525
- z8.object({
2526
- type: z8.literal("http"),
2527
- url: z8.string(),
2528
- headers: z8.record(z8.string(), z8.string()).optional()
2431
+ z7.object({
2432
+ type: z7.literal("http"),
2433
+ url: z7.string(),
2434
+ headers: z7.record(z7.string(), z7.string()).optional()
2529
2435
  }),
2530
- z8.object({
2531
- type: z8.literal("sse"),
2532
- url: z8.string(),
2533
- headers: z8.record(z8.string(), z8.string()).optional()
2436
+ z7.object({
2437
+ type: z7.literal("sse"),
2438
+ url: z7.string(),
2439
+ headers: z7.record(z7.string(), z7.string()).optional()
2534
2440
  })
2535
2441
  ]);
2536
- var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
2537
- var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
2538
- var turnRequestSchema = z8.object({
2442
+ var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
2443
+ var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
2444
+ var turnRequestSchema = z7.object({
2539
2445
  // Server-composed system prompt (core + capability prose + adapter addendum +
2540
2446
  // charter). One string to the adapter.
2541
- systemPrompt: z8.string(),
2447
+ systemPrompt: z7.string(),
2542
2448
  // Server-composed per-turn user text (anchor reminder + the triggering message).
2543
- prompt: z8.string(),
2449
+ prompt: z7.string(),
2544
2450
  // The multi-block user-message body (text + vision).
2545
- content: z8.array(contentBlockSchema),
2451
+ content: z7.array(contentBlockSchema),
2546
2452
  // Portable-or-dialect run-config (above).
2547
2453
  config: resolvedRunConfigSchema,
2548
2454
  // Abstract capability grants; the adapter maps them to tool names.
2549
2455
  policy: hostPolicySchema,
2550
2456
  // Prior opaque session state, or null for a fresh session.
2551
- session: z8.string().nullable(),
2457
+ session: z7.string().nullable(),
2552
2458
  // The cabane control-plane coordinates for this turn's MCP + post-back.
2553
- cabane: z8.object({
2554
- mcpUrl: z8.string(),
2555
- bearer: z8.string(),
2556
- activeConversationId: z8.string(),
2459
+ cabane: z7.object({
2460
+ mcpUrl: z7.string(),
2461
+ bearer: z7.string(),
2462
+ activeConversationId: z7.string(),
2557
2463
  // CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
2558
2464
  // EXTERNAL adapters (Codex / opencode) mount it by URL under the key
2559
2465
  // `cabane_companion` — using the same `bearer` (the turn token) and the same
@@ -2563,7 +2469,7 @@ var turnRequestSchema = z8.object({
2563
2469
  // claude-code ignores it (it mounts the in-process instance instead), and
2564
2470
  // every existing `cabane`-block fixture keeps parsing unchanged; the
2565
2471
  // companion always populates it (`build-options.ts`).
2566
- turnControlUrl: z8.string().optional(),
2472
+ turnControlUrl: z7.string().optional(),
2567
2473
  // CT598: the workspace this turn runs in. The claude-code/opencode/codex
2568
2474
  // adapters never need it (they reach Cabane through the `cabane` MCP server,
2569
2475
  // which takes `workspaceId` as a per-tool arg the model supplies); the
@@ -2573,53 +2479,39 @@ var turnRequestSchema = z8.object({
2573
2479
  // adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
2574
2480
  // always populates it (`build-options.ts`), and the native adapter fails the
2575
2481
  // turn loudly when it is somehow absent rather than guessing.
2576
- workspaceId: z8.string().optional(),
2482
+ workspaceId: z7.string().optional(),
2577
2483
  // CT752: the server-resolved workspace surface this credential exposes.
2578
2484
  // Readiness uses this explicit fact to require `sdk` for code mode and the
2579
2485
  // granular floor for classic mode; inventory contents alone cannot infer it
2580
2486
  // because `sdk` is intentionally also available on the classic surface.
2581
- workspaceToolSurface: z8.enum(["code", "classic"]).optional()
2487
+ workspaceToolSurface: z7.enum(["code", "classic"]).optional()
2582
2488
  }),
2583
2489
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
2584
2490
  // prepare hook, and the resolved user MCP servers.
2585
- local: z8.object({
2586
- cwd: z8.string().optional(),
2587
- env: z8.record(z8.string(), z8.string()).optional(),
2588
- nativeWorkAssignment: z8.object({
2589
- itemId: z8.string(),
2590
- executionId: z8.string(),
2591
- activationEpoch: z8.number().int().nonnegative()
2592
- }).strict().optional(),
2491
+ local: z7.object({
2492
+ cwd: z7.string().optional(),
2493
+ env: z7.record(z7.string(), z7.string()).optional(),
2593
2494
  mcpServers: resolvedMcpServersSchema.optional(),
2594
2495
  // CT289: machine-local claude-code adapter knobs the operator sets on a
2595
2496
  // companion they run themselves — the auto-memory escape hatch. `autoMemory:
2596
2497
  // true` opts back into Claude Code's auto-memory (governed by the operator's
2597
2498
  // own `.claude/settings.json`); absent/false leaves the adapter's force-off
2598
- // default in place (see `buildClaudeCodeOptions`). The In-Cabane executor never
2599
- // sets it, so house stays force-off unconditionally.
2600
- claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
2499
+ // default in place (see `buildClaudeCodeOptions`).
2500
+ claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
2601
2501
  }),
2602
2502
  // Host-owned injected servers (host-filled) — e.g. the summon server.
2603
- extra: z8.object({
2604
- mcpServers: hostInjectedServersSchema,
2605
- // CT666: the per-turn turn-control HANDLER for the in-process cabane-native
2606
- // runtime — the seam that gives it ask/wake_me/summon_agent/sub_agent/skip_turn
2607
- // without a subprocess `cabane_companion` MCP server. Typed `unknown` for the same
2608
- // reason as `mcpServers`: it's a live host object (a `NativeTurnControl` whose
2609
- // methods close over the dispatcher's per-turn state), passed to the native
2610
- // adapter WITHOUT the contract package inspecting it. The subprocess adapters
2611
- // ignore it (they get the same verbs from the injected SDK server instead).
2612
- turnControl: z8.unknown().optional()
2503
+ extra: z7.object({
2504
+ mcpServers: hostInjectedServersSchema
2613
2505
  })
2614
2506
  });
2615
2507
 
2616
2508
  // packages/agent-runtime/src/conformance.ts
2617
- import { z as z9 } from "zod";
2618
- var conformanceFixtureSchema = z9.object({
2619
- name: z9.string(),
2509
+ import { z as z8 } from "zod";
2510
+ var conformanceFixtureSchema = z8.object({
2511
+ name: z8.string(),
2620
2512
  request: turnRequestSchema,
2621
- nativeStream: z9.array(z9.unknown()),
2622
- expected: z9.array(turnEventSchema)
2513
+ nativeStream: z8.array(z8.unknown()),
2514
+ expected: z8.array(turnEventSchema)
2623
2515
  });
2624
2516
 
2625
2517
  // packages/agent-runtime/src/transcript.ts
@@ -2629,8 +2521,8 @@ function createTerminalTextBuffer() {
2629
2521
  async function safeEmit(emit, event, onError) {
2630
2522
  try {
2631
2523
  await emit(event);
2632
- } catch (err2) {
2633
- onError?.(err2, event.type);
2524
+ } catch (err) {
2525
+ onError?.(err, event.type);
2634
2526
  }
2635
2527
  }
2636
2528
  async function processAssistantMessage(msg, emit, pending, buffer, onError) {
@@ -2891,16 +2783,16 @@ var TurnPump = class {
2891
2783
  // minimal note. Skipped when cancelled or already final. The held-text flush
2892
2784
  // that precedes it is a classification concern, driven by the caller before
2893
2785
  // this runs.
2894
- async finalize(ok2) {
2895
- if (!ok2 || this.opts.signal.aborted || this.emittedFinal) return;
2786
+ async finalize(ok) {
2787
+ if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
2896
2788
  const body = this.lastProgressBody ?? this.opts.emptyFinalBody;
2897
2789
  const seq = this.opts.nextSeq();
2898
2790
  try {
2899
2791
  await this.opts.commit.commitMessage({ body, kind: "final", seq });
2900
2792
  this.emittedFinal = true;
2901
2793
  this.finalReplyBody = body;
2902
- } catch (err2) {
2903
- this.opts.onError?.(err2, "empty-final");
2794
+ } catch (err) {
2795
+ this.opts.onError?.(err, "empty-final");
2904
2796
  }
2905
2797
  }
2906
2798
  // Whether the turn has committed its `final` row — read by the host to decide
@@ -2924,7 +2816,7 @@ import {
2924
2816
  var CLAUDE_CODE_ADDENDUM = "";
2925
2817
 
2926
2818
  // packages/agent-runtime/src/claude-code/policy.ts
2927
- import { z as z10 } from "zod";
2819
+ import { z as z9 } from "zod";
2928
2820
  var HOST_FS_TOOLS = [
2929
2821
  // shell + local filesystem
2930
2822
  "Bash",
@@ -2974,30 +2866,20 @@ function withThinkingSummaries(thinking) {
2974
2866
  if (thinking.type === "disabled") return thinking;
2975
2867
  return { display: "summarized", ...thinking };
2976
2868
  }
2977
- var claudeCodeDialectSchema = z10.object({
2978
- thinking: z10.discriminatedUnion("type", [
2979
- z10.object({
2980
- type: z10.literal("adaptive"),
2981
- display: z10.enum(["summarized", "omitted"]).optional()
2869
+ var claudeCodeDialectSchema = z9.object({
2870
+ thinking: z9.discriminatedUnion("type", [
2871
+ z9.object({
2872
+ type: z9.literal("adaptive"),
2873
+ display: z9.enum(["summarized", "omitted"]).optional()
2982
2874
  }),
2983
- z10.object({
2984
- type: z10.literal("enabled"),
2985
- budgetTokens: z10.number().int().positive().optional(),
2986
- display: z10.enum(["summarized", "omitted"]).optional()
2875
+ z9.object({
2876
+ type: z9.literal("enabled"),
2877
+ budgetTokens: z9.number().int().positive().optional(),
2878
+ display: z9.enum(["summarized", "omitted"]).optional()
2987
2879
  }),
2988
- z10.object({ type: z10.literal("disabled") })
2880
+ z9.object({ type: z9.literal("disabled") })
2989
2881
  ]).optional(),
2990
- allowedTools: z10.array(z10.string()).optional(),
2991
- disallowedTools: z10.array(z10.string()).optional(),
2992
- // Which claude-code harness shape to run. `coding` switches to the
2993
- // `claude_code` preset + project settings + always-allow `PreToolUse` hook;
2994
- // `custom`/`assistant` (or absent) use a plain-string prompt + bypass. This
2995
- // is the claude-code-specific PRESET selector — kept distinct from
2996
- // `policy.hostFs` (the host-fs BLOCK), because companion `custom` mode wants host
2997
- // fs available (via its own allowlist) WITHOUT the coding harness, and in-app
2998
- // `custom` wants host fs blocked — neither of which a single `hostFs` boolean
2999
- // can express alongside the preset choice.
3000
- mode: z10.enum(["assistant", "coding", "custom"]).optional()
2882
+ hostAccess: z9.boolean().optional()
3001
2883
  }).loose();
3002
2884
  function readThinking(runtimeOptions) {
3003
2885
  const dialect = runtimeOptions?.["claude-code"];
@@ -3076,18 +2958,15 @@ function buildClaudeCodeOptions(req, augment) {
3076
2958
  };
3077
2959
  }
3078
2960
  const dialect = claudeCodeDialectSchema.safeParse(config.runtimeOptions?.["claude-code"] ?? {});
3079
- const customAllowed = dialect.success ? dialect.data.allowedTools ?? [] : [];
3080
- const customDisallowed = dialect.success ? dialect.data.disallowedTools ?? [] : [];
3081
- const useCodingPreset = (dialect.success ? dialect.data.mode : void 0) === "coding";
2961
+ const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
3082
2962
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
3083
2963
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
3084
2964
  const allowedTools = dedupe([
3085
2965
  cabaneGlob,
3086
2966
  ...extraServerGlobs,
3087
- ...policy.web ? DEFAULT_WEB_TOOLS : [],
3088
- ...customAllowed
2967
+ ...policy.web ? DEFAULT_WEB_TOOLS : []
3089
2968
  ]);
3090
- const disallowedTools = dedupe([...disallowedToolsFor(policy), ...customDisallowed]);
2969
+ const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
3091
2970
  const resumeDecision = decideResume(req.session, cwd);
3092
2971
  const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
3093
2972
  const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
@@ -3153,7 +3032,7 @@ async function* decodeSdkStream(iter, ctx) {
3153
3032
  out.push(event);
3154
3033
  };
3155
3034
  let sessionEmitted = false;
3156
- let ok2 = false;
3035
+ let ok = false;
3157
3036
  let resultReason;
3158
3037
  let sawResult = false;
3159
3038
  let usage;
@@ -3199,8 +3078,8 @@ async function* decodeSdkStream(iter, ctx) {
3199
3078
  if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
3200
3079
  }
3201
3080
  } else if (msg.type === "auth_status") {
3202
- const err2 = msg.error;
3203
- if (typeof err2 === "string" && err2.length > 0) authError = err2;
3081
+ const err = msg.error;
3082
+ if (typeof err === "string" && err.length > 0) authError = err;
3204
3083
  } else if (msg.type === "result") {
3205
3084
  sawResult = true;
3206
3085
  usage = readSdkUsage(msg);
@@ -3212,7 +3091,7 @@ async function* decodeSdkStream(iter, ctx) {
3212
3091
  }
3213
3092
  const isError = msg.is_error === true;
3214
3093
  if (msg.subtype === "success" && !isError) {
3215
- ok2 = true;
3094
+ ok = true;
3216
3095
  } else {
3217
3096
  const resultText = msg.result ?? "";
3218
3097
  const terminalReason = msg.terminal_reason;
@@ -3226,26 +3105,26 @@ async function* decodeSdkStream(iter, ctx) {
3226
3105
  ...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
3227
3106
  } : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
3228
3107
  resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
3229
- ok2 = false;
3108
+ ok = false;
3230
3109
  }
3231
3110
  break;
3232
3111
  }
3233
3112
  }
3234
- } catch (err2) {
3235
- if (ctx.signal.aborted) throw err2;
3236
- const failure = classifyErrorText(err2 instanceof Error ? err2.message : String(err2));
3237
- if (!failure) throw err2;
3238
- ok2 = false;
3113
+ } catch (err) {
3114
+ if (ctx.signal.aborted) throw err;
3115
+ const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
3116
+ if (!failure) throw err;
3117
+ ok = false;
3239
3118
  resultReason = encodeFailureReason(failure);
3240
3119
  sawResult = true;
3241
3120
  }
3242
3121
  if (ctx.signal.aborted) return;
3243
- await flushHeldText(buffer, emit, ok2);
3122
+ await flushHeldText(buffer, emit, ok);
3244
3123
  yield* drain(out);
3245
- if (!ok2 && !resultReason && !sawResult) resultReason = "no_result";
3124
+ if (!ok && !resultReason && !sawResult) resultReason = "no_result";
3246
3125
  yield {
3247
3126
  type: "result",
3248
- ok: ok2,
3127
+ ok,
3249
3128
  ...resultReason ? { reason: resultReason } : {},
3250
3129
  ...usage ? { usage } : {},
3251
3130
  ...resolvedModel ? { resolvedModel } : {}
@@ -3808,9 +3687,9 @@ function readSessionId(properties) {
3808
3687
  }
3809
3688
  function readSessionError(properties) {
3810
3689
  const props = asRecord(properties);
3811
- const err2 = props?.error;
3812
- if (typeof err2 === "string") return err2;
3813
- const rec2 = asRecord(err2);
3690
+ const err = props?.error;
3691
+ if (typeof err === "string") return err;
3692
+ const rec2 = asRecord(err);
3814
3693
  if (!rec2) return "unknown";
3815
3694
  const { name, message } = deepestError(rec2);
3816
3695
  if (message && name && !isGenericErrorName(name)) return `${name}: ${message}`;
@@ -3846,7 +3725,7 @@ async function* decodeOpencodeStream(events, ctx) {
3846
3725
  const pending = /* @__PURE__ */ new Map();
3847
3726
  const startedTools = /* @__PURE__ */ new Set();
3848
3727
  const finishedTools = /* @__PURE__ */ new Set();
3849
- let ok2 = false;
3728
+ let ok = false;
3850
3729
  let reason;
3851
3730
  let settled = false;
3852
3731
  const userMessageIds = /* @__PURE__ */ new Set();
@@ -3909,7 +3788,7 @@ async function* decodeOpencodeStream(events, ctx) {
3909
3788
  const sealed = sealHeld(held, true);
3910
3789
  held = null;
3911
3790
  if (sealed) yield sealed;
3912
- ok2 = true;
3791
+ ok = true;
3913
3792
  settled = true;
3914
3793
  break;
3915
3794
  } else if (ev.type === "session.error") {
@@ -3918,7 +3797,7 @@ async function* decodeOpencodeStream(events, ctx) {
3918
3797
  const sealed = sealHeld(held, false);
3919
3798
  held = null;
3920
3799
  if (sealed) yield sealed;
3921
- ok2 = false;
3800
+ ok = false;
3922
3801
  const errorText = readSessionError(ev.properties);
3923
3802
  const failure = classifyErrorText(errorText);
3924
3803
  reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
@@ -3933,7 +3812,7 @@ async function* decodeOpencodeStream(events, ctx) {
3933
3812
  if (sealed) yield sealed;
3934
3813
  reason = "no_terminal";
3935
3814
  }
3936
- yield { type: "result", ok: ok2, ...reason ? { reason } : {} };
3815
+ yield { type: "result", ok, ...reason ? { reason } : {} };
3937
3816
  }
3938
3817
  function hasToolInput(input) {
3939
3818
  return !!input && typeof input === "object" && Object.keys(input).length > 0;
@@ -3947,7 +3826,7 @@ function sealHeld(held, terminal) {
3947
3826
  }
3948
3827
 
3949
3828
  // packages/agent-runtime/src/opencode/policy.ts
3950
- import { z as z11 } from "zod";
3829
+ import { z as z10 } from "zod";
3951
3830
  var OPENCODE_HOST_TOOLS = [
3952
3831
  "bash",
3953
3832
  "edit",
@@ -3974,8 +3853,8 @@ function opencodeToolPolicy(policy) {
3974
3853
  deny(OPENCODE_UI_PROMPT_TOOLS);
3975
3854
  return { tools, allowAllHostTools: policy.hostFs };
3976
3855
  }
3977
- var opencodeDialectSchema = z11.object({
3978
- agent: z11.string().min(1).optional()
3856
+ var opencodeDialectSchema = z10.object({
3857
+ agent: z10.string().min(1).optional()
3979
3858
  }).loose();
3980
3859
  function readOpencodeDialect(runtimeOptions) {
3981
3860
  const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
@@ -4196,9 +4075,9 @@ function createHttpOpencodeTransport(opts) {
4196
4075
  // The lock is released when this stream finishes draining.
4197
4076
  events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
4198
4077
  };
4199
- } catch (err2) {
4078
+ } catch (err) {
4200
4079
  release();
4201
- throw err2;
4080
+ throw err;
4202
4081
  }
4203
4082
  }
4204
4083
  };
@@ -4791,9 +4670,9 @@ function readItemType(item) {
4791
4670
  function readErrorMessage(ev) {
4792
4671
  const direct = str(ev.message);
4793
4672
  if (direct) return direct;
4794
- const err2 = asRecord2(ev.error);
4795
- if (err2) {
4796
- const m = str(err2.message);
4673
+ const err = asRecord2(ev.error);
4674
+ if (err) {
4675
+ const m = str(err.message);
4797
4676
  if (m) return m;
4798
4677
  }
4799
4678
  return "unknown";
@@ -4849,7 +4728,7 @@ async function* decodeCodexStream(events, ctx) {
4849
4728
  const startedTools = /* @__PURE__ */ new Set();
4850
4729
  const finishedTools = /* @__PURE__ */ new Set();
4851
4730
  let sessionEmitted = false;
4852
- let ok2 = false;
4731
+ let ok = false;
4853
4732
  let reason;
4854
4733
  let usage;
4855
4734
  let settled = false;
@@ -4880,7 +4759,7 @@ async function* decodeCodexStream(events, ctx) {
4880
4759
  const message = readItemMessage(item);
4881
4760
  if (isModelMetadataError(message)) {
4882
4761
  yield* flushInterim();
4883
- ok2 = false;
4762
+ ok = false;
4884
4763
  reason = `model_unavailable:${message.slice(0, 200)}`;
4885
4764
  settled = true;
4886
4765
  break;
@@ -4935,13 +4814,13 @@ async function* decodeCodexStream(events, ctx) {
4935
4814
  held = null;
4936
4815
  if (sealed) yield sealed;
4937
4816
  usage = readUsage(ev);
4938
- ok2 = true;
4817
+ ok = true;
4939
4818
  settled = true;
4940
4819
  break;
4941
4820
  }
4942
4821
  if (ev.type === "turn.failed" || ev.type === "error") {
4943
4822
  yield* flushInterim();
4944
- ok2 = false;
4823
+ ok = false;
4945
4824
  const text = readErrorMessage(ev);
4946
4825
  const failure = classifyErrorText(text);
4947
4826
  reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
@@ -4959,7 +4838,7 @@ async function* decodeCodexStream(events, ctx) {
4959
4838
  const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
4960
4839
  yield {
4961
4840
  type: "result",
4962
- ok: ok2,
4841
+ ok,
4963
4842
  ...reason ? { reason } : {},
4964
4843
  ...usage ? { usage } : {},
4965
4844
  ...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
@@ -4977,7 +4856,7 @@ function sealHeld2(held, terminal) {
4977
4856
  }
4978
4857
 
4979
4858
  // packages/agent-runtime/src/codex/policy.ts
4980
- import { z as z12 } from "zod";
4859
+ import { z as z11 } from "zod";
4981
4860
  function codexToolPolicy(policy) {
4982
4861
  return policy.hostFs ? {
4983
4862
  permissionProfile: "cabane-coding",
@@ -4992,8 +4871,8 @@ function codexToolPolicy(policy) {
4992
4871
  };
4993
4872
  }
4994
4873
  var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
4995
- var codexDialectSchema = z12.object({
4996
- modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
4874
+ var codexDialectSchema = z11.object({
4875
+ modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
4997
4876
  }).loose();
4998
4877
  function readCodexDialect(runtimeOptions) {
4999
4878
  const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
@@ -5001,7 +4880,7 @@ function readCodexDialect(runtimeOptions) {
5001
4880
  }
5002
4881
 
5003
4882
  // packages/agent-runtime/src/codex/model.ts
5004
- var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default", "codex"]);
4883
+ var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
5005
4884
  function parseCodexModel(model) {
5006
4885
  const sep = model.indexOf("/");
5007
4886
  const bare = sep === -1 || model.slice(0, sep) !== "openai" ? model : model.slice(sep + 1);
@@ -5088,9 +4967,11 @@ function buildConfig(req) {
5088
4967
  };
5089
4968
  }
5090
4969
  const policy = codexToolPolicy(req.policy);
4970
+ const tmpDir = req.local.env?.TMPDIR;
5091
4971
  return {
5092
4972
  mcp_servers,
5093
4973
  experimental_use_rmcp_client: true,
4974
+ ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
5094
4975
  ...policy.permissionProfile ? {
5095
4976
  // CT733: named permission profiles are Codex's split-filesystem path.
5096
4977
  // `:root = read` preserves coding-mode host reads; the one explicit
@@ -5368,7 +5249,7 @@ var CodexExec = class {
5368
5249
  signal: args.signal
5369
5250
  });
5370
5251
  let spawnError = null;
5371
- child.once("error", (err2) => spawnError = err2);
5252
+ child.once("error", (err) => spawnError = err);
5372
5253
  if (!child.stdin) {
5373
5254
  child.kill();
5374
5255
  throw new Error("Child process has no stdin");
@@ -6272,1027 +6153,6 @@ var CODEX_CONFORMANCE_FIXTURES = [
6272
6153
  }
6273
6154
  ];
6274
6155
 
6275
- // packages/agent-runtime/src/cabane-native/addendum.ts
6276
- var CABANE_NATIVE_ADDENDUM = `## Your tools (native runtime)
6277
-
6278
- You are running on Cabane's own agent runtime. You have a small, curated set of workspace tools, all prefixed \`cabane_\`:
6279
-
6280
- - \`cabane_list\` \u2014 list a folder's files and subfolders.
6281
- - \`cabane_read\` \u2014 read one file's contents by path.
6282
- - \`cabane_search\` \u2014 substring search across file/folder names, file contents, and conversations.
6283
- - \`cabane_write\` \u2014 create or overwrite a file (pass \`overwrite: true\` to replace).
6284
- - \`cabane_edit\` \u2014 find/replace inside an existing file.
6285
- - \`cabane_mkdir\` \u2014 create a folder (pass \`recursive: true\` to also make missing parents).
6286
- - \`cabane_move\` \u2014 move or rename a file or folder.
6287
- - \`cabane_delete\` \u2014 delete a file or folder (folders need \`recursive: true\`; soft-delete, recoverable).
6288
- - \`cabane_context\` \u2014 gather an object plus its related context (files, conversations, people) in one call \u2014 your first move to understand what surrounds something.
6289
- - \`cabane_messages\` \u2014 read a conversation's messages by id (from \`cabane_search\` / \`cabane_context\`).
6290
-
6291
- Paths are workspace-relative with a leading slash (\`/notes/todo.md\`). This is a deliberately curated tool set \u2014 if a job needs an operation that isn't here, say so plainly in your reply rather than inventing a tool. Your reply text is streamed straight into the conversation; there is no separate send step.`;
6292
- var CABANE_NATIVE_ADDENDUM_CODE_MODE = `## Your tools (native runtime \u2014 code mode)
6293
-
6294
- You are running on Cabane's own agent runtime, in **code mode**. Your one workspace tool is \`sdk\`: you act on the workspace by writing a short TypeScript program against the ambient \`cabane\` object and submitting it as the \`code\` argument. Only what your program \`return\`s (plus \`console.log\`) comes back \u2014 so gather, filter, and summarize *inside* the program and return the small answer, not the raw material.
6295
-
6296
- There is **no** \`cabane_read\` / \`cabane_write\` / \`cabane_search\` / \`cabane_edit\` tool \u2014 those are \`cabane\` SDK calls *inside* your program (\`cabane.files.read(path)\`, \`cabane.find.search(q)\`, \u2026), not tools you call directly. The full SDK surface \u2014 every method and its options \u2014 is documented below this note in your prompt.
6297
-
6298
- Beside \`sdk\` you also have a few **plain-named** tools that act on the TURN itself, not the workspace (they can't be a program's \`return\` value):
6299
- - \`ask\` \u2014 put a structured question to a human and END your turn (they may answer in days).
6300
- - \`wake_me\` \u2014 end this turn now and be re-dispatched later to check a condition (a PR merging, a reply landing).
6301
- - \`summon_agent\` \u2014 pull a peer agent into THIS conversation to reply on your turn.
6302
- - \`sub_agent\` \u2014 spawn a private worker with a fresh context window; its result comes back here later (don't wait \u2014 end your turn).
6303
- - \`skip_turn\` \u2014 end your turn with NO reply, when the message doesn't need one from you.
6304
- - \`mint_render_token\` \u2014 mint a short-lived credential to load a workspace HTML page in a browser.
6305
-
6306
- Your reply text is streamed straight into the conversation; there is no separate send step.`;
6307
-
6308
- // packages/agent-runtime/src/cabane-native/context.ts
6309
- var DEFAULT_HISTORY_LIMIT = 20;
6310
- var DEFAULT_MAX_HISTORY_CHARS = 24e3;
6311
- async function assembleMessages(systemPrompt, content, fallbackPrompt, opts) {
6312
- const messages = [{ role: "system", content: systemPrompt }];
6313
- const history = await fetchRecentHistory(opts);
6314
- for (const m of history) messages.push(m);
6315
- messages.push({ role: "user", content: currentUserText(content, fallbackPrompt) });
6316
- return messages;
6317
- }
6318
- function currentUserText(content, fallbackPrompt) {
6319
- const text = content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
6320
- return text.length > 0 ? text : fallbackPrompt;
6321
- }
6322
- async function fetchRecentHistory(opts) {
6323
- const doFetch = opts.fetchImpl ?? fetch;
6324
- const limit = opts.historyLimit ?? DEFAULT_HISTORY_LIMIT;
6325
- const url = new URL(
6326
- `${opts.apiRoot}/workspaces/${opts.workspaceId}/conversations/${opts.conversationId}/messages`
6327
- );
6328
- url.searchParams.set("limit", String(limit));
6329
- url.searchParams.set("order", "desc");
6330
- let rows;
6331
- try {
6332
- const res = await doFetch(url.toString(), {
6333
- headers: { Authorization: `Bearer ${opts.bearer}` }
6334
- });
6335
- if (!res.ok) return [];
6336
- const body = await res.json();
6337
- rows = body.messages ?? [];
6338
- } catch {
6339
- return [];
6340
- }
6341
- const chronological = [...rows].reverse();
6342
- while (chronological.length > 0 && chronological[chronological.length - 1].role === "user") {
6343
- chronological.pop();
6344
- }
6345
- const mapped = [];
6346
- for (const r of chronological) {
6347
- const role = r.role === "agent" ? "assistant" : r.role === "user" ? "user" : null;
6348
- if (!role) continue;
6349
- const body = (r.body ?? "").trim();
6350
- if (body.length === 0) continue;
6351
- mapped.push({ role, content: body });
6352
- }
6353
- return capHistory(mapped, opts.maxHistoryChars ?? DEFAULT_MAX_HISTORY_CHARS);
6354
- }
6355
- function capHistory(messages, maxChars) {
6356
- let total = messages.reduce((n, m) => n + m.content.length, 0);
6357
- let start2 = 0;
6358
- while (total > maxChars && start2 < messages.length) {
6359
- total -= messages[start2].content.length;
6360
- start2 += 1;
6361
- }
6362
- return messages.slice(start2);
6363
- }
6364
-
6365
- // packages/agent-runtime/src/cabane-native/model.ts
6366
- var CABANE_NATIVE_MODEL_PREFIX = "cabane-native/";
6367
- function parseCabaneNativeModel(model) {
6368
- return model.startsWith(CABANE_NATIVE_MODEL_PREFIX) ? model.slice(CABANE_NATIVE_MODEL_PREFIX.length) : model;
6369
- }
6370
-
6371
- // packages/agent-runtime/src/cabane-native/policy.ts
6372
- import { z as z13 } from "zod";
6373
- var NATIVE_SURFACES = ["code", "classic"];
6374
- var cabaneNativeDialectSchema = z13.object({ surface: z13.enum(NATIVE_SURFACES).optional() }).loose();
6375
- function readCabaneNativeDialect(runtimeOptions) {
6376
- const parsed = cabaneNativeDialectSchema.safeParse(runtimeOptions?.["cabane-native"] ?? {});
6377
- return parsed.success ? parsed.data : {};
6378
- }
6379
- function cabaneNativeSurface(runtimeOptions) {
6380
- return readCabaneNativeDialect(runtimeOptions).surface ?? "code";
6381
- }
6382
-
6383
- // packages/agent-runtime/src/cabane-native/tools.ts
6384
- var TOOL_RESULT_MAX_CHARS = 8e3;
6385
- var CABANE_NATIVE_TOOLS = [
6386
- {
6387
- type: "function",
6388
- function: {
6389
- name: "cabane_list",
6390
- description: "List the files and subfolders at a workspace folder path. Omit `path` for the root.",
6391
- parameters: {
6392
- type: "object",
6393
- properties: {
6394
- path: {
6395
- type: "string",
6396
- description: "Workspace folder path, e.g. /notes. Defaults to /."
6397
- }
6398
- }
6399
- }
6400
- }
6401
- },
6402
- {
6403
- type: "function",
6404
- function: {
6405
- name: "cabane_read",
6406
- description: "Read the contents of one file at a workspace path.",
6407
- parameters: {
6408
- type: "object",
6409
- properties: {
6410
- path: { type: "string", description: "Workspace file path, e.g. /notes/todo.md." }
6411
- },
6412
- required: ["path"]
6413
- }
6414
- }
6415
- },
6416
- {
6417
- type: "function",
6418
- function: {
6419
- name: "cabane_search",
6420
- description: "Case-insensitive substring search across file names and file contents. Optionally scope to a subtree with `path`.",
6421
- parameters: {
6422
- type: "object",
6423
- properties: {
6424
- q: { type: "string", description: "The search string." },
6425
- path: {
6426
- type: "string",
6427
- description: "Optional workspace subtree to scope the search to."
6428
- }
6429
- },
6430
- required: ["q"]
6431
- }
6432
- }
6433
- },
6434
- {
6435
- type: "function",
6436
- function: {
6437
- name: "cabane_write",
6438
- description: "Create a file at a workspace path (missing parent folders are created). Pass `overwrite: true` to replace an existing file instead of failing on a name conflict.",
6439
- parameters: {
6440
- type: "object",
6441
- properties: {
6442
- path: { type: "string", description: "Workspace file path, e.g. /notes/new.md." },
6443
- content: { type: "string", description: "The file contents." },
6444
- overwrite: { type: "boolean", description: "Replace an existing file (default false)." }
6445
- },
6446
- required: ["path", "content"]
6447
- }
6448
- }
6449
- },
6450
- {
6451
- type: "function",
6452
- function: {
6453
- name: "cabane_edit",
6454
- description: "Modify an existing file with a single find/replace. By default `find` must occur exactly once; set `replaceAll: true` to replace every occurrence.",
6455
- parameters: {
6456
- type: "object",
6457
- properties: {
6458
- path: { type: "string", description: "Workspace file path to edit." },
6459
- find: { type: "string", description: "The substring to find." },
6460
- replace: { type: "string", description: "The replacement." },
6461
- replaceAll: { type: "boolean", description: "Replace every occurrence (default false)." }
6462
- },
6463
- required: ["path", "find", "replace"]
6464
- }
6465
- }
6466
- },
6467
- {
6468
- type: "function",
6469
- function: {
6470
- name: "cabane_mkdir",
6471
- description: "Create a folder at a workspace path. Pass `recursive: true` to also create any missing parent folders (like `mkdir -p`); otherwise the parent must already exist.",
6472
- parameters: {
6473
- type: "object",
6474
- properties: {
6475
- path: { type: "string", description: "Workspace folder path, e.g. /notes/archive." },
6476
- recursive: {
6477
- type: "boolean",
6478
- description: "Create missing parent folders too (default false)."
6479
- }
6480
- },
6481
- required: ["path"]
6482
- }
6483
- }
6484
- },
6485
- {
6486
- type: "function",
6487
- function: {
6488
- name: "cabane_move",
6489
- description: "Move and/or rename a file or folder. `fromPath` is the current path; `toPath` is the new full path (its parent must already exist). Same parent + new name renames in place; a different parent moves it.",
6490
- parameters: {
6491
- type: "object",
6492
- properties: {
6493
- fromPath: { type: "string", description: "The current file/folder path." },
6494
- toPath: { type: "string", description: "The new full path (parent + name)." }
6495
- },
6496
- required: ["fromPath", "toPath"]
6497
- }
6498
- }
6499
- },
6500
- {
6501
- type: "function",
6502
- function: {
6503
- name: "cabane_delete",
6504
- description: "Delete the file or folder at a workspace path. Folders require `recursive: true` (deletes everything inside). Soft delete \u2014 the entry is recoverable from the trash, not erased.",
6505
- parameters: {
6506
- type: "object",
6507
- properties: {
6508
- path: { type: "string", description: "Workspace file/folder path to delete." },
6509
- recursive: {
6510
- type: "boolean",
6511
- description: "Required to delete a non-empty folder (default false)."
6512
- }
6513
- },
6514
- required: ["path"]
6515
- }
6516
- }
6517
- },
6518
- {
6519
- type: "function",
6520
- function: {
6521
- name: "cabane_context",
6522
- description: "Gather a workspace object plus its most relevant connected context \u2014 related files, conversations, people \u2014 in one call. Your first move to understand what surrounds something before acting. `ref` is the object id, or its workspace path when `kind` is `file` or `folder`.",
6523
- parameters: {
6524
- type: "object",
6525
- properties: {
6526
- kind: {
6527
- type: "string",
6528
- enum: ["conversation", "file", "folder", "user", "agent", "channel"],
6529
- description: "The kind of object to orient around."
6530
- },
6531
- ref: {
6532
- type: "string",
6533
- description: "The object id (or the workspace path for a file/folder)."
6534
- }
6535
- },
6536
- required: ["kind", "ref"]
6537
- }
6538
- }
6539
- },
6540
- {
6541
- type: "function",
6542
- function: {
6543
- name: "cabane_messages",
6544
- description: 'Read a page of messages from a conversation by id (get ids from `cabane_search` or `cabane_context`). `order: "desc"` (default) returns newest-first \u2014 where a thread is now; `order: "asc"` returns oldest-first \u2014 how it began. Your own current thread is already in context; use this to catch up on other conversations or to page further back.',
6545
- parameters: {
6546
- type: "object",
6547
- properties: {
6548
- conversationId: { type: "string", description: "The conversation id to read." },
6549
- order: {
6550
- type: "string",
6551
- enum: ["asc", "desc"],
6552
- description: "newest-first (desc, default) or oldest-first (asc)."
6553
- },
6554
- limit: {
6555
- type: "number",
6556
- description: "Max messages to return (default 50, max 100)."
6557
- }
6558
- },
6559
- required: ["conversationId"]
6560
- }
6561
- }
6562
- }
6563
- ];
6564
- var CABANE_NATIVE_CODE_TOOL = {
6565
- type: "function",
6566
- function: {
6567
- name: "sdk",
6568
- description: "Run a TypeScript program against the workspace. `code` is the program \u2014 the body of an async function, so top-level `await` works and `return <value>` produces the result. Write against the ambient `cabane` object (no imports); only what you `return` plus `console.log` comes back, so gather/filter/summarize inside the program and return the small answer. The full `cabane` SDK surface \u2014 every method and its options \u2014 is documented in your system prompt above.",
6569
- parameters: {
6570
- type: "object",
6571
- properties: {
6572
- code: {
6573
- type: "string",
6574
- description: "The TypeScript program to run (an async function body)."
6575
- }
6576
- },
6577
- required: ["code"]
6578
- }
6579
- }
6580
- };
6581
- var CABANE_NATIVE_RENDER_TOKEN_TOOL = {
6582
- type: "function",
6583
- function: {
6584
- name: "mint_render_token",
6585
- description: "Mint a short-lived, scoped credential that lets a browser load a workspace HTML page at the content-isolation origin. Returns `{ url, cookieName, cookieValue, expiresAt, pathPrefix }`. `pathPrefix` (default `/`) narrows the grant to a folder or single file; `ttlSeconds` (default 30 min, max 86400) overrides the TTL.",
6586
- parameters: {
6587
- type: "object",
6588
- properties: {
6589
- pathPrefix: {
6590
- type: "string",
6591
- description: "Workspace-rooted path-prefix the token authorizes; `/` for whole-workspace."
6592
- },
6593
- ttlSeconds: {
6594
- type: "number",
6595
- description: "Override the default 30-minute TTL; capped at 24h (86400s)."
6596
- }
6597
- }
6598
- }
6599
- }
6600
- };
6601
- function summarizeCabaneToolArgs(name, args) {
6602
- if (name === "mint_render_token") return str2(args.pathPrefix) ?? "/";
6603
- if (name === "sdk") {
6604
- const code = str2(args.code) ?? "";
6605
- const firstLine = code.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
6606
- return firstLine.length > 80 ? `${firstLine.slice(0, 80)}\u2026` : firstLine;
6607
- }
6608
- if (name === "cabane_search") return str2(args.q) ?? "";
6609
- if (name === "cabane_move") {
6610
- const from = str2(args.fromPath) ?? "";
6611
- const to = str2(args.toPath) ?? "";
6612
- return from && to ? `${from} \u2192 ${to}` : from || to;
6613
- }
6614
- if (name === "cabane_context") return str2(args.ref) ?? "";
6615
- if (name === "cabane_messages") return str2(args.conversationId) ?? "";
6616
- return str2(args.path) ?? "";
6617
- }
6618
- async function executeCabaneNativeTool(name, args, ctx, signal) {
6619
- const doFetch = ctx.fetchImpl ?? fetch;
6620
- const wsBase = `${ctx.apiRoot}/workspaces/${ctx.workspaceId}`;
6621
- const headers = { Authorization: `Bearer ${ctx.bearer}`, "Content-Type": "application/json" };
6622
- const call = async (method, path3, init2) => {
6623
- const url = new URL(`${wsBase}${path3}`);
6624
- for (const [k, v] of Object.entries(init2?.query ?? {})) {
6625
- if (v !== void 0) url.searchParams.set(k, v);
6626
- }
6627
- let res;
6628
- try {
6629
- res = await doFetch(url.toString(), {
6630
- method,
6631
- headers,
6632
- ...init2?.body !== void 0 ? { body: JSON.stringify(init2.body) } : {},
6633
- signal
6634
- });
6635
- } catch (err2) {
6636
- return {
6637
- ok: false,
6638
- result: `error: request failed: ${err2 instanceof Error ? err2.message : String(err2)}`
6639
- };
6640
- }
6641
- const contentType = res.headers.get("content-type") ?? "";
6642
- const text = contentType.includes("application/json") ? JSON.stringify(await res.json().catch(() => ({}))) : await res.text().catch(() => "");
6643
- if (!res.ok) return { ok: false, result: truncate2(`error ${res.status}: ${text}`) };
6644
- return { ok: true, result: truncate2(text) };
6645
- };
6646
- switch (name) {
6647
- case "sdk":
6648
- return call("POST", "/sdk", {
6649
- body: {
6650
- program: str2(args.code) ?? "",
6651
- ...ctx.conversationId ? { activeConversationId: ctx.conversationId } : {}
6652
- }
6653
- });
6654
- case "mint_render_token":
6655
- return call("POST", "/render-tokens", {
6656
- body: {
6657
- pathPrefix: str2(args.pathPrefix) ?? "/",
6658
- ...typeof args.ttlSeconds === "number" ? { ttlSeconds: args.ttlSeconds } : {}
6659
- }
6660
- });
6661
- case "cabane_list":
6662
- return call("GET", "/files/tree", { query: { path: str2(args.path) ?? "/" } });
6663
- case "cabane_read":
6664
- return call("GET", "/files/content", { query: { path: str2(args.path) } });
6665
- case "cabane_search":
6666
- return call("GET", "/search", { query: { q: str2(args.q), path: str2(args.path) } });
6667
- case "cabane_write": {
6668
- const overwrite = args.overwrite === true;
6669
- return overwrite ? call("PUT", "/files/content", {
6670
- body: { path: str2(args.path), content: str2(args.content) }
6671
- }) : call("POST", "/files", {
6672
- body: { path: str2(args.path), content: str2(args.content), mkdirs: true }
6673
- });
6674
- }
6675
- case "cabane_edit":
6676
- return call("PATCH", "/files", {
6677
- body: {
6678
- path: str2(args.path),
6679
- find: str2(args.find),
6680
- replace: str2(args.replace) ?? "",
6681
- ...args.replaceAll === true ? { replaceAll: true } : {}
6682
- }
6683
- });
6684
- case "cabane_mkdir":
6685
- return call("POST", "/folders", {
6686
- body: {
6687
- path: str2(args.path),
6688
- ...args.recursive === true ? { recursive: true } : {}
6689
- }
6690
- });
6691
- case "cabane_move":
6692
- return call("POST", "/entries/move", {
6693
- body: { fromPath: str2(args.fromPath), toPath: str2(args.toPath) }
6694
- });
6695
- case "cabane_delete":
6696
- return call("DELETE", "/entries", {
6697
- query: {
6698
- path: str2(args.path),
6699
- recursive: args.recursive === true ? "true" : void 0
6700
- }
6701
- });
6702
- case "cabane_context":
6703
- return call("POST", "/graph/query", {
6704
- body: { preset: "orient", args: { kind: str2(args.kind), ref: str2(args.ref) } }
6705
- });
6706
- case "cabane_messages":
6707
- return call(
6708
- "GET",
6709
- `/conversations/${encodeURIComponent(str2(args.conversationId) ?? "")}/messages`,
6710
- {
6711
- query: {
6712
- order: str2(args.order),
6713
- limit: typeof args.limit === "number" ? String(args.limit) : void 0
6714
- }
6715
- }
6716
- );
6717
- default:
6718
- return { ok: false, result: `error: unknown tool "${name}"` };
6719
- }
6720
- }
6721
- function str2(v) {
6722
- return typeof v === "string" ? v : void 0;
6723
- }
6724
- function truncate2(s) {
6725
- return s.length > TOOL_RESULT_MAX_CHARS ? `${s.slice(0, TOOL_RESULT_MAX_CHARS)}
6726
- \u2026 [truncated]` : s;
6727
- }
6728
-
6729
- // packages/agent-runtime/src/cabane-native/turn-control.ts
6730
- function describeSubAgentError(status2, body) {
6731
- const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
6732
- switch (code) {
6733
- case "callout_cap_exceeded":
6734
- return "sub_agent: you already have the maximum open sub-agents for this thread. Wait for some to return \u2014 you're woken once they're all back \u2014 before spawning more.";
6735
- case "callout_depth_exceeded":
6736
- return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
6737
- case "dispatch_agent_not_found":
6738
- return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
6739
- case "dispatch_return_requires_turn":
6740
- case "dispatch_return_requires_agent":
6741
- case "dispatch_return_requires_dispatch":
6742
- return `sub_agent: the spawn was rejected (${code}). This is a turn-context problem, not something to retry blindly \u2014 report it rather than looping.`;
6743
- default:
6744
- return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
6745
- }
6746
- }
6747
- var CABANE_NATIVE_TURN_CONTROL_TOOLS = [
6748
- {
6749
- type: "function",
6750
- function: {
6751
- name: "ask",
6752
- description: "Ask a HUMAN a structured question you need answered to continue, then END your turn (do not wait \u2014 a reply may take days). Pass `targetUserId` (a workspace member id). Single form: a one-sentence `headline` (the question, capitalized, ending in `?`) + a short `question` body of framing, with 2\u20134 `options` when the answer is a bounded/yes-no choice. Or `questions`: 1\u20135 items each `{ headline, body?, options? }` when several decisions land at once. Provide EITHER `question` or `questions`, never both. Your surrounding context goes in your reply; the ask carries the question.",
6753
- parameters: {
6754
- type: "object",
6755
- properties: {
6756
- targetUserId: {
6757
- type: "string",
6758
- description: "The workspace member (human) to ask \u2014 a user id from the roster."
6759
- },
6760
- question: {
6761
- type: "string",
6762
- description: "Single-question form: a short body of framing (one or two sentences)."
6763
- },
6764
- headline: {
6765
- type: "string",
6766
- description: "Single-question form: the question itself as one clear capitalized sentence ending in `?`."
6767
- },
6768
- options: {
6769
- type: "array",
6770
- items: { type: "string" },
6771
- description: "Single-question form: 2\u20134 suggested one-click answers."
6772
- },
6773
- questions: {
6774
- type: "array",
6775
- items: {
6776
- type: "object",
6777
- properties: {
6778
- headline: { type: "string" },
6779
- body: { type: "string" },
6780
- options: { type: "array", items: { type: "string" } }
6781
- },
6782
- required: ["headline"]
6783
- },
6784
- description: "Multi-question form: 1\u20135 questions to ask at once."
6785
- }
6786
- },
6787
- required: ["targetUserId"]
6788
- }
6789
- }
6790
- },
6791
- {
6792
- type: "function",
6793
- function: {
6794
- name: "wake_me",
6795
- description: 'Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, to CHECK a condition that has not happened yet (a PR merging, a reply landing, "check back in five minutes"). Pass EXACTLY ONE of `afterSeconds` (a relative delay, \u226560) or `at` (an absolute ISO-8601 timestamp WITH a zone you compute yourself). `note` is the message to your future self \u2014 write the condition to re-check. It arms when the turn ends; one wake per turn.',
6796
- parameters: {
6797
- type: "object",
6798
- properties: {
6799
- afterSeconds: {
6800
- type: "number",
6801
- description: "Relative delay in seconds from when this turn ends (floor 60)."
6802
- },
6803
- at: {
6804
- type: "string",
6805
- description: "Absolute ISO-8601 timestamp with a zone (e.g. 2026-07-16T09:00:00-07:00)."
6806
- },
6807
- note: {
6808
- type: "string",
6809
- description: "A note to your future self \u2014 becomes the wake body (the condition to re-check)."
6810
- }
6811
- },
6812
- required: ["note"]
6813
- }
6814
- }
6815
- },
6816
- {
6817
- type: "function",
6818
- function: {
6819
- name: "summon_agent",
6820
- description: "Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Pass the peer `agentId` (from the agent roster). The peer is dispatched on your final reply and picks up this conversation as context, so write the context/ask into your reply first. Writing `@handle` in prose summons no one \u2014 this tool is the only in-thread lever. Single target, last call wins; summoning yourself is a no-op.",
6821
- parameters: {
6822
- type: "object",
6823
- properties: {
6824
- agentId: {
6825
- type: "string",
6826
- description: "The peer agent to summon \u2014 a workspace agent id from the roster."
6827
- }
6828
- },
6829
- required: ["agentId"]
6830
- }
6831
- }
6832
- },
6833
- {
6834
- type: "function",
6835
- function: {
6836
- name: "sub_agent",
6837
- description: "Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window whose result comes back to you automatically. It does NOT return inline: this returns the child's id immediately and the outcome lands LATER as a message in this conversation (you're woken once every sub-agent you have out here has returned). So don't wait \u2014 finish what else this turn can do and end your turn. `prompt` is the child's self-contained opening instruction; `agentId` (optional) dispatches a peer instead of yourself; `title` (optional) names the child thread. Reach for it to isolate a big read or fan out N independent pieces in parallel.",
6838
- parameters: {
6839
- type: "object",
6840
- properties: {
6841
- prompt: {
6842
- type: "string",
6843
- description: "The sub-agent's self-contained opening instruction."
6844
- },
6845
- agentId: {
6846
- type: "string",
6847
- description: "Optional peer to run the sub-agent as; omit to spawn yourself."
6848
- },
6849
- title: { type: "string", description: "Optional title for the child thread." }
6850
- },
6851
- required: ["prompt"]
6852
- }
6853
- }
6854
- },
6855
- {
6856
- type: "function",
6857
- function: {
6858
- name: "skip_turn",
6859
- description: "End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 an aside, a question already answered, a pile-on someone else has. Your turn ends silently (no message bubble). `reason` is a short free-text note for telemetry. Prefer this over posting a low-value \"ok!\"; skipping IS the whole turn.",
6860
- parameters: {
6861
- type: "object",
6862
- properties: {
6863
- reason: {
6864
- type: "string",
6865
- description: "Short reason you are declining \u2014 used for telemetry."
6866
- }
6867
- },
6868
- required: ["reason"]
6869
- }
6870
- }
6871
- }
6872
- ];
6873
- var CABANE_NATIVE_TURN_CONTROL_NAMES = new Set(
6874
- CABANE_NATIVE_TURN_CONTROL_TOOLS.map((t) => t.function.name)
6875
- );
6876
- function summarizeTurnControlArgs(name, args) {
6877
- if (name === "ask") return str3(args.headline) ?? str3(args.question) ?? "";
6878
- if (name === "wake_me") return str3(args.note) ?? "";
6879
- if (name === "summon_agent") return str3(args.agentId) ?? "";
6880
- if (name === "sub_agent") return str3(args.title) ?? truncateHead(str3(args.prompt) ?? "", 60);
6881
- if (name === "skip_turn") return str3(args.reason) ?? "";
6882
- return "";
6883
- }
6884
- async function executeNativeTurnControl(name, args, tc) {
6885
- switch (name) {
6886
- case "summon_agent": {
6887
- const agentId = str3(args.agentId);
6888
- if (!agentId) return err("summon_agent: `agentId` is required.");
6889
- tc.summon(agentId);
6890
- return ok({ summoned: agentId });
6891
- }
6892
- case "skip_turn": {
6893
- const reason = str3(args.reason);
6894
- if (!reason) return err("skip_turn: `reason` is required.");
6895
- tc.skip(reason);
6896
- return ok({ skipped: true });
6897
- }
6898
- case "ask": {
6899
- const targetUserId = str3(args.targetUserId);
6900
- if (!targetUserId) return err("ask: `targetUserId` is required.");
6901
- const hasSingle = args.question !== void 0;
6902
- const hasArray = Array.isArray(args.questions) && args.questions.length > 0;
6903
- if (hasSingle && hasArray)
6904
- return err("ask: provide either `question` or `questions`, not both.");
6905
- if (!hasSingle && !hasArray) return err("ask: provide `question` or `questions`.");
6906
- const payload = hasArray ? { targetUserId, questions: args.questions } : {
6907
- targetUserId,
6908
- question: str3(args.question),
6909
- ...str3(args.headline) ? { headline: str3(args.headline) } : {},
6910
- ...Array.isArray(args.options) ? { options: args.options } : {}
6911
- };
6912
- tc.ask(payload);
6913
- return ok({ asked: targetUserId });
6914
- }
6915
- case "wake_me": {
6916
- const note = str3(args.note);
6917
- if (!note) return err("wake_me: `note` is required.");
6918
- const hasAfter = typeof args.afterSeconds === "number";
6919
- const hasAt = typeof args.at === "string";
6920
- if (hasAfter && hasAt)
6921
- return err("wake_me: provide either `afterSeconds` or `at`, not both.");
6922
- if (!hasAfter && !hasAt) return err("wake_me: provide `afterSeconds` or `at`.");
6923
- tc.wake({
6924
- ...hasAfter ? { afterSeconds: args.afterSeconds } : {},
6925
- ...hasAt ? { at: args.at } : {},
6926
- note
6927
- });
6928
- return ok({ armed: hasAt ? { at: args.at } : { afterSeconds: args.afterSeconds } });
6929
- }
6930
- case "sub_agent": {
6931
- const prompt = str3(args.prompt);
6932
- if (!prompt) return err("sub_agent: `prompt` is required.");
6933
- const result = await tc.subAgent({
6934
- prompt,
6935
- ...str3(args.agentId) ? { agentId: str3(args.agentId) } : {},
6936
- ...str3(args.title) ? { title: str3(args.title) } : {}
6937
- });
6938
- if (!result.ok) return err(result.error);
6939
- return ok({
6940
- conversationId: result.conversationId,
6941
- note: "Spawned. Don't wait \u2014 finish what else this turn can do, then end your turn; the result posts back here."
6942
- });
6943
- }
6944
- default:
6945
- return err(`unknown turn-control tool "${name}"`);
6946
- }
6947
- }
6948
- function ok(payload) {
6949
- return { ok: true, result: JSON.stringify(payload) };
6950
- }
6951
- function err(message) {
6952
- return { ok: false, result: `error: ${message}` };
6953
- }
6954
- function str3(v) {
6955
- return typeof v === "string" ? v : void 0;
6956
- }
6957
- function truncateHead(s, n) {
6958
- return s.length > n ? `${s.slice(0, n)}\u2026` : s;
6959
- }
6960
-
6961
- // packages/agent-runtime/src/cabane-native/loop.ts
6962
- var DEFAULT_MAX_ITERATIONS = 12;
6963
- async function* runCabaneNativeTurn(req, signal, deps) {
6964
- if (!req.config.model) {
6965
- yield { type: "result", ok: false, reason: "no_model" };
6966
- return;
6967
- }
6968
- const model = parseCabaneNativeModel(req.config.model);
6969
- const surface = cabaneNativeSurface(req.config.runtimeOptions);
6970
- const turnControl = req.extra.turnControl;
6971
- const tools = surface === "code" ? [
6972
- CABANE_NATIVE_CODE_TOOL,
6973
- CABANE_NATIVE_RENDER_TOKEN_TOOL,
6974
- ...turnControl ? CABANE_NATIVE_TURN_CONTROL_TOOLS : []
6975
- ] : CABANE_NATIVE_TOOLS;
6976
- const toolCtx = {
6977
- apiRoot: deps.apiRoot,
6978
- workspaceId: deps.workspaceId,
6979
- bearer: deps.bearer,
6980
- // CT666: the origin conversation, forwarded by the `sdk` tool to the `/sdk`
6981
- // endpoint so a code-mode program's returning callout binds to this turn.
6982
- ...deps.conversationId ? { conversationId: deps.conversationId } : {},
6983
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
6984
- };
6985
- const messages = await assembleMessages(req.systemPrompt, req.content, req.prompt, {
6986
- apiRoot: deps.apiRoot,
6987
- workspaceId: deps.workspaceId,
6988
- bearer: deps.bearer,
6989
- conversationId: deps.conversationId,
6990
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
6991
- ...deps.historyLimit !== void 0 ? { historyLimit: deps.historyLimit } : {}
6992
- });
6993
- if (signal.aborted) return;
6994
- const maxIterations = deps.maxIterations ?? DEFAULT_MAX_ITERATIONS;
6995
- let usage;
6996
- let resolvedModel = model;
6997
- for (let iteration = 0; iteration < maxIterations; iteration++) {
6998
- let text = "";
6999
- const toolAcc = /* @__PURE__ */ new Map();
7000
- let finishReason;
7001
- let errored2;
7002
- for await (const ev of deps.provider.stream({ model, messages, tools }, signal)) {
7003
- if (signal.aborted) return;
7004
- switch (ev.type) {
7005
- case "text":
7006
- text += ev.delta;
7007
- break;
7008
- case "tool_call": {
7009
- const cur = toolAcc.get(ev.index) ?? { id: `call_${ev.index}`, name: "", args: "" };
7010
- if (ev.id) cur.id = ev.id;
7011
- if (ev.name) cur.name = ev.name;
7012
- if (ev.argumentsDelta) cur.args += ev.argumentsDelta;
7013
- toolAcc.set(ev.index, cur);
7014
- break;
7015
- }
7016
- case "usage":
7017
- usage = {
7018
- inputTokens: ev.inputTokens,
7019
- outputTokens: ev.outputTokens,
7020
- contextTokens: ev.inputTokens
7021
- };
7022
- break;
7023
- case "model":
7024
- resolvedModel = ev.model;
7025
- break;
7026
- case "error":
7027
- errored2 = ev.message;
7028
- break;
7029
- case "done":
7030
- finishReason = ev.finishReason;
7031
- break;
7032
- }
7033
- }
7034
- if (signal.aborted) return;
7035
- if (errored2 !== void 0) {
7036
- const sealed = sealText(text, false);
7037
- if (sealed) yield sealed;
7038
- const failure = classifyErrorText(errored2);
7039
- yield {
7040
- type: "result",
7041
- ok: false,
7042
- reason: failure ? encodeFailureReason(failure) : `error:${errored2.slice(0, 200)}`,
7043
- ...usage ? { usage } : {},
7044
- ...resolvedModel ? { resolvedModel } : {}
7045
- };
7046
- return;
7047
- }
7048
- const toolCalls = [...toolAcc.entries()].sort((a, b) => a[0] - b[0]).map(([, v]) => v);
7049
- if (toolCalls.length === 0) {
7050
- const sealed = sealText(text, true);
7051
- if (sealed) yield sealed;
7052
- yield {
7053
- type: "result",
7054
- ok: true,
7055
- ...usage ? { usage } : {},
7056
- ...resolvedModel ? { resolvedModel } : {}
7057
- };
7058
- return;
7059
- }
7060
- const sealedInterim = sealText(text, false);
7061
- if (sealedInterim) yield sealedInterim;
7062
- const assistantToolCalls = toolCalls.map((t) => ({
7063
- id: t.id,
7064
- type: "function",
7065
- function: { name: t.name, arguments: t.args || "{}" }
7066
- }));
7067
- messages.push({ role: "assistant", content: text, tool_calls: assistantToolCalls });
7068
- for (const t of toolCalls) {
7069
- if (signal.aborted) return;
7070
- const args = parseArgs(t.args);
7071
- const displayName = prettyToolName(t.name);
7072
- const isTurnControl = CABANE_NATIVE_TURN_CONTROL_NAMES.has(t.name);
7073
- const summary = isTurnControl ? summarizeTurnControlArgs(t.name, args) : summarizeCabaneToolArgs(t.name, args);
7074
- yield { type: "tool", id: t.id, name: displayName, phase: "start", summary, input: args };
7075
- const result = isTurnControl && turnControl ? await executeNativeTurnControl(t.name, args, turnControl) : await executeCabaneNativeTool(t.name, args, toolCtx, signal);
7076
- if (signal.aborted) return;
7077
- yield {
7078
- type: "tool",
7079
- id: t.id,
7080
- name: displayName,
7081
- phase: result.ok ? "done" : "error",
7082
- summary,
7083
- input: args,
7084
- result: result.result
7085
- };
7086
- messages.push({ role: "tool", tool_call_id: t.id, content: result.result });
7087
- }
7088
- }
7089
- deps.onWarn?.("cabane-native: turn hit the tool-iteration cap; force-settling", {
7090
- maxIterations
7091
- });
7092
- yield {
7093
- type: "text",
7094
- body: `(Stopped after ${maxIterations} tool steps without a final answer.)`,
7095
- terminal: true
7096
- };
7097
- yield {
7098
- type: "result",
7099
- ok: true,
7100
- ...usage ? { usage } : {},
7101
- ...resolvedModel ? { resolvedModel } : {}
7102
- };
7103
- }
7104
- function sealText(text, terminal) {
7105
- const body = text.trim();
7106
- if (body.length === 0) return null;
7107
- return { type: "text", body, terminal };
7108
- }
7109
- function parseArgs(raw) {
7110
- if (!raw.trim()) return {};
7111
- try {
7112
- const parsed = JSON.parse(raw);
7113
- return parsed && typeof parsed === "object" ? parsed : {};
7114
- } catch {
7115
- return {};
7116
- }
7117
- }
7118
-
7119
- // packages/agent-runtime/src/cabane-native/provider.ts
7120
- var DEFAULT_OPENROUTER_BASE = "https://openrouter.ai/api/v1";
7121
- function createOpenRouterProvider(opts) {
7122
- const base = (opts.baseUrl ?? DEFAULT_OPENROUTER_BASE).replace(/\/$/, "");
7123
- const doFetch = opts.fetchImpl ?? fetch;
7124
- return {
7125
- async *stream(req, signal) {
7126
- let res;
7127
- try {
7128
- res = await doFetch(`${base}/chat/completions`, {
7129
- method: "POST",
7130
- headers: {
7131
- Authorization: `Bearer ${opts.apiKey}`,
7132
- "Content-Type": "application/json",
7133
- // OpenRouter attribution headers (optional, but polite + used for
7134
- // routing/analytics on their side).
7135
- "HTTP-Referer": "https://cabane.ai",
7136
- "X-Title": "Cabane"
7137
- },
7138
- body: JSON.stringify({
7139
- model: req.model,
7140
- messages: req.messages,
7141
- ...req.tools.length > 0 ? { tools: req.tools } : {},
7142
- stream: true,
7143
- // Ask OpenRouter to append a trailing usage chunk to the stream.
7144
- stream_options: { include_usage: true }
7145
- }),
7146
- signal
7147
- });
7148
- } catch (err2) {
7149
- if (signal.aborted) return;
7150
- yield { type: "error", message: `request failed: ${errText(err2)}` };
7151
- return;
7152
- }
7153
- if (!res.ok || !res.body) {
7154
- const bodyText = await res.text().catch(() => "");
7155
- yield { type: "error", message: providerErrorMessage(res.status, bodyText) };
7156
- return;
7157
- }
7158
- const decoder = new TextDecoder();
7159
- const reader = res.body.getReader();
7160
- let buffer = "";
7161
- let finishReason;
7162
- let modelEmitted = false;
7163
- try {
7164
- for (; ; ) {
7165
- if (signal.aborted) return;
7166
- const { value, done } = await reader.read();
7167
- if (done) break;
7168
- buffer += decoder.decode(value, { stream: true });
7169
- let nl;
7170
- while ((nl = buffer.indexOf("\n")) !== -1) {
7171
- const line = buffer.slice(0, nl).trim();
7172
- buffer = buffer.slice(nl + 1);
7173
- if (!line || line.startsWith(":")) continue;
7174
- if (!line.startsWith("data:")) continue;
7175
- const data = line.slice("data:".length).trim();
7176
- if (data === "[DONE]") {
7177
- yield { type: "done", ...finishReason ? { finishReason } : {} };
7178
- return;
7179
- }
7180
- let chunk;
7181
- try {
7182
- chunk = JSON.parse(data);
7183
- } catch {
7184
- continue;
7185
- }
7186
- if (chunk.error) {
7187
- yield { type: "error", message: chunk.error.message ?? "provider error" };
7188
- return;
7189
- }
7190
- if (!modelEmitted && chunk.model) {
7191
- modelEmitted = true;
7192
- yield { type: "model", model: chunk.model };
7193
- }
7194
- const choice = chunk.choices?.[0];
7195
- if (choice) {
7196
- const delta = choice.delta;
7197
- if (delta?.content) yield { type: "text", delta: delta.content };
7198
- if (delta?.tool_calls) {
7199
- for (const tc of delta.tool_calls) {
7200
- yield {
7201
- type: "tool_call",
7202
- index: tc.index,
7203
- ...tc.id ? { id: tc.id } : {},
7204
- ...tc.function?.name ? { name: tc.function.name } : {},
7205
- ...tc.function?.arguments !== void 0 ? { argumentsDelta: tc.function.arguments } : {}
7206
- };
7207
- }
7208
- }
7209
- if (choice.finish_reason) finishReason = choice.finish_reason;
7210
- }
7211
- if (chunk.usage) {
7212
- yield {
7213
- type: "usage",
7214
- inputTokens: chunk.usage.prompt_tokens ?? 0,
7215
- outputTokens: chunk.usage.completion_tokens ?? 0
7216
- };
7217
- }
7218
- }
7219
- }
7220
- } catch (err2) {
7221
- if (signal.aborted) return;
7222
- yield { type: "error", message: `stream read failed: ${errText(err2)}` };
7223
- return;
7224
- }
7225
- yield { type: "done", ...finishReason ? { finishReason } : {} };
7226
- }
7227
- };
7228
- }
7229
- function providerErrorMessage(status2, body) {
7230
- let detail = body.slice(0, 300);
7231
- try {
7232
- const parsed = JSON.parse(body);
7233
- if (parsed.error?.message) detail = parsed.error.message;
7234
- } catch {
7235
- }
7236
- return `HTTP ${status2}: ${detail}`;
7237
- }
7238
- function errText(err2) {
7239
- return err2 instanceof Error ? err2.message : String(err2);
7240
- }
7241
-
7242
- // packages/agent-runtime/src/cabane-native/index.ts
7243
- var CABANE_NATIVE_RUNTIME_NAME = "cabane-native";
7244
- function createCabaneNativeAdapter(deps = {}) {
7245
- const provider = deps.provider ?? (deps.apiKey ? createOpenRouterProvider({
7246
- apiKey: deps.apiKey,
7247
- ...deps.baseUrl ? { baseUrl: deps.baseUrl } : {},
7248
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
7249
- }) : null);
7250
- return {
7251
- name: CABANE_NATIVE_RUNTIME_NAME,
7252
- // CT666: the native runtime's addendum is now SURFACE-aware. Code mode (the
7253
- // default In-Cabane surface) mounts the single `sdk` tool, so it's taught the
7254
- // code-mode addendum (act by writing a `cabane` SDK program; the `cabane_*`
7255
- // names don't exist here); classic (the retained CT663 10-tool fallback) is
7256
- // taught the granular `cabane_*` listing. The caller (turn-context) passes
7257
- // `codeMode` off the SAME lifted surface value the loop mounts tools from, so
7258
- // the prompt and the mounted surface can't drift. (Supersedes CT614's
7259
- // flag-independent addendum: native no longer ignores this arg.)
7260
- promptAddendum: (codeMode) => codeMode ? CABANE_NATIVE_ADDENDUM_CODE_MODE : CABANE_NATIVE_ADDENDUM,
7261
- dialectSchema: cabaneNativeDialectSchema,
7262
- async *runTurn(req, signal) {
7263
- if (!provider) {
7264
- yield {
7265
- type: "result",
7266
- ok: false,
7267
- reason: "cabane_native_unavailable:no OPENROUTER_API_KEY configured on this device"
7268
- };
7269
- return;
7270
- }
7271
- const workspaceId = req.cabane.workspaceId;
7272
- if (!workspaceId) {
7273
- yield {
7274
- type: "result",
7275
- ok: false,
7276
- reason: "cabane_native_unavailable:turn carried no workspaceId"
7277
- };
7278
- return;
7279
- }
7280
- const apiRoot = req.cabane.mcpUrl.replace(/\/mcp\/?$/, "");
7281
- yield* runCabaneNativeTurn(req, signal, {
7282
- provider,
7283
- apiRoot,
7284
- workspaceId,
7285
- bearer: req.cabane.bearer,
7286
- conversationId: req.cabane.activeConversationId,
7287
- ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {},
7288
- ...deps.onWarn ? { onWarn: deps.onWarn } : {},
7289
- ...deps.maxIterations !== void 0 ? { maxIterations: deps.maxIterations } : {}
7290
- });
7291
- }
7292
- };
7293
- }
7294
- var cabaneNativeAdapter = createCabaneNativeAdapter();
7295
-
7296
6156
  // packages/agent-runtime/src/claude-code/sdk.ts
7297
6157
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
7298
6158
 
@@ -7350,7 +6210,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync
7350
6210
  import { join as join12 } from "path";
7351
6211
 
7352
6212
  // src/summon.ts
7353
- import { z as z14 } from "zod";
6213
+ import { z as z12 } from "zod";
7354
6214
  var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
7355
6215
  var SUMMON_AGENT_TOOL = "summon_agent";
7356
6216
  var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
@@ -7384,7 +6244,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7384
6244
  SUMMON_AGENT_TOOL,
7385
6245
  "Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId` (discover handles + ids via `list_agents`). The peer is dispatched on your turn's final reply, so write the context/ask into that reply first \u2014 it receives your message + this conversation to work from. Writing `@handle` in your prose does NOT summon anyone (agent prose never dispatches); this tool is the only in-thread lever. Single target \u2014 the last call wins. Summoning yourself is a no-op. Reach for it when the human wants the peer's answer right HERE, in front of them \u2014 the reply lands in this thread, so there's no return to wire (a return is for work YOU consume, never a courtesy notification). A handoff to a DIFFERENT conversation is `create_conversation` / `post_message` with their `dispatch` field instead.",
7386
6246
  {
7387
- agentId: z14.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
6247
+ agentId: z12.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
7388
6248
  },
7389
6249
  async (args) => {
7390
6250
  summonState.agentId = args.agentId;
@@ -7399,7 +6259,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7399
6259
  SKIP_TURN_TOOL,
7400
6260
  `End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
7401
6261
  {
7402
- reason: z14.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
6262
+ reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
7403
6263
  },
7404
6264
  async (args) => {
7405
6265
  skipState.skipped = true;
@@ -7416,21 +6276,21 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7416
6276
  ASK_TOOL,
7417
6277
  "Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact, a go/no-go). Pass `targetUserId` (a workspace member's user id \u2014 get it from `mcp__cabane__list_members`). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once (\"three calls before I build: A? B? C?\"), a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label OR a sentence that carries its own context; short/binary sets render as inline buttons, long ones stack full-width. Provide EITHER `question` (single) or `questions` (array), never both. The ask is recorded as a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
7418
6278
  {
7419
- targetUserId: z14.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
7420
- question: z14.string().min(1).max(400).optional().describe(
6279
+ targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
6280
+ question: z12.string().min(1).max(400).optional().describe(
7421
6281
  "SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
7422
6282
  ),
7423
- headline: z14.string().min(1).max(120).optional().describe(
6283
+ headline: z12.string().min(1).max(120).optional().describe(
7424
6284
  'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
7425
6285
  ),
7426
- options: z14.array(z14.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
7427
- questions: z14.array(
7428
- z14.object({
7429
- headline: z14.string().min(1).max(120).describe(
6286
+ options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
6287
+ questions: z12.array(
6288
+ z12.object({
6289
+ headline: z12.string().min(1).max(120).describe(
7430
6290
  'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
7431
6291
  ),
7432
- body: z14.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
7433
- options: z14.array(z14.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
6292
+ body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
6293
+ options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
7434
6294
  })
7435
6295
  ).min(1).max(5).optional().describe(
7436
6296
  "MULTI-question form: 1\u20135 questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or `question`/`headline`/`options`, not both."
@@ -7487,13 +6347,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7487
6347
  SUB_AGENT_TOOL,
7488
6348
  "Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window, whose result comes back to you automatically. Modeled on the Task tool, with ONE deliberate difference: it does NOT return the result inline. A callee's turn can run for minutes and no turn may hold an unbounded wait, so the shape is spawn-now, results-on-wake \u2014 this returns immediately with the child's `conversationId`, and the outcome lands LATER as a message in THIS conversation; you're woken once every sub-agent you have out in this conversation has returned. So DON'T wait for it: after spawning, finish whatever else this turn can do and end your turn (never poll the child with reads in a loop \u2014 the wake is automatic). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake when all are in); series = one call per turn. `prompt` is the child's opening instruction \u2014 make it self-contained (the sub-agent starts fresh, with only this prompt + the thread it lands in). `agentId` (optional) dispatches a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is the pure sub-worker with a clean context window. `title` (optional) names the child thread (results link it, so a legible title helps). A single sub-agent has no wall-clock advantage (you idle either way) \u2014 it pays when the callee has capability/context you lack, or to isolate a big read from your own session; the real win is fan-out. Don't spawn one for a lookup you can do in-turn with your own tools. The result returns to YOU to act on \u2014 reach for it when you're the consumer of the output, not as a way to notify a human: if a person just wants to read the result, dispatch a plain (no-return) conversation and link it instead of spawning a sub-agent.",
7489
6349
  {
7490
- prompt: z14.string().min(1).max(65536).describe(
6350
+ prompt: z12.string().min(1).max(65536).describe(
7491
6351
  "The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
7492
6352
  ),
7493
- agentId: z14.string().uuid().optional().describe(
6353
+ agentId: z12.string().uuid().optional().describe(
7494
6354
  "Optional peer to run the sub-agent as (a workspace agent id from `list_agents`); omit to spawn yourself with a fresh context window."
7495
6355
  ),
7496
- title: z14.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6356
+ title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
7497
6357
  },
7498
6358
  async (args) => {
7499
6359
  const result = await subAgentCreate(args);
@@ -7523,13 +6383,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7523
6383
  WAKE_ME_TOOL,
7524
6384
  'Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn\'t happened yet (a conversation isn\'t done, a PR isn\'t merged, a human hasn\'t answered), arm a wake, end your turn, and you\'re woken later to CHECK \u2014 read the workspace, and either act or re-arm. This is the loop behind "check back in five minutes", "keep checking until {condition}", and a scheduled re-try ("re-send once that agent\'s limit resets"). Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging, a deploy landing, a throttled dispatch to re-send. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a scheduled appointment, a known reset time. A speculative far-future check-in you invented yourself \u2014 "in two weeks I\'ll see whether this feature is used" \u2014 is the one thing not to arm: if no one asked and you can\'t name both what clears the wait and why it takes that long, don\'t arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like "tomorrow morning"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you\'ll be steered to raise an `ask` to the human instead. If a wake can\'t be armed you\'re re-dispatched with a note explaining why \u2014 never a silent drop.',
7525
6385
  {
7526
- afterSeconds: z14.number().int().positive().optional().describe(
6386
+ afterSeconds: z12.number().int().positive().optional().describe(
7527
6387
  "Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
7528
6388
  ),
7529
- at: z14.string().datetime({ offset: true }).optional().describe(
6389
+ at: z12.string().datetime({ offset: true }).optional().describe(
7530
6390
  "Absolute ISO-8601 timestamp WITH a zone (`Z` or `\xB1HH:MM`), e.g. `2026-07-16T09:00:00-07:00`. YOU compute it from a natural-language phrase using the current datetime in your turn context. Provide EITHER this or `afterSeconds`, not both."
7531
6391
  ),
7532
- note: z14.string().min(1).max(2e3).describe(
6392
+ note: z12.string().min(1).max(2e3).describe(
7533
6393
  'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
7534
6394
  )
7535
6395
  },
@@ -7609,7 +6469,6 @@ function buildCompanionTurnRequest(params) {
7609
6469
  local: {
7610
6470
  ...params.cwd ? { cwd: params.cwd } : {},
7611
6471
  ...params.env ? { env: params.env } : {},
7612
- ...params.nativeWorkAssignment ? { nativeWorkAssignment: params.nativeWorkAssignment } : {},
7613
6472
  // User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
7614
6473
  // adapter's `ResolvedMcpServers`.
7615
6474
  ...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
@@ -7617,10 +6476,9 @@ function buildCompanionTurnRequest(params) {
7617
6476
  ...params.claudeCode ? { claudeCode: params.claudeCode } : {}
7618
6477
  },
7619
6478
  // Host-injected: the companion-local summon server (for the subprocess adapters,
7620
- // under its own namespace) + the cabane-native turn-control handler (CT666).
6479
+ // under its own namespace).
7621
6480
  extra: {
7622
- mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer },
7623
- turnControl: params.turnControl
6481
+ mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
7624
6482
  }
7625
6483
  };
7626
6484
  }
@@ -7629,7 +6487,7 @@ function trimSlash3(s) {
7629
6487
  }
7630
6488
 
7631
6489
  // src/prepared.ts
7632
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6490
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
7633
6491
  import { join as join9 } from "path";
7634
6492
  function dirFor(workspaceId) {
7635
6493
  return join9(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
@@ -7637,23 +6495,18 @@ function dirFor(workspaceId) {
7637
6495
  function conversationDir(workspaceId, conversationId) {
7638
6496
  return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
7639
6497
  }
7640
- function pathFor3(workspaceId, conversationId, agentId, assignmentKey) {
7641
- const suffix = assignmentKey ? `--${encodeURIComponent(assignmentKey)}` : "";
7642
- return join9(
7643
- conversationDir(workspaceId, conversationId),
7644
- `${encodeURIComponent(agentId)}${suffix}.json`
7645
- );
6498
+ function pathFor3(workspaceId, conversationId, agentId) {
6499
+ return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
7646
6500
  }
7647
- function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
7648
- const path3 = pathFor3(workspaceId, conversationId, agentId, assignmentKey);
6501
+ function readPrepared(workspaceId, conversationId, agentId) {
6502
+ const path3 = pathFor3(workspaceId, conversationId, agentId);
7649
6503
  if (!existsSync7(path3)) return null;
7650
6504
  try {
7651
- const parsed = JSON.parse(readFileSync7(path3, "utf8"));
6505
+ const parsed = JSON.parse(readFileSync6(path3, "utf8"));
7652
6506
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
7653
6507
  return {
7654
6508
  cwd: parsed.cwd,
7655
- ...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {},
7656
- ...parsed.nativeWorkAssignment && typeof parsed.nativeWorkAssignment === "object" ? { nativeWorkAssignment: parsed.nativeWorkAssignment } : {}
6509
+ ...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
7657
6510
  };
7658
6511
  }
7659
6512
  return null;
@@ -7661,42 +6514,42 @@ function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
7661
6514
  return null;
7662
6515
  }
7663
6516
  }
7664
- function writePrepared(workspaceId, conversationId, agentId, result, assignmentKey) {
6517
+ function writePrepared(workspaceId, conversationId, agentId, result) {
7665
6518
  mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
7666
6519
  writeFileSync6(
7667
- pathFor3(workspaceId, conversationId, agentId, assignmentKey),
6520
+ pathFor3(workspaceId, conversationId, agentId),
7668
6521
  JSON.stringify(result) + "\n",
7669
6522
  "utf8"
7670
6523
  );
7671
6524
  }
7672
6525
 
7673
6526
  // src/secrets.ts
7674
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
6527
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
7675
6528
  import { join as join10 } from "path";
7676
- import { z as z15 } from "zod";
6529
+ import { z as z13 } from "zod";
7677
6530
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
7678
6531
  function secretsPath() {
7679
6532
  return join10(cabaneDir(), "secrets.json");
7680
6533
  }
7681
- var secretStoreSchema = z15.record(z15.string(), z15.string());
6534
+ var secretStoreSchema = z13.record(z13.string(), z13.string());
7682
6535
  function loadSecretStore() {
7683
6536
  const path3 = secretsPath();
7684
6537
  if (!existsSync8(path3)) return makeStore({});
7685
6538
  let raw;
7686
6539
  try {
7687
- raw = readFileSync8(path3, "utf8");
7688
- } catch (err2) {
6540
+ raw = readFileSync7(path3, "utf8");
6541
+ } catch (err) {
7689
6542
  throw new ConfigError(
7690
- `couldn't read ${path3}: ${err2 instanceof Error ? err2.message : String(err2)}`
6543
+ `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
7691
6544
  );
7692
6545
  }
7693
6546
  if (raw.trim().length === 0) return makeStore({});
7694
6547
  let parsed;
7695
6548
  try {
7696
6549
  parsed = JSON.parse(raw);
7697
- } catch (err2) {
6550
+ } catch (err) {
7698
6551
  throw new ConfigError(
7699
- `${path3} is not valid JSON: ${err2 instanceof Error ? err2.message : String(err2)}. It must be a flat object of "NAME": "value" secret pairs.`
6552
+ `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
7700
6553
  );
7701
6554
  }
7702
6555
  const result = secretStoreSchema.safeParse(parsed);
@@ -7710,8 +6563,8 @@ function loadSecretStore() {
7710
6563
  function loadSecretStoreTolerant(onWarn) {
7711
6564
  try {
7712
6565
  return loadSecretStore();
7713
- } catch (err2) {
7714
- onWarn?.(err2 instanceof Error ? err2.message : String(err2));
6566
+ } catch (err) {
6567
+ onWarn?.(err instanceof Error ? err.message : String(err));
7715
6568
  return makeStore({});
7716
6569
  }
7717
6570
  }
@@ -7782,8 +6635,8 @@ var TranscriptWriter = class {
7782
6635
  } catch {
7783
6636
  }
7784
6637
  pruneOld(dir2, RETAIN);
7785
- } catch (err2) {
7786
- this.fail(err2);
6638
+ } catch (err) {
6639
+ this.fail(err);
7787
6640
  }
7788
6641
  this.line({ type: "_meta", ...meta });
7789
6642
  }
@@ -7799,14 +6652,14 @@ var TranscriptWriter = class {
7799
6652
  if (this.broken) return;
7800
6653
  try {
7801
6654
  appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
7802
- } catch (err2) {
7803
- this.fail(err2);
6655
+ } catch (err) {
6656
+ this.fail(err);
7804
6657
  }
7805
6658
  }
7806
- fail(err2) {
6659
+ fail(err) {
7807
6660
  if (this.broken) return;
7808
6661
  this.broken = true;
7809
- this.onWarn?.(`transcript write failed (${err2 instanceof Error ? err2.message : String(err2)})`);
6662
+ this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
7810
6663
  }
7811
6664
  };
7812
6665
  function fileName(meta) {
@@ -7844,9 +6697,9 @@ var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
7844
6697
  var TurnCommitter = class {
7845
6698
  constructor(deps) {
7846
6699
  this.deps = deps;
7847
- this.onError = (err2, hook) => {
6700
+ this.onError = (err, hook) => {
7848
6701
  deps.log.warn(
7849
- { err: err2 instanceof Error ? err2.message : String(err2), hook },
6702
+ { err: err instanceof Error ? err.message : String(err), hook },
7850
6703
  "dispatcher: transcript callback failed"
7851
6704
  );
7852
6705
  };
@@ -7911,9 +6764,9 @@ var TurnCommitter = class {
7911
6764
  signal: deps.signal,
7912
6765
  nextSeq: deps.nextSeq,
7913
6766
  emptyFinalBody: EMPTY_FINAL_BODY,
7914
- onError: (err2) => {
6767
+ onError: (err) => {
7915
6768
  deps.log.warn(
7916
- { err: err2 instanceof Error ? err2.message : String(err2) },
6769
+ { err: err instanceof Error ? err.message : String(err) },
7917
6770
  "dispatcher: empty-final commit failed"
7918
6771
  );
7919
6772
  }
@@ -7936,8 +6789,8 @@ var TurnCommitter = class {
7936
6789
  if (event.type === "session" || event.type === "result") return;
7937
6790
  try {
7938
6791
  await this.emit(event);
7939
- } catch (err2) {
7940
- this.onError(err2, event.type);
6792
+ } catch (err) {
6793
+ this.onError(err, event.type);
7941
6794
  }
7942
6795
  }
7943
6796
  // End-of-turn empty-final promotion. The held-text flush is now the adapter's
@@ -8133,10 +6986,27 @@ var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't disp
8133
6986
  var SKIPPED_MARKER_BODY = "(skipped)";
8134
6987
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
8135
6988
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
8136
- var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 45 * 6e4;
6989
+ var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
8137
6990
  function runKey(conversationId, agentId) {
8138
6991
  return `${conversationId}|${agentId}`;
8139
6992
  }
6993
+ function describeSubAgentError(status2, body) {
6994
+ const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
6995
+ switch (code) {
6996
+ case "callout_cap_exceeded":
6997
+ return "sub_agent: you already have the maximum open sub-agents for this thread. Wait for some to return \u2014 you're woken once they're all back \u2014 before spawning more.";
6998
+ case "callout_depth_exceeded":
6999
+ return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
7000
+ case "dispatch_agent_not_found":
7001
+ return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
7002
+ case "dispatch_return_requires_turn":
7003
+ case "dispatch_return_requires_agent":
7004
+ case "dispatch_return_requires_dispatch":
7005
+ return `sub_agent: the spawn was rejected (${code}). This is a turn-context problem, not something to retry blindly \u2014 report it rather than looping.`;
7006
+ default:
7007
+ return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
7008
+ }
7009
+ }
8140
7010
  var Dispatcher = class {
8141
7011
  constructor(opts) {
8142
7012
  this.opts = opts;
@@ -8163,7 +7033,7 @@ var Dispatcher = class {
8163
7033
  // clear it — the SJ383 `finally` after the SDK loop is the one clear, and
8164
7034
  // every pre-run exit returns before reaching it. So each pre-run failure has
8165
7035
  // to clear `active_run_started_at` itself, mirroring that `finally`, or the
8166
- // indicator strands until the 90-min age sweep.
7036
+ // indicator strands until the 12h age sweep.
8167
7037
  //
8168
7038
  // `errorReason` controls the server's duplicate-notice rule (the active-run
8169
7039
  // PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
@@ -8183,9 +7053,9 @@ var Dispatcher = class {
8183
7053
  payload.agentId,
8184
7054
  body
8185
7055
  );
8186
- } catch (err2) {
7056
+ } catch (err) {
8187
7057
  turnLog.warn(
8188
- { err: err2 instanceof Error ? err2.message : String(err2) },
7058
+ { err: err instanceof Error ? err.message : String(err) },
8189
7059
  "dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
8190
7060
  );
8191
7061
  }
@@ -8211,16 +7081,16 @@ var Dispatcher = class {
8211
7081
  payload.messageId,
8212
7082
  turnId
8213
7083
  );
8214
- } catch (err2) {
8215
- const status2 = err2 instanceof ApiError ? err2.status : 0;
7084
+ } catch (err) {
7085
+ const status2 = err instanceof ApiError ? err.status : 0;
8216
7086
  if (status2 === 404) {
8217
7087
  turnLog.warn(
8218
- { err: err2 instanceof Error ? err2.message : String(err2) },
7088
+ { err: err instanceof Error ? err.message : String(err) },
8219
7089
  "dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
8220
7090
  );
8221
7091
  return this.concludeBeforeRun(payload, turnLog, startedAt, "turn_context_not_found");
8222
7092
  }
8223
- const reason = err2 instanceof Error ? err2.message : String(err2);
7093
+ const reason = err instanceof Error ? err.message : String(err);
8224
7094
  turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
8225
7095
  const fetchReason = `fetch_failed: ${reason}`;
8226
7096
  return this.concludeBeforeRun(payload, turnLog, startedAt, fetchReason, fetchReason);
@@ -8283,20 +7153,11 @@ var Dispatcher = class {
8283
7153
  effectiveCwd = void 0;
8284
7154
  }
8285
7155
  let hookEnv;
8286
- let preparedNativeAssignment;
8287
7156
  if (prepareHook) {
8288
- const assignment = turnContext.conversation.nativeWorkAssignment;
8289
- const assignmentKey = assignment ? `${assignment.executionId}:${assignment.activationEpoch}` : void 0;
8290
- const cached2 = readPrepared(
8291
- workspaceId,
8292
- payload.conversationId,
8293
- payload.agentId,
8294
- assignmentKey
8295
- );
7157
+ const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
8296
7158
  if (cached2) {
8297
7159
  effectiveCwd = cached2.cwd;
8298
7160
  hookEnv = cached2.env;
8299
- preparedNativeAssignment = cached2.nativeWorkAssignment;
8300
7161
  } else {
8301
7162
  const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
8302
7163
  let preparingStarted = false;
@@ -8311,9 +7172,9 @@ var Dispatcher = class {
8311
7172
  summary: "",
8312
7173
  phase,
8313
7174
  seq
8314
- }).catch((err2) => {
7175
+ }).catch((err) => {
8315
7176
  turnLog.warn(
8316
- { err: err2 instanceof Error ? err2.message : String(err2), phase },
7177
+ { err: err instanceof Error ? err.message : String(err), phase },
8317
7178
  "dispatcher: preparing-activity report failed (continuing with the hook)"
8318
7179
  );
8319
7180
  });
@@ -8335,25 +7196,17 @@ var Dispatcher = class {
8335
7196
  // tasker prepare hook keys its per-task env off. Defaults to `[]` for
8336
7197
  // an older API. The conversation anchor is gone (CT319).
8337
7198
  triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
8338
- ...turnContext.conversation.nativeWorkAssignment ? { nativeWorkAssignment: turnContext.conversation.nativeWorkAssignment } : {},
8339
7199
  title: turnContext.conversation.title
8340
7200
  });
8341
7201
  clearTimeout(preparingTimer);
8342
7202
  if (preparingStarted) reportPreparing("done");
8343
- writePrepared(
8344
- workspaceId,
8345
- payload.conversationId,
8346
- payload.agentId,
8347
- result,
8348
- assignmentKey
8349
- );
7203
+ writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
8350
7204
  effectiveCwd = result.cwd;
8351
7205
  hookEnv = result.env;
8352
- preparedNativeAssignment = result.nativeWorkAssignment;
8353
- } catch (err2) {
7206
+ } catch (err) {
8354
7207
  clearTimeout(preparingTimer);
8355
7208
  if (preparingStarted) reportPreparing("error");
8356
- const reason = err2 instanceof Error ? err2.message : String(err2);
7209
+ const reason = err instanceof Error ? err.message : String(err);
8357
7210
  turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
8358
7211
  try {
8359
7212
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
@@ -8376,6 +7229,19 @@ ${reason}`,
8376
7229
  }
8377
7230
  }
8378
7231
  }
7232
+ let turnEnv = hookEnv;
7233
+ if (effectiveCwd && turnContext.runtime === "codex") {
7234
+ const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
7235
+ try {
7236
+ mkdirSync10(tmpDir, { recursive: true });
7237
+ turnEnv = { ...hookEnv, TMPDIR: tmpDir };
7238
+ } catch (err) {
7239
+ turnLog.warn(
7240
+ { err: err instanceof Error ? err.message : String(err), tmpDir },
7241
+ "dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
7242
+ );
7243
+ }
7244
+ }
8379
7245
  const key = runKey(payload.conversationId, payload.agentId);
8380
7246
  const abortController = new AbortController();
8381
7247
  this.aborts.set(key, abortController);
@@ -8390,9 +7256,9 @@ ${reason}`,
8390
7256
  // this live turn for an abandoned one and close it with a `stopped`.
8391
7257
  turnId
8392
7258
  });
8393
- } catch (err2) {
7259
+ } catch (err) {
8394
7260
  turnLog.warn(
8395
- { err: err2 instanceof Error ? err2.message : String(err2) },
7261
+ { err: err instanceof Error ? err.message : String(err) },
8396
7262
  "dispatcher: active-run flag set failed terminally; proceeding"
8397
7263
  );
8398
7264
  }
@@ -8429,35 +7295,6 @@ ${reason}`,
8429
7295
  subAgentCreate,
8430
7296
  wakeState
8431
7297
  );
8432
- const nativeTurnControl = {
8433
- summon: (agentId) => {
8434
- summonState.agentId = agentId;
8435
- },
8436
- skip: (reason) => {
8437
- skipState.skipped = true;
8438
- skipState.reason = reason;
8439
- },
8440
- ask: (p) => {
8441
- askState.targetUserId = p.targetUserId;
8442
- if (p.questions && p.questions.length > 0) {
8443
- askState.questions = p.questions;
8444
- askState.question = null;
8445
- askState.headline = null;
8446
- askState.options = null;
8447
- } else {
8448
- askState.question = p.question ?? null;
8449
- askState.headline = p.headline ?? null;
8450
- askState.options = p.options ?? null;
8451
- askState.questions = null;
8452
- }
8453
- },
8454
- wake: (p) => {
8455
- wakeState.afterSeconds = p.afterSeconds ?? null;
8456
- wakeState.at = p.at ?? null;
8457
- wakeState.note = p.note;
8458
- },
8459
- subAgent: subAgentCreate
8460
- };
8461
7298
  const request = buildCompanionTurnRequest({
8462
7299
  turnContext,
8463
7300
  baseUrl: this.opts.baseUrl,
@@ -8467,11 +7304,11 @@ ${reason}`,
8467
7304
  ...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
8468
7305
  // SJ524: the hook-resolved cwd overrides the static local cwd.
8469
7306
  ...effectiveCwd ? { cwd: effectiveCwd } : {},
8470
- ...hookEnv ? { env: hookEnv } : {},
8471
- ...preparedNativeAssignment ? { nativeWorkAssignment: preparedNativeAssignment } : {},
7307
+ // CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
7308
+ // TMPDIR (falls back to `hookEnv` when no cwd was resolved).
7309
+ ...turnEnv ? { env: turnEnv } : {},
8472
7310
  mcpServers: resolvedMcpServers,
8473
7311
  summonServer,
8474
- turnControl: nativeTurnControl,
8475
7312
  // CT238: this turn's conversation, forwarded as the active-conversation
8476
7313
  // header so a cross-thread post/spawn stamps its origin.
8477
7314
  activeConversationId: payload.conversationId,
@@ -8489,22 +7326,19 @@ ${reason}`,
8489
7326
  if (this.opts.codexEnabled) {
8490
7327
  adapters.push(createCodexAdapter({ enabled: true, onWarn }));
8491
7328
  }
8492
- if (this.opts.cabaneNativeApiKey) {
8493
- adapters.push(createCabaneNativeAdapter({ apiKey: this.opts.cabaneNativeApiKey, onWarn }));
8494
- }
8495
7329
  const registry = createAdapterRegistry(adapters);
8496
7330
  let adapter;
8497
7331
  try {
8498
7332
  adapter = selectAdapter(registry, turnContext.runtime);
8499
- } catch (err2) {
8500
- if (!(err2 instanceof RuntimeUnavailableError)) throw err2;
7333
+ } catch (err) {
7334
+ if (!(err instanceof RuntimeUnavailableError)) throw err;
8501
7335
  turnLog.error(
8502
- { runtime: err2.runtime, available: err2.available },
7336
+ { runtime: err.runtime, available: err.available },
8503
7337
  "dispatcher: turn runtime not available on this device"
8504
7338
  );
8505
7339
  try {
8506
7340
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8507
- body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err2.message}`,
7341
+ body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
8508
7342
  kind: "final",
8509
7343
  turnId,
8510
7344
  parentMessageId: payload.messageId
@@ -8519,7 +7353,7 @@ ${reason}`,
8519
7353
  payload,
8520
7354
  turnLog,
8521
7355
  startedAt,
8522
- `runtime_unavailable:${err2.runtime}`
7356
+ `runtime_unavailable:${err.runtime}`
8523
7357
  );
8524
7358
  }
8525
7359
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
@@ -8653,9 +7487,9 @@ ${reason}`,
8653
7487
  skipState.skipped = true;
8654
7488
  skipState.reason = intent.skipReason;
8655
7489
  }
8656
- } catch (err2) {
7490
+ } catch (err) {
8657
7491
  turnLog.warn(
8658
- { err: err2 instanceof Error ? err2.message : String(err2) },
7492
+ { err: err instanceof Error ? err.message : String(err) },
8659
7493
  "dispatcher: turn-control intent fetch failed; turn-control effects for this turn are dropped"
8660
7494
  );
8661
7495
  }
@@ -8701,9 +7535,9 @@ ${reason}`,
8701
7535
  payload.agentId,
8702
7536
  { agentSessionId: event.state }
8703
7537
  );
8704
- } catch (err2) {
7538
+ } catch (err) {
8705
7539
  turnLog.warn(
8706
- { err: err2 instanceof Error ? err2.message : String(err2) },
7540
+ { err: err instanceof Error ? err.message : String(err) },
8707
7541
  "dispatcher: session-id write failed (will retry next turn)"
8708
7542
  );
8709
7543
  }
@@ -8752,18 +7586,18 @@ ${reason}`,
8752
7586
  parentMessageId: payload.messageId,
8753
7587
  ...skipWake ? { wake: skipWake } : {}
8754
7588
  });
8755
- } catch (err2) {
7589
+ } catch (err) {
8756
7590
  turnLog.warn(
8757
- { err: err2 instanceof Error ? err2.message : String(err2) },
7591
+ { err: err instanceof Error ? err.message : String(err) },
8758
7592
  "dispatcher: skipped-marker commit failed"
8759
7593
  );
8760
7594
  }
8761
7595
  } else {
8762
7596
  await committer.finalize(okResult);
8763
7597
  }
8764
- } catch (err2) {
7598
+ } catch (err) {
8765
7599
  okResult = false;
8766
- resultReason = err2 instanceof Error ? err2.message : String(err2);
7600
+ resultReason = err instanceof Error ? err.message : String(err);
8767
7601
  turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
8768
7602
  } finally {
8769
7603
  if (idleTimer) clearTimeout(idleTimer);
@@ -8799,9 +7633,9 @@ ${reason}`,
8799
7633
  // CT113: the stopped marker is still "about" the triggering message.
8800
7634
  parentMessageId: payload.messageId
8801
7635
  });
8802
- } catch (err2) {
7636
+ } catch (err) {
8803
7637
  turnLog.warn(
8804
- { err: err2 instanceof Error ? err2.message : String(err2) },
7638
+ { err: err instanceof Error ? err.message : String(err) },
8805
7639
  "dispatcher: stopped-marker commit failed"
8806
7640
  );
8807
7641
  }
@@ -8842,9 +7676,9 @@ ${reason}`,
8842
7676
  payload.agentId,
8843
7677
  body
8844
7678
  );
8845
- } catch (err2) {
7679
+ } catch (err) {
8846
7680
  turnLog.warn(
8847
- { err: err2 instanceof Error ? err2.message : String(err2) },
7681
+ { err: err instanceof Error ? err.message : String(err) },
8848
7682
  "dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
8849
7683
  );
8850
7684
  }
@@ -8912,7 +7746,6 @@ function buildCompanionManifest(opts) {
8912
7746
  if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
8913
7747
  if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
8914
7748
  if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
8915
- if (opts.cabaneNative) runtimes.push({ name: "cabane-native", version: null });
8916
7749
  return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
8917
7750
  }
8918
7751
 
@@ -9116,7 +7949,7 @@ import {
9116
7949
  existsSync as existsSync10,
9117
7950
  mkdirSync as mkdirSync11,
9118
7951
  readdirSync as readdirSync2,
9119
- readFileSync as readFileSync9,
7952
+ readFileSync as readFileSync8,
9120
7953
  renameSync as renameSync3,
9121
7954
  rmSync as rmSync5,
9122
7955
  writeFileSync as writeFileSync7
@@ -9150,13 +7983,13 @@ var Outbox = class {
9150
7983
  try {
9151
7984
  writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
9152
7985
  renameSync3(tmp, target);
9153
- } catch (err2) {
7986
+ } catch (err) {
9154
7987
  try {
9155
7988
  rmSync5(tmp, { force: true });
9156
7989
  } catch {
9157
7990
  }
9158
7991
  this.log?.warn(
9159
- { workspaceId: this.workspaceId, err: err2 instanceof Error ? err2.message : String(err2) },
7992
+ { workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
9160
7993
  "companion outbox: failed to persist entry"
9161
7994
  );
9162
7995
  return;
@@ -9181,7 +8014,7 @@ var Outbox = class {
9181
8014
  if (!name.endsWith(".json")) continue;
9182
8015
  const full = join13(dir2, name);
9183
8016
  try {
9184
- const parsed = JSON.parse(readFileSync9(full, "utf8"));
8017
+ const parsed = JSON.parse(readFileSync8(full, "utf8"));
9185
8018
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
9186
8019
  entries.push(parsed);
9187
8020
  } else {
@@ -9253,40 +8086,43 @@ var Outbox = class {
9253
8086
  };
9254
8087
 
9255
8088
  // src/run-config.ts
9256
- import { z as z16 } from "zod";
9257
- var mcpStdioServerSchema = z16.object({
9258
- type: z16.literal("stdio").optional(),
9259
- command: z16.string().min(1),
9260
- args: z16.array(z16.string()).optional(),
9261
- env: z16.record(z16.string(), z16.string()).optional()
8089
+ import { z as z14 } from "zod";
8090
+ var mcpStdioServerSchema = z14.object({
8091
+ type: z14.literal("stdio").optional(),
8092
+ command: z14.string().min(1),
8093
+ args: z14.array(z14.string()).optional(),
8094
+ env: z14.record(z14.string(), z14.string()).optional()
9262
8095
  });
9263
- var mcpHttpServerSchema = z16.object({
9264
- type: z16.literal("http"),
9265
- url: z16.string().url(),
9266
- headers: z16.record(z16.string(), z16.string()).optional()
8096
+ var mcpHttpServerSchema = z14.object({
8097
+ type: z14.literal("http"),
8098
+ url: z14.string().url(),
8099
+ headers: z14.record(z14.string(), z14.string()).optional()
9267
8100
  });
9268
- var mcpSseServerSchema = z16.object({
9269
- type: z16.literal("sse"),
9270
- url: z16.string().url(),
9271
- headers: z16.record(z16.string(), z16.string()).optional()
8101
+ var mcpSseServerSchema = z14.object({
8102
+ type: z14.literal("sse"),
8103
+ url: z14.string().url(),
8104
+ headers: z14.record(z14.string(), z14.string()).optional()
9272
8105
  });
9273
- var mcpServerDefSchema = z16.union([
8106
+ var mcpServerDefSchema = z14.union([
9274
8107
  mcpHttpServerSchema,
9275
8108
  mcpSseServerSchema,
9276
8109
  mcpStdioServerSchema
9277
8110
  ]);
9278
- var thinkingConfigSchema = z16.discriminatedUnion("type", [
9279
- z16.object({ type: z16.literal("adaptive") }),
9280
- z16.object({ type: z16.literal("enabled"), budgetTokens: z16.number().int().positive().optional() }),
9281
- z16.object({ type: z16.literal("disabled") })
8111
+ var thinkingConfigSchema = z14.discriminatedUnion("type", [
8112
+ z14.object({ type: z14.literal("adaptive") }),
8113
+ z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
8114
+ z14.object({ type: z14.literal("disabled") })
9282
8115
  ]);
9283
- var effortSchema = z16.enum(["low", "medium", "high", "xhigh", "max"]);
9284
- var runConfigSchema = z16.object({
9285
- mode: z16.enum(["assistant", "coding", "custom"]).optional(),
9286
- allowedTools: z16.array(z16.string()).optional(),
9287
- disallowedTools: z16.array(z16.string()).optional(),
9288
- mcpServers: z16.record(z16.string(), mcpServerDefSchema).optional(),
9289
- model: z16.string().min(1).optional(),
8116
+ var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
8117
+ var runConfigSchema = z14.object({
8118
+ // CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
8119
+ // trio + its custom tool lists — `true` grants the host filesystem/shell, absent
8120
+ // is the locked surface. Kept in lockstep with `@cabane/shared`'s
8121
+ // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
8122
+ // stripped, so an older companion riding a newer server never rejects the config).
8123
+ hostAccess: z14.boolean().optional(),
8124
+ mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
8125
+ model: z14.string().min(1).optional(),
9290
8126
  thinking: thinkingConfigSchema.optional(),
9291
8127
  effort: effortSchema.optional()
9292
8128
  });
@@ -9331,20 +8167,20 @@ var SseSubscriber = class {
9331
8167
  try {
9332
8168
  await this.connect();
9333
8169
  backoff = 500;
9334
- } catch (err2) {
8170
+ } catch (err) {
9335
8171
  if (this.aborted) return;
9336
- if (err2 instanceof ApiError && (err2.status === 401 || err2.status === 403)) {
8172
+ if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
9337
8173
  this.opts.log.error(
9338
- { workspaceId: this.opts.workspaceId, status: err2.status },
8174
+ { workspaceId: this.opts.workspaceId, status: err.status },
9339
8175
  "SSE auth failed \u2014 tearing down this workspace subscriber"
9340
8176
  );
9341
- this.opts.onAuthFailure(err2.status);
8177
+ this.opts.onAuthFailure(err.status);
9342
8178
  return;
9343
8179
  }
9344
8180
  this.opts.log.warn(
9345
8181
  {
9346
8182
  workspaceId: this.opts.workspaceId,
9347
- err: err2 instanceof Error ? err2.message : String(err2),
8183
+ err: err instanceof Error ? err.message : String(err),
9348
8184
  backoff
9349
8185
  },
9350
8186
  "SSE disconnected; reconnecting"
@@ -9487,7 +8323,7 @@ var CompanionSupervisor = class {
9487
8323
  if (!this.config.deviceToken) {
9488
8324
  this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
9489
8325
  process.stdout.write(
9490
- "companion: this device is not paired \u2014 run `cabane-companion pair` and paste the string from the cabane app.\n"
8326
+ "companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
9491
8327
  );
9492
8328
  return;
9493
8329
  }
@@ -9542,9 +8378,6 @@ var CompanionSupervisor = class {
9542
8378
  // like opencode — the CLI's presence is the operator's responsibility;
9543
8379
  // a misconfigured device fails the turn loudly, never silently).
9544
8380
  codex: isCodexEnabled(this.config),
9545
- // CT598: advertise the native runtime when an OpenRouter key is set. Key
9546
- // absent → not advertised, so a native turn never routes here.
9547
- cabaneNative: isCabaneNativeEnabled(),
9548
8381
  // CT571/CT586: each runtime's `version` from the latest harness probe
9549
8382
  // (fail-soft to null). Informational only — the server matches on name.
9550
8383
  versions: this.harnessVersions
@@ -9562,9 +8395,9 @@ var CompanionSupervisor = class {
9562
8395
  this.hub.setDevice({ deviceId: res.deviceId });
9563
8396
  this.deviceId = res.deviceId;
9564
8397
  this.checkVersionSkew(res.serverVersion);
9565
- } catch (err2) {
8398
+ } catch (err) {
9566
8399
  this.log.warn(
9567
- { err: err2 instanceof Error ? err2.message : String(err2) },
8400
+ { err: err instanceof Error ? err.message : String(err) },
9568
8401
  "companion: device heartbeat failed (will retry on next tick)"
9569
8402
  );
9570
8403
  }
@@ -9600,12 +8433,12 @@ var CompanionSupervisor = class {
9600
8433
  const resp = await this.deviceApi.getAssignments();
9601
8434
  items = resp.assignments;
9602
8435
  device = resp.device;
9603
- } catch (err2) {
8436
+ } catch (err) {
9604
8437
  this.log.error(
9605
- { err: err2 instanceof Error ? err2.message : String(err2) },
8438
+ { err: err instanceof Error ? err.message : String(err) },
9606
8439
  "companion: assignments pull failed \u2014 check the device is still active in the cabane app"
9607
8440
  );
9608
- this.hub.setDeviceError(err2 instanceof Error ? err2.message : String(err2));
8441
+ this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
9609
8442
  return;
9610
8443
  }
9611
8444
  this.hub.setDevice({ deviceId: device.id, deviceLabel: device.label });
@@ -9682,7 +8515,7 @@ var CompanionSupervisor = class {
9682
8515
  agentId: it.agentId,
9683
8516
  username: it.agentUsername,
9684
8517
  displayName: it.agentDisplayName,
9685
- mode: runConfig.mode ?? "assistant",
8518
+ mode: runConfig.hostAccess ? "full" : "none",
9686
8519
  hasCredential: false,
9687
8520
  missingSecrets: missing
9688
8521
  });
@@ -9702,7 +8535,7 @@ var CompanionSupervisor = class {
9702
8535
  agentId: it.agentId,
9703
8536
  username: it.agentUsername,
9704
8537
  displayName: it.agentDisplayName,
9705
- mode: runConfig.mode ?? "assistant",
8538
+ mode: runConfig.hostAccess ? "full" : "none",
9706
8539
  hasCredential: true,
9707
8540
  missingSecrets: missing
9708
8541
  });
@@ -9786,12 +8619,9 @@ var CompanionSupervisor = class {
9786
8619
  // CT481: register the codex adapter when this device offers codex; unset
9787
8620
  // leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
9788
8621
  ...isCodexEnabled(this.config) ? { codexEnabled: true } : {},
9789
- // CT598: register the cabane-native adapter when an OpenRouter key is set;
9790
- // unset leaves a `cabane-native/…` turn to fail loudly (no silent fallback).
9791
- ...cabaneNativeApiKey() ? { cabaneNativeApiKey: cabaneNativeApiKey() } : {},
9792
8622
  // CT556: per-turn timeout watchdog windows, from the companion's own env
9793
8623
  // (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
9794
- // dispatcher's baked-in defaults (10 min idle / 45 min total).
8624
+ // dispatcher's baked-in defaults (10 min idle / 6h total).
9795
8625
  ...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
9796
8626
  ...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
9797
8627
  observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
@@ -9858,9 +8688,9 @@ var CompanionSupervisor = class {
9858
8688
  let wire;
9859
8689
  try {
9860
8690
  wire = JSON.parse(ev.data);
9861
- } catch (err2) {
8691
+ } catch (err) {
9862
8692
  this.log.warn(
9863
- { err: err2 instanceof Error ? err2.message : String(err2) },
8693
+ { err: err instanceof Error ? err.message : String(err) },
9864
8694
  "malformed SSE payload"
9865
8695
  );
9866
8696
  return;
@@ -9907,13 +8737,13 @@ var CompanionSupervisor = class {
9907
8737
  }
9908
8738
  const chainKey = `${payload.conversationId}|${payload.agentId}`;
9909
8739
  const prev = wr.chains.get(chainKey) ?? Promise.resolve();
9910
- const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err2) => {
8740
+ const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err) => {
9911
8741
  this.log.warn(
9912
8742
  {
9913
8743
  workspaceId: wr.workspaceId,
9914
8744
  conversationId: payload.conversationId,
9915
8745
  agentId: payload.agentId,
9916
- err: err2 instanceof Error ? err2.message : String(err2)
8746
+ err: err instanceof Error ? err.message : String(err)
9917
8747
  },
9918
8748
  "companion: conversation turn handler threw"
9919
8749
  );
@@ -10005,10 +8835,10 @@ var CompanionSupervisor = class {
10005
8835
  } else {
10006
8836
  drainDelay = DRAIN_BASE_MS;
10007
8837
  }
10008
- } catch (err2) {
8838
+ } catch (err) {
10009
8839
  drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
10010
8840
  this.log.warn(
10011
- { agentId, err: err2 instanceof Error ? err2.message : String(err2) },
8841
+ { agentId, err: err instanceof Error ? err.message : String(err) },
10012
8842
  "companion: outbox drain pass threw (will retry with backoff)"
10013
8843
  );
10014
8844
  } finally {
@@ -10075,9 +8905,9 @@ var CompanionSupervisor = class {
10075
8905
  codex: signals.codexVersion
10076
8906
  };
10077
8907
  this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
10078
- } catch (err2) {
8908
+ } catch (err) {
10079
8909
  this.log.warn(
10080
- { err: err2 instanceof Error ? err2.message : String(err2) },
8910
+ { err: err instanceof Error ? err.message : String(err) },
10081
8911
  "companion: harness probe failed (will retry on next beat)"
10082
8912
  );
10083
8913
  }
@@ -10175,10 +9005,13 @@ var CompanionSupervisor = class {
10175
9005
  await Promise.race([
10176
9006
  Promise.allSettled(turns),
10177
9007
  new Promise((resolve) => {
10178
- timer = setTimeout(() => {
10179
- timedOut = true;
10180
- resolve();
10181
- }, Math.max(0, graceMs));
9008
+ timer = setTimeout(
9009
+ () => {
9010
+ timedOut = true;
9011
+ resolve();
9012
+ },
9013
+ Math.max(0, graceMs)
9014
+ );
10182
9015
  timer.unref?.();
10183
9016
  })
10184
9017
  ]);
@@ -10232,17 +9065,17 @@ var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
10232
9065
  "ERR_STREAM_DESTROYED",
10233
9066
  "ERR_STREAM_WRITE_AFTER_END"
10234
9067
  ]);
10235
- function errorCode(err2) {
10236
- if (err2 && typeof err2 === "object" && "code" in err2) {
10237
- const code = err2.code;
9068
+ function errorCode(err) {
9069
+ if (err && typeof err === "object" && "code" in err) {
9070
+ const code = err.code;
10238
9071
  if (typeof code === "string") return code;
10239
9072
  }
10240
9073
  return void 0;
10241
9074
  }
10242
- function isRecoverableSocketError(err2) {
10243
- const code = errorCode(err2);
9075
+ function isRecoverableSocketError(err) {
9076
+ const code = errorCode(err);
10244
9077
  if (code && RECOVERABLE_CODES.has(code)) return true;
10245
- const message = err2 instanceof Error ? err2.message : String(err2);
9078
+ const message = err instanceof Error ? err.message : String(err);
10246
9079
  return /\bEPIPE\b|\bECONNRESET\b/.test(message);
10247
9080
  }
10248
9081
  function installProcessSafetyNet(log, opts = {}) {
@@ -10251,16 +9084,16 @@ function installProcessSafetyNet(log, opts = {}) {
10251
9084
  stream.on("error", () => {
10252
9085
  });
10253
9086
  }
10254
- proc.on("uncaughtException", (err2) => handleUncaught(log, err2, "uncaughtException"));
9087
+ proc.on("uncaughtException", (err) => handleUncaught(log, err, "uncaughtException"));
10255
9088
  proc.on(
10256
9089
  "unhandledRejection",
10257
9090
  (reason) => handleUncaught(log, reason, "unhandledRejection")
10258
9091
  );
10259
9092
  }
10260
- function handleUncaught(log, err2, origin) {
10261
- const message = err2 instanceof Error ? err2.message : String(err2);
10262
- const code = errorCode(err2);
10263
- if (isRecoverableSocketError(err2)) {
9093
+ function handleUncaught(log, err, origin) {
9094
+ const message = err instanceof Error ? err.message : String(err);
9095
+ const code = errorCode(err);
9096
+ if (isRecoverableSocketError(err)) {
10264
9097
  log.warn(
10265
9098
  { origin, code, err: message },
10266
9099
  "companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
@@ -10268,13 +9101,13 @@ function handleUncaught(log, err2, origin) {
10268
9101
  return;
10269
9102
  }
10270
9103
  log.error(
10271
- { origin, code, err: message, stack: err2 instanceof Error ? err2.stack : void 0 },
9104
+ { origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
10272
9105
  "companion: uncaught error (kept running \u2014 see the stack above)"
10273
9106
  );
10274
9107
  }
10275
9108
 
10276
9109
  // src/crash-marker.ts
10277
- import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync10, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9110
+ import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
10278
9111
  import { join as join14 } from "path";
10279
9112
  function crashMarkerPath() {
10280
9113
  return join14(cabaneDir(), "last-error.json");
@@ -10305,14 +9138,14 @@ async function createCompanionRuntime(opts = {}) {
10305
9138
  cfg = requireConfig();
10306
9139
  claudeCode = await probeClaude();
10307
9140
  await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
10308
- } catch (err2) {
9141
+ } catch (err) {
10309
9142
  recordCrash({
10310
- reason: err2 instanceof Error ? err2.message : String(err2),
10311
- ...errorCode(err2) ? { code: errorCode(err2) } : {},
9143
+ reason: err instanceof Error ? err.message : String(err),
9144
+ ...errorCode(err) ? { code: errorCode(err) } : {},
10312
9145
  origin: "startup",
10313
9146
  at: (/* @__PURE__ */ new Date()).toISOString()
10314
9147
  });
10315
- throw err2;
9148
+ throw err;
10316
9149
  }
10317
9150
  if (cfg.logLevel) log.level = cfg.logLevel;
10318
9151
  const harnessVersions = await probeHarnessVersions({
@@ -10403,6 +9236,8 @@ async function createCompanionRuntime(opts = {}) {
10403
9236
 
10404
9237
  // src/commands/start.ts
10405
9238
  var FORCE_EXIT_MS = 4e3;
9239
+ var DEPLOY_REEXEC_EXIT = 75;
9240
+ var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
10406
9241
  async function start(opts = {}) {
10407
9242
  const result = await createCompanionRuntime({
10408
9243
  ...opts.port !== void 0 ? { port: opts.port } : {}
@@ -10426,7 +9261,7 @@ Cabane Companion is running.
10426
9261
  `);
10427
9262
  if (!runtime.config.deviceToken) {
10428
9263
  process.stdout.write(
10429
- `This device isn't paired yet \u2014 run \`cabane-companion pair\` and paste the string from the cabane app.
9264
+ `This device isn't paired yet \u2014 run \`cabane-companion pair\`, then confirm the short code in Settings \u2192 Connectors.
10430
9265
 
10431
9266
  `
10432
9267
  );
@@ -10466,25 +9301,25 @@ companion: received ${signal}, shutting down\u2026
10466
9301
  if (shuttingDown) return;
10467
9302
  shuttingDown = true;
10468
9303
  const configuredGrace = Number.parseInt(
10469
- process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ?? "60000",
9304
+ process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ?? String(DEPLOY_GRACE_MS),
10470
9305
  10
10471
9306
  );
10472
- const graceMs = Number.isFinite(configuredGrace) ? configuredGrace : 6e4;
9307
+ const graceMs = Number.isFinite(configuredGrace) ? configuredGrace : DEPLOY_GRACE_MS;
10473
9308
  process.stdout.write(`
10474
9309
  companion: deploy drain requested (${graceMs}ms grace)\u2026
10475
9310
  `);
10476
9311
  void runtime.drainForRestart(graceMs).then(({ drained }) => {
10477
9312
  process.stdout.write(
10478
- drained ? "companion: deploy drain complete.\n" : "companion: deploy grace expired; unfinished turns will resume after restart.\n"
9313
+ drained ? "companion: deploy drain complete; re-execing onto the new dist.\n" : "companion: deploy grace expired; re-execing \u2014 unfinished turns resume after restart.\n"
10479
9314
  );
10480
9315
  resolve();
10481
- process.exit(0);
10482
- }).catch((err2) => {
9316
+ process.exit(DEPLOY_REEXEC_EXIT);
9317
+ }).catch((err) => {
10483
9318
  process.stderr.write(
10484
- `companion: deploy drain failed: ${err2 instanceof Error ? err2.message : String(err2)}
9319
+ `companion: deploy drain failed: ${err instanceof Error ? err.message : String(err)}
10485
9320
  `
10486
9321
  );
10487
- process.exit(1);
9322
+ process.exit(DEPLOY_REEXEC_EXIT);
10488
9323
  });
10489
9324
  });
10490
9325
  });
@@ -10496,7 +9331,7 @@ async function status() {
10496
9331
  const cfg = loadConfig();
10497
9332
  if (!cfg || !cfg.deviceToken) {
10498
9333
  process.stdout.write(
10499
- "companion: not paired. Register a device in the cabane app (Settings \u2192 Agents \u2192 Companions), then run `cabane-companion pair` and paste the string when prompted.\n"
9334
+ "companion: not paired. Run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
10500
9335
  );
10501
9336
  process.exitCode = 1;
10502
9337
  return;
@@ -10628,7 +9463,7 @@ function isAlive(kill, pid) {
10628
9463
  }
10629
9464
 
10630
9465
  // src/commands/transcript.ts
10631
- import { existsSync as existsSync12, readFileSync as readFileSync11, readdirSync as readdirSync4 } from "fs";
9466
+ import { existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
10632
9467
  import { isAbsolute, join as join15 } from "path";
10633
9468
  async function transcript(opts = {}) {
10634
9469
  const dir2 = transcriptsDir();
@@ -10722,14 +9557,14 @@ var TranscriptFollower = class {
10722
9557
  function isComplete(content) {
10723
9558
  for (const line of content.split("\n")) {
10724
9559
  if (!line.trim()) continue;
10725
- if (str4(rec(safeParse(line))?.type) === "_outcome") return true;
9560
+ if (str2(rec(safeParse(line))?.type) === "_outcome") return true;
10726
9561
  }
10727
9562
  return false;
10728
9563
  }
10729
9564
  async function followTranscripts(dir2) {
10730
9565
  const follower = new TranscriptFollower({
10731
9566
  listFiles: () => listFiles(dir2),
10732
- read: (f) => readFileSync11(join15(dir2, f), "utf8"),
9567
+ read: (f) => readFileSync10(join15(dir2, f), "utf8"),
10733
9568
  write: (s) => process.stdout.write(s),
10734
9569
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
10735
9570
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -10771,13 +9606,13 @@ function printList(dir2) {
10771
9606
  for (const f of files.slice(0, 20)) {
10772
9607
  const { meta, outcome } = peek(join15(dir2, f));
10773
9608
  const when = fmtTime(rec(meta)?.ts);
10774
- const ws = str4(rec(meta)?.workspaceSlug);
9609
+ const ws = str2(rec(meta)?.workspaceSlug);
10775
9610
  const o = rec(outcome);
10776
- const verdict = o ? o.ok === true ? "ok" : `ERROR${str4(o.reason) ? ` (${str4(o.reason)})` : ""}` : "\u2026";
9611
+ const verdict = o ? o.ok === true ? "ok" : `ERROR${str2(o.reason) ? ` (${str2(o.reason)})` : ""}` : "\u2026";
10777
9612
  process.stdout.write(
10778
9613
  ` ${f}
10779
9614
  ${when} \xB7 ${ws} \xB7 ${verdict}
10780
- \u201C${excerpt(str4(rec(meta)?.message), 70)}\u201D
9615
+ \u201C${excerpt(str2(rec(meta)?.message), 70)}\u201D
10781
9616
 
10782
9617
  `
10783
9618
  );
@@ -10790,10 +9625,10 @@ function peek(path3) {
10790
9625
  let meta;
10791
9626
  let outcome;
10792
9627
  try {
10793
- for (const line of readFileSync11(path3, "utf8").split("\n")) {
9628
+ for (const line of readFileSync10(path3, "utf8").split("\n")) {
10794
9629
  if (!line.trim()) continue;
10795
9630
  const o = safeParse(line);
10796
- const t = str4(rec(o)?.type);
9631
+ const t = str2(rec(o)?.type);
10797
9632
  if (t === "_meta") meta = o;
10798
9633
  else if (t === "_outcome") outcome = o;
10799
9634
  }
@@ -10823,10 +9658,10 @@ function resolveTarget(dir2, target) {
10823
9658
  function renderFile(path3) {
10824
9659
  let content;
10825
9660
  try {
10826
- content = readFileSync11(path3, "utf8");
10827
- } catch (err2) {
9661
+ content = readFileSync10(path3, "utf8");
9662
+ } catch (err) {
10828
9663
  throw new CompanionError(
10829
- `couldn't read ${path3}: ${err2 instanceof Error ? err2.message : String(err2)}`
9664
+ `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
10830
9665
  );
10831
9666
  }
10832
9667
  return renderTranscript(content.split("\n"));
@@ -10838,19 +9673,19 @@ function renderTranscript(jsonlLines) {
10838
9673
  if (!raw.trim()) continue;
10839
9674
  const obj = rec(safeParse(raw));
10840
9675
  if (!obj) continue;
10841
- switch (str4(obj.type)) {
9676
+ switch (str2(obj.type)) {
10842
9677
  case "_meta":
10843
9678
  out.push(
10844
- `${fmtTime(obj.ts)} \xB7 workspace=${str4(obj.workspaceSlug)} \xB7 conversation=${str4(obj.conversationId)}`
9679
+ `${fmtTime(obj.ts)} \xB7 workspace=${str2(obj.workspaceSlug)} \xB7 conversation=${str2(obj.conversationId)}`
10845
9680
  );
10846
- out.push("", `> USER: ${str4(obj.message)}`, "");
9681
+ out.push("", `> USER: ${str2(obj.message)}`, "");
10847
9682
  break;
10848
9683
  case "system":
10849
- if (str4(obj.subtype) === "init") {
10850
- out.push(`[session ${str4(obj.session_id) || "?"} \xB7 model ${str4(obj.model) || "?"}]`);
9684
+ if (str2(obj.subtype) === "init") {
9685
+ out.push(`[session ${str2(obj.session_id) || "?"} \xB7 model ${str2(obj.model) || "?"}]`);
10851
9686
  const servers = Array.isArray(obj.mcp_servers) ? obj.mcp_servers.map((s) => {
10852
9687
  const r = rec(s);
10853
- return r ? `${str4(r.name) || "?"}${str4(r.status) ? `(${str4(r.status)})` : ""}` : "";
9688
+ return r ? `${str2(r.name) || "?"}${str2(r.status) ? `(${str2(r.status)})` : ""}` : "";
10854
9689
  }).filter(Boolean).join(", ") : "";
10855
9690
  if (servers) out.push(` MCP servers: ${servers}`);
10856
9691
  if (Array.isArray(obj.tools)) out.push(` tools: ${obj.tools.length} available`);
@@ -10876,8 +9711,8 @@ function renderTranscript(jsonlLines) {
10876
9711
  if (!Array.isArray(content)) break;
10877
9712
  for (const b of content) {
10878
9713
  const block = rec(b);
10879
- if (!block || str4(block.type) !== "tool_result") continue;
10880
- const id = str4(block.tool_use_id);
9714
+ if (!block || str2(block.type) !== "tool_result") continue;
9715
+ const id = str2(block.tool_use_id);
10881
9716
  const label = pending.get(id) ?? "tool";
10882
9717
  pending.delete(id);
10883
9718
  const tag = block.is_error === true ? "ERROR" : "ok";
@@ -10886,18 +9721,18 @@ function renderTranscript(jsonlLines) {
10886
9721
  break;
10887
9722
  }
10888
9723
  case "result": {
10889
- const isErr = obj.is_error === true || str4(obj.subtype) !== "success";
9724
+ const isErr = obj.is_error === true || str2(obj.subtype) !== "success";
10890
9725
  const dur = typeof obj.duration_ms === "number" ? ` \xB7 ${obj.duration_ms}ms` : "";
10891
9726
  out.push(
10892
- `[result ${isErr ? "error" : "ok"}${str4(obj.subtype) ? ` \xB7 ${str4(obj.subtype)}` : ""}${dur}]`
9727
+ `[result ${isErr ? "error" : "ok"}${str2(obj.subtype) ? ` \xB7 ${str2(obj.subtype)}` : ""}${dur}]`
10893
9728
  );
10894
- if (isErr && str4(obj.result).trim()) out.push(` ${indent(str4(obj.result))}`);
9729
+ if (isErr && str2(obj.result).trim()) out.push(` ${indent(str2(obj.result))}`);
10895
9730
  break;
10896
9731
  }
10897
9732
  case "_outcome": {
10898
9733
  const dur = typeof obj.durationMs === "number" ? ` \xB7 ${obj.durationMs}ms` : "";
10899
9734
  out.push(
10900
- `[outcome ${obj.ok === true ? "ok" : "error"}${str4(obj.reason) ? ` \xB7 ${str4(obj.reason)}` : ""}${dur}]`
9735
+ `[outcome ${obj.ok === true ? "ok" : "error"}${str2(obj.reason) ? ` \xB7 ${str2(obj.reason)}` : ""}${dur}]`
10901
9736
  );
10902
9737
  break;
10903
9738
  }
@@ -10915,7 +9750,7 @@ function safeParse(s) {
10915
9750
  function rec(v) {
10916
9751
  return v && typeof v === "object" ? v : null;
10917
9752
  }
10918
- function str4(v) {
9753
+ function str2(v) {
10919
9754
  return typeof v === "string" ? v : "";
10920
9755
  }
10921
9756
  function fmtTime(ts) {
@@ -10954,22 +9789,14 @@ program.name("cabane-companion").description(
10954
9789
  ).version(COMPANION_VERSION);
10955
9790
  program.command("pair").description(
10956
9791
  "pair this device with cabane \u2014 shows a short code you confirm in Settings \u2192 Devices."
10957
- ).argument(
10958
- "[string]",
10959
- "(legacy) pairing string copied from cabane. Implies --legacy; omit it for the device-code flow."
10960
- ).option("--server <url>", "the cabane instance to pair with (default https://cabane.ai)").option("--legacy", "use the base64url pairing-string paste instead of the device-code flow").option(
10961
- "--file <path>",
10962
- "(legacy) read the pairing string from a file instead of passing it as an argument"
10963
- ).action(
10964
- async (pairingString, opts) => {
10965
- await pair({
10966
- ...pairingString !== void 0 ? { arg: pairingString } : {},
10967
- ...opts.server !== void 0 ? { server: opts.server } : {},
10968
- ...opts.legacy ? { legacy: true } : {},
10969
- ...opts.file !== void 0 ? { file: opts.file } : {}
10970
- });
10971
- }
10972
- );
9792
+ ).allowExcessArguments(false).option("--server <url>", "the cabane instance to pair with (default https://cabane.ai)").action(async (opts) => {
9793
+ await pair({
9794
+ ...opts.server !== void 0 ? { server: opts.server } : {}
9795
+ });
9796
+ });
9797
+ program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
9798
+ writeCompletedPairing(readFileSync11(0, "utf8"));
9799
+ });
10973
9800
  program.command("start").description("pull this device\u2019s assigned agents from cabane and run them.").option("--open", "open the dashboard in a browser on startup (default: off)").option("--no-open", "don't auto-open the dashboard (overrides config.autoOpen)").option("--daemon", "run detached in the background (terminal returns; replies keep landing)").option("--port <port>", "dashboard port (default 7474; falls through if taken)", parsePort).action(async (opts) => {
10974
9801
  if (opts.daemon) {
10975
9802
  await startDaemon({
@@ -11015,19 +9842,19 @@ function parsePort(raw) {
11015
9842
  }
11016
9843
  return n;
11017
9844
  }
11018
- program.parseAsync(process.argv).catch((err2) => {
11019
- if (err2 instanceof CompanionError) {
11020
- process.stderr.write(`error: ${err2.message}
9845
+ program.parseAsync(process.argv).catch((err) => {
9846
+ if (err instanceof CompanionError) {
9847
+ process.stderr.write(`error: ${err.message}
11021
9848
  `);
11022
9849
  process.exitCode = 1;
11023
9850
  return;
11024
9851
  }
11025
- if (err2 && typeof err2 === "object" && "name" in err2 && err2.name === "ExitPromptError") {
9852
+ if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
11026
9853
  process.stderr.write("cancelled\n");
11027
9854
  process.exitCode = 130;
11028
9855
  return;
11029
9856
  }
11030
- process.stderr.write(`${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}
9857
+ process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
11031
9858
  `);
11032
9859
  process.exitCode = 1;
11033
9860
  });