squadrant 0.19.0 → 0.19.1

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.
@@ -804,8 +804,9 @@ function isStickyAttention(state) {
804
804
  function nextPendingTool(current, ev, now) {
805
805
  if (ev.note === "agent.hook.PreToolUse")
806
806
  return { name: ev.tool ?? "tool", since: now };
807
- if (ev.note === "posttooluse" || ev.note === "agent.hook.UserPromptSubmit")
807
+ if (ev.note === "posttooluse" || ev.note === "agent.hook.PostToolUse" || ev.note === "agent.hook.UserPromptSubmit") {
808
808
  return void 0;
809
+ }
809
810
  return current;
810
811
  }
811
812
  function nextPendingMonitor(current, ev, now) {
@@ -908,6 +909,7 @@ function reduce(rec, ev, now) {
908
909
  case "task.stalled":
909
910
  case "task.idle":
910
911
  case "task.quiet":
912
+ case "task.warn":
911
913
  case "task.timeout":
912
914
  case "task.reconcile-failed":
913
915
  return rec;
@@ -929,6 +931,15 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStal
929
931
  if (rec.pendingTool) {
930
932
  if (now - rec.pendingTool.since <= toolStallMs)
931
933
  return null;
934
+ if (rec.lastEvent === "task.turn.completed") {
935
+ return {
936
+ ...rec,
937
+ state: "awaiting-input",
938
+ pendingTool: void 0,
939
+ pendingMonitor: void 0,
940
+ lastEvent: "watchdog.tool-stall-recovered"
941
+ };
942
+ }
932
943
  return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
933
944
  }
934
945
  if (rec.pendingMonitor) {
@@ -1250,7 +1261,7 @@ function createDaemon(deps) {
1250
1261
  }
1251
1262
  }
1252
1263
  }
1253
- if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
1264
+ if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state) && r.state !== "awaiting-input") {
1254
1265
  const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
1255
1266
  const refTime = r.workingStretchStartedAt ?? r.createdAt;
1256
1267
  if (t - refTime > ceiling) {
@@ -1296,6 +1307,11 @@ function createDaemon(deps) {
1296
1307
  const idle = evaluateStall(r, t);
1297
1308
  if (idle) {
1298
1309
  store.put(idle);
1310
+ if (idle.state === "awaiting-input") {
1311
+ const recoveredEvent = { type: "task.turn.completed", id: r.id, turnId: "watchdog-recover" };
1312
+ firePush(deps, r.project, r.state, idle, recoveredEvent, lastCaptainTurnAt.get(r.id));
1313
+ continue;
1314
+ }
1299
1315
  const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : idle.pendingMonitor ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: "Monitor", elapsedMs: t - idle.pendingMonitor.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
1300
1316
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
1301
1317
  continue;
@@ -1403,6 +1419,7 @@ var init_reduce = __esm({
1403
1419
  "task.stalled",
1404
1420
  "task.idle",
1405
1421
  "task.quiet",
1422
+ "task.warn",
1406
1423
  "task.timeout",
1407
1424
  "task.reconcile-failed",
1408
1425
  "task.cancelled",
@@ -1901,7 +1918,18 @@ function projectHealth(input) {
1901
1918
  ref: captainName,
1902
1919
  state: captainState,
1903
1920
  lastSeenMs: null,
1904
- detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries) \u2014 draft/ghost text blocking captain pane; input never touched, delivers automatically once cleared` : void 0
1921
+ detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries, reason: ${deferral.reason ?? "unknown"})` : void 0
1922
+ });
1923
+ const deliveryState = captainState === "stopped" ? "stopped" : deferral?.stuck || deferral && deferral.maxDeferCount > 0 ? "stale" : "alive";
1924
+ const deliveryDetail = captainState === "stopped" ? "delivery paused (captain stopped)" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries, reason: ${deferral.reason ?? "unknown"})` : deferral && deferral.maxDeferCount > 0 ? `delivery deferred (${deferral.maxDeferCount} retries, reason: ${deferral.reason ?? "unknown"})` : void 0;
1925
+ out.push({
1926
+ kind: "delivery",
1927
+ project,
1928
+ ref: "delivery",
1929
+ state: deliveryState,
1930
+ lastSeenMs: null,
1931
+ detail: deliveryDetail,
1932
+ stuck: deferral?.stuck
1905
1933
  });
1906
1934
  if (commandPresent !== null) {
1907
1935
  out.push({
@@ -2003,8 +2031,23 @@ function createStore(root) {
2003
2031
  };
2004
2032
  const projDir = (p) => assertUnderRoot(join6(root, safeSegment("project", p)));
2005
2033
  const taskFile = (p, id) => assertUnderRoot(join6(projDir(p), `${safeSegment("id", id)}.json`));
2034
+ const readRecord = (project, id) => {
2035
+ const f = taskFile(project, id);
2036
+ if (!existsSync6(f))
2037
+ return void 0;
2038
+ try {
2039
+ return JSON.parse(readFileSync4(f, "utf-8"));
2040
+ } catch {
2041
+ return void 0;
2042
+ }
2043
+ };
2006
2044
  return {
2007
2045
  put(rec) {
2046
+ const existing = readRecord(rec.project, rec.id);
2047
+ if (existing && TERMINAL_STATES.has(existing.state) && TERMINAL_STATES.has(rec.state) && (existing.state !== rec.state || existing.lastEvent !== rec.lastEvent)) {
2048
+ console.error(`[squadrant] REJECTED terminal\u2192terminal overwrite of ${rec.project}/${rec.id}: already ${existing.state}/${existing.lastEvent} \u2014 refusing ${rec.state}/${rec.lastEvent}; original terminal record preserved (#595)`);
2049
+ return;
2050
+ }
2008
2051
  mkdirSync2(projDir(rec.project), { recursive: true });
2009
2052
  const dest = taskFile(rec.project, rec.id);
2010
2053
  const tmp = `${dest}.tmp`;
@@ -2012,14 +2055,7 @@ function createStore(root) {
2012
2055
  renameSync(tmp, dest);
2013
2056
  },
2014
2057
  get(project, id) {
2015
- const f = taskFile(project, id);
2016
- if (!existsSync6(f))
2017
- return void 0;
2018
- try {
2019
- return JSON.parse(readFileSync4(f, "utf-8"));
2020
- } catch {
2021
- return void 0;
2022
- }
2058
+ return readRecord(project, id);
2023
2059
  },
2024
2060
  list(project) {
2025
2061
  const d = projDir(project);
@@ -2058,6 +2094,7 @@ function createStore(root) {
2058
2094
  }
2059
2095
  var init_store = __esm({
2060
2096
  "packages/core/dist/store.js"() {
2097
+ init_dist();
2061
2098
  }
2062
2099
  });
2063
2100
 
@@ -2424,7 +2461,7 @@ function computeDaemonDrift(nodeBin) {
2424
2461
  const foreignInstall = detectForeignInstall(parsedCurrent, entry, parsedCurrent !== null && existsSync8(parsedCurrent.daemonEntry));
2425
2462
  return { plistPath: p, target, desired, current, changed, programChanged, foreignInstall };
2426
2463
  }
2427
- function applyDaemonDrift(drift) {
2464
+ function reconcilePlistAndService(drift) {
2428
2465
  if (drift.changed) {
2429
2466
  mkdirSync4(dirname2(drift.plistPath), { recursive: true });
2430
2467
  writeFileSync5(drift.plistPath, drift.desired);
@@ -2440,8 +2477,47 @@ function applyDaemonDrift(drift) {
2440
2477
  execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, drift.plistPath], { stdio: "ignore" });
2441
2478
  } catch {
2442
2479
  }
2480
+ }
2481
+ function applyDaemonDrift(drift) {
2482
+ reconcilePlistAndService(drift);
2443
2483
  execFileSync3("launchctl", ["kickstart", drift.target], { stdio: "ignore" });
2444
2484
  }
2485
+ function getDaemonPid(target) {
2486
+ try {
2487
+ const out = execFileSync3("launchctl", ["print", target], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
2488
+ const m = out.match(/\bpid\s*=\s*(\d+)/);
2489
+ return m ? parseInt(m[1], 10) : null;
2490
+ } catch {
2491
+ return null;
2492
+ }
2493
+ }
2494
+ function forceKickstartAndVerify(target, opts = {}) {
2495
+ const pollAttempts = opts.pollAttempts ?? 15;
2496
+ const pollDelayMs = opts.pollDelayMs ?? 300;
2497
+ const kickstartRetries = opts.kickstartRetries ?? 5;
2498
+ const kickstartRetryDelayMs = opts.kickstartRetryDelayMs ?? 300;
2499
+ const pidBefore = getDaemonPid(target);
2500
+ for (let i = 0; i < kickstartRetries; i++) {
2501
+ try {
2502
+ execFileSync3("launchctl", ["kickstart", "-k", target], { stdio: "ignore" });
2503
+ break;
2504
+ } catch (e) {
2505
+ if (i === kickstartRetries - 1)
2506
+ throw e;
2507
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, kickstartRetryDelayMs);
2508
+ }
2509
+ }
2510
+ let pidAfter = null;
2511
+ for (let i = 0; i < pollAttempts; i++) {
2512
+ pidAfter = getDaemonPid(target);
2513
+ if (pidAfter !== null && pidAfter !== pidBefore)
2514
+ break;
2515
+ if (i < pollAttempts - 1) {
2516
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollDelayMs);
2517
+ }
2518
+ }
2519
+ return { target, pidBefore, pidAfter, restarted: pidAfter !== null && pidAfter !== pidBefore };
2520
+ }
2445
2521
  function isOperatorInitiatedCommand(topLevelArg) {
2446
2522
  return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
2447
2523
  }
@@ -2483,11 +2559,18 @@ function printForeignInstallError(foreign) {
2483
2559
  Two squadrant installs on this machine will keep fighting over the daemon (#670). Uninstall the one you don't use, then run \`squadrant heal daemon\` to reconcile.
2484
2560
  `;
2485
2561
  }
2486
- function reregisterDaemon(nodeBin = process.execPath) {
2487
- if (!tryAcquireDaemonLock())
2488
- return;
2562
+ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
2563
+ if (!tryAcquireDaemonLock()) {
2564
+ throw new Error("could not acquire the daemon lock \u2014 another squadrant process is already restarting the daemon");
2565
+ }
2489
2566
  try {
2490
- applyDaemonDrift(computeDaemonDrift(nodeBin));
2567
+ const drift = computeDaemonDrift(nodeBin);
2568
+ reconcilePlistAndService(drift);
2569
+ const result = forceKickstartAndVerify(drift.target, kickstartOpts);
2570
+ if (!result.restarted) {
2571
+ throw new Error(`daemon did not restart (pid before=${result.pidBefore ?? "none"}, after=${result.pidAfter ?? "none"}) \u2014 \`launchctl kickstart -k ${drift.target}\` ran but the pid never changed`);
2572
+ }
2573
+ return result;
2491
2574
  } finally {
2492
2575
  releaseDaemonLock();
2493
2576
  }
@@ -2875,6 +2958,10 @@ function detectTrailingQuestion(text) {
2875
2958
  return lastLine;
2876
2959
  return null;
2877
2960
  }
2961
+ function isQuotedLine(raw) {
2962
+ const noAnsi = raw.replace(/\[[0-9;]*m/g, "");
2963
+ return QUOTED_PREFIX_RE.test(noAnsi);
2964
+ }
2878
2965
  function stripChrome(raw) {
2879
2966
  let line = raw.replace(/\[[0-9;]*m/g, "");
2880
2967
  line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
@@ -2933,8 +3020,11 @@ function classifyPaneTail(tail) {
2933
3020
  if (q)
2934
3021
  return { kind: "question", text: q };
2935
3022
  let errLine = null;
2936
- for (const c of cleaned) {
2937
- if (c != null && ERROR_BANNER_RE.some((re) => re.test(c)))
3023
+ for (let i = 0; i < cleaned.length; i++) {
3024
+ const c = cleaned[i];
3025
+ if (c == null || isQuotedLine(raw[i]))
3026
+ continue;
3027
+ if (ERROR_BANNER_RE.some((re) => re.test(c)))
2938
3028
  errLine = c;
2939
3029
  }
2940
3030
  if (errLine)
@@ -2977,6 +3067,22 @@ function createInteractiveProbe(deps) {
2977
3067
  const verdict = classifyPaneTail(tail);
2978
3068
  if (!verdict)
2979
3069
  continue;
3070
+ if (verdict.kind === "error") {
3071
+ const alive = deps.checkAlive ? await deps.checkAlive(rec) : "unknown";
3072
+ if (alive !== "gone") {
3073
+ const message = `CREW WARN ${rec.name}: pane shows an error string \u2014 crew still ${alive}, not terminalized (pane-detected): ${verdict.text}`;
3074
+ deps.log(`probe -> ${message}`);
3075
+ if (deps.notify) {
3076
+ const warnEvent = { type: "task.warn", id: rec.id, message };
3077
+ try {
3078
+ await deps.notify({ project: rec.project, message, record: rec, event: warnEvent });
3079
+ } catch (e) {
3080
+ deps.log(`probe notify failed for ${rec.id}: ${e.message}`);
3081
+ }
3082
+ }
3083
+ continue;
3084
+ }
3085
+ }
2980
3086
  const event = verdict.kind === "error" ? {
2981
3087
  type: "task.failed",
2982
3088
  id: rec.id,
@@ -2998,7 +3104,7 @@ function createInteractiveProbe(deps) {
2998
3104
  }
2999
3105
  return { tick };
3000
3106
  }
3001
- var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE;
3107
+ var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE, QUOTED_PREFIX_RE;
3002
3108
  var init_interactive_probe = __esm({
3003
3109
  "packages/core/dist/daemon/interactive-probe.js"() {
3004
3110
  STALE_THRESHOLD_MS = 5 * 60 * 1e3;
@@ -3014,6 +3120,7 @@ var init_interactive_probe = __esm({
3014
3120
  PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
3015
3121
  PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
3016
3122
  STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
3123
+ QUOTED_PREFIX_RE = /^\s*(?:[┃│▏▕]|>|[+-]|\d+[\t:→])\s/;
3017
3124
  }
3018
3125
  });
3019
3126
 
@@ -3029,6 +3136,7 @@ function createProbes(ctx) {
3029
3136
  };
3030
3137
  function buildInteractiveProbe(deps) {
3031
3138
  const directPaneReader = createDirectCrewPaneReader(deps.cmux, captainNameForProject);
3139
+ const checkAlive = createDirectSurfaceLivenessProbe(deps.cmux, captainNameForProject);
3032
3140
  const probe = createInteractiveProbe({
3033
3141
  project: "_all_",
3034
3142
  listTasks: async () => store.listAll(),
@@ -3040,7 +3148,9 @@ function createProbes(ctx) {
3040
3148
  }
3041
3149
  },
3042
3150
  now: () => Date.now(),
3043
- log
3151
+ log,
3152
+ checkAlive,
3153
+ notify: ctx.notify
3044
3154
  });
3045
3155
  let probing = false;
3046
3156
  return async () => {
@@ -3420,6 +3530,7 @@ async function runCrewSpawn(input, config, deps) {
3420
3530
  const crewRole = config.defaults.roles?.crew;
3421
3531
  const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
3422
3532
  const crewModel = input.model ?? route?.model ?? configModel;
3533
+ const crewThinking = input.thinking ?? config.defaults.roles?.crew?.thinking;
3423
3534
  if (agentName !== "claude") {
3424
3535
  deps.onModelResolved?.({ agentName, model: crewModel });
3425
3536
  }
@@ -3455,7 +3566,8 @@ async function runCrewSpawn(input, config, deps) {
3455
3566
  // crew apart from an unrelated session instead of an auto-derived cwd
3456
3567
  // basename (only the claude driver reads this — other agents ignore it).
3457
3568
  sessionName: crewSessionName(input.project, name),
3458
- ...crewModel ? { model: crewModel } : {}
3569
+ ...crewModel ? { model: crewModel } : {},
3570
+ ...crewThinking ? { thinking: crewThinking } : {}
3459
3571
  });
3460
3572
  const direction2 = input.direction ?? "tab";
3461
3573
  const title2 = titleFor(input.project, name);
@@ -3463,7 +3575,13 @@ async function runCrewSpawn(input, config, deps) {
3463
3575
  const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
3464
3576
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${niceCrewCommand(cliCommand2)}`);
3465
3577
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
3466
- const claudeResult = await deps.sendFirstTurn(pane2, `${firstTurnTask}
3578
+ let claudeFirstTurn = firstTurnTask;
3579
+ if (Buffer.byteLength(claudeFirstTurn, "utf8") > FIRST_TURN_INLINE_MAX_BYTES) {
3580
+ const spillFile = path9.join(os4.tmpdir(), `squadrant-task-${rec.id}.md`);
3581
+ fs9.writeFileSync(spillFile, claudeFirstTurn, "utf8");
3582
+ claudeFirstTurn = `Full task is at ${spillFile} \u2014 cat it and follow it exactly.`;
3583
+ }
3584
+ const claudeResult = await deps.sendFirstTurn(pane2, `${claudeFirstTurn}
3467
3585
 
3468
3586
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
3469
3587
  if (!claudeResult.delivered) {
@@ -3563,7 +3681,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3563
3681
  if (!crew) {
3564
3682
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
3565
3683
  }
3566
- const blockedByModalMessage = () => `Crew '${name}' has an interactive prompt open (AskUserQuestion/permission) \u2014 message NOT delivered, to avoid confirming its default option. Wait for the prompt to close, then re-send with 'squadrant crew send ${project} ${name}'.`;
3684
+ const blockedByModalMessage = () => `Crew '${name}' has an interactive prompt open (AskUserQuestion/permission) \u2014 message NOT delivered, to avoid confirming its default option. To answer it deliberately: squadrant crew read ${project} ${name} to see the options, then squadrant crew answer ${project} ${name} <n>.`;
3567
3685
  if (deps.isBlockedByModal && await deps.isBlockedByModal(crew)) {
3568
3686
  throw new Error(blockedByModalMessage());
3569
3687
  }
@@ -3577,10 +3695,12 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3577
3695
  const heldForMin = Math.round((Date.now() - task.operatorHold.since) / 6e4);
3578
3696
  throw new Error(`Crew '${name}' is under operator takeover (held ${heldForMin}m${task.operatorHold.note ? `: ${task.operatorHold.note}` : ""}). The operator is working in that tab \u2014 sending a message disrupts their conversation. Ask them to run 'squadrant crew handback ${project} ${name}', or pass --force if they told you to.`);
3579
3697
  }
3698
+ let reopened = false;
3580
3699
  try {
3581
3700
  if (task) {
3582
3701
  if (TERMINAL_STATES.has(task.state)) {
3583
3702
  await deps.emitEvent(project, { type: "task.reopened", id: task.id });
3703
+ reopened = true;
3584
3704
  } else if (task.state === "blocked" || task.state === "awaiting-input" || task.state === "review") {
3585
3705
  await deps.emitEvent(project, { type: "task.started", id: task.id });
3586
3706
  }
@@ -3606,7 +3726,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3606
3726
  throw new Error(`Message to crew '${name}' is held: ${outcome.reason}. Resolve it in the crew's session, then re-send.`);
3607
3727
  }
3608
3728
  if (!fallsBackToPane(outcome)) {
3609
- return;
3729
+ return { reopened };
3610
3730
  }
3611
3731
  }
3612
3732
  if (mode === "shadow" && channel && task) {
@@ -3629,7 +3749,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3629
3749
  if (!paneOk) {
3630
3750
  throw new Error(`Message not delivered to crew '${name}' \u2014 the paste/submit could not be confirmed. Re-send with 'squadrant crew send ${project} ${name}'.`);
3631
3751
  }
3632
- return;
3752
+ return { reopened };
3633
3753
  }
3634
3754
  const { delivered, blockedByModal } = await deliver(crew, message);
3635
3755
  if (blockedByModal) {
@@ -3638,6 +3758,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3638
3758
  if (!delivered) {
3639
3759
  throw new Error(`Message not delivered to crew '${name}' \u2014 the paste/submit could not be confirmed. Re-send with 'squadrant crew send ${project} ${name}'.`);
3640
3760
  }
3761
+ return { reopened };
3641
3762
  }
3642
3763
  async function runCrewRead(project, name, runtime, workspaceId) {
3643
3764
  const crew = await findCrewPane(runtime, workspaceId, project, name);
@@ -3740,7 +3861,7 @@ async function runCrewList(project, runtime, workspaceId) {
3740
3861
  surfaceId: c.surfaceId
3741
3862
  }));
3742
3863
  }
3743
- var CC_SOCKS_DIR, TEMPLATES_DIR, STATE_ROOT, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
3864
+ var CC_SOCKS_DIR, FIRST_TURN_INLINE_MAX_BYTES, TEMPLATES_DIR, STATE_ROOT, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
3744
3865
  var init_crew_spawn = __esm({
3745
3866
  "packages/core/dist/crew-spawn.js"() {
3746
3867
  init_control_channel();
@@ -3749,6 +3870,7 @@ var init_crew_spawn = __esm({
3749
3870
  init_crew_protocol();
3750
3871
  init_crew_lifecycle();
3751
3872
  CC_SOCKS_DIR = "/tmp/cc-socks";
3873
+ FIRST_TURN_INLINE_MAX_BYTES = 1200;
3752
3874
  TEMPLATES_DIR = path9.join(os4.homedir(), ".config", "squadrant", "templates");
3753
3875
  STATE_ROOT = path9.join(os4.homedir(), ".config", "squadrant", "state");
3754
3876
  CLOSE_LOOKUP_RETRIES = 3;
@@ -3795,13 +3917,16 @@ var init_captain_channel = __esm({
3795
3917
  function discoverCaptainSurface(surfaces, captainTitle) {
3796
3918
  return surfaces.find((s) => s.title === captainTitle) ?? null;
3797
3919
  }
3798
- function reapOrphanedCrews(store, project) {
3920
+ async function reapOrphanedCrews(store, project, isSurfaceAlive) {
3799
3921
  let reaped = 0;
3800
3922
  for (const r of store.list(project)) {
3801
3923
  if (TERMINAL_STATES.has(r.state))
3802
3924
  continue;
3803
3925
  if (r.mode !== "interactive")
3804
3926
  continue;
3927
+ const liveness = await isSurfaceAlive(r);
3928
+ if (liveness !== "gone")
3929
+ continue;
3805
3930
  store.put({ ...r, state: "cancelled", lastEvent: "captain-stopped" });
3806
3931
  reaped++;
3807
3932
  }
@@ -3855,10 +3980,13 @@ async function runLivenessTick(deps) {
3855
3980
  const prev = deps.registry.get(project);
3856
3981
  if (prev && prev.lastState === "start")
3857
3982
  entry.startedAt = prev.startedAt;
3983
+ const prevState = deriveCaptainState(prev);
3858
3984
  deps.registry.apply(entry);
3859
3985
  if (winner.pid != null)
3860
3986
  deps.registry.setPidAlive(project, deps.isPidAlive(winner.pid), now);
3861
- logEntry(deps.log, project, deps.registry.get(project));
3987
+ const updated = deps.registry.get(project);
3988
+ if (deriveCaptainState(updated) !== prevState)
3989
+ logEntry(deps.log, project, updated);
3862
3990
  }
3863
3991
  for (const e of deps.registry.all()) {
3864
3992
  if (e.role !== "captain" || e.lastState !== "start" || seen.has(e.project))
@@ -3876,12 +4004,13 @@ async function runLivenessTick(deps) {
3876
4004
  continue;
3877
4005
  const state = deriveCaptainState(e);
3878
4006
  if (state === "stopped" || state === "gone")
3879
- deps.reap(e.project);
4007
+ await deps.reap(e.project);
3880
4008
  }
3881
4009
  }
3882
4010
  }
3883
- function createDelivery(ctx, daemonCmux) {
4011
+ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
3884
4012
  const { stateRoot, store, log, livenessRegistry, isPidAlive, opts, telegramBridge } = ctx;
4013
+ const surfaceProbe = isSurfaceAlive ?? (async () => "unknown");
3885
4014
  const notifyFault = ctx.notifyFault ?? (() => {
3886
4015
  });
3887
4016
  const defaultNotify = async (args) => {
@@ -3904,12 +4033,22 @@ function createDelivery(ctx, daemonCmux) {
3904
4033
  }
3905
4034
  };
3906
4035
  if (!daemonCmux) {
3907
- return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0 };
4036
+ return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0, inFlightDelivery: () => null };
3908
4037
  }
3909
4038
  const cmux2 = daemonCmux;
3910
4039
  const cfg = loadConfig();
3911
4040
  const deliveries = /* @__PURE__ */ new Map();
3912
4041
  const deliveryStats = (project) => deliveries.get(project)?.stats();
4042
+ const lastDeferred = /* @__PURE__ */ new Map();
4043
+ const inFlightDelivery = () => {
4044
+ let worst = null;
4045
+ for (const [project, v] of lastDeferred) {
4046
+ if (!worst || v.deferCount > worst.deferCount)
4047
+ worst = { project, ...v };
4048
+ }
4049
+ return worst;
4050
+ };
4051
+ const projectBackoff = /* @__PURE__ */ new Map();
3913
4052
  const stuckNotified = /* @__PURE__ */ new Set();
3914
4053
  const sessionStartMs = Date.now();
3915
4054
  let delivering = false;
@@ -3920,8 +4059,8 @@ function createDelivery(ctx, daemonCmux) {
3920
4059
  isPidAlive,
3921
4060
  now: () => Date.now(),
3922
4061
  log,
3923
- reap: (project) => {
3924
- const reaped = reapOrphanedCrews(store, project);
4062
+ reap: async (project) => {
4063
+ const reaped = await reapOrphanedCrews(store, project, surfaceProbe);
3925
4064
  if (reaped > 0) {
3926
4065
  const title = cfg.projects?.[project]?.captainName ?? `${project}-captain`;
3927
4066
  log(`captain ${title}: reaped ${reaped} orphaned crew(s)`);
@@ -3937,82 +4076,116 @@ function createDelivery(ctx, daemonCmux) {
3937
4076
  cfg.commandName
3938
4077
  ])];
3939
4078
  for (const project of allProjects) {
3940
- const projCfg = cfg.projects?.[project];
3941
- const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
3942
- const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
3943
- let surface = null;
3944
- if (wsId) {
3945
- const surfaces = await cmux2.listSurfaces(wsId);
3946
- surface = discoverCaptainSurface(surfaces, captainTitle);
3947
- }
3948
- if (!surface)
3949
- surface = injectedSurfaces[project] ?? null;
3950
- if (!surface)
3951
- continue;
3952
- const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
3953
- const lastAcked = cursor?.lastAckedSeq ?? 0;
3954
- let d = deliveries.get(project);
3955
- if (!d) {
3956
- d = new CaptainDelivery({
3957
- maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
3958
- stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
3959
- });
3960
- deliveries.set(project, d);
3961
- }
3962
- for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
3963
- if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
3964
- if (!TERMINAL_KINDS.has(entry.kind)) {
3965
- const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
3966
- if (!isExemptMessage) {
3967
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
3968
- await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
3969
- continue;
4079
+ try {
4080
+ const backoff = projectBackoff.get(project);
4081
+ if (backoff && Date.now() < backoff.nextAttemptAt)
4082
+ continue;
4083
+ const projCfg = cfg.projects?.[project];
4084
+ const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
4085
+ let surface = null;
4086
+ const resolveCaptainSurface = async () => {
4087
+ const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
4088
+ if (!wsId)
4089
+ return injectedSurfaces[project] ?? null;
4090
+ const surfaces = await cmux2.listSurfaces(wsId);
4091
+ return discoverCaptainSurface(surfaces, captainTitle) ?? injectedSurfaces[project] ?? null;
4092
+ };
4093
+ surface = await resolveCaptainSurface();
4094
+ if (!surface)
4095
+ continue;
4096
+ const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
4097
+ const lastAcked = cursor?.lastAckedSeq ?? 0;
4098
+ let d = deliveries.get(project);
4099
+ if (!d) {
4100
+ d = new CaptainDelivery({
4101
+ maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
4102
+ stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
4103
+ });
4104
+ deliveries.set(project, d);
4105
+ }
4106
+ for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
4107
+ if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
4108
+ if (!TERMINAL_KINDS.has(entry.kind)) {
4109
+ const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
4110
+ if (!isExemptMessage) {
4111
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
4112
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4113
+ continue;
4114
+ }
4115
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
4116
+ } else {
4117
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4118
+ }
4119
+ }
4120
+ const result = await d.deliver(entry, async (text, sendOpts) => {
4121
+ let handledByChannel = false;
4122
+ try {
4123
+ const mode = ctx.captainChannelMode?.() ?? "off";
4124
+ const r = await deliverToCaptain(project, text, {
4125
+ channel: ctx.captainChannel,
4126
+ mode,
4127
+ log
4128
+ });
4129
+ handledByChannel = r.handled;
4130
+ } catch (e) {
4131
+ log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4132
+ }
4133
+ if (handledByChannel) {
4134
+ return;
4135
+ }
4136
+ try {
4137
+ return await cmux2.send(surface, text, sendOpts);
4138
+ } catch (e) {
4139
+ if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
4140
+ throw e;
4141
+ const next = await resolveCaptainSurface();
4142
+ const same = next !== null && next.workspaceId === surface.workspaceId && next.surfaceId === surface.surfaceId;
4143
+ if (!next || same) {
4144
+ log(`delivery project=${project}: probe-failed but surface re-resolution found ${next ? "the same dead surface" : "no captain surface"} \u2014 deferring`);
4145
+ throw e;
4146
+ }
4147
+ log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
4148
+ surface = next;
4149
+ return cmux2.send(next, text, sendOpts);
3970
4150
  }
3971
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
4151
+ });
4152
+ if ("delivered" in result) {
4153
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
4154
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4155
+ lastDeferred.delete(project);
4156
+ projectBackoff.delete(project);
3972
4157
  } else {
3973
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4158
+ const { maxDeferCount, stuck: stuck2 } = d.stats();
4159
+ if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4160
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4161
+ }
4162
+ lastDeferred.set(project, { seq: entry.seq, deferCount: maxDeferCount });
4163
+ if (stuck2) {
4164
+ const streak = (projectBackoff.get(project)?.streak ?? 0) + 1;
4165
+ const backoffMs = Math.min(6e4, 1e3 * 2 ** streak);
4166
+ projectBackoff.set(project, { nextAttemptAt: Date.now() + backoffMs, streak });
4167
+ }
4168
+ break;
3974
4169
  }
3975
4170
  }
3976
- const result = await d.deliver(entry, async (text, sendOpts) => {
3977
- let handledByChannel = false;
4171
+ const stuck = d.stats().stuck;
4172
+ if (stuck && !stuckNotified.has(project)) {
4173
+ stuckNotified.add(project);
4174
+ const { maxDeferCount, reason } = d.stats();
4175
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
4176
+ const text = STUCK_ALERT_TEXT[reason ?? "unknown"](maxDeferCount);
3978
4177
  try {
3979
- const mode = ctx.captainChannelMode?.() ?? "off";
3980
- const r = await deliverToCaptain(project, text, {
3981
- channel: ctx.captainChannel,
3982
- mode,
3983
- log
3984
- });
3985
- handledByChannel = r.handled;
4178
+ await appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
3986
4179
  } catch (e) {
3987
- log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
3988
- }
3989
- if (handledByChannel) {
3990
- return;
3991
- }
3992
- return cmux2.send(surface, text, sendOpts);
3993
- });
3994
- if ("delivered" in result) {
3995
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
3996
- await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
3997
- } else {
3998
- const { maxDeferCount } = d.stats();
3999
- if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4000
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4180
+ log(`delivery stuck alert failed project=${project}: ${e.message}`);
4001
4181
  }
4002
- break;
4182
+ Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
4183
+ telegramBridge?.pushRaw(project, text);
4184
+ } else if (!stuck && stuckNotified.has(project)) {
4185
+ stuckNotified.delete(project);
4003
4186
  }
4004
- }
4005
- const stuck = d.stats().stuck;
4006
- if (stuck && !stuckNotified.has(project)) {
4007
- stuckNotified.add(project);
4008
- const { maxDeferCount, reason } = d.stats();
4009
- log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
4010
- const text = reason === "modal" ? `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${maxDeferCount}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.` : `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
4011
- appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
4012
- Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
4013
- telegramBridge?.pushRaw(project, text);
4014
- } else if (!stuck && stuckNotified.has(project)) {
4015
- stuckNotified.delete(project);
4187
+ } catch (e) {
4188
+ log(`delivery project=${project}: unhandled error \u2014 ${e.message}`);
4016
4189
  }
4017
4190
  }
4018
4191
  };
@@ -4026,19 +4199,28 @@ function createDelivery(ctx, daemonCmux) {
4026
4199
  delivering = false;
4027
4200
  }
4028
4201
  };
4029
- return { defaultNotify, deliveryTick, deliveryStats };
4202
+ return { defaultNotify, deliveryTick, deliveryStats, inFlightDelivery };
4030
4203
  }
4031
- var CURSOR_SUBSCRIBER, TERMINAL_KINDS;
4204
+ var CURSOR_SUBSCRIBER, TERMINAL_KINDS, STUCK_ALERT_TEXT;
4032
4205
  var init_delivery_loop = __esm({
4033
4206
  "packages/core/dist/daemon/delivery-loop.js"() {
4034
4207
  init_mailbox();
4035
4208
  init_captain_delivery();
4209
+ init_defer_delivery();
4036
4210
  init_dist();
4037
4211
  init_interactive_probe();
4038
4212
  init_liveness2();
4039
4213
  init_captain_channel();
4040
4214
  CURSOR_SUBSCRIBER = "captain";
4041
4215
  TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
4216
+ STUCK_ALERT_TEXT = {
4217
+ "no-box": (n) => `\u26A0\uFE0F DELIVERY STUCK: your captain pane's input box could not be confirmed visible (an overlay, menu, or scrolled view may be covering it) and has blocked pending notification(s) for ${n}+ retries. This keeps retrying safely and will deliver automatically once the input box is visible again.`,
4218
+ modal: (n) => `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${n}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.`,
4219
+ draft: (n) => `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${n}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`,
4220
+ "probe-failed": (n) => `\u26A0\uFE0F DELIVERY STUCK: reading your captain pane failed (stale/dead surface reference or cmux unavailable) and has blocked pending notification(s) for ${n}+ retries. Delivery re-resolves the pane automatically; if this persists after a captain restart, bounce the daemon to refresh its surface references.`,
4221
+ stable: (n) => `\u26A0\uFE0F DELIVERY STUCK: pending notification(s) have been blocked for ${n}+ retries. This keeps retrying safely and will deliver automatically once the blocker clears.`,
4222
+ unknown: (n) => `\u26A0\uFE0F DELIVERY STUCK: pending notification(s) have been blocked for ${n}+ retries. This keeps retrying safely and will deliver automatically once the blocker clears.`
4223
+ };
4042
4224
  }
4043
4225
  });
4044
4226
 
@@ -4126,10 +4308,72 @@ var init_server = __esm({
4126
4308
  }
4127
4309
  });
4128
4310
 
4311
+ // packages/core/dist/daemon/exit-marker.js
4312
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
4313
+ import { join as join11 } from "path";
4314
+ function exitMarkerPath(stateRoot) {
4315
+ return join11(stateRoot, "exit-marker.json");
4316
+ }
4317
+ function writeExitMarker(stateRoot, marker, log) {
4318
+ try {
4319
+ writeFileSync8(exitMarkerPath(stateRoot), JSON.stringify(marker));
4320
+ } catch (e) {
4321
+ log(`exit marker write failed: ${e.message}`);
4322
+ }
4323
+ }
4324
+ function consumeExitMarker(stateRoot, now = Date.now) {
4325
+ const p = exitMarkerPath(stateRoot);
4326
+ if (!existsSync9(p))
4327
+ return { marker: null };
4328
+ let marker = null;
4329
+ try {
4330
+ marker = JSON.parse(readFileSync8(p, "utf-8"));
4331
+ } catch {
4332
+ marker = null;
4333
+ }
4334
+ try {
4335
+ unlinkSync3(p);
4336
+ } catch {
4337
+ }
4338
+ if (!marker)
4339
+ return { marker: null };
4340
+ const gapMs = Math.max(0, now() - new Date(marker.ts).getTime());
4341
+ return { marker, gapMs };
4342
+ }
4343
+ function runningMarkerPath(stateRoot) {
4344
+ return join11(stateRoot, "running-marker.json");
4345
+ }
4346
+ function writeRunningMarker(stateRoot, marker, log) {
4347
+ try {
4348
+ writeFileSync8(runningMarkerPath(stateRoot), JSON.stringify(marker));
4349
+ } catch (e) {
4350
+ log(`running marker write failed: ${e.message}`);
4351
+ }
4352
+ }
4353
+ function readRunningMarker(stateRoot) {
4354
+ try {
4355
+ return JSON.parse(readFileSync8(runningMarkerPath(stateRoot), "utf-8"));
4356
+ } catch {
4357
+ return null;
4358
+ }
4359
+ }
4360
+ function removeRunningMarker(stateRoot, log) {
4361
+ try {
4362
+ unlinkSync3(runningMarkerPath(stateRoot));
4363
+ } catch (e) {
4364
+ if (e.code !== "ENOENT")
4365
+ log(`running marker remove failed: ${e.message}`);
4366
+ }
4367
+ }
4368
+ var init_exit_marker = __esm({
4369
+ "packages/core/dist/daemon/exit-marker.js"() {
4370
+ }
4371
+ });
4372
+
4129
4373
  // packages/core/dist/daemon/snapshot-gather.js
4130
4374
  import { fileURLToPath as fileURLToPath2 } from "url";
4131
- import { join as join11 } from "path";
4132
- import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
4375
+ import { join as join12 } from "path";
4376
+ import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
4133
4377
  function distBuiltAt() {
4134
4378
  try {
4135
4379
  return statSync3(SELF_PATH).mtimeMs;
@@ -4182,7 +4426,7 @@ function gatherStoreStats(store, stateRoot, project) {
4182
4426
  for (const r of store.list(project))
4183
4427
  byState[r.state] = (byState[r.state] ?? 0) + 1;
4184
4428
  let corruptCount = 0;
4185
- const dir = join11(stateRoot, project);
4429
+ const dir = join12(stateRoot, project);
4186
4430
  try {
4187
4431
  for (const n of readdirSync3(dir)) {
4188
4432
  if (n.includes(".corrupt.")) {
@@ -4192,7 +4436,7 @@ function gatherStoreStats(store, stateRoot, project) {
4192
4436
  if (!n.endsWith(".json"))
4193
4437
  continue;
4194
4438
  try {
4195
- JSON.parse(readFileSync8(join11(dir, n), "utf-8"));
4439
+ JSON.parse(readFileSync9(join12(dir, n), "utf-8"));
4196
4440
  } catch {
4197
4441
  corruptCount++;
4198
4442
  }
@@ -4207,7 +4451,7 @@ function gatherResults(resultsDir) {
4207
4451
  try {
4208
4452
  for (const n of readdirSync3(resultsDir)) {
4209
4453
  try {
4210
- const s = statSync3(join11(resultsDir, n));
4454
+ const s = statSync3(join12(resultsDir, n));
4211
4455
  if (s.isFile()) {
4212
4456
  fileCount++;
4213
4457
  totalBytes += s.size;
@@ -4227,19 +4471,20 @@ var init_snapshot_gather = __esm({
4227
4471
  });
4228
4472
 
4229
4473
  // packages/core/dist/daemon/start.js
4230
- import { join as join12, dirname as dirname3 } from "path";
4474
+ import { join as join13, dirname as dirname3 } from "path";
4231
4475
  import { readdir } from "fs/promises";
4232
4476
  function startDaemon(ctx, opts, pkgVersion) {
4233
4477
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
4234
4478
  const { daemonCmux } = ctx;
4235
4479
  const probes = createProbes(ctx);
4236
- const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats } = createDelivery(ctx, daemonCmux);
4480
+ const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
4481
+ const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats, inFlightDelivery } = createDelivery(ctx, daemonCmux, surfaceProbe);
4237
4482
  const baseNotify = opts.notify ?? defaultNotify;
4238
4483
  const notify = ctx.telegramBridge ? async (args) => {
4239
4484
  await baseNotify(args);
4240
4485
  ctx.telegramBridge.pushLifecycle(args.project, args.event);
4241
4486
  } : baseNotify;
4242
- const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
4487
+ ctx.notify = notify;
4243
4488
  const ingest = (project) => (e) => void ctx.d.handle({ kind: "event", project, event: e });
4244
4489
  const d = createDaemon({
4245
4490
  store,
@@ -4303,7 +4548,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4303
4548
  return out;
4304
4549
  }
4305
4550
  async function gatherSnapshotInputs(now) {
4306
- const logPath2 = join12(dirname3(stateRoot), "squadrantd.log");
4551
+ const logPath2 = join13(dirname3(stateRoot), "squadrantd.log");
4307
4552
  const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4308
4553
  const projects = await Promise.all(tier2Projects.map(async (project) => {
4309
4554
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
@@ -4396,6 +4641,33 @@ function startDaemon(ctx, opts, pkgVersion) {
4396
4641
  })();
4397
4642
  const server = createServer2(ctx, { buildHealth, gatherSnapshotInputs, cancelPromotionsFor, broadcast });
4398
4643
  log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
4644
+ const bootTs = (/* @__PURE__ */ new Date()).toISOString();
4645
+ {
4646
+ const sendDownAlert = (minutes, reasonText) => {
4647
+ const text = `\u26A0\uFE0F daemon was down for ${minutes} min (last exit reason=${reasonText})`;
4648
+ const alertProjects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4649
+ for (const project of alertProjects) {
4650
+ appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`boot-gap alert failed project=${project}: ${e.message}`));
4651
+ }
4652
+ };
4653
+ const { marker, gapMs } = consumeExitMarker(stateRoot);
4654
+ const prevRunning = readRunningMarker(stateRoot);
4655
+ if (marker) {
4656
+ log(`previous exit ts=${marker.ts} reason=${marker.reason} gap=${((gapMs ?? 0) / 1e3).toFixed(1)}s`);
4657
+ if ((gapMs ?? 0) > 6e4)
4658
+ sendDownAlert(Math.round((gapMs ?? 0) / 6e4), marker.reason);
4659
+ } else if (prevRunning) {
4660
+ const lastHeartbeatMs = new Date(prevRunning.lastHeartbeatTs).getTime();
4661
+ const uncleanGapMs = Math.max(0, Date.now() - lastHeartbeatMs);
4662
+ log(`previous exit: UNCLEAN (no marker; last heartbeat ${prevRunning.lastHeartbeatTs}, gap=${(uncleanGapMs / 1e3).toFixed(1)}s)`);
4663
+ if (uncleanGapMs > 6e4) {
4664
+ sendDownAlert(Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
4665
+ }
4666
+ } else {
4667
+ log("previous exit: none (clean or first boot)");
4668
+ }
4669
+ writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: bootTs }, log);
4670
+ }
4399
4671
  let deliveryTick = initialDeliveryTick;
4400
4672
  let probeTick;
4401
4673
  if (daemonCmux) {
@@ -4436,9 +4708,10 @@ function startDaemon(ctx, opts, pkgVersion) {
4436
4708
  keepCount: opts.mailboxConfig?.keepCount ?? 3
4437
4709
  };
4438
4710
  let rotationTimer;
4711
+ let rotationTick;
4439
4712
  if (rotationInterval > 0) {
4440
- const inboxPath = join12(stateRoot, "inbox");
4441
- rotationTimer = setInterval(async () => {
4713
+ const inboxPath = join13(stateRoot, "inbox");
4714
+ rotationTick = async () => {
4442
4715
  try {
4443
4716
  let entries;
4444
4717
  try {
@@ -4451,13 +4724,23 @@ function startDaemon(ctx, opts, pkgVersion) {
4451
4724
  await rotateIfNeeded({ stateRoot, project, ...mboxCfg });
4452
4725
  } catch (e) {
4453
4726
  log(`rotation timer error: ${e.message}`);
4727
+ } finally {
4728
+ writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: (/* @__PURE__ */ new Date()).toISOString() }, log);
4454
4729
  }
4730
+ };
4731
+ rotationTimer = setInterval(() => {
4732
+ void rotationTick();
4455
4733
  }, rotationInterval);
4456
4734
  rotationTimer.unref?.();
4457
4735
  }
4458
4736
  return {
4459
4737
  stop(reason = "requested") {
4460
- log(`exit pid=${process.pid} reason=${reason}`);
4738
+ const ppid = process.ppid;
4739
+ const uptimeMs = Math.round(process.uptime() * 1e3);
4740
+ const inFlight = inFlightDelivery();
4741
+ log(`exit pid=${process.pid} reason=${reason} ppid=${ppid} launchd=${ppid === 1} uptimeMs=${uptimeMs} inFlightDelivery=${inFlight ? `${inFlight.project}#${inFlight.seq}(defers=${inFlight.deferCount})` : "none"}`);
4742
+ writeExitMarker(stateRoot, { ts: (/* @__PURE__ */ new Date()).toISOString(), pid: process.pid, reason, ppid, uptimeMs, inFlightDelivery: inFlight }, log);
4743
+ removeRunningMarker(stateRoot, log);
4461
4744
  if (deliveryTimer)
4462
4745
  clearInterval(deliveryTimer);
4463
4746
  if (probeTimer)
@@ -4486,7 +4769,8 @@ function startDaemon(ctx, opts, pkgVersion) {
4486
4769
  }));
4487
4770
  },
4488
4771
  tickDelivery: deliveryTick,
4489
- tickProbe: probeTick
4772
+ tickProbe: probeTick,
4773
+ tickRotation: rotationTick
4490
4774
  };
4491
4775
  }
4492
4776
  var CURSOR_SUBSCRIBER2, SNAPSHOT_LOG_WINDOW_MS;
@@ -4498,6 +4782,7 @@ var init_start = __esm({
4498
4782
  init_gates();
4499
4783
  init_server();
4500
4784
  init_mailbox();
4785
+ init_exit_marker();
4501
4786
  init_liveness2();
4502
4787
  init_dist();
4503
4788
  init_snapshot_gather();
@@ -5492,9 +5777,9 @@ var init_bridge = __esm({
5492
5777
 
5493
5778
  // packages/core/dist/restart-daemon.js
5494
5779
  import { execFileSync as execFileSync4 } from "child_process";
5495
- import { existsSync as existsSync9 } from "fs";
5780
+ import { existsSync as existsSync10 } from "fs";
5496
5781
  function defaultIsRunning() {
5497
- return existsSync9(DAEMON_SOCK_PATH);
5782
+ return existsSync10(DAEMON_SOCK_PATH);
5498
5783
  }
5499
5784
  function defaultRunKickstart() {
5500
5785
  const uid = process.getuid?.() ?? 0;
@@ -6032,6 +6317,76 @@ var init_side_session = __esm({
6032
6317
  }
6033
6318
  });
6034
6319
 
6320
+ // packages/core/dist/crew-answer.js
6321
+ function describeOptions(options) {
6322
+ return options.map((o) => ` ${o.highlighted ? "\u276F" : " "} ${o.index}. ${o.label}`).join("\n");
6323
+ }
6324
+ function resolveOption(options, selector) {
6325
+ const trimmed = selector.trim();
6326
+ if (/^\d+$/.test(trimmed)) {
6327
+ const byIndex = options.find((o) => o.index === Number(trimmed));
6328
+ if (!byIndex) {
6329
+ throw new Error(`No option ${trimmed} in the visible prompt. Visible options:
6330
+ ${describeOptions(options)}`);
6331
+ }
6332
+ return byIndex;
6333
+ }
6334
+ const lower = trimmed.toLowerCase();
6335
+ const exact = options.filter((o) => o.label.toLowerCase() === lower);
6336
+ if (exact.length === 1)
6337
+ return exact[0];
6338
+ if (exact.length > 1) {
6339
+ throw new Error(`Option text "${selector}" matches multiple options ambiguously:
6340
+ ${describeOptions(exact)}`);
6341
+ }
6342
+ const prefix = options.filter((o) => o.label.toLowerCase().startsWith(lower));
6343
+ if (prefix.length === 1)
6344
+ return prefix[0];
6345
+ if (prefix.length > 1) {
6346
+ throw new Error(`Option text "${selector}" matches multiple options ambiguously:
6347
+ ${describeOptions(prefix)}`);
6348
+ }
6349
+ throw new Error(`No option matches "${selector}". Visible options:
6350
+ ${describeOptions(options)}`);
6351
+ }
6352
+ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps, opts) {
6353
+ const crew = await findCrewPane(runtime, workspaceId, project, name);
6354
+ if (!crew) {
6355
+ throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
6356
+ }
6357
+ const options = await deps.readModalOptions(crew);
6358
+ if (!options) {
6359
+ throw new Error(`Crew '${name}' has no interactive option prompt visible right now \u2014 nothing to answer. Read its screen with 'squadrant crew read ${project} ${name}' to check its state.`);
6360
+ }
6361
+ const target = resolveOption(options, option);
6362
+ if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
6363
+ throw new Error(`Refusing: option ${target.index} is "${target.label}", which does not contain expected text "${opts.expect}". Option order is model-generated and can shift between renders \u2014 re-check with 'squadrant crew read ${project} ${name}'.
6364
+ Visible options:
6365
+ ${describeOptions(options)}`);
6366
+ }
6367
+ const log = deps.log ?? (() => {
6368
+ });
6369
+ log(`\u2192 selecting ${target.index}. "${target.label}"`);
6370
+ const current = options.find((o) => o.highlighted) ?? options[0];
6371
+ const steps = target.index - current.index;
6372
+ const key = steps >= 0 ? "Down" : "Up";
6373
+ for (let i = 0; i < Math.abs(steps); i++) {
6374
+ await runtime.sendKeyToPane(crew, key);
6375
+ }
6376
+ await runtime.sendKeyToPane(crew, "Enter");
6377
+ if (opts?.text) {
6378
+ await runtime.pasteToPane(crew, opts.text);
6379
+ await runtime.sendKeyToPane(crew, "Enter");
6380
+ }
6381
+ const after = await deps.readModalOptions(crew);
6382
+ return { selected: target, closed: after === null };
6383
+ }
6384
+ var init_crew_answer = __esm({
6385
+ "packages/core/dist/crew-answer.js"() {
6386
+ init_crew_spawn();
6387
+ }
6388
+ });
6389
+
6035
6390
  // packages/core/dist/lifecycle-source.js
6036
6391
  function reduceLifecycle(prev, next) {
6037
6392
  if (next.origin === "agent") {
@@ -6066,6 +6421,7 @@ __export(dist_exports, {
6066
6421
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
6067
6422
  DEFAULT_TASK_TIMEOUT_MS: () => DEFAULT_TASK_TIMEOUT_MS,
6068
6423
  DeferDelivery: () => DeferDelivery,
6424
+ FIRST_TURN_INLINE_MAX_BYTES: () => FIRST_TURN_INLINE_MAX_BYTES,
6069
6425
  GROUP_DISPATCH_WARMUP_POLL_MS: () => GROUP_DISPATCH_WARMUP_POLL_MS,
6070
6426
  GROUP_DISPATCH_WARMUP_TIMEOUT_MS: () => GROUP_DISPATCH_WARMUP_TIMEOUT_MS,
6071
6427
  IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
@@ -6098,6 +6454,7 @@ __export(dist_exports, {
6098
6454
  classifyHealth: () => classifyHealth,
6099
6455
  closeWorkItem: () => closeWorkItem,
6100
6456
  computeTemplateHash: () => computeTemplateHash,
6457
+ consumeExitMarker: () => consumeExitMarker,
6101
6458
  createAttach: () => createAttach,
6102
6459
  createCrewPaneReader: () => createCrewPaneReader,
6103
6460
  createDaemon: () => createDaemon,
@@ -6138,13 +6495,17 @@ __export(dist_exports, {
6138
6495
  encodeMsg: () => encodeMsg,
6139
6496
  ensureDaemon: () => ensureDaemon,
6140
6497
  evaluateStall: () => evaluateStall,
6498
+ exitMarkerPath: () => exitMarkerPath,
6141
6499
  fallsBackToPane: () => fallsBackToPane,
6500
+ findCrewPane: () => findCrewPane,
6142
6501
  findOpenChildren: () => findOpenChildren,
6143
6502
  findProjectByThread: () => findProjectByThread,
6144
6503
  findWorkItemById: () => findWorkItemById,
6504
+ forceKickstartAndVerify: () => forceKickstartAndVerify,
6145
6505
  formatInbound: () => formatInbound,
6146
6506
  formatInboundReceipt: () => formatInboundReceipt,
6147
6507
  formatLifecycle: () => formatLifecycle,
6508
+ getDaemonPid: () => getDaemonPid,
6148
6509
  healCmdFor: () => healCmdFor,
6149
6510
  isAuthorized: () => isAuthorized,
6150
6511
  isBareSpawn: () => isBareSpawn,
@@ -6179,6 +6540,7 @@ __export(dist_exports, {
6179
6540
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
6180
6541
  readCursor: () => readCursor,
6181
6542
  readFromCursor: () => readFromCursor,
6543
+ readRunningMarker: () => readRunningMarker,
6182
6544
  reapCrewChildren: () => reapCrewChildren,
6183
6545
  reapOrphanedCrews: () => reapOrphanedCrews,
6184
6546
  reconcileLiveness: () => reconcileLiveness,
@@ -6187,6 +6549,7 @@ __export(dist_exports, {
6187
6549
  reduce: () => reduce,
6188
6550
  reduceLifecycle: () => reduceLifecycle,
6189
6551
  releaseDaemonLock: () => releaseDaemonLock,
6552
+ removeRunningMarker: () => removeRunningMarker,
6190
6553
  renderPlist: () => renderPlist,
6191
6554
  reregisterDaemon: () => reregisterDaemon,
6192
6555
  resolveAgentBinDirs: () => resolveAgentBinDirs,
@@ -6198,6 +6561,7 @@ __export(dist_exports, {
6198
6561
  resolveSetupUserId: () => resolveSetupUserId,
6199
6562
  restartDaemonIfRunning: () => restartDaemonIfRunning,
6200
6563
  rotateIfNeeded: () => rotateIfNeeded,
6564
+ runCrewAnswer: () => runCrewAnswer,
6201
6565
  runCrewClose: () => runCrewClose,
6202
6566
  runCrewList: () => runCrewList,
6203
6567
  runCrewRead: () => runCrewRead,
@@ -6217,6 +6581,7 @@ __export(dist_exports, {
6217
6581
  runTelegramPostSetup: () => runTelegramPostSetup,
6218
6582
  runTelegramSend: () => runTelegramSend,
6219
6583
  runTelegramStatus: () => runTelegramStatus,
6584
+ runningMarkerPath: () => runningMarkerPath,
6220
6585
  sanitizePathForPlist: () => sanitizePathForPlist,
6221
6586
  saveSessions: () => saveSessions,
6222
6587
  saveState: () => saveState,
@@ -6242,6 +6607,8 @@ __export(dist_exports, {
6242
6607
  waitForCaptainDelivery: () => waitForCaptainDelivery,
6243
6608
  waitForWarmup: () => waitForWarmup,
6244
6609
  writeCursor: () => writeCursor,
6610
+ writeExitMarker: () => writeExitMarker,
6611
+ writeRunningMarker: () => writeRunningMarker,
6245
6612
  writeTelegramConfig: () => writeTelegramConfig
6246
6613
  });
6247
6614
  var init_dist2 = __esm({
@@ -6264,6 +6631,7 @@ var init_dist2 = __esm({
6264
6631
  init_attach();
6265
6632
  init_start();
6266
6633
  init_delivery_loop();
6634
+ init_exit_marker();
6267
6635
  init_interactive_probe();
6268
6636
  init_captain_delivery();
6269
6637
  init_defer_delivery();
@@ -6277,6 +6645,7 @@ var init_dist2 = __esm({
6277
6645
  init_launch_workspace();
6278
6646
  init_side_session();
6279
6647
  init_crew_spawn();
6648
+ init_crew_answer();
6280
6649
  init_lifecycle_source();
6281
6650
  init_control_channel();
6282
6651
  init_captain_channel();
@@ -6294,9 +6663,10 @@ init_dist2();
6294
6663
  init_dist2();
6295
6664
  init_dist2();
6296
6665
  init_dist2();
6297
- import { join as join20, dirname as dirname4, resolve as resolve3 } from "path";
6666
+ import { join as join22, dirname as dirname4, resolve as resolve3 } from "path";
6667
+ import { homedir as homedir13 } from "os";
6298
6668
  import { fileURLToPath as fileURLToPath3 } from "url";
6299
- import { readFileSync as readFileSync14, statSync as statSync4, existsSync as existsSync12 } from "fs";
6669
+ import { readFileSync as readFileSync15, statSync as statSync4, existsSync as existsSync13 } from "fs";
6300
6670
 
6301
6671
  // packages/agents/dist/drivers/claude.js
6302
6672
  import { execSync as execSync2 } from "child_process";
@@ -6612,10 +6982,10 @@ function toSnapshot(ev) {
6612
6982
  // packages/agents/dist/codex/config.js
6613
6983
  import { readFile as readFile6 } from "fs/promises";
6614
6984
  import { homedir as homedir7 } from "os";
6615
- import { join as join13 } from "path";
6985
+ import { join as join14 } from "path";
6616
6986
  async function resolveCodexModel() {
6617
- const home = process.env["CODEX_HOME"] ?? join13(homedir7(), ".codex");
6618
- const configPath = join13(home, "config.toml");
6987
+ const home = process.env["CODEX_HOME"] ?? join14(homedir7(), ".codex");
6988
+ const configPath = join14(home, "config.toml");
6619
6989
  let text;
6620
6990
  try {
6621
6991
  text = await readFile6(configPath, "utf8");
@@ -7105,9 +7475,9 @@ var OpencodeSseBridge = class {
7105
7475
 
7106
7476
  // packages/agents/dist/interactive/claude.js
7107
7477
  import { execSync as execSync6 } from "child_process";
7108
- import { readFileSync as readFileSync9 } from "fs";
7478
+ import { readFileSync as readFileSync10 } from "fs";
7109
7479
  import { homedir as homedir8 } from "os";
7110
- import { join as join14 } from "path";
7480
+ import { join as join15 } from "path";
7111
7481
  var nextAskUserQuestionRequestId = Date.now();
7112
7482
 
7113
7483
  // packages/agents/dist/headless/types.js
@@ -7271,14 +7641,14 @@ function runHeadless(opts) {
7271
7641
  }
7272
7642
 
7273
7643
  // packages/agents/dist/claude/peer-registry-source.js
7274
- import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
7275
- import { join as join16 } from "path";
7644
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
7645
+ import { join as join17 } from "path";
7276
7646
 
7277
7647
  // packages/agents/dist/claude/registry.js
7278
7648
  import fs14 from "fs";
7279
- import { join as join15 } from "path";
7649
+ import { join as join16 } from "path";
7280
7650
  import { homedir as homedir9 } from "os";
7281
- var CLAUDE_SESSIONS_DIR = join15(homedir9(), ".claude", "sessions");
7651
+ var CLAUDE_SESSIONS_DIR = join16(homedir9(), ".claude", "sessions");
7282
7652
  var PID_JSON = /^(\d+)\.json$/;
7283
7653
  function parseRegistryDir(files, readFile7) {
7284
7654
  const out = [];
@@ -7324,7 +7694,7 @@ function readClaudeStatusBySocketPath(socketPath) {
7324
7694
  } catch {
7325
7695
  return void 0;
7326
7696
  }
7327
- const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join15(CLAUDE_SESSIONS_DIR, name), "utf8"));
7697
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join16(CLAUDE_SESSIONS_DIR, name), "utf8"));
7328
7698
  const entry = entries.find((e) => e.messagingSocketPath === socketPath);
7329
7699
  if (!entry)
7330
7700
  return void 0;
@@ -7356,7 +7726,7 @@ var ClaudePeerRegistrySource = class {
7356
7726
  log;
7357
7727
  constructor(o = {}) {
7358
7728
  this.readdir = o.readdir ?? (() => readdirSync4(CLAUDE_SESSIONS_DIR));
7359
- this.readFile = o.readFile ?? ((n) => readFileSync10(join16(CLAUDE_SESSIONS_DIR, n), "utf8"));
7729
+ this.readFile = o.readFile ?? ((n) => readFileSync11(join17(CLAUDE_SESSIONS_DIR, n), "utf8"));
7360
7730
  this.isAlive = o.isAlive ?? defaultIsAlive;
7361
7731
  this.now = o.now ?? Date.now;
7362
7732
  this.pollMs = o.pollMs ?? 2e3;
@@ -8133,7 +8503,10 @@ function createCmuxDriver() {
8133
8503
  let screen = "";
8134
8504
  try {
8135
8505
  screen = await cmux(["read-screen", "--workspace", ws, "--surface", sf]);
8136
- } catch {
8506
+ } catch (e) {
8507
+ process.stderr.write(`[squadrant] read-screen failed for ${ws}/${sf}: ${e.message}
8508
+ `);
8509
+ throw new DeferDelivery(null, "probe-failed");
8137
8510
  }
8138
8511
  const draft = parseDraftFromScreen(screen);
8139
8512
  if (draft === null)
@@ -8333,7 +8706,7 @@ var NotifierRegistry = class {
8333
8706
 
8334
8707
  // packages/workspaces/dist/workspaces/obsidian.js
8335
8708
  import fs15 from "fs/promises";
8336
- import { existsSync as existsSync10 } from "fs";
8709
+ import { existsSync as existsSync11 } from "fs";
8337
8710
  import path19 from "path";
8338
8711
 
8339
8712
  // packages/workspaces/dist/workspaces/registry.js
@@ -8449,6 +8822,16 @@ var CmuxEventsBridge = class {
8449
8822
  }
8450
8823
  if (f?.type !== "event" || f.category !== "agent")
8451
8824
  return;
8825
+ if (f.name === "agent.hook.PostToolUse") {
8826
+ const p2 = f.payload ?? {};
8827
+ if (p2.phase === "received")
8828
+ return;
8829
+ const rec2 = this.deps.resolve({ cwd: p2.cwd, source: p2._source ?? f.source, sessionId: p2.session_id });
8830
+ if (!rec2)
8831
+ return;
8832
+ this.deps.emit({ type: "task.progress", id: rec2.id, note: f.name });
8833
+ return;
8834
+ }
8452
8835
  const runState = f.name ? deriveRunState(f.name) : null;
8453
8836
  if (!runState)
8454
8837
  return;
@@ -8477,8 +8860,8 @@ var CmuxEventsBridge = class {
8477
8860
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
8478
8861
  init_dist2();
8479
8862
  init_dist();
8480
- import { readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
8481
- import { join as join17 } from "path";
8863
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
8864
+ import { join as join18 } from "path";
8482
8865
  import { homedir as homedir10 } from "os";
8483
8866
 
8484
8867
  // packages/workspaces/dist/cmux-daemon/store-fingerprint.js
@@ -8576,7 +8959,7 @@ function readLivenessSnapshot(files, readFile7, projects, argvRecovery = {}) {
8576
8959
 
8577
8960
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
8578
8961
  async function readCmuxLiveness() {
8579
- const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join17(homedir10(), ".cmuxterm");
8962
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join18(homedir10(), ".cmuxterm");
8580
8963
  const projects = loadConfig().projects;
8581
8964
  let files;
8582
8965
  try {
@@ -8584,7 +8967,7 @@ async function readCmuxLiveness() {
8584
8967
  } catch (e) {
8585
8968
  throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
8586
8969
  }
8587
- return readLivenessSnapshot(files, (f) => readFileSync11(join17(dir, f), "utf-8"), projects);
8970
+ return readLivenessSnapshot(files, (f) => readFileSync12(join18(dir, f), "utf-8"), projects);
8588
8971
  }
8589
8972
  var DaemonCmux = class {
8590
8973
  driver;
@@ -8647,9 +9030,9 @@ var DaemonCmux = class {
8647
9030
  };
8648
9031
 
8649
9032
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
8650
- import { join as join18 } from "path";
9033
+ import { join as join19 } from "path";
8651
9034
  import { homedir as homedir11 } from "os";
8652
- import { watch, readdirSync as readdirSync6, readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
9035
+ import { watch, readdirSync as readdirSync6, readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
8653
9036
  var CmuxStoreSource = class {
8654
9037
  name = "cmux-store";
8655
9038
  stateDir;
@@ -8670,12 +9053,12 @@ var CmuxStoreSource = class {
8670
9053
  active = false;
8671
9054
  lastError = null;
8672
9055
  constructor(opts = {}) {
8673
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join18(homedir11(), ".cmuxterm");
9056
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join19(homedir11(), ".cmuxterm");
8674
9057
  this.debounceMs = opts.debounceMs ?? 50;
8675
9058
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
8676
9059
  this.listFiles = opts.listFiles ?? defaultListFiles;
8677
9060
  this.readFile = opts.readFile ?? defaultReadFile;
8678
- this.fileExists = opts.fileExists ?? existsSync11;
9061
+ this.fileExists = opts.fileExists ?? existsSync12;
8679
9062
  this.watchDir = opts.watchDir ?? defaultWatchDir;
8680
9063
  this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
8681
9064
  this.cancelTimer = opts.cancelTimer ?? clearTimeout;
@@ -8732,7 +9115,7 @@ var CmuxStoreSource = class {
8732
9115
  }
8733
9116
  scanFile(filename) {
8734
9117
  const deps = this.deps;
8735
- const filePath = join18(this.stateDir, filename);
9118
+ const filePath = join19(this.stateDir, filename);
8736
9119
  const lockPath = `${filePath}.lock`;
8737
9120
  if (this.fileExists(lockPath)) {
8738
9121
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -8805,7 +9188,7 @@ function defaultListFiles(dir) {
8805
9188
  }
8806
9189
  function defaultReadFile(path21) {
8807
9190
  try {
8808
- return readFileSync12(path21, "utf-8");
9191
+ return readFileSync13(path21, "utf-8");
8809
9192
  } catch {
8810
9193
  return void 0;
8811
9194
  }
@@ -8820,9 +9203,9 @@ function defaultWatchDir(dir, cb) {
8820
9203
  }
8821
9204
 
8822
9205
  // packages/workspaces/dist/native-hooks/native-hook-source.js
8823
- import { join as join19 } from "path";
9206
+ import { join as join20 } from "path";
8824
9207
  import { homedir as homedir12 } from "os";
8825
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
9208
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
8826
9209
  var CLAUDE_HOOK_EVENTS = [
8827
9210
  ["SessionStart", "session-start"],
8828
9211
  ["UserPromptSubmit", "prompt-submit"],
@@ -8834,7 +9217,7 @@ var CLAUDE_HOOK_EVENTS = [
8834
9217
  ];
8835
9218
  var DEFAULT_HOOK_CMD = "squadrant hooks";
8836
9219
  function installClaudeHooks(opts = {}) {
8837
- const settingsPath = opts.settingsPath ?? join19(homedir12(), ".claude", "settings.json");
9220
+ const settingsPath = opts.settingsPath ?? join20(homedir12(), ".claude", "settings.json");
8838
9221
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
8839
9222
  const readFile7 = opts.readFile ?? defaultReadFile2;
8840
9223
  const writeFile6 = opts.writeFile ?? defaultWriteFile;
@@ -9004,14 +9387,14 @@ function extractDetail(sub, payload) {
9004
9387
  }
9005
9388
  function defaultReadFile2(path21) {
9006
9389
  try {
9007
- return readFileSync13(path21, "utf-8");
9390
+ return readFileSync14(path21, "utf-8");
9008
9391
  } catch {
9009
9392
  return void 0;
9010
9393
  }
9011
9394
  }
9012
9395
  function defaultWriteFile(path21, content) {
9013
9396
  mkdirSync6(path21.replace(/\/[^/]+$/, ""), { recursive: true });
9014
- writeFileSync8(path21, content, "utf-8");
9397
+ writeFileSync9(path21, content, "utf-8");
9015
9398
  }
9016
9399
 
9017
9400
  // packages/workspaces/dist/crew-pane.js
@@ -9139,27 +9522,56 @@ async function maybeBroadcastDaemonRestart(opts) {
9139
9522
 
9140
9523
  // packages/cli/src/lib/captain-channel-factory.ts
9141
9524
  import { createServer as createServer3, connect as netConnect } from "net";
9142
- import { unlinkSync as unlinkSync3 } from "fs";
9525
+ import fs16 from "fs";
9526
+ import { join as join21 } from "path";
9143
9527
  import { randomUUID as randomUUID5 } from "crypto";
9144
9528
  import chalk2 from "chalk";
9145
9529
  init_dist2();
9146
9530
  var shared;
9531
+ var registryEntryPath = () => join21(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
9532
+ function unregisterSenderIdentity() {
9533
+ try {
9534
+ fs16.unlinkSync(registryEntryPath());
9535
+ } catch {
9536
+ }
9537
+ }
9538
+ function registerSenderIdentity(socketPath) {
9539
+ try {
9540
+ unregisterSenderIdentity();
9541
+ fs16.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
9542
+ fs16.writeFileSync(
9543
+ registryEntryPath(),
9544
+ JSON.stringify({
9545
+ pid: process.pid,
9546
+ sessionId: randomUUID5(),
9547
+ name: "squadrantd",
9548
+ messagingSocketPath: socketPath,
9549
+ kind: "daemon",
9550
+ peerProtocol: 1
9551
+ })
9552
+ );
9553
+ process.on("exit", unregisterSenderIdentity);
9554
+ } catch {
9555
+ }
9556
+ }
9147
9557
  async function sharedReceiptListener() {
9148
9558
  if (shared) return shared;
9559
+ const socketPath = `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`;
9149
9560
  const listener = new ClaudeReceiptListener({
9150
- socketPath: `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`,
9561
+ socketPath,
9151
9562
  createServer: (h) => createServer3(h),
9152
9563
  // A UDS path is not cleaned up when a process is killed, so our own leftover
9153
9564
  // must never be the reason we refuse to start.
9154
9565
  unlinkStale: (p) => {
9155
9566
  try {
9156
- unlinkSync3(p);
9567
+ fs16.unlinkSync(p);
9157
9568
  } catch {
9158
9569
  }
9159
9570
  },
9160
9571
  log: (m) => console.error(chalk2.dim(m))
9161
9572
  });
9162
9573
  await listener.start();
9574
+ registerSenderIdentity(socketPath);
9163
9575
  shared = listener;
9164
9576
  return shared;
9165
9577
  }
@@ -9207,11 +9619,11 @@ async function buildCaptainChannelWithRetry(opts = {}) {
9207
9619
 
9208
9620
  // packages/cli/src/squadrantd.ts
9209
9621
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
9210
- var CLI_BIN = join20(dirname4(SELF_PATH2), "index.js");
9622
+ var CLI_BIN = join22(dirname4(SELF_PATH2), "index.js");
9211
9623
  function readPkgVersion() {
9212
9624
  try {
9213
- const pkgPath = join20(dirname4(SELF_PATH2), "..", "package.json");
9214
- return JSON.parse(readFileSync14(pkgPath, "utf-8")).version ?? "unknown";
9625
+ const pkgPath = join22(dirname4(SELF_PATH2), "..", "package.json");
9626
+ return JSON.parse(readFileSync15(pkgPath, "utf-8")).version ?? "unknown";
9215
9627
  } catch {
9216
9628
  return "unknown";
9217
9629
  }
@@ -9328,7 +9740,7 @@ function startSquadrantd(opts = {}) {
9328
9740
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
9329
9741
  );
9330
9742
  },
9331
- cursorFile: join20(stateRoot, "cmux-events.seq"),
9743
+ cursorFile: join22(stateRoot, "cmux-events.seq"),
9332
9744
  log
9333
9745
  });
9334
9746
  const cmuxStoreSource = new CmuxStoreSource({ log });
@@ -9584,9 +9996,20 @@ function logCrashMarker(kind, err) {
9584
9996
  const message = err instanceof Error ? err.stack ?? err.message : String(err);
9585
9997
  process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${kind} pid=${process.pid} error=${message}
9586
9998
  `);
9587
- }
9588
- function isMonorepoCheckout(scriptPath, dirExists = existsSync12) {
9589
- return dirExists(join20(dirname4(resolve3(scriptPath)), "..", "packages"));
9999
+ const stateRoot = join22(homedir13(), ".config", "squadrant", "state");
10000
+ writeExitMarker(stateRoot, {
10001
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10002
+ pid: process.pid,
10003
+ reason: kind,
10004
+ ppid: process.ppid,
10005
+ uptimeMs: Math.round(process.uptime() * 1e3),
10006
+ inFlightDelivery: null
10007
+ // crash path has no access to the delivery loop's live state
10008
+ }, (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
10009
+ `));
10010
+ }
10011
+ function isMonorepoCheckout(scriptPath, dirExists = existsSync13) {
10012
+ return dirExists(join22(dirname4(resolve3(scriptPath)), "..", "packages"));
9590
10013
  }
9591
10014
  function isLinkedWorktree(scriptPath, statFile = (p) => {
9592
10015
  try {
@@ -9595,7 +10018,7 @@ function isLinkedWorktree(scriptPath, statFile = (p) => {
9595
10018
  return void 0;
9596
10019
  }
9597
10020
  }) {
9598
- const dotGit = join20(dirname4(resolve3(scriptPath)), "..", ".git");
10021
+ const dotGit = join22(dirname4(resolve3(scriptPath)), "..", ".git");
9599
10022
  return statFile(dotGit)?.isFile === true;
9600
10023
  }
9601
10024
  if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {