squadrant 0.21.0 → 0.22.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.
package/dist/index.js CHANGED
@@ -156,12 +156,7 @@ function getDefaultConfig() {
156
156
  // resolver fallback is the safety net).
157
157
  controlChannel: { claude: "on", opencode: "on" },
158
158
  crewRouting: {
159
- rules: [
160
- { tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
161
- { tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
162
- { tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
163
- { tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
164
- ]
159
+ rules: DEFAULT_CREW_ROUTING_RULES.map((r) => ({ ...r }))
165
160
  }
166
161
  },
167
162
  metrics: {
@@ -203,7 +198,7 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
203
198
  function resolveHome(p) {
204
199
  return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
205
200
  }
206
- var ROUTER_KINDS, GATE_MODES, GATE_POLICIES, THINKING_LEVELS, CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
201
+ var ROUTER_KINDS, GATE_MODES, GATE_POLICIES, THINKING_LEVELS, CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
207
202
  var init_config = __esm({
208
203
  "packages/shared/dist/config.js"() {
209
204
  init_config_io();
@@ -215,6 +210,201 @@ var init_config = __esm({
215
210
  DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
216
211
  CONFIG_DIR = path2.dirname(DEFAULT_CONFIG_PATH);
217
212
  DAEMON_SOCK_PATH = path2.join(CONFIG_DIR, "squadrant.sock");
213
+ DEFAULT_CREW_ROUTING_RULES = [
214
+ { tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
215
+ { tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
216
+ { tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
217
+ { tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
218
+ ];
219
+ }
220
+ });
221
+
222
+ // packages/shared/dist/provider-preset.js
223
+ function isProviderPresetId(v2) {
224
+ return v2 === "a" || v2 === "b" || v2 === "c" || v2 === "d";
225
+ }
226
+ function presetCrewRouting(id) {
227
+ const base = (tier) => {
228
+ const found = DEFAULT_CREW_ROUTING_RULES.find((r) => r.tier === tier);
229
+ return found ? { ...found } : { tier, match: "", agent: "claude" };
230
+ };
231
+ const codex = (tier) => ({
232
+ tier: base(tier).tier,
233
+ match: base(tier).match,
234
+ agent: "codex"
235
+ });
236
+ switch (id) {
237
+ case "a":
238
+ return DEFAULT_CREW_ROUTING_RULES.map((r) => ({ ...r }));
239
+ case "b":
240
+ return [
241
+ { ...base("extreme"), agent: "opencode", model: OPENCODE_MODEL },
242
+ { ...base("hard"), agent: "opencode", model: OPENCODE_MODEL },
243
+ codex("mobile"),
244
+ { ...base("daily"), agent: "opencode", model: OPENCODE_MODEL }
245
+ ];
246
+ case "c":
247
+ return [
248
+ { ...base("extreme"), agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL },
249
+ { ...base("hard"), agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL },
250
+ codex("mobile"),
251
+ { ...base("daily"), agent: "opencode", model: OPENCODE_MODEL }
252
+ ];
253
+ case "d":
254
+ return ["extreme", "hard", "mobile", "daily"].map(codex);
255
+ }
256
+ }
257
+ function providerPresetDefaults(id, router) {
258
+ switch (id) {
259
+ case "a":
260
+ return {
261
+ roles: {
262
+ command: { agent: "claude", model: "opus" },
263
+ captain: { agent: "claude", model: "opus" },
264
+ crew: { agent: "claude", model: "sonnet" },
265
+ exploration: { agent: "claude", model: "haiku" },
266
+ side: { agent: "claude", model: "opus" }
267
+ },
268
+ permissions: { command: "auto", captain: "auto", crew: "auto" },
269
+ crewRouting: { rules: presetCrewRouting("a") }
270
+ };
271
+ case "b": {
272
+ const opencode = { agent: "opencode", model: OPENCODE_MODEL };
273
+ return {
274
+ roles: {
275
+ command: { ...opencode },
276
+ captain: { ...opencode },
277
+ crew: { ...opencode },
278
+ exploration: { ...opencode },
279
+ side: { ...opencode }
280
+ },
281
+ permissions: { command: "auto", captain: "auto", crew: "auto" },
282
+ crewRouting: { rules: presetCrewRouting("b") }
283
+ };
284
+ }
285
+ case "c": {
286
+ if (!router) {
287
+ throw new Error("preset C (claude harness + router backend) requires a router config \u2014 set defaults.router first");
288
+ }
289
+ const routed = { agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL };
290
+ return {
291
+ roles: {
292
+ command: { ...routed },
293
+ captain: { ...routed },
294
+ crew: { ...routed },
295
+ exploration: { ...routed },
296
+ side: { ...routed }
297
+ },
298
+ // Auto mode's classifier is hardcoded to Sonnet 5 and fails closed on a
299
+ // router upstream — every claude session must use a manual mode so the
300
+ // U7 gate owns the prompt.
301
+ permissions: { command: "default", captain: "default", crew: "default" },
302
+ crewRouting: { rules: presetCrewRouting("c") },
303
+ router,
304
+ gate: { mode: "on" }
305
+ };
306
+ }
307
+ case "d": {
308
+ const codex = { agent: "codex" };
309
+ return {
310
+ roles: {
311
+ command: { ...codex },
312
+ captain: { ...codex },
313
+ crew: { ...codex },
314
+ exploration: { ...codex },
315
+ side: { ...codex }
316
+ },
317
+ permissions: {},
318
+ crewRouting: { rules: presetCrewRouting("d") }
319
+ };
320
+ }
321
+ }
322
+ }
323
+ function detectProviderPresetId(config) {
324
+ const crew = config.defaults?.roles?.crew;
325
+ if (crew?.agent === "opencode")
326
+ return "b";
327
+ if (crew?.agent === "codex")
328
+ return "d";
329
+ if (crew?.backend && crew.backend !== "native")
330
+ return "c";
331
+ return "a";
332
+ }
333
+ function applyProviderPreset(config, id, opts) {
334
+ const preset = providerPresetDefaults(id, opts.router);
335
+ const next = structuredClone(config);
336
+ const changes = [];
337
+ if (opts.overwrite) {
338
+ next.defaults.roles = { ...preset.roles };
339
+ changes.push("defaults.roles");
340
+ } else if (next.defaults.roles === void 0) {
341
+ next.defaults.roles = { ...preset.roles };
342
+ changes.push("defaults.roles");
343
+ }
344
+ if (opts.overwrite) {
345
+ next.defaults.crewRouting = { rules: preset.crewRouting.rules.map((r) => ({ ...r })) };
346
+ changes.push("defaults.crewRouting");
347
+ } else if (next.defaults.crewRouting === void 0) {
348
+ next.defaults.crewRouting = { rules: preset.crewRouting.rules.map((r) => ({ ...r })) };
349
+ changes.push("defaults.crewRouting");
350
+ }
351
+ const existingPerms = next.defaults.permissions ?? {};
352
+ if (opts.overwrite) {
353
+ next.defaults.permissions = { ...existingPerms, ...preset.permissions };
354
+ if (Object.keys(preset.permissions).length > 0)
355
+ changes.push("defaults.permissions");
356
+ } else if (next.defaults.permissions === void 0 && Object.keys(preset.permissions).length > 0) {
357
+ next.defaults.permissions = { ...preset.permissions };
358
+ changes.push("defaults.permissions");
359
+ }
360
+ if (preset.router) {
361
+ if (opts.overwrite || next.defaults.router === void 0) {
362
+ next.defaults.router = structuredClone(preset.router);
363
+ changes.push("defaults.router");
364
+ }
365
+ }
366
+ if (preset.gate) {
367
+ if (opts.overwrite || next.defaults.gate === void 0) {
368
+ next.defaults.gate = { ...preset.gate };
369
+ changes.push("defaults.gate");
370
+ }
371
+ }
372
+ return { config: next, changes };
373
+ }
374
+ var PROVIDER_PRESETS, ROUTER_PRESET_MODEL, DEFAULT_ROUTER_KIND, DEFAULT_ROUTER_BASE_URL, OPENCODE_MODEL;
375
+ var init_provider_preset = __esm({
376
+ "packages/shared/dist/provider-preset.js"() {
377
+ init_config();
378
+ PROVIDER_PRESETS = [
379
+ {
380
+ id: "a",
381
+ label: "Claude Code (Pro/Max)",
382
+ summary: "Claude harness on your subscription/API key. Real auto mode.",
383
+ requiresAnthropic: true
384
+ },
385
+ {
386
+ id: "b",
387
+ label: "opencode (no Anthropic subscription)",
388
+ summary: "All roles run on the opencode harness. Auto mode is model-agnostic.",
389
+ requiresAnthropic: false
390
+ },
391
+ {
392
+ id: "c",
393
+ label: "Claude harness + router backend (advanced)",
394
+ summary: "Claude harness against a router upstream. The U7 gate replaces auto mode.",
395
+ requiresAnthropic: false
396
+ },
397
+ {
398
+ id: "d",
399
+ label: "Codex (ChatGPT Pro)",
400
+ summary: "All roles run on the codex harness.",
401
+ requiresAnthropic: false
402
+ }
403
+ ];
404
+ ROUTER_PRESET_MODEL = "deepseek-v4.1-flash";
405
+ DEFAULT_ROUTER_KIND = "opencode-go";
406
+ DEFAULT_ROUTER_BASE_URL = "https://opencode.ai/zen/go";
407
+ OPENCODE_MODEL = "opencode-go/deepseek-v4.1-flash";
218
408
  }
219
409
  });
220
410
 
@@ -1465,11 +1655,16 @@ __export(dist_exports, {
1465
1655
  CREW_SKILLS: () => CREW_SKILLS,
1466
1656
  DAEMON_SOCK_PATH: () => DAEMON_SOCK_PATH,
1467
1657
  DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
1658
+ DEFAULT_CREW_ROUTING_RULES: () => DEFAULT_CREW_ROUTING_RULES,
1468
1659
  DEFAULT_NOTIFY: () => DEFAULT_NOTIFY,
1660
+ DEFAULT_ROUTER_BASE_URL: () => DEFAULT_ROUTER_BASE_URL,
1661
+ DEFAULT_ROUTER_KIND: () => DEFAULT_ROUTER_KIND,
1469
1662
  GATE_MODES: () => GATE_MODES,
1470
1663
  GATE_POLICIES: () => GATE_POLICIES,
1471
1664
  MANAGED_TARGETS: () => MANAGED_TARGETS,
1665
+ PROVIDER_PRESETS: () => PROVIDER_PRESETS,
1472
1666
  ROUTER_KINDS: () => ROUTER_KINDS,
1667
+ ROUTER_PRESET_MODEL: () => ROUTER_PRESET_MODEL,
1473
1668
  SAFE_KINDS: () => SAFE_KINDS,
1474
1669
  SOCKET_CONTROL_MODE_PATH: () => SOCKET_CONTROL_MODE_PATH,
1475
1670
  SPOKE_SUBDIRS: () => SPOKE_SUBDIRS,
@@ -1478,6 +1673,7 @@ __export(dist_exports, {
1478
1673
  THINKING_LEVELS: () => THINKING_LEVELS,
1479
1674
  UPDATE_CHECK_STATE_PATH: () => UPDATE_CHECK_STATE_PATH,
1480
1675
  addWorktree: () => addWorktree,
1676
+ applyProviderPreset: () => applyProviderPreset,
1481
1677
  applySafeFixes: () => applySafeFixes,
1482
1678
  captainSessionName: () => captainSessionName,
1483
1679
  checkForUpdate: () => checkForUpdate,
@@ -1493,6 +1689,7 @@ __export(dist_exports, {
1493
1689
  defaultStatePath: () => defaultStatePath,
1494
1690
  detectDrift: () => detectDrift,
1495
1691
  detectInstallManager: () => detectInstallManager,
1692
+ detectProviderPresetId: () => detectProviderPresetId,
1496
1693
  ensureCmuxAutoConfig: () => ensureCmuxAutoConfig,
1497
1694
  ensureDirSync: () => ensureDirSync,
1498
1695
  ensureRuntimeSynced: () => ensureRuntimeSynced,
@@ -1511,6 +1708,7 @@ __export(dist_exports, {
1511
1708
  isGateMode: () => isGateMode,
1512
1709
  isGatePolicy: () => isGatePolicy,
1513
1710
  isNewerVersion: () => isNewerVersion,
1711
+ isProviderPresetId: () => isProviderPresetId,
1514
1712
  isQuieter: () => isQuieter,
1515
1713
  isRouterKind: () => isRouterKind,
1516
1714
  isThinkingLevel: () => isThinkingLevel,
@@ -1527,6 +1725,7 @@ __export(dist_exports, {
1527
1725
  parseThinkingLevel: () => parseThinkingLevel,
1528
1726
  probeCmuxDaemonDirect: () => probeCmuxDaemonDirect,
1529
1727
  projectConfigPath: () => projectConfigPath,
1728
+ providerPresetDefaults: () => providerPresetDefaults,
1530
1729
  readConfigFileSync: () => readConfigFileSync,
1531
1730
  readDailyLog: () => readDailyLog,
1532
1731
  readProjectLevelSource: () => readProjectLevelSource,
@@ -1556,6 +1755,7 @@ __export(dist_exports, {
1556
1755
  var init_dist = __esm({
1557
1756
  "packages/shared/dist/index.js"() {
1558
1757
  init_config();
1758
+ init_provider_preset();
1559
1759
  init_project_config();
1560
1760
  init_effort();
1561
1761
  init_router_model();
@@ -2793,8 +2993,8 @@ function reconcileLiveness(prev, next) {
2793
2993
  if (!prev)
2794
2994
  return next;
2795
2995
  if (next.source === "scan") {
2796
- const pidAlive = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
2797
- return { ...prev, pidAlive, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
2996
+ const pidAlive2 = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
2997
+ return { ...prev, pidAlive: pidAlive2, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
2798
2998
  }
2799
2999
  if (next.startedAt >= prev.startedAt || next.lastState === "end")
2800
3000
  return next;
@@ -3540,6 +3740,68 @@ function createDirectSurfaceLivenessProbe(cmux2, getCaptainTitle) {
3540
3740
  }
3541
3741
  };
3542
3742
  }
3743
+ function hasModalOptionList(screen) {
3744
+ if (!screen)
3745
+ return false;
3746
+ const lines = screen.split(/\r?\n/);
3747
+ let bottomHR = -1;
3748
+ let topHR = -1;
3749
+ for (let i = lines.length - 1; i >= 0; i--) {
3750
+ if (HR_RE.test(lines[i])) {
3751
+ if (bottomHR === -1)
3752
+ bottomHR = i;
3753
+ else {
3754
+ topHR = i;
3755
+ break;
3756
+ }
3757
+ }
3758
+ }
3759
+ if (topHR === -1)
3760
+ return false;
3761
+ return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
3762
+ }
3763
+ function classifyCaptainPane(screen, opts = {}) {
3764
+ if (screen == null || screen.trim() === "")
3765
+ return { status: "unreadable" };
3766
+ if (hasModalOptionList(screen))
3767
+ return { status: "locked" };
3768
+ if (WORKING_RE.test(screen))
3769
+ return { status: "mid-turn" };
3770
+ const idleAfterMs = opts.idleAfterMs ?? 0;
3771
+ const stale = opts.outputAgoMs !== void 0 && opts.outputAgoMs >= idleAfterMs;
3772
+ return stale ? { status: "no-session" } : { status: "mid-turn" };
3773
+ }
3774
+ function screenHash(screen) {
3775
+ let h = 2166136261;
3776
+ for (let i = 0; i < screen.length; i++) {
3777
+ h ^= screen.charCodeAt(i);
3778
+ h = Math.imul(h, 16777619) >>> 0;
3779
+ }
3780
+ return h.toString(16).padStart(8, "0");
3781
+ }
3782
+ function createCaptainPaneReader(cmux2, getCaptainTitle, opts = {}) {
3783
+ const idleAfterMs = opts.idleAfterMs ?? PANE_IDLE_AFTER_MS;
3784
+ return async (project) => {
3785
+ try {
3786
+ const wsId = await cmux2.findWorkspaceId(getCaptainTitle(project));
3787
+ if (!wsId)
3788
+ return { status: "no-session" };
3789
+ const surfaces = await cmux2.listSurfaces(wsId);
3790
+ const want = getCaptainTitle(project);
3791
+ const pane = surfaces.find((s) => s.title === want) ?? surfaces[0];
3792
+ if (!pane)
3793
+ return { status: "no-session" };
3794
+ const screen = await cmux2.readPaneScreen(pane);
3795
+ if (screen == null)
3796
+ return { status: "unreadable" };
3797
+ const outputAgoMs = opts.noteScreen?.(project, screenHash(screen));
3798
+ return classifyCaptainPane(screen, { outputAgoMs, idleAfterMs });
3799
+ } catch (e) {
3800
+ opts.log?.(`captain pane read failed project=${project}: ${e.message}`);
3801
+ return { status: "unreadable" };
3802
+ }
3803
+ };
3804
+ }
3543
3805
  function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
3544
3806
  return async (rec) => {
3545
3807
  try {
@@ -3562,11 +3824,14 @@ function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
3562
3824
  }
3563
3825
  };
3564
3826
  }
3565
- var TAIL_LINES;
3827
+ var TAIL_LINES, HR_RE, WORKING_RE, PANE_IDLE_AFTER_MS;
3566
3828
  var init_crew_pane_reader = __esm({
3567
3829
  "packages/core/dist/crew-pane-reader.js"() {
3568
3830
  init_dist();
3569
3831
  TAIL_LINES = 25;
3832
+ HR_RE = /^\s*─{10,}\s*$/;
3833
+ WORKING_RE = /esc to interrupt|\btokens?\b.*\b(used|left)\b|↓\s*[\d.]+\s*k?\s*tokens?\b|\(\d+m?\s*\d+s\b/i;
3834
+ PANE_IDLE_AFTER_MS = 10 * 6e4;
3570
3835
  }
3571
3836
  });
3572
3837
 
@@ -4237,6 +4502,11 @@ function assertBackendUsable(o) {
4237
4502
  function shouldBuildRouterService(router, isVitest) {
4238
4503
  return !!router && !isVitest;
4239
4504
  }
4505
+ function claudeEnvShadowsRouter(claudeEnv) {
4506
+ if (!claudeEnv)
4507
+ return [];
4508
+ return Object.keys(claudeEnv).filter((key) => key.startsWith("ANTHROPIC_"));
4509
+ }
4240
4510
  var init_router_resolution = __esm({
4241
4511
  "packages/core/dist/router-resolution.js"() {
4242
4512
  init_dist();
@@ -4263,6 +4533,14 @@ function buildRouterEnv(creds, model) {
4263
4533
  env.ANTHROPIC_MODEL = model;
4264
4534
  return env;
4265
4535
  }
4536
+ function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
4537
+ const merged = { ...routerEnv };
4538
+ for (const [key, value] of Object.entries(claudeEnv ?? {})) {
4539
+ if (!key.startsWith("ANTHROPIC_"))
4540
+ merged[key] = value;
4541
+ }
4542
+ return merged;
4543
+ }
4266
4544
  function formatCustomHeaders(headers) {
4267
4545
  const entries = Object.entries(headers ?? {});
4268
4546
  if (entries.length === 0)
@@ -4576,10 +4854,18 @@ async function runCrewSpawn(input, config, deps) {
4576
4854
  if (agentName === "claude") {
4577
4855
  let routerEnv = {};
4578
4856
  if (backend !== "native") {
4857
+ if (!deps.writeRouterSettings) {
4858
+ 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`);
4859
+ }
4579
4860
  if (!deps.routerCredentials) {
4580
4861
  throw new Error(`backend '${backend}' requires router credentials, but the spawn path has no daemon credentials provider`);
4581
4862
  }
4582
4863
  routerEnv = buildRouterEnv(await deps.routerCredentials({ project: input.project, backend }), crewModel);
4864
+ const shadowed = claudeEnvShadowsRouter(config.defaults.claudeEnv);
4865
+ if (shadowed.length > 0) {
4866
+ 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).
4867
+ `);
4868
+ }
4583
4869
  }
4584
4870
  ensureSocksDir();
4585
4871
  const messagingSocketPath = path9.join(CC_SOCKS_DIR, `squadrant-${randomUUID3()}.sock`);
@@ -4592,6 +4878,12 @@ async function runCrewSpawn(input, config, deps) {
4592
4878
  name,
4593
4879
  messagingSocketPath
4594
4880
  });
4881
+ const routerSettingsPath = Object.keys(routerEnv).length > 0 ? deps.writeRouterSettings({
4882
+ stateRoot: STATE_ROOT,
4883
+ project: input.project,
4884
+ taskId: rec.id,
4885
+ env: mergeClaudeEnvRouterSettings(routerEnv, config.defaults.claudeEnv)
4886
+ }) : void 0;
4595
4887
  let hooksInstalled = false;
4596
4888
  try {
4597
4889
  deps.writeSettingsLocal(spawnCwd);
@@ -4607,18 +4899,25 @@ async function runCrewSpawn(input, config, deps) {
4607
4899
  messagingSocketPath,
4608
4900
  // Permission mode is config-driven so squadrant can default crews to 'auto'
4609
4901
  // or keep the semi-automatic 'acceptEdits' gate. Falls back to 'acceptEdits'.
4610
- permissionMode: config.defaults.permissions?.crew ?? "acceptEdits",
4902
+ // #772: a router-backed crew MUST use 'default' — 'auto' makes the built-in
4903
+ // Sonnet-5 classifier own PermissionRequest and the gate yields, which is
4904
+ // exactly the fail-closed dead end U7 exists to fix. The rubric owns the
4905
+ // permission mode for a routed spawn; the operator's config cannot override
4906
+ // it back into the broken path.
4907
+ permissionMode: backend !== "native" ? "default" : config.defaults.permissions?.crew ?? "acceptEdits",
4611
4908
  // #708: self-describing name so ListAgents/the registry can tell this
4612
4909
  // crew apart from an unrelated session instead of an auto-derived cwd
4613
4910
  // basename (only the claude driver reads this — other agents ignore it).
4614
4911
  sessionName: crewSessionName(input.project, name),
4615
4912
  ...crewModel ? { model: crewModel } : {},
4616
- ...crewThinking ? { thinking: crewThinking } : {}
4913
+ ...crewThinking ? { thinking: crewThinking } : {},
4914
+ ...routerSettingsPath ? { settingsPath: routerSettingsPath } : {}
4617
4915
  });
4618
4916
  const direction2 = input.direction ?? "tab";
4619
4917
  const title2 = titleFor(input.project, name);
4620
4918
  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}`;
4919
+ const gateEnv = backend !== "native" ? " SQUADRANT_GATE=on" : "";
4920
+ const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}${gateEnv}`;
4622
4921
  const routerPrefix = Object.keys(routerEnv).length > 0 ? ` ${renderEnvAssignments(routerEnv)}` : "";
4623
4922
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix}${routerPrefix} ${niceCrewCommand(cliCommand2)}`);
4624
4923
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
@@ -5397,8 +5696,9 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
5397
5696
  if (mode !== "off")
5398
5697
  throw new DeferDelivery(null, "no-channel");
5399
5698
  }
5699
+ const paneOpts = { ...sendOpts, agent };
5400
5700
  try {
5401
- return await cmux2.send(surface, text, sendOpts);
5701
+ return await cmux2.send(surface, text, paneOpts);
5402
5702
  } catch (e) {
5403
5703
  if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
5404
5704
  throw e;
@@ -5410,7 +5710,7 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
5410
5710
  }
5411
5711
  log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
5412
5712
  surface = next;
5413
- return cmux2.send(next, text, sendOpts);
5713
+ return cmux2.send(next, text, paneOpts);
5414
5714
  }
5415
5715
  });
5416
5716
  if ("delivered" in result) {
@@ -6341,16 +6641,26 @@ function createRunCommand(cliBin) {
6341
6641
  }
6342
6642
  };
6343
6643
  }
6644
+ function captainAliveStateFromHealth(rows, project) {
6645
+ const row = rows.find((h) => h.kind === "captain" && h.project === project);
6646
+ if (!row)
6647
+ return "unknown";
6648
+ if (row.state === "alive")
6649
+ return "alive";
6650
+ if (row.state === "gone" || row.state === "stopped")
6651
+ return "dead";
6652
+ return "unknown";
6653
+ }
6344
6654
  function isCaptainAliveFromHealth(rows, project) {
6345
- return rows.some((h) => h.kind === "captain" && h.project === project && h.state === "alive");
6655
+ return captainAliveStateFromHealth(rows, project) === "alive";
6346
6656
  }
6347
6657
  function createIsCaptainAlive(sock) {
6348
6658
  return async (project) => {
6349
6659
  try {
6350
6660
  const health = await sendRequest(sock, { kind: "health", project }, 5e3);
6351
- return isCaptainAliveFromHealth(health ?? [], project);
6661
+ return captainAliveStateFromHealth(health ?? [], project);
6352
6662
  } catch {
6353
- return false;
6663
+ return "unknown";
6354
6664
  }
6355
6665
  };
6356
6666
  }
@@ -6387,16 +6697,19 @@ function createEnsureCaptainAlive(deps) {
6387
6697
  const now = deps.now ?? (() => Date.now());
6388
6698
  const inFlight = /* @__PURE__ */ new Map();
6389
6699
  async function run(project) {
6390
- if (await deps.isAlive(project))
6700
+ const first = await deps.isAlive(project);
6701
+ if (first === "alive")
6391
6702
  return "alive";
6392
6703
  await deps.launch(project);
6393
6704
  const deadline = now() + warmupTimeoutMs;
6705
+ let last = first;
6394
6706
  while (now() < deadline) {
6395
- if (await deps.isAlive(project))
6707
+ last = await deps.isAlive(project);
6708
+ if (last === "alive")
6396
6709
  return "launched";
6397
6710
  await sleep3(pollMs);
6398
6711
  }
6399
- return "timeout";
6712
+ return last === "dead" ? "timeout" : "unknown";
6400
6713
  }
6401
6714
  return function ensure(project) {
6402
6715
  const existing = inFlight.get(project);
@@ -6451,6 +6764,26 @@ ${ev.question}`;
6451
6764
  function formatInbound(text) {
6452
6765
  return `\u{1F4E9} [from Telegram] ${text}`;
6453
6766
  }
6767
+ function mediaKind(m) {
6768
+ for (const [field, name] of MEDIA_KINDS) {
6769
+ if (m[field] !== void 0)
6770
+ return name;
6771
+ }
6772
+ return void 0;
6773
+ }
6774
+ function mediaMarker(kind) {
6775
+ return `[${kind} attached - not forwarded]`;
6776
+ }
6777
+ function inboundBody(text, kind) {
6778
+ if (kind === void 0)
6779
+ return text ?? "";
6780
+ const marker = mediaMarker(kind);
6781
+ return text ? `${text}
6782
+ ${marker}` : marker;
6783
+ }
6784
+ function formatMediaReceipt(kind, hasCaption) {
6785
+ return hasCaption ? `\u{1F4CE} ${kind} not forwarded \u2014 your caption reached the captain` : `\u{1F4CE} ${kind} not forwarded \u2014 the captain was told it arrived, nothing else was sent`;
6786
+ }
6454
6787
  function formatUsageLine(u) {
6455
6788
  const reqs = u.requests === 1 ? "1 req" : `${u.requests} reqs`;
6456
6789
  const named = Object.keys(u.models).filter((m) => m !== "unknown").sort((a, b) => u.models[b].costUsd - u.models[a].costUsd);
@@ -6462,8 +6795,19 @@ function maskToken(token) {
6462
6795
  return token;
6463
6796
  return "*".repeat(token.length - 4) + token.slice(-4);
6464
6797
  }
6798
+ var MEDIA_KINDS;
6465
6799
  var init_format = __esm({
6466
6800
  "packages/core/dist/telegram/format.js"() {
6801
+ MEDIA_KINDS = [
6802
+ ["photo", "photo"],
6803
+ ["voice", "voice message"],
6804
+ ["video_note", "video message"],
6805
+ ["video", "video"],
6806
+ ["audio", "audio file"],
6807
+ ["document", "document"],
6808
+ ["animation", "animation"],
6809
+ ["sticker", "sticker"]
6810
+ ];
6467
6811
  }
6468
6812
  });
6469
6813
 
@@ -6486,6 +6830,8 @@ function loadState(stateRoot) {
6486
6830
  };
6487
6831
  if (typeof data.lastUserId === "number")
6488
6832
  result.lastUserId = data.lastUserId;
6833
+ if (data.pending && Object.keys(data.pending).length > 0)
6834
+ result.pending = data.pending;
6489
6835
  return result;
6490
6836
  } catch {
6491
6837
  return { offset: 0, topics: {}, notify: {} };
@@ -6499,6 +6845,19 @@ function setTopic(stateRoot, project, topicId, scope = "project") {
6499
6845
  s.topics[topicKey(project, scope)] = topicId;
6500
6846
  saveState(stateRoot, s);
6501
6847
  }
6848
+ function pruneTopics(stateRoot, keep) {
6849
+ const s = loadState(stateRoot);
6850
+ const removed = Object.keys(s.topics).filter((key) => {
6851
+ const sep3 = key.indexOf("::");
6852
+ return !keep(sep3 === -1 ? key : key.slice(0, sep3));
6853
+ });
6854
+ if (removed.length === 0)
6855
+ return [];
6856
+ for (const key of removed)
6857
+ delete s.topics[key];
6858
+ saveState(stateRoot, s);
6859
+ return removed;
6860
+ }
6502
6861
  function isNotifyActive(stateRoot, project) {
6503
6862
  return loadState(stateRoot).notify[project] === true;
6504
6863
  }
@@ -6512,6 +6871,42 @@ function setNotify(stateRoot, project, active) {
6512
6871
  s.notify[project] = active;
6513
6872
  saveState(stateRoot, s);
6514
6873
  }
6874
+ function loadPending(stateRoot) {
6875
+ return loadState(stateRoot).pending ?? {};
6876
+ }
6877
+ function setPending(stateRoot, project, p) {
6878
+ const s = loadState(stateRoot);
6879
+ s.pending = { ...s.pending ?? {}, [project]: p };
6880
+ saveState(stateRoot, s);
6881
+ }
6882
+ function clearPending(stateRoot, project) {
6883
+ const s = loadState(stateRoot);
6884
+ if (!s.pending || !(project in s.pending))
6885
+ return;
6886
+ delete s.pending[project];
6887
+ if (Object.keys(s.pending).length === 0)
6888
+ delete s.pending;
6889
+ saveState(stateRoot, s);
6890
+ }
6891
+ function markPendingWarned(stateRoot, project, at) {
6892
+ const s = loadState(stateRoot);
6893
+ const p = s.pending?.[project];
6894
+ if (!p)
6895
+ return;
6896
+ s.pending[project] = { ...p, warnedAt: at };
6897
+ saveState(stateRoot, s);
6898
+ }
6899
+ function notePaneScreen(stateRoot, project, hash, at) {
6900
+ const s = loadState(stateRoot);
6901
+ const p = s.pending?.[project];
6902
+ if (!p)
6903
+ return void 0;
6904
+ if (p.paneHash === hash && p.paneChangedAt !== void 0)
6905
+ return at - p.paneChangedAt;
6906
+ s.pending[project] = { ...p, paneHash: hash, paneChangedAt: at };
6907
+ saveState(stateRoot, s);
6908
+ return void 0;
6909
+ }
6515
6910
  function findProjectByThread(stateRoot, threadId) {
6516
6911
  const s = loadState(stateRoot);
6517
6912
  for (const [key, id] of Object.entries(s.topics)) {
@@ -6534,17 +6929,18 @@ var init_state = __esm({
6534
6929
  function createTelegramClient(opts) {
6535
6930
  const fetchImpl = opts.fetch ?? fetch;
6536
6931
  const base = `https://api.telegram.org/bot${opts.token}`;
6537
- async function call(method, body) {
6932
+ async function call(method, body, signal) {
6538
6933
  const res = await fetchImpl(`${base}/${method}`, {
6539
6934
  method: "POST",
6540
6935
  headers: { "content-type": "application/json" },
6541
- body: JSON.stringify(body)
6936
+ body: JSON.stringify(body),
6937
+ ...signal ? { signal } : {}
6542
6938
  });
6543
6939
  const json = await res.json();
6544
6940
  if (!res.ok || !json.ok) {
6545
6941
  const code = json.error_code ?? res.status;
6546
6942
  const desc = json.description ?? "unknown error";
6547
- throw new Error(`telegram ${method} failed (${code}): ${desc}`);
6943
+ throw new TelegramApiError(code, `telegram ${method} failed (${code}): ${desc}`, json.parameters?.retry_after);
6548
6944
  }
6549
6945
  return json.result;
6550
6946
  }
@@ -6553,8 +6949,8 @@ function createTelegramClient(opts) {
6553
6949
  const r = await call("getMe", {});
6554
6950
  return { id: r.id, username: r.username };
6555
6951
  },
6556
- getUpdates(offset, timeoutSec = 50) {
6557
- return call("getUpdates", { offset, timeout: timeoutSec });
6952
+ getUpdates(offset, timeoutSec = 50, signal) {
6953
+ return call("getUpdates", { offset, timeout: timeoutSec }, signal);
6558
6954
  },
6559
6955
  async sendMessage(chatId, threadId, text, replyMarkup) {
6560
6956
  const body = { chat_id: chatId, text };
@@ -6577,6 +6973,9 @@ function createTelegramClient(opts) {
6577
6973
  const r = await call("createForumTopic", { chat_id: chatId, name });
6578
6974
  return r.message_thread_id;
6579
6975
  },
6976
+ async deleteForumTopic(chatId, threadId) {
6977
+ await call("deleteForumTopic", { chat_id: chatId, message_thread_id: threadId });
6978
+ },
6580
6979
  async setMyCommands(commands) {
6581
6980
  await call("setMyCommands", { commands });
6582
6981
  },
@@ -6585,11 +6984,241 @@ function createTelegramClient(opts) {
6585
6984
  if (threadId !== void 0)
6586
6985
  body.message_thread_id = threadId;
6587
6986
  await call("sendChatAction", body);
6987
+ },
6988
+ async setMessageReaction(chatId, messageId, emoji) {
6989
+ await call("setMessageReaction", {
6990
+ chat_id: chatId,
6991
+ message_id: messageId,
6992
+ reaction: [{ type: "emoji", emoji }]
6993
+ });
6588
6994
  }
6589
6995
  };
6590
6996
  }
6997
+ var TelegramApiError;
6591
6998
  var init_client = __esm({
6592
6999
  "packages/core/dist/telegram/client.js"() {
7000
+ TelegramApiError = class extends Error {
7001
+ code;
7002
+ retryAfterSec;
7003
+ constructor(code, message, retryAfterSec) {
7004
+ super(message);
7005
+ this.code = code;
7006
+ this.retryAfterSec = retryAfterSec;
7007
+ this.name = "TelegramApiError";
7008
+ }
7009
+ };
7010
+ }
7011
+ });
7012
+
7013
+ // packages/core/dist/telegram/inbound-lifecycle.js
7014
+ function defaultSchedule(fn, ms) {
7015
+ const t = setTimeout(fn, ms);
7016
+ t.unref?.();
7017
+ return () => clearTimeout(t);
7018
+ }
7019
+ function formatWatchdogWarning(project, read, minutes) {
7020
+ const head = `\u23F1\uFE0F [${project}] no reply for ${minutes} minutes`;
7021
+ switch (read.status) {
7022
+ case "locked":
7023
+ return `${head} \u2014 the captain is LOCKED on a prompt, waiting for a human. Answer it in the pane (or with \`squadrant crew answer\`) and it will continue.`;
7024
+ case "mid-turn":
7025
+ return `${head} \u2014 the captain is still working (a turn is in flight), just slow. No action needed yet.`;
7026
+ case "no-session":
7027
+ return `${head} \u2014 the captain looks GONE (no live session in its pane). Run \`squadrant launch ${project}\`.`;
7028
+ case "unreadable":
7029
+ return `${head} \u2014 couldn't read the captain's pane to classify it. Check it manually.`;
7030
+ }
7031
+ }
7032
+ function createInboundLifecycle(opts) {
7033
+ const { stateRoot, cfg, sendChatAction, sendReply, readPane, log } = opts;
7034
+ const now = opts.now ?? Date.now;
7035
+ const schedule = opts.schedule ?? defaultSchedule;
7036
+ const typingMs = opts.typingMs ?? TYPING_KEEPALIVE_MS;
7037
+ const watchdogMs = opts.watchdogMs ?? WATCHDOG_MS;
7038
+ const warnAtMs = watchdogMs + (opts.watchdogMs === void 0 ? WATCHDOG_GRACE_MS : 0);
7039
+ const active = /* @__PURE__ */ new Set();
7040
+ let running = false;
7041
+ function issue(project, threadId) {
7042
+ void Promise.resolve(sendChatAction(cfg.supergroupId, threadId, "typing")).catch((e) => log(`telegram typing keep-alive failed project=${project}: ${e.message}`));
7043
+ }
7044
+ function drain(project) {
7045
+ active.add(project);
7046
+ const tick = () => {
7047
+ if (!running || !active.has(project))
7048
+ return;
7049
+ const p = loadPending(stateRoot)[project];
7050
+ if (!p) {
7051
+ active.delete(project);
7052
+ return;
7053
+ }
7054
+ if (now() - p.startedAt >= warnAtMs && p.warnedAt === void 0) {
7055
+ void warn(project, p.threadId);
7056
+ }
7057
+ issue(project, p.threadId);
7058
+ schedule(tick, typingMs);
7059
+ };
7060
+ schedule(tick, typingMs);
7061
+ }
7062
+ async function warn(project, threadId) {
7063
+ markPendingWarned(stateRoot, project, now());
7064
+ let read = { status: "unreadable" };
7065
+ try {
7066
+ if (readPane)
7067
+ read = await readPane(project);
7068
+ } catch (e) {
7069
+ log(`telegram watchdog pane read failed project=${project}: ${e.message}`);
7070
+ }
7071
+ try {
7072
+ await sendReply(threadId, formatWatchdogWarning(project, read, Math.floor(watchdogMs / 6e4)));
7073
+ } catch (e) {
7074
+ log(`telegram watchdog warning failed project=${project}: ${e.message}`);
7075
+ }
7076
+ }
7077
+ return {
7078
+ start() {
7079
+ running = true;
7080
+ for (const [project, p] of Object.entries(loadPending(stateRoot))) {
7081
+ void (async () => {
7082
+ let read = { status: "unreadable" };
7083
+ try {
7084
+ if (readPane)
7085
+ read = await readPane(project);
7086
+ } catch (e) {
7087
+ log(`telegram resume pane read failed project=${project}: ${e.message}`);
7088
+ }
7089
+ const still = loadPending(stateRoot)[project];
7090
+ if (!still || still.startedAt !== p.startedAt)
7091
+ return;
7092
+ if (read.status === "no-session") {
7093
+ clearPending(stateRoot, project);
7094
+ log(`telegram typing lifecycle cleared for ${project}: captain session gone on boot`);
7095
+ return;
7096
+ }
7097
+ log(`telegram typing lifecycle resumed for ${project} (thread ${p.threadId})`);
7098
+ issue(project, p.threadId);
7099
+ drain(project);
7100
+ })();
7101
+ }
7102
+ },
7103
+ stop() {
7104
+ running = false;
7105
+ active.clear();
7106
+ },
7107
+ begin(project, threadId) {
7108
+ setPending(stateRoot, project, { threadId, startedAt: now() });
7109
+ issue(project, threadId);
7110
+ if (running && !active.has(project))
7111
+ drain(project);
7112
+ },
7113
+ pending() {
7114
+ return loadPending(stateRoot);
7115
+ }
7116
+ };
7117
+ }
7118
+ var TYPING_KEEPALIVE_MS, WATCHDOG_MS, WATCHDOG_GRACE_MS;
7119
+ var init_inbound_lifecycle = __esm({
7120
+ "packages/core/dist/telegram/inbound-lifecycle.js"() {
7121
+ init_state();
7122
+ TYPING_KEEPALIVE_MS = 4e3;
7123
+ WATCHDOG_MS = 15 * 6e4;
7124
+ WATCHDOG_GRACE_MS = 1;
7125
+ }
7126
+ });
7127
+
7128
+ // packages/core/dist/telegram/notify.js
7129
+ function runTelegramStatus(opts) {
7130
+ const tg = opts.config.telegram;
7131
+ const env = opts.env ?? process.env;
7132
+ const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
7133
+ const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
7134
+ const sep3 = key.indexOf("::");
7135
+ return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
7136
+ });
7137
+ return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
7138
+ }
7139
+ function runTelegramNotifySet(opts) {
7140
+ setNotify(opts.stateRoot, opts.project, opts.active);
7141
+ }
7142
+ function runTelegramNotifyPref(args) {
7143
+ const { project, dimension, value, root } = args;
7144
+ if (dimension === "crew") {
7145
+ if (!["all", "alert_only", "done_only", "none"].includes(value))
7146
+ return { ok: false, message: "crew must be all|alert_only|done_only|none" };
7147
+ saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
7148
+ return { ok: true };
7149
+ }
7150
+ if (value !== "on" && value !== "off")
7151
+ return { ok: false, message: "cap must be on|off" };
7152
+ saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
7153
+ return { ok: true };
7154
+ }
7155
+ function runTelegramNotifyStatus(opts) {
7156
+ const s = loadState(opts.stateRoot);
7157
+ const projects = /* @__PURE__ */ new Set();
7158
+ for (const key of Object.keys(s.topics)) {
7159
+ const sep3 = key.indexOf("::");
7160
+ projects.add(sep3 === -1 ? key : key.slice(0, sep3));
7161
+ }
7162
+ for (const p of Object.keys(s.notify))
7163
+ projects.add(p);
7164
+ return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
7165
+ }
7166
+ function capAllowed(project, globalNotify, root) {
7167
+ return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
7168
+ }
7169
+ function confirmationText(project, before, after, dim) {
7170
+ if (dim === "active")
7171
+ return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
7172
+ if (dim === "cap")
7173
+ return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
7174
+ return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
7175
+ }
7176
+ async function runNotifyConfirmation(opts) {
7177
+ const { quieter, dim } = isQuieter(opts.before, opts.after);
7178
+ if (!quieter || dim === null)
7179
+ return false;
7180
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7181
+ if (topicId === void 0)
7182
+ return false;
7183
+ const text = confirmationText(opts.project, opts.before, opts.after, dim);
7184
+ try {
7185
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
7186
+ return true;
7187
+ } catch {
7188
+ console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
7189
+ return false;
7190
+ }
7191
+ }
7192
+ async function runTelegramSend(opts) {
7193
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7194
+ if (topicId === void 0) {
7195
+ throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
7196
+ }
7197
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
7198
+ clearPending(opts.stateRoot, opts.project);
7199
+ return { chatId: opts.cfg.supergroupId, topicId };
7200
+ }
7201
+ async function runTelegramLink(opts) {
7202
+ const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7203
+ if (existing !== void 0)
7204
+ return { topicId: existing, created: false };
7205
+ const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
7206
+ const winner = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7207
+ if (winner !== void 0) {
7208
+ try {
7209
+ await opts.client.deleteForumTopic?.(opts.cfg.supergroupId, topicId);
7210
+ } catch {
7211
+ }
7212
+ return { topicId: winner, created: false };
7213
+ }
7214
+ setTopic(opts.stateRoot, opts.project, topicId);
7215
+ return { topicId, created: true };
7216
+ }
7217
+ var init_notify = __esm({
7218
+ "packages/core/dist/telegram/notify.js"() {
7219
+ init_dist();
7220
+ init_state();
7221
+ init_format();
6593
7222
  }
6594
7223
  });
6595
7224
 
@@ -6677,8 +7306,10 @@ var init_tiers = __esm({
6677
7306
  });
6678
7307
 
6679
7308
  // packages/core/dist/telegram/bridge.js
7309
+ import fs11 from "fs";
6680
7310
  import os5 from "os";
6681
7311
  import path12 from "path";
7312
+ import crypto2 from "crypto";
6682
7313
  function formatInboundReceipt(project, outcome) {
6683
7314
  if (!outcome)
6684
7315
  return void 0;
@@ -6694,6 +7325,85 @@ function formatInboundReceipt(project, outcome) {
6694
7325
  return void 0;
6695
7326
  }
6696
7327
  }
7328
+ function telegramPollLockPath(token) {
7329
+ const hash = crypto2.createHash("sha256").update(token).digest("hex").slice(0, 16);
7330
+ return path12.join(os5.tmpdir(), `squadrant-telegram-poll-${hash}.lock`);
7331
+ }
7332
+ function pidAlive(pid) {
7333
+ try {
7334
+ process.kill(pid, 0);
7335
+ return true;
7336
+ } catch (e) {
7337
+ return e.code === "EPERM";
7338
+ }
7339
+ }
7340
+ function lockIsHeld(file) {
7341
+ let pid;
7342
+ let ageMs;
7343
+ try {
7344
+ const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
7345
+ pid = Number.isFinite(parsed) ? parsed : void 0;
7346
+ ageMs = Date.now() - fs11.statSync(file).mtimeMs;
7347
+ } catch {
7348
+ return false;
7349
+ }
7350
+ if (pid !== void 0)
7351
+ return pidAlive(pid);
7352
+ return ageMs < POLL_LOCK_STALE_MS;
7353
+ }
7354
+ function acquirePollLock(token) {
7355
+ const file = telegramPollLockPath(token);
7356
+ for (let attempt = 0; attempt < 2; attempt++) {
7357
+ try {
7358
+ const fd = fs11.openSync(file, "wx");
7359
+ fs11.writeSync(fd, String(process.pid));
7360
+ fs11.closeSync(fd);
7361
+ return () => {
7362
+ try {
7363
+ fs11.unlinkSync(file);
7364
+ } catch {
7365
+ }
7366
+ };
7367
+ } catch (e) {
7368
+ if (e.code !== "EEXIST")
7369
+ return null;
7370
+ if (attempt === 0 && !lockIsHeld(file)) {
7371
+ try {
7372
+ fs11.unlinkSync(file);
7373
+ } catch {
7374
+ }
7375
+ continue;
7376
+ }
7377
+ return null;
7378
+ }
7379
+ }
7380
+ return null;
7381
+ }
7382
+ function defaultSchedule2(fn, ms) {
7383
+ const t = setTimeout(fn, ms);
7384
+ t.unref?.();
7385
+ return () => clearTimeout(t);
7386
+ }
7387
+ function foreignPollLockHolder(token, selfPid = process.pid) {
7388
+ const file = telegramPollLockPath(token);
7389
+ let pid;
7390
+ try {
7391
+ const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
7392
+ if (!Number.isFinite(parsed))
7393
+ return null;
7394
+ pid = parsed;
7395
+ } catch {
7396
+ return null;
7397
+ }
7398
+ if (pid === selfPid)
7399
+ return null;
7400
+ return pidAlive(pid) ? pid : null;
7401
+ }
7402
+ function pollBackoffMs(err, pollMs) {
7403
+ if (err instanceof TelegramApiError && err.retryAfterSec !== void 0)
7404
+ return err.retryAfterSec * 1e3;
7405
+ return pollMs;
7406
+ }
6697
7407
  function parseNotifyPref(text) {
6698
7408
  const parts = text.trim().split(/\s+/);
6699
7409
  if (stripBotMention(parts[0] ?? "").toLowerCase() !== "/notify")
@@ -6720,9 +7430,17 @@ function notifyToggle(text) {
6720
7430
  }
6721
7431
  function createTelegramBridge(opts) {
6722
7432
  const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
7433
+ const lifecycle = opts.lifecycle;
6723
7434
  const configRoot = opts.configRoot ?? path12.join(os5.homedir(), ".config", "squadrant");
6724
7435
  const pollMs = cfg.pollMs ?? 1e3;
7436
+ const schedule = opts.schedule ?? defaultSchedule2;
7437
+ const lockToken = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
7438
+ let releasePollLock = null;
7439
+ let pollAbort = null;
6725
7440
  let running = false;
7441
+ let pollStopped = false;
7442
+ let selfHealCancel = null;
7443
+ let conflictCount = 0;
6726
7444
  let lastSuccessfulPollAt = null;
6727
7445
  let lastError = null;
6728
7446
  let lastErrorAt = null;
@@ -6732,12 +7450,8 @@ function createTelegramBridge(opts) {
6732
7450
  saveState(stateRoot, s);
6733
7451
  }
6734
7452
  async function sendToTopic(project, text) {
6735
- let threadId = loadState(stateRoot).topics[topicKey(project)];
6736
- if (threadId === void 0) {
6737
- threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
6738
- setTopic(stateRoot, project, threadId);
6739
- }
6740
- await client.sendMessage(cfg.supergroupId, threadId, text);
7453
+ const { topicId } = await runTelegramLink({ project, cfg, client, stateRoot });
7454
+ await client.sendMessage(cfg.supergroupId, topicId, text);
6741
7455
  }
6742
7456
  async function deliverOutbound(project, ev) {
6743
7457
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
@@ -6924,10 +7638,13 @@ ${formatUsageLine(usage2)}`;
6924
7638
  }
6925
7639
  await runChannelCommand(text, fromId, void 0);
6926
7640
  }
6927
- async function handleProjectTopic(text, threadId, fromId) {
7641
+ async function handleProjectTopic(text, threadId, fromId, messageId, media) {
6928
7642
  const resolved = findProjectByThread(stateRoot, threadId);
6929
- if (!resolved)
7643
+ if (!resolved) {
7644
+ log(`telegram inbound in unbound topic thread=${threadId} \u2014 no project linked, message dropped`);
7645
+ await reply(threadId, "\u26A0\uFE0F this topic isn't linked to a project \u2014 run: squadrant telegram link <project>");
6930
7646
  return;
7647
+ }
6931
7648
  if (isBareSpawn(text)) {
6932
7649
  if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
6933
7650
  await reply(threadId, "\u26D4 not authorized");
@@ -6977,21 +7694,16 @@ ${formatUsageLine(usage2)}`;
6977
7694
  await runChannelCommand(text, fromId, threadId);
6978
7695
  return;
6979
7696
  }
6980
- void client.sendChatAction(cfg.supergroupId, threadId, "typing").catch((e) => {
6981
- log(`telegram sendChatAction failed: ${e.message}`);
6982
- });
7697
+ if (messageId !== void 0) {
7698
+ void Promise.resolve(client.setMessageReaction?.(cfg.supergroupId, messageId, "\u{1F44D}")).catch((e) => log(`telegram stage-1 reaction failed project=${resolved.project}: ${e.message}`));
7699
+ }
6983
7700
  setNotify(stateRoot, resolved.project, true);
6984
7701
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
6985
- try {
6986
- const r = await ensureCaptainAlive(resolved.project);
7702
+ void ensureCaptainAlive(resolved.project).then((r) => {
6987
7703
  if (r === "timeout") {
6988
- await reply(threadId, `\u274C couldn't reach ${resolved.project} captain \u2014 saved to mailbox, will deliver when you open the workspace.`);
6989
- } else {
6990
- await reply(threadId, `\u{1F4E8} delivered to ${resolved.project} captain`);
7704
+ log(`telegram auto-launch: warmup timed out project=${resolved.project} \u2014 delivery verdict (not this probe) decides the receipt`);
6991
7705
  }
6992
- } catch (e) {
6993
- log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
6994
- }
7706
+ }).catch((e) => log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`));
6995
7707
  }
6996
7708
  let handled = false;
6997
7709
  let outcome;
@@ -7000,13 +7712,22 @@ ${formatUsageLine(usage2)}`;
7000
7712
  handled = res.handled;
7001
7713
  outcome = res.outcome;
7002
7714
  }
7715
+ const deliveryHeld = outcome?.status === "held";
7003
7716
  if (!handled) {
7004
7717
  await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
7718
+ outcome = void 0;
7005
7719
  }
7006
7720
  const receipt = formatInboundReceipt(resolved.project, outcome);
7007
7721
  if (receipt && sendReply) {
7008
7722
  await sendReply(threadId, receipt).catch((e) => log(`telegram receipt failed project=${resolved.project}: ${e.message}`));
7009
7723
  }
7724
+ if (media) {
7725
+ await reply(threadId, formatMediaReceipt(media.kind, media.hasCaption));
7726
+ }
7727
+ if (deliveryHeld)
7728
+ return;
7729
+ await reply(threadId, CAPTAIN_RECEIVED_ACK);
7730
+ lifecycle?.begin(resolved.project, threadId);
7010
7731
  }
7011
7732
  async function handleUpdate(u) {
7012
7733
  if (u.callback_query) {
@@ -7014,7 +7735,11 @@ ${formatUsageLine(usage2)}`;
7014
7735
  return;
7015
7736
  }
7016
7737
  const m = u.message;
7017
- if (!m || m.text === void 0)
7738
+ if (!m)
7739
+ return;
7740
+ const kind = mediaKind(m);
7741
+ const text = m.text ?? m.caption;
7742
+ if (text === void 0 && kind === void 0)
7018
7743
  return;
7019
7744
  if (!cfg.chats.includes(m.chat.id))
7020
7745
  return;
@@ -7028,7 +7753,7 @@ ${formatUsageLine(usage2)}`;
7028
7753
  await reply(threadId, "\u26D4 not authorized");
7029
7754
  return;
7030
7755
  }
7031
- const task = m.text.trim();
7756
+ const task = (text ?? "").trim();
7032
7757
  if (!task) {
7033
7758
  await reply(threadId, "spawn cancelled \u2014 empty task");
7034
7759
  return;
@@ -7039,39 +7764,122 @@ ${formatUsageLine(usage2)}`;
7039
7764
  return;
7040
7765
  }
7041
7766
  if (m.message_thread_id === void 0) {
7767
+ if (m.text === void 0)
7768
+ return;
7042
7769
  await handleGeneral(m.text, m.from?.id);
7043
7770
  return;
7044
7771
  }
7045
- await handleProjectTopic(m.text, m.message_thread_id, m.from?.id);
7772
+ await handleProjectTopic(inboundBody(text, kind), m.message_thread_id, m.from?.id, m.message_id, kind === void 0 ? void 0 : { kind, hasCaption: text !== void 0 });
7773
+ }
7774
+ function acquirePollLockOrLog() {
7775
+ if (!lockToken)
7776
+ return true;
7777
+ releasePollLock = acquirePollLock(lockToken);
7778
+ if (releasePollLock)
7779
+ return true;
7780
+ const holder = foreignPollLockHolder(lockToken);
7781
+ log(`telegram inbound poll disabled: another consumer already polls this bot token (single-consumer guard, #830)${holder !== null ? ` \u2014 lock held by pid ${holder}` : ""}`);
7782
+ return false;
7783
+ }
7784
+ function scheduleSelfHeal() {
7785
+ selfHealCancel?.();
7786
+ selfHealCancel = schedule(() => {
7787
+ selfHealCancel = null;
7788
+ if (pollStopped || running)
7789
+ return;
7790
+ if (!acquirePollLockOrLog()) {
7791
+ scheduleSelfHeal();
7792
+ return;
7793
+ }
7794
+ running = true;
7795
+ conflictCount = 0;
7796
+ log("telegram inbound poll re-attempting after a 409 stop (#850)");
7797
+ void pollLoop();
7798
+ }, CONFLICT_409_SELF_HEAL_MS);
7046
7799
  }
7047
7800
  async function pollLoop() {
7048
7801
  while (running) {
7802
+ const ac = new AbortController();
7803
+ pollAbort = ac;
7804
+ let waitMs = pollMs;
7049
7805
  try {
7050
7806
  const offset = loadState(stateRoot).offset;
7051
- const updates = await client.getUpdates(offset, LONG_POLL_SEC);
7807
+ const updates = await client.getUpdates(offset, LONG_POLL_SEC, ac.signal);
7808
+ conflictCount = 0;
7052
7809
  for (const u of updates) {
7053
7810
  await handleUpdate(u);
7054
7811
  persistOffset(u.update_id + 1);
7055
7812
  }
7056
7813
  lastSuccessfulPollAt = Date.now();
7057
7814
  } catch (e) {
7058
- lastError = e.message;
7815
+ if (ac.signal.aborted)
7816
+ break;
7817
+ const msg = e.message;
7818
+ lastError = msg;
7059
7819
  lastErrorAt = Date.now();
7060
- log(`telegram inbound poll failed: ${e.message}`);
7820
+ if (e instanceof TelegramApiError && e.code === 409) {
7821
+ conflictCount += 1;
7822
+ if (conflictCount >= CONFLICT_409_TERMINAL_AFTER) {
7823
+ log(`telegram inbound poll stopped: repeated 409 conflict (another getUpdates consumer holds this token) \u2014 ${msg}`);
7824
+ break;
7825
+ }
7826
+ } else {
7827
+ log(`telegram inbound poll failed: ${msg}`);
7828
+ }
7829
+ waitMs = pollBackoffMs(e, pollMs);
7830
+ } finally {
7831
+ pollAbort = null;
7061
7832
  }
7062
7833
  if (running)
7063
- await sleep2(pollMs);
7834
+ await sleep2(waitMs);
7064
7835
  }
7836
+ running = false;
7837
+ releasePollLock?.();
7838
+ releasePollLock = null;
7839
+ if (!pollStopped)
7840
+ scheduleSelfHeal();
7065
7841
  }
7066
7842
  return {
7067
7843
  start() {
7068
7844
  if (running)
7069
7845
  return;
7846
+ pollStopped = false;
7847
+ selfHealCancel?.();
7848
+ selfHealCancel = null;
7849
+ if (!acquirePollLockOrLog())
7850
+ return;
7851
+ const known = opts.registeredProjects?.();
7852
+ if (known !== void 0) {
7853
+ try {
7854
+ const removed = pruneTopics(stateRoot, (p) => known.includes(p));
7855
+ if (removed.length > 0)
7856
+ log(`telegram registry pruned ${removed.length} stale topic link(s): ${removed.join(", ")}`);
7857
+ } catch (e) {
7858
+ log(`telegram registry prune failed: ${e.message}`);
7859
+ }
7860
+ }
7070
7861
  running = true;
7862
+ conflictCount = 0;
7863
+ try {
7864
+ lifecycle?.start();
7865
+ } catch (e) {
7866
+ log(`telegram lifecycle start failed: ${e.message}`);
7867
+ }
7071
7868
  void pollLoop();
7072
7869
  },
7073
7870
  stop() {
7074
7871
  running = false;
7872
+ pollStopped = true;
7873
+ selfHealCancel?.();
7874
+ selfHealCancel = null;
7875
+ try {
7876
+ lifecycle?.stop();
7877
+ } catch (e) {
7878
+ log(`telegram lifecycle stop failed: ${e.message}`);
7879
+ }
7880
+ pollAbort?.abort();
7881
+ releasePollLock?.();
7882
+ releasePollLock = null;
7075
7883
  },
7076
7884
  pushLifecycle(project, ev) {
7077
7885
  void deliverOutbound(project, ev).catch((e) => {
@@ -7088,17 +7896,23 @@ ${formatUsageLine(usage2)}`;
7088
7896
  }
7089
7897
  };
7090
7898
  }
7091
- var LONG_POLL_SEC, sleep2, CREW_TIERS, RECOGNIZED_CHANNEL_COMMANDS;
7899
+ var CAPTAIN_RECEIVED_ACK, LONG_POLL_SEC, POLL_LOCK_STALE_MS, CONFLICT_409_TERMINAL_AFTER, CONFLICT_409_SELF_HEAL_MS, sleep2, CREW_TIERS, RECOGNIZED_CHANNEL_COMMANDS;
7092
7900
  var init_bridge = __esm({
7093
7901
  "packages/core/dist/telegram/bridge.js"() {
7094
7902
  init_dist();
7903
+ init_client();
7095
7904
  init_auth();
7096
7905
  init_commands();
7097
7906
  init_format();
7907
+ init_notify();
7098
7908
  init_panels();
7099
7909
  init_state();
7100
7910
  init_tiers();
7911
+ CAPTAIN_RECEIVED_ACK = "\u2705 captain received";
7101
7912
  LONG_POLL_SEC = 50;
7913
+ POLL_LOCK_STALE_MS = 2 * 6e4;
7914
+ CONFLICT_409_TERMINAL_AFTER = 3;
7915
+ CONFLICT_409_SELF_HEAL_MS = 5 * 6e4;
7102
7916
  sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
7103
7917
  CREW_TIERS = ["all", "alert_only", "done_only", "none"];
7104
7918
  RECOGNIZED_CHANNEL_COMMANDS = /* @__PURE__ */ new Set(["status", "projects", "crews", "launch", "effort", "spawn"]);
@@ -7143,7 +7957,7 @@ var init_restart_daemon = __esm({
7143
7957
  });
7144
7958
 
7145
7959
  // packages/core/dist/telegram/setup.js
7146
- import fs11 from "fs";
7960
+ import fs12 from "fs";
7147
7961
  function resolveSetupGroup(existingSupergroupId, opts) {
7148
7962
  if (existingSupergroupId !== void 0 && !opts.redetect)
7149
7963
  return "reuse";
@@ -7194,130 +8008,42 @@ function writeTelegramConfig(configPath, opts) {
7194
8008
  let config;
7195
8009
  let raw = null;
7196
8010
  try {
7197
- raw = fs11.readFileSync(configPath, "utf-8");
8011
+ raw = fs12.readFileSync(configPath, "utf-8");
7198
8012
  } catch (err) {
7199
8013
  if (err.code !== "ENOENT") {
7200
8014
  throw new Error(`refusing to overwrite unreadable config at ${configPath}: ${String(err)}`);
7201
- }
7202
- }
7203
- if (raw !== null) {
7204
- try {
7205
- config = JSON.parse(raw);
7206
- } catch (err) {
7207
- throw new Error(`refusing to overwrite corrupt config at ${configPath}: ${String(err)}`);
7208
- }
7209
- } else {
7210
- config = {};
7211
- }
7212
- const prev = config.telegram && typeof config.telegram === "object" ? config.telegram : {};
7213
- const next = {
7214
- botToken: opts.token,
7215
- supergroupId: opts.supergroupId,
7216
- chats: [opts.supergroupId]
7217
- };
7218
- const users = opts.users ?? prev.users;
7219
- const remoteControl = opts.remoteControl ?? prev.remoteControl;
7220
- if (users !== void 0)
7221
- next.users = users;
7222
- if (remoteControl !== void 0)
7223
- next.remoteControl = remoteControl;
7224
- config.telegram = next;
7225
- writeConfigFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7226
- }
7227
- var init_setup = __esm({
7228
- "packages/core/dist/telegram/setup.js"() {
7229
- init_dist();
7230
- init_state();
7231
- init_bot_commands();
7232
- init_restart_daemon();
7233
- }
7234
- });
7235
-
7236
- // packages/core/dist/telegram/notify.js
7237
- function runTelegramStatus(opts) {
7238
- const tg = opts.config.telegram;
7239
- const env = opts.env ?? process.env;
7240
- const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
7241
- const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
7242
- const sep3 = key.indexOf("::");
7243
- return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
7244
- });
7245
- return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
7246
- }
7247
- function runTelegramNotifySet(opts) {
7248
- setNotify(opts.stateRoot, opts.project, opts.active);
7249
- }
7250
- function runTelegramNotifyPref(args) {
7251
- const { project, dimension, value, root } = args;
7252
- if (dimension === "crew") {
7253
- if (!["all", "alert_only", "done_only", "none"].includes(value))
7254
- return { ok: false, message: "crew must be all|alert_only|done_only|none" };
7255
- saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
7256
- return { ok: true };
7257
- }
7258
- if (value !== "on" && value !== "off")
7259
- return { ok: false, message: "cap must be on|off" };
7260
- saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
7261
- return { ok: true };
7262
- }
7263
- function runTelegramNotifyStatus(opts) {
7264
- const s = loadState(opts.stateRoot);
7265
- const projects = /* @__PURE__ */ new Set();
7266
- for (const key of Object.keys(s.topics)) {
7267
- const sep3 = key.indexOf("::");
7268
- projects.add(sep3 === -1 ? key : key.slice(0, sep3));
7269
- }
7270
- for (const p of Object.keys(s.notify))
7271
- projects.add(p);
7272
- return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
7273
- }
7274
- function capAllowed(project, globalNotify, root) {
7275
- return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
7276
- }
7277
- function confirmationText(project, before, after, dim) {
7278
- if (dim === "active")
7279
- return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
7280
- if (dim === "cap")
7281
- return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
7282
- return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
7283
- }
7284
- async function runNotifyConfirmation(opts) {
7285
- const { quieter, dim } = isQuieter(opts.before, opts.after);
7286
- if (!quieter || dim === null)
7287
- return false;
7288
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7289
- if (topicId === void 0)
7290
- return false;
7291
- const text = confirmationText(opts.project, opts.before, opts.after, dim);
7292
- try {
7293
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
7294
- return true;
7295
- } catch {
7296
- console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
7297
- return false;
7298
- }
7299
- }
7300
- async function runTelegramSend(opts) {
7301
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7302
- if (topicId === void 0) {
7303
- throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
8015
+ }
7304
8016
  }
7305
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
7306
- return { chatId: opts.cfg.supergroupId, topicId };
7307
- }
7308
- async function runTelegramLink(opts) {
7309
- const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7310
- if (existing !== void 0)
7311
- return { topicId: existing, created: false };
7312
- const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
7313
- setTopic(opts.stateRoot, opts.project, topicId);
7314
- return { topicId, created: true };
8017
+ if (raw !== null) {
8018
+ try {
8019
+ config = JSON.parse(raw);
8020
+ } catch (err) {
8021
+ throw new Error(`refusing to overwrite corrupt config at ${configPath}: ${String(err)}`);
8022
+ }
8023
+ } else {
8024
+ config = {};
8025
+ }
8026
+ const prev = config.telegram && typeof config.telegram === "object" ? config.telegram : {};
8027
+ const next = {
8028
+ botToken: opts.token,
8029
+ supergroupId: opts.supergroupId,
8030
+ chats: [opts.supergroupId]
8031
+ };
8032
+ const users = opts.users ?? prev.users;
8033
+ const remoteControl = opts.remoteControl ?? prev.remoteControl;
8034
+ if (users !== void 0)
8035
+ next.users = users;
8036
+ if (remoteControl !== void 0)
8037
+ next.remoteControl = remoteControl;
8038
+ config.telegram = next;
8039
+ writeConfigFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7315
8040
  }
7316
- var init_notify = __esm({
7317
- "packages/core/dist/telegram/notify.js"() {
8041
+ var init_setup = __esm({
8042
+ "packages/core/dist/telegram/setup.js"() {
7318
8043
  init_dist();
7319
8044
  init_state();
7320
- init_format();
8045
+ init_bot_commands();
8046
+ init_restart_daemon();
7321
8047
  }
7322
8048
  });
7323
8049
 
@@ -7332,6 +8058,7 @@ var init_telegram = __esm({
7332
8058
  init_format();
7333
8059
  init_state();
7334
8060
  init_client();
8061
+ init_inbound_lifecycle();
7335
8062
  init_bridge();
7336
8063
  init_setup();
7337
8064
  init_notify();
@@ -7526,6 +8253,26 @@ function writeJson(res, status, body) {
7526
8253
  res.writeHead(status, { "content-type": "application/json" });
7527
8254
  res.end(JSON.stringify(body));
7528
8255
  }
8256
+ function pathnameOf(url) {
8257
+ if (!url)
8258
+ return "";
8259
+ const q = url.indexOf("?");
8260
+ return q === -1 ? url : url.slice(0, q);
8261
+ }
8262
+ function modelsList(ids) {
8263
+ const data = ids.map((id) => ({
8264
+ type: "model",
8265
+ id,
8266
+ display_name: id,
8267
+ created_at: "1970-01-01T00:00:00Z"
8268
+ }));
8269
+ return {
8270
+ data,
8271
+ has_more: false,
8272
+ first_id: ids[0] ?? null,
8273
+ last_id: ids[ids.length - 1] ?? null
8274
+ };
8275
+ }
7529
8276
  function buildUpstreamHeaders(req, upstream, stream) {
7530
8277
  const headers = {
7531
8278
  "content-type": "application/json",
@@ -7554,6 +8301,7 @@ function createRouterShim(opts) {
7554
8301
  let lastError;
7555
8302
  let upstreamReachable = true;
7556
8303
  const upstreamUrl = joinUrl(opts.upstream.baseUrl, "/v1/messages");
8304
+ const advertisedModels = new Set(opts.models ?? []);
7557
8305
  function emitUsage(u) {
7558
8306
  try {
7559
8307
  opts.onUsage?.(u);
@@ -7583,6 +8331,8 @@ function createRouterShim(opts) {
7583
8331
  const body = parsed;
7584
8332
  const wantsStream = body.stream === true;
7585
8333
  const model = typeof body.model === "string" ? body.model : void 0;
8334
+ if (model)
8335
+ advertisedModels.add(model);
7586
8336
  const sanitized = sanitizeRequest(body, opts.upstream);
7587
8337
  let upstreamRes;
7588
8338
  try {
@@ -7655,8 +8405,20 @@ function createRouterShim(opts) {
7655
8405
  emitUsage(usage2);
7656
8406
  writeJson(res, upstreamRes.status, upstreamBody);
7657
8407
  }
8408
+ async function handleCountTokens(req, res) {
8409
+ let text;
8410
+ try {
8411
+ text = await readBody(req);
8412
+ } catch {
8413
+ const e = anthropicError(400, "invalid_request_error", "malformed request body");
8414
+ writeJson(res, e.status, e.body);
8415
+ return;
8416
+ }
8417
+ writeJson(res, 200, { input_tokens: Math.max(1, Math.ceil(text.length / 4)) });
8418
+ }
7658
8419
  function handler(req, res) {
7659
- if (req.method === "POST" && req.url === "/v1/messages") {
8420
+ const path39 = pathnameOf(req.url);
8421
+ if (req.method === "POST" && path39 === "/v1/messages") {
7660
8422
  void handleMessages(req, res).catch((err) => {
7661
8423
  log(`router internal error: ${err instanceof Error ? err.message : String(err)}`);
7662
8424
  if (!res.headersSent) {
@@ -7668,7 +8430,18 @@ function createRouterShim(opts) {
7668
8430
  });
7669
8431
  return;
7670
8432
  }
7671
- if (req.method === "GET" && req.url === "/healthz") {
8433
+ if (req.method === "POST" && path39 === "/v1/messages/count_tokens") {
8434
+ void handleCountTokens(req, res).catch(() => {
8435
+ if (!res.headersSent)
8436
+ writeJson(res, 502, anthropicError(502, "api_error", "router internal error").body);
8437
+ });
8438
+ return;
8439
+ }
8440
+ if (req.method === "GET" && path39 === "/v1/models") {
8441
+ writeJson(res, 200, modelsList([...advertisedModels]));
8442
+ return;
8443
+ }
8444
+ if (req.method === "GET" && path39 === "/healthz") {
7672
8445
  writeJson(res, 200, { ready: server?.listening === true, upstreamReachable, lastError });
7673
8446
  return;
7674
8447
  }
@@ -7793,12 +8566,17 @@ function createRouterService(router, projects, deps = {}) {
7793
8566
  tokenByProject.set(p, t);
7794
8567
  projectTokens.set(t, p);
7795
8568
  }
8569
+ const models = [
8570
+ ...deps.models ?? [],
8571
+ ...Object.values(router.models ?? {}).map((m) => m.upstream)
8572
+ ];
7796
8573
  const ledger = createUsageLedger();
7797
8574
  const shim = createRouterShim({
7798
8575
  upstream,
7799
8576
  projectTokens,
7800
8577
  port: router.port ?? 0,
7801
8578
  log,
8579
+ models,
7802
8580
  onUsage: (u) => ledger.record(u),
7803
8581
  ...deps.fetch ? { fetch: deps.fetch } : {}
7804
8582
  });
@@ -7861,12 +8639,53 @@ var init_router = __esm({
7861
8639
  }
7862
8640
  });
7863
8641
 
8642
+ // packages/core/dist/captain-router.js
8643
+ async function prepareCaptainRoute(o) {
8644
+ assertBackendUsable({ backend: o.backend, agent: o.agentName, router: o.config.defaults.router });
8645
+ if (o.backend === "native") {
8646
+ return { backend: "native", model: o.model, permissionMode: o.configuredPermissionMode, env: {} };
8647
+ }
8648
+ if (!o.deps.routerCredentials) {
8649
+ throw new Error(`captain launch: backend '${o.backend}' requires router credentials from the daemon, but this launch path has no credentials provider`);
8650
+ }
8651
+ if (!o.deps.writeRouterSettings) {
8652
+ 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`);
8653
+ }
8654
+ const model = resolveRouterModel(o.model, o.agentName, o.config.defaults.router);
8655
+ const creds = await o.deps.routerCredentials({ project: o.project, backend: o.backend });
8656
+ const routerEnv = buildRouterEnv(creds, model);
8657
+ const shadowed = claudeEnvShadowsRouter(o.config.defaults.claudeEnv);
8658
+ if (shadowed.length > 0) {
8659
+ 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).`);
8660
+ }
8661
+ const settingsPath = o.deps.writeRouterSettings({
8662
+ stateRoot: o.stateRoot,
8663
+ project: o.project,
8664
+ taskId: "captain",
8665
+ env: mergeClaudeEnvRouterSettings(routerEnv, o.config.defaults.claudeEnv)
8666
+ });
8667
+ return {
8668
+ backend: o.backend,
8669
+ model,
8670
+ permissionMode: "default",
8671
+ env: { ...routerEnv, SQUADRANT_GATE: "on" },
8672
+ settingsPath
8673
+ };
8674
+ }
8675
+ var init_captain_router = __esm({
8676
+ "packages/core/dist/captain-router.js"() {
8677
+ init_dist();
8678
+ init_router_resolution();
8679
+ init_env();
8680
+ }
8681
+ });
8682
+
7864
8683
  // packages/core/dist/permission-gate.js
7865
8684
  import { createHash } from "crypto";
7866
8685
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
7867
8686
  import { dirname as dirname5, join as join15 } from "path";
7868
8687
  function isGateSession(env) {
7869
- return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1";
8688
+ return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1" || env.SQUADRANT_ROLE === "captain";
7870
8689
  }
7871
8690
  function resolveGateMode(env, gate) {
7872
8691
  const fromEnv = env.SQUADRANT_GATE;
@@ -8062,7 +8881,15 @@ async function callClassifier(o) {
8062
8881
  return "ask";
8063
8882
  }
8064
8883
  const data = await res.json();
8884
+ if (data?.stop_reason === "max_tokens") {
8885
+ o.log?.("gate: classifier exhausted max_tokens before a verdict \u2014 asking");
8886
+ return "ask";
8887
+ }
8065
8888
  const text = Array.isArray(data?.content) ? data.content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ") : "";
8889
+ if (!text.trim()) {
8890
+ o.log?.("gate: classifier returned no text verdict \u2014 asking");
8891
+ return "ask";
8892
+ }
8066
8893
  return parseClassifierVerdict(text);
8067
8894
  } catch (e) {
8068
8895
  o.log?.(`gate: classifier unavailable (${e.message}) \u2014 asking`);
@@ -8081,7 +8908,7 @@ ${toolPayload}`).digest("hex");
8081
8908
  }
8082
8909
  async function evaluatePermissionRequest(input) {
8083
8910
  if (!isGateSession(input.env)) {
8084
- return { decision: "yield", reason: "not a squadrant crew/side session" };
8911
+ return { decision: "yield", reason: "not a squadrant crew/side/captain session" };
8085
8912
  }
8086
8913
  const gate = input.config.defaults.gate;
8087
8914
  const mode = resolveGateMode(input.env, gate);
@@ -8167,7 +8994,7 @@ var init_permission_gate = __esm({
8167
8994
  DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
8168
8995
  DEFAULT_CACHE_MAX_ENTRIES = 500;
8169
8996
  DEFAULT_CLASSIFIER_TIMEOUT_MS = 5e3;
8170
- CLASSIFIER_MAX_TOKENS = 8;
8997
+ CLASSIFIER_MAX_TOKENS = 256;
8171
8998
  MAX_PAYLOAD_CHARS = 2e3;
8172
8999
  DEFAULT_GATE_DENY_RULES = [
8173
9000
  // Flag-order-agnostic: both recursive and force must be present, in any order,
@@ -8497,7 +9324,7 @@ var init_launch_workspace = __esm({
8497
9324
  });
8498
9325
 
8499
9326
  // packages/core/dist/side-session.js
8500
- import fs12 from "fs";
9327
+ import fs13 from "fs";
8501
9328
  function sideTitleFor(project, name) {
8502
9329
  return `\u{1F5D2} ${project}:${name}`;
8503
9330
  }
@@ -8599,7 +9426,7 @@ async function runSideClose(runtime, workspaceId, project, name, projPath, workt
8599
9426
  await runtime.closePane(pane);
8600
9427
  if (projPath) {
8601
9428
  const wtPath = worktreePath(projPath, worktreeDir, project, name);
8602
- if (fs12.existsSync(wtPath)) {
9429
+ if (fs13.existsSync(wtPath)) {
8603
9430
  try {
8604
9431
  removeWorktree(projPath, wtPath);
8605
9432
  } catch (e) {
@@ -9036,8 +9863,10 @@ __export(dist_exports2, {
9036
9863
  AGENT_BINS: () => AGENT_BINS,
9037
9864
  ANTHROPIC_ONLY_REQUEST_FIELDS: () => ANTHROPIC_ONLY_REQUEST_FIELDS,
9038
9865
  BOT_COMMANDS: () => BOT_COMMANDS,
9866
+ CAPTAIN_RECEIVED_ACK: () => CAPTAIN_RECEIVED_ACK,
9039
9867
  CC_SOCKS_DIR: () => CC_SOCKS_DIR,
9040
9868
  CMUX_PRESERVE_CLAUDE_AUTH_ENV: () => CMUX_PRESERVE_CLAUDE_AUTH_ENV,
9869
+ CONFLICT_409_SELF_HEAL_MS: () => CONFLICT_409_SELF_HEAL_MS,
9041
9870
  CREW_GONE_MS: () => CREW_GONE_MS,
9042
9871
  CREW_STALE_MS: () => CREW_STALE_MS,
9043
9872
  CaptainDelivery: () => CaptainDelivery,
@@ -9057,6 +9886,7 @@ __export(dist_exports2, {
9057
9886
  LivenessRegistry: () => LivenessRegistry,
9058
9887
  MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
9059
9888
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
9889
+ PANE_IDLE_AFTER_MS: () => PANE_IDLE_AFTER_MS,
9060
9890
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
9061
9891
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
9062
9892
  READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
@@ -9069,6 +9899,10 @@ __export(dist_exports2, {
9069
9899
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
9070
9900
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
9071
9901
  TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
9902
+ TYPING_KEEPALIVE_MS: () => TYPING_KEEPALIVE_MS,
9903
+ TelegramApiError: () => TelegramApiError,
9904
+ WATCHDOG_GRACE_MS: () => WATCHDOG_GRACE_MS,
9905
+ WATCHDOG_MS: () => WATCHDOG_MS,
9072
9906
  WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
9073
9907
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
9074
9908
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
@@ -9090,14 +9924,19 @@ __export(dist_exports2, {
9090
9924
  callClassifier: () => callClassifier,
9091
9925
  capAllowed: () => capAllowed,
9092
9926
  capOutput: () => capOutput,
9927
+ captainAliveStateFromHealth: () => captainAliveStateFromHealth,
9093
9928
  captainRecordPath: () => captainRecordPath,
9094
9929
  captainSocketPath: () => captainSocketPath,
9095
9930
  checkFact: () => checkFact,
9931
+ classifyCaptainPane: () => classifyCaptainPane,
9096
9932
  classifyHealth: () => classifyHealth,
9933
+ claudeEnvShadowsRouter: () => claudeEnvShadowsRouter,
9934
+ clearPending: () => clearPending,
9097
9935
  closeWorkItem: () => closeWorkItem,
9098
9936
  computeTemplateHash: () => computeTemplateHash,
9099
9937
  consumeExitMarker: () => consumeExitMarker,
9100
9938
  createAttach: () => createAttach,
9939
+ createCaptainPaneReader: () => createCaptainPaneReader,
9101
9940
  createCrewPaneReader: () => createCrewPaneReader,
9102
9941
  createDaemon: () => createDaemon,
9103
9942
  createDecoder: () => createDecoder,
@@ -9106,6 +9945,7 @@ __export(dist_exports2, {
9106
9945
  createDirectSurfaceLivenessProbe: () => createDirectSurfaceLivenessProbe,
9107
9946
  createEnsureCaptainAlive: () => createEnsureCaptainAlive,
9108
9947
  createEventsSource: () => createEventsSource,
9948
+ createInboundLifecycle: () => createInboundLifecycle,
9109
9949
  createInteractiveProbe: () => createInteractiveProbe,
9110
9950
  createIsCaptainAlive: () => createIsCaptainAlive,
9111
9951
  createLaunch: () => createLaunch,
@@ -9161,12 +10001,16 @@ __export(dist_exports2, {
9161
10001
  formatInbound: () => formatInbound,
9162
10002
  formatInboundReceipt: () => formatInboundReceipt,
9163
10003
  formatLifecycle: () => formatLifecycle,
10004
+ formatMediaReceipt: () => formatMediaReceipt,
9164
10005
  formatPermissionDecision: () => formatPermissionDecision,
9165
10006
  formatUsageLine: () => formatUsageLine,
10007
+ formatWatchdogWarning: () => formatWatchdogWarning,
9166
10008
  freshTrace: () => freshTrace,
9167
10009
  gateCacheKey: () => gateCacheKey,
9168
10010
  getDaemonPid: () => getDaemonPid,
10011
+ hasModalOptionList: () => hasModalOptionList,
9169
10012
  healCmdFor: () => healCmdFor,
10013
+ inboundBody: () => inboundBody,
9170
10014
  isAuthorized: () => isAuthorized,
9171
10015
  isBareSpawn: () => isBareSpawn,
9172
10016
  isCaptainAlive: () => isCaptainAlive,
@@ -9188,17 +10032,23 @@ __export(dist_exports2, {
9188
10032
  kickstartArgv: () => kickstartArgv,
9189
10033
  launchOneWorkspace: () => launchOneWorkspace,
9190
10034
  listSessions: () => listSessions,
10035
+ loadPending: () => loadPending,
9191
10036
  loadSessions: () => loadSessions,
9192
10037
  loadState: () => loadState,
9193
10038
  mailboxStats: () => mailboxStats,
9194
10039
  makeGate: () => makeGate,
10040
+ markPendingWarned: () => markPendingWarned,
9195
10041
  maskToken: () => maskToken,
9196
10042
  matchTier1Deny: () => matchTier1Deny,
10043
+ mediaKind: () => mediaKind,
10044
+ mediaMarker: () => mediaMarker,
10045
+ mergeClaudeEnvRouterSettings: () => mergeClaudeEnvRouterSettings,
9197
10046
  mintToken: () => mintToken,
9198
10047
  nameFromTitle: () => nameFromTitle,
9199
10048
  newestSessionInDirectory: () => newestSessionInDirectory,
9200
10049
  nextAutoName: () => nextAutoName,
9201
10050
  niceCrewCommand: () => niceCrewCommand,
10051
+ notePaneScreen: () => notePaneScreen,
9202
10052
  notifyToggle: () => notifyToggle,
9203
10053
  parseBearer: () => parseBearer,
9204
10054
  parseClassifierVerdict: () => parseClassifierVerdict,
@@ -9207,12 +10057,15 @@ __export(dist_exports2, {
9207
10057
  parseNotifyPref: () => parseNotifyPref,
9208
10058
  parseProgramArgs: () => parseProgramArgs,
9209
10059
  plistPath: () => plistPath,
10060
+ pollBackoffMs: () => pollBackoffMs,
9210
10061
  pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
10062
+ prepareCaptainRoute: () => prepareCaptainRoute,
9211
10063
  printForeignInstallError: () => printForeignInstallError,
9212
10064
  printVersionUpgradeNotice: () => printVersionUpgradeNotice,
9213
10065
  probeCaptainChannel: () => probeCaptainChannel,
9214
10066
  programArgsBlock: () => programArgsBlock,
9215
10067
  projectHealth: () => projectHealth,
10068
+ pruneTopics: () => pruneTopics,
9216
10069
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
9217
10070
  readCaptainAddress: () => readCaptainAddress,
9218
10071
  readCursor: () => readCursor,
@@ -9279,9 +10132,11 @@ __export(dist_exports2, {
9279
10132
  saveSessions: () => saveSessions,
9280
10133
  saveState: () => saveState,
9281
10134
  screenHasSplashMarker: () => screenHasSplashMarker,
10135
+ screenHash: () => screenHash,
9282
10136
  sendRequest: () => sendRequest,
9283
10137
  setLastUserId: () => setLastUserId,
9284
10138
  setNotify: () => setNotify,
10139
+ setPending: () => setPending,
9285
10140
  setTopic: () => setTopic,
9286
10141
  shellQuote: () => shellQuote,
9287
10142
  shouldBuildRouterService: () => shouldBuildRouterService,
@@ -9294,6 +10149,7 @@ __export(dist_exports2, {
9294
10149
  startServer: () => startServer,
9295
10150
  stripBotMention: () => stripBotMention,
9296
10151
  surfaceVerdict: () => surfaceVerdict,
10152
+ telegramPollLockPath: () => telegramPollLockPath,
9297
10153
  timeoutGate: () => timeoutGate,
9298
10154
  titleFor: () => titleFor,
9299
10155
  toControlEvent: () => toControlEvent,
@@ -9340,6 +10196,7 @@ var init_dist2 = __esm({
9340
10196
  init_router();
9341
10197
  init_crew_routing();
9342
10198
  init_router_resolution();
10199
+ init_captain_router();
9343
10200
  init_permission_gate();
9344
10201
  init_restart_daemon();
9345
10202
  init_group_dispatch();
@@ -9362,7 +10219,7 @@ var init_dist2 = __esm({
9362
10219
  });
9363
10220
 
9364
10221
  // packages/agents/dist/claude/registry.js
9365
- import fs13 from "fs";
10222
+ import fs14 from "fs";
9366
10223
  import { join as join17 } from "path";
9367
10224
  import { homedir as homedir7 } from "os";
9368
10225
  function parseRegistryDir(files, readFile7) {
@@ -9407,11 +10264,11 @@ function readClaudeStatus(task) {
9407
10264
  return void 0;
9408
10265
  let files;
9409
10266
  try {
9410
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
10267
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
9411
10268
  } catch {
9412
10269
  return void 0;
9413
10270
  }
9414
- const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
10271
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
9415
10272
  let entry;
9416
10273
  if (task.messagingSocketPath) {
9417
10274
  entry = entries.find((e) => e.messagingSocketPath === task.messagingSocketPath);
@@ -9426,11 +10283,11 @@ function readClaudeStatus(task) {
9426
10283
  function readClaudeStatusByCwd(cwd) {
9427
10284
  let files;
9428
10285
  try {
9429
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
10286
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
9430
10287
  } catch {
9431
10288
  return void 0;
9432
10289
  }
9433
- const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
10290
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
9434
10291
  const entry = entries.find((e) => e.cwd === cwd);
9435
10292
  if (!entry)
9436
10293
  return void 0;
@@ -9439,11 +10296,11 @@ function readClaudeStatusByCwd(cwd) {
9439
10296
  function readClaudeStatusBySocketPath(socketPath2) {
9440
10297
  let files;
9441
10298
  try {
9442
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
10299
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
9443
10300
  } catch {
9444
10301
  return void 0;
9445
10302
  }
9446
- const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
10303
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
9447
10304
  const entry = entries.find((e) => e.messagingSocketPath === socketPath2);
9448
10305
  if (!entry)
9449
10306
  return void 0;
@@ -9866,9 +10723,9 @@ var init_registry2 = __esm({
9866
10723
  });
9867
10724
 
9868
10725
  // packages/agents/dist/drivers/launch-cmd.js
9869
- import fs14 from "fs";
10726
+ import fs15 from "fs";
9870
10727
  import path13 from "path";
9871
- function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName, thinking, captainBoot) {
10728
+ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName, thinking, captainBoot, settingsPath) {
9872
10729
  const driver = registry.getDriver(agentName);
9873
10730
  if (driver.name === "claude") {
9874
10731
  let cmd = fresh ? "claude" : "claude -c";
@@ -9881,6 +10738,8 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
9881
10738
  cmd += " --permission-mode auto";
9882
10739
  } else if (permissionMode === "bypassPermissions") {
9883
10740
  cmd += " --dangerously-skip-permissions";
10741
+ } else if (permissionMode === "default") {
10742
+ cmd += " --permission-mode default";
9884
10743
  }
9885
10744
  if (model) {
9886
10745
  cmd += ` --model ${model}`;
@@ -9888,15 +10747,18 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
9888
10747
  if (thinking) {
9889
10748
  cmd += ` --effort ${thinking}`;
9890
10749
  }
10750
+ if (settingsPath) {
10751
+ cmd += ` --settings ${settingsPath}`;
10752
+ }
9891
10753
  if (templatesDir) {
9892
10754
  const roleFile2 = path13.join(templatesDir, `${role}.claude.md`);
9893
10755
  const legacyRoleFile = path13.join(templatesDir, `${role}.CLAUDE.md`);
9894
- const actualRoleFile = fs14.existsSync(roleFile2) ? roleFile2 : fs14.existsSync(legacyRoleFile) ? legacyRoleFile : null;
10756
+ const actualRoleFile = fs15.existsSync(roleFile2) ? roleFile2 : fs15.existsSync(legacyRoleFile) ? legacyRoleFile : null;
9895
10757
  if (actualRoleFile) {
9896
10758
  cmd += ` --append-system-prompt-file ${actualRoleFile}`;
9897
10759
  }
9898
10760
  const pluginDir = path13.join(templatesDir, "..", "plugin");
9899
- if (fs14.existsSync(pluginDir)) {
10761
+ if (fs15.existsSync(pluginDir)) {
9900
10762
  cmd += ` --plugin-dir ${pluginDir}`;
9901
10763
  }
9902
10764
  }
@@ -9912,7 +10774,7 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
9912
10774
  role,
9913
10775
  model,
9914
10776
  autoApprove: true,
9915
- promptFile: roleFile && fs14.existsSync(roleFile) ? roleFile : void 0,
10777
+ promptFile: roleFile && fs15.existsSync(roleFile) ? roleFile : void 0,
9916
10778
  ...captainBoot && role === "captain" ? {
9917
10779
  interactive: true,
9918
10780
  ...captainBoot.port ? { port: captainBoot.port } : {},
@@ -10284,7 +11146,7 @@ var init_opencode2 = __esm({
10284
11146
  });
10285
11147
 
10286
11148
  // packages/agents/dist/projection/opencode-skills.js
10287
- import fs15 from "fs";
11149
+ import fs16 from "fs";
10288
11150
  import os10 from "os";
10289
11151
  import path18 from "path";
10290
11152
  function defaultOpencodeSkillsRoot(env = process.env) {
@@ -10303,62 +11165,62 @@ function parseSkillName(raw) {
10303
11165
  }
10304
11166
  function listSourceSkills(sourceSkillsDir) {
10305
11167
  const out = /* @__PURE__ */ new Map();
10306
- if (!fs15.existsSync(sourceSkillsDir))
11168
+ if (!fs16.existsSync(sourceSkillsDir))
10307
11169
  return out;
10308
- for (const entry of fs15.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
11170
+ for (const entry of fs16.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
10309
11171
  if (!entry.isDirectory())
10310
11172
  continue;
10311
11173
  const skillPath = path18.join(sourceSkillsDir, entry.name, "SKILL.md");
10312
- if (!fs15.existsSync(skillPath))
11174
+ if (!fs16.existsSync(skillPath))
10313
11175
  continue;
10314
- const name = parseSkillName(fs15.readFileSync(skillPath, "utf-8"));
11176
+ const name = parseSkillName(fs16.readFileSync(skillPath, "utf-8"));
10315
11177
  if (name && SAFE_NAME.test(name))
10316
11178
  out.set(name, skillPath);
10317
11179
  }
10318
11180
  return out;
10319
11181
  }
10320
11182
  function hasMarker(dir) {
10321
- return fs15.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
11183
+ return fs16.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
10322
11184
  }
10323
11185
  function syncOpencodeSkills(opts) {
10324
11186
  const result = { written: [], unchanged: [], removed: [], skipped: [] };
10325
11187
  const wanted = listSourceSkills(opts.sourceSkillsDir);
10326
- if (wanted.size === 0 && !fs15.existsSync(opts.sourceSkillsDir))
11188
+ if (wanted.size === 0 && !fs16.existsSync(opts.sourceSkillsDir))
10327
11189
  return result;
10328
11190
  if (!opts.dryRun)
10329
- fs15.mkdirSync(opts.skillsRoot, { recursive: true });
11191
+ fs16.mkdirSync(opts.skillsRoot, { recursive: true });
10330
11192
  for (const [name, srcPath] of wanted) {
10331
11193
  const targetDir = path18.join(opts.skillsRoot, name);
10332
11194
  const targetSkill = path18.join(targetDir, "SKILL.md");
10333
11195
  const managed = hasMarker(targetDir);
10334
- if (fs15.existsSync(targetSkill) && !managed) {
11196
+ if (fs16.existsSync(targetSkill) && !managed) {
10335
11197
  result.skipped.push({ name, reason: "foreign-collision" });
10336
11198
  continue;
10337
11199
  }
10338
- const current = fs15.existsSync(targetSkill) ? fs15.readFileSync(targetSkill) : null;
10339
- if (current && current.equals(fs15.readFileSync(srcPath))) {
11200
+ const current = fs16.existsSync(targetSkill) ? fs16.readFileSync(targetSkill) : null;
11201
+ if (current && current.equals(fs16.readFileSync(srcPath))) {
10340
11202
  if (!managed && !opts.dryRun) {
10341
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
11203
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
10342
11204
  }
10343
11205
  result.unchanged.push(name);
10344
11206
  continue;
10345
11207
  }
10346
11208
  if (!opts.dryRun) {
10347
- fs15.mkdirSync(targetDir, { recursive: true });
10348
- fs15.copyFileSync(srcPath, targetSkill);
10349
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
11209
+ fs16.mkdirSync(targetDir, { recursive: true });
11210
+ fs16.copyFileSync(srcPath, targetSkill);
11211
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
10350
11212
  }
10351
11213
  result.written.push(name);
10352
11214
  }
10353
- if (fs15.existsSync(opts.skillsRoot)) {
10354
- for (const entry of fs15.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
11215
+ if (fs16.existsSync(opts.skillsRoot)) {
11216
+ for (const entry of fs16.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
10355
11217
  if (!entry.isDirectory() || wanted.has(entry.name))
10356
11218
  continue;
10357
11219
  const dir = path18.join(opts.skillsRoot, entry.name);
10358
11220
  if (!hasMarker(dir))
10359
11221
  continue;
10360
11222
  if (!opts.dryRun)
10361
- fs15.rmSync(dir, { recursive: true, force: true });
11223
+ fs16.rmSync(dir, { recursive: true, force: true });
10362
11224
  result.removed.push(entry.name);
10363
11225
  }
10364
11226
  }
@@ -12167,6 +13029,9 @@ var init_peer_channel = __esm({
12167
13029
  return { status: "accepted", via: this.name, confirmed: true };
12168
13030
  }
12169
13031
  }
13032
+ if (statusBefore === "busy" || statusBefore === "shell") {
13033
+ return { status: "queued", via: this.name };
13034
+ }
12170
13035
  if (statusBefore === "idle") {
12171
13036
  await this.deps.sleep(window);
12172
13037
  const afterInfo = this.deps.statusFor(taskId);
@@ -12312,7 +13177,7 @@ var init_receipt_listener = __esm({
12312
13177
  });
12313
13178
 
12314
13179
  // packages/agents/dist/claude/api-key-approval.js
12315
- import fs16 from "fs";
13180
+ import fs17 from "fs";
12316
13181
  import os12 from "os";
12317
13182
  import path20 from "path";
12318
13183
  function ensureClaudeApiKeyApproved(apiKey, opts = {}) {
@@ -12322,12 +13187,12 @@ function ensureClaudeApiKeyApproved(apiKey, opts = {}) {
12322
13187
  const file = opts.claudeJsonPath ?? path20.join(os12.homedir(), ".claude.json");
12323
13188
  const log = opts.log ?? (() => {
12324
13189
  });
12325
- if (!fs16.existsSync(file)) {
13190
+ if (!fs17.existsSync(file)) {
12326
13191
  return { changed: false, reason: "no ~/.claude.json yet \u2014 skipping routed-key pre-approval" };
12327
13192
  }
12328
13193
  let doc;
12329
13194
  try {
12330
- doc = JSON.parse(fs16.readFileSync(file, "utf8"));
13195
+ doc = JSON.parse(fs17.readFileSync(file, "utf8"));
12331
13196
  } catch {
12332
13197
  return {
12333
13198
  changed: false,
@@ -12349,11 +13214,11 @@ function ensureClaudeApiKeyApproved(apiKey, opts = {}) {
12349
13214
  };
12350
13215
  const tmp = `${file}.squadrant-tmp`;
12351
13216
  try {
12352
- fs16.writeFileSync(tmp, JSON.stringify(doc, null, 2) + "\n", { mode: fs16.statSync(file).mode & 511 });
12353
- fs16.renameSync(tmp, file);
13217
+ fs17.writeFileSync(tmp, JSON.stringify(doc, null, 2) + "\n", { mode: fs17.statSync(file).mode & 511 });
13218
+ fs17.renameSync(tmp, file);
12354
13219
  } catch (e) {
12355
13220
  try {
12356
- fs16.unlinkSync(tmp);
13221
+ fs17.unlinkSync(tmp);
12357
13222
  } catch {
12358
13223
  }
12359
13224
  return {
@@ -12709,11 +13574,11 @@ function parseDraftFromScreen(screen) {
12709
13574
  if (!screen)
12710
13575
  return null;
12711
13576
  const lines = screen.split(/\r?\n/);
12712
- const HR_RE = /^\s*─{10,}\s*$/;
13577
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12713
13578
  let bottomHR = -1;
12714
13579
  let topHR = -1;
12715
13580
  for (let i = lines.length - 1; i >= 0; i--) {
12716
- if (HR_RE.test(lines[i])) {
13581
+ if (HR_RE2.test(lines[i])) {
12717
13582
  if (bottomHR === -1) {
12718
13583
  bottomHR = i;
12719
13584
  } else {
@@ -12751,11 +13616,11 @@ function hasCCInputBox(screen) {
12751
13616
  if (!screen)
12752
13617
  return false;
12753
13618
  const lines = screen.split(/\r?\n/);
12754
- const HR_RE = /^\s*─{10,}\s*$/;
13619
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12755
13620
  let bottomHR = -1;
12756
13621
  let topHR = -1;
12757
13622
  for (let i = lines.length - 1; i >= 0; i--) {
12758
- if (HR_RE.test(lines[i])) {
13623
+ if (HR_RE2.test(lines[i])) {
12759
13624
  if (bottomHR === -1)
12760
13625
  bottomHR = i;
12761
13626
  else {
@@ -12768,15 +13633,15 @@ function hasCCInputBox(screen) {
12768
13633
  return false;
12769
13634
  return lines.slice(topHR + 1, bottomHR).some((l) => /[>❯]/.test(l));
12770
13635
  }
12771
- function hasModalOptionList(screen) {
13636
+ function hasModalOptionList2(screen) {
12772
13637
  if (!screen)
12773
13638
  return false;
12774
13639
  const lines = screen.split(/\r?\n/);
12775
- const HR_RE = /^\s*─{10,}\s*$/;
13640
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12776
13641
  let bottomHR = -1;
12777
13642
  let topHR = -1;
12778
13643
  for (let i = lines.length - 1; i >= 0; i--) {
12779
- if (HR_RE.test(lines[i])) {
13644
+ if (HR_RE2.test(lines[i])) {
12780
13645
  if (bottomHR === -1)
12781
13646
  bottomHR = i;
12782
13647
  else {
@@ -12790,14 +13655,14 @@ function hasModalOptionList(screen) {
12790
13655
  return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
12791
13656
  }
12792
13657
  function parseModalOptions(screen) {
12793
- if (!hasModalOptionList(screen))
13658
+ if (!hasModalOptionList2(screen))
12794
13659
  return null;
12795
13660
  const lines = screen.split(/\r?\n/);
12796
- const HR_RE = /^\s*─{10,}\s*$/;
13661
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12797
13662
  let bottomHR = -1;
12798
13663
  let topHR = -1;
12799
13664
  for (let i = lines.length - 1; i >= 0; i--) {
12800
- if (HR_RE.test(lines[i])) {
13665
+ if (HR_RE2.test(lines[i])) {
12801
13666
  if (bottomHR === -1)
12802
13667
  bottomHR = i;
12803
13668
  else {
@@ -12822,11 +13687,11 @@ function readInputBoxRaw(screen, opts) {
12822
13687
  if (!screen)
12823
13688
  return null;
12824
13689
  const lines = screen.split(/\r?\n/);
12825
- const HR_RE = /^\s*─{10,}\s*$/;
13690
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12826
13691
  let bottomHR = -1;
12827
13692
  let topHR = -1;
12828
13693
  for (let i = lines.length - 1; i >= 0; i--) {
12829
- if (HR_RE.test(lines[i])) {
13694
+ if (HR_RE2.test(lines[i])) {
12830
13695
  if (bottomHR === -1)
12831
13696
  bottomHR = i;
12832
13697
  else {
@@ -12859,6 +13724,30 @@ function classifyOpencodeStartupSurface(screen) {
12859
13724
  return "loading";
12860
13725
  return OC_IDLE_MARKERS.some((m) => screenHasSplashMarker(screen, m)) ? "idle" : "loading";
12861
13726
  }
13727
+ function parseOpencodeDraftFromScreen(screen) {
13728
+ if (!screen)
13729
+ return null;
13730
+ if (OC_BUSY_RE.test(screen))
13731
+ return null;
13732
+ const lines = screen.split(/\r?\n/);
13733
+ let bottom = -1;
13734
+ for (let i = lines.length - 1; i >= 0; i--) {
13735
+ if (OC_BOX_BOTTOM_RE.test(lines[i])) {
13736
+ bottom = i;
13737
+ break;
13738
+ }
13739
+ }
13740
+ if (bottom === -1)
13741
+ return null;
13742
+ const content = [];
13743
+ for (let i = bottom - 1; i >= 0 && OC_BOX_SIDE_RE.test(lines[i]); i--) {
13744
+ content.push(lines[i].replace(/^\s*┃\s*/, "").replace(/[\s┃│]+$/, ""));
13745
+ }
13746
+ if (content.length === 0)
13747
+ return null;
13748
+ const hasDraft = content.some((l) => l !== "" && !OC_BOX_STATUS_RE.test(l) && !screenHasSplashMarker(l, "Ask anything"));
13749
+ return hasDraft ? null : "";
13750
+ }
12862
13751
  function sendDebugEnabled() {
12863
13752
  return !!process.env.SQUADRANT_DEBUG_SEND;
12864
13753
  }
@@ -13074,10 +13963,10 @@ function createCmuxDriver() {
13074
13963
  `);
13075
13964
  throw new DeferDelivery(null, "probe-failed");
13076
13965
  }
13077
- const draft = parseDraftFromScreen(screen);
13966
+ const draft = opts?.agent === "opencode" ? parseOpencodeDraftFromScreen(screen) : parseDraftFromScreen(screen);
13078
13967
  if (draft === null)
13079
13968
  throw new DeferDelivery(null, "no-box");
13080
- if (hasModalOptionList(screen))
13969
+ if (hasModalOptionList2(screen))
13081
13970
  throw new DeferDelivery(null, "modal");
13082
13971
  if (draft === "") {
13083
13972
  await deliver();
@@ -13181,7 +14070,7 @@ function createCmuxDriver() {
13181
14070
  }
13182
14071
  };
13183
14072
  }
13184
- var CMUX_TIMEOUT, CmuxTimeoutError, CC_INITIALIZED_RE, CC_WORKING_RE, OC_IDLE_MARKERS;
14073
+ var CMUX_TIMEOUT, CmuxTimeoutError, CC_INITIALIZED_RE, CC_WORKING_RE, OC_IDLE_MARKERS, OC_BOX_BOTTOM_RE, OC_BOX_SIDE_RE, OC_BOX_STATUS_RE, OC_BUSY_RE;
13185
14074
  var init_cmux = __esm({
13186
14075
  "packages/workspaces/dist/runtimes/cmux.js"() {
13187
14076
  init_dist();
@@ -13199,6 +14088,10 @@ var init_cmux = __esm({
13199
14088
  CC_INITIALIZED_RE = /⏵⏵|Ctx Used|for shortcuts|accept edits/i;
13200
14089
  CC_WORKING_RE = /↓\s*[\d.]+\s*k?\s*tokens?\b|esc to interrupt|\bshell still running\b|·\s*\d+\s*shell\b|\(\d+m?\s*\d*s\b/i;
13201
14090
  OC_IDLE_MARKERS = ["Ask anything", "ctrl+p commands"];
14091
+ OC_BOX_BOTTOM_RE = /^\s*╹[▀]{6,}\s*$/;
14092
+ OC_BOX_SIDE_RE = /^\s*┃/;
14093
+ OC_BOX_STATUS_RE = /^Build\s*·/i;
14094
+ OC_BUSY_RE = /esc\s+interrupt/i;
13202
14095
  }
13203
14096
  });
13204
14097
 
@@ -13323,7 +14216,7 @@ var init_notifiers = __esm({
13323
14216
  });
13324
14217
 
13325
14218
  // packages/workspaces/dist/workspaces/obsidian.js
13326
- import fs17 from "fs/promises";
14219
+ import fs18 from "fs/promises";
13327
14220
  import { existsSync as existsSync11 } from "fs";
13328
14221
  import path21 from "path";
13329
14222
  function resolveInRoot(root, relative) {
@@ -13348,16 +14241,16 @@ function createObsidianDriver(scope) {
13348
14241
  };
13349
14242
  },
13350
14243
  async read(rel) {
13351
- return fs17.readFile(resolveInRoot(root, rel), "utf-8");
14244
+ return fs18.readFile(resolveInRoot(root, rel), "utf-8");
13352
14245
  },
13353
14246
  async write(rel, content) {
13354
14247
  const abs = resolveInRoot(root, rel);
13355
- await fs17.mkdir(path21.dirname(abs), { recursive: true });
13356
- await fs17.writeFile(abs, content);
14248
+ await fs18.mkdir(path21.dirname(abs), { recursive: true });
14249
+ await fs18.writeFile(abs, content);
13357
14250
  },
13358
14251
  async exists(rel) {
13359
14252
  try {
13360
- await fs17.access(resolveInRoot(root, rel));
14253
+ await fs18.access(resolveInRoot(root, rel));
13361
14254
  return true;
13362
14255
  } catch {
13363
14256
  return false;
@@ -13365,13 +14258,13 @@ function createObsidianDriver(scope) {
13365
14258
  },
13366
14259
  async list(rel) {
13367
14260
  try {
13368
- return await fs17.readdir(resolveInRoot(root, rel));
14261
+ return await fs18.readdir(resolveInRoot(root, rel));
13369
14262
  } catch {
13370
14263
  return [];
13371
14264
  }
13372
14265
  },
13373
14266
  async mkdir(rel) {
13374
- await fs17.mkdir(resolveInRoot(root, rel), { recursive: true });
14267
+ await fs18.mkdir(resolveInRoot(root, rel), { recursive: true });
13375
14268
  }
13376
14269
  };
13377
14270
  }
@@ -14269,7 +15162,7 @@ async function resolveCaptainWorkspace(project) {
14269
15162
  }
14270
15163
  async function paneHasOpenModal(runtime, pane) {
14271
15164
  const screen = await runtime.readPaneScreen(pane) ?? "";
14272
- return hasModalOptionList(screen);
15165
+ return hasModalOptionList2(screen);
14273
15166
  }
14274
15167
  async function readModalOptions(runtime, pane) {
14275
15168
  const screen = await runtime.readPaneScreen(pane) ?? "";
@@ -14277,7 +15170,7 @@ async function readModalOptions(runtime, pane) {
14277
15170
  }
14278
15171
  async function confirmedSendToPane(runtime, pane, message) {
14279
15172
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
14280
- if (hasModalOptionList(preSendScreen)) {
15173
+ if (hasModalOptionList2(preSendScreen)) {
14281
15174
  return { delivered: false, blockedByModal: true };
14282
15175
  }
14283
15176
  await runtime.pasteToPane(pane, message);
@@ -14530,9 +15423,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
14530
15423
  const resolved = resolveTarget(registry, config, target, !!opts.command);
14531
15424
  await needRef(resolved);
14532
15425
  const finalProject = opts.command ? config.commandName : target;
14533
- const { join: join35, dirname: dirname11 } = await import("path");
15426
+ const { join: join36, dirname: dirname12 } = await import("path");
14534
15427
  const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
14535
- const stateRoot = join35(dirname11(DEFAULT_CONFIG_PATH2), "state");
15428
+ const stateRoot = join36(dirname12(DEFAULT_CONFIG_PATH2), "state");
14536
15429
  const seq = await appendCaptainMessage2({
14537
15430
  stateRoot,
14538
15431
  project: finalProject,
@@ -14641,7 +15534,7 @@ init_dist3();
14641
15534
  import { Command as Command38 } from "commander";
14642
15535
  import { existsSync as existsSync14, readFileSync as readFileSync21 } from "fs";
14643
15536
  import { fileURLToPath as fileURLToPath6 } from "url";
14644
- import { dirname as dirname10, join as join34 } from "path";
15537
+ import { dirname as dirname11, join as join35 } from "path";
14645
15538
  import { homedir as homedir20 } from "os";
14646
15539
 
14647
15540
  // packages/cli/src/commands/doctor.ts
@@ -14651,7 +15544,7 @@ init_dist();
14651
15544
  init_dist4();
14652
15545
  import { Command } from "commander";
14653
15546
  import { execSync as execSync8 } from "child_process";
14654
- import fs18 from "fs";
15547
+ import fs19 from "fs";
14655
15548
  import { stat } from "fs/promises";
14656
15549
  import path22 from "path";
14657
15550
  import chalk3 from "chalk";
@@ -14755,7 +15648,7 @@ function settingsHaveAgentTeams() {
14755
15648
  try {
14756
15649
  const home = process.env.HOME || "";
14757
15650
  const settings = JSON.parse(
14758
- fs18.readFileSync(`${home}/.claude/settings.json`, "utf-8")
15651
+ fs19.readFileSync(`${home}/.claude/settings.json`, "utf-8")
14759
15652
  );
14760
15653
  return settings?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1";
14761
15654
  } catch {
@@ -14766,7 +15659,7 @@ function pluginInstalled(pluginKey) {
14766
15659
  try {
14767
15660
  const home = process.env.HOME || "";
14768
15661
  const plugins = JSON.parse(
14769
- fs18.readFileSync(
15662
+ fs19.readFileSync(
14770
15663
  `${home}/.claude/plugins/installed_plugins.json`,
14771
15664
  "utf-8"
14772
15665
  )
@@ -14812,7 +15705,7 @@ ${lines.join("\n")}
14812
15705
  }
14813
15706
  function readPackageVersion(packageJsonPath) {
14814
15707
  try {
14815
- const pkg2 = JSON.parse(fs18.readFileSync(packageJsonPath, "utf-8"));
15708
+ const pkg2 = JSON.parse(fs19.readFileSync(packageJsonPath, "utf-8"));
14816
15709
  return typeof pkg2.version === "string" ? pkg2.version : null;
14817
15710
  } catch {
14818
15711
  return null;
@@ -14848,7 +15741,7 @@ var doctorCommand = new Command("doctor").description("Check system health and p
14848
15741
  ));
14849
15742
  results.push(check(
14850
15743
  "Obsidian installed",
14851
- commandExists("obsidian") || fs18.existsSync("/Applications/Obsidian.app"),
15744
+ commandExists("obsidian") || fs19.existsSync("/Applications/Obsidian.app"),
14852
15745
  "Install from: https://obsidian.md"
14853
15746
  ));
14854
15747
  results.push(check(
@@ -14956,7 +15849,7 @@ var doctorCommand = new Command("doctor").description("Check system health and p
14956
15849
  results.push(
14957
15850
  check(
14958
15851
  "Squadrant config exists",
14959
- fs18.existsSync(
15852
+ fs19.existsSync(
14960
15853
  process.env.SQUADRANT_CONFIG || `${process.env.HOME}/.config/squadrant/config.json`
14961
15854
  ),
14962
15855
  "Run: squadrant init"
@@ -15040,7 +15933,7 @@ init_dist();
15040
15933
  init_dist4();
15041
15934
  init_dist();
15042
15935
  import { Command as Command2 } from "commander";
15043
- import fs19 from "fs";
15936
+ import fs20 from "fs";
15044
15937
  import path23 from "path";
15045
15938
  import os13 from "os";
15046
15939
  import readline from "readline";
@@ -15140,6 +16033,13 @@ function mergeCrewPermissions(settings) {
15140
16033
  permissions.allow = [.../* @__PURE__ */ new Set([...existing, ...CREW_PERMISSION_ALLOWLIST])];
15141
16034
  return next;
15142
16035
  }
16036
+ function writeRouterSettings(o) {
16037
+ const dir = join26(o.stateRoot, o.project, o.taskId);
16038
+ mkdirSync9(dir, { recursive: true });
16039
+ const file = join26(dir, "router-settings.json");
16040
+ writeFileSync12(file, JSON.stringify({ env: o.env }, null, 2));
16041
+ return file;
16042
+ }
15143
16043
  function writePerCrewSettingsLocal(o) {
15144
16044
  const dir = join26(o.projectCwd, ".claude");
15145
16045
  mkdirSync9(dir, { recursive: true });
@@ -15250,25 +16150,57 @@ function writePerCrewOpencodeConfig(o) {
15250
16150
  return file;
15251
16151
  }
15252
16152
 
16153
+ // packages/cli/src/lib/claude-auth.ts
16154
+ import { execSync as execSync9 } from "child_process";
16155
+ function parseClaudeAuthStatus(raw) {
16156
+ let parsed;
16157
+ try {
16158
+ parsed = JSON.parse(raw);
16159
+ } catch {
16160
+ return null;
16161
+ }
16162
+ if (typeof parsed !== "object" || parsed === null) return null;
16163
+ const obj = parsed;
16164
+ if (typeof obj.loggedIn !== "boolean") return null;
16165
+ return {
16166
+ authenticated: obj.loggedIn,
16167
+ method: typeof obj.authMethod === "string" ? obj.authMethod : void 0,
16168
+ provider: typeof obj.apiProvider === "string" ? obj.apiProvider : void 0
16169
+ };
16170
+ }
16171
+ function detectClaudeAuth(probe = {}) {
16172
+ const run = probe.run ?? (() => execSync9("claude auth status", {
16173
+ encoding: "utf8",
16174
+ stdio: ["ignore", "pipe", "ignore"],
16175
+ timeout: 1e4
16176
+ }));
16177
+ try {
16178
+ const parsed = parseClaudeAuthStatus(run());
16179
+ return parsed ?? { authenticated: false, reason: "unreadable `claude auth status` output" };
16180
+ } catch {
16181
+ return { authenticated: false, reason: "`claude auth status` unavailable" };
16182
+ }
16183
+ }
16184
+
15253
16185
  // packages/cli/src/commands/init.ts
15254
16186
  init_dist3();
15255
16187
  function findPackageRoot() {
15256
16188
  let dir = path23.dirname(new URL(import.meta.url).pathname);
15257
16189
  while (dir !== "/") {
15258
- if (fs19.existsSync(path23.join(dir, "package.json"))) return dir;
16190
+ if (fs20.existsSync(path23.join(dir, "package.json"))) return dir;
15259
16191
  dir = path23.dirname(dir);
15260
16192
  }
15261
16193
  return process.cwd();
15262
16194
  }
15263
16195
  function copyDirRecursive(src, dest) {
15264
- fs19.mkdirSync(dest, { recursive: true });
15265
- for (const entry of fs19.readdirSync(src, { withFileTypes: true })) {
16196
+ fs20.mkdirSync(dest, { recursive: true });
16197
+ for (const entry of fs20.readdirSync(src, { withFileTypes: true })) {
15266
16198
  const srcPath = path23.join(src, entry.name);
15267
16199
  const destPath = path23.join(dest, entry.name);
15268
16200
  if (entry.isDirectory()) {
15269
16201
  copyDirRecursive(srcPath, destPath);
15270
16202
  } else {
15271
- fs19.copyFileSync(srcPath, destPath);
16203
+ fs20.copyFileSync(srcPath, destPath);
15272
16204
  }
15273
16205
  }
15274
16206
  }
@@ -15285,16 +16217,70 @@ function promptLine(question) {
15285
16217
  });
15286
16218
  });
15287
16219
  }
15288
- var initCommand = new Command2("init").description("Guided first-time setup: hub vault, agents, plugins, projects (re-run-safe)").option("--hub <path>", "Hub vault path", "~/squadrant-hub").action(async (opts) => {
16220
+ async function resolveRouter(config, opts, isTTY) {
16221
+ if (config.defaults?.router) return config.defaults.router;
16222
+ let kind = opts.routerKind;
16223
+ let baseUrl = opts.routerBaseUrl;
16224
+ let apiKeyEnv = opts.routerApiKeyEnv;
16225
+ if (isTTY && (!kind || !baseUrl)) {
16226
+ const k = await promptLine(chalk4.cyan(` Router kind [${DEFAULT_ROUTER_KIND}]: `));
16227
+ kind = kind ?? (k || DEFAULT_ROUTER_KIND);
16228
+ const b = await promptLine(chalk4.cyan(` Router base URL [${DEFAULT_ROUTER_BASE_URL}]: `));
16229
+ baseUrl = baseUrl ?? (b || DEFAULT_ROUTER_BASE_URL);
16230
+ const e = await promptLine(chalk4.cyan(" Env var holding the API key (optional): "));
16231
+ apiKeyEnv = apiKeyEnv ?? (e || void 0);
16232
+ }
16233
+ const resolvedKind = kind && isRouterKind(kind) ? kind : DEFAULT_ROUTER_KIND;
16234
+ const resolvedBase = baseUrl || DEFAULT_ROUTER_BASE_URL;
16235
+ if (!isTTY && !opts.routerBaseUrl) {
16236
+ console.log(chalk4.dim(` - no router flags given; defaulting to ${resolvedKind} (${resolvedBase})`));
16237
+ console.log(chalk4.dim(" set a credential: squadrant config set defaults.router.apiKeyEnv <VAR>"));
16238
+ }
16239
+ return { kind: resolvedKind, baseUrl: resolvedBase, ...apiKeyEnv ? { apiKeyEnv } : {} };
16240
+ }
16241
+ async function askProvider(current, auth) {
16242
+ console.log(chalk4.bold("\n Choose your provider:"));
16243
+ for (const p of PROVIDER_PRESETS) {
16244
+ const currentTag = p.id === current ? chalk4.dim(" (current)") : "";
16245
+ console.log(` ${chalk4.cyan(`${p.id})`)} ${p.label}${currentTag}`);
16246
+ console.log(chalk4.dim(` ${p.summary}`));
16247
+ }
16248
+ if (!auth.authenticated) {
16249
+ console.log(chalk4.yellow("\n \u26A0 No Anthropic credential detected \u2014 preset a will not work; consider b or c."));
16250
+ }
16251
+ const answer = (await promptLine(chalk4.cyan(`
16252
+ Provider [a/b/c/d] (default ${current}): `))).toLowerCase();
16253
+ if (!answer) return current;
16254
+ if (isProviderPresetId(answer)) return answer;
16255
+ console.log(chalk4.yellow(` \u26A0 Unknown choice '${answer}' \u2014 keeping '${current}'.`));
16256
+ return current;
16257
+ }
16258
+ function warnIfNoAnthropic(preset, auth) {
16259
+ if (preset !== "a" || auth.authenticated) return;
16260
+ console.log(chalk4.yellow(" \u26A0 No Anthropic credential detected (`claude auth status`)."));
16261
+ console.log(chalk4.dim(" Preset A (Claude Pro/Max or an API key) may not work \u2014 consider preset b or c."));
16262
+ }
16263
+ var initCommand = new Command2("init").description("Guided first-time setup: provider, hub vault, agents, plugins, projects (re-run-safe)").option("--hub <path>", "Hub vault path", "~/squadrant-hub").option("--preset <id>", "provider preset: a|b|c|d (non-interactive)").option("--router-kind <kind>", "router kind for --preset c (opencode-go|openrouter|ccr|litellm|custom)").option("--router-base-url <url>", "router base URL for --preset c").option("--router-api-key-env <env>", "env var holding the router credential for --preset c").action(async (opts) => {
15289
16264
  const hubPath = resolveHome(opts.hub);
15290
16265
  const pkgRoot = findPackageRoot();
15291
16266
  const configDir = path23.join(os13.homedir(), ".config", "squadrant");
15292
16267
  const isTTY = process.stdin.isTTY === true;
15293
16268
  console.log(chalk4.bold("\nSquadrant Init\n"));
15294
- if (!isTTY) {
16269
+ let explicitPreset;
16270
+ if (opts.preset !== void 0) {
16271
+ const candidate = opts.preset.toLowerCase();
16272
+ if (!isProviderPresetId(candidate)) {
16273
+ console.error(chalk4.red(` \u2718 Unknown --preset '${opts.preset}'. Valid values: a, b, c, d`));
16274
+ process.exitCode = 1;
16275
+ return;
16276
+ }
16277
+ explicitPreset = candidate;
16278
+ }
16279
+ if (!isTTY && !explicitPreset) {
15295
16280
  console.log(" Run these steps to get started:\n");
15296
- console.log(chalk4.bold(" 1/5 Hub vault"));
16281
+ console.log(chalk4.bold(" 1/5 Hub vault + provider"));
15297
16282
  console.log(chalk4.cyan(` squadrant init --hub ${opts.hub}`));
16283
+ console.log(chalk4.dim(" Choose a provider: squadrant init --preset a|b|c|d"));
15298
16284
  console.log(chalk4.bold("\n 2/5 Agent + projection setup"));
15299
16285
  console.log(" (handled automatically by: " + chalk4.cyan("squadrant init") + ")");
15300
16286
  console.log(chalk4.bold("\n 3/5 Plugins \u2014 open Claude Code and run:"));
@@ -15314,8 +16300,8 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
15314
16300
  }
15315
16301
  const wsRegistry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
15316
16302
  try {
15317
- if (fs19.existsSync(DEFAULT_CONFIG_PATH)) {
15318
- const existing = JSON.parse(fs19.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
16303
+ if (fs20.existsSync(DEFAULT_CONFIG_PATH)) {
16304
+ const existing = JSON.parse(fs20.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
15319
16305
  wsRegistry.get(existing.workspace ?? "obsidian");
15320
16306
  }
15321
16307
  } catch (err) {
@@ -15323,45 +16309,76 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
15323
16309
  return;
15324
16310
  }
15325
16311
  stepHeader(1, 5, "Hub vault");
15326
- if (fs19.existsSync(DEFAULT_CONFIG_PATH)) {
16312
+ const configExists = fs20.existsSync(DEFAULT_CONFIG_PATH);
16313
+ let config;
16314
+ if (configExists) {
15327
16315
  console.log(chalk4.yellow(" \u26A0 Config already exists, skipping creation"));
16316
+ config = loadConfig();
15328
16317
  } else {
15329
- const config = getDefaultConfig();
16318
+ config = getDefaultConfig();
15330
16319
  config.hubVault = hubPath;
16320
+ }
16321
+ const auth = detectClaudeAuth();
16322
+ let chosenPreset = explicitPreset;
16323
+ if (!chosenPreset && isTTY) {
16324
+ chosenPreset = await askProvider(detectProviderPresetId(config), auth);
16325
+ }
16326
+ let appliedChanges = [];
16327
+ if (chosenPreset) {
16328
+ const label = PROVIDER_PRESETS.find((p) => p.id === chosenPreset)?.label ?? chosenPreset;
16329
+ const router = chosenPreset === "c" ? await resolveRouter(config, opts, isTTY) : void 0;
16330
+ const overwrite = !configExists || explicitPreset !== void 0;
16331
+ const result = applyProviderPreset(config, chosenPreset, { router, overwrite });
16332
+ appliedChanges = result.changes;
16333
+ config = result.config;
16334
+ console.log(chalk4.bold(`
16335
+ Provider: ${chosenPreset.toUpperCase()} \u2014 ${label}`));
16336
+ if (appliedChanges.length) {
16337
+ console.log(" Will change:");
16338
+ for (const change of appliedChanges) console.log(chalk4.dim(` - ${change}`));
16339
+ } else {
16340
+ console.log(chalk4.dim(" - provider config already set (unchanged)"));
16341
+ }
16342
+ if (!isTTY) warnIfNoAnthropic(chosenPreset, auth);
16343
+ }
16344
+ if (!configExists) {
15331
16345
  saveConfig(config);
15332
16346
  console.log(chalk4.green(` \u2714 Config created at ${DEFAULT_CONFIG_PATH}`));
16347
+ } else if (appliedChanges.length) {
16348
+ saveConfig(config);
16349
+ console.log(chalk4.green(` \u2714 Config updated at ${DEFAULT_CONFIG_PATH}`));
15333
16350
  }
15334
16351
  const hubTemplate = path23.join(pkgRoot, "obsidian", "hub");
15335
- if (fs19.existsSync(hubPath)) {
16352
+ if (fs20.existsSync(hubPath)) {
15336
16353
  console.log(chalk4.yellow(` \u26A0 Hub vault already exists at ${hubPath}`));
15337
- } else if (fs19.existsSync(hubTemplate)) {
16354
+ } else if (fs20.existsSync(hubTemplate)) {
15338
16355
  copyDirRecursive(hubTemplate, hubPath);
15339
16356
  console.log(chalk4.green(` \u2714 Hub vault scaffolded at ${hubPath}`));
15340
16357
  } else {
15341
- fs19.mkdirSync(hubPath, { recursive: true });
16358
+ fs20.mkdirSync(hubPath, { recursive: true });
15342
16359
  console.log(chalk4.yellow(` \u26A0 Hub template not found; created empty directory at ${hubPath}`));
15343
16360
  }
15344
16361
  const hubDashboardSrc = path23.join(pkgRoot, "obsidian", "hub", "dashboard.md");
15345
16362
  const hubDashboardDest = path23.join(hubPath, "dashboard.md");
15346
- if (fs19.existsSync(hubDashboardSrc)) {
15347
- fs19.copyFileSync(hubDashboardSrc, hubDashboardDest);
16363
+ if (fs20.existsSync(hubDashboardSrc)) {
16364
+ fs20.copyFileSync(hubDashboardSrc, hubDashboardDest);
15348
16365
  console.log(chalk4.green(` \u2714 Dashboard refreshed`));
15349
16366
  }
15350
- fs19.mkdirSync(path23.join(hubPath, "projects"), { recursive: true });
16367
+ fs20.mkdirSync(path23.join(hubPath, "projects"), { recursive: true });
15351
16368
  ensureRuntimeSynced({ sourceRoot: pkgRoot, runtimeRoot: configDir });
15352
16369
  console.log(chalk4.green(` \u2714 Runtime assets synced to ${configDir}`));
15353
16370
  stepHeader(2, 5, "Agent + projection setup");
15354
16371
  const settingsPath = path23.join(os13.homedir(), ".claude", "settings.json");
15355
16372
  try {
15356
16373
  let settings = {};
15357
- if (fs19.existsSync(settingsPath)) {
15358
- settings = JSON.parse(fs19.readFileSync(settingsPath, "utf-8"));
16374
+ if (fs20.existsSync(settingsPath)) {
16375
+ settings = JSON.parse(fs20.readFileSync(settingsPath, "utf-8"));
15359
16376
  }
15360
16377
  const env = settings.env || {};
15361
16378
  if (env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
15362
16379
  settings.env = { ...env, CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1" };
15363
- fs19.mkdirSync(path23.dirname(settingsPath), { recursive: true });
15364
- fs19.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
16380
+ fs20.mkdirSync(path23.dirname(settingsPath), { recursive: true });
16381
+ fs20.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
15365
16382
  console.log(chalk4.green(" \u2714 Agent Teams enabled in ~/.claude/settings.json"));
15366
16383
  } else {
15367
16384
  console.log(chalk4.green(" \u2714 Agent Teams already enabled"));
@@ -15441,7 +16458,7 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
15441
16458
  init_dist();
15442
16459
  init_dist2();
15443
16460
  import { Command as Command3 } from "commander";
15444
- import fs20 from "fs";
16461
+ import fs21 from "fs";
15445
16462
  import path24 from "path";
15446
16463
  import chalk5 from "chalk";
15447
16464
  function restartAfterProjectsAdd(opts) {
@@ -15456,20 +16473,20 @@ function restartAfterProjectsAdd(opts) {
15456
16473
  function findPackageRoot2() {
15457
16474
  let dir = path24.dirname(new URL(import.meta.url).pathname);
15458
16475
  while (dir !== "/") {
15459
- if (fs20.existsSync(path24.join(dir, "package.json"))) return dir;
16476
+ if (fs21.existsSync(path24.join(dir, "package.json"))) return dir;
15460
16477
  dir = path24.dirname(dir);
15461
16478
  }
15462
16479
  return process.cwd();
15463
16480
  }
15464
16481
  function copyDirRecursive2(src, dest) {
15465
- fs20.mkdirSync(dest, { recursive: true });
15466
- for (const entry of fs20.readdirSync(src, { withFileTypes: true })) {
16482
+ fs21.mkdirSync(dest, { recursive: true });
16483
+ for (const entry of fs21.readdirSync(src, { withFileTypes: true })) {
15467
16484
  const srcPath = path24.join(src, entry.name);
15468
16485
  const destPath = path24.join(dest, entry.name);
15469
16486
  if (entry.isDirectory()) {
15470
16487
  copyDirRecursive2(srcPath, destPath);
15471
16488
  } else {
15472
- fs20.copyFileSync(srcPath, destPath);
16489
+ fs21.copyFileSync(srcPath, destPath);
15473
16490
  }
15474
16491
  }
15475
16492
  }
@@ -15505,7 +16522,7 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
15505
16522
  process.exit(1);
15506
16523
  }
15507
16524
  const resolvedPath = resolveHome(projectPath);
15508
- if (!fs20.existsSync(path24.join(resolvedPath, ".git"))) {
16525
+ if (!fs21.existsSync(path24.join(resolvedPath, ".git"))) {
15509
16526
  console.log(chalk5.yellow(`
15510
16527
  \u26A0 No .git found at ${resolvedPath}. Make sure this is the project root, not a parent directory.
15511
16528
  `));
@@ -15574,19 +16591,19 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
15574
16591
  restartAfterProjectsAdd({ noRestart: opts.restart === false });
15575
16592
  const pkgRoot = findPackageRoot2();
15576
16593
  const spokeTemplate = path24.join(pkgRoot, "obsidian", "spoke");
15577
- if (fs20.existsSync(spokeVault)) {
16594
+ if (fs21.existsSync(spokeVault)) {
15578
16595
  console.log(chalk5.yellow(` \u26A0 Spoke vault already exists at ${spokeVault}, skipping scaffold`));
15579
- } else if (fs20.existsSync(spokeTemplate)) {
16596
+ } else if (fs21.existsSync(spokeTemplate)) {
15580
16597
  copyDirRecursive2(spokeTemplate, spokeVault);
15581
16598
  const statusPath = path24.join(spokeVault, "status.md");
15582
- if (fs20.existsSync(statusPath)) {
15583
- const content = fs20.readFileSync(statusPath, "utf-8");
16599
+ if (fs21.existsSync(statusPath)) {
16600
+ const content = fs21.readFileSync(statusPath, "utf-8");
15584
16601
  const updated = content.replace(/^project: unnamed/m, `project: ${name}`);
15585
- fs20.writeFileSync(statusPath, updated);
16602
+ fs21.writeFileSync(statusPath, updated);
15586
16603
  }
15587
16604
  console.log(chalk5.green(` \u2714 Spoke vault scaffolded at ${spokeVault}`));
15588
16605
  } else {
15589
- fs20.mkdirSync(spokeVault, { recursive: true });
16606
+ fs21.mkdirSync(spokeVault, { recursive: true });
15590
16607
  console.log(chalk5.yellow(` \u26A0 Spoke template not found; created empty dir at ${spokeVault}`));
15591
16608
  }
15592
16609
  console.log("");
@@ -15654,7 +16671,7 @@ init_dist4();
15654
16671
  init_dist3();
15655
16672
  init_dist();
15656
16673
  import { Command as Command5 } from "commander";
15657
- import { execSync as execSync9 } from "child_process";
16674
+ import { execSync as execSync10 } from "child_process";
15658
16675
  import path25 from "path";
15659
16676
  import os14 from "os";
15660
16677
  import chalk7 from "chalk";
@@ -15665,7 +16682,7 @@ var TASK_PROMPTS = {
15665
16682
  "wiki-aggregate": "Run a wiki aggregation pass using the squadrant:command-ops skill. Scan each spoke wiki index, identify shared knowledge worth promoting, write hub wiki pages, and exit when done."
15666
16683
  };
15667
16684
  function detectCurrentWorkspace() {
15668
- const out = execSync9(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
16685
+ const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
15669
16686
  const match = out.match(/workspace:\d+/);
15670
16687
  if (!match) {
15671
16688
  throw new Error("Could not detect current cmux workspace. Run `squadrant command` from inside a cmux workspace.");
@@ -16533,6 +17550,7 @@ async function runCrewSpawn2(input) {
16533
17550
  },
16534
17551
  writeSettingsLocal: (cwd) => writePerCrewSettingsLocal({ projectCwd: cwd }),
16535
17552
  writeOpencodeConfig: writePerCrewOpencodeConfig,
17553
+ writeRouterSettings,
16536
17554
  sendFirstTurn: (pane, firstTurn, preLaunchScreen, opts) => sendFirstTurnWhenReady(runtime, pane, firstTurn, preLaunchScreen, opts),
16537
17555
  getFreePort,
16538
17556
  sendCodexFirstTurn,
@@ -16982,7 +18000,7 @@ init_dist4();
16982
18000
  init_dist();
16983
18001
  init_dist2();
16984
18002
  import { Command as Command12 } from "commander";
16985
- import fs21 from "fs";
18003
+ import fs22 from "fs";
16986
18004
  import path26 from "path";
16987
18005
  import os15 from "os";
16988
18006
  import chalk12 from "chalk";
@@ -17019,7 +18037,7 @@ async function runSideSpawn2(input) {
17019
18037
  prompt: input.topic,
17020
18038
  workdir: spawnCwd,
17021
18039
  role: "side",
17022
- promptFile: fs21.existsSync(promptFile) ? promptFile : void 0,
18040
+ promptFile: fs22.existsSync(promptFile) ? promptFile : void 0,
17023
18041
  interactive: true,
17024
18042
  permissionMode: config.defaults.permissions?.crew ?? "auto",
17025
18043
  ...sideModel ? { model: sideModel } : {},
@@ -17126,7 +18144,7 @@ sideCommand.command("close").description("Close a side-session (closes its tab)"
17126
18144
  init_dist();
17127
18145
  init_dist4();
17128
18146
  import { Command as Command13 } from "commander";
17129
- import { execSync as execSync10 } from "child_process";
18147
+ import { execSync as execSync11 } from "child_process";
17130
18148
  import chalk14 from "chalk";
17131
18149
 
17132
18150
  // packages/web/dist/read-status.js
@@ -17282,7 +18300,7 @@ function renderDashboard(rows, opts) {
17282
18300
 
17283
18301
  // packages/web/dist/sync-hub.js
17284
18302
  init_dist();
17285
- import fs22 from "fs";
18303
+ import fs23 from "fs";
17286
18304
  import path27 from "path";
17287
18305
  function buildMirrorMarkdown(s) {
17288
18306
  const fenced = "```";
@@ -17309,8 +18327,8 @@ function buildMirrorMarkdown(s) {
17309
18327
  function syncHub(deps) {
17310
18328
  if (!deps.config.hubVault)
17311
18329
  return [];
17312
- const writeFile6 = deps.writeFile ?? ((p, c) => fs22.writeFileSync(p, c));
17313
- const mkdir6 = deps.mkdir ?? ((p) => fs22.mkdirSync(p, { recursive: true }));
18330
+ const writeFile6 = deps.writeFile ?? ((p, c) => fs23.writeFileSync(p, c));
18331
+ const mkdir6 = deps.mkdir ?? ((p) => fs23.mkdirSync(p, { recursive: true }));
17314
18332
  const projectsDir = path27.join(resolveHome(deps.config.hubVault), "projects");
17315
18333
  mkdir6(projectsDir);
17316
18334
  const out = [];
@@ -17330,6 +18348,7 @@ function syncHub(deps) {
17330
18348
  // packages/web/dist/web-server.js
17331
18349
  init_dist2();
17332
18350
  import { createServer as createServer5 } from "http";
18351
+ import { dirname as dirname7, join as join29 } from "path";
17333
18352
 
17334
18353
  // packages/web/dist/snapshot-merge.js
17335
18354
  function mergeSnapshot(daemon, external, now) {
@@ -17795,6 +18814,24 @@ function renderEnv(ext) {
17795
18814
  out.push(`</section>`);
17796
18815
  return out.join("");
17797
18816
  }
18817
+ function renderLogs() {
18818
+ return [
18819
+ `<section class="panel" data-panel="logs" role="tabpanel" aria-label="Logs">`,
18820
+ sectionHead("Daemon log", "Live tail of squadrantd.log \u2014 lifecycle events, delivery attempts, auto-launches and errors as the daemon writes them."),
18821
+ `<div class="log-controls">`,
18822
+ `<div class="filter-bar" role="group" aria-label="Filter log lines by severity">`,
18823
+ `<button class="filter-btn on" data-log-sev="all" aria-pressed="true">All</button>`,
18824
+ `<button class="filter-btn" data-log-sev="warn" aria-pressed="false">Warn+</button>`,
18825
+ `<button class="filter-btn" data-log-sev="error" aria-pressed="false">Errors</button>`,
18826
+ `</div>`,
18827
+ `<button class="filter-btn" data-log-pause aria-pressed="false">Pause</button>`,
18828
+ `<button class="filter-btn" data-log-copy>Copy</button>`,
18829
+ `<span class="log-status" data-log-status>connecting\u2026</span>`,
18830
+ `</div>`,
18831
+ `<div class="log-view mono" data-log-view aria-live="polite" aria-label="Daemon log lines"></div>`,
18832
+ `</section>`
18833
+ ].join("");
18834
+ }
17798
18835
  var ATTN_ORDER = { stale: 0, gone: 1, alive: 2, unknown: 3, stopped: 4 };
17799
18836
  var STATE_LABEL = { alive: "running", stale: "blocked", gone: "errored", unknown: "idle", stopped: "offline" };
17800
18837
  var STATE_CLS = { alive: "s-alive", stale: "s-stale", gone: "s-gone", unknown: "s-unknown", stopped: "s-stopped" };
@@ -17923,7 +18960,8 @@ var TABS = [
17923
18960
  ["overview", "Overview"],
17924
18961
  ["projects", "Projects"],
17925
18962
  ["daemon", "Daemon"],
17926
- ["environment", "Environment"]
18963
+ ["environment", "Environment"],
18964
+ ["logs", "Logs"]
17927
18965
  ];
17928
18966
  function tabNav() {
17929
18967
  const btns = TABS.map(([id, label], i) => `<button class="tab${i === 0 ? " on" : ""}" data-tab="${id}" role="tab" aria-selected="${i === 0 ? "true" : "false"}">${esc(label)}</button>`).join("");
@@ -17972,6 +19010,7 @@ function renderContent(snap) {
17972
19010
  out.push(renderProjects(snap, col.now));
17973
19011
  out.push(renderDaemon(snap));
17974
19012
  out.push(renderEnv(snap.external));
19013
+ out.push(renderLogs());
17975
19014
  out.push(`</div>`);
17976
19015
  out.push(metricsBlob(col, linkLost));
17977
19016
  return out.join("\n");
@@ -18213,6 +19252,17 @@ body{margin:0;min-height:100vh;background:radial-gradient(1200px 760px at 50% -2
18213
19252
 
18214
19253
  .empty{border:1px dashed var(--bezel);border-radius:12px;padding:24px;color:var(--ink-dim);text-align:center;background:var(--panel)}
18215
19254
 
19255
+ /* Logs (live daemon-log tail) */
19256
+ .log-controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:0 0 12px}
19257
+ .log-controls .filter-bar{margin:0}
19258
+ .log-status{margin-left:auto;color:var(--ink-dim);font-size:11px}
19259
+ .log-view{height:min(58vh,520px);overflow:auto;background:#0b1220;border:1px solid var(--bezel);border-radius:12px;
19260
+ padding:12px 14px;box-shadow:var(--shadow);font-size:12px;line-height:1.55}
19261
+ .log-line{white-space:pre-wrap;word-break:break-word;color:#c7d2e0}
19262
+ .log-line.l-warn{color:#f0c674}
19263
+ .log-line.l-error{color:#ff8a80}
19264
+ .log-view .empty-log{color:#6b7a90}
19265
+
18216
19266
  :focus-visible{outline:2px solid var(--hud);outline-offset:2px;border-radius:4px}
18217
19267
 
18218
19268
  @keyframes beat{0%,100%{box-shadow:0 0 0 0 rgba(58,210,159,.45)}50%{box-shadow:0 0 0 6px rgba(58,210,159,0)}}
@@ -18226,6 +19276,7 @@ body{margin:0;min-height:100vh;background:radial-gradient(1200px 760px at 50% -2
18226
19276
  var CLIENT_JS = `
18227
19277
  (function(){
18228
19278
  var activeTab='live';var hist={};var prev={};var currentFilter='all';var currentSort='attention';var sortAsc=true;var currentSearch='';var expandedProjects={};
19279
+ var logLines=[];var logSev='all';var logPaused=false;var logAutoscroll=true;var logConnected=false;var logScrollSave=0;var LOG_MAX=1000;
18229
19280
  function pushHist(k,v,t){var a=hist[k]||(hist[k]=[]);if(a.length&&a[a.length-1].t===t)return;a.push({t:t,v:v});if(a.length>48)a.shift();}
18230
19281
  function ingest(){var el=document.getElementById('squadrant-metrics');if(!el)return;var m;try{m=JSON.parse(el.textContent);}catch(e){return;}pushHist('errors',m.errors,m.t);pushHist('behind',m.behind,m.t);pushHist('crewAge',Math.round(m.crewAgeMs/1000),m.t);pushHist('defers',m.maxDeferCount,m.t);}
18231
19282
  function sparkSVG(a){var W=100,H=28,p=2;if(!a.length)return'';var vs=a.map(function(x){return x.v;});var mx=Math.max.apply(null,vs),mn=Math.min.apply(null,vs);if(mx===mn)mx=mn+1;var n=a.length;var pts=a.map(function(x,i){var px=n>1?p+i/(n-1)*(W-2*p):W/2;var py=H-p-(x.v-mn)/(mx-mn)*(H-2*p);return[px,py];});var d=pts.map(function(pt,i){return(i?'L':'M')+pt[0].toFixed(1)+' '+pt[1].toFixed(1);}).join(' ');var last=pts[pts.length-1];var area=d+' L'+last[0].toFixed(1)+' '+H+' L'+pts[0][0].toFixed(1)+' '+H+' Z';return'<path class="spark-area" d="'+area+'"/><path class="spark-line" d="'+d+'"/><circle class="spark-dot" cx="'+last[0].toFixed(1)+'" cy="'+last[1].toFixed(1)+'" r="2"/>';}
@@ -18238,9 +19289,19 @@ var CLIENT_JS = `
18238
19289
  function applySearch(){var q=currentSearch.toLowerCase();document.querySelectorAll('[data-panel="live"] .live-row').forEach(function(r){if(!q){if(!hideByFilter(r.getAttribute('data-rollup'),currentFilter))r.hidden=false;return;}var pn=(r.getAttribute('data-project')||'').toLowerCase();var dd=((r.querySelector('.live-detail')||{}).textContent||'').toLowerCase();var rl=r.getAttribute('data-rollup');if(hideByFilter(rl,currentFilter)){r.hidden=true;return;}r.hidden=pn.indexOf(q)===-1&&dd.indexOf(q)===-1;});}
18239
19290
  function applySort(){var table=document.querySelector('[data-panel="live"] [data-live-table]');if(!table)return;var tbody=table.querySelector('tbody');if(!tbody)return;var rows=Array.prototype.slice.call(tbody.querySelectorAll('.live-row'));var dir=sortAsc?1:-1;rows.sort(function(a,b){var va,vb;if(currentSort==='name'){va=(a.getAttribute('data-project')||'').toLowerCase();vb=(b.getAttribute('data-project')||'').toLowerCase();}else if(currentSort==='attention'){var order={gone:0,stale:1,alive:2,unknown:3,stopped:4};va=order[a.getAttribute('data-rollup')]??99;vb=order[b.getAttribute('data-rollup')]??99;if(va===vb){va=(a.getAttribute('data-project')||'').toLowerCase();vb=(b.getAttribute('data-project')||'').toLowerCase();return va<vb?-1:va>vb?1:0;}}else if(currentSort==='activity'){va=parseInt(a.getAttribute('data-max-age'))||0;vb=parseInt(b.getAttribute('data-max-age'))||0;}return va<vb?-dir:va>vb?dir:0;});rows.forEach(function(r){tbody.appendChild(r);});}
18240
19291
  function applyExpand(){document.querySelectorAll('[data-panel="live"] .live-row').forEach(function(r){var pn=r.getAttribute('data-project');var er=document.querySelector('[data-panel="live"] .live-expand[data-project="'+pn+'"]');if(er){var show=!!expandedProjects[pn];er.hidden=!show;r.classList.toggle('expanded',show);}});}
18241
- function refresh(){ingest();applyTab();drawSparks();countUps();applyFilter();applySearch();applySort();applyExpand();}
18242
- document.addEventListener('click',function(e){var t=e.target&&e.target.closest?e.target.closest('[data-tab]'):null;if(t){activeTab=t.getAttribute('data-tab');applyTab();var c=document.getElementById('content');if(c){c.classList.remove('swap');void c.offsetWidth;c.classList.add('swap');}}var fb=e.target&&e.target.closest?e.target.closest('[data-filter]'):null;if(fb){currentFilter=fb.getAttribute('data-filter');applyFilter();applySearch();}var sh=e.target&&e.target.closest?e.target.closest('[data-sort]'):null;if(sh){var key=sh.getAttribute('data-sort');if(key===currentSort)sortAsc=!sortAsc;else{currentSort=key;sortAsc=true;}applySort();}var lr=e.target&&e.target.closest?e.target.closest('[data-panel="live"] .live-row'):null;if(lr&&!e.target.closest('[data-sort]')){var pn=lr.getAttribute('data-project');var er=document.querySelector('[data-panel="live"] .live-expand[data-project="'+pn+'"]');if(er){var show=er.hidden;er.hidden=!show;lr.classList.toggle('expanded',!!show);expandedProjects[pn]=!!show;}}});
18243
- document.addEventListener('keydown',function(e){var ae=document.activeElement;if((e.key==='ArrowRight'||e.key==='ArrowLeft')&&ae&&ae.getAttribute&&ae.getAttribute('data-tab')){var o=['live','overview','projects','daemon','environment'];var i=o.indexOf(activeTab);i=(i+(e.key==='ArrowRight'?1:o.length-1))%o.length;activeTab=o[i];applyTab();var nt=document.querySelector('[data-tab="'+activeTab+'"]');if(nt)nt.focus();e.preventDefault();}});
19292
+ function logSevOf(s){return /error|failed|fatal/i.test(s)?'error':/warn/i.test(s)?'warn':'info';}
19293
+ function logMatch(l){if(logSev==='all')return true;if(logSev==='error')return logSevOf(l)==='error';if(logSev==='warn')return logSevOf(l)!=='info';return true;}
19294
+ function escHtml(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
19295
+ function logVisible(){return logLines.filter(logMatch);}
19296
+ function renderLogs(){var v=document.querySelector('[data-log-view]');if(!v)return;var vis=logVisible();var html='';for(var i=0;i<vis.length;i++){html+='<div class="log-line l-'+logSevOf(vis[i])+'">'+escHtml(vis[i])+'</div>';}v.innerHTML=html||'<div class="empty-log">waiting for log output\u2026</div>';if(logAutoscroll)v.scrollTop=v.scrollHeight;else v.scrollTop=logScrollSave;
19297
+ var sb=document.querySelectorAll('[data-log-sev]');for(var j=0;j<sb.length;j++){var on=sb[j].getAttribute('data-log-sev')===logSev;sb[j].classList.toggle('on',on);sb[j].setAttribute('aria-pressed',on?'true':'false');}
19298
+ var pb=document.querySelector('[data-log-pause]');if(pb){pb.textContent=logPaused?'Resume':'Pause';pb.classList.toggle('on',logPaused);pb.setAttribute('aria-pressed',logPaused?'true':'false');}
19299
+ var st=document.querySelector('[data-log-status]');if(st){if(!logConnected)st.textContent='log stream reconnecting\u2026';else st.textContent=(logPaused?'paused \xB7 ':'')+vis.length+(vis.length===logLines.length?'':'/'+logLines.length)+' lines';}
19300
+ }
19301
+ function ingestLog(lines){if(!lines||!lines.length)return;logLines=logLines.concat(lines);if(logLines.length>LOG_MAX)logLines=logLines.slice(logLines.length-LOG_MAX);if(activeTab==='logs'&&!logPaused)renderLogs();}
19302
+ function refresh(){ingest();applyTab();drawSparks();countUps();applyFilter();applySearch();applySort();applyExpand();if(activeTab==='logs')renderLogs();}
19303
+ document.addEventListener('click',function(e){var t=e.target&&e.target.closest?e.target.closest('[data-tab]'):null;if(t){activeTab=t.getAttribute('data-tab');applyTab();var c=document.getElementById('content');if(c){c.classList.remove('swap');void c.offsetWidth;c.classList.add('swap');}if(activeTab==='logs')renderLogs();}var fb=e.target&&e.target.closest?e.target.closest('[data-filter]'):null;if(fb){currentFilter=fb.getAttribute('data-filter');applyFilter();applySearch();}var sh=e.target&&e.target.closest?e.target.closest('[data-sort]'):null;if(sh){var key=sh.getAttribute('data-sort');if(key===currentSort)sortAsc=!sortAsc;else{currentSort=key;sortAsc=true;}applySort();}var sv=e.target&&e.target.closest?e.target.closest('[data-log-sev]'):null;if(sv){logSev=sv.getAttribute('data-log-sev');renderLogs();}var lp=e.target&&e.target.closest?e.target.closest('[data-log-pause]'):null;if(lp){logPaused=!logPaused;renderLogs();}var cp=e.target&&e.target.closest?e.target.closest('[data-log-copy]'):null;if(cp&&navigator.clipboard){navigator.clipboard.writeText(logVisible().join('\\n'));}var lr=e.target&&e.target.closest?e.target.closest('[data-panel="live"] .live-row'):null;if(lr&&!e.target.closest('[data-sort]')){var pn=lr.getAttribute('data-project');var er=document.querySelector('[data-panel="live"] .live-expand[data-project="'+pn+'"]');if(er){var show=er.hidden;er.hidden=!show;lr.classList.toggle('expanded',!!show);expandedProjects[pn]=!!show;}}});
19304
+ document.addEventListener('keydown',function(e){var ae=document.activeElement;if((e.key==='ArrowRight'||e.key==='ArrowLeft')&&ae&&ae.getAttribute&&ae.getAttribute('data-tab')){var o=['live','overview','projects','daemon','environment','logs'];var i=o.indexOf(activeTab);i=(i+(e.key==='ArrowRight'?1:o.length-1))%o.length;activeTab=o[i];applyTab();var nt=document.querySelector('[data-tab="'+activeTab+'"]');if(nt)nt.focus();e.preventDefault();}});
18244
19305
  document.addEventListener('input',function(e){var sb=e.target&&e.target.closest?e.target.closest('[data-live-search]'):null;if(sb){currentSearch=sb.value;applySearch();}});
18245
19306
  var led=document.getElementById('led');var conn=document.getElementById('conn');var updated=document.getElementById('updated');
18246
19307
  function tickAge(){if(!generatedAt)return;var s=Math.max(0,Math.round((Date.now()-generatedAt)/1000));updated.textContent='updated '+s+'s ago';}
@@ -18249,6 +19310,11 @@ var CLIENT_JS = `
18249
19310
  es.onopen=function(){conn.textContent='live';conn.className='conn live';led.className='led live';};
18250
19311
  es.onerror=function(){conn.textContent='reconnecting';conn.className='conn down';led.className='led down';};
18251
19312
  es.onmessage=function(e){try{var d=JSON.parse(e.data);document.getElementById('content').innerHTML=d.contentHtml;generatedAt=d.generatedAt;tickAge();refresh();}catch(_){}};
19313
+ document.addEventListener('scroll',function(e){var v=e.target;if(v&&v.getAttribute&&v.getAttribute('data-log-view')!==null){logAutoscroll=v.scrollTop+v.clientHeight>=v.scrollHeight-6;logScrollSave=v.scrollTop;}},true);
19314
+ var les=new EventSource('/logs');
19315
+ les.onopen=function(){logConnected=true;renderLogs();};
19316
+ les.onerror=function(){logConnected=false;renderLogs();};
19317
+ les.onmessage=function(e){try{var d=JSON.parse(e.data);if(!logPaused)ingestLog(d&&d.lines);}catch(_){}};
18252
19318
  refresh();tickAge();
18253
19319
  })();
18254
19320
  `.trim();
@@ -18271,7 +19337,100 @@ function renderHtml(snap, opts = {}) {
18271
19337
  ].join("");
18272
19338
  }
18273
19339
 
19340
+ // packages/web/dist/log-tail.js
19341
+ import { closeSync as closeSync3, openSync as openSync3, readSync as readSync2, statSync as statSync4 } from "fs";
19342
+ var LOG_TAIL_BYTES = 256 * 1024;
19343
+ var LOG_RING_SIZE = 1e3;
19344
+ function defaultSize(path39) {
19345
+ try {
19346
+ return statSync4(path39).size;
19347
+ } catch {
19348
+ return 0;
19349
+ }
19350
+ }
19351
+ function defaultRead(path39, start, len) {
19352
+ if (len <= 0)
19353
+ return "";
19354
+ try {
19355
+ const fd = openSync3(path39, "r");
19356
+ try {
19357
+ const buf = Buffer.alloc(len);
19358
+ const n = readSync2(fd, buf, 0, len, start);
19359
+ return buf.subarray(0, n).toString("utf-8");
19360
+ } finally {
19361
+ closeSync3(fd);
19362
+ }
19363
+ } catch {
19364
+ return "";
19365
+ }
19366
+ }
19367
+ var LogTailer = class {
19368
+ path;
19369
+ ringSize;
19370
+ offset = 0;
19371
+ pending = "";
19372
+ ring = [];
19373
+ size;
19374
+ read;
19375
+ constructor(path39, ringSize = LOG_RING_SIZE, deps = {}) {
19376
+ this.path = path39;
19377
+ this.ringSize = ringSize;
19378
+ this.size = deps.size ?? defaultSize;
19379
+ this.read = deps.read ?? defaultRead;
19380
+ }
19381
+ /** Seed history from the last `maxBytes` and position the cursor at EOF.
19382
+ * Idempotent; call once at startup. A trailing partial line stays pending
19383
+ * so the next append completes it without loss or duplication. */
19384
+ init(maxBytes = LOG_TAIL_BYTES) {
19385
+ const size = this.size(this.path);
19386
+ this.pending = "";
19387
+ this.ring = [];
19388
+ if (size <= 0) {
19389
+ this.offset = size;
19390
+ return;
19391
+ }
19392
+ const start = Math.max(0, size - maxBytes);
19393
+ const parts = this.read(this.path, start, size - start).split("\n");
19394
+ const rest = parts.pop() ?? "";
19395
+ if (start > 0 && parts.length)
19396
+ parts.shift();
19397
+ this.push(parts);
19398
+ this.pending = rest;
19399
+ this.offset = size;
19400
+ }
19401
+ /** Complete lines appended since the previous poll. */
19402
+ poll() {
19403
+ const size = this.size(this.path);
19404
+ if (size < this.offset) {
19405
+ this.offset = 0;
19406
+ this.pending = "";
19407
+ }
19408
+ if (size <= this.offset)
19409
+ return [];
19410
+ const text = this.read(this.path, this.offset, size - this.offset);
19411
+ if (!text)
19412
+ return [];
19413
+ this.offset += Buffer.byteLength(text);
19414
+ const parts = (this.pending + text).split("\n");
19415
+ this.pending = parts.pop() ?? "";
19416
+ this.push(parts);
19417
+ return parts;
19418
+ }
19419
+ /** The buffered backlog (oldest first) replayed to a newly-connected client. */
19420
+ backlog() {
19421
+ return this.ring.slice();
19422
+ }
19423
+ push(lines) {
19424
+ for (const l of lines)
19425
+ this.ring.push(l);
19426
+ const over = this.ring.length - this.ringSize;
19427
+ if (over > 0)
19428
+ this.ring.splice(0, over);
19429
+ }
19430
+ };
19431
+
18274
19432
  // packages/web/dist/web-server.js
19433
+ var LOG_POLL_MS = 1e3;
18275
19434
  async function querySnapshot(sockPath) {
18276
19435
  try {
18277
19436
  const reply = await sendRequest(sockPath, { kind: "snapshot" });
@@ -18284,6 +19443,9 @@ async function startWebServer(opts) {
18284
19443
  const host = opts.host ?? "127.0.0.1";
18285
19444
  const now = opts.now ?? (() => Date.now());
18286
19445
  const clients = /* @__PURE__ */ new Set();
19446
+ const logClients = /* @__PURE__ */ new Set();
19447
+ const logPath2 = opts.logPath ?? join29(dirname7(opts.sockPath), "squadrantd.log");
19448
+ const tailer = new LogTailer(logPath2);
18287
19449
  let latest = null;
18288
19450
  async function tick() {
18289
19451
  const daemon = await querySnapshot(opts.sockPath);
@@ -18299,6 +19461,19 @@ async function startWebServer(opts) {
18299
19461
  }
18300
19462
  }
18301
19463
  }
19464
+ function broadcastLog(lines) {
19465
+ if (lines.length === 0)
19466
+ return;
19467
+ const payload = `data: ${JSON.stringify({ lines })}
19468
+
19469
+ `;
19470
+ for (const res of logClients) {
19471
+ try {
19472
+ res.write(payload);
19473
+ } catch {
19474
+ }
19475
+ }
19476
+ }
18302
19477
  const server = createServer5((req, res) => {
18303
19478
  if (req.method !== "GET") {
18304
19479
  res.writeHead(405).end();
@@ -18324,6 +19499,22 @@ async function startWebServer(opts) {
18324
19499
  req.on("close", () => clients.delete(res));
18325
19500
  return;
18326
19501
  }
19502
+ if (req.url === "/logs") {
19503
+ res.writeHead(200, {
19504
+ "Content-Type": "text/event-stream",
19505
+ "Cache-Control": "no-cache",
19506
+ Connection: "keep-alive"
19507
+ });
19508
+ res.write("retry: 3000\n\n");
19509
+ const backlog = tailer.backlog();
19510
+ if (backlog.length)
19511
+ res.write(`data: ${JSON.stringify({ lines: backlog })}
19512
+
19513
+ `);
19514
+ logClients.add(res);
19515
+ req.on("close", () => logClients.delete(res));
19516
+ return;
19517
+ }
18327
19518
  res.writeHead(404).end();
18328
19519
  });
18329
19520
  await new Promise((resolve4, reject) => {
@@ -18338,17 +19529,33 @@ async function startWebServer(opts) {
18338
19529
  });
18339
19530
  }, opts.intervalMs);
18340
19531
  timer.unref?.();
19532
+ tailer.init();
19533
+ const logTimer = setInterval(() => {
19534
+ try {
19535
+ broadcastLog(tailer.poll());
19536
+ } catch {
19537
+ }
19538
+ }, LOG_POLL_MS);
19539
+ logTimer.unref?.();
18341
19540
  return {
18342
19541
  port: boundPort,
18343
19542
  close: () => new Promise((resolve4) => {
18344
19543
  clearInterval(timer);
19544
+ clearInterval(logTimer);
18345
19545
  for (const res of clients) {
18346
19546
  try {
18347
19547
  res.end();
18348
19548
  } catch {
18349
19549
  }
18350
19550
  }
19551
+ for (const res of logClients) {
19552
+ try {
19553
+ res.end();
19554
+ } catch {
19555
+ }
19556
+ }
18351
19557
  clients.clear();
19558
+ logClients.clear();
18352
19559
  server.close(() => resolve4());
18353
19560
  })
18354
19561
  };
@@ -18358,7 +19565,7 @@ async function startWebServer(opts) {
18358
19565
  init_dist();
18359
19566
  var SOCK3 = DAEMON_SOCK_PATH;
18360
19567
  function detectCurrentWorkspace2() {
18361
- const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
19568
+ const out = execSync11(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
18362
19569
  const match = out.match(/workspace:\d+/);
18363
19570
  if (!match) {
18364
19571
  throw new Error("Could not detect current cmux workspace. Run `squadrant dashboard --pane` from inside a cmux workspace.");
@@ -18441,8 +19648,8 @@ init_dist3();
18441
19648
  init_dist4();
18442
19649
  init_dist2();
18443
19650
  import { Command as Command14 } from "commander";
18444
- import { execSync as execSync11, execFileSync as execFileSync10 } from "child_process";
18445
- import fs23 from "fs";
19651
+ import { execSync as execSync12, execFileSync as execFileSync10 } from "child_process";
19652
+ import fs24 from "fs";
18446
19653
  import path28 from "path";
18447
19654
  import os16 from "os";
18448
19655
  import chalk15 from "chalk";
@@ -18528,6 +19735,10 @@ function resolveLaunchAgent(overrides, roleConfig, roleModelDefault) {
18528
19735
  thinking: overrides.thinking ?? roleConfig?.thinking
18529
19736
  };
18530
19737
  }
19738
+ function resolveLaunchBackend(o) {
19739
+ const roleBackend = o.agentName === "claude" ? o.roleConfig?.backend : void 0;
19740
+ return o.backendOverride ?? roleBackend ?? "native";
19741
+ }
18531
19742
 
18532
19743
  // packages/cli/src/commands/launch.ts
18533
19744
  init_dist2();
@@ -18569,17 +19780,24 @@ function resolveAnthropicRefusal(role, workspaceName, agentName, model, readOpen
18569
19780
  function ensureCmuxReady(headless) {
18570
19781
  if (headless || isInsideCmux()) return;
18571
19782
  console.log(chalk15.yellow("\n Not running inside cmux. Opening cmux app...\n"));
18572
- execSync11(`open "${CMUX_APP}"`, { stdio: "inherit" });
19783
+ execSync12(`open "${CMUX_APP}"`, { stdio: "inherit" });
18573
19784
  console.log(chalk15.bold(" Run `squadrant launch` from inside a cmux workspace.\n"));
18574
19785
  process.exit(0);
18575
19786
  }
18576
19787
  var launchCommand = new Command14("launch").description(
18577
19788
  "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) => {
19789
+ ).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
19790
  if (opts.fresh && opts.keep) {
18580
19791
  console.error(chalk15.red("\n \u2718 --fresh and --keep are mutually exclusive\n"));
18581
19792
  process.exit(1);
18582
19793
  }
19794
+ if (opts.backend !== void 0 && !isBackendMode(opts.backend)) {
19795
+ console.error(chalk15.red(`
19796
+ \u2718 Invalid --backend value '${opts.backend}'. Valid values: native, direct, proxy
19797
+ `));
19798
+ process.exit(1);
19799
+ }
19800
+ const backendOverride = opts.backend;
18583
19801
  let thinkingOverride;
18584
19802
  try {
18585
19803
  thinkingOverride = opts.thinking ? parseThinkingLevel(opts.thinking) : void 0;
@@ -18606,6 +19824,7 @@ var launchCommand = new Command14("launch").description(
18606
19824
  roleConfig,
18607
19825
  config.defaults.models?.[role]
18608
19826
  );
19827
+ const backend = resolveLaunchBackend({ backendOverride, agentName, roleConfig });
18609
19828
  const refusal = resolveAnthropicRefusal(role, workspaceName, agentName, model);
18610
19829
  if (refusal) {
18611
19830
  console.error(chalk15.red(`
@@ -18635,6 +19854,29 @@ var launchCommand = new Command14("launch").description(
18635
19854
  if (isOpencodeCaptain && projectName) {
18636
19855
  captainOpencodeConfigPath = writePerCrewOpencodeConfig({ stateRoot, project: projectName, taskId: "captain" });
18637
19856
  }
19857
+ let route;
19858
+ if (role === "captain") {
19859
+ try {
19860
+ route = await prepareCaptainRoute({
19861
+ backend,
19862
+ agentName,
19863
+ configuredPermissionMode: permissionMode,
19864
+ project: projectName ?? "",
19865
+ model,
19866
+ stateRoot,
19867
+ config,
19868
+ deps: {
19869
+ routerCredentials: (o) => fetchRouterCredentials(o.project, o.backend, { call: squadrantdCall }),
19870
+ writeRouterSettings
19871
+ },
19872
+ warn: (m) => console.error(chalk15.yellow(` \u26A0 ${m}`))
19873
+ });
19874
+ } catch (err) {
19875
+ console.error(chalk15.red(` \u2718 ${err.message}`));
19876
+ hadFailure = true;
19877
+ return;
19878
+ }
19879
+ }
18638
19880
  const priorRecord = projectName ? readCaptainAddress(stateRoot, projectName) : null;
18639
19881
  const captainPort = isOpencodeCaptain ? await getFreePort() : void 0;
18640
19882
  let opencodeResumeSessionId;
@@ -18660,15 +19902,18 @@ var launchCommand = new Command14("launch").description(
18660
19902
  registry,
18661
19903
  role,
18662
19904
  forceFresh,
18663
- permissionMode,
18664
- model,
19905
+ route?.permissionMode ?? permissionMode,
19906
+ route?.model ?? model,
18665
19907
  TEMPLATES_DIR4,
18666
19908
  resolveCaptainSocketPath(captainChannelEnabled, projectName, workspaceName),
18667
19909
  resolveCaptainSessionName(agentName, projectName),
18668
19910
  thinking,
18669
- captainBoot
19911
+ captainBoot,
19912
+ route?.settingsPath
18670
19913
  );
18671
- return captainOpencodeConfigPath ? `OPENCODE_CONFIG=${captainOpencodeConfigPath} ${baseCmd}` : baseCmd;
19914
+ const gatePrefix = route && Object.keys(route.env).length > 0 ? `${renderEnvAssignments(route.env)} ` : "";
19915
+ const cmd = `${gatePrefix}${baseCmd}`;
19916
+ return captainOpencodeConfigPath ? `OPENCODE_CONFIG=${captainOpencodeConfigPath} ${cmd}` : cmd;
18672
19917
  },
18673
19918
  initialPrompt,
18674
19919
  runtime,
@@ -18732,12 +19977,12 @@ var launchCommand = new Command14("launch").description(
18732
19977
  }
18733
19978
  if (opts.all) {
18734
19979
  const hubPath = resolveHome(config.hubVault);
18735
- fs23.mkdirSync(hubPath, { recursive: true });
19980
+ fs24.mkdirSync(hubPath, { recursive: true });
18736
19981
  console.log(chalk15.bold("\nLaunching all captain workspaces\n"));
18737
19982
  for (const [name, proj] of Object.entries(config.projects)) {
18738
19983
  const projPath = resolveHome(proj.path);
18739
19984
  const spokePath = resolveHome(proj.spokeVault);
18740
- if (!fs23.existsSync(spokePath)) {
19985
+ if (!fs24.existsSync(spokePath)) {
18741
19986
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(name, config);
18742
19987
  await ensureSpokeLayout(spokeDriver);
18743
19988
  console.log(chalk15.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -18775,7 +20020,7 @@ Launching ${selected.length} captain workspace(s) in parallel
18775
20020
  const proj = config.projects[name];
18776
20021
  const projPath = resolveHome(proj.path);
18777
20022
  const spokePath = resolveHome(proj.spokeVault);
18778
- if (!fs23.existsSync(spokePath)) {
20023
+ if (!fs24.existsSync(spokePath)) {
18779
20024
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(name, config);
18780
20025
  await ensureSpokeLayout(spokeDriver);
18781
20026
  console.log(chalk15.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -18799,7 +20044,7 @@ Launching ${selected.length} captain workspace(s) in parallel
18799
20044
  const proj = config.projects[project];
18800
20045
  const projPath = resolveHome(proj.path);
18801
20046
  const spokePath = resolveHome(proj.spokeVault);
18802
- if (!fs23.existsSync(spokePath)) {
20047
+ if (!fs24.existsSync(spokePath)) {
18803
20048
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(project, config);
18804
20049
  await ensureSpokeLayout(spokeDriver);
18805
20050
  console.log(chalk15.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -18935,24 +20180,24 @@ Shutting down captain workspace for '${project}'...
18935
20180
  // packages/cli/src/commands/feedback.ts
18936
20181
  init_dist();
18937
20182
  import { Command as Command16 } from "commander";
18938
- import fs24 from "fs";
20183
+ import fs25 from "fs";
18939
20184
  import os17 from "os";
18940
20185
  import path29 from "path";
18941
20186
  import { fileURLToPath as fileURLToPath3 } from "url";
18942
- import { execSync as execSync12 } from "child_process";
20187
+ import { execSync as execSync13 } from "child_process";
18943
20188
  import chalk17 from "chalk";
18944
20189
  var REPO_URL = "https://github.com/tu11aa/squadrant";
18945
20190
  function readPkgVersion() {
18946
20191
  try {
18947
20192
  const pkgPath = path29.join(path29.dirname(fileURLToPath3(import.meta.url)), "..", "package.json");
18948
- return JSON.parse(fs24.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
20193
+ return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
18949
20194
  } catch {
18950
20195
  return "unknown";
18951
20196
  }
18952
20197
  }
18953
20198
  function readMetrics(metricsPath) {
18954
20199
  try {
18955
- return JSON.parse(fs24.readFileSync(metricsPath, "utf-8"));
20200
+ return JSON.parse(fs25.readFileSync(metricsPath, "utf-8"));
18956
20201
  } catch {
18957
20202
  return {};
18958
20203
  }
@@ -18998,7 +20243,7 @@ var feedbackCommand = new Command16("feedback").description("Open a pre-filled G
18998
20243
  console.log(chalk17.dim(` URL: ${issueUrl.substring(0, 80)}...
18999
20244
  `));
19000
20245
  try {
19001
- execSync12(`open "${issueUrl}"`, { stdio: "ignore" });
20246
+ execSync13(`open "${issueUrl}"`, { stdio: "ignore" });
19002
20247
  console.log(chalk17.green(" \u2714 Browser opened\n"));
19003
20248
  } catch {
19004
20249
  console.log(chalk17.yellow(" \u26A0 Could not open browser automatically."));
@@ -19400,7 +20645,7 @@ init_dist4();
19400
20645
  init_dist();
19401
20646
  import { Command as Command21 } from "commander";
19402
20647
  import chalk22 from "chalk";
19403
- import fs25 from "fs";
20648
+ import fs26 from "fs";
19404
20649
  import path30 from "path";
19405
20650
  import { fileURLToPath as fileURLToPath4 } from "url";
19406
20651
  function parseScope(v2) {
@@ -19412,7 +20657,7 @@ function parseScope(v2) {
19412
20657
  function findPackageRoot3() {
19413
20658
  let dir = path30.dirname(fileURLToPath4(import.meta.url));
19414
20659
  while (dir !== "/" && dir !== "") {
19415
- if (fs25.existsSync(path30.join(dir, "package.json"))) return dir;
20660
+ if (fs26.existsSync(path30.join(dir, "package.json"))) return dir;
19416
20661
  dir = path30.dirname(dir);
19417
20662
  }
19418
20663
  return process.cwd();
@@ -19609,12 +20854,12 @@ init_dist();
19609
20854
  init_dist();
19610
20855
  init_dist2();
19611
20856
  import { Command as Command23 } from "commander";
19612
- import fs26 from "fs";
20857
+ import fs27 from "fs";
19613
20858
  import { fileURLToPath as fileURLToPath5 } from "url";
19614
- import { dirname as dirname7, join as join29 } from "path";
20859
+ import { dirname as dirname8, join as join30 } from "path";
19615
20860
  import chalk23 from "chalk";
19616
20861
  function runConfigCheck(opts) {
19617
- const raw = JSON.parse(fs26.readFileSync(opts.configPath, "utf-8"));
20862
+ const raw = JSON.parse(fs27.readFileSync(opts.configPath, "utf-8"));
19618
20863
  const def = getDefaultConfig();
19619
20864
  const items = detectDrift(raw, def);
19620
20865
  let working = raw;
@@ -19702,7 +20947,7 @@ function printItems(items) {
19702
20947
  var configCommand = new Command23("config").description("Inspect and reconcile squadrant config");
19703
20948
  configCommand.command("check").description("Detect config drift vs the current default schema").option("--fix", "Apply the safe tier (add missing, remove deprecated)", false).option("--accept", "Stamp the current version without changing config (dismiss advisories)", false).option("--json", "Output drift items as JSON", false).action((opts) => {
19704
20949
  const pkgVersion = readPkgVersion2();
19705
- if (!fs26.existsSync(DEFAULT_CONFIG_PATH)) {
20950
+ if (!fs27.existsSync(DEFAULT_CONFIG_PATH)) {
19706
20951
  console.log(chalk23.yellow("No config found \u2014 run `squadrant init` first."));
19707
20952
  return;
19708
20953
  }
@@ -19747,14 +20992,14 @@ configCommand.command("set").description("Write a config value by dotted key (e.
19747
20992
  }
19748
20993
  });
19749
20994
  function readPkgVersion2() {
19750
- const pkgPath = join29(dirname7(fileURLToPath5(import.meta.url)), "..", "package.json");
19751
- return JSON.parse(fs26.readFileSync(pkgPath, "utf-8")).version;
20995
+ const pkgPath = join30(dirname8(fileURLToPath5(import.meta.url)), "..", "package.json");
20996
+ return JSON.parse(fs27.readFileSync(pkgPath, "utf-8")).version;
19752
20997
  }
19753
20998
 
19754
20999
  // packages/cli/src/commands/heal.ts
19755
21000
  init_dist();
19756
21001
  import { execFileSync as execFileSync11 } from "child_process";
19757
- import { join as join30 } from "path";
21002
+ import { join as join31 } from "path";
19758
21003
  import { homedir as homedir17 } from "os";
19759
21004
  import { Command as Command24 } from "commander";
19760
21005
  import chalk24 from "chalk";
@@ -19994,8 +21239,8 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
19994
21239
  }
19995
21240
  const config = loadConfig();
19996
21241
  const projects = opts.all ? Object.keys(config.projects) : [project];
19997
- const stateRoot = join30(homedir17(), ".config", "squadrant", "state");
19998
- const registry = new LivenessRegistry({ path: join30(stateRoot, "liveness.json") });
21242
+ const stateRoot = join31(homedir17(), ".config", "squadrant", "state");
21243
+ const registry = new LivenessRegistry({ path: join31(stateRoot, "liveness.json") });
19999
21244
  registry.load();
20000
21245
  const code = await runHealCaptain(projects, {
20001
21246
  liveness: readCmuxLiveness,
@@ -20027,7 +21272,7 @@ import chalk26 from "chalk";
20027
21272
  init_dist();
20028
21273
  init_dist2();
20029
21274
  import { Command as Command25 } from "commander";
20030
- import { execSync as execSync13 } from "child_process";
21275
+ import { execSync as execSync14 } from "child_process";
20031
21276
  import chalk25 from "chalk";
20032
21277
  async function runDispatch(toProject, task, opts) {
20033
21278
  const fromProject = resolveCurrentProject(loadConfig());
@@ -20043,7 +21288,7 @@ async function runDispatch(toProject, task, opts) {
20043
21288
  warmupTimeoutMs: opts.warmupTimeout,
20044
21289
  bootCaptain: async (project) => {
20045
21290
  try {
20046
- execSync13(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
21291
+ execSync14(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
20047
21292
  } catch {
20048
21293
  throw new Error(`failed to launch captain for '${project}' \u2014 is squadrant installed?`);
20049
21294
  }
@@ -20079,7 +21324,7 @@ init_dist();
20079
21324
  init_dist2();
20080
21325
  init_runtime2();
20081
21326
  init_require_daemon();
20082
- import { join as join32, dirname as dirname8 } from "path";
21327
+ import { join as join33, dirname as dirname9 } from "path";
20083
21328
  import { homedir as homedir18 } from "os";
20084
21329
  import { Command as Command27 } from "commander";
20085
21330
  import chalk28 from "chalk";
@@ -20088,23 +21333,23 @@ import chalk28 from "chalk";
20088
21333
  init_dist3();
20089
21334
  init_dist2();
20090
21335
  import { createServer as createServer6, connect as netConnect2 } from "net";
20091
- import fs27 from "fs";
20092
- import { join as join31 } from "path";
21336
+ import fs28 from "fs";
21337
+ import { join as join32 } from "path";
20093
21338
  import { randomUUID as randomUUID7 } from "crypto";
20094
21339
  import chalk27 from "chalk";
20095
21340
  var shared;
20096
- var registryEntryPath = () => join31(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
21341
+ var registryEntryPath = () => join32(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
20097
21342
  function unregisterSenderIdentity() {
20098
21343
  try {
20099
- fs27.unlinkSync(registryEntryPath());
21344
+ fs28.unlinkSync(registryEntryPath());
20100
21345
  } catch {
20101
21346
  }
20102
21347
  }
20103
21348
  function registerSenderIdentity(socketPath2) {
20104
21349
  try {
20105
21350
  unregisterSenderIdentity();
20106
- fs27.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
20107
- fs27.writeFileSync(
21351
+ fs28.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
21352
+ fs28.writeFileSync(
20108
21353
  registryEntryPath(),
20109
21354
  JSON.stringify({
20110
21355
  pid: process.pid,
@@ -20130,7 +21375,7 @@ async function sharedReceiptListener() {
20130
21375
  // must never be the reason we refuse to start.
20131
21376
  unlinkStale: (p) => {
20132
21377
  try {
20133
- fs27.unlinkSync(p);
21378
+ fs28.unlinkSync(p);
20134
21379
  } catch {
20135
21380
  }
20136
21381
  },
@@ -20200,7 +21445,7 @@ async function runPing(project, message) {
20200
21445
  await requireDaemon();
20201
21446
  await needRef(resolved);
20202
21447
  const mode = resolveCaptainChannelMode(config.defaults);
20203
- const stateRoot = join32(homedir18(), ".config", "squadrant", "state");
21448
+ const stateRoot = join33(homedir18(), ".config", "squadrant", "state");
20204
21449
  const { channels, agentFor } = mode === "off" ? { channels: {}, agentFor: () => void 0 } : await buildCaptainChannels({ stateRoot, configAgent: config.defaults.roles?.captain?.agent });
20205
21450
  const channel = channels[agentFor(project) ?? ""];
20206
21451
  const { handled, outcome } = await deliverToCaptain(project, message, {
@@ -20209,7 +21454,7 @@ async function runPing(project, message) {
20209
21454
  log: (m) => console.error(chalk28.dim(m))
20210
21455
  });
20211
21456
  if (!handled) {
20212
- const stateRoot2 = join32(dirname8(DEFAULT_CONFIG_PATH), "state");
21457
+ const stateRoot2 = join33(dirname9(DEFAULT_CONFIG_PATH), "state");
20213
21458
  await appendCaptainMessage({
20214
21459
  stateRoot: stateRoot2,
20215
21460
  project,
@@ -20288,7 +21533,7 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
20288
21533
  // packages/cli/src/commands/effort.ts
20289
21534
  init_dist();
20290
21535
  init_dist2();
20291
- import fs28 from "fs";
21536
+ import fs29 from "fs";
20292
21537
  import path31 from "path";
20293
21538
  import { Command as Command29 } from "commander";
20294
21539
  import chalk30 from "chalk";
@@ -20332,7 +21577,7 @@ function effortScopeLabel(projectName) {
20332
21577
  }
20333
21578
  function canonical(p) {
20334
21579
  try {
20335
- return fs28.realpathSync(p);
21580
+ return fs29.realpathSync(p);
20336
21581
  } catch {
20337
21582
  return path31.resolve(p);
20338
21583
  }
@@ -20394,7 +21639,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
20394
21639
 
20395
21640
  // packages/cli/src/commands/tokens.ts
20396
21641
  init_dist();
20397
- import fs29 from "fs";
21642
+ import fs30 from "fs";
20398
21643
  import path32 from "path";
20399
21644
  import os18 from "os";
20400
21645
  import readline3 from "readline";
@@ -20450,7 +21695,7 @@ function foldTranscriptLine(agg, rawLine, state) {
20450
21695
  async function aggregateTranscriptFile(filePath) {
20451
21696
  const agg = emptySessionAggregate();
20452
21697
  const state = { lastCacheRead: null };
20453
- const rl = readline3.createInterface({ input: fs29.createReadStream(filePath), crlfDelay: Infinity });
21698
+ const rl = readline3.createInterface({ input: fs30.createReadStream(filePath), crlfDelay: Infinity });
20454
21699
  for await (const line of rl) {
20455
21700
  foldTranscriptLine(agg, line, state);
20456
21701
  }
@@ -20518,7 +21763,7 @@ function buildRoleReport(role, sessions) {
20518
21763
  }
20519
21764
  async function readdirSafe(dir) {
20520
21765
  try {
20521
- return await fs29.promises.readdir(dir);
21766
+ return await fs30.promises.readdir(dir);
20522
21767
  } catch {
20523
21768
  return [];
20524
21769
  }
@@ -20685,12 +21930,12 @@ var tokensCommand = new Command30("tokens").description(
20685
21930
  // packages/cli/src/commands/telegram.ts
20686
21931
  init_dist();
20687
21932
  init_dist2();
20688
- import { join as join33, dirname as dirname9 } from "path";
21933
+ import { join as join34, dirname as dirname10 } from "path";
20689
21934
  import { emitKeypressEvents } from "readline";
20690
21935
  import { Command as Command31 } from "commander";
20691
21936
  import chalk32 from "chalk";
20692
21937
  function defaultStateRoot() {
20693
- return join33(dirname9(DEFAULT_CONFIG_PATH), "state");
21938
+ return join34(dirname10(DEFAULT_CONFIG_PATH), "state");
20694
21939
  }
20695
21940
  async function questionMasked() {
20696
21941
  return new Promise((resolve4) => {
@@ -21019,7 +22264,7 @@ import { Command as Command33 } from "commander";
21019
22264
  import { homedir as homedir19 } from "os";
21020
22265
 
21021
22266
  // packages/cli/src/lib/captain-session-registry.ts
21022
- import fs30 from "fs";
22267
+ import fs31 from "fs";
21023
22268
  import path33 from "path";
21024
22269
 
21025
22270
  // packages/cli/src/lib/handoff-facts.ts
@@ -21071,15 +22316,15 @@ function assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, now, ext
21071
22316
  // packages/cli/src/lib/captain-session-registry.ts
21072
22317
  var CAPTAIN_SESSION_REGISTRY_FILE = "captain-sessions.jsonl";
21073
22318
  function appendCaptainSession(spokeVault, record) {
21074
- fs30.mkdirSync(spokeVault, { recursive: true });
22319
+ fs31.mkdirSync(spokeVault, { recursive: true });
21075
22320
  const file = path33.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
21076
- fs30.appendFileSync(file, JSON.stringify(record) + "\n");
22321
+ fs31.appendFileSync(file, JSON.stringify(record) + "\n");
21077
22322
  }
21078
22323
  function readCaptainSessionRegistry(spokeVault) {
21079
22324
  const file = path33.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
21080
- if (!fs30.existsSync(file)) return [];
22325
+ if (!fs31.existsSync(file)) return [];
21081
22326
  const records = [];
21082
- for (const line of fs30.readFileSync(file, "utf-8").split("\n")) {
22327
+ for (const line of fs31.readFileSync(file, "utf-8").split("\n")) {
21083
22328
  if (!line.trim()) continue;
21084
22329
  try {
21085
22330
  records.push(JSON.parse(line));
@@ -21110,9 +22355,8 @@ async function runGatePermissionRequest(deps) {
21110
22355
  const p = payload;
21111
22356
  const taskId = env.SQUADRANT_CREW_TASK_ID;
21112
22357
  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" };
22358
+ if (!isGateSession(env)) {
22359
+ return { decision: "yield", reason: "not a squadrant crew/side/captain session" };
21116
22360
  }
21117
22361
  const stdout = deps.stdout ?? (() => {
21118
22362
  });
@@ -21457,7 +22701,7 @@ import os19 from "os";
21457
22701
  // packages/cli/src/lib/handoff-live-repo.ts
21458
22702
  init_dist();
21459
22703
  import { execFileSync as execFileSync12 } from "child_process";
21460
- import fs31 from "fs";
22704
+ import fs32 from "fs";
21461
22705
  import path35 from "path";
21462
22706
 
21463
22707
  // packages/cli/src/lib/handoff-branch-state.ts
@@ -21597,7 +22841,7 @@ function localAheadOfBase(runner, projectPath, base) {
21597
22841
  }
21598
22842
  function readFetchAgeMs(projectPath, now) {
21599
22843
  try {
21600
- const stat2 = fs31.statSync(path35.join(projectPath, ".git", "FETCH_HEAD"));
22844
+ const stat2 = fs32.statSync(path35.join(projectPath, ".git", "FETCH_HEAD"));
21601
22845
  return Math.max(0, now - stat2.mtime.getTime());
21602
22846
  } catch {
21603
22847
  return null;
@@ -21668,7 +22912,7 @@ function gatherLiveRepoState(projectPath, fallbackBaseBranch, tasks, runner = de
21668
22912
 
21669
22913
  // packages/cli/src/lib/handoff-claude-mem.ts
21670
22914
  import { createRequire } from "module";
21671
- import fs32 from "fs";
22915
+ import fs33 from "fs";
21672
22916
  var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
21673
22917
  var CLAUDE_MEM_RECENCY_LIMIT = 20;
21674
22918
  function decisionText(row) {
@@ -21682,7 +22926,7 @@ function decisionText(row) {
21682
22926
  return row.narrative ?? "";
21683
22927
  }
21684
22928
  function queryClaudeMem(dbPath, project) {
21685
- if (!fs32.existsSync(dbPath)) return null;
22929
+ if (!fs33.existsSync(dbPath)) return null;
21686
22930
  let db;
21687
22931
  try {
21688
22932
  db = new DatabaseSync(dbPath, { readOnly: true });
@@ -21725,7 +22969,7 @@ function queryClaudeMem(dbPath, project) {
21725
22969
  }
21726
22970
 
21727
22971
  // packages/cli/src/lib/handoff-transcript.ts
21728
- import fs33 from "fs";
22972
+ import fs34 from "fs";
21729
22973
  var TRANSCRIPT_BYTE_CAP = 2e5;
21730
22974
  var DIGEST_BYTE_CAP = 8 * 1024;
21731
22975
  var PROMPT_TRUNCATE_CHARS = 300;
@@ -21834,27 +23078,27 @@ function extractMessages(tailText) {
21834
23078
  return { lastUserMessage, lastAssistantText, digest };
21835
23079
  }
21836
23080
  function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
21837
- if (!fs33.existsSync(transcriptPath)) return null;
21838
- const content = fs33.readFileSync(transcriptPath, "utf-8");
23081
+ if (!fs34.existsSync(transcriptPath)) return null;
23082
+ const content = fs34.readFileSync(transcriptPath, "utf-8");
21839
23083
  const { lastUserMessage, lastAssistantText, digest } = extractMessages(tailOf(content, byteCap));
21840
- const mtimeIso = fs33.statSync(transcriptPath).mtime.toISOString();
23084
+ const mtimeIso = fs34.statSync(transcriptPath).mtime.toISOString();
21841
23085
  return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText, digest };
21842
23086
  }
21843
23087
 
21844
23088
  // packages/cli/src/lib/handoff-archive.ts
21845
- import fs34 from "fs";
23089
+ import fs35 from "fs";
21846
23090
  import path36 from "path";
21847
23091
  function readNewestArchivedHandoff(spokeVault, now) {
21848
23092
  const dir = path36.join(spokeVault, "handoffs");
21849
- if (!fs34.existsSync(dir)) return null;
21850
- const candidates = fs34.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
23093
+ if (!fs35.existsSync(dir)) return null;
23094
+ const candidates = fs35.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
21851
23095
  const full = path36.join(dir, e.name);
21852
- return { name: e.name, full, mtime: fs34.statSync(full).mtime };
23096
+ return { name: e.name, full, mtime: fs35.statSync(full).mtime };
21853
23097
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
21854
23098
  for (const candidate of candidates) {
21855
23099
  let content;
21856
23100
  try {
21857
- content = JSON.parse(fs34.readFileSync(candidate.full, "utf-8"));
23101
+ content = JSON.parse(fs35.readFileSync(candidate.full, "utf-8"));
21858
23102
  } catch {
21859
23103
  continue;
21860
23104
  }
@@ -22197,15 +23441,15 @@ init_dist();
22197
23441
  init_dist();
22198
23442
  init_dist();
22199
23443
  init_dist();
22200
- var __dirname = dirname10(fileURLToPath6(import.meta.url));
22201
- var pkg = JSON.parse(readFileSync21(join34(__dirname, "..", "package.json"), "utf-8"));
23444
+ var __dirname = dirname11(fileURLToPath6(import.meta.url));
23445
+ var pkg = JSON.parse(readFileSync21(join35(__dirname, "..", "package.json"), "utf-8"));
22202
23446
  ensureRuntimeSynced({
22203
- sourceRoot: join34(__dirname, ".."),
22204
- runtimeRoot: join34(homedir20(), ".config", "squadrant")
23447
+ sourceRoot: join35(__dirname, ".."),
23448
+ runtimeRoot: join35(homedir20(), ".config", "squadrant")
22205
23449
  });
22206
23450
  if (!process.env.VITEST) {
22207
23451
  try {
22208
- syncShippedOpencodeSkills({ pkgRoot: join34(__dirname, "..") });
23452
+ syncShippedOpencodeSkills({ pkgRoot: join35(__dirname, "..") });
22209
23453
  } catch (err) {
22210
23454
  process.stderr.write(
22211
23455
  `squadrant: opencode skills sync skipped: ${err.message}
@@ -22215,7 +23459,7 @@ if (!process.env.VITEST) {
22215
23459
  }
22216
23460
  if (process.argv[2] !== "config") {
22217
23461
  try {
22218
- const cfgPath = join34(homedir20(), ".config", "squadrant", "config.json");
23462
+ const cfgPath = join35(homedir20(), ".config", "squadrant", "config.json");
22219
23463
  if (existsSync14(cfgPath)) {
22220
23464
  const cfg = JSON.parse(readConfigFileSync(cfgPath));
22221
23465
  if (needsCheck(cfg, pkg.version)) {