squadrant 0.14.3 → 0.16.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 path19 = opts.path ?? defaultCmuxConfigPath();
237
+ if (!existsSync(path19)) {
238
+ mkdirSync(dirname(path19), { recursive: true });
239
+ writeFileSync(path19, MINIMAL_TEMPLATE);
240
+ return { path: path19, changed: true, alreadySet: false };
238
241
  }
239
- const text = readFileSync(path17, "utf-8");
242
+ const text = readFileSync(path19, "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: path19, 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(path19, applyEdits(text, edits));
251
+ return { path: path19, 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(path19) {
371
374
  try {
372
- return JSON.parse(readFileSync4(path17, "utf-8"));
375
+ return JSON.parse(readFileSync4(path19, "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,
@@ -416,16 +419,25 @@ var compatManifest = {
416
419
  }
417
420
  };
418
421
 
422
+ // packages/shared/dist/lib/update-check.js
423
+ import fs3 from "fs";
424
+ import path3 from "path";
425
+ import os3 from "os";
426
+ import https from "https";
427
+ var UPDATE_CHECK_STATE_PATH = path3.join(os3.homedir(), ".config", "squadrant", "update-check.json");
428
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
429
+ var FAILURE_RETRY_MS = 60 * 60 * 1e3;
430
+
419
431
  // packages/shared/dist/lib/git-worktree.js
420
432
  import { execFileSync as execFileSync2 } from "child_process";
421
- import path3 from "path";
433
+ import path4 from "path";
422
434
 
423
435
  // packages/shared/dist/lib/resolve-text-input.js
424
- import fs3 from "fs";
436
+ import fs4 from "fs";
425
437
 
426
438
  // packages/shared/dist/lib/runtime-sync.js
427
- import fs4 from "fs";
428
- import path4 from "path";
439
+ import fs5 from "fs";
440
+ import path5 from "path";
429
441
 
430
442
  // packages/shared/dist/lib/tool-compat.js
431
443
  function parseSemVer(v) {
@@ -459,13 +471,13 @@ function checkToolCompat(name, rawVersion, entry) {
459
471
  }
460
472
 
461
473
  // packages/shared/dist/lib/canonical-source.js
462
- import fs5 from "fs";
463
- import path5 from "path";
474
+ import fs6 from "fs";
475
+ import path6 from "path";
464
476
 
465
477
  // packages/shared/dist/lib/daily-logs.js
466
478
  import { execSync } from "child_process";
467
- import fs6 from "fs";
468
- import path6 from "path";
479
+ import fs7 from "fs";
480
+ import path7 from "path";
469
481
  import matter from "gray-matter";
470
482
 
471
483
  // packages/core/dist/state-machine.js
@@ -626,7 +638,7 @@ function formatMessage(rec, event) {
626
638
  return `CREW STALLED ${tag}: no heartbeat in ${rec.heartbeatBudgetMs}ms`;
627
639
  }
628
640
  case "awaiting-input":
629
- return `CREW IDLE ${tag}: turn ended / awaiting your input \u2014 review and reply or close.`;
641
+ return `CREW IDLE ${tag}: turn ended, awaiting your reply.`;
630
642
  default:
631
643
  return null;
632
644
  }
@@ -1007,7 +1019,7 @@ function createDaemon(deps) {
1007
1019
  }
1008
1020
 
1009
1021
  // packages/core/dist/mailbox.js
1010
- import { promises as fs7 } from "fs";
1022
+ import { promises as fs8 } from "fs";
1011
1023
  import { join as join5 } from "path";
1012
1024
  import { randomUUID } from "crypto";
1013
1025
  function inboxDir(stateRoot) {
@@ -1024,7 +1036,7 @@ async function listRotatedOldestFirst(stateRoot, project) {
1024
1036
  const dir = inboxDir(stateRoot);
1025
1037
  let entries;
1026
1038
  try {
1027
- entries = await fs7.readdir(dir);
1039
+ entries = await fs8.readdir(dir);
1028
1040
  } catch {
1029
1041
  return [];
1030
1042
  }
@@ -1033,7 +1045,7 @@ async function listRotatedOldestFirst(stateRoot, project) {
1033
1045
  }
1034
1046
  async function readMaxSeqFromFile(file) {
1035
1047
  try {
1036
- const buf = await fs7.readFile(file, "utf-8");
1048
+ const buf = await fs8.readFile(file, "utf-8");
1037
1049
  if (!buf.trim())
1038
1050
  return 0;
1039
1051
  const lines = buf.trim().split("\n");
@@ -1075,12 +1087,12 @@ function withProjectLock(project, fn) {
1075
1087
  function appendEntry(stateRoot, project, build) {
1076
1088
  return withProjectLock(project, async () => {
1077
1089
  const dir = inboxDir(stateRoot);
1078
- await fs7.mkdir(dir, { recursive: true });
1090
+ await fs8.mkdir(dir, { recursive: true });
1079
1091
  const file = logPath(stateRoot, project);
1080
1092
  const lastSeq = await readMaxSeq(stateRoot, project);
1081
1093
  const seq = lastSeq + 1;
1082
1094
  const entry = build(seq);
1083
- await fs7.appendFile(file, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
1095
+ await fs8.appendFile(file, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
1084
1096
  return seq;
1085
1097
  });
1086
1098
  }
@@ -1111,7 +1123,7 @@ function cursorPath(stateRoot, project, subscriber) {
1111
1123
  async function readCursor(opts) {
1112
1124
  let buf;
1113
1125
  try {
1114
- buf = await fs7.readFile(cursorPath(opts.stateRoot, opts.project, opts.subscriber), "utf-8");
1126
+ buf = await fs8.readFile(cursorPath(opts.stateRoot, opts.project, opts.subscriber), "utf-8");
1115
1127
  } catch (e) {
1116
1128
  if (e.code === "ENOENT")
1117
1129
  return null;
@@ -1126,7 +1138,7 @@ async function readCursor(opts) {
1126
1138
  }
1127
1139
  }
1128
1140
  async function writeCursor(opts) {
1129
- await fs7.mkdir(inboxDir(opts.stateRoot), { recursive: true });
1141
+ await fs8.mkdir(inboxDir(opts.stateRoot), { recursive: true });
1130
1142
  const dest = cursorPath(opts.stateRoot, opts.project, opts.subscriber);
1131
1143
  const tmp = `${dest}.${process.pid}.${randomUUID()}.tmp`;
1132
1144
  const data = {
@@ -1134,7 +1146,7 @@ async function writeCursor(opts) {
1134
1146
  subscriber: opts.subscriber,
1135
1147
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1136
1148
  };
1137
- const handle = await fs7.open(tmp, "w");
1149
+ const handle = await fs8.open(tmp, "w");
1138
1150
  try {
1139
1151
  await handle.writeFile(JSON.stringify(data), { encoding: "utf-8" });
1140
1152
  await handle.sync();
@@ -1142,9 +1154,9 @@ async function writeCursor(opts) {
1142
1154
  await handle.close();
1143
1155
  }
1144
1156
  try {
1145
- await fs7.rename(tmp, dest);
1157
+ await fs8.rename(tmp, dest);
1146
1158
  } catch (e) {
1147
- await fs7.unlink(tmp).catch(() => {
1159
+ await fs8.unlink(tmp).catch(() => {
1148
1160
  });
1149
1161
  throw e;
1150
1162
  }
@@ -1155,7 +1167,7 @@ async function* readFromCursor(opts) {
1155
1167
  for (const file of files) {
1156
1168
  let buf;
1157
1169
  try {
1158
- buf = await fs7.readFile(file, "utf-8");
1170
+ buf = await fs8.readFile(file, "utf-8");
1159
1171
  } catch (e) {
1160
1172
  if (e.code === "ENOENT")
1161
1173
  continue;
@@ -1181,7 +1193,7 @@ async function mailboxStats(stateRoot, project) {
1181
1193
  let sizeBytes = 0;
1182
1194
  for (const f of [file, ...rotated]) {
1183
1195
  try {
1184
- sizeBytes += (await fs7.stat(f)).size;
1196
+ sizeBytes += (await fs8.stat(f)).size;
1185
1197
  } catch (e) {
1186
1198
  if (e.code !== "ENOENT")
1187
1199
  throw e;
@@ -1197,7 +1209,7 @@ async function mailboxStats(stateRoot, project) {
1197
1209
  }
1198
1210
  async function oldestEntryAgeMs(file) {
1199
1211
  try {
1200
- const buf = await fs7.readFile(file, "utf-8");
1212
+ const buf = await fs8.readFile(file, "utf-8");
1201
1213
  const firstLine = buf.split("\n").find((l) => l.trim());
1202
1214
  if (!firstLine)
1203
1215
  return 0;
@@ -1212,7 +1224,7 @@ async function rotateIfNeeded(opts) {
1212
1224
  const file = logPath(opts.stateRoot, opts.project);
1213
1225
  let size = 0;
1214
1226
  try {
1215
- size = (await fs7.stat(file)).size;
1227
+ size = (await fs8.stat(file)).size;
1216
1228
  } catch (e) {
1217
1229
  if (e.code === "ENOENT")
1218
1230
  return { rotated: false };
@@ -1228,22 +1240,22 @@ async function rotateIfNeeded(opts) {
1228
1240
  const dst = `${file}.${n + 1}`;
1229
1241
  if (n + 1 > opts.keepCount) {
1230
1242
  try {
1231
- await fs7.unlink(src);
1243
+ await fs8.unlink(src);
1232
1244
  } catch (e) {
1233
1245
  if (e.code !== "ENOENT")
1234
1246
  throw e;
1235
1247
  }
1236
1248
  } else {
1237
1249
  try {
1238
- await fs7.rename(src, dst);
1250
+ await fs8.rename(src, dst);
1239
1251
  } catch (e) {
1240
1252
  if (e.code !== "ENOENT")
1241
1253
  throw e;
1242
1254
  }
1243
1255
  }
1244
1256
  }
1245
- await fs7.rename(file, `${file}.1`);
1246
- await fs7.writeFile(file, "", { encoding: "utf-8" });
1257
+ await fs8.rename(file, `${file}.1`);
1258
+ await fs8.writeFile(file, "", { encoding: "utf-8" });
1247
1259
  return { rotated: true, from: file, to: `${file}.1` };
1248
1260
  });
1249
1261
  }
@@ -1443,14 +1455,14 @@ var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
1443
1455
  function projectHealth(input) {
1444
1456
  const { project, now, captainName, captainStopped, commandPresent, crews } = input;
1445
1457
  const out = [];
1446
- const captainState = captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown";
1458
+ const captainState = input.captainState ?? (captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown");
1447
1459
  out.push({
1448
1460
  kind: "captain",
1449
1461
  project,
1450
1462
  ref: captainName,
1451
1463
  state: captainState,
1452
1464
  lastSeenMs: null,
1453
- detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : void 0
1465
+ detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : void 0
1454
1466
  });
1455
1467
  if (commandPresent !== null) {
1456
1468
  out.push({
@@ -1481,6 +1493,26 @@ function presence(p) {
1481
1493
  return "unknown";
1482
1494
  return p ? "alive" : "gone";
1483
1495
  }
1496
+ function deriveCaptainState(e) {
1497
+ if (!e)
1498
+ return "unknown";
1499
+ if (e.lastState === "end")
1500
+ return "stopped";
1501
+ if (!e.pidAlive)
1502
+ return "gone";
1503
+ return "alive";
1504
+ }
1505
+ function reconcileLiveness(prev, next) {
1506
+ if (!prev)
1507
+ return next;
1508
+ if (next.source === "scan") {
1509
+ const pidAlive = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
1510
+ return { ...prev, pidAlive, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
1511
+ }
1512
+ if (next.startedAt >= prev.startedAt || next.lastState === "end")
1513
+ return next;
1514
+ return prev;
1515
+ }
1484
1516
 
1485
1517
  // packages/core/dist/store.js
1486
1518
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, rmSync as rmSync3, statSync } from "fs";
@@ -1639,7 +1671,73 @@ function makeGate(opts) {
1639
1671
  import { homedir as homedir5 } from "os";
1640
1672
  import { join as join8 } from "path";
1641
1673
  import { spawn as realSpawn } from "child_process";
1642
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
1674
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
1675
+
1676
+ // packages/core/dist/daemon/liveness-registry.js
1677
+ import { writeFileSync as writeFileSync6, readFileSync as readFileSync7, renameSync as renameSync2 } from "fs";
1678
+ var LivenessRegistry = class {
1679
+ path;
1680
+ readFile;
1681
+ writeFile;
1682
+ map = /* @__PURE__ */ new Map();
1683
+ constructor(opts) {
1684
+ this.path = opts.path;
1685
+ this.readFile = opts.readFile ?? ((p) => {
1686
+ try {
1687
+ return readFileSync7(p, "utf-8");
1688
+ } catch {
1689
+ return void 0;
1690
+ }
1691
+ });
1692
+ this.writeFile = opts.writeFile ?? ((p, c) => {
1693
+ writeFileSync6(`${p}.tmp`, c);
1694
+ renameSync2(`${p}.tmp`, p);
1695
+ });
1696
+ }
1697
+ load() {
1698
+ const raw = this.readFile(this.path);
1699
+ if (!raw)
1700
+ return;
1701
+ try {
1702
+ const arr = JSON.parse(raw);
1703
+ this.map = new Map(arr.map((e) => [e.project, e]));
1704
+ } catch {
1705
+ this.map = /* @__PURE__ */ new Map();
1706
+ }
1707
+ }
1708
+ get(project) {
1709
+ return this.map.get(project);
1710
+ }
1711
+ all() {
1712
+ return [...this.map.values()];
1713
+ }
1714
+ apply(next) {
1715
+ this.map.set(next.project, reconcileLiveness(this.map.get(next.project), next));
1716
+ this.persist();
1717
+ }
1718
+ markEnded(project, at) {
1719
+ const e = this.map.get(project);
1720
+ if (!e)
1721
+ return;
1722
+ this.map.set(project, { ...e, lastState: "end", lastSeenAt: at });
1723
+ this.persist();
1724
+ }
1725
+ setPidAlive(project, alive, at) {
1726
+ const e = this.map.get(project);
1727
+ if (!e)
1728
+ return;
1729
+ this.map.set(project, { ...e, pidAlive: alive, lastSeenAt: at });
1730
+ this.persist();
1731
+ }
1732
+ persist() {
1733
+ try {
1734
+ this.writeFile(this.path, JSON.stringify(this.all(), null, 2));
1735
+ } catch {
1736
+ }
1737
+ }
1738
+ };
1739
+
1740
+ // packages/core/dist/daemon/context.js
1643
1741
  function defaultIsPidAlive(pid) {
1644
1742
  try {
1645
1743
  process.kill(pid, 0);
@@ -1660,7 +1758,7 @@ function buildContext(opts) {
1660
1758
  mkdirSync5(resultsDir, { recursive: true });
1661
1759
  const writeResult = (id, payload) => {
1662
1760
  const p = join8(resultsDir, `${id}.txt`);
1663
- writeFileSync6(p, payload);
1761
+ writeFileSync7(p, payload);
1664
1762
  return p;
1665
1763
  };
1666
1764
  const log = (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
@@ -1681,8 +1779,11 @@ function buildContext(opts) {
1681
1779
  attachConns: /* @__PURE__ */ new Map(),
1682
1780
  inFlightHeadlessIds: /* @__PURE__ */ new Set(),
1683
1781
  activeHeadlessKills: /* @__PURE__ */ new Set(),
1684
- captainMissingStreak: /* @__PURE__ */ new Map(),
1685
- stoppedProjects: /* @__PURE__ */ new Set(),
1782
+ livenessRegistry: (() => {
1783
+ const r = new LivenessRegistry({ path: join8(stateRoot, "liveness.json") });
1784
+ r.load();
1785
+ return r;
1786
+ })(),
1686
1787
  resendFirstTurn: opts.resendFirstTurn,
1687
1788
  // Late-bound — start.ts fills these before first use:
1688
1789
  d: null,
@@ -2036,7 +2137,6 @@ var CaptainDelivery = class {
2036
2137
 
2037
2138
  // packages/core/dist/daemon/delivery-loop.js
2038
2139
  var CURSOR_SUBSCRIBER = "captain";
2039
- var CAPTAIN_GONE_STREAK_K = 3;
2040
2140
  var TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
2041
2141
  function discoverCaptainSurface(surfaces, captainTitle) {
2042
2142
  return surfaces.find((s) => s.title === captainTitle) ?? null;
@@ -2053,8 +2153,71 @@ function reapOrphanedCrews(store, project) {
2053
2153
  }
2054
2154
  return reaped;
2055
2155
  }
2156
+ function logEntry(log, project, e) {
2157
+ if (!log || !e)
2158
+ return;
2159
+ log(`[${e.role}/${e.source}] ${project} pid=${e.pid} \u2192 ${deriveCaptainState(e)}`);
2160
+ }
2161
+ async function runLivenessTick(deps) {
2162
+ const now = deps.now();
2163
+ let records = [];
2164
+ try {
2165
+ records = await deps.liveness();
2166
+ } catch {
2167
+ return;
2168
+ }
2169
+ const seen = /* @__PURE__ */ new Set();
2170
+ const byProject = /* @__PURE__ */ new Map();
2171
+ for (const r of records) {
2172
+ if (r.role !== "captain")
2173
+ continue;
2174
+ let arr = byProject.get(r.project);
2175
+ if (!arr) {
2176
+ arr = [];
2177
+ byProject.set(r.project, arr);
2178
+ }
2179
+ arr.push(r);
2180
+ }
2181
+ for (const [project, recs] of byProject) {
2182
+ seen.add(project);
2183
+ const winner = recs.find((r) => r.pid == null || deps.isPidAlive(r.pid)) ?? recs[0];
2184
+ const entry = {
2185
+ project,
2186
+ role: "captain",
2187
+ pid: winner.pid,
2188
+ sessionId: winner.sessionId,
2189
+ startedAt: now,
2190
+ lastState: "start",
2191
+ lastSeenAt: now,
2192
+ pidAlive: winner.pid != null ? deps.isPidAlive(winner.pid) : true,
2193
+ source: "runtime"
2194
+ };
2195
+ const prev = deps.registry.get(project);
2196
+ if (prev && prev.lastState === "start")
2197
+ entry.startedAt = prev.startedAt;
2198
+ deps.registry.apply(entry);
2199
+ if (winner.pid != null)
2200
+ deps.registry.setPidAlive(project, deps.isPidAlive(winner.pid), now);
2201
+ logEntry(deps.log, project, deps.registry.get(project));
2202
+ }
2203
+ for (const e of deps.registry.all()) {
2204
+ if (e.role === "captain" && e.lastState === "start" && !seen.has(e.project)) {
2205
+ deps.registry.markEnded(e.project, now);
2206
+ logEntry(deps.log, e.project, deps.registry.get(e.project));
2207
+ }
2208
+ }
2209
+ if (deps.reap) {
2210
+ for (const e of deps.registry.all()) {
2211
+ if (e.role !== "captain")
2212
+ continue;
2213
+ const state = deriveCaptainState(e);
2214
+ if (state === "stopped" || state === "gone")
2215
+ deps.reap(e.project);
2216
+ }
2217
+ }
2218
+ }
2056
2219
  function createDelivery(ctx, daemonCmux) {
2057
- const { stateRoot, store, log, captainMissingStreak, stoppedProjects, opts } = ctx;
2220
+ const { stateRoot, store, log, livenessRegistry, isPidAlive, opts } = ctx;
2058
2221
  const defaultNotify = async (args) => {
2059
2222
  try {
2060
2223
  await appendToMailbox({
@@ -2080,45 +2243,41 @@ function createDelivery(ctx, daemonCmux) {
2080
2243
  const sessionStartMs = Date.now();
2081
2244
  let delivering = false;
2082
2245
  const deliveryCore = async () => {
2246
+ await runLivenessTick({
2247
+ registry: livenessRegistry,
2248
+ liveness: () => cmux2.liveness ? cmux2.liveness() : Promise.resolve([]),
2249
+ isPidAlive,
2250
+ now: () => Date.now(),
2251
+ log,
2252
+ reap: (project) => {
2253
+ const reaped = reapOrphanedCrews(store, project);
2254
+ if (reaped > 0) {
2255
+ const title = cfg.projects?.[project]?.captainName ?? `${project}-captain`;
2256
+ log(`captain ${title}: reaped ${reaped} orphaned crew(s)`);
2257
+ }
2258
+ return reaped;
2259
+ }
2260
+ });
2083
2261
  const injectedSurfaces = opts.captainSurfaces ?? {};
2084
2262
  const allProjects = [.../* @__PURE__ */ new Set([
2085
2263
  ...Object.keys(cfg.projects ?? {}),
2086
2264
  ...Object.keys(injectedSurfaces),
2087
- ...store.listAll().map((t) => t.project)
2265
+ ...store.listAll().map((t) => t.project),
2266
+ cfg.commandName
2088
2267
  ])];
2089
2268
  for (const project of allProjects) {
2090
2269
  const projCfg = cfg.projects?.[project];
2091
- const captainTitle = projCfg?.captainName ?? `${project}-captain`;
2270
+ const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
2092
2271
  const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
2093
2272
  let surface = null;
2094
- let surfacesLength = 0;
2095
2273
  if (wsId) {
2096
2274
  const surfaces = await cmux2.listSurfaces(wsId);
2097
- surfacesLength = surfaces.length;
2098
2275
  surface = discoverCaptainSurface(surfaces, captainTitle);
2099
2276
  }
2100
2277
  if (!surface)
2101
2278
  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
- }
2279
+ if (!surface)
2120
2280
  continue;
2121
- }
2122
2281
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
2123
2282
  const lastAcked = cursor?.lastAckedSeq ?? 0;
2124
2283
  let d = deliveries.get(project);
@@ -2132,11 +2291,16 @@ function createDelivery(ctx, daemonCmux) {
2132
2291
  for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
2133
2292
  if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
2134
2293
  if (!TERMINAL_KINDS.has(entry.kind)) {
2135
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
2136
- await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
2137
- continue;
2294
+ const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
2295
+ if (!isExemptMessage) {
2296
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
2297
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
2298
+ continue;
2299
+ }
2300
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
2301
+ } else {
2302
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
2138
2303
  }
2139
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
2140
2304
  }
2141
2305
  const result = await d.deliver(entry, (text, sendOpts) => cmux2.send(surface, text, sendOpts));
2142
2306
  if ("delivered" in result) {
@@ -2240,7 +2404,7 @@ function createServer2(ctx, handlers) {
2240
2404
  // packages/core/dist/daemon/snapshot-gather.js
2241
2405
  import { fileURLToPath as fileURLToPath2 } from "url";
2242
2406
  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";
2407
+ import { statSync as statSync2, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
2244
2408
  var SELF_PATH = fileURLToPath2(import.meta.url);
2245
2409
  function distBuiltAt() {
2246
2410
  try {
@@ -2249,10 +2413,10 @@ function distBuiltAt() {
2249
2413
  return 0;
2250
2414
  }
2251
2415
  }
2252
- function gatherLogStats(path17, now, windowMs) {
2416
+ function gatherLogStats(path19, now, windowMs) {
2253
2417
  let sizeBytes = 0;
2254
2418
  try {
2255
- sizeBytes = statSync2(path17).size;
2419
+ sizeBytes = statSync2(path19).size;
2256
2420
  } catch {
2257
2421
  return { errorCount: 0, sizeBytes: 0, windowMs };
2258
2422
  }
@@ -2263,7 +2427,7 @@ function gatherLogStats(path17, now, windowMs) {
2263
2427
  const len = sizeBytes - start;
2264
2428
  let text = "";
2265
2429
  try {
2266
- const fd = openSync2(path17, "r");
2430
+ const fd = openSync2(path19, "r");
2267
2431
  try {
2268
2432
  const buf = Buffer.alloc(len);
2269
2433
  readSync(fd, buf, 0, len, start);
@@ -2304,7 +2468,7 @@ function gatherStoreStats(store, stateRoot, project) {
2304
2468
  if (!n.endsWith(".json"))
2305
2469
  continue;
2306
2470
  try {
2307
- JSON.parse(readFileSync7(join9(dir, n), "utf-8"));
2471
+ JSON.parse(readFileSync8(join9(dir, n), "utf-8"));
2308
2472
  } catch {
2309
2473
  corruptCount++;
2310
2474
  }
@@ -2389,14 +2553,13 @@ function startDaemon(ctx, opts, pkgVersion) {
2389
2553
  for (const project of names) {
2390
2554
  const proj = config.projects[project];
2391
2555
  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;
2556
+ const capEntry = ctx.livenessRegistry.get(project);
2395
2557
  out.push(...projectHealth({
2396
2558
  project,
2397
2559
  now,
2398
2560
  captainName,
2399
- captainStopped,
2561
+ captainStopped: null,
2562
+ captainState: deriveCaptainState(capEntry),
2400
2563
  commandPresent: null,
2401
2564
  crews: store.list(project)
2402
2565
  }));
@@ -2496,7 +2659,7 @@ function startDaemon(ctx, opts, pkgVersion) {
2496
2659
  }
2497
2660
  })();
2498
2661
  const server = createServer2(ctx, { buildHealth, gatherSnapshotInputs, cancelPromotionsFor, broadcast });
2499
- log(`started pid=${process.pid} sock=${ctx.sockPath} stateRoot=${stateRoot}`);
2662
+ log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
2500
2663
  let deliveryTick = initialDeliveryTick;
2501
2664
  let probeTick;
2502
2665
  if (daemonCmux) {
@@ -2557,7 +2720,8 @@ function startDaemon(ctx, opts, pkgVersion) {
2557
2720
  rotationTimer.unref?.();
2558
2721
  }
2559
2722
  return {
2560
- stop() {
2723
+ stop(reason = "requested") {
2724
+ log(`exit pid=${process.pid} reason=${reason}`);
2561
2725
  if (deliveryTimer)
2562
2726
  clearInterval(deliveryTimer);
2563
2727
  if (probeTimer)
@@ -2581,7 +2745,7 @@ function startDaemon(ctx, opts, pkgVersion) {
2581
2745
  for (const kill of ctx.activeHeadlessKills)
2582
2746
  kill();
2583
2747
  return new Promise((resolve2) => server.close(() => {
2584
- log("stopped");
2748
+ log(`exit-complete pid=${process.pid}`);
2585
2749
  resolve2();
2586
2750
  }));
2587
2751
  },
@@ -2592,8 +2756,8 @@ function startDaemon(ctx, opts, pkgVersion) {
2592
2756
 
2593
2757
  // packages/core/dist/session-freshness.js
2594
2758
  import crypto from "crypto";
2595
- import fs8 from "fs";
2596
- import path7 from "path";
2759
+ import fs9 from "fs";
2760
+ import path8 from "path";
2597
2761
 
2598
2762
  // packages/core/dist/crew-protocol.js
2599
2763
  function buildCompletionProtocol(taskId, project) {
@@ -2753,21 +2917,33 @@ function createRunCommand(cliBin) {
2753
2917
  }
2754
2918
  };
2755
2919
  }
2920
+ function isCaptainAliveFromHealth(rows, project) {
2921
+ return rows.some((h) => h.kind === "captain" && h.project === project && h.state === "alive");
2922
+ }
2756
2923
  function createIsCaptainAlive(sock) {
2757
2924
  return async (project) => {
2758
2925
  try {
2759
2926
  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";
2927
+ return isCaptainAliveFromHealth(health ?? [], project);
2762
2928
  } catch {
2763
2929
  return false;
2764
2930
  }
2765
2931
  };
2766
2932
  }
2767
- function createLaunch(cliBin) {
2768
- return async (project) => {
2769
- await pExecFile(process.execPath, [cliBin, "launch", project], { timeout: 3e4 });
2770
- };
2933
+ function createLaunch(cliBin, log) {
2934
+ return (project) => new Promise((resolve2, reject) => {
2935
+ execFile(process.execPath, [cliBin, "launch", project, "--headless"], { timeout: 3e4 }, (err, stdout, stderr) => {
2936
+ const output = capOutput(stdout ?? "", stderr ?? "");
2937
+ if (err) {
2938
+ log?.(`launch ${project} failed: ${output}`);
2939
+ reject(err);
2940
+ return;
2941
+ }
2942
+ if (output !== "(no output)")
2943
+ log?.(`launch ${project}: ${output}`);
2944
+ resolve2();
2945
+ });
2946
+ });
2771
2947
  }
2772
2948
 
2773
2949
  // packages/core/dist/telegram/ensure-captain.js
@@ -2836,17 +3012,17 @@ function formatInbound(text) {
2836
3012
  }
2837
3013
 
2838
3014
  // packages/core/dist/telegram/state.js
2839
- import fs9 from "fs";
2840
- import path8 from "path";
3015
+ import fs10 from "fs";
3016
+ import path9 from "path";
2841
3017
  function statePath(stateRoot) {
2842
- return path8.join(stateRoot, "telegram-state.json");
3018
+ return path9.join(stateRoot, "telegram-state.json");
2843
3019
  }
2844
3020
  function topicKey(project, scope = "project") {
2845
3021
  return `${project}::${scope}`;
2846
3022
  }
2847
3023
  function loadState(stateRoot) {
2848
3024
  try {
2849
- const raw = fs9.readFileSync(statePath(stateRoot), "utf-8");
3025
+ const raw = fs10.readFileSync(statePath(stateRoot), "utf-8");
2850
3026
  const data = JSON.parse(raw);
2851
3027
  const result = {
2852
3028
  offset: typeof data.offset === "number" ? data.offset : 0,
@@ -2861,8 +3037,8 @@ function loadState(stateRoot) {
2861
3037
  }
2862
3038
  }
2863
3039
  function saveState(stateRoot, s) {
2864
- fs9.mkdirSync(stateRoot, { recursive: true });
2865
- fs9.writeFileSync(statePath(stateRoot), JSON.stringify(s, null, 2) + "\n");
3040
+ fs10.mkdirSync(stateRoot, { recursive: true });
3041
+ fs10.writeFileSync(statePath(stateRoot), JSON.stringify(s, null, 2) + "\n");
2866
3042
  }
2867
3043
  function setTopic(stateRoot, project, topicId, scope = "project") {
2868
3044
  const s = loadState(stateRoot);
@@ -2952,8 +3128,8 @@ function createTelegramClient(opts) {
2952
3128
  }
2953
3129
 
2954
3130
  // packages/core/dist/telegram/bridge.js
2955
- import os3 from "os";
2956
- import path9 from "path";
3131
+ import os4 from "os";
3132
+ import path10 from "path";
2957
3133
 
2958
3134
  // packages/core/dist/telegram/panels.js
2959
3135
  var mark = (on, label) => on ? `\u2022 ${label}` : label;
@@ -3058,7 +3234,7 @@ function notifyToggle(text) {
3058
3234
  }
3059
3235
  function createTelegramBridge(opts) {
3060
3236
  const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
3061
- const configRoot = opts.configRoot ?? path9.join(os3.homedir(), ".config", "squadrant");
3237
+ const configRoot = opts.configRoot ?? path10.join(os4.homedir(), ".config", "squadrant");
3062
3238
  const pollMs = cfg.pollMs ?? 1e3;
3063
3239
  let running = false;
3064
3240
  let lastSuccessfulPollAt = null;
@@ -3183,14 +3359,14 @@ function createTelegramBridge(opts) {
3183
3359
  }
3184
3360
  function currentEffort() {
3185
3361
  try {
3186
- return loadConfig(path9.join(configRoot, "config.json")).defaults.effort ?? "balance";
3362
+ return loadConfig(path10.join(configRoot, "config.json")).defaults.effort ?? "balance";
3187
3363
  } catch {
3188
3364
  return "balance";
3189
3365
  }
3190
3366
  }
3191
3367
  function projectNames() {
3192
3368
  try {
3193
- return Object.keys(loadConfig(path9.join(configRoot, "config.json")).projects);
3369
+ return Object.keys(loadConfig(path10.join(configRoot, "config.json")).projects);
3194
3370
  } catch {
3195
3371
  return [];
3196
3372
  }
@@ -3309,8 +3485,11 @@ function createTelegramBridge(opts) {
3309
3485
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
3310
3486
  try {
3311
3487
  const r = await ensureCaptainAlive(resolved.project);
3312
- if (r === "timeout")
3313
- await reply(threadId, "\u26A0\uFE0F captain didn't warm up; message queued.");
3488
+ if (r === "timeout") {
3489
+ await reply(threadId, `\u274C couldn't reach ${resolved.project} captain \u2014 saved to mailbox, will deliver when you open the workspace.`);
3490
+ } else {
3491
+ await reply(threadId, `\u{1F4E8} delivered to ${resolved.project} captain`);
3492
+ }
3314
3493
  } catch (e) {
3315
3494
  log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
3316
3495
  }
@@ -3394,7 +3573,7 @@ function createTelegramBridge(opts) {
3394
3573
  }
3395
3574
 
3396
3575
  // packages/core/dist/telegram/setup.js
3397
- import fs10 from "fs";
3576
+ import fs11 from "fs";
3398
3577
 
3399
3578
  // packages/core/dist/restart-daemon.js
3400
3579
  import { execFileSync as execFileSync4 } from "child_process";
@@ -3410,14 +3589,14 @@ import { join as join12 } from "path";
3410
3589
  var DEFAULT_SOCK_PATH2 = join12(homedir7(), ".config", "squadrant", "squadrant.sock");
3411
3590
 
3412
3591
  // packages/core/dist/side-session.js
3413
- import fs11 from "fs";
3592
+ import fs12 from "fs";
3414
3593
 
3415
3594
  // packages/core/dist/crew-spawn.js
3416
- import fs12 from "fs";
3417
- import os4 from "os";
3418
- import path10 from "path";
3419
- var TEMPLATES_DIR = path10.join(os4.homedir(), ".config", "squadrant", "templates");
3420
- var STATE_ROOT = path10.join(os4.homedir(), ".config", "squadrant", "state");
3595
+ import fs13 from "fs";
3596
+ import os5 from "os";
3597
+ import path11 from "path";
3598
+ var TEMPLATES_DIR = path11.join(os5.homedir(), ".config", "squadrant", "templates");
3599
+ var STATE_ROOT = path11.join(os5.homedir(), ".config", "squadrant", "state");
3421
3600
 
3422
3601
  // packages/core/dist/lifecycle-source.js
3423
3602
  function reduceLifecycle(prev, next) {
@@ -3449,28 +3628,28 @@ import { execSync as execSync4 } from "child_process";
3449
3628
  import { execSync as execSync5 } from "child_process";
3450
3629
 
3451
3630
  // packages/agents/dist/drivers/launch-cmd.js
3452
- import fs13 from "fs";
3453
- import path11 from "path";
3631
+ import fs14 from "fs";
3632
+ import path12 from "path";
3454
3633
 
3455
3634
  // packages/agents/dist/projection/cursor.js
3456
3635
  import { mkdir, readFile, writeFile } from "fs/promises";
3457
- import path12 from "path";
3458
- import os5 from "os";
3636
+ import path13 from "path";
3637
+ import os6 from "os";
3459
3638
 
3460
3639
  // packages/agents/dist/projection/codex.js
3461
3640
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
3462
- import path13 from "path";
3463
- import os6 from "os";
3641
+ import path14 from "path";
3642
+ import os7 from "os";
3464
3643
 
3465
3644
  // packages/agents/dist/projection/gemini.js
3466
3645
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
3467
- import path14 from "path";
3468
- import os7 from "os";
3646
+ import path15 from "path";
3647
+ import os8 from "os";
3469
3648
 
3470
3649
  // packages/agents/dist/projection/opencode.js
3471
3650
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
3472
- import path15 from "path";
3473
- import os8 from "os";
3651
+ import path16 from "path";
3652
+ import os9 from "os";
3474
3653
 
3475
3654
  // packages/agents/dist/codex/app-server-client.js
3476
3655
  import { EventEmitter } from "events";
@@ -4237,7 +4416,7 @@ var OpencodeSseBridge = class {
4237
4416
 
4238
4417
  // packages/agents/dist/interactive/claude.js
4239
4418
  import { execSync as execSync6 } from "child_process";
4240
- import { readFileSync as readFileSync8 } from "fs";
4419
+ import { readFileSync as readFileSync9 } from "fs";
4241
4420
  import { homedir as homedir9 } from "os";
4242
4421
  import { join as join14 } from "path";
4243
4422
 
@@ -4863,13 +5042,45 @@ function createCmuxDriver() {
4863
5042
  };
4864
5043
  }
4865
5044
 
5045
+ // packages/workspaces/dist/runtimes/registry.js
5046
+ var DEFAULT_RUNTIME = "cmux";
5047
+ var RuntimeRegistry = class {
5048
+ drivers;
5049
+ constructor(drivers) {
5050
+ this.drivers = drivers;
5051
+ }
5052
+ forProject(projectName, config) {
5053
+ const projectRuntime = config.projects[projectName]?.runtime;
5054
+ const runtimeName = projectRuntime ?? config.runtime ?? DEFAULT_RUNTIME;
5055
+ return this.get(runtimeName);
5056
+ }
5057
+ global(config) {
5058
+ const runtimeName = config.runtime ?? DEFAULT_RUNTIME;
5059
+ return this.get(runtimeName);
5060
+ }
5061
+ get(name) {
5062
+ const driver = this.drivers[name];
5063
+ if (!driver) {
5064
+ throw new Error(`Unknown runtime '${name}' \u2014 no driver registered`);
5065
+ }
5066
+ return driver;
5067
+ }
5068
+ async probeAll() {
5069
+ const results = {};
5070
+ for (const [name, driver] of Object.entries(this.drivers)) {
5071
+ results[name] = await driver.probe();
5072
+ }
5073
+ return results;
5074
+ }
5075
+ };
5076
+
4866
5077
  // packages/workspaces/dist/notifiers/cmux.js
4867
5078
  import { execFileSync as execFileSync6, execSync as execSync7 } from "child_process";
4868
5079
 
4869
5080
  // packages/workspaces/dist/workspaces/obsidian.js
4870
- import fs14 from "fs/promises";
5081
+ import fs15 from "fs/promises";
4871
5082
  import { existsSync as existsSync9 } from "fs";
4872
- import path16 from "path";
5083
+ import path17 from "path";
4873
5084
 
4874
5085
  // packages/workspaces/dist/cmux-daemon/events-bridge.js
4875
5086
  import { spawn as nodeSpawn2 } from "child_process";
@@ -5005,6 +5216,71 @@ var CmuxEventsBridge = class {
5005
5216
  }
5006
5217
  };
5007
5218
 
5219
+ // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
5220
+ import { readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
5221
+ import { join as join15 } from "path";
5222
+ import { homedir as homedir10 } from "os";
5223
+
5224
+ // packages/workspaces/dist/cmux-daemon/store-fingerprint.js
5225
+ function roleFromTemplate(args) {
5226
+ const i = args?.indexOf("--append-system-prompt-file") ?? -1;
5227
+ const tmpl = i >= 0 && args ? (args[i + 1] ?? "").split("/").pop() ?? "" : "";
5228
+ if (tmpl.startsWith("captain"))
5229
+ return "captain";
5230
+ if (tmpl.startsWith("crew"))
5231
+ return "crew";
5232
+ if (tmpl.startsWith("command"))
5233
+ return "command";
5234
+ return "unknown";
5235
+ }
5236
+ function projectFromCwd(cwd, projects) {
5237
+ for (const [name, p] of Object.entries(projects)) {
5238
+ const projPath = resolveHome(p.path);
5239
+ if (cwd === projPath || cwd.startsWith(`${projPath}/`))
5240
+ return name;
5241
+ }
5242
+ return void 0;
5243
+ }
5244
+ function parseStoreRecords(fileContent, projects) {
5245
+ let parsed;
5246
+ try {
5247
+ parsed = JSON.parse(fileContent);
5248
+ } catch (e) {
5249
+ throw new Error(`parseStoreRecords: invalid JSON: ${e.message}`);
5250
+ }
5251
+ const out = [];
5252
+ for (const s of Object.values(parsed.sessions ?? {})) {
5253
+ const cwd = s.cwd ?? s.launchCommand?.workingDirectory ?? "";
5254
+ const project = projectFromCwd(cwd, projects);
5255
+ if (!project || !s.sessionId)
5256
+ continue;
5257
+ out.push({
5258
+ role: roleFromTemplate(s.launchCommand?.arguments),
5259
+ project,
5260
+ pid: typeof s.pid === "number" ? s.pid : null,
5261
+ sessionId: s.sessionId,
5262
+ present: true,
5263
+ isRestorable: s.isRestorable
5264
+ });
5265
+ }
5266
+ return out;
5267
+ }
5268
+ function readLivenessSnapshot(files, readFile6, projects) {
5269
+ const out = [];
5270
+ let successes = 0;
5271
+ for (const f of files) {
5272
+ try {
5273
+ out.push(...parseStoreRecords(readFile6(f), projects));
5274
+ successes++;
5275
+ } catch {
5276
+ }
5277
+ }
5278
+ if (files.length > 0 && successes === 0) {
5279
+ throw new Error(`readLivenessSnapshot: all ${files.length} store file(s) unreadable/corrupt this tick`);
5280
+ }
5281
+ return out;
5282
+ }
5283
+
5008
5284
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
5009
5285
  var DaemonCmux = class {
5010
5286
  driver;
@@ -5056,12 +5332,28 @@ var DaemonCmux = class {
5056
5332
  return false;
5057
5333
  }
5058
5334
  }
5335
+ /**
5336
+ * Ground-truth liveness from cmux's own hook-sessions store (§5.4).
5337
+ * THROWS (does not return []) when the dir can't be listed, or every store
5338
+ * file failed to read/parse — see the class doc above.
5339
+ */
5340
+ async liveness() {
5341
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
5342
+ const projects = loadConfig().projects;
5343
+ let files;
5344
+ try {
5345
+ files = readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
5346
+ } catch (e) {
5347
+ throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
5348
+ }
5349
+ return readLivenessSnapshot(files, (f) => readFileSync10(join15(dir, f), "utf-8"), projects);
5350
+ }
5059
5351
  };
5060
5352
 
5061
5353
  // 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";
5354
+ import { join as join16 } from "path";
5355
+ import { homedir as homedir11 } from "os";
5356
+ import { watch, readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
5065
5357
  var CmuxStoreSource = class {
5066
5358
  name = "cmux-store";
5067
5359
  stateDir;
@@ -5082,7 +5374,7 @@ var CmuxStoreSource = class {
5082
5374
  active = false;
5083
5375
  lastError = null;
5084
5376
  constructor(opts = {}) {
5085
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
5377
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join16(homedir11(), ".cmuxterm");
5086
5378
  this.debounceMs = opts.debounceMs ?? 50;
5087
5379
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
5088
5380
  this.listFiles = opts.listFiles ?? defaultListFiles;
@@ -5144,7 +5436,7 @@ var CmuxStoreSource = class {
5144
5436
  }
5145
5437
  scanFile(filename) {
5146
5438
  const deps = this.deps;
5147
- const filePath = join15(this.stateDir, filename);
5439
+ const filePath = join16(this.stateDir, filename);
5148
5440
  const lockPath = `${filePath}.lock`;
5149
5441
  if (this.fileExists(lockPath)) {
5150
5442
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -5210,14 +5502,14 @@ function defaultIsPidAlive2(pid) {
5210
5502
  }
5211
5503
  function defaultListFiles(dir) {
5212
5504
  try {
5213
- return readdirSync3(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
5505
+ return readdirSync4(dir).filter((f) => f.endsWith("-hook-sessions.json") && !f.endsWith(".lock"));
5214
5506
  } catch {
5215
5507
  return [];
5216
5508
  }
5217
5509
  }
5218
- function defaultReadFile(path17) {
5510
+ function defaultReadFile(path19) {
5219
5511
  try {
5220
- return readFileSync9(path17, "utf-8");
5512
+ return readFileSync11(path19, "utf-8");
5221
5513
  } catch {
5222
5514
  return void 0;
5223
5515
  }
@@ -5232,9 +5524,9 @@ function defaultWatchDir(dir, cb) {
5232
5524
  }
5233
5525
 
5234
5526
  // 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";
5527
+ import { join as join17 } from "path";
5528
+ import { homedir as homedir12 } from "os";
5529
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
5238
5530
  var CLAUDE_HOOK_EVENTS = [
5239
5531
  ["SessionStart", "session-start"],
5240
5532
  ["UserPromptSubmit", "prompt-submit"],
@@ -5246,7 +5538,7 @@ var CLAUDE_HOOK_EVENTS = [
5246
5538
  ];
5247
5539
  var DEFAULT_HOOK_CMD = "squadrant hooks";
5248
5540
  function installClaudeHooks(opts = {}) {
5249
- const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
5541
+ const settingsPath = opts.settingsPath ?? join17(homedir12(), ".claude", "settings.json");
5250
5542
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
5251
5543
  const readFile6 = opts.readFile ?? defaultReadFile2;
5252
5544
  const writeFile5 = opts.writeFile ?? defaultWriteFile;
@@ -5392,16 +5684,16 @@ function extractDetail(sub, payload) {
5392
5684
  }
5393
5685
  return void 0;
5394
5686
  }
5395
- function defaultReadFile2(path17) {
5687
+ function defaultReadFile2(path19) {
5396
5688
  try {
5397
- return readFileSync10(path17, "utf-8");
5689
+ return readFileSync12(path19, "utf-8");
5398
5690
  } catch {
5399
5691
  return void 0;
5400
5692
  }
5401
5693
  }
5402
- function defaultWriteFile(path17, content) {
5403
- mkdirSync6(path17.replace(/\/[^/]+$/, ""), { recursive: true });
5404
- writeFileSync7(path17, content, "utf-8");
5694
+ function defaultWriteFile(path19, content) {
5695
+ mkdirSync6(path19.replace(/\/[^/]+$/, ""), { recursive: true });
5696
+ writeFileSync8(path19, content, "utf-8");
5405
5697
  }
5406
5698
 
5407
5699
  // packages/workspaces/dist/crew-pane.js
@@ -5427,6 +5719,9 @@ async function settleInputBox(runtime, pane) {
5427
5719
  }
5428
5720
  async function confirmedSendToPane(runtime, pane, message) {
5429
5721
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
5722
+ if (hasModalOptionList(preSendScreen)) {
5723
+ return { delivered: false, blockedByModal: true };
5724
+ }
5430
5725
  await runtime.pasteToPane(pane, message);
5431
5726
  let sawDraft = await settleInputBox(runtime, pane);
5432
5727
  await runtime.sendKeyToPane(pane, "Enter");
@@ -5468,14 +5763,66 @@ async function resendCrewFirstTurn(runtime, captainName, project, name, message)
5468
5763
  return confirmedSendToPane(runtime, pane, message);
5469
5764
  }
5470
5765
 
5766
+ // packages/cli/src/lib/daemon-restart-broadcast.ts
5767
+ import fs16 from "fs";
5768
+ import path18 from "path";
5769
+ function statePath2(stateRoot) {
5770
+ return path18.join(stateRoot, "daemon-restart-state.json");
5771
+ }
5772
+ function computeRestartSignature(version, buildMtimeMs) {
5773
+ return `${version}::${buildMtimeMs}`;
5774
+ }
5775
+ function readPersistedRestartSignature(stateRoot) {
5776
+ try {
5777
+ const raw = fs16.readFileSync(statePath2(stateRoot), "utf-8");
5778
+ const data = JSON.parse(raw);
5779
+ return typeof data.signature === "string" ? data.signature : null;
5780
+ } catch {
5781
+ return null;
5782
+ }
5783
+ }
5784
+ function writePersistedRestartSignature(stateRoot, signature) {
5785
+ fs16.mkdirSync(stateRoot, { recursive: true });
5786
+ fs16.writeFileSync(statePath2(stateRoot), JSON.stringify({ signature }, null, 2) + "\n");
5787
+ }
5788
+ function restartNotice(version, isDevRebuild) {
5789
+ const suffix = isDevRebuild ? " (dev build)" : "";
5790
+ 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.`;
5791
+ }
5792
+ async function notifyCaptainsOfDaemonRestart(version, config, driver, isDevRebuild = false, appendCaptainMessage2) {
5793
+ const notice = restartNotice(version, isDevRebuild);
5794
+ for (const [projName] of Object.entries(config.projects)) {
5795
+ try {
5796
+ const proj = config.projects[projName];
5797
+ const ref = await driver.status(proj.captainName);
5798
+ if (ref) {
5799
+ await appendCaptainMessage2(projName, notice);
5800
+ }
5801
+ } catch {
5802
+ }
5803
+ }
5804
+ }
5805
+ async function maybeBroadcastDaemonRestart(opts) {
5806
+ try {
5807
+ const { version, buildMtimeMs, stateRoot, config, driver, appendCaptainMessage: appendCaptainMessage2 } = opts;
5808
+ const signature = computeRestartSignature(version, buildMtimeMs);
5809
+ const previous = readPersistedRestartSignature(stateRoot);
5810
+ if (previous === signature) return;
5811
+ const isDevRebuild = previous !== null && previous.split("::")[0] === version;
5812
+ await notifyCaptainsOfDaemonRestart(version, config, driver, isDevRebuild, appendCaptainMessage2);
5813
+ writePersistedRestartSignature(stateRoot, signature);
5814
+ } catch {
5815
+ }
5816
+ }
5817
+
5471
5818
  // packages/cli/src/squadrantd.ts
5472
5819
  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");
5820
+ var CLI_BIN = join18(dirname5(SELF_PATH2), "index.js");
5821
+ var DAEMON_SOCK = join18(homedir13(), ".config", "squadrant", "squadrant.sock");
5475
5822
  function readPkgVersion() {
5476
5823
  try {
5477
- const pkgPath = join17(dirname5(SELF_PATH2), "..", "package.json");
5478
- return JSON.parse(readFileSync11(pkgPath, "utf-8")).version ?? "unknown";
5824
+ const pkgPath = join18(dirname5(SELF_PATH2), "..", "package.json");
5825
+ return JSON.parse(readFileSync13(pkgPath, "utf-8")).version ?? "unknown";
5479
5826
  } catch {
5480
5827
  return "unknown";
5481
5828
  }
@@ -5490,7 +5837,7 @@ function buildTelegramBridge(cfg, stateRoot, log) {
5490
5837
  const client = createTelegramClient({ token });
5491
5838
  const ensureCaptainAlive = createEnsureCaptainAlive({
5492
5839
  isAlive: createIsCaptainAlive(DAEMON_SOCK),
5493
- launch: createLaunch(CLI_BIN)
5840
+ launch: createLaunch(CLI_BIN, log)
5494
5841
  });
5495
5842
  const runCommand = createRunCommand(CLI_BIN);
5496
5843
  const sendReply = (threadId, text, replyMarkup) => client.sendMessage(cfg.supergroupId, threadId, text, replyMarkup);
@@ -5558,7 +5905,7 @@ function startSquadrantd(opts = {}) {
5558
5905
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
5559
5906
  );
5560
5907
  },
5561
- cursorFile: join17(stateRoot, "cmux-events.seq"),
5908
+ cursorFile: join18(stateRoot, "cmux-events.seq"),
5562
5909
  log
5563
5910
  });
5564
5911
  const cmuxStoreSource = new CmuxStoreSource({ log });
@@ -5713,8 +6060,25 @@ ${buildCompletionProtocol(rec.id, rec.project)}`;
5713
6060
  log(`codex app-server source start failed: ${e.message}`);
5714
6061
  }
5715
6062
  }
6063
+ if (!process.env.VITEST) {
6064
+ try {
6065
+ const buildMtimeMs = statSync3(SELF_PATH2).mtimeMs;
6066
+ const restartConfig = loadConfig();
6067
+ const registry = new RuntimeRegistry({ cmux: createCmuxDriver() });
6068
+ void maybeBroadcastDaemonRestart({
6069
+ version: PKG_VERSION,
6070
+ buildMtimeMs,
6071
+ stateRoot,
6072
+ config: restartConfig,
6073
+ driver: registry.global(restartConfig),
6074
+ appendCaptainMessage: (project, text) => appendCaptainMessage({ stateRoot, project, text, source: "daemon" })
6075
+ });
6076
+ } catch (e) {
6077
+ log(`daemon-restart broadcast setup failed: ${e.message}`);
6078
+ }
6079
+ }
5716
6080
  const origStop = h.stop.bind(h);
5717
- h.stop = async () => {
6081
+ h.stop = async (reason) => {
5718
6082
  try {
5719
6083
  cmuxStoreSource.stop();
5720
6084
  } catch {
@@ -5727,28 +6091,42 @@ ${buildCompletionProtocol(rec.id, rec.project)}`;
5727
6091
  codexAppServerSource.stop();
5728
6092
  } catch {
5729
6093
  }
5730
- return origStop();
6094
+ return origStop(reason);
5731
6095
  };
5732
6096
  return h;
5733
6097
  }
6098
+ function logCrashMarker(kind, err) {
6099
+ const message = err instanceof Error ? err.stack ?? err.message : String(err);
6100
+ process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${kind} pid=${process.pid} error=${message}
6101
+ `);
6102
+ }
5734
6103
  if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
6104
+ process.on("uncaughtException", (err) => {
6105
+ logCrashMarker("uncaughtException", err);
6106
+ process.exit(1);
6107
+ });
6108
+ process.on("unhandledRejection", (reason) => {
6109
+ logCrashMarker("unhandledRejection", reason);
6110
+ process.exit(1);
6111
+ });
5735
6112
  void (async () => {
5736
6113
  const arg = process.argv[2];
5737
6114
  if (arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") {
5738
6115
  process.stdout.write("squadrantd: launchd-managed daemon entry (no CLI args). Use `squadrant` for commands.\n");
5739
6116
  process.exit(0);
5740
6117
  }
5741
- const sock = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
6118
+ const sock = join18(homedir13(), ".config", "squadrant", "squadrant.sock");
5742
6119
  if (await isDaemonSocketLive(sock)) {
5743
6120
  process.stderr.write(`[squadrantd] refusing to start: a live daemon already owns ${sock}
5744
6121
  `);
5745
6122
  process.exit(0);
5746
6123
  }
5747
6124
  const h = startSquadrantd({ sweepMs: 3e4 });
5748
- process.on("SIGTERM", () => {
5749
- h.stop();
5750
- process.exit(0);
5751
- });
6125
+ const shutdown = (signal) => {
6126
+ void h.stop(signal).finally(() => process.exit(0));
6127
+ };
6128
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
6129
+ process.on("SIGINT", () => shutdown("SIGINT"));
5752
6130
  })();
5753
6131
  }
5754
6132
  export {