squadrant 0.19.5 → 0.20.0

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.
@@ -85,6 +85,9 @@ var init_config_io = __esm({
85
85
  import path2 from "path";
86
86
  import os from "os";
87
87
  import chalk from "chalk";
88
+ function isBackendMode(v2) {
89
+ return v2 === "native" || v2 === "direct" || v2 === "proxy";
90
+ }
88
91
  function getDefaultConfig() {
89
92
  return {
90
93
  commandName: "\u{1F3DB}\uFE0F command",
@@ -245,6 +248,20 @@ var init_effort = __esm({
245
248
  }
246
249
  });
247
250
 
251
+ // packages/shared/dist/router-model.js
252
+ function resolveRouterModel(model, agentName, router) {
253
+ if (!model)
254
+ return void 0;
255
+ const alias = router?.models?.[model];
256
+ if (!alias)
257
+ return model;
258
+ return alias.agents?.[agentName] ?? alias.upstream;
259
+ }
260
+ var init_router_model = __esm({
261
+ "packages/shared/dist/router-model.js"() {
262
+ }
263
+ });
264
+
248
265
  // packages/shared/dist/types/runtime.js
249
266
  var init_runtime = __esm({
250
267
  "packages/shared/dist/types/runtime.js"() {
@@ -298,22 +315,22 @@ function defaultCmuxConfigPath() {
298
315
  return join(homedir(), ".config", "cmux", "cmux.json");
299
316
  }
300
317
  function ensureSocketAutomation(opts = {}) {
301
- const path21 = opts.path ?? defaultCmuxConfigPath();
302
- if (!existsSync(path21)) {
303
- mkdirSync(dirname(path21), { recursive: true });
304
- writeFileSync(path21, MINIMAL_TEMPLATE);
305
- return { path: path21, changed: true, alreadySet: false };
318
+ const path23 = opts.path ?? defaultCmuxConfigPath();
319
+ if (!existsSync(path23)) {
320
+ mkdirSync(dirname(path23), { recursive: true });
321
+ writeFileSync(path23, MINIMAL_TEMPLATE);
322
+ return { path: path23, changed: true, alreadySet: false };
306
323
  }
307
- const text = readFileSync(path21, "utf-8");
324
+ const text = readFileSync(path23, "utf-8");
308
325
  const current = parse(text)?.automation?.socketControlMode;
309
326
  if (current === AUTOMATION_MODE) {
310
- return { path: path21, changed: false, alreadySet: true };
327
+ return { path: path23, changed: false, alreadySet: true };
311
328
  }
312
329
  const edits = modify(text, [...SOCKET_CONTROL_MODE_PATH], AUTOMATION_MODE, {
313
330
  formattingOptions: { insertSpaces: true, tabSize: 2 }
314
331
  });
315
- writeFileSync(path21, applyEdits(text, edits));
316
- return { path: path21, changed: true, alreadySet: false };
332
+ writeFileSync(path23, applyEdits(text, edits));
333
+ return { path: path23, changed: true, alreadySet: false };
317
334
  }
318
335
  var SOCKET_CONTROL_MODE_PATH, AUTOMATION_MODE, MINIMAL_TEMPLATE;
319
336
  var init_cmux_config = __esm({
@@ -463,9 +480,9 @@ import { join as join4 } from "path";
463
480
  function defaultStatePath() {
464
481
  return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
465
482
  }
466
- function readState(path21) {
483
+ function readState(path23) {
467
484
  try {
468
- return JSON.parse(readConfigFileSync(path21));
485
+ return JSON.parse(readConfigFileSync(path23));
469
486
  } catch {
470
487
  return {};
471
488
  }
@@ -511,7 +528,7 @@ var init_compat_manifest = __esm({
511
528
  "packages/shared/dist/lib/compat-manifest.js"() {
512
529
  compatManifest = {
513
530
  tools: {
514
- cmux: { min: "0.64.0", lastVerified: "0.64.17" },
531
+ cmux: { min: "0.64.0", lastVerified: "0.64.22" },
515
532
  claude: { min: "2.1.32" },
516
533
  node: { min: "18.0.0", lastVerified: "24.6.0" },
517
534
  // presence-checked; no floor enforced yet
@@ -526,6 +543,7 @@ var init_compat_manifest = __esm({
526
543
  // packages/shared/dist/lib/config-drift.js
527
544
  var init_config_drift = __esm({
528
545
  "packages/shared/dist/lib/config-drift.js"() {
546
+ init_config();
529
547
  }
530
548
  });
531
549
 
@@ -762,6 +780,7 @@ var init_dist = __esm({
762
780
  init_config();
763
781
  init_project_config();
764
782
  init_effort();
783
+ init_router_model();
765
784
  init_runtime();
766
785
  init_liveness();
767
786
  init_control();
@@ -1234,7 +1253,7 @@ function createDaemon(deps) {
1234
1253
  async sweep() {
1235
1254
  const t = now();
1236
1255
  const surfaceAlive = deps.isSurfaceAlive ?? (async () => "unknown");
1237
- const projects = new Set(store.listAll().map((r) => r.project));
1256
+ const projects = new Set(store.listAll().map((r) => r.project).filter((p) => Boolean(p)));
1238
1257
  for (const project of projects) {
1239
1258
  const terminal = store.list(project).filter((r) => TERMINAL_STATES.has(r.state)).sort((a, b) => b.lastHeartbeat - a.lastHeartbeat);
1240
1259
  for (const r of terminal.slice(TERMINAL_RECORD_KEEP_PER_PROJECT)) {
@@ -2029,6 +2048,12 @@ function safeSegment(kind, s) {
2029
2048
  }
2030
2049
  return s;
2031
2050
  }
2051
+ function isTaskRecord(r) {
2052
+ if (typeof r !== "object" || r === null)
2053
+ return false;
2054
+ const rec = r;
2055
+ return typeof rec.id === "string" && rec.id.length > 0 && typeof rec.project === "string" && rec.project.length > 0;
2056
+ }
2032
2057
  function createStore(root) {
2033
2058
  const rootResolved = resolve(root);
2034
2059
  const assertUnderRoot = (target) => {
@@ -2076,7 +2101,7 @@ function createStore(root) {
2076
2101
  } catch {
2077
2102
  return void 0;
2078
2103
  }
2079
- }).filter((r) => r !== void 0);
2104
+ }).filter(isTaskRecord);
2080
2105
  },
2081
2106
  listAll() {
2082
2107
  if (!existsSync6(root))
@@ -2364,10 +2389,10 @@ function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
2364
2389
  isUpgrade: isVersionUpgrade(parsed.daemonEntry, thisEntry)
2365
2390
  };
2366
2391
  }
2367
- function sanitizePathForPlist(path21) {
2392
+ function sanitizePathForPlist(path23) {
2368
2393
  const seen = /* @__PURE__ */ new Set();
2369
2394
  const stable = [];
2370
- for (const p of path21.split(":")) {
2395
+ for (const p of path23.split(":")) {
2371
2396
  if (!p)
2372
2397
  continue;
2373
2398
  if (p.includes("/.claude/plugins/"))
@@ -2569,6 +2594,9 @@ function isOperatorInitiatedCommand(topLevelArg) {
2569
2594
  function isReadOnlyCrewCommand(argv) {
2570
2595
  return argv[2] === "crew" && argv[3] !== void 0 && READ_ONLY_CREW_SUBCOMMANDS.has(argv[3]);
2571
2596
  }
2597
+ function isReadOnlyTopLevelCommand(argv) {
2598
+ return argv[2] !== void 0 && READ_ONLY_TOP_LEVEL_COMMANDS.has(argv[2]);
2599
+ }
2572
2600
  function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2573
2601
  if (restartInFlight)
2574
2602
  return;
@@ -2636,7 +2664,7 @@ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
2636
2664
  releaseDaemonLock();
2637
2665
  }
2638
2666
  }
2639
- var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS, READ_ONLY_CREW_SUBCOMMANDS;
2667
+ var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS, READ_ONLY_CREW_SUBCOMMANDS, READ_ONLY_TOP_LEVEL_COMMANDS;
2640
2668
  var init_launchd = __esm({
2641
2669
  "packages/core/dist/launchd.js"() {
2642
2670
  LABEL = "com.squadrant.daemon";
@@ -2644,6 +2672,7 @@ var init_launchd = __esm({
2644
2672
  restartInFlight = false;
2645
2673
  OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
2646
2674
  READ_ONLY_CREW_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "read", "tasks"]);
2675
+ READ_ONLY_TOP_LEVEL_COMMANDS = /* @__PURE__ */ new Set(["sessions", "whoami"]);
2647
2676
  }
2648
2677
  });
2649
2678
 
@@ -2923,6 +2952,7 @@ function buildContext(opts) {
2923
2952
  opencodeBridge: null,
2924
2953
  cmuxEventsBridge: null,
2925
2954
  telegramBridge: void 0,
2955
+ routerService: void 0,
2926
2956
  notifyFault: opts.notifyFault ?? (() => {
2927
2957
  }),
2928
2958
  lifecycleSources: opts.lifecycleSources ?? [],
@@ -3408,6 +3438,72 @@ var init_control_channel = __esm({
3408
3438
  }
3409
3439
  });
3410
3440
 
3441
+ // packages/core/dist/router-resolution.js
3442
+ function resolveBackend(explicit, routeBackend, roleBackend) {
3443
+ return explicit ?? routeBackend ?? roleBackend ?? "native";
3444
+ }
3445
+ function assertBackendUsable(o) {
3446
+ if (!isBackendMode(o.backend)) {
3447
+ throw new Error(`unknown backend '${o.backend}'; expected native|direct|proxy`);
3448
+ }
3449
+ if (o.backend === "native")
3450
+ return;
3451
+ if (o.agent !== "claude") {
3452
+ throw new Error(`backend '${o.backend}' is claude-only; agent '${o.agent}' must use backend 'native'`);
3453
+ }
3454
+ if (!o.router) {
3455
+ throw new Error(`backend '${o.backend}' selected for agent 'claude' but defaults.router is not configured`);
3456
+ }
3457
+ }
3458
+ function shouldBuildRouterService(router, isVitest) {
3459
+ return !!router && !isVitest;
3460
+ }
3461
+ var init_router_resolution = __esm({
3462
+ "packages/core/dist/router-resolution.js"() {
3463
+ init_dist();
3464
+ }
3465
+ });
3466
+
3467
+ // packages/core/dist/router/env.js
3468
+ function buildRouterEnv(creds, model) {
3469
+ const env = {
3470
+ ANTHROPIC_BASE_URL: creds.baseUrl,
3471
+ // Empty string, never unset: an unset key lets Claude Code fall back to
3472
+ // authenticating against Anthropic directly (U1 §Env contract).
3473
+ ANTHROPIC_API_KEY: creds.backend === "direct" ? creds.apiKey ?? "" : "",
3474
+ [CMUX_PRESERVE_CLAUDE_AUTH_ENV]: "1"
3475
+ };
3476
+ if (creds.backend === "proxy") {
3477
+ env.ANTHROPIC_AUTH_TOKEN = creds.token ?? "";
3478
+ } else {
3479
+ const headers = formatCustomHeaders(creds.extraHeaders);
3480
+ if (headers)
3481
+ env.ANTHROPIC_CUSTOM_HEADERS = headers;
3482
+ }
3483
+ if (model)
3484
+ env.ANTHROPIC_MODEL = model;
3485
+ return env;
3486
+ }
3487
+ function formatCustomHeaders(headers) {
3488
+ const entries = Object.entries(headers ?? {});
3489
+ if (entries.length === 0)
3490
+ return void 0;
3491
+ return entries.map(([name, value]) => `${name}: ${value}`).join("\n");
3492
+ }
3493
+ function renderEnvAssignments(env) {
3494
+ return Object.keys(env).sort().map((key) => `${key}=${ansiCQuote(env[key] ?? "")}`).join(" ");
3495
+ }
3496
+ function ansiCQuote(value) {
3497
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\x0a");
3498
+ return `$'${escaped}'`;
3499
+ }
3500
+ var CMUX_PRESERVE_CLAUDE_AUTH_ENV;
3501
+ var init_env = __esm({
3502
+ "packages/core/dist/router/env.js"() {
3503
+ CMUX_PRESERVE_CLAUDE_AUTH_ENV = "CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV";
3504
+ }
3505
+ });
3506
+
3411
3507
  // packages/core/dist/crew-routing.js
3412
3508
  function resolveCrewRoute(taskText, config) {
3413
3509
  const rules = config.defaults.crewRouting?.rules;
@@ -3419,6 +3515,7 @@ function resolveCrewRoute(taskText, config) {
3419
3515
  return {
3420
3516
  agent: rule.agent,
3421
3517
  ...rule.model !== void 0 ? { model: rule.model } : {},
3518
+ ...rule.backend !== void 0 ? { backend: rule.backend } : {},
3422
3519
  tier: rule.tier,
3423
3520
  matchedRule: rule.match
3424
3521
  };
@@ -3556,17 +3653,28 @@ async function pollFirstTurnConfirmedAt(getTaskRecord, project, id, scrapeSettle
3556
3653
  await new Promise((r) => setTimeout(r, FIRST_TURN_HOOK_POLL_INTERVAL_MS));
3557
3654
  }
3558
3655
  }
3559
- function firstTrueOrBothFalse(a, b) {
3656
+ function firstTrueOrBothFalse(a, b, deadlineMs) {
3560
3657
  return new Promise((resolve4) => {
3658
+ let done = false;
3659
+ const finish = (ok2) => {
3660
+ if (done)
3661
+ return;
3662
+ done = true;
3663
+ clearTimeout(timer);
3664
+ resolve4(ok2);
3665
+ };
3666
+ const timer = setTimeout(() => finish(false), deadlineMs);
3561
3667
  let settledFalseCount = 0;
3562
3668
  const onSettle = (ok2) => {
3669
+ if (done)
3670
+ return;
3563
3671
  if (ok2) {
3564
- resolve4(true);
3672
+ finish(true);
3565
3673
  return;
3566
3674
  }
3567
3675
  settledFalseCount++;
3568
3676
  if (settledFalseCount === 2)
3569
- resolve4(false);
3677
+ finish(false);
3570
3678
  };
3571
3679
  a.then(onSettle, () => onSettle(false));
3572
3680
  b.then(onSettle, () => onSettle(false));
@@ -3654,6 +3762,11 @@ async function runCrewSpawn(input, config, deps) {
3654
3762
  if (!agent) {
3655
3763
  throw new Error(`Unknown agent '${agentName}'. Known: claude, codex, gemini, opencode.`);
3656
3764
  }
3765
+ const crewRoleCfg = config.defaults.roles?.crew;
3766
+ const roleBackend = crewRoleCfg && crewRoleCfg.agent === agent.name ? crewRoleCfg.backend : void 0;
3767
+ const backend = resolveBackend(input.backend, route?.backend, roleBackend);
3768
+ assertBackendUsable({ backend, agent: agent.name, router: config.defaults.router });
3769
+ deps.onBackendResolved?.({ backend });
3657
3770
  if (agentName === "codex") {
3658
3771
  const codexRoleFile = path9.join(TEMPLATES_DIR, `crew.${agent.templateSuffix}.md`);
3659
3772
  const roleInstructions = fs9.existsSync(codexRoleFile) ? fs9.readFileSync(codexRoleFile, "utf8") : void 0;
@@ -3676,12 +3789,19 @@ async function runCrewSpawn(input, config, deps) {
3676
3789
  const interactive = agent.name === "claude" || agent.name === "opencode";
3677
3790
  const crewRole = config.defaults.roles?.crew;
3678
3791
  const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
3679
- const crewModel = input.model ?? route?.model ?? configModel;
3792
+ const crewModel = resolveRouterModel(input.model ?? route?.model ?? configModel, agent.name, config.defaults.router);
3680
3793
  const crewThinking = input.thinking ?? config.defaults.roles?.crew?.thinking;
3681
3794
  if (agentName !== "claude") {
3682
3795
  deps.onModelResolved?.({ agentName, model: crewModel });
3683
3796
  }
3684
3797
  if (agentName === "claude") {
3798
+ let routerEnv = {};
3799
+ if (backend !== "native") {
3800
+ if (!deps.routerCredentials) {
3801
+ throw new Error(`backend '${backend}' requires router credentials, but the spawn path has no daemon credentials provider`);
3802
+ }
3803
+ routerEnv = buildRouterEnv(await deps.routerCredentials({ project: input.project, backend }), crewModel);
3804
+ }
3685
3805
  ensureSocksDir();
3686
3806
  const messagingSocketPath = path9.join(CC_SOCKS_DIR, `squadrant-${randomUUID3()}.sock`);
3687
3807
  const rec = await deps.dispatchCrew({
@@ -3720,7 +3840,8 @@ async function runCrewSpawn(input, config, deps) {
3720
3840
  const title2 = titleFor(input.project, name);
3721
3841
  const pane2 = await deps.runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
3722
3842
  const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
3723
- await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${niceCrewCommand(cliCommand2)}`);
3843
+ const routerPrefix = Object.keys(routerEnv).length > 0 ? ` ${renderEnvAssignments(routerEnv)}` : "";
3844
+ await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix}${routerPrefix} ${niceCrewCommand(cliCommand2)}`);
3724
3845
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
3725
3846
  let claudeFirstTurn = firstTurnTask;
3726
3847
  if (Buffer.byteLength(claudeFirstTurn, "utf8") > FIRST_TURN_INLINE_MAX_BYTES) {
@@ -3733,7 +3854,7 @@ async function runCrewSpawn(input, config, deps) {
3733
3854
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
3734
3855
  const scrapeDelivered = sendPromise.then((r) => r.delivered).catch(() => false);
3735
3856
  const cancelHookPoll = { stopped: false };
3736
- const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id, scrapeDelivered, cancelHookPoll)) : await scrapeDelivered;
3857
+ const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id, scrapeDelivered, cancelHookPoll), FIRST_TURN_HOOK_CONFIRM_MAX_MS) : await scrapeDelivered;
3737
3858
  cancelHookPoll.stopped = true;
3738
3859
  if (!delivered) {
3739
3860
  process.stderr.write(`\u26A0\uFE0F First turn not delivered for crew '${name}' \u2014 use 'squadrant crew send ${input.project} ${name}' to re-send the task.
@@ -4021,6 +4142,8 @@ var init_crew_spawn = __esm({
4021
4142
  "packages/core/dist/crew-spawn.js"() {
4022
4143
  init_control_channel();
4023
4144
  init_dist();
4145
+ init_router_resolution();
4146
+ init_env();
4024
4147
  init_crew_routing();
4025
4148
  init_crew_protocol();
4026
4149
  init_crew_lifecycle();
@@ -4071,6 +4194,164 @@ var init_captain_channel = __esm({
4071
4194
  }
4072
4195
  });
4073
4196
 
4197
+ // packages/core/dist/captain-record.js
4198
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, realpathSync, writeFileSync as writeFileSync8 } from "fs";
4199
+ import { dirname as dirname3, join as join11 } from "path";
4200
+ function captainRecordPath(stateRoot, project) {
4201
+ return join11(stateRoot, project, "captain.json");
4202
+ }
4203
+ function readCaptainAddress(stateRoot, project) {
4204
+ try {
4205
+ const parsed = JSON.parse(readFileSync8(captainRecordPath(stateRoot, project), "utf-8"));
4206
+ return parsed && typeof parsed.agent === "string" ? parsed : null;
4207
+ } catch {
4208
+ return null;
4209
+ }
4210
+ }
4211
+ function writeCaptainAddress(stateRoot, project, addr) {
4212
+ const p = captainRecordPath(stateRoot, project);
4213
+ mkdirSync6(dirname3(p), { recursive: true });
4214
+ writeFileSync8(p, JSON.stringify(addr, null, 2) + "\n");
4215
+ }
4216
+ function realpathOrSelf(p) {
4217
+ try {
4218
+ return realpathSync(p);
4219
+ } catch {
4220
+ return p;
4221
+ }
4222
+ }
4223
+ function sameDirectory(a, b) {
4224
+ if (!a)
4225
+ return false;
4226
+ const strip = (s) => realpathOrSelf(s).replace(/\/+$/, "");
4227
+ return strip(a) === strip(b);
4228
+ }
4229
+ var init_captain_record = __esm({
4230
+ "packages/core/dist/captain-record.js"() {
4231
+ }
4232
+ });
4233
+
4234
+ // packages/core/dist/opencode-session.js
4235
+ import { execFileSync as execFileSync4 } from "child_process";
4236
+ async function listSessions(port, fetchImpl = fetch, timeoutMs = 5e3) {
4237
+ for (const path23 of ["/session", "/api/session"]) {
4238
+ const ac = new AbortController();
4239
+ const t = setTimeout(() => ac.abort(), timeoutMs);
4240
+ try {
4241
+ const res = await fetchImpl(`http://127.0.0.1:${port}${path23}`, { signal: ac.signal });
4242
+ if (!res.ok)
4243
+ continue;
4244
+ const rows = await res.json();
4245
+ if (Array.isArray(rows))
4246
+ return rows;
4247
+ } catch {
4248
+ } finally {
4249
+ clearTimeout(t);
4250
+ }
4251
+ }
4252
+ return [];
4253
+ }
4254
+ function parseLiveOpencodeServers(psOutput) {
4255
+ const out = [];
4256
+ for (const line of psOutput.split("\n")) {
4257
+ const m = line.match(/^\s*(\d+)\s+(.+)$/);
4258
+ if (!m)
4259
+ continue;
4260
+ const pid = Number(m[1]);
4261
+ const command = m[2].trim();
4262
+ const exe = command.split(/\s+/)[0] ?? "";
4263
+ if (!/(^|\/)opencode$/.test(exe))
4264
+ continue;
4265
+ const portRaw = command.match(/--port[= ](\d+)/)?.[1];
4266
+ if (!portRaw)
4267
+ continue;
4268
+ const sessionId = command.match(/--session[= ](\S+)/)?.[1];
4269
+ out.push({ pid, port: Number(portRaw), ...sessionId ? { sessionId } : {} });
4270
+ }
4271
+ return out;
4272
+ }
4273
+ function defaultPsOutput() {
4274
+ try {
4275
+ return execFileSync4("ps", ["-axo", "pid=,command="], { encoding: "utf-8", timeout: 2e3 });
4276
+ } catch {
4277
+ return "";
4278
+ }
4279
+ }
4280
+ function defaultCwdOf(pid) {
4281
+ try {
4282
+ const out = execFileSync4("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], { encoding: "utf-8", timeout: 2e3 });
4283
+ const line = out.split("\n").find((l) => l.startsWith("n"));
4284
+ return line ? line.slice(1) : null;
4285
+ } catch {
4286
+ return null;
4287
+ }
4288
+ }
4289
+ function discoverLiveOpencodeServer(opts) {
4290
+ const servers = parseLiveOpencodeServers((opts.psOutput ?? defaultPsOutput)());
4291
+ if (opts.sessionId) {
4292
+ const bySession = servers.find((s) => s.sessionId === opts.sessionId);
4293
+ if (bySession)
4294
+ return bySession;
4295
+ }
4296
+ const cwdOf = opts.cwdOf ?? defaultCwdOf;
4297
+ return servers.find((s) => sameDirectory(cwdOf(s.pid) ?? void 0, opts.directory)) ?? null;
4298
+ }
4299
+ function newestSessionInDirectory(rows, directory, createdAfterMs) {
4300
+ const hits = rows.filter((r) => sameDirectory(r.directory, directory) && (createdAfterMs === void 0 || (r.time?.created ?? 0) >= createdAfterMs));
4301
+ if (hits.length === 0)
4302
+ return null;
4303
+ return hits.reduce((a, b) => (b.time?.updated ?? 0) > (a.time?.updated ?? 0) ? b : a).id;
4304
+ }
4305
+ async function pollNewestSessionInDirectory(opts) {
4306
+ const timeoutMs = opts.timeoutMs ?? 6e4;
4307
+ const intervalMs = opts.intervalMs ?? 2e3;
4308
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4309
+ const deadline = Date.now() + timeoutMs;
4310
+ for (; ; ) {
4311
+ const id = newestSessionInDirectory(await listSessions(opts.port, opts.fetchImpl), opts.directory, opts.createdAfterMs);
4312
+ if (id)
4313
+ return id;
4314
+ if (Date.now() >= deadline)
4315
+ return null;
4316
+ await sleep3(intervalMs);
4317
+ }
4318
+ }
4319
+ async function resolveAndPersistOpencodeCaptain(opts) {
4320
+ if (opts.sessionId) {
4321
+ writeCaptainAddress(opts.stateRoot, opts.project, {
4322
+ agent: "opencode",
4323
+ port: opts.port,
4324
+ sessionId: opts.sessionId,
4325
+ directory: opts.directory,
4326
+ launchedAt: opts.launchedAt
4327
+ });
4328
+ return opts.sessionId;
4329
+ }
4330
+ const sessionId = await pollNewestSessionInDirectory({
4331
+ port: opts.port,
4332
+ directory: opts.directory,
4333
+ createdAfterMs: Date.parse(opts.launchedAt),
4334
+ timeoutMs: opts.timeoutMs,
4335
+ sleep: opts.sleep,
4336
+ fetchImpl: opts.fetchImpl
4337
+ });
4338
+ if (!sessionId)
4339
+ return null;
4340
+ writeCaptainAddress(opts.stateRoot, opts.project, {
4341
+ agent: "opencode",
4342
+ port: opts.port,
4343
+ sessionId,
4344
+ directory: opts.directory,
4345
+ launchedAt: opts.launchedAt
4346
+ });
4347
+ return sessionId;
4348
+ }
4349
+ var init_opencode_session = __esm({
4350
+ "packages/core/dist/opencode-session.js"() {
4351
+ init_captain_record();
4352
+ }
4353
+ });
4354
+
4074
4355
  // packages/core/dist/daemon/delivery-loop.js
4075
4356
  function discoverCaptainSurface(surfaces, captainTitle) {
4076
4357
  return surfaces.find((s) => s.title === captainTitle) ?? null;
@@ -4196,7 +4477,12 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4196
4477
  const cmux2 = daemonCmux;
4197
4478
  const cfg = loadConfig();
4198
4479
  const deliveries = /* @__PURE__ */ new Map();
4199
- const deliveryStats = (project) => deliveries.get(project)?.stats();
4480
+ const deliveryStats = (project) => {
4481
+ const s = deliveries.get(project)?.stats();
4482
+ if (!s)
4483
+ return s;
4484
+ return s.reason === "no-channel" ? { ...s, stuck: true } : s;
4485
+ };
4200
4486
  const lastDeferred = /* @__PURE__ */ new Map();
4201
4487
  const inFlightDelivery = () => {
4202
4488
  let worst = null;
@@ -4282,20 +4568,55 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4282
4568
  deliverEntry = { ...entry, message: `${prefix}${entry.message}` };
4283
4569
  }
4284
4570
  const result = await d.deliver(deliverEntry, async (text, sendOpts) => {
4285
- let handledByChannel = false;
4286
- try {
4287
- const mode = ctx.captainChannelMode?.() ?? "off";
4288
- const r = await deliverToCaptain(project, text, {
4289
- channel: ctx.captainChannel,
4290
- mode,
4291
- log
4292
- });
4293
- handledByChannel = r.handled;
4294
- } catch (e) {
4295
- log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4296
- }
4297
- if (handledByChannel) {
4298
- return;
4571
+ const agent = ctx.captainAgentFor?.(project);
4572
+ const picked = ctx.captainAgentFor ? agent ? ctx.captainChannels?.[agent] : void 0 : ctx.captainChannel;
4573
+ const mode = ctx.captainChannelMode?.() ?? "off";
4574
+ if (picked) {
4575
+ try {
4576
+ const r = await deliverToCaptain(project, text, { channel: picked, mode, log });
4577
+ if (r.handled)
4578
+ return;
4579
+ } catch (e) {
4580
+ log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4581
+ }
4582
+ if (agent === "opencode" && mode !== "off") {
4583
+ const rec = readCaptainAddress(stateRoot, project);
4584
+ let healed = false;
4585
+ if (rec?.port) {
4586
+ const fresh = newestSessionInDirectory(await listSessions(rec.port), rec.directory, Date.parse(rec.launchedAt));
4587
+ if (fresh && fresh !== rec.sessionId) {
4588
+ writeCaptainAddress(stateRoot, project, { ...rec, sessionId: fresh });
4589
+ healed = true;
4590
+ }
4591
+ }
4592
+ if (!healed && rec) {
4593
+ const live = discoverLiveOpencodeServer({ directory: rec.directory, sessionId: rec.sessionId });
4594
+ if (live && (live.port !== rec.port || live.sessionId && live.sessionId !== rec.sessionId)) {
4595
+ writeCaptainAddress(stateRoot, project, {
4596
+ ...rec,
4597
+ port: live.port,
4598
+ sessionId: live.sessionId ?? rec.sessionId
4599
+ });
4600
+ log(`captain-channel ${project}: re-resolved opencode address \u2192 port ${live.port}`);
4601
+ healed = true;
4602
+ }
4603
+ }
4604
+ if (healed) {
4605
+ try {
4606
+ const retry = await deliverToCaptain(project, text, { channel: picked, mode, log });
4607
+ if (retry.handled)
4608
+ return;
4609
+ } catch (e) {
4610
+ log(`captain-channel ${project}: retry after re-resolve threw \u2014 ${e.message}`);
4611
+ }
4612
+ log(`captain-channel ${project}: re-resolved address still unreachable \u2014 falling back to pane`);
4613
+ } else {
4614
+ log(`captain-channel ${project}: no live opencode server \u2014 falling back to pane`);
4615
+ }
4616
+ }
4617
+ } else if (ctx.captainAgentFor && (agent === "claude" || agent === "opencode")) {
4618
+ if (mode !== "off")
4619
+ throw new DeferDelivery(null, "no-channel");
4299
4620
  }
4300
4621
  try {
4301
4622
  return await cmux2.send(surface, text, sendOpts);
@@ -4319,20 +4640,23 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4319
4640
  lastDeferred.delete(project);
4320
4641
  projectBackoff.delete(project);
4321
4642
  } else {
4322
- const { maxDeferCount, stuck: stuck2 } = d.stats();
4643
+ const { maxDeferCount } = d.stats();
4644
+ const stuckNow = deliveryStats(project)?.stuck ?? false;
4323
4645
  if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4324
4646
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4325
4647
  }
4326
4648
  lastDeferred.set(project, { seq: entry.seq, deferCount: maxDeferCount });
4327
- if (stuck2) {
4649
+ if (stuckNow) {
4328
4650
  const streak = (projectBackoff.get(project)?.streak ?? 0) + 1;
4329
- const backoffMs = Math.min(6e4, 1e3 * 2 ** streak);
4651
+ const cap = d.stats().reason === "no-channel" ? 3e5 : 6e4;
4652
+ const backoffMs = Math.min(cap, 1e3 * 2 ** streak);
4330
4653
  projectBackoff.set(project, { nextAttemptAt: Date.now() + backoffMs, streak });
4331
4654
  }
4332
4655
  break;
4333
4656
  }
4334
4657
  }
4335
- const stuck = d.stats().stuck;
4658
+ const stats = d.stats();
4659
+ const stuck = stats.stuck || stats.reason === "no-channel";
4336
4660
  if (stuck && !stuckNotified.has(project)) {
4337
4661
  stuckNotified.add(project);
4338
4662
  const { maxDeferCount, reason } = d.stats();
@@ -4376,6 +4700,8 @@ var init_delivery_loop = __esm({
4376
4700
  init_down_alert();
4377
4701
  init_liveness2();
4378
4702
  init_captain_channel();
4703
+ init_captain_record();
4704
+ init_opencode_session();
4379
4705
  CURSOR_SUBSCRIBER = "captain";
4380
4706
  TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
4381
4707
  STUCK_ALERT_TEXT = {
@@ -4383,6 +4709,7 @@ var init_delivery_loop = __esm({
4383
4709
  modal: (n) => `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${n}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.`,
4384
4710
  draft: (n) => `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${n}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`,
4385
4711
  "probe-failed": (n) => `\u26A0\uFE0F DELIVERY STUCK: reading your captain pane failed (stale/dead surface reference or cmux unavailable) and has blocked pending notification(s) for ${n}+ retries. Delivery re-resolves the pane automatically; if this persists after a captain restart, bounce the daemon to refresh its surface references.`,
4712
+ "no-channel": (n) => `\u26A0\uFE0F CAPTAIN NOT DELIVERABLE: this project's captain has no control channel (missing launch record, or a manually opened session). Crew notifications are queued and not lost, but cannot be delivered until the captain is launched by squadrant. Run \`squadrant launch <project>\` \u2014 a manually opened \`opencode -c\` cannot receive lifecycle notifications. (blocked for ${n}+ retries)`,
4386
4713
  stable: (n) => `\u26A0\uFE0F DELIVERY STUCK: pending notification(s) have been blocked for ${n}+ retries. This keeps retrying safely and will deliver automatically once the blocker clears.`,
4387
4714
  unknown: (n) => `\u26A0\uFE0F DELIVERY STUCK: pending notification(s) have been blocked for ${n}+ retries. This keeps retrying safely and will deliver automatically once the blocker clears.`
4388
4715
  };
@@ -4410,6 +4737,32 @@ var init_gates = __esm({
4410
4737
  }
4411
4738
  });
4412
4739
 
4740
+ // packages/core/dist/router/credentials.js
4741
+ function buildRouterCredentialsRequest(project, backend) {
4742
+ return { kind: "router-credentials", project, backend };
4743
+ }
4744
+ async function resolveRouterCredentials(service, project, backend, deps = {}) {
4745
+ if (backend === "proxy") {
4746
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4747
+ const attempts = deps.attempts ?? ROUTER_READY_ATTEMPTS;
4748
+ const delayMs = deps.delayMs ?? ROUTER_READY_DELAY_MS;
4749
+ for (let i = 0; i < attempts; i++) {
4750
+ const health = await service.health();
4751
+ if (health.ready)
4752
+ break;
4753
+ await sleep3(delayMs);
4754
+ }
4755
+ }
4756
+ return service.credentialsFor(project, backend);
4757
+ }
4758
+ var ROUTER_READY_ATTEMPTS, ROUTER_READY_DELAY_MS;
4759
+ var init_credentials = __esm({
4760
+ "packages/core/dist/router/credentials.js"() {
4761
+ ROUTER_READY_ATTEMPTS = 10;
4762
+ ROUTER_READY_DELAY_MS = 200;
4763
+ }
4764
+ });
4765
+
4413
4766
  // packages/core/dist/daemon/server.js
4414
4767
  function createServer2(ctx, handlers) {
4415
4768
  const { store, log, attachConns } = ctx;
@@ -4435,6 +4788,13 @@ function createServer2(ctx, handlers) {
4435
4788
  if (msg.kind === "event") {
4436
4789
  return ctx.d.handle(msg);
4437
4790
  }
4791
+ if (msg.kind === "router-credentials") {
4792
+ const service = ctx.routerService;
4793
+ if (!service) {
4794
+ throw new Error("router backend selected but squadrantd has no router service \u2014 configure defaults.router and restart the daemon");
4795
+ }
4796
+ return resolveRouterCredentials(service, msg.project, msg.backend);
4797
+ }
4438
4798
  return ctx.d.handle(msg);
4439
4799
  },
4440
4800
  onAttach: (conn, frame) => {
@@ -4470,18 +4830,19 @@ function createServer2(ctx, handlers) {
4470
4830
  var init_server = __esm({
4471
4831
  "packages/core/dist/daemon/server.js"() {
4472
4832
  init_protocol();
4833
+ init_credentials();
4473
4834
  }
4474
4835
  });
4475
4836
 
4476
4837
  // packages/core/dist/daemon/exit-marker.js
4477
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
4478
- import { join as join11 } from "path";
4838
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
4839
+ import { join as join12 } from "path";
4479
4840
  function exitMarkerPath(stateRoot) {
4480
- return join11(stateRoot, "exit-marker.json");
4841
+ return join12(stateRoot, "exit-marker.json");
4481
4842
  }
4482
4843
  function writeExitMarker(stateRoot, marker, log) {
4483
4844
  try {
4484
- writeFileSync8(exitMarkerPath(stateRoot), JSON.stringify(marker));
4845
+ writeFileSync9(exitMarkerPath(stateRoot), JSON.stringify(marker));
4485
4846
  } catch (e) {
4486
4847
  log(`exit marker write failed: ${e.message}`);
4487
4848
  }
@@ -4492,7 +4853,7 @@ function consumeExitMarker(stateRoot, now = Date.now) {
4492
4853
  return { marker: null };
4493
4854
  let marker = null;
4494
4855
  try {
4495
- marker = JSON.parse(readFileSync8(p, "utf-8"));
4856
+ marker = JSON.parse(readFileSync9(p, "utf-8"));
4496
4857
  } catch {
4497
4858
  marker = null;
4498
4859
  }
@@ -4506,18 +4867,18 @@ function consumeExitMarker(stateRoot, now = Date.now) {
4506
4867
  return { marker, gapMs };
4507
4868
  }
4508
4869
  function runningMarkerPath(stateRoot) {
4509
- return join11(stateRoot, "running-marker.json");
4870
+ return join12(stateRoot, "running-marker.json");
4510
4871
  }
4511
4872
  function writeRunningMarker(stateRoot, marker, log) {
4512
4873
  try {
4513
- writeFileSync8(runningMarkerPath(stateRoot), JSON.stringify(marker));
4874
+ writeFileSync9(runningMarkerPath(stateRoot), JSON.stringify(marker));
4514
4875
  } catch (e) {
4515
4876
  log(`running marker write failed: ${e.message}`);
4516
4877
  }
4517
4878
  }
4518
4879
  function readRunningMarker(stateRoot) {
4519
4880
  try {
4520
- return JSON.parse(readFileSync8(runningMarkerPath(stateRoot), "utf-8"));
4881
+ return JSON.parse(readFileSync9(runningMarkerPath(stateRoot), "utf-8"));
4521
4882
  } catch {
4522
4883
  return null;
4523
4884
  }
@@ -4537,8 +4898,8 @@ var init_exit_marker = __esm({
4537
4898
 
4538
4899
  // packages/core/dist/daemon/snapshot-gather.js
4539
4900
  import { fileURLToPath as fileURLToPath2 } from "url";
4540
- import { join as join12 } from "path";
4541
- import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
4901
+ import { join as join13 } from "path";
4902
+ import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
4542
4903
  function distBuiltAt() {
4543
4904
  try {
4544
4905
  return statSync3(SELF_PATH).mtimeMs;
@@ -4546,10 +4907,10 @@ function distBuiltAt() {
4546
4907
  return 0;
4547
4908
  }
4548
4909
  }
4549
- function gatherLogStats(path21, now, windowMs) {
4910
+ function gatherLogStats(path23, now, windowMs) {
4550
4911
  let sizeBytes = 0;
4551
4912
  try {
4552
- sizeBytes = statSync3(path21).size;
4913
+ sizeBytes = statSync3(path23).size;
4553
4914
  } catch {
4554
4915
  return { errorCount: 0, sizeBytes: 0, windowMs };
4555
4916
  }
@@ -4560,7 +4921,7 @@ function gatherLogStats(path21, now, windowMs) {
4560
4921
  const len = sizeBytes - start;
4561
4922
  let text = "";
4562
4923
  try {
4563
- const fd = openSync2(path21, "r");
4924
+ const fd = openSync2(path23, "r");
4564
4925
  try {
4565
4926
  const buf = Buffer.alloc(len);
4566
4927
  readSync(fd, buf, 0, len, start);
@@ -4591,7 +4952,7 @@ function gatherStoreStats(store, stateRoot, project) {
4591
4952
  for (const r of store.list(project))
4592
4953
  byState[r.state] = (byState[r.state] ?? 0) + 1;
4593
4954
  let corruptCount = 0;
4594
- const dir = join12(stateRoot, project);
4955
+ const dir = join13(stateRoot, project);
4595
4956
  try {
4596
4957
  for (const n of readdirSync3(dir)) {
4597
4958
  if (n.includes(".corrupt.")) {
@@ -4601,7 +4962,7 @@ function gatherStoreStats(store, stateRoot, project) {
4601
4962
  if (!n.endsWith(".json"))
4602
4963
  continue;
4603
4964
  try {
4604
- JSON.parse(readFileSync9(join12(dir, n), "utf-8"));
4965
+ JSON.parse(readFileSync10(join13(dir, n), "utf-8"));
4605
4966
  } catch {
4606
4967
  corruptCount++;
4607
4968
  }
@@ -4616,7 +4977,7 @@ function gatherResults(resultsDir) {
4616
4977
  try {
4617
4978
  for (const n of readdirSync3(resultsDir)) {
4618
4979
  try {
4619
- const s = statSync3(join12(resultsDir, n));
4980
+ const s = statSync3(join13(resultsDir, n));
4620
4981
  if (s.isFile()) {
4621
4982
  fileCount++;
4622
4983
  totalBytes += s.size;
@@ -4636,7 +4997,7 @@ var init_snapshot_gather = __esm({
4636
4997
  });
4637
4998
 
4638
4999
  // packages/core/dist/daemon/start.js
4639
- import { join as join13, dirname as dirname3 } from "path";
5000
+ import { join as join14, dirname as dirname4 } from "path";
4640
5001
  import { readdir } from "fs/promises";
4641
5002
  function startDaemon(ctx, opts, pkgVersion) {
4642
5003
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
@@ -4713,7 +5074,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4713
5074
  return out;
4714
5075
  }
4715
5076
  async function gatherSnapshotInputs(now) {
4716
- const logPath2 = join13(dirname3(stateRoot), "squadrantd.log");
5077
+ const logPath2 = join14(dirname4(stateRoot), "squadrantd.log");
4717
5078
  const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4718
5079
  const projects = await Promise.all(tier2Projects.map(async (project) => {
4719
5080
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
@@ -4790,6 +5151,9 @@ function startDaemon(ctx, opts, pkgVersion) {
4790
5151
  log(`telegram bridge start failed: ${e.message}`);
4791
5152
  }
4792
5153
  }
5154
+ if (ctx.routerService) {
5155
+ void ctx.routerService.start().then(() => log(`router: listening ${ctx.routerService.url()}`)).catch((e) => log(`router service start failed: ${e.message}`));
5156
+ }
4793
5157
  const autoConfigSafe = !!opts.runCmuxAutoConfig || !process.env.VITEST;
4794
5158
  if (autoConfigSafe) {
4795
5159
  try {
@@ -4875,7 +5239,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4875
5239
  let rotationTimer;
4876
5240
  let rotationTick;
4877
5241
  if (rotationInterval > 0) {
4878
- const inboxPath = join13(stateRoot, "inbox");
5242
+ const inboxPath = join14(stateRoot, "inbox");
4879
5243
  rotationTick = async () => {
4880
5244
  try {
4881
5245
  let entries;
@@ -4899,7 +5263,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4899
5263
  rotationTimer.unref?.();
4900
5264
  }
4901
5265
  return {
4902
- stop(reason = "requested") {
5266
+ async stop(reason = "requested") {
4903
5267
  const ppid = process.ppid;
4904
5268
  const uptimeMs = Math.round(process.uptime() * 1e3);
4905
5269
  const inFlight = inFlightDelivery();
@@ -4928,6 +5292,10 @@ function startDaemon(ctx, opts, pkgVersion) {
4928
5292
  }
4929
5293
  for (const kill of ctx.activeHeadlessKills)
4930
5294
  kill();
5295
+ try {
5296
+ await ctx.routerService?.stop();
5297
+ } catch {
5298
+ }
4931
5299
  return new Promise((resolve4) => server.close(() => {
4932
5300
  log(`exit-complete pid=${process.pid}`);
4933
5301
  resolve4();
@@ -5942,7 +6310,7 @@ var init_bridge = __esm({
5942
6310
  });
5943
6311
 
5944
6312
  // packages/core/dist/restart-daemon.js
5945
- import { execFileSync as execFileSync4 } from "child_process";
6313
+ import { execFileSync as execFileSync5 } from "child_process";
5946
6314
  import { existsSync as existsSync10 } from "fs";
5947
6315
  function defaultIsRunning() {
5948
6316
  return existsSync10(DAEMON_SOCK_PATH);
@@ -5952,7 +6320,7 @@ function defaultRunKickstart() {
5952
6320
  const target = `gui/${uid}/${LABEL}`;
5953
6321
  if (tryAcquireDaemonLock()) {
5954
6322
  try {
5955
- execFileSync4("launchctl", kickstartArgv(target, true), { stdio: "ignore" });
6323
+ execFileSync5("launchctl", kickstartArgv(target, true), { stdio: "ignore" });
5956
6324
  } finally {
5957
6325
  releaseDaemonLock();
5958
6326
  }
@@ -6174,129 +6542,675 @@ var init_telegram = __esm({
6174
6542
  }
6175
6543
  });
6176
6544
 
6177
- // packages/core/dist/group-dispatch.js
6178
- import { randomUUID as randomUUID4 } from "crypto";
6179
- function resolveCurrentProject(config) {
6180
- const cwd = process.cwd();
6181
- for (const [name, proj] of Object.entries(config.projects)) {
6182
- const resolvedPath = resolveHome(proj.path);
6183
- if (cwd.startsWith(resolvedPath))
6184
- return name;
6545
+ // packages/core/dist/router/types.js
6546
+ var init_types = __esm({
6547
+ "packages/core/dist/router/types.js"() {
6185
6548
  }
6186
- return null;
6549
+ });
6550
+
6551
+ // packages/core/dist/router/auth.js
6552
+ import { randomBytes as randomBytes2 } from "crypto";
6553
+ function mintToken() {
6554
+ return randomBytes2(32).toString("base64url");
6187
6555
  }
6188
- async function isCaptainAlive(project, sockPath = DAEMON_SOCK_PATH) {
6189
- try {
6190
- const health = await sendRequest(sockPath, { kind: "health", project }, 5e3);
6191
- const captain = health?.find((h) => h.kind === "captain" && h.project === project);
6192
- return captain?.state === "alive";
6193
- } catch {
6194
- return false;
6195
- }
6556
+ function parseBearer(header) {
6557
+ if (!header)
6558
+ return null;
6559
+ const m = /^Bearer\s+(.+)$/i.exec(header.trim());
6560
+ return m ? m[1].trim() : null;
6196
6561
  }
6197
- async function waitForWarmup(project, sockPath = DAEMON_SOCK_PATH, timeoutMs = GROUP_DISPATCH_WARMUP_TIMEOUT_MS, pollMs = GROUP_DISPATCH_WARMUP_POLL_MS) {
6198
- const deadline = Date.now() + timeoutMs;
6199
- while (Date.now() < deadline) {
6200
- if (await isCaptainAlive(project, sockPath))
6201
- return true;
6202
- await new Promise((r) => setTimeout(r, pollMs));
6203
- }
6204
- return false;
6562
+ function resolveProject(tokens, header) {
6563
+ const token = parseBearer(header);
6564
+ if (!token)
6565
+ return null;
6566
+ return tokens.get(token) ?? null;
6205
6567
  }
6206
- async function dispatchToSibling(opts) {
6207
- const config = loadConfig();
6208
- const fromCfg = config.projects[opts.fromProject];
6209
- const toCfg = config.projects[opts.toProject];
6210
- if (!toCfg) {
6211
- throw new Error(`target project '${opts.toProject}' not found in config`);
6568
+ var init_auth2 = __esm({
6569
+ "packages/core/dist/router/auth.js"() {
6212
6570
  }
6213
- const sameGroup = !!fromCfg?.group && !!toCfg.group && fromCfg.group === toCfg.group;
6214
- if (toCfg.acceptDelegations === false) {
6215
- throw new Error(`cannot dispatch to '${opts.toProject}': project has acceptDelegations set to false`);
6571
+ });
6572
+
6573
+ // packages/core/dist/router/errors.js
6574
+ function anthropicError(status, type, message) {
6575
+ return { status, body: { type: "error", error: { type, message } } };
6576
+ }
6577
+ var init_errors = __esm({
6578
+ "packages/core/dist/router/errors.js"() {
6216
6579
  }
6217
- const sockPath = opts.sockPath ?? DAEMON_SOCK_PATH;
6218
- const alive = await isCaptainAlive(opts.toProject, sockPath);
6219
- if (!alive) {
6220
- if (!sameGroup) {
6221
- throw new Error(`cannot dispatch to '${opts.toProject}': captain is not running and cross-group dispatch does not auto-boot it. Use 'squadrant ping ${opts.toProject} "<msg>"' or start it manually with 'squadrant launch ${opts.toProject}', then retry.`);
6222
- }
6223
- if (opts.bootCaptain) {
6224
- await opts.bootCaptain(opts.toProject);
6225
- }
6226
- const warmed = await waitForWarmup(opts.toProject, sockPath, opts.warmupTimeoutMs, opts.warmupPollMs);
6227
- if (!warmed) {
6228
- throw new Error(`dispatch to '${opts.toProject}' timed out waiting for captain warmup (>${(opts.warmupTimeoutMs ?? GROUP_DISPATCH_WARMUP_TIMEOUT_MS) / 1e3}s)`);
6580
+ });
6581
+
6582
+ // packages/core/dist/router/sanitize.js
6583
+ function isObj(v2) {
6584
+ return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
6585
+ }
6586
+ function sanitizeRequest(body, upstream) {
6587
+ const out = { ...body };
6588
+ if (!upstream.isAnthropic) {
6589
+ for (const f of ANTHROPIC_ONLY_REQUEST_FIELDS)
6590
+ delete out[f];
6591
+ if (Array.isArray(out.tools)) {
6592
+ out.tools = out.tools.filter((t) => !(isObj(t) && typeof t.type === "string" && SERVER_TOOL_TYPE_RE.test(t.type)));
6229
6593
  }
6230
6594
  }
6231
- const now = Date.now();
6232
- const attemptId = randomUUID4();
6233
- const record = {
6234
- id: randomUUID4(),
6235
- project: opts.toProject,
6236
- originProject: opts.fromProject,
6237
- provider: opts.provider ?? "claude",
6238
- mode: opts.mode ?? "headless",
6239
- state: "submitted",
6240
- task: opts.task,
6241
- createdAt: now,
6242
- lastHeartbeat: now,
6243
- lastEvent: "dispatch",
6244
- heartbeatBudgetMs: 3e5,
6245
- attempts: [{ attemptId, startedAt: now, lastHeartbeatAt: now }]
6246
- };
6247
- const result = await sendRequest(sockPath, { kind: "dispatch", record });
6248
- return result;
6595
+ return out;
6249
6596
  }
6250
- var GROUP_DISPATCH_WARMUP_TIMEOUT_MS, GROUP_DISPATCH_WARMUP_POLL_MS;
6251
- var init_group_dispatch = __esm({
6252
- "packages/core/dist/group-dispatch.js"() {
6253
- init_dist();
6254
- init_protocol();
6255
- GROUP_DISPATCH_WARMUP_TIMEOUT_MS = 12e4;
6256
- GROUP_DISPATCH_WARMUP_POLL_MS = 1e3;
6597
+ var SERVER_TOOL_TYPE_RE, ANTHROPIC_ONLY_REQUEST_FIELDS;
6598
+ var init_sanitize = __esm({
6599
+ "packages/core/dist/router/sanitize.js"() {
6600
+ SERVER_TOOL_TYPE_RE = /^(bash|text_editor|str_replace_editor|computer|web_search|code_execution|memory)_\d+/;
6601
+ ANTHROPIC_ONLY_REQUEST_FIELDS = ["container", "context_management", "mcp_servers"];
6257
6602
  }
6258
6603
  });
6259
6604
 
6260
- // packages/core/dist/launch-workspace.js
6261
- async function deliverStartupPrompt(runtime, refId, prompt, opts = {}) {
6262
- const classify = opts.classifyScreen ?? (() => "idle");
6263
- const readyTimeoutMs = opts.readyTimeoutMs ?? 3e4;
6264
- const settleMs = opts.settleMs ?? 2500;
6265
- const pollMs = opts.pollMs ?? 1e3;
6266
- const maxAttempts = opts.maxAttempts ?? 3;
6267
- const sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
6268
- const read = async () => runtime.readScreen(refId).catch(() => "");
6269
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
6270
- const deadline = Date.now() + readyTimeoutMs;
6271
- let preSend = await read();
6272
- let state = classify(preSend);
6273
- while (state === "loading" && Date.now() < deadline) {
6274
- await sleep3(pollMs);
6275
- preSend = await read();
6276
- state = classify(preSend);
6277
- }
6278
- if (state === "working")
6279
- return;
6280
- await runtime.send(refId, prompt).catch(() => {
6281
- });
6282
- if (state === "loading")
6283
- return;
6284
- await sleep3(settleMs);
6285
- const after = await read();
6286
- if (after !== preSend)
6287
- return;
6605
+ // packages/core/dist/router/stream.js
6606
+ import { Transform } from "stream";
6607
+ import { StringDecoder } from "string_decoder";
6608
+ function num(v2) {
6609
+ if (typeof v2 === "number")
6610
+ return Number.isFinite(v2) ? v2 : void 0;
6611
+ if (typeof v2 === "string" && v2.trim() !== "") {
6612
+ const n = Number(v2);
6613
+ return Number.isFinite(n) ? n : void 0;
6288
6614
  }
6615
+ return void 0;
6289
6616
  }
6290
- async function bootWorkspace(opts) {
6291
- const { runtime, workspaceName, agentCmd, cwd, navigate = false, forceFresh = false, pinToTop = false, initialPrompt } = opts;
6292
- const existing = await runtime.status(workspaceName);
6293
- if (existing && forceFresh) {
6294
- opts.onStoppingStale?.(workspaceName);
6295
- await runtime.stop(existing.id);
6296
- } else if (existing) {
6297
- opts.onAlreadyExists?.(workspaceName);
6298
- opts.selectWorkspace?.(existing.id);
6299
- return;
6617
+ function absorb(evt, acc) {
6618
+ if (typeof evt !== "object" || evt === null)
6619
+ return false;
6620
+ const e = evt;
6621
+ const message = typeof e.message === "object" && e.message !== null ? e.message : void 0;
6622
+ const usage2 = e.usage ?? message?.usage;
6623
+ let saw = false;
6624
+ if (usage2) {
6625
+ const input = num(usage2.input_tokens);
6626
+ if (input !== void 0) {
6627
+ acc.inputTokens = input;
6628
+ saw = true;
6629
+ }
6630
+ const output = num(usage2.output_tokens);
6631
+ if (output !== void 0) {
6632
+ acc.outputTokens = output;
6633
+ saw = true;
6634
+ }
6635
+ const read = num(usage2.cache_read_input_tokens);
6636
+ if (read !== void 0) {
6637
+ acc.cacheReadTokens = read;
6638
+ saw = true;
6639
+ }
6640
+ const write = num(usage2.cache_creation_input_tokens);
6641
+ if (write !== void 0) {
6642
+ acc.cacheWriteTokens = write;
6643
+ saw = true;
6644
+ }
6645
+ if (usage2.cost !== void 0) {
6646
+ const cost = num(usage2.cost);
6647
+ if (cost !== void 0) {
6648
+ acc.costUsd = cost;
6649
+ saw = true;
6650
+ }
6651
+ }
6652
+ }
6653
+ if (e.cost !== void 0) {
6654
+ const cost = num(e.cost);
6655
+ if (cost !== void 0) {
6656
+ acc.costUsd = cost;
6657
+ saw = true;
6658
+ }
6659
+ }
6660
+ return saw;
6661
+ }
6662
+ function scan(bufRef, acc) {
6663
+ let saw = false;
6664
+ let idx;
6665
+ while ((idx = bufRef.value.indexOf("\n")) !== -1) {
6666
+ const line = bufRef.value.slice(0, idx).trim();
6667
+ bufRef.value = bufRef.value.slice(idx + 1);
6668
+ if (line.startsWith("data:")) {
6669
+ const payload = line.slice(5).trim();
6670
+ if (payload && payload !== "[DONE]") {
6671
+ try {
6672
+ if (absorb(JSON.parse(payload), acc))
6673
+ saw = true;
6674
+ } catch {
6675
+ }
6676
+ }
6677
+ }
6678
+ }
6679
+ return saw;
6680
+ }
6681
+ function createUsageTee(project, onUsage) {
6682
+ const buf = { value: "" };
6683
+ const acc = { project };
6684
+ const decoder = new StringDecoder("utf8");
6685
+ let sawUsage = false;
6686
+ return new Transform({
6687
+ transform(chunk, _enc, cb) {
6688
+ buf.value += decoder.write(chunk);
6689
+ sawUsage = scan(buf, acc) || sawUsage;
6690
+ cb(null, chunk);
6691
+ },
6692
+ flush(cb) {
6693
+ buf.value += decoder.end();
6694
+ if (buf.value.trim() !== "") {
6695
+ buf.value += "\n";
6696
+ sawUsage = scan(buf, acc) || sawUsage;
6697
+ }
6698
+ if (sawUsage) {
6699
+ try {
6700
+ onUsage(acc);
6701
+ } catch {
6702
+ }
6703
+ }
6704
+ cb();
6705
+ }
6706
+ });
6707
+ }
6708
+ function usageFromJson(project, body) {
6709
+ const acc = { project };
6710
+ return absorb({ usage: body.usage, cost: body.cost }, acc) ? acc : void 0;
6711
+ }
6712
+ var init_stream = __esm({
6713
+ "packages/core/dist/router/stream.js"() {
6714
+ }
6715
+ });
6716
+
6717
+ // packages/core/dist/router/shim.js
6718
+ import { createServer as createServer3 } from "http";
6719
+ import { Readable } from "stream";
6720
+ function joinUrl(base, path23) {
6721
+ return base.replace(/\/+$/, "") + path23;
6722
+ }
6723
+ async function readBody(req) {
6724
+ const chunks = [];
6725
+ for await (const c of req)
6726
+ chunks.push(c);
6727
+ return Buffer.concat(chunks).toString("utf8");
6728
+ }
6729
+ function writeJson(res, status, body) {
6730
+ res.writeHead(status, { "content-type": "application/json" });
6731
+ res.end(JSON.stringify(body));
6732
+ }
6733
+ function buildUpstreamHeaders(req, upstream, stream) {
6734
+ const headers = {
6735
+ "content-type": "application/json",
6736
+ accept: stream ? "text/event-stream" : "application/json"
6737
+ };
6738
+ for (const [k, v2] of Object.entries(req.headers)) {
6739
+ const key = k.toLowerCase();
6740
+ if (key.startsWith("anthropic-") && typeof v2 === "string")
6741
+ headers[key] = v2;
6742
+ }
6743
+ for (const [k, v2] of Object.entries(upstream.extraHeaders ?? {}))
6744
+ headers[k.toLowerCase()] = v2;
6745
+ const authName = (upstream.authHeader ?? DEFAULT_AUTH_HEADER).toLowerCase();
6746
+ headers[authName] = authName === "authorization" ? `Bearer ${upstream.apiKey}` : upstream.apiKey;
6747
+ if (!headers["anthropic-version"])
6748
+ headers["anthropic-version"] = "2023-06-01";
6749
+ return headers;
6750
+ }
6751
+ function createRouterShim(opts) {
6752
+ const fetchImpl = opts.fetch ?? fetch;
6753
+ const log = opts.log ?? (() => {
6754
+ });
6755
+ let server;
6756
+ let starting;
6757
+ let boundPort = 0;
6758
+ let lastError;
6759
+ let upstreamReachable = true;
6760
+ const upstreamUrl = joinUrl(opts.upstream.baseUrl, "/v1/messages");
6761
+ function emitUsage(u) {
6762
+ try {
6763
+ opts.onUsage?.(u);
6764
+ } catch {
6765
+ }
6766
+ }
6767
+ async function handleMessages(req, res) {
6768
+ const project = resolveProject(opts.projectTokens, req.headers.authorization);
6769
+ if (!project) {
6770
+ const e = anthropicError(401, "authentication_error", "invalid squadrant router token");
6771
+ writeJson(res, e.status, e.body);
6772
+ return;
6773
+ }
6774
+ let parsed;
6775
+ try {
6776
+ parsed = JSON.parse(await readBody(req));
6777
+ } catch {
6778
+ const e = anthropicError(400, "invalid_request_error", "malformed JSON body");
6779
+ writeJson(res, e.status, e.body);
6780
+ return;
6781
+ }
6782
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
6783
+ const e = anthropicError(400, "invalid_request_error", "request body must be a JSON object");
6784
+ writeJson(res, e.status, e.body);
6785
+ return;
6786
+ }
6787
+ const body = parsed;
6788
+ const wantsStream = body.stream === true;
6789
+ const sanitized = sanitizeRequest(body, opts.upstream);
6790
+ let upstreamRes;
6791
+ try {
6792
+ upstreamRes = await fetchImpl(upstreamUrl, {
6793
+ method: "POST",
6794
+ headers: buildUpstreamHeaders(req, opts.upstream, wantsStream),
6795
+ body: JSON.stringify(sanitized)
6796
+ });
6797
+ } catch (err) {
6798
+ upstreamReachable = false;
6799
+ lastError = err instanceof Error ? err.message : String(err);
6800
+ const e = anthropicError(502, "api_error", `router upstream unreachable: ${lastError}`);
6801
+ writeJson(res, e.status, e.body);
6802
+ return;
6803
+ }
6804
+ upstreamReachable = true;
6805
+ if (!upstreamRes.ok) {
6806
+ const text2 = await upstreamRes.text();
6807
+ log(`router upstream ${upstreamRes.status}: ${text2.slice(0, 500)}`);
6808
+ let message = text2.slice(0, 500);
6809
+ try {
6810
+ const parsedErr = JSON.parse(text2);
6811
+ message = parsedErr.error?.message ?? message;
6812
+ } catch {
6813
+ }
6814
+ if (upstreamRes.status >= 500) {
6815
+ upstreamReachable = false;
6816
+ lastError = message || "upstream error";
6817
+ const e2 = anthropicError(502, "api_error", lastError);
6818
+ writeJson(res, e2.status, e2.body);
6819
+ return;
6820
+ }
6821
+ const e = anthropicError(upstreamRes.status, "api_error", message || "upstream error");
6822
+ writeJson(res, e.status, e.body);
6823
+ return;
6824
+ }
6825
+ if (wantsStream && upstreamRes.body) {
6826
+ res.writeHead(upstreamRes.status, {
6827
+ "content-type": upstreamRes.headers.get("content-type") || "text/event-stream",
6828
+ "cache-control": "no-cache",
6829
+ connection: "keep-alive"
6830
+ });
6831
+ const nodeStream = Readable.fromWeb(upstreamRes.body);
6832
+ nodeStream.on("error", (err) => {
6833
+ log(`router upstream stream error: ${err instanceof Error ? err.message : String(err)}`);
6834
+ try {
6835
+ res.end();
6836
+ } catch {
6837
+ }
6838
+ });
6839
+ const tee = createUsageTee(project, emitUsage);
6840
+ tee.on("error", (err) => {
6841
+ log(`router usage tee error: ${err instanceof Error ? err.message : String(err)}`);
6842
+ });
6843
+ nodeStream.pipe(tee).pipe(res);
6844
+ res.on("close", () => nodeStream.destroy());
6845
+ return;
6846
+ }
6847
+ const text = await upstreamRes.text();
6848
+ let upstreamBody;
6849
+ try {
6850
+ upstreamBody = JSON.parse(text);
6851
+ } catch {
6852
+ const e = anthropicError(502, "api_error", "upstream returned non-JSON");
6853
+ writeJson(res, e.status, e.body);
6854
+ return;
6855
+ }
6856
+ const usage2 = usageFromJson(project, upstreamBody);
6857
+ if (usage2)
6858
+ emitUsage(usage2);
6859
+ writeJson(res, upstreamRes.status, upstreamBody);
6860
+ }
6861
+ function handler(req, res) {
6862
+ if (req.method === "POST" && req.url === "/v1/messages") {
6863
+ void handleMessages(req, res).catch((err) => {
6864
+ log(`router internal error: ${err instanceof Error ? err.message : String(err)}`);
6865
+ if (!res.headersSent) {
6866
+ const e = anthropicError(502, "api_error", "router internal error");
6867
+ writeJson(res, e.status, e.body);
6868
+ } else {
6869
+ res.end();
6870
+ }
6871
+ });
6872
+ return;
6873
+ }
6874
+ if (req.method === "GET" && req.url === "/healthz") {
6875
+ writeJson(res, 200, { ready: server?.listening === true, upstreamReachable, lastError });
6876
+ return;
6877
+ }
6878
+ writeJson(res, 404, anthropicError(404, "not_found_error", "not found").body);
6879
+ }
6880
+ return {
6881
+ async start() {
6882
+ if (server)
6883
+ return;
6884
+ if (starting)
6885
+ return starting;
6886
+ starting = new Promise((resolve4, reject) => {
6887
+ const s = createServer3(handler);
6888
+ const onStartupError = (err) => {
6889
+ s.off("error", onStartupError);
6890
+ reject(err);
6891
+ };
6892
+ s.once("error", onStartupError);
6893
+ s.listen(opts.port ?? 0, opts.host ?? "127.0.0.1", () => {
6894
+ s.off("error", onStartupError);
6895
+ s.on("error", (err) => {
6896
+ lastError = err instanceof Error ? err.message : String(err);
6897
+ log(`router server error: ${lastError}`);
6898
+ });
6899
+ server = s;
6900
+ const addr = s.address();
6901
+ boundPort = typeof addr === "object" && addr ? addr.port : 0;
6902
+ resolve4();
6903
+ });
6904
+ });
6905
+ try {
6906
+ await starting;
6907
+ } finally {
6908
+ starting = void 0;
6909
+ }
6910
+ },
6911
+ async stop() {
6912
+ try {
6913
+ await starting;
6914
+ } catch {
6915
+ }
6916
+ const s = server;
6917
+ if (!s)
6918
+ return;
6919
+ server = void 0;
6920
+ boundPort = 0;
6921
+ s.closeAllConnections?.();
6922
+ await new Promise((resolve4) => s.close(() => resolve4()));
6923
+ },
6924
+ url() {
6925
+ return `http://${opts.host ?? "127.0.0.1"}:${boundPort}`;
6926
+ },
6927
+ async health() {
6928
+ return { ready: server?.listening === true, upstreamReachable, lastError };
6929
+ }
6930
+ };
6931
+ }
6932
+ var DEFAULT_AUTH_HEADER;
6933
+ var init_shim = __esm({
6934
+ "packages/core/dist/router/shim.js"() {
6935
+ init_auth2();
6936
+ init_errors();
6937
+ init_sanitize();
6938
+ init_stream();
6939
+ DEFAULT_AUTH_HEADER = "Authorization";
6940
+ }
6941
+ });
6942
+
6943
+ // packages/core/dist/router/service.js
6944
+ function resolveRouterUpstream(router, env = process.env) {
6945
+ const credential = router.apiKey ?? (router.apiKeyEnv ? env[router.apiKeyEnv] : void 0) ?? "";
6946
+ const authHeader = router.authHeader ?? (router.kind === "opencode-go" ? "x-api-key" : "Authorization");
6947
+ return {
6948
+ baseUrl: router.baseUrl,
6949
+ apiKey: credential,
6950
+ authHeader,
6951
+ extraHeaders: router.extraHeaders,
6952
+ isAnthropic: router.isAnthropic ?? false
6953
+ };
6954
+ }
6955
+ function createRouterService(router, projects, deps = {}) {
6956
+ const log = deps.log ?? (() => {
6957
+ });
6958
+ const upstream = resolveRouterUpstream(router);
6959
+ if (!upstream.apiKey) {
6960
+ log("router: no credential configured (set defaults.router.apiKey or apiKeyEnv) \u2014 routed spawns will fail");
6961
+ }
6962
+ const tokenByProject = /* @__PURE__ */ new Map();
6963
+ const projectTokens = /* @__PURE__ */ new Map();
6964
+ for (const p of projects) {
6965
+ const t = mintToken();
6966
+ tokenByProject.set(p, t);
6967
+ projectTokens.set(t, p);
6968
+ }
6969
+ const shim = createRouterShim({
6970
+ upstream,
6971
+ projectTokens,
6972
+ port: router.port ?? 0,
6973
+ log,
6974
+ ...deps.fetch ? { fetch: deps.fetch } : {}
6975
+ });
6976
+ let started = false;
6977
+ return {
6978
+ async start() {
6979
+ await shim.start();
6980
+ started = true;
6981
+ },
6982
+ async stop() {
6983
+ await shim.stop();
6984
+ started = false;
6985
+ },
6986
+ url: () => {
6987
+ if (!started)
6988
+ throw new Error("router service not started");
6989
+ return shim.url();
6990
+ },
6991
+ health: () => shim.health(),
6992
+ credentialsFor(project, backend) {
6993
+ if (!upstream.apiKey) {
6994
+ throw new Error("defaults.router credential is missing (set apiKey or apiKeyEnv)");
6995
+ }
6996
+ if (backend === "direct") {
6997
+ return { backend, baseUrl: router.baseUrl, apiKey: upstream.apiKey, extraHeaders: upstream.extraHeaders };
6998
+ }
6999
+ if (!started) {
7000
+ throw new Error("router service not started");
7001
+ }
7002
+ const token = tokenByProject.get(project);
7003
+ if (!token) {
7004
+ throw new Error(`router service has no token for project '${project}'`);
7005
+ }
7006
+ return { backend, baseUrl: shim.url(), token };
7007
+ }
7008
+ };
7009
+ }
7010
+ var init_service = __esm({
7011
+ "packages/core/dist/router/service.js"() {
7012
+ init_auth2();
7013
+ init_shim();
7014
+ }
7015
+ });
7016
+
7017
+ // packages/core/dist/router/index.js
7018
+ var init_router = __esm({
7019
+ "packages/core/dist/router/index.js"() {
7020
+ init_types();
7021
+ init_auth2();
7022
+ init_errors();
7023
+ init_sanitize();
7024
+ init_stream();
7025
+ init_shim();
7026
+ init_service();
7027
+ init_env();
7028
+ init_credentials();
7029
+ }
7030
+ });
7031
+
7032
+ // packages/core/dist/group-dispatch.js
7033
+ import { randomUUID as randomUUID4 } from "crypto";
7034
+ import { createConnection as createConnection2 } from "net";
7035
+ import { join as join15 } from "path";
7036
+ function resolveCurrentProject(config) {
7037
+ const cwd = process.cwd();
7038
+ for (const [name, proj] of Object.entries(config.projects)) {
7039
+ const resolvedPath = resolveHome(proj.path);
7040
+ if (cwd.startsWith(resolvedPath))
7041
+ return name;
7042
+ }
7043
+ return null;
7044
+ }
7045
+ async function probeCaptainChannel(project, opts = {}) {
7046
+ const stateRoot = opts.stateRoot ?? join15(CONFIG_DIR, "state");
7047
+ const addr = readCaptainAddress(stateRoot, project);
7048
+ if (!addr)
7049
+ return false;
7050
+ if (addr.agent === "opencode") {
7051
+ if (addr.port == null)
7052
+ return false;
7053
+ return httpReachable(addr.port, opts.fetchImpl ?? fetch, opts.timeoutMs ?? 5e3);
7054
+ }
7055
+ try {
7056
+ return await (opts.socketAccepts ?? socketAccepts)(captainSocketPath(project));
7057
+ } catch {
7058
+ return false;
7059
+ }
7060
+ }
7061
+ async function httpReachable(port, fetchImpl, timeoutMs) {
7062
+ for (const path23 of ["/session", "/api/session"]) {
7063
+ const ac = new AbortController();
7064
+ const t = setTimeout(() => ac.abort(), timeoutMs);
7065
+ try {
7066
+ const res = await fetchImpl(`http://127.0.0.1:${port}${path23}`, { signal: ac.signal });
7067
+ if (res.ok)
7068
+ return true;
7069
+ } catch {
7070
+ } finally {
7071
+ clearTimeout(t);
7072
+ }
7073
+ }
7074
+ return false;
7075
+ }
7076
+ function socketAccepts(socketPath, timeoutMs = 2e3) {
7077
+ return new Promise((resolve4) => {
7078
+ let settled = false;
7079
+ const sock = createConnection2(socketPath);
7080
+ const done = (v2) => {
7081
+ if (settled)
7082
+ return;
7083
+ settled = true;
7084
+ sock.destroy();
7085
+ resolve4(v2);
7086
+ };
7087
+ sock.setTimeout(timeoutMs, () => done(false));
7088
+ sock.once("connect", () => done(true));
7089
+ sock.once("error", () => done(false));
7090
+ });
7091
+ }
7092
+ async function isCaptainAlive(project, sockPath = DAEMON_SOCK_PATH, channelAlive = probeCaptainChannel) {
7093
+ try {
7094
+ const health = await sendRequest(sockPath, { kind: "health", project }, 5e3);
7095
+ const captain = health?.find((h) => h.kind === "captain" && h.project === project);
7096
+ if (captain?.state === "alive")
7097
+ return true;
7098
+ } catch {
7099
+ }
7100
+ try {
7101
+ return await channelAlive(project);
7102
+ } catch {
7103
+ return false;
7104
+ }
7105
+ }
7106
+ async function waitForWarmup(project, sockPath = DAEMON_SOCK_PATH, timeoutMs = GROUP_DISPATCH_WARMUP_TIMEOUT_MS, pollMs = GROUP_DISPATCH_WARMUP_POLL_MS, channelAlive) {
7107
+ const deadline = Date.now() + timeoutMs;
7108
+ while (Date.now() < deadline) {
7109
+ if (await isCaptainAlive(project, sockPath, channelAlive))
7110
+ return true;
7111
+ await new Promise((r) => setTimeout(r, pollMs));
7112
+ }
7113
+ return false;
7114
+ }
7115
+ async function dispatchToSibling(opts) {
7116
+ const config = loadConfig();
7117
+ const fromCfg = config.projects[opts.fromProject];
7118
+ const toCfg = config.projects[opts.toProject];
7119
+ if (!toCfg) {
7120
+ throw new Error(`target project '${opts.toProject}' not found in config`);
7121
+ }
7122
+ const sameGroup = !!fromCfg?.group && !!toCfg.group && fromCfg.group === toCfg.group;
7123
+ if (toCfg.acceptDelegations === false) {
7124
+ throw new Error(`cannot dispatch to '${opts.toProject}': project has acceptDelegations set to false`);
7125
+ }
7126
+ const sockPath = opts.sockPath ?? DAEMON_SOCK_PATH;
7127
+ const alive = await isCaptainAlive(opts.toProject, sockPath, opts.channelAlive);
7128
+ if (!alive) {
7129
+ if (!sameGroup) {
7130
+ throw new Error(`cannot dispatch to '${opts.toProject}': captain is not running and cross-group dispatch does not auto-boot it. Use 'squadrant ping ${opts.toProject} "<msg>"' or start it manually with 'squadrant launch ${opts.toProject}', then retry.`);
7131
+ }
7132
+ if (opts.bootCaptain) {
7133
+ await opts.bootCaptain(opts.toProject);
7134
+ }
7135
+ const warmed = await waitForWarmup(opts.toProject, sockPath, opts.warmupTimeoutMs, opts.warmupPollMs, opts.channelAlive);
7136
+ if (!warmed) {
7137
+ throw new Error(`dispatch to '${opts.toProject}' timed out waiting for captain warmup (>${(opts.warmupTimeoutMs ?? GROUP_DISPATCH_WARMUP_TIMEOUT_MS) / 1e3}s)`);
7138
+ }
7139
+ }
7140
+ const now = Date.now();
7141
+ const attemptId = randomUUID4();
7142
+ const record = {
7143
+ id: randomUUID4(),
7144
+ project: opts.toProject,
7145
+ originProject: opts.fromProject,
7146
+ provider: opts.provider ?? "claude",
7147
+ mode: opts.mode ?? "headless",
7148
+ state: "submitted",
7149
+ task: opts.task,
7150
+ createdAt: now,
7151
+ lastHeartbeat: now,
7152
+ lastEvent: "dispatch",
7153
+ heartbeatBudgetMs: 3e5,
7154
+ attempts: [{ attemptId, startedAt: now, lastHeartbeatAt: now }]
7155
+ };
7156
+ const result = await sendRequest(sockPath, { kind: "dispatch", record });
7157
+ return result;
7158
+ }
7159
+ var GROUP_DISPATCH_WARMUP_TIMEOUT_MS, GROUP_DISPATCH_WARMUP_POLL_MS;
7160
+ var init_group_dispatch = __esm({
7161
+ "packages/core/dist/group-dispatch.js"() {
7162
+ init_dist();
7163
+ init_protocol();
7164
+ init_captain_record();
7165
+ init_captain_channel();
7166
+ GROUP_DISPATCH_WARMUP_TIMEOUT_MS = 12e4;
7167
+ GROUP_DISPATCH_WARMUP_POLL_MS = 1e3;
7168
+ }
7169
+ });
7170
+
7171
+ // packages/core/dist/launch-workspace.js
7172
+ async function deliverStartupPrompt(runtime, refId, prompt, opts = {}) {
7173
+ const classify = opts.classifyScreen ?? (() => "idle");
7174
+ const readyTimeoutMs = opts.readyTimeoutMs ?? 3e4;
7175
+ const settleMs = opts.settleMs ?? 2500;
7176
+ const pollMs = opts.pollMs ?? 1e3;
7177
+ const maxAttempts = opts.maxAttempts ?? 3;
7178
+ const sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
7179
+ const read = async () => runtime.readScreen(refId).catch(() => "");
7180
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
7181
+ const deadline = Date.now() + readyTimeoutMs;
7182
+ let preSend = await read();
7183
+ let state = classify(preSend);
7184
+ while (state === "loading" && Date.now() < deadline) {
7185
+ await sleep3(pollMs);
7186
+ preSend = await read();
7187
+ state = classify(preSend);
7188
+ }
7189
+ if (state === "working")
7190
+ return true;
7191
+ await runtime.send(refId, prompt).catch(() => {
7192
+ });
7193
+ await sleep3(settleMs);
7194
+ const after = await read();
7195
+ if (after !== preSend)
7196
+ return true;
7197
+ if (state === "loading")
7198
+ break;
7199
+ }
7200
+ process.stderr.write(`\u26A0\uFE0F Startup prompt not confirmed on ${refId} after ${maxAttempts} attempt(s) \u2014 the agent may be idle with no startup prompt. Re-run launch, or send it manually.
7201
+ `);
7202
+ return false;
7203
+ }
7204
+ async function bootWorkspace(opts) {
7205
+ const { runtime, workspaceName, agentCmd, cwd, navigate = false, forceFresh = false, pinToTop = false, initialPrompt } = opts;
7206
+ const existing = await runtime.status(workspaceName);
7207
+ if (existing && forceFresh) {
7208
+ opts.onStoppingStale?.(workspaceName);
7209
+ await runtime.stop(existing.id);
7210
+ } else if (existing) {
7211
+ opts.onAlreadyExists?.(workspaceName);
7212
+ opts.selectWorkspace?.(existing.id);
7213
+ return;
6300
7214
  }
6301
7215
  const rawCurrent = opts.getCurrentWorkspace?.() ?? null;
6302
7216
  const currentRef = rawCurrent?.match(/workspace:\d+/)?.[0];
@@ -6898,8 +7812,10 @@ var init_source = __esm({
6898
7812
  var dist_exports = {};
6899
7813
  __export(dist_exports, {
6900
7814
  AGENT_BINS: () => AGENT_BINS,
7815
+ ANTHROPIC_ONLY_REQUEST_FIELDS: () => ANTHROPIC_ONLY_REQUEST_FIELDS,
6901
7816
  BOT_COMMANDS: () => BOT_COMMANDS,
6902
7817
  CC_SOCKS_DIR: () => CC_SOCKS_DIR,
7818
+ CMUX_PRESERVE_CLAUDE_AUTH_ENV: () => CMUX_PRESERVE_CLAUDE_AUTH_ENV,
6903
7819
  CREW_GONE_MS: () => CREW_GONE_MS,
6904
7820
  CREW_STALE_MS: () => CREW_STALE_MS,
6905
7821
  CaptainDelivery: () => CaptainDelivery,
@@ -6919,6 +7835,10 @@ __export(dist_exports, {
6919
7835
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
6920
7836
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
6921
7837
  READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
7838
+ READ_ONLY_TOP_LEVEL_COMMANDS: () => READ_ONLY_TOP_LEVEL_COMMANDS,
7839
+ ROUTER_READY_ATTEMPTS: () => ROUTER_READY_ATTEMPTS,
7840
+ ROUTER_READY_DELAY_MS: () => ROUTER_READY_DELAY_MS,
7841
+ SERVER_TOOL_TYPE_RE: () => SERVER_TOOL_TYPE_RE,
6922
7842
  STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
6923
7843
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
6924
7844
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
@@ -6927,17 +7847,22 @@ __export(dist_exports, {
6927
7847
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
6928
7848
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
6929
7849
  ageText: () => ageText,
7850
+ anthropicError: () => anthropicError,
6930
7851
  appendCaptainMessage: () => appendCaptainMessage,
6931
7852
  appendToMailbox: () => appendToMailbox,
6932
7853
  assembleDaemonSnapshot: () => assembleDaemonSnapshot,
7854
+ assertBackendUsable: () => assertBackendUsable,
6933
7855
  bootWorkspace: () => bootWorkspace,
6934
7856
  buildCompletionProtocol: () => buildCompletionProtocol,
6935
7857
  buildContext: () => buildContext,
6936
7858
  buildDaemonPath: () => buildDaemonPath,
6937
7859
  buildFreshness: () => buildFreshness,
7860
+ buildRouterCredentialsRequest: () => buildRouterCredentialsRequest,
7861
+ buildRouterEnv: () => buildRouterEnv,
6938
7862
  buildSideFirstTurn: () => buildSideFirstTurn,
6939
7863
  capAllowed: () => capAllowed,
6940
7864
  capOutput: () => capOutput,
7865
+ captainRecordPath: () => captainRecordPath,
6941
7866
  captainSocketPath: () => captainSocketPath,
6942
7867
  checkFact: () => checkFact,
6943
7868
  classifyHealth: () => classifyHealth,
@@ -6956,11 +7881,14 @@ __export(dist_exports, {
6956
7881
  createInteractiveProbe: () => createInteractiveProbe,
6957
7882
  createIsCaptainAlive: () => createIsCaptainAlive,
6958
7883
  createLaunch: () => createLaunch,
7884
+ createRouterService: () => createRouterService,
7885
+ createRouterShim: () => createRouterShim,
6959
7886
  createRunCommand: () => createRunCommand,
6960
7887
  createStore: () => createStore,
6961
7888
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
6962
7889
  createTelegramBridge: () => createTelegramBridge,
6963
7890
  createTelegramClient: () => createTelegramClient,
7891
+ createUsageTee: () => createUsageTee,
6964
7892
  createWorkItem: () => createWorkItem,
6965
7893
  createWorkStore: () => createWorkStore,
6966
7894
  crewPaneTitle: () => crewPaneTitle,
@@ -6981,6 +7909,7 @@ __export(dist_exports, {
6981
7909
  detectGroupAndUser: () => detectGroupAndUser,
6982
7910
  detectGroupId: () => detectGroupId,
6983
7911
  discoverCaptainSurface: () => discoverCaptainSurface,
7912
+ discoverLiveOpencodeServer: () => discoverLiveOpencodeServer,
6984
7913
  dispatchToSibling: () => dispatchToSibling,
6985
7914
  encodeFrame: () => encodeFrame,
6986
7915
  encodeMsg: () => encodeMsg,
@@ -6994,6 +7923,7 @@ __export(dist_exports, {
6994
7923
  findProjectByThread: () => findProjectByThread,
6995
7924
  findWorkItemById: () => findWorkItemById,
6996
7925
  forceKickstartAndVerify: () => forceKickstartAndVerify,
7926
+ formatCustomHeaders: () => formatCustomHeaders,
6997
7927
  formatInbound: () => formatInbound,
6998
7928
  formatInboundReceipt: () => formatInboundReceipt,
6999
7929
  formatLifecycle: () => formatLifecycle,
@@ -7010,33 +7940,44 @@ __export(dist_exports, {
7010
7940
  isNotifyActive: () => isNotifyActive,
7011
7941
  isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
7012
7942
  isReadOnlyCrewCommand: () => isReadOnlyCrewCommand,
7943
+ isReadOnlyTopLevelCommand: () => isReadOnlyTopLevelCommand,
7013
7944
  isSideTitle: () => isSideTitle,
7014
7945
  isStickyAttention: () => isStickyAttention,
7015
7946
  isTurnAccepted: () => isTurnAccepted,
7016
7947
  isVersionUpgrade: () => isVersionUpgrade,
7948
+ joinUrl: () => joinUrl,
7017
7949
  kickstartArgv: () => kickstartArgv,
7018
7950
  launchOneWorkspace: () => launchOneWorkspace,
7951
+ listSessions: () => listSessions,
7019
7952
  loadSessions: () => loadSessions,
7020
7953
  loadState: () => loadState,
7021
7954
  mailboxStats: () => mailboxStats,
7022
7955
  makeGate: () => makeGate,
7023
7956
  maskToken: () => maskToken,
7957
+ mintToken: () => mintToken,
7024
7958
  nameFromTitle: () => nameFromTitle,
7959
+ newestSessionInDirectory: () => newestSessionInDirectory,
7025
7960
  nextAutoName: () => nextAutoName,
7026
7961
  niceCrewCommand: () => niceCrewCommand,
7027
7962
  notifyToggle: () => notifyToggle,
7963
+ parseBearer: () => parseBearer,
7028
7964
  parseCommand: () => parseCommand,
7965
+ parseLiveOpencodeServers: () => parseLiveOpencodeServers,
7029
7966
  parseNotifyPref: () => parseNotifyPref,
7030
7967
  parseProgramArgs: () => parseProgramArgs,
7031
7968
  plistPath: () => plistPath,
7969
+ pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
7032
7970
  printForeignInstallError: () => printForeignInstallError,
7033
7971
  printVersionUpgradeNotice: () => printVersionUpgradeNotice,
7972
+ probeCaptainChannel: () => probeCaptainChannel,
7034
7973
  programArgsBlock: () => programArgsBlock,
7035
7974
  projectHealth: () => projectHealth,
7036
7975
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
7976
+ readCaptainAddress: () => readCaptainAddress,
7037
7977
  readCursor: () => readCursor,
7038
7978
  readFromCursor: () => readFromCursor,
7039
7979
  readRunningMarker: () => readRunningMarker,
7980
+ realpathOrSelf: () => realpathOrSelf,
7040
7981
  reapCrewChildren: () => reapCrewChildren,
7041
7982
  reapOrphanedCrews: () => reapOrphanedCrews,
7042
7983
  reconcileLiveness: () => reconcileLiveness,
@@ -7046,12 +7987,18 @@ __export(dist_exports, {
7046
7987
  reduceLifecycle: () => reduceLifecycle,
7047
7988
  releaseDaemonLock: () => releaseDaemonLock,
7048
7989
  removeRunningMarker: () => removeRunningMarker,
7990
+ renderEnvAssignments: () => renderEnvAssignments,
7049
7991
  renderPlist: () => renderPlist,
7050
7992
  reregisterDaemon: () => reregisterDaemon,
7051
7993
  resolveAgentBinDirs: () => resolveAgentBinDirs,
7994
+ resolveAndPersistOpencodeCaptain: () => resolveAndPersistOpencodeCaptain,
7995
+ resolveBackend: () => resolveBackend,
7052
7996
  resolveCrewRoute: () => resolveCrewRoute,
7053
7997
  resolveCurrentProject: () => resolveCurrentProject,
7054
7998
  resolveGate: () => resolveGate,
7999
+ resolveProject: () => resolveProject,
8000
+ resolveRouterCredentials: () => resolveRouterCredentials,
8001
+ resolveRouterUpstream: () => resolveRouterUpstream,
7055
8002
  resolveSetupGroup: () => resolveSetupGroup,
7056
8003
  resolveSetupToken: () => resolveSetupToken,
7057
8004
  resolveSetupUserId: () => resolveSetupUserId,
@@ -7079,7 +8026,9 @@ __export(dist_exports, {
7079
8026
  runTelegramSend: () => runTelegramSend,
7080
8027
  runTelegramStatus: () => runTelegramStatus,
7081
8028
  runningMarkerPath: () => runningMarkerPath,
8029
+ sameDirectory: () => sameDirectory,
7082
8030
  sanitizePathForPlist: () => sanitizePathForPlist,
8031
+ sanitizeRequest: () => sanitizeRequest,
7083
8032
  saveSessions: () => saveSessions,
7084
8033
  saveState: () => saveState,
7085
8034
  screenHasSplashMarker: () => screenHasSplashMarker,
@@ -7088,6 +8037,7 @@ __export(dist_exports, {
7088
8037
  setNotify: () => setNotify,
7089
8038
  setTopic: () => setTopic,
7090
8039
  shellQuote: () => shellQuote,
8040
+ shouldBuildRouterService: () => shouldBuildRouterService,
7091
8041
  shouldStartFresh: () => shouldStartFresh,
7092
8042
  sideNameFromTitle: () => sideNameFromTitle,
7093
8043
  sideNextAutoName: () => sideNextAutoName,
@@ -7103,8 +8053,10 @@ __export(dist_exports, {
7103
8053
  topicKey: () => topicKey,
7104
8054
  topicName: () => topicName,
7105
8055
  tryAcquireDaemonLock: () => tryAcquireDaemonLock,
8056
+ usageFromJson: () => usageFromJson,
7106
8057
  waitForCaptainDelivery: () => waitForCaptainDelivery,
7107
8058
  waitForWarmup: () => waitForWarmup,
8059
+ writeCaptainAddress: () => writeCaptainAddress,
7108
8060
  writeCursor: () => writeCursor,
7109
8061
  writeExitMarker: () => writeExitMarker,
7110
8062
  writeRunningMarker: () => writeRunningMarker,
@@ -7138,7 +8090,9 @@ var init_dist2 = __esm({
7138
8090
  init_crew_protocol();
7139
8091
  init_crew_lifecycle();
7140
8092
  init_telegram();
8093
+ init_router();
7141
8094
  init_crew_routing();
8095
+ init_router_resolution();
7142
8096
  init_restart_daemon();
7143
8097
  init_group_dispatch();
7144
8098
  init_launch_workspace();
@@ -7154,6 +8108,8 @@ var init_dist2 = __esm({
7154
8108
  init_source();
7155
8109
  init_control_channel();
7156
8110
  init_captain_channel();
8111
+ init_captain_record();
8112
+ init_opencode_session();
7157
8113
  }
7158
8114
  });
7159
8115
 
@@ -7169,14 +8125,75 @@ init_dist2();
7169
8125
  init_dist2();
7170
8126
  init_dist2();
7171
8127
  init_dist2();
7172
- import { join as join22, dirname as dirname4, resolve as resolve3 } from "path";
7173
- import { homedir as homedir13 } from "os";
8128
+ import { join as join26, dirname as dirname5, resolve as resolve3 } from "path";
8129
+ import { homedir as homedir14 } from "os";
7174
8130
  import { fileURLToPath as fileURLToPath3 } from "url";
7175
- import { readFileSync as readFileSync15, statSync as statSync4, existsSync as existsSync13 } from "fs";
8131
+ import { readFileSync as readFileSync18, statSync as statSync4, existsSync as existsSync13 } from "fs";
7176
8132
 
7177
8133
  // packages/agents/dist/drivers/claude.js
7178
8134
  import { execSync as execSync2 } from "child_process";
7179
8135
 
8136
+ // packages/agents/dist/sessions/claude-sessions.js
8137
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
8138
+ import { join as join17 } from "path";
8139
+
8140
+ // packages/agents/dist/claude/registry.js
8141
+ import fs13 from "fs";
8142
+ import { join as join16 } from "path";
8143
+ import { homedir as homedir7 } from "os";
8144
+ var CLAUDE_SESSIONS_DIR = join16(homedir7(), ".claude", "sessions");
8145
+ var PID_JSON = /^(\d+)\.json$/;
8146
+ function parseRegistryDir(files, readFile7) {
8147
+ const out = [];
8148
+ for (const name of files) {
8149
+ const m = PID_JSON.exec(name);
8150
+ if (!m)
8151
+ continue;
8152
+ let json;
8153
+ try {
8154
+ json = JSON.parse(readFile7(name));
8155
+ } catch {
8156
+ continue;
8157
+ }
8158
+ out.push({ ...json, pid: parseInt(m[1], 10) });
8159
+ }
8160
+ return out;
8161
+ }
8162
+ function toLifecycleSnapshot(entry, taskId, alive, now) {
8163
+ const at = entry.statusUpdatedAt ?? now;
8164
+ const base = { taskId, alive, origin: "agent", at, pid: entry.pid };
8165
+ if (!alive)
8166
+ return { ...base, state: "unknown" };
8167
+ switch (entry.status) {
8168
+ case "busy":
8169
+ case "shell":
8170
+ return { ...base, state: "running" };
8171
+ case "idle":
8172
+ return { ...base, state: "idle" };
8173
+ case "waiting":
8174
+ return {
8175
+ ...base,
8176
+ state: "needsInput",
8177
+ detail: { reason: entry.waitingFor, note: entry.waitingFor }
8178
+ };
8179
+ default:
8180
+ return { ...base, state: "unknown" };
8181
+ }
8182
+ }
8183
+ function readClaudeStatusBySocketPath(socketPath) {
8184
+ let files;
8185
+ try {
8186
+ files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
8187
+ } catch {
8188
+ return void 0;
8189
+ }
8190
+ const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join16(CLAUDE_SESSIONS_DIR, name), "utf8"));
8191
+ const entry = entries.find((e) => e.messagingSocketPath === socketPath);
8192
+ if (!entry)
8193
+ return void 0;
8194
+ return { status: entry.status, statusUpdatedAt: entry.statusUpdatedAt, sessionId: entry.sessionId };
8195
+ }
8196
+
7180
8197
  // packages/agents/dist/drivers/codex.js
7181
8198
  import { execSync as execSync3 } from "child_process";
7182
8199
 
@@ -7186,8 +8203,14 @@ import { execSync as execSync4 } from "child_process";
7186
8203
  // packages/agents/dist/drivers/opencode.js
7187
8204
  import { execSync as execSync5 } from "child_process";
7188
8205
 
8206
+ // packages/agents/dist/sessions/opencode-sessions.js
8207
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
8208
+ import { join as join18 } from "path";
8209
+ import { homedir as homedir8 } from "os";
8210
+ var SQUADRANT_STATE_DIR = join18(homedir8(), ".config", "squadrant", "state");
8211
+
7189
8212
  // packages/agents/dist/drivers/launch-cmd.js
7190
- import fs13 from "fs";
8213
+ import fs14 from "fs";
7191
8214
  import path13 from "path";
7192
8215
 
7193
8216
  // packages/agents/dist/projection/cursor.js
@@ -7210,10 +8233,102 @@ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from
7210
8233
  import path17 from "path";
7211
8234
  import os9 from "os";
7212
8235
 
8236
+ // packages/agents/dist/projection/opencode-skills.js
8237
+ import fs15 from "fs";
8238
+ import os10 from "os";
8239
+ import path18 from "path";
8240
+ var OPENCODE_SKILL_MARKER = ".squadrant-managed";
8241
+ var MARKER_BODY = "squadrant\n";
8242
+ function defaultOpencodeSkillsRoot(env = process.env) {
8243
+ const configHome = env.XDG_CONFIG_HOME || path18.join(os10.homedir(), ".config");
8244
+ return path18.join(configHome, "opencode", "skills");
8245
+ }
8246
+ var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
8247
+ function parseSkillName(raw) {
8248
+ const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
8249
+ if (!fm)
8250
+ return null;
8251
+ const name = fm[1].match(/^name:\s*(.+)$/m)?.[1]?.trim();
8252
+ const description = fm[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
8253
+ if (!name || !description)
8254
+ return null;
8255
+ return name;
8256
+ }
8257
+ function listSourceSkills(sourceSkillsDir) {
8258
+ const out = /* @__PURE__ */ new Map();
8259
+ if (!fs15.existsSync(sourceSkillsDir))
8260
+ return out;
8261
+ for (const entry of fs15.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
8262
+ if (!entry.isDirectory())
8263
+ continue;
8264
+ const skillPath = path18.join(sourceSkillsDir, entry.name, "SKILL.md");
8265
+ if (!fs15.existsSync(skillPath))
8266
+ continue;
8267
+ const name = parseSkillName(fs15.readFileSync(skillPath, "utf-8"));
8268
+ if (name && SAFE_NAME.test(name))
8269
+ out.set(name, skillPath);
8270
+ }
8271
+ return out;
8272
+ }
8273
+ function hasMarker(dir) {
8274
+ return fs15.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
8275
+ }
8276
+ function syncOpencodeSkills(opts) {
8277
+ const result = { written: [], unchanged: [], removed: [], skipped: [] };
8278
+ const wanted = listSourceSkills(opts.sourceSkillsDir);
8279
+ if (wanted.size === 0 && !fs15.existsSync(opts.sourceSkillsDir))
8280
+ return result;
8281
+ if (!opts.dryRun)
8282
+ fs15.mkdirSync(opts.skillsRoot, { recursive: true });
8283
+ for (const [name, srcPath] of wanted) {
8284
+ const targetDir = path18.join(opts.skillsRoot, name);
8285
+ const targetSkill = path18.join(targetDir, "SKILL.md");
8286
+ const managed = hasMarker(targetDir);
8287
+ if (fs15.existsSync(targetSkill) && !managed) {
8288
+ result.skipped.push({ name, reason: "foreign-collision" });
8289
+ continue;
8290
+ }
8291
+ const current = fs15.existsSync(targetSkill) ? fs15.readFileSync(targetSkill) : null;
8292
+ if (current && current.equals(fs15.readFileSync(srcPath))) {
8293
+ if (!managed && !opts.dryRun) {
8294
+ fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
8295
+ }
8296
+ result.unchanged.push(name);
8297
+ continue;
8298
+ }
8299
+ if (!opts.dryRun) {
8300
+ fs15.mkdirSync(targetDir, { recursive: true });
8301
+ fs15.copyFileSync(srcPath, targetSkill);
8302
+ fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
8303
+ }
8304
+ result.written.push(name);
8305
+ }
8306
+ if (fs15.existsSync(opts.skillsRoot)) {
8307
+ for (const entry of fs15.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
8308
+ if (!entry.isDirectory() || wanted.has(entry.name))
8309
+ continue;
8310
+ const dir = path18.join(opts.skillsRoot, entry.name);
8311
+ if (!hasMarker(dir))
8312
+ continue;
8313
+ if (!opts.dryRun)
8314
+ fs15.rmSync(dir, { recursive: true, force: true });
8315
+ result.removed.push(entry.name);
8316
+ }
8317
+ }
8318
+ return result;
8319
+ }
8320
+ function syncShippedOpencodeSkills(opts) {
8321
+ return syncOpencodeSkills({
8322
+ sourceSkillsDir: path18.join(opts.pkgRoot, "plugin", "skills"),
8323
+ skillsRoot: opts.skillsRoot ?? defaultOpencodeSkillsRoot(),
8324
+ dryRun: opts.dryRun
8325
+ });
8326
+ }
8327
+
7213
8328
  // packages/agents/dist/projection/claude.js
7214
8329
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
7215
- import path18 from "path";
7216
- import os10 from "os";
8330
+ import path19 from "path";
8331
+ import os11 from "os";
7217
8332
 
7218
8333
  // packages/agents/dist/codex/app-server-client.js
7219
8334
  import { EventEmitter } from "events";
@@ -7487,11 +8602,11 @@ function toSnapshot(ev) {
7487
8602
 
7488
8603
  // packages/agents/dist/codex/config.js
7489
8604
  import { readFile as readFile6 } from "fs/promises";
7490
- import { homedir as homedir7 } from "os";
7491
- import { join as join14 } from "path";
8605
+ import { homedir as homedir9 } from "os";
8606
+ import { join as join19 } from "path";
7492
8607
  async function resolveCodexModel() {
7493
- const home = process.env["CODEX_HOME"] ?? join14(homedir7(), ".codex");
7494
- const configPath = join14(home, "config.toml");
8608
+ const home = process.env["CODEX_HOME"] ?? join19(homedir9(), ".codex");
8609
+ const configPath = join19(home, "config.toml");
7495
8610
  let text;
7496
8611
  try {
7497
8612
  text = await readFile6(configPath, "utf8");
@@ -8003,9 +9118,9 @@ var OpencodeSseBridge = class {
8003
9118
 
8004
9119
  // packages/agents/dist/interactive/claude.js
8005
9120
  import { execSync as execSync6 } from "child_process";
8006
- import { readFileSync as readFileSync10 } from "fs";
8007
- import { homedir as homedir8 } from "os";
8008
- import { join as join15 } from "path";
9121
+ import { readFileSync as readFileSync13 } from "fs";
9122
+ import { homedir as homedir10 } from "os";
9123
+ import { join as join20 } from "path";
8009
9124
  var nextHookRequestId = Date.now();
8010
9125
 
8011
9126
  // packages/agents/dist/headless/types.js
@@ -8169,67 +9284,8 @@ function runHeadless(opts) {
8169
9284
  }
8170
9285
 
8171
9286
  // packages/agents/dist/claude/peer-registry-source.js
8172
- import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
8173
- import { join as join17 } from "path";
8174
-
8175
- // packages/agents/dist/claude/registry.js
8176
- import fs14 from "fs";
8177
- import { join as join16 } from "path";
8178
- import { homedir as homedir9 } from "os";
8179
- var CLAUDE_SESSIONS_DIR = join16(homedir9(), ".claude", "sessions");
8180
- var PID_JSON = /^(\d+)\.json$/;
8181
- function parseRegistryDir(files, readFile7) {
8182
- const out = [];
8183
- for (const name of files) {
8184
- const m = PID_JSON.exec(name);
8185
- if (!m)
8186
- continue;
8187
- let json;
8188
- try {
8189
- json = JSON.parse(readFile7(name));
8190
- } catch {
8191
- continue;
8192
- }
8193
- out.push({ ...json, pid: parseInt(m[1], 10) });
8194
- }
8195
- return out;
8196
- }
8197
- function toLifecycleSnapshot(entry, taskId, alive, now) {
8198
- const at = entry.statusUpdatedAt ?? now;
8199
- const base = { taskId, alive, origin: "agent", at, pid: entry.pid };
8200
- if (!alive)
8201
- return { ...base, state: "unknown" };
8202
- switch (entry.status) {
8203
- case "busy":
8204
- case "shell":
8205
- return { ...base, state: "running" };
8206
- case "idle":
8207
- return { ...base, state: "idle" };
8208
- case "waiting":
8209
- return {
8210
- ...base,
8211
- state: "needsInput",
8212
- detail: { reason: entry.waitingFor, note: entry.waitingFor }
8213
- };
8214
- default:
8215
- return { ...base, state: "unknown" };
8216
- }
8217
- }
8218
- function readClaudeStatusBySocketPath(socketPath) {
8219
- let files;
8220
- try {
8221
- files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
8222
- } catch {
8223
- return void 0;
8224
- }
8225
- const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join16(CLAUDE_SESSIONS_DIR, name), "utf8"));
8226
- const entry = entries.find((e) => e.messagingSocketPath === socketPath);
8227
- if (!entry)
8228
- return void 0;
8229
- return { status: entry.status, statusUpdatedAt: entry.statusUpdatedAt, sessionId: entry.sessionId };
8230
- }
8231
-
8232
- // packages/agents/dist/claude/peer-registry-source.js
9287
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14 } from "fs";
9288
+ import { join as join21 } from "path";
8233
9289
  function defaultIsAlive(pid) {
8234
9290
  try {
8235
9291
  process.kill(pid, 0);
@@ -8253,8 +9309,8 @@ var ClaudePeerRegistrySource = class {
8253
9309
  pollMs;
8254
9310
  log;
8255
9311
  constructor(o = {}) {
8256
- this.readdir = o.readdir ?? (() => readdirSync4(CLAUDE_SESSIONS_DIR));
8257
- this.readFile = o.readFile ?? ((n) => readFileSync11(join17(CLAUDE_SESSIONS_DIR, n), "utf8"));
9312
+ this.readdir = o.readdir ?? (() => readdirSync6(CLAUDE_SESSIONS_DIR));
9313
+ this.readFile = o.readFile ?? ((n) => readFileSync14(join21(CLAUDE_SESSIONS_DIR, n), "utf8"));
8258
9314
  this.isAlive = o.isAlive ?? defaultIsAlive;
8259
9315
  this.now = o.now ?? Date.now;
8260
9316
  this.pollMs = o.pollMs ?? 2e3;
@@ -8566,6 +9622,119 @@ var ClaudeReceiptListener = class {
8566
9622
  }
8567
9623
  };
8568
9624
 
9625
+ // packages/agents/dist/claude/api-key-approval.js
9626
+ import fs16 from "fs";
9627
+ import os12 from "os";
9628
+ import path20 from "path";
9629
+
9630
+ // packages/agents/dist/opencode/http-channel.js
9631
+ var OpencodeHttpChannel = class {
9632
+ name = "opencode-http";
9633
+ agent = "opencode";
9634
+ /** taskId → resolved session id. Invalidated on 404 (see send()). */
9635
+ sessionByTask = /* @__PURE__ */ new Map();
9636
+ fetchImpl;
9637
+ portFor;
9638
+ sessionFor;
9639
+ timeoutMs;
9640
+ log;
9641
+ constructor(deps) {
9642
+ this.fetchImpl = deps.fetchImpl ?? fetch;
9643
+ this.portFor = deps.portFor;
9644
+ this.sessionFor = deps.sessionFor;
9645
+ this.timeoutMs = deps.timeoutMs ?? 5e3;
9646
+ this.log = deps.log;
9647
+ }
9648
+ async send(taskId, message) {
9649
+ const port = this.portFor(taskId);
9650
+ if (port == null)
9651
+ return { status: "unsupported" };
9652
+ const sessionId = this.sessionFor?.(taskId) ?? await this.resolveSession(taskId, port);
9653
+ if (!sessionId)
9654
+ return { status: "gone" };
9655
+ let res;
9656
+ try {
9657
+ res = await this.request(`http://127.0.0.1:${port}/session/${sessionId}/prompt_async`, {
9658
+ method: "POST",
9659
+ headers: { "content-type": "application/json" },
9660
+ body: JSON.stringify({ parts: [{ type: "text", text: message }] })
9661
+ });
9662
+ } catch (e) {
9663
+ this.log?.(`opencode-http send transport error for ${taskId}: ${e.message}`);
9664
+ return { status: "gone" };
9665
+ }
9666
+ if (res.status === 204 || res.status === 200) {
9667
+ return { status: "accepted", via: this.name };
9668
+ }
9669
+ if (res.status === 404) {
9670
+ this.sessionByTask.delete(taskId);
9671
+ this.log?.(`opencode-http: session ${sessionId} for ${taskId} is gone (404)`);
9672
+ return { status: "gone" };
9673
+ }
9674
+ this.log?.(`opencode-http: unexpected status ${res.status} for ${taskId}`);
9675
+ return { status: "gone" };
9676
+ }
9677
+ /**
9678
+ * Non-mutating reachability check. MUST NOT deliver anything — shadow mode
9679
+ * calls this alongside a real pane send, and a POST here would deliver the
9680
+ * message twice.
9681
+ */
9682
+ async probe(taskId) {
9683
+ const port = this.portFor(taskId);
9684
+ if (port == null)
9685
+ return { status: "unsupported" };
9686
+ const sessionId = this.sessionFor?.(taskId) ?? await this.resolveSession(taskId, port);
9687
+ return sessionId ? { status: "reachable", via: this.name } : { status: "gone" };
9688
+ }
9689
+ // ── private ───────────────────────────────────────────────────────────────
9690
+ /**
9691
+ * Resolve (and cache) the crew's session id.
9692
+ *
9693
+ * opencode 1.18.18 is mid-migration from /session/* to /api/session/*, so both
9694
+ * are tried. This is a capability probe, NOT a version comparison — the honest
9695
+ * check is "does this route answer", and neither path is a promised-stable
9696
+ * contract. Re-run the smoke suite when opencode is upgraded.
9697
+ */
9698
+ async resolveSession(taskId, port) {
9699
+ const cached = this.sessionByTask.get(taskId);
9700
+ if (cached)
9701
+ return cached;
9702
+ for (const path23 of ["/session?", "/api/session?"]) {
9703
+ let res;
9704
+ try {
9705
+ res = await this.request(`http://127.0.0.1:${port}${path23}`, { method: "GET" });
9706
+ } catch {
9707
+ return void 0;
9708
+ }
9709
+ if (!res.ok)
9710
+ continue;
9711
+ let sessions;
9712
+ try {
9713
+ sessions = await res.json();
9714
+ } catch {
9715
+ continue;
9716
+ }
9717
+ if (!Array.isArray(sessions) || sessions.length === 0)
9718
+ continue;
9719
+ const newest = sessions.reduce((a, b) => (b.time?.updated ?? 0) > (a.time?.updated ?? 0) ? b : a);
9720
+ if (!newest?.id)
9721
+ continue;
9722
+ this.sessionByTask.set(taskId, newest.id);
9723
+ return newest.id;
9724
+ }
9725
+ return void 0;
9726
+ }
9727
+ async request(url, init) {
9728
+ const ac = new AbortController();
9729
+ const t = setTimeout(() => ac.abort(), this.timeoutMs);
9730
+ try {
9731
+ return await this.fetchImpl(url, { ...init, signal: ac.signal });
9732
+ } finally {
9733
+ clearTimeout(t);
9734
+ }
9735
+ }
9736
+ };
9737
+
8569
9738
  // packages/agents/dist/opencode/fact-adapter.js
8570
9739
  function createOpencodeFactAdapter(deps) {
8571
9740
  return {
@@ -8598,6 +9767,9 @@ function createOpencodeFactAdapter(deps) {
8598
9767
  }
8599
9768
  if (type === "permission.replied")
8600
9769
  return [{ kind: "activity" }];
9770
+ if (type === "server.connected" || type === "server.heartbeat") {
9771
+ return [{ kind: "activity" }];
9772
+ }
8601
9773
  return [{ kind: "unknown", name: type }];
8602
9774
  }
8603
9775
  };
@@ -8608,7 +9780,8 @@ init_dist();
8608
9780
  init_dist();
8609
9781
  init_dist();
8610
9782
  init_dist2();
8611
- import { execFile as execFile2, execFileSync as execFileSync5 } from "child_process";
9783
+ init_dist2();
9784
+ import { execFile as execFile2, execFileSync as execFileSync6 } from "child_process";
8612
9785
  var CMUX_TIMEOUT = 15e3;
8613
9786
  var CmuxTimeoutError = class extends Error {
8614
9787
  constructor(cmd) {
@@ -9204,9 +10377,9 @@ var NotifierRegistry = class {
9204
10377
  };
9205
10378
 
9206
10379
  // packages/workspaces/dist/workspaces/obsidian.js
9207
- import fs15 from "fs/promises";
10380
+ import fs17 from "fs/promises";
9208
10381
  import { existsSync as existsSync11 } from "fs";
9209
- import path19 from "path";
10382
+ import path21 from "path";
9210
10383
 
9211
10384
  // packages/workspaces/dist/workspaces/registry.js
9212
10385
  init_dist();
@@ -9359,16 +10532,16 @@ var CmuxEventsBridge = class {
9359
10532
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
9360
10533
  init_dist2();
9361
10534
  init_dist();
9362
- import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
9363
- import { join as join18 } from "path";
9364
- import { homedir as homedir10 } from "os";
10535
+ import { readdirSync as readdirSync7, readFileSync as readFileSync15 } from "fs";
10536
+ import { join as join22 } from "path";
10537
+ import { homedir as homedir11 } from "os";
9365
10538
 
9366
10539
  // packages/workspaces/dist/cmux-daemon/store-fingerprint.js
9367
10540
  init_dist();
9368
10541
  init_dist2();
9369
- import { execFileSync as execFileSync6 } from "child_process";
10542
+ import { execFileSync as execFileSync7 } from "child_process";
9370
10543
  function defaultExecPs(pid) {
9371
- return execFileSync6("ps", ["-o", "command=", "-p", String(pid)], {
10544
+ return execFileSync7("ps", ["-o", "command=", "-p", String(pid)], {
9372
10545
  encoding: "utf-8",
9373
10546
  stdio: ["ignore", "pipe", "ignore"]
9374
10547
  });
@@ -9458,15 +10631,15 @@ function readLivenessSnapshot(files, readFile7, projects, argvRecovery = {}) {
9458
10631
 
9459
10632
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
9460
10633
  async function readCmuxLiveness() {
9461
- const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join18(homedir10(), ".cmuxterm");
10634
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join22(homedir11(), ".cmuxterm");
9462
10635
  const projects = loadConfig().projects;
9463
10636
  let files;
9464
10637
  try {
9465
- files = readdirSync5(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
10638
+ files = readdirSync7(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
9466
10639
  } catch (e) {
9467
10640
  throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
9468
10641
  }
9469
- return readLivenessSnapshot(files, (f) => readFileSync12(join18(dir, f), "utf-8"), projects);
10642
+ return readLivenessSnapshot(files, (f) => readFileSync15(join22(dir, f), "utf-8"), projects);
9470
10643
  }
9471
10644
  var DaemonCmux = class {
9472
10645
  driver;
@@ -9529,13 +10702,15 @@ var DaemonCmux = class {
9529
10702
  };
9530
10703
 
9531
10704
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
9532
- import { join as join19 } from "path";
9533
- import { homedir as homedir11 } from "os";
9534
- import { watch, readdirSync as readdirSync6, readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
10705
+ import { join as join23 } from "path";
10706
+ import { homedir as homedir12 } from "os";
10707
+ import { watch, readdirSync as readdirSync8, readFileSync as readFileSync16, existsSync as existsSync12 } from "fs";
9535
10708
  var CmuxStoreSource = class {
9536
10709
  name = "cmux-store";
9537
10710
  stateDir;
9538
10711
  debounceMs;
10712
+ lockRetryMs;
10713
+ maxLockRetries;
9539
10714
  isPidAlive;
9540
10715
  listFiles;
9541
10716
  readFile;
@@ -9547,13 +10722,19 @@ var CmuxStoreSource = class {
9547
10722
  deps;
9548
10723
  stopWatcher;
9549
10724
  debounceTimer;
10725
+ /** filename → pending lock-retry timer (one per locked file). */
10726
+ lockRetryTimers = /* @__PURE__ */ new Map();
10727
+ /** filename → re-scan attempts used in the current lock episode. */
10728
+ lockRetryCounts = /* @__PURE__ */ new Map();
9550
10729
  /** taskId → last reported snapshot (for snapshot() liveness floor). */
9551
10730
  cache = /* @__PURE__ */ new Map();
9552
10731
  active = false;
9553
10732
  lastError = null;
9554
10733
  constructor(opts = {}) {
9555
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join19(homedir11(), ".cmuxterm");
10734
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join23(homedir12(), ".cmuxterm");
9556
10735
  this.debounceMs = opts.debounceMs ?? 50;
10736
+ this.lockRetryMs = opts.lockRetryMs ?? 50;
10737
+ this.maxLockRetries = opts.maxLockRetries ?? 3;
9557
10738
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
9558
10739
  this.listFiles = opts.listFiles ?? defaultListFiles;
9559
10740
  this.readFile = opts.readFile ?? defaultReadFile;
@@ -9581,6 +10762,11 @@ var CmuxStoreSource = class {
9581
10762
  this.cancelTimer(this.debounceTimer);
9582
10763
  this.debounceTimer = void 0;
9583
10764
  }
10765
+ for (const timer of this.lockRetryTimers.values()) {
10766
+ this.cancelTimer(timer);
10767
+ }
10768
+ this.lockRetryTimers.clear();
10769
+ this.lockRetryCounts.clear();
9584
10770
  this.stopWatcher?.();
9585
10771
  this.stopWatcher = void 0;
9586
10772
  this.deps = void 0;
@@ -9614,12 +10800,13 @@ var CmuxStoreSource = class {
9614
10800
  }
9615
10801
  scanFile(filename) {
9616
10802
  const deps = this.deps;
9617
- const filePath = join19(this.stateDir, filename);
10803
+ const filePath = join23(this.stateDir, filename);
9618
10804
  const lockPath = `${filePath}.lock`;
9619
10805
  if (this.fileExists(lockPath)) {
9620
- this.log(`cmux-store: skipping ${filename} (locked)`);
10806
+ this.scheduleLockRetry(filename);
9621
10807
  return;
9622
10808
  }
10809
+ this.clearLockRetry(filename);
9623
10810
  const raw = this.readFile(filePath);
9624
10811
  if (!raw)
9625
10812
  return;
@@ -9634,6 +10821,31 @@ var CmuxStoreSource = class {
9634
10821
  this.processSession(session, deps);
9635
10822
  }
9636
10823
  }
10824
+ /** Bounded, one-timer-per-file retry for a file cmux currently has locked. */
10825
+ scheduleLockRetry(filename) {
10826
+ if (this.lockRetryTimers.has(filename))
10827
+ return;
10828
+ const attempts = this.lockRetryCounts.get(filename) ?? 0;
10829
+ if (attempts >= this.maxLockRetries)
10830
+ return;
10831
+ if (attempts === 0)
10832
+ this.log(`cmux-store: skipping ${filename} (locked)`);
10833
+ this.lockRetryCounts.set(filename, attempts + 1);
10834
+ const timer = this.scheduleTimer(() => {
10835
+ this.lockRetryTimers.delete(filename);
10836
+ this.scanFile(filename);
10837
+ }, this.lockRetryMs);
10838
+ this.lockRetryTimers.set(filename, timer);
10839
+ }
10840
+ /** A successful (unlocked) scan ends the lock episode. */
10841
+ clearLockRetry(filename) {
10842
+ const timer = this.lockRetryTimers.get(filename);
10843
+ if (timer !== void 0) {
10844
+ this.cancelTimer(timer);
10845
+ this.lockRetryTimers.delete(filename);
10846
+ }
10847
+ this.lockRetryCounts.delete(filename);
10848
+ }
9637
10849
  processSession(session, deps) {
9638
10850
  if (!session.sessionId || !session.cwd || typeof session.pid !== "number")
9639
10851
  return;
@@ -9680,14 +10892,14 @@ function defaultIsPidAlive2(pid) {
9680
10892
  }
9681
10893
  function defaultListFiles(dir) {
9682
10894
  try {
9683
- return readdirSync6(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
10895
+ return readdirSync8(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
9684
10896
  } catch {
9685
10897
  return [];
9686
10898
  }
9687
10899
  }
9688
- function defaultReadFile(path21) {
10900
+ function defaultReadFile(path23) {
9689
10901
  try {
9690
- return readFileSync13(path21, "utf-8");
10902
+ return readFileSync16(path23, "utf-8");
9691
10903
  } catch {
9692
10904
  return void 0;
9693
10905
  }
@@ -9702,9 +10914,9 @@ function defaultWatchDir(dir, cb) {
9702
10914
  }
9703
10915
 
9704
10916
  // packages/workspaces/dist/native-hooks/native-hook-source.js
9705
- import { join as join20 } from "path";
9706
- import { homedir as homedir12 } from "os";
9707
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
10917
+ import { join as join24 } from "path";
10918
+ import { homedir as homedir13 } from "os";
10919
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
9708
10920
  var CLAUDE_HOOK_EVENTS = [
9709
10921
  ["SessionStart", "session-start"],
9710
10922
  ["UserPromptSubmit", "prompt-submit"],
@@ -9722,7 +10934,7 @@ var CLAUDE_HOOK_EVENTS = [
9722
10934
  ];
9723
10935
  var DEFAULT_HOOK_CMD = "squadrant hooks";
9724
10936
  function installClaudeHooks(opts = {}) {
9725
- const settingsPath = opts.settingsPath ?? join20(homedir12(), ".claude", "settings.json");
10937
+ const settingsPath = opts.settingsPath ?? join24(homedir13(), ".claude", "settings.json");
9726
10938
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
9727
10939
  const readFile7 = opts.readFile ?? defaultReadFile2;
9728
10940
  const writeFile6 = opts.writeFile ?? defaultWriteFile;
@@ -9894,16 +11106,16 @@ function extractDetail(sub, payload) {
9894
11106
  }
9895
11107
  return void 0;
9896
11108
  }
9897
- function defaultReadFile2(path21) {
11109
+ function defaultReadFile2(path23) {
9898
11110
  try {
9899
- return readFileSync14(path21, "utf-8");
11111
+ return readFileSync17(path23, "utf-8");
9900
11112
  } catch {
9901
11113
  return void 0;
9902
11114
  }
9903
11115
  }
9904
- function defaultWriteFile(path21, content) {
9905
- mkdirSync6(path21.replace(/\/[^/]+$/, ""), { recursive: true });
9906
- writeFileSync9(path21, content, "utf-8");
11116
+ function defaultWriteFile(path23, content) {
11117
+ mkdirSync7(path23.replace(/\/[^/]+$/, ""), { recursive: true });
11118
+ writeFileSync10(path23, content, "utf-8");
9907
11119
  }
9908
11120
 
9909
11121
  // packages/workspaces/dist/crew-pane.js
@@ -9983,9 +11195,9 @@ init_dist();
9983
11195
 
9984
11196
  // packages/cli/src/lib/daemon-restart-broadcast.ts
9985
11197
  init_dist();
9986
- import path20 from "path";
11198
+ import path22 from "path";
9987
11199
  function statePath2(stateRoot) {
9988
- return path20.join(stateRoot, "daemon-restart-state.json");
11200
+ return path22.join(stateRoot, "daemon-restart-state.json");
9989
11201
  }
9990
11202
  function computeRestartSignature(version, buildMtimeMs) {
9991
11203
  return `${version}::${buildMtimeMs}`;
@@ -10033,25 +11245,25 @@ async function maybeBroadcastDaemonRestart(opts) {
10033
11245
  }
10034
11246
 
10035
11247
  // packages/cli/src/lib/captain-channel-factory.ts
10036
- import { createServer as createServer3, connect as netConnect } from "net";
10037
- import fs16 from "fs";
10038
- import { join as join21 } from "path";
11248
+ import { createServer as createServer4, connect as netConnect } from "net";
11249
+ import fs18 from "fs";
11250
+ import { join as join25 } from "path";
10039
11251
  import { randomUUID as randomUUID5 } from "crypto";
10040
11252
  import chalk2 from "chalk";
10041
11253
  init_dist2();
10042
11254
  var shared;
10043
- var registryEntryPath = () => join21(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
11255
+ var registryEntryPath = () => join25(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
10044
11256
  function unregisterSenderIdentity() {
10045
11257
  try {
10046
- fs16.unlinkSync(registryEntryPath());
11258
+ fs18.unlinkSync(registryEntryPath());
10047
11259
  } catch {
10048
11260
  }
10049
11261
  }
10050
11262
  function registerSenderIdentity(socketPath) {
10051
11263
  try {
10052
11264
  unregisterSenderIdentity();
10053
- fs16.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
10054
- fs16.writeFileSync(
11265
+ fs18.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
11266
+ fs18.writeFileSync(
10055
11267
  registryEntryPath(),
10056
11268
  JSON.stringify({
10057
11269
  pid: process.pid,
@@ -10072,12 +11284,12 @@ async function sharedReceiptListener() {
10072
11284
  const socketPath = `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`;
10073
11285
  const listener = new ClaudeReceiptListener({
10074
11286
  socketPath,
10075
- createServer: (h) => createServer3(h),
11287
+ createServer: (h) => createServer4(h),
10076
11288
  // A UDS path is not cleaned up when a process is killed, so our own leftover
10077
11289
  // must never be the reason we refuse to start.
10078
11290
  unlinkStale: (p) => {
10079
11291
  try {
10080
- fs16.unlinkSync(p);
11292
+ fs18.unlinkSync(p);
10081
11293
  } catch {
10082
11294
  }
10083
11295
  },
@@ -10102,15 +11314,28 @@ async function buildCaptainChannel() {
10102
11314
  // in /tmp/x registers cwd "/private/tmp/x" on macOS, so cwd matching silently
10103
11315
  // missed and every captain ping reported accepted-unconfirmed.
10104
11316
  statusFor: (taskId) => readClaudeStatusBySocketPath(captainSocketPath(taskId)),
10105
- wire: (p, e) => writeLine(p, e, { connect: (path21) => netConnect(path21) }),
11317
+ wire: (p, e) => writeLine(p, e, { connect: (path23) => netConnect(path23) }),
10106
11318
  receipts,
10107
11319
  newMsgId: () => randomUUID5(),
10108
11320
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
10109
11321
  log: (m) => console.error(chalk2.dim(m))
10110
11322
  });
10111
11323
  }
10112
- async function buildCaptainChannelWithRetry(opts = {}) {
10113
- const build = opts.build ?? buildCaptainChannel;
11324
+ async function buildCaptainChannels(opts) {
11325
+ const claude = await buildCaptainChannel();
11326
+ const opencode = new OpencodeHttpChannel({
11327
+ portFor: (project) => readCaptainAddress(opts.stateRoot, project)?.port,
11328
+ sessionFor: (project) => readCaptainAddress(opts.stateRoot, project)?.sessionId
11329
+ });
11330
+ return {
11331
+ channels: { claude, opencode },
11332
+ agentFor: (project) => resolveCaptainAgent(opts.stateRoot, project, opts.configAgent)
11333
+ };
11334
+ }
11335
+ function resolveCaptainAgent(stateRoot, project, configAgent) {
11336
+ return readCaptainAddress(stateRoot, project)?.agent ?? configAgent;
11337
+ }
11338
+ async function retryForever(build, opts = {}) {
10114
11339
  const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => {
10115
11340
  const t = setTimeout(r, ms);
10116
11341
  t.unref?.();
@@ -10129,14 +11354,20 @@ async function buildCaptainChannelWithRetry(opts = {}) {
10129
11354
  }
10130
11355
  }
10131
11356
  }
11357
+ function buildCaptainChannelsWithRetry(opts) {
11358
+ return retryForever(
11359
+ () => buildCaptainChannels({ stateRoot: opts.stateRoot, configAgent: opts.configAgent }),
11360
+ opts
11361
+ );
11362
+ }
10132
11363
 
10133
11364
  // packages/cli/src/squadrantd.ts
10134
11365
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
10135
- var CLI_BIN = join22(dirname4(SELF_PATH2), "index.js");
11366
+ var CLI_BIN = join26(dirname5(SELF_PATH2), "index.js");
10136
11367
  function readPkgVersion() {
10137
11368
  try {
10138
- const pkgPath = join22(dirname4(SELF_PATH2), "..", "package.json");
10139
- return JSON.parse(readFileSync15(pkgPath, "utf-8")).version ?? "unknown";
11369
+ const pkgPath = join26(dirname5(SELF_PATH2), "..", "package.json");
11370
+ return JSON.parse(readFileSync18(pkgPath, "utf-8")).version ?? "unknown";
10140
11371
  } catch {
10141
11372
  return "unknown";
10142
11373
  }
@@ -10158,7 +11389,7 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound) {
10158
11389
  return createTelegramBridge({
10159
11390
  cfg,
10160
11391
  stateRoot,
10161
- configRoot: dirname4(stateRoot),
11392
+ configRoot: dirname5(stateRoot),
10162
11393
  client,
10163
11394
  appendCaptainMessage,
10164
11395
  log,
@@ -10283,7 +11514,7 @@ function startSquadrantd(opts = {}) {
10283
11514
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
10284
11515
  );
10285
11516
  },
10286
- cursorFile: join22(stateRoot, "cmux-events.seq"),
11517
+ cursorFile: join26(stateRoot, "cmux-events.seq"),
10287
11518
  log
10288
11519
  });
10289
11520
  const cmuxStoreSource = new CmuxStoreSource({ log });
@@ -10310,14 +11541,23 @@ function startSquadrantd(opts = {}) {
10310
11541
  log
10311
11542
  }))
10312
11543
  ) : void 0);
11544
+ const cfg = loadConfig();
11545
+ const routerCfg = cfg.defaults.router;
11546
+ ctx.routerService = opts.routerService ?? (shouldBuildRouterService(routerCfg, !!process.env.VITEST) && routerCfg ? createRouterService(routerCfg, Object.keys(cfg.projects), { log }) : void 0);
10313
11547
  if (opts.notifyFault) ctx.notifyFault = opts.notifyFault;
10314
11548
  else if (!process.env.VITEST) ctx.notifyFault = buildNotifyFault(log);
10315
11549
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
10316
11550
  if (!process.env.VITEST) {
10317
11551
  ctx.captainChannelMode = () => loadConfig().defaults.captainChannel ?? "off";
10318
11552
  if (ctx.captainChannelMode() !== "off") {
10319
- void buildCaptainChannelWithRetry({ log }).then((ch) => {
10320
- ctx.captainChannel = ch;
11553
+ void buildCaptainChannelsWithRetry({
11554
+ stateRoot: join26(homedir14(), ".config", "squadrant", "state"),
11555
+ configAgent: loadConfig().defaults.roles?.captain?.agent,
11556
+ log
11557
+ }).then(({ channels, agentFor }) => {
11558
+ ctx.captainChannels = channels;
11559
+ ctx.captainAgentFor = agentFor;
11560
+ ctx.captainChannel = channels.claude;
10321
11561
  }).catch((e) => log(`captain-channel: unexpected retry-loop error: ${e.message}`));
10322
11562
  }
10323
11563
  }
@@ -10426,6 +11666,11 @@ ${buildCompletionProtocol(rec.id, rec.project)}`;
10426
11666
  } catch (e) {
10427
11667
  log(`native hook install failed: ${e.message}`);
10428
11668
  }
11669
+ try {
11670
+ syncShippedOpencodeSkills({ pkgRoot: join26(dirname5(SELF_PATH2), "..") });
11671
+ } catch (e) {
11672
+ log(`opencode skills sync failed: ${e.message}`);
11673
+ }
10429
11674
  const hookPrevSnaps = /* @__PURE__ */ new Map();
10430
11675
  const hookDeps = {
10431
11676
  resolve: (hint) => {
@@ -10560,7 +11805,7 @@ function logCrashMarker(kind, err) {
10560
11805
  const message = err instanceof Error ? err.stack ?? err.message : String(err);
10561
11806
  process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${kind} pid=${process.pid} error=${message}
10562
11807
  `);
10563
- const stateRoot = join22(homedir13(), ".config", "squadrant", "state");
11808
+ const stateRoot = join26(homedir14(), ".config", "squadrant", "state");
10564
11809
  writeExitMarker(stateRoot, {
10565
11810
  ts: (/* @__PURE__ */ new Date()).toISOString(),
10566
11811
  pid: process.pid,
@@ -10573,7 +11818,7 @@ function logCrashMarker(kind, err) {
10573
11818
  `));
10574
11819
  }
10575
11820
  function isMonorepoCheckout(scriptPath, dirExists = existsSync13) {
10576
- return dirExists(join22(dirname4(resolve3(scriptPath)), "..", "packages"));
11821
+ return dirExists(join26(dirname5(resolve3(scriptPath)), "..", "packages"));
10577
11822
  }
10578
11823
  function isLinkedWorktree(scriptPath, statFile = (p) => {
10579
11824
  try {
@@ -10582,7 +11827,7 @@ function isLinkedWorktree(scriptPath, statFile = (p) => {
10582
11827
  return void 0;
10583
11828
  }
10584
11829
  }) {
10585
- const dotGit = join22(dirname4(resolve3(scriptPath)), "..", ".git");
11830
+ const dotGit = join26(dirname5(resolve3(scriptPath)), "..", ".git");
10586
11831
  return statFile(dotGit)?.isFile === true;
10587
11832
  }
10588
11833
  if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {