squadrant 0.14.2 → 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,
@@ -588,6 +591,8 @@ function recoverStall(rec, now) {
588
591
  }
589
592
 
590
593
  // packages/core/dist/daemon/reduce.js
594
+ var DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS = 12e4;
595
+ var DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS = 6e4;
591
596
  var DEFAULT_TASK_TIMEOUT_MS = 8 * 60 * 60 * 1e3;
592
597
  var TERMINAL_RECORD_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
593
598
  var TERMINAL_RECORD_KEEP_PER_PROJECT = 20;
@@ -708,6 +713,71 @@ function createDaemon(deps) {
708
713
  const { store, now } = deps;
709
714
  const lastCaptainTurnAt = /* @__PURE__ */ new Map();
710
715
  const quietNotifiedAt = /* @__PURE__ */ new Map();
716
+ const resendAttemptedAt = /* @__PURE__ */ new Map();
717
+ const firstTurnUndeliveredBudgetMs = deps.firstTurnUndeliveredBudgetMs ?? DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS;
718
+ const firstTurnResendCooldownMs = deps.firstTurnResendCooldownMs ?? DEFAULT_FIRST_TURN_RESEND_COOLDOWN_MS;
719
+ async function applyEvent(project, event) {
720
+ if (!KNOWN_EVENT_TYPES.has(event.type)) {
721
+ throw new Error(`unknown event type '${event.type}' \u2014 not a valid ControlEvent`);
722
+ }
723
+ const cur = store.get(project, event.id);
724
+ if (!cur)
725
+ throw new Error(`unknown task ${event.id}`);
726
+ if (event.type === "task.started")
727
+ lastCaptainTurnAt.set(event.id, now());
728
+ if (event.type === "task.session.ended" && !TERMINAL_STATES.has(cur.state)) {
729
+ const liveness = deps.isSurfaceAlive ? await deps.isSurfaceAlive(cur) : "unknown";
730
+ if (liveness !== "gone")
731
+ return cur;
732
+ }
733
+ const next = reduce(cur, event, now());
734
+ if (next !== cur) {
735
+ store.put(next);
736
+ firePush(deps, project, cur.state, next, event, lastCaptainTurnAt.get(next.id));
737
+ }
738
+ return next;
739
+ }
740
+ async function attemptFirstTurnRecovery(r, undeliveredMs) {
741
+ const lastAttempt = resendAttemptedAt.get(r.id);
742
+ if (lastAttempt != null && now() - lastAttempt < firstTurnResendCooldownMs)
743
+ return;
744
+ resendAttemptedAt.set(r.id, now());
745
+ const fresh = store.get(r.project, r.id);
746
+ if (!fresh || fresh.firstTurnConfirmedAt)
747
+ return;
748
+ const tag = crewTag(fresh);
749
+ const fireNotify = (message) => {
750
+ if (!deps.notify)
751
+ return;
752
+ const synthEvent = { type: "task.quiet", id: fresh.id, quietMs: undeliveredMs };
753
+ try {
754
+ const p = deps.notify({ project: fresh.project, message, record: store.get(fresh.project, fresh.id) ?? fresh, event: synthEvent });
755
+ if (p && typeof p.catch === "function")
756
+ p.catch(() => {
757
+ });
758
+ } catch {
759
+ }
760
+ };
761
+ if (!deps.resendFirstTurn) {
762
+ fireNotify(`\u26A0\uFE0F CREW UNDELIVERED ${tag}: first turn may not have landed (0 activity) \u2014 re-send the task or check the spawn.`);
763
+ return;
764
+ }
765
+ let result;
766
+ try {
767
+ result = await deps.resendFirstTurn(fresh);
768
+ } catch {
769
+ result = { delivered: false };
770
+ }
771
+ if (result.delivered) {
772
+ try {
773
+ await applyEvent(fresh.project, { type: "task.first-turn.confirmed", id: fresh.id });
774
+ } catch {
775
+ }
776
+ fireNotify(`\u{1F501} CREW FIRST-TURN AUTO-RESENT ${tag}: first turn had not landed after ${Math.round(undeliveredMs / 1e3)}s \u2014 re-sent automatically.`);
777
+ } else {
778
+ fireNotify(`\u26A0\uFE0F CREW UNDELIVERED ${tag}: first turn may not have landed \u2014 auto-resend attempted but the pane wasn't ready; will retry.`);
779
+ }
780
+ }
711
781
  return {
712
782
  async handle(req) {
713
783
  switch (req.kind) {
@@ -754,22 +824,7 @@ function createDaemon(deps) {
754
824
  if (!KNOWN_EVENT_TYPES.has(req.event.type)) {
755
825
  throw new Error(`unknown event type '${req.event.type}' \u2014 not a valid ControlEvent`);
756
826
  }
757
- const cur = store.get(req.project, req.event.id);
758
- if (!cur)
759
- throw new Error(`unknown task ${req.event.id}`);
760
- if (req.event.type === "task.started")
761
- lastCaptainTurnAt.set(req.event.id, now());
762
- if (req.event.type === "task.session.ended" && !TERMINAL_STATES.has(cur.state)) {
763
- const liveness = deps.isSurfaceAlive ? await deps.isSurfaceAlive(cur) : "unknown";
764
- if (liveness !== "gone")
765
- return cur;
766
- }
767
- const next = reduce(cur, req.event, now());
768
- if (next !== cur) {
769
- store.put(next);
770
- firePush(deps, req.project, cur.state, next, req.event, lastCaptainTurnAt.get(next.id));
771
- }
772
- return next;
827
+ return applyEvent(req.project, req.event);
773
828
  }
774
829
  case "status": {
775
830
  const r = store.get(req.project, req.id);
@@ -828,6 +883,7 @@ function createDaemon(deps) {
828
883
  store.delete(r.project, r.id);
829
884
  }
830
885
  }
886
+ const recoveryPromises = [];
831
887
  for (const r of store.listAll()) {
832
888
  if (TERMINAL_STATES.has(r.state) && t - r.lastHeartbeat > TERMINAL_RECORD_TTL_MS) {
833
889
  store.delete(r.project, r.id);
@@ -869,21 +925,9 @@ function createDaemon(deps) {
869
925
  }
870
926
  }
871
927
  if (r.mode === "interactive" && r.state === "submitted" && !r.firstTurnConfirmedAt) {
872
- const elapsed = t - r.lastHeartbeat;
873
- if (elapsed > r.heartbeatBudgetMs) {
874
- if (deps.notify && quietNotifiedAt.get(r.id) !== r.lastHeartbeat) {
875
- quietNotifiedAt.set(r.id, r.lastHeartbeat);
876
- const tag = crewTag(r);
877
- const synthEvent = { type: "task.quiet", id: r.id, quietMs: elapsed };
878
- const message = `\u26A0\uFE0F CREW UNDELIVERED ${tag}: first turn may not have landed (crew never started) \u2014 re-send the task or check the spawn.`;
879
- try {
880
- const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
881
- if (p && typeof p.catch === "function")
882
- p.catch(() => {
883
- });
884
- } catch {
885
- }
886
- }
928
+ const undeliveredMs = t - r.createdAt;
929
+ if (undeliveredMs > firstTurnUndeliveredBudgetMs) {
930
+ recoveryPromises.push(attemptFirstTurnRecovery(r, undeliveredMs));
887
931
  continue;
888
932
  }
889
933
  }
@@ -894,7 +938,14 @@ function createDaemon(deps) {
894
938
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
895
939
  continue;
896
940
  }
897
- if (r.mode === "interactive" && r.state === "working" && !r.pendingTool) {
941
+ if (r.mode === "interactive" && r.state === "working" && !r.pendingTool && !r.firstTurnConfirmedAt) {
942
+ const undeliveredMs = t - r.createdAt;
943
+ if (undeliveredMs > firstTurnUndeliveredBudgetMs) {
944
+ recoveryPromises.push(attemptFirstTurnRecovery(r, undeliveredMs));
945
+ continue;
946
+ }
947
+ }
948
+ if (r.mode === "interactive" && r.state === "working" && !r.pendingTool && r.firstTurnConfirmedAt) {
898
949
  const liveness = r.attempts.at(-1)?.lastHeartbeatAt ?? r.lastHeartbeat;
899
950
  const quiet = t - liveness;
900
951
  if (quiet > r.heartbeatBudgetMs) {
@@ -902,13 +953,8 @@ function createDaemon(deps) {
902
953
  quietNotifiedAt.set(r.id, liveness);
903
954
  const tag = crewTag(r);
904
955
  const synthEvent = { type: "task.quiet", id: r.id, quietMs: quiet };
905
- let message;
906
- if (!r.firstTurnConfirmedAt) {
907
- message = `\u26A0\uFE0F CREW UNDELIVERED ${tag}: first turn may not have landed (0 activity) \u2014 re-send the task or check the spawn.`;
908
- } else {
909
- const mins = Math.max(1, Math.round(quiet / 6e4));
910
- message = `CREW QUIET ${tag}: working ~${mins}min with no tool activity \u2014 likely deep thinking (no reply expected yet).`;
911
- }
956
+ const mins = Math.max(1, Math.round(quiet / 6e4));
957
+ const message = `CREW QUIET ${tag}: working ~${mins}min with no tool activity \u2014 likely deep thinking (no reply expected yet).`;
912
958
  try {
913
959
  const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
914
960
  if (p && typeof p.catch === "function")
@@ -926,6 +972,7 @@ function createDaemon(deps) {
926
972
  if (recovered && t - r.lastHeartbeat <= r.heartbeatBudgetMs)
927
973
  store.put(recovered);
928
974
  }
975
+ await Promise.all(recoveryPromises);
929
976
  },
930
977
  async reconcile() {
931
978
  const alive = deps.isPidAlive ?? (() => true);
@@ -1399,14 +1446,14 @@ var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
1399
1446
  function projectHealth(input) {
1400
1447
  const { project, now, captainName, captainStopped, commandPresent, crews } = input;
1401
1448
  const out = [];
1402
- const captainState = captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown";
1449
+ const captainState = input.captainState ?? (captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown");
1403
1450
  out.push({
1404
1451
  kind: "captain",
1405
1452
  project,
1406
1453
  ref: captainName,
1407
1454
  state: captainState,
1408
1455
  lastSeenMs: null,
1409
- 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
1410
1457
  });
1411
1458
  if (commandPresent !== null) {
1412
1459
  out.push({
@@ -1437,6 +1484,26 @@ function presence(p) {
1437
1484
  return "unknown";
1438
1485
  return p ? "alive" : "gone";
1439
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
+ }
1440
1507
 
1441
1508
  // packages/core/dist/store.js
1442
1509
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, rmSync as rmSync3, statSync } from "fs";
@@ -1595,7 +1662,73 @@ function makeGate(opts) {
1595
1662
  import { homedir as homedir5 } from "os";
1596
1663
  import { join as join8 } from "path";
1597
1664
  import { spawn as realSpawn } from "child_process";
1598
- 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
1599
1732
  function defaultIsPidAlive(pid) {
1600
1733
  try {
1601
1734
  process.kill(pid, 0);
@@ -1616,7 +1749,7 @@ function buildContext(opts) {
1616
1749
  mkdirSync5(resultsDir, { recursive: true });
1617
1750
  const writeResult = (id, payload) => {
1618
1751
  const p = join8(resultsDir, `${id}.txt`);
1619
- writeFileSync6(p, payload);
1752
+ writeFileSync7(p, payload);
1620
1753
  return p;
1621
1754
  };
1622
1755
  const log = (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
@@ -1637,8 +1770,12 @@ function buildContext(opts) {
1637
1770
  attachConns: /* @__PURE__ */ new Map(),
1638
1771
  inFlightHeadlessIds: /* @__PURE__ */ new Set(),
1639
1772
  activeHeadlessKills: /* @__PURE__ */ new Set(),
1640
- captainMissingStreak: /* @__PURE__ */ new Map(),
1641
- stoppedProjects: /* @__PURE__ */ new Set(),
1773
+ livenessRegistry: (() => {
1774
+ const r = new LivenessRegistry({ path: join8(stateRoot, "liveness.json") });
1775
+ r.load();
1776
+ return r;
1777
+ })(),
1778
+ resendFirstTurn: opts.resendFirstTurn,
1642
1779
  // Late-bound — start.ts fills these before first use:
1643
1780
  d: null,
1644
1781
  notify: null,
@@ -1991,7 +2128,6 @@ var CaptainDelivery = class {
1991
2128
 
1992
2129
  // packages/core/dist/daemon/delivery-loop.js
1993
2130
  var CURSOR_SUBSCRIBER = "captain";
1994
- var CAPTAIN_GONE_STREAK_K = 3;
1995
2131
  var TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
1996
2132
  function discoverCaptainSurface(surfaces, captainTitle) {
1997
2133
  return surfaces.find((s) => s.title === captainTitle) ?? null;
@@ -2008,8 +2144,62 @@ function reapOrphanedCrews(store, project) {
2008
2144
  }
2009
2145
  return reaped;
2010
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
+ }
2011
2201
  function createDelivery(ctx, daemonCmux) {
2012
- const { stateRoot, store, log, captainMissingStreak, stoppedProjects, opts } = ctx;
2202
+ const { stateRoot, store, log, livenessRegistry, isPidAlive, opts } = ctx;
2013
2203
  const defaultNotify = async (args) => {
2014
2204
  try {
2015
2205
  await appendToMailbox({
@@ -2035,6 +2225,21 @@ function createDelivery(ctx, daemonCmux) {
2035
2225
  const sessionStartMs = Date.now();
2036
2226
  let delivering = false;
2037
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
+ });
2038
2243
  const injectedSurfaces = opts.captainSurfaces ?? {};
2039
2244
  const allProjects = [.../* @__PURE__ */ new Set([
2040
2245
  ...Object.keys(cfg.projects ?? {}),
@@ -2046,34 +2251,14 @@ function createDelivery(ctx, daemonCmux) {
2046
2251
  const captainTitle = projCfg?.captainName ?? `${project}-captain`;
2047
2252
  const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
2048
2253
  let surface = null;
2049
- let surfacesLength = 0;
2050
2254
  if (wsId) {
2051
2255
  const surfaces = await cmux2.listSurfaces(wsId);
2052
- surfacesLength = surfaces.length;
2053
2256
  surface = discoverCaptainSurface(surfaces, captainTitle);
2054
2257
  }
2055
2258
  if (!surface)
2056
2259
  surface = injectedSurfaces[project] ?? null;
2057
- if (surface) {
2058
- if (stoppedProjects.has(project)) {
2059
- stoppedProjects.delete(project);
2060
- captainMissingStreak.set(project, 0);
2061
- }
2062
- captainMissingStreak.set(project, 0);
2063
- } else {
2064
- if (surfacesLength > 0) {
2065
- const streak = (captainMissingStreak.get(project) ?? 0) + 1;
2066
- captainMissingStreak.set(project, streak);
2067
- if (streak >= CAPTAIN_GONE_STREAK_K) {
2068
- if (!stoppedProjects.has(project)) {
2069
- stoppedProjects.add(project);
2070
- const reaped = reapOrphanedCrews(store, project);
2071
- log(`captain ${captainTitle}: surface gone for ${CAPTAIN_GONE_STREAK_K} sweeps \u2014 stopping delivery${reaped > 0 ? `, reaped ${reaped} orphaned crew(s)` : ""}`);
2072
- }
2073
- }
2074
- }
2260
+ if (!surface)
2075
2261
  continue;
2076
- }
2077
2262
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
2078
2263
  const lastAcked = cursor?.lastAckedSeq ?? 0;
2079
2264
  let d = deliveries.get(project);
@@ -2195,7 +2380,7 @@ function createServer2(ctx, handlers) {
2195
2380
  // packages/core/dist/daemon/snapshot-gather.js
2196
2381
  import { fileURLToPath as fileURLToPath2 } from "url";
2197
2382
  import { join as join9 } from "path";
2198
- 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";
2199
2384
  var SELF_PATH = fileURLToPath2(import.meta.url);
2200
2385
  function distBuiltAt() {
2201
2386
  try {
@@ -2204,10 +2389,10 @@ function distBuiltAt() {
2204
2389
  return 0;
2205
2390
  }
2206
2391
  }
2207
- function gatherLogStats(path17, now, windowMs) {
2392
+ function gatherLogStats(path18, now, windowMs) {
2208
2393
  let sizeBytes = 0;
2209
2394
  try {
2210
- sizeBytes = statSync2(path17).size;
2395
+ sizeBytes = statSync2(path18).size;
2211
2396
  } catch {
2212
2397
  return { errorCount: 0, sizeBytes: 0, windowMs };
2213
2398
  }
@@ -2218,7 +2403,7 @@ function gatherLogStats(path17, now, windowMs) {
2218
2403
  const len = sizeBytes - start;
2219
2404
  let text = "";
2220
2405
  try {
2221
- const fd = openSync2(path17, "r");
2406
+ const fd = openSync2(path18, "r");
2222
2407
  try {
2223
2408
  const buf = Buffer.alloc(len);
2224
2409
  readSync(fd, buf, 0, len, start);
@@ -2259,7 +2444,7 @@ function gatherStoreStats(store, stateRoot, project) {
2259
2444
  if (!n.endsWith(".json"))
2260
2445
  continue;
2261
2446
  try {
2262
- JSON.parse(readFileSync7(join9(dir, n), "utf-8"));
2447
+ JSON.parse(readFileSync8(join9(dir, n), "utf-8"));
2263
2448
  } catch {
2264
2449
  corruptCount++;
2265
2450
  }
@@ -2309,6 +2494,7 @@ function startDaemon(ctx, opts, pkgVersion) {
2309
2494
  notify,
2310
2495
  taskTimeoutMs,
2311
2496
  isSurfaceAlive: surfaceProbe,
2497
+ resendFirstTurn: ctx.resendFirstTurn,
2312
2498
  launchHeadless: opts.launchHeadless,
2313
2499
  isHeadlessInFlight: (id) => inFlightHeadlessIds.has(id),
2314
2500
  launchInteractive: async (rec) => {
@@ -2343,14 +2529,13 @@ function startDaemon(ctx, opts, pkgVersion) {
2343
2529
  for (const project of names) {
2344
2530
  const proj = config.projects[project];
2345
2531
  const captainName = proj?.captainName ?? `${project}-captain`;
2346
- const stopped = ctx.stoppedProjects.has(project);
2347
- const streak = ctx.captainMissingStreak.get(project);
2348
- const captainStopped = stopped ? true : streak === 0 ? false : null;
2532
+ const capEntry = ctx.livenessRegistry.get(project);
2349
2533
  out.push(...projectHealth({
2350
2534
  project,
2351
2535
  now,
2352
2536
  captainName,
2353
- captainStopped,
2537
+ captainStopped: null,
2538
+ captainState: deriveCaptainState(capEntry),
2354
2539
  commandPresent: null,
2355
2540
  crews: store.list(project)
2356
2541
  }));
@@ -2549,6 +2734,21 @@ import crypto from "crypto";
2549
2734
  import fs8 from "fs";
2550
2735
  import path7 from "path";
2551
2736
 
2737
+ // packages/core/dist/crew-protocol.js
2738
+ function buildCompletionProtocol(taskId, project) {
2739
+ return [
2740
+ "---",
2741
+ "COMPLETION PROTOCOL (required): When this task is fully complete, your FINAL action MUST be to run exactly:",
2742
+ ` squadrant crew signal done --task-id ${taskId} --project ${project} --message "<one-line summary>"`,
2743
+ "Run it as a discrete final step AFTER you report your results. If you are blocked or need a decision, instead run:",
2744
+ ` squadrant crew signal blocked --task-id ${taskId} --project ${project} --question "<your question>"`,
2745
+ "If this task failed because of a defect in squadrant itself (not an API/infra blip, a config/user error, or an expected failure), say so in your signal done/blocked message so the captain can check tu11aa/squadrant and file it. Don't file issues from the crew."
2746
+ ].join("\n");
2747
+ }
2748
+ function titleFor(project, name) {
2749
+ return `\u{1F527} ${project}:${name}`;
2750
+ }
2751
+
2552
2752
  // packages/core/dist/crew-lifecycle.js
2553
2753
  import { exec as nodeExec } from "child_process";
2554
2754
 
@@ -2692,21 +2892,33 @@ function createRunCommand(cliBin) {
2692
2892
  }
2693
2893
  };
2694
2894
  }
2895
+ function isCaptainAliveFromHealth(rows, project) {
2896
+ return rows.some((h) => h.kind === "captain" && h.project === project && h.state === "alive");
2897
+ }
2695
2898
  function createIsCaptainAlive(sock) {
2696
2899
  return async (project) => {
2697
2900
  try {
2698
2901
  const health = await sendRequest(sock, { kind: "health", project }, 5e3);
2699
- const captain = health?.find((h) => h.kind === "captain" && h.project === project);
2700
- return captain != null && captain.state !== "gone" && captain.state !== "unknown";
2902
+ return isCaptainAliveFromHealth(health ?? [], project);
2701
2903
  } catch {
2702
2904
  return false;
2703
2905
  }
2704
2906
  };
2705
2907
  }
2706
- function createLaunch(cliBin) {
2707
- return async (project) => {
2708
- await pExecFile(process.execPath, [cliBin, "launch", project], { timeout: 3e4 });
2709
- };
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
+ });
2710
2922
  }
2711
2923
 
2712
2924
  // packages/core/dist/telegram/ensure-captain.js
@@ -3248,8 +3460,11 @@ function createTelegramBridge(opts) {
3248
3460
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
3249
3461
  try {
3250
3462
  const r = await ensureCaptainAlive(resolved.project);
3251
- if (r === "timeout")
3252
- 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
+ }
3253
3468
  } catch (e) {
3254
3469
  log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
3255
3470
  }
@@ -4079,7 +4294,7 @@ var OpencodeSseBridge = class {
4079
4294
  const fetchImpl = this.deps.fetchImpl ?? fetch;
4080
4295
  const sleep3 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4081
4296
  const reconnectMs = this.deps.reconnectMs ?? 500;
4082
- const maxBoot = this.deps.maxBootAttempts ?? 60;
4297
+ const maxBoot = this.deps.maxBootAttempts ?? 240;
4083
4298
  const url = `http://127.0.0.1:${port}/event`;
4084
4299
  let booted = false;
4085
4300
  let bootAttempts = 0;
@@ -4176,7 +4391,7 @@ var OpencodeSseBridge = class {
4176
4391
 
4177
4392
  // packages/agents/dist/interactive/claude.js
4178
4393
  import { execSync as execSync6 } from "child_process";
4179
- import { readFileSync as readFileSync8 } from "fs";
4394
+ import { readFileSync as readFileSync9 } from "fs";
4180
4395
  import { homedir as homedir9 } from "os";
4181
4396
  import { join as join14 } from "path";
4182
4397
 
@@ -4433,6 +4648,27 @@ function parseDraftFromScreen(screen) {
4433
4648
  }
4434
4649
  return "";
4435
4650
  }
4651
+ function hasCCInputBox(screen) {
4652
+ if (!screen)
4653
+ return false;
4654
+ const lines = screen.split(/\r?\n/);
4655
+ const HR_RE = /^\s*─{10,}\s*$/;
4656
+ let bottomHR = -1;
4657
+ let topHR = -1;
4658
+ for (let i = lines.length - 1; i >= 0; i--) {
4659
+ if (HR_RE.test(lines[i])) {
4660
+ if (bottomHR === -1)
4661
+ bottomHR = i;
4662
+ else {
4663
+ topHR = i;
4664
+ break;
4665
+ }
4666
+ }
4667
+ }
4668
+ if (topHR === -1)
4669
+ return false;
4670
+ return lines.slice(topHR + 1, bottomHR).some((l) => /[>❯]/.test(l));
4671
+ }
4436
4672
  function hasModalOptionList(screen) {
4437
4673
  if (!screen)
4438
4674
  return false;
@@ -4483,6 +4719,15 @@ function readInputBoxRaw(screen, opts) {
4483
4719
  const joined = parts.join("");
4484
4720
  return opts?.trim === false ? joined : joined.replace(/\s+$/, "");
4485
4721
  }
4722
+ var CC_INITIALIZED_RE = /⏵⏵|Ctx Used|for shortcuts|accept edits/i;
4723
+ var CC_WORKING_RE = /↓\s*[\d.]+\s*k?\s*tokens?\b|esc to interrupt|\bshell still running\b|·\s*\d+\s*shell\b|\(\d+m?\s*\d*s\b/i;
4724
+ function classifyStartupSurface(screen) {
4725
+ if (CC_WORKING_RE.test(screen))
4726
+ return "working";
4727
+ if (CC_INITIALIZED_RE.test(screen))
4728
+ return "idle";
4729
+ return "loading";
4730
+ }
4486
4731
  function sendDebugEnabled() {
4487
4732
  return !!process.env.SQUADRANT_DEBUG_SEND;
4488
4733
  }
@@ -4772,6 +5017,38 @@ function createCmuxDriver() {
4772
5017
  };
4773
5018
  }
4774
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
+
4775
5052
  // packages/workspaces/dist/notifiers/cmux.js
4776
5053
  import { execFileSync as execFileSync6, execSync as execSync7 } from "child_process";
4777
5054
 
@@ -4914,6 +5191,71 @@ var CmuxEventsBridge = class {
4914
5191
  }
4915
5192
  };
4916
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
+
4917
5259
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
4918
5260
  var DaemonCmux = class {
4919
5261
  driver;
@@ -4965,12 +5307,28 @@ var DaemonCmux = class {
4965
5307
  return false;
4966
5308
  }
4967
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
+ }
4968
5326
  };
4969
5327
 
4970
5328
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
4971
- import { join as join15 } from "path";
4972
- import { homedir as homedir10 } from "os";
4973
- 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";
4974
5332
  var CmuxStoreSource = class {
4975
5333
  name = "cmux-store";
4976
5334
  stateDir;
@@ -4991,7 +5349,7 @@ var CmuxStoreSource = class {
4991
5349
  active = false;
4992
5350
  lastError = null;
4993
5351
  constructor(opts = {}) {
4994
- 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");
4995
5353
  this.debounceMs = opts.debounceMs ?? 50;
4996
5354
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
4997
5355
  this.listFiles = opts.listFiles ?? defaultListFiles;
@@ -5053,7 +5411,7 @@ var CmuxStoreSource = class {
5053
5411
  }
5054
5412
  scanFile(filename) {
5055
5413
  const deps = this.deps;
5056
- const filePath = join15(this.stateDir, filename);
5414
+ const filePath = join16(this.stateDir, filename);
5057
5415
  const lockPath = `${filePath}.lock`;
5058
5416
  if (this.fileExists(lockPath)) {
5059
5417
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -5119,14 +5477,14 @@ function defaultIsPidAlive2(pid) {
5119
5477
  }
5120
5478
  function defaultListFiles(dir) {
5121
5479
  try {
5122
- 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"));
5123
5481
  } catch {
5124
5482
  return [];
5125
5483
  }
5126
5484
  }
5127
- function defaultReadFile(path17) {
5485
+ function defaultReadFile(path18) {
5128
5486
  try {
5129
- return readFileSync9(path17, "utf-8");
5487
+ return readFileSync11(path18, "utf-8");
5130
5488
  } catch {
5131
5489
  return void 0;
5132
5490
  }
@@ -5141,9 +5499,9 @@ function defaultWatchDir(dir, cb) {
5141
5499
  }
5142
5500
 
5143
5501
  // packages/workspaces/dist/native-hooks/native-hook-source.js
5144
- import { join as join16 } from "path";
5145
- import { homedir as homedir11 } from "os";
5146
- 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";
5147
5505
  var CLAUDE_HOOK_EVENTS = [
5148
5506
  ["SessionStart", "session-start"],
5149
5507
  ["UserPromptSubmit", "prompt-submit"],
@@ -5155,7 +5513,7 @@ var CLAUDE_HOOK_EVENTS = [
5155
5513
  ];
5156
5514
  var DEFAULT_HOOK_CMD = "squadrant hooks";
5157
5515
  function installClaudeHooks(opts = {}) {
5158
- const settingsPath = opts.settingsPath ?? join16(homedir11(), ".claude", "settings.json");
5516
+ const settingsPath = opts.settingsPath ?? join17(homedir12(), ".claude", "settings.json");
5159
5517
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
5160
5518
  const readFile6 = opts.readFile ?? defaultReadFile2;
5161
5519
  const writeFile5 = opts.writeFile ?? defaultWriteFile;
@@ -5301,29 +5659,141 @@ function extractDetail(sub, payload) {
5301
5659
  }
5302
5660
  return void 0;
5303
5661
  }
5304
- function defaultReadFile2(path17) {
5662
+ function defaultReadFile2(path18) {
5305
5663
  try {
5306
- return readFileSync10(path17, "utf-8");
5664
+ return readFileSync12(path18, "utf-8");
5307
5665
  } catch {
5308
5666
  return void 0;
5309
5667
  }
5310
5668
  }
5311
- function defaultWriteFile(path17, content) {
5312
- mkdirSync6(path17.replace(/\/[^/]+$/, ""), { recursive: true });
5313
- writeFileSync7(path17, content, "utf-8");
5669
+ function defaultWriteFile(path18, content) {
5670
+ mkdirSync6(path18.replace(/\/[^/]+$/, ""), { recursive: true });
5671
+ writeFileSync8(path18, content, "utf-8");
5314
5672
  }
5315
5673
 
5316
5674
  // packages/workspaces/dist/crew-pane.js
5317
5675
  import net from "net";
5676
+ var POST_SEND_CHECK_MS = 750;
5677
+ var SETTLE_POLL_MS = 400;
5678
+ var SETTLE_MAX_POLLS = 8;
5679
+ var SUBMIT_RETRY_LIMIT = 4;
5680
+ async function settleInputBox(runtime, pane) {
5681
+ let prev = await runtime.readPaneScreen(pane) ?? "";
5682
+ let sawContent = parseDraftFromScreen(prev) !== "" && parseDraftFromScreen(prev) !== null;
5683
+ for (let i = 0; i < SETTLE_MAX_POLLS; i++) {
5684
+ await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
5685
+ const cur = await runtime.readPaneScreen(pane) ?? "";
5686
+ const draft = parseDraftFromScreen(cur);
5687
+ if (draft !== "" && draft !== null)
5688
+ sawContent = true;
5689
+ if (cur === prev)
5690
+ return sawContent;
5691
+ prev = cur;
5692
+ }
5693
+ return sawContent;
5694
+ }
5695
+ async function confirmedSendToPane(runtime, pane, message) {
5696
+ const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
5697
+ await runtime.pasteToPane(pane, message);
5698
+ let sawDraft = await settleInputBox(runtime, pane);
5699
+ await runtime.sendKeyToPane(pane, "Enter");
5700
+ let repasted = false;
5701
+ for (let attempt = 0; attempt < SUBMIT_RETRY_LIMIT; attempt++) {
5702
+ await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
5703
+ const afterScreen = await runtime.readPaneScreen(pane) ?? "";
5704
+ const draft = parseDraftFromScreen(afterScreen);
5705
+ if (draft !== "" && draft !== null)
5706
+ sawDraft = true;
5707
+ if (draft === "" && sawDraft)
5708
+ return { delivered: true };
5709
+ if (draft === null && afterScreen !== preSendScreen && sawDraft)
5710
+ return { delivered: true };
5711
+ const settled = await settleInputBox(runtime, pane);
5712
+ if (settled)
5713
+ sawDraft = true;
5714
+ if (!sawDraft && !repasted) {
5715
+ repasted = true;
5716
+ await runtime.pasteToPane(pane, message);
5717
+ }
5718
+ await runtime.sendKeyToPane(pane, "Enter");
5719
+ }
5720
+ return { delivered: false };
5721
+ }
5722
+ async function resendCrewFirstTurn(runtime, captainName, project, name, message) {
5723
+ const captain = await runtime.status(captainName);
5724
+ if (!captain)
5725
+ return { delivered: false };
5726
+ const surfaces = await runtime.listSurfaces(captain.id);
5727
+ const want = titleFor(project, name);
5728
+ const pane = surfaces.find((s) => s.title === want);
5729
+ if (!pane)
5730
+ return { delivered: false };
5731
+ const screen = await runtime.readPaneScreen(pane) ?? "";
5732
+ if (!hasCCInputBox(screen) || classifyStartupSurface(screen) !== "idle") {
5733
+ return { delivered: false };
5734
+ }
5735
+ return confirmedSendToPane(runtime, pane, message);
5736
+ }
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
+ }
5318
5788
 
5319
5789
  // packages/cli/src/squadrantd.ts
5320
5790
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
5321
- var CLI_BIN = join17(dirname5(SELF_PATH2), "index.js");
5322
- 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");
5323
5793
  function readPkgVersion() {
5324
5794
  try {
5325
- const pkgPath = join17(dirname5(SELF_PATH2), "..", "package.json");
5326
- 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";
5327
5797
  } catch {
5328
5798
  return "unknown";
5329
5799
  }
@@ -5338,7 +5808,7 @@ function buildTelegramBridge(cfg, stateRoot, log) {
5338
5808
  const client = createTelegramClient({ token });
5339
5809
  const ensureCaptainAlive = createEnsureCaptainAlive({
5340
5810
  isAlive: createIsCaptainAlive(DAEMON_SOCK),
5341
- launch: createLaunch(CLI_BIN)
5811
+ launch: createLaunch(CLI_BIN, log)
5342
5812
  });
5343
5813
  const runCommand = createRunCommand(CLI_BIN);
5344
5814
  const sendReply = (threadId, text, replyMarkup) => client.sendMessage(cfg.supergroupId, threadId, text, replyMarkup);
@@ -5406,7 +5876,7 @@ function startSquadrantd(opts = {}) {
5406
5876
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
5407
5877
  );
5408
5878
  },
5409
- cursorFile: join17(stateRoot, "cmux-events.seq"),
5879
+ cursorFile: join18(stateRoot, "cmux-events.seq"),
5410
5880
  log
5411
5881
  });
5412
5882
  const cmuxStoreSource = new CmuxStoreSource({ log });
@@ -5418,6 +5888,16 @@ function startSquadrantd(opts = {}) {
5418
5888
  const tgCfg = loadConfig().telegram;
5419
5889
  ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
5420
5890
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
5891
+ const resendRuntime = createCmuxDriver();
5892
+ ctx.resendFirstTurn = opts.resendFirstTurn ?? (async (rec) => {
5893
+ if (rec.provider !== "claude" || !rec.name) return { delivered: false };
5894
+ const proj = loadConfig().projects[rec.project];
5895
+ const captainName = proj?.captainName ?? `${rec.project}-captain`;
5896
+ const message = `${rec.task}
5897
+
5898
+ ${buildCompletionProtocol(rec.id, rec.project)}`;
5899
+ return resendCrewFirstTurn(resendRuntime, captainName, rec.project, rec.name, message);
5900
+ });
5421
5901
  const launchHeadless = opts.launchHeadless ?? (async (rec) => {
5422
5902
  const ingest = (e) => void ctx.d.handle({ kind: "event", project: rec.project, event: e });
5423
5903
  const handle = runHeadless({
@@ -5551,6 +6031,22 @@ function startSquadrantd(opts = {}) {
5551
6031
  log(`codex app-server source start failed: ${e.message}`);
5552
6032
  }
5553
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
+ }
5554
6050
  const origStop = h.stop.bind(h);
5555
6051
  h.stop = async () => {
5556
6052
  try {
@@ -5576,7 +6072,7 @@ if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
5576
6072
  process.stdout.write("squadrantd: launchd-managed daemon entry (no CLI args). Use `squadrant` for commands.\n");
5577
6073
  process.exit(0);
5578
6074
  }
5579
- const sock = join17(homedir12(), ".config", "squadrant", "squadrant.sock");
6075
+ const sock = join18(homedir13(), ".config", "squadrant", "squadrant.sock");
5580
6076
  if (await isDaemonSocketLive(sock)) {
5581
6077
  process.stderr.write(`[squadrantd] refusing to start: a live daemon already owns ${sock}
5582
6078
  `);