squadrant 0.14.3 → 0.15.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.
@@ -60,10 +60,10 @@ var init_snapshot = __esm({
60
60
  });
61
61
 
62
62
  // packages/cli/src/squadrantd.ts
63
- import { join as join17, dirname as dirname5 } from "path";
64
- import { homedir as homedir12 } from "os";
63
+ import { join as join18, dirname as dirname5 } from "path";
64
+ import { homedir as homedir13 } from "os";
65
65
  import { fileURLToPath as fileURLToPath3 } from "url";
66
- import { readFileSync as readFileSync11 } from "fs";
66
+ import { readFileSync as readFileSync13, statSync as statSync3 } from "fs";
67
67
 
68
68
  // packages/shared/dist/config.js
69
69
  import fs from "fs";
@@ -154,6 +154,9 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
154
154
  fs.mkdirSync(dir, { recursive: true });
155
155
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
156
156
  }
157
+ function resolveHome(p) {
158
+ return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
159
+ }
157
160
 
158
161
  // packages/shared/dist/project-config.js
159
162
  import fs2 from "fs";
@@ -230,22 +233,22 @@ var MINIMAL_TEMPLATE = [
230
233
  ``
231
234
  ].join("\n");
232
235
  function ensureSocketAutomation(opts = {}) {
233
- const path17 = opts.path ?? defaultCmuxConfigPath();
234
- if (!existsSync(path17)) {
235
- mkdirSync(dirname(path17), { recursive: true });
236
- writeFileSync(path17, MINIMAL_TEMPLATE);
237
- return { path: path17, changed: true, alreadySet: false };
236
+ const path18 = opts.path ?? defaultCmuxConfigPath();
237
+ if (!existsSync(path18)) {
238
+ mkdirSync(dirname(path18), { recursive: true });
239
+ writeFileSync(path18, MINIMAL_TEMPLATE);
240
+ return { path: path18, changed: true, alreadySet: false };
238
241
  }
239
- const text = readFileSync(path17, "utf-8");
242
+ const text = readFileSync(path18, "utf-8");
240
243
  const current = parse(text)?.automation?.socketControlMode;
241
244
  if (current === AUTOMATION_MODE) {
242
- return { path: path17, changed: false, alreadySet: true };
245
+ return { path: path18, changed: false, alreadySet: true };
243
246
  }
244
247
  const edits = modify(text, [...SOCKET_CONTROL_MODE_PATH], AUTOMATION_MODE, {
245
248
  formattingOptions: { insertSpaces: true, tabSize: 2 }
246
249
  });
247
- writeFileSync(path17, applyEdits(text, edits));
248
- return { path: path17, changed: true, alreadySet: false };
250
+ writeFileSync(path18, applyEdits(text, edits));
251
+ return { path: path18, changed: true, alreadySet: false };
249
252
  }
250
253
 
251
254
  // packages/shared/dist/lib/cmux-probe.js
@@ -367,15 +370,15 @@ function sleep(ms) {
367
370
  function defaultStatePath() {
368
371
  return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
369
372
  }
370
- function readState(path17) {
373
+ function readState(path18) {
371
374
  try {
372
- return JSON.parse(readFileSync4(path17, "utf-8"));
375
+ return JSON.parse(readFileSync4(path18, "utf-8"));
373
376
  } catch {
374
377
  return {};
375
378
  }
376
379
  }
377
380
  async function ensureCmuxAutoConfig(opts = {}) {
378
- const statePath2 = opts.statePath ?? defaultStatePath();
381
+ const statePath3 = opts.statePath ?? defaultStatePath();
379
382
  const ensureConfig = opts.ensureConfig ?? ensureSocketAutomation;
380
383
  const probe = opts.probe ?? probeCmuxDaemonDirect;
381
384
  const cfg = ensureConfig({ path: opts.configPath });
@@ -383,15 +386,15 @@ async function ensureCmuxAutoConfig(opts = {}) {
383
386
  const needsRestart = verdict === "denied";
384
387
  let promptedThisRun = false;
385
388
  if (needsRestart) {
386
- const already = readState(statePath2).promptedRestart === true;
389
+ const already = readState(statePath3).promptedRestart === true;
387
390
  if (!already) {
388
- mkdirSync2(dirname2(statePath2), { recursive: true });
389
- writeFileSync3(statePath2, JSON.stringify({ promptedRestart: true }));
391
+ mkdirSync2(dirname2(statePath3), { recursive: true });
392
+ writeFileSync3(statePath3, JSON.stringify({ promptedRestart: true }));
390
393
  promptedThisRun = true;
391
394
  }
392
395
  } else if (verdict === "reachable") {
393
- if (existsSync4(statePath2))
394
- rmSync2(statePath2, { force: true });
396
+ if (existsSync4(statePath3))
397
+ rmSync2(statePath3, { force: true });
395
398
  }
396
399
  return {
397
400
  configPath: cfg.path,
@@ -1443,14 +1446,14 @@ var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
1443
1446
  function projectHealth(input) {
1444
1447
  const { project, now, captainName, captainStopped, commandPresent, crews } = input;
1445
1448
  const out = [];
1446
- const captainState = captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown";
1449
+ const captainState = input.captainState ?? (captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown");
1447
1450
  out.push({
1448
1451
  kind: "captain",
1449
1452
  project,
1450
1453
  ref: captainName,
1451
1454
  state: captainState,
1452
1455
  lastSeenMs: null,
1453
- detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : void 0
1456
+ detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : void 0
1454
1457
  });
1455
1458
  if (commandPresent !== null) {
1456
1459
  out.push({
@@ -1481,6 +1484,26 @@ function presence(p) {
1481
1484
  return "unknown";
1482
1485
  return p ? "alive" : "gone";
1483
1486
  }
1487
+ function deriveCaptainState(e) {
1488
+ if (!e)
1489
+ return "unknown";
1490
+ if (e.lastState === "end")
1491
+ return "stopped";
1492
+ if (!e.pidAlive)
1493
+ return "gone";
1494
+ return "alive";
1495
+ }
1496
+ function reconcileLiveness(prev, next) {
1497
+ if (!prev)
1498
+ return next;
1499
+ if (next.source === "scan") {
1500
+ const pidAlive = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
1501
+ return { ...prev, pidAlive, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
1502
+ }
1503
+ if (next.startedAt >= prev.startedAt || next.lastState === "end")
1504
+ return next;
1505
+ return prev;
1506
+ }
1484
1507
 
1485
1508
  // packages/core/dist/store.js
1486
1509
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, rmSync as rmSync3, statSync } from "fs";
@@ -1639,7 +1662,73 @@ function makeGate(opts) {
1639
1662
  import { homedir as homedir5 } from "os";
1640
1663
  import { join as join8 } from "path";
1641
1664
  import { spawn as realSpawn } from "child_process";
1642
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
1665
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
1666
+
1667
+ // packages/core/dist/daemon/liveness-registry.js
1668
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync7, renameSync as renameSync2 } from "fs";
1669
+ var LivenessRegistry = class {
1670
+ path;
1671
+ readFile;
1672
+ writeFile;
1673
+ map = /* @__PURE__ */ new Map();
1674
+ constructor(opts) {
1675
+ this.path = opts.path;
1676
+ this.readFile = opts.readFile ?? ((p) => {
1677
+ try {
1678
+ return readFileSync7(p, "utf-8");
1679
+ } catch {
1680
+ return void 0;
1681
+ }
1682
+ });
1683
+ this.writeFile = opts.writeFile ?? ((p, c) => {
1684
+ writeFileSync6(`${p}.tmp`, c);
1685
+ renameSync2(`${p}.tmp`, p);
1686
+ });
1687
+ }
1688
+ load() {
1689
+ const raw = this.readFile(this.path);
1690
+ if (!raw)
1691
+ return;
1692
+ try {
1693
+ const arr = JSON.parse(raw);
1694
+ this.map = new Map(arr.map((e) => [e.project, e]));
1695
+ } catch {
1696
+ this.map = /* @__PURE__ */ new Map();
1697
+ }
1698
+ }
1699
+ get(project) {
1700
+ return this.map.get(project);
1701
+ }
1702
+ all() {
1703
+ return [...this.map.values()];
1704
+ }
1705
+ apply(next) {
1706
+ this.map.set(next.project, reconcileLiveness(this.map.get(next.project), next));
1707
+ this.persist();
1708
+ }
1709
+ markEnded(project, at) {
1710
+ const e = this.map.get(project);
1711
+ if (!e)
1712
+ return;
1713
+ this.map.set(project, { ...e, lastState: "end", lastSeenAt: at });
1714
+ this.persist();
1715
+ }
1716
+ setPidAlive(project, alive, at) {
1717
+ const e = this.map.get(project);
1718
+ if (!e)
1719
+ return;
1720
+ this.map.set(project, { ...e, pidAlive: alive, lastSeenAt: at });
1721
+ this.persist();
1722
+ }
1723
+ persist() {
1724
+ try {
1725
+ this.writeFile(this.path, JSON.stringify(this.all(), null, 2));
1726
+ } catch {
1727
+ }
1728
+ }
1729
+ };
1730
+
1731
+ // packages/core/dist/daemon/context.js
1643
1732
  function defaultIsPidAlive(pid) {
1644
1733
  try {
1645
1734
  process.kill(pid, 0);
@@ -1660,7 +1749,7 @@ function buildContext(opts) {
1660
1749
  mkdirSync5(resultsDir, { recursive: true });
1661
1750
  const writeResult = (id, payload) => {
1662
1751
  const p = join8(resultsDir, `${id}.txt`);
1663
- writeFileSync6(p, payload);
1752
+ writeFileSync7(p, payload);
1664
1753
  return p;
1665
1754
  };
1666
1755
  const log = (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
@@ -1681,8 +1770,11 @@ function buildContext(opts) {
1681
1770
  attachConns: /* @__PURE__ */ new Map(),
1682
1771
  inFlightHeadlessIds: /* @__PURE__ */ new Set(),
1683
1772
  activeHeadlessKills: /* @__PURE__ */ new Set(),
1684
- captainMissingStreak: /* @__PURE__ */ new Map(),
1685
- stoppedProjects: /* @__PURE__ */ new Set(),
1773
+ livenessRegistry: (() => {
1774
+ const r = new LivenessRegistry({ path: join8(stateRoot, "liveness.json") });
1775
+ r.load();
1776
+ return r;
1777
+ })(),
1686
1778
  resendFirstTurn: opts.resendFirstTurn,
1687
1779
  // Late-bound — start.ts fills these before first use:
1688
1780
  d: null,
@@ -2036,7 +2128,6 @@ var CaptainDelivery = class {
2036
2128
 
2037
2129
  // packages/core/dist/daemon/delivery-loop.js
2038
2130
  var CURSOR_SUBSCRIBER = "captain";
2039
- var CAPTAIN_GONE_STREAK_K = 3;
2040
2131
  var TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
2041
2132
  function discoverCaptainSurface(surfaces, captainTitle) {
2042
2133
  return surfaces.find((s) => s.title === captainTitle) ?? null;
@@ -2053,8 +2144,62 @@ function reapOrphanedCrews(store, project) {
2053
2144
  }
2054
2145
  return reaped;
2055
2146
  }
2147
+ function logEntry(log, project, e) {
2148
+ if (!log || !e)
2149
+ return;
2150
+ log(`[${e.role}/${e.source}] ${project} pid=${e.pid} \u2192 ${deriveCaptainState(e)}`);
2151
+ }
2152
+ async function runLivenessTick(deps) {
2153
+ const now = deps.now();
2154
+ let records = [];
2155
+ try {
2156
+ records = await deps.liveness();
2157
+ } catch {
2158
+ return;
2159
+ }
2160
+ const seen = /* @__PURE__ */ new Set();
2161
+ for (const r of records) {
2162
+ if (r.role !== "captain")
2163
+ continue;
2164
+ seen.add(r.project);
2165
+ const entry = {
2166
+ project: r.project,
2167
+ role: "captain",
2168
+ pid: r.pid,
2169
+ sessionId: r.sessionId,
2170
+ startedAt: now,
2171
+ lastState: "start",
2172
+ lastSeenAt: now,
2173
+ pidAlive: r.pid != null ? deps.isPidAlive(r.pid) : true,
2174
+ // pid:null hibernated → alive-unknown
2175
+ source: "runtime"
2176
+ };
2177
+ const prev = deps.registry.get(r.project);
2178
+ if (prev && prev.lastState === "start")
2179
+ entry.startedAt = prev.startedAt;
2180
+ deps.registry.apply(entry);
2181
+ if (r.pid != null)
2182
+ deps.registry.setPidAlive(r.project, deps.isPidAlive(r.pid), now);
2183
+ logEntry(deps.log, r.project, deps.registry.get(r.project));
2184
+ }
2185
+ for (const e of deps.registry.all()) {
2186
+ if (e.role === "captain" && e.lastState === "start" && !seen.has(e.project)) {
2187
+ deps.registry.markEnded(e.project, now);
2188
+ logEntry(deps.log, e.project, deps.registry.get(e.project));
2189
+ }
2190
+ }
2191
+ if (deps.reap) {
2192
+ for (const e of deps.registry.all()) {
2193
+ if (e.role !== "captain")
2194
+ continue;
2195
+ const state = deriveCaptainState(e);
2196
+ if (state === "stopped" || state === "gone")
2197
+ deps.reap(e.project);
2198
+ }
2199
+ }
2200
+ }
2056
2201
  function createDelivery(ctx, daemonCmux) {
2057
- const { stateRoot, store, log, captainMissingStreak, stoppedProjects, opts } = ctx;
2202
+ const { stateRoot, store, log, livenessRegistry, isPidAlive, opts } = ctx;
2058
2203
  const defaultNotify = async (args) => {
2059
2204
  try {
2060
2205
  await appendToMailbox({
@@ -2080,6 +2225,21 @@ function createDelivery(ctx, daemonCmux) {
2080
2225
  const sessionStartMs = Date.now();
2081
2226
  let delivering = false;
2082
2227
  const deliveryCore = async () => {
2228
+ await runLivenessTick({
2229
+ registry: livenessRegistry,
2230
+ liveness: () => cmux2.liveness ? cmux2.liveness() : Promise.resolve([]),
2231
+ isPidAlive,
2232
+ now: () => Date.now(),
2233
+ log,
2234
+ reap: (project) => {
2235
+ const reaped = reapOrphanedCrews(store, project);
2236
+ if (reaped > 0) {
2237
+ const title = cfg.projects?.[project]?.captainName ?? `${project}-captain`;
2238
+ log(`captain ${title}: reaped ${reaped} orphaned crew(s)`);
2239
+ }
2240
+ return reaped;
2241
+ }
2242
+ });
2083
2243
  const injectedSurfaces = opts.captainSurfaces ?? {};
2084
2244
  const allProjects = [.../* @__PURE__ */ new Set([
2085
2245
  ...Object.keys(cfg.projects ?? {}),
@@ -2091,34 +2251,14 @@ function createDelivery(ctx, daemonCmux) {
2091
2251
  const captainTitle = projCfg?.captainName ?? `${project}-captain`;
2092
2252
  const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
2093
2253
  let surface = null;
2094
- let surfacesLength = 0;
2095
2254
  if (wsId) {
2096
2255
  const surfaces = await cmux2.listSurfaces(wsId);
2097
- surfacesLength = surfaces.length;
2098
2256
  surface = discoverCaptainSurface(surfaces, captainTitle);
2099
2257
  }
2100
2258
  if (!surface)
2101
2259
  surface = injectedSurfaces[project] ?? null;
2102
- if (surface) {
2103
- if (stoppedProjects.has(project)) {
2104
- stoppedProjects.delete(project);
2105
- captainMissingStreak.set(project, 0);
2106
- }
2107
- captainMissingStreak.set(project, 0);
2108
- } else {
2109
- if (surfacesLength > 0) {
2110
- const streak = (captainMissingStreak.get(project) ?? 0) + 1;
2111
- captainMissingStreak.set(project, streak);
2112
- if (streak >= CAPTAIN_GONE_STREAK_K) {
2113
- if (!stoppedProjects.has(project)) {
2114
- stoppedProjects.add(project);
2115
- const reaped = reapOrphanedCrews(store, project);
2116
- log(`captain ${captainTitle}: surface gone for ${CAPTAIN_GONE_STREAK_K} sweeps \u2014 stopping delivery${reaped > 0 ? `, reaped ${reaped} orphaned crew(s)` : ""}`);
2117
- }
2118
- }
2119
- }
2260
+ if (!surface)
2120
2261
  continue;
2121
- }
2122
2262
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
2123
2263
  const lastAcked = cursor?.lastAckedSeq ?? 0;
2124
2264
  let d = deliveries.get(project);
@@ -2240,7 +2380,7 @@ function createServer2(ctx, handlers) {
2240
2380
  // packages/core/dist/daemon/snapshot-gather.js
2241
2381
  import { fileURLToPath as fileURLToPath2 } from "url";
2242
2382
  import { join as join9 } from "path";
2243
- import { statSync as statSync2, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
2383
+ import { statSync as statSync2, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
2244
2384
  var SELF_PATH = fileURLToPath2(import.meta.url);
2245
2385
  function distBuiltAt() {
2246
2386
  try {
@@ -2249,10 +2389,10 @@ function distBuiltAt() {
2249
2389
  return 0;
2250
2390
  }
2251
2391
  }
2252
- function gatherLogStats(path17, now, windowMs) {
2392
+ function gatherLogStats(path18, now, windowMs) {
2253
2393
  let sizeBytes = 0;
2254
2394
  try {
2255
- sizeBytes = statSync2(path17).size;
2395
+ sizeBytes = statSync2(path18).size;
2256
2396
  } catch {
2257
2397
  return { errorCount: 0, sizeBytes: 0, windowMs };
2258
2398
  }
@@ -2263,7 +2403,7 @@ function gatherLogStats(path17, now, windowMs) {
2263
2403
  const len = sizeBytes - start;
2264
2404
  let text = "";
2265
2405
  try {
2266
- const fd = openSync2(path17, "r");
2406
+ const fd = openSync2(path18, "r");
2267
2407
  try {
2268
2408
  const buf = Buffer.alloc(len);
2269
2409
  readSync(fd, buf, 0, len, start);
@@ -2304,7 +2444,7 @@ function gatherStoreStats(store, stateRoot, project) {
2304
2444
  if (!n.endsWith(".json"))
2305
2445
  continue;
2306
2446
  try {
2307
- JSON.parse(readFileSync7(join9(dir, n), "utf-8"));
2447
+ JSON.parse(readFileSync8(join9(dir, n), "utf-8"));
2308
2448
  } catch {
2309
2449
  corruptCount++;
2310
2450
  }
@@ -2389,14 +2529,13 @@ function startDaemon(ctx, opts, pkgVersion) {
2389
2529
  for (const project of names) {
2390
2530
  const proj = config.projects[project];
2391
2531
  const captainName = proj?.captainName ?? `${project}-captain`;
2392
- const stopped = ctx.stoppedProjects.has(project);
2393
- const streak = ctx.captainMissingStreak.get(project);
2394
- const captainStopped = stopped ? true : streak === 0 ? false : null;
2532
+ const capEntry = ctx.livenessRegistry.get(project);
2395
2533
  out.push(...projectHealth({
2396
2534
  project,
2397
2535
  now,
2398
2536
  captainName,
2399
- captainStopped,
2537
+ captainStopped: null,
2538
+ captainState: deriveCaptainState(capEntry),
2400
2539
  commandPresent: null,
2401
2540
  crews: store.list(project)
2402
2541
  }));
@@ -2753,21 +2892,33 @@ function createRunCommand(cliBin) {
2753
2892
  }
2754
2893
  };
2755
2894
  }
2895
+ function isCaptainAliveFromHealth(rows, project) {
2896
+ return rows.some((h) => h.kind === "captain" && h.project === project && h.state === "alive");
2897
+ }
2756
2898
  function createIsCaptainAlive(sock) {
2757
2899
  return async (project) => {
2758
2900
  try {
2759
2901
  const health = await sendRequest(sock, { kind: "health", project }, 5e3);
2760
- const captain = health?.find((h) => h.kind === "captain" && h.project === project);
2761
- return captain != null && captain.state !== "gone" && captain.state !== "unknown";
2902
+ return isCaptainAliveFromHealth(health ?? [], project);
2762
2903
  } catch {
2763
2904
  return false;
2764
2905
  }
2765
2906
  };
2766
2907
  }
2767
- function createLaunch(cliBin) {
2768
- return async (project) => {
2769
- await pExecFile(process.execPath, [cliBin, "launch", project], { timeout: 3e4 });
2770
- };
2908
+ function createLaunch(cliBin, log) {
2909
+ return (project) => new Promise((resolve2, reject) => {
2910
+ execFile(process.execPath, [cliBin, "launch", project, "--headless"], { timeout: 3e4 }, (err, stdout, stderr) => {
2911
+ const output = capOutput(stdout ?? "", stderr ?? "");
2912
+ if (err) {
2913
+ log?.(`launch ${project} failed: ${output}`);
2914
+ reject(err);
2915
+ return;
2916
+ }
2917
+ if (output !== "(no output)")
2918
+ log?.(`launch ${project}: ${output}`);
2919
+ resolve2();
2920
+ });
2921
+ });
2771
2922
  }
2772
2923
 
2773
2924
  // packages/core/dist/telegram/ensure-captain.js
@@ -3309,8 +3460,11 @@ function createTelegramBridge(opts) {
3309
3460
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
3310
3461
  try {
3311
3462
  const r = await ensureCaptainAlive(resolved.project);
3312
- if (r === "timeout")
3313
- await reply(threadId, "\u26A0\uFE0F captain didn't warm up; message queued.");
3463
+ if (r === "timeout") {
3464
+ await reply(threadId, `\u274C couldn't reach ${resolved.project} captain \u2014 saved to mailbox, will deliver when you open the workspace.`);
3465
+ } else {
3466
+ await reply(threadId, `\u{1F4E8} delivered to ${resolved.project} captain`);
3467
+ }
3314
3468
  } catch (e) {
3315
3469
  log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
3316
3470
  }
@@ -4237,7 +4391,7 @@ var OpencodeSseBridge = class {
4237
4391
 
4238
4392
  // packages/agents/dist/interactive/claude.js
4239
4393
  import { execSync as execSync6 } from "child_process";
4240
- import { readFileSync as readFileSync8 } from "fs";
4394
+ import { readFileSync as readFileSync9 } from "fs";
4241
4395
  import { homedir as homedir9 } from "os";
4242
4396
  import { join as join14 } from "path";
4243
4397
 
@@ -4863,6 +5017,38 @@ function createCmuxDriver() {
4863
5017
  };
4864
5018
  }
4865
5019
 
5020
+ // packages/workspaces/dist/runtimes/registry.js
5021
+ var DEFAULT_RUNTIME = "cmux";
5022
+ var RuntimeRegistry = class {
5023
+ drivers;
5024
+ constructor(drivers) {
5025
+ this.drivers = drivers;
5026
+ }
5027
+ forProject(projectName, config) {
5028
+ const projectRuntime = config.projects[projectName]?.runtime;
5029
+ const runtimeName = projectRuntime ?? config.runtime ?? DEFAULT_RUNTIME;
5030
+ return this.get(runtimeName);
5031
+ }
5032
+ global(config) {
5033
+ const runtimeName = config.runtime ?? DEFAULT_RUNTIME;
5034
+ return this.get(runtimeName);
5035
+ }
5036
+ get(name) {
5037
+ const driver = this.drivers[name];
5038
+ if (!driver) {
5039
+ throw new Error(`Unknown runtime '${name}' \u2014 no driver registered`);
5040
+ }
5041
+ return driver;
5042
+ }
5043
+ async probeAll() {
5044
+ const results = {};
5045
+ for (const [name, driver] of Object.entries(this.drivers)) {
5046
+ results[name] = await driver.probe();
5047
+ }
5048
+ return results;
5049
+ }
5050
+ };
5051
+
4866
5052
  // packages/workspaces/dist/notifiers/cmux.js
4867
5053
  import { execFileSync as execFileSync6, execSync as execSync7 } from "child_process";
4868
5054
 
@@ -5005,6 +5191,71 @@ var CmuxEventsBridge = class {
5005
5191
  }
5006
5192
  };
5007
5193
 
5194
+ // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
5195
+ import { readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
5196
+ import { join as join15 } from "path";
5197
+ import { homedir as homedir10 } from "os";
5198
+
5199
+ // packages/workspaces/dist/cmux-daemon/store-fingerprint.js
5200
+ function roleFromTemplate(args) {
5201
+ const i = args?.indexOf("--append-system-prompt-file") ?? -1;
5202
+ const tmpl = i >= 0 && args ? (args[i + 1] ?? "").split("/").pop() ?? "" : "";
5203
+ if (tmpl.startsWith("captain"))
5204
+ return "captain";
5205
+ if (tmpl.startsWith("crew"))
5206
+ return "crew";
5207
+ if (tmpl.startsWith("command"))
5208
+ return "command";
5209
+ return "unknown";
5210
+ }
5211
+ function projectFromCwd(cwd, projects) {
5212
+ for (const [name, p] of Object.entries(projects)) {
5213
+ const projPath = resolveHome(p.path);
5214
+ if (cwd === projPath || cwd.startsWith(`${projPath}/`))
5215
+ return name;
5216
+ }
5217
+ return void 0;
5218
+ }
5219
+ function parseStoreRecords(fileContent, projects) {
5220
+ let parsed;
5221
+ try {
5222
+ parsed = JSON.parse(fileContent);
5223
+ } catch (e) {
5224
+ throw new Error(`parseStoreRecords: invalid JSON: ${e.message}`);
5225
+ }
5226
+ const out = [];
5227
+ for (const s of Object.values(parsed.sessions ?? {})) {
5228
+ const cwd = s.cwd ?? s.launchCommand?.workingDirectory ?? "";
5229
+ const project = projectFromCwd(cwd, projects);
5230
+ if (!project || !s.sessionId)
5231
+ continue;
5232
+ out.push({
5233
+ role: roleFromTemplate(s.launchCommand?.arguments),
5234
+ project,
5235
+ pid: typeof s.pid === "number" ? s.pid : null,
5236
+ sessionId: s.sessionId,
5237
+ present: true,
5238
+ isRestorable: s.isRestorable
5239
+ });
5240
+ }
5241
+ return out;
5242
+ }
5243
+ function readLivenessSnapshot(files, readFile6, projects) {
5244
+ const out = [];
5245
+ let successes = 0;
5246
+ for (const f of files) {
5247
+ try {
5248
+ out.push(...parseStoreRecords(readFile6(f), projects));
5249
+ successes++;
5250
+ } catch {
5251
+ }
5252
+ }
5253
+ if (files.length > 0 && successes === 0) {
5254
+ throw new Error(`readLivenessSnapshot: all ${files.length} store file(s) unreadable/corrupt this tick`);
5255
+ }
5256
+ return out;
5257
+ }
5258
+
5008
5259
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
5009
5260
  var DaemonCmux = class {
5010
5261
  driver;
@@ -5056,12 +5307,28 @@ var DaemonCmux = class {
5056
5307
  return false;
5057
5308
  }
5058
5309
  }
5310
+ /**
5311
+ * Ground-truth liveness from cmux's own hook-sessions store (§5.4).
5312
+ * THROWS (does not return []) when the dir can't be listed, or every store
5313
+ * file failed to read/parse — see the class doc above.
5314
+ */
5315
+ async liveness() {
5316
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
5317
+ const projects = loadConfig().projects;
5318
+ let files;
5319
+ try {
5320
+ files = readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
5321
+ } catch (e) {
5322
+ throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
5323
+ }
5324
+ return readLivenessSnapshot(files, (f) => readFileSync10(join15(dir, f), "utf-8"), projects);
5325
+ }
5059
5326
  };
5060
5327
 
5061
5328
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
5062
- import { join as join15 } from "path";
5063
- import { homedir as homedir10 } from "os";
5064
- import { watch, readdirSync as readdirSync3, readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
5329
+ import { join as join16 } from "path";
5330
+ import { homedir as homedir11 } from "os";
5331
+ import { watch, readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
5065
5332
  var CmuxStoreSource = class {
5066
5333
  name = "cmux-store";
5067
5334
  stateDir;
@@ -5082,7 +5349,7 @@ var CmuxStoreSource = class {
5082
5349
  active = false;
5083
5350
  lastError = null;
5084
5351
  constructor(opts = {}) {
5085
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
5352
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join16(homedir11(), ".cmuxterm");
5086
5353
  this.debounceMs = opts.debounceMs ?? 50;
5087
5354
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
5088
5355
  this.listFiles = opts.listFiles ?? defaultListFiles;
@@ -5144,7 +5411,7 @@ var CmuxStoreSource = class {
5144
5411
  }
5145
5412
  scanFile(filename) {
5146
5413
  const deps = this.deps;
5147
- const filePath = join15(this.stateDir, filename);
5414
+ const filePath = join16(this.stateDir, filename);
5148
5415
  const lockPath = `${filePath}.lock`;
5149
5416
  if (this.fileExists(lockPath)) {
5150
5417
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -5210,14 +5477,14 @@ function defaultIsPidAlive2(pid) {
5210
5477
  }
5211
5478
  function defaultListFiles(dir) {
5212
5479
  try {
5213
- return readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
5480
+ return readdirSync4(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
5214
5481
  } catch {
5215
5482
  return [];
5216
5483
  }
5217
5484
  }
5218
- function defaultReadFile(path17) {
5485
+ function defaultReadFile(path18) {
5219
5486
  try {
5220
- return readFileSync9(path17, "utf-8");
5487
+ return readFileSync11(path18, "utf-8");
5221
5488
  } catch {
5222
5489
  return void 0;
5223
5490
  }
@@ -5232,9 +5499,9 @@ function defaultWatchDir(dir, cb) {
5232
5499
  }
5233
5500
 
5234
5501
  // packages/workspaces/dist/native-hooks/native-hook-source.js
5235
- import { join as join16 } from "path";
5236
- import { homedir as homedir11 } from "os";
5237
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
5502
+ import { join as join17 } from "path";
5503
+ import { homedir as homedir12 } from "os";
5504
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
5238
5505
  var CLAUDE_HOOK_EVENTS = [
5239
5506
  ["SessionStart", "session-start"],
5240
5507
  ["UserPromptSubmit", "prompt-submit"],
@@ -5246,7 +5513,7 @@ var CLAUDE_HOOK_EVENTS = [
5246
5513
  ];
5247
5514
  var DEFAULT_HOOK_CMD = "squadrant hooks";
5248
5515
  function installClaudeHooks(opts = {}) {
5249
- const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
5516
+ const settingsPath = opts.settingsPath ?? join17(homedir12(), ".claude", "settings.json");
5250
5517
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
5251
5518
  const readFile6 = opts.readFile ?? defaultReadFile2;
5252
5519
  const writeFile5 = opts.writeFile ?? defaultWriteFile;
@@ -5392,16 +5659,16 @@ function extractDetail(sub, payload) {
5392
5659
  }
5393
5660
  return void 0;
5394
5661
  }
5395
- function defaultReadFile2(path17) {
5662
+ function defaultReadFile2(path18) {
5396
5663
  try {
5397
- return readFileSync10(path17, "utf-8");
5664
+ return readFileSync12(path18, "utf-8");
5398
5665
  } catch {
5399
5666
  return void 0;
5400
5667
  }
5401
5668
  }
5402
- function defaultWriteFile(path17, content) {
5403
- mkdirSync6(path17.replace(/\/[^/]+$/, ""), { recursive: true });
5404
- writeFileSync7(path17, content, "utf-8");
5669
+ function defaultWriteFile(path18, content) {
5670
+ mkdirSync6(path18.replace(/\/[^/]+$/, ""), { recursive: true });
5671
+ writeFileSync8(path18, content, "utf-8");
5405
5672
  }
5406
5673
 
5407
5674
  // packages/workspaces/dist/crew-pane.js
@@ -5468,14 +5735,65 @@ async function resendCrewFirstTurn(runtime, captainName, project, name, message)
5468
5735
  return confirmedSendToPane(runtime, pane, message);
5469
5736
  }
5470
5737
 
5738
+ // packages/cli/src/lib/daemon-restart-broadcast.ts
5739
+ import fs15 from "fs";
5740
+ import path17 from "path";
5741
+ function statePath2(stateRoot) {
5742
+ return path17.join(stateRoot, "daemon-restart-state.json");
5743
+ }
5744
+ function computeRestartSignature(version, buildMtimeMs) {
5745
+ return `${version}::${buildMtimeMs}`;
5746
+ }
5747
+ function readPersistedRestartSignature(stateRoot) {
5748
+ try {
5749
+ const raw = fs15.readFileSync(statePath2(stateRoot), "utf-8");
5750
+ const data = JSON.parse(raw);
5751
+ return typeof data.signature === "string" ? data.signature : null;
5752
+ } catch {
5753
+ return null;
5754
+ }
5755
+ }
5756
+ function writePersistedRestartSignature(stateRoot, signature) {
5757
+ fs15.mkdirSync(stateRoot, { recursive: true });
5758
+ fs15.writeFileSync(statePath2(stateRoot), JSON.stringify({ signature }, null, 2) + "\n");
5759
+ }
5760
+ function restartNotice(version, isDevRebuild) {
5761
+ const suffix = isDevRebuild ? " (dev build)" : "";
5762
+ return `\u26A0\uFE0F Daemon restarted \u2192 v${version}${suffix} (control-plane bounced). Re-verify in-flight crews \u2014 a crew mid-first-turn may need a crew send.`;
5763
+ }
5764
+ async function notifyCaptainsOfDaemonRestart(version, config, driver, isDevRebuild = false) {
5765
+ const notice = restartNotice(version, isDevRebuild);
5766
+ for (const [, proj] of Object.entries(config.projects)) {
5767
+ try {
5768
+ const ref = await driver.status(proj.captainName);
5769
+ if (ref) {
5770
+ await driver.send(ref.id, notice);
5771
+ }
5772
+ } catch {
5773
+ }
5774
+ }
5775
+ }
5776
+ async function maybeBroadcastDaemonRestart(opts) {
5777
+ try {
5778
+ const { version, buildMtimeMs, stateRoot, config, driver } = opts;
5779
+ const signature = computeRestartSignature(version, buildMtimeMs);
5780
+ const previous = readPersistedRestartSignature(stateRoot);
5781
+ if (previous === signature) return;
5782
+ const isDevRebuild = previous !== null && previous.split("::")[0] === version;
5783
+ await notifyCaptainsOfDaemonRestart(version, config, driver, isDevRebuild);
5784
+ writePersistedRestartSignature(stateRoot, signature);
5785
+ } catch {
5786
+ }
5787
+ }
5788
+
5471
5789
  // packages/cli/src/squadrantd.ts
5472
5790
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
5473
- var CLI_BIN = join17(dirname5(SELF_PATH2), "index.js");
5474
- var DAEMON_SOCK = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
5791
+ var CLI_BIN = join18(dirname5(SELF_PATH2), "index.js");
5792
+ var DAEMON_SOCK = join18(homedir13(), ".config", "squadrant", "squadrant.sock");
5475
5793
  function readPkgVersion() {
5476
5794
  try {
5477
- const pkgPath = join17(dirname5(SELF_PATH2), "..", "package.json");
5478
- return JSON.parse(readFileSync11(pkgPath, "utf-8")).version ?? "unknown";
5795
+ const pkgPath = join18(dirname5(SELF_PATH2), "..", "package.json");
5796
+ return JSON.parse(readFileSync13(pkgPath, "utf-8")).version ?? "unknown";
5479
5797
  } catch {
5480
5798
  return "unknown";
5481
5799
  }
@@ -5490,7 +5808,7 @@ function buildTelegramBridge(cfg, stateRoot, log) {
5490
5808
  const client = createTelegramClient({ token });
5491
5809
  const ensureCaptainAlive = createEnsureCaptainAlive({
5492
5810
  isAlive: createIsCaptainAlive(DAEMON_SOCK),
5493
- launch: createLaunch(CLI_BIN)
5811
+ launch: createLaunch(CLI_BIN, log)
5494
5812
  });
5495
5813
  const runCommand = createRunCommand(CLI_BIN);
5496
5814
  const sendReply = (threadId, text, replyMarkup) => client.sendMessage(cfg.supergroupId, threadId, text, replyMarkup);
@@ -5558,7 +5876,7 @@ function startSquadrantd(opts = {}) {
5558
5876
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
5559
5877
  );
5560
5878
  },
5561
- cursorFile: join17(stateRoot, "cmux-events.seq"),
5879
+ cursorFile: join18(stateRoot, "cmux-events.seq"),
5562
5880
  log
5563
5881
  });
5564
5882
  const cmuxStoreSource = new CmuxStoreSource({ log });
@@ -5713,6 +6031,22 @@ ${buildCompletionProtocol(rec.id, rec.project)}`;
5713
6031
  log(`codex app-server source start failed: ${e.message}`);
5714
6032
  }
5715
6033
  }
6034
+ if (!process.env.VITEST) {
6035
+ try {
6036
+ const buildMtimeMs = statSync3(SELF_PATH2).mtimeMs;
6037
+ const restartConfig = loadConfig();
6038
+ const registry = new RuntimeRegistry({ cmux: createCmuxDriver() });
6039
+ void maybeBroadcastDaemonRestart({
6040
+ version: PKG_VERSION,
6041
+ buildMtimeMs,
6042
+ stateRoot,
6043
+ config: restartConfig,
6044
+ driver: registry.global(restartConfig)
6045
+ });
6046
+ } catch (e) {
6047
+ log(`daemon-restart broadcast setup failed: ${e.message}`);
6048
+ }
6049
+ }
5716
6050
  const origStop = h.stop.bind(h);
5717
6051
  h.stop = async () => {
5718
6052
  try {
@@ -5738,7 +6072,7 @@ if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
5738
6072
  process.stdout.write("squadrantd: launchd-managed daemon entry (no CLI args). Use `squadrant` for commands.\n");
5739
6073
  process.exit(0);
5740
6074
  }
5741
- const sock = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
6075
+ const sock = join18(homedir13(), ".config", "squadrant", "squadrant.sock");
5742
6076
  if (await isDaemonSocketLive(sock)) {
5743
6077
  process.stderr.write(`[squadrantd] refusing to start: a live daemon already owns ${sock}
5744
6078
  `);