squadrant 0.21.1 → 0.23.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
@@ -98,6 +98,9 @@ function isGateMode(v2) {
98
98
  function isGatePolicy(v2) {
99
99
  return GATE_POLICIES.includes(v2);
100
100
  }
101
+ function isGateEngine(v2) {
102
+ return GATE_ENGINES.includes(v2);
103
+ }
101
104
  function isThinkingLevel(v2) {
102
105
  return THINKING_LEVELS.includes(v2);
103
106
  }
@@ -156,12 +159,7 @@ function getDefaultConfig() {
156
159
  // resolver fallback is the safety net).
157
160
  controlChannel: { claude: "on", opencode: "on" },
158
161
  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
- ]
162
+ rules: DEFAULT_CREW_ROUTING_RULES.map((r) => ({ ...r }))
165
163
  }
166
164
  },
167
165
  metrics: {
@@ -203,18 +201,214 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
203
201
  function resolveHome(p) {
204
202
  return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
205
203
  }
206
- var ROUTER_KINDS, GATE_MODES, GATE_POLICIES, THINKING_LEVELS, CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
204
+ var ROUTER_KINDS, GATE_MODES, GATE_POLICIES, GATE_ENGINES, THINKING_LEVELS, CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
207
205
  var init_config = __esm({
208
206
  "packages/shared/dist/config.js"() {
209
207
  init_config_io();
210
208
  ROUTER_KINDS = ["opencode-go", "openrouter", "ccr", "litellm", "custom"];
211
209
  GATE_MODES = ["on", "off", "auto"];
212
210
  GATE_POLICIES = ["deny-dangerous", "ask-on-doubt"];
211
+ GATE_ENGINES = ["router", "auto-gate"];
213
212
  THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
214
213
  CONTROL_CHANNEL_MODES = /* @__PURE__ */ new Set(["off", "shadow", "on"]);
215
214
  DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
216
215
  CONFIG_DIR = path2.dirname(DEFAULT_CONFIG_PATH);
217
216
  DAEMON_SOCK_PATH = path2.join(CONFIG_DIR, "squadrant.sock");
217
+ DEFAULT_CREW_ROUTING_RULES = [
218
+ { tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
219
+ { tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
220
+ { tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
221
+ { tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
222
+ ];
223
+ }
224
+ });
225
+
226
+ // packages/shared/dist/provider-preset.js
227
+ function isProviderPresetId(v2) {
228
+ return v2 === "a" || v2 === "b" || v2 === "c" || v2 === "d";
229
+ }
230
+ function presetCrewRouting(id) {
231
+ const base = (tier) => {
232
+ const found = DEFAULT_CREW_ROUTING_RULES.find((r) => r.tier === tier);
233
+ return found ? { ...found } : { tier, match: "", agent: "claude" };
234
+ };
235
+ const codex = (tier) => ({
236
+ tier: base(tier).tier,
237
+ match: base(tier).match,
238
+ agent: "codex"
239
+ });
240
+ switch (id) {
241
+ case "a":
242
+ return DEFAULT_CREW_ROUTING_RULES.map((r) => ({ ...r }));
243
+ case "b":
244
+ return [
245
+ { ...base("extreme"), agent: "opencode", model: OPENCODE_MODEL },
246
+ { ...base("hard"), agent: "opencode", model: OPENCODE_MODEL },
247
+ codex("mobile"),
248
+ { ...base("daily"), agent: "opencode", model: OPENCODE_MODEL }
249
+ ];
250
+ case "c":
251
+ return [
252
+ { ...base("extreme"), agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL },
253
+ { ...base("hard"), agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL },
254
+ codex("mobile"),
255
+ { ...base("daily"), agent: "opencode", model: OPENCODE_MODEL }
256
+ ];
257
+ case "d":
258
+ return ["extreme", "hard", "mobile", "daily"].map(codex);
259
+ }
260
+ }
261
+ function providerPresetDefaults(id, router) {
262
+ switch (id) {
263
+ case "a":
264
+ return {
265
+ roles: {
266
+ command: { agent: "claude", model: "opus" },
267
+ captain: { agent: "claude", model: "opus" },
268
+ crew: { agent: "claude", model: "sonnet" },
269
+ exploration: { agent: "claude", model: "haiku" },
270
+ side: { agent: "claude", model: "opus" }
271
+ },
272
+ permissions: { command: "auto", captain: "auto", crew: "auto" },
273
+ crewRouting: { rules: presetCrewRouting("a") }
274
+ };
275
+ case "b": {
276
+ const opencode = { agent: "opencode", model: OPENCODE_MODEL };
277
+ return {
278
+ roles: {
279
+ command: { ...opencode },
280
+ captain: { ...opencode },
281
+ crew: { ...opencode },
282
+ exploration: { ...opencode },
283
+ side: { ...opencode }
284
+ },
285
+ permissions: { command: "auto", captain: "auto", crew: "auto" },
286
+ crewRouting: { rules: presetCrewRouting("b") }
287
+ };
288
+ }
289
+ case "c": {
290
+ if (!router) {
291
+ throw new Error("preset C (claude harness + router backend) requires a router config \u2014 set defaults.router first");
292
+ }
293
+ const routed = { agent: "claude", backend: "proxy", model: ROUTER_PRESET_MODEL };
294
+ return {
295
+ roles: {
296
+ command: { ...routed },
297
+ captain: { ...routed },
298
+ crew: { ...routed },
299
+ exploration: { ...routed },
300
+ side: { ...routed }
301
+ },
302
+ // Auto mode's classifier is hardcoded to Sonnet 5 and fails closed on a
303
+ // router upstream — every claude session must use a manual mode so the
304
+ // U7 gate owns the prompt.
305
+ permissions: { command: "default", captain: "default", crew: "default" },
306
+ crewRouting: { rules: presetCrewRouting("c") },
307
+ router,
308
+ gate: { mode: "on" }
309
+ };
310
+ }
311
+ case "d": {
312
+ const codex = { agent: "codex" };
313
+ return {
314
+ roles: {
315
+ command: { ...codex },
316
+ captain: { ...codex },
317
+ crew: { ...codex },
318
+ exploration: { ...codex },
319
+ side: { ...codex }
320
+ },
321
+ permissions: {},
322
+ crewRouting: { rules: presetCrewRouting("d") }
323
+ };
324
+ }
325
+ }
326
+ }
327
+ function detectProviderPresetId(config) {
328
+ const crew = config.defaults?.roles?.crew;
329
+ if (crew?.agent === "opencode")
330
+ return "b";
331
+ if (crew?.agent === "codex")
332
+ return "d";
333
+ if (crew?.backend && crew.backend !== "native")
334
+ return "c";
335
+ return "a";
336
+ }
337
+ function applyProviderPreset(config, id, opts) {
338
+ const preset = providerPresetDefaults(id, opts.router);
339
+ const next = structuredClone(config);
340
+ const changes = [];
341
+ if (opts.overwrite) {
342
+ next.defaults.roles = { ...preset.roles };
343
+ changes.push("defaults.roles");
344
+ } else if (next.defaults.roles === void 0) {
345
+ next.defaults.roles = { ...preset.roles };
346
+ changes.push("defaults.roles");
347
+ }
348
+ if (opts.overwrite) {
349
+ next.defaults.crewRouting = { rules: preset.crewRouting.rules.map((r) => ({ ...r })) };
350
+ changes.push("defaults.crewRouting");
351
+ } else if (next.defaults.crewRouting === void 0) {
352
+ next.defaults.crewRouting = { rules: preset.crewRouting.rules.map((r) => ({ ...r })) };
353
+ changes.push("defaults.crewRouting");
354
+ }
355
+ const existingPerms = next.defaults.permissions ?? {};
356
+ if (opts.overwrite) {
357
+ next.defaults.permissions = { ...existingPerms, ...preset.permissions };
358
+ if (Object.keys(preset.permissions).length > 0)
359
+ changes.push("defaults.permissions");
360
+ } else if (next.defaults.permissions === void 0 && Object.keys(preset.permissions).length > 0) {
361
+ next.defaults.permissions = { ...preset.permissions };
362
+ changes.push("defaults.permissions");
363
+ }
364
+ if (preset.router) {
365
+ if (opts.overwrite || next.defaults.router === void 0) {
366
+ next.defaults.router = structuredClone(preset.router);
367
+ changes.push("defaults.router");
368
+ }
369
+ }
370
+ if (preset.gate) {
371
+ if (opts.overwrite || next.defaults.gate === void 0) {
372
+ next.defaults.gate = { ...preset.gate };
373
+ changes.push("defaults.gate");
374
+ }
375
+ }
376
+ return { config: next, changes };
377
+ }
378
+ var PROVIDER_PRESETS, ROUTER_PRESET_MODEL, DEFAULT_ROUTER_KIND, DEFAULT_ROUTER_BASE_URL, OPENCODE_MODEL;
379
+ var init_provider_preset = __esm({
380
+ "packages/shared/dist/provider-preset.js"() {
381
+ init_config();
382
+ PROVIDER_PRESETS = [
383
+ {
384
+ id: "a",
385
+ label: "Claude Code (Pro/Max)",
386
+ summary: "Claude harness on your subscription/API key. Real auto mode.",
387
+ requiresAnthropic: true
388
+ },
389
+ {
390
+ id: "b",
391
+ label: "opencode (no Anthropic subscription)",
392
+ summary: "All roles run on the opencode harness. Auto mode is model-agnostic.",
393
+ requiresAnthropic: false
394
+ },
395
+ {
396
+ id: "c",
397
+ label: "Claude harness + router backend (advanced)",
398
+ summary: "Claude harness against a router upstream. The U7 gate replaces auto mode.",
399
+ requiresAnthropic: false
400
+ },
401
+ {
402
+ id: "d",
403
+ label: "Codex (ChatGPT Pro)",
404
+ summary: "All roles run on the codex harness.",
405
+ requiresAnthropic: false
406
+ }
407
+ ];
408
+ ROUTER_PRESET_MODEL = "deepseek-v4.1-flash";
409
+ DEFAULT_ROUTER_KIND = "opencode-go";
410
+ DEFAULT_ROUTER_BASE_URL = "https://opencode.ai/zen/go";
411
+ OPENCODE_MODEL = "opencode-go/deepseek-v4.1-flash";
218
412
  }
219
413
  });
220
414
 
@@ -1465,11 +1659,17 @@ __export(dist_exports, {
1465
1659
  CREW_SKILLS: () => CREW_SKILLS,
1466
1660
  DAEMON_SOCK_PATH: () => DAEMON_SOCK_PATH,
1467
1661
  DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
1662
+ DEFAULT_CREW_ROUTING_RULES: () => DEFAULT_CREW_ROUTING_RULES,
1468
1663
  DEFAULT_NOTIFY: () => DEFAULT_NOTIFY,
1664
+ DEFAULT_ROUTER_BASE_URL: () => DEFAULT_ROUTER_BASE_URL,
1665
+ DEFAULT_ROUTER_KIND: () => DEFAULT_ROUTER_KIND,
1666
+ GATE_ENGINES: () => GATE_ENGINES,
1469
1667
  GATE_MODES: () => GATE_MODES,
1470
1668
  GATE_POLICIES: () => GATE_POLICIES,
1471
1669
  MANAGED_TARGETS: () => MANAGED_TARGETS,
1670
+ PROVIDER_PRESETS: () => PROVIDER_PRESETS,
1472
1671
  ROUTER_KINDS: () => ROUTER_KINDS,
1672
+ ROUTER_PRESET_MODEL: () => ROUTER_PRESET_MODEL,
1473
1673
  SAFE_KINDS: () => SAFE_KINDS,
1474
1674
  SOCKET_CONTROL_MODE_PATH: () => SOCKET_CONTROL_MODE_PATH,
1475
1675
  SPOKE_SUBDIRS: () => SPOKE_SUBDIRS,
@@ -1478,6 +1678,7 @@ __export(dist_exports, {
1478
1678
  THINKING_LEVELS: () => THINKING_LEVELS,
1479
1679
  UPDATE_CHECK_STATE_PATH: () => UPDATE_CHECK_STATE_PATH,
1480
1680
  addWorktree: () => addWorktree,
1681
+ applyProviderPreset: () => applyProviderPreset,
1481
1682
  applySafeFixes: () => applySafeFixes,
1482
1683
  captainSessionName: () => captainSessionName,
1483
1684
  checkForUpdate: () => checkForUpdate,
@@ -1493,6 +1694,7 @@ __export(dist_exports, {
1493
1694
  defaultStatePath: () => defaultStatePath,
1494
1695
  detectDrift: () => detectDrift,
1495
1696
  detectInstallManager: () => detectInstallManager,
1697
+ detectProviderPresetId: () => detectProviderPresetId,
1496
1698
  ensureCmuxAutoConfig: () => ensureCmuxAutoConfig,
1497
1699
  ensureDirSync: () => ensureDirSync,
1498
1700
  ensureRuntimeSynced: () => ensureRuntimeSynced,
@@ -1508,9 +1710,11 @@ __export(dist_exports, {
1508
1710
  isBackendMode: () => isBackendMode,
1509
1711
  isCacheStale: () => isCacheStale,
1510
1712
  isDaemonCachedKey: () => isDaemonCachedKey,
1713
+ isGateEngine: () => isGateEngine,
1511
1714
  isGateMode: () => isGateMode,
1512
1715
  isGatePolicy: () => isGatePolicy,
1513
1716
  isNewerVersion: () => isNewerVersion,
1717
+ isProviderPresetId: () => isProviderPresetId,
1514
1718
  isQuieter: () => isQuieter,
1515
1719
  isRouterKind: () => isRouterKind,
1516
1720
  isThinkingLevel: () => isThinkingLevel,
@@ -1527,6 +1731,7 @@ __export(dist_exports, {
1527
1731
  parseThinkingLevel: () => parseThinkingLevel,
1528
1732
  probeCmuxDaemonDirect: () => probeCmuxDaemonDirect,
1529
1733
  projectConfigPath: () => projectConfigPath,
1734
+ providerPresetDefaults: () => providerPresetDefaults,
1530
1735
  readConfigFileSync: () => readConfigFileSync,
1531
1736
  readDailyLog: () => readDailyLog,
1532
1737
  readProjectLevelSource: () => readProjectLevelSource,
@@ -1556,6 +1761,7 @@ __export(dist_exports, {
1556
1761
  var init_dist = __esm({
1557
1762
  "packages/shared/dist/index.js"() {
1558
1763
  init_config();
1764
+ init_provider_preset();
1559
1765
  init_project_config();
1560
1766
  init_effort();
1561
1767
  init_router_model();
@@ -1827,10 +2033,11 @@ function formatDelegationReport(rec, originProject, targetProject) {
1827
2033
  return null;
1828
2034
  }
1829
2035
  }
1830
- function firePush(deps, project, prev, next, event, lastCaptainTurnAt) {
2036
+ function firePush(deps, project, prev, next, event, lastCaptainTurnAt, prevRec) {
1831
2037
  if (!deps.notify)
1832
2038
  return;
1833
- if (prev === next.state)
2039
+ const questionChanged = prev === "blocked" && next.state === "blocked" && prevRec != null && (prevRec.question ?? "").trim() !== (next.question ?? "").trim();
2040
+ if (prev === next.state && !questionChanged)
1834
2041
  return;
1835
2042
  if (!ATTENTION_STATES.has(next.state))
1836
2043
  return;
@@ -1868,6 +2075,7 @@ function createDaemon(deps) {
1868
2075
  const { store, now } = deps;
1869
2076
  const lastCaptainTurnAt = /* @__PURE__ */ new Map();
1870
2077
  const quietNotifiedAt = /* @__PURE__ */ new Map();
2078
+ const blockedRenudgedFor = /* @__PURE__ */ new Map();
1871
2079
  const resendAttemptedAt = /* @__PURE__ */ new Map();
1872
2080
  const firstTurnUndeliveredBudgetMs = deps.firstTurnUndeliveredBudgetMs ?? DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS;
1873
2081
  const firstTurnResendCooldownMs = deps.firstTurnResendCooldownMs ?? DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS;
@@ -1888,7 +2096,7 @@ function createDaemon(deps) {
1888
2096
  const next = reduce(cur, event, now());
1889
2097
  if (next !== cur) {
1890
2098
  store.put(next);
1891
- firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id));
2099
+ firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id), cur);
1892
2100
  }
1893
2101
  return next;
1894
2102
  }
@@ -2040,6 +2248,8 @@ function createDaemon(deps) {
2040
2248
  }
2041
2249
  const recoveryPromises = [];
2042
2250
  for (const r of store.listAll()) {
2251
+ if (r.state !== "blocked")
2252
+ blockedRenudgedFor.delete(r.id);
2043
2253
  if (TERMINAL_STATES.has(r.state) && t - r.lastHeartbeat > TERMINAL_RECORD_TTL_MS) {
2044
2254
  store.delete(r.project, r.id);
2045
2255
  continue;
@@ -2102,6 +2312,27 @@ function createDaemon(deps) {
2102
2312
  continue;
2103
2313
  }
2104
2314
  }
2315
+ if (r.state === "blocked" && !r.operatorHold) {
2316
+ const threshold = deps.blockedRenudgeMs ?? DEFAULT_BLOCKED_RENUDGE_MS;
2317
+ const blockedFor = t - r.lastHeartbeat;
2318
+ const episodeKey = r.question ?? "";
2319
+ if (blockedFor > threshold && blockedRenudgedFor.get(r.id) !== episodeKey) {
2320
+ blockedRenudgedFor.set(r.id, episodeKey);
2321
+ if (deps.notify) {
2322
+ const tag = crewTag(r);
2323
+ const mins = Math.max(1, Math.round(blockedFor / 6e4));
2324
+ const message = `CREW BLOCKED REMINDER ${tag}: still blocked ~${mins}min (reminder \u2014 same question, not a new one): ${(r.question ?? "(no question)").trim()}`;
2325
+ const synthEvent = { type: "task.blocked", id: r.id, reason: "renudge", question: r.question ?? "" };
2326
+ try {
2327
+ const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
2328
+ if (p && typeof p.catch === "function")
2329
+ p.catch(() => {
2330
+ });
2331
+ } catch {
2332
+ }
2333
+ }
2334
+ }
2335
+ }
2105
2336
  if (r.mode === "interactive" && r.state === "submitted" && !r.firstTurnConfirmedAt) {
2106
2337
  const undeliveredMs = t - r.createdAt;
2107
2338
  if (undeliveredMs > firstTurnUndeliveredBudgetMs) {
@@ -2191,7 +2422,7 @@ function createDaemon(deps) {
2191
2422
  }
2192
2423
  };
2193
2424
  }
2194
- var DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS, DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS, DEFAULT_TASK_TIMEOUT_MS, TERMINAL_RECORD_TTL_MS, TERMINAL_RECORD_KEEP_PER_PROJECT, ATTENTION_STATES, REAPABLE_SURFACE_STATES, IDLE_DEBOUNCE_MS, KNOWN_EVENT_TYPES;
2425
+ var DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS, DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS, DEFAULT_BLOCKED_RENUDGE_MS, DEFAULT_TASK_TIMEOUT_MS, TERMINAL_RECORD_TTL_MS, TERMINAL_RECORD_KEEP_PER_PROJECT, ATTENTION_STATES, REAPABLE_SURFACE_STATES, IDLE_DEBOUNCE_MS, KNOWN_EVENT_TYPES;
2195
2426
  var init_reduce = __esm({
2196
2427
  "packages/core/dist/daemon/reduce.js"() {
2197
2428
  init_dist();
@@ -2199,6 +2430,7 @@ var init_reduce = __esm({
2199
2430
  init_watchdog();
2200
2431
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS = 12e4;
2201
2432
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS = 6e4;
2433
+ DEFAULT_BLOCKED_RENUDGE_MS = 30 * 60 * 1e3;
2202
2434
  DEFAULT_TASK_TIMEOUT_MS = 8 * 60 * 60 * 1e3;
2203
2435
  TERMINAL_RECORD_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
2204
2436
  TERMINAL_RECORD_KEEP_PER_PROJECT = 20;
@@ -2793,8 +3025,8 @@ function reconcileLiveness(prev, next) {
2793
3025
  if (!prev)
2794
3026
  return next;
2795
3027
  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) };
3028
+ const pidAlive2 = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
3029
+ return { ...prev, pidAlive: pidAlive2, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
2798
3030
  }
2799
3031
  if (next.startedAt >= prev.startedAt || next.lastState === "end")
2800
3032
  return next;
@@ -3540,6 +3772,68 @@ function createDirectSurfaceLivenessProbe(cmux2, getCaptainTitle) {
3540
3772
  }
3541
3773
  };
3542
3774
  }
3775
+ function hasModalOptionList(screen) {
3776
+ if (!screen)
3777
+ return false;
3778
+ const lines = screen.split(/\r?\n/);
3779
+ let bottomHR = -1;
3780
+ let topHR = -1;
3781
+ for (let i = lines.length - 1; i >= 0; i--) {
3782
+ if (HR_RE.test(lines[i])) {
3783
+ if (bottomHR === -1)
3784
+ bottomHR = i;
3785
+ else {
3786
+ topHR = i;
3787
+ break;
3788
+ }
3789
+ }
3790
+ }
3791
+ if (topHR === -1)
3792
+ return false;
3793
+ return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
3794
+ }
3795
+ function classifyCaptainPane(screen, opts = {}) {
3796
+ if (screen == null || screen.trim() === "")
3797
+ return { status: "unreadable" };
3798
+ if (hasModalOptionList(screen))
3799
+ return { status: "locked" };
3800
+ if (WORKING_RE.test(screen))
3801
+ return { status: "mid-turn" };
3802
+ const idleAfterMs = opts.idleAfterMs ?? 0;
3803
+ const stale = opts.outputAgoMs !== void 0 && opts.outputAgoMs >= idleAfterMs;
3804
+ return stale ? { status: "no-session" } : { status: "mid-turn" };
3805
+ }
3806
+ function screenHash(screen) {
3807
+ let h = 2166136261;
3808
+ for (let i = 0; i < screen.length; i++) {
3809
+ h ^= screen.charCodeAt(i);
3810
+ h = Math.imul(h, 16777619) >>> 0;
3811
+ }
3812
+ return h.toString(16).padStart(8, "0");
3813
+ }
3814
+ function createCaptainPaneReader(cmux2, getCaptainTitle, opts = {}) {
3815
+ const idleAfterMs = opts.idleAfterMs ?? PANE_IDLE_AFTER_MS;
3816
+ return async (project) => {
3817
+ try {
3818
+ const wsId = await cmux2.findWorkspaceId(getCaptainTitle(project));
3819
+ if (!wsId)
3820
+ return { status: "no-session" };
3821
+ const surfaces = await cmux2.listSurfaces(wsId);
3822
+ const want = getCaptainTitle(project);
3823
+ const pane = surfaces.find((s) => s.title === want) ?? surfaces[0];
3824
+ if (!pane)
3825
+ return { status: "no-session" };
3826
+ const screen = await cmux2.readPaneScreen(pane);
3827
+ if (screen == null)
3828
+ return { status: "unreadable" };
3829
+ const outputAgoMs = opts.noteScreen?.(project, screenHash(screen));
3830
+ return classifyCaptainPane(screen, { outputAgoMs, idleAfterMs });
3831
+ } catch (e) {
3832
+ opts.log?.(`captain pane read failed project=${project}: ${e.message}`);
3833
+ return { status: "unreadable" };
3834
+ }
3835
+ };
3836
+ }
3543
3837
  function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
3544
3838
  return async (rec) => {
3545
3839
  try {
@@ -3562,11 +3856,14 @@ function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
3562
3856
  }
3563
3857
  };
3564
3858
  }
3565
- var TAIL_LINES;
3859
+ var TAIL_LINES, HR_RE, WORKING_RE, PANE_IDLE_AFTER_MS;
3566
3860
  var init_crew_pane_reader = __esm({
3567
3861
  "packages/core/dist/crew-pane-reader.js"() {
3568
3862
  init_dist();
3569
3863
  TAIL_LINES = 25;
3864
+ HR_RE = /^\s*─{10,}\s*$/;
3865
+ 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;
3866
+ PANE_IDLE_AFTER_MS = 10 * 6e4;
3570
3867
  }
3571
3868
  });
3572
3869
 
@@ -4264,8 +4561,11 @@ function buildRouterEnv(creds, model) {
4264
4561
  if (headers)
4265
4562
  env.ANTHROPIC_CUSTOM_HEADERS = headers;
4266
4563
  }
4267
- if (model)
4564
+ if (model) {
4268
4565
  env.ANTHROPIC_MODEL = model;
4566
+ env.CLAUDE_CODE_SUBAGENT_MODEL = model;
4567
+ env.ANTHROPIC_SMALL_FAST_MODEL = model;
4568
+ }
4269
4569
  return env;
4270
4570
  }
4271
4571
  function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
@@ -5431,8 +5731,9 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
5431
5731
  if (mode !== "off")
5432
5732
  throw new DeferDelivery(null, "no-channel");
5433
5733
  }
5734
+ const paneOpts = { ...sendOpts, agent };
5434
5735
  try {
5435
- return await cmux2.send(surface, text, sendOpts);
5736
+ return await cmux2.send(surface, text, paneOpts);
5436
5737
  } catch (e) {
5437
5738
  if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
5438
5739
  throw e;
@@ -5444,7 +5745,7 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
5444
5745
  }
5445
5746
  log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
5446
5747
  surface = next;
5447
- return cmux2.send(next, text, sendOpts);
5748
+ return cmux2.send(next, text, paneOpts);
5448
5749
  }
5449
5750
  });
5450
5751
  if ("delivered" in result) {
@@ -6375,16 +6676,26 @@ function createRunCommand(cliBin) {
6375
6676
  }
6376
6677
  };
6377
6678
  }
6679
+ function captainAliveStateFromHealth(rows, project) {
6680
+ const row = rows.find((h) => h.kind === "captain" && h.project === project);
6681
+ if (!row)
6682
+ return "unknown";
6683
+ if (row.state === "alive")
6684
+ return "alive";
6685
+ if (row.state === "gone" || row.state === "stopped")
6686
+ return "dead";
6687
+ return "unknown";
6688
+ }
6378
6689
  function isCaptainAliveFromHealth(rows, project) {
6379
- return rows.some((h) => h.kind === "captain" && h.project === project && h.state === "alive");
6690
+ return captainAliveStateFromHealth(rows, project) === "alive";
6380
6691
  }
6381
6692
  function createIsCaptainAlive(sock) {
6382
6693
  return async (project) => {
6383
6694
  try {
6384
6695
  const health = await sendRequest(sock, { kind: "health", project }, 5e3);
6385
- return isCaptainAliveFromHealth(health ?? [], project);
6696
+ return captainAliveStateFromHealth(health ?? [], project);
6386
6697
  } catch {
6387
- return false;
6698
+ return "unknown";
6388
6699
  }
6389
6700
  };
6390
6701
  }
@@ -6421,16 +6732,19 @@ function createEnsureCaptainAlive(deps) {
6421
6732
  const now = deps.now ?? (() => Date.now());
6422
6733
  const inFlight = /* @__PURE__ */ new Map();
6423
6734
  async function run(project) {
6424
- if (await deps.isAlive(project))
6735
+ const first = await deps.isAlive(project);
6736
+ if (first === "alive")
6425
6737
  return "alive";
6426
6738
  await deps.launch(project);
6427
6739
  const deadline = now() + warmupTimeoutMs;
6740
+ let last = first;
6428
6741
  while (now() < deadline) {
6429
- if (await deps.isAlive(project))
6742
+ last = await deps.isAlive(project);
6743
+ if (last === "alive")
6430
6744
  return "launched";
6431
6745
  await sleep3(pollMs);
6432
6746
  }
6433
- return "timeout";
6747
+ return last === "dead" ? "timeout" : "unknown";
6434
6748
  }
6435
6749
  return function ensure(project) {
6436
6750
  const existing = inFlight.get(project);
@@ -6485,6 +6799,26 @@ ${ev.question}`;
6485
6799
  function formatInbound(text) {
6486
6800
  return `\u{1F4E9} [from Telegram] ${text}`;
6487
6801
  }
6802
+ function mediaKind(m) {
6803
+ for (const [field, name] of MEDIA_KINDS) {
6804
+ if (m[field] !== void 0)
6805
+ return name;
6806
+ }
6807
+ return void 0;
6808
+ }
6809
+ function mediaMarker(kind) {
6810
+ return `[${kind} attached - not forwarded]`;
6811
+ }
6812
+ function inboundBody(text, kind) {
6813
+ if (kind === void 0)
6814
+ return text ?? "";
6815
+ const marker = mediaMarker(kind);
6816
+ return text ? `${text}
6817
+ ${marker}` : marker;
6818
+ }
6819
+ function formatMediaReceipt(kind, hasCaption) {
6820
+ 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`;
6821
+ }
6488
6822
  function formatUsageLine(u) {
6489
6823
  const reqs = u.requests === 1 ? "1 req" : `${u.requests} reqs`;
6490
6824
  const named = Object.keys(u.models).filter((m) => m !== "unknown").sort((a, b) => u.models[b].costUsd - u.models[a].costUsd);
@@ -6496,8 +6830,19 @@ function maskToken(token) {
6496
6830
  return token;
6497
6831
  return "*".repeat(token.length - 4) + token.slice(-4);
6498
6832
  }
6833
+ var MEDIA_KINDS;
6499
6834
  var init_format = __esm({
6500
6835
  "packages/core/dist/telegram/format.js"() {
6836
+ MEDIA_KINDS = [
6837
+ ["photo", "photo"],
6838
+ ["voice", "voice message"],
6839
+ ["video_note", "video message"],
6840
+ ["video", "video"],
6841
+ ["audio", "audio file"],
6842
+ ["document", "document"],
6843
+ ["animation", "animation"],
6844
+ ["sticker", "sticker"]
6845
+ ];
6501
6846
  }
6502
6847
  });
6503
6848
 
@@ -6520,6 +6865,8 @@ function loadState(stateRoot) {
6520
6865
  };
6521
6866
  if (typeof data.lastUserId === "number")
6522
6867
  result.lastUserId = data.lastUserId;
6868
+ if (data.pending && Object.keys(data.pending).length > 0)
6869
+ result.pending = data.pending;
6523
6870
  return result;
6524
6871
  } catch {
6525
6872
  return { offset: 0, topics: {}, notify: {} };
@@ -6533,6 +6880,19 @@ function setTopic(stateRoot, project, topicId, scope = "project") {
6533
6880
  s.topics[topicKey(project, scope)] = topicId;
6534
6881
  saveState(stateRoot, s);
6535
6882
  }
6883
+ function pruneTopics(stateRoot, keep) {
6884
+ const s = loadState(stateRoot);
6885
+ const removed = Object.keys(s.topics).filter((key) => {
6886
+ const sep3 = key.indexOf("::");
6887
+ return !keep(sep3 === -1 ? key : key.slice(0, sep3));
6888
+ });
6889
+ if (removed.length === 0)
6890
+ return [];
6891
+ for (const key of removed)
6892
+ delete s.topics[key];
6893
+ saveState(stateRoot, s);
6894
+ return removed;
6895
+ }
6536
6896
  function isNotifyActive(stateRoot, project) {
6537
6897
  return loadState(stateRoot).notify[project] === true;
6538
6898
  }
@@ -6546,6 +6906,42 @@ function setNotify(stateRoot, project, active) {
6546
6906
  s.notify[project] = active;
6547
6907
  saveState(stateRoot, s);
6548
6908
  }
6909
+ function loadPending(stateRoot) {
6910
+ return loadState(stateRoot).pending ?? {};
6911
+ }
6912
+ function setPending(stateRoot, project, p) {
6913
+ const s = loadState(stateRoot);
6914
+ s.pending = { ...s.pending ?? {}, [project]: p };
6915
+ saveState(stateRoot, s);
6916
+ }
6917
+ function clearPending(stateRoot, project) {
6918
+ const s = loadState(stateRoot);
6919
+ if (!s.pending || !(project in s.pending))
6920
+ return;
6921
+ delete s.pending[project];
6922
+ if (Object.keys(s.pending).length === 0)
6923
+ delete s.pending;
6924
+ saveState(stateRoot, s);
6925
+ }
6926
+ function markPendingWarned(stateRoot, project, at) {
6927
+ const s = loadState(stateRoot);
6928
+ const p = s.pending?.[project];
6929
+ if (!p)
6930
+ return;
6931
+ s.pending[project] = { ...p, warnedAt: at };
6932
+ saveState(stateRoot, s);
6933
+ }
6934
+ function notePaneScreen(stateRoot, project, hash, at) {
6935
+ const s = loadState(stateRoot);
6936
+ const p = s.pending?.[project];
6937
+ if (!p)
6938
+ return void 0;
6939
+ if (p.paneHash === hash && p.paneChangedAt !== void 0)
6940
+ return at - p.paneChangedAt;
6941
+ s.pending[project] = { ...p, paneHash: hash, paneChangedAt: at };
6942
+ saveState(stateRoot, s);
6943
+ return void 0;
6944
+ }
6549
6945
  function findProjectByThread(stateRoot, threadId) {
6550
6946
  const s = loadState(stateRoot);
6551
6947
  for (const [key, id] of Object.entries(s.topics)) {
@@ -6568,17 +6964,18 @@ var init_state = __esm({
6568
6964
  function createTelegramClient(opts) {
6569
6965
  const fetchImpl = opts.fetch ?? fetch;
6570
6966
  const base = `https://api.telegram.org/bot${opts.token}`;
6571
- async function call(method, body) {
6967
+ async function call(method, body, signal) {
6572
6968
  const res = await fetchImpl(`${base}/${method}`, {
6573
6969
  method: "POST",
6574
6970
  headers: { "content-type": "application/json" },
6575
- body: JSON.stringify(body)
6971
+ body: JSON.stringify(body),
6972
+ ...signal ? { signal } : {}
6576
6973
  });
6577
6974
  const json = await res.json();
6578
6975
  if (!res.ok || !json.ok) {
6579
6976
  const code = json.error_code ?? res.status;
6580
6977
  const desc = json.description ?? "unknown error";
6581
- throw new Error(`telegram ${method} failed (${code}): ${desc}`);
6978
+ throw new TelegramApiError(code, `telegram ${method} failed (${code}): ${desc}`, json.parameters?.retry_after);
6582
6979
  }
6583
6980
  return json.result;
6584
6981
  }
@@ -6587,8 +6984,8 @@ function createTelegramClient(opts) {
6587
6984
  const r = await call("getMe", {});
6588
6985
  return { id: r.id, username: r.username };
6589
6986
  },
6590
- getUpdates(offset, timeoutSec = 50) {
6591
- return call("getUpdates", { offset, timeout: timeoutSec });
6987
+ getUpdates(offset, timeoutSec = 50, signal) {
6988
+ return call("getUpdates", { offset, timeout: timeoutSec }, signal);
6592
6989
  },
6593
6990
  async sendMessage(chatId, threadId, text, replyMarkup) {
6594
6991
  const body = { chat_id: chatId, text };
@@ -6611,6 +7008,9 @@ function createTelegramClient(opts) {
6611
7008
  const r = await call("createForumTopic", { chat_id: chatId, name });
6612
7009
  return r.message_thread_id;
6613
7010
  },
7011
+ async deleteForumTopic(chatId, threadId) {
7012
+ await call("deleteForumTopic", { chat_id: chatId, message_thread_id: threadId });
7013
+ },
6614
7014
  async setMyCommands(commands) {
6615
7015
  await call("setMyCommands", { commands });
6616
7016
  },
@@ -6619,11 +7019,241 @@ function createTelegramClient(opts) {
6619
7019
  if (threadId !== void 0)
6620
7020
  body.message_thread_id = threadId;
6621
7021
  await call("sendChatAction", body);
7022
+ },
7023
+ async setMessageReaction(chatId, messageId, emoji) {
7024
+ await call("setMessageReaction", {
7025
+ chat_id: chatId,
7026
+ message_id: messageId,
7027
+ reaction: [{ type: "emoji", emoji }]
7028
+ });
6622
7029
  }
6623
7030
  };
6624
7031
  }
7032
+ var TelegramApiError;
6625
7033
  var init_client = __esm({
6626
7034
  "packages/core/dist/telegram/client.js"() {
7035
+ TelegramApiError = class extends Error {
7036
+ code;
7037
+ retryAfterSec;
7038
+ constructor(code, message, retryAfterSec) {
7039
+ super(message);
7040
+ this.code = code;
7041
+ this.retryAfterSec = retryAfterSec;
7042
+ this.name = "TelegramApiError";
7043
+ }
7044
+ };
7045
+ }
7046
+ });
7047
+
7048
+ // packages/core/dist/telegram/inbound-lifecycle.js
7049
+ function defaultSchedule(fn, ms) {
7050
+ const t = setTimeout(fn, ms);
7051
+ t.unref?.();
7052
+ return () => clearTimeout(t);
7053
+ }
7054
+ function formatWatchdogWarning(project, read, minutes) {
7055
+ const head = `\u23F1\uFE0F [${project}] no reply for ${minutes} minutes`;
7056
+ switch (read.status) {
7057
+ case "locked":
7058
+ 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.`;
7059
+ case "mid-turn":
7060
+ return `${head} \u2014 the captain is still working (a turn is in flight), just slow. No action needed yet.`;
7061
+ case "no-session":
7062
+ return `${head} \u2014 the captain looks GONE (no live session in its pane). Run \`squadrant launch ${project}\`.`;
7063
+ case "unreadable":
7064
+ return `${head} \u2014 couldn't read the captain's pane to classify it. Check it manually.`;
7065
+ }
7066
+ }
7067
+ function createInboundLifecycle(opts) {
7068
+ const { stateRoot, cfg, sendChatAction, sendReply, readPane, log } = opts;
7069
+ const now = opts.now ?? Date.now;
7070
+ const schedule = opts.schedule ?? defaultSchedule;
7071
+ const typingMs = opts.typingMs ?? TYPING_KEEPALIVE_MS;
7072
+ const watchdogMs = opts.watchdogMs ?? WATCHDOG_MS;
7073
+ const warnAtMs = watchdogMs + (opts.watchdogMs === void 0 ? WATCHDOG_GRACE_MS : 0);
7074
+ const active = /* @__PURE__ */ new Set();
7075
+ let running = false;
7076
+ function issue(project, threadId) {
7077
+ void Promise.resolve(sendChatAction(cfg.supergroupId, threadId, "typing")).catch((e) => log(`telegram typing keep-alive failed project=${project}: ${e.message}`));
7078
+ }
7079
+ function drain(project) {
7080
+ active.add(project);
7081
+ const tick = () => {
7082
+ if (!running || !active.has(project))
7083
+ return;
7084
+ const p = loadPending(stateRoot)[project];
7085
+ if (!p) {
7086
+ active.delete(project);
7087
+ return;
7088
+ }
7089
+ if (now() - p.startedAt >= warnAtMs && p.warnedAt === void 0) {
7090
+ void warn(project, p.threadId);
7091
+ }
7092
+ issue(project, p.threadId);
7093
+ schedule(tick, typingMs);
7094
+ };
7095
+ schedule(tick, typingMs);
7096
+ }
7097
+ async function warn(project, threadId) {
7098
+ markPendingWarned(stateRoot, project, now());
7099
+ let read = { status: "unreadable" };
7100
+ try {
7101
+ if (readPane)
7102
+ read = await readPane(project);
7103
+ } catch (e) {
7104
+ log(`telegram watchdog pane read failed project=${project}: ${e.message}`);
7105
+ }
7106
+ try {
7107
+ await sendReply(threadId, formatWatchdogWarning(project, read, Math.floor(watchdogMs / 6e4)));
7108
+ } catch (e) {
7109
+ log(`telegram watchdog warning failed project=${project}: ${e.message}`);
7110
+ }
7111
+ }
7112
+ return {
7113
+ start() {
7114
+ running = true;
7115
+ for (const [project, p] of Object.entries(loadPending(stateRoot))) {
7116
+ void (async () => {
7117
+ let read = { status: "unreadable" };
7118
+ try {
7119
+ if (readPane)
7120
+ read = await readPane(project);
7121
+ } catch (e) {
7122
+ log(`telegram resume pane read failed project=${project}: ${e.message}`);
7123
+ }
7124
+ const still = loadPending(stateRoot)[project];
7125
+ if (!still || still.startedAt !== p.startedAt)
7126
+ return;
7127
+ if (read.status === "no-session") {
7128
+ clearPending(stateRoot, project);
7129
+ log(`telegram typing lifecycle cleared for ${project}: captain session gone on boot`);
7130
+ return;
7131
+ }
7132
+ log(`telegram typing lifecycle resumed for ${project} (thread ${p.threadId})`);
7133
+ issue(project, p.threadId);
7134
+ drain(project);
7135
+ })();
7136
+ }
7137
+ },
7138
+ stop() {
7139
+ running = false;
7140
+ active.clear();
7141
+ },
7142
+ begin(project, threadId) {
7143
+ setPending(stateRoot, project, { threadId, startedAt: now() });
7144
+ issue(project, threadId);
7145
+ if (running && !active.has(project))
7146
+ drain(project);
7147
+ },
7148
+ pending() {
7149
+ return loadPending(stateRoot);
7150
+ }
7151
+ };
7152
+ }
7153
+ var TYPING_KEEPALIVE_MS, WATCHDOG_MS, WATCHDOG_GRACE_MS;
7154
+ var init_inbound_lifecycle = __esm({
7155
+ "packages/core/dist/telegram/inbound-lifecycle.js"() {
7156
+ init_state();
7157
+ TYPING_KEEPALIVE_MS = 4e3;
7158
+ WATCHDOG_MS = 15 * 6e4;
7159
+ WATCHDOG_GRACE_MS = 1;
7160
+ }
7161
+ });
7162
+
7163
+ // packages/core/dist/telegram/notify.js
7164
+ function runTelegramStatus(opts) {
7165
+ const tg = opts.config.telegram;
7166
+ const env = opts.env ?? process.env;
7167
+ const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
7168
+ const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
7169
+ const sep3 = key.indexOf("::");
7170
+ return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
7171
+ });
7172
+ return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
7173
+ }
7174
+ function runTelegramNotifySet(opts) {
7175
+ setNotify(opts.stateRoot, opts.project, opts.active);
7176
+ }
7177
+ function runTelegramNotifyPref(args) {
7178
+ const { project, dimension, value, root } = args;
7179
+ if (dimension === "crew") {
7180
+ if (!["all", "alert_only", "done_only", "none"].includes(value))
7181
+ return { ok: false, message: "crew must be all|alert_only|done_only|none" };
7182
+ saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
7183
+ return { ok: true };
7184
+ }
7185
+ if (value !== "on" && value !== "off")
7186
+ return { ok: false, message: "cap must be on|off" };
7187
+ saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
7188
+ return { ok: true };
7189
+ }
7190
+ function runTelegramNotifyStatus(opts) {
7191
+ const s = loadState(opts.stateRoot);
7192
+ const projects = /* @__PURE__ */ new Set();
7193
+ for (const key of Object.keys(s.topics)) {
7194
+ const sep3 = key.indexOf("::");
7195
+ projects.add(sep3 === -1 ? key : key.slice(0, sep3));
7196
+ }
7197
+ for (const p of Object.keys(s.notify))
7198
+ projects.add(p);
7199
+ return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
7200
+ }
7201
+ function capAllowed(project, globalNotify, root) {
7202
+ return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
7203
+ }
7204
+ function confirmationText(project, before, after, dim) {
7205
+ if (dim === "active")
7206
+ return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
7207
+ if (dim === "cap")
7208
+ return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
7209
+ return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
7210
+ }
7211
+ async function runNotifyConfirmation(opts) {
7212
+ const { quieter, dim } = isQuieter(opts.before, opts.after);
7213
+ if (!quieter || dim === null)
7214
+ return false;
7215
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7216
+ if (topicId === void 0)
7217
+ return false;
7218
+ const text = confirmationText(opts.project, opts.before, opts.after, dim);
7219
+ try {
7220
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
7221
+ return true;
7222
+ } catch {
7223
+ console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
7224
+ return false;
7225
+ }
7226
+ }
7227
+ async function runTelegramSend(opts) {
7228
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7229
+ if (topicId === void 0) {
7230
+ throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
7231
+ }
7232
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
7233
+ clearPending(opts.stateRoot, opts.project);
7234
+ return { chatId: opts.cfg.supergroupId, topicId };
7235
+ }
7236
+ async function runTelegramLink(opts) {
7237
+ const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7238
+ if (existing !== void 0)
7239
+ return { topicId: existing, created: false };
7240
+ const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
7241
+ const winner = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7242
+ if (winner !== void 0) {
7243
+ try {
7244
+ await opts.client.deleteForumTopic?.(opts.cfg.supergroupId, topicId);
7245
+ } catch {
7246
+ }
7247
+ return { topicId: winner, created: false };
7248
+ }
7249
+ setTopic(opts.stateRoot, opts.project, topicId);
7250
+ return { topicId, created: true };
7251
+ }
7252
+ var init_notify = __esm({
7253
+ "packages/core/dist/telegram/notify.js"() {
7254
+ init_dist();
7255
+ init_state();
7256
+ init_format();
6627
7257
  }
6628
7258
  });
6629
7259
 
@@ -6711,8 +7341,10 @@ var init_tiers = __esm({
6711
7341
  });
6712
7342
 
6713
7343
  // packages/core/dist/telegram/bridge.js
7344
+ import fs11 from "fs";
6714
7345
  import os5 from "os";
6715
7346
  import path12 from "path";
7347
+ import crypto2 from "crypto";
6716
7348
  function formatInboundReceipt(project, outcome) {
6717
7349
  if (!outcome)
6718
7350
  return void 0;
@@ -6728,6 +7360,85 @@ function formatInboundReceipt(project, outcome) {
6728
7360
  return void 0;
6729
7361
  }
6730
7362
  }
7363
+ function telegramPollLockPath(token) {
7364
+ const hash = crypto2.createHash("sha256").update(token).digest("hex").slice(0, 16);
7365
+ return path12.join(os5.tmpdir(), `squadrant-telegram-poll-${hash}.lock`);
7366
+ }
7367
+ function pidAlive(pid) {
7368
+ try {
7369
+ process.kill(pid, 0);
7370
+ return true;
7371
+ } catch (e) {
7372
+ return e.code === "EPERM";
7373
+ }
7374
+ }
7375
+ function lockIsHeld(file) {
7376
+ let pid;
7377
+ let ageMs;
7378
+ try {
7379
+ const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
7380
+ pid = Number.isFinite(parsed) ? parsed : void 0;
7381
+ ageMs = Date.now() - fs11.statSync(file).mtimeMs;
7382
+ } catch {
7383
+ return false;
7384
+ }
7385
+ if (pid !== void 0)
7386
+ return pidAlive(pid);
7387
+ return ageMs < POLL_LOCK_STALE_MS;
7388
+ }
7389
+ function acquirePollLock(token) {
7390
+ const file = telegramPollLockPath(token);
7391
+ for (let attempt = 0; attempt < 2; attempt++) {
7392
+ try {
7393
+ const fd = fs11.openSync(file, "wx");
7394
+ fs11.writeSync(fd, String(process.pid));
7395
+ fs11.closeSync(fd);
7396
+ return () => {
7397
+ try {
7398
+ fs11.unlinkSync(file);
7399
+ } catch {
7400
+ }
7401
+ };
7402
+ } catch (e) {
7403
+ if (e.code !== "EEXIST")
7404
+ return null;
7405
+ if (attempt === 0 && !lockIsHeld(file)) {
7406
+ try {
7407
+ fs11.unlinkSync(file);
7408
+ } catch {
7409
+ }
7410
+ continue;
7411
+ }
7412
+ return null;
7413
+ }
7414
+ }
7415
+ return null;
7416
+ }
7417
+ function defaultSchedule2(fn, ms) {
7418
+ const t = setTimeout(fn, ms);
7419
+ t.unref?.();
7420
+ return () => clearTimeout(t);
7421
+ }
7422
+ function foreignPollLockHolder(token, selfPid = process.pid) {
7423
+ const file = telegramPollLockPath(token);
7424
+ let pid;
7425
+ try {
7426
+ const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
7427
+ if (!Number.isFinite(parsed))
7428
+ return null;
7429
+ pid = parsed;
7430
+ } catch {
7431
+ return null;
7432
+ }
7433
+ if (pid === selfPid)
7434
+ return null;
7435
+ return pidAlive(pid) ? pid : null;
7436
+ }
7437
+ function pollBackoffMs(err, pollMs) {
7438
+ if (err instanceof TelegramApiError && err.retryAfterSec !== void 0)
7439
+ return err.retryAfterSec * 1e3;
7440
+ return pollMs;
7441
+ }
6731
7442
  function parseNotifyPref(text) {
6732
7443
  const parts = text.trim().split(/\s+/);
6733
7444
  if (stripBotMention(parts[0] ?? "").toLowerCase() !== "/notify")
@@ -6754,9 +7465,17 @@ function notifyToggle(text) {
6754
7465
  }
6755
7466
  function createTelegramBridge(opts) {
6756
7467
  const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
7468
+ const lifecycle = opts.lifecycle;
6757
7469
  const configRoot = opts.configRoot ?? path12.join(os5.homedir(), ".config", "squadrant");
6758
7470
  const pollMs = cfg.pollMs ?? 1e3;
7471
+ const schedule = opts.schedule ?? defaultSchedule2;
7472
+ const lockToken = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
7473
+ let releasePollLock = null;
7474
+ let pollAbort = null;
6759
7475
  let running = false;
7476
+ let pollStopped = false;
7477
+ let selfHealCancel = null;
7478
+ let conflictCount = 0;
6760
7479
  let lastSuccessfulPollAt = null;
6761
7480
  let lastError = null;
6762
7481
  let lastErrorAt = null;
@@ -6766,12 +7485,8 @@ function createTelegramBridge(opts) {
6766
7485
  saveState(stateRoot, s);
6767
7486
  }
6768
7487
  async function sendToTopic(project, text) {
6769
- let threadId = loadState(stateRoot).topics[topicKey(project)];
6770
- if (threadId === void 0) {
6771
- threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
6772
- setTopic(stateRoot, project, threadId);
6773
- }
6774
- await client.sendMessage(cfg.supergroupId, threadId, text);
7488
+ const { topicId } = await runTelegramLink({ project, cfg, client, stateRoot });
7489
+ await client.sendMessage(cfg.supergroupId, topicId, text);
6775
7490
  }
6776
7491
  async function deliverOutbound(project, ev) {
6777
7492
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
@@ -6958,10 +7673,13 @@ ${formatUsageLine(usage2)}`;
6958
7673
  }
6959
7674
  await runChannelCommand(text, fromId, void 0);
6960
7675
  }
6961
- async function handleProjectTopic(text, threadId, fromId) {
7676
+ async function handleProjectTopic(text, threadId, fromId, messageId, media) {
6962
7677
  const resolved = findProjectByThread(stateRoot, threadId);
6963
- if (!resolved)
7678
+ if (!resolved) {
7679
+ log(`telegram inbound in unbound topic thread=${threadId} \u2014 no project linked, message dropped`);
7680
+ await reply(threadId, "\u26A0\uFE0F this topic isn't linked to a project \u2014 run: squadrant telegram link <project>");
6964
7681
  return;
7682
+ }
6965
7683
  if (isBareSpawn(text)) {
6966
7684
  if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
6967
7685
  await reply(threadId, "\u26D4 not authorized");
@@ -7011,21 +7729,16 @@ ${formatUsageLine(usage2)}`;
7011
7729
  await runChannelCommand(text, fromId, threadId);
7012
7730
  return;
7013
7731
  }
7014
- void client.sendChatAction(cfg.supergroupId, threadId, "typing").catch((e) => {
7015
- log(`telegram sendChatAction failed: ${e.message}`);
7016
- });
7732
+ if (messageId !== void 0) {
7733
+ void Promise.resolve(client.setMessageReaction?.(cfg.supergroupId, messageId, "\u{1F44D}")).catch((e) => log(`telegram stage-1 reaction failed project=${resolved.project}: ${e.message}`));
7734
+ }
7017
7735
  setNotify(stateRoot, resolved.project, true);
7018
7736
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
7019
- try {
7020
- const r = await ensureCaptainAlive(resolved.project);
7737
+ void ensureCaptainAlive(resolved.project).then((r) => {
7021
7738
  if (r === "timeout") {
7022
- await reply(threadId, `\u274C couldn't reach ${resolved.project} captain \u2014 saved to mailbox, will deliver when you open the workspace.`);
7023
- } else {
7024
- await reply(threadId, `\u{1F4E8} delivered to ${resolved.project} captain`);
7739
+ log(`telegram auto-launch: warmup timed out project=${resolved.project} \u2014 delivery verdict (not this probe) decides the receipt`);
7025
7740
  }
7026
- } catch (e) {
7027
- log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
7028
- }
7741
+ }).catch((e) => log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`));
7029
7742
  }
7030
7743
  let handled = false;
7031
7744
  let outcome;
@@ -7034,13 +7747,22 @@ ${formatUsageLine(usage2)}`;
7034
7747
  handled = res.handled;
7035
7748
  outcome = res.outcome;
7036
7749
  }
7750
+ const deliveryHeld = outcome?.status === "held";
7037
7751
  if (!handled) {
7038
7752
  await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
7753
+ outcome = void 0;
7039
7754
  }
7040
7755
  const receipt = formatInboundReceipt(resolved.project, outcome);
7041
7756
  if (receipt && sendReply) {
7042
7757
  await sendReply(threadId, receipt).catch((e) => log(`telegram receipt failed project=${resolved.project}: ${e.message}`));
7043
7758
  }
7759
+ if (media) {
7760
+ await reply(threadId, formatMediaReceipt(media.kind, media.hasCaption));
7761
+ }
7762
+ if (deliveryHeld)
7763
+ return;
7764
+ await reply(threadId, CAPTAIN_RECEIVED_ACK);
7765
+ lifecycle?.begin(resolved.project, threadId);
7044
7766
  }
7045
7767
  async function handleUpdate(u) {
7046
7768
  if (u.callback_query) {
@@ -7048,7 +7770,11 @@ ${formatUsageLine(usage2)}`;
7048
7770
  return;
7049
7771
  }
7050
7772
  const m = u.message;
7051
- if (!m || m.text === void 0)
7773
+ if (!m)
7774
+ return;
7775
+ const kind = mediaKind(m);
7776
+ const text = m.text ?? m.caption;
7777
+ if (text === void 0 && kind === void 0)
7052
7778
  return;
7053
7779
  if (!cfg.chats.includes(m.chat.id))
7054
7780
  return;
@@ -7062,7 +7788,7 @@ ${formatUsageLine(usage2)}`;
7062
7788
  await reply(threadId, "\u26D4 not authorized");
7063
7789
  return;
7064
7790
  }
7065
- const task = m.text.trim();
7791
+ const task = (text ?? "").trim();
7066
7792
  if (!task) {
7067
7793
  await reply(threadId, "spawn cancelled \u2014 empty task");
7068
7794
  return;
@@ -7073,39 +7799,122 @@ ${formatUsageLine(usage2)}`;
7073
7799
  return;
7074
7800
  }
7075
7801
  if (m.message_thread_id === void 0) {
7802
+ if (m.text === void 0)
7803
+ return;
7076
7804
  await handleGeneral(m.text, m.from?.id);
7077
7805
  return;
7078
7806
  }
7079
- await handleProjectTopic(m.text, m.message_thread_id, m.from?.id);
7807
+ 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 });
7808
+ }
7809
+ function acquirePollLockOrLog() {
7810
+ if (!lockToken)
7811
+ return true;
7812
+ releasePollLock = acquirePollLock(lockToken);
7813
+ if (releasePollLock)
7814
+ return true;
7815
+ const holder = foreignPollLockHolder(lockToken);
7816
+ log(`telegram inbound poll disabled: another consumer already polls this bot token (single-consumer guard, #830)${holder !== null ? ` \u2014 lock held by pid ${holder}` : ""}`);
7817
+ return false;
7818
+ }
7819
+ function scheduleSelfHeal() {
7820
+ selfHealCancel?.();
7821
+ selfHealCancel = schedule(() => {
7822
+ selfHealCancel = null;
7823
+ if (pollStopped || running)
7824
+ return;
7825
+ if (!acquirePollLockOrLog()) {
7826
+ scheduleSelfHeal();
7827
+ return;
7828
+ }
7829
+ running = true;
7830
+ conflictCount = 0;
7831
+ log("telegram inbound poll re-attempting after a 409 stop (#850)");
7832
+ void pollLoop();
7833
+ }, CONFLICT_409_SELF_HEAL_MS);
7080
7834
  }
7081
7835
  async function pollLoop() {
7082
7836
  while (running) {
7837
+ const ac = new AbortController();
7838
+ pollAbort = ac;
7839
+ let waitMs = pollMs;
7083
7840
  try {
7084
7841
  const offset = loadState(stateRoot).offset;
7085
- const updates = await client.getUpdates(offset, LONG_POLL_SEC);
7842
+ const updates = await client.getUpdates(offset, LONG_POLL_SEC, ac.signal);
7843
+ conflictCount = 0;
7086
7844
  for (const u of updates) {
7087
7845
  await handleUpdate(u);
7088
7846
  persistOffset(u.update_id + 1);
7089
7847
  }
7090
7848
  lastSuccessfulPollAt = Date.now();
7091
7849
  } catch (e) {
7092
- lastError = e.message;
7850
+ if (ac.signal.aborted)
7851
+ break;
7852
+ const msg = e.message;
7853
+ lastError = msg;
7093
7854
  lastErrorAt = Date.now();
7094
- log(`telegram inbound poll failed: ${e.message}`);
7855
+ if (e instanceof TelegramApiError && e.code === 409) {
7856
+ conflictCount += 1;
7857
+ if (conflictCount >= CONFLICT_409_TERMINAL_AFTER) {
7858
+ log(`telegram inbound poll stopped: repeated 409 conflict (another getUpdates consumer holds this token) \u2014 ${msg}`);
7859
+ break;
7860
+ }
7861
+ } else {
7862
+ log(`telegram inbound poll failed: ${msg}`);
7863
+ }
7864
+ waitMs = pollBackoffMs(e, pollMs);
7865
+ } finally {
7866
+ pollAbort = null;
7095
7867
  }
7096
7868
  if (running)
7097
- await sleep2(pollMs);
7869
+ await sleep2(waitMs);
7098
7870
  }
7871
+ running = false;
7872
+ releasePollLock?.();
7873
+ releasePollLock = null;
7874
+ if (!pollStopped)
7875
+ scheduleSelfHeal();
7099
7876
  }
7100
7877
  return {
7101
7878
  start() {
7102
7879
  if (running)
7103
7880
  return;
7881
+ pollStopped = false;
7882
+ selfHealCancel?.();
7883
+ selfHealCancel = null;
7884
+ if (!acquirePollLockOrLog())
7885
+ return;
7886
+ const known = opts.registeredProjects?.();
7887
+ if (known !== void 0) {
7888
+ try {
7889
+ const removed = pruneTopics(stateRoot, (p) => known.includes(p));
7890
+ if (removed.length > 0)
7891
+ log(`telegram registry pruned ${removed.length} stale topic link(s): ${removed.join(", ")}`);
7892
+ } catch (e) {
7893
+ log(`telegram registry prune failed: ${e.message}`);
7894
+ }
7895
+ }
7104
7896
  running = true;
7897
+ conflictCount = 0;
7898
+ try {
7899
+ lifecycle?.start();
7900
+ } catch (e) {
7901
+ log(`telegram lifecycle start failed: ${e.message}`);
7902
+ }
7105
7903
  void pollLoop();
7106
7904
  },
7107
7905
  stop() {
7108
7906
  running = false;
7907
+ pollStopped = true;
7908
+ selfHealCancel?.();
7909
+ selfHealCancel = null;
7910
+ try {
7911
+ lifecycle?.stop();
7912
+ } catch (e) {
7913
+ log(`telegram lifecycle stop failed: ${e.message}`);
7914
+ }
7915
+ pollAbort?.abort();
7916
+ releasePollLock?.();
7917
+ releasePollLock = null;
7109
7918
  },
7110
7919
  pushLifecycle(project, ev) {
7111
7920
  void deliverOutbound(project, ev).catch((e) => {
@@ -7122,17 +7931,23 @@ ${formatUsageLine(usage2)}`;
7122
7931
  }
7123
7932
  };
7124
7933
  }
7125
- var LONG_POLL_SEC, sleep2, CREW_TIERS, RECOGNIZED_CHANNEL_COMMANDS;
7934
+ 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;
7126
7935
  var init_bridge = __esm({
7127
7936
  "packages/core/dist/telegram/bridge.js"() {
7128
7937
  init_dist();
7938
+ init_client();
7129
7939
  init_auth();
7130
7940
  init_commands();
7131
7941
  init_format();
7942
+ init_notify();
7132
7943
  init_panels();
7133
7944
  init_state();
7134
7945
  init_tiers();
7946
+ CAPTAIN_RECEIVED_ACK = "\u2705 captain received";
7135
7947
  LONG_POLL_SEC = 50;
7948
+ POLL_LOCK_STALE_MS = 2 * 6e4;
7949
+ CONFLICT_409_TERMINAL_AFTER = 3;
7950
+ CONFLICT_409_SELF_HEAL_MS = 5 * 6e4;
7136
7951
  sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
7137
7952
  CREW_TIERS = ["all", "alert_only", "done_only", "none"];
7138
7953
  RECOGNIZED_CHANNEL_COMMANDS = /* @__PURE__ */ new Set(["status", "projects", "crews", "launch", "effort", "spawn"]);
@@ -7177,7 +7992,7 @@ var init_restart_daemon = __esm({
7177
7992
  });
7178
7993
 
7179
7994
  // packages/core/dist/telegram/setup.js
7180
- import fs11 from "fs";
7995
+ import fs12 from "fs";
7181
7996
  function resolveSetupGroup(existingSupergroupId, opts) {
7182
7997
  if (existingSupergroupId !== void 0 && !opts.redetect)
7183
7998
  return "reuse";
@@ -7228,7 +8043,7 @@ function writeTelegramConfig(configPath, opts) {
7228
8043
  let config;
7229
8044
  let raw = null;
7230
8045
  try {
7231
- raw = fs11.readFileSync(configPath, "utf-8");
8046
+ raw = fs12.readFileSync(configPath, "utf-8");
7232
8047
  } catch (err) {
7233
8048
  if (err.code !== "ENOENT") {
7234
8049
  throw new Error(`refusing to overwrite unreadable config at ${configPath}: ${String(err)}`);
@@ -7251,107 +8066,19 @@ function writeTelegramConfig(configPath, opts) {
7251
8066
  };
7252
8067
  const users = opts.users ?? prev.users;
7253
8068
  const remoteControl = opts.remoteControl ?? prev.remoteControl;
7254
- if (users !== void 0)
7255
- next.users = users;
7256
- if (remoteControl !== void 0)
7257
- next.remoteControl = remoteControl;
7258
- config.telegram = next;
7259
- writeConfigFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7260
- }
7261
- var init_setup = __esm({
7262
- "packages/core/dist/telegram/setup.js"() {
7263
- init_dist();
7264
- init_state();
7265
- init_bot_commands();
7266
- init_restart_daemon();
7267
- }
7268
- });
7269
-
7270
- // packages/core/dist/telegram/notify.js
7271
- function runTelegramStatus(opts) {
7272
- const tg = opts.config.telegram;
7273
- const env = opts.env ?? process.env;
7274
- const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
7275
- const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
7276
- const sep3 = key.indexOf("::");
7277
- return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
7278
- });
7279
- return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
7280
- }
7281
- function runTelegramNotifySet(opts) {
7282
- setNotify(opts.stateRoot, opts.project, opts.active);
7283
- }
7284
- function runTelegramNotifyPref(args) {
7285
- const { project, dimension, value, root } = args;
7286
- if (dimension === "crew") {
7287
- if (!["all", "alert_only", "done_only", "none"].includes(value))
7288
- return { ok: false, message: "crew must be all|alert_only|done_only|none" };
7289
- saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
7290
- return { ok: true };
7291
- }
7292
- if (value !== "on" && value !== "off")
7293
- return { ok: false, message: "cap must be on|off" };
7294
- saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
7295
- return { ok: true };
7296
- }
7297
- function runTelegramNotifyStatus(opts) {
7298
- const s = loadState(opts.stateRoot);
7299
- const projects = /* @__PURE__ */ new Set();
7300
- for (const key of Object.keys(s.topics)) {
7301
- const sep3 = key.indexOf("::");
7302
- projects.add(sep3 === -1 ? key : key.slice(0, sep3));
7303
- }
7304
- for (const p of Object.keys(s.notify))
7305
- projects.add(p);
7306
- return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
7307
- }
7308
- function capAllowed(project, globalNotify, root) {
7309
- return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
7310
- }
7311
- function confirmationText(project, before, after, dim) {
7312
- if (dim === "active")
7313
- return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
7314
- if (dim === "cap")
7315
- return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
7316
- return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
7317
- }
7318
- async function runNotifyConfirmation(opts) {
7319
- const { quieter, dim } = isQuieter(opts.before, opts.after);
7320
- if (!quieter || dim === null)
7321
- return false;
7322
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7323
- if (topicId === void 0)
7324
- return false;
7325
- const text = confirmationText(opts.project, opts.before, opts.after, dim);
7326
- try {
7327
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
7328
- return true;
7329
- } catch {
7330
- console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
7331
- return false;
7332
- }
7333
- }
7334
- async function runTelegramSend(opts) {
7335
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7336
- if (topicId === void 0) {
7337
- throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
7338
- }
7339
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
7340
- return { chatId: opts.cfg.supergroupId, topicId };
7341
- }
7342
- async function runTelegramLink(opts) {
7343
- const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
7344
- if (existing !== void 0)
7345
- return { topicId: existing, created: false };
7346
- const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
7347
- setTopic(opts.stateRoot, opts.project, topicId);
7348
- return { topicId, created: true };
8069
+ if (users !== void 0)
8070
+ next.users = users;
8071
+ if (remoteControl !== void 0)
8072
+ next.remoteControl = remoteControl;
8073
+ config.telegram = next;
8074
+ writeConfigFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7349
8075
  }
7350
- var init_notify = __esm({
7351
- "packages/core/dist/telegram/notify.js"() {
8076
+ var init_setup = __esm({
8077
+ "packages/core/dist/telegram/setup.js"() {
7352
8078
  init_dist();
7353
8079
  init_state();
7354
- init_format();
8080
+ init_bot_commands();
8081
+ init_restart_daemon();
7355
8082
  }
7356
8083
  });
7357
8084
 
@@ -7366,6 +8093,7 @@ var init_telegram = __esm({
7366
8093
  init_format();
7367
8094
  init_state();
7368
8095
  init_client();
8096
+ init_inbound_lifecycle();
7369
8097
  init_bridge();
7370
8098
  init_setup();
7371
8099
  init_notify();
@@ -8000,6 +8728,12 @@ function resolveGateMode(env, gate) {
8000
8728
  return fromEnv;
8001
8729
  return gate?.mode && isGateMode(gate.mode) ? gate.mode : "auto";
8002
8730
  }
8731
+ function resolveGateEngine(env, gate) {
8732
+ const fromEnv = env.SQUADRANT_GATE_ENGINE;
8733
+ if (fromEnv !== void 0 && isGateEngine(fromEnv))
8734
+ return fromEnv;
8735
+ return gate?.engine && isGateEngine(gate.engine) ? gate.engine : "router";
8736
+ }
8003
8737
  function resolveGatePolicy(env, gate) {
8004
8738
  const fromEnv = env.SQUADRANT_GATE_POLICY;
8005
8739
  if (fromEnv !== void 0 && isGatePolicy(fromEnv))
@@ -8384,6 +9118,59 @@ var init_permission_gate = __esm({
8384
9118
  }
8385
9119
  });
8386
9120
 
9121
+ // packages/core/dist/auto-gate.js
9122
+ import { createAutoGate, projectGateConfig } from "@squadrant-ai/auto-gate";
9123
+ function gateForProjection(config) {
9124
+ const gate = config.defaults.gate;
9125
+ if (!gate)
9126
+ return {};
9127
+ const out = {};
9128
+ if (gate.mode)
9129
+ out.mode = gate.mode;
9130
+ if (gate.tools)
9131
+ out.tools = gate.tools;
9132
+ if (gate.deny)
9133
+ out.deny = gate.deny;
9134
+ return out;
9135
+ }
9136
+ function createSquadrantAutoGate(deps) {
9137
+ const env = deps.env ?? process.env;
9138
+ const taskId = env.SQUADRANT_CREW_TASK_ID;
9139
+ const project = deps.project ?? env.SQUADRANT_CREW_PROJECT;
9140
+ const mode = resolveGateMode(env, deps.config.defaults.gate);
9141
+ const projected = projectGateConfig({ gate: { ...gateForProjection(deps.config), mode } });
9142
+ const enabled = mode === "on";
9143
+ const blockedSignal = async (ctx) => {
9144
+ if (!taskId || !project || !deps.sendBlocked)
9145
+ return;
9146
+ const event = {
9147
+ type: "task.blocked",
9148
+ id: taskId,
9149
+ reason: ctx.reason,
9150
+ question: `crew needs permission to run ${ctx.tool}`
9151
+ };
9152
+ await deps.sendBlocked(project, event);
9153
+ };
9154
+ const host = createAutoGate({
9155
+ config: projected,
9156
+ readIntent: (p) => readUserIntent(p) ?? void 0,
9157
+ blockedSignal,
9158
+ ...deps.decide ? { decide: deps.decide } : {},
9159
+ ...project ? { project } : {},
9160
+ env,
9161
+ log: deps.log
9162
+ });
9163
+ if (!enabled) {
9164
+ return { config: host.config, decideClaudeHookPayload: async () => void 0 };
9165
+ }
9166
+ return host;
9167
+ }
9168
+ var init_auto_gate = __esm({
9169
+ "packages/core/dist/auto-gate.js"() {
9170
+ init_permission_gate();
9171
+ }
9172
+ });
9173
+
8387
9174
  // packages/core/dist/group-dispatch.js
8388
9175
  import { randomUUID as randomUUID4 } from "crypto";
8389
9176
  import { createConnection as createConnection2 } from "net";
@@ -8631,7 +9418,7 @@ var init_launch_workspace = __esm({
8631
9418
  });
8632
9419
 
8633
9420
  // packages/core/dist/side-session.js
8634
- import fs12 from "fs";
9421
+ import fs13 from "fs";
8635
9422
  function sideTitleFor(project, name) {
8636
9423
  return `\u{1F5D2} ${project}:${name}`;
8637
9424
  }
@@ -8733,7 +9520,7 @@ async function runSideClose(runtime, workspaceId, project, name, projPath, workt
8733
9520
  await runtime.closePane(pane);
8734
9521
  if (projPath) {
8735
9522
  const wtPath = worktreePath(projPath, worktreeDir, project, name);
8736
- if (fs12.existsSync(wtPath)) {
9523
+ if (fs13.existsSync(wtPath)) {
8737
9524
  try {
8738
9525
  removeWorktree(projPath, wtPath);
8739
9526
  } catch (e) {
@@ -8790,10 +9577,11 @@ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps,
8790
9577
  if (!crew) {
8791
9578
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
8792
9579
  }
8793
- const options = await deps.readModalOptions(crew);
8794
- if (!options) {
9580
+ const modal = await deps.readModalOptions(crew);
9581
+ if (!modal || modal.options.length === 0) {
8795
9582
  throw new Error(`Crew '${name}' has no interactive option prompt visible right now \u2014 nothing to answer. Read its screen with 'squadrant crew read ${project} ${name}' to check its state.`);
8796
9583
  }
9584
+ const options = modal.options;
8797
9585
  const target = resolveOption(options, option);
8798
9586
  if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
8799
9587
  throw new Error(`Refusing: option ${target.index} is "${target.label}", which does not contain expected text "${opts.expect}". Option order is model-generated and can shift between renders \u2014 re-check with 'squadrant crew read ${project} ${name}'.
@@ -8805,7 +9593,9 @@ ${describeOptions(options)}`);
8805
9593
  log(`\u2192 selecting ${target.index}. "${target.label}"`);
8806
9594
  const current = options.find((o) => o.highlighted) ?? options[0];
8807
9595
  const steps = target.index - current.index;
8808
- const key = steps >= 0 ? "Down" : "Up";
9596
+ const forwardKey = modal.axis === "horizontal" ? "Right" : "Down";
9597
+ const backwardKey = modal.axis === "horizontal" ? "Left" : "Up";
9598
+ const key = steps >= 0 ? forwardKey : backwardKey;
8809
9599
  for (let i = 0; i < Math.abs(steps); i++) {
8810
9600
  await runtime.sendKeyToPane(crew, key);
8811
9601
  }
@@ -9170,11 +9960,14 @@ __export(dist_exports2, {
9170
9960
  AGENT_BINS: () => AGENT_BINS,
9171
9961
  ANTHROPIC_ONLY_REQUEST_FIELDS: () => ANTHROPIC_ONLY_REQUEST_FIELDS,
9172
9962
  BOT_COMMANDS: () => BOT_COMMANDS,
9963
+ CAPTAIN_RECEIVED_ACK: () => CAPTAIN_RECEIVED_ACK,
9173
9964
  CC_SOCKS_DIR: () => CC_SOCKS_DIR,
9174
9965
  CMUX_PRESERVE_CLAUDE_AUTH_ENV: () => CMUX_PRESERVE_CLAUDE_AUTH_ENV,
9966
+ CONFLICT_409_SELF_HEAL_MS: () => CONFLICT_409_SELF_HEAL_MS,
9175
9967
  CREW_GONE_MS: () => CREW_GONE_MS,
9176
9968
  CREW_STALE_MS: () => CREW_STALE_MS,
9177
9969
  CaptainDelivery: () => CaptainDelivery,
9970
+ DEFAULT_BLOCKED_RENUDGE_MS: () => DEFAULT_BLOCKED_RENUDGE_MS,
9178
9971
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS: () => DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS,
9179
9972
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
9180
9973
  DEFAULT_GATE_DENY_RULES: () => DEFAULT_GATE_DENY_RULES,
@@ -9191,6 +9984,7 @@ __export(dist_exports2, {
9191
9984
  LivenessRegistry: () => LivenessRegistry,
9192
9985
  MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
9193
9986
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
9987
+ PANE_IDLE_AFTER_MS: () => PANE_IDLE_AFTER_MS,
9194
9988
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
9195
9989
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
9196
9990
  READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
@@ -9203,6 +9997,10 @@ __export(dist_exports2, {
9203
9997
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
9204
9998
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
9205
9999
  TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
10000
+ TYPING_KEEPALIVE_MS: () => TYPING_KEEPALIVE_MS,
10001
+ TelegramApiError: () => TelegramApiError,
10002
+ WATCHDOG_GRACE_MS: () => WATCHDOG_GRACE_MS,
10003
+ WATCHDOG_MS: () => WATCHDOG_MS,
9206
10004
  WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
9207
10005
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
9208
10006
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
@@ -9224,15 +10022,19 @@ __export(dist_exports2, {
9224
10022
  callClassifier: () => callClassifier,
9225
10023
  capAllowed: () => capAllowed,
9226
10024
  capOutput: () => capOutput,
10025
+ captainAliveStateFromHealth: () => captainAliveStateFromHealth,
9227
10026
  captainRecordPath: () => captainRecordPath,
9228
10027
  captainSocketPath: () => captainSocketPath,
9229
10028
  checkFact: () => checkFact,
10029
+ classifyCaptainPane: () => classifyCaptainPane,
9230
10030
  classifyHealth: () => classifyHealth,
9231
10031
  claudeEnvShadowsRouter: () => claudeEnvShadowsRouter,
10032
+ clearPending: () => clearPending,
9232
10033
  closeWorkItem: () => closeWorkItem,
9233
10034
  computeTemplateHash: () => computeTemplateHash,
9234
10035
  consumeExitMarker: () => consumeExitMarker,
9235
10036
  createAttach: () => createAttach,
10037
+ createCaptainPaneReader: () => createCaptainPaneReader,
9236
10038
  createCrewPaneReader: () => createCrewPaneReader,
9237
10039
  createDaemon: () => createDaemon,
9238
10040
  createDecoder: () => createDecoder,
@@ -9241,12 +10043,14 @@ __export(dist_exports2, {
9241
10043
  createDirectSurfaceLivenessProbe: () => createDirectSurfaceLivenessProbe,
9242
10044
  createEnsureCaptainAlive: () => createEnsureCaptainAlive,
9243
10045
  createEventsSource: () => createEventsSource,
10046
+ createInboundLifecycle: () => createInboundLifecycle,
9244
10047
  createInteractiveProbe: () => createInteractiveProbe,
9245
10048
  createIsCaptainAlive: () => createIsCaptainAlive,
9246
10049
  createLaunch: () => createLaunch,
9247
10050
  createRouterService: () => createRouterService,
9248
10051
  createRouterShim: () => createRouterShim,
9249
10052
  createRunCommand: () => createRunCommand,
10053
+ createSquadrantAutoGate: () => createSquadrantAutoGate,
9250
10054
  createStore: () => createStore,
9251
10055
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
9252
10056
  createTelegramBridge: () => createTelegramBridge,
@@ -9296,12 +10100,16 @@ __export(dist_exports2, {
9296
10100
  formatInbound: () => formatInbound,
9297
10101
  formatInboundReceipt: () => formatInboundReceipt,
9298
10102
  formatLifecycle: () => formatLifecycle,
10103
+ formatMediaReceipt: () => formatMediaReceipt,
9299
10104
  formatPermissionDecision: () => formatPermissionDecision,
9300
10105
  formatUsageLine: () => formatUsageLine,
10106
+ formatWatchdogWarning: () => formatWatchdogWarning,
9301
10107
  freshTrace: () => freshTrace,
9302
10108
  gateCacheKey: () => gateCacheKey,
9303
10109
  getDaemonPid: () => getDaemonPid,
10110
+ hasModalOptionList: () => hasModalOptionList,
9304
10111
  healCmdFor: () => healCmdFor,
10112
+ inboundBody: () => inboundBody,
9305
10113
  isAuthorized: () => isAuthorized,
9306
10114
  isBareSpawn: () => isBareSpawn,
9307
10115
  isCaptainAlive: () => isCaptainAlive,
@@ -9323,18 +10131,23 @@ __export(dist_exports2, {
9323
10131
  kickstartArgv: () => kickstartArgv,
9324
10132
  launchOneWorkspace: () => launchOneWorkspace,
9325
10133
  listSessions: () => listSessions,
10134
+ loadPending: () => loadPending,
9326
10135
  loadSessions: () => loadSessions,
9327
10136
  loadState: () => loadState,
9328
10137
  mailboxStats: () => mailboxStats,
9329
10138
  makeGate: () => makeGate,
10139
+ markPendingWarned: () => markPendingWarned,
9330
10140
  maskToken: () => maskToken,
9331
10141
  matchTier1Deny: () => matchTier1Deny,
10142
+ mediaKind: () => mediaKind,
10143
+ mediaMarker: () => mediaMarker,
9332
10144
  mergeClaudeEnvRouterSettings: () => mergeClaudeEnvRouterSettings,
9333
10145
  mintToken: () => mintToken,
9334
10146
  nameFromTitle: () => nameFromTitle,
9335
10147
  newestSessionInDirectory: () => newestSessionInDirectory,
9336
10148
  nextAutoName: () => nextAutoName,
9337
10149
  niceCrewCommand: () => niceCrewCommand,
10150
+ notePaneScreen: () => notePaneScreen,
9338
10151
  notifyToggle: () => notifyToggle,
9339
10152
  parseBearer: () => parseBearer,
9340
10153
  parseClassifierVerdict: () => parseClassifierVerdict,
@@ -9343,6 +10156,7 @@ __export(dist_exports2, {
9343
10156
  parseNotifyPref: () => parseNotifyPref,
9344
10157
  parseProgramArgs: () => parseProgramArgs,
9345
10158
  plistPath: () => plistPath,
10159
+ pollBackoffMs: () => pollBackoffMs,
9346
10160
  pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
9347
10161
  prepareCaptainRoute: () => prepareCaptainRoute,
9348
10162
  printForeignInstallError: () => printForeignInstallError,
@@ -9350,6 +10164,7 @@ __export(dist_exports2, {
9350
10164
  probeCaptainChannel: () => probeCaptainChannel,
9351
10165
  programArgsBlock: () => programArgsBlock,
9352
10166
  projectHealth: () => projectHealth,
10167
+ pruneTopics: () => pruneTopics,
9353
10168
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
9354
10169
  readCaptainAddress: () => readCaptainAddress,
9355
10170
  readCursor: () => readCursor,
@@ -9377,6 +10192,7 @@ __export(dist_exports2, {
9377
10192
  resolveCurrentProject: () => resolveCurrentProject,
9378
10193
  resolveGate: () => resolveGate,
9379
10194
  resolveGateDenyRules: () => resolveGateDenyRules,
10195
+ resolveGateEngine: () => resolveGateEngine,
9380
10196
  resolveGateMode: () => resolveGateMode,
9381
10197
  resolveGatePolicy: () => resolveGatePolicy,
9382
10198
  resolveGateTools: () => resolveGateTools,
@@ -9416,9 +10232,11 @@ __export(dist_exports2, {
9416
10232
  saveSessions: () => saveSessions,
9417
10233
  saveState: () => saveState,
9418
10234
  screenHasSplashMarker: () => screenHasSplashMarker,
10235
+ screenHash: () => screenHash,
9419
10236
  sendRequest: () => sendRequest,
9420
10237
  setLastUserId: () => setLastUserId,
9421
10238
  setNotify: () => setNotify,
10239
+ setPending: () => setPending,
9422
10240
  setTopic: () => setTopic,
9423
10241
  shellQuote: () => shellQuote,
9424
10242
  shouldBuildRouterService: () => shouldBuildRouterService,
@@ -9431,6 +10249,7 @@ __export(dist_exports2, {
9431
10249
  startServer: () => startServer,
9432
10250
  stripBotMention: () => stripBotMention,
9433
10251
  surfaceVerdict: () => surfaceVerdict,
10252
+ telegramPollLockPath: () => telegramPollLockPath,
9434
10253
  timeoutGate: () => timeoutGate,
9435
10254
  titleFor: () => titleFor,
9436
10255
  toControlEvent: () => toControlEvent,
@@ -9479,6 +10298,7 @@ var init_dist2 = __esm({
9479
10298
  init_router_resolution();
9480
10299
  init_captain_router();
9481
10300
  init_permission_gate();
10301
+ init_auto_gate();
9482
10302
  init_restart_daemon();
9483
10303
  init_group_dispatch();
9484
10304
  init_launch_workspace();
@@ -9500,7 +10320,7 @@ var init_dist2 = __esm({
9500
10320
  });
9501
10321
 
9502
10322
  // packages/agents/dist/claude/registry.js
9503
- import fs13 from "fs";
10323
+ import fs14 from "fs";
9504
10324
  import { join as join17 } from "path";
9505
10325
  import { homedir as homedir7 } from "os";
9506
10326
  function parseRegistryDir(files, readFile7) {
@@ -9545,11 +10365,11 @@ function readClaudeStatus(task) {
9545
10365
  return void 0;
9546
10366
  let files;
9547
10367
  try {
9548
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
10368
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
9549
10369
  } catch {
9550
10370
  return void 0;
9551
10371
  }
9552
- const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
10372
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
9553
10373
  let entry;
9554
10374
  if (task.messagingSocketPath) {
9555
10375
  entry = entries.find((e) => e.messagingSocketPath === task.messagingSocketPath);
@@ -9564,11 +10384,11 @@ function readClaudeStatus(task) {
9564
10384
  function readClaudeStatusByCwd(cwd) {
9565
10385
  let files;
9566
10386
  try {
9567
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
10387
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
9568
10388
  } catch {
9569
10389
  return void 0;
9570
10390
  }
9571
- const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
10391
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
9572
10392
  const entry = entries.find((e) => e.cwd === cwd);
9573
10393
  if (!entry)
9574
10394
  return void 0;
@@ -9577,11 +10397,11 @@ function readClaudeStatusByCwd(cwd) {
9577
10397
  function readClaudeStatusBySocketPath(socketPath2) {
9578
10398
  let files;
9579
10399
  try {
9580
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
10400
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
9581
10401
  } catch {
9582
10402
  return void 0;
9583
10403
  }
9584
- const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
10404
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
9585
10405
  const entry = entries.find((e) => e.messagingSocketPath === socketPath2);
9586
10406
  if (!entry)
9587
10407
  return void 0;
@@ -10004,7 +10824,7 @@ var init_registry2 = __esm({
10004
10824
  });
10005
10825
 
10006
10826
  // packages/agents/dist/drivers/launch-cmd.js
10007
- import fs14 from "fs";
10827
+ import fs15 from "fs";
10008
10828
  import path13 from "path";
10009
10829
  function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName, thinking, captainBoot, settingsPath) {
10010
10830
  const driver = registry.getDriver(agentName);
@@ -10034,12 +10854,12 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
10034
10854
  if (templatesDir) {
10035
10855
  const roleFile2 = path13.join(templatesDir, `${role}.claude.md`);
10036
10856
  const legacyRoleFile = path13.join(templatesDir, `${role}.CLAUDE.md`);
10037
- const actualRoleFile = fs14.existsSync(roleFile2) ? roleFile2 : fs14.existsSync(legacyRoleFile) ? legacyRoleFile : null;
10857
+ const actualRoleFile = fs15.existsSync(roleFile2) ? roleFile2 : fs15.existsSync(legacyRoleFile) ? legacyRoleFile : null;
10038
10858
  if (actualRoleFile) {
10039
10859
  cmd += ` --append-system-prompt-file ${actualRoleFile}`;
10040
10860
  }
10041
10861
  const pluginDir = path13.join(templatesDir, "..", "plugin");
10042
- if (fs14.existsSync(pluginDir)) {
10862
+ if (fs15.existsSync(pluginDir)) {
10043
10863
  cmd += ` --plugin-dir ${pluginDir}`;
10044
10864
  }
10045
10865
  }
@@ -10055,7 +10875,7 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
10055
10875
  role,
10056
10876
  model,
10057
10877
  autoApprove: true,
10058
- promptFile: roleFile && fs14.existsSync(roleFile) ? roleFile : void 0,
10878
+ promptFile: roleFile && fs15.existsSync(roleFile) ? roleFile : void 0,
10059
10879
  ...captainBoot && role === "captain" ? {
10060
10880
  interactive: true,
10061
10881
  ...captainBoot.port ? { port: captainBoot.port } : {},
@@ -10427,7 +11247,7 @@ var init_opencode2 = __esm({
10427
11247
  });
10428
11248
 
10429
11249
  // packages/agents/dist/projection/opencode-skills.js
10430
- import fs15 from "fs";
11250
+ import fs16 from "fs";
10431
11251
  import os10 from "os";
10432
11252
  import path18 from "path";
10433
11253
  function defaultOpencodeSkillsRoot(env = process.env) {
@@ -10446,62 +11266,62 @@ function parseSkillName(raw) {
10446
11266
  }
10447
11267
  function listSourceSkills(sourceSkillsDir) {
10448
11268
  const out = /* @__PURE__ */ new Map();
10449
- if (!fs15.existsSync(sourceSkillsDir))
11269
+ if (!fs16.existsSync(sourceSkillsDir))
10450
11270
  return out;
10451
- for (const entry of fs15.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
11271
+ for (const entry of fs16.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
10452
11272
  if (!entry.isDirectory())
10453
11273
  continue;
10454
11274
  const skillPath = path18.join(sourceSkillsDir, entry.name, "SKILL.md");
10455
- if (!fs15.existsSync(skillPath))
11275
+ if (!fs16.existsSync(skillPath))
10456
11276
  continue;
10457
- const name = parseSkillName(fs15.readFileSync(skillPath, "utf-8"));
11277
+ const name = parseSkillName(fs16.readFileSync(skillPath, "utf-8"));
10458
11278
  if (name && SAFE_NAME.test(name))
10459
11279
  out.set(name, skillPath);
10460
11280
  }
10461
11281
  return out;
10462
11282
  }
10463
11283
  function hasMarker(dir) {
10464
- return fs15.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
11284
+ return fs16.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
10465
11285
  }
10466
11286
  function syncOpencodeSkills(opts) {
10467
11287
  const result = { written: [], unchanged: [], removed: [], skipped: [] };
10468
11288
  const wanted = listSourceSkills(opts.sourceSkillsDir);
10469
- if (wanted.size === 0 && !fs15.existsSync(opts.sourceSkillsDir))
11289
+ if (wanted.size === 0 && !fs16.existsSync(opts.sourceSkillsDir))
10470
11290
  return result;
10471
11291
  if (!opts.dryRun)
10472
- fs15.mkdirSync(opts.skillsRoot, { recursive: true });
11292
+ fs16.mkdirSync(opts.skillsRoot, { recursive: true });
10473
11293
  for (const [name, srcPath] of wanted) {
10474
11294
  const targetDir = path18.join(opts.skillsRoot, name);
10475
11295
  const targetSkill = path18.join(targetDir, "SKILL.md");
10476
11296
  const managed = hasMarker(targetDir);
10477
- if (fs15.existsSync(targetSkill) && !managed) {
11297
+ if (fs16.existsSync(targetSkill) && !managed) {
10478
11298
  result.skipped.push({ name, reason: "foreign-collision" });
10479
11299
  continue;
10480
11300
  }
10481
- const current = fs15.existsSync(targetSkill) ? fs15.readFileSync(targetSkill) : null;
10482
- if (current && current.equals(fs15.readFileSync(srcPath))) {
11301
+ const current = fs16.existsSync(targetSkill) ? fs16.readFileSync(targetSkill) : null;
11302
+ if (current && current.equals(fs16.readFileSync(srcPath))) {
10483
11303
  if (!managed && !opts.dryRun) {
10484
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
11304
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
10485
11305
  }
10486
11306
  result.unchanged.push(name);
10487
11307
  continue;
10488
11308
  }
10489
11309
  if (!opts.dryRun) {
10490
- fs15.mkdirSync(targetDir, { recursive: true });
10491
- fs15.copyFileSync(srcPath, targetSkill);
10492
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
11310
+ fs16.mkdirSync(targetDir, { recursive: true });
11311
+ fs16.copyFileSync(srcPath, targetSkill);
11312
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
10493
11313
  }
10494
11314
  result.written.push(name);
10495
11315
  }
10496
- if (fs15.existsSync(opts.skillsRoot)) {
10497
- for (const entry of fs15.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
11316
+ if (fs16.existsSync(opts.skillsRoot)) {
11317
+ for (const entry of fs16.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
10498
11318
  if (!entry.isDirectory() || wanted.has(entry.name))
10499
11319
  continue;
10500
11320
  const dir = path18.join(opts.skillsRoot, entry.name);
10501
11321
  if (!hasMarker(dir))
10502
11322
  continue;
10503
11323
  if (!opts.dryRun)
10504
- fs15.rmSync(dir, { recursive: true, force: true });
11324
+ fs16.rmSync(dir, { recursive: true, force: true });
10505
11325
  result.removed.push(entry.name);
10506
11326
  }
10507
11327
  }
@@ -12310,6 +13130,9 @@ var init_peer_channel = __esm({
12310
13130
  return { status: "accepted", via: this.name, confirmed: true };
12311
13131
  }
12312
13132
  }
13133
+ if (statusBefore === "busy" || statusBefore === "shell") {
13134
+ return { status: "queued", via: this.name };
13135
+ }
12313
13136
  if (statusBefore === "idle") {
12314
13137
  await this.deps.sleep(window);
12315
13138
  const afterInfo = this.deps.statusFor(taskId);
@@ -12455,7 +13278,7 @@ var init_receipt_listener = __esm({
12455
13278
  });
12456
13279
 
12457
13280
  // packages/agents/dist/claude/api-key-approval.js
12458
- import fs16 from "fs";
13281
+ import fs17 from "fs";
12459
13282
  import os12 from "os";
12460
13283
  import path20 from "path";
12461
13284
  function ensureClaudeApiKeyApproved(apiKey, opts = {}) {
@@ -12465,12 +13288,12 @@ function ensureClaudeApiKeyApproved(apiKey, opts = {}) {
12465
13288
  const file = opts.claudeJsonPath ?? path20.join(os12.homedir(), ".claude.json");
12466
13289
  const log = opts.log ?? (() => {
12467
13290
  });
12468
- if (!fs16.existsSync(file)) {
13291
+ if (!fs17.existsSync(file)) {
12469
13292
  return { changed: false, reason: "no ~/.claude.json yet \u2014 skipping routed-key pre-approval" };
12470
13293
  }
12471
13294
  let doc;
12472
13295
  try {
12473
- doc = JSON.parse(fs16.readFileSync(file, "utf8"));
13296
+ doc = JSON.parse(fs17.readFileSync(file, "utf8"));
12474
13297
  } catch {
12475
13298
  return {
12476
13299
  changed: false,
@@ -12492,11 +13315,11 @@ function ensureClaudeApiKeyApproved(apiKey, opts = {}) {
12492
13315
  };
12493
13316
  const tmp = `${file}.squadrant-tmp`;
12494
13317
  try {
12495
- fs16.writeFileSync(tmp, JSON.stringify(doc, null, 2) + "\n", { mode: fs16.statSync(file).mode & 511 });
12496
- fs16.renameSync(tmp, file);
13318
+ fs17.writeFileSync(tmp, JSON.stringify(doc, null, 2) + "\n", { mode: fs17.statSync(file).mode & 511 });
13319
+ fs17.renameSync(tmp, file);
12497
13320
  } catch (e) {
12498
13321
  try {
12499
- fs16.unlinkSync(tmp);
13322
+ fs17.unlinkSync(tmp);
12500
13323
  } catch {
12501
13324
  }
12502
13325
  return {
@@ -12852,11 +13675,11 @@ function parseDraftFromScreen(screen) {
12852
13675
  if (!screen)
12853
13676
  return null;
12854
13677
  const lines = screen.split(/\r?\n/);
12855
- const HR_RE = /^\s*─{10,}\s*$/;
13678
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12856
13679
  let bottomHR = -1;
12857
13680
  let topHR = -1;
12858
13681
  for (let i = lines.length - 1; i >= 0; i--) {
12859
- if (HR_RE.test(lines[i])) {
13682
+ if (HR_RE2.test(lines[i])) {
12860
13683
  if (bottomHR === -1) {
12861
13684
  bottomHR = i;
12862
13685
  } else {
@@ -12894,11 +13717,11 @@ function hasCCInputBox(screen) {
12894
13717
  if (!screen)
12895
13718
  return false;
12896
13719
  const lines = screen.split(/\r?\n/);
12897
- const HR_RE = /^\s*─{10,}\s*$/;
13720
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12898
13721
  let bottomHR = -1;
12899
13722
  let topHR = -1;
12900
13723
  for (let i = lines.length - 1; i >= 0; i--) {
12901
- if (HR_RE.test(lines[i])) {
13724
+ if (HR_RE2.test(lines[i])) {
12902
13725
  if (bottomHR === -1)
12903
13726
  bottomHR = i;
12904
13727
  else {
@@ -12911,15 +13734,15 @@ function hasCCInputBox(screen) {
12911
13734
  return false;
12912
13735
  return lines.slice(topHR + 1, bottomHR).some((l) => /[>❯]/.test(l));
12913
13736
  }
12914
- function hasModalOptionList(screen) {
13737
+ function hasModalOptionList2(screen) {
12915
13738
  if (!screen)
12916
13739
  return false;
12917
13740
  const lines = screen.split(/\r?\n/);
12918
- const HR_RE = /^\s*─{10,}\s*$/;
13741
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12919
13742
  let bottomHR = -1;
12920
13743
  let topHR = -1;
12921
13744
  for (let i = lines.length - 1; i >= 0; i--) {
12922
- if (HR_RE.test(lines[i])) {
13745
+ if (HR_RE2.test(lines[i])) {
12923
13746
  if (bottomHR === -1)
12924
13747
  bottomHR = i;
12925
13748
  else {
@@ -12933,14 +13756,14 @@ function hasModalOptionList(screen) {
12933
13756
  return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
12934
13757
  }
12935
13758
  function parseModalOptions(screen) {
12936
- if (!hasModalOptionList(screen))
13759
+ if (!hasModalOptionList2(screen))
12937
13760
  return null;
12938
13761
  const lines = screen.split(/\r?\n/);
12939
- const HR_RE = /^\s*─{10,}\s*$/;
13762
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12940
13763
  let bottomHR = -1;
12941
13764
  let topHR = -1;
12942
13765
  for (let i = lines.length - 1; i >= 0; i--) {
12943
- if (HR_RE.test(lines[i])) {
13766
+ if (HR_RE2.test(lines[i])) {
12944
13767
  if (bottomHR === -1)
12945
13768
  bottomHR = i;
12946
13769
  else {
@@ -12961,15 +13784,46 @@ function parseModalOptions(screen) {
12961
13784
  }
12962
13785
  return options.length > 0 ? options : null;
12963
13786
  }
13787
+ function parseOpencodePermissionOptions(screen) {
13788
+ if (!screen)
13789
+ return null;
13790
+ if (!/△\s*Permission required/.test(screen))
13791
+ return null;
13792
+ const lines = screen.split(/\r?\n/);
13793
+ const bar = lines.find((l) => /⇆\s*select/.test(l));
13794
+ if (!bar)
13795
+ return null;
13796
+ let region = bar.split("\u21C6")[0];
13797
+ const fullscreenIdx = region.toLowerCase().lastIndexOf("fullscreen");
13798
+ if (fullscreenIdx !== -1) {
13799
+ region = region.slice(0, fullscreenIdx).replace(/\s+\S+\s*$/, "");
13800
+ }
13801
+ region = region.replace(/^[\s┃│╭╮╰╯|]+/, "").trim();
13802
+ if (!region)
13803
+ return null;
13804
+ const labels = region.split(/\s{2,}/).map((s) => s.trim()).filter(Boolean);
13805
+ if (labels.length === 0)
13806
+ return null;
13807
+ return labels.map((label, i) => ({ index: i + 1, label, highlighted: i === 0 }));
13808
+ }
13809
+ function parseModal(screen) {
13810
+ const claude = parseModalOptions(screen);
13811
+ if (claude)
13812
+ return { options: claude, axis: "vertical" };
13813
+ const opencode = parseOpencodePermissionOptions(screen);
13814
+ if (opencode)
13815
+ return { options: opencode, axis: "horizontal" };
13816
+ return null;
13817
+ }
12964
13818
  function readInputBoxRaw(screen, opts) {
12965
13819
  if (!screen)
12966
13820
  return null;
12967
13821
  const lines = screen.split(/\r?\n/);
12968
- const HR_RE = /^\s*─{10,}\s*$/;
13822
+ const HR_RE2 = /^\s*─{10,}\s*$/;
12969
13823
  let bottomHR = -1;
12970
13824
  let topHR = -1;
12971
13825
  for (let i = lines.length - 1; i >= 0; i--) {
12972
- if (HR_RE.test(lines[i])) {
13826
+ if (HR_RE2.test(lines[i])) {
12973
13827
  if (bottomHR === -1)
12974
13828
  bottomHR = i;
12975
13829
  else {
@@ -13002,6 +13856,30 @@ function classifyOpencodeStartupSurface(screen) {
13002
13856
  return "loading";
13003
13857
  return OC_IDLE_MARKERS.some((m) => screenHasSplashMarker(screen, m)) ? "idle" : "loading";
13004
13858
  }
13859
+ function parseOpencodeDraftFromScreen(screen) {
13860
+ if (!screen)
13861
+ return null;
13862
+ if (OC_BUSY_RE.test(screen))
13863
+ return null;
13864
+ const lines = screen.split(/\r?\n/);
13865
+ let bottom = -1;
13866
+ for (let i = lines.length - 1; i >= 0; i--) {
13867
+ if (OC_BOX_BOTTOM_RE.test(lines[i])) {
13868
+ bottom = i;
13869
+ break;
13870
+ }
13871
+ }
13872
+ if (bottom === -1)
13873
+ return null;
13874
+ const content = [];
13875
+ for (let i = bottom - 1; i >= 0 && OC_BOX_SIDE_RE.test(lines[i]); i--) {
13876
+ content.push(lines[i].replace(/^\s*┃\s*/, "").replace(/[\s┃│]+$/, ""));
13877
+ }
13878
+ if (content.length === 0)
13879
+ return null;
13880
+ const hasDraft = content.some((l) => l !== "" && !OC_BOX_STATUS_RE.test(l) && !screenHasSplashMarker(l, "Ask anything"));
13881
+ return hasDraft ? null : "";
13882
+ }
13005
13883
  function sendDebugEnabled() {
13006
13884
  return !!process.env.SQUADRANT_DEBUG_SEND;
13007
13885
  }
@@ -13217,10 +14095,10 @@ function createCmuxDriver() {
13217
14095
  `);
13218
14096
  throw new DeferDelivery(null, "probe-failed");
13219
14097
  }
13220
- const draft = parseDraftFromScreen(screen);
14098
+ const draft = opts?.agent === "opencode" ? parseOpencodeDraftFromScreen(screen) : parseDraftFromScreen(screen);
13221
14099
  if (draft === null)
13222
14100
  throw new DeferDelivery(null, "no-box");
13223
- if (hasModalOptionList(screen))
14101
+ if (hasModalOptionList2(screen))
13224
14102
  throw new DeferDelivery(null, "modal");
13225
14103
  if (draft === "") {
13226
14104
  await deliver();
@@ -13324,7 +14202,7 @@ function createCmuxDriver() {
13324
14202
  }
13325
14203
  };
13326
14204
  }
13327
- var CMUX_TIMEOUT, CmuxTimeoutError, CC_INITIALIZED_RE, CC_WORKING_RE, OC_IDLE_MARKERS;
14205
+ 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;
13328
14206
  var init_cmux = __esm({
13329
14207
  "packages/workspaces/dist/runtimes/cmux.js"() {
13330
14208
  init_dist();
@@ -13342,6 +14220,10 @@ var init_cmux = __esm({
13342
14220
  CC_INITIALIZED_RE = /⏵⏵|Ctx Used|for shortcuts|accept edits/i;
13343
14221
  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;
13344
14222
  OC_IDLE_MARKERS = ["Ask anything", "ctrl+p commands"];
14223
+ OC_BOX_BOTTOM_RE = /^\s*╹[▀]{6,}\s*$/;
14224
+ OC_BOX_SIDE_RE = /^\s*┃/;
14225
+ OC_BOX_STATUS_RE = /^Build\s*·/i;
14226
+ OC_BUSY_RE = /esc\s+interrupt/i;
13345
14227
  }
13346
14228
  });
13347
14229
 
@@ -13466,7 +14348,7 @@ var init_notifiers = __esm({
13466
14348
  });
13467
14349
 
13468
14350
  // packages/workspaces/dist/workspaces/obsidian.js
13469
- import fs17 from "fs/promises";
14351
+ import fs18 from "fs/promises";
13470
14352
  import { existsSync as existsSync11 } from "fs";
13471
14353
  import path21 from "path";
13472
14354
  function resolveInRoot(root, relative) {
@@ -13491,16 +14373,16 @@ function createObsidianDriver(scope) {
13491
14373
  };
13492
14374
  },
13493
14375
  async read(rel) {
13494
- return fs17.readFile(resolveInRoot(root, rel), "utf-8");
14376
+ return fs18.readFile(resolveInRoot(root, rel), "utf-8");
13495
14377
  },
13496
14378
  async write(rel, content) {
13497
14379
  const abs = resolveInRoot(root, rel);
13498
- await fs17.mkdir(path21.dirname(abs), { recursive: true });
13499
- await fs17.writeFile(abs, content);
14380
+ await fs18.mkdir(path21.dirname(abs), { recursive: true });
14381
+ await fs18.writeFile(abs, content);
13500
14382
  },
13501
14383
  async exists(rel) {
13502
14384
  try {
13503
- await fs17.access(resolveInRoot(root, rel));
14385
+ await fs18.access(resolveInRoot(root, rel));
13504
14386
  return true;
13505
14387
  } catch {
13506
14388
  return false;
@@ -13508,13 +14390,13 @@ function createObsidianDriver(scope) {
13508
14390
  },
13509
14391
  async list(rel) {
13510
14392
  try {
13511
- return await fs17.readdir(resolveInRoot(root, rel));
14393
+ return await fs18.readdir(resolveInRoot(root, rel));
13512
14394
  } catch {
13513
14395
  return [];
13514
14396
  }
13515
14397
  },
13516
14398
  async mkdir(rel) {
13517
- await fs17.mkdir(resolveInRoot(root, rel), { recursive: true });
14399
+ await fs18.mkdir(resolveInRoot(root, rel), { recursive: true });
13518
14400
  }
13519
14401
  };
13520
14402
  }
@@ -14142,6 +15024,47 @@ function removeCommandHandlers(entries, command) {
14142
15024
  }
14143
15025
  return removed;
14144
15026
  }
15027
+ function isForeignAutoGateCommand(command) {
15028
+ if (typeof command !== "string")
15029
+ return false;
15030
+ if (command.includes(FOREIGN_AUTO_GATE_MARKER))
15031
+ return true;
15032
+ const first = command.trim().split(/\s+/)[0] ?? "";
15033
+ return /(^|[/\\])auto-gate$/.test(first);
15034
+ }
15035
+ function findForeignGateHandlers(entries) {
15036
+ const found = [];
15037
+ for (const entry of entries) {
15038
+ const hooks = entry?.hooks;
15039
+ if (!Array.isArray(hooks))
15040
+ continue;
15041
+ for (const h of hooks) {
15042
+ const cmd = h?.command;
15043
+ if (isForeignAutoGateCommand(cmd))
15044
+ found.push(cmd);
15045
+ }
15046
+ }
15047
+ return found;
15048
+ }
15049
+ function removeForeignGateHandlers(entries) {
15050
+ const removed = [];
15051
+ for (let i = entries.length - 1; i >= 0; i--) {
15052
+ const entry = entries[i];
15053
+ if (!Array.isArray(entry?.hooks))
15054
+ continue;
15055
+ entry.hooks = entry.hooks.filter((h) => {
15056
+ const cmd = h?.command;
15057
+ if (isForeignAutoGateCommand(cmd)) {
15058
+ removed.push(cmd);
15059
+ return false;
15060
+ }
15061
+ return true;
15062
+ });
15063
+ if (entry.hooks.length === 0)
15064
+ entries.splice(i, 1);
15065
+ }
15066
+ return removed;
15067
+ }
14145
15068
  function installClaudeHooks(opts = {}) {
14146
15069
  const settingsPath = opts.settingsPath ?? join25(homedir13(), ".claude", "settings.json");
14147
15070
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
@@ -14179,6 +15102,23 @@ function installClaudeHooks(opts = {}) {
14179
15102
  changed = true;
14180
15103
  migrated.push(`${eventName}/${sub}`);
14181
15104
  }
15105
+ const foreign = findForeignGateHandlers(entries);
15106
+ if (foreign.length > 0) {
15107
+ if (opts.gateMode === "on") {
15108
+ const removed = removeForeignGateHandlers(entries);
15109
+ if (removed.length > 0)
15110
+ changed = true;
15111
+ for (const command2 of removed) {
15112
+ log(`native-hook: removed foreign auto-gate PermissionRequest handler in ${settingsPath} [#P6 \xA715#5]: ${command2}`);
15113
+ opts.recordGateAction?.({ action: "removed-foreign-gate", command: command2 });
15114
+ }
15115
+ } else {
15116
+ for (const command2 of foreign) {
15117
+ log(`native-hook: foreign auto-gate PermissionRequest handler present in ${settingsPath} but squadrant's gate is '${opts.gateMode ?? "auto"}' (not 'on') \u2014 foreign entry left untouched [#P6 \xA715#5]: ${command2}`);
15118
+ opts.recordGateAction?.({ action: "left-foreign-gate", command: command2 });
15119
+ }
15120
+ }
15121
+ }
14182
15122
  }
14183
15123
  const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
14184
15124
  if (!alreadyPresent) {
@@ -14209,11 +15149,55 @@ function installClaudeHooks(opts = {}) {
14209
15149
  changed = true;
14210
15150
  }
14211
15151
  }
15152
+ if (typeof settings.env === "object" && settings.env !== null && !Array.isArray(settings.env)) {
15153
+ if (reconcileSubagentModels(settings.env, log))
15154
+ changed = true;
15155
+ }
14212
15156
  if (changed) {
14213
15157
  writeFile6(settingsPath, JSON.stringify(settings, null, 2));
14214
15158
  }
14215
15159
  return settingsPath;
14216
15160
  }
15161
+ function isCustomUpstream(baseUrl) {
15162
+ if (typeof baseUrl !== "string" || baseUrl.length === 0)
15163
+ return false;
15164
+ try {
15165
+ return new URL(baseUrl).hostname.toLowerCase() !== "api.anthropic.com";
15166
+ } catch {
15167
+ return false;
15168
+ }
15169
+ }
15170
+ function isAnthropicModelId(value) {
15171
+ if (typeof value !== "string")
15172
+ return false;
15173
+ const v2 = value.trim().toLowerCase();
15174
+ return v2 === "sonnet" || v2 === "opus" || v2 === "haiku" || v2.startsWith("claude-");
15175
+ }
15176
+ function reconcileSubagentModels(env, log) {
15177
+ if (!isCustomUpstream(env.ANTHROPIC_BASE_URL))
15178
+ return false;
15179
+ const model = env.ANTHROPIC_MODEL;
15180
+ if (typeof model !== "string" || model.length === 0)
15181
+ return false;
15182
+ let changed = false;
15183
+ for (const key of [SUBAGENT_MODEL_KEY, SMALL_FAST_MODEL_KEY]) {
15184
+ const current = env[key];
15185
+ if (current === void 0) {
15186
+ env[key] = model;
15187
+ changed = true;
15188
+ log(`native-hook: set ${key}='${model}' to match ANTHROPIC_MODEL on a custom upstream`);
15189
+ } else if (isAnthropicModelId(current)) {
15190
+ if (current !== model) {
15191
+ env[key] = model;
15192
+ changed = true;
15193
+ log(`native-hook: aligned ${key} '${String(current)}' \u2192 '${model}' for a custom upstream`);
15194
+ }
15195
+ } else {
15196
+ log(`native-hook: ${key} '${String(current)}' is a custom model id \u2014 left untouched`);
15197
+ }
15198
+ }
15199
+ return changed;
15200
+ }
14217
15201
  function mapSubToLifecycle(sub) {
14218
15202
  switch (sub) {
14219
15203
  case "session-start":
@@ -14263,7 +15247,7 @@ function defaultWriteFile(path39, content) {
14263
15247
  mkdirSync8(path39.replace(/\/[^/]+$/, ""), { recursive: true });
14264
15248
  writeFileSync11(path39, content, "utf-8");
14265
15249
  }
14266
- var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, GATE_HOOK_CMD, NativeHookSource;
15250
+ var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, GATE_HOOK_CMD, FOREIGN_AUTO_GATE_MARKER, SUBAGENT_MODEL_KEY, SMALL_FAST_MODEL_KEY, NativeHookSource;
14267
15251
  var init_native_hook_source = __esm({
14268
15252
  "packages/workspaces/dist/native-hooks/native-hook-source.js"() {
14269
15253
  CLAUDE_HOOK_EVENTS = [
@@ -14283,6 +15267,9 @@ var init_native_hook_source = __esm({
14283
15267
  ];
14284
15268
  DEFAULT_HOOK_CMD = "squadrant hooks";
14285
15269
  GATE_HOOK_CMD = "squadrant gate";
15270
+ FOREIGN_AUTO_GATE_MARKER = "auto-gate-managed";
15271
+ SUBAGENT_MODEL_KEY = "CLAUDE_CODE_SUBAGENT_MODEL";
15272
+ SMALL_FAST_MODEL_KEY = "ANTHROPIC_SMALL_FAST_MODEL";
14286
15273
  NativeHookSource = class {
14287
15274
  name = "native-hook";
14288
15275
  hookInstall;
@@ -14412,15 +15399,15 @@ async function resolveCaptainWorkspace(project) {
14412
15399
  }
14413
15400
  async function paneHasOpenModal(runtime, pane) {
14414
15401
  const screen = await runtime.readPaneScreen(pane) ?? "";
14415
- return hasModalOptionList(screen);
15402
+ return hasModalOptionList2(screen);
14416
15403
  }
14417
15404
  async function readModalOptions(runtime, pane) {
14418
15405
  const screen = await runtime.readPaneScreen(pane) ?? "";
14419
- return parseModalOptions(screen);
15406
+ return parseModal(screen);
14420
15407
  }
14421
15408
  async function confirmedSendToPane(runtime, pane, message) {
14422
15409
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
14423
- if (hasModalOptionList(preSendScreen)) {
15410
+ if (hasModalOptionList2(preSendScreen)) {
14424
15411
  return { delivered: false, blockedByModal: true };
14425
15412
  }
14426
15413
  await runtime.pasteToPane(pane, message);
@@ -14673,9 +15660,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
14673
15660
  const resolved = resolveTarget(registry, config, target, !!opts.command);
14674
15661
  await needRef(resolved);
14675
15662
  const finalProject = opts.command ? config.commandName : target;
14676
- const { join: join35, dirname: dirname11 } = await import("path");
15663
+ const { join: join36, dirname: dirname12 } = await import("path");
14677
15664
  const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
14678
- const stateRoot = join35(dirname11(DEFAULT_CONFIG_PATH2), "state");
15665
+ const stateRoot = join36(dirname12(DEFAULT_CONFIG_PATH2), "state");
14679
15666
  const seq = await appendCaptainMessage2({
14680
15667
  stateRoot,
14681
15668
  project: finalProject,
@@ -14784,7 +15771,7 @@ init_dist3();
14784
15771
  import { Command as Command38 } from "commander";
14785
15772
  import { existsSync as existsSync14, readFileSync as readFileSync21 } from "fs";
14786
15773
  import { fileURLToPath as fileURLToPath6 } from "url";
14787
- import { dirname as dirname10, join as join34 } from "path";
15774
+ import { dirname as dirname11, join as join35 } from "path";
14788
15775
  import { homedir as homedir20 } from "os";
14789
15776
 
14790
15777
  // packages/cli/src/commands/doctor.ts
@@ -14794,7 +15781,7 @@ init_dist();
14794
15781
  init_dist4();
14795
15782
  import { Command } from "commander";
14796
15783
  import { execSync as execSync8 } from "child_process";
14797
- import fs18 from "fs";
15784
+ import fs19 from "fs";
14798
15785
  import { stat } from "fs/promises";
14799
15786
  import path22 from "path";
14800
15787
  import chalk3 from "chalk";
@@ -14898,7 +15885,7 @@ function settingsHaveAgentTeams() {
14898
15885
  try {
14899
15886
  const home = process.env.HOME || "";
14900
15887
  const settings = JSON.parse(
14901
- fs18.readFileSync(`${home}/.claude/settings.json`, "utf-8")
15888
+ fs19.readFileSync(`${home}/.claude/settings.json`, "utf-8")
14902
15889
  );
14903
15890
  return settings?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1";
14904
15891
  } catch {
@@ -14909,7 +15896,7 @@ function pluginInstalled(pluginKey) {
14909
15896
  try {
14910
15897
  const home = process.env.HOME || "";
14911
15898
  const plugins = JSON.parse(
14912
- fs18.readFileSync(
15899
+ fs19.readFileSync(
14913
15900
  `${home}/.claude/plugins/installed_plugins.json`,
14914
15901
  "utf-8"
14915
15902
  )
@@ -14955,7 +15942,7 @@ ${lines.join("\n")}
14955
15942
  }
14956
15943
  function readPackageVersion(packageJsonPath) {
14957
15944
  try {
14958
- const pkg2 = JSON.parse(fs18.readFileSync(packageJsonPath, "utf-8"));
15945
+ const pkg2 = JSON.parse(fs19.readFileSync(packageJsonPath, "utf-8"));
14959
15946
  return typeof pkg2.version === "string" ? pkg2.version : null;
14960
15947
  } catch {
14961
15948
  return null;
@@ -14991,7 +15978,7 @@ var doctorCommand = new Command("doctor").description("Check system health and p
14991
15978
  ));
14992
15979
  results.push(check(
14993
15980
  "Obsidian installed",
14994
- commandExists("obsidian") || fs18.existsSync("/Applications/Obsidian.app"),
15981
+ commandExists("obsidian") || fs19.existsSync("/Applications/Obsidian.app"),
14995
15982
  "Install from: https://obsidian.md"
14996
15983
  ));
14997
15984
  results.push(check(
@@ -15099,7 +16086,7 @@ var doctorCommand = new Command("doctor").description("Check system health and p
15099
16086
  results.push(
15100
16087
  check(
15101
16088
  "Squadrant config exists",
15102
- fs18.existsSync(
16089
+ fs19.existsSync(
15103
16090
  process.env.SQUADRANT_CONFIG || `${process.env.HOME}/.config/squadrant/config.json`
15104
16091
  ),
15105
16092
  "Run: squadrant init"
@@ -15183,7 +16170,7 @@ init_dist();
15183
16170
  init_dist4();
15184
16171
  init_dist();
15185
16172
  import { Command as Command2 } from "commander";
15186
- import fs19 from "fs";
16173
+ import fs20 from "fs";
15187
16174
  import path23 from "path";
15188
16175
  import os13 from "os";
15189
16176
  import readline from "readline";
@@ -15400,25 +16387,57 @@ function writePerCrewOpencodeConfig(o) {
15400
16387
  return file;
15401
16388
  }
15402
16389
 
16390
+ // packages/cli/src/lib/claude-auth.ts
16391
+ import { execSync as execSync9 } from "child_process";
16392
+ function parseClaudeAuthStatus(raw) {
16393
+ let parsed;
16394
+ try {
16395
+ parsed = JSON.parse(raw);
16396
+ } catch {
16397
+ return null;
16398
+ }
16399
+ if (typeof parsed !== "object" || parsed === null) return null;
16400
+ const obj = parsed;
16401
+ if (typeof obj.loggedIn !== "boolean") return null;
16402
+ return {
16403
+ authenticated: obj.loggedIn,
16404
+ method: typeof obj.authMethod === "string" ? obj.authMethod : void 0,
16405
+ provider: typeof obj.apiProvider === "string" ? obj.apiProvider : void 0
16406
+ };
16407
+ }
16408
+ function detectClaudeAuth(probe = {}) {
16409
+ const run = probe.run ?? (() => execSync9("claude auth status", {
16410
+ encoding: "utf8",
16411
+ stdio: ["ignore", "pipe", "ignore"],
16412
+ timeout: 1e4
16413
+ }));
16414
+ try {
16415
+ const parsed = parseClaudeAuthStatus(run());
16416
+ return parsed ?? { authenticated: false, reason: "unreadable `claude auth status` output" };
16417
+ } catch {
16418
+ return { authenticated: false, reason: "`claude auth status` unavailable" };
16419
+ }
16420
+ }
16421
+
15403
16422
  // packages/cli/src/commands/init.ts
15404
16423
  init_dist3();
15405
16424
  function findPackageRoot() {
15406
16425
  let dir = path23.dirname(new URL(import.meta.url).pathname);
15407
16426
  while (dir !== "/") {
15408
- if (fs19.existsSync(path23.join(dir, "package.json"))) return dir;
16427
+ if (fs20.existsSync(path23.join(dir, "package.json"))) return dir;
15409
16428
  dir = path23.dirname(dir);
15410
16429
  }
15411
16430
  return process.cwd();
15412
16431
  }
15413
16432
  function copyDirRecursive(src, dest) {
15414
- fs19.mkdirSync(dest, { recursive: true });
15415
- for (const entry of fs19.readdirSync(src, { withFileTypes: true })) {
16433
+ fs20.mkdirSync(dest, { recursive: true });
16434
+ for (const entry of fs20.readdirSync(src, { withFileTypes: true })) {
15416
16435
  const srcPath = path23.join(src, entry.name);
15417
16436
  const destPath = path23.join(dest, entry.name);
15418
16437
  if (entry.isDirectory()) {
15419
16438
  copyDirRecursive(srcPath, destPath);
15420
16439
  } else {
15421
- fs19.copyFileSync(srcPath, destPath);
16440
+ fs20.copyFileSync(srcPath, destPath);
15422
16441
  }
15423
16442
  }
15424
16443
  }
@@ -15435,16 +16454,70 @@ function promptLine(question) {
15435
16454
  });
15436
16455
  });
15437
16456
  }
15438
- 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) => {
16457
+ async function resolveRouter(config, opts, isTTY) {
16458
+ if (config.defaults?.router) return config.defaults.router;
16459
+ let kind = opts.routerKind;
16460
+ let baseUrl = opts.routerBaseUrl;
16461
+ let apiKeyEnv = opts.routerApiKeyEnv;
16462
+ if (isTTY && (!kind || !baseUrl)) {
16463
+ const k = await promptLine(chalk4.cyan(` Router kind [${DEFAULT_ROUTER_KIND}]: `));
16464
+ kind = kind ?? (k || DEFAULT_ROUTER_KIND);
16465
+ const b = await promptLine(chalk4.cyan(` Router base URL [${DEFAULT_ROUTER_BASE_URL}]: `));
16466
+ baseUrl = baseUrl ?? (b || DEFAULT_ROUTER_BASE_URL);
16467
+ const e = await promptLine(chalk4.cyan(" Env var holding the API key (optional): "));
16468
+ apiKeyEnv = apiKeyEnv ?? (e || void 0);
16469
+ }
16470
+ const resolvedKind = kind && isRouterKind(kind) ? kind : DEFAULT_ROUTER_KIND;
16471
+ const resolvedBase = baseUrl || DEFAULT_ROUTER_BASE_URL;
16472
+ if (!isTTY && !opts.routerBaseUrl) {
16473
+ console.log(chalk4.dim(` - no router flags given; defaulting to ${resolvedKind} (${resolvedBase})`));
16474
+ console.log(chalk4.dim(" set a credential: squadrant config set defaults.router.apiKeyEnv <VAR>"));
16475
+ }
16476
+ return { kind: resolvedKind, baseUrl: resolvedBase, ...apiKeyEnv ? { apiKeyEnv } : {} };
16477
+ }
16478
+ async function askProvider(current, auth) {
16479
+ console.log(chalk4.bold("\n Choose your provider:"));
16480
+ for (const p of PROVIDER_PRESETS) {
16481
+ const currentTag = p.id === current ? chalk4.dim(" (current)") : "";
16482
+ console.log(` ${chalk4.cyan(`${p.id})`)} ${p.label}${currentTag}`);
16483
+ console.log(chalk4.dim(` ${p.summary}`));
16484
+ }
16485
+ if (!auth.authenticated) {
16486
+ console.log(chalk4.yellow("\n \u26A0 No Anthropic credential detected \u2014 preset a will not work; consider b or c."));
16487
+ }
16488
+ const answer = (await promptLine(chalk4.cyan(`
16489
+ Provider [a/b/c/d] (default ${current}): `))).toLowerCase();
16490
+ if (!answer) return current;
16491
+ if (isProviderPresetId(answer)) return answer;
16492
+ console.log(chalk4.yellow(` \u26A0 Unknown choice '${answer}' \u2014 keeping '${current}'.`));
16493
+ return current;
16494
+ }
16495
+ function warnIfNoAnthropic(preset, auth) {
16496
+ if (preset !== "a" || auth.authenticated) return;
16497
+ console.log(chalk4.yellow(" \u26A0 No Anthropic credential detected (`claude auth status`)."));
16498
+ console.log(chalk4.dim(" Preset A (Claude Pro/Max or an API key) may not work \u2014 consider preset b or c."));
16499
+ }
16500
+ 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) => {
15439
16501
  const hubPath = resolveHome(opts.hub);
15440
16502
  const pkgRoot = findPackageRoot();
15441
16503
  const configDir = path23.join(os13.homedir(), ".config", "squadrant");
15442
16504
  const isTTY = process.stdin.isTTY === true;
15443
16505
  console.log(chalk4.bold("\nSquadrant Init\n"));
15444
- if (!isTTY) {
16506
+ let explicitPreset;
16507
+ if (opts.preset !== void 0) {
16508
+ const candidate = opts.preset.toLowerCase();
16509
+ if (!isProviderPresetId(candidate)) {
16510
+ console.error(chalk4.red(` \u2718 Unknown --preset '${opts.preset}'. Valid values: a, b, c, d`));
16511
+ process.exitCode = 1;
16512
+ return;
16513
+ }
16514
+ explicitPreset = candidate;
16515
+ }
16516
+ if (!isTTY && !explicitPreset) {
15445
16517
  console.log(" Run these steps to get started:\n");
15446
- console.log(chalk4.bold(" 1/5 Hub vault"));
16518
+ console.log(chalk4.bold(" 1/5 Hub vault + provider"));
15447
16519
  console.log(chalk4.cyan(` squadrant init --hub ${opts.hub}`));
16520
+ console.log(chalk4.dim(" Choose a provider: squadrant init --preset a|b|c|d"));
15448
16521
  console.log(chalk4.bold("\n 2/5 Agent + projection setup"));
15449
16522
  console.log(" (handled automatically by: " + chalk4.cyan("squadrant init") + ")");
15450
16523
  console.log(chalk4.bold("\n 3/5 Plugins \u2014 open Claude Code and run:"));
@@ -15464,8 +16537,8 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
15464
16537
  }
15465
16538
  const wsRegistry = new WorkspaceRegistry({ obsidian: createObsidianDriver });
15466
16539
  try {
15467
- if (fs19.existsSync(DEFAULT_CONFIG_PATH)) {
15468
- const existing = JSON.parse(fs19.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
16540
+ if (fs20.existsSync(DEFAULT_CONFIG_PATH)) {
16541
+ const existing = JSON.parse(fs20.readFileSync(DEFAULT_CONFIG_PATH, "utf-8"));
15469
16542
  wsRegistry.get(existing.workspace ?? "obsidian");
15470
16543
  }
15471
16544
  } catch (err) {
@@ -15473,45 +16546,76 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
15473
16546
  return;
15474
16547
  }
15475
16548
  stepHeader(1, 5, "Hub vault");
15476
- if (fs19.existsSync(DEFAULT_CONFIG_PATH)) {
16549
+ const configExists = fs20.existsSync(DEFAULT_CONFIG_PATH);
16550
+ let config;
16551
+ if (configExists) {
15477
16552
  console.log(chalk4.yellow(" \u26A0 Config already exists, skipping creation"));
16553
+ config = loadConfig();
15478
16554
  } else {
15479
- const config = getDefaultConfig();
16555
+ config = getDefaultConfig();
15480
16556
  config.hubVault = hubPath;
16557
+ }
16558
+ const auth = detectClaudeAuth();
16559
+ let chosenPreset = explicitPreset;
16560
+ if (!chosenPreset && isTTY) {
16561
+ chosenPreset = await askProvider(detectProviderPresetId(config), auth);
16562
+ }
16563
+ let appliedChanges = [];
16564
+ if (chosenPreset) {
16565
+ const label = PROVIDER_PRESETS.find((p) => p.id === chosenPreset)?.label ?? chosenPreset;
16566
+ const router = chosenPreset === "c" ? await resolveRouter(config, opts, isTTY) : void 0;
16567
+ const overwrite = !configExists || explicitPreset !== void 0;
16568
+ const result = applyProviderPreset(config, chosenPreset, { router, overwrite });
16569
+ appliedChanges = result.changes;
16570
+ config = result.config;
16571
+ console.log(chalk4.bold(`
16572
+ Provider: ${chosenPreset.toUpperCase()} \u2014 ${label}`));
16573
+ if (appliedChanges.length) {
16574
+ console.log(" Will change:");
16575
+ for (const change of appliedChanges) console.log(chalk4.dim(` - ${change}`));
16576
+ } else {
16577
+ console.log(chalk4.dim(" - provider config already set (unchanged)"));
16578
+ }
16579
+ if (!isTTY) warnIfNoAnthropic(chosenPreset, auth);
16580
+ }
16581
+ if (!configExists) {
15481
16582
  saveConfig(config);
15482
16583
  console.log(chalk4.green(` \u2714 Config created at ${DEFAULT_CONFIG_PATH}`));
16584
+ } else if (appliedChanges.length) {
16585
+ saveConfig(config);
16586
+ console.log(chalk4.green(` \u2714 Config updated at ${DEFAULT_CONFIG_PATH}`));
15483
16587
  }
15484
16588
  const hubTemplate = path23.join(pkgRoot, "obsidian", "hub");
15485
- if (fs19.existsSync(hubPath)) {
16589
+ if (fs20.existsSync(hubPath)) {
15486
16590
  console.log(chalk4.yellow(` \u26A0 Hub vault already exists at ${hubPath}`));
15487
- } else if (fs19.existsSync(hubTemplate)) {
16591
+ } else if (fs20.existsSync(hubTemplate)) {
15488
16592
  copyDirRecursive(hubTemplate, hubPath);
15489
16593
  console.log(chalk4.green(` \u2714 Hub vault scaffolded at ${hubPath}`));
15490
16594
  } else {
15491
- fs19.mkdirSync(hubPath, { recursive: true });
16595
+ fs20.mkdirSync(hubPath, { recursive: true });
15492
16596
  console.log(chalk4.yellow(` \u26A0 Hub template not found; created empty directory at ${hubPath}`));
15493
16597
  }
15494
16598
  const hubDashboardSrc = path23.join(pkgRoot, "obsidian", "hub", "dashboard.md");
15495
16599
  const hubDashboardDest = path23.join(hubPath, "dashboard.md");
15496
- if (fs19.existsSync(hubDashboardSrc)) {
15497
- fs19.copyFileSync(hubDashboardSrc, hubDashboardDest);
16600
+ if (fs20.existsSync(hubDashboardSrc)) {
16601
+ fs20.copyFileSync(hubDashboardSrc, hubDashboardDest);
15498
16602
  console.log(chalk4.green(` \u2714 Dashboard refreshed`));
15499
16603
  }
15500
- fs19.mkdirSync(path23.join(hubPath, "projects"), { recursive: true });
16604
+ fs20.mkdirSync(path23.join(hubPath, "projects"), { recursive: true });
15501
16605
  ensureRuntimeSynced({ sourceRoot: pkgRoot, runtimeRoot: configDir });
15502
16606
  console.log(chalk4.green(` \u2714 Runtime assets synced to ${configDir}`));
15503
16607
  stepHeader(2, 5, "Agent + projection setup");
15504
16608
  const settingsPath = path23.join(os13.homedir(), ".claude", "settings.json");
15505
16609
  try {
15506
16610
  let settings = {};
15507
- if (fs19.existsSync(settingsPath)) {
15508
- settings = JSON.parse(fs19.readFileSync(settingsPath, "utf-8"));
16611
+ if (fs20.existsSync(settingsPath)) {
16612
+ settings = JSON.parse(fs20.readFileSync(settingsPath, "utf-8"));
15509
16613
  }
15510
16614
  const env = settings.env || {};
15511
16615
  if (env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
15512
16616
  settings.env = { ...env, CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1" };
15513
- fs19.mkdirSync(path23.dirname(settingsPath), { recursive: true });
15514
- fs19.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
16617
+ fs20.mkdirSync(path23.dirname(settingsPath), { recursive: true });
16618
+ fs20.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
15515
16619
  console.log(chalk4.green(" \u2714 Agent Teams enabled in ~/.claude/settings.json"));
15516
16620
  } else {
15517
16621
  console.log(chalk4.green(" \u2714 Agent Teams already enabled"));
@@ -15591,7 +16695,7 @@ var initCommand = new Command2("init").description("Guided first-time setup: hub
15591
16695
  init_dist();
15592
16696
  init_dist2();
15593
16697
  import { Command as Command3 } from "commander";
15594
- import fs20 from "fs";
16698
+ import fs21 from "fs";
15595
16699
  import path24 from "path";
15596
16700
  import chalk5 from "chalk";
15597
16701
  function restartAfterProjectsAdd(opts) {
@@ -15606,20 +16710,20 @@ function restartAfterProjectsAdd(opts) {
15606
16710
  function findPackageRoot2() {
15607
16711
  let dir = path24.dirname(new URL(import.meta.url).pathname);
15608
16712
  while (dir !== "/") {
15609
- if (fs20.existsSync(path24.join(dir, "package.json"))) return dir;
16713
+ if (fs21.existsSync(path24.join(dir, "package.json"))) return dir;
15610
16714
  dir = path24.dirname(dir);
15611
16715
  }
15612
16716
  return process.cwd();
15613
16717
  }
15614
16718
  function copyDirRecursive2(src, dest) {
15615
- fs20.mkdirSync(dest, { recursive: true });
15616
- for (const entry of fs20.readdirSync(src, { withFileTypes: true })) {
16719
+ fs21.mkdirSync(dest, { recursive: true });
16720
+ for (const entry of fs21.readdirSync(src, { withFileTypes: true })) {
15617
16721
  const srcPath = path24.join(src, entry.name);
15618
16722
  const destPath = path24.join(dest, entry.name);
15619
16723
  if (entry.isDirectory()) {
15620
16724
  copyDirRecursive2(srcPath, destPath);
15621
16725
  } else {
15622
- fs20.copyFileSync(srcPath, destPath);
16726
+ fs21.copyFileSync(srcPath, destPath);
15623
16727
  }
15624
16728
  }
15625
16729
  }
@@ -15655,7 +16759,7 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
15655
16759
  process.exit(1);
15656
16760
  }
15657
16761
  const resolvedPath = resolveHome(projectPath);
15658
- if (!fs20.existsSync(path24.join(resolvedPath, ".git"))) {
16762
+ if (!fs21.existsSync(path24.join(resolvedPath, ".git"))) {
15659
16763
  console.log(chalk5.yellow(`
15660
16764
  \u26A0 No .git found at ${resolvedPath}. Make sure this is the project root, not a parent directory.
15661
16765
  `));
@@ -15724,19 +16828,19 @@ var addCmd = new Command3("add").description("Register a project").argument("<na
15724
16828
  restartAfterProjectsAdd({ noRestart: opts.restart === false });
15725
16829
  const pkgRoot = findPackageRoot2();
15726
16830
  const spokeTemplate = path24.join(pkgRoot, "obsidian", "spoke");
15727
- if (fs20.existsSync(spokeVault)) {
16831
+ if (fs21.existsSync(spokeVault)) {
15728
16832
  console.log(chalk5.yellow(` \u26A0 Spoke vault already exists at ${spokeVault}, skipping scaffold`));
15729
- } else if (fs20.existsSync(spokeTemplate)) {
16833
+ } else if (fs21.existsSync(spokeTemplate)) {
15730
16834
  copyDirRecursive2(spokeTemplate, spokeVault);
15731
16835
  const statusPath = path24.join(spokeVault, "status.md");
15732
- if (fs20.existsSync(statusPath)) {
15733
- const content = fs20.readFileSync(statusPath, "utf-8");
16836
+ if (fs21.existsSync(statusPath)) {
16837
+ const content = fs21.readFileSync(statusPath, "utf-8");
15734
16838
  const updated = content.replace(/^project: unnamed/m, `project: ${name}`);
15735
- fs20.writeFileSync(statusPath, updated);
16839
+ fs21.writeFileSync(statusPath, updated);
15736
16840
  }
15737
16841
  console.log(chalk5.green(` \u2714 Spoke vault scaffolded at ${spokeVault}`));
15738
16842
  } else {
15739
- fs20.mkdirSync(spokeVault, { recursive: true });
16843
+ fs21.mkdirSync(spokeVault, { recursive: true });
15740
16844
  console.log(chalk5.yellow(` \u26A0 Spoke template not found; created empty dir at ${spokeVault}`));
15741
16845
  }
15742
16846
  console.log("");
@@ -15804,7 +16908,7 @@ init_dist4();
15804
16908
  init_dist3();
15805
16909
  init_dist();
15806
16910
  import { Command as Command5 } from "commander";
15807
- import { execSync as execSync9 } from "child_process";
16911
+ import { execSync as execSync10 } from "child_process";
15808
16912
  import path25 from "path";
15809
16913
  import os14 from "os";
15810
16914
  import chalk7 from "chalk";
@@ -15815,7 +16919,7 @@ var TASK_PROMPTS = {
15815
16919
  "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."
15816
16920
  };
15817
16921
  function detectCurrentWorkspace() {
15818
- const out = execSync9(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
16922
+ const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
15819
16923
  const match = out.match(/workspace:\d+/);
15820
16924
  if (!match) {
15821
16925
  throw new Error("Could not detect current cmux workspace. Run `squadrant command` from inside a cmux workspace.");
@@ -17133,7 +18237,7 @@ init_dist4();
17133
18237
  init_dist();
17134
18238
  init_dist2();
17135
18239
  import { Command as Command12 } from "commander";
17136
- import fs21 from "fs";
18240
+ import fs22 from "fs";
17137
18241
  import path26 from "path";
17138
18242
  import os15 from "os";
17139
18243
  import chalk12 from "chalk";
@@ -17170,7 +18274,7 @@ async function runSideSpawn2(input) {
17170
18274
  prompt: input.topic,
17171
18275
  workdir: spawnCwd,
17172
18276
  role: "side",
17173
- promptFile: fs21.existsSync(promptFile) ? promptFile : void 0,
18277
+ promptFile: fs22.existsSync(promptFile) ? promptFile : void 0,
17174
18278
  interactive: true,
17175
18279
  permissionMode: config.defaults.permissions?.crew ?? "auto",
17176
18280
  ...sideModel ? { model: sideModel } : {},
@@ -17277,7 +18381,7 @@ sideCommand.command("close").description("Close a side-session (closes its tab)"
17277
18381
  init_dist();
17278
18382
  init_dist4();
17279
18383
  import { Command as Command13 } from "commander";
17280
- import { execSync as execSync10 } from "child_process";
18384
+ import { execSync as execSync11 } from "child_process";
17281
18385
  import chalk14 from "chalk";
17282
18386
 
17283
18387
  // packages/web/dist/read-status.js
@@ -17433,7 +18537,7 @@ function renderDashboard(rows, opts) {
17433
18537
 
17434
18538
  // packages/web/dist/sync-hub.js
17435
18539
  init_dist();
17436
- import fs22 from "fs";
18540
+ import fs23 from "fs";
17437
18541
  import path27 from "path";
17438
18542
  function buildMirrorMarkdown(s) {
17439
18543
  const fenced = "```";
@@ -17460,8 +18564,8 @@ function buildMirrorMarkdown(s) {
17460
18564
  function syncHub(deps) {
17461
18565
  if (!deps.config.hubVault)
17462
18566
  return [];
17463
- const writeFile6 = deps.writeFile ?? ((p, c) => fs22.writeFileSync(p, c));
17464
- const mkdir6 = deps.mkdir ?? ((p) => fs22.mkdirSync(p, { recursive: true }));
18567
+ const writeFile6 = deps.writeFile ?? ((p, c) => fs23.writeFileSync(p, c));
18568
+ const mkdir6 = deps.mkdir ?? ((p) => fs23.mkdirSync(p, { recursive: true }));
17465
18569
  const projectsDir = path27.join(resolveHome(deps.config.hubVault), "projects");
17466
18570
  mkdir6(projectsDir);
17467
18571
  const out = [];
@@ -17481,6 +18585,7 @@ function syncHub(deps) {
17481
18585
  // packages/web/dist/web-server.js
17482
18586
  init_dist2();
17483
18587
  import { createServer as createServer5 } from "http";
18588
+ import { dirname as dirname7, join as join29 } from "path";
17484
18589
 
17485
18590
  // packages/web/dist/snapshot-merge.js
17486
18591
  function mergeSnapshot(daemon, external, now) {
@@ -17946,6 +19051,24 @@ function renderEnv(ext) {
17946
19051
  out.push(`</section>`);
17947
19052
  return out.join("");
17948
19053
  }
19054
+ function renderLogs() {
19055
+ return [
19056
+ `<section class="panel" data-panel="logs" role="tabpanel" aria-label="Logs">`,
19057
+ sectionHead("Daemon log", "Live tail of squadrantd.log \u2014 lifecycle events, delivery attempts, auto-launches and errors as the daemon writes them."),
19058
+ `<div class="log-controls">`,
19059
+ `<div class="filter-bar" role="group" aria-label="Filter log lines by severity">`,
19060
+ `<button class="filter-btn on" data-log-sev="all" aria-pressed="true">All</button>`,
19061
+ `<button class="filter-btn" data-log-sev="warn" aria-pressed="false">Warn+</button>`,
19062
+ `<button class="filter-btn" data-log-sev="error" aria-pressed="false">Errors</button>`,
19063
+ `</div>`,
19064
+ `<button class="filter-btn" data-log-pause aria-pressed="false">Pause</button>`,
19065
+ `<button class="filter-btn" data-log-copy>Copy</button>`,
19066
+ `<span class="log-status" data-log-status>connecting\u2026</span>`,
19067
+ `</div>`,
19068
+ `<div class="log-view mono" data-log-view aria-live="polite" aria-label="Daemon log lines"></div>`,
19069
+ `</section>`
19070
+ ].join("");
19071
+ }
17949
19072
  var ATTN_ORDER = { stale: 0, gone: 1, alive: 2, unknown: 3, stopped: 4 };
17950
19073
  var STATE_LABEL = { alive: "running", stale: "blocked", gone: "errored", unknown: "idle", stopped: "offline" };
17951
19074
  var STATE_CLS = { alive: "s-alive", stale: "s-stale", gone: "s-gone", unknown: "s-unknown", stopped: "s-stopped" };
@@ -18074,7 +19197,8 @@ var TABS = [
18074
19197
  ["overview", "Overview"],
18075
19198
  ["projects", "Projects"],
18076
19199
  ["daemon", "Daemon"],
18077
- ["environment", "Environment"]
19200
+ ["environment", "Environment"],
19201
+ ["logs", "Logs"]
18078
19202
  ];
18079
19203
  function tabNav() {
18080
19204
  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("");
@@ -18123,6 +19247,7 @@ function renderContent(snap) {
18123
19247
  out.push(renderProjects(snap, col.now));
18124
19248
  out.push(renderDaemon(snap));
18125
19249
  out.push(renderEnv(snap.external));
19250
+ out.push(renderLogs());
18126
19251
  out.push(`</div>`);
18127
19252
  out.push(metricsBlob(col, linkLost));
18128
19253
  return out.join("\n");
@@ -18364,6 +19489,17 @@ body{margin:0;min-height:100vh;background:radial-gradient(1200px 760px at 50% -2
18364
19489
 
18365
19490
  .empty{border:1px dashed var(--bezel);border-radius:12px;padding:24px;color:var(--ink-dim);text-align:center;background:var(--panel)}
18366
19491
 
19492
+ /* Logs (live daemon-log tail) */
19493
+ .log-controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:0 0 12px}
19494
+ .log-controls .filter-bar{margin:0}
19495
+ .log-status{margin-left:auto;color:var(--ink-dim);font-size:11px}
19496
+ .log-view{height:min(58vh,520px);overflow:auto;background:#0b1220;border:1px solid var(--bezel);border-radius:12px;
19497
+ padding:12px 14px;box-shadow:var(--shadow);font-size:12px;line-height:1.55}
19498
+ .log-line{white-space:pre-wrap;word-break:break-word;color:#c7d2e0}
19499
+ .log-line.l-warn{color:#f0c674}
19500
+ .log-line.l-error{color:#ff8a80}
19501
+ .log-view .empty-log{color:#6b7a90}
19502
+
18367
19503
  :focus-visible{outline:2px solid var(--hud);outline-offset:2px;border-radius:4px}
18368
19504
 
18369
19505
  @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)}}
@@ -18377,6 +19513,7 @@ body{margin:0;min-height:100vh;background:radial-gradient(1200px 760px at 50% -2
18377
19513
  var CLIENT_JS = `
18378
19514
  (function(){
18379
19515
  var activeTab='live';var hist={};var prev={};var currentFilter='all';var currentSort='attention';var sortAsc=true;var currentSearch='';var expandedProjects={};
19516
+ var logLines=[];var logSev='all';var logPaused=false;var logAutoscroll=true;var logConnected=false;var logScrollSave=0;var LOG_MAX=1000;
18380
19517
  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();}
18381
19518
  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);}
18382
19519
  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"/>';}
@@ -18389,9 +19526,19 @@ var CLIENT_JS = `
18389
19526
  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;});}
18390
19527
  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);});}
18391
19528
  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);}});}
18392
- function refresh(){ingest();applyTab();drawSparks();countUps();applyFilter();applySearch();applySort();applyExpand();}
18393
- 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;}}});
18394
- 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();}});
19529
+ function logSevOf(s){return /error|failed|fatal/i.test(s)?'error':/warn/i.test(s)?'warn':'info';}
19530
+ function logMatch(l){if(logSev==='all')return true;if(logSev==='error')return logSevOf(l)==='error';if(logSev==='warn')return logSevOf(l)!=='info';return true;}
19531
+ function escHtml(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
19532
+ function logVisible(){return logLines.filter(logMatch);}
19533
+ 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;
19534
+ 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');}
19535
+ 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');}
19536
+ 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';}
19537
+ }
19538
+ 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();}
19539
+ function refresh(){ingest();applyTab();drawSparks();countUps();applyFilter();applySearch();applySort();applyExpand();if(activeTab==='logs')renderLogs();}
19540
+ 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;}}});
19541
+ 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();}});
18395
19542
  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();}});
18396
19543
  var led=document.getElementById('led');var conn=document.getElementById('conn');var updated=document.getElementById('updated');
18397
19544
  function tickAge(){if(!generatedAt)return;var s=Math.max(0,Math.round((Date.now()-generatedAt)/1000));updated.textContent='updated '+s+'s ago';}
@@ -18400,6 +19547,11 @@ var CLIENT_JS = `
18400
19547
  es.onopen=function(){conn.textContent='live';conn.className='conn live';led.className='led live';};
18401
19548
  es.onerror=function(){conn.textContent='reconnecting';conn.className='conn down';led.className='led down';};
18402
19549
  es.onmessage=function(e){try{var d=JSON.parse(e.data);document.getElementById('content').innerHTML=d.contentHtml;generatedAt=d.generatedAt;tickAge();refresh();}catch(_){}};
19550
+ 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);
19551
+ var les=new EventSource('/logs');
19552
+ les.onopen=function(){logConnected=true;renderLogs();};
19553
+ les.onerror=function(){logConnected=false;renderLogs();};
19554
+ les.onmessage=function(e){try{var d=JSON.parse(e.data);if(!logPaused)ingestLog(d&&d.lines);}catch(_){}};
18403
19555
  refresh();tickAge();
18404
19556
  })();
18405
19557
  `.trim();
@@ -18422,7 +19574,100 @@ function renderHtml(snap, opts = {}) {
18422
19574
  ].join("");
18423
19575
  }
18424
19576
 
19577
+ // packages/web/dist/log-tail.js
19578
+ import { closeSync as closeSync3, openSync as openSync3, readSync as readSync2, statSync as statSync4 } from "fs";
19579
+ var LOG_TAIL_BYTES = 256 * 1024;
19580
+ var LOG_RING_SIZE = 1e3;
19581
+ function defaultSize(path39) {
19582
+ try {
19583
+ return statSync4(path39).size;
19584
+ } catch {
19585
+ return 0;
19586
+ }
19587
+ }
19588
+ function defaultRead(path39, start, len) {
19589
+ if (len <= 0)
19590
+ return "";
19591
+ try {
19592
+ const fd = openSync3(path39, "r");
19593
+ try {
19594
+ const buf = Buffer.alloc(len);
19595
+ const n = readSync2(fd, buf, 0, len, start);
19596
+ return buf.subarray(0, n).toString("utf-8");
19597
+ } finally {
19598
+ closeSync3(fd);
19599
+ }
19600
+ } catch {
19601
+ return "";
19602
+ }
19603
+ }
19604
+ var LogTailer = class {
19605
+ path;
19606
+ ringSize;
19607
+ offset = 0;
19608
+ pending = "";
19609
+ ring = [];
19610
+ size;
19611
+ read;
19612
+ constructor(path39, ringSize = LOG_RING_SIZE, deps = {}) {
19613
+ this.path = path39;
19614
+ this.ringSize = ringSize;
19615
+ this.size = deps.size ?? defaultSize;
19616
+ this.read = deps.read ?? defaultRead;
19617
+ }
19618
+ /** Seed history from the last `maxBytes` and position the cursor at EOF.
19619
+ * Idempotent; call once at startup. A trailing partial line stays pending
19620
+ * so the next append completes it without loss or duplication. */
19621
+ init(maxBytes = LOG_TAIL_BYTES) {
19622
+ const size = this.size(this.path);
19623
+ this.pending = "";
19624
+ this.ring = [];
19625
+ if (size <= 0) {
19626
+ this.offset = size;
19627
+ return;
19628
+ }
19629
+ const start = Math.max(0, size - maxBytes);
19630
+ const parts = this.read(this.path, start, size - start).split("\n");
19631
+ const rest = parts.pop() ?? "";
19632
+ if (start > 0 && parts.length)
19633
+ parts.shift();
19634
+ this.push(parts);
19635
+ this.pending = rest;
19636
+ this.offset = size;
19637
+ }
19638
+ /** Complete lines appended since the previous poll. */
19639
+ poll() {
19640
+ const size = this.size(this.path);
19641
+ if (size < this.offset) {
19642
+ this.offset = 0;
19643
+ this.pending = "";
19644
+ }
19645
+ if (size <= this.offset)
19646
+ return [];
19647
+ const text = this.read(this.path, this.offset, size - this.offset);
19648
+ if (!text)
19649
+ return [];
19650
+ this.offset += Buffer.byteLength(text);
19651
+ const parts = (this.pending + text).split("\n");
19652
+ this.pending = parts.pop() ?? "";
19653
+ this.push(parts);
19654
+ return parts;
19655
+ }
19656
+ /** The buffered backlog (oldest first) replayed to a newly-connected client. */
19657
+ backlog() {
19658
+ return this.ring.slice();
19659
+ }
19660
+ push(lines) {
19661
+ for (const l of lines)
19662
+ this.ring.push(l);
19663
+ const over = this.ring.length - this.ringSize;
19664
+ if (over > 0)
19665
+ this.ring.splice(0, over);
19666
+ }
19667
+ };
19668
+
18425
19669
  // packages/web/dist/web-server.js
19670
+ var LOG_POLL_MS = 1e3;
18426
19671
  async function querySnapshot(sockPath) {
18427
19672
  try {
18428
19673
  const reply = await sendRequest(sockPath, { kind: "snapshot" });
@@ -18435,6 +19680,9 @@ async function startWebServer(opts) {
18435
19680
  const host = opts.host ?? "127.0.0.1";
18436
19681
  const now = opts.now ?? (() => Date.now());
18437
19682
  const clients = /* @__PURE__ */ new Set();
19683
+ const logClients = /* @__PURE__ */ new Set();
19684
+ const logPath2 = opts.logPath ?? join29(dirname7(opts.sockPath), "squadrantd.log");
19685
+ const tailer = new LogTailer(logPath2);
18438
19686
  let latest = null;
18439
19687
  async function tick() {
18440
19688
  const daemon = await querySnapshot(opts.sockPath);
@@ -18450,6 +19698,19 @@ async function startWebServer(opts) {
18450
19698
  }
18451
19699
  }
18452
19700
  }
19701
+ function broadcastLog(lines) {
19702
+ if (lines.length === 0)
19703
+ return;
19704
+ const payload = `data: ${JSON.stringify({ lines })}
19705
+
19706
+ `;
19707
+ for (const res of logClients) {
19708
+ try {
19709
+ res.write(payload);
19710
+ } catch {
19711
+ }
19712
+ }
19713
+ }
18453
19714
  const server = createServer5((req, res) => {
18454
19715
  if (req.method !== "GET") {
18455
19716
  res.writeHead(405).end();
@@ -18475,6 +19736,22 @@ async function startWebServer(opts) {
18475
19736
  req.on("close", () => clients.delete(res));
18476
19737
  return;
18477
19738
  }
19739
+ if (req.url === "/logs") {
19740
+ res.writeHead(200, {
19741
+ "Content-Type": "text/event-stream",
19742
+ "Cache-Control": "no-cache",
19743
+ Connection: "keep-alive"
19744
+ });
19745
+ res.write("retry: 3000\n\n");
19746
+ const backlog = tailer.backlog();
19747
+ if (backlog.length)
19748
+ res.write(`data: ${JSON.stringify({ lines: backlog })}
19749
+
19750
+ `);
19751
+ logClients.add(res);
19752
+ req.on("close", () => logClients.delete(res));
19753
+ return;
19754
+ }
18478
19755
  res.writeHead(404).end();
18479
19756
  });
18480
19757
  await new Promise((resolve4, reject) => {
@@ -18489,17 +19766,33 @@ async function startWebServer(opts) {
18489
19766
  });
18490
19767
  }, opts.intervalMs);
18491
19768
  timer.unref?.();
19769
+ tailer.init();
19770
+ const logTimer = setInterval(() => {
19771
+ try {
19772
+ broadcastLog(tailer.poll());
19773
+ } catch {
19774
+ }
19775
+ }, LOG_POLL_MS);
19776
+ logTimer.unref?.();
18492
19777
  return {
18493
19778
  port: boundPort,
18494
19779
  close: () => new Promise((resolve4) => {
18495
19780
  clearInterval(timer);
19781
+ clearInterval(logTimer);
18496
19782
  for (const res of clients) {
18497
19783
  try {
18498
19784
  res.end();
18499
19785
  } catch {
18500
19786
  }
18501
19787
  }
19788
+ for (const res of logClients) {
19789
+ try {
19790
+ res.end();
19791
+ } catch {
19792
+ }
19793
+ }
18502
19794
  clients.clear();
19795
+ logClients.clear();
18503
19796
  server.close(() => resolve4());
18504
19797
  })
18505
19798
  };
@@ -18509,7 +19802,7 @@ async function startWebServer(opts) {
18509
19802
  init_dist();
18510
19803
  var SOCK3 = DAEMON_SOCK_PATH;
18511
19804
  function detectCurrentWorkspace2() {
18512
- const out = execSync10(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
19805
+ const out = execSync11(`"${resolveCmuxBin()}" current-workspace`, { encoding: "utf-8" }).trim();
18513
19806
  const match = out.match(/workspace:\d+/);
18514
19807
  if (!match) {
18515
19808
  throw new Error("Could not detect current cmux workspace. Run `squadrant dashboard --pane` from inside a cmux workspace.");
@@ -18592,8 +19885,8 @@ init_dist3();
18592
19885
  init_dist4();
18593
19886
  init_dist2();
18594
19887
  import { Command as Command14 } from "commander";
18595
- import { execSync as execSync11, execFileSync as execFileSync10 } from "child_process";
18596
- import fs23 from "fs";
19888
+ import { execSync as execSync12, execFileSync as execFileSync10 } from "child_process";
19889
+ import fs24 from "fs";
18597
19890
  import path28 from "path";
18598
19891
  import os16 from "os";
18599
19892
  import chalk15 from "chalk";
@@ -18724,7 +20017,7 @@ function resolveAnthropicRefusal(role, workspaceName, agentName, model, readOpen
18724
20017
  function ensureCmuxReady(headless) {
18725
20018
  if (headless || isInsideCmux()) return;
18726
20019
  console.log(chalk15.yellow("\n Not running inside cmux. Opening cmux app...\n"));
18727
- execSync11(`open "${CMUX_APP}"`, { stdio: "inherit" });
20020
+ execSync12(`open "${CMUX_APP}"`, { stdio: "inherit" });
18728
20021
  console.log(chalk15.bold(" Run `squadrant launch` from inside a cmux workspace.\n"));
18729
20022
  process.exit(0);
18730
20023
  }
@@ -18921,12 +20214,12 @@ var launchCommand = new Command14("launch").description(
18921
20214
  }
18922
20215
  if (opts.all) {
18923
20216
  const hubPath = resolveHome(config.hubVault);
18924
- fs23.mkdirSync(hubPath, { recursive: true });
20217
+ fs24.mkdirSync(hubPath, { recursive: true });
18925
20218
  console.log(chalk15.bold("\nLaunching all captain workspaces\n"));
18926
20219
  for (const [name, proj] of Object.entries(config.projects)) {
18927
20220
  const projPath = resolveHome(proj.path);
18928
20221
  const spokePath = resolveHome(proj.spokeVault);
18929
- if (!fs23.existsSync(spokePath)) {
20222
+ if (!fs24.existsSync(spokePath)) {
18930
20223
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(name, config);
18931
20224
  await ensureSpokeLayout(spokeDriver);
18932
20225
  console.log(chalk15.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -18964,7 +20257,7 @@ Launching ${selected.length} captain workspace(s) in parallel
18964
20257
  const proj = config.projects[name];
18965
20258
  const projPath = resolveHome(proj.path);
18966
20259
  const spokePath = resolveHome(proj.spokeVault);
18967
- if (!fs23.existsSync(spokePath)) {
20260
+ if (!fs24.existsSync(spokePath)) {
18968
20261
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(name, config);
18969
20262
  await ensureSpokeLayout(spokeDriver);
18970
20263
  console.log(chalk15.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -18988,7 +20281,7 @@ Launching ${selected.length} captain workspace(s) in parallel
18988
20281
  const proj = config.projects[project];
18989
20282
  const projPath = resolveHome(proj.path);
18990
20283
  const spokePath = resolveHome(proj.spokeVault);
18991
- if (!fs23.existsSync(spokePath)) {
20284
+ if (!fs24.existsSync(spokePath)) {
18992
20285
  const spokeDriver = new WorkspaceRegistry({ obsidian: createObsidianDriver }).forProject(project, config);
18993
20286
  await ensureSpokeLayout(spokeDriver);
18994
20287
  console.log(chalk15.cyan(` \u2714 Created spoke vault at ${spokePath}`));
@@ -19124,24 +20417,24 @@ Shutting down captain workspace for '${project}'...
19124
20417
  // packages/cli/src/commands/feedback.ts
19125
20418
  init_dist();
19126
20419
  import { Command as Command16 } from "commander";
19127
- import fs24 from "fs";
20420
+ import fs25 from "fs";
19128
20421
  import os17 from "os";
19129
20422
  import path29 from "path";
19130
20423
  import { fileURLToPath as fileURLToPath3 } from "url";
19131
- import { execSync as execSync12 } from "child_process";
20424
+ import { execSync as execSync13 } from "child_process";
19132
20425
  import chalk17 from "chalk";
19133
20426
  var REPO_URL = "https://github.com/tu11aa/squadrant";
19134
20427
  function readPkgVersion() {
19135
20428
  try {
19136
20429
  const pkgPath = path29.join(path29.dirname(fileURLToPath3(import.meta.url)), "..", "package.json");
19137
- return JSON.parse(fs24.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
20430
+ return JSON.parse(fs25.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
19138
20431
  } catch {
19139
20432
  return "unknown";
19140
20433
  }
19141
20434
  }
19142
20435
  function readMetrics(metricsPath) {
19143
20436
  try {
19144
- return JSON.parse(fs24.readFileSync(metricsPath, "utf-8"));
20437
+ return JSON.parse(fs25.readFileSync(metricsPath, "utf-8"));
19145
20438
  } catch {
19146
20439
  return {};
19147
20440
  }
@@ -19187,7 +20480,7 @@ var feedbackCommand = new Command16("feedback").description("Open a pre-filled G
19187
20480
  console.log(chalk17.dim(` URL: ${issueUrl.substring(0, 80)}...
19188
20481
  `));
19189
20482
  try {
19190
- execSync12(`open "${issueUrl}"`, { stdio: "ignore" });
20483
+ execSync13(`open "${issueUrl}"`, { stdio: "ignore" });
19191
20484
  console.log(chalk17.green(" \u2714 Browser opened\n"));
19192
20485
  } catch {
19193
20486
  console.log(chalk17.yellow(" \u26A0 Could not open browser automatically."));
@@ -19589,7 +20882,7 @@ init_dist4();
19589
20882
  init_dist();
19590
20883
  import { Command as Command21 } from "commander";
19591
20884
  import chalk22 from "chalk";
19592
- import fs25 from "fs";
20885
+ import fs26 from "fs";
19593
20886
  import path30 from "path";
19594
20887
  import { fileURLToPath as fileURLToPath4 } from "url";
19595
20888
  function parseScope(v2) {
@@ -19601,7 +20894,7 @@ function parseScope(v2) {
19601
20894
  function findPackageRoot3() {
19602
20895
  let dir = path30.dirname(fileURLToPath4(import.meta.url));
19603
20896
  while (dir !== "/" && dir !== "") {
19604
- if (fs25.existsSync(path30.join(dir, "package.json"))) return dir;
20897
+ if (fs26.existsSync(path30.join(dir, "package.json"))) return dir;
19605
20898
  dir = path30.dirname(dir);
19606
20899
  }
19607
20900
  return process.cwd();
@@ -19798,12 +21091,12 @@ init_dist();
19798
21091
  init_dist();
19799
21092
  init_dist2();
19800
21093
  import { Command as Command23 } from "commander";
19801
- import fs26 from "fs";
21094
+ import fs27 from "fs";
19802
21095
  import { fileURLToPath as fileURLToPath5 } from "url";
19803
- import { dirname as dirname7, join as join29 } from "path";
21096
+ import { dirname as dirname8, join as join30 } from "path";
19804
21097
  import chalk23 from "chalk";
19805
21098
  function runConfigCheck(opts) {
19806
- const raw = JSON.parse(fs26.readFileSync(opts.configPath, "utf-8"));
21099
+ const raw = JSON.parse(fs27.readFileSync(opts.configPath, "utf-8"));
19807
21100
  const def = getDefaultConfig();
19808
21101
  const items = detectDrift(raw, def);
19809
21102
  let working = raw;
@@ -19891,7 +21184,7 @@ function printItems(items) {
19891
21184
  var configCommand = new Command23("config").description("Inspect and reconcile squadrant config");
19892
21185
  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) => {
19893
21186
  const pkgVersion = readPkgVersion2();
19894
- if (!fs26.existsSync(DEFAULT_CONFIG_PATH)) {
21187
+ if (!fs27.existsSync(DEFAULT_CONFIG_PATH)) {
19895
21188
  console.log(chalk23.yellow("No config found \u2014 run `squadrant init` first."));
19896
21189
  return;
19897
21190
  }
@@ -19936,14 +21229,14 @@ configCommand.command("set").description("Write a config value by dotted key (e.
19936
21229
  }
19937
21230
  });
19938
21231
  function readPkgVersion2() {
19939
- const pkgPath = join29(dirname7(fileURLToPath5(import.meta.url)), "..", "package.json");
19940
- return JSON.parse(fs26.readFileSync(pkgPath, "utf-8")).version;
21232
+ const pkgPath = join30(dirname8(fileURLToPath5(import.meta.url)), "..", "package.json");
21233
+ return JSON.parse(fs27.readFileSync(pkgPath, "utf-8")).version;
19941
21234
  }
19942
21235
 
19943
21236
  // packages/cli/src/commands/heal.ts
19944
21237
  init_dist();
19945
21238
  import { execFileSync as execFileSync11 } from "child_process";
19946
- import { join as join30 } from "path";
21239
+ import { join as join31 } from "path";
19947
21240
  import { homedir as homedir17 } from "os";
19948
21241
  import { Command as Command24 } from "commander";
19949
21242
  import chalk24 from "chalk";
@@ -20183,8 +21476,8 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
20183
21476
  }
20184
21477
  const config = loadConfig();
20185
21478
  const projects = opts.all ? Object.keys(config.projects) : [project];
20186
- const stateRoot = join30(homedir17(), ".config", "squadrant", "state");
20187
- const registry = new LivenessRegistry({ path: join30(stateRoot, "liveness.json") });
21479
+ const stateRoot = join31(homedir17(), ".config", "squadrant", "state");
21480
+ const registry = new LivenessRegistry({ path: join31(stateRoot, "liveness.json") });
20188
21481
  registry.load();
20189
21482
  const code = await runHealCaptain(projects, {
20190
21483
  liveness: readCmuxLiveness,
@@ -20216,7 +21509,7 @@ import chalk26 from "chalk";
20216
21509
  init_dist();
20217
21510
  init_dist2();
20218
21511
  import { Command as Command25 } from "commander";
20219
- import { execSync as execSync13 } from "child_process";
21512
+ import { execSync as execSync14 } from "child_process";
20220
21513
  import chalk25 from "chalk";
20221
21514
  async function runDispatch(toProject, task, opts) {
20222
21515
  const fromProject = resolveCurrentProject(loadConfig());
@@ -20232,7 +21525,7 @@ async function runDispatch(toProject, task, opts) {
20232
21525
  warmupTimeoutMs: opts.warmupTimeout,
20233
21526
  bootCaptain: async (project) => {
20234
21527
  try {
20235
- execSync13(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
21528
+ execSync14(`squadrant launch ${project}`, { stdio: "ignore", timeout: 15e3 });
20236
21529
  } catch {
20237
21530
  throw new Error(`failed to launch captain for '${project}' \u2014 is squadrant installed?`);
20238
21531
  }
@@ -20268,7 +21561,7 @@ init_dist();
20268
21561
  init_dist2();
20269
21562
  init_runtime2();
20270
21563
  init_require_daemon();
20271
- import { join as join32, dirname as dirname8 } from "path";
21564
+ import { join as join33, dirname as dirname9 } from "path";
20272
21565
  import { homedir as homedir18 } from "os";
20273
21566
  import { Command as Command27 } from "commander";
20274
21567
  import chalk28 from "chalk";
@@ -20277,23 +21570,23 @@ import chalk28 from "chalk";
20277
21570
  init_dist3();
20278
21571
  init_dist2();
20279
21572
  import { createServer as createServer6, connect as netConnect2 } from "net";
20280
- import fs27 from "fs";
20281
- import { join as join31 } from "path";
21573
+ import fs28 from "fs";
21574
+ import { join as join32 } from "path";
20282
21575
  import { randomUUID as randomUUID7 } from "crypto";
20283
21576
  import chalk27 from "chalk";
20284
21577
  var shared;
20285
- var registryEntryPath = () => join31(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
21578
+ var registryEntryPath = () => join32(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
20286
21579
  function unregisterSenderIdentity() {
20287
21580
  try {
20288
- fs27.unlinkSync(registryEntryPath());
21581
+ fs28.unlinkSync(registryEntryPath());
20289
21582
  } catch {
20290
21583
  }
20291
21584
  }
20292
21585
  function registerSenderIdentity(socketPath2) {
20293
21586
  try {
20294
21587
  unregisterSenderIdentity();
20295
- fs27.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
20296
- fs27.writeFileSync(
21588
+ fs28.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
21589
+ fs28.writeFileSync(
20297
21590
  registryEntryPath(),
20298
21591
  JSON.stringify({
20299
21592
  pid: process.pid,
@@ -20319,7 +21612,7 @@ async function sharedReceiptListener() {
20319
21612
  // must never be the reason we refuse to start.
20320
21613
  unlinkStale: (p) => {
20321
21614
  try {
20322
- fs27.unlinkSync(p);
21615
+ fs28.unlinkSync(p);
20323
21616
  } catch {
20324
21617
  }
20325
21618
  },
@@ -20389,7 +21682,7 @@ async function runPing(project, message) {
20389
21682
  await requireDaemon();
20390
21683
  await needRef(resolved);
20391
21684
  const mode = resolveCaptainChannelMode(config.defaults);
20392
- const stateRoot = join32(homedir18(), ".config", "squadrant", "state");
21685
+ const stateRoot = join33(homedir18(), ".config", "squadrant", "state");
20393
21686
  const { channels, agentFor } = mode === "off" ? { channels: {}, agentFor: () => void 0 } : await buildCaptainChannels({ stateRoot, configAgent: config.defaults.roles?.captain?.agent });
20394
21687
  const channel = channels[agentFor(project) ?? ""];
20395
21688
  const { handled, outcome } = await deliverToCaptain(project, message, {
@@ -20398,7 +21691,7 @@ async function runPing(project, message) {
20398
21691
  log: (m) => console.error(chalk28.dim(m))
20399
21692
  });
20400
21693
  if (!handled) {
20401
- const stateRoot2 = join32(dirname8(DEFAULT_CONFIG_PATH), "state");
21694
+ const stateRoot2 = join33(dirname9(DEFAULT_CONFIG_PATH), "state");
20402
21695
  await appendCaptainMessage({
20403
21696
  stateRoot: stateRoot2,
20404
21697
  project,
@@ -20477,7 +21770,7 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
20477
21770
  // packages/cli/src/commands/effort.ts
20478
21771
  init_dist();
20479
21772
  init_dist2();
20480
- import fs28 from "fs";
21773
+ import fs29 from "fs";
20481
21774
  import path31 from "path";
20482
21775
  import { Command as Command29 } from "commander";
20483
21776
  import chalk30 from "chalk";
@@ -20521,7 +21814,7 @@ function effortScopeLabel(projectName) {
20521
21814
  }
20522
21815
  function canonical(p) {
20523
21816
  try {
20524
- return fs28.realpathSync(p);
21817
+ return fs29.realpathSync(p);
20525
21818
  } catch {
20526
21819
  return path31.resolve(p);
20527
21820
  }
@@ -20583,7 +21876,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
20583
21876
 
20584
21877
  // packages/cli/src/commands/tokens.ts
20585
21878
  init_dist();
20586
- import fs29 from "fs";
21879
+ import fs30 from "fs";
20587
21880
  import path32 from "path";
20588
21881
  import os18 from "os";
20589
21882
  import readline3 from "readline";
@@ -20639,7 +21932,7 @@ function foldTranscriptLine(agg, rawLine, state) {
20639
21932
  async function aggregateTranscriptFile(filePath) {
20640
21933
  const agg = emptySessionAggregate();
20641
21934
  const state = { lastCacheRead: null };
20642
- const rl = readline3.createInterface({ input: fs29.createReadStream(filePath), crlfDelay: Infinity });
21935
+ const rl = readline3.createInterface({ input: fs30.createReadStream(filePath), crlfDelay: Infinity });
20643
21936
  for await (const line of rl) {
20644
21937
  foldTranscriptLine(agg, line, state);
20645
21938
  }
@@ -20707,7 +22000,7 @@ function buildRoleReport(role, sessions) {
20707
22000
  }
20708
22001
  async function readdirSafe(dir) {
20709
22002
  try {
20710
- return await fs29.promises.readdir(dir);
22003
+ return await fs30.promises.readdir(dir);
20711
22004
  } catch {
20712
22005
  return [];
20713
22006
  }
@@ -20874,12 +22167,12 @@ var tokensCommand = new Command30("tokens").description(
20874
22167
  // packages/cli/src/commands/telegram.ts
20875
22168
  init_dist();
20876
22169
  init_dist2();
20877
- import { join as join33, dirname as dirname9 } from "path";
22170
+ import { join as join34, dirname as dirname10 } from "path";
20878
22171
  import { emitKeypressEvents } from "readline";
20879
22172
  import { Command as Command31 } from "commander";
20880
22173
  import chalk32 from "chalk";
20881
22174
  function defaultStateRoot() {
20882
- return join33(dirname9(DEFAULT_CONFIG_PATH), "state");
22175
+ return join34(dirname10(DEFAULT_CONFIG_PATH), "state");
20883
22176
  }
20884
22177
  async function questionMasked() {
20885
22178
  return new Promise((resolve4) => {
@@ -21208,7 +22501,7 @@ import { Command as Command33 } from "commander";
21208
22501
  import { homedir as homedir19 } from "os";
21209
22502
 
21210
22503
  // packages/cli/src/lib/captain-session-registry.ts
21211
- import fs30 from "fs";
22504
+ import fs31 from "fs";
21212
22505
  import path33 from "path";
21213
22506
 
21214
22507
  // packages/cli/src/lib/handoff-facts.ts
@@ -21260,15 +22553,15 @@ function assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, now, ext
21260
22553
  // packages/cli/src/lib/captain-session-registry.ts
21261
22554
  var CAPTAIN_SESSION_REGISTRY_FILE = "captain-sessions.jsonl";
21262
22555
  function appendCaptainSession(spokeVault, record) {
21263
- fs30.mkdirSync(spokeVault, { recursive: true });
22556
+ fs31.mkdirSync(spokeVault, { recursive: true });
21264
22557
  const file = path33.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
21265
- fs30.appendFileSync(file, JSON.stringify(record) + "\n");
22558
+ fs31.appendFileSync(file, JSON.stringify(record) + "\n");
21266
22559
  }
21267
22560
  function readCaptainSessionRegistry(spokeVault) {
21268
22561
  const file = path33.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
21269
- if (!fs30.existsSync(file)) return [];
22562
+ if (!fs31.existsSync(file)) return [];
21270
22563
  const records = [];
21271
- for (const line of fs30.readFileSync(file, "utf-8").split("\n")) {
22564
+ for (const line of fs31.readFileSync(file, "utf-8").split("\n")) {
21272
22565
  if (!line.trim()) continue;
21273
22566
  try {
21274
22567
  records.push(JSON.parse(line));
@@ -21293,6 +22586,30 @@ init_dist();
21293
22586
  init_dist3();
21294
22587
  import { Command as Command32 } from "commander";
21295
22588
  var SOCK4 = DAEMON_SOCK_PATH;
22589
+ async function runAutoGateClaudeHook(o) {
22590
+ const make = o.deps.createAutoGate ?? createSquadrantAutoGate;
22591
+ const autoGate = make({
22592
+ config: o.config,
22593
+ env: o.env,
22594
+ ...o.project ? { project: o.project } : {},
22595
+ ...o.deps.sendEvent ? { sendBlocked: o.deps.sendEvent } : {},
22596
+ log: o.log
22597
+ });
22598
+ const out = await autoGate.decideClaudeHookPayload(o.deps.rawPayload ?? "");
22599
+ if (out === void 0) {
22600
+ o.log("gate auto-gate: ask/yield \u2014 normal dialog");
22601
+ return { decision: "ask", tier: 2, reason: "auto-gate: ask/yield" };
22602
+ }
22603
+ o.stdout(out);
22604
+ let decision = "allow";
22605
+ try {
22606
+ const parsed = JSON.parse(out);
22607
+ if (parsed?.hookSpecificOutput?.decision?.behavior === "deny") decision = "deny";
22608
+ } catch {
22609
+ }
22610
+ o.log(`gate auto-gate ${decision}`);
22611
+ return { decision, tier: 2, reason: "auto-gate" };
22612
+ }
21296
22613
  async function runGatePermissionRequest(deps) {
21297
22614
  const env = deps.env ?? process.env;
21298
22615
  const payload = deps.payload ?? {};
@@ -21307,6 +22624,10 @@ async function runGatePermissionRequest(deps) {
21307
22624
  const log = deps.log ?? ((m) => process.stderr.write(`[squadrant] ${m}
21308
22625
  `));
21309
22626
  const config = deps.config ?? (deps.loadConfigFn ?? loadConfig)();
22627
+ const gate = config.defaults.gate;
22628
+ if (resolveGateMode(env, gate) === "on" && resolveGateEngine(env, gate) === "auto-gate") {
22629
+ return runAutoGateClaudeHook({ deps, env, config, project, stdout, log });
22630
+ }
21310
22631
  const cwd = deps.cwd ?? (typeof p.cwd === "string" && p.cwd ? p.cwd : env.SQUADRANT_CREW_CWD ?? process.cwd());
21311
22632
  const transcriptPath = typeof p.transcript_path === "string" && p.transcript_path ? p.transcript_path : deriveTranscriptPath(
21312
22633
  typeof p.session_id === "string" ? p.session_id : "",
@@ -21370,6 +22691,7 @@ function gateCommand() {
21370
22691
  }
21371
22692
  await runGatePermissionRequest({
21372
22693
  payload,
22694
+ rawPayload: stdin,
21373
22695
  env: process.env,
21374
22696
  stdout: (s) => process.stdout.write(s),
21375
22697
  sendEvent: async (project, ev) => {
@@ -21452,6 +22774,7 @@ function hooksCommand() {
21452
22774
  if (sub === "permission-request") {
21453
22775
  await runGatePermissionRequest({
21454
22776
  payload,
22777
+ rawPayload: stdin,
21455
22778
  env: process.env,
21456
22779
  stdout: (s) => process.stdout.write(s),
21457
22780
  sendEvent: async (project2, ev2) => {
@@ -21645,7 +22968,7 @@ import os19 from "os";
21645
22968
  // packages/cli/src/lib/handoff-live-repo.ts
21646
22969
  init_dist();
21647
22970
  import { execFileSync as execFileSync12 } from "child_process";
21648
- import fs31 from "fs";
22971
+ import fs32 from "fs";
21649
22972
  import path35 from "path";
21650
22973
 
21651
22974
  // packages/cli/src/lib/handoff-branch-state.ts
@@ -21785,7 +23108,7 @@ function localAheadOfBase(runner, projectPath, base) {
21785
23108
  }
21786
23109
  function readFetchAgeMs(projectPath, now) {
21787
23110
  try {
21788
- const stat2 = fs31.statSync(path35.join(projectPath, ".git", "FETCH_HEAD"));
23111
+ const stat2 = fs32.statSync(path35.join(projectPath, ".git", "FETCH_HEAD"));
21789
23112
  return Math.max(0, now - stat2.mtime.getTime());
21790
23113
  } catch {
21791
23114
  return null;
@@ -21856,7 +23179,7 @@ function gatherLiveRepoState(projectPath, fallbackBaseBranch, tasks, runner = de
21856
23179
 
21857
23180
  // packages/cli/src/lib/handoff-claude-mem.ts
21858
23181
  import { createRequire } from "module";
21859
- import fs32 from "fs";
23182
+ import fs33 from "fs";
21860
23183
  var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
21861
23184
  var CLAUDE_MEM_RECENCY_LIMIT = 20;
21862
23185
  function decisionText(row) {
@@ -21870,7 +23193,7 @@ function decisionText(row) {
21870
23193
  return row.narrative ?? "";
21871
23194
  }
21872
23195
  function queryClaudeMem(dbPath, project) {
21873
- if (!fs32.existsSync(dbPath)) return null;
23196
+ if (!fs33.existsSync(dbPath)) return null;
21874
23197
  let db;
21875
23198
  try {
21876
23199
  db = new DatabaseSync(dbPath, { readOnly: true });
@@ -21913,7 +23236,7 @@ function queryClaudeMem(dbPath, project) {
21913
23236
  }
21914
23237
 
21915
23238
  // packages/cli/src/lib/handoff-transcript.ts
21916
- import fs33 from "fs";
23239
+ import fs34 from "fs";
21917
23240
  var TRANSCRIPT_BYTE_CAP = 2e5;
21918
23241
  var DIGEST_BYTE_CAP = 8 * 1024;
21919
23242
  var PROMPT_TRUNCATE_CHARS = 300;
@@ -22022,27 +23345,27 @@ function extractMessages(tailText) {
22022
23345
  return { lastUserMessage, lastAssistantText, digest };
22023
23346
  }
22024
23347
  function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
22025
- if (!fs33.existsSync(transcriptPath)) return null;
22026
- const content = fs33.readFileSync(transcriptPath, "utf-8");
23348
+ if (!fs34.existsSync(transcriptPath)) return null;
23349
+ const content = fs34.readFileSync(transcriptPath, "utf-8");
22027
23350
  const { lastUserMessage, lastAssistantText, digest } = extractMessages(tailOf(content, byteCap));
22028
- const mtimeIso = fs33.statSync(transcriptPath).mtime.toISOString();
23351
+ const mtimeIso = fs34.statSync(transcriptPath).mtime.toISOString();
22029
23352
  return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText, digest };
22030
23353
  }
22031
23354
 
22032
23355
  // packages/cli/src/lib/handoff-archive.ts
22033
- import fs34 from "fs";
23356
+ import fs35 from "fs";
22034
23357
  import path36 from "path";
22035
23358
  function readNewestArchivedHandoff(spokeVault, now) {
22036
23359
  const dir = path36.join(spokeVault, "handoffs");
22037
- if (!fs34.existsSync(dir)) return null;
22038
- const candidates = fs34.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
23360
+ if (!fs35.existsSync(dir)) return null;
23361
+ const candidates = fs35.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
22039
23362
  const full = path36.join(dir, e.name);
22040
- return { name: e.name, full, mtime: fs34.statSync(full).mtime };
23363
+ return { name: e.name, full, mtime: fs35.statSync(full).mtime };
22041
23364
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
22042
23365
  for (const candidate of candidates) {
22043
23366
  let content;
22044
23367
  try {
22045
- content = JSON.parse(fs34.readFileSync(candidate.full, "utf-8"));
23368
+ content = JSON.parse(fs35.readFileSync(candidate.full, "utf-8"));
22046
23369
  } catch {
22047
23370
  continue;
22048
23371
  }
@@ -22385,15 +23708,15 @@ init_dist();
22385
23708
  init_dist();
22386
23709
  init_dist();
22387
23710
  init_dist();
22388
- var __dirname = dirname10(fileURLToPath6(import.meta.url));
22389
- var pkg = JSON.parse(readFileSync21(join34(__dirname, "..", "package.json"), "utf-8"));
23711
+ var __dirname = dirname11(fileURLToPath6(import.meta.url));
23712
+ var pkg = JSON.parse(readFileSync21(join35(__dirname, "..", "package.json"), "utf-8"));
22390
23713
  ensureRuntimeSynced({
22391
- sourceRoot: join34(__dirname, ".."),
22392
- runtimeRoot: join34(homedir20(), ".config", "squadrant")
23714
+ sourceRoot: join35(__dirname, ".."),
23715
+ runtimeRoot: join35(homedir20(), ".config", "squadrant")
22393
23716
  });
22394
23717
  if (!process.env.VITEST) {
22395
23718
  try {
22396
- syncShippedOpencodeSkills({ pkgRoot: join34(__dirname, "..") });
23719
+ syncShippedOpencodeSkills({ pkgRoot: join35(__dirname, "..") });
22397
23720
  } catch (err) {
22398
23721
  process.stderr.write(
22399
23722
  `squadrant: opencode skills sync skipped: ${err.message}
@@ -22403,7 +23726,7 @@ if (!process.env.VITEST) {
22403
23726
  }
22404
23727
  if (process.argv[2] !== "config") {
22405
23728
  try {
22406
- const cfgPath = join34(homedir20(), ".config", "squadrant", "config.json");
23729
+ const cfgPath = join35(homedir20(), ".config", "squadrant", "config.json");
22407
23730
  if (existsSync14(cfgPath)) {
22408
23731
  const cfg = JSON.parse(readConfigFileSync(cfgPath));
22409
23732
  if (needsCheck(cfg, pkg.version)) {