@cabane/companion 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/daemon.ts
7
- import { spawn as spawn3 } from "child_process";
7
+ import { spawn as spawn4 } from "child_process";
8
8
  import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2 } from "fs";
9
9
  import { fileURLToPath } from "url";
10
10
 
@@ -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() {
@@ -522,11 +457,94 @@ function getLogger() {
522
457
  }
523
458
 
524
459
  // src/prereqs.ts
460
+ import { spawn as spawn3 } from "child_process";
461
+
462
+ // src/harness-versions.ts
525
463
  import { spawn as spawn2 } from "child_process";
464
+ var EMPTY = { claudeCode: null, opencode: null, codex: null };
465
+ function parseVersionToken(raw) {
466
+ if (!raw) return null;
467
+ const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
468
+ return m ? m[0] : null;
469
+ }
470
+ async function probeCliVersion(command, spawnImpl = spawn2) {
471
+ return new Promise((resolve) => {
472
+ let settled = false;
473
+ const done = (v) => {
474
+ if (!settled) {
475
+ settled = true;
476
+ resolve(v);
477
+ }
478
+ };
479
+ let child;
480
+ try {
481
+ child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
482
+ } catch {
483
+ done(null);
484
+ return;
485
+ }
486
+ let out = "";
487
+ child.stdout?.on("data", (chunk) => {
488
+ if (out.length < 4096) out += chunk.toString();
489
+ });
490
+ child.once("error", () => done(null));
491
+ child.once("exit", (code) => done(code === 0 ? parseVersionToken(out) : null));
492
+ });
493
+ }
494
+ async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
495
+ try {
496
+ const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
497
+ const res = await fetchImpl(`${base}/global/health`, {
498
+ headers: { accept: "application/json" }
499
+ });
500
+ if (!res.ok) return null;
501
+ return extractOpencodeVersion(await res.json());
502
+ } catch {
503
+ return null;
504
+ }
505
+ }
506
+ function extractOpencodeVersion(json) {
507
+ if (!json || typeof json !== "object") return null;
508
+ const obj = json;
509
+ const candidates = [obj.version];
510
+ for (const v of Object.values(obj)) {
511
+ if (v && typeof v === "object") candidates.push(v.version);
512
+ }
513
+ for (const c of candidates) {
514
+ if (typeof c === "string") {
515
+ const parsed = parseVersionToken(c);
516
+ if (parsed) return parsed;
517
+ }
518
+ }
519
+ return null;
520
+ }
521
+ async function probeHarnessVersions(opts, deps = {}) {
522
+ const probeClaudeCode = deps.probeClaudeCode ?? (() => probeCliVersion("claude"));
523
+ const probeCodex = deps.probeCodex ?? (() => probeCliVersion("codex"));
524
+ const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
525
+ const [claudeCode, codex, opencode] = await Promise.all([
526
+ opts.claudeCode ? safe(probeClaudeCode) : Promise.resolve(null),
527
+ opts.codex ? safe(probeCodex) : Promise.resolve(null),
528
+ opts.opencodeServerUrl ? safe(() => probeOpencode(opts.opencodeServerUrl)) : Promise.resolve(null)
529
+ ]);
530
+ return { claudeCode, opencode, codex };
531
+ }
532
+ function emptyHarnessVersions() {
533
+ return { ...EMPTY };
534
+ }
535
+ async function safe(fn) {
536
+ try {
537
+ return await fn();
538
+ } catch {
539
+ return null;
540
+ }
541
+ }
542
+
543
+ // src/prereqs.ts
526
544
  async function claudeOnPath() {
527
545
  return new Promise((resolve) => {
528
546
  let settled = false;
529
- const child = spawn2("claude", ["--version"], { stdio: "ignore" });
547
+ const child = spawn3("claude", ["--version"], { stdio: "ignore" });
530
548
  child.once("error", () => {
531
549
  if (!settled) {
532
550
  settled = true;
@@ -541,8 +559,20 @@ async function claudeOnPath() {
541
559
  });
542
560
  });
543
561
  }
562
+ var CODEX_PROBE_TIMEOUT_MS = 4e3;
563
+ async function codexOnPath() {
564
+ const version = await Promise.race([
565
+ probeCliVersion("codex"),
566
+ new Promise((resolve) => {
567
+ const timer = setTimeout(() => resolve(null), CODEX_PROBE_TIMEOUT_MS);
568
+ timer.unref?.();
569
+ })
570
+ ]);
571
+ return version !== null;
572
+ }
544
573
  async function ensureRuntimeAvailable(cfg, deps = {}) {
545
574
  const probeClaude = deps.probeClaude ?? claudeOnPath;
575
+ const probeCodex = deps.probeCodex ?? codexOnPath;
546
576
  const warn = deps.warn ?? ((message) => process.stderr.write(`${message}
547
577
  `));
548
578
  if (await probeClaude()) return;
@@ -551,15 +581,15 @@ async function ensureRuntimeAvailable(cfg, deps = {}) {
551
581
  ...isCodexEnabled(cfg) ? ["codex"] : []
552
582
  ];
553
583
  if (alternates.length > 0) {
584
+ const exposed = alternates.join(" + ");
554
585
  warn(
555
- `warning: Claude Code isn\u2019t on your PATH, so this device will run ${alternates.join(
556
- " + "
557
- )} only \u2014 it advertises just those runtimes, so assign it matching models (a Claude-model agent won\u2019t be routed here). Install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) if you want it to run Claude models too.`
586
+ `warning: Claude Code isn\u2019t on your PATH, so this device exposes ${exposed} only \u2014 it advertises nothing else, so a Claude-model agent won\u2019t be routed here. Assign it agents on models ${exposed} can run, or install Claude Code (\`npm i -g @anthropic-ai/claude-code\`) and log in if you want this device to run Claude models too.`
558
587
  );
559
588
  return;
560
589
  }
590
+ const installedButUnexposed = await probeCodex() ? 'Codex is installed on this machine but not exposed \u2014 enable it with `{ "codex": { "enabled": true } }` in ~/.cabane/config.json. Otherwise, expose a harness:\n' : "Expose at least one:\n";
561
591
  throw new CompanionError(
562
- "Claude Code, the companion\u2019s default runtime, is not on your PATH. Install it with `npm i -g @anthropic-ai/claude-code`, log in (`claude` then follow the prompts), and run `cabane-companion start` again. The default runtime uses your local Claude Code subscription to run each agent turn; opencode is supported as an alternate runtime you configure per device (see the companion README)."
592
+ "This device exposes no harness, so no agent turn can run here. " + installedButUnexposed + ' \u2022 Claude Code \u2014 install it (`npm i -g @anthropic-ai/claude-code`) and log in (`claude`, then follow the prompts); it\u2019s exposed automatically once `claude` is on your PATH.\n \u2022 Codex \u2014 install the Codex CLI and log in (`codex login`), then add `{ "codex": { "enabled": true } }` to ~/.cabane/config.json.\n \u2022 opencode \u2014 run `opencode serve --port 4096`, then add `{ "opencode": { "serverUrl": "http://127.0.0.1:4096" } }` to ~/.cabane/config.json.\nThen run `cabane-companion start` again (see the companion README).'
563
593
  );
564
594
  }
565
595
 
@@ -631,8 +661,8 @@ async function verifyRuntime(state, fetchImpl = fetch) {
631
661
  let res;
632
662
  try {
633
663
  res = await fetchImpl(url, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
634
- } catch (err2) {
635
- return isConnRefused(err2) ? "stale" : "unknown";
664
+ } catch (err) {
665
+ return isConnRefused(err) ? "stale" : "unknown";
636
666
  }
637
667
  if (!res.ok) return "unknown";
638
668
  let body;
@@ -644,9 +674,9 @@ async function verifyRuntime(state, fetchImpl = fetch) {
644
674
  if (typeof body.instance_id !== "string") return "unknown";
645
675
  return body.instance_id === state.instanceId ? "ours" : "stale";
646
676
  }
647
- function isConnRefused(err2) {
648
- if (!err2 || typeof err2 !== "object") return false;
649
- const cause = err2.cause;
677
+ function isConnRefused(err) {
678
+ if (!err || typeof err !== "object") return false;
679
+ const cause = err.cause;
650
680
  return !!cause && typeof cause === "object" && cause.code === "ECONNREFUSED";
651
681
  }
652
682
  function trimSlash(s) {
@@ -713,7 +743,7 @@ function defaultSpawnDetached(args) {
713
743
  mkdirSync4(cabaneDir(), { recursive: true });
714
744
  const logFd = openSync2(companionLogPath(), "a");
715
745
  try {
716
- return spawn3(process.execPath, [cliPath, ...args], {
746
+ return spawn4(process.execPath, [cliPath, ...args], {
717
747
  detached: true,
718
748
  stdio: ["ignore", logFd, logFd],
719
749
  env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
@@ -737,11 +767,11 @@ import {
737
767
  writeFileSync as writeFileSync3
738
768
  } from "fs";
739
769
  import { dirname as dirname3, join as join4 } from "path";
740
- import { z as z4 } from "zod";
770
+ import { z as z3 } from "zod";
741
771
  function credentialsPath() {
742
772
  return join4(cabaneDir(), "credentials.json");
743
773
  }
744
- var credentialStoreSchema = z4.record(z4.string(), z4.string());
774
+ var credentialStoreSchema = z3.record(z3.string(), z3.string());
745
775
  function load() {
746
776
  const path3 = credentialsPath();
747
777
  if (!existsSync3(path3)) return {};
@@ -774,12 +804,12 @@ function save(map) {
774
804
  } catch {
775
805
  }
776
806
  renameSync2(tmp, path3);
777
- } catch (err2) {
807
+ } catch (err) {
778
808
  try {
779
809
  rmSync3(tmp, { force: true });
780
810
  } catch {
781
811
  }
782
- throw err2;
812
+ throw err;
783
813
  }
784
814
  }
785
815
  function getCredential(agentId) {
@@ -818,13 +848,13 @@ async function logout(opts = {}) {
818
848
  }
819
849
  if (!opts.yes) {
820
850
  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) {
851
+ const ok = await confirm({ message, default: false });
852
+ if (!ok) {
823
853
  process.stdout.write("cancelled\n");
824
854
  return;
825
855
  }
826
856
  }
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.`;
857
+ 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
858
  if (opts.purge) {
829
859
  deleteConfig();
830
860
  clearCredentials();
@@ -841,10 +871,6 @@ async function logout(opts = {}) {
841
871
  );
842
872
  }
843
873
 
844
- // src/commands/pair.ts
845
- import { readFileSync as readFileSync4 } from "fs";
846
- import { password } from "@inquirer/prompts";
847
-
848
874
  // src/enrollment.ts
849
875
  function trimBase(baseUrl) {
850
876
  return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
@@ -857,9 +883,9 @@ async function postJson(baseUrl, path3, body) {
857
883
  headers: { "content-type": "application/json", accept: "application/json" },
858
884
  body: JSON.stringify(body)
859
885
  });
860
- } catch (err2) {
886
+ } catch (err) {
861
887
  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.`
888
+ `couldn't reach cabane at ${baseUrl}: ${err instanceof Error ? err.message : String(err)}. Check the server URL (pass --server <url>) and your connection.`
863
889
  );
864
890
  }
865
891
  const raw = await res.text();
@@ -874,7 +900,7 @@ async function postJson(baseUrl, path3, body) {
874
900
  if (res.status >= 400) {
875
901
  if (res.status === 404 && path3.endsWith("/code")) {
876
902
  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.`
903
+ `this cabane server (${baseUrl}) doesn't support device-flow pairing yet. Update the server to a version that supports \`cabane-companion pair\`.`
878
904
  );
879
905
  }
880
906
  const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
@@ -957,34 +983,6 @@ function writePairedConfig(paired) {
957
983
  }
958
984
 
959
985
  // 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
986
  function writePairedConfigCli(paired) {
989
987
  const { note } = writePairedConfig(paired);
990
988
  if (note) process.stderr.write(`note: ${note}
@@ -996,32 +994,35 @@ Run \`cabane-companion start\` \u2014 it will pull the agents assigned to this d
996
994
  `
997
995
  );
998
996
  }
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;
997
+ function writeCompletedPairing(raw) {
998
+ let paired;
999
+ try {
1000
+ paired = JSON.parse(raw);
1001
+ } catch {
1002
+ throw new Error("invalid completed pairing payload: expected JSON on stdin.");
1011
1003
  }
1004
+ if (!paired || typeof paired !== "object" || typeof paired.baseUrl !== "string" || typeof paired.deviceToken !== "string") {
1005
+ throw new Error("invalid completed pairing payload: baseUrl and deviceToken are required.");
1006
+ }
1007
+ writePairedConfigCli(paired);
1008
+ }
1009
+ async function pair(opts = {}) {
1012
1010
  const baseUrl = resolvePairBaseUrl(opts.server);
1013
1011
  const paired = await runDeviceFlow(baseUrl, (line) => process.stdout.write(`${line}
1014
1012
  `));
1015
1013
  writePairedConfigCli(paired);
1016
1014
  }
1017
1015
 
1016
+ // src/cli.ts
1017
+ import { readFileSync as readFileSync11 } from "fs";
1018
+
1018
1019
  // src/browser.ts
1019
- import { spawn as spawn4 } from "child_process";
1020
+ import { spawn as spawn5 } from "child_process";
1020
1021
  import { platform } from "process";
1021
1022
  function openBrowser(url) {
1022
1023
  try {
1023
1024
  const { command, args } = openerFor(url);
1024
- const child = spawn4(command, args, { stdio: "ignore", detached: true });
1025
+ const child = spawn5(command, args, { stdio: "ignore", detached: true });
1025
1026
  child.on("error", () => {
1026
1027
  });
1027
1028
  child.unref();
@@ -1463,15 +1464,15 @@ var DEFAULT_PORT = 7474;
1463
1464
  var PORT_FALLBACK_SPAN = 10;
1464
1465
  function buildDashboardApp(deps) {
1465
1466
  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);
1467
+ app.onError((err, c) => {
1468
+ if (err instanceof ApiError) {
1469
+ const status2 = err.status >= 400 && err.status < 600 ? err.status : 502;
1470
+ return c.json({ error: err.message }, status2);
1470
1471
  }
1471
- if (err2 instanceof CompanionError) {
1472
- return c.json({ error: err2.message }, 400);
1472
+ if (err instanceof CompanionError) {
1473
+ return c.json({ error: err.message }, 400);
1473
1474
  }
1474
- return c.json({ error: err2 instanceof Error ? err2.message : "internal error" }, 500);
1475
+ return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
1475
1476
  });
1476
1477
  registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
1477
1478
  return app;
@@ -1492,12 +1493,12 @@ async function startDashboard(opts) {
1492
1493
  server.closeAllConnections?.();
1493
1494
  })
1494
1495
  };
1495
- } catch (err2) {
1496
- if (isAddrInUse(err2)) {
1497
- lastErr = err2;
1496
+ } catch (err) {
1497
+ if (isAddrInUse(err)) {
1498
+ lastErr = err;
1498
1499
  continue;
1499
1500
  }
1500
- throw err2;
1501
+ throw err;
1501
1502
  }
1502
1503
  }
1503
1504
  throw new CompanionError(
@@ -1513,102 +1514,21 @@ function listen(app, port) {
1513
1514
  resolve(server);
1514
1515
  }
1515
1516
  });
1516
- server.on("error", (err2) => {
1517
+ server.on("error", (err) => {
1517
1518
  if (!settled) {
1518
1519
  settled = true;
1519
- reject(err2);
1520
+ reject(err);
1520
1521
  }
1521
1522
  });
1522
1523
  });
1523
1524
  }
1524
- function isAddrInUse(err2) {
1525
- return Boolean(err2 && typeof err2 === "object" && "code" in err2 && err2.code === "EADDRINUSE");
1525
+ function isAddrInUse(err) {
1526
+ return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
1526
1527
  }
1527
1528
  function resolveStaticDir() {
1528
1529
  return join6(dirname4(fileURLToPath2(import.meta.url)), "static");
1529
1530
  }
1530
1531
 
1531
- // src/harness-versions.ts
1532
- import { spawn as spawn5 } from "child_process";
1533
- var EMPTY = { claudeCode: null, opencode: null, codex: null };
1534
- function parseVersionToken(raw) {
1535
- if (!raw) return null;
1536
- const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
1537
- return m ? m[0] : null;
1538
- }
1539
- async function probeCliVersion(command, spawnImpl = spawn5) {
1540
- return new Promise((resolve) => {
1541
- let settled = false;
1542
- const done = (v) => {
1543
- if (!settled) {
1544
- settled = true;
1545
- resolve(v);
1546
- }
1547
- };
1548
- let child;
1549
- try {
1550
- child = spawnImpl(command, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
1551
- } catch {
1552
- done(null);
1553
- return;
1554
- }
1555
- let out = "";
1556
- child.stdout?.on("data", (chunk) => {
1557
- if (out.length < 4096) out += chunk.toString();
1558
- });
1559
- child.once("error", () => done(null));
1560
- child.once("exit", (code) => done(code === 0 ? parseVersionToken(out) : null));
1561
- });
1562
- }
1563
- async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
1564
- try {
1565
- const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
1566
- const res = await fetchImpl(`${base}/global/health`, {
1567
- headers: { accept: "application/json" }
1568
- });
1569
- if (!res.ok) return null;
1570
- return extractOpencodeVersion(await res.json());
1571
- } catch {
1572
- return null;
1573
- }
1574
- }
1575
- function extractOpencodeVersion(json) {
1576
- if (!json || typeof json !== "object") return null;
1577
- const obj = json;
1578
- const candidates = [obj.version];
1579
- for (const v of Object.values(obj)) {
1580
- if (v && typeof v === "object") candidates.push(v.version);
1581
- }
1582
- for (const c of candidates) {
1583
- if (typeof c === "string") {
1584
- const parsed = parseVersionToken(c);
1585
- if (parsed) return parsed;
1586
- }
1587
- }
1588
- return null;
1589
- }
1590
- async function probeHarnessVersions(opts, deps = {}) {
1591
- const probeClaudeCode = deps.probeClaudeCode ?? (() => probeCliVersion("claude"));
1592
- const probeCodex = deps.probeCodex ?? (() => probeCliVersion("codex"));
1593
- const probeOpencode = deps.probeOpencode ?? ((url) => probeOpencodeVersion(url));
1594
- const [claudeCode, codex, opencode] = await Promise.all([
1595
- opts.claudeCode ? safe(probeClaudeCode) : Promise.resolve(null),
1596
- opts.codex ? safe(probeCodex) : Promise.resolve(null),
1597
- opts.opencodeServerUrl ? safe(() => probeOpencode(opts.opencodeServerUrl)) : Promise.resolve(null)
1598
- ]);
1599
- return { claudeCode, opencode, codex };
1600
- }
1601
- function emptyHarnessVersions() {
1602
- return { ...EMPTY };
1603
- }
1604
- async function safe(fn) {
1605
- try {
1606
- return await fn();
1607
- } catch {
1608
- return null;
1609
- }
1610
- }
1611
-
1612
1532
  // src/api.ts
1613
1533
  var RETRY_BACKOFF_MS = [250, 750];
1614
1534
  var ACTIVE_RUN_OUTBOX_SEQ = 0;
@@ -1665,10 +1585,10 @@ var CabaneApi = class {
1665
1585
  for (let attempt = 1; ; attempt++) {
1666
1586
  try {
1667
1587
  return await this.attempt(method, path3, body, signal);
1668
- } catch (err2) {
1669
- if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err2)) throw err2;
1588
+ } catch (err) {
1589
+ if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
1670
1590
  await sleep2(RETRY_BACKOFF_MS[attempt - 1], signal);
1671
- if (signal?.aborted) throw err2;
1591
+ if (signal?.aborted) throw err;
1672
1592
  }
1673
1593
  }
1674
1594
  }
@@ -1696,14 +1616,14 @@ var CabaneApi = class {
1696
1616
  retry: true,
1697
1617
  ...signal ? { signal } : {}
1698
1618
  });
1699
- } catch (err2) {
1619
+ } catch (err) {
1700
1620
  const outbox = this.opts.outbox;
1701
- if (!outbox) throw err2;
1702
- if (signal?.aborted || isAbortError(err2)) throw err2;
1703
- if (!isRetryable(err2)) throw err2;
1621
+ if (!outbox) throw err;
1622
+ if (signal?.aborted || isAbortError(err)) throw err;
1623
+ if (!isRetryable(err)) throw err;
1704
1624
  outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
1705
1625
  this.opts.log?.warn(
1706
- { kind, turnId, seq, err: err2 instanceof Error ? err2.message : String(err2) },
1626
+ { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1707
1627
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
1708
1628
  );
1709
1629
  }
@@ -1727,10 +1647,10 @@ var CabaneApi = class {
1727
1647
  await this.request(entry.method, entry.path, entry.body, { retry: true });
1728
1648
  outbox.remove(entry.turnId, entry.seq);
1729
1649
  progressed = true;
1730
- } catch (err2) {
1731
- if (err2 instanceof ApiError && err2.status >= 400 && err2.status < 500) {
1650
+ } catch (err) {
1651
+ if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
1732
1652
  this.opts.log?.warn(
1733
- { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err2.status },
1653
+ { kind: entry.kind, turnId: entry.turnId, seq: entry.seq, status: err.status },
1734
1654
  "companion outbox: discarding entry on terminal 4xx (will never land)"
1735
1655
  );
1736
1656
  outbox.remove(entry.turnId, entry.seq);
@@ -1878,11 +1798,11 @@ var CabaneApi = class {
1878
1798
  try {
1879
1799
  await this.request("PATCH", path3, body, { retry: true });
1880
1800
  outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1881
- } catch (err2) {
1882
- if (!outbox) throw err2;
1883
- if (!isRetryable(err2)) {
1801
+ } catch (err) {
1802
+ if (!outbox) throw err;
1803
+ if (!isRetryable(err)) {
1884
1804
  outbox.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1885
- throw err2;
1805
+ throw err;
1886
1806
  }
1887
1807
  outbox.persist({
1888
1808
  enqueuedAt: Date.now(),
@@ -1894,7 +1814,7 @@ var CabaneApi = class {
1894
1814
  kind: "active-run"
1895
1815
  });
1896
1816
  this.opts.log?.warn(
1897
- { conversationId, agentId, err: err2 instanceof Error ? err2.message : String(err2) },
1817
+ { conversationId, agentId, err: err instanceof Error ? err.message : String(err) },
1898
1818
  "companion: active-run write queued to outbox after transient failure (will drain when the API returns)"
1899
1819
  );
1900
1820
  }
@@ -1993,13 +1913,13 @@ var CabaneApi = class {
1993
1913
  return res.messages.find((m) => m.id === messageId2) ?? null;
1994
1914
  }
1995
1915
  };
1996
- function isRetryable(err2) {
1997
- if (err2 instanceof ApiError) return err2.status >= 500;
1998
- if (isAbortError(err2)) return false;
1916
+ function isRetryable(err) {
1917
+ if (err instanceof ApiError) return err.status >= 500;
1918
+ if (isAbortError(err)) return false;
1999
1919
  return true;
2000
1920
  }
2001
- function isAbortError(err2) {
2002
- return err2 instanceof Error && err2.name === "AbortError";
1921
+ function isAbortError(err) {
1922
+ return err instanceof Error && err.name === "AbortError";
2003
1923
  }
2004
1924
  function sleep2(ms, signal) {
2005
1925
  return new Promise((resolve) => {
@@ -2017,8 +1937,8 @@ function sleep2(ms, signal) {
2017
1937
  }
2018
1938
  function errorMessage(status2, body) {
2019
1939
  if (body && typeof body === "object" && "error" in body) {
2020
- const err2 = body.error;
2021
- if (typeof err2 === "string") return `${status2} ${err2}`;
1940
+ const err = body.error;
1941
+ if (typeof err === "string") return `${status2} ${err}`;
2022
1942
  }
2023
1943
  if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
2024
1944
  return `${status2} error`;
@@ -2074,15 +1994,15 @@ var DeviceApi = class {
2074
1994
  };
2075
1995
  function errorMessage2(status2, body) {
2076
1996
  if (body && typeof body === "object" && "error" in body) {
2077
- const err2 = body.error;
2078
- if (typeof err2 === "string") return `${status2} ${err2}`;
1997
+ const err = body.error;
1998
+ if (typeof err === "string") return `${status2} ${err}`;
2079
1999
  }
2080
2000
  if (typeof body === "string" && body.length > 0) return `${status2} ${body.slice(0, 200)}`;
2081
2001
  return `${status2} error`;
2082
2002
  }
2083
2003
 
2084
2004
  // src/cursor.ts
2085
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2005
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2086
2006
  import { join as join7 } from "path";
2087
2007
  function pathFor(workspaceId) {
2088
2008
  return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
@@ -2090,7 +2010,7 @@ function pathFor(workspaceId) {
2090
2010
  function readCursor(workspaceId) {
2091
2011
  const path3 = pathFor(workspaceId);
2092
2012
  if (!existsSync5(path3)) return null;
2093
- const raw = readFileSync5(path3, "utf8").trim();
2013
+ const raw = readFileSync4(path3, "utf8").trim();
2094
2014
  return raw.length > 0 ? raw : null;
2095
2015
  }
2096
2016
  function writeCursor(workspaceId, eventId) {
@@ -2139,7 +2059,7 @@ var CursorTracker = class {
2139
2059
  };
2140
2060
 
2141
2061
  // src/dispatch-dedupe.ts
2142
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
2062
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
2143
2063
  import { join as join8 } from "path";
2144
2064
  var MAX_IDS = 256;
2145
2065
  function dir(log) {
@@ -2152,7 +2072,7 @@ function readIds(log, workspaceId) {
2152
2072
  const path3 = pathFor2(log, workspaceId);
2153
2073
  if (!existsSync6(path3)) return [];
2154
2074
  try {
2155
- return readFileSync6(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2075
+ return readFileSync5(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2156
2076
  } catch {
2157
2077
  return [];
2158
2078
  }
@@ -2192,7 +2112,7 @@ function readResumeCounts(workspaceId) {
2192
2112
  const path3 = resumePathFor(workspaceId);
2193
2113
  if (!existsSync6(path3)) return out;
2194
2114
  try {
2195
- for (const line of readFileSync6(path3, "utf8").split("\n")) {
2115
+ for (const line of readFileSync5(path3, "utf8").split("\n")) {
2196
2116
  const trimmed = line.trim();
2197
2117
  if (!trimmed) continue;
2198
2118
  const tab = trimmed.lastIndexOf(" ");
@@ -2228,44 +2148,44 @@ function noResume() {
2228
2148
  var TURN_PROTOCOL_VERSION = 1;
2229
2149
 
2230
2150
  // packages/agent-runtime/src/host-policy.ts
2231
- import { z as z5 } from "zod";
2232
- var hostPolicySchema = z5.object({
2151
+ import { z as z4 } from "zod";
2152
+ var hostPolicySchema = z4.object({
2233
2153
  // Host filesystem + shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the
2234
2154
  // notebook read/write pair, git-worktree tools, and the `REPL` host code-exec
2235
2155
  // tool. Off under the locked assistant surface (today's `DISALLOWED_TOOLS`);
2236
2156
  // on under `coding` mode.
2237
- hostFs: z5.boolean(),
2157
+ hostFs: z4.boolean(),
2238
2158
  // Public web (today's `WebSearch` / `WebFetch`, the `DEFAULT_WEB_TOOLS`). Public
2239
2159
  // web, not host reach — granted by default today, but expressible as a grant.
2240
- web: z5.boolean(),
2160
+ web: z4.boolean(),
2241
2161
  // Browser automation (the Playwright MCP surface). Varies by host: a companion has
2242
2162
  // it, the house executor does not (CT230).
2243
- browser: z5.boolean(),
2163
+ browser: z4.boolean(),
2244
2164
  // User-configured MCP servers permitted. False for the house executor
2245
2165
  // (CT227: Cabane agents run no user MCP servers), true for a personal companion.
2246
- userMcp: z5.boolean(),
2166
+ userMcp: z4.boolean(),
2247
2167
  // Subagent / sub-dispatch (`Task` / `Agent`). A per-host GRANT (see the CT261
2248
2168
  // amendment above): `false` on the locked assistant/house surface (banned via
2249
2169
  // `DISALLOWED_TOOLS`), `true` under coding / custom where the fan-out is left to
2250
2170
  // the coding allow-hook (which reaches sub-agent tool calls, CT680) or the user
2251
2171
  // allowlist. The subagent completes within the turn, so
2252
2172
  // it's not the turn-model invariant `scheduling` is.
2253
- subagents: z5.boolean(),
2173
+ subagents: z4.boolean(),
2254
2174
  // ── Hard platform invariants — always denied, never granted ────────────────
2255
2175
  // Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*`
2256
2176
  // families, `Monitor`, `Workflow`, …). A turn is one query that resolves when
2257
2177
  // `result` fires; a scheduled callback fires after the reply window has closed
2258
2178
  // and strands the agent (the CT155/CT156 rule).
2259
- scheduling: z5.literal("never"),
2179
+ scheduling: z4.literal("never"),
2260
2180
  // Human-facing UI prompts (`AskUserQuestion`). A Cabane conversation has no
2261
2181
  // handler to answer a structured prompt, so the call hangs the turn
2262
2182
  // (`UNSUPPORTED_TOOLS`). The agent asks in its reply instead.
2263
- uiPrompts: z5.literal("never")
2183
+ uiPrompts: z4.literal("never")
2264
2184
  });
2265
2185
 
2266
2186
  // packages/agent-runtime/src/turn-event.ts
2267
- import { z as z6 } from "zod";
2268
- var turnEventSchema = z6.discriminatedUnion("type", [
2187
+ import { z as z5 } from "zod";
2188
+ var turnEventSchema = z5.discriminatedUnion("type", [
2269
2189
  // The runtime's opaque session state, emitted when the adapter learns it (e.g.
2270
2190
  // the SDK `system/init` frame). The platform stores `state` verbatim per
2271
2191
  // (conversation, agent) and hands it back on the next turn; only the adapter
@@ -2285,19 +2205,19 @@ var turnEventSchema = z6.discriminatedUnion("type", [
2285
2205
  // on the companion, after the server committed the manifest). Runtime-neutral: a
2286
2206
  // plain boolean, not a runtime-specific reason string (that stays in the
2287
2207
  // 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()
2208
+ z5.object({
2209
+ type: z5.literal("session"),
2210
+ state: z5.string(),
2211
+ degraded: z5.boolean().optional()
2292
2212
  }),
2293
2213
  // One readable thinking summary. Maps `onThinking({ text })`. Transient —
2294
2214
  // surfaced live, never persisted as durable content.
2295
- z6.object({ type: z6.literal("thinking"), text: z6.string() }),
2215
+ z5.object({ type: z5.literal("thinking"), text: z5.string() }),
2296
2216
  // Assistant text. Maps `onAssistantText({ text, final })` — `text`→`body`,
2297
2217
  // `final`→`terminal`. `terminal: false` is interim narration (commits as a
2298
2218
  // `progress` row); `terminal: true` is the turn's closing reply (commits as
2299
2219
  // the `final` row).
2300
- z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
2220
+ z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
2301
2221
  // A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
2302
2222
  // `toolName`→`name` (already prefix-stripped: `cabane_read`, not
2303
2223
  // `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
@@ -2312,15 +2232,15 @@ var turnEventSchema = z6.discriminatedUnion("type", [
2312
2232
  // dropped the prefix; null for a host / built-in tool. The client tags Cabane
2313
2233
  // MCP calls (`=== 'cabane'`) with a subtle glyph. Nullable + optional so a
2314
2234
  // 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()
2235
+ z5.object({
2236
+ type: z5.literal("tool"),
2237
+ id: z5.string(),
2238
+ name: z5.string(),
2239
+ phase: z5.enum(["start", "done", "error"]),
2240
+ summary: z5.string(),
2241
+ input: z5.unknown().optional(),
2242
+ result: z5.unknown().optional(),
2243
+ mcpServer: z5.string().nullable().optional()
2324
2244
  }),
2325
2245
  // The turn's terminal outcome. Maps the `{ ok, reason }` both loops return
2326
2246
  // inline. `ok:false` carries a machine reason (`no_session`, an error code);
@@ -2364,34 +2284,34 @@ var turnEventSchema = z6.discriminatedUnion("type", [
2364
2284
  // `message_turn_metadata.resolved_model` / `resolved_config`. Optional ⇒
2365
2285
  // backward-compatible: an old adapter/companion omits them, a cancel has no result
2366
2286
  // 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()
2287
+ z5.object({
2288
+ type: z5.literal("result"),
2289
+ ok: z5.boolean(),
2290
+ reason: z5.string().optional(),
2291
+ usage: z5.object({
2292
+ inputTokens: z5.number(),
2293
+ outputTokens: z5.number(),
2294
+ cacheReadTokens: z5.number().optional(),
2295
+ cacheCreationTokens: z5.number().optional(),
2296
+ contextTokens: z5.number().optional(),
2297
+ contextWindow: z5.number().optional()
2378
2298
  }).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()
2299
+ resolvedModel: z5.string().optional(),
2300
+ resolvedConfig: z5.object({
2301
+ effort: z5.string().optional(),
2302
+ thinking: z5.string().optional(),
2303
+ reasoningEffort: z5.string().optional()
2384
2304
  }).optional()
2385
2305
  })
2386
2306
  ]);
2387
2307
 
2388
2308
  // 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") })
2309
+ import { z as z6 } from "zod";
2310
+ var turnFailureSchema = z6.discriminatedUnion("kind", [
2311
+ z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
2312
+ z6.object({ kind: z6.literal("rate_limited") }),
2313
+ z6.object({ kind: z6.literal("server_error") }),
2314
+ z6.object({ kind: z6.literal("auth_expired") })
2395
2315
  ]);
2396
2316
  var USAGE_CAPPED = "usage_capped";
2397
2317
  var RATE_LIMITED = "rate_limited";
@@ -2497,63 +2417,63 @@ var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
2497
2417
  var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
2498
2418
 
2499
2419
  // 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() })
2420
+ import { z as z7 } from "zod";
2421
+ var contentBlockSchema = z7.discriminatedUnion("type", [
2422
+ z7.object({ type: z7.literal("text"), text: z7.string() }),
2423
+ z7.object({
2424
+ type: z7.literal("image"),
2425
+ source: z7.object({ type: z7.literal("url"), url: z7.string() })
2506
2426
  }),
2507
- z8.object({
2508
- type: z8.literal("document"),
2509
- source: z8.object({ type: z8.literal("url"), url: z8.string() })
2427
+ z7.object({
2428
+ type: z7.literal("document"),
2429
+ source: z7.object({ type: z7.literal("url"), url: z7.string() })
2510
2430
  })
2511
2431
  ]);
2512
- var effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
2513
- var resolvedRunConfigSchema = z8.object({
2514
- model: z8.string().nullable(),
2432
+ var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
2433
+ var resolvedRunConfigSchema = z7.object({
2434
+ model: z7.string().nullable(),
2515
2435
  effort: effortLevelSchema.optional(),
2516
- runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
2436
+ runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
2517
2437
  });
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()
2438
+ var resolvedMcpServerSchema = z7.union([
2439
+ z7.object({
2440
+ type: z7.literal("stdio").optional(),
2441
+ command: z7.string(),
2442
+ args: z7.array(z7.string()).optional(),
2443
+ env: z7.record(z7.string(), z7.string()).optional()
2524
2444
  }),
2525
- z8.object({
2526
- type: z8.literal("http"),
2527
- url: z8.string(),
2528
- headers: z8.record(z8.string(), z8.string()).optional()
2445
+ z7.object({
2446
+ type: z7.literal("http"),
2447
+ url: z7.string(),
2448
+ headers: z7.record(z7.string(), z7.string()).optional()
2529
2449
  }),
2530
- z8.object({
2531
- type: z8.literal("sse"),
2532
- url: z8.string(),
2533
- headers: z8.record(z8.string(), z8.string()).optional()
2450
+ z7.object({
2451
+ type: z7.literal("sse"),
2452
+ url: z7.string(),
2453
+ headers: z7.record(z7.string(), z7.string()).optional()
2534
2454
  })
2535
2455
  ]);
2536
- var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
2537
- var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
2538
- var turnRequestSchema = z8.object({
2456
+ var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
2457
+ var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
2458
+ var turnRequestSchema = z7.object({
2539
2459
  // Server-composed system prompt (core + capability prose + adapter addendum +
2540
2460
  // charter). One string to the adapter.
2541
- systemPrompt: z8.string(),
2461
+ systemPrompt: z7.string(),
2542
2462
  // Server-composed per-turn user text (anchor reminder + the triggering message).
2543
- prompt: z8.string(),
2463
+ prompt: z7.string(),
2544
2464
  // The multi-block user-message body (text + vision).
2545
- content: z8.array(contentBlockSchema),
2465
+ content: z7.array(contentBlockSchema),
2546
2466
  // Portable-or-dialect run-config (above).
2547
2467
  config: resolvedRunConfigSchema,
2548
2468
  // Abstract capability grants; the adapter maps them to tool names.
2549
2469
  policy: hostPolicySchema,
2550
2470
  // Prior opaque session state, or null for a fresh session.
2551
- session: z8.string().nullable(),
2471
+ session: z7.string().nullable(),
2552
2472
  // 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(),
2473
+ cabane: z7.object({
2474
+ mcpUrl: z7.string(),
2475
+ bearer: z7.string(),
2476
+ activeConversationId: z7.string(),
2557
2477
  // CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
2558
2478
  // EXTERNAL adapters (Codex / opencode) mount it by URL under the key
2559
2479
  // `cabane_companion` — using the same `bearer` (the turn token) and the same
@@ -2563,7 +2483,7 @@ var turnRequestSchema = z8.object({
2563
2483
  // claude-code ignores it (it mounts the in-process instance instead), and
2564
2484
  // every existing `cabane`-block fixture keeps parsing unchanged; the
2565
2485
  // companion always populates it (`build-options.ts`).
2566
- turnControlUrl: z8.string().optional(),
2486
+ turnControlUrl: z7.string().optional(),
2567
2487
  // CT598: the workspace this turn runs in. The claude-code/opencode/codex
2568
2488
  // adapters never need it (they reach Cabane through the `cabane` MCP server,
2569
2489
  // which takes `workspaceId` as a per-tool arg the model supplies); the
@@ -2573,53 +2493,39 @@ var turnRequestSchema = z8.object({
2573
2493
  // adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
2574
2494
  // always populates it (`build-options.ts`), and the native adapter fails the
2575
2495
  // turn loudly when it is somehow absent rather than guessing.
2576
- workspaceId: z8.string().optional(),
2496
+ workspaceId: z7.string().optional(),
2577
2497
  // CT752: the server-resolved workspace surface this credential exposes.
2578
2498
  // Readiness uses this explicit fact to require `sdk` for code mode and the
2579
2499
  // granular floor for classic mode; inventory contents alone cannot infer it
2580
2500
  // because `sdk` is intentionally also available on the classic surface.
2581
- workspaceToolSurface: z8.enum(["code", "classic"]).optional()
2501
+ workspaceToolSurface: z7.enum(["code", "classic"]).optional()
2582
2502
  }),
2583
2503
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
2584
2504
  // 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(),
2505
+ local: z7.object({
2506
+ cwd: z7.string().optional(),
2507
+ env: z7.record(z7.string(), z7.string()).optional(),
2593
2508
  mcpServers: resolvedMcpServersSchema.optional(),
2594
2509
  // CT289: machine-local claude-code adapter knobs the operator sets on a
2595
2510
  // companion they run themselves — the auto-memory escape hatch. `autoMemory:
2596
2511
  // true` opts back into Claude Code's auto-memory (governed by the operator's
2597
2512
  // 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()
2513
+ // default in place (see `buildClaudeCodeOptions`).
2514
+ claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
2601
2515
  }),
2602
2516
  // 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()
2517
+ extra: z7.object({
2518
+ mcpServers: hostInjectedServersSchema
2613
2519
  })
2614
2520
  });
2615
2521
 
2616
2522
  // packages/agent-runtime/src/conformance.ts
2617
- import { z as z9 } from "zod";
2618
- var conformanceFixtureSchema = z9.object({
2619
- name: z9.string(),
2523
+ import { z as z8 } from "zod";
2524
+ var conformanceFixtureSchema = z8.object({
2525
+ name: z8.string(),
2620
2526
  request: turnRequestSchema,
2621
- nativeStream: z9.array(z9.unknown()),
2622
- expected: z9.array(turnEventSchema)
2527
+ nativeStream: z8.array(z8.unknown()),
2528
+ expected: z8.array(turnEventSchema)
2623
2529
  });
2624
2530
 
2625
2531
  // packages/agent-runtime/src/transcript.ts
@@ -2629,8 +2535,8 @@ function createTerminalTextBuffer() {
2629
2535
  async function safeEmit(emit, event, onError) {
2630
2536
  try {
2631
2537
  await emit(event);
2632
- } catch (err2) {
2633
- onError?.(err2, event.type);
2538
+ } catch (err) {
2539
+ onError?.(err, event.type);
2634
2540
  }
2635
2541
  }
2636
2542
  async function processAssistantMessage(msg, emit, pending, buffer, onError) {
@@ -2891,16 +2797,16 @@ var TurnPump = class {
2891
2797
  // minimal note. Skipped when cancelled or already final. The held-text flush
2892
2798
  // that precedes it is a classification concern, driven by the caller before
2893
2799
  // this runs.
2894
- async finalize(ok2) {
2895
- if (!ok2 || this.opts.signal.aborted || this.emittedFinal) return;
2800
+ async finalize(ok) {
2801
+ if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
2896
2802
  const body = this.lastProgressBody ?? this.opts.emptyFinalBody;
2897
2803
  const seq = this.opts.nextSeq();
2898
2804
  try {
2899
2805
  await this.opts.commit.commitMessage({ body, kind: "final", seq });
2900
2806
  this.emittedFinal = true;
2901
2807
  this.finalReplyBody = body;
2902
- } catch (err2) {
2903
- this.opts.onError?.(err2, "empty-final");
2808
+ } catch (err) {
2809
+ this.opts.onError?.(err, "empty-final");
2904
2810
  }
2905
2811
  }
2906
2812
  // Whether the turn has committed its `final` row — read by the host to decide
@@ -2924,7 +2830,7 @@ import {
2924
2830
  var CLAUDE_CODE_ADDENDUM = "";
2925
2831
 
2926
2832
  // packages/agent-runtime/src/claude-code/policy.ts
2927
- import { z as z10 } from "zod";
2833
+ import { z as z9 } from "zod";
2928
2834
  var HOST_FS_TOOLS = [
2929
2835
  // shell + local filesystem
2930
2836
  "Bash",
@@ -2974,30 +2880,20 @@ function withThinkingSummaries(thinking) {
2974
2880
  if (thinking.type === "disabled") return thinking;
2975
2881
  return { display: "summarized", ...thinking };
2976
2882
  }
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()
2883
+ var claudeCodeDialectSchema = z9.object({
2884
+ thinking: z9.discriminatedUnion("type", [
2885
+ z9.object({
2886
+ type: z9.literal("adaptive"),
2887
+ display: z9.enum(["summarized", "omitted"]).optional()
2982
2888
  }),
2983
- z10.object({
2984
- type: z10.literal("enabled"),
2985
- budgetTokens: z10.number().int().positive().optional(),
2986
- display: z10.enum(["summarized", "omitted"]).optional()
2889
+ z9.object({
2890
+ type: z9.literal("enabled"),
2891
+ budgetTokens: z9.number().int().positive().optional(),
2892
+ display: z9.enum(["summarized", "omitted"]).optional()
2987
2893
  }),
2988
- z10.object({ type: z10.literal("disabled") })
2894
+ z9.object({ type: z9.literal("disabled") })
2989
2895
  ]).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()
2896
+ hostAccess: z9.boolean().optional()
3001
2897
  }).loose();
3002
2898
  function readThinking(runtimeOptions) {
3003
2899
  const dialect = runtimeOptions?.["claude-code"];
@@ -3076,23 +2972,25 @@ function buildClaudeCodeOptions(req, augment) {
3076
2972
  };
3077
2973
  }
3078
2974
  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";
2975
+ const useCodingPreset = dialect.success ? dialect.data.hostAccess ?? false : false;
3082
2976
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
3083
2977
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
3084
2978
  const allowedTools = dedupe([
3085
2979
  cabaneGlob,
3086
2980
  ...extraServerGlobs,
3087
- ...policy.web ? DEFAULT_WEB_TOOLS : [],
3088
- ...customAllowed
2981
+ ...policy.web ? DEFAULT_WEB_TOOLS : []
3089
2982
  ]);
3090
- const disallowedTools = dedupe([...disallowedToolsFor(policy), ...customDisallowed]);
2983
+ const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
3091
2984
  const resumeDecision = decideResume(req.session, cwd);
3092
2985
  const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
3093
2986
  const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
3094
2987
  const devControlsAutoMemory = req.local.claudeCode?.autoMemory === true;
3095
2988
  const model = parseClaudeCodeModel(config.model);
2989
+ if (model === null) {
2990
+ console.warn(
2991
+ `[agent-runtime/claude-code] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 the SDK will fall back to its bundled default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
2992
+ );
2993
+ }
3096
2994
  const base = {
3097
2995
  // model / effort: `model` is pinned only when the config names a real one —
3098
2996
  // omitted for "let it choose" (see `parseClaudeCodeModel`), so the SDK picks
@@ -3153,7 +3051,7 @@ async function* decodeSdkStream(iter, ctx) {
3153
3051
  out.push(event);
3154
3052
  };
3155
3053
  let sessionEmitted = false;
3156
- let ok2 = false;
3054
+ let ok = false;
3157
3055
  let resultReason;
3158
3056
  let sawResult = false;
3159
3057
  let usage;
@@ -3199,8 +3097,8 @@ async function* decodeSdkStream(iter, ctx) {
3199
3097
  if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
3200
3098
  }
3201
3099
  } else if (msg.type === "auth_status") {
3202
- const err2 = msg.error;
3203
- if (typeof err2 === "string" && err2.length > 0) authError = err2;
3100
+ const err = msg.error;
3101
+ if (typeof err === "string" && err.length > 0) authError = err;
3204
3102
  } else if (msg.type === "result") {
3205
3103
  sawResult = true;
3206
3104
  usage = readSdkUsage(msg);
@@ -3212,7 +3110,7 @@ async function* decodeSdkStream(iter, ctx) {
3212
3110
  }
3213
3111
  const isError = msg.is_error === true;
3214
3112
  if (msg.subtype === "success" && !isError) {
3215
- ok2 = true;
3113
+ ok = true;
3216
3114
  } else {
3217
3115
  const resultText = msg.result ?? "";
3218
3116
  const terminalReason = msg.terminal_reason;
@@ -3226,26 +3124,26 @@ async function* decodeSdkStream(iter, ctx) {
3226
3124
  ...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
3227
3125
  } : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
3228
3126
  resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
3229
- ok2 = false;
3127
+ ok = false;
3230
3128
  }
3231
3129
  break;
3232
3130
  }
3233
3131
  }
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;
3132
+ } catch (err) {
3133
+ if (ctx.signal.aborted) throw err;
3134
+ const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
3135
+ if (!failure) throw err;
3136
+ ok = false;
3239
3137
  resultReason = encodeFailureReason(failure);
3240
3138
  sawResult = true;
3241
3139
  }
3242
3140
  if (ctx.signal.aborted) return;
3243
- await flushHeldText(buffer, emit, ok2);
3141
+ await flushHeldText(buffer, emit, ok);
3244
3142
  yield* drain(out);
3245
- if (!ok2 && !resultReason && !sawResult) resultReason = "no_result";
3143
+ if (!ok && !resultReason && !sawResult) resultReason = "no_result";
3246
3144
  yield {
3247
3145
  type: "result",
3248
- ok: ok2,
3146
+ ok,
3249
3147
  ...resultReason ? { reason: resultReason } : {},
3250
3148
  ...usage ? { usage } : {},
3251
3149
  ...resolvedModel ? { resolvedModel } : {}
@@ -3714,26 +3612,20 @@ function selectAdapter(registry, runtime) {
3714
3612
 
3715
3613
  // packages/agent-runtime/src/opencode/addendum.ts
3716
3614
  var OPENCODE_ADDENDUM = [
3717
- "Your Cabane tools have plain names \u2014 `read`, `write`, `search`, `edit`,",
3718
- "`post_message`, and so on; the host tools are plain verbs too (`bash`, `read`,",
3719
- "`edit`). If a tool appears in this prompt with an `mcp__\u2026__` prefix, that",
3615
+ "Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
3616
+ "workspace by writing a TypeScript program and calling `sdk` with it); the",
3617
+ "turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
3618
+ "plain-named too, as are the few ancillary cabane tools (`list_workspaces`,",
3619
+ "`read_binary`, `upload`, `begin_upload`, `finalize_upload`,",
3620
+ "`mint_render_token`), and the host tools are plain verbs (`bash`, `read`,",
3621
+ "`edit`). There is no `write`/`search`/`edit` CABANE tool here \u2014 those are",
3622
+ "`cabane` SDK calls inside your program, not tools (a bare `read`/`edit` is the",
3623
+ "HOST tool). If a tool appears in this prompt with an `mcp__\u2026__` prefix, that",
3720
3624
  "prefix is not part of its name \u2014 call the tool by its plain verb. Write your",
3721
3625
  "closing reply as the last thing you say in the turn: you can interleave",
3722
3626
  "narration with tool calls, but only your final message is recorded as the",
3723
3627
  "turn\u2019s reply."
3724
3628
  ].join(" ");
3725
- var OPENCODE_ADDENDUM_CODE_MODE = [
3726
- "Your one Cabane workspace tool has a plain name \u2014 `sdk` (you act on the",
3727
- "workspace by writing a TypeScript program and calling `sdk` with it); the",
3728
- "turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
3729
- "plain-named too, and the host tools are plain verbs (`bash`, `read`, `edit`).",
3730
- "There is no `write`/`search`/`edit` CABANE tool here \u2014 those are `cabane` SDK",
3731
- "calls inside your program, not tools (a bare `read`/`edit` is the HOST tool). If",
3732
- "a tool appears in this prompt with an `mcp__\u2026__` prefix, that prefix is not part",
3733
- "of its name \u2014 call the tool by its plain verb. Write your closing reply as the",
3734
- "last thing you say in the turn: you can interleave narration with tool calls, but",
3735
- "only your final message is recorded as the turn\u2019s reply."
3736
- ].join(" ");
3737
3629
 
3738
3630
  // packages/agent-runtime/src/opencode/events.ts
3739
3631
  function asRecord(v) {
@@ -3808,9 +3700,9 @@ function readSessionId(properties) {
3808
3700
  }
3809
3701
  function readSessionError(properties) {
3810
3702
  const props = asRecord(properties);
3811
- const err2 = props?.error;
3812
- if (typeof err2 === "string") return err2;
3813
- const rec2 = asRecord(err2);
3703
+ const err = props?.error;
3704
+ if (typeof err === "string") return err;
3705
+ const rec2 = asRecord(err);
3814
3706
  if (!rec2) return "unknown";
3815
3707
  const { name, message } = deepestError(rec2);
3816
3708
  if (message && name && !isGenericErrorName(name)) return `${name}: ${message}`;
@@ -3846,7 +3738,7 @@ async function* decodeOpencodeStream(events, ctx) {
3846
3738
  const pending = /* @__PURE__ */ new Map();
3847
3739
  const startedTools = /* @__PURE__ */ new Set();
3848
3740
  const finishedTools = /* @__PURE__ */ new Set();
3849
- let ok2 = false;
3741
+ let ok = false;
3850
3742
  let reason;
3851
3743
  let settled = false;
3852
3744
  const userMessageIds = /* @__PURE__ */ new Set();
@@ -3909,7 +3801,7 @@ async function* decodeOpencodeStream(events, ctx) {
3909
3801
  const sealed = sealHeld(held, true);
3910
3802
  held = null;
3911
3803
  if (sealed) yield sealed;
3912
- ok2 = true;
3804
+ ok = true;
3913
3805
  settled = true;
3914
3806
  break;
3915
3807
  } else if (ev.type === "session.error") {
@@ -3918,7 +3810,7 @@ async function* decodeOpencodeStream(events, ctx) {
3918
3810
  const sealed = sealHeld(held, false);
3919
3811
  held = null;
3920
3812
  if (sealed) yield sealed;
3921
- ok2 = false;
3813
+ ok = false;
3922
3814
  const errorText = readSessionError(ev.properties);
3923
3815
  const failure = classifyErrorText(errorText);
3924
3816
  reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
@@ -3933,7 +3825,7 @@ async function* decodeOpencodeStream(events, ctx) {
3933
3825
  if (sealed) yield sealed;
3934
3826
  reason = "no_terminal";
3935
3827
  }
3936
- yield { type: "result", ok: ok2, ...reason ? { reason } : {} };
3828
+ yield { type: "result", ok, ...reason ? { reason } : {} };
3937
3829
  }
3938
3830
  function hasToolInput(input) {
3939
3831
  return !!input && typeof input === "object" && Object.keys(input).length > 0;
@@ -3947,7 +3839,7 @@ function sealHeld(held, terminal) {
3947
3839
  }
3948
3840
 
3949
3841
  // packages/agent-runtime/src/opencode/policy.ts
3950
- import { z as z11 } from "zod";
3842
+ import { z as z10 } from "zod";
3951
3843
  var OPENCODE_HOST_TOOLS = [
3952
3844
  "bash",
3953
3845
  "edit",
@@ -3974,8 +3866,8 @@ function opencodeToolPolicy(policy) {
3974
3866
  deny(OPENCODE_UI_PROMPT_TOOLS);
3975
3867
  return { tools, allowAllHostTools: policy.hostFs };
3976
3868
  }
3977
- var opencodeDialectSchema = z11.object({
3978
- agent: z11.string().min(1).optional()
3869
+ var opencodeDialectSchema = z10.object({
3870
+ agent: z10.string().min(1).optional()
3979
3871
  }).loose();
3980
3872
  function readOpencodeDialect(runtimeOptions) {
3981
3873
  const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
@@ -4196,9 +4088,9 @@ function createHttpOpencodeTransport(opts) {
4196
4088
  // The lock is released when this stream finishes draining.
4197
4089
  events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
4198
4090
  };
4199
- } catch (err2) {
4091
+ } catch (err) {
4200
4092
  release();
4201
- throw err2;
4093
+ throw err;
4202
4094
  }
4203
4095
  }
4204
4096
  };
@@ -4285,7 +4177,7 @@ function createOpencodeAdapter(deps = {}) {
4285
4177
  name: "opencode",
4286
4178
  // CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
4287
4179
  // `code`, not the granular cabane names it no longer has.
4288
- promptAddendum: (codeMode = false) => codeMode ? OPENCODE_ADDENDUM_CODE_MODE : OPENCODE_ADDENDUM,
4180
+ promptAddendum: () => OPENCODE_ADDENDUM,
4289
4181
  dialectSchema: opencodeDialectSchema,
4290
4182
  async *runTurn(req, signal) {
4291
4183
  if (!transport) {
@@ -4685,14 +4577,6 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
4685
4577
 
4686
4578
  // packages/agent-runtime/src/codex/addendum.ts
4687
4579
  var CODEX_ADDENDUM = [
4688
- "Your Cabane tools have plain names \u2014 `read`, `write`, `search`, `edit`,",
4689
- "`post_message`, and so on. If a tool appears in this prompt with an `mcp__\u2026__`",
4690
- "prefix, that prefix is not part of its name \u2014 call the tool by its plain verb.",
4691
- "Write your closing reply as the last thing you say in the turn: you can",
4692
- "interleave narration with tool calls, but only your final message is recorded",
4693
- "as the turn\u2019s reply."
4694
- ].join(" ");
4695
- var CODEX_ADDENDUM_CODE_MODE = [
4696
4580
  "Your Cabane workspace tool is exposed to Codex as the qualified MCP tool",
4697
4581
  "`mcp__cabane__sdk`. On Codex versions that defer MCP tools, locate it in the",
4698
4582
  "`functions.exec` deferred-tool inventory and invoke that exact qualified tool",
@@ -4701,13 +4585,14 @@ var CODEX_ADDENDUM_CODE_MODE = [
4701
4585
  "declare the SDK absent without attempting discovery and invocation. The SDK",
4702
4586
  "call runs a TypeScript program against the ambient `cabane` object. The",
4703
4587
  "turn-control verbs (`ask`, `wake_me`, `summon_agent`, `skip_turn`) are",
4704
- "qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too). There is",
4705
- "no Cabane `read`/`write`/`search`/`edit` tool here \u2014 those",
4706
- "are `cabane` SDK calls inside your program, not tools. If a tool appears in this",
4707
- "prompt with an `mcp__\u2026__` prefix, preserve that qualified name. Write your",
4708
- "closing reply as the last thing you say in the",
4709
- "turn: you can interleave narration with tool calls, but only your final message",
4710
- "is recorded as the turn\u2019s reply."
4588
+ "qualified `mcp__cabane_companion__\u2026` tools (and may be deferred too), as are",
4589
+ "the few ancillary cabane tools (`mcp__cabane__list_workspaces`,",
4590
+ "`mcp__cabane__read_binary`, the upload tools, `mcp__cabane__mint_render_token`).",
4591
+ "There is no Cabane `read`/`write`/`search`/`edit` tool here \u2014 those are `cabane`",
4592
+ "SDK calls inside your program, not tools. If a tool appears in this prompt with",
4593
+ "an `mcp__\u2026__` prefix, preserve that qualified name. Write your closing reply as",
4594
+ "the last thing you say in the turn: you can interleave narration with tool",
4595
+ "calls, but only your final message is recorded as the turn\u2019s reply."
4711
4596
  ].join(" ");
4712
4597
 
4713
4598
  // packages/agent-runtime/src/codex/events.ts
@@ -4791,9 +4676,9 @@ function readItemType(item) {
4791
4676
  function readErrorMessage(ev) {
4792
4677
  const direct = str(ev.message);
4793
4678
  if (direct) return direct;
4794
- const err2 = asRecord2(ev.error);
4795
- if (err2) {
4796
- const m = str(err2.message);
4679
+ const err = asRecord2(ev.error);
4680
+ if (err) {
4681
+ const m = str(err.message);
4797
4682
  if (m) return m;
4798
4683
  }
4799
4684
  return "unknown";
@@ -4849,7 +4734,7 @@ async function* decodeCodexStream(events, ctx) {
4849
4734
  const startedTools = /* @__PURE__ */ new Set();
4850
4735
  const finishedTools = /* @__PURE__ */ new Set();
4851
4736
  let sessionEmitted = false;
4852
- let ok2 = false;
4737
+ let ok = false;
4853
4738
  let reason;
4854
4739
  let usage;
4855
4740
  let settled = false;
@@ -4880,7 +4765,7 @@ async function* decodeCodexStream(events, ctx) {
4880
4765
  const message = readItemMessage(item);
4881
4766
  if (isModelMetadataError(message)) {
4882
4767
  yield* flushInterim();
4883
- ok2 = false;
4768
+ ok = false;
4884
4769
  reason = `model_unavailable:${message.slice(0, 200)}`;
4885
4770
  settled = true;
4886
4771
  break;
@@ -4935,13 +4820,13 @@ async function* decodeCodexStream(events, ctx) {
4935
4820
  held = null;
4936
4821
  if (sealed) yield sealed;
4937
4822
  usage = readUsage(ev);
4938
- ok2 = true;
4823
+ ok = true;
4939
4824
  settled = true;
4940
4825
  break;
4941
4826
  }
4942
4827
  if (ev.type === "turn.failed" || ev.type === "error") {
4943
4828
  yield* flushInterim();
4944
- ok2 = false;
4829
+ ok = false;
4945
4830
  const text = readErrorMessage(ev);
4946
4831
  const failure = classifyErrorText(text);
4947
4832
  reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
@@ -4959,7 +4844,7 @@ async function* decodeCodexStream(events, ctx) {
4959
4844
  const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
4960
4845
  yield {
4961
4846
  type: "result",
4962
- ok: ok2,
4847
+ ok,
4963
4848
  ...reason ? { reason } : {},
4964
4849
  ...usage ? { usage } : {},
4965
4850
  ...ctx.resolvedModel ? { resolvedModel: ctx.resolvedModel } : {},
@@ -4977,7 +4862,7 @@ function sealHeld2(held, terminal) {
4977
4862
  }
4978
4863
 
4979
4864
  // packages/agent-runtime/src/codex/policy.ts
4980
- import { z as z12 } from "zod";
4865
+ import { z as z11 } from "zod";
4981
4866
  function codexToolPolicy(policy) {
4982
4867
  return policy.hostFs ? {
4983
4868
  permissionProfile: "cabane-coding",
@@ -4992,8 +4877,8 @@ function codexToolPolicy(policy) {
4992
4877
  };
4993
4878
  }
4994
4879
  var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
4995
- var codexDialectSchema = z12.object({
4996
- modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
4880
+ var codexDialectSchema = z11.object({
4881
+ modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
4997
4882
  }).loose();
4998
4883
  function readCodexDialect(runtimeOptions) {
4999
4884
  const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
@@ -5001,7 +4886,7 @@ function readCodexDialect(runtimeOptions) {
5001
4886
  }
5002
4887
 
5003
4888
  // packages/agent-runtime/src/codex/model.ts
5004
- var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default", "codex"]);
4889
+ var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
5005
4890
  function parseCodexModel(model) {
5006
4891
  const sep = model.indexOf("/");
5007
4892
  const bare = sep === -1 || model.slice(0, sep) !== "openai" ? model : model.slice(sep + 1);
@@ -5016,10 +4901,16 @@ function buildRunSpec2(req, resumeThreadId) {
5016
4901
  const { policy, config } = req;
5017
4902
  const directory = req.local.cwd ?? "";
5018
4903
  const dialect = readCodexDialect(config.runtimeOptions);
4904
+ const model = config.model ? parseCodexModel(config.model) : null;
4905
+ if (model === null) {
4906
+ console.warn(
4907
+ `[agent-runtime/codex] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 Codex will fall back to its own default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
4908
+ );
4909
+ }
5019
4910
  return {
5020
4911
  resumeThreadId,
5021
4912
  directory,
5022
- model: config.model ? parseCodexModel(config.model) : null,
4913
+ model,
5023
4914
  policy: codexToolPolicy(policy),
5024
4915
  skipGitRepoCheck: true,
5025
4916
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
@@ -5088,9 +4979,11 @@ function buildConfig(req) {
5088
4979
  };
5089
4980
  }
5090
4981
  const policy = codexToolPolicy(req.policy);
4982
+ const tmpDir = req.local.env?.TMPDIR;
5091
4983
  return {
5092
4984
  mcp_servers,
5093
4985
  experimental_use_rmcp_client: true,
4986
+ ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
5094
4987
  ...policy.permissionProfile ? {
5095
4988
  // CT733: named permission profiles are Codex's split-filesystem path.
5096
4989
  // `:root = read` preserves coding-mode host reads; the one explicit
@@ -5368,7 +5261,7 @@ var CodexExec = class {
5368
5261
  signal: args.signal
5369
5262
  });
5370
5263
  let spawnError = null;
5371
- child.once("error", (err2) => spawnError = err2);
5264
+ child.once("error", (err) => spawnError = err);
5372
5265
  if (!child.stdin) {
5373
5266
  child.kill();
5374
5267
  throw new Error("Child process has no stdin");
@@ -5685,7 +5578,7 @@ function createCodexAdapter(deps = {}) {
5685
5578
  name: "codex",
5686
5579
  // CT614: surface-aware — a code-mode turn (only `code` mounted) is taught
5687
5580
  // `code`, not the granular names it no longer has.
5688
- promptAddendum: (codeMode = false) => codeMode ? CODEX_ADDENDUM_CODE_MODE : CODEX_ADDENDUM,
5581
+ promptAddendum: () => CODEX_ADDENDUM,
5689
5582
  dialectSchema: codexDialectSchema,
5690
5583
  async *runTurn(req, signal) {
5691
5584
  if (!transport) {
@@ -6272,1029 +6165,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
6272
6165
  }
6273
6166
  ];
6274
6167
 
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
- // packages/agent-runtime/src/claude-code/sdk.ts
7297
- import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
6168
+ // packages/agent-runtime/src/claude-code/sdk.ts
6169
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
7298
6170
 
7299
6171
  // src/connector-health.ts
7300
6172
  var ConnectorHealthStore = class {
@@ -7350,7 +6222,7 @@ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync
7350
6222
  import { join as join12 } from "path";
7351
6223
 
7352
6224
  // src/summon.ts
7353
- import { z as z14 } from "zod";
6225
+ import { z as z12 } from "zod";
7354
6226
  var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
7355
6227
  var SUMMON_AGENT_TOOL = "summon_agent";
7356
6228
  var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
@@ -7384,7 +6256,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7384
6256
  SUMMON_AGENT_TOOL,
7385
6257
  "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
6258
  {
7387
- agentId: z14.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
6259
+ agentId: z12.string().uuid().describe("The peer agent to summon \u2014 a workspace agent id from `list_agents`.")
7388
6260
  },
7389
6261
  async (args) => {
7390
6262
  summonState.agentId = args.agentId;
@@ -7399,7 +6271,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7399
6271
  SKIP_TURN_TOOL,
7400
6272
  `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
6273
  {
7402
- reason: z14.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
6274
+ reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
7403
6275
  },
7404
6276
  async (args) => {
7405
6277
  skipState.skipped = true;
@@ -7416,21 +6288,21 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7416
6288
  ASK_TOOL,
7417
6289
  "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
6290
  {
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(
6291
+ targetUserId: z12.string().uuid().describe("The workspace member (human) to ask \u2014 a user id from `list_members`."),
6292
+ question: z12.string().min(1).max(400).optional().describe(
7421
6293
  "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
6294
  ),
7423
- headline: z14.string().min(1).max(120).optional().describe(
6295
+ headline: z12.string().min(1).max(120).optional().describe(
7424
6296
  '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
6297
  ),
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(
6298
+ 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."),
6299
+ questions: z12.array(
6300
+ z12.object({
6301
+ headline: z12.string().min(1).max(120).describe(
7430
6302
  'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
7431
6303
  ),
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.")
6304
+ body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
6305
+ 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
6306
  })
7435
6307
  ).min(1).max(5).optional().describe(
7436
6308
  "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 +6359,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7487
6359
  SUB_AGENT_TOOL,
7488
6360
  "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
6361
  {
7490
- prompt: z14.string().min(1).max(65536).describe(
6362
+ prompt: z12.string().min(1).max(65536).describe(
7491
6363
  "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
6364
  ),
7493
- agentId: z14.string().uuid().optional().describe(
6365
+ agentId: z12.string().uuid().optional().describe(
7494
6366
  "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
6367
  ),
7496
- title: z14.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6368
+ title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
7497
6369
  },
7498
6370
  async (args) => {
7499
6371
  const result = await subAgentCreate(args);
@@ -7523,13 +6395,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
7523
6395
  WAKE_ME_TOOL,
7524
6396
  '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
6397
  {
7526
- afterSeconds: z14.number().int().positive().optional().describe(
6398
+ afterSeconds: z12.number().int().positive().optional().describe(
7527
6399
  "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
6400
  ),
7529
- at: z14.string().datetime({ offset: true }).optional().describe(
6401
+ at: z12.string().datetime({ offset: true }).optional().describe(
7530
6402
  "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
6403
  ),
7532
- note: z14.string().min(1).max(2e3).describe(
6404
+ note: z12.string().min(1).max(2e3).describe(
7533
6405
  '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
6406
  )
7535
6407
  },
@@ -7609,7 +6481,6 @@ function buildCompanionTurnRequest(params) {
7609
6481
  local: {
7610
6482
  ...params.cwd ? { cwd: params.cwd } : {},
7611
6483
  ...params.env ? { env: params.env } : {},
7612
- ...params.nativeWorkAssignment ? { nativeWorkAssignment: params.nativeWorkAssignment } : {},
7613
6484
  // User MCP servers (already `${PLACEHOLDER}`-resolved). Structurally the
7614
6485
  // adapter's `ResolvedMcpServers`.
7615
6486
  ...Object.keys(params.mcpServers).length > 0 ? { mcpServers: params.mcpServers } : {},
@@ -7617,10 +6488,9 @@ function buildCompanionTurnRequest(params) {
7617
6488
  ...params.claudeCode ? { claudeCode: params.claudeCode } : {}
7618
6489
  },
7619
6490
  // Host-injected: the companion-local summon server (for the subprocess adapters,
7620
- // under its own namespace) + the cabane-native turn-control handler (CT666).
6491
+ // under its own namespace).
7621
6492
  extra: {
7622
- mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer },
7623
- turnControl: params.turnControl
6493
+ mcpServers: { [COMPANION_LOCAL_MCP_SERVER]: params.summonServer }
7624
6494
  }
7625
6495
  };
7626
6496
  }
@@ -7629,7 +6499,7 @@ function trimSlash3(s) {
7629
6499
  }
7630
6500
 
7631
6501
  // src/prepared.ts
7632
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6502
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
7633
6503
  import { join as join9 } from "path";
7634
6504
  function dirFor(workspaceId) {
7635
6505
  return join9(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
@@ -7637,23 +6507,18 @@ function dirFor(workspaceId) {
7637
6507
  function conversationDir(workspaceId, conversationId) {
7638
6508
  return join9(dirFor(workspaceId), encodeURIComponent(conversationId));
7639
6509
  }
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
- );
6510
+ function pathFor3(workspaceId, conversationId, agentId) {
6511
+ return join9(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
7646
6512
  }
7647
- function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
7648
- const path3 = pathFor3(workspaceId, conversationId, agentId, assignmentKey);
6513
+ function readPrepared(workspaceId, conversationId, agentId) {
6514
+ const path3 = pathFor3(workspaceId, conversationId, agentId);
7649
6515
  if (!existsSync7(path3)) return null;
7650
6516
  try {
7651
- const parsed = JSON.parse(readFileSync7(path3, "utf8"));
6517
+ const parsed = JSON.parse(readFileSync6(path3, "utf8"));
7652
6518
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
7653
6519
  return {
7654
6520
  cwd: parsed.cwd,
7655
- ...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {},
7656
- ...parsed.nativeWorkAssignment && typeof parsed.nativeWorkAssignment === "object" ? { nativeWorkAssignment: parsed.nativeWorkAssignment } : {}
6521
+ ...parsed.env && typeof parsed.env === "object" ? { env: parsed.env } : {}
7657
6522
  };
7658
6523
  }
7659
6524
  return null;
@@ -7661,42 +6526,42 @@ function readPrepared(workspaceId, conversationId, agentId, assignmentKey) {
7661
6526
  return null;
7662
6527
  }
7663
6528
  }
7664
- function writePrepared(workspaceId, conversationId, agentId, result, assignmentKey) {
6529
+ function writePrepared(workspaceId, conversationId, agentId, result) {
7665
6530
  mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
7666
6531
  writeFileSync6(
7667
- pathFor3(workspaceId, conversationId, agentId, assignmentKey),
6532
+ pathFor3(workspaceId, conversationId, agentId),
7668
6533
  JSON.stringify(result) + "\n",
7669
6534
  "utf8"
7670
6535
  );
7671
6536
  }
7672
6537
 
7673
6538
  // src/secrets.ts
7674
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
6539
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
7675
6540
  import { join as join10 } from "path";
7676
- import { z as z15 } from "zod";
6541
+ import { z as z13 } from "zod";
7677
6542
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
7678
6543
  function secretsPath() {
7679
6544
  return join10(cabaneDir(), "secrets.json");
7680
6545
  }
7681
- var secretStoreSchema = z15.record(z15.string(), z15.string());
6546
+ var secretStoreSchema = z13.record(z13.string(), z13.string());
7682
6547
  function loadSecretStore() {
7683
6548
  const path3 = secretsPath();
7684
6549
  if (!existsSync8(path3)) return makeStore({});
7685
6550
  let raw;
7686
6551
  try {
7687
- raw = readFileSync8(path3, "utf8");
7688
- } catch (err2) {
6552
+ raw = readFileSync7(path3, "utf8");
6553
+ } catch (err) {
7689
6554
  throw new ConfigError(
7690
- `couldn't read ${path3}: ${err2 instanceof Error ? err2.message : String(err2)}`
6555
+ `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
7691
6556
  );
7692
6557
  }
7693
6558
  if (raw.trim().length === 0) return makeStore({});
7694
6559
  let parsed;
7695
6560
  try {
7696
6561
  parsed = JSON.parse(raw);
7697
- } catch (err2) {
6562
+ } catch (err) {
7698
6563
  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.`
6564
+ `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
7700
6565
  );
7701
6566
  }
7702
6567
  const result = secretStoreSchema.safeParse(parsed);
@@ -7710,8 +6575,8 @@ function loadSecretStore() {
7710
6575
  function loadSecretStoreTolerant(onWarn) {
7711
6576
  try {
7712
6577
  return loadSecretStore();
7713
- } catch (err2) {
7714
- onWarn?.(err2 instanceof Error ? err2.message : String(err2));
6578
+ } catch (err) {
6579
+ onWarn?.(err instanceof Error ? err.message : String(err));
7715
6580
  return makeStore({});
7716
6581
  }
7717
6582
  }
@@ -7782,8 +6647,8 @@ var TranscriptWriter = class {
7782
6647
  } catch {
7783
6648
  }
7784
6649
  pruneOld(dir2, RETAIN);
7785
- } catch (err2) {
7786
- this.fail(err2);
6650
+ } catch (err) {
6651
+ this.fail(err);
7787
6652
  }
7788
6653
  this.line({ type: "_meta", ...meta });
7789
6654
  }
@@ -7799,14 +6664,14 @@ var TranscriptWriter = class {
7799
6664
  if (this.broken) return;
7800
6665
  try {
7801
6666
  appendFileSync(this.path, JSON.stringify(obj) + "\n", { mode: 384 });
7802
- } catch (err2) {
7803
- this.fail(err2);
6667
+ } catch (err) {
6668
+ this.fail(err);
7804
6669
  }
7805
6670
  }
7806
- fail(err2) {
6671
+ fail(err) {
7807
6672
  if (this.broken) return;
7808
6673
  this.broken = true;
7809
- this.onWarn?.(`transcript write failed (${err2 instanceof Error ? err2.message : String(err2)})`);
6674
+ this.onWarn?.(`transcript write failed (${err instanceof Error ? err.message : String(err)})`);
7810
6675
  }
7811
6676
  };
7812
6677
  function fileName(meta) {
@@ -7844,9 +6709,9 @@ var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
7844
6709
  var TurnCommitter = class {
7845
6710
  constructor(deps) {
7846
6711
  this.deps = deps;
7847
- this.onError = (err2, hook) => {
6712
+ this.onError = (err, hook) => {
7848
6713
  deps.log.warn(
7849
- { err: err2 instanceof Error ? err2.message : String(err2), hook },
6714
+ { err: err instanceof Error ? err.message : String(err), hook },
7850
6715
  "dispatcher: transcript callback failed"
7851
6716
  );
7852
6717
  };
@@ -7911,9 +6776,9 @@ var TurnCommitter = class {
7911
6776
  signal: deps.signal,
7912
6777
  nextSeq: deps.nextSeq,
7913
6778
  emptyFinalBody: EMPTY_FINAL_BODY,
7914
- onError: (err2) => {
6779
+ onError: (err) => {
7915
6780
  deps.log.warn(
7916
- { err: err2 instanceof Error ? err2.message : String(err2) },
6781
+ { err: err instanceof Error ? err.message : String(err) },
7917
6782
  "dispatcher: empty-final commit failed"
7918
6783
  );
7919
6784
  }
@@ -7936,8 +6801,8 @@ var TurnCommitter = class {
7936
6801
  if (event.type === "session" || event.type === "result") return;
7937
6802
  try {
7938
6803
  await this.emit(event);
7939
- } catch (err2) {
7940
- this.onError(err2, event.type);
6804
+ } catch (err) {
6805
+ this.onError(err, event.type);
7941
6806
  }
7942
6807
  }
7943
6808
  // End-of-turn empty-final promotion. The held-text flush is now the adapter's
@@ -8133,10 +6998,27 @@ var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't disp
8133
6998
  var SKIPPED_MARKER_BODY = "(skipped)";
8134
6999
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
8135
7000
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
8136
- var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 45 * 6e4;
7001
+ var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
8137
7002
  function runKey(conversationId, agentId) {
8138
7003
  return `${conversationId}|${agentId}`;
8139
7004
  }
7005
+ function describeSubAgentError(status2, body) {
7006
+ const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
7007
+ switch (code) {
7008
+ case "callout_cap_exceeded":
7009
+ 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.";
7010
+ case "callout_depth_exceeded":
7011
+ return "sub_agent: this would nest sub-agents too deep (max 3 levels). Have the current worker report back rather than spawning another layer.";
7012
+ case "dispatch_agent_not_found":
7013
+ return "sub_agent: no live agent in this workspace matches that `agentId`. Check `list_agents`, or omit `agentId` to spawn yourself.";
7014
+ case "dispatch_return_requires_turn":
7015
+ case "dispatch_return_requires_agent":
7016
+ case "dispatch_return_requires_dispatch":
7017
+ 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.`;
7018
+ default:
7019
+ return `sub_agent: the spawn failed (${code ?? `HTTP ${status2}`}).`;
7020
+ }
7021
+ }
8140
7022
  var Dispatcher = class {
8141
7023
  constructor(opts) {
8142
7024
  this.opts = opts;
@@ -8163,7 +7045,7 @@ var Dispatcher = class {
8163
7045
  // clear it — the SJ383 `finally` after the SDK loop is the one clear, and
8164
7046
  // every pre-run exit returns before reaching it. So each pre-run failure has
8165
7047
  // to clear `active_run_started_at` itself, mirroring that `finally`, or the
8166
- // indicator strands until the 90-min age sweep.
7048
+ // indicator strands until the 12h age sweep.
8167
7049
  //
8168
7050
  // `errorReason` controls the server's duplicate-notice rule (the active-run
8169
7051
  // PATCH handler in conversations.ts): a clear carrying `errorReason` makes the
@@ -8183,9 +7065,9 @@ var Dispatcher = class {
8183
7065
  payload.agentId,
8184
7066
  body
8185
7067
  );
8186
- } catch (err2) {
7068
+ } catch (err) {
8187
7069
  turnLog.warn(
8188
- { err: err2 instanceof Error ? err2.message : String(err2) },
7070
+ { err: err instanceof Error ? err.message : String(err) },
8189
7071
  "dispatcher: pre-run active-run clear failed terminally; server age-sweep is the backstop"
8190
7072
  );
8191
7073
  }
@@ -8211,16 +7093,16 @@ var Dispatcher = class {
8211
7093
  payload.messageId,
8212
7094
  turnId
8213
7095
  );
8214
- } catch (err2) {
8215
- const status2 = err2 instanceof ApiError ? err2.status : 0;
7096
+ } catch (err) {
7097
+ const status2 = err instanceof ApiError ? err.status : 0;
8216
7098
  if (status2 === 404) {
8217
7099
  turnLog.warn(
8218
- { err: err2 instanceof Error ? err2.message : String(err2) },
7100
+ { err: err instanceof Error ? err.message : String(err) },
8219
7101
  "dispatcher: turn-context 404 (stale event \u2014 conversation/message/participant gone); skipping"
8220
7102
  );
8221
7103
  return this.concludeBeforeRun(payload, turnLog, startedAt, "turn_context_not_found");
8222
7104
  }
8223
- const reason = err2 instanceof Error ? err2.message : String(err2);
7105
+ const reason = err instanceof Error ? err.message : String(err);
8224
7106
  turnLog.error({ err: reason }, "dispatcher: failed to fetch turn context");
8225
7107
  const fetchReason = `fetch_failed: ${reason}`;
8226
7108
  return this.concludeBeforeRun(payload, turnLog, startedAt, fetchReason, fetchReason);
@@ -8283,20 +7165,11 @@ var Dispatcher = class {
8283
7165
  effectiveCwd = void 0;
8284
7166
  }
8285
7167
  let hookEnv;
8286
- let preparedNativeAssignment;
8287
7168
  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
- );
7169
+ const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
8296
7170
  if (cached2) {
8297
7171
  effectiveCwd = cached2.cwd;
8298
7172
  hookEnv = cached2.env;
8299
- preparedNativeAssignment = cached2.nativeWorkAssignment;
8300
7173
  } else {
8301
7174
  const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
8302
7175
  let preparingStarted = false;
@@ -8311,9 +7184,9 @@ var Dispatcher = class {
8311
7184
  summary: "",
8312
7185
  phase,
8313
7186
  seq
8314
- }).catch((err2) => {
7187
+ }).catch((err) => {
8315
7188
  turnLog.warn(
8316
- { err: err2 instanceof Error ? err2.message : String(err2), phase },
7189
+ { err: err instanceof Error ? err.message : String(err), phase },
8317
7190
  "dispatcher: preparing-activity report failed (continuing with the hook)"
8318
7191
  );
8319
7192
  });
@@ -8335,25 +7208,17 @@ var Dispatcher = class {
8335
7208
  // tasker prepare hook keys its per-task env off. Defaults to `[]` for
8336
7209
  // an older API. The conversation anchor is gone (CT319).
8337
7210
  triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
8338
- ...turnContext.conversation.nativeWorkAssignment ? { nativeWorkAssignment: turnContext.conversation.nativeWorkAssignment } : {},
8339
7211
  title: turnContext.conversation.title
8340
7212
  });
8341
7213
  clearTimeout(preparingTimer);
8342
7214
  if (preparingStarted) reportPreparing("done");
8343
- writePrepared(
8344
- workspaceId,
8345
- payload.conversationId,
8346
- payload.agentId,
8347
- result,
8348
- assignmentKey
8349
- );
7215
+ writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
8350
7216
  effectiveCwd = result.cwd;
8351
7217
  hookEnv = result.env;
8352
- preparedNativeAssignment = result.nativeWorkAssignment;
8353
- } catch (err2) {
7218
+ } catch (err) {
8354
7219
  clearTimeout(preparingTimer);
8355
7220
  if (preparingStarted) reportPreparing("error");
8356
- const reason = err2 instanceof Error ? err2.message : String(err2);
7221
+ const reason = err instanceof Error ? err.message : String(err);
8357
7222
  turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
8358
7223
  try {
8359
7224
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
@@ -8376,6 +7241,19 @@ ${reason}`,
8376
7241
  }
8377
7242
  }
8378
7243
  }
7244
+ let turnEnv = hookEnv;
7245
+ if (effectiveCwd && turnContext.runtime === "codex") {
7246
+ const tmpDir = join12(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
7247
+ try {
7248
+ mkdirSync10(tmpDir, { recursive: true });
7249
+ turnEnv = { ...hookEnv, TMPDIR: tmpDir };
7250
+ } catch (err) {
7251
+ turnLog.warn(
7252
+ { err: err instanceof Error ? err.message : String(err), tmpDir },
7253
+ "dispatcher: failed to create per-turn TMPDIR \u2014 proceeding with the inherited temp dir"
7254
+ );
7255
+ }
7256
+ }
8379
7257
  const key = runKey(payload.conversationId, payload.agentId);
8380
7258
  const abortController = new AbortController();
8381
7259
  this.aborts.set(key, abortController);
@@ -8390,9 +7268,9 @@ ${reason}`,
8390
7268
  // this live turn for an abandoned one and close it with a `stopped`.
8391
7269
  turnId
8392
7270
  });
8393
- } catch (err2) {
7271
+ } catch (err) {
8394
7272
  turnLog.warn(
8395
- { err: err2 instanceof Error ? err2.message : String(err2) },
7273
+ { err: err instanceof Error ? err.message : String(err) },
8396
7274
  "dispatcher: active-run flag set failed terminally; proceeding"
8397
7275
  );
8398
7276
  }
@@ -8429,35 +7307,6 @@ ${reason}`,
8429
7307
  subAgentCreate,
8430
7308
  wakeState
8431
7309
  );
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
7310
  const request = buildCompanionTurnRequest({
8462
7311
  turnContext,
8463
7312
  baseUrl: this.opts.baseUrl,
@@ -8467,11 +7316,11 @@ ${reason}`,
8467
7316
  ...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
8468
7317
  // SJ524: the hook-resolved cwd overrides the static local cwd.
8469
7318
  ...effectiveCwd ? { cwd: effectiveCwd } : {},
8470
- ...hookEnv ? { env: hookEnv } : {},
8471
- ...preparedNativeAssignment ? { nativeWorkAssignment: preparedNativeAssignment } : {},
7319
+ // CT804: `turnEnv` = the prepare-hook env plus the per-turn checkout-local
7320
+ // TMPDIR (falls back to `hookEnv` when no cwd was resolved).
7321
+ ...turnEnv ? { env: turnEnv } : {},
8472
7322
  mcpServers: resolvedMcpServers,
8473
7323
  summonServer,
8474
- turnControl: nativeTurnControl,
8475
7324
  // CT238: this turn's conversation, forwarded as the active-conversation
8476
7325
  // header so a cross-thread post/spawn stamps its origin.
8477
7326
  activeConversationId: payload.conversationId,
@@ -8489,22 +7338,19 @@ ${reason}`,
8489
7338
  if (this.opts.codexEnabled) {
8490
7339
  adapters.push(createCodexAdapter({ enabled: true, onWarn }));
8491
7340
  }
8492
- if (this.opts.cabaneNativeApiKey) {
8493
- adapters.push(createCabaneNativeAdapter({ apiKey: this.opts.cabaneNativeApiKey, onWarn }));
8494
- }
8495
7341
  const registry = createAdapterRegistry(adapters);
8496
7342
  let adapter;
8497
7343
  try {
8498
7344
  adapter = selectAdapter(registry, turnContext.runtime);
8499
- } catch (err2) {
8500
- if (!(err2 instanceof RuntimeUnavailableError)) throw err2;
7345
+ } catch (err) {
7346
+ if (!(err instanceof RuntimeUnavailableError)) throw err;
8501
7347
  turnLog.error(
8502
- { runtime: err2.runtime, available: err2.available },
7348
+ { runtime: err.runtime, available: err.available },
8503
7349
  "dispatcher: turn runtime not available on this device"
8504
7350
  );
8505
7351
  try {
8506
7352
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
8507
- body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err2.message}`,
7353
+ body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
8508
7354
  kind: "final",
8509
7355
  turnId,
8510
7356
  parentMessageId: payload.messageId
@@ -8519,7 +7365,7 @@ ${reason}`,
8519
7365
  payload,
8520
7366
  turnLog,
8521
7367
  startedAt,
8522
- `runtime_unavailable:${err2.runtime}`
7368
+ `runtime_unavailable:${err.runtime}`
8523
7369
  );
8524
7370
  }
8525
7371
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
@@ -8653,9 +7499,9 @@ ${reason}`,
8653
7499
  skipState.skipped = true;
8654
7500
  skipState.reason = intent.skipReason;
8655
7501
  }
8656
- } catch (err2) {
7502
+ } catch (err) {
8657
7503
  turnLog.warn(
8658
- { err: err2 instanceof Error ? err2.message : String(err2) },
7504
+ { err: err instanceof Error ? err.message : String(err) },
8659
7505
  "dispatcher: turn-control intent fetch failed; turn-control effects for this turn are dropped"
8660
7506
  );
8661
7507
  }
@@ -8701,9 +7547,9 @@ ${reason}`,
8701
7547
  payload.agentId,
8702
7548
  { agentSessionId: event.state }
8703
7549
  );
8704
- } catch (err2) {
7550
+ } catch (err) {
8705
7551
  turnLog.warn(
8706
- { err: err2 instanceof Error ? err2.message : String(err2) },
7552
+ { err: err instanceof Error ? err.message : String(err) },
8707
7553
  "dispatcher: session-id write failed (will retry next turn)"
8708
7554
  );
8709
7555
  }
@@ -8752,18 +7598,18 @@ ${reason}`,
8752
7598
  parentMessageId: payload.messageId,
8753
7599
  ...skipWake ? { wake: skipWake } : {}
8754
7600
  });
8755
- } catch (err2) {
7601
+ } catch (err) {
8756
7602
  turnLog.warn(
8757
- { err: err2 instanceof Error ? err2.message : String(err2) },
7603
+ { err: err instanceof Error ? err.message : String(err) },
8758
7604
  "dispatcher: skipped-marker commit failed"
8759
7605
  );
8760
7606
  }
8761
7607
  } else {
8762
7608
  await committer.finalize(okResult);
8763
7609
  }
8764
- } catch (err2) {
7610
+ } catch (err) {
8765
7611
  okResult = false;
8766
- resultReason = err2 instanceof Error ? err2.message : String(err2);
7612
+ resultReason = err instanceof Error ? err.message : String(err);
8767
7613
  turnLog.error({ err: resultReason }, "dispatcher: SDK query threw");
8768
7614
  } finally {
8769
7615
  if (idleTimer) clearTimeout(idleTimer);
@@ -8799,9 +7645,9 @@ ${reason}`,
8799
7645
  // CT113: the stopped marker is still "about" the triggering message.
8800
7646
  parentMessageId: payload.messageId
8801
7647
  });
8802
- } catch (err2) {
7648
+ } catch (err) {
8803
7649
  turnLog.warn(
8804
- { err: err2 instanceof Error ? err2.message : String(err2) },
7650
+ { err: err instanceof Error ? err.message : String(err) },
8805
7651
  "dispatcher: stopped-marker commit failed"
8806
7652
  );
8807
7653
  }
@@ -8842,9 +7688,9 @@ ${reason}`,
8842
7688
  payload.agentId,
8843
7689
  body
8844
7690
  );
8845
- } catch (err2) {
7691
+ } catch (err) {
8846
7692
  turnLog.warn(
8847
- { err: err2 instanceof Error ? err2.message : String(err2) },
7693
+ { err: err instanceof Error ? err.message : String(err) },
8848
7694
  "dispatcher: active-run clear failed terminally; server age-sweep is the backstop"
8849
7695
  );
8850
7696
  }
@@ -8912,7 +7758,6 @@ function buildCompanionManifest(opts) {
8912
7758
  if (opts.claudeCode) runtimes.push({ name: "claude-code", version: v.claudeCode ?? null });
8913
7759
  if (opts.opencode) runtimes.push({ name: "opencode", version: v.opencode ?? null });
8914
7760
  if (opts.codex) runtimes.push({ name: "codex", version: v.codex ?? null });
8915
- if (opts.cabaneNative) runtimes.push({ name: "cabane-native", version: null });
8916
7761
  return { runtimes, capabilities: { ...DEVICE_MANIFEST.capabilities } };
8917
7762
  }
8918
7763
 
@@ -9035,8 +7880,8 @@ async function probeHarnessSignals(cfg, deps = {}) {
9035
7880
  return {
9036
7881
  claudeOnPath: claudeOnPathResult,
9037
7882
  claudeVersion,
9038
- // A parseable `codex --version` is our presence signal (codex isn't the
9039
- // default runtime; its config flag is the manifest gate either way).
7883
+ // A parseable `codex --version` is our presence signal (presence alone never
7884
+ // exposes codex; its config flag is the manifest gate either way).
9040
7885
  codexOnPath: codexVersion !== null,
9041
7886
  codexVersion,
9042
7887
  codexEnabled: isCodexEnabled(cfg),
@@ -9116,7 +7961,7 @@ import {
9116
7961
  existsSync as existsSync10,
9117
7962
  mkdirSync as mkdirSync11,
9118
7963
  readdirSync as readdirSync2,
9119
- readFileSync as readFileSync9,
7964
+ readFileSync as readFileSync8,
9120
7965
  renameSync as renameSync3,
9121
7966
  rmSync as rmSync5,
9122
7967
  writeFileSync as writeFileSync7
@@ -9150,13 +7995,13 @@ var Outbox = class {
9150
7995
  try {
9151
7996
  writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
9152
7997
  renameSync3(tmp, target);
9153
- } catch (err2) {
7998
+ } catch (err) {
9154
7999
  try {
9155
8000
  rmSync5(tmp, { force: true });
9156
8001
  } catch {
9157
8002
  }
9158
8003
  this.log?.warn(
9159
- { workspaceId: this.workspaceId, err: err2 instanceof Error ? err2.message : String(err2) },
8004
+ { workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
9160
8005
  "companion outbox: failed to persist entry"
9161
8006
  );
9162
8007
  return;
@@ -9181,7 +8026,7 @@ var Outbox = class {
9181
8026
  if (!name.endsWith(".json")) continue;
9182
8027
  const full = join13(dir2, name);
9183
8028
  try {
9184
- const parsed = JSON.parse(readFileSync9(full, "utf8"));
8029
+ const parsed = JSON.parse(readFileSync8(full, "utf8"));
9185
8030
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
9186
8031
  entries.push(parsed);
9187
8032
  } else {
@@ -9253,40 +8098,43 @@ var Outbox = class {
9253
8098
  };
9254
8099
 
9255
8100
  // 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()
8101
+ import { z as z14 } from "zod";
8102
+ var mcpStdioServerSchema = z14.object({
8103
+ type: z14.literal("stdio").optional(),
8104
+ command: z14.string().min(1),
8105
+ args: z14.array(z14.string()).optional(),
8106
+ env: z14.record(z14.string(), z14.string()).optional()
9262
8107
  });
9263
- var mcpHttpServerSchema = z16.object({
9264
- type: z16.literal("http"),
9265
- url: z16.string().url(),
9266
- headers: z16.record(z16.string(), z16.string()).optional()
8108
+ var mcpHttpServerSchema = z14.object({
8109
+ type: z14.literal("http"),
8110
+ url: z14.string().url(),
8111
+ headers: z14.record(z14.string(), z14.string()).optional()
9267
8112
  });
9268
- var mcpSseServerSchema = z16.object({
9269
- type: z16.literal("sse"),
9270
- url: z16.string().url(),
9271
- headers: z16.record(z16.string(), z16.string()).optional()
8113
+ var mcpSseServerSchema = z14.object({
8114
+ type: z14.literal("sse"),
8115
+ url: z14.string().url(),
8116
+ headers: z14.record(z14.string(), z14.string()).optional()
9272
8117
  });
9273
- var mcpServerDefSchema = z16.union([
8118
+ var mcpServerDefSchema = z14.union([
9274
8119
  mcpHttpServerSchema,
9275
8120
  mcpSseServerSchema,
9276
8121
  mcpStdioServerSchema
9277
8122
  ]);
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") })
8123
+ var thinkingConfigSchema = z14.discriminatedUnion("type", [
8124
+ z14.object({ type: z14.literal("adaptive") }),
8125
+ z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
8126
+ z14.object({ type: z14.literal("disabled") })
9282
8127
  ]);
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(),
8128
+ var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
8129
+ var runConfigSchema = z14.object({
8130
+ // CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
8131
+ // trio + its custom tool lists — `true` grants the host filesystem/shell, absent
8132
+ // is the locked surface. Kept in lockstep with `@cabane/shared`'s
8133
+ // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
8134
+ // stripped, so an older companion riding a newer server never rejects the config).
8135
+ hostAccess: z14.boolean().optional(),
8136
+ mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
8137
+ model: z14.string().min(1).optional(),
9290
8138
  thinking: thinkingConfigSchema.optional(),
9291
8139
  effort: effortSchema.optional()
9292
8140
  });
@@ -9331,20 +8179,20 @@ var SseSubscriber = class {
9331
8179
  try {
9332
8180
  await this.connect();
9333
8181
  backoff = 500;
9334
- } catch (err2) {
8182
+ } catch (err) {
9335
8183
  if (this.aborted) return;
9336
- if (err2 instanceof ApiError && (err2.status === 401 || err2.status === 403)) {
8184
+ if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
9337
8185
  this.opts.log.error(
9338
- { workspaceId: this.opts.workspaceId, status: err2.status },
8186
+ { workspaceId: this.opts.workspaceId, status: err.status },
9339
8187
  "SSE auth failed \u2014 tearing down this workspace subscriber"
9340
8188
  );
9341
- this.opts.onAuthFailure(err2.status);
8189
+ this.opts.onAuthFailure(err.status);
9342
8190
  return;
9343
8191
  }
9344
8192
  this.opts.log.warn(
9345
8193
  {
9346
8194
  workspaceId: this.opts.workspaceId,
9347
- err: err2 instanceof Error ? err2.message : String(err2),
8195
+ err: err instanceof Error ? err.message : String(err),
9348
8196
  backoff
9349
8197
  },
9350
8198
  "SSE disconnected; reconnecting"
@@ -9487,7 +8335,7 @@ var CompanionSupervisor = class {
9487
8335
  if (!this.config.deviceToken) {
9488
8336
  this.log.warn("companion: not paired (no device token) \u2014 run `cabane-companion pair`");
9489
8337
  process.stdout.write(
9490
- "companion: this device is not paired \u2014 run `cabane-companion pair` and paste the string from the cabane app.\n"
8338
+ "companion: this device is not paired \u2014 run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
9491
8339
  );
9492
8340
  return;
9493
8341
  }
@@ -9542,9 +8390,6 @@ var CompanionSupervisor = class {
9542
8390
  // like opencode — the CLI's presence is the operator's responsibility;
9543
8391
  // a misconfigured device fails the turn loudly, never silently).
9544
8392
  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
8393
  // CT571/CT586: each runtime's `version` from the latest harness probe
9549
8394
  // (fail-soft to null). Informational only — the server matches on name.
9550
8395
  versions: this.harnessVersions
@@ -9562,9 +8407,9 @@ var CompanionSupervisor = class {
9562
8407
  this.hub.setDevice({ deviceId: res.deviceId });
9563
8408
  this.deviceId = res.deviceId;
9564
8409
  this.checkVersionSkew(res.serverVersion);
9565
- } catch (err2) {
8410
+ } catch (err) {
9566
8411
  this.log.warn(
9567
- { err: err2 instanceof Error ? err2.message : String(err2) },
8412
+ { err: err instanceof Error ? err.message : String(err) },
9568
8413
  "companion: device heartbeat failed (will retry on next tick)"
9569
8414
  );
9570
8415
  }
@@ -9600,12 +8445,12 @@ var CompanionSupervisor = class {
9600
8445
  const resp = await this.deviceApi.getAssignments();
9601
8446
  items = resp.assignments;
9602
8447
  device = resp.device;
9603
- } catch (err2) {
8448
+ } catch (err) {
9604
8449
  this.log.error(
9605
- { err: err2 instanceof Error ? err2.message : String(err2) },
8450
+ { err: err instanceof Error ? err.message : String(err) },
9606
8451
  "companion: assignments pull failed \u2014 check the device is still active in the cabane app"
9607
8452
  );
9608
- this.hub.setDeviceError(err2 instanceof Error ? err2.message : String(err2));
8453
+ this.hub.setDeviceError(err instanceof Error ? err.message : String(err));
9609
8454
  return;
9610
8455
  }
9611
8456
  this.hub.setDevice({ deviceId: device.id, deviceLabel: device.label });
@@ -9682,7 +8527,7 @@ var CompanionSupervisor = class {
9682
8527
  agentId: it.agentId,
9683
8528
  username: it.agentUsername,
9684
8529
  displayName: it.agentDisplayName,
9685
- mode: runConfig.mode ?? "assistant",
8530
+ mode: runConfig.hostAccess ? "full" : "none",
9686
8531
  hasCredential: false,
9687
8532
  missingSecrets: missing
9688
8533
  });
@@ -9702,7 +8547,7 @@ var CompanionSupervisor = class {
9702
8547
  agentId: it.agentId,
9703
8548
  username: it.agentUsername,
9704
8549
  displayName: it.agentDisplayName,
9705
- mode: runConfig.mode ?? "assistant",
8550
+ mode: runConfig.hostAccess ? "full" : "none",
9706
8551
  hasCredential: true,
9707
8552
  missingSecrets: missing
9708
8553
  });
@@ -9786,12 +8631,9 @@ var CompanionSupervisor = class {
9786
8631
  // CT481: register the codex adapter when this device offers codex; unset
9787
8632
  // leaves an `openai/…` turn to fail loudly (no silent claude-code fallback).
9788
8633
  ...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
8634
  // CT556: per-turn timeout watchdog windows, from the companion's own env
9793
8635
  // (`AGENT_IDLE_TIMEOUT_MS` / `AGENT_TOTAL_TIMEOUT_MS`). Unset → the
9794
- // dispatcher's baked-in defaults (10 min idle / 45 min total).
8636
+ // dispatcher's baked-in defaults (10 min idle / 6h total).
9795
8637
  ...positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") !== void 0 ? { idleTimeoutMs: positiveIntEnv("AGENT_IDLE_TIMEOUT_MS") } : {},
9796
8638
  ...positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") !== void 0 ? { totalTimeoutMs: positiveIntEnv("AGENT_TOTAL_TIMEOUT_MS") } : {},
9797
8639
  observer: this.hub.observerFor(ctx.workspaceId, ctx.agentId, ctx.workspaceSlug),
@@ -9858,9 +8700,9 @@ var CompanionSupervisor = class {
9858
8700
  let wire;
9859
8701
  try {
9860
8702
  wire = JSON.parse(ev.data);
9861
- } catch (err2) {
8703
+ } catch (err) {
9862
8704
  this.log.warn(
9863
- { err: err2 instanceof Error ? err2.message : String(err2) },
8705
+ { err: err instanceof Error ? err.message : String(err) },
9864
8706
  "malformed SSE payload"
9865
8707
  );
9866
8708
  return;
@@ -9907,13 +8749,13 @@ var CompanionSupervisor = class {
9907
8749
  }
9908
8750
  const chainKey = `${payload.conversationId}|${payload.agentId}`;
9909
8751
  const prev = wr.chains.get(chainKey) ?? Promise.resolve();
9910
- const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err2) => {
8752
+ const tail = prev.then(() => this.runDispatch(wr, ev, payload, agent)).catch((err) => {
9911
8753
  this.log.warn(
9912
8754
  {
9913
8755
  workspaceId: wr.workspaceId,
9914
8756
  conversationId: payload.conversationId,
9915
8757
  agentId: payload.agentId,
9916
- err: err2 instanceof Error ? err2.message : String(err2)
8758
+ err: err instanceof Error ? err.message : String(err)
9917
8759
  },
9918
8760
  "companion: conversation turn handler threw"
9919
8761
  );
@@ -10005,10 +8847,10 @@ var CompanionSupervisor = class {
10005
8847
  } else {
10006
8848
  drainDelay = DRAIN_BASE_MS;
10007
8849
  }
10008
- } catch (err2) {
8850
+ } catch (err) {
10009
8851
  drainDelay = Math.min(drainDelay * 2, DRAIN_MAX_MS);
10010
8852
  this.log.warn(
10011
- { agentId, err: err2 instanceof Error ? err2.message : String(err2) },
8853
+ { agentId, err: err instanceof Error ? err.message : String(err) },
10012
8854
  "companion: outbox drain pass threw (will retry with backoff)"
10013
8855
  );
10014
8856
  } finally {
@@ -10075,9 +8917,9 @@ var CompanionSupervisor = class {
10075
8917
  codex: signals.codexVersion
10076
8918
  };
10077
8919
  this.hub.setHarnesses(deriveHarnessSnapshot(signals).harnesses);
10078
- } catch (err2) {
8920
+ } catch (err) {
10079
8921
  this.log.warn(
10080
- { err: err2 instanceof Error ? err2.message : String(err2) },
8922
+ { err: err instanceof Error ? err.message : String(err) },
10081
8923
  "companion: harness probe failed (will retry on next beat)"
10082
8924
  );
10083
8925
  }
@@ -10175,10 +9017,13 @@ var CompanionSupervisor = class {
10175
9017
  await Promise.race([
10176
9018
  Promise.allSettled(turns),
10177
9019
  new Promise((resolve) => {
10178
- timer = setTimeout(() => {
10179
- timedOut = true;
10180
- resolve();
10181
- }, Math.max(0, graceMs));
9020
+ timer = setTimeout(
9021
+ () => {
9022
+ timedOut = true;
9023
+ resolve();
9024
+ },
9025
+ Math.max(0, graceMs)
9026
+ );
10182
9027
  timer.unref?.();
10183
9028
  })
10184
9029
  ]);
@@ -10232,17 +9077,17 @@ var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
10232
9077
  "ERR_STREAM_DESTROYED",
10233
9078
  "ERR_STREAM_WRITE_AFTER_END"
10234
9079
  ]);
10235
- function errorCode(err2) {
10236
- if (err2 && typeof err2 === "object" && "code" in err2) {
10237
- const code = err2.code;
9080
+ function errorCode(err) {
9081
+ if (err && typeof err === "object" && "code" in err) {
9082
+ const code = err.code;
10238
9083
  if (typeof code === "string") return code;
10239
9084
  }
10240
9085
  return void 0;
10241
9086
  }
10242
- function isRecoverableSocketError(err2) {
10243
- const code = errorCode(err2);
9087
+ function isRecoverableSocketError(err) {
9088
+ const code = errorCode(err);
10244
9089
  if (code && RECOVERABLE_CODES.has(code)) return true;
10245
- const message = err2 instanceof Error ? err2.message : String(err2);
9090
+ const message = err instanceof Error ? err.message : String(err);
10246
9091
  return /\bEPIPE\b|\bECONNRESET\b/.test(message);
10247
9092
  }
10248
9093
  function installProcessSafetyNet(log, opts = {}) {
@@ -10251,16 +9096,16 @@ function installProcessSafetyNet(log, opts = {}) {
10251
9096
  stream.on("error", () => {
10252
9097
  });
10253
9098
  }
10254
- proc.on("uncaughtException", (err2) => handleUncaught(log, err2, "uncaughtException"));
9099
+ proc.on("uncaughtException", (err) => handleUncaught(log, err, "uncaughtException"));
10255
9100
  proc.on(
10256
9101
  "unhandledRejection",
10257
9102
  (reason) => handleUncaught(log, reason, "unhandledRejection")
10258
9103
  );
10259
9104
  }
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)) {
9105
+ function handleUncaught(log, err, origin) {
9106
+ const message = err instanceof Error ? err.message : String(err);
9107
+ const code = errorCode(err);
9108
+ if (isRecoverableSocketError(err)) {
10264
9109
  log.warn(
10265
9110
  { origin, code, err: message },
10266
9111
  "companion: recovered from a socket error (kept running \u2014 a broken pipe never kills the companion)"
@@ -10268,13 +9113,13 @@ function handleUncaught(log, err2, origin) {
10268
9113
  return;
10269
9114
  }
10270
9115
  log.error(
10271
- { origin, code, err: message, stack: err2 instanceof Error ? err2.stack : void 0 },
9116
+ { origin, code, err: message, stack: err instanceof Error ? err.stack : void 0 },
10272
9117
  "companion: uncaught error (kept running \u2014 see the stack above)"
10273
9118
  );
10274
9119
  }
10275
9120
 
10276
9121
  // src/crash-marker.ts
10277
- import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync10, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9122
+ import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
10278
9123
  import { join as join14 } from "path";
10279
9124
  function crashMarkerPath() {
10280
9125
  return join14(cabaneDir(), "last-error.json");
@@ -10305,14 +9150,14 @@ async function createCompanionRuntime(opts = {}) {
10305
9150
  cfg = requireConfig();
10306
9151
  claudeCode = await probeClaude();
10307
9152
  await ensureRuntimeAvailable(cfg, { probeClaude: async () => claudeCode });
10308
- } catch (err2) {
9153
+ } catch (err) {
10309
9154
  recordCrash({
10310
- reason: err2 instanceof Error ? err2.message : String(err2),
10311
- ...errorCode(err2) ? { code: errorCode(err2) } : {},
9155
+ reason: err instanceof Error ? err.message : String(err),
9156
+ ...errorCode(err) ? { code: errorCode(err) } : {},
10312
9157
  origin: "startup",
10313
9158
  at: (/* @__PURE__ */ new Date()).toISOString()
10314
9159
  });
10315
- throw err2;
9160
+ throw err;
10316
9161
  }
10317
9162
  if (cfg.logLevel) log.level = cfg.logLevel;
10318
9163
  const harnessVersions = await probeHarnessVersions({
@@ -10403,6 +9248,8 @@ async function createCompanionRuntime(opts = {}) {
10403
9248
 
10404
9249
  // src/commands/start.ts
10405
9250
  var FORCE_EXIT_MS = 4e3;
9251
+ var DEPLOY_REEXEC_EXIT = 75;
9252
+ var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
10406
9253
  async function start(opts = {}) {
10407
9254
  const result = await createCompanionRuntime({
10408
9255
  ...opts.port !== void 0 ? { port: opts.port } : {}
@@ -10426,7 +9273,7 @@ Cabane Companion is running.
10426
9273
  `);
10427
9274
  if (!runtime.config.deviceToken) {
10428
9275
  process.stdout.write(
10429
- `This device isn't paired yet \u2014 run \`cabane-companion pair\` and paste the string from the cabane app.
9276
+ `This device isn't paired yet \u2014 run \`cabane-companion pair\`, then confirm the short code in Settings \u2192 Connectors.
10430
9277
 
10431
9278
  `
10432
9279
  );
@@ -10466,25 +9313,25 @@ companion: received ${signal}, shutting down\u2026
10466
9313
  if (shuttingDown) return;
10467
9314
  shuttingDown = true;
10468
9315
  const configuredGrace = Number.parseInt(
10469
- process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ?? "60000",
9316
+ process.env.CABANE_COMPANION_DEPLOY_GRACE_MS ?? String(DEPLOY_GRACE_MS),
10470
9317
  10
10471
9318
  );
10472
- const graceMs = Number.isFinite(configuredGrace) ? configuredGrace : 6e4;
9319
+ const graceMs = Number.isFinite(configuredGrace) ? configuredGrace : DEPLOY_GRACE_MS;
10473
9320
  process.stdout.write(`
10474
9321
  companion: deploy drain requested (${graceMs}ms grace)\u2026
10475
9322
  `);
10476
9323
  void runtime.drainForRestart(graceMs).then(({ drained }) => {
10477
9324
  process.stdout.write(
10478
- drained ? "companion: deploy drain complete.\n" : "companion: deploy grace expired; unfinished turns will resume after restart.\n"
9325
+ 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
9326
  );
10480
9327
  resolve();
10481
- process.exit(0);
10482
- }).catch((err2) => {
9328
+ process.exit(DEPLOY_REEXEC_EXIT);
9329
+ }).catch((err) => {
10483
9330
  process.stderr.write(
10484
- `companion: deploy drain failed: ${err2 instanceof Error ? err2.message : String(err2)}
9331
+ `companion: deploy drain failed: ${err instanceof Error ? err.message : String(err)}
10485
9332
  `
10486
9333
  );
10487
- process.exit(1);
9334
+ process.exit(DEPLOY_REEXEC_EXIT);
10488
9335
  });
10489
9336
  });
10490
9337
  });
@@ -10496,7 +9343,7 @@ async function status() {
10496
9343
  const cfg = loadConfig();
10497
9344
  if (!cfg || !cfg.deviceToken) {
10498
9345
  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"
9346
+ "companion: not paired. Run `cabane-companion pair`, then confirm the short code in Settings \u2192 Connectors.\n"
10500
9347
  );
10501
9348
  process.exitCode = 1;
10502
9349
  return;
@@ -10628,7 +9475,7 @@ function isAlive(kill, pid) {
10628
9475
  }
10629
9476
 
10630
9477
  // src/commands/transcript.ts
10631
- import { existsSync as existsSync12, readFileSync as readFileSync11, readdirSync as readdirSync4 } from "fs";
9478
+ import { existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
10632
9479
  import { isAbsolute, join as join15 } from "path";
10633
9480
  async function transcript(opts = {}) {
10634
9481
  const dir2 = transcriptsDir();
@@ -10722,14 +9569,14 @@ var TranscriptFollower = class {
10722
9569
  function isComplete(content) {
10723
9570
  for (const line of content.split("\n")) {
10724
9571
  if (!line.trim()) continue;
10725
- if (str4(rec(safeParse(line))?.type) === "_outcome") return true;
9572
+ if (str2(rec(safeParse(line))?.type) === "_outcome") return true;
10726
9573
  }
10727
9574
  return false;
10728
9575
  }
10729
9576
  async function followTranscripts(dir2) {
10730
9577
  const follower = new TranscriptFollower({
10731
9578
  listFiles: () => listFiles(dir2),
10732
- read: (f) => readFileSync11(join15(dir2, f), "utf8"),
9579
+ read: (f) => readFileSync10(join15(dir2, f), "utf8"),
10733
9580
  write: (s) => process.stdout.write(s),
10734
9581
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
10735
9582
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -10771,13 +9618,13 @@ function printList(dir2) {
10771
9618
  for (const f of files.slice(0, 20)) {
10772
9619
  const { meta, outcome } = peek(join15(dir2, f));
10773
9620
  const when = fmtTime(rec(meta)?.ts);
10774
- const ws = str4(rec(meta)?.workspaceSlug);
9621
+ const ws = str2(rec(meta)?.workspaceSlug);
10775
9622
  const o = rec(outcome);
10776
- const verdict = o ? o.ok === true ? "ok" : `ERROR${str4(o.reason) ? ` (${str4(o.reason)})` : ""}` : "\u2026";
9623
+ const verdict = o ? o.ok === true ? "ok" : `ERROR${str2(o.reason) ? ` (${str2(o.reason)})` : ""}` : "\u2026";
10777
9624
  process.stdout.write(
10778
9625
  ` ${f}
10779
9626
  ${when} \xB7 ${ws} \xB7 ${verdict}
10780
- \u201C${excerpt(str4(rec(meta)?.message), 70)}\u201D
9627
+ \u201C${excerpt(str2(rec(meta)?.message), 70)}\u201D
10781
9628
 
10782
9629
  `
10783
9630
  );
@@ -10790,10 +9637,10 @@ function peek(path3) {
10790
9637
  let meta;
10791
9638
  let outcome;
10792
9639
  try {
10793
- for (const line of readFileSync11(path3, "utf8").split("\n")) {
9640
+ for (const line of readFileSync10(path3, "utf8").split("\n")) {
10794
9641
  if (!line.trim()) continue;
10795
9642
  const o = safeParse(line);
10796
- const t = str4(rec(o)?.type);
9643
+ const t = str2(rec(o)?.type);
10797
9644
  if (t === "_meta") meta = o;
10798
9645
  else if (t === "_outcome") outcome = o;
10799
9646
  }
@@ -10823,10 +9670,10 @@ function resolveTarget(dir2, target) {
10823
9670
  function renderFile(path3) {
10824
9671
  let content;
10825
9672
  try {
10826
- content = readFileSync11(path3, "utf8");
10827
- } catch (err2) {
9673
+ content = readFileSync10(path3, "utf8");
9674
+ } catch (err) {
10828
9675
  throw new CompanionError(
10829
- `couldn't read ${path3}: ${err2 instanceof Error ? err2.message : String(err2)}`
9676
+ `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
10830
9677
  );
10831
9678
  }
10832
9679
  return renderTranscript(content.split("\n"));
@@ -10838,19 +9685,19 @@ function renderTranscript(jsonlLines) {
10838
9685
  if (!raw.trim()) continue;
10839
9686
  const obj = rec(safeParse(raw));
10840
9687
  if (!obj) continue;
10841
- switch (str4(obj.type)) {
9688
+ switch (str2(obj.type)) {
10842
9689
  case "_meta":
10843
9690
  out.push(
10844
- `${fmtTime(obj.ts)} \xB7 workspace=${str4(obj.workspaceSlug)} \xB7 conversation=${str4(obj.conversationId)}`
9691
+ `${fmtTime(obj.ts)} \xB7 workspace=${str2(obj.workspaceSlug)} \xB7 conversation=${str2(obj.conversationId)}`
10845
9692
  );
10846
- out.push("", `> USER: ${str4(obj.message)}`, "");
9693
+ out.push("", `> USER: ${str2(obj.message)}`, "");
10847
9694
  break;
10848
9695
  case "system":
10849
- if (str4(obj.subtype) === "init") {
10850
- out.push(`[session ${str4(obj.session_id) || "?"} \xB7 model ${str4(obj.model) || "?"}]`);
9696
+ if (str2(obj.subtype) === "init") {
9697
+ out.push(`[session ${str2(obj.session_id) || "?"} \xB7 model ${str2(obj.model) || "?"}]`);
10851
9698
  const servers = Array.isArray(obj.mcp_servers) ? obj.mcp_servers.map((s) => {
10852
9699
  const r = rec(s);
10853
- return r ? `${str4(r.name) || "?"}${str4(r.status) ? `(${str4(r.status)})` : ""}` : "";
9700
+ return r ? `${str2(r.name) || "?"}${str2(r.status) ? `(${str2(r.status)})` : ""}` : "";
10854
9701
  }).filter(Boolean).join(", ") : "";
10855
9702
  if (servers) out.push(` MCP servers: ${servers}`);
10856
9703
  if (Array.isArray(obj.tools)) out.push(` tools: ${obj.tools.length} available`);
@@ -10876,8 +9723,8 @@ function renderTranscript(jsonlLines) {
10876
9723
  if (!Array.isArray(content)) break;
10877
9724
  for (const b of content) {
10878
9725
  const block = rec(b);
10879
- if (!block || str4(block.type) !== "tool_result") continue;
10880
- const id = str4(block.tool_use_id);
9726
+ if (!block || str2(block.type) !== "tool_result") continue;
9727
+ const id = str2(block.tool_use_id);
10881
9728
  const label = pending.get(id) ?? "tool";
10882
9729
  pending.delete(id);
10883
9730
  const tag = block.is_error === true ? "ERROR" : "ok";
@@ -10886,18 +9733,18 @@ function renderTranscript(jsonlLines) {
10886
9733
  break;
10887
9734
  }
10888
9735
  case "result": {
10889
- const isErr = obj.is_error === true || str4(obj.subtype) !== "success";
9736
+ const isErr = obj.is_error === true || str2(obj.subtype) !== "success";
10890
9737
  const dur = typeof obj.duration_ms === "number" ? ` \xB7 ${obj.duration_ms}ms` : "";
10891
9738
  out.push(
10892
- `[result ${isErr ? "error" : "ok"}${str4(obj.subtype) ? ` \xB7 ${str4(obj.subtype)}` : ""}${dur}]`
9739
+ `[result ${isErr ? "error" : "ok"}${str2(obj.subtype) ? ` \xB7 ${str2(obj.subtype)}` : ""}${dur}]`
10893
9740
  );
10894
- if (isErr && str4(obj.result).trim()) out.push(` ${indent(str4(obj.result))}`);
9741
+ if (isErr && str2(obj.result).trim()) out.push(` ${indent(str2(obj.result))}`);
10895
9742
  break;
10896
9743
  }
10897
9744
  case "_outcome": {
10898
9745
  const dur = typeof obj.durationMs === "number" ? ` \xB7 ${obj.durationMs}ms` : "";
10899
9746
  out.push(
10900
- `[outcome ${obj.ok === true ? "ok" : "error"}${str4(obj.reason) ? ` \xB7 ${str4(obj.reason)}` : ""}${dur}]`
9747
+ `[outcome ${obj.ok === true ? "ok" : "error"}${str2(obj.reason) ? ` \xB7 ${str2(obj.reason)}` : ""}${dur}]`
10901
9748
  );
10902
9749
  break;
10903
9750
  }
@@ -10915,7 +9762,7 @@ function safeParse(s) {
10915
9762
  function rec(v) {
10916
9763
  return v && typeof v === "object" ? v : null;
10917
9764
  }
10918
- function str4(v) {
9765
+ function str2(v) {
10919
9766
  return typeof v === "string" ? v : "";
10920
9767
  }
10921
9768
  function fmtTime(ts) {
@@ -10954,22 +9801,14 @@ program.name("cabane-companion").description(
10954
9801
  ).version(COMPANION_VERSION);
10955
9802
  program.command("pair").description(
10956
9803
  "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
- );
9804
+ ).allowExcessArguments(false).option("--server <url>", "the cabane instance to pair with (default https://cabane.ai)").action(async (opts) => {
9805
+ await pair({
9806
+ ...opts.server !== void 0 ? { server: opts.server } : {}
9807
+ });
9808
+ });
9809
+ program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
9810
+ writeCompletedPairing(readFileSync11(0, "utf8"));
9811
+ });
10973
9812
  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
9813
  if (opts.daemon) {
10975
9814
  await startDaemon({
@@ -11015,19 +9854,19 @@ function parsePort(raw) {
11015
9854
  }
11016
9855
  return n;
11017
9856
  }
11018
- program.parseAsync(process.argv).catch((err2) => {
11019
- if (err2 instanceof CompanionError) {
11020
- process.stderr.write(`error: ${err2.message}
9857
+ program.parseAsync(process.argv).catch((err) => {
9858
+ if (err instanceof CompanionError) {
9859
+ process.stderr.write(`error: ${err.message}
11021
9860
  `);
11022
9861
  process.exitCode = 1;
11023
9862
  return;
11024
9863
  }
11025
- if (err2 && typeof err2 === "object" && "name" in err2 && err2.name === "ExitPromptError") {
9864
+ if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
11026
9865
  process.stderr.write("cancelled\n");
11027
9866
  process.exitCode = 130;
11028
9867
  return;
11029
9868
  }
11030
- process.stderr.write(`${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}
9869
+ process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
11031
9870
  `);
11032
9871
  process.exitCode = 1;
11033
9872
  });