squadrant 0.21.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4237,6 +4237,11 @@ function assertBackendUsable(o) {
4237
4237
  function shouldBuildRouterService(router, isVitest) {
4238
4238
  return !!router && !isVitest;
4239
4239
  }
4240
+ function claudeEnvShadowsRouter(claudeEnv) {
4241
+ if (!claudeEnv)
4242
+ return [];
4243
+ return Object.keys(claudeEnv).filter((key) => key.startsWith("ANTHROPIC_"));
4244
+ }
4240
4245
  var init_router_resolution = __esm({
4241
4246
  "packages/core/dist/router-resolution.js"() {
4242
4247
  init_dist();
@@ -4263,6 +4268,14 @@ function buildRouterEnv(creds, model) {
4263
4268
  env.ANTHROPIC_MODEL = model;
4264
4269
  return env;
4265
4270
  }
4271
+ function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
4272
+ const merged = { ...routerEnv };
4273
+ for (const [key, value] of Object.entries(claudeEnv ?? {})) {
4274
+ if (!key.startsWith("ANTHROPIC_"))
4275
+ merged[key] = value;
4276
+ }
4277
+ return merged;
4278
+ }
4266
4279
  function formatCustomHeaders(headers) {
4267
4280
  const entries = Object.entries(headers ?? {});
4268
4281
  if (entries.length === 0)
@@ -4576,10 +4589,18 @@ async function runCrewSpawn(input, config, deps) {
4576
4589
  if (agentName === "claude") {
4577
4590
  let routerEnv = {};
4578
4591
  if (backend !== "native") {
4592
+ if (!deps.writeRouterSettings) {
4593
+ throw new Error(`backend '${backend}' requires a per-spawn --settings writer to outrank defaults.claudeEnv, but the spawn path has none \u2014 refusing to launch a routed session that would silently bypass the router shim`);
4594
+ }
4579
4595
  if (!deps.routerCredentials) {
4580
4596
  throw new Error(`backend '${backend}' requires router credentials, but the spawn path has no daemon credentials provider`);
4581
4597
  }
4582
4598
  routerEnv = buildRouterEnv(await deps.routerCredentials({ project: input.project, backend }), crewModel);
4599
+ const shadowed = claudeEnvShadowsRouter(config.defaults.claudeEnv);
4600
+ if (shadowed.length > 0) {
4601
+ process.stderr.write(`\u26A0\uFE0F backend '${backend}' selected but defaults.claudeEnv sets ${shadowed.join(", ")} \u2014 these would shadow the router shim. Overriding via a per-spawn --settings env block (outranks ~/.claude/settings.json).
4602
+ `);
4603
+ }
4583
4604
  }
4584
4605
  ensureSocksDir();
4585
4606
  const messagingSocketPath = path9.join(CC_SOCKS_DIR, `squadrant-${randomUUID3()}.sock`);
@@ -4592,6 +4613,12 @@ async function runCrewSpawn(input, config, deps) {
4592
4613
  name,
4593
4614
  messagingSocketPath
4594
4615
  });
4616
+ const routerSettingsPath = Object.keys(routerEnv).length > 0 ? deps.writeRouterSettings({
4617
+ stateRoot: STATE_ROOT,
4618
+ project: input.project,
4619
+ taskId: rec.id,
4620
+ env: mergeClaudeEnvRouterSettings(routerEnv, config.defaults.claudeEnv)
4621
+ }) : void 0;
4595
4622
  let hooksInstalled = false;
4596
4623
  try {
4597
4624
  deps.writeSettingsLocal(spawnCwd);
@@ -4607,18 +4634,25 @@ async function runCrewSpawn(input, config, deps) {
4607
4634
  messagingSocketPath,
4608
4635
  // Permission mode is config-driven so squadrant can default crews to 'auto'
4609
4636
  // or keep the semi-automatic 'acceptEdits' gate. Falls back to 'acceptEdits'.
4610
- permissionMode: config.defaults.permissions?.crew ?? "acceptEdits",
4637
+ // #772: a router-backed crew MUST use 'default' — 'auto' makes the built-in
4638
+ // Sonnet-5 classifier own PermissionRequest and the gate yields, which is
4639
+ // exactly the fail-closed dead end U7 exists to fix. The rubric owns the
4640
+ // permission mode for a routed spawn; the operator's config cannot override
4641
+ // it back into the broken path.
4642
+ permissionMode: backend !== "native" ? "default" : config.defaults.permissions?.crew ?? "acceptEdits",
4611
4643
  // #708: self-describing name so ListAgents/the registry can tell this
4612
4644
  // crew apart from an unrelated session instead of an auto-derived cwd
4613
4645
  // basename (only the claude driver reads this — other agents ignore it).
4614
4646
  sessionName: crewSessionName(input.project, name),
4615
4647
  ...crewModel ? { model: crewModel } : {},
4616
- ...crewThinking ? { thinking: crewThinking } : {}
4648
+ ...crewThinking ? { thinking: crewThinking } : {},
4649
+ ...routerSettingsPath ? { settingsPath: routerSettingsPath } : {}
4617
4650
  });
4618
4651
  const direction2 = input.direction ?? "tab";
4619
4652
  const title2 = titleFor(input.project, name);
4620
4653
  const pane2 = await deps.runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
4621
- const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
4654
+ const gateEnv = backend !== "native" ? " SQUADRANT_GATE=on" : "";
4655
+ const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}${gateEnv}`;
4622
4656
  const routerPrefix = Object.keys(routerEnv).length > 0 ? ` ${renderEnvAssignments(routerEnv)}` : "";
4623
4657
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix}${routerPrefix} ${niceCrewCommand(cliCommand2)}`);
4624
4658
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
@@ -7526,6 +7560,26 @@ function writeJson(res, status, body) {
7526
7560
  res.writeHead(status, { "content-type": "application/json" });
7527
7561
  res.end(JSON.stringify(body));
7528
7562
  }
7563
+ function pathnameOf(url) {
7564
+ if (!url)
7565
+ return "";
7566
+ const q = url.indexOf("?");
7567
+ return q === -1 ? url : url.slice(0, q);
7568
+ }
7569
+ function modelsList(ids) {
7570
+ const data = ids.map((id) => ({
7571
+ type: "model",
7572
+ id,
7573
+ display_name: id,
7574
+ created_at: "1970-01-01T00:00:00Z"
7575
+ }));
7576
+ return {
7577
+ data,
7578
+ has_more: false,
7579
+ first_id: ids[0] ?? null,
7580
+ last_id: ids[ids.length - 1] ?? null
7581
+ };
7582
+ }
7529
7583
  function buildUpstreamHeaders(req, upstream, stream) {
7530
7584
  const headers = {
7531
7585
  "content-type": "application/json",
@@ -7554,6 +7608,7 @@ function createRouterShim(opts) {
7554
7608
  let lastError;
7555
7609
  let upstreamReachable = true;
7556
7610
  const upstreamUrl = joinUrl(opts.upstream.baseUrl, "/v1/messages");
7611
+ const advertisedModels = new Set(opts.models ?? []);
7557
7612
  function emitUsage(u) {
7558
7613
  try {
7559
7614
  opts.onUsage?.(u);
@@ -7583,6 +7638,8 @@ function createRouterShim(opts) {
7583
7638
  const body = parsed;
7584
7639
  const wantsStream = body.stream === true;
7585
7640
  const model = typeof body.model === "string" ? body.model : void 0;
7641
+ if (model)
7642
+ advertisedModels.add(model);
7586
7643
  const sanitized = sanitizeRequest(body, opts.upstream);
7587
7644
  let upstreamRes;
7588
7645
  try {
@@ -7655,8 +7712,20 @@ function createRouterShim(opts) {
7655
7712
  emitUsage(usage2);
7656
7713
  writeJson(res, upstreamRes.status, upstreamBody);
7657
7714
  }
7715
+ async function handleCountTokens(req, res) {
7716
+ let text;
7717
+ try {
7718
+ text = await readBody(req);
7719
+ } catch {
7720
+ const e = anthropicError(400, "invalid_request_error", "malformed request body");
7721
+ writeJson(res, e.status, e.body);
7722
+ return;
7723
+ }
7724
+ writeJson(res, 200, { input_tokens: Math.max(1, Math.ceil(text.length / 4)) });
7725
+ }
7658
7726
  function handler(req, res) {
7659
- if (req.method === "POST" && req.url === "/v1/messages") {
7727
+ const path39 = pathnameOf(req.url);
7728
+ if (req.method === "POST" && path39 === "/v1/messages") {
7660
7729
  void handleMessages(req, res).catch((err) => {
7661
7730
  log(`router internal error: ${err instanceof Error ? err.message : String(err)}`);
7662
7731
  if (!res.headersSent) {
@@ -7668,7 +7737,18 @@ function createRouterShim(opts) {
7668
7737
  });
7669
7738
  return;
7670
7739
  }
7671
- if (req.method === "GET" && req.url === "/healthz") {
7740
+ if (req.method === "POST" && path39 === "/v1/messages/count_tokens") {
7741
+ void handleCountTokens(req, res).catch(() => {
7742
+ if (!res.headersSent)
7743
+ writeJson(res, 502, anthropicError(502, "api_error", "router internal error").body);
7744
+ });
7745
+ return;
7746
+ }
7747
+ if (req.method === "GET" && path39 === "/v1/models") {
7748
+ writeJson(res, 200, modelsList([...advertisedModels]));
7749
+ return;
7750
+ }
7751
+ if (req.method === "GET" && path39 === "/healthz") {
7672
7752
  writeJson(res, 200, { ready: server?.listening === true, upstreamReachable, lastError });
7673
7753
  return;
7674
7754
  }
@@ -7793,12 +7873,17 @@ function createRouterService(router, projects, deps = {}) {
7793
7873
  tokenByProject.set(p, t);
7794
7874
  projectTokens.set(t, p);
7795
7875
  }
7876
+ const models = [
7877
+ ...deps.models ?? [],
7878
+ ...Object.values(router.models ?? {}).map((m) => m.upstream)
7879
+ ];
7796
7880
  const ledger = createUsageLedger();
7797
7881
  const shim = createRouterShim({
7798
7882
  upstream,
7799
7883
  projectTokens,
7800
7884
  port: router.port ?? 0,
7801
7885
  log,
7886
+ models,
7802
7887
  onUsage: (u) => ledger.record(u),
7803
7888
  ...deps.fetch ? { fetch: deps.fetch } : {}
7804
7889
  });
@@ -7861,12 +7946,53 @@ var init_router = __esm({
7861
7946
  }
7862
7947
  });
7863
7948
 
7949
+ // packages/core/dist/captain-router.js
7950
+ async function prepareCaptainRoute(o) {
7951
+ assertBackendUsable({ backend: o.backend, agent: o.agentName, router: o.config.defaults.router });
7952
+ if (o.backend === "native") {
7953
+ return { backend: "native", model: o.model, permissionMode: o.configuredPermissionMode, env: {} };
7954
+ }
7955
+ if (!o.deps.routerCredentials) {
7956
+ throw new Error(`captain launch: backend '${o.backend}' requires router credentials from the daemon, but this launch path has no credentials provider`);
7957
+ }
7958
+ if (!o.deps.writeRouterSettings) {
7959
+ throw new Error(`captain launch: backend '${o.backend}' requires a per-spawn --settings writer to outrank defaults.claudeEnv, but this launch path has none \u2014 refusing to launch a routed captain that would silently bypass the router shim`);
7960
+ }
7961
+ const model = resolveRouterModel(o.model, o.agentName, o.config.defaults.router);
7962
+ const creds = await o.deps.routerCredentials({ project: o.project, backend: o.backend });
7963
+ const routerEnv = buildRouterEnv(creds, model);
7964
+ const shadowed = claudeEnvShadowsRouter(o.config.defaults.claudeEnv);
7965
+ if (shadowed.length > 0) {
7966
+ o.warn?.(`backend '${o.backend}' selected but defaults.claudeEnv sets ${shadowed.join(", ")} \u2014 these would shadow the router shim. Overriding via a per-spawn --settings env block (outranks ~/.claude/settings.json).`);
7967
+ }
7968
+ const settingsPath = o.deps.writeRouterSettings({
7969
+ stateRoot: o.stateRoot,
7970
+ project: o.project,
7971
+ taskId: "captain",
7972
+ env: mergeClaudeEnvRouterSettings(routerEnv, o.config.defaults.claudeEnv)
7973
+ });
7974
+ return {
7975
+ backend: o.backend,
7976
+ model,
7977
+ permissionMode: "default",
7978
+ env: { ...routerEnv, SQUADRANT_GATE: "on" },
7979
+ settingsPath
7980
+ };
7981
+ }
7982
+ var init_captain_router = __esm({
7983
+ "packages/core/dist/captain-router.js"() {
7984
+ init_dist();
7985
+ init_router_resolution();
7986
+ init_env();
7987
+ }
7988
+ });
7989
+
7864
7990
  // packages/core/dist/permission-gate.js
7865
7991
  import { createHash } from "crypto";
7866
7992
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
7867
7993
  import { dirname as dirname5, join as join15 } from "path";
7868
7994
  function isGateSession(env) {
7869
- return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1";
7995
+ return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1" || env.SQUADRANT_ROLE === "captain";
7870
7996
  }
7871
7997
  function resolveGateMode(env, gate) {
7872
7998
  const fromEnv = env.SQUADRANT_GATE;
@@ -8062,7 +8188,15 @@ async function callClassifier(o) {
8062
8188
  return "ask";
8063
8189
  }
8064
8190
  const data = await res.json();
8191
+ if (data?.stop_reason === "max_tokens") {
8192
+ o.log?.("gate: classifier exhausted max_tokens before a verdict \u2014 asking");
8193
+ return "ask";
8194
+ }
8065
8195
  const text = Array.isArray(data?.content) ? data.content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ") : "";
8196
+ if (!text.trim()) {
8197
+ o.log?.("gate: classifier returned no text verdict \u2014 asking");
8198
+ return "ask";
8199
+ }
8066
8200
  return parseClassifierVerdict(text);
8067
8201
  } catch (e) {
8068
8202
  o.log?.(`gate: classifier unavailable (${e.message}) \u2014 asking`);
@@ -8081,7 +8215,7 @@ ${toolPayload}`).digest("hex");
8081
8215
  }
8082
8216
  async function evaluatePermissionRequest(input) {
8083
8217
  if (!isGateSession(input.env)) {
8084
- return { decision: "yield", reason: "not a squadrant crew/side session" };
8218
+ return { decision: "yield", reason: "not a squadrant crew/side/captain session" };
8085
8219
  }
8086
8220
  const gate = input.config.defaults.gate;
8087
8221
  const mode = resolveGateMode(input.env, gate);
@@ -8167,7 +8301,7 @@ var init_permission_gate = __esm({
8167
8301
  DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
8168
8302
  DEFAULT_CACHE_MAX_ENTRIES = 500;
8169
8303
  DEFAULT_CLASSIFIER_TIMEOUT_MS = 5e3;
8170
- CLASSIFIER_MAX_TOKENS = 8;
8304
+ CLASSIFIER_MAX_TOKENS = 256;
8171
8305
  MAX_PAYLOAD_CHARS = 2e3;
8172
8306
  DEFAULT_GATE_DENY_RULES = [
8173
8307
  // Flag-order-agnostic: both recursive and force must be present, in any order,
@@ -9094,6 +9228,7 @@ __export(dist_exports2, {
9094
9228
  captainSocketPath: () => captainSocketPath,
9095
9229
  checkFact: () => checkFact,
9096
9230
  classifyHealth: () => classifyHealth,
9231
+ claudeEnvShadowsRouter: () => claudeEnvShadowsRouter,
9097
9232
  closeWorkItem: () => closeWorkItem,
9098
9233
  computeTemplateHash: () => computeTemplateHash,
9099
9234
  consumeExitMarker: () => consumeExitMarker,
@@ -9194,6 +9329,7 @@ __export(dist_exports2, {
9194
9329
  makeGate: () => makeGate,
9195
9330
  maskToken: () => maskToken,
9196
9331
  matchTier1Deny: () => matchTier1Deny,
9332
+ mergeClaudeEnvRouterSettings: () => mergeClaudeEnvRouterSettings,
9197
9333
  mintToken: () => mintToken,
9198
9334
  nameFromTitle: () => nameFromTitle,
9199
9335
  newestSessionInDirectory: () => newestSessionInDirectory,
@@ -9208,6 +9344,7 @@ __export(dist_exports2, {
9208
9344
  parseProgramArgs: () => parseProgramArgs,
9209
9345
  plistPath: () => plistPath,
9210
9346
  pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
9347
+ prepareCaptainRoute: () => prepareCaptainRoute,
9211
9348
  printForeignInstallError: () => printForeignInstallError,
9212
9349
  printVersionUpgradeNotice: () => printVersionUpgradeNotice,
9213
9350
  probeCaptainChannel: () => probeCaptainChannel,
@@ -9340,6 +9477,7 @@ var init_dist2 = __esm({
9340
9477
  init_router();
9341
9478
  init_crew_routing();
9342
9479
  init_router_resolution();
9480
+ init_captain_router();
9343
9481
  init_permission_gate();
9344
9482
  init_restart_daemon();
9345
9483
  init_group_dispatch();
@@ -9868,7 +10006,7 @@ var init_registry2 = __esm({
9868
10006
  // packages/agents/dist/drivers/launch-cmd.js
9869
10007
  import fs14 from "fs";
9870
10008
  import path13 from "path";
9871
- function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName, thinking, captainBoot) {
10009
+ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName, thinking, captainBoot, settingsPath) {
9872
10010
  const driver = registry.getDriver(agentName);
9873
10011
  if (driver.name === "claude") {
9874
10012
  let cmd = fresh ? "claude" : "claude -c";
@@ -9881,6 +10019,8 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
9881
10019
  cmd += " --permission-mode auto";
9882
10020
  } else if (permissionMode === "bypassPermissions") {
9883
10021
  cmd += " --dangerously-skip-permissions";
10022
+ } else if (permissionMode === "default") {
10023
+ cmd += " --permission-mode default";
9884
10024
  }
9885
10025
  if (model) {
9886
10026
  cmd += ` --model ${model}`;
@@ -9888,6 +10028,9 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
9888
10028
  if (thinking) {
9889
10029
  cmd += ` --effort ${thinking}`;
9890
10030
  }
10031
+ if (settingsPath) {
10032
+ cmd += ` --settings ${settingsPath}`;
10033
+ }
9891
10034
  if (templatesDir) {
9892
10035
  const roleFile2 = path13.join(templatesDir, `${role}.claude.md`);
9893
10036
  const legacyRoleFile = path13.join(templatesDir, `${role}.CLAUDE.md`);
@@ -15140,6 +15283,13 @@ function mergeCrewPermissions(settings) {
15140
15283
  permissions.allow = [.../* @__PURE__ */ new Set([...existing, ...CREW_PERMISSION_ALLOWLIST])];
15141
15284
  return next;
15142
15285
  }
15286
+ function writeRouterSettings(o) {
15287
+ const dir = join26(o.stateRoot, o.project, o.taskId);
15288
+ mkdirSync9(dir, { recursive: true });
15289
+ const file = join26(dir, "router-settings.json");
15290
+ writeFileSync12(file, JSON.stringify({ env: o.env }, null, 2));
15291
+ return file;
15292
+ }
15143
15293
  function writePerCrewSettingsLocal(o) {
15144
15294
  const dir = join26(o.projectCwd, ".claude");
15145
15295
  mkdirSync9(dir, { recursive: true });
@@ -16533,6 +16683,7 @@ async function runCrewSpawn2(input) {
16533
16683
  },
16534
16684
  writeSettingsLocal: (cwd) => writePerCrewSettingsLocal({ projectCwd: cwd }),
16535
16685
  writeOpencodeConfig: writePerCrewOpencodeConfig,
16686
+ writeRouterSettings,
16536
16687
  sendFirstTurn: (pane, firstTurn, preLaunchScreen, opts) => sendFirstTurnWhenReady(runtime, pane, firstTurn, preLaunchScreen, opts),
16537
16688
  getFreePort,
16538
16689
  sendCodexFirstTurn,
@@ -18528,6 +18679,10 @@ function resolveLaunchAgent(overrides, roleConfig, roleModelDefault) {
18528
18679
  thinking: overrides.thinking ?? roleConfig?.thinking
18529
18680
  };
18530
18681
  }
18682
+ function resolveLaunchBackend(o) {
18683
+ const roleBackend = o.agentName === "claude" ? o.roleConfig?.backend : void 0;
18684
+ return o.backendOverride ?? roleBackend ?? "native";
18685
+ }
18531
18686
 
18532
18687
  // packages/cli/src/commands/launch.ts
18533
18688
  init_dist2();
@@ -18575,11 +18730,18 @@ function ensureCmuxReady(headless) {
18575
18730
  }
18576
18731
  var launchCommand = new Command14("launch").description(
18577
18732
  "Launch a project captain (with project arg) or all captains (--all). Use `squadrant command` for one-shot Command tasks."
18578
- ).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--keep", "Resume the latest session even on a new day / after a template change").option("--all", "Launch all captain workspaces").option("--headless", "Skip the interactive cmux-app requirement (used by the daemon to boot captains without a terminal)").option("--agent <name>", "Override captain agent for this launch (claude|codex|gemini|opencode); takes precedence over defaults.roles.captain.agent").option("--model <name>", "Override captain model for this launch; takes precedence over defaults.roles.captain.model").option("--thinking <level>", `Override captain thinking level for this launch (${THINKING_LEVELS.join("|")}) \u2192 claude --effort; takes precedence over defaults.roles.captain.thinking`).action(async (project, opts) => {
18733
+ ).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--keep", "Resume the latest session even on a new day / after a template change").option("--all", "Launch all captain workspaces").option("--headless", "Skip the interactive cmux-app requirement (used by the daemon to boot captains without a terminal)").option("--agent <name>", "Override captain agent for this launch (claude|codex|gemini|opencode); takes precedence over defaults.roles.captain.agent").option("--model <name>", "Override captain model for this launch; takes precedence over defaults.roles.captain.model").option("--thinking <level>", `Override captain thinking level for this launch (${THINKING_LEVELS.join("|")}) \u2192 claude --effort; takes precedence over defaults.roles.captain.thinking`).option("--backend <mode>", "Override captain backend for this launch (native|direct|proxy); takes precedence over defaults.roles.captain.backend").action(async (project, opts) => {
18579
18734
  if (opts.fresh && opts.keep) {
18580
18735
  console.error(chalk15.red("\n \u2718 --fresh and --keep are mutually exclusive\n"));
18581
18736
  process.exit(1);
18582
18737
  }
18738
+ if (opts.backend !== void 0 && !isBackendMode(opts.backend)) {
18739
+ console.error(chalk15.red(`
18740
+ \u2718 Invalid --backend value '${opts.backend}'. Valid values: native, direct, proxy
18741
+ `));
18742
+ process.exit(1);
18743
+ }
18744
+ const backendOverride = opts.backend;
18583
18745
  let thinkingOverride;
18584
18746
  try {
18585
18747
  thinkingOverride = opts.thinking ? parseThinkingLevel(opts.thinking) : void 0;
@@ -18606,6 +18768,7 @@ var launchCommand = new Command14("launch").description(
18606
18768
  roleConfig,
18607
18769
  config.defaults.models?.[role]
18608
18770
  );
18771
+ const backend = resolveLaunchBackend({ backendOverride, agentName, roleConfig });
18609
18772
  const refusal = resolveAnthropicRefusal(role, workspaceName, agentName, model);
18610
18773
  if (refusal) {
18611
18774
  console.error(chalk15.red(`
@@ -18635,6 +18798,29 @@ var launchCommand = new Command14("launch").description(
18635
18798
  if (isOpencodeCaptain && projectName) {
18636
18799
  captainOpencodeConfigPath = writePerCrewOpencodeConfig({ stateRoot, project: projectName, taskId: "captain" });
18637
18800
  }
18801
+ let route;
18802
+ if (role === "captain") {
18803
+ try {
18804
+ route = await prepareCaptainRoute({
18805
+ backend,
18806
+ agentName,
18807
+ configuredPermissionMode: permissionMode,
18808
+ project: projectName ?? "",
18809
+ model,
18810
+ stateRoot,
18811
+ config,
18812
+ deps: {
18813
+ routerCredentials: (o) => fetchRouterCredentials(o.project, o.backend, { call: squadrantdCall }),
18814
+ writeRouterSettings
18815
+ },
18816
+ warn: (m) => console.error(chalk15.yellow(` \u26A0 ${m}`))
18817
+ });
18818
+ } catch (err) {
18819
+ console.error(chalk15.red(` \u2718 ${err.message}`));
18820
+ hadFailure = true;
18821
+ return;
18822
+ }
18823
+ }
18638
18824
  const priorRecord = projectName ? readCaptainAddress(stateRoot, projectName) : null;
18639
18825
  const captainPort = isOpencodeCaptain ? await getFreePort() : void 0;
18640
18826
  let opencodeResumeSessionId;
@@ -18660,15 +18846,18 @@ var launchCommand = new Command14("launch").description(
18660
18846
  registry,
18661
18847
  role,
18662
18848
  forceFresh,
18663
- permissionMode,
18664
- model,
18849
+ route?.permissionMode ?? permissionMode,
18850
+ route?.model ?? model,
18665
18851
  TEMPLATES_DIR4,
18666
18852
  resolveCaptainSocketPath(captainChannelEnabled, projectName, workspaceName),
18667
18853
  resolveCaptainSessionName(agentName, projectName),
18668
18854
  thinking,
18669
- captainBoot
18855
+ captainBoot,
18856
+ route?.settingsPath
18670
18857
  );
18671
- return captainOpencodeConfigPath ? `OPENCODE_CONFIG=${captainOpencodeConfigPath} ${baseCmd}` : baseCmd;
18858
+ const gatePrefix = route && Object.keys(route.env).length > 0 ? `${renderEnvAssignments(route.env)} ` : "";
18859
+ const cmd = `${gatePrefix}${baseCmd}`;
18860
+ return captainOpencodeConfigPath ? `OPENCODE_CONFIG=${captainOpencodeConfigPath} ${cmd}` : cmd;
18672
18861
  },
18673
18862
  initialPrompt,
18674
18863
  runtime,
@@ -21110,9 +21299,8 @@ async function runGatePermissionRequest(deps) {
21110
21299
  const p = payload;
21111
21300
  const taskId = env.SQUADRANT_CREW_TASK_ID;
21112
21301
  const project = env.SQUADRANT_CREW_PROJECT;
21113
- const isSide = env[SIDE_SESSION_ENV] === "1";
21114
- if (!taskId && !isSide) {
21115
- return { decision: "yield", reason: "not a squadrant crew/side session" };
21302
+ if (!isGateSession(env)) {
21303
+ return { decision: "yield", reason: "not a squadrant crew/side/captain session" };
21116
21304
  }
21117
21305
  const stdout = deps.stdout ?? (() => {
21118
21306
  });