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.
@@ -94,6 +94,9 @@ function isGateMode(v2) {
94
94
  function isGatePolicy(v2) {
95
95
  return GATE_POLICIES.includes(v2);
96
96
  }
97
+ function isGateEngine(v2) {
98
+ return GATE_ENGINES.includes(v2);
99
+ }
97
100
  function getDefaultConfig() {
98
101
  return {
99
102
  commandName: "\u{1F3DB}\uFE0F command",
@@ -135,12 +138,7 @@ function getDefaultConfig() {
135
138
  // resolver fallback is the safety net).
136
139
  controlChannel: { claude: "on", opencode: "on" },
137
140
  crewRouting: {
138
- rules: [
139
- { tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
140
- { tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
141
- { tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
142
- { tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
143
- ]
141
+ rules: DEFAULT_CREW_ROUTING_RULES.map((r) => ({ ...r }))
144
142
  }
145
143
  },
146
144
  metrics: {
@@ -182,15 +180,29 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
182
180
  function resolveHome(p) {
183
181
  return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
184
182
  }
185
- var GATE_MODES, GATE_POLICIES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
183
+ var GATE_MODES, GATE_POLICIES, GATE_ENGINES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
186
184
  var init_config = __esm({
187
185
  "packages/shared/dist/config.js"() {
188
186
  init_config_io();
189
187
  GATE_MODES = ["on", "off", "auto"];
190
188
  GATE_POLICIES = ["deny-dangerous", "ask-on-doubt"];
189
+ GATE_ENGINES = ["router", "auto-gate"];
191
190
  DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
192
191
  CONFIG_DIR = path2.dirname(DEFAULT_CONFIG_PATH);
193
192
  DAEMON_SOCK_PATH = path2.join(CONFIG_DIR, "squadrant.sock");
193
+ DEFAULT_CREW_ROUTING_RULES = [
194
+ { tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
195
+ { tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
196
+ { tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
197
+ { tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
198
+ ];
199
+ }
200
+ });
201
+
202
+ // packages/shared/dist/provider-preset.js
203
+ var init_provider_preset = __esm({
204
+ "packages/shared/dist/provider-preset.js"() {
205
+ init_config();
194
206
  }
195
207
  });
196
208
 
@@ -790,6 +802,7 @@ var init_daemon_keys = __esm({
790
802
  var init_dist = __esm({
791
803
  "packages/shared/dist/index.js"() {
792
804
  init_config();
805
+ init_provider_preset();
793
806
  init_project_config();
794
807
  init_effort();
795
808
  init_router_model();
@@ -1061,10 +1074,11 @@ function formatDelegationReport(rec, originProject, targetProject) {
1061
1074
  return null;
1062
1075
  }
1063
1076
  }
1064
- function firePush(deps, project, prev, next, event, lastCaptainTurnAt) {
1077
+ function firePush(deps, project, prev, next, event, lastCaptainTurnAt, prevRec) {
1065
1078
  if (!deps.notify)
1066
1079
  return;
1067
- if (prev === next.state)
1080
+ const questionChanged = prev === "blocked" && next.state === "blocked" && prevRec != null && (prevRec.question ?? "").trim() !== (next.question ?? "").trim();
1081
+ if (prev === next.state && !questionChanged)
1068
1082
  return;
1069
1083
  if (!ATTENTION_STATES.has(next.state))
1070
1084
  return;
@@ -1102,6 +1116,7 @@ function createDaemon(deps) {
1102
1116
  const { store, now } = deps;
1103
1117
  const lastCaptainTurnAt = /* @__PURE__ */ new Map();
1104
1118
  const quietNotifiedAt = /* @__PURE__ */ new Map();
1119
+ const blockedRenudgedFor = /* @__PURE__ */ new Map();
1105
1120
  const resendAttemptedAt = /* @__PURE__ */ new Map();
1106
1121
  const firstTurnUndeliveredBudgetMs = deps.firstTurnUndeliveredBudgetMs ?? DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS;
1107
1122
  const firstTurnResendCooldownMs = deps.firstTurnResendCooldownMs ?? DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS;
@@ -1122,7 +1137,7 @@ function createDaemon(deps) {
1122
1137
  const next = reduce(cur, event, now());
1123
1138
  if (next !== cur) {
1124
1139
  store.put(next);
1125
- firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id));
1140
+ firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id), cur);
1126
1141
  }
1127
1142
  return next;
1128
1143
  }
@@ -1274,6 +1289,8 @@ function createDaemon(deps) {
1274
1289
  }
1275
1290
  const recoveryPromises = [];
1276
1291
  for (const r of store.listAll()) {
1292
+ if (r.state !== "blocked")
1293
+ blockedRenudgedFor.delete(r.id);
1277
1294
  if (TERMINAL_STATES.has(r.state) && t - r.lastHeartbeat > TERMINAL_RECORD_TTL_MS) {
1278
1295
  store.delete(r.project, r.id);
1279
1296
  continue;
@@ -1336,6 +1353,27 @@ function createDaemon(deps) {
1336
1353
  continue;
1337
1354
  }
1338
1355
  }
1356
+ if (r.state === "blocked" && !r.operatorHold) {
1357
+ const threshold = deps.blockedRenudgeMs ?? DEFAULT_BLOCKED_RENUDGE_MS;
1358
+ const blockedFor = t - r.lastHeartbeat;
1359
+ const episodeKey = r.question ?? "";
1360
+ if (blockedFor > threshold && blockedRenudgedFor.get(r.id) !== episodeKey) {
1361
+ blockedRenudgedFor.set(r.id, episodeKey);
1362
+ if (deps.notify) {
1363
+ const tag = crewTag(r);
1364
+ const mins = Math.max(1, Math.round(blockedFor / 6e4));
1365
+ const message = `CREW BLOCKED REMINDER ${tag}: still blocked ~${mins}min (reminder \u2014 same question, not a new one): ${(r.question ?? "(no question)").trim()}`;
1366
+ const synthEvent = { type: "task.blocked", id: r.id, reason: "renudge", question: r.question ?? "" };
1367
+ try {
1368
+ const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
1369
+ if (p && typeof p.catch === "function")
1370
+ p.catch(() => {
1371
+ });
1372
+ } catch {
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1339
1377
  if (r.mode === "interactive" && r.state === "submitted" && !r.firstTurnConfirmedAt) {
1340
1378
  const undeliveredMs = t - r.createdAt;
1341
1379
  if (undeliveredMs > firstTurnUndeliveredBudgetMs) {
@@ -1425,7 +1463,7 @@ function createDaemon(deps) {
1425
1463
  }
1426
1464
  };
1427
1465
  }
1428
- 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;
1466
+ 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;
1429
1467
  var init_reduce = __esm({
1430
1468
  "packages/core/dist/daemon/reduce.js"() {
1431
1469
  init_dist();
@@ -1433,6 +1471,7 @@ var init_reduce = __esm({
1433
1471
  init_watchdog();
1434
1472
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS = 12e4;
1435
1473
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS = 6e4;
1474
+ DEFAULT_BLOCKED_RENUDGE_MS = 30 * 60 * 1e3;
1436
1475
  DEFAULT_TASK_TIMEOUT_MS = 8 * 60 * 60 * 1e3;
1437
1476
  TERMINAL_RECORD_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1438
1477
  TERMINAL_RECORD_KEEP_PER_PROJECT = 20;
@@ -2027,8 +2066,8 @@ function reconcileLiveness(prev, next) {
2027
2066
  if (!prev)
2028
2067
  return next;
2029
2068
  if (next.source === "scan") {
2030
- const pidAlive = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
2031
- return { ...prev, pidAlive, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
2069
+ const pidAlive2 = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
2070
+ return { ...prev, pidAlive: pidAlive2, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
2032
2071
  }
2033
2072
  if (next.startedAt >= prev.startedAt || next.lastState === "end")
2034
2073
  return next;
@@ -2774,6 +2813,68 @@ function createDirectSurfaceLivenessProbe(cmux2, getCaptainTitle) {
2774
2813
  }
2775
2814
  };
2776
2815
  }
2816
+ function hasModalOptionList(screen) {
2817
+ if (!screen)
2818
+ return false;
2819
+ const lines = screen.split(/\r?\n/);
2820
+ let bottomHR = -1;
2821
+ let topHR = -1;
2822
+ for (let i = lines.length - 1; i >= 0; i--) {
2823
+ if (HR_RE.test(lines[i])) {
2824
+ if (bottomHR === -1)
2825
+ bottomHR = i;
2826
+ else {
2827
+ topHR = i;
2828
+ break;
2829
+ }
2830
+ }
2831
+ }
2832
+ if (topHR === -1)
2833
+ return false;
2834
+ return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
2835
+ }
2836
+ function classifyCaptainPane(screen, opts = {}) {
2837
+ if (screen == null || screen.trim() === "")
2838
+ return { status: "unreadable" };
2839
+ if (hasModalOptionList(screen))
2840
+ return { status: "locked" };
2841
+ if (WORKING_RE.test(screen))
2842
+ return { status: "mid-turn" };
2843
+ const idleAfterMs = opts.idleAfterMs ?? 0;
2844
+ const stale = opts.outputAgoMs !== void 0 && opts.outputAgoMs >= idleAfterMs;
2845
+ return stale ? { status: "no-session" } : { status: "mid-turn" };
2846
+ }
2847
+ function screenHash(screen) {
2848
+ let h = 2166136261;
2849
+ for (let i = 0; i < screen.length; i++) {
2850
+ h ^= screen.charCodeAt(i);
2851
+ h = Math.imul(h, 16777619) >>> 0;
2852
+ }
2853
+ return h.toString(16).padStart(8, "0");
2854
+ }
2855
+ function createCaptainPaneReader(cmux2, getCaptainTitle, opts = {}) {
2856
+ const idleAfterMs = opts.idleAfterMs ?? PANE_IDLE_AFTER_MS;
2857
+ return async (project) => {
2858
+ try {
2859
+ const wsId = await cmux2.findWorkspaceId(getCaptainTitle(project));
2860
+ if (!wsId)
2861
+ return { status: "no-session" };
2862
+ const surfaces = await cmux2.listSurfaces(wsId);
2863
+ const want = getCaptainTitle(project);
2864
+ const pane = surfaces.find((s) => s.title === want) ?? surfaces[0];
2865
+ if (!pane)
2866
+ return { status: "no-session" };
2867
+ const screen = await cmux2.readPaneScreen(pane);
2868
+ if (screen == null)
2869
+ return { status: "unreadable" };
2870
+ const outputAgoMs = opts.noteScreen?.(project, screenHash(screen));
2871
+ return classifyCaptainPane(screen, { outputAgoMs, idleAfterMs });
2872
+ } catch (e) {
2873
+ opts.log?.(`captain pane read failed project=${project}: ${e.message}`);
2874
+ return { status: "unreadable" };
2875
+ }
2876
+ };
2877
+ }
2777
2878
  function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
2778
2879
  return async (rec) => {
2779
2880
  try {
@@ -2796,11 +2897,14 @@ function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
2796
2897
  }
2797
2898
  };
2798
2899
  }
2799
- var TAIL_LINES;
2900
+ var TAIL_LINES, HR_RE, WORKING_RE, PANE_IDLE_AFTER_MS;
2800
2901
  var init_crew_pane_reader = __esm({
2801
2902
  "packages/core/dist/crew-pane-reader.js"() {
2802
2903
  init_dist();
2803
2904
  TAIL_LINES = 25;
2905
+ HR_RE = /^\s*─{10,}\s*$/;
2906
+ 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;
2907
+ PANE_IDLE_AFTER_MS = 10 * 6e4;
2804
2908
  }
2805
2909
  });
2806
2910
 
@@ -3498,8 +3602,11 @@ function buildRouterEnv(creds, model) {
3498
3602
  if (headers)
3499
3603
  env.ANTHROPIC_CUSTOM_HEADERS = headers;
3500
3604
  }
3501
- if (model)
3605
+ if (model) {
3502
3606
  env.ANTHROPIC_MODEL = model;
3607
+ env.CLAUDE_CODE_SUBAGENT_MODEL = model;
3608
+ env.ANTHROPIC_SMALL_FAST_MODEL = model;
3609
+ }
3503
3610
  return env;
3504
3611
  }
3505
3612
  function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
@@ -4665,8 +4772,9 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4665
4772
  if (mode !== "off")
4666
4773
  throw new DeferDelivery(null, "no-channel");
4667
4774
  }
4775
+ const paneOpts = { ...sendOpts, agent };
4668
4776
  try {
4669
- return await cmux2.send(surface, text, sendOpts);
4777
+ return await cmux2.send(surface, text, paneOpts);
4670
4778
  } catch (e) {
4671
4779
  if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
4672
4780
  throw e;
@@ -4678,7 +4786,7 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4678
4786
  }
4679
4787
  log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
4680
4788
  surface = next;
4681
- return cmux2.send(next, text, sendOpts);
4789
+ return cmux2.send(next, text, paneOpts);
4682
4790
  }
4683
4791
  });
4684
4792
  if ("delivered" in result) {
@@ -5609,16 +5717,26 @@ function createRunCommand(cliBin) {
5609
5717
  }
5610
5718
  };
5611
5719
  }
5720
+ function captainAliveStateFromHealth(rows, project) {
5721
+ const row = rows.find((h) => h.kind === "captain" && h.project === project);
5722
+ if (!row)
5723
+ return "unknown";
5724
+ if (row.state === "alive")
5725
+ return "alive";
5726
+ if (row.state === "gone" || row.state === "stopped")
5727
+ return "dead";
5728
+ return "unknown";
5729
+ }
5612
5730
  function isCaptainAliveFromHealth(rows, project) {
5613
- return rows.some((h) => h.kind === "captain" && h.project === project && h.state === "alive");
5731
+ return captainAliveStateFromHealth(rows, project) === "alive";
5614
5732
  }
5615
5733
  function createIsCaptainAlive(sock) {
5616
5734
  return async (project) => {
5617
5735
  try {
5618
5736
  const health = await sendRequest(sock, { kind: "health", project }, 5e3);
5619
- return isCaptainAliveFromHealth(health ?? [], project);
5737
+ return captainAliveStateFromHealth(health ?? [], project);
5620
5738
  } catch {
5621
- return false;
5739
+ return "unknown";
5622
5740
  }
5623
5741
  };
5624
5742
  }
@@ -5655,16 +5773,19 @@ function createEnsureCaptainAlive(deps) {
5655
5773
  const now = deps.now ?? (() => Date.now());
5656
5774
  const inFlight = /* @__PURE__ */ new Map();
5657
5775
  async function run(project) {
5658
- if (await deps.isAlive(project))
5776
+ const first = await deps.isAlive(project);
5777
+ if (first === "alive")
5659
5778
  return "alive";
5660
5779
  await deps.launch(project);
5661
5780
  const deadline = now() + warmupTimeoutMs;
5781
+ let last = first;
5662
5782
  while (now() < deadline) {
5663
- if (await deps.isAlive(project))
5783
+ last = await deps.isAlive(project);
5784
+ if (last === "alive")
5664
5785
  return "launched";
5665
5786
  await sleep3(pollMs);
5666
5787
  }
5667
- return "timeout";
5788
+ return last === "dead" ? "timeout" : "unknown";
5668
5789
  }
5669
5790
  return function ensure(project) {
5670
5791
  const existing = inFlight.get(project);
@@ -5719,6 +5840,26 @@ ${ev.question}`;
5719
5840
  function formatInbound(text) {
5720
5841
  return `\u{1F4E9} [from Telegram] ${text}`;
5721
5842
  }
5843
+ function mediaKind(m) {
5844
+ for (const [field, name] of MEDIA_KINDS) {
5845
+ if (m[field] !== void 0)
5846
+ return name;
5847
+ }
5848
+ return void 0;
5849
+ }
5850
+ function mediaMarker(kind) {
5851
+ return `[${kind} attached - not forwarded]`;
5852
+ }
5853
+ function inboundBody(text, kind) {
5854
+ if (kind === void 0)
5855
+ return text ?? "";
5856
+ const marker = mediaMarker(kind);
5857
+ return text ? `${text}
5858
+ ${marker}` : marker;
5859
+ }
5860
+ function formatMediaReceipt(kind, hasCaption) {
5861
+ 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`;
5862
+ }
5722
5863
  function formatUsageLine(u) {
5723
5864
  const reqs = u.requests === 1 ? "1 req" : `${u.requests} reqs`;
5724
5865
  const named = Object.keys(u.models).filter((m) => m !== "unknown").sort((a, b) => u.models[b].costUsd - u.models[a].costUsd);
@@ -5730,8 +5871,19 @@ function maskToken(token) {
5730
5871
  return token;
5731
5872
  return "*".repeat(token.length - 4) + token.slice(-4);
5732
5873
  }
5874
+ var MEDIA_KINDS;
5733
5875
  var init_format = __esm({
5734
5876
  "packages/core/dist/telegram/format.js"() {
5877
+ MEDIA_KINDS = [
5878
+ ["photo", "photo"],
5879
+ ["voice", "voice message"],
5880
+ ["video_note", "video message"],
5881
+ ["video", "video"],
5882
+ ["audio", "audio file"],
5883
+ ["document", "document"],
5884
+ ["animation", "animation"],
5885
+ ["sticker", "sticker"]
5886
+ ];
5735
5887
  }
5736
5888
  });
5737
5889
 
@@ -5754,6 +5906,8 @@ function loadState(stateRoot) {
5754
5906
  };
5755
5907
  if (typeof data.lastUserId === "number")
5756
5908
  result.lastUserId = data.lastUserId;
5909
+ if (data.pending && Object.keys(data.pending).length > 0)
5910
+ result.pending = data.pending;
5757
5911
  return result;
5758
5912
  } catch {
5759
5913
  return { offset: 0, topics: {}, notify: {} };
@@ -5767,6 +5921,19 @@ function setTopic(stateRoot, project, topicId, scope = "project") {
5767
5921
  s.topics[topicKey(project, scope)] = topicId;
5768
5922
  saveState(stateRoot, s);
5769
5923
  }
5924
+ function pruneTopics(stateRoot, keep) {
5925
+ const s = loadState(stateRoot);
5926
+ const removed = Object.keys(s.topics).filter((key) => {
5927
+ const sep3 = key.indexOf("::");
5928
+ return !keep(sep3 === -1 ? key : key.slice(0, sep3));
5929
+ });
5930
+ if (removed.length === 0)
5931
+ return [];
5932
+ for (const key of removed)
5933
+ delete s.topics[key];
5934
+ saveState(stateRoot, s);
5935
+ return removed;
5936
+ }
5770
5937
  function isNotifyActive(stateRoot, project) {
5771
5938
  return loadState(stateRoot).notify[project] === true;
5772
5939
  }
@@ -5780,6 +5947,42 @@ function setNotify(stateRoot, project, active) {
5780
5947
  s.notify[project] = active;
5781
5948
  saveState(stateRoot, s);
5782
5949
  }
5950
+ function loadPending(stateRoot) {
5951
+ return loadState(stateRoot).pending ?? {};
5952
+ }
5953
+ function setPending(stateRoot, project, p) {
5954
+ const s = loadState(stateRoot);
5955
+ s.pending = { ...s.pending ?? {}, [project]: p };
5956
+ saveState(stateRoot, s);
5957
+ }
5958
+ function clearPending(stateRoot, project) {
5959
+ const s = loadState(stateRoot);
5960
+ if (!s.pending || !(project in s.pending))
5961
+ return;
5962
+ delete s.pending[project];
5963
+ if (Object.keys(s.pending).length === 0)
5964
+ delete s.pending;
5965
+ saveState(stateRoot, s);
5966
+ }
5967
+ function markPendingWarned(stateRoot, project, at) {
5968
+ const s = loadState(stateRoot);
5969
+ const p = s.pending?.[project];
5970
+ if (!p)
5971
+ return;
5972
+ s.pending[project] = { ...p, warnedAt: at };
5973
+ saveState(stateRoot, s);
5974
+ }
5975
+ function notePaneScreen(stateRoot, project, hash, at) {
5976
+ const s = loadState(stateRoot);
5977
+ const p = s.pending?.[project];
5978
+ if (!p)
5979
+ return void 0;
5980
+ if (p.paneHash === hash && p.paneChangedAt !== void 0)
5981
+ return at - p.paneChangedAt;
5982
+ s.pending[project] = { ...p, paneHash: hash, paneChangedAt: at };
5983
+ saveState(stateRoot, s);
5984
+ return void 0;
5985
+ }
5783
5986
  function findProjectByThread(stateRoot, threadId) {
5784
5987
  const s = loadState(stateRoot);
5785
5988
  for (const [key, id] of Object.entries(s.topics)) {
@@ -5802,17 +6005,18 @@ var init_state = __esm({
5802
6005
  function createTelegramClient(opts) {
5803
6006
  const fetchImpl = opts.fetch ?? fetch;
5804
6007
  const base = `https://api.telegram.org/bot${opts.token}`;
5805
- async function call(method, body) {
6008
+ async function call(method, body, signal) {
5806
6009
  const res = await fetchImpl(`${base}/${method}`, {
5807
6010
  method: "POST",
5808
6011
  headers: { "content-type": "application/json" },
5809
- body: JSON.stringify(body)
6012
+ body: JSON.stringify(body),
6013
+ ...signal ? { signal } : {}
5810
6014
  });
5811
6015
  const json = await res.json();
5812
6016
  if (!res.ok || !json.ok) {
5813
6017
  const code = json.error_code ?? res.status;
5814
6018
  const desc = json.description ?? "unknown error";
5815
- throw new Error(`telegram ${method} failed (${code}): ${desc}`);
6019
+ throw new TelegramApiError(code, `telegram ${method} failed (${code}): ${desc}`, json.parameters?.retry_after);
5816
6020
  }
5817
6021
  return json.result;
5818
6022
  }
@@ -5821,8 +6025,8 @@ function createTelegramClient(opts) {
5821
6025
  const r = await call("getMe", {});
5822
6026
  return { id: r.id, username: r.username };
5823
6027
  },
5824
- getUpdates(offset, timeoutSec = 50) {
5825
- return call("getUpdates", { offset, timeout: timeoutSec });
6028
+ getUpdates(offset, timeoutSec = 50, signal) {
6029
+ return call("getUpdates", { offset, timeout: timeoutSec }, signal);
5826
6030
  },
5827
6031
  async sendMessage(chatId, threadId, text, replyMarkup) {
5828
6032
  const body = { chat_id: chatId, text };
@@ -5845,6 +6049,9 @@ function createTelegramClient(opts) {
5845
6049
  const r = await call("createForumTopic", { chat_id: chatId, name });
5846
6050
  return r.message_thread_id;
5847
6051
  },
6052
+ async deleteForumTopic(chatId, threadId) {
6053
+ await call("deleteForumTopic", { chat_id: chatId, message_thread_id: threadId });
6054
+ },
5848
6055
  async setMyCommands(commands) {
5849
6056
  await call("setMyCommands", { commands });
5850
6057
  },
@@ -5853,11 +6060,241 @@ function createTelegramClient(opts) {
5853
6060
  if (threadId !== void 0)
5854
6061
  body.message_thread_id = threadId;
5855
6062
  await call("sendChatAction", body);
6063
+ },
6064
+ async setMessageReaction(chatId, messageId, emoji) {
6065
+ await call("setMessageReaction", {
6066
+ chat_id: chatId,
6067
+ message_id: messageId,
6068
+ reaction: [{ type: "emoji", emoji }]
6069
+ });
5856
6070
  }
5857
6071
  };
5858
6072
  }
6073
+ var TelegramApiError;
5859
6074
  var init_client = __esm({
5860
6075
  "packages/core/dist/telegram/client.js"() {
6076
+ TelegramApiError = class extends Error {
6077
+ code;
6078
+ retryAfterSec;
6079
+ constructor(code, message, retryAfterSec) {
6080
+ super(message);
6081
+ this.code = code;
6082
+ this.retryAfterSec = retryAfterSec;
6083
+ this.name = "TelegramApiError";
6084
+ }
6085
+ };
6086
+ }
6087
+ });
6088
+
6089
+ // packages/core/dist/telegram/inbound-lifecycle.js
6090
+ function defaultSchedule(fn, ms) {
6091
+ const t = setTimeout(fn, ms);
6092
+ t.unref?.();
6093
+ return () => clearTimeout(t);
6094
+ }
6095
+ function formatWatchdogWarning(project, read, minutes) {
6096
+ const head = `\u23F1\uFE0F [${project}] no reply for ${minutes} minutes`;
6097
+ switch (read.status) {
6098
+ case "locked":
6099
+ 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.`;
6100
+ case "mid-turn":
6101
+ return `${head} \u2014 the captain is still working (a turn is in flight), just slow. No action needed yet.`;
6102
+ case "no-session":
6103
+ return `${head} \u2014 the captain looks GONE (no live session in its pane). Run \`squadrant launch ${project}\`.`;
6104
+ case "unreadable":
6105
+ return `${head} \u2014 couldn't read the captain's pane to classify it. Check it manually.`;
6106
+ }
6107
+ }
6108
+ function createInboundLifecycle(opts) {
6109
+ const { stateRoot, cfg, sendChatAction, sendReply, readPane, log } = opts;
6110
+ const now = opts.now ?? Date.now;
6111
+ const schedule = opts.schedule ?? defaultSchedule;
6112
+ const typingMs = opts.typingMs ?? TYPING_KEEPALIVE_MS;
6113
+ const watchdogMs = opts.watchdogMs ?? WATCHDOG_MS;
6114
+ const warnAtMs = watchdogMs + (opts.watchdogMs === void 0 ? WATCHDOG_GRACE_MS : 0);
6115
+ const active = /* @__PURE__ */ new Set();
6116
+ let running = false;
6117
+ function issue(project, threadId) {
6118
+ void Promise.resolve(sendChatAction(cfg.supergroupId, threadId, "typing")).catch((e) => log(`telegram typing keep-alive failed project=${project}: ${e.message}`));
6119
+ }
6120
+ function drain(project) {
6121
+ active.add(project);
6122
+ const tick = () => {
6123
+ if (!running || !active.has(project))
6124
+ return;
6125
+ const p = loadPending(stateRoot)[project];
6126
+ if (!p) {
6127
+ active.delete(project);
6128
+ return;
6129
+ }
6130
+ if (now() - p.startedAt >= warnAtMs && p.warnedAt === void 0) {
6131
+ void warn(project, p.threadId);
6132
+ }
6133
+ issue(project, p.threadId);
6134
+ schedule(tick, typingMs);
6135
+ };
6136
+ schedule(tick, typingMs);
6137
+ }
6138
+ async function warn(project, threadId) {
6139
+ markPendingWarned(stateRoot, project, now());
6140
+ let read = { status: "unreadable" };
6141
+ try {
6142
+ if (readPane)
6143
+ read = await readPane(project);
6144
+ } catch (e) {
6145
+ log(`telegram watchdog pane read failed project=${project}: ${e.message}`);
6146
+ }
6147
+ try {
6148
+ await sendReply(threadId, formatWatchdogWarning(project, read, Math.floor(watchdogMs / 6e4)));
6149
+ } catch (e) {
6150
+ log(`telegram watchdog warning failed project=${project}: ${e.message}`);
6151
+ }
6152
+ }
6153
+ return {
6154
+ start() {
6155
+ running = true;
6156
+ for (const [project, p] of Object.entries(loadPending(stateRoot))) {
6157
+ void (async () => {
6158
+ let read = { status: "unreadable" };
6159
+ try {
6160
+ if (readPane)
6161
+ read = await readPane(project);
6162
+ } catch (e) {
6163
+ log(`telegram resume pane read failed project=${project}: ${e.message}`);
6164
+ }
6165
+ const still = loadPending(stateRoot)[project];
6166
+ if (!still || still.startedAt !== p.startedAt)
6167
+ return;
6168
+ if (read.status === "no-session") {
6169
+ clearPending(stateRoot, project);
6170
+ log(`telegram typing lifecycle cleared for ${project}: captain session gone on boot`);
6171
+ return;
6172
+ }
6173
+ log(`telegram typing lifecycle resumed for ${project} (thread ${p.threadId})`);
6174
+ issue(project, p.threadId);
6175
+ drain(project);
6176
+ })();
6177
+ }
6178
+ },
6179
+ stop() {
6180
+ running = false;
6181
+ active.clear();
6182
+ },
6183
+ begin(project, threadId) {
6184
+ setPending(stateRoot, project, { threadId, startedAt: now() });
6185
+ issue(project, threadId);
6186
+ if (running && !active.has(project))
6187
+ drain(project);
6188
+ },
6189
+ pending() {
6190
+ return loadPending(stateRoot);
6191
+ }
6192
+ };
6193
+ }
6194
+ var TYPING_KEEPALIVE_MS, WATCHDOG_MS, WATCHDOG_GRACE_MS;
6195
+ var init_inbound_lifecycle = __esm({
6196
+ "packages/core/dist/telegram/inbound-lifecycle.js"() {
6197
+ init_state();
6198
+ TYPING_KEEPALIVE_MS = 4e3;
6199
+ WATCHDOG_MS = 15 * 6e4;
6200
+ WATCHDOG_GRACE_MS = 1;
6201
+ }
6202
+ });
6203
+
6204
+ // packages/core/dist/telegram/notify.js
6205
+ function runTelegramStatus(opts) {
6206
+ const tg = opts.config.telegram;
6207
+ const env = opts.env ?? process.env;
6208
+ const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
6209
+ const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
6210
+ const sep3 = key.indexOf("::");
6211
+ return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
6212
+ });
6213
+ return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
6214
+ }
6215
+ function runTelegramNotifySet(opts) {
6216
+ setNotify(opts.stateRoot, opts.project, opts.active);
6217
+ }
6218
+ function runTelegramNotifyPref(args) {
6219
+ const { project, dimension, value, root } = args;
6220
+ if (dimension === "crew") {
6221
+ if (!["all", "alert_only", "done_only", "none"].includes(value))
6222
+ return { ok: false, message: "crew must be all|alert_only|done_only|none" };
6223
+ saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
6224
+ return { ok: true };
6225
+ }
6226
+ if (value !== "on" && value !== "off")
6227
+ return { ok: false, message: "cap must be on|off" };
6228
+ saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
6229
+ return { ok: true };
6230
+ }
6231
+ function runTelegramNotifyStatus(opts) {
6232
+ const s = loadState(opts.stateRoot);
6233
+ const projects = /* @__PURE__ */ new Set();
6234
+ for (const key of Object.keys(s.topics)) {
6235
+ const sep3 = key.indexOf("::");
6236
+ projects.add(sep3 === -1 ? key : key.slice(0, sep3));
6237
+ }
6238
+ for (const p of Object.keys(s.notify))
6239
+ projects.add(p);
6240
+ return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
6241
+ }
6242
+ function capAllowed(project, globalNotify, root) {
6243
+ return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
6244
+ }
6245
+ function confirmationText(project, before, after, dim) {
6246
+ if (dim === "active")
6247
+ return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
6248
+ if (dim === "cap")
6249
+ return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
6250
+ return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
6251
+ }
6252
+ async function runNotifyConfirmation(opts) {
6253
+ const { quieter, dim } = isQuieter(opts.before, opts.after);
6254
+ if (!quieter || dim === null)
6255
+ return false;
6256
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6257
+ if (topicId === void 0)
6258
+ return false;
6259
+ const text = confirmationText(opts.project, opts.before, opts.after, dim);
6260
+ try {
6261
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
6262
+ return true;
6263
+ } catch {
6264
+ console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
6265
+ return false;
6266
+ }
6267
+ }
6268
+ async function runTelegramSend(opts) {
6269
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6270
+ if (topicId === void 0) {
6271
+ throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
6272
+ }
6273
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
6274
+ clearPending(opts.stateRoot, opts.project);
6275
+ return { chatId: opts.cfg.supergroupId, topicId };
6276
+ }
6277
+ async function runTelegramLink(opts) {
6278
+ const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6279
+ if (existing !== void 0)
6280
+ return { topicId: existing, created: false };
6281
+ const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
6282
+ const winner = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6283
+ if (winner !== void 0) {
6284
+ try {
6285
+ await opts.client.deleteForumTopic?.(opts.cfg.supergroupId, topicId);
6286
+ } catch {
6287
+ }
6288
+ return { topicId: winner, created: false };
6289
+ }
6290
+ setTopic(opts.stateRoot, opts.project, topicId);
6291
+ return { topicId, created: true };
6292
+ }
6293
+ var init_notify = __esm({
6294
+ "packages/core/dist/telegram/notify.js"() {
6295
+ init_dist();
6296
+ init_state();
6297
+ init_format();
5861
6298
  }
5862
6299
  });
5863
6300
 
@@ -5945,8 +6382,10 @@ var init_tiers = __esm({
5945
6382
  });
5946
6383
 
5947
6384
  // packages/core/dist/telegram/bridge.js
6385
+ import fs11 from "fs";
5948
6386
  import os5 from "os";
5949
6387
  import path12 from "path";
6388
+ import crypto2 from "crypto";
5950
6389
  function formatInboundReceipt(project, outcome) {
5951
6390
  if (!outcome)
5952
6391
  return void 0;
@@ -5962,6 +6401,85 @@ function formatInboundReceipt(project, outcome) {
5962
6401
  return void 0;
5963
6402
  }
5964
6403
  }
6404
+ function telegramPollLockPath(token) {
6405
+ const hash = crypto2.createHash("sha256").update(token).digest("hex").slice(0, 16);
6406
+ return path12.join(os5.tmpdir(), `squadrant-telegram-poll-${hash}.lock`);
6407
+ }
6408
+ function pidAlive(pid) {
6409
+ try {
6410
+ process.kill(pid, 0);
6411
+ return true;
6412
+ } catch (e) {
6413
+ return e.code === "EPERM";
6414
+ }
6415
+ }
6416
+ function lockIsHeld(file) {
6417
+ let pid;
6418
+ let ageMs;
6419
+ try {
6420
+ const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
6421
+ pid = Number.isFinite(parsed) ? parsed : void 0;
6422
+ ageMs = Date.now() - fs11.statSync(file).mtimeMs;
6423
+ } catch {
6424
+ return false;
6425
+ }
6426
+ if (pid !== void 0)
6427
+ return pidAlive(pid);
6428
+ return ageMs < POLL_LOCK_STALE_MS;
6429
+ }
6430
+ function acquirePollLock(token) {
6431
+ const file = telegramPollLockPath(token);
6432
+ for (let attempt = 0; attempt < 2; attempt++) {
6433
+ try {
6434
+ const fd = fs11.openSync(file, "wx");
6435
+ fs11.writeSync(fd, String(process.pid));
6436
+ fs11.closeSync(fd);
6437
+ return () => {
6438
+ try {
6439
+ fs11.unlinkSync(file);
6440
+ } catch {
6441
+ }
6442
+ };
6443
+ } catch (e) {
6444
+ if (e.code !== "EEXIST")
6445
+ return null;
6446
+ if (attempt === 0 && !lockIsHeld(file)) {
6447
+ try {
6448
+ fs11.unlinkSync(file);
6449
+ } catch {
6450
+ }
6451
+ continue;
6452
+ }
6453
+ return null;
6454
+ }
6455
+ }
6456
+ return null;
6457
+ }
6458
+ function defaultSchedule2(fn, ms) {
6459
+ const t = setTimeout(fn, ms);
6460
+ t.unref?.();
6461
+ return () => clearTimeout(t);
6462
+ }
6463
+ function foreignPollLockHolder(token, selfPid = process.pid) {
6464
+ const file = telegramPollLockPath(token);
6465
+ let pid;
6466
+ try {
6467
+ const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
6468
+ if (!Number.isFinite(parsed))
6469
+ return null;
6470
+ pid = parsed;
6471
+ } catch {
6472
+ return null;
6473
+ }
6474
+ if (pid === selfPid)
6475
+ return null;
6476
+ return pidAlive(pid) ? pid : null;
6477
+ }
6478
+ function pollBackoffMs(err, pollMs) {
6479
+ if (err instanceof TelegramApiError && err.retryAfterSec !== void 0)
6480
+ return err.retryAfterSec * 1e3;
6481
+ return pollMs;
6482
+ }
5965
6483
  function parseNotifyPref(text) {
5966
6484
  const parts = text.trim().split(/\s+/);
5967
6485
  if (stripBotMention(parts[0] ?? "").toLowerCase() !== "/notify")
@@ -5988,9 +6506,17 @@ function notifyToggle(text) {
5988
6506
  }
5989
6507
  function createTelegramBridge(opts) {
5990
6508
  const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
6509
+ const lifecycle = opts.lifecycle;
5991
6510
  const configRoot = opts.configRoot ?? path12.join(os5.homedir(), ".config", "squadrant");
5992
6511
  const pollMs = cfg.pollMs ?? 1e3;
6512
+ const schedule = opts.schedule ?? defaultSchedule2;
6513
+ const lockToken = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
6514
+ let releasePollLock = null;
6515
+ let pollAbort = null;
5993
6516
  let running = false;
6517
+ let pollStopped = false;
6518
+ let selfHealCancel = null;
6519
+ let conflictCount = 0;
5994
6520
  let lastSuccessfulPollAt = null;
5995
6521
  let lastError = null;
5996
6522
  let lastErrorAt = null;
@@ -6000,12 +6526,8 @@ function createTelegramBridge(opts) {
6000
6526
  saveState(stateRoot, s);
6001
6527
  }
6002
6528
  async function sendToTopic(project, text) {
6003
- let threadId = loadState(stateRoot).topics[topicKey(project)];
6004
- if (threadId === void 0) {
6005
- threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
6006
- setTopic(stateRoot, project, threadId);
6007
- }
6008
- await client.sendMessage(cfg.supergroupId, threadId, text);
6529
+ const { topicId } = await runTelegramLink({ project, cfg, client, stateRoot });
6530
+ await client.sendMessage(cfg.supergroupId, topicId, text);
6009
6531
  }
6010
6532
  async function deliverOutbound(project, ev) {
6011
6533
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
@@ -6192,10 +6714,13 @@ ${formatUsageLine(usage2)}`;
6192
6714
  }
6193
6715
  await runChannelCommand(text, fromId, void 0);
6194
6716
  }
6195
- async function handleProjectTopic(text, threadId, fromId) {
6717
+ async function handleProjectTopic(text, threadId, fromId, messageId, media) {
6196
6718
  const resolved = findProjectByThread(stateRoot, threadId);
6197
- if (!resolved)
6719
+ if (!resolved) {
6720
+ log(`telegram inbound in unbound topic thread=${threadId} \u2014 no project linked, message dropped`);
6721
+ await reply(threadId, "\u26A0\uFE0F this topic isn't linked to a project \u2014 run: squadrant telegram link <project>");
6198
6722
  return;
6723
+ }
6199
6724
  if (isBareSpawn(text)) {
6200
6725
  if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
6201
6726
  await reply(threadId, "\u26D4 not authorized");
@@ -6245,21 +6770,16 @@ ${formatUsageLine(usage2)}`;
6245
6770
  await runChannelCommand(text, fromId, threadId);
6246
6771
  return;
6247
6772
  }
6248
- void client.sendChatAction(cfg.supergroupId, threadId, "typing").catch((e) => {
6249
- log(`telegram sendChatAction failed: ${e.message}`);
6250
- });
6773
+ if (messageId !== void 0) {
6774
+ void Promise.resolve(client.setMessageReaction?.(cfg.supergroupId, messageId, "\u{1F44D}")).catch((e) => log(`telegram stage-1 reaction failed project=${resolved.project}: ${e.message}`));
6775
+ }
6251
6776
  setNotify(stateRoot, resolved.project, true);
6252
6777
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
6253
- try {
6254
- const r = await ensureCaptainAlive(resolved.project);
6778
+ void ensureCaptainAlive(resolved.project).then((r) => {
6255
6779
  if (r === "timeout") {
6256
- await reply(threadId, `\u274C couldn't reach ${resolved.project} captain \u2014 saved to mailbox, will deliver when you open the workspace.`);
6257
- } else {
6258
- await reply(threadId, `\u{1F4E8} delivered to ${resolved.project} captain`);
6780
+ log(`telegram auto-launch: warmup timed out project=${resolved.project} \u2014 delivery verdict (not this probe) decides the receipt`);
6259
6781
  }
6260
- } catch (e) {
6261
- log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
6262
- }
6782
+ }).catch((e) => log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`));
6263
6783
  }
6264
6784
  let handled = false;
6265
6785
  let outcome;
@@ -6268,13 +6788,22 @@ ${formatUsageLine(usage2)}`;
6268
6788
  handled = res.handled;
6269
6789
  outcome = res.outcome;
6270
6790
  }
6791
+ const deliveryHeld = outcome?.status === "held";
6271
6792
  if (!handled) {
6272
6793
  await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
6794
+ outcome = void 0;
6273
6795
  }
6274
6796
  const receipt = formatInboundReceipt(resolved.project, outcome);
6275
6797
  if (receipt && sendReply) {
6276
6798
  await sendReply(threadId, receipt).catch((e) => log(`telegram receipt failed project=${resolved.project}: ${e.message}`));
6277
6799
  }
6800
+ if (media) {
6801
+ await reply(threadId, formatMediaReceipt(media.kind, media.hasCaption));
6802
+ }
6803
+ if (deliveryHeld)
6804
+ return;
6805
+ await reply(threadId, CAPTAIN_RECEIVED_ACK);
6806
+ lifecycle?.begin(resolved.project, threadId);
6278
6807
  }
6279
6808
  async function handleUpdate(u) {
6280
6809
  if (u.callback_query) {
@@ -6282,7 +6811,11 @@ ${formatUsageLine(usage2)}`;
6282
6811
  return;
6283
6812
  }
6284
6813
  const m = u.message;
6285
- if (!m || m.text === void 0)
6814
+ if (!m)
6815
+ return;
6816
+ const kind = mediaKind(m);
6817
+ const text = m.text ?? m.caption;
6818
+ if (text === void 0 && kind === void 0)
6286
6819
  return;
6287
6820
  if (!cfg.chats.includes(m.chat.id))
6288
6821
  return;
@@ -6296,7 +6829,7 @@ ${formatUsageLine(usage2)}`;
6296
6829
  await reply(threadId, "\u26D4 not authorized");
6297
6830
  return;
6298
6831
  }
6299
- const task = m.text.trim();
6832
+ const task = (text ?? "").trim();
6300
6833
  if (!task) {
6301
6834
  await reply(threadId, "spawn cancelled \u2014 empty task");
6302
6835
  return;
@@ -6307,39 +6840,122 @@ ${formatUsageLine(usage2)}`;
6307
6840
  return;
6308
6841
  }
6309
6842
  if (m.message_thread_id === void 0) {
6843
+ if (m.text === void 0)
6844
+ return;
6310
6845
  await handleGeneral(m.text, m.from?.id);
6311
6846
  return;
6312
6847
  }
6313
- await handleProjectTopic(m.text, m.message_thread_id, m.from?.id);
6848
+ 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 });
6849
+ }
6850
+ function acquirePollLockOrLog() {
6851
+ if (!lockToken)
6852
+ return true;
6853
+ releasePollLock = acquirePollLock(lockToken);
6854
+ if (releasePollLock)
6855
+ return true;
6856
+ const holder = foreignPollLockHolder(lockToken);
6857
+ log(`telegram inbound poll disabled: another consumer already polls this bot token (single-consumer guard, #830)${holder !== null ? ` \u2014 lock held by pid ${holder}` : ""}`);
6858
+ return false;
6859
+ }
6860
+ function scheduleSelfHeal() {
6861
+ selfHealCancel?.();
6862
+ selfHealCancel = schedule(() => {
6863
+ selfHealCancel = null;
6864
+ if (pollStopped || running)
6865
+ return;
6866
+ if (!acquirePollLockOrLog()) {
6867
+ scheduleSelfHeal();
6868
+ return;
6869
+ }
6870
+ running = true;
6871
+ conflictCount = 0;
6872
+ log("telegram inbound poll re-attempting after a 409 stop (#850)");
6873
+ void pollLoop();
6874
+ }, CONFLICT_409_SELF_HEAL_MS);
6314
6875
  }
6315
6876
  async function pollLoop() {
6316
6877
  while (running) {
6878
+ const ac = new AbortController();
6879
+ pollAbort = ac;
6880
+ let waitMs = pollMs;
6317
6881
  try {
6318
6882
  const offset = loadState(stateRoot).offset;
6319
- const updates = await client.getUpdates(offset, LONG_POLL_SEC);
6883
+ const updates = await client.getUpdates(offset, LONG_POLL_SEC, ac.signal);
6884
+ conflictCount = 0;
6320
6885
  for (const u of updates) {
6321
6886
  await handleUpdate(u);
6322
6887
  persistOffset(u.update_id + 1);
6323
6888
  }
6324
6889
  lastSuccessfulPollAt = Date.now();
6325
6890
  } catch (e) {
6326
- lastError = e.message;
6891
+ if (ac.signal.aborted)
6892
+ break;
6893
+ const msg = e.message;
6894
+ lastError = msg;
6327
6895
  lastErrorAt = Date.now();
6328
- log(`telegram inbound poll failed: ${e.message}`);
6896
+ if (e instanceof TelegramApiError && e.code === 409) {
6897
+ conflictCount += 1;
6898
+ if (conflictCount >= CONFLICT_409_TERMINAL_AFTER) {
6899
+ log(`telegram inbound poll stopped: repeated 409 conflict (another getUpdates consumer holds this token) \u2014 ${msg}`);
6900
+ break;
6901
+ }
6902
+ } else {
6903
+ log(`telegram inbound poll failed: ${msg}`);
6904
+ }
6905
+ waitMs = pollBackoffMs(e, pollMs);
6906
+ } finally {
6907
+ pollAbort = null;
6329
6908
  }
6330
6909
  if (running)
6331
- await sleep2(pollMs);
6910
+ await sleep2(waitMs);
6332
6911
  }
6912
+ running = false;
6913
+ releasePollLock?.();
6914
+ releasePollLock = null;
6915
+ if (!pollStopped)
6916
+ scheduleSelfHeal();
6333
6917
  }
6334
6918
  return {
6335
6919
  start() {
6336
6920
  if (running)
6337
6921
  return;
6922
+ pollStopped = false;
6923
+ selfHealCancel?.();
6924
+ selfHealCancel = null;
6925
+ if (!acquirePollLockOrLog())
6926
+ return;
6927
+ const known = opts.registeredProjects?.();
6928
+ if (known !== void 0) {
6929
+ try {
6930
+ const removed = pruneTopics(stateRoot, (p) => known.includes(p));
6931
+ if (removed.length > 0)
6932
+ log(`telegram registry pruned ${removed.length} stale topic link(s): ${removed.join(", ")}`);
6933
+ } catch (e) {
6934
+ log(`telegram registry prune failed: ${e.message}`);
6935
+ }
6936
+ }
6338
6937
  running = true;
6938
+ conflictCount = 0;
6939
+ try {
6940
+ lifecycle?.start();
6941
+ } catch (e) {
6942
+ log(`telegram lifecycle start failed: ${e.message}`);
6943
+ }
6339
6944
  void pollLoop();
6340
6945
  },
6341
6946
  stop() {
6342
6947
  running = false;
6948
+ pollStopped = true;
6949
+ selfHealCancel?.();
6950
+ selfHealCancel = null;
6951
+ try {
6952
+ lifecycle?.stop();
6953
+ } catch (e) {
6954
+ log(`telegram lifecycle stop failed: ${e.message}`);
6955
+ }
6956
+ pollAbort?.abort();
6957
+ releasePollLock?.();
6958
+ releasePollLock = null;
6343
6959
  },
6344
6960
  pushLifecycle(project, ev) {
6345
6961
  void deliverOutbound(project, ev).catch((e) => {
@@ -6356,17 +6972,23 @@ ${formatUsageLine(usage2)}`;
6356
6972
  }
6357
6973
  };
6358
6974
  }
6359
- var LONG_POLL_SEC, sleep2, CREW_TIERS, RECOGNIZED_CHANNEL_COMMANDS;
6975
+ 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;
6360
6976
  var init_bridge = __esm({
6361
6977
  "packages/core/dist/telegram/bridge.js"() {
6362
6978
  init_dist();
6979
+ init_client();
6363
6980
  init_auth();
6364
6981
  init_commands();
6365
6982
  init_format();
6983
+ init_notify();
6366
6984
  init_panels();
6367
6985
  init_state();
6368
6986
  init_tiers();
6987
+ CAPTAIN_RECEIVED_ACK = "\u2705 captain received";
6369
6988
  LONG_POLL_SEC = 50;
6989
+ POLL_LOCK_STALE_MS = 2 * 6e4;
6990
+ CONFLICT_409_TERMINAL_AFTER = 3;
6991
+ CONFLICT_409_SELF_HEAL_MS = 5 * 6e4;
6370
6992
  sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
6371
6993
  CREW_TIERS = ["all", "alert_only", "done_only", "none"];
6372
6994
  RECOGNIZED_CHANNEL_COMMANDS = /* @__PURE__ */ new Set(["status", "projects", "crews", "launch", "effort", "spawn"]);
@@ -6411,7 +7033,7 @@ var init_restart_daemon = __esm({
6411
7033
  });
6412
7034
 
6413
7035
  // packages/core/dist/telegram/setup.js
6414
- import fs11 from "fs";
7036
+ import fs12 from "fs";
6415
7037
  function resolveSetupGroup(existingSupergroupId, opts) {
6416
7038
  if (existingSupergroupId !== void 0 && !opts.redetect)
6417
7039
  return "reuse";
@@ -6462,7 +7084,7 @@ function writeTelegramConfig(configPath, opts) {
6462
7084
  let config;
6463
7085
  let raw = null;
6464
7086
  try {
6465
- raw = fs11.readFileSync(configPath, "utf-8");
7087
+ raw = fs12.readFileSync(configPath, "utf-8");
6466
7088
  } catch (err) {
6467
7089
  if (err.code !== "ENOENT") {
6468
7090
  throw new Error(`refusing to overwrite unreadable config at ${configPath}: ${String(err)}`);
@@ -6501,94 +7123,6 @@ var init_setup = __esm({
6501
7123
  }
6502
7124
  });
6503
7125
 
6504
- // packages/core/dist/telegram/notify.js
6505
- function runTelegramStatus(opts) {
6506
- const tg = opts.config.telegram;
6507
- const env = opts.env ?? process.env;
6508
- const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
6509
- const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
6510
- const sep3 = key.indexOf("::");
6511
- return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
6512
- });
6513
- return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
6514
- }
6515
- function runTelegramNotifySet(opts) {
6516
- setNotify(opts.stateRoot, opts.project, opts.active);
6517
- }
6518
- function runTelegramNotifyPref(args) {
6519
- const { project, dimension, value, root } = args;
6520
- if (dimension === "crew") {
6521
- if (!["all", "alert_only", "done_only", "none"].includes(value))
6522
- return { ok: false, message: "crew must be all|alert_only|done_only|none" };
6523
- saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
6524
- return { ok: true };
6525
- }
6526
- if (value !== "on" && value !== "off")
6527
- return { ok: false, message: "cap must be on|off" };
6528
- saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
6529
- return { ok: true };
6530
- }
6531
- function runTelegramNotifyStatus(opts) {
6532
- const s = loadState(opts.stateRoot);
6533
- const projects = /* @__PURE__ */ new Set();
6534
- for (const key of Object.keys(s.topics)) {
6535
- const sep3 = key.indexOf("::");
6536
- projects.add(sep3 === -1 ? key : key.slice(0, sep3));
6537
- }
6538
- for (const p of Object.keys(s.notify))
6539
- projects.add(p);
6540
- return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
6541
- }
6542
- function capAllowed(project, globalNotify, root) {
6543
- return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
6544
- }
6545
- function confirmationText(project, before, after, dim) {
6546
- if (dim === "active")
6547
- return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
6548
- if (dim === "cap")
6549
- return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
6550
- return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
6551
- }
6552
- async function runNotifyConfirmation(opts) {
6553
- const { quieter, dim } = isQuieter(opts.before, opts.after);
6554
- if (!quieter || dim === null)
6555
- return false;
6556
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6557
- if (topicId === void 0)
6558
- return false;
6559
- const text = confirmationText(opts.project, opts.before, opts.after, dim);
6560
- try {
6561
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
6562
- return true;
6563
- } catch {
6564
- console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
6565
- return false;
6566
- }
6567
- }
6568
- async function runTelegramSend(opts) {
6569
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6570
- if (topicId === void 0) {
6571
- throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
6572
- }
6573
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
6574
- return { chatId: opts.cfg.supergroupId, topicId };
6575
- }
6576
- async function runTelegramLink(opts) {
6577
- const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6578
- if (existing !== void 0)
6579
- return { topicId: existing, created: false };
6580
- const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
6581
- setTopic(opts.stateRoot, opts.project, topicId);
6582
- return { topicId, created: true };
6583
- }
6584
- var init_notify = __esm({
6585
- "packages/core/dist/telegram/notify.js"() {
6586
- init_dist();
6587
- init_state();
6588
- init_format();
6589
- }
6590
- });
6591
-
6592
7126
  // packages/core/dist/telegram/index.js
6593
7127
  var init_telegram = __esm({
6594
7128
  "packages/core/dist/telegram/index.js"() {
@@ -6600,6 +7134,7 @@ var init_telegram = __esm({
6600
7134
  init_format();
6601
7135
  init_state();
6602
7136
  init_client();
7137
+ init_inbound_lifecycle();
6603
7138
  init_bridge();
6604
7139
  init_setup();
6605
7140
  init_notify();
@@ -7234,6 +7769,12 @@ function resolveGateMode(env, gate) {
7234
7769
  return fromEnv;
7235
7770
  return gate?.mode && isGateMode(gate.mode) ? gate.mode : "auto";
7236
7771
  }
7772
+ function resolveGateEngine(env, gate) {
7773
+ const fromEnv = env.SQUADRANT_GATE_ENGINE;
7774
+ if (fromEnv !== void 0 && isGateEngine(fromEnv))
7775
+ return fromEnv;
7776
+ return gate?.engine && isGateEngine(gate.engine) ? gate.engine : "router";
7777
+ }
7237
7778
  function resolveGatePolicy(env, gate) {
7238
7779
  const fromEnv = env.SQUADRANT_GATE_POLICY;
7239
7780
  if (fromEnv !== void 0 && isGatePolicy(fromEnv))
@@ -7618,6 +8159,59 @@ var init_permission_gate = __esm({
7618
8159
  }
7619
8160
  });
7620
8161
 
8162
+ // packages/core/dist/auto-gate.js
8163
+ import { createAutoGate, projectGateConfig } from "@squadrant-ai/auto-gate";
8164
+ function gateForProjection(config) {
8165
+ const gate = config.defaults.gate;
8166
+ if (!gate)
8167
+ return {};
8168
+ const out = {};
8169
+ if (gate.mode)
8170
+ out.mode = gate.mode;
8171
+ if (gate.tools)
8172
+ out.tools = gate.tools;
8173
+ if (gate.deny)
8174
+ out.deny = gate.deny;
8175
+ return out;
8176
+ }
8177
+ function createSquadrantAutoGate(deps) {
8178
+ const env = deps.env ?? process.env;
8179
+ const taskId = env.SQUADRANT_CREW_TASK_ID;
8180
+ const project = deps.project ?? env.SQUADRANT_CREW_PROJECT;
8181
+ const mode = resolveGateMode(env, deps.config.defaults.gate);
8182
+ const projected = projectGateConfig({ gate: { ...gateForProjection(deps.config), mode } });
8183
+ const enabled = mode === "on";
8184
+ const blockedSignal = async (ctx) => {
8185
+ if (!taskId || !project || !deps.sendBlocked)
8186
+ return;
8187
+ const event = {
8188
+ type: "task.blocked",
8189
+ id: taskId,
8190
+ reason: ctx.reason,
8191
+ question: `crew needs permission to run ${ctx.tool}`
8192
+ };
8193
+ await deps.sendBlocked(project, event);
8194
+ };
8195
+ const host = createAutoGate({
8196
+ config: projected,
8197
+ readIntent: (p) => readUserIntent(p) ?? void 0,
8198
+ blockedSignal,
8199
+ ...deps.decide ? { decide: deps.decide } : {},
8200
+ ...project ? { project } : {},
8201
+ env,
8202
+ log: deps.log
8203
+ });
8204
+ if (!enabled) {
8205
+ return { config: host.config, decideClaudeHookPayload: async () => void 0 };
8206
+ }
8207
+ return host;
8208
+ }
8209
+ var init_auto_gate = __esm({
8210
+ "packages/core/dist/auto-gate.js"() {
8211
+ init_permission_gate();
8212
+ }
8213
+ });
8214
+
7621
8215
  // packages/core/dist/group-dispatch.js
7622
8216
  import { randomUUID as randomUUID4 } from "crypto";
7623
8217
  import { createConnection as createConnection2 } from "net";
@@ -7865,7 +8459,7 @@ var init_launch_workspace = __esm({
7865
8459
  });
7866
8460
 
7867
8461
  // packages/core/dist/side-session.js
7868
- import fs12 from "fs";
8462
+ import fs13 from "fs";
7869
8463
  function sideTitleFor(project, name) {
7870
8464
  return `\u{1F5D2} ${project}:${name}`;
7871
8465
  }
@@ -7967,7 +8561,7 @@ async function runSideClose(runtime, workspaceId, project, name, projPath, workt
7967
8561
  await runtime.closePane(pane);
7968
8562
  if (projPath) {
7969
8563
  const wtPath = worktreePath(projPath, worktreeDir, project, name);
7970
- if (fs12.existsSync(wtPath)) {
8564
+ if (fs13.existsSync(wtPath)) {
7971
8565
  try {
7972
8566
  removeWorktree(projPath, wtPath);
7973
8567
  } catch (e) {
@@ -8024,10 +8618,11 @@ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps,
8024
8618
  if (!crew) {
8025
8619
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
8026
8620
  }
8027
- const options = await deps.readModalOptions(crew);
8028
- if (!options) {
8621
+ const modal = await deps.readModalOptions(crew);
8622
+ if (!modal || modal.options.length === 0) {
8029
8623
  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.`);
8030
8624
  }
8625
+ const options = modal.options;
8031
8626
  const target = resolveOption(options, option);
8032
8627
  if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
8033
8628
  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}'.
@@ -8039,7 +8634,9 @@ ${describeOptions(options)}`);
8039
8634
  log(`\u2192 selecting ${target.index}. "${target.label}"`);
8040
8635
  const current = options.find((o) => o.highlighted) ?? options[0];
8041
8636
  const steps = target.index - current.index;
8042
- const key = steps >= 0 ? "Down" : "Up";
8637
+ const forwardKey = modal.axis === "horizontal" ? "Right" : "Down";
8638
+ const backwardKey = modal.axis === "horizontal" ? "Left" : "Up";
8639
+ const key = steps >= 0 ? forwardKey : backwardKey;
8043
8640
  for (let i = 0; i < Math.abs(steps); i++) {
8044
8641
  await runtime.sendKeyToPane(crew, key);
8045
8642
  }
@@ -8404,11 +9001,14 @@ __export(dist_exports, {
8404
9001
  AGENT_BINS: () => AGENT_BINS,
8405
9002
  ANTHROPIC_ONLY_REQUEST_FIELDS: () => ANTHROPIC_ONLY_REQUEST_FIELDS,
8406
9003
  BOT_COMMANDS: () => BOT_COMMANDS,
9004
+ CAPTAIN_RECEIVED_ACK: () => CAPTAIN_RECEIVED_ACK,
8407
9005
  CC_SOCKS_DIR: () => CC_SOCKS_DIR,
8408
9006
  CMUX_PRESERVE_CLAUDE_AUTH_ENV: () => CMUX_PRESERVE_CLAUDE_AUTH_ENV,
9007
+ CONFLICT_409_SELF_HEAL_MS: () => CONFLICT_409_SELF_HEAL_MS,
8409
9008
  CREW_GONE_MS: () => CREW_GONE_MS,
8410
9009
  CREW_STALE_MS: () => CREW_STALE_MS,
8411
9010
  CaptainDelivery: () => CaptainDelivery,
9011
+ DEFAULT_BLOCKED_RENUDGE_MS: () => DEFAULT_BLOCKED_RENUDGE_MS,
8412
9012
  DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS: () => DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS,
8413
9013
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
8414
9014
  DEFAULT_GATE_DENY_RULES: () => DEFAULT_GATE_DENY_RULES,
@@ -8425,6 +9025,7 @@ __export(dist_exports, {
8425
9025
  LivenessRegistry: () => LivenessRegistry,
8426
9026
  MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
8427
9027
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
9028
+ PANE_IDLE_AFTER_MS: () => PANE_IDLE_AFTER_MS,
8428
9029
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
8429
9030
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
8430
9031
  READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
@@ -8437,6 +9038,10 @@ __export(dist_exports, {
8437
9038
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
8438
9039
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
8439
9040
  TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
9041
+ TYPING_KEEPALIVE_MS: () => TYPING_KEEPALIVE_MS,
9042
+ TelegramApiError: () => TelegramApiError,
9043
+ WATCHDOG_GRACE_MS: () => WATCHDOG_GRACE_MS,
9044
+ WATCHDOG_MS: () => WATCHDOG_MS,
8440
9045
  WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
8441
9046
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
8442
9047
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
@@ -8458,15 +9063,19 @@ __export(dist_exports, {
8458
9063
  callClassifier: () => callClassifier,
8459
9064
  capAllowed: () => capAllowed,
8460
9065
  capOutput: () => capOutput,
9066
+ captainAliveStateFromHealth: () => captainAliveStateFromHealth,
8461
9067
  captainRecordPath: () => captainRecordPath,
8462
9068
  captainSocketPath: () => captainSocketPath,
8463
9069
  checkFact: () => checkFact,
9070
+ classifyCaptainPane: () => classifyCaptainPane,
8464
9071
  classifyHealth: () => classifyHealth,
8465
9072
  claudeEnvShadowsRouter: () => claudeEnvShadowsRouter,
9073
+ clearPending: () => clearPending,
8466
9074
  closeWorkItem: () => closeWorkItem,
8467
9075
  computeTemplateHash: () => computeTemplateHash,
8468
9076
  consumeExitMarker: () => consumeExitMarker,
8469
9077
  createAttach: () => createAttach,
9078
+ createCaptainPaneReader: () => createCaptainPaneReader,
8470
9079
  createCrewPaneReader: () => createCrewPaneReader,
8471
9080
  createDaemon: () => createDaemon,
8472
9081
  createDecoder: () => createDecoder,
@@ -8475,12 +9084,14 @@ __export(dist_exports, {
8475
9084
  createDirectSurfaceLivenessProbe: () => createDirectSurfaceLivenessProbe,
8476
9085
  createEnsureCaptainAlive: () => createEnsureCaptainAlive,
8477
9086
  createEventsSource: () => createEventsSource,
9087
+ createInboundLifecycle: () => createInboundLifecycle,
8478
9088
  createInteractiveProbe: () => createInteractiveProbe,
8479
9089
  createIsCaptainAlive: () => createIsCaptainAlive,
8480
9090
  createLaunch: () => createLaunch,
8481
9091
  createRouterService: () => createRouterService,
8482
9092
  createRouterShim: () => createRouterShim,
8483
9093
  createRunCommand: () => createRunCommand,
9094
+ createSquadrantAutoGate: () => createSquadrantAutoGate,
8484
9095
  createStore: () => createStore,
8485
9096
  createSurfaceLivenessProbe: () => createSurfaceLivenessProbe,
8486
9097
  createTelegramBridge: () => createTelegramBridge,
@@ -8530,12 +9141,16 @@ __export(dist_exports, {
8530
9141
  formatInbound: () => formatInbound,
8531
9142
  formatInboundReceipt: () => formatInboundReceipt,
8532
9143
  formatLifecycle: () => formatLifecycle,
9144
+ formatMediaReceipt: () => formatMediaReceipt,
8533
9145
  formatPermissionDecision: () => formatPermissionDecision,
8534
9146
  formatUsageLine: () => formatUsageLine,
9147
+ formatWatchdogWarning: () => formatWatchdogWarning,
8535
9148
  freshTrace: () => freshTrace,
8536
9149
  gateCacheKey: () => gateCacheKey,
8537
9150
  getDaemonPid: () => getDaemonPid,
9151
+ hasModalOptionList: () => hasModalOptionList,
8538
9152
  healCmdFor: () => healCmdFor,
9153
+ inboundBody: () => inboundBody,
8539
9154
  isAuthorized: () => isAuthorized,
8540
9155
  isBareSpawn: () => isBareSpawn,
8541
9156
  isCaptainAlive: () => isCaptainAlive,
@@ -8557,18 +9172,23 @@ __export(dist_exports, {
8557
9172
  kickstartArgv: () => kickstartArgv,
8558
9173
  launchOneWorkspace: () => launchOneWorkspace,
8559
9174
  listSessions: () => listSessions,
9175
+ loadPending: () => loadPending,
8560
9176
  loadSessions: () => loadSessions,
8561
9177
  loadState: () => loadState,
8562
9178
  mailboxStats: () => mailboxStats,
8563
9179
  makeGate: () => makeGate,
9180
+ markPendingWarned: () => markPendingWarned,
8564
9181
  maskToken: () => maskToken,
8565
9182
  matchTier1Deny: () => matchTier1Deny,
9183
+ mediaKind: () => mediaKind,
9184
+ mediaMarker: () => mediaMarker,
8566
9185
  mergeClaudeEnvRouterSettings: () => mergeClaudeEnvRouterSettings,
8567
9186
  mintToken: () => mintToken,
8568
9187
  nameFromTitle: () => nameFromTitle,
8569
9188
  newestSessionInDirectory: () => newestSessionInDirectory,
8570
9189
  nextAutoName: () => nextAutoName,
8571
9190
  niceCrewCommand: () => niceCrewCommand,
9191
+ notePaneScreen: () => notePaneScreen,
8572
9192
  notifyToggle: () => notifyToggle,
8573
9193
  parseBearer: () => parseBearer,
8574
9194
  parseClassifierVerdict: () => parseClassifierVerdict,
@@ -8577,6 +9197,7 @@ __export(dist_exports, {
8577
9197
  parseNotifyPref: () => parseNotifyPref,
8578
9198
  parseProgramArgs: () => parseProgramArgs,
8579
9199
  plistPath: () => plistPath,
9200
+ pollBackoffMs: () => pollBackoffMs,
8580
9201
  pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
8581
9202
  prepareCaptainRoute: () => prepareCaptainRoute,
8582
9203
  printForeignInstallError: () => printForeignInstallError,
@@ -8584,6 +9205,7 @@ __export(dist_exports, {
8584
9205
  probeCaptainChannel: () => probeCaptainChannel,
8585
9206
  programArgsBlock: () => programArgsBlock,
8586
9207
  projectHealth: () => projectHealth,
9208
+ pruneTopics: () => pruneTopics,
8587
9209
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
8588
9210
  readCaptainAddress: () => readCaptainAddress,
8589
9211
  readCursor: () => readCursor,
@@ -8611,6 +9233,7 @@ __export(dist_exports, {
8611
9233
  resolveCurrentProject: () => resolveCurrentProject,
8612
9234
  resolveGate: () => resolveGate,
8613
9235
  resolveGateDenyRules: () => resolveGateDenyRules,
9236
+ resolveGateEngine: () => resolveGateEngine,
8614
9237
  resolveGateMode: () => resolveGateMode,
8615
9238
  resolveGatePolicy: () => resolveGatePolicy,
8616
9239
  resolveGateTools: () => resolveGateTools,
@@ -8650,9 +9273,11 @@ __export(dist_exports, {
8650
9273
  saveSessions: () => saveSessions,
8651
9274
  saveState: () => saveState,
8652
9275
  screenHasSplashMarker: () => screenHasSplashMarker,
9276
+ screenHash: () => screenHash,
8653
9277
  sendRequest: () => sendRequest,
8654
9278
  setLastUserId: () => setLastUserId,
8655
9279
  setNotify: () => setNotify,
9280
+ setPending: () => setPending,
8656
9281
  setTopic: () => setTopic,
8657
9282
  shellQuote: () => shellQuote,
8658
9283
  shouldBuildRouterService: () => shouldBuildRouterService,
@@ -8665,6 +9290,7 @@ __export(dist_exports, {
8665
9290
  startServer: () => startServer,
8666
9291
  stripBotMention: () => stripBotMention,
8667
9292
  surfaceVerdict: () => surfaceVerdict,
9293
+ telegramPollLockPath: () => telegramPollLockPath,
8668
9294
  timeoutGate: () => timeoutGate,
8669
9295
  titleFor: () => titleFor,
8670
9296
  toControlEvent: () => toControlEvent,
@@ -8713,6 +9339,7 @@ var init_dist2 = __esm({
8713
9339
  init_router_resolution();
8714
9340
  init_captain_router();
8715
9341
  init_permission_gate();
9342
+ init_auto_gate();
8716
9343
  init_restart_daemon();
8717
9344
  init_group_dispatch();
8718
9345
  init_launch_workspace();
@@ -8758,7 +9385,7 @@ import { readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs"
8758
9385
  import { join as join18 } from "path";
8759
9386
 
8760
9387
  // packages/agents/dist/claude/registry.js
8761
- import fs13 from "fs";
9388
+ import fs14 from "fs";
8762
9389
  import { join as join17 } from "path";
8763
9390
  import { homedir as homedir7 } from "os";
8764
9391
  var CLAUDE_SESSIONS_DIR = join17(homedir7(), ".claude", "sessions");
@@ -8803,11 +9430,11 @@ function toLifecycleSnapshot(entry, taskId, alive, now) {
8803
9430
  function readClaudeStatusBySocketPath(socketPath) {
8804
9431
  let files;
8805
9432
  try {
8806
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
9433
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
8807
9434
  } catch {
8808
9435
  return void 0;
8809
9436
  }
8810
- const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
9437
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
8811
9438
  const entry = entries.find((e) => e.messagingSocketPath === socketPath);
8812
9439
  if (!entry)
8813
9440
  return void 0;
@@ -8830,7 +9457,7 @@ import { homedir as homedir8 } from "os";
8830
9457
  var SQUADRANT_STATE_DIR = join19(homedir8(), ".config", "squadrant", "state");
8831
9458
 
8832
9459
  // packages/agents/dist/drivers/launch-cmd.js
8833
- import fs14 from "fs";
9460
+ import fs15 from "fs";
8834
9461
  import path13 from "path";
8835
9462
 
8836
9463
  // packages/agents/dist/projection/cursor.js
@@ -8854,7 +9481,7 @@ import path17 from "path";
8854
9481
  import os9 from "os";
8855
9482
 
8856
9483
  // packages/agents/dist/projection/opencode-skills.js
8857
- import fs15 from "fs";
9484
+ import fs16 from "fs";
8858
9485
  import os10 from "os";
8859
9486
  import path18 from "path";
8860
9487
  var OPENCODE_SKILL_MARKER = ".squadrant-managed";
@@ -8876,62 +9503,62 @@ function parseSkillName(raw) {
8876
9503
  }
8877
9504
  function listSourceSkills(sourceSkillsDir) {
8878
9505
  const out = /* @__PURE__ */ new Map();
8879
- if (!fs15.existsSync(sourceSkillsDir))
9506
+ if (!fs16.existsSync(sourceSkillsDir))
8880
9507
  return out;
8881
- for (const entry of fs15.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
9508
+ for (const entry of fs16.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
8882
9509
  if (!entry.isDirectory())
8883
9510
  continue;
8884
9511
  const skillPath = path18.join(sourceSkillsDir, entry.name, "SKILL.md");
8885
- if (!fs15.existsSync(skillPath))
9512
+ if (!fs16.existsSync(skillPath))
8886
9513
  continue;
8887
- const name = parseSkillName(fs15.readFileSync(skillPath, "utf-8"));
9514
+ const name = parseSkillName(fs16.readFileSync(skillPath, "utf-8"));
8888
9515
  if (name && SAFE_NAME.test(name))
8889
9516
  out.set(name, skillPath);
8890
9517
  }
8891
9518
  return out;
8892
9519
  }
8893
9520
  function hasMarker(dir) {
8894
- return fs15.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
9521
+ return fs16.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
8895
9522
  }
8896
9523
  function syncOpencodeSkills(opts) {
8897
9524
  const result = { written: [], unchanged: [], removed: [], skipped: [] };
8898
9525
  const wanted = listSourceSkills(opts.sourceSkillsDir);
8899
- if (wanted.size === 0 && !fs15.existsSync(opts.sourceSkillsDir))
9526
+ if (wanted.size === 0 && !fs16.existsSync(opts.sourceSkillsDir))
8900
9527
  return result;
8901
9528
  if (!opts.dryRun)
8902
- fs15.mkdirSync(opts.skillsRoot, { recursive: true });
9529
+ fs16.mkdirSync(opts.skillsRoot, { recursive: true });
8903
9530
  for (const [name, srcPath] of wanted) {
8904
9531
  const targetDir = path18.join(opts.skillsRoot, name);
8905
9532
  const targetSkill = path18.join(targetDir, "SKILL.md");
8906
9533
  const managed = hasMarker(targetDir);
8907
- if (fs15.existsSync(targetSkill) && !managed) {
9534
+ if (fs16.existsSync(targetSkill) && !managed) {
8908
9535
  result.skipped.push({ name, reason: "foreign-collision" });
8909
9536
  continue;
8910
9537
  }
8911
- const current = fs15.existsSync(targetSkill) ? fs15.readFileSync(targetSkill) : null;
8912
- if (current && current.equals(fs15.readFileSync(srcPath))) {
9538
+ const current = fs16.existsSync(targetSkill) ? fs16.readFileSync(targetSkill) : null;
9539
+ if (current && current.equals(fs16.readFileSync(srcPath))) {
8913
9540
  if (!managed && !opts.dryRun) {
8914
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
9541
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
8915
9542
  }
8916
9543
  result.unchanged.push(name);
8917
9544
  continue;
8918
9545
  }
8919
9546
  if (!opts.dryRun) {
8920
- fs15.mkdirSync(targetDir, { recursive: true });
8921
- fs15.copyFileSync(srcPath, targetSkill);
8922
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
9547
+ fs16.mkdirSync(targetDir, { recursive: true });
9548
+ fs16.copyFileSync(srcPath, targetSkill);
9549
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
8923
9550
  }
8924
9551
  result.written.push(name);
8925
9552
  }
8926
- if (fs15.existsSync(opts.skillsRoot)) {
8927
- for (const entry of fs15.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
9553
+ if (fs16.existsSync(opts.skillsRoot)) {
9554
+ for (const entry of fs16.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
8928
9555
  if (!entry.isDirectory() || wanted.has(entry.name))
8929
9556
  continue;
8930
9557
  const dir = path18.join(opts.skillsRoot, entry.name);
8931
9558
  if (!hasMarker(dir))
8932
9559
  continue;
8933
9560
  if (!opts.dryRun)
8934
- fs15.rmSync(dir, { recursive: true, force: true });
9561
+ fs16.rmSync(dir, { recursive: true, force: true });
8935
9562
  result.removed.push(entry.name);
8936
9563
  }
8937
9564
  }
@@ -10105,6 +10732,9 @@ var ClaudePeerChannel = class {
10105
10732
  return { status: "accepted", via: this.name, confirmed: true };
10106
10733
  }
10107
10734
  }
10735
+ if (statusBefore === "busy" || statusBefore === "shell") {
10736
+ return { status: "queued", via: this.name };
10737
+ }
10108
10738
  if (statusBefore === "idle") {
10109
10739
  await this.deps.sleep(window);
10110
10740
  const afterInfo = this.deps.statusFor(taskId);
@@ -10243,7 +10873,7 @@ var ClaudeReceiptListener = class {
10243
10873
  };
10244
10874
 
10245
10875
  // packages/agents/dist/claude/api-key-approval.js
10246
- import fs16 from "fs";
10876
+ import fs17 from "fs";
10247
10877
  import os12 from "os";
10248
10878
  import path20 from "path";
10249
10879
 
@@ -10485,11 +11115,11 @@ function parseDraftFromScreen(screen) {
10485
11115
  if (!screen)
10486
11116
  return null;
10487
11117
  const lines = screen.split(/\r?\n/);
10488
- const HR_RE = /^\s*─{10,}\s*$/;
11118
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10489
11119
  let bottomHR = -1;
10490
11120
  let topHR = -1;
10491
11121
  for (let i = lines.length - 1; i >= 0; i--) {
10492
- if (HR_RE.test(lines[i])) {
11122
+ if (HR_RE2.test(lines[i])) {
10493
11123
  if (bottomHR === -1) {
10494
11124
  bottomHR = i;
10495
11125
  } else {
@@ -10527,11 +11157,11 @@ function hasCCInputBox(screen) {
10527
11157
  if (!screen)
10528
11158
  return false;
10529
11159
  const lines = screen.split(/\r?\n/);
10530
- const HR_RE = /^\s*─{10,}\s*$/;
11160
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10531
11161
  let bottomHR = -1;
10532
11162
  let topHR = -1;
10533
11163
  for (let i = lines.length - 1; i >= 0; i--) {
10534
- if (HR_RE.test(lines[i])) {
11164
+ if (HR_RE2.test(lines[i])) {
10535
11165
  if (bottomHR === -1)
10536
11166
  bottomHR = i;
10537
11167
  else {
@@ -10544,15 +11174,15 @@ function hasCCInputBox(screen) {
10544
11174
  return false;
10545
11175
  return lines.slice(topHR + 1, bottomHR).some((l) => /[>❯]/.test(l));
10546
11176
  }
10547
- function hasModalOptionList(screen) {
11177
+ function hasModalOptionList2(screen) {
10548
11178
  if (!screen)
10549
11179
  return false;
10550
11180
  const lines = screen.split(/\r?\n/);
10551
- const HR_RE = /^\s*─{10,}\s*$/;
11181
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10552
11182
  let bottomHR = -1;
10553
11183
  let topHR = -1;
10554
11184
  for (let i = lines.length - 1; i >= 0; i--) {
10555
- if (HR_RE.test(lines[i])) {
11185
+ if (HR_RE2.test(lines[i])) {
10556
11186
  if (bottomHR === -1)
10557
11187
  bottomHR = i;
10558
11188
  else {
@@ -10569,11 +11199,11 @@ function readInputBoxRaw(screen, opts) {
10569
11199
  if (!screen)
10570
11200
  return null;
10571
11201
  const lines = screen.split(/\r?\n/);
10572
- const HR_RE = /^\s*─{10,}\s*$/;
11202
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10573
11203
  let bottomHR = -1;
10574
11204
  let topHR = -1;
10575
11205
  for (let i = lines.length - 1; i >= 0; i--) {
10576
- if (HR_RE.test(lines[i])) {
11206
+ if (HR_RE2.test(lines[i])) {
10577
11207
  if (bottomHR === -1)
10578
11208
  bottomHR = i;
10579
11209
  else {
@@ -10603,6 +11233,34 @@ function classifyStartupSurface(screen) {
10603
11233
  return "idle";
10604
11234
  return "loading";
10605
11235
  }
11236
+ var OC_BOX_BOTTOM_RE = /^\s*╹[▀]{6,}\s*$/;
11237
+ var OC_BOX_SIDE_RE = /^\s*┃/;
11238
+ var OC_BOX_STATUS_RE = /^Build\s*·/i;
11239
+ var OC_BUSY_RE = /esc\s+interrupt/i;
11240
+ function parseOpencodeDraftFromScreen(screen) {
11241
+ if (!screen)
11242
+ return null;
11243
+ if (OC_BUSY_RE.test(screen))
11244
+ return null;
11245
+ const lines = screen.split(/\r?\n/);
11246
+ let bottom = -1;
11247
+ for (let i = lines.length - 1; i >= 0; i--) {
11248
+ if (OC_BOX_BOTTOM_RE.test(lines[i])) {
11249
+ bottom = i;
11250
+ break;
11251
+ }
11252
+ }
11253
+ if (bottom === -1)
11254
+ return null;
11255
+ const content = [];
11256
+ for (let i = bottom - 1; i >= 0 && OC_BOX_SIDE_RE.test(lines[i]); i--) {
11257
+ content.push(lines[i].replace(/^\s*┃\s*/, "").replace(/[\s┃│]+$/, ""));
11258
+ }
11259
+ if (content.length === 0)
11260
+ return null;
11261
+ const hasDraft = content.some((l) => l !== "" && !OC_BOX_STATUS_RE.test(l) && !screenHasSplashMarker(l, "Ask anything"));
11262
+ return hasDraft ? null : "";
11263
+ }
10606
11264
  function sendDebugEnabled() {
10607
11265
  return !!process.env.SQUADRANT_DEBUG_SEND;
10608
11266
  }
@@ -10818,10 +11476,10 @@ function createCmuxDriver() {
10818
11476
  `);
10819
11477
  throw new DeferDelivery(null, "probe-failed");
10820
11478
  }
10821
- const draft = parseDraftFromScreen(screen);
11479
+ const draft = opts?.agent === "opencode" ? parseOpencodeDraftFromScreen(screen) : parseDraftFromScreen(screen);
10822
11480
  if (draft === null)
10823
11481
  throw new DeferDelivery(null, "no-box");
10824
- if (hasModalOptionList(screen))
11482
+ if (hasModalOptionList2(screen))
10825
11483
  throw new DeferDelivery(null, "modal");
10826
11484
  if (draft === "") {
10827
11485
  await deliver();
@@ -11015,7 +11673,7 @@ var NotifierRegistry = class {
11015
11673
  };
11016
11674
 
11017
11675
  // packages/workspaces/dist/workspaces/obsidian.js
11018
- import fs17 from "fs/promises";
11676
+ import fs18 from "fs/promises";
11019
11677
  import { existsSync as existsSync11 } from "fs";
11020
11678
  import path21 from "path";
11021
11679
 
@@ -11590,6 +12248,48 @@ function removeCommandHandlers(entries, command) {
11590
12248
  }
11591
12249
  return removed;
11592
12250
  }
12251
+ var FOREIGN_AUTO_GATE_MARKER = "auto-gate-managed";
12252
+ function isForeignAutoGateCommand(command) {
12253
+ if (typeof command !== "string")
12254
+ return false;
12255
+ if (command.includes(FOREIGN_AUTO_GATE_MARKER))
12256
+ return true;
12257
+ const first = command.trim().split(/\s+/)[0] ?? "";
12258
+ return /(^|[/\\])auto-gate$/.test(first);
12259
+ }
12260
+ function findForeignGateHandlers(entries) {
12261
+ const found = [];
12262
+ for (const entry of entries) {
12263
+ const hooks = entry?.hooks;
12264
+ if (!Array.isArray(hooks))
12265
+ continue;
12266
+ for (const h of hooks) {
12267
+ const cmd = h?.command;
12268
+ if (isForeignAutoGateCommand(cmd))
12269
+ found.push(cmd);
12270
+ }
12271
+ }
12272
+ return found;
12273
+ }
12274
+ function removeForeignGateHandlers(entries) {
12275
+ const removed = [];
12276
+ for (let i = entries.length - 1; i >= 0; i--) {
12277
+ const entry = entries[i];
12278
+ if (!Array.isArray(entry?.hooks))
12279
+ continue;
12280
+ entry.hooks = entry.hooks.filter((h) => {
12281
+ const cmd = h?.command;
12282
+ if (isForeignAutoGateCommand(cmd)) {
12283
+ removed.push(cmd);
12284
+ return false;
12285
+ }
12286
+ return true;
12287
+ });
12288
+ if (entry.hooks.length === 0)
12289
+ entries.splice(i, 1);
12290
+ }
12291
+ return removed;
12292
+ }
11593
12293
  function installClaudeHooks(opts = {}) {
11594
12294
  const settingsPath = opts.settingsPath ?? join25(homedir13(), ".claude", "settings.json");
11595
12295
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
@@ -11627,6 +12327,23 @@ function installClaudeHooks(opts = {}) {
11627
12327
  changed = true;
11628
12328
  migrated.push(`${eventName}/${sub}`);
11629
12329
  }
12330
+ const foreign = findForeignGateHandlers(entries);
12331
+ if (foreign.length > 0) {
12332
+ if (opts.gateMode === "on") {
12333
+ const removed = removeForeignGateHandlers(entries);
12334
+ if (removed.length > 0)
12335
+ changed = true;
12336
+ for (const command2 of removed) {
12337
+ log(`native-hook: removed foreign auto-gate PermissionRequest handler in ${settingsPath} [#P6 \xA715#5]: ${command2}`);
12338
+ opts.recordGateAction?.({ action: "removed-foreign-gate", command: command2 });
12339
+ }
12340
+ } else {
12341
+ for (const command2 of foreign) {
12342
+ 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}`);
12343
+ opts.recordGateAction?.({ action: "left-foreign-gate", command: command2 });
12344
+ }
12345
+ }
12346
+ }
11630
12347
  }
11631
12348
  const alreadyPresent = entries.some((m) => Array.isArray(m.hooks) && m.hooks.some((h) => typeof h.command === "string" && h.command === command));
11632
12349
  if (!alreadyPresent) {
@@ -11657,11 +12374,57 @@ function installClaudeHooks(opts = {}) {
11657
12374
  changed = true;
11658
12375
  }
11659
12376
  }
12377
+ if (typeof settings.env === "object" && settings.env !== null && !Array.isArray(settings.env)) {
12378
+ if (reconcileSubagentModels(settings.env, log))
12379
+ changed = true;
12380
+ }
11660
12381
  if (changed) {
11661
12382
  writeFile6(settingsPath, JSON.stringify(settings, null, 2));
11662
12383
  }
11663
12384
  return settingsPath;
11664
12385
  }
12386
+ var SUBAGENT_MODEL_KEY = "CLAUDE_CODE_SUBAGENT_MODEL";
12387
+ var SMALL_FAST_MODEL_KEY = "ANTHROPIC_SMALL_FAST_MODEL";
12388
+ function isCustomUpstream(baseUrl) {
12389
+ if (typeof baseUrl !== "string" || baseUrl.length === 0)
12390
+ return false;
12391
+ try {
12392
+ return new URL(baseUrl).hostname.toLowerCase() !== "api.anthropic.com";
12393
+ } catch {
12394
+ return false;
12395
+ }
12396
+ }
12397
+ function isAnthropicModelId(value) {
12398
+ if (typeof value !== "string")
12399
+ return false;
12400
+ const v2 = value.trim().toLowerCase();
12401
+ return v2 === "sonnet" || v2 === "opus" || v2 === "haiku" || v2.startsWith("claude-");
12402
+ }
12403
+ function reconcileSubagentModels(env, log) {
12404
+ if (!isCustomUpstream(env.ANTHROPIC_BASE_URL))
12405
+ return false;
12406
+ const model = env.ANTHROPIC_MODEL;
12407
+ if (typeof model !== "string" || model.length === 0)
12408
+ return false;
12409
+ let changed = false;
12410
+ for (const key of [SUBAGENT_MODEL_KEY, SMALL_FAST_MODEL_KEY]) {
12411
+ const current = env[key];
12412
+ if (current === void 0) {
12413
+ env[key] = model;
12414
+ changed = true;
12415
+ log(`native-hook: set ${key}='${model}' to match ANTHROPIC_MODEL on a custom upstream`);
12416
+ } else if (isAnthropicModelId(current)) {
12417
+ if (current !== model) {
12418
+ env[key] = model;
12419
+ changed = true;
12420
+ log(`native-hook: aligned ${key} '${String(current)}' \u2192 '${model}' for a custom upstream`);
12421
+ }
12422
+ } else {
12423
+ log(`native-hook: ${key} '${String(current)}' is a custom model id \u2014 left untouched`);
12424
+ }
12425
+ }
12426
+ return changed;
12427
+ }
11665
12428
  function mapSubToLifecycle(sub) {
11666
12429
  switch (sub) {
11667
12430
  case "session-start":
@@ -11811,7 +12574,7 @@ async function settleInputBox(runtime, pane) {
11811
12574
  }
11812
12575
  async function confirmedSendToPane(runtime, pane, message) {
11813
12576
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
11814
- if (hasModalOptionList(preSendScreen)) {
12577
+ if (hasModalOptionList2(preSendScreen)) {
11815
12578
  return { delivered: false, blockedByModal: true };
11816
12579
  }
11817
12580
  await runtime.pasteToPane(pane, message);
@@ -11914,7 +12677,7 @@ async function maybeBroadcastDaemonRestart(opts) {
11914
12677
 
11915
12678
  // packages/cli/src/lib/captain-channel-factory.ts
11916
12679
  import { createServer as createServer4, connect as netConnect } from "net";
11917
- import fs18 from "fs";
12680
+ import fs19 from "fs";
11918
12681
  import { join as join26 } from "path";
11919
12682
  import { randomUUID as randomUUID5 } from "crypto";
11920
12683
  import chalk2 from "chalk";
@@ -11923,15 +12686,15 @@ var shared;
11923
12686
  var registryEntryPath = () => join26(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
11924
12687
  function unregisterSenderIdentity() {
11925
12688
  try {
11926
- fs18.unlinkSync(registryEntryPath());
12689
+ fs19.unlinkSync(registryEntryPath());
11927
12690
  } catch {
11928
12691
  }
11929
12692
  }
11930
12693
  function registerSenderIdentity(socketPath) {
11931
12694
  try {
11932
12695
  unregisterSenderIdentity();
11933
- fs18.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
11934
- fs18.writeFileSync(
12696
+ fs19.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
12697
+ fs19.writeFileSync(
11935
12698
  registryEntryPath(),
11936
12699
  JSON.stringify({
11937
12700
  pid: process.pid,
@@ -11957,7 +12720,7 @@ async function sharedReceiptListener() {
11957
12720
  // must never be the reason we refuse to start.
11958
12721
  unlinkStale: (p) => {
11959
12722
  try {
11960
- fs18.unlinkSync(p);
12723
+ fs19.unlinkSync(p);
11961
12724
  } catch {
11962
12725
  }
11963
12726
  },
@@ -12054,6 +12817,33 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound, usageFor) {
12054
12817
  });
12055
12818
  const runCommand = createRunCommand(CLI_BIN);
12056
12819
  const sendReply = (threadId, text, replyMarkup) => client.sendMessage(cfg.supergroupId, threadId, text, replyMarkup);
12820
+ const lifecycle = createInboundLifecycle({
12821
+ stateRoot,
12822
+ cfg: { supergroupId: cfg.supergroupId },
12823
+ sendChatAction: (chatId, threadId, action) => client.sendChatAction(chatId, threadId, action),
12824
+ sendReply: async (threadId, text) => {
12825
+ await client.sendMessage(cfg.supergroupId, threadId, text);
12826
+ },
12827
+ readPane: createCaptainPaneReader(
12828
+ new DaemonCmux(createCmuxDriver()),
12829
+ (project) => {
12830
+ try {
12831
+ return loadConfig().projects[project]?.captainName ?? `${project}-captain`;
12832
+ } catch {
12833
+ return `${project}-captain`;
12834
+ }
12835
+ },
12836
+ {
12837
+ log,
12838
+ // #839 staleness, measured for real: cmux has no pane activity
12839
+ // timestamp, so the reader hashes the screen and asks the persisted
12840
+ // pending store how long that exact screen has been up. Persisted, so a
12841
+ // daemon restart cannot hand a dead captain a fresh 15 minutes.
12842
+ noteScreen: (project, hash) => notePaneScreen(stateRoot, project, hash, Date.now())
12843
+ }
12844
+ ),
12845
+ log
12846
+ });
12057
12847
  return createTelegramBridge({
12058
12848
  cfg,
12059
12849
  stateRoot,
@@ -12065,7 +12855,20 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound, usageFor) {
12065
12855
  runCommand,
12066
12856
  sendReply,
12067
12857
  deliverInbound,
12068
- usageFor
12858
+ usageFor,
12859
+ lifecycle,
12860
+ // #321 registry prune. Read the file directly rather than via loadConfig():
12861
+ // loadConfig swallows every failure and returns defaults, so an unreadable
12862
+ // config would look like "no projects registered" and the prune would delete
12863
+ // every link on the machine. `undefined` correctly means "don't prune".
12864
+ registeredProjects: () => {
12865
+ try {
12866
+ const parsed = JSON.parse(readFileSync19(join27(dirname6(stateRoot), "config.json"), "utf8"));
12867
+ return Object.keys(parsed.projects ?? {});
12868
+ } catch {
12869
+ return void 0;
12870
+ }
12871
+ }
12069
12872
  });
12070
12873
  }
12071
12874
  function buildNotifyFault(log) {
@@ -12187,7 +12990,15 @@ function startSquadrantd(opts = {}) {
12187
12990
  log
12188
12991
  });
12189
12992
  const cmuxStoreSource = new CmuxStoreSource({ log });
12190
- const nativeHookSource = new NativeHookSource({ log, hookInstall: { claudeEnv: loadConfig().defaults.claudeEnv } });
12993
+ const nativeHookSource = new NativeHookSource({
12994
+ log,
12995
+ hookInstall: {
12996
+ claudeEnv: loadConfig().defaults.claudeEnv,
12997
+ // #P6 (§15#5): only an explicit gate mode "on" lets squadrant remove a
12998
+ // foreign auto-gate PermissionRequest handler; auto/absent leaves it alone.
12999
+ gateMode: loadConfig().defaults.gate?.mode
13000
+ }
13001
+ });
12191
13002
  ctx.codexDriver = codexDriver;
12192
13003
  ctx.opencodeBridge = opencodeBridge;
12193
13004
  ctx.cmuxEventsBridge = cmuxEventsBridge;