squadrant 0.19.4 → 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();
@@ -887,6 +906,11 @@ function reduce(rec, ev, now) {
887
906
  return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
888
907
  case "task.turn.started":
889
908
  return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
909
+ case "task.turn.failed":
910
+ // #763: a crew's turn died on an API error (StopFailure hook). Anti-#2576:
911
+ // NOT task.failed — a hook may never terminalize a task. Falls through to
912
+ // the exact same turn-boundary transition as task.turn.completed below;
913
+ // only the notify message (reduce.ts formatMessage) differs.
890
914
  case "task.turn.completed":
891
915
  if (isStickyAttention(rec.state))
892
916
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
@@ -1002,6 +1026,9 @@ function formatMessage(rec, event) {
1002
1026
  return `CREW STALLED ${tag}: no heartbeat in ${rec.heartbeatBudgetMs}ms`;
1003
1027
  }
1004
1028
  case "awaiting-input":
1029
+ if (event?.type === "task.turn.failed") {
1030
+ return `CREW TURN FAILED ${tag}: API error ended the turn (not a stall) \u2014 ${event.error}`;
1031
+ }
1005
1032
  return `CREW IDLE ${tag}: turn ended, awaiting your reply.`;
1006
1033
  default:
1007
1034
  return null;
@@ -1226,7 +1253,7 @@ function createDaemon(deps) {
1226
1253
  async sweep() {
1227
1254
  const t = now();
1228
1255
  const surfaceAlive = deps.isSurfaceAlive ?? (async () => "unknown");
1229
- 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)));
1230
1257
  for (const project of projects) {
1231
1258
  const terminal = store.list(project).filter((r) => TERMINAL_STATES.has(r.state)).sort((a, b) => b.lastHeartbeat - a.lastHeartbeat);
1232
1259
  for (const r of terminal.slice(TERMINAL_RECORD_KEEP_PER_PROJECT)) {
@@ -1411,6 +1438,7 @@ var init_reduce = __esm({
1411
1438
  "task.session",
1412
1439
  "task.turn.started",
1413
1440
  "task.turn.completed",
1441
+ "task.turn.failed",
1414
1442
  "task.delta",
1415
1443
  "task.input.requested",
1416
1444
  "task.approval.requested",
@@ -2020,6 +2048,12 @@ function safeSegment(kind, s) {
2020
2048
  }
2021
2049
  return s;
2022
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
+ }
2023
2057
  function createStore(root) {
2024
2058
  const rootResolved = resolve(root);
2025
2059
  const assertUnderRoot = (target) => {
@@ -2067,7 +2101,7 @@ function createStore(root) {
2067
2101
  } catch {
2068
2102
  return void 0;
2069
2103
  }
2070
- }).filter((r) => r !== void 0);
2104
+ }).filter(isTaskRecord);
2071
2105
  },
2072
2106
  listAll() {
2073
2107
  if (!existsSync6(root))
@@ -2355,10 +2389,10 @@ function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
2355
2389
  isUpgrade: isVersionUpgrade(parsed.daemonEntry, thisEntry)
2356
2390
  };
2357
2391
  }
2358
- function sanitizePathForPlist(path21) {
2392
+ function sanitizePathForPlist(path23) {
2359
2393
  const seen = /* @__PURE__ */ new Set();
2360
2394
  const stable = [];
2361
- for (const p of path21.split(":")) {
2395
+ for (const p of path23.split(":")) {
2362
2396
  if (!p)
2363
2397
  continue;
2364
2398
  if (p.includes("/.claude/plugins/"))
@@ -2560,6 +2594,9 @@ function isOperatorInitiatedCommand(topLevelArg) {
2560
2594
  function isReadOnlyCrewCommand(argv) {
2561
2595
  return argv[2] === "crew" && argv[3] !== void 0 && READ_ONLY_CREW_SUBCOMMANDS.has(argv[3]);
2562
2596
  }
2597
+ function isReadOnlyTopLevelCommand(argv) {
2598
+ return argv[2] !== void 0 && READ_ONLY_TOP_LEVEL_COMMANDS.has(argv[2]);
2599
+ }
2563
2600
  function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2564
2601
  if (restartInFlight)
2565
2602
  return;
@@ -2627,7 +2664,7 @@ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
2627
2664
  releaseDaemonLock();
2628
2665
  }
2629
2666
  }
2630
- 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;
2631
2668
  var init_launchd = __esm({
2632
2669
  "packages/core/dist/launchd.js"() {
2633
2670
  LABEL = "com.squadrant.daemon";
@@ -2635,6 +2672,7 @@ var init_launchd = __esm({
2635
2672
  restartInFlight = false;
2636
2673
  OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
2637
2674
  READ_ONLY_CREW_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "read", "tasks"]);
2675
+ READ_ONLY_TOP_LEVEL_COMMANDS = /* @__PURE__ */ new Set(["sessions", "whoami"]);
2638
2676
  }
2639
2677
  });
2640
2678
 
@@ -2914,6 +2952,7 @@ function buildContext(opts) {
2914
2952
  opencodeBridge: null,
2915
2953
  cmuxEventsBridge: null,
2916
2954
  telegramBridge: void 0,
2955
+ routerService: void 0,
2917
2956
  notifyFault: opts.notifyFault ?? (() => {
2918
2957
  }),
2919
2958
  lifecycleSources: opts.lifecycleSources ?? [],
@@ -3399,6 +3438,72 @@ var init_control_channel = __esm({
3399
3438
  }
3400
3439
  });
3401
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
+
3402
3507
  // packages/core/dist/crew-routing.js
3403
3508
  function resolveCrewRoute(taskText, config) {
3404
3509
  const rules = config.defaults.crewRouting?.rules;
@@ -3410,6 +3515,7 @@ function resolveCrewRoute(taskText, config) {
3410
3515
  return {
3411
3516
  agent: rule.agent,
3412
3517
  ...rule.model !== void 0 ? { model: rule.model } : {},
3518
+ ...rule.backend !== void 0 ? { backend: rule.backend } : {},
3413
3519
  tier: rule.tier,
3414
3520
  matchedRule: rule.match
3415
3521
  };
@@ -3547,17 +3653,28 @@ async function pollFirstTurnConfirmedAt(getTaskRecord, project, id, scrapeSettle
3547
3653
  await new Promise((r) => setTimeout(r, FIRST_TURN_HOOK_POLL_INTERVAL_MS));
3548
3654
  }
3549
3655
  }
3550
- function firstTrueOrBothFalse(a, b) {
3656
+ function firstTrueOrBothFalse(a, b, deadlineMs) {
3551
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);
3552
3667
  let settledFalseCount = 0;
3553
3668
  const onSettle = (ok2) => {
3669
+ if (done)
3670
+ return;
3554
3671
  if (ok2) {
3555
- resolve4(true);
3672
+ finish(true);
3556
3673
  return;
3557
3674
  }
3558
3675
  settledFalseCount++;
3559
3676
  if (settledFalseCount === 2)
3560
- resolve4(false);
3677
+ finish(false);
3561
3678
  };
3562
3679
  a.then(onSettle, () => onSettle(false));
3563
3680
  b.then(onSettle, () => onSettle(false));
@@ -3645,6 +3762,11 @@ async function runCrewSpawn(input, config, deps) {
3645
3762
  if (!agent) {
3646
3763
  throw new Error(`Unknown agent '${agentName}'. Known: claude, codex, gemini, opencode.`);
3647
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 });
3648
3770
  if (agentName === "codex") {
3649
3771
  const codexRoleFile = path9.join(TEMPLATES_DIR, `crew.${agent.templateSuffix}.md`);
3650
3772
  const roleInstructions = fs9.existsSync(codexRoleFile) ? fs9.readFileSync(codexRoleFile, "utf8") : void 0;
@@ -3667,12 +3789,19 @@ async function runCrewSpawn(input, config, deps) {
3667
3789
  const interactive = agent.name === "claude" || agent.name === "opencode";
3668
3790
  const crewRole = config.defaults.roles?.crew;
3669
3791
  const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
3670
- const crewModel = input.model ?? route?.model ?? configModel;
3792
+ const crewModel = resolveRouterModel(input.model ?? route?.model ?? configModel, agent.name, config.defaults.router);
3671
3793
  const crewThinking = input.thinking ?? config.defaults.roles?.crew?.thinking;
3672
3794
  if (agentName !== "claude") {
3673
3795
  deps.onModelResolved?.({ agentName, model: crewModel });
3674
3796
  }
3675
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
+ }
3676
3805
  ensureSocksDir();
3677
3806
  const messagingSocketPath = path9.join(CC_SOCKS_DIR, `squadrant-${randomUUID3()}.sock`);
3678
3807
  const rec = await deps.dispatchCrew({
@@ -3711,7 +3840,8 @@ async function runCrewSpawn(input, config, deps) {
3711
3840
  const title2 = titleFor(input.project, name);
3712
3841
  const pane2 = await deps.runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
3713
3842
  const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
3714
- 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)}`);
3715
3845
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
3716
3846
  let claudeFirstTurn = firstTurnTask;
3717
3847
  if (Buffer.byteLength(claudeFirstTurn, "utf8") > FIRST_TURN_INLINE_MAX_BYTES) {
@@ -3724,7 +3854,7 @@ async function runCrewSpawn(input, config, deps) {
3724
3854
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
3725
3855
  const scrapeDelivered = sendPromise.then((r) => r.delivered).catch(() => false);
3726
3856
  const cancelHookPoll = { stopped: false };
3727
- 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;
3728
3858
  cancelHookPoll.stopped = true;
3729
3859
  if (!delivered) {
3730
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.
@@ -4012,6 +4142,8 @@ var init_crew_spawn = __esm({
4012
4142
  "packages/core/dist/crew-spawn.js"() {
4013
4143
  init_control_channel();
4014
4144
  init_dist();
4145
+ init_router_resolution();
4146
+ init_env();
4015
4147
  init_crew_routing();
4016
4148
  init_crew_protocol();
4017
4149
  init_crew_lifecycle();
@@ -4062,6 +4194,164 @@ var init_captain_channel = __esm({
4062
4194
  }
4063
4195
  });
4064
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
+
4065
4355
  // packages/core/dist/daemon/delivery-loop.js
4066
4356
  function discoverCaptainSurface(surfaces, captainTitle) {
4067
4357
  return surfaces.find((s) => s.title === captainTitle) ?? null;
@@ -4187,7 +4477,12 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4187
4477
  const cmux2 = daemonCmux;
4188
4478
  const cfg = loadConfig();
4189
4479
  const deliveries = /* @__PURE__ */ new Map();
4190
- 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
+ };
4191
4486
  const lastDeferred = /* @__PURE__ */ new Map();
4192
4487
  const inFlightDelivery = () => {
4193
4488
  let worst = null;
@@ -4273,20 +4568,55 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4273
4568
  deliverEntry = { ...entry, message: `${prefix}${entry.message}` };
4274
4569
  }
4275
4570
  const result = await d.deliver(deliverEntry, async (text, sendOpts) => {
4276
- let handledByChannel = false;
4277
- try {
4278
- const mode = ctx.captainChannelMode?.() ?? "off";
4279
- const r = await deliverToCaptain(project, text, {
4280
- channel: ctx.captainChannel,
4281
- mode,
4282
- log
4283
- });
4284
- handledByChannel = r.handled;
4285
- } catch (e) {
4286
- log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4287
- }
4288
- if (handledByChannel) {
4289
- 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");
4290
4620
  }
4291
4621
  try {
4292
4622
  return await cmux2.send(surface, text, sendOpts);
@@ -4310,20 +4640,23 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4310
4640
  lastDeferred.delete(project);
4311
4641
  projectBackoff.delete(project);
4312
4642
  } else {
4313
- const { maxDeferCount, stuck: stuck2 } = d.stats();
4643
+ const { maxDeferCount } = d.stats();
4644
+ const stuckNow = deliveryStats(project)?.stuck ?? false;
4314
4645
  if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4315
4646
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4316
4647
  }
4317
4648
  lastDeferred.set(project, { seq: entry.seq, deferCount: maxDeferCount });
4318
- if (stuck2) {
4649
+ if (stuckNow) {
4319
4650
  const streak = (projectBackoff.get(project)?.streak ?? 0) + 1;
4320
- 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);
4321
4653
  projectBackoff.set(project, { nextAttemptAt: Date.now() + backoffMs, streak });
4322
4654
  }
4323
4655
  break;
4324
4656
  }
4325
4657
  }
4326
- const stuck = d.stats().stuck;
4658
+ const stats = d.stats();
4659
+ const stuck = stats.stuck || stats.reason === "no-channel";
4327
4660
  if (stuck && !stuckNotified.has(project)) {
4328
4661
  stuckNotified.add(project);
4329
4662
  const { maxDeferCount, reason } = d.stats();
@@ -4367,6 +4700,8 @@ var init_delivery_loop = __esm({
4367
4700
  init_down_alert();
4368
4701
  init_liveness2();
4369
4702
  init_captain_channel();
4703
+ init_captain_record();
4704
+ init_opencode_session();
4370
4705
  CURSOR_SUBSCRIBER = "captain";
4371
4706
  TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
4372
4707
  STUCK_ALERT_TEXT = {
@@ -4374,6 +4709,7 @@ var init_delivery_loop = __esm({
4374
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.`,
4375
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.`,
4376
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)`,
4377
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.`,
4378
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.`
4379
4715
  };
@@ -4401,6 +4737,32 @@ var init_gates = __esm({
4401
4737
  }
4402
4738
  });
4403
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
+
4404
4766
  // packages/core/dist/daemon/server.js
4405
4767
  function createServer2(ctx, handlers) {
4406
4768
  const { store, log, attachConns } = ctx;
@@ -4426,6 +4788,13 @@ function createServer2(ctx, handlers) {
4426
4788
  if (msg.kind === "event") {
4427
4789
  return ctx.d.handle(msg);
4428
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
+ }
4429
4798
  return ctx.d.handle(msg);
4430
4799
  },
4431
4800
  onAttach: (conn, frame) => {
@@ -4461,18 +4830,19 @@ function createServer2(ctx, handlers) {
4461
4830
  var init_server = __esm({
4462
4831
  "packages/core/dist/daemon/server.js"() {
4463
4832
  init_protocol();
4833
+ init_credentials();
4464
4834
  }
4465
4835
  });
4466
4836
 
4467
4837
  // packages/core/dist/daemon/exit-marker.js
4468
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
4469
- 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";
4470
4840
  function exitMarkerPath(stateRoot) {
4471
- return join11(stateRoot, "exit-marker.json");
4841
+ return join12(stateRoot, "exit-marker.json");
4472
4842
  }
4473
4843
  function writeExitMarker(stateRoot, marker, log) {
4474
4844
  try {
4475
- writeFileSync8(exitMarkerPath(stateRoot), JSON.stringify(marker));
4845
+ writeFileSync9(exitMarkerPath(stateRoot), JSON.stringify(marker));
4476
4846
  } catch (e) {
4477
4847
  log(`exit marker write failed: ${e.message}`);
4478
4848
  }
@@ -4483,7 +4853,7 @@ function consumeExitMarker(stateRoot, now = Date.now) {
4483
4853
  return { marker: null };
4484
4854
  let marker = null;
4485
4855
  try {
4486
- marker = JSON.parse(readFileSync8(p, "utf-8"));
4856
+ marker = JSON.parse(readFileSync9(p, "utf-8"));
4487
4857
  } catch {
4488
4858
  marker = null;
4489
4859
  }
@@ -4497,18 +4867,18 @@ function consumeExitMarker(stateRoot, now = Date.now) {
4497
4867
  return { marker, gapMs };
4498
4868
  }
4499
4869
  function runningMarkerPath(stateRoot) {
4500
- return join11(stateRoot, "running-marker.json");
4870
+ return join12(stateRoot, "running-marker.json");
4501
4871
  }
4502
4872
  function writeRunningMarker(stateRoot, marker, log) {
4503
4873
  try {
4504
- writeFileSync8(runningMarkerPath(stateRoot), JSON.stringify(marker));
4874
+ writeFileSync9(runningMarkerPath(stateRoot), JSON.stringify(marker));
4505
4875
  } catch (e) {
4506
4876
  log(`running marker write failed: ${e.message}`);
4507
4877
  }
4508
4878
  }
4509
4879
  function readRunningMarker(stateRoot) {
4510
4880
  try {
4511
- return JSON.parse(readFileSync8(runningMarkerPath(stateRoot), "utf-8"));
4881
+ return JSON.parse(readFileSync9(runningMarkerPath(stateRoot), "utf-8"));
4512
4882
  } catch {
4513
4883
  return null;
4514
4884
  }
@@ -4528,8 +4898,8 @@ var init_exit_marker = __esm({
4528
4898
 
4529
4899
  // packages/core/dist/daemon/snapshot-gather.js
4530
4900
  import { fileURLToPath as fileURLToPath2 } from "url";
4531
- import { join as join12 } from "path";
4532
- 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";
4533
4903
  function distBuiltAt() {
4534
4904
  try {
4535
4905
  return statSync3(SELF_PATH).mtimeMs;
@@ -4537,10 +4907,10 @@ function distBuiltAt() {
4537
4907
  return 0;
4538
4908
  }
4539
4909
  }
4540
- function gatherLogStats(path21, now, windowMs) {
4910
+ function gatherLogStats(path23, now, windowMs) {
4541
4911
  let sizeBytes = 0;
4542
4912
  try {
4543
- sizeBytes = statSync3(path21).size;
4913
+ sizeBytes = statSync3(path23).size;
4544
4914
  } catch {
4545
4915
  return { errorCount: 0, sizeBytes: 0, windowMs };
4546
4916
  }
@@ -4551,7 +4921,7 @@ function gatherLogStats(path21, now, windowMs) {
4551
4921
  const len = sizeBytes - start;
4552
4922
  let text = "";
4553
4923
  try {
4554
- const fd = openSync2(path21, "r");
4924
+ const fd = openSync2(path23, "r");
4555
4925
  try {
4556
4926
  const buf = Buffer.alloc(len);
4557
4927
  readSync(fd, buf, 0, len, start);
@@ -4582,7 +4952,7 @@ function gatherStoreStats(store, stateRoot, project) {
4582
4952
  for (const r of store.list(project))
4583
4953
  byState[r.state] = (byState[r.state] ?? 0) + 1;
4584
4954
  let corruptCount = 0;
4585
- const dir = join12(stateRoot, project);
4955
+ const dir = join13(stateRoot, project);
4586
4956
  try {
4587
4957
  for (const n of readdirSync3(dir)) {
4588
4958
  if (n.includes(".corrupt.")) {
@@ -4592,7 +4962,7 @@ function gatherStoreStats(store, stateRoot, project) {
4592
4962
  if (!n.endsWith(".json"))
4593
4963
  continue;
4594
4964
  try {
4595
- JSON.parse(readFileSync9(join12(dir, n), "utf-8"));
4965
+ JSON.parse(readFileSync10(join13(dir, n), "utf-8"));
4596
4966
  } catch {
4597
4967
  corruptCount++;
4598
4968
  }
@@ -4607,7 +4977,7 @@ function gatherResults(resultsDir) {
4607
4977
  try {
4608
4978
  for (const n of readdirSync3(resultsDir)) {
4609
4979
  try {
4610
- const s = statSync3(join12(resultsDir, n));
4980
+ const s = statSync3(join13(resultsDir, n));
4611
4981
  if (s.isFile()) {
4612
4982
  fileCount++;
4613
4983
  totalBytes += s.size;
@@ -4627,7 +4997,7 @@ var init_snapshot_gather = __esm({
4627
4997
  });
4628
4998
 
4629
4999
  // packages/core/dist/daemon/start.js
4630
- import { join as join13, dirname as dirname3 } from "path";
5000
+ import { join as join14, dirname as dirname4 } from "path";
4631
5001
  import { readdir } from "fs/promises";
4632
5002
  function startDaemon(ctx, opts, pkgVersion) {
4633
5003
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
@@ -4704,7 +5074,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4704
5074
  return out;
4705
5075
  }
4706
5076
  async function gatherSnapshotInputs(now) {
4707
- const logPath2 = join13(dirname3(stateRoot), "squadrantd.log");
5077
+ const logPath2 = join14(dirname4(stateRoot), "squadrantd.log");
4708
5078
  const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4709
5079
  const projects = await Promise.all(tier2Projects.map(async (project) => {
4710
5080
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
@@ -4781,6 +5151,9 @@ function startDaemon(ctx, opts, pkgVersion) {
4781
5151
  log(`telegram bridge start failed: ${e.message}`);
4782
5152
  }
4783
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
+ }
4784
5157
  const autoConfigSafe = !!opts.runCmuxAutoConfig || !process.env.VITEST;
4785
5158
  if (autoConfigSafe) {
4786
5159
  try {
@@ -4866,7 +5239,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4866
5239
  let rotationTimer;
4867
5240
  let rotationTick;
4868
5241
  if (rotationInterval > 0) {
4869
- const inboxPath = join13(stateRoot, "inbox");
5242
+ const inboxPath = join14(stateRoot, "inbox");
4870
5243
  rotationTick = async () => {
4871
5244
  try {
4872
5245
  let entries;
@@ -4890,7 +5263,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4890
5263
  rotationTimer.unref?.();
4891
5264
  }
4892
5265
  return {
4893
- stop(reason = "requested") {
5266
+ async stop(reason = "requested") {
4894
5267
  const ppid = process.ppid;
4895
5268
  const uptimeMs = Math.round(process.uptime() * 1e3);
4896
5269
  const inFlight = inFlightDelivery();
@@ -4919,6 +5292,10 @@ function startDaemon(ctx, opts, pkgVersion) {
4919
5292
  }
4920
5293
  for (const kill of ctx.activeHeadlessKills)
4921
5294
  kill();
5295
+ try {
5296
+ await ctx.routerService?.stop();
5297
+ } catch {
5298
+ }
4922
5299
  return new Promise((resolve4) => server.close(() => {
4923
5300
  log(`exit-complete pid=${process.pid}`);
4924
5301
  resolve4();
@@ -5933,7 +6310,7 @@ var init_bridge = __esm({
5933
6310
  });
5934
6311
 
5935
6312
  // packages/core/dist/restart-daemon.js
5936
- import { execFileSync as execFileSync4 } from "child_process";
6313
+ import { execFileSync as execFileSync5 } from "child_process";
5937
6314
  import { existsSync as existsSync10 } from "fs";
5938
6315
  function defaultIsRunning() {
5939
6316
  return existsSync10(DAEMON_SOCK_PATH);
@@ -5943,7 +6320,7 @@ function defaultRunKickstart() {
5943
6320
  const target = `gui/${uid}/${LABEL}`;
5944
6321
  if (tryAcquireDaemonLock()) {
5945
6322
  try {
5946
- execFileSync4("launchctl", kickstartArgv(target, true), { stdio: "ignore" });
6323
+ execFileSync5("launchctl", kickstartArgv(target, true), { stdio: "ignore" });
5947
6324
  } finally {
5948
6325
  releaseDaemonLock();
5949
6326
  }
@@ -6165,128 +6542,674 @@ var init_telegram = __esm({
6165
6542
  }
6166
6543
  });
6167
6544
 
6168
- // packages/core/dist/group-dispatch.js
6169
- import { randomUUID as randomUUID4 } from "crypto";
6170
- function resolveCurrentProject(config) {
6171
- const cwd = process.cwd();
6172
- for (const [name, proj] of Object.entries(config.projects)) {
6173
- const resolvedPath = resolveHome(proj.path);
6174
- if (cwd.startsWith(resolvedPath))
6175
- return name;
6545
+ // packages/core/dist/router/types.js
6546
+ var init_types = __esm({
6547
+ "packages/core/dist/router/types.js"() {
6176
6548
  }
6177
- 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");
6178
6555
  }
6179
- async function isCaptainAlive(project, sockPath = DAEMON_SOCK_PATH) {
6180
- try {
6181
- const health = await sendRequest(sockPath, { kind: "health", project }, 5e3);
6182
- const captain = health?.find((h) => h.kind === "captain" && h.project === project);
6183
- return captain?.state === "alive";
6184
- } catch {
6185
- return false;
6186
- }
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;
6187
6561
  }
6188
- async function waitForWarmup(project, sockPath = DAEMON_SOCK_PATH, timeoutMs = GROUP_DISPATCH_WARMUP_TIMEOUT_MS, pollMs = GROUP_DISPATCH_WARMUP_POLL_MS) {
6189
- const deadline = Date.now() + timeoutMs;
6190
- while (Date.now() < deadline) {
6191
- if (await isCaptainAlive(project, sockPath))
6192
- return true;
6193
- await new Promise((r) => setTimeout(r, pollMs));
6194
- }
6195
- return false;
6562
+ function resolveProject(tokens, header) {
6563
+ const token = parseBearer(header);
6564
+ if (!token)
6565
+ return null;
6566
+ return tokens.get(token) ?? null;
6196
6567
  }
6197
- async function dispatchToSibling(opts) {
6198
- const config = loadConfig();
6199
- const fromCfg = config.projects[opts.fromProject];
6200
- const toCfg = config.projects[opts.toProject];
6201
- if (!toCfg) {
6202
- throw new Error(`target project '${opts.toProject}' not found in config`);
6568
+ var init_auth2 = __esm({
6569
+ "packages/core/dist/router/auth.js"() {
6203
6570
  }
6204
- const sameGroup = !!fromCfg?.group && !!toCfg.group && fromCfg.group === toCfg.group;
6205
- if (toCfg.acceptDelegations === false) {
6206
- 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"() {
6207
6579
  }
6208
- const sockPath = opts.sockPath ?? DAEMON_SOCK_PATH;
6209
- const alive = await isCaptainAlive(opts.toProject, sockPath);
6210
- if (!alive) {
6211
- if (!sameGroup) {
6212
- 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.`);
6213
- }
6214
- if (opts.bootCaptain) {
6215
- await opts.bootCaptain(opts.toProject);
6216
- }
6217
- const warmed = await waitForWarmup(opts.toProject, sockPath, opts.warmupTimeoutMs, opts.warmupPollMs);
6218
- if (!warmed) {
6219
- 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)));
6220
6593
  }
6221
6594
  }
6222
- const now = Date.now();
6223
- const attemptId = randomUUID4();
6224
- const record = {
6225
- id: randomUUID4(),
6226
- project: opts.toProject,
6227
- originProject: opts.fromProject,
6228
- provider: opts.provider ?? "claude",
6229
- mode: opts.mode ?? "headless",
6230
- state: "submitted",
6231
- task: opts.task,
6232
- createdAt: now,
6233
- lastHeartbeat: now,
6234
- lastEvent: "dispatch",
6235
- heartbeatBudgetMs: 3e5,
6236
- attempts: [{ attemptId, startedAt: now, lastHeartbeatAt: now }]
6237
- };
6238
- const result = await sendRequest(sockPath, { kind: "dispatch", record });
6239
- return result;
6595
+ return out;
6240
6596
  }
6241
- var GROUP_DISPATCH_WARMUP_TIMEOUT_MS, GROUP_DISPATCH_WARMUP_POLL_MS;
6242
- var init_group_dispatch = __esm({
6243
- "packages/core/dist/group-dispatch.js"() {
6244
- init_dist();
6245
- init_protocol();
6246
- GROUP_DISPATCH_WARMUP_TIMEOUT_MS = 12e4;
6247
- 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"];
6248
6602
  }
6249
6603
  });
6250
6604
 
6251
- // packages/core/dist/launch-workspace.js
6252
- async function deliverStartupPrompt(runtime, refId, prompt, opts = {}) {
6253
- const classify = opts.classifyScreen ?? (() => "idle");
6254
- const readyTimeoutMs = opts.readyTimeoutMs ?? 3e4;
6255
- const settleMs = opts.settleMs ?? 2500;
6256
- const pollMs = opts.pollMs ?? 1e3;
6257
- const maxAttempts = opts.maxAttempts ?? 3;
6258
- const sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
6259
- const read = async () => runtime.readScreen(refId).catch(() => "");
6260
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
6261
- const deadline = Date.now() + readyTimeoutMs;
6262
- let preSend = await read();
6263
- let state = classify(preSend);
6264
- while (state === "loading" && Date.now() < deadline) {
6265
- await sleep3(pollMs);
6266
- preSend = await read();
6267
- state = classify(preSend);
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;
6614
+ }
6615
+ return void 0;
6616
+ }
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
+ }
6268
6677
  }
6269
- if (state === "working")
6270
- return;
6271
- await runtime.send(refId, prompt).catch(() => {
6272
- });
6273
- if (state === "loading")
6274
- return;
6275
- await sleep3(settleMs);
6276
- const after = await read();
6277
- if (after !== preSend)
6278
- return;
6279
6678
  }
6679
+ return saw;
6280
6680
  }
6281
- async function bootWorkspace(opts) {
6282
- const { runtime, workspaceName, agentCmd, cwd, navigate = false, forceFresh = false, pinToTop = false, initialPrompt } = opts;
6283
- const existing = await runtime.status(workspaceName);
6284
- if (existing && forceFresh) {
6285
- opts.onStoppingStale?.(workspaceName);
6286
- await runtime.stop(existing.id);
6287
- } else if (existing) {
6288
- opts.onAlreadyExists?.(workspaceName);
6289
- opts.selectWorkspace?.(existing.id);
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);
6290
7213
  return;
6291
7214
  }
6292
7215
  const rawCurrent = opts.getCurrentWorkspace?.() ?? null;
@@ -6889,8 +7812,10 @@ var init_source = __esm({
6889
7812
  var dist_exports = {};
6890
7813
  __export(dist_exports, {
6891
7814
  AGENT_BINS: () => AGENT_BINS,
7815
+ ANTHROPIC_ONLY_REQUEST_FIELDS: () => ANTHROPIC_ONLY_REQUEST_FIELDS,
6892
7816
  BOT_COMMANDS: () => BOT_COMMANDS,
6893
7817
  CC_SOCKS_DIR: () => CC_SOCKS_DIR,
7818
+ CMUX_PRESERVE_CLAUDE_AUTH_ENV: () => CMUX_PRESERVE_CLAUDE_AUTH_ENV,
6894
7819
  CREW_GONE_MS: () => CREW_GONE_MS,
6895
7820
  CREW_STALE_MS: () => CREW_STALE_MS,
6896
7821
  CaptainDelivery: () => CaptainDelivery,
@@ -6910,6 +7835,10 @@ __export(dist_exports, {
6910
7835
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
6911
7836
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
6912
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,
6913
7842
  STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
6914
7843
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
6915
7844
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
@@ -6918,17 +7847,22 @@ __export(dist_exports, {
6918
7847
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
6919
7848
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
6920
7849
  ageText: () => ageText,
7850
+ anthropicError: () => anthropicError,
6921
7851
  appendCaptainMessage: () => appendCaptainMessage,
6922
7852
  appendToMailbox: () => appendToMailbox,
6923
7853
  assembleDaemonSnapshot: () => assembleDaemonSnapshot,
7854
+ assertBackendUsable: () => assertBackendUsable,
6924
7855
  bootWorkspace: () => bootWorkspace,
6925
7856
  buildCompletionProtocol: () => buildCompletionProtocol,
6926
7857
  buildContext: () => buildContext,
6927
7858
  buildDaemonPath: () => buildDaemonPath,
6928
7859
  buildFreshness: () => buildFreshness,
7860
+ buildRouterCredentialsRequest: () => buildRouterCredentialsRequest,
7861
+ buildRouterEnv: () => buildRouterEnv,
6929
7862
  buildSideFirstTurn: () => buildSideFirstTurn,
6930
7863
  capAllowed: () => capAllowed,
6931
7864
  capOutput: () => capOutput,
7865
+ captainRecordPath: () => captainRecordPath,
6932
7866
  captainSocketPath: () => captainSocketPath,
6933
7867
  checkFact: () => checkFact,
6934
7868
  classifyHealth: () => classifyHealth,
@@ -6947,11 +7881,14 @@ __export(dist_exports, {
6947
7881
  createInteractiveProbe: () => createInteractiveProbe,
6948
7882
  createIsCaptainAlive: () => createIsCaptainAlive,
6949
7883
  createLaunch: () => createLaunch,
7884
+ createRouterService: () => createRouterService,
7885
+ createRouterShim: () => createRouterShim,
6950
7886
  createRunCommand: () => createRunCommand,
6951
7887
  createStore: () => createStore,
6952
7888
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
6953
7889
  createTelegramBridge: () => createTelegramBridge,
6954
7890
  createTelegramClient: () => createTelegramClient,
7891
+ createUsageTee: () => createUsageTee,
6955
7892
  createWorkItem: () => createWorkItem,
6956
7893
  createWorkStore: () => createWorkStore,
6957
7894
  crewPaneTitle: () => crewPaneTitle,
@@ -6972,6 +7909,7 @@ __export(dist_exports, {
6972
7909
  detectGroupAndUser: () => detectGroupAndUser,
6973
7910
  detectGroupId: () => detectGroupId,
6974
7911
  discoverCaptainSurface: () => discoverCaptainSurface,
7912
+ discoverLiveOpencodeServer: () => discoverLiveOpencodeServer,
6975
7913
  dispatchToSibling: () => dispatchToSibling,
6976
7914
  encodeFrame: () => encodeFrame,
6977
7915
  encodeMsg: () => encodeMsg,
@@ -6985,6 +7923,7 @@ __export(dist_exports, {
6985
7923
  findProjectByThread: () => findProjectByThread,
6986
7924
  findWorkItemById: () => findWorkItemById,
6987
7925
  forceKickstartAndVerify: () => forceKickstartAndVerify,
7926
+ formatCustomHeaders: () => formatCustomHeaders,
6988
7927
  formatInbound: () => formatInbound,
6989
7928
  formatInboundReceipt: () => formatInboundReceipt,
6990
7929
  formatLifecycle: () => formatLifecycle,
@@ -7001,33 +7940,44 @@ __export(dist_exports, {
7001
7940
  isNotifyActive: () => isNotifyActive,
7002
7941
  isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
7003
7942
  isReadOnlyCrewCommand: () => isReadOnlyCrewCommand,
7943
+ isReadOnlyTopLevelCommand: () => isReadOnlyTopLevelCommand,
7004
7944
  isSideTitle: () => isSideTitle,
7005
7945
  isStickyAttention: () => isStickyAttention,
7006
7946
  isTurnAccepted: () => isTurnAccepted,
7007
7947
  isVersionUpgrade: () => isVersionUpgrade,
7948
+ joinUrl: () => joinUrl,
7008
7949
  kickstartArgv: () => kickstartArgv,
7009
7950
  launchOneWorkspace: () => launchOneWorkspace,
7951
+ listSessions: () => listSessions,
7010
7952
  loadSessions: () => loadSessions,
7011
7953
  loadState: () => loadState,
7012
7954
  mailboxStats: () => mailboxStats,
7013
7955
  makeGate: () => makeGate,
7014
7956
  maskToken: () => maskToken,
7957
+ mintToken: () => mintToken,
7015
7958
  nameFromTitle: () => nameFromTitle,
7959
+ newestSessionInDirectory: () => newestSessionInDirectory,
7016
7960
  nextAutoName: () => nextAutoName,
7017
7961
  niceCrewCommand: () => niceCrewCommand,
7018
7962
  notifyToggle: () => notifyToggle,
7963
+ parseBearer: () => parseBearer,
7019
7964
  parseCommand: () => parseCommand,
7965
+ parseLiveOpencodeServers: () => parseLiveOpencodeServers,
7020
7966
  parseNotifyPref: () => parseNotifyPref,
7021
7967
  parseProgramArgs: () => parseProgramArgs,
7022
7968
  plistPath: () => plistPath,
7969
+ pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
7023
7970
  printForeignInstallError: () => printForeignInstallError,
7024
7971
  printVersionUpgradeNotice: () => printVersionUpgradeNotice,
7972
+ probeCaptainChannel: () => probeCaptainChannel,
7025
7973
  programArgsBlock: () => programArgsBlock,
7026
7974
  projectHealth: () => projectHealth,
7027
7975
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
7976
+ readCaptainAddress: () => readCaptainAddress,
7028
7977
  readCursor: () => readCursor,
7029
7978
  readFromCursor: () => readFromCursor,
7030
7979
  readRunningMarker: () => readRunningMarker,
7980
+ realpathOrSelf: () => realpathOrSelf,
7031
7981
  reapCrewChildren: () => reapCrewChildren,
7032
7982
  reapOrphanedCrews: () => reapOrphanedCrews,
7033
7983
  reconcileLiveness: () => reconcileLiveness,
@@ -7037,12 +7987,18 @@ __export(dist_exports, {
7037
7987
  reduceLifecycle: () => reduceLifecycle,
7038
7988
  releaseDaemonLock: () => releaseDaemonLock,
7039
7989
  removeRunningMarker: () => removeRunningMarker,
7990
+ renderEnvAssignments: () => renderEnvAssignments,
7040
7991
  renderPlist: () => renderPlist,
7041
7992
  reregisterDaemon: () => reregisterDaemon,
7042
7993
  resolveAgentBinDirs: () => resolveAgentBinDirs,
7994
+ resolveAndPersistOpencodeCaptain: () => resolveAndPersistOpencodeCaptain,
7995
+ resolveBackend: () => resolveBackend,
7043
7996
  resolveCrewRoute: () => resolveCrewRoute,
7044
7997
  resolveCurrentProject: () => resolveCurrentProject,
7045
7998
  resolveGate: () => resolveGate,
7999
+ resolveProject: () => resolveProject,
8000
+ resolveRouterCredentials: () => resolveRouterCredentials,
8001
+ resolveRouterUpstream: () => resolveRouterUpstream,
7046
8002
  resolveSetupGroup: () => resolveSetupGroup,
7047
8003
  resolveSetupToken: () => resolveSetupToken,
7048
8004
  resolveSetupUserId: () => resolveSetupUserId,
@@ -7070,7 +8026,9 @@ __export(dist_exports, {
7070
8026
  runTelegramSend: () => runTelegramSend,
7071
8027
  runTelegramStatus: () => runTelegramStatus,
7072
8028
  runningMarkerPath: () => runningMarkerPath,
8029
+ sameDirectory: () => sameDirectory,
7073
8030
  sanitizePathForPlist: () => sanitizePathForPlist,
8031
+ sanitizeRequest: () => sanitizeRequest,
7074
8032
  saveSessions: () => saveSessions,
7075
8033
  saveState: () => saveState,
7076
8034
  screenHasSplashMarker: () => screenHasSplashMarker,
@@ -7079,6 +8037,7 @@ __export(dist_exports, {
7079
8037
  setNotify: () => setNotify,
7080
8038
  setTopic: () => setTopic,
7081
8039
  shellQuote: () => shellQuote,
8040
+ shouldBuildRouterService: () => shouldBuildRouterService,
7082
8041
  shouldStartFresh: () => shouldStartFresh,
7083
8042
  sideNameFromTitle: () => sideNameFromTitle,
7084
8043
  sideNextAutoName: () => sideNextAutoName,
@@ -7094,8 +8053,10 @@ __export(dist_exports, {
7094
8053
  topicKey: () => topicKey,
7095
8054
  topicName: () => topicName,
7096
8055
  tryAcquireDaemonLock: () => tryAcquireDaemonLock,
8056
+ usageFromJson: () => usageFromJson,
7097
8057
  waitForCaptainDelivery: () => waitForCaptainDelivery,
7098
8058
  waitForWarmup: () => waitForWarmup,
8059
+ writeCaptainAddress: () => writeCaptainAddress,
7099
8060
  writeCursor: () => writeCursor,
7100
8061
  writeExitMarker: () => writeExitMarker,
7101
8062
  writeRunningMarker: () => writeRunningMarker,
@@ -7129,7 +8090,9 @@ var init_dist2 = __esm({
7129
8090
  init_crew_protocol();
7130
8091
  init_crew_lifecycle();
7131
8092
  init_telegram();
8093
+ init_router();
7132
8094
  init_crew_routing();
8095
+ init_router_resolution();
7133
8096
  init_restart_daemon();
7134
8097
  init_group_dispatch();
7135
8098
  init_launch_workspace();
@@ -7145,6 +8108,8 @@ var init_dist2 = __esm({
7145
8108
  init_source();
7146
8109
  init_control_channel();
7147
8110
  init_captain_channel();
8111
+ init_captain_record();
8112
+ init_opencode_session();
7148
8113
  }
7149
8114
  });
7150
8115
 
@@ -7160,14 +8125,75 @@ init_dist2();
7160
8125
  init_dist2();
7161
8126
  init_dist2();
7162
8127
  init_dist2();
7163
- import { join as join22, dirname as dirname4, resolve as resolve3 } from "path";
7164
- 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";
7165
8130
  import { fileURLToPath as fileURLToPath3 } from "url";
7166
- 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";
7167
8132
 
7168
8133
  // packages/agents/dist/drivers/claude.js
7169
8134
  import { execSync as execSync2 } from "child_process";
7170
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
+
7171
8197
  // packages/agents/dist/drivers/codex.js
7172
8198
  import { execSync as execSync3 } from "child_process";
7173
8199
 
@@ -7177,8 +8203,14 @@ import { execSync as execSync4 } from "child_process";
7177
8203
  // packages/agents/dist/drivers/opencode.js
7178
8204
  import { execSync as execSync5 } from "child_process";
7179
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
+
7180
8212
  // packages/agents/dist/drivers/launch-cmd.js
7181
- import fs13 from "fs";
8213
+ import fs14 from "fs";
7182
8214
  import path13 from "path";
7183
8215
 
7184
8216
  // packages/agents/dist/projection/cursor.js
@@ -7201,10 +8233,102 @@ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from
7201
8233
  import path17 from "path";
7202
8234
  import os9 from "os";
7203
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
+
7204
8328
  // packages/agents/dist/projection/claude.js
7205
8329
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
7206
- import path18 from "path";
7207
- import os10 from "os";
8330
+ import path19 from "path";
8331
+ import os11 from "os";
7208
8332
 
7209
8333
  // packages/agents/dist/codex/app-server-client.js
7210
8334
  import { EventEmitter } from "events";
@@ -7478,11 +8602,11 @@ function toSnapshot(ev) {
7478
8602
 
7479
8603
  // packages/agents/dist/codex/config.js
7480
8604
  import { readFile as readFile6 } from "fs/promises";
7481
- import { homedir as homedir7 } from "os";
7482
- import { join as join14 } from "path";
8605
+ import { homedir as homedir9 } from "os";
8606
+ import { join as join19 } from "path";
7483
8607
  async function resolveCodexModel() {
7484
- const home = process.env["CODEX_HOME"] ?? join14(homedir7(), ".codex");
7485
- const configPath = join14(home, "config.toml");
8608
+ const home = process.env["CODEX_HOME"] ?? join19(homedir9(), ".codex");
8609
+ const configPath = join19(home, "config.toml");
7486
8610
  let text;
7487
8611
  try {
7488
8612
  text = await readFile6(configPath, "utf8");
@@ -7994,10 +9118,10 @@ var OpencodeSseBridge = class {
7994
9118
 
7995
9119
  // packages/agents/dist/interactive/claude.js
7996
9120
  import { execSync as execSync6 } from "child_process";
7997
- import { readFileSync as readFileSync10 } from "fs";
7998
- import { homedir as homedir8 } from "os";
7999
- import { join as join15 } from "path";
8000
- var nextAskUserQuestionRequestId = Date.now();
9121
+ import { readFileSync as readFileSync13 } from "fs";
9122
+ import { homedir as homedir10 } from "os";
9123
+ import { join as join20 } from "path";
9124
+ var nextHookRequestId = Date.now();
8001
9125
 
8002
9126
  // packages/agents/dist/headless/types.js
8003
9127
  var HEADLESS_ERROR_TAIL = 2e3;
@@ -8160,67 +9284,8 @@ function runHeadless(opts) {
8160
9284
  }
8161
9285
 
8162
9286
  // packages/agents/dist/claude/peer-registry-source.js
8163
- import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
8164
- import { join as join17 } from "path";
8165
-
8166
- // packages/agents/dist/claude/registry.js
8167
- import fs14 from "fs";
8168
- import { join as join16 } from "path";
8169
- import { homedir as homedir9 } from "os";
8170
- var CLAUDE_SESSIONS_DIR = join16(homedir9(), ".claude", "sessions");
8171
- var PID_JSON = /^(\d+)\.json$/;
8172
- function parseRegistryDir(files, readFile7) {
8173
- const out = [];
8174
- for (const name of files) {
8175
- const m = PID_JSON.exec(name);
8176
- if (!m)
8177
- continue;
8178
- let json;
8179
- try {
8180
- json = JSON.parse(readFile7(name));
8181
- } catch {
8182
- continue;
8183
- }
8184
- out.push({ ...json, pid: parseInt(m[1], 10) });
8185
- }
8186
- return out;
8187
- }
8188
- function toLifecycleSnapshot(entry, taskId, alive, now) {
8189
- const at = entry.statusUpdatedAt ?? now;
8190
- const base = { taskId, alive, origin: "agent", at, pid: entry.pid };
8191
- if (!alive)
8192
- return { ...base, state: "unknown" };
8193
- switch (entry.status) {
8194
- case "busy":
8195
- case "shell":
8196
- return { ...base, state: "running" };
8197
- case "idle":
8198
- return { ...base, state: "idle" };
8199
- case "waiting":
8200
- return {
8201
- ...base,
8202
- state: "needsInput",
8203
- detail: { reason: entry.waitingFor, note: entry.waitingFor }
8204
- };
8205
- default:
8206
- return { ...base, state: "unknown" };
8207
- }
8208
- }
8209
- function readClaudeStatusBySocketPath(socketPath) {
8210
- let files;
8211
- try {
8212
- files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
8213
- } catch {
8214
- return void 0;
8215
- }
8216
- const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join16(CLAUDE_SESSIONS_DIR, name), "utf8"));
8217
- const entry = entries.find((e) => e.messagingSocketPath === socketPath);
8218
- if (!entry)
8219
- return void 0;
8220
- return { status: entry.status, statusUpdatedAt: entry.statusUpdatedAt, sessionId: entry.sessionId };
8221
- }
8222
-
8223
- // 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";
8224
9289
  function defaultIsAlive(pid) {
8225
9290
  try {
8226
9291
  process.kill(pid, 0);
@@ -8244,8 +9309,8 @@ var ClaudePeerRegistrySource = class {
8244
9309
  pollMs;
8245
9310
  log;
8246
9311
  constructor(o = {}) {
8247
- this.readdir = o.readdir ?? (() => readdirSync4(CLAUDE_SESSIONS_DIR));
8248
- 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"));
8249
9314
  this.isAlive = o.isAlive ?? defaultIsAlive;
8250
9315
  this.now = o.now ?? Date.now;
8251
9316
  this.pollMs = o.pollMs ?? 2e3;
@@ -8557,6 +9622,119 @@ var ClaudeReceiptListener = class {
8557
9622
  }
8558
9623
  };
8559
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
+
8560
9738
  // packages/agents/dist/opencode/fact-adapter.js
8561
9739
  function createOpencodeFactAdapter(deps) {
8562
9740
  return {
@@ -8589,6 +9767,9 @@ function createOpencodeFactAdapter(deps) {
8589
9767
  }
8590
9768
  if (type === "permission.replied")
8591
9769
  return [{ kind: "activity" }];
9770
+ if (type === "server.connected" || type === "server.heartbeat") {
9771
+ return [{ kind: "activity" }];
9772
+ }
8592
9773
  return [{ kind: "unknown", name: type }];
8593
9774
  }
8594
9775
  };
@@ -8599,7 +9780,8 @@ init_dist();
8599
9780
  init_dist();
8600
9781
  init_dist();
8601
9782
  init_dist2();
8602
- import { execFile as execFile2, execFileSync as execFileSync5 } from "child_process";
9783
+ init_dist2();
9784
+ import { execFile as execFile2, execFileSync as execFileSync6 } from "child_process";
8603
9785
  var CMUX_TIMEOUT = 15e3;
8604
9786
  var CmuxTimeoutError = class extends Error {
8605
9787
  constructor(cmd) {
@@ -9195,9 +10377,9 @@ var NotifierRegistry = class {
9195
10377
  };
9196
10378
 
9197
10379
  // packages/workspaces/dist/workspaces/obsidian.js
9198
- import fs15 from "fs/promises";
10380
+ import fs17 from "fs/promises";
9199
10381
  import { existsSync as existsSync11 } from "fs";
9200
- import path19 from "path";
10382
+ import path21 from "path";
9201
10383
 
9202
10384
  // packages/workspaces/dist/workspaces/registry.js
9203
10385
  init_dist();
@@ -9350,16 +10532,16 @@ var CmuxEventsBridge = class {
9350
10532
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
9351
10533
  init_dist2();
9352
10534
  init_dist();
9353
- import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
9354
- import { join as join18 } from "path";
9355
- 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";
9356
10538
 
9357
10539
  // packages/workspaces/dist/cmux-daemon/store-fingerprint.js
9358
10540
  init_dist();
9359
10541
  init_dist2();
9360
- import { execFileSync as execFileSync6 } from "child_process";
10542
+ import { execFileSync as execFileSync7 } from "child_process";
9361
10543
  function defaultExecPs(pid) {
9362
- return execFileSync6("ps", ["-o", "command=", "-p", String(pid)], {
10544
+ return execFileSync7("ps", ["-o", "command=", "-p", String(pid)], {
9363
10545
  encoding: "utf-8",
9364
10546
  stdio: ["ignore", "pipe", "ignore"]
9365
10547
  });
@@ -9449,15 +10631,15 @@ function readLivenessSnapshot(files, readFile7, projects, argvRecovery = {}) {
9449
10631
 
9450
10632
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
9451
10633
  async function readCmuxLiveness() {
9452
- 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");
9453
10635
  const projects = loadConfig().projects;
9454
10636
  let files;
9455
10637
  try {
9456
- 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"));
9457
10639
  } catch (e) {
9458
10640
  throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
9459
10641
  }
9460
- return readLivenessSnapshot(files, (f) => readFileSync12(join18(dir, f), "utf-8"), projects);
10642
+ return readLivenessSnapshot(files, (f) => readFileSync15(join22(dir, f), "utf-8"), projects);
9461
10643
  }
9462
10644
  var DaemonCmux = class {
9463
10645
  driver;
@@ -9520,13 +10702,15 @@ var DaemonCmux = class {
9520
10702
  };
9521
10703
 
9522
10704
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
9523
- import { join as join19 } from "path";
9524
- import { homedir as homedir11 } from "os";
9525
- 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";
9526
10708
  var CmuxStoreSource = class {
9527
10709
  name = "cmux-store";
9528
10710
  stateDir;
9529
10711
  debounceMs;
10712
+ lockRetryMs;
10713
+ maxLockRetries;
9530
10714
  isPidAlive;
9531
10715
  listFiles;
9532
10716
  readFile;
@@ -9538,13 +10722,19 @@ var CmuxStoreSource = class {
9538
10722
  deps;
9539
10723
  stopWatcher;
9540
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();
9541
10729
  /** taskId → last reported snapshot (for snapshot() liveness floor). */
9542
10730
  cache = /* @__PURE__ */ new Map();
9543
10731
  active = false;
9544
10732
  lastError = null;
9545
10733
  constructor(opts = {}) {
9546
- 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");
9547
10735
  this.debounceMs = opts.debounceMs ?? 50;
10736
+ this.lockRetryMs = opts.lockRetryMs ?? 50;
10737
+ this.maxLockRetries = opts.maxLockRetries ?? 3;
9548
10738
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
9549
10739
  this.listFiles = opts.listFiles ?? defaultListFiles;
9550
10740
  this.readFile = opts.readFile ?? defaultReadFile;
@@ -9572,6 +10762,11 @@ var CmuxStoreSource = class {
9572
10762
  this.cancelTimer(this.debounceTimer);
9573
10763
  this.debounceTimer = void 0;
9574
10764
  }
10765
+ for (const timer of this.lockRetryTimers.values()) {
10766
+ this.cancelTimer(timer);
10767
+ }
10768
+ this.lockRetryTimers.clear();
10769
+ this.lockRetryCounts.clear();
9575
10770
  this.stopWatcher?.();
9576
10771
  this.stopWatcher = void 0;
9577
10772
  this.deps = void 0;
@@ -9605,12 +10800,13 @@ var CmuxStoreSource = class {
9605
10800
  }
9606
10801
  scanFile(filename) {
9607
10802
  const deps = this.deps;
9608
- const filePath = join19(this.stateDir, filename);
10803
+ const filePath = join23(this.stateDir, filename);
9609
10804
  const lockPath = `${filePath}.lock`;
9610
10805
  if (this.fileExists(lockPath)) {
9611
- this.log(`cmux-store: skipping ${filename} (locked)`);
10806
+ this.scheduleLockRetry(filename);
9612
10807
  return;
9613
10808
  }
10809
+ this.clearLockRetry(filename);
9614
10810
  const raw = this.readFile(filePath);
9615
10811
  if (!raw)
9616
10812
  return;
@@ -9625,6 +10821,31 @@ var CmuxStoreSource = class {
9625
10821
  this.processSession(session, deps);
9626
10822
  }
9627
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
+ }
9628
10849
  processSession(session, deps) {
9629
10850
  if (!session.sessionId || !session.cwd || typeof session.pid !== "number")
9630
10851
  return;
@@ -9671,14 +10892,14 @@ function defaultIsPidAlive2(pid) {
9671
10892
  }
9672
10893
  function defaultListFiles(dir) {
9673
10894
  try {
9674
- 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"));
9675
10896
  } catch {
9676
10897
  return [];
9677
10898
  }
9678
10899
  }
9679
- function defaultReadFile(path21) {
10900
+ function defaultReadFile(path23) {
9680
10901
  try {
9681
- return readFileSync13(path21, "utf-8");
10902
+ return readFileSync16(path23, "utf-8");
9682
10903
  } catch {
9683
10904
  return void 0;
9684
10905
  }
@@ -9693,9 +10914,9 @@ function defaultWatchDir(dir, cb) {
9693
10914
  }
9694
10915
 
9695
10916
  // packages/workspaces/dist/native-hooks/native-hook-source.js
9696
- import { join as join20 } from "path";
9697
- import { homedir as homedir12 } from "os";
9698
- 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";
9699
10920
  var CLAUDE_HOOK_EVENTS = [
9700
10921
  ["SessionStart", "session-start"],
9701
10922
  ["UserPromptSubmit", "prompt-submit"],
@@ -9703,11 +10924,17 @@ var CLAUDE_HOOK_EVENTS = [
9703
10924
  ["Stop", "stop"],
9704
10925
  ["Notification", "notification"],
9705
10926
  ["PreToolUse", "ask-question", "AskUserQuestion"],
9706
- ["SessionEnd", "session-end"]
10927
+ ["SessionEnd", "session-end"],
10928
+ // #760: fires ~6s before the matching Notification, carrying tool_name +
10929
+ // tool_input directly — a strictly better task.blocked source.
10930
+ ["PermissionRequest", "permission-request"],
10931
+ // #763: the turn died on an API error — today squadrant has no source for
10932
+ // this at all; without it the watchdog reports a plain stall (wrong story).
10933
+ ["StopFailure", "stop-failure"]
9707
10934
  ];
9708
10935
  var DEFAULT_HOOK_CMD = "squadrant hooks";
9709
10936
  function installClaudeHooks(opts = {}) {
9710
- const settingsPath = opts.settingsPath ?? join20(homedir12(), ".claude", "settings.json");
10937
+ const settingsPath = opts.settingsPath ?? join24(homedir13(), ".claude", "settings.json");
9711
10938
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
9712
10939
  const readFile7 = opts.readFile ?? defaultReadFile2;
9713
10940
  const writeFile6 = opts.writeFile ?? defaultWriteFile;
@@ -9781,6 +11008,10 @@ function mapSubToLifecycle(sub) {
9781
11008
  return "needsInput";
9782
11009
  case "ask-question":
9783
11010
  return "needsInput";
11011
+ case "permission-request":
11012
+ return "needsInput";
11013
+ case "stop-failure":
11014
+ return "idle";
9784
11015
  case "session-end":
9785
11016
  return "session-end";
9786
11017
  default:
@@ -9875,16 +11106,16 @@ function extractDetail(sub, payload) {
9875
11106
  }
9876
11107
  return void 0;
9877
11108
  }
9878
- function defaultReadFile2(path21) {
11109
+ function defaultReadFile2(path23) {
9879
11110
  try {
9880
- return readFileSync14(path21, "utf-8");
11111
+ return readFileSync17(path23, "utf-8");
9881
11112
  } catch {
9882
11113
  return void 0;
9883
11114
  }
9884
11115
  }
9885
- function defaultWriteFile(path21, content) {
9886
- mkdirSync6(path21.replace(/\/[^/]+$/, ""), { recursive: true });
9887
- writeFileSync9(path21, content, "utf-8");
11116
+ function defaultWriteFile(path23, content) {
11117
+ mkdirSync7(path23.replace(/\/[^/]+$/, ""), { recursive: true });
11118
+ writeFileSync10(path23, content, "utf-8");
9888
11119
  }
9889
11120
 
9890
11121
  // packages/workspaces/dist/crew-pane.js
@@ -9964,9 +11195,9 @@ init_dist();
9964
11195
 
9965
11196
  // packages/cli/src/lib/daemon-restart-broadcast.ts
9966
11197
  init_dist();
9967
- import path20 from "path";
11198
+ import path22 from "path";
9968
11199
  function statePath2(stateRoot) {
9969
- return path20.join(stateRoot, "daemon-restart-state.json");
11200
+ return path22.join(stateRoot, "daemon-restart-state.json");
9970
11201
  }
9971
11202
  function computeRestartSignature(version, buildMtimeMs) {
9972
11203
  return `${version}::${buildMtimeMs}`;
@@ -10014,25 +11245,25 @@ async function maybeBroadcastDaemonRestart(opts) {
10014
11245
  }
10015
11246
 
10016
11247
  // packages/cli/src/lib/captain-channel-factory.ts
10017
- import { createServer as createServer3, connect as netConnect } from "net";
10018
- import fs16 from "fs";
10019
- 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";
10020
11251
  import { randomUUID as randomUUID5 } from "crypto";
10021
11252
  import chalk2 from "chalk";
10022
11253
  init_dist2();
10023
11254
  var shared;
10024
- var registryEntryPath = () => join21(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
11255
+ var registryEntryPath = () => join25(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
10025
11256
  function unregisterSenderIdentity() {
10026
11257
  try {
10027
- fs16.unlinkSync(registryEntryPath());
11258
+ fs18.unlinkSync(registryEntryPath());
10028
11259
  } catch {
10029
11260
  }
10030
11261
  }
10031
11262
  function registerSenderIdentity(socketPath) {
10032
11263
  try {
10033
11264
  unregisterSenderIdentity();
10034
- fs16.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
10035
- fs16.writeFileSync(
11265
+ fs18.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
11266
+ fs18.writeFileSync(
10036
11267
  registryEntryPath(),
10037
11268
  JSON.stringify({
10038
11269
  pid: process.pid,
@@ -10053,12 +11284,12 @@ async function sharedReceiptListener() {
10053
11284
  const socketPath = `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`;
10054
11285
  const listener = new ClaudeReceiptListener({
10055
11286
  socketPath,
10056
- createServer: (h) => createServer3(h),
11287
+ createServer: (h) => createServer4(h),
10057
11288
  // A UDS path is not cleaned up when a process is killed, so our own leftover
10058
11289
  // must never be the reason we refuse to start.
10059
11290
  unlinkStale: (p) => {
10060
11291
  try {
10061
- fs16.unlinkSync(p);
11292
+ fs18.unlinkSync(p);
10062
11293
  } catch {
10063
11294
  }
10064
11295
  },
@@ -10083,15 +11314,28 @@ async function buildCaptainChannel() {
10083
11314
  // in /tmp/x registers cwd "/private/tmp/x" on macOS, so cwd matching silently
10084
11315
  // missed and every captain ping reported accepted-unconfirmed.
10085
11316
  statusFor: (taskId) => readClaudeStatusBySocketPath(captainSocketPath(taskId)),
10086
- wire: (p, e) => writeLine(p, e, { connect: (path21) => netConnect(path21) }),
11317
+ wire: (p, e) => writeLine(p, e, { connect: (path23) => netConnect(path23) }),
10087
11318
  receipts,
10088
11319
  newMsgId: () => randomUUID5(),
10089
11320
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
10090
11321
  log: (m) => console.error(chalk2.dim(m))
10091
11322
  });
10092
11323
  }
10093
- async function buildCaptainChannelWithRetry(opts = {}) {
10094
- 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 = {}) {
10095
11339
  const sleep3 = opts.sleep ?? ((ms) => new Promise((r) => {
10096
11340
  const t = setTimeout(r, ms);
10097
11341
  t.unref?.();
@@ -10110,14 +11354,20 @@ async function buildCaptainChannelWithRetry(opts = {}) {
10110
11354
  }
10111
11355
  }
10112
11356
  }
11357
+ function buildCaptainChannelsWithRetry(opts) {
11358
+ return retryForever(
11359
+ () => buildCaptainChannels({ stateRoot: opts.stateRoot, configAgent: opts.configAgent }),
11360
+ opts
11361
+ );
11362
+ }
10113
11363
 
10114
11364
  // packages/cli/src/squadrantd.ts
10115
11365
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
10116
- var CLI_BIN = join22(dirname4(SELF_PATH2), "index.js");
11366
+ var CLI_BIN = join26(dirname5(SELF_PATH2), "index.js");
10117
11367
  function readPkgVersion() {
10118
11368
  try {
10119
- const pkgPath = join22(dirname4(SELF_PATH2), "..", "package.json");
10120
- 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";
10121
11371
  } catch {
10122
11372
  return "unknown";
10123
11373
  }
@@ -10139,7 +11389,7 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound) {
10139
11389
  return createTelegramBridge({
10140
11390
  cfg,
10141
11391
  stateRoot,
10142
- configRoot: dirname4(stateRoot),
11392
+ configRoot: dirname5(stateRoot),
10143
11393
  client,
10144
11394
  appendCaptainMessage,
10145
11395
  log,
@@ -10264,7 +11514,7 @@ function startSquadrantd(opts = {}) {
10264
11514
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
10265
11515
  );
10266
11516
  },
10267
- cursorFile: join22(stateRoot, "cmux-events.seq"),
11517
+ cursorFile: join26(stateRoot, "cmux-events.seq"),
10268
11518
  log
10269
11519
  });
10270
11520
  const cmuxStoreSource = new CmuxStoreSource({ log });
@@ -10291,14 +11541,23 @@ function startSquadrantd(opts = {}) {
10291
11541
  log
10292
11542
  }))
10293
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);
10294
11547
  if (opts.notifyFault) ctx.notifyFault = opts.notifyFault;
10295
11548
  else if (!process.env.VITEST) ctx.notifyFault = buildNotifyFault(log);
10296
11549
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
10297
11550
  if (!process.env.VITEST) {
10298
11551
  ctx.captainChannelMode = () => loadConfig().defaults.captainChannel ?? "off";
10299
11552
  if (ctx.captainChannelMode() !== "off") {
10300
- void buildCaptainChannelWithRetry({ log }).then((ch) => {
10301
- 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;
10302
11561
  }).catch((e) => log(`captain-channel: unexpected retry-loop error: ${e.message}`));
10303
11562
  }
10304
11563
  }
@@ -10407,6 +11666,11 @@ ${buildCompletionProtocol(rec.id, rec.project)}`;
10407
11666
  } catch (e) {
10408
11667
  log(`native hook install failed: ${e.message}`);
10409
11668
  }
11669
+ try {
11670
+ syncShippedOpencodeSkills({ pkgRoot: join26(dirname5(SELF_PATH2), "..") });
11671
+ } catch (e) {
11672
+ log(`opencode skills sync failed: ${e.message}`);
11673
+ }
10410
11674
  const hookPrevSnaps = /* @__PURE__ */ new Map();
10411
11675
  const hookDeps = {
10412
11676
  resolve: (hint) => {
@@ -10541,7 +11805,7 @@ function logCrashMarker(kind, err) {
10541
11805
  const message = err instanceof Error ? err.stack ?? err.message : String(err);
10542
11806
  process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${kind} pid=${process.pid} error=${message}
10543
11807
  `);
10544
- const stateRoot = join22(homedir13(), ".config", "squadrant", "state");
11808
+ const stateRoot = join26(homedir14(), ".config", "squadrant", "state");
10545
11809
  writeExitMarker(stateRoot, {
10546
11810
  ts: (/* @__PURE__ */ new Date()).toISOString(),
10547
11811
  pid: process.pid,
@@ -10554,7 +11818,7 @@ function logCrashMarker(kind, err) {
10554
11818
  `));
10555
11819
  }
10556
11820
  function isMonorepoCheckout(scriptPath, dirExists = existsSync13) {
10557
- return dirExists(join22(dirname4(resolve3(scriptPath)), "..", "packages"));
11821
+ return dirExists(join26(dirname5(resolve3(scriptPath)), "..", "packages"));
10558
11822
  }
10559
11823
  function isLinkedWorktree(scriptPath, statFile = (p) => {
10560
11824
  try {
@@ -10563,7 +11827,7 @@ function isLinkedWorktree(scriptPath, statFile = (p) => {
10563
11827
  return void 0;
10564
11828
  }
10565
11829
  }) {
10566
- const dotGit = join22(dirname4(resolve3(scriptPath)), "..", ".git");
11830
+ const dotGit = join26(dirname5(resolve3(scriptPath)), "..", ".git");
10567
11831
  return statFile(dotGit)?.isFile === true;
10568
11832
  }
10569
11833
  if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {