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.
package/dist/index.js CHANGED
@@ -86,6 +86,15 @@ var init_config_io = __esm({
86
86
  import path2 from "path";
87
87
  import os from "os";
88
88
  import chalk from "chalk";
89
+ function isThinkingLevel(v) {
90
+ return THINKING_LEVELS.includes(v);
91
+ }
92
+ function parseThinkingLevel(v) {
93
+ if (!isThinkingLevel(v)) {
94
+ throw new Error(`Invalid --thinking value '${v}'. Valid values: ${THINKING_LEVELS.join(", ")}`);
95
+ }
96
+ return v;
97
+ }
89
98
  function resolveControlChannelMode(cfg, agent) {
90
99
  const v = cfg?.[agent];
91
100
  return v && CONTROL_CHANNEL_MODES.has(v) ? v : "off";
@@ -178,10 +187,11 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
178
187
  function resolveHome(p) {
179
188
  return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
180
189
  }
181
- var CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
190
+ var THINKING_LEVELS, CONTROL_CHANNEL_MODES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
182
191
  var init_config = __esm({
183
192
  "packages/shared/dist/config.js"() {
184
193
  init_config_io();
194
+ THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
185
195
  CONTROL_CHANNEL_MODES = /* @__PURE__ */ new Set(["off", "shadow", "on"]);
186
196
  DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
187
197
  CONFIG_DIR = path2.dirname(DEFAULT_CONFIG_PATH);
@@ -1374,6 +1384,7 @@ __export(dist_exports, {
1374
1384
  SPOKE_SUBDIRS: () => SPOKE_SUBDIRS,
1375
1385
  TERMINAL_STATES: () => TERMINAL_STATES,
1376
1386
  TERMINAL_WORK_STATES: () => TERMINAL_WORK_STATES,
1387
+ THINKING_LEVELS: () => THINKING_LEVELS,
1377
1388
  UPDATE_CHECK_STATE_PATH: () => UPDATE_CHECK_STATE_PATH,
1378
1389
  addWorktree: () => addWorktree,
1379
1390
  applySafeFixes: () => applySafeFixes,
@@ -1407,6 +1418,7 @@ __export(dist_exports, {
1407
1418
  isDaemonCachedKey: () => isDaemonCachedKey,
1408
1419
  isNewerVersion: () => isNewerVersion,
1409
1420
  isQuieter: () => isQuieter,
1421
+ isThinkingLevel: () => isThinkingLevel,
1410
1422
  isUpdateCheckDisabled: () => isUpdateCheckDisabled,
1411
1423
  iso: () => iso,
1412
1424
  loadConfig: () => loadConfig,
@@ -1417,6 +1429,7 @@ __export(dist_exports, {
1417
1429
  needsCheck: () => needsCheck,
1418
1430
  notifyIfUpdateAvailable: () => notifyIfUpdateAvailable,
1419
1431
  parseSection: () => parseSection,
1432
+ parseThinkingLevel: () => parseThinkingLevel,
1420
1433
  probeCmuxDaemonDirect: () => probeCmuxDaemonDirect,
1421
1434
  projectConfigPath: () => projectConfigPath,
1422
1435
  readConfigFileSync: () => readConfigFileSync,
@@ -1491,8 +1504,9 @@ function isStickyAttention(state) {
1491
1504
  function nextPendingTool(current, ev, now) {
1492
1505
  if (ev.note === "agent.hook.PreToolUse")
1493
1506
  return { name: ev.tool ?? "tool", since: now };
1494
- if (ev.note === "posttooluse" || ev.note === "agent.hook.UserPromptSubmit")
1507
+ if (ev.note === "posttooluse" || ev.note === "agent.hook.PostToolUse" || ev.note === "agent.hook.UserPromptSubmit") {
1495
1508
  return void 0;
1509
+ }
1496
1510
  return current;
1497
1511
  }
1498
1512
  function nextPendingMonitor(current, ev, now) {
@@ -1595,6 +1609,7 @@ function reduce(rec, ev, now) {
1595
1609
  case "task.stalled":
1596
1610
  case "task.idle":
1597
1611
  case "task.quiet":
1612
+ case "task.warn":
1598
1613
  case "task.timeout":
1599
1614
  case "task.reconcile-failed":
1600
1615
  return rec;
@@ -1616,6 +1631,15 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStal
1616
1631
  if (rec.pendingTool) {
1617
1632
  if (now - rec.pendingTool.since <= toolStallMs)
1618
1633
  return null;
1634
+ if (rec.lastEvent === "task.turn.completed") {
1635
+ return {
1636
+ ...rec,
1637
+ state: "awaiting-input",
1638
+ pendingTool: void 0,
1639
+ pendingMonitor: void 0,
1640
+ lastEvent: "watchdog.tool-stall-recovered"
1641
+ };
1642
+ }
1619
1643
  return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
1620
1644
  }
1621
1645
  if (rec.pendingMonitor) {
@@ -1937,7 +1961,7 @@ function createDaemon(deps) {
1937
1961
  }
1938
1962
  }
1939
1963
  }
1940
- if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
1964
+ if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state) && r.state !== "awaiting-input") {
1941
1965
  const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
1942
1966
  const refTime = r.workingStretchStartedAt ?? r.createdAt;
1943
1967
  if (t - refTime > ceiling) {
@@ -1983,6 +2007,11 @@ function createDaemon(deps) {
1983
2007
  const idle = evaluateStall(r, t);
1984
2008
  if (idle) {
1985
2009
  store.put(idle);
2010
+ if (idle.state === "awaiting-input") {
2011
+ const recoveredEvent = { type: "task.turn.completed", id: r.id, turnId: "watchdog-recover" };
2012
+ firePush(deps, r.project, r.state, idle, recoveredEvent, lastCaptainTurnAt.get(r.id));
2013
+ continue;
2014
+ }
1986
2015
  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 };
1987
2016
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
1988
2017
  continue;
@@ -2090,6 +2119,7 @@ var init_reduce = __esm({
2090
2119
  "task.stalled",
2091
2120
  "task.idle",
2092
2121
  "task.quiet",
2122
+ "task.warn",
2093
2123
  "task.timeout",
2094
2124
  "task.reconcile-failed",
2095
2125
  "task.cancelled",
@@ -2588,7 +2618,18 @@ function projectHealth(input) {
2588
2618
  ref: captainName,
2589
2619
  state: captainState,
2590
2620
  lastSeenMs: null,
2591
- 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
2621
+ 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
2622
+ });
2623
+ const deliveryState = captainState === "stopped" ? "stopped" : deferral?.stuck || deferral && deferral.maxDeferCount > 0 ? "stale" : "alive";
2624
+ 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;
2625
+ out.push({
2626
+ kind: "delivery",
2627
+ project,
2628
+ ref: "delivery",
2629
+ state: deliveryState,
2630
+ lastSeenMs: null,
2631
+ detail: deliveryDetail,
2632
+ stuck: deferral?.stuck
2592
2633
  });
2593
2634
  if (commandPresent !== null) {
2594
2635
  out.push({
@@ -2690,8 +2731,23 @@ function createStore(root) {
2690
2731
  };
2691
2732
  const projDir = (p) => assertUnderRoot(join6(root, safeSegment("project", p)));
2692
2733
  const taskFile = (p, id) => assertUnderRoot(join6(projDir(p), `${safeSegment("id", id)}.json`));
2734
+ const readRecord = (project, id) => {
2735
+ const f = taskFile(project, id);
2736
+ if (!existsSync6(f))
2737
+ return void 0;
2738
+ try {
2739
+ return JSON.parse(readFileSync4(f, "utf-8"));
2740
+ } catch {
2741
+ return void 0;
2742
+ }
2743
+ };
2693
2744
  return {
2694
2745
  put(rec) {
2746
+ const existing = readRecord(rec.project, rec.id);
2747
+ if (existing && TERMINAL_STATES.has(existing.state) && TERMINAL_STATES.has(rec.state) && (existing.state !== rec.state || existing.lastEvent !== rec.lastEvent)) {
2748
+ 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)`);
2749
+ return;
2750
+ }
2695
2751
  mkdirSync2(projDir(rec.project), { recursive: true });
2696
2752
  const dest = taskFile(rec.project, rec.id);
2697
2753
  const tmp = `${dest}.tmp`;
@@ -2699,14 +2755,7 @@ function createStore(root) {
2699
2755
  renameSync(tmp, dest);
2700
2756
  },
2701
2757
  get(project, id) {
2702
- const f = taskFile(project, id);
2703
- if (!existsSync6(f))
2704
- return void 0;
2705
- try {
2706
- return JSON.parse(readFileSync4(f, "utf-8"));
2707
- } catch {
2708
- return void 0;
2709
- }
2758
+ return readRecord(project, id);
2710
2759
  },
2711
2760
  list(project) {
2712
2761
  const d = projDir(project);
@@ -2745,6 +2794,7 @@ function createStore(root) {
2745
2794
  }
2746
2795
  var init_store = __esm({
2747
2796
  "packages/core/dist/store.js"() {
2797
+ init_dist();
2748
2798
  }
2749
2799
  });
2750
2800
 
@@ -3111,7 +3161,7 @@ function computeDaemonDrift(nodeBin) {
3111
3161
  const foreignInstall = detectForeignInstall(parsedCurrent, entry, parsedCurrent !== null && existsSync8(parsedCurrent.daemonEntry));
3112
3162
  return { plistPath: p, target, desired, current, changed, programChanged, foreignInstall };
3113
3163
  }
3114
- function applyDaemonDrift(drift) {
3164
+ function reconcilePlistAndService(drift) {
3115
3165
  if (drift.changed) {
3116
3166
  mkdirSync4(dirname2(drift.plistPath), { recursive: true });
3117
3167
  writeFileSync5(drift.plistPath, drift.desired);
@@ -3127,8 +3177,47 @@ function applyDaemonDrift(drift) {
3127
3177
  execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, drift.plistPath], { stdio: "ignore" });
3128
3178
  } catch {
3129
3179
  }
3180
+ }
3181
+ function applyDaemonDrift(drift) {
3182
+ reconcilePlistAndService(drift);
3130
3183
  execFileSync3("launchctl", ["kickstart", drift.target], { stdio: "ignore" });
3131
3184
  }
3185
+ function getDaemonPid(target) {
3186
+ try {
3187
+ const out = execFileSync3("launchctl", ["print", target], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
3188
+ const m = out.match(/\bpid\s*=\s*(\d+)/);
3189
+ return m ? parseInt(m[1], 10) : null;
3190
+ } catch {
3191
+ return null;
3192
+ }
3193
+ }
3194
+ function forceKickstartAndVerify(target, opts = {}) {
3195
+ const pollAttempts = opts.pollAttempts ?? 15;
3196
+ const pollDelayMs = opts.pollDelayMs ?? 300;
3197
+ const kickstartRetries = opts.kickstartRetries ?? 5;
3198
+ const kickstartRetryDelayMs = opts.kickstartRetryDelayMs ?? 300;
3199
+ const pidBefore = getDaemonPid(target);
3200
+ for (let i = 0; i < kickstartRetries; i++) {
3201
+ try {
3202
+ execFileSync3("launchctl", ["kickstart", "-k", target], { stdio: "ignore" });
3203
+ break;
3204
+ } catch (e) {
3205
+ if (i === kickstartRetries - 1)
3206
+ throw e;
3207
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, kickstartRetryDelayMs);
3208
+ }
3209
+ }
3210
+ let pidAfter = null;
3211
+ for (let i = 0; i < pollAttempts; i++) {
3212
+ pidAfter = getDaemonPid(target);
3213
+ if (pidAfter !== null && pidAfter !== pidBefore)
3214
+ break;
3215
+ if (i < pollAttempts - 1) {
3216
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollDelayMs);
3217
+ }
3218
+ }
3219
+ return { target, pidBefore, pidAfter, restarted: pidAfter !== null && pidAfter !== pidBefore };
3220
+ }
3132
3221
  function isOperatorInitiatedCommand(topLevelArg) {
3133
3222
  return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
3134
3223
  }
@@ -3170,11 +3259,18 @@ function printForeignInstallError(foreign) {
3170
3259
  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.
3171
3260
  `;
3172
3261
  }
3173
- function reregisterDaemon(nodeBin = process.execPath) {
3174
- if (!tryAcquireDaemonLock())
3175
- return;
3262
+ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
3263
+ if (!tryAcquireDaemonLock()) {
3264
+ throw new Error("could not acquire the daemon lock \u2014 another squadrant process is already restarting the daemon");
3265
+ }
3176
3266
  try {
3177
- applyDaemonDrift(computeDaemonDrift(nodeBin));
3267
+ const drift = computeDaemonDrift(nodeBin);
3268
+ reconcilePlistAndService(drift);
3269
+ const result = forceKickstartAndVerify(drift.target, kickstartOpts);
3270
+ if (!result.restarted) {
3271
+ 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`);
3272
+ }
3273
+ return result;
3178
3274
  } finally {
3179
3275
  releaseDaemonLock();
3180
3276
  }
@@ -3562,6 +3658,10 @@ function detectTrailingQuestion(text) {
3562
3658
  return lastLine;
3563
3659
  return null;
3564
3660
  }
3661
+ function isQuotedLine(raw) {
3662
+ const noAnsi = raw.replace(/\[[0-9;]*m/g, "");
3663
+ return QUOTED_PREFIX_RE.test(noAnsi);
3664
+ }
3565
3665
  function stripChrome(raw) {
3566
3666
  let line = raw.replace(/\[[0-9;]*m/g, "");
3567
3667
  line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
@@ -3620,8 +3720,11 @@ function classifyPaneTail(tail) {
3620
3720
  if (q)
3621
3721
  return { kind: "question", text: q };
3622
3722
  let errLine = null;
3623
- for (const c of cleaned) {
3624
- if (c != null && ERROR_BANNER_RE.some((re) => re.test(c)))
3723
+ for (let i = 0; i < cleaned.length; i++) {
3724
+ const c = cleaned[i];
3725
+ if (c == null || isQuotedLine(raw[i]))
3726
+ continue;
3727
+ if (ERROR_BANNER_RE.some((re) => re.test(c)))
3625
3728
  errLine = c;
3626
3729
  }
3627
3730
  if (errLine)
@@ -3664,6 +3767,22 @@ function createInteractiveProbe(deps) {
3664
3767
  const verdict = classifyPaneTail(tail);
3665
3768
  if (!verdict)
3666
3769
  continue;
3770
+ if (verdict.kind === "error") {
3771
+ const alive = deps.checkAlive ? await deps.checkAlive(rec) : "unknown";
3772
+ if (alive !== "gone") {
3773
+ const message = `CREW WARN ${rec.name}: pane shows an error string \u2014 crew still ${alive}, not terminalized (pane-detected): ${verdict.text}`;
3774
+ deps.log(`probe -> ${message}`);
3775
+ if (deps.notify) {
3776
+ const warnEvent = { type: "task.warn", id: rec.id, message };
3777
+ try {
3778
+ await deps.notify({ project: rec.project, message, record: rec, event: warnEvent });
3779
+ } catch (e) {
3780
+ deps.log(`probe notify failed for ${rec.id}: ${e.message}`);
3781
+ }
3782
+ }
3783
+ continue;
3784
+ }
3785
+ }
3667
3786
  const event = verdict.kind === "error" ? {
3668
3787
  type: "task.failed",
3669
3788
  id: rec.id,
@@ -3685,7 +3804,7 @@ function createInteractiveProbe(deps) {
3685
3804
  }
3686
3805
  return { tick };
3687
3806
  }
3688
- var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE;
3807
+ var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE, QUOTED_PREFIX_RE;
3689
3808
  var init_interactive_probe = __esm({
3690
3809
  "packages/core/dist/daemon/interactive-probe.js"() {
3691
3810
  STALE_THRESHOLD_MS = 5 * 60 * 1e3;
@@ -3701,6 +3820,7 @@ var init_interactive_probe = __esm({
3701
3820
  PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
3702
3821
  PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
3703
3822
  STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
3823
+ QUOTED_PREFIX_RE = /^\s*(?:[┃│▏▕]|>|[+-]|\d+[\t:→])\s/;
3704
3824
  }
3705
3825
  });
3706
3826
 
@@ -3716,6 +3836,7 @@ function createProbes(ctx) {
3716
3836
  };
3717
3837
  function buildInteractiveProbe(deps) {
3718
3838
  const directPaneReader = createDirectCrewPaneReader(deps.cmux, captainNameForProject);
3839
+ const checkAlive = createDirectSurfaceLivenessProbe(deps.cmux, captainNameForProject);
3719
3840
  const probe = createInteractiveProbe({
3720
3841
  project: "_all_",
3721
3842
  listTasks: async () => store.listAll(),
@@ -3727,7 +3848,9 @@ function createProbes(ctx) {
3727
3848
  }
3728
3849
  },
3729
3850
  now: () => Date.now(),
3730
- log
3851
+ log,
3852
+ checkAlive,
3853
+ notify: ctx.notify
3731
3854
  });
3732
3855
  let probing = false;
3733
3856
  return async () => {
@@ -4107,6 +4230,7 @@ async function runCrewSpawn(input, config, deps) {
4107
4230
  const crewRole = config.defaults.roles?.crew;
4108
4231
  const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
4109
4232
  const crewModel = input.model ?? route?.model ?? configModel;
4233
+ const crewThinking = input.thinking ?? config.defaults.roles?.crew?.thinking;
4110
4234
  if (agentName !== "claude") {
4111
4235
  deps.onModelResolved?.({ agentName, model: crewModel });
4112
4236
  }
@@ -4142,7 +4266,8 @@ async function runCrewSpawn(input, config, deps) {
4142
4266
  // crew apart from an unrelated session instead of an auto-derived cwd
4143
4267
  // basename (only the claude driver reads this — other agents ignore it).
4144
4268
  sessionName: crewSessionName(input.project, name),
4145
- ...crewModel ? { model: crewModel } : {}
4269
+ ...crewModel ? { model: crewModel } : {},
4270
+ ...crewThinking ? { thinking: crewThinking } : {}
4146
4271
  });
4147
4272
  const direction2 = input.direction ?? "tab";
4148
4273
  const title2 = titleFor(input.project, name);
@@ -4150,7 +4275,13 @@ async function runCrewSpawn(input, config, deps) {
4150
4275
  const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
4151
4276
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${niceCrewCommand(cliCommand2)}`);
4152
4277
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
4153
- const claudeResult = await deps.sendFirstTurn(pane2, `${firstTurnTask}
4278
+ let claudeFirstTurn = firstTurnTask;
4279
+ if (Buffer.byteLength(claudeFirstTurn, "utf8") > FIRST_TURN_INLINE_MAX_BYTES) {
4280
+ const spillFile = path9.join(os4.tmpdir(), `squadrant-task-${rec.id}.md`);
4281
+ fs9.writeFileSync(spillFile, claudeFirstTurn, "utf8");
4282
+ claudeFirstTurn = `Full task is at ${spillFile} \u2014 cat it and follow it exactly.`;
4283
+ }
4284
+ const claudeResult = await deps.sendFirstTurn(pane2, `${claudeFirstTurn}
4154
4285
 
4155
4286
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
4156
4287
  if (!claudeResult.delivered) {
@@ -4250,7 +4381,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4250
4381
  if (!crew) {
4251
4382
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
4252
4383
  }
4253
- 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}'.`;
4384
+ 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>.`;
4254
4385
  if (deps.isBlockedByModal && await deps.isBlockedByModal(crew)) {
4255
4386
  throw new Error(blockedByModalMessage());
4256
4387
  }
@@ -4264,10 +4395,12 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4264
4395
  const heldForMin = Math.round((Date.now() - task.operatorHold.since) / 6e4);
4265
4396
  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.`);
4266
4397
  }
4398
+ let reopened = false;
4267
4399
  try {
4268
4400
  if (task) {
4269
4401
  if (TERMINAL_STATES.has(task.state)) {
4270
4402
  await deps.emitEvent(project, { type: "task.reopened", id: task.id });
4403
+ reopened = true;
4271
4404
  } else if (task.state === "blocked" || task.state === "awaiting-input" || task.state === "review") {
4272
4405
  await deps.emitEvent(project, { type: "task.started", id: task.id });
4273
4406
  }
@@ -4293,7 +4426,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4293
4426
  throw new Error(`Message to crew '${name}' is held: ${outcome.reason}. Resolve it in the crew's session, then re-send.`);
4294
4427
  }
4295
4428
  if (!fallsBackToPane(outcome)) {
4296
- return;
4429
+ return { reopened };
4297
4430
  }
4298
4431
  }
4299
4432
  if (mode === "shadow" && channel && task) {
@@ -4316,7 +4449,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4316
4449
  if (!paneOk) {
4317
4450
  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}'.`);
4318
4451
  }
4319
- return;
4452
+ return { reopened };
4320
4453
  }
4321
4454
  const { delivered, blockedByModal } = await deliver(crew, message);
4322
4455
  if (blockedByModal) {
@@ -4325,6 +4458,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4325
4458
  if (!delivered) {
4326
4459
  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}'.`);
4327
4460
  }
4461
+ return { reopened };
4328
4462
  }
4329
4463
  async function runCrewRead(project, name, runtime, workspaceId) {
4330
4464
  const crew = await findCrewPane(runtime, workspaceId, project, name);
@@ -4427,7 +4561,7 @@ async function runCrewList(project, runtime, workspaceId) {
4427
4561
  surfaceId: c.surfaceId
4428
4562
  }));
4429
4563
  }
4430
- var CC_SOCKS_DIR, TEMPLATES_DIR, STATE_ROOT, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
4564
+ var CC_SOCKS_DIR, FIRST_TURN_INLINE_MAX_BYTES, TEMPLATES_DIR, STATE_ROOT, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
4431
4565
  var init_crew_spawn = __esm({
4432
4566
  "packages/core/dist/crew-spawn.js"() {
4433
4567
  init_control_channel();
@@ -4436,6 +4570,7 @@ var init_crew_spawn = __esm({
4436
4570
  init_crew_protocol();
4437
4571
  init_crew_lifecycle();
4438
4572
  CC_SOCKS_DIR = "/tmp/cc-socks";
4573
+ FIRST_TURN_INLINE_MAX_BYTES = 1200;
4439
4574
  TEMPLATES_DIR = path9.join(os4.homedir(), ".config", "squadrant", "templates");
4440
4575
  STATE_ROOT = path9.join(os4.homedir(), ".config", "squadrant", "state");
4441
4576
  CLOSE_LOOKUP_RETRIES = 3;
@@ -4482,13 +4617,16 @@ var init_captain_channel = __esm({
4482
4617
  function discoverCaptainSurface(surfaces, captainTitle) {
4483
4618
  return surfaces.find((s) => s.title === captainTitle) ?? null;
4484
4619
  }
4485
- function reapOrphanedCrews(store, project) {
4620
+ async function reapOrphanedCrews(store, project, isSurfaceAlive) {
4486
4621
  let reaped = 0;
4487
4622
  for (const r of store.list(project)) {
4488
4623
  if (TERMINAL_STATES.has(r.state))
4489
4624
  continue;
4490
4625
  if (r.mode !== "interactive")
4491
4626
  continue;
4627
+ const liveness = await isSurfaceAlive(r);
4628
+ if (liveness !== "gone")
4629
+ continue;
4492
4630
  store.put({ ...r, state: "cancelled", lastEvent: "captain-stopped" });
4493
4631
  reaped++;
4494
4632
  }
@@ -4542,10 +4680,13 @@ async function runLivenessTick(deps) {
4542
4680
  const prev = deps.registry.get(project);
4543
4681
  if (prev && prev.lastState === "start")
4544
4682
  entry.startedAt = prev.startedAt;
4683
+ const prevState = deriveCaptainState(prev);
4545
4684
  deps.registry.apply(entry);
4546
4685
  if (winner.pid != null)
4547
4686
  deps.registry.setPidAlive(project, deps.isPidAlive(winner.pid), now);
4548
- logEntry(deps.log, project, deps.registry.get(project));
4687
+ const updated = deps.registry.get(project);
4688
+ if (deriveCaptainState(updated) !== prevState)
4689
+ logEntry(deps.log, project, updated);
4549
4690
  }
4550
4691
  for (const e of deps.registry.all()) {
4551
4692
  if (e.role !== "captain" || e.lastState !== "start" || seen.has(e.project))
@@ -4563,12 +4704,13 @@ async function runLivenessTick(deps) {
4563
4704
  continue;
4564
4705
  const state = deriveCaptainState(e);
4565
4706
  if (state === "stopped" || state === "gone")
4566
- deps.reap(e.project);
4707
+ await deps.reap(e.project);
4567
4708
  }
4568
4709
  }
4569
4710
  }
4570
- function createDelivery(ctx, daemonCmux) {
4711
+ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4571
4712
  const { stateRoot, store, log, livenessRegistry, isPidAlive, opts, telegramBridge } = ctx;
4713
+ const surfaceProbe = isSurfaceAlive ?? (async () => "unknown");
4572
4714
  const notifyFault = ctx.notifyFault ?? (() => {
4573
4715
  });
4574
4716
  const defaultNotify = async (args) => {
@@ -4591,12 +4733,22 @@ function createDelivery(ctx, daemonCmux) {
4591
4733
  }
4592
4734
  };
4593
4735
  if (!daemonCmux) {
4594
- return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0 };
4736
+ return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0, inFlightDelivery: () => null };
4595
4737
  }
4596
4738
  const cmux2 = daemonCmux;
4597
4739
  const cfg = loadConfig();
4598
4740
  const deliveries = /* @__PURE__ */ new Map();
4599
4741
  const deliveryStats = (project) => deliveries.get(project)?.stats();
4742
+ const lastDeferred = /* @__PURE__ */ new Map();
4743
+ const inFlightDelivery = () => {
4744
+ let worst2 = null;
4745
+ for (const [project, v] of lastDeferred) {
4746
+ if (!worst2 || v.deferCount > worst2.deferCount)
4747
+ worst2 = { project, ...v };
4748
+ }
4749
+ return worst2;
4750
+ };
4751
+ const projectBackoff = /* @__PURE__ */ new Map();
4600
4752
  const stuckNotified = /* @__PURE__ */ new Set();
4601
4753
  const sessionStartMs = Date.now();
4602
4754
  let delivering = false;
@@ -4607,8 +4759,8 @@ function createDelivery(ctx, daemonCmux) {
4607
4759
  isPidAlive,
4608
4760
  now: () => Date.now(),
4609
4761
  log,
4610
- reap: (project) => {
4611
- const reaped = reapOrphanedCrews(store, project);
4762
+ reap: async (project) => {
4763
+ const reaped = await reapOrphanedCrews(store, project, surfaceProbe);
4612
4764
  if (reaped > 0) {
4613
4765
  const title = cfg.projects?.[project]?.captainName ?? `${project}-captain`;
4614
4766
  log(`captain ${title}: reaped ${reaped} orphaned crew(s)`);
@@ -4624,82 +4776,116 @@ function createDelivery(ctx, daemonCmux) {
4624
4776
  cfg.commandName
4625
4777
  ])];
4626
4778
  for (const project of allProjects) {
4627
- const projCfg = cfg.projects?.[project];
4628
- const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
4629
- const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
4630
- let surface = null;
4631
- if (wsId) {
4632
- const surfaces = await cmux2.listSurfaces(wsId);
4633
- surface = discoverCaptainSurface(surfaces, captainTitle);
4634
- }
4635
- if (!surface)
4636
- surface = injectedSurfaces[project] ?? null;
4637
- if (!surface)
4638
- continue;
4639
- const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
4640
- const lastAcked = cursor?.lastAckedSeq ?? 0;
4641
- let d = deliveries.get(project);
4642
- if (!d) {
4643
- d = new CaptainDelivery({
4644
- maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
4645
- stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
4646
- });
4647
- deliveries.set(project, d);
4648
- }
4649
- for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
4650
- if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
4651
- if (!TERMINAL_KINDS.has(entry.kind)) {
4652
- const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
4653
- if (!isExemptMessage) {
4654
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
4655
- await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4656
- continue;
4779
+ try {
4780
+ const backoff = projectBackoff.get(project);
4781
+ if (backoff && Date.now() < backoff.nextAttemptAt)
4782
+ continue;
4783
+ const projCfg = cfg.projects?.[project];
4784
+ const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
4785
+ let surface = null;
4786
+ const resolveCaptainSurface = async () => {
4787
+ const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
4788
+ if (!wsId)
4789
+ return injectedSurfaces[project] ?? null;
4790
+ const surfaces = await cmux2.listSurfaces(wsId);
4791
+ return discoverCaptainSurface(surfaces, captainTitle) ?? injectedSurfaces[project] ?? null;
4792
+ };
4793
+ surface = await resolveCaptainSurface();
4794
+ if (!surface)
4795
+ continue;
4796
+ const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
4797
+ const lastAcked = cursor?.lastAckedSeq ?? 0;
4798
+ let d = deliveries.get(project);
4799
+ if (!d) {
4800
+ d = new CaptainDelivery({
4801
+ maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
4802
+ stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
4803
+ });
4804
+ deliveries.set(project, d);
4805
+ }
4806
+ for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
4807
+ if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
4808
+ if (!TERMINAL_KINDS.has(entry.kind)) {
4809
+ const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
4810
+ if (!isExemptMessage) {
4811
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
4812
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4813
+ continue;
4814
+ }
4815
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
4816
+ } else {
4817
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4657
4818
  }
4658
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
4819
+ }
4820
+ const result = await d.deliver(entry, async (text, sendOpts) => {
4821
+ let handledByChannel = false;
4822
+ try {
4823
+ const mode = ctx.captainChannelMode?.() ?? "off";
4824
+ const r = await deliverToCaptain(project, text, {
4825
+ channel: ctx.captainChannel,
4826
+ mode,
4827
+ log
4828
+ });
4829
+ handledByChannel = r.handled;
4830
+ } catch (e) {
4831
+ log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4832
+ }
4833
+ if (handledByChannel) {
4834
+ return;
4835
+ }
4836
+ try {
4837
+ return await cmux2.send(surface, text, sendOpts);
4838
+ } catch (e) {
4839
+ if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
4840
+ throw e;
4841
+ const next = await resolveCaptainSurface();
4842
+ const same = next !== null && next.workspaceId === surface.workspaceId && next.surfaceId === surface.surfaceId;
4843
+ if (!next || same) {
4844
+ log(`delivery project=${project}: probe-failed but surface re-resolution found ${next ? "the same dead surface" : "no captain surface"} \u2014 deferring`);
4845
+ throw e;
4846
+ }
4847
+ log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
4848
+ surface = next;
4849
+ return cmux2.send(next, text, sendOpts);
4850
+ }
4851
+ });
4852
+ if ("delivered" in result) {
4853
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
4854
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4855
+ lastDeferred.delete(project);
4856
+ projectBackoff.delete(project);
4659
4857
  } else {
4660
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4858
+ const { maxDeferCount, stuck: stuck2 } = d.stats();
4859
+ if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4860
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4861
+ }
4862
+ lastDeferred.set(project, { seq: entry.seq, deferCount: maxDeferCount });
4863
+ if (stuck2) {
4864
+ const streak = (projectBackoff.get(project)?.streak ?? 0) + 1;
4865
+ const backoffMs = Math.min(6e4, 1e3 * 2 ** streak);
4866
+ projectBackoff.set(project, { nextAttemptAt: Date.now() + backoffMs, streak });
4867
+ }
4868
+ break;
4661
4869
  }
4662
4870
  }
4663
- const result = await d.deliver(entry, async (text, sendOpts) => {
4664
- let handledByChannel = false;
4871
+ const stuck = d.stats().stuck;
4872
+ if (stuck && !stuckNotified.has(project)) {
4873
+ stuckNotified.add(project);
4874
+ const { maxDeferCount, reason } = d.stats();
4875
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
4876
+ const text = STUCK_ALERT_TEXT[reason ?? "unknown"](maxDeferCount);
4665
4877
  try {
4666
- const mode = ctx.captainChannelMode?.() ?? "off";
4667
- const r = await deliverToCaptain(project, text, {
4668
- channel: ctx.captainChannel,
4669
- mode,
4670
- log
4671
- });
4672
- handledByChannel = r.handled;
4878
+ await appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
4673
4879
  } catch (e) {
4674
- log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4675
- }
4676
- if (handledByChannel) {
4677
- return;
4678
- }
4679
- return cmux2.send(surface, text, sendOpts);
4680
- });
4681
- if ("delivered" in result) {
4682
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
4683
- await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4684
- } else {
4685
- const { maxDeferCount } = d.stats();
4686
- if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4687
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4880
+ log(`delivery stuck alert failed project=${project}: ${e.message}`);
4688
4881
  }
4689
- break;
4882
+ Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
4883
+ telegramBridge?.pushRaw(project, text);
4884
+ } else if (!stuck && stuckNotified.has(project)) {
4885
+ stuckNotified.delete(project);
4690
4886
  }
4691
- }
4692
- const stuck = d.stats().stuck;
4693
- if (stuck && !stuckNotified.has(project)) {
4694
- stuckNotified.add(project);
4695
- const { maxDeferCount, reason } = d.stats();
4696
- log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
4697
- 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.`;
4698
- appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
4699
- Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
4700
- telegramBridge?.pushRaw(project, text);
4701
- } else if (!stuck && stuckNotified.has(project)) {
4702
- stuckNotified.delete(project);
4887
+ } catch (e) {
4888
+ log(`delivery project=${project}: unhandled error \u2014 ${e.message}`);
4703
4889
  }
4704
4890
  }
4705
4891
  };
@@ -4713,19 +4899,28 @@ function createDelivery(ctx, daemonCmux) {
4713
4899
  delivering = false;
4714
4900
  }
4715
4901
  };
4716
- return { defaultNotify, deliveryTick, deliveryStats };
4902
+ return { defaultNotify, deliveryTick, deliveryStats, inFlightDelivery };
4717
4903
  }
4718
- var CURSOR_SUBSCRIBER, TERMINAL_KINDS;
4904
+ var CURSOR_SUBSCRIBER, TERMINAL_KINDS, STUCK_ALERT_TEXT;
4719
4905
  var init_delivery_loop = __esm({
4720
4906
  "packages/core/dist/daemon/delivery-loop.js"() {
4721
4907
  init_mailbox();
4722
4908
  init_captain_delivery();
4909
+ init_defer_delivery();
4723
4910
  init_dist();
4724
4911
  init_interactive_probe();
4725
4912
  init_liveness2();
4726
4913
  init_captain_channel();
4727
4914
  CURSOR_SUBSCRIBER = "captain";
4728
4915
  TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
4916
+ STUCK_ALERT_TEXT = {
4917
+ "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.`,
4918
+ 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.`,
4919
+ 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.`,
4920
+ "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.`,
4921
+ 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.`,
4922
+ 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.`
4923
+ };
4729
4924
  }
4730
4925
  });
4731
4926
 
@@ -4813,10 +5008,72 @@ var init_server = __esm({
4813
5008
  }
4814
5009
  });
4815
5010
 
5011
+ // packages/core/dist/daemon/exit-marker.js
5012
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
5013
+ import { join as join11 } from "path";
5014
+ function exitMarkerPath(stateRoot) {
5015
+ return join11(stateRoot, "exit-marker.json");
5016
+ }
5017
+ function writeExitMarker(stateRoot, marker, log) {
5018
+ try {
5019
+ writeFileSync8(exitMarkerPath(stateRoot), JSON.stringify(marker));
5020
+ } catch (e) {
5021
+ log(`exit marker write failed: ${e.message}`);
5022
+ }
5023
+ }
5024
+ function consumeExitMarker(stateRoot, now = Date.now) {
5025
+ const p = exitMarkerPath(stateRoot);
5026
+ if (!existsSync9(p))
5027
+ return { marker: null };
5028
+ let marker = null;
5029
+ try {
5030
+ marker = JSON.parse(readFileSync8(p, "utf-8"));
5031
+ } catch {
5032
+ marker = null;
5033
+ }
5034
+ try {
5035
+ unlinkSync3(p);
5036
+ } catch {
5037
+ }
5038
+ if (!marker)
5039
+ return { marker: null };
5040
+ const gapMs = Math.max(0, now() - new Date(marker.ts).getTime());
5041
+ return { marker, gapMs };
5042
+ }
5043
+ function runningMarkerPath(stateRoot) {
5044
+ return join11(stateRoot, "running-marker.json");
5045
+ }
5046
+ function writeRunningMarker(stateRoot, marker, log) {
5047
+ try {
5048
+ writeFileSync8(runningMarkerPath(stateRoot), JSON.stringify(marker));
5049
+ } catch (e) {
5050
+ log(`running marker write failed: ${e.message}`);
5051
+ }
5052
+ }
5053
+ function readRunningMarker(stateRoot) {
5054
+ try {
5055
+ return JSON.parse(readFileSync8(runningMarkerPath(stateRoot), "utf-8"));
5056
+ } catch {
5057
+ return null;
5058
+ }
5059
+ }
5060
+ function removeRunningMarker(stateRoot, log) {
5061
+ try {
5062
+ unlinkSync3(runningMarkerPath(stateRoot));
5063
+ } catch (e) {
5064
+ if (e.code !== "ENOENT")
5065
+ log(`running marker remove failed: ${e.message}`);
5066
+ }
5067
+ }
5068
+ var init_exit_marker = __esm({
5069
+ "packages/core/dist/daemon/exit-marker.js"() {
5070
+ }
5071
+ });
5072
+
4816
5073
  // packages/core/dist/daemon/snapshot-gather.js
4817
5074
  import { fileURLToPath as fileURLToPath2 } from "url";
4818
- import { join as join11 } from "path";
4819
- import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
5075
+ import { join as join12 } from "path";
5076
+ import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
4820
5077
  function distBuiltAt() {
4821
5078
  try {
4822
5079
  return statSync3(SELF_PATH).mtimeMs;
@@ -4869,7 +5126,7 @@ function gatherStoreStats(store, stateRoot, project) {
4869
5126
  for (const r of store.list(project))
4870
5127
  byState[r.state] = (byState[r.state] ?? 0) + 1;
4871
5128
  let corruptCount = 0;
4872
- const dir = join11(stateRoot, project);
5129
+ const dir = join12(stateRoot, project);
4873
5130
  try {
4874
5131
  for (const n of readdirSync3(dir)) {
4875
5132
  if (n.includes(".corrupt.")) {
@@ -4879,7 +5136,7 @@ function gatherStoreStats(store, stateRoot, project) {
4879
5136
  if (!n.endsWith(".json"))
4880
5137
  continue;
4881
5138
  try {
4882
- JSON.parse(readFileSync8(join11(dir, n), "utf-8"));
5139
+ JSON.parse(readFileSync9(join12(dir, n), "utf-8"));
4883
5140
  } catch {
4884
5141
  corruptCount++;
4885
5142
  }
@@ -4894,7 +5151,7 @@ function gatherResults(resultsDir) {
4894
5151
  try {
4895
5152
  for (const n of readdirSync3(resultsDir)) {
4896
5153
  try {
4897
- const s = statSync3(join11(resultsDir, n));
5154
+ const s = statSync3(join12(resultsDir, n));
4898
5155
  if (s.isFile()) {
4899
5156
  fileCount++;
4900
5157
  totalBytes += s.size;
@@ -4914,19 +5171,20 @@ var init_snapshot_gather = __esm({
4914
5171
  });
4915
5172
 
4916
5173
  // packages/core/dist/daemon/start.js
4917
- import { join as join12, dirname as dirname3 } from "path";
5174
+ import { join as join13, dirname as dirname3 } from "path";
4918
5175
  import { readdir } from "fs/promises";
4919
5176
  function startDaemon(ctx, opts, pkgVersion) {
4920
5177
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
4921
5178
  const { daemonCmux } = ctx;
4922
5179
  const probes = createProbes(ctx);
4923
- const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats } = createDelivery(ctx, daemonCmux);
5180
+ const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
5181
+ const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats, inFlightDelivery } = createDelivery(ctx, daemonCmux, surfaceProbe);
4924
5182
  const baseNotify = opts.notify ?? defaultNotify;
4925
5183
  const notify = ctx.telegramBridge ? async (args) => {
4926
5184
  await baseNotify(args);
4927
5185
  ctx.telegramBridge.pushLifecycle(args.project, args.event);
4928
5186
  } : baseNotify;
4929
- const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
5187
+ ctx.notify = notify;
4930
5188
  const ingest = (project) => (e) => void ctx.d.handle({ kind: "event", project, event: e });
4931
5189
  const d = createDaemon({
4932
5190
  store,
@@ -4990,7 +5248,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4990
5248
  return out;
4991
5249
  }
4992
5250
  async function gatherSnapshotInputs(now) {
4993
- const logPath2 = join12(dirname3(stateRoot), "squadrantd.log");
5251
+ const logPath2 = join13(dirname3(stateRoot), "squadrantd.log");
4994
5252
  const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4995
5253
  const projects = await Promise.all(tier2Projects.map(async (project) => {
4996
5254
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
@@ -5083,6 +5341,33 @@ function startDaemon(ctx, opts, pkgVersion) {
5083
5341
  })();
5084
5342
  const server = createServer2(ctx, { buildHealth, gatherSnapshotInputs, cancelPromotionsFor, broadcast });
5085
5343
  log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
5344
+ const bootTs = (/* @__PURE__ */ new Date()).toISOString();
5345
+ {
5346
+ const sendDownAlert = (minutes, reasonText) => {
5347
+ const text = `\u26A0\uFE0F daemon was down for ${minutes} min (last exit reason=${reasonText})`;
5348
+ const alertProjects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
5349
+ for (const project of alertProjects) {
5350
+ appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`boot-gap alert failed project=${project}: ${e.message}`));
5351
+ }
5352
+ };
5353
+ const { marker, gapMs } = consumeExitMarker(stateRoot);
5354
+ const prevRunning = readRunningMarker(stateRoot);
5355
+ if (marker) {
5356
+ log(`previous exit ts=${marker.ts} reason=${marker.reason} gap=${((gapMs ?? 0) / 1e3).toFixed(1)}s`);
5357
+ if ((gapMs ?? 0) > 6e4)
5358
+ sendDownAlert(Math.round((gapMs ?? 0) / 6e4), marker.reason);
5359
+ } else if (prevRunning) {
5360
+ const lastHeartbeatMs = new Date(prevRunning.lastHeartbeatTs).getTime();
5361
+ const uncleanGapMs = Math.max(0, Date.now() - lastHeartbeatMs);
5362
+ log(`previous exit: UNCLEAN (no marker; last heartbeat ${prevRunning.lastHeartbeatTs}, gap=${(uncleanGapMs / 1e3).toFixed(1)}s)`);
5363
+ if (uncleanGapMs > 6e4) {
5364
+ sendDownAlert(Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
5365
+ }
5366
+ } else {
5367
+ log("previous exit: none (clean or first boot)");
5368
+ }
5369
+ writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: bootTs }, log);
5370
+ }
5086
5371
  let deliveryTick = initialDeliveryTick;
5087
5372
  let probeTick;
5088
5373
  if (daemonCmux) {
@@ -5123,9 +5408,10 @@ function startDaemon(ctx, opts, pkgVersion) {
5123
5408
  keepCount: opts.mailboxConfig?.keepCount ?? 3
5124
5409
  };
5125
5410
  let rotationTimer;
5411
+ let rotationTick;
5126
5412
  if (rotationInterval > 0) {
5127
- const inboxPath = join12(stateRoot, "inbox");
5128
- rotationTimer = setInterval(async () => {
5413
+ const inboxPath = join13(stateRoot, "inbox");
5414
+ rotationTick = async () => {
5129
5415
  try {
5130
5416
  let entries;
5131
5417
  try {
@@ -5138,13 +5424,23 @@ function startDaemon(ctx, opts, pkgVersion) {
5138
5424
  await rotateIfNeeded({ stateRoot, project, ...mboxCfg });
5139
5425
  } catch (e) {
5140
5426
  log(`rotation timer error: ${e.message}`);
5427
+ } finally {
5428
+ writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: (/* @__PURE__ */ new Date()).toISOString() }, log);
5141
5429
  }
5430
+ };
5431
+ rotationTimer = setInterval(() => {
5432
+ void rotationTick();
5142
5433
  }, rotationInterval);
5143
5434
  rotationTimer.unref?.();
5144
5435
  }
5145
5436
  return {
5146
5437
  stop(reason = "requested") {
5147
- log(`exit pid=${process.pid} reason=${reason}`);
5438
+ const ppid = process.ppid;
5439
+ const uptimeMs = Math.round(process.uptime() * 1e3);
5440
+ const inFlight = inFlightDelivery();
5441
+ log(`exit pid=${process.pid} reason=${reason} ppid=${ppid} launchd=${ppid === 1} uptimeMs=${uptimeMs} inFlightDelivery=${inFlight ? `${inFlight.project}#${inFlight.seq}(defers=${inFlight.deferCount})` : "none"}`);
5442
+ writeExitMarker(stateRoot, { ts: (/* @__PURE__ */ new Date()).toISOString(), pid: process.pid, reason, ppid, uptimeMs, inFlightDelivery: inFlight }, log);
5443
+ removeRunningMarker(stateRoot, log);
5148
5444
  if (deliveryTimer)
5149
5445
  clearInterval(deliveryTimer);
5150
5446
  if (probeTimer)
@@ -5173,7 +5469,8 @@ function startDaemon(ctx, opts, pkgVersion) {
5173
5469
  }));
5174
5470
  },
5175
5471
  tickDelivery: deliveryTick,
5176
- tickProbe: probeTick
5472
+ tickProbe: probeTick,
5473
+ tickRotation: rotationTick
5177
5474
  };
5178
5475
  }
5179
5476
  var CURSOR_SUBSCRIBER2, SNAPSHOT_LOG_WINDOW_MS;
@@ -5185,6 +5482,7 @@ var init_start = __esm({
5185
5482
  init_gates();
5186
5483
  init_server();
5187
5484
  init_mailbox();
5485
+ init_exit_marker();
5188
5486
  init_liveness2();
5189
5487
  init_dist();
5190
5488
  init_snapshot_gather();
@@ -6179,9 +6477,9 @@ var init_bridge = __esm({
6179
6477
 
6180
6478
  // packages/core/dist/restart-daemon.js
6181
6479
  import { execFileSync as execFileSync4 } from "child_process";
6182
- import { existsSync as existsSync9 } from "fs";
6480
+ import { existsSync as existsSync10 } from "fs";
6183
6481
  function defaultIsRunning() {
6184
- return existsSync9(DAEMON_SOCK_PATH);
6482
+ return existsSync10(DAEMON_SOCK_PATH);
6185
6483
  }
6186
6484
  function defaultRunKickstart() {
6187
6485
  const uid = process.getuid?.() ?? 0;
@@ -6719,6 +7017,76 @@ var init_side_session = __esm({
6719
7017
  }
6720
7018
  });
6721
7019
 
7020
+ // packages/core/dist/crew-answer.js
7021
+ function describeOptions(options) {
7022
+ return options.map((o) => ` ${o.highlighted ? "\u276F" : " "} ${o.index}. ${o.label}`).join("\n");
7023
+ }
7024
+ function resolveOption(options, selector) {
7025
+ const trimmed = selector.trim();
7026
+ if (/^\d+$/.test(trimmed)) {
7027
+ const byIndex = options.find((o) => o.index === Number(trimmed));
7028
+ if (!byIndex) {
7029
+ throw new Error(`No option ${trimmed} in the visible prompt. Visible options:
7030
+ ${describeOptions(options)}`);
7031
+ }
7032
+ return byIndex;
7033
+ }
7034
+ const lower = trimmed.toLowerCase();
7035
+ const exact = options.filter((o) => o.label.toLowerCase() === lower);
7036
+ if (exact.length === 1)
7037
+ return exact[0];
7038
+ if (exact.length > 1) {
7039
+ throw new Error(`Option text "${selector}" matches multiple options ambiguously:
7040
+ ${describeOptions(exact)}`);
7041
+ }
7042
+ const prefix = options.filter((o) => o.label.toLowerCase().startsWith(lower));
7043
+ if (prefix.length === 1)
7044
+ return prefix[0];
7045
+ if (prefix.length > 1) {
7046
+ throw new Error(`Option text "${selector}" matches multiple options ambiguously:
7047
+ ${describeOptions(prefix)}`);
7048
+ }
7049
+ throw new Error(`No option matches "${selector}". Visible options:
7050
+ ${describeOptions(options)}`);
7051
+ }
7052
+ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps, opts) {
7053
+ const crew = await findCrewPane(runtime, workspaceId, project, name);
7054
+ if (!crew) {
7055
+ throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
7056
+ }
7057
+ const options = await deps.readModalOptions(crew);
7058
+ if (!options) {
7059
+ 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.`);
7060
+ }
7061
+ const target = resolveOption(options, option);
7062
+ if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
7063
+ 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}'.
7064
+ Visible options:
7065
+ ${describeOptions(options)}`);
7066
+ }
7067
+ const log = deps.log ?? (() => {
7068
+ });
7069
+ log(`\u2192 selecting ${target.index}. "${target.label}"`);
7070
+ const current = options.find((o) => o.highlighted) ?? options[0];
7071
+ const steps = target.index - current.index;
7072
+ const key = steps >= 0 ? "Down" : "Up";
7073
+ for (let i = 0; i < Math.abs(steps); i++) {
7074
+ await runtime.sendKeyToPane(crew, key);
7075
+ }
7076
+ await runtime.sendKeyToPane(crew, "Enter");
7077
+ if (opts?.text) {
7078
+ await runtime.pasteToPane(crew, opts.text);
7079
+ await runtime.sendKeyToPane(crew, "Enter");
7080
+ }
7081
+ const after = await deps.readModalOptions(crew);
7082
+ return { selected: target, closed: after === null };
7083
+ }
7084
+ var init_crew_answer = __esm({
7085
+ "packages/core/dist/crew-answer.js"() {
7086
+ init_crew_spawn();
7087
+ }
7088
+ });
7089
+
6722
7090
  // packages/core/dist/lifecycle-source.js
6723
7091
  function reduceLifecycle(prev, next) {
6724
7092
  if (next.origin === "agent") {
@@ -6753,6 +7121,7 @@ __export(dist_exports2, {
6753
7121
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
6754
7122
  DEFAULT_TASK_TIMEOUT_MS: () => DEFAULT_TASK_TIMEOUT_MS,
6755
7123
  DeferDelivery: () => DeferDelivery,
7124
+ FIRST_TURN_INLINE_MAX_BYTES: () => FIRST_TURN_INLINE_MAX_BYTES,
6756
7125
  GROUP_DISPATCH_WARMUP_POLL_MS: () => GROUP_DISPATCH_WARMUP_POLL_MS,
6757
7126
  GROUP_DISPATCH_WARMUP_TIMEOUT_MS: () => GROUP_DISPATCH_WARMUP_TIMEOUT_MS,
6758
7127
  IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
@@ -6785,6 +7154,7 @@ __export(dist_exports2, {
6785
7154
  classifyHealth: () => classifyHealth,
6786
7155
  closeWorkItem: () => closeWorkItem,
6787
7156
  computeTemplateHash: () => computeTemplateHash,
7157
+ consumeExitMarker: () => consumeExitMarker,
6788
7158
  createAttach: () => createAttach,
6789
7159
  createCrewPaneReader: () => createCrewPaneReader,
6790
7160
  createDaemon: () => createDaemon,
@@ -6825,13 +7195,17 @@ __export(dist_exports2, {
6825
7195
  encodeMsg: () => encodeMsg,
6826
7196
  ensureDaemon: () => ensureDaemon,
6827
7197
  evaluateStall: () => evaluateStall,
7198
+ exitMarkerPath: () => exitMarkerPath,
6828
7199
  fallsBackToPane: () => fallsBackToPane,
7200
+ findCrewPane: () => findCrewPane,
6829
7201
  findOpenChildren: () => findOpenChildren,
6830
7202
  findProjectByThread: () => findProjectByThread,
6831
7203
  findWorkItemById: () => findWorkItemById,
7204
+ forceKickstartAndVerify: () => forceKickstartAndVerify,
6832
7205
  formatInbound: () => formatInbound,
6833
7206
  formatInboundReceipt: () => formatInboundReceipt,
6834
7207
  formatLifecycle: () => formatLifecycle,
7208
+ getDaemonPid: () => getDaemonPid,
6835
7209
  healCmdFor: () => healCmdFor,
6836
7210
  isAuthorized: () => isAuthorized,
6837
7211
  isBareSpawn: () => isBareSpawn,
@@ -6866,6 +7240,7 @@ __export(dist_exports2, {
6866
7240
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
6867
7241
  readCursor: () => readCursor,
6868
7242
  readFromCursor: () => readFromCursor,
7243
+ readRunningMarker: () => readRunningMarker,
6869
7244
  reapCrewChildren: () => reapCrewChildren,
6870
7245
  reapOrphanedCrews: () => reapOrphanedCrews,
6871
7246
  reconcileLiveness: () => reconcileLiveness,
@@ -6874,6 +7249,7 @@ __export(dist_exports2, {
6874
7249
  reduce: () => reduce,
6875
7250
  reduceLifecycle: () => reduceLifecycle,
6876
7251
  releaseDaemonLock: () => releaseDaemonLock,
7252
+ removeRunningMarker: () => removeRunningMarker,
6877
7253
  renderPlist: () => renderPlist,
6878
7254
  reregisterDaemon: () => reregisterDaemon,
6879
7255
  resolveAgentBinDirs: () => resolveAgentBinDirs,
@@ -6885,6 +7261,7 @@ __export(dist_exports2, {
6885
7261
  resolveSetupUserId: () => resolveSetupUserId,
6886
7262
  restartDaemonIfRunning: () => restartDaemonIfRunning,
6887
7263
  rotateIfNeeded: () => rotateIfNeeded,
7264
+ runCrewAnswer: () => runCrewAnswer,
6888
7265
  runCrewClose: () => runCrewClose,
6889
7266
  runCrewList: () => runCrewList,
6890
7267
  runCrewRead: () => runCrewRead,
@@ -6904,6 +7281,7 @@ __export(dist_exports2, {
6904
7281
  runTelegramPostSetup: () => runTelegramPostSetup,
6905
7282
  runTelegramSend: () => runTelegramSend,
6906
7283
  runTelegramStatus: () => runTelegramStatus,
7284
+ runningMarkerPath: () => runningMarkerPath,
6907
7285
  sanitizePathForPlist: () => sanitizePathForPlist,
6908
7286
  saveSessions: () => saveSessions,
6909
7287
  saveState: () => saveState,
@@ -6929,6 +7307,8 @@ __export(dist_exports2, {
6929
7307
  waitForCaptainDelivery: () => waitForCaptainDelivery,
6930
7308
  waitForWarmup: () => waitForWarmup,
6931
7309
  writeCursor: () => writeCursor,
7310
+ writeExitMarker: () => writeExitMarker,
7311
+ writeRunningMarker: () => writeRunningMarker,
6932
7312
  writeTelegramConfig: () => writeTelegramConfig
6933
7313
  });
6934
7314
  var init_dist2 = __esm({
@@ -6951,6 +7331,7 @@ var init_dist2 = __esm({
6951
7331
  init_attach();
6952
7332
  init_start();
6953
7333
  init_delivery_loop();
7334
+ init_exit_marker();
6954
7335
  init_interactive_probe();
6955
7336
  init_captain_delivery();
6956
7337
  init_defer_delivery();
@@ -6964,6 +7345,7 @@ var init_dist2 = __esm({
6964
7345
  init_launch_workspace();
6965
7346
  init_side_session();
6966
7347
  init_crew_spawn();
7348
+ init_crew_answer();
6967
7349
  init_lifecycle_source();
6968
7350
  init_control_channel();
6969
7351
  init_captain_channel();
@@ -7120,6 +7502,35 @@ function hasModalOptionList(screen) {
7120
7502
  return false;
7121
7503
  return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
7122
7504
  }
7505
+ function parseModalOptions(screen) {
7506
+ if (!hasModalOptionList(screen))
7507
+ return null;
7508
+ const lines = screen.split(/\r?\n/);
7509
+ const HR_RE = /^\s*─{10,}\s*$/;
7510
+ let bottomHR = -1;
7511
+ let topHR = -1;
7512
+ for (let i = lines.length - 1; i >= 0; i--) {
7513
+ if (HR_RE.test(lines[i])) {
7514
+ if (bottomHR === -1)
7515
+ bottomHR = i;
7516
+ else {
7517
+ topHR = i;
7518
+ break;
7519
+ }
7520
+ }
7521
+ }
7522
+ if (topHR === -1)
7523
+ return null;
7524
+ const OPTION_RE3 = /^\s*(❯)?\s*(\d+)\.\s*(.*?)\s*$/;
7525
+ const options = [];
7526
+ for (const line of lines.slice(topHR + 1, bottomHR)) {
7527
+ const m = line.match(OPTION_RE3);
7528
+ if (!m)
7529
+ continue;
7530
+ options.push({ index: Number(m[2]), label: m[3], highlighted: m[1] === "\u276F" });
7531
+ }
7532
+ return options.length > 0 ? options : null;
7533
+ }
7123
7534
  function readInputBoxRaw(screen, opts) {
7124
7535
  if (!screen)
7125
7536
  return null;
@@ -7366,7 +7777,10 @@ function createCmuxDriver() {
7366
7777
  let screen = "";
7367
7778
  try {
7368
7779
  screen = await cmux(["read-screen", "--workspace", ws, "--surface", sf]);
7369
- } catch {
7780
+ } catch (e) {
7781
+ process.stderr.write(`[squadrant] read-screen failed for ${ws}/${sf}: ${e.message}
7782
+ `);
7783
+ throw new DeferDelivery(null, "probe-failed");
7370
7784
  }
7371
7785
  const draft = parseDraftFromScreen(screen);
7372
7786
  if (draft === null)
@@ -7616,7 +8030,7 @@ var init_notifiers = __esm({
7616
8030
 
7617
8031
  // packages/workspaces/dist/workspaces/obsidian.js
7618
8032
  import fs13 from "fs/promises";
7619
- import { existsSync as existsSync10 } from "fs";
8033
+ import { existsSync as existsSync11 } from "fs";
7620
8034
  import path13 from "path";
7621
8035
  function resolveInRoot(root, relative) {
7622
8036
  const joined = path13.resolve(root, relative);
@@ -7636,7 +8050,7 @@ function createObsidianDriver(scope) {
7636
8050
  async probe() {
7637
8051
  return {
7638
8052
  installed: true,
7639
- rootExists: existsSync10(root)
8053
+ rootExists: existsSync11(root)
7640
8054
  };
7641
8055
  },
7642
8056
  async read(rel) {
@@ -7834,6 +8248,16 @@ var init_events_bridge = __esm({
7834
8248
  }
7835
8249
  if (f?.type !== "event" || f.category !== "agent")
7836
8250
  return;
8251
+ if (f.name === "agent.hook.PostToolUse") {
8252
+ const p2 = f.payload ?? {};
8253
+ if (p2.phase === "received")
8254
+ return;
8255
+ const rec2 = this.deps.resolve({ cwd: p2.cwd, source: p2._source ?? f.source, sessionId: p2.session_id });
8256
+ if (!rec2)
8257
+ return;
8258
+ this.deps.emit({ type: "task.progress", id: rec2.id, note: f.name });
8259
+ return;
8260
+ }
7837
8261
  const runState = f.name ? deriveRunState(f.name) : null;
7838
8262
  if (!runState)
7839
8263
  return;
@@ -7960,11 +8384,11 @@ var init_store_fingerprint = __esm({
7960
8384
  });
7961
8385
 
7962
8386
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
7963
- import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
7964
- import { join as join13 } from "path";
8387
+ import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
8388
+ import { join as join14 } from "path";
7965
8389
  import { homedir as homedir7 } from "os";
7966
8390
  async function readCmuxLiveness() {
7967
- const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join13(homedir7(), ".cmuxterm");
8391
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir7(), ".cmuxterm");
7968
8392
  const projects = loadConfig().projects;
7969
8393
  let files;
7970
8394
  try {
@@ -7972,7 +8396,7 @@ async function readCmuxLiveness() {
7972
8396
  } catch (e) {
7973
8397
  throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
7974
8398
  }
7975
- return readLivenessSnapshot(files, (f) => readFileSync9(join13(dir, f), "utf-8"), projects);
8399
+ return readLivenessSnapshot(files, (f) => readFileSync10(join14(dir, f), "utf-8"), projects);
7976
8400
  }
7977
8401
  var DaemonCmux;
7978
8402
  var init_daemon_cmux = __esm({
@@ -8043,9 +8467,9 @@ var init_daemon_cmux = __esm({
8043
8467
  });
8044
8468
 
8045
8469
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
8046
- import { join as join14 } from "path";
8470
+ import { join as join15 } from "path";
8047
8471
  import { homedir as homedir8 } from "os";
8048
- import { watch, readdirSync as readdirSync5, readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
8472
+ import { watch, readdirSync as readdirSync5, readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
8049
8473
  function parseLifecycleState(s) {
8050
8474
  if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
8051
8475
  return s;
@@ -8069,7 +8493,7 @@ function defaultListFiles(dir) {
8069
8493
  }
8070
8494
  function defaultReadFile(path36) {
8071
8495
  try {
8072
- return readFileSync10(path36, "utf-8");
8496
+ return readFileSync11(path36, "utf-8");
8073
8497
  } catch {
8074
8498
  return void 0;
8075
8499
  }
@@ -8105,12 +8529,12 @@ var init_cmux_store_source = __esm({
8105
8529
  active = false;
8106
8530
  lastError = null;
8107
8531
  constructor(opts = {}) {
8108
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir8(), ".cmuxterm");
8532
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir8(), ".cmuxterm");
8109
8533
  this.debounceMs = opts.debounceMs ?? 50;
8110
8534
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
8111
8535
  this.listFiles = opts.listFiles ?? defaultListFiles;
8112
8536
  this.readFile = opts.readFile ?? defaultReadFile;
8113
- this.fileExists = opts.fileExists ?? existsSync11;
8537
+ this.fileExists = opts.fileExists ?? existsSync12;
8114
8538
  this.watchDir = opts.watchDir ?? defaultWatchDir;
8115
8539
  this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
8116
8540
  this.cancelTimer = opts.cancelTimer ?? clearTimeout;
@@ -8167,7 +8591,7 @@ var init_cmux_store_source = __esm({
8167
8591
  }
8168
8592
  scanFile(filename) {
8169
8593
  const deps = this.deps;
8170
- const filePath = join14(this.stateDir, filename);
8594
+ const filePath = join15(this.stateDir, filename);
8171
8595
  const lockPath = `${filePath}.lock`;
8172
8596
  if (this.fileExists(lockPath)) {
8173
8597
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -8221,11 +8645,11 @@ var init_cmux_store_source = __esm({
8221
8645
  });
8222
8646
 
8223
8647
  // packages/workspaces/dist/native-hooks/native-hook-source.js
8224
- import { join as join15 } from "path";
8648
+ import { join as join16 } from "path";
8225
8649
  import { homedir as homedir9 } from "os";
8226
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
8650
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
8227
8651
  function installClaudeHooks(opts = {}) {
8228
- const settingsPath = opts.settingsPath ?? join15(homedir9(), ".claude", "settings.json");
8652
+ const settingsPath = opts.settingsPath ?? join16(homedir9(), ".claude", "settings.json");
8229
8653
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
8230
8654
  const readFile7 = opts.readFile ?? defaultReadFile2;
8231
8655
  const writeFile6 = opts.writeFile ?? defaultWriteFile;
@@ -8321,14 +8745,14 @@ function extractDetail(sub, payload) {
8321
8745
  }
8322
8746
  function defaultReadFile2(path36) {
8323
8747
  try {
8324
- return readFileSync11(path36, "utf-8");
8748
+ return readFileSync12(path36, "utf-8");
8325
8749
  } catch {
8326
8750
  return void 0;
8327
8751
  }
8328
8752
  }
8329
8753
  function defaultWriteFile(path36, content) {
8330
8754
  mkdirSync6(path36.replace(/\/[^/]+$/, ""), { recursive: true });
8331
- writeFileSync8(path36, content, "utf-8");
8755
+ writeFileSync9(path36, content, "utf-8");
8332
8756
  }
8333
8757
  var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
8334
8758
  var init_native_hook_source = __esm({
@@ -8474,6 +8898,10 @@ async function paneHasOpenModal(runtime, pane) {
8474
8898
  const screen = await runtime.readPaneScreen(pane) ?? "";
8475
8899
  return hasModalOptionList(screen);
8476
8900
  }
8901
+ async function readModalOptions(runtime, pane) {
8902
+ const screen = await runtime.readPaneScreen(pane) ?? "";
8903
+ return parseModalOptions(screen);
8904
+ }
8477
8905
  async function confirmedSendToPane(runtime, pane, message) {
8478
8906
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
8479
8907
  if (hasModalOptionList(preSendScreen)) {
@@ -8629,6 +9057,7 @@ __export(dist_exports3, {
8629
9057
  mapSubToLifecycle: () => mapSubToLifecycle,
8630
9058
  paneHasOpenModal: () => paneHasOpenModal,
8631
9059
  readCmuxLiveness: () => readCmuxLiveness,
9060
+ readModalOptions: () => readModalOptions,
8632
9061
  resendCrewFirstTurn: () => resendCrewFirstTurn,
8633
9062
  resolveCaptainWorkspace: () => resolveCaptainWorkspace,
8634
9063
  sendFirstTurnWhenReady: () => sendFirstTurnWhenReady
@@ -8680,6 +9109,9 @@ function createClaudeDriver() {
8680
9109
  if (opts.model) {
8681
9110
  cmd += ` --model ${opts.model}`;
8682
9111
  }
9112
+ if (opts.thinking) {
9113
+ cmd += ` --effort ${opts.thinking}`;
9114
+ }
8683
9115
  if (opts.autoApprove) {
8684
9116
  cmd += " --dangerously-skip-permissions";
8685
9117
  } else if (opts.permissionMode) {
@@ -8956,7 +9388,7 @@ var init_registry4 = __esm({
8956
9388
  // packages/agents/dist/drivers/launch-cmd.js
8957
9389
  import fs14 from "fs";
8958
9390
  import path14 from "path";
8959
- function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName) {
9391
+ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName, thinking) {
8960
9392
  const driver = registry.getDriver(agentName);
8961
9393
  if (driver.name === "claude") {
8962
9394
  let cmd = fresh ? "claude" : "claude -c";
@@ -8973,6 +9405,9 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
8973
9405
  if (model) {
8974
9406
  cmd += ` --model ${model}`;
8975
9407
  }
9408
+ if (thinking) {
9409
+ cmd += ` --effort ${thinking}`;
9410
+ }
8976
9411
  if (templatesDir) {
8977
9412
  const roleFile2 = path14.join(templatesDir, `${role}.claude.md`);
8978
9413
  const legacyRoleFile = path14.join(templatesDir, `${role}.CLAUDE.md`);
@@ -9753,10 +10188,10 @@ var init_codex_app_server_source = __esm({
9753
10188
  // packages/agents/dist/codex/config.js
9754
10189
  import { readFile as readFile6 } from "fs/promises";
9755
10190
  import { homedir as homedir10 } from "os";
9756
- import { join as join16 } from "path";
10191
+ import { join as join17 } from "path";
9757
10192
  async function resolveCodexModel() {
9758
- const home = process.env["CODEX_HOME"] ?? join16(homedir10(), ".codex");
9759
- const configPath = join16(home, "config.toml");
10193
+ const home = process.env["CODEX_HOME"] ?? join17(homedir10(), ".codex");
10194
+ const configPath = join17(home, "config.toml");
9760
10195
  let text;
9761
10196
  try {
9762
10197
  text = await readFile6(configPath, "utf8");
@@ -10277,9 +10712,9 @@ var init_sse_bridge = __esm({
10277
10712
 
10278
10713
  // packages/agents/dist/interactive/claude.js
10279
10714
  import { execSync as execSync7 } from "child_process";
10280
- import { readFileSync as readFileSync12 } from "fs";
10715
+ import { readFileSync as readFileSync13 } from "fs";
10281
10716
  import { homedir as homedir11 } from "os";
10282
- import { join as join17 } from "path";
10717
+ import { join as join18 } from "path";
10283
10718
  function probeClaudeSettingsFlag() {
10284
10719
  try {
10285
10720
  const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -10337,11 +10772,11 @@ function deriveTranscriptPath(sessionId, cwd) {
10337
10772
  if (!sessionId || !cwd)
10338
10773
  return null;
10339
10774
  const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
10340
- return join17(homedir11(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
10775
+ return join18(homedir11(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
10341
10776
  }
10342
10777
  function readLastAssistantText(transcriptPath) {
10343
10778
  try {
10344
- const raw = readFileSync12(transcriptPath, "utf-8");
10779
+ const raw = readFileSync13(transcriptPath, "utf-8");
10345
10780
  const lines = raw.split(/\r?\n/);
10346
10781
  for (let i = lines.length - 1; i >= 0; i--) {
10347
10782
  const line = lines[i].trim();
@@ -10407,6 +10842,29 @@ function formatAskUserQuestionPrompt(toolInput) {
10407
10842
  }
10408
10843
  return parts.length > 0 ? parts.join(" | ") : null;
10409
10844
  }
10845
+ function decideCaptainMemoryWrite(toolName, toolInput, env, homeDir) {
10846
+ if (!env.SQUADRANT_CREW_TASK_ID)
10847
+ return { decision: "allow" };
10848
+ if (toolName === "Bash") {
10849
+ const command = toolInput?.command;
10850
+ if (typeof command === "string" && CAPTAIN_MEMORY_PATH_RE.test(command)) {
10851
+ return { decision: "deny", reason: DENY_REASON };
10852
+ }
10853
+ return { decision: "allow" };
10854
+ }
10855
+ const field = FILE_PATH_FIELD_BY_TOOL[toolName];
10856
+ if (!field)
10857
+ return { decision: "allow" };
10858
+ const filePath = toolInput?.[field];
10859
+ if (typeof filePath !== "string" || !filePath)
10860
+ return { decision: "allow" };
10861
+ const home = homeDir.endsWith("/") ? homeDir.slice(0, -1) : homeDir;
10862
+ if (!filePath.startsWith(home))
10863
+ return { decision: "allow" };
10864
+ if (!CAPTAIN_MEMORY_PATH_RE.test(filePath))
10865
+ return { decision: "allow" };
10866
+ return { decision: "deny", reason: DENY_REASON };
10867
+ }
10410
10868
  function mapClaudeHookToEvent(event, payload, taskId) {
10411
10869
  switch (event) {
10412
10870
  case "PreToolUse": {
@@ -10442,7 +10900,7 @@ function mapClaudeHookToEvent(event, payload, taskId) {
10442
10900
  return null;
10443
10901
  }
10444
10902
  }
10445
- var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, claudeInteractive;
10903
+ var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, CAPTAIN_MEMORY_PATH_RE, DENY_REASON, FILE_PATH_FIELD_BY_TOOL, claudeInteractive;
10446
10904
  var init_claude3 = __esm({
10447
10905
  "packages/agents/dist/interactive/claude.js"() {
10448
10906
  EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit"];
@@ -10450,6 +10908,14 @@ var init_claude3 = __esm({
10450
10908
  ["PreToolUse", "AskUserQuestion"]
10451
10909
  ];
10452
10910
  nextAskUserQuestionRequestId = Date.now();
10911
+ CAPTAIN_MEMORY_PATH_RE = /\.claude\/projects\/.*\/memory\//;
10912
+ DENY_REASON = "Crews do not write captain memory. Put the finding in your done/blocked message; the captain decides what is durable (#556).";
10913
+ FILE_PATH_FIELD_BY_TOOL = {
10914
+ Write: "file_path",
10915
+ Edit: "file_path",
10916
+ MultiEdit: "file_path",
10917
+ NotebookEdit: "notebook_path"
10918
+ };
10453
10919
  claudeInteractive = {
10454
10920
  provider: "claude",
10455
10921
  tier: "strong",
@@ -10505,8 +10971,11 @@ function classifyPaneTail2(tail) {
10505
10971
  if (q)
10506
10972
  return { kind: "question", text: q };
10507
10973
  let errLine = null;
10508
- for (const c of cleaned) {
10509
- if (c != null && ERROR_BANNER_RE2.some((re) => re.test(c)))
10974
+ for (let i = 0; i < cleaned.length; i++) {
10975
+ const c = cleaned[i];
10976
+ if (c == null || isQuotedLine2(raw[i]))
10977
+ continue;
10978
+ if (ERROR_BANNER_RE2.some((re) => re.test(c)))
10510
10979
  errLine = c;
10511
10980
  }
10512
10981
  if (errLine) {
@@ -10518,6 +10987,10 @@ function classifyPaneTail2(tail) {
10518
10987
  }
10519
10988
  return null;
10520
10989
  }
10990
+ function isQuotedLine2(raw) {
10991
+ const noAnsi = raw.replace(/\[[0-9;]*m/g, "");
10992
+ return QUOTED_PREFIX_RE2.test(noAnsi);
10993
+ }
10521
10994
  function stripChrome2(raw) {
10522
10995
  let line = raw.replace(/\[[0-9;]*m/g, "");
10523
10996
  line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
@@ -10532,7 +11005,7 @@ function stripChrome2(raw) {
10532
11005
  return null;
10533
11006
  return trimmed;
10534
11007
  }
10535
- var ERROR_BANNER_RE2, RETRYING_RE, EXHAUSTED_RE, OPTION_RE2, PICKER_FOOTER_RE2, PURE_CHROME_RE2, STATUS_LINE_RE2;
11008
+ var ERROR_BANNER_RE2, RETRYING_RE, EXHAUSTED_RE, QUOTED_PREFIX_RE2, OPTION_RE2, PICKER_FOOTER_RE2, PURE_CHROME_RE2, STATUS_LINE_RE2;
10536
11009
  var init_pane_classifier = __esm({
10537
11010
  "packages/agents/dist/interactive/pane-classifier.js"() {
10538
11011
  init_claude3();
@@ -10547,6 +11020,7 @@ var init_pane_classifier = __esm({
10547
11020
  ];
10548
11021
  RETRYING_RE = /\bRetrying\b|\battempt\s+\d+\s*\/\s*\d+/i;
10549
11022
  EXHAUSTED_RE = /\bretr(?:y|ies)\s+(?:exhausted|limit\s+(?:reached|exceeded))\b|\bmaximum\s+retries\b/i;
11023
+ QUOTED_PREFIX_RE2 = /^\s*(?:[┃│▏▕]|>|[+-]|\d+[\t:→])\s/;
10550
11024
  OPTION_RE2 = /^[❯>›]?\s*(\d+)\.\s+(.*\S)\s*$/;
10551
11025
  PICKER_FOOTER_RE2 = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
10552
11026
  PURE_CHROME_RE2 = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
@@ -10776,7 +11250,7 @@ var init_headless_launcher = __esm({
10776
11250
 
10777
11251
  // packages/agents/dist/claude/registry.js
10778
11252
  import fs15 from "fs";
10779
- import { join as join18 } from "path";
11253
+ import { join as join19 } from "path";
10780
11254
  import { homedir as homedir12 } from "os";
10781
11255
  function parseRegistryDir(files, readFile7) {
10782
11256
  const out = [];
@@ -10824,7 +11298,7 @@ function readClaudeStatus(task) {
10824
11298
  } catch {
10825
11299
  return void 0;
10826
11300
  }
10827
- const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join18(CLAUDE_SESSIONS_DIR, name), "utf8"));
11301
+ const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
10828
11302
  let entry;
10829
11303
  if (task.messagingSocketPath) {
10830
11304
  entry = entries.find((e) => e.messagingSocketPath === task.messagingSocketPath);
@@ -10843,7 +11317,7 @@ function readClaudeStatusByCwd(cwd) {
10843
11317
  } catch {
10844
11318
  return void 0;
10845
11319
  }
10846
- const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join18(CLAUDE_SESSIONS_DIR, name), "utf8"));
11320
+ const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
10847
11321
  const entry = entries.find((e) => e.cwd === cwd);
10848
11322
  if (!entry)
10849
11323
  return void 0;
@@ -10856,7 +11330,7 @@ function readClaudeStatusBySocketPath(socketPath2) {
10856
11330
  } catch {
10857
11331
  return void 0;
10858
11332
  }
10859
- const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join18(CLAUDE_SESSIONS_DIR, name), "utf8"));
11333
+ const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
10860
11334
  const entry = entries.find((e) => e.messagingSocketPath === socketPath2);
10861
11335
  if (!entry)
10862
11336
  return void 0;
@@ -10865,14 +11339,14 @@ function readClaudeStatusBySocketPath(socketPath2) {
10865
11339
  var CLAUDE_SESSIONS_DIR, PID_JSON;
10866
11340
  var init_registry8 = __esm({
10867
11341
  "packages/agents/dist/claude/registry.js"() {
10868
- CLAUDE_SESSIONS_DIR = join18(homedir12(), ".claude", "sessions");
11342
+ CLAUDE_SESSIONS_DIR = join19(homedir12(), ".claude", "sessions");
10869
11343
  PID_JSON = /^(\d+)\.json$/;
10870
11344
  }
10871
11345
  });
10872
11346
 
10873
11347
  // packages/agents/dist/claude/peer-registry-source.js
10874
- import { readdirSync as readdirSync6, readFileSync as readFileSync13 } from "fs";
10875
- import { join as join19 } from "path";
11348
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14 } from "fs";
11349
+ import { join as join20 } from "path";
10876
11350
  function defaultIsAlive(pid) {
10877
11351
  try {
10878
11352
  process.kill(pid, 0);
@@ -10901,7 +11375,7 @@ var init_peer_registry_source = __esm({
10901
11375
  log;
10902
11376
  constructor(o = {}) {
10903
11377
  this.readdir = o.readdir ?? (() => readdirSync6(CLAUDE_SESSIONS_DIR));
10904
- this.readFile = o.readFile ?? ((n) => readFileSync13(join19(CLAUDE_SESSIONS_DIR, n), "utf8"));
11378
+ this.readFile = o.readFile ?? ((n) => readFileSync14(join20(CLAUDE_SESSIONS_DIR, n), "utf8"));
10905
11379
  this.isAlive = o.isAlive ?? defaultIsAlive;
10906
11380
  this.now = o.now ?? Date.now;
10907
11381
  this.pollMs = o.pollMs ?? 2e3;
@@ -11447,6 +11921,7 @@ __export(dist_exports4, {
11447
11921
  createGeminiEmitter: () => createGeminiEmitter,
11448
11922
  createOpencodeDriver: () => createOpencodeDriver,
11449
11923
  createOpencodeEmitter: () => createOpencodeEmitter,
11924
+ decideCaptainMemoryWrite: () => decideCaptainMemoryWrite,
11450
11925
  deriveTranscriptPath: () => deriveTranscriptPath,
11451
11926
  detectTrailingQuestion: () => detectTrailingQuestion2,
11452
11927
  formatAskUserQuestionPrompt: () => formatAskUserQuestionPrompt,
@@ -11568,9 +12043,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
11568
12043
  const resolved = resolveTarget(registry, config, target, !!opts.command);
11569
12044
  await needRef(resolved);
11570
12045
  const finalProject = opts.command ? config.commandName : target;
11571
- const { join: join28, dirname: dirname9 } = await import("path");
12046
+ const { join: join30, dirname: dirname9 } = await import("path");
11572
12047
  const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
11573
- const stateRoot = join28(dirname9(DEFAULT_CONFIG_PATH2), "state");
12048
+ const stateRoot = join30(dirname9(DEFAULT_CONFIG_PATH2), "state");
11574
12049
  const seq = await appendCaptainMessage2({
11575
12050
  stateRoot,
11576
12051
  project: finalProject,
@@ -11676,10 +12151,10 @@ var init_runtime2 = __esm({
11676
12151
  init_dist();
11677
12152
  init_dist2();
11678
12153
  import { Command as Command35 } from "commander";
11679
- import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
12154
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
11680
12155
  import { fileURLToPath as fileURLToPath6 } from "url";
11681
- import { dirname as dirname8, join as join27 } from "path";
11682
- import { homedir as homedir17 } from "os";
12156
+ import { dirname as dirname8, join as join29 } from "path";
12157
+ import { homedir as homedir18 } from "os";
11683
12158
 
11684
12159
  // packages/cli/src/commands/doctor.ts
11685
12160
  init_dist();
@@ -12085,8 +12560,8 @@ import chalk4 from "chalk";
12085
12560
 
12086
12561
  // packages/cli/src/lib/per-crew-settings.ts
12087
12562
  init_dist4();
12088
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
12089
- import { dirname as dirname4, join as join20 } from "path";
12563
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
12564
+ import { dirname as dirname4, join as join21 } from "path";
12090
12565
  import { homedir as homedir13 } from "os";
12091
12566
  var CREW_PERMISSION_ALLOWLIST = [
12092
12567
  // git — read + safe mutations (reset/clean/config intentionally excluded)
@@ -12178,21 +12653,21 @@ function mergeCrewPermissions(settings) {
12178
12653
  return next;
12179
12654
  }
12180
12655
  function writePerCrewSettingsLocal(o) {
12181
- const dir = join20(o.projectCwd, ".claude");
12656
+ const dir = join21(o.projectCwd, ".claude");
12182
12657
  mkdirSync7(dir, { recursive: true });
12183
- const file = join20(dir, "settings.local.json");
12658
+ const file = join21(dir, "settings.local.json");
12184
12659
  let existing = {};
12185
12660
  try {
12186
- const raw = healStaleCockpitRefs(readFileSync14(file, "utf-8"));
12661
+ const raw = healStaleCockpitRefs(readFileSync15(file, "utf-8"));
12187
12662
  existing = JSON.parse(raw);
12188
12663
  } catch {
12189
12664
  }
12190
12665
  const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
12191
12666
  const merged = mergeCrewPermissions(withHooks);
12192
- writeFileSync9(file, JSON.stringify(merged, null, 2));
12667
+ writeFileSync10(file, JSON.stringify(merged, null, 2));
12193
12668
  return file;
12194
12669
  }
12195
- var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join20(homedir13(), ".config", "opencode", "opencode.json");
12670
+ var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join21(homedir13(), ".config", "opencode", "opencode.json");
12196
12671
  function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
12197
12672
  mkdirSync7(dirname4(configPath), { recursive: true });
12198
12673
  const defaultConfig = {
@@ -12200,7 +12675,7 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
12200
12675
  model: "anthropic/claude-sonnet-4-5"
12201
12676
  };
12202
12677
  try {
12203
- writeFileSync9(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
12678
+ writeFileSync10(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
12204
12679
  return configPath;
12205
12680
  } catch (err) {
12206
12681
  if (err.code === "EEXIST") return null;
@@ -12209,16 +12684,16 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
12209
12684
  }
12210
12685
  function readGlobalOpencodeModel(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
12211
12686
  try {
12212
- const parsed = JSON.parse(readFileSync14(configPath, "utf-8"));
12687
+ const parsed = JSON.parse(readFileSync15(configPath, "utf-8"));
12213
12688
  return typeof parsed.model === "string" ? parsed.model : void 0;
12214
12689
  } catch {
12215
12690
  return void 0;
12216
12691
  }
12217
12692
  }
12218
12693
  function writePerCrewOpencodeConfig(o) {
12219
- const dir = join20(o.stateRoot, o.project, o.taskId);
12694
+ const dir = join21(o.stateRoot, o.project, o.taskId);
12220
12695
  mkdirSync7(dir, { recursive: true });
12221
- const file = join20(dir, "opencode.json");
12696
+ const file = join21(dir, "opencode.json");
12222
12697
  const config = {
12223
12698
  permission: {
12224
12699
  read: "allow",
@@ -12233,7 +12708,7 @@ function writePerCrewOpencodeConfig(o) {
12233
12708
  external_directory: { "**": "allow" }
12234
12709
  }
12235
12710
  };
12236
- writeFileSync9(file, JSON.stringify(config, null, 2));
12711
+ writeFileSync10(file, JSON.stringify(config, null, 2));
12237
12712
  return file;
12238
12713
  }
12239
12714
 
@@ -12721,8 +13196,8 @@ import { createConnection as createConnection3 } from "net";
12721
13196
  import { randomUUID as randomUUID5 } from "crypto";
12722
13197
  import { execFileSync as execFileSync7 } from "child_process";
12723
13198
  import { homedir as homedir14 } from "os";
12724
- import { join as join21 } from "path";
12725
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
13199
+ import { join as join22 } from "path";
13200
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
12726
13201
 
12727
13202
  // packages/cli/src/commands/crew-output.ts
12728
13203
  function tailLines(text, maxLines = 40, maxBytes = 4096) {
@@ -13180,10 +13655,10 @@ function buildSignalRequest(signal, o) {
13180
13655
  return { kind: "event", project, event };
13181
13656
  }
13182
13657
  function defaultWriteResult(id, payload) {
13183
- const dir = join21(homedir14(), ".config", "squadrant", "state", "_results");
13658
+ const dir = join22(homedir14(), ".config", "squadrant", "state", "_results");
13184
13659
  mkdirSync8(dir, { recursive: true });
13185
- const file = join21(dir, `${id}.txt`);
13186
- writeFileSync10(file, payload);
13660
+ const file = join22(dir, `${id}.txt`);
13661
+ writeFileSync11(file, payload);
13187
13662
  return file;
13188
13663
  }
13189
13664
  async function runCrewSignal(signal, o, deps) {
@@ -13196,7 +13671,7 @@ async function runCrewSignal(signal, o, deps) {
13196
13671
  const current = await deps.call(buildStatusRequest(project, taskId));
13197
13672
  if (current && TERMINAL_STATES.has(current.state)) {
13198
13673
  throw new Error(
13199
- `Task ${taskId} is already terminal (state=${current.state}) \u2014 signal '${signal}' would be silently ignored by the daemon. Stop here: your task record was never reopened for this turn. Ask the captain to run 'squadrant crew send' to reopen it before signaling again.`
13674
+ `Task ${taskId} is already terminal (state=${current.state}) \u2014 signal '${signal}' would be silently ignored by the daemon. Stop here: your task record was never reopened for this turn. Ask the captain to run 'squadrant crew send' to reopen it \u2014 its output confirms with "\u21BB Task was terminal \u2014 reopened to working" (#595) \u2014 before signaling again.`
13200
13675
  );
13201
13676
  }
13202
13677
  const req = buildSignalRequest(signal, { ...o, writeResult: o.writeResult ?? defaultWriteResult });
@@ -13259,6 +13734,20 @@ async function runCrewApprove(project, crew, deps) {
13259
13734
  });
13260
13735
  return prUrl;
13261
13736
  }
13737
+ async function runCrewReply(project, id, message, deps) {
13738
+ const tasks = await deps.listTasks(project);
13739
+ const matches = tasks.filter((t) => t.id === id || t.id.startsWith(id));
13740
+ if (matches.length === 0) throw new Error(`unknown task ${id}`);
13741
+ if (matches.length > 1) {
13742
+ throw new Error(`task id '${id}' is ambiguous \u2014 matches ${matches.map((t) => t.id).join(", ")}`);
13743
+ }
13744
+ const target = matches[0];
13745
+ if (!target.name) {
13746
+ throw new Error(`task ${id} has no crew name on record \u2014 cannot deliver via 'crew reply'`);
13747
+ }
13748
+ await deps.sendCrew(project, target.name, message);
13749
+ return deps.getStatus(project, target.id);
13750
+ }
13262
13751
  function addControlPlaneCrewCommands(crew) {
13263
13752
  crew.command("dispatch <project> <task>").description("Dispatch a crew task via the control-plane daemon").requiredOption("--provider <p>", "claude|opencode|codex (gemini: experimental, headless not supported)").option("--mode <m>", "headless|interactive", "interactive").option("--cwd <dir>", "working dir for the crew (project/worktree); required for codex to edit code").action(async (project, task, opts) => {
13264
13753
  const req = buildDispatchRequest({ project, task, provider: opts.provider, mode: opts.mode, cwd: opts.cwd });
@@ -13295,14 +13784,18 @@ function addControlPlaneCrewCommands(crew) {
13295
13784
  const compact = opts.json !== true;
13296
13785
  process.stdout.write(formatCompactTasks(records, { compact }) + "\n");
13297
13786
  });
13298
- crew.command("reply <project> <id> <message>").description("Reply to a blocked control-plane task (delivery deferred), or resolve a gate via --gate").option("--gate <gateId>", "resolve a pending gate by id (codex interactive, spec \xA74.9)").action(async (project, id, message, opts) => {
13787
+ crew.command("reply <project> <id> [message]").description("Reply to a blocked crew task \u2014 delivers the message first, then transitions state; never transitions on a dropped message. Resolve a gate via --gate.").option("--message-file <path>", "Read message from file instead of positional arg ('-' for stdin)").option("--gate <gateId>", "resolve a pending gate by id (codex interactive, spec \xA74.9)").action(async (project, id, message, opts) => {
13788
+ const resolvedMessage = await resolveTextInput({ positional: message, filePath: opts.messageFile, label: "message" });
13299
13789
  if (opts.gate) {
13300
- const r2 = await squadrantdCall(buildGateResolveRequest({ project, gateId: opts.gate, message }));
13790
+ const r2 = await squadrantdCall(buildGateResolveRequest({ project, gateId: opts.gate, message: resolvedMessage }));
13301
13791
  process.stdout.write(JSON.stringify(r2) + "\n");
13302
13792
  return;
13303
13793
  }
13304
- process.stderr.write("reply delivery is not yet wired (deferred); state transitioned only\n");
13305
- const r = await squadrantdCall({ kind: "reply", project, id, message });
13794
+ const r = await runCrewReply(project, id, resolvedMessage, {
13795
+ listTasks: async (p) => await squadrantdCall({ kind: "list", project: p }),
13796
+ sendCrew: (p, name, msg) => runCrewSend2(p, name, msg),
13797
+ getStatus: (p, taskId) => squadrantdCall(buildStatusRequest(p, taskId))
13798
+ });
13306
13799
  process.stdout.write(JSON.stringify(r) + "\n");
13307
13800
  });
13308
13801
  crew.command("_hook <event>", { hidden: true }).description("internal: bridge from claude Stop/SubagentStop/SessionEnd hooks to squadrantd").action(async (event) => {
@@ -13556,14 +14049,30 @@ async function runCrewList2(project) {
13556
14049
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
13557
14050
  return runCrewList(project, runtime, workspaceId);
13558
14051
  }
14052
+ async function runCrewAnswer2(project, name, option, opts) {
14053
+ const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
14054
+ return runCrewAnswer(
14055
+ project,
14056
+ name,
14057
+ option,
14058
+ runtime,
14059
+ workspaceId,
14060
+ {
14061
+ readModalOptions: (pane) => readModalOptions(runtime, pane),
14062
+ log: (m) => console.log(chalk10.dim(m))
14063
+ },
14064
+ opts
14065
+ );
14066
+ }
13559
14067
  var crewCommand = new Command10("crew").description(
13560
14068
  "Spawn and manage interactive crew sessions next to the project's captain"
13561
14069
  );
13562
14070
  crewCommand.command("spawn").description(
13563
14071
  "Spawn an interactive crew session as a tab in the captain's workspace (use --direction to split into a pane instead)"
13564
- ).argument("<project>", "Project name (must be registered)").argument("[task]", "Initial task prompt for the crew session (omit with --task-file)").option("--name <name>", "Crew name (default: auto-generated crew-N)").option("--direction <dir>", "Placement: tab (default) or split direction (right|left|up|down)", "tab").option("--agent <name>", "Agent CLI to use (claude|codex|gemini|opencode)", "claude").option("--approval", "gate risky tools so the captain approves them (codex: approvalPolicy='untrusted'; opencode: bash:'ask')", false).option("--shared", "run the crew in the root checkout instead of an isolated worktree (for small/one-off tasks)", false).option("--task-file <path>", "Read task prompt from file instead of positional arg ('-' for stdin)").option("--model <alias>", "Override crew model for this spawn (e.g. sonnet, opus); takes precedence over config defaults.roles.crew.model").action(
14072
+ ).argument("<project>", "Project name (must be registered)").argument("[task]", "Initial task prompt for the crew session (omit with --task-file)").option("--name <name>", "Crew name (default: auto-generated crew-N)").option("--direction <dir>", "Placement: tab (default) or split direction (right|left|up|down)", "tab").option("--agent <name>", "Agent CLI to use (claude|codex|gemini|opencode)", "claude").option("--approval", "gate risky tools so the captain approves them (codex: approvalPolicy='untrusted'; opencode: bash:'ask')", false).option("--shared", "run the crew in the root checkout instead of an isolated worktree (for small/one-off tasks)", false).option("--task-file <path>", "Read task prompt from file instead of positional arg ('-' for stdin)").option("--model <alias>", "Override crew model for this spawn (e.g. sonnet, opus); takes precedence over config defaults.roles.crew.model").option("--thinking <level>", `Override crew thinking level for this spawn (${THINKING_LEVELS.join("|")}) \u2192 claude --effort; takes precedence over config defaults.roles.crew.thinking`).action(
13565
14073
  async (project, task, opts, cmd) => {
13566
14074
  try {
14075
+ const thinking = opts.thinking ? parseThinkingLevel(opts.thinking) : void 0;
13567
14076
  const resolvedTask = await resolveTextInput({ positional: task, filePath: opts.taskFile, label: "task" });
13568
14077
  const agentExplicit = cmd.getOptionValueSource("agent") === "cli";
13569
14078
  const pane = await runCrewSpawn2({
@@ -13578,6 +14087,7 @@ crewCommand.command("spawn").description(
13578
14087
  ...opts.approval ? { approvalPolicy: "untrusted", approval: true } : {},
13579
14088
  ...opts.shared ? { shared: true } : {},
13580
14089
  ...opts.model ? { model: opts.model } : {},
14090
+ ...thinking ? { thinking } : {},
13581
14091
  // #458: pass the raw file path (not stdin) so runCrewSpawn can copy it
13582
14092
  // into the isolated worktree root for relative-path access.
13583
14093
  ...opts.taskFile && opts.taskFile !== "-" ? { taskFile: opts.taskFile } : {}
@@ -13631,13 +14141,29 @@ crewCommand.command("list").description("List live crew sessions for a project")
13631
14141
  crewCommand.command("send").description("Send a follow-up message to an existing crew session").argument("<project>", "Project name").argument("<name>", "Crew name (e.g. crew-1)").argument("[message]", "Message to send (omit with --message-file)").option("--message-file <path>", "Read message from file instead of positional arg ('-' for stdin)").option("--force", "override an operator takeover (only when the operator told you to)", false).action(async (project, name, message, opts) => {
13632
14142
  try {
13633
14143
  const resolvedMessage = await resolveTextInput({ positional: message, filePath: opts.messageFile, label: "message" });
13634
- await runCrewSend2(project, name, resolvedMessage, opts);
14144
+ const { reopened } = await runCrewSend2(project, name, resolvedMessage, opts);
14145
+ if (reopened) console.log(chalk10.cyan(`\u21BB Task was terminal \u2014 reopened to working`));
13635
14146
  console.log(chalk10.green(`\u2714 Sent to ${project}:${name}`));
13636
14147
  } catch (e) {
13637
14148
  console.error(chalk10.red(e.message));
13638
14149
  process.exit(1);
13639
14150
  }
13640
14151
  });
14152
+ crewCommand.command("answer").description(
14153
+ "Deliberately answer a crew's open AskUserQuestion/permission prompt (#592) \u2014 never an implicit default"
14154
+ ).argument("<project>", "Project name").argument("<name>", "Crew name").argument("<option>", "1-based option index, or an exact/prefix match of the option's text").option("--expect <text>", "Refuse unless the resolved option's label contains this text (guards against option order shifting)").option("--text <answer>", "For a free-text option (e.g. 'Type something.'): select it, then type this answer and submit").action(async (project, name, option, opts) => {
14155
+ try {
14156
+ const { selected, closed } = await runCrewAnswer2(project, name, option, opts);
14157
+ if (closed) {
14158
+ console.log(chalk10.green(`\u2714 Answered ${project}:${name} with ${selected.index}. "${selected.label}" \u2014 prompt closed`));
14159
+ } else {
14160
+ console.log(chalk10.yellow(`\u26A0 Sent ${selected.index}. "${selected.label}" to ${project}:${name}, but the prompt still appears open \u2014 read it again with 'squadrant crew read ${project} ${name}'`));
14161
+ }
14162
+ } catch (err) {
14163
+ console.error(chalk10.red(err.message));
14164
+ process.exit(1);
14165
+ }
14166
+ });
13641
14167
  crewCommand.command("read").description("Read the current screen of a crew session (tail by default; use --full for the entire scrollback)").argument("<project>", "Project name").argument("<name>", "Crew name").option("--lines <N>", "Number of trailing lines to show", "40").option("--full", "Show the entire scrollback (overrides --lines)").action(async (project, name, opts) => {
13642
14168
  try {
13643
14169
  const screen = await runCrewRead2(project, name);
@@ -14214,9 +14740,9 @@ function mergeSnapshot(daemon, external, now) {
14214
14740
  // packages/web/dist/probes.js
14215
14741
  init_dist();
14216
14742
  init_dist();
14217
- import { join as join22 } from "path";
14743
+ import { join as join23 } from "path";
14218
14744
  import { homedir as homedir15 } from "os";
14219
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
14745
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
14220
14746
  import { execFile as execFile4 } from "child_process";
14221
14747
  var DEFAULT_TIMEOUT_MS2 = 2e3;
14222
14748
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
@@ -14252,7 +14778,7 @@ function vaultProbe(run, dir) {
14252
14778
  return { state: "unknown", detail: "no vault configured" };
14253
14779
  if (!run.pathExists(dir))
14254
14780
  return { state: "gone", detail: "vault directory missing" };
14255
- if (!run.pathExists(join22(dir, ".obsidian")))
14781
+ if (!run.pathExists(join23(dir, ".obsidian")))
14256
14782
  return { state: "gone", detail: "no .obsidian/ (not a vault)" };
14257
14783
  return { state: "alive" };
14258
14784
  } catch {
@@ -14320,13 +14846,13 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS2) {
14320
14846
  const sessions = probeSessions(run);
14321
14847
  return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
14322
14848
  }
14323
- var SESSIONS_PATH = join22(homedir15(), ".config", "squadrant", "sessions.json");
14849
+ var SESSIONS_PATH = join23(homedir15(), ".config", "squadrant", "sessions.json");
14324
14850
  function onPath(cli) {
14325
14851
  const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
14326
- return dirs.some((d) => existsSync12(join22(d, cli)));
14852
+ return dirs.some((d) => existsSync13(join23(d, cli)));
14327
14853
  }
14328
14854
  function readSessionsHashes() {
14329
- const raw = JSON.parse(readFileSync15(SESSIONS_PATH, "utf-8"));
14855
+ const raw = JSON.parse(readFileSync16(SESSIONS_PATH, "utf-8"));
14330
14856
  const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
14331
14857
  return [...new Set(hashes)];
14332
14858
  }
@@ -14340,7 +14866,7 @@ function defaultProbeRunners() {
14340
14866
  }
14341
14867
  }),
14342
14868
  probeOnPath: async (cli) => onPath(cli),
14343
- pathExists: (p) => existsSync12(p),
14869
+ pathExists: (p) => existsSync13(p),
14344
14870
  loadConfig: () => loadConfig(),
14345
14871
  loadSessionsHashes: () => readSessionsHashes()
14346
14872
  };
@@ -15398,7 +15924,9 @@ async function selectCaptainsInteractive(entries, yesterday = getYesterday()) {
15398
15924
  function resolveLaunchAgent(overrides, roleConfig, roleModelDefault) {
15399
15925
  return {
15400
15926
  agentName: overrides.agent ?? roleConfig?.agent ?? "claude",
15401
- model: overrides.model ?? roleConfig?.model ?? roleModelDefault
15927
+ model: overrides.model ?? roleConfig?.model ?? roleModelDefault,
15928
+ // No built-in default: unset ⇒ flag omitted ⇒ the agent's own effort.
15929
+ thinking: overrides.thinking ?? roleConfig?.thinking
15402
15930
  };
15403
15931
  }
15404
15932
 
@@ -15435,11 +15963,20 @@ function ensureCmuxReady(headless) {
15435
15963
  }
15436
15964
  var launchCommand = new Command14("launch").description(
15437
15965
  "Launch a project captain (with project arg) or all captains (--all). Use `squadrant command` for one-shot Command tasks."
15438
- ).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--keep", "Resume the latest session even on a new day / after a template change").option("--all", "Launch all captain workspaces").option("--headless", "Skip the interactive cmux-app requirement (used by the daemon to boot captains without a terminal)").option("--agent <name>", "Override captain agent for this launch (claude|codex|gemini|opencode); takes precedence over defaults.roles.captain.agent").option("--model <name>", "Override captain model for this launch; takes precedence over defaults.roles.captain.model").action(async (project, opts) => {
15966
+ ).argument("[project]", "Project name to launch captain for").option("--fresh", "Start a new session instead of resuming the last one").option("--keep", "Resume the latest session even on a new day / after a template change").option("--all", "Launch all captain workspaces").option("--headless", "Skip the interactive cmux-app requirement (used by the daemon to boot captains without a terminal)").option("--agent <name>", "Override captain agent for this launch (claude|codex|gemini|opencode); takes precedence over defaults.roles.captain.agent").option("--model <name>", "Override captain model for this launch; takes precedence over defaults.roles.captain.model").option("--thinking <level>", `Override captain thinking level for this launch (${THINKING_LEVELS.join("|")}) \u2192 claude --effort; takes precedence over defaults.roles.captain.thinking`).action(async (project, opts) => {
15439
15967
  if (opts.fresh && opts.keep) {
15440
15968
  console.error(chalk15.red("\n \u2718 --fresh and --keep are mutually exclusive\n"));
15441
15969
  process.exit(1);
15442
15970
  }
15971
+ let thinkingOverride;
15972
+ try {
15973
+ thinkingOverride = opts.thinking ? parseThinkingLevel(opts.thinking) : void 0;
15974
+ } catch (err) {
15975
+ console.error(chalk15.red(`
15976
+ \u2718 ${err.message}
15977
+ `));
15978
+ process.exit(1);
15979
+ }
15443
15980
  const config = loadConfig();
15444
15981
  let hadFailure = false;
15445
15982
  const drivers = {
@@ -15452,8 +15989,8 @@ var launchCommand = new Command14("launch").description(
15452
15989
  const runtimes = new RuntimeRegistry({ cmux: createCmuxDriver() });
15453
15990
  async function launchOne(workspaceName, role, cwd, permissionMode, navigate, pinToTop = false, projectName) {
15454
15991
  const roleConfig = config.defaults.roles?.[role];
15455
- const { agentName, model } = resolveLaunchAgent(
15456
- { agent: opts.agent, model: opts.model },
15992
+ const { agentName, model, thinking } = resolveLaunchAgent(
15993
+ { agent: opts.agent, model: opts.model, thinking: thinkingOverride },
15457
15994
  roleConfig,
15458
15995
  config.defaults.models?.[role]
15459
15996
  );
@@ -15496,7 +16033,8 @@ var launchCommand = new Command14("launch").description(
15496
16033
  model,
15497
16034
  TEMPLATES_DIR4,
15498
16035
  resolveCaptainSocketPath(captainChannelEnabled, projectName, workspaceName),
15499
- resolveCaptainSessionName(agentName, projectName)
16036
+ resolveCaptainSessionName(agentName, projectName),
16037
+ thinking
15500
16038
  );
15501
16039
  },
15502
16040
  initialPrompt,
@@ -16403,7 +16941,7 @@ init_dist2();
16403
16941
  import { Command as Command23 } from "commander";
16404
16942
  import fs24 from "fs";
16405
16943
  import { fileURLToPath as fileURLToPath5 } from "url";
16406
- import { dirname as dirname5, join as join23 } from "path";
16944
+ import { dirname as dirname5, join as join24 } from "path";
16407
16945
  import chalk23 from "chalk";
16408
16946
  function runConfigCheck(opts) {
16409
16947
  const raw = JSON.parse(fs24.readFileSync(opts.configPath, "utf-8"));
@@ -16539,14 +17077,14 @@ configCommand.command("set").description("Write a config value by dotted key (e.
16539
17077
  }
16540
17078
  });
16541
17079
  function readPkgVersion2() {
16542
- const pkgPath = join23(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
17080
+ const pkgPath = join24(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
16543
17081
  return JSON.parse(fs24.readFileSync(pkgPath, "utf-8")).version;
16544
17082
  }
16545
17083
 
16546
17084
  // packages/cli/src/commands/heal.ts
16547
17085
  init_dist();
16548
17086
  import { execFileSync as execFileSync9 } from "child_process";
16549
- import { join as join24 } from "path";
17087
+ import { join as join25 } from "path";
16550
17088
  import { homedir as homedir16 } from "os";
16551
17089
  import { Command as Command24 } from "commander";
16552
17090
  import chalk24 from "chalk";
@@ -16562,9 +17100,16 @@ function buildHealStatus(components) {
16562
17100
  project: c.project,
16563
17101
  ref: c.ref,
16564
17102
  state: c.state,
17103
+ detail: c.detail,
17104
+ stuck: c.stuck,
16565
17105
  healCmd: healCmdFor(c)
16566
17106
  }));
16567
- const healthy = out.every((c) => c.healCmd === null);
17107
+ const healthy = out.every((c) => {
17108
+ if (c.kind === "delivery") {
17109
+ return c.stuck !== true;
17110
+ }
17111
+ return c.healCmd === null;
17112
+ });
16568
17113
  return { healthy, components: out };
16569
17114
  }
16570
17115
  async function runHealStatus(opts) {
@@ -16601,15 +17146,37 @@ async function runHealStatus(opts) {
16601
17146
  }
16602
17147
  if (result.healthy) {
16603
17148
  stdout.write(chalk24.green("\u2714 all components healthy\n"));
17149
+ for (const c of result.components) {
17150
+ if (c.kind === "delivery" && c.state === "stale") {
17151
+ const glyph = chalk24.yellow("\u2022");
17152
+ const stateColor2 = chalk24.yellow;
17153
+ stdout.write(` ${glyph} ${c.kind.padEnd(8)} ${c.ref.padEnd(16)} ${stateColor2(c.state.padEnd(8))} ${c.project}
17154
+ `);
17155
+ if (c.detail) {
17156
+ stdout.write(` detail: ${c.detail}
17157
+ `);
17158
+ }
17159
+ }
17160
+ }
16604
17161
  return 0;
16605
17162
  }
16606
17163
  stdout.write(chalk24.bold("Unhealthy components:\n\n"));
16607
17164
  for (const c of result.components) {
16608
- if (c.healCmd) {
16609
- stdout.write(` ${chalk24.red("\u2718")} ${c.kind.padEnd(8)} ${c.ref.padEnd(16)} ${chalk24.red(c.state.padEnd(8))} ${c.project}
17165
+ const isUnhealthy = c.kind === "delivery" ? c.stuck === true : c.healCmd !== null;
17166
+ const isAdvisoryDelivery = c.kind === "delivery" && c.state === "stale" && c.stuck !== true;
17167
+ if (isUnhealthy || isAdvisoryDelivery) {
17168
+ const glyph = c.state === "stale" || isAdvisoryDelivery ? chalk24.yellow("\u2022") : chalk24.red("\u2718");
17169
+ const stateColor2 = c.state === "stale" || isAdvisoryDelivery ? chalk24.yellow : chalk24.red;
17170
+ stdout.write(` ${glyph} ${c.kind.padEnd(8)} ${c.ref.padEnd(16)} ${stateColor2(c.state.padEnd(8))} ${c.project}
16610
17171
  `);
16611
- stdout.write(` heal: ${chalk24.cyan(c.healCmd)}
17172
+ if (c.detail) {
17173
+ stdout.write(` detail: ${c.detail}
17174
+ `);
17175
+ }
17176
+ if (c.healCmd) {
17177
+ stdout.write(` heal: ${chalk24.cyan(c.healCmd)}
16612
17178
  `);
17179
+ }
16613
17180
  }
16614
17181
  }
16615
17182
  return 2;
@@ -16755,8 +17322,8 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
16755
17322
  }
16756
17323
  const config = loadConfig();
16757
17324
  const projects = opts.all ? Object.keys(config.projects) : [project];
16758
- const stateRoot = join24(homedir16(), ".config", "squadrant", "state");
16759
- const registry = new LivenessRegistry({ path: join24(stateRoot, "liveness.json") });
17325
+ const stateRoot = join25(homedir16(), ".config", "squadrant", "state");
17326
+ const registry = new LivenessRegistry({ path: join25(stateRoot, "liveness.json") });
16760
17327
  registry.load();
16761
17328
  const code = await runHealCaptain(projects, {
16762
17329
  liveness: readCmuxLiveness,
@@ -16840,7 +17407,7 @@ init_dist();
16840
17407
  init_dist2();
16841
17408
  init_runtime2();
16842
17409
  init_require_daemon();
16843
- import { join as join25, dirname as dirname6 } from "path";
17410
+ import { join as join27, dirname as dirname6 } from "path";
16844
17411
  import { Command as Command27 } from "commander";
16845
17412
  import chalk28 from "chalk";
16846
17413
 
@@ -16848,26 +17415,55 @@ import chalk28 from "chalk";
16848
17415
  init_dist4();
16849
17416
  init_dist2();
16850
17417
  import { createServer as createServer5, connect as netConnect2 } from "net";
16851
- import { unlinkSync as unlinkSync3 } from "fs";
17418
+ import fs25 from "fs";
17419
+ import { join as join26 } from "path";
16852
17420
  import { randomUUID as randomUUID7 } from "crypto";
16853
17421
  import chalk27 from "chalk";
16854
17422
  var shared;
17423
+ var registryEntryPath = () => join26(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
17424
+ function unregisterSenderIdentity() {
17425
+ try {
17426
+ fs25.unlinkSync(registryEntryPath());
17427
+ } catch {
17428
+ }
17429
+ }
17430
+ function registerSenderIdentity(socketPath2) {
17431
+ try {
17432
+ unregisterSenderIdentity();
17433
+ fs25.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
17434
+ fs25.writeFileSync(
17435
+ registryEntryPath(),
17436
+ JSON.stringify({
17437
+ pid: process.pid,
17438
+ sessionId: randomUUID7(),
17439
+ name: "squadrantd",
17440
+ messagingSocketPath: socketPath2,
17441
+ kind: "daemon",
17442
+ peerProtocol: 1
17443
+ })
17444
+ );
17445
+ process.on("exit", unregisterSenderIdentity);
17446
+ } catch {
17447
+ }
17448
+ }
16855
17449
  async function sharedReceiptListener() {
16856
17450
  if (shared) return shared;
17451
+ const socketPath2 = `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`;
16857
17452
  const listener = new ClaudeReceiptListener({
16858
- socketPath: `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`,
17453
+ socketPath: socketPath2,
16859
17454
  createServer: (h) => createServer5(h),
16860
17455
  // A UDS path is not cleaned up when a process is killed, so our own leftover
16861
17456
  // must never be the reason we refuse to start.
16862
17457
  unlinkStale: (p) => {
16863
17458
  try {
16864
- unlinkSync3(p);
17459
+ fs25.unlinkSync(p);
16865
17460
  } catch {
16866
17461
  }
16867
17462
  },
16868
17463
  log: (m) => console.error(chalk27.dim(m))
16869
17464
  });
16870
17465
  await listener.start();
17466
+ registerSenderIdentity(socketPath2);
16871
17467
  shared = listener;
16872
17468
  return shared;
16873
17469
  }
@@ -16922,7 +17518,7 @@ async function runPing(project, message) {
16922
17518
  log: (m) => console.error(chalk28.dim(m))
16923
17519
  });
16924
17520
  if (!handled) {
16925
- const stateRoot = join25(dirname6(DEFAULT_CONFIG_PATH), "state");
17521
+ const stateRoot = join27(dirname6(DEFAULT_CONFIG_PATH), "state");
16926
17522
  await appendCaptainMessage({
16927
17523
  stateRoot,
16928
17524
  project,
@@ -17001,7 +17597,7 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
17001
17597
  // packages/cli/src/commands/effort.ts
17002
17598
  init_dist();
17003
17599
  init_dist2();
17004
- import fs25 from "fs";
17600
+ import fs26 from "fs";
17005
17601
  import path29 from "path";
17006
17602
  import { Command as Command29 } from "commander";
17007
17603
  import chalk30 from "chalk";
@@ -17045,7 +17641,7 @@ function effortScopeLabel(projectName) {
17045
17641
  }
17046
17642
  function canonical(p) {
17047
17643
  try {
17048
- return fs25.realpathSync(p);
17644
+ return fs26.realpathSync(p);
17049
17645
  } catch {
17050
17646
  return path29.resolve(p);
17051
17647
  }
@@ -17107,7 +17703,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
17107
17703
 
17108
17704
  // packages/cli/src/commands/tokens.ts
17109
17705
  init_dist();
17110
- import fs26 from "fs";
17706
+ import fs27 from "fs";
17111
17707
  import path30 from "path";
17112
17708
  import os16 from "os";
17113
17709
  import readline3 from "readline";
@@ -17163,7 +17759,7 @@ function foldTranscriptLine(agg, rawLine, state) {
17163
17759
  async function aggregateTranscriptFile(filePath) {
17164
17760
  const agg = emptySessionAggregate();
17165
17761
  const state = { lastCacheRead: null };
17166
- const rl = readline3.createInterface({ input: fs26.createReadStream(filePath), crlfDelay: Infinity });
17762
+ const rl = readline3.createInterface({ input: fs27.createReadStream(filePath), crlfDelay: Infinity });
17167
17763
  for await (const line of rl) {
17168
17764
  foldTranscriptLine(agg, line, state);
17169
17765
  }
@@ -17231,7 +17827,7 @@ function buildRoleReport(role, sessions) {
17231
17827
  }
17232
17828
  async function readdirSafe(dir) {
17233
17829
  try {
17234
- return await fs26.promises.readdir(dir);
17830
+ return await fs27.promises.readdir(dir);
17235
17831
  } catch {
17236
17832
  return [];
17237
17833
  }
@@ -17398,12 +17994,12 @@ var tokensCommand = new Command30("tokens").description(
17398
17994
  // packages/cli/src/commands/telegram.ts
17399
17995
  init_dist();
17400
17996
  init_dist2();
17401
- import { join as join26, dirname as dirname7 } from "path";
17997
+ import { join as join28, dirname as dirname7 } from "path";
17402
17998
  import { emitKeypressEvents } from "readline";
17403
17999
  import { Command as Command31 } from "commander";
17404
18000
  import chalk32 from "chalk";
17405
18001
  function defaultStateRoot() {
17406
- return join26(dirname7(DEFAULT_CONFIG_PATH), "state");
18002
+ return join28(dirname7(DEFAULT_CONFIG_PATH), "state");
17407
18003
  }
17408
18004
  async function questionMasked() {
17409
18005
  return new Promise((resolve4) => {
@@ -17729,9 +18325,10 @@ init_dist2();
17729
18325
  init_dist4();
17730
18326
  init_dist();
17731
18327
  import { Command as Command32 } from "commander";
18328
+ import { homedir as homedir17 } from "os";
17732
18329
 
17733
18330
  // packages/cli/src/lib/captain-session-registry.ts
17734
- import fs27 from "fs";
18331
+ import fs28 from "fs";
17735
18332
  import path31 from "path";
17736
18333
 
17737
18334
  // packages/cli/src/lib/handoff-facts.ts
@@ -17783,15 +18380,15 @@ function assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, now, ext
17783
18380
  // packages/cli/src/lib/captain-session-registry.ts
17784
18381
  var CAPTAIN_SESSION_REGISTRY_FILE = "captain-sessions.jsonl";
17785
18382
  function appendCaptainSession(spokeVault, record) {
17786
- fs27.mkdirSync(spokeVault, { recursive: true });
18383
+ fs28.mkdirSync(spokeVault, { recursive: true });
17787
18384
  const file = path31.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
17788
- fs27.appendFileSync(file, JSON.stringify(record) + "\n");
18385
+ fs28.appendFileSync(file, JSON.stringify(record) + "\n");
17789
18386
  }
17790
18387
  function readCaptainSessionRegistry(spokeVault) {
17791
18388
  const file = path31.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
17792
- if (!fs27.existsSync(file)) return [];
18389
+ if (!fs28.existsSync(file)) return [];
17793
18390
  const records = [];
17794
- for (const line of fs27.readFileSync(file, "utf-8").split("\n")) {
18391
+ for (const line of fs28.readFileSync(file, "utf-8").split("\n")) {
17795
18392
  if (!line.trim()) continue;
17796
18393
  try {
17797
18394
  records.push(JSON.parse(line));
@@ -17878,6 +18475,26 @@ function hooksCommand() {
17878
18475
  if (!taskId || !project) {
17879
18476
  process.exit(0);
17880
18477
  }
18478
+ if (sub === "pre-tool-use") {
18479
+ const p = payload;
18480
+ const guard = decideCaptainMemoryWrite(
18481
+ typeof p?.tool_name === "string" ? p.tool_name : "",
18482
+ p?.tool_input,
18483
+ process.env,
18484
+ homedir17()
18485
+ );
18486
+ if (guard.decision === "deny") {
18487
+ console.error(`[squadrant] denied crew write into captain memory (task ${taskId}): ${guard.reason}`);
18488
+ process.stdout.write(JSON.stringify({
18489
+ hookSpecificOutput: {
18490
+ hookEventName: "PreToolUse",
18491
+ permissionDecision: "deny",
18492
+ permissionDecisionReason: guard.reason
18493
+ }
18494
+ }));
18495
+ process.exit(0);
18496
+ }
18497
+ }
17881
18498
  const ev = mapHookSub(sub, payload, taskId);
17882
18499
  if (!ev) {
17883
18500
  process.exit(0);
@@ -18038,7 +18655,7 @@ import os17 from "os";
18038
18655
  // packages/cli/src/lib/handoff-live-repo.ts
18039
18656
  init_dist();
18040
18657
  import { execFileSync as execFileSync10 } from "child_process";
18041
- import fs28 from "fs";
18658
+ import fs29 from "fs";
18042
18659
  import path33 from "path";
18043
18660
 
18044
18661
  // packages/cli/src/lib/handoff-branch-state.ts
@@ -18178,7 +18795,7 @@ function localAheadOfBase(runner, projectPath, base) {
18178
18795
  }
18179
18796
  function readFetchAgeMs(projectPath, now) {
18180
18797
  try {
18181
- const stat2 = fs28.statSync(path33.join(projectPath, ".git", "FETCH_HEAD"));
18798
+ const stat2 = fs29.statSync(path33.join(projectPath, ".git", "FETCH_HEAD"));
18182
18799
  return Math.max(0, now - stat2.mtime.getTime());
18183
18800
  } catch {
18184
18801
  return null;
@@ -18249,7 +18866,7 @@ function gatherLiveRepoState(projectPath, fallbackBaseBranch, tasks, runner = de
18249
18866
 
18250
18867
  // packages/cli/src/lib/handoff-claude-mem.ts
18251
18868
  import { createRequire } from "module";
18252
- import fs29 from "fs";
18869
+ import fs30 from "fs";
18253
18870
  var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
18254
18871
  var CLAUDE_MEM_RECENCY_LIMIT = 20;
18255
18872
  function decisionText(row) {
@@ -18263,7 +18880,7 @@ function decisionText(row) {
18263
18880
  return row.narrative ?? "";
18264
18881
  }
18265
18882
  function queryClaudeMem(dbPath, project) {
18266
- if (!fs29.existsSync(dbPath)) return null;
18883
+ if (!fs30.existsSync(dbPath)) return null;
18267
18884
  let db;
18268
18885
  try {
18269
18886
  db = new DatabaseSync(dbPath, { readOnly: true });
@@ -18306,7 +18923,7 @@ function queryClaudeMem(dbPath, project) {
18306
18923
  }
18307
18924
 
18308
18925
  // packages/cli/src/lib/handoff-transcript.ts
18309
- import fs30 from "fs";
18926
+ import fs31 from "fs";
18310
18927
  var TRANSCRIPT_BYTE_CAP = 2e5;
18311
18928
  function tailOf(content, byteCap) {
18312
18929
  const buf = Buffer.from(content, "utf-8");
@@ -18335,27 +18952,27 @@ function extractMessages(tailText) {
18335
18952
  return { lastUserMessage, lastAssistantText };
18336
18953
  }
18337
18954
  function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
18338
- if (!fs30.existsSync(transcriptPath)) return null;
18339
- const content = fs30.readFileSync(transcriptPath, "utf-8");
18955
+ if (!fs31.existsSync(transcriptPath)) return null;
18956
+ const content = fs31.readFileSync(transcriptPath, "utf-8");
18340
18957
  const { lastUserMessage, lastAssistantText } = extractMessages(tailOf(content, byteCap));
18341
- const mtimeIso = fs30.statSync(transcriptPath).mtime.toISOString();
18958
+ const mtimeIso = fs31.statSync(transcriptPath).mtime.toISOString();
18342
18959
  return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText };
18343
18960
  }
18344
18961
 
18345
18962
  // packages/cli/src/lib/handoff-archive.ts
18346
- import fs31 from "fs";
18963
+ import fs32 from "fs";
18347
18964
  import path34 from "path";
18348
18965
  function readNewestArchivedHandoff(spokeVault, now) {
18349
18966
  const dir = path34.join(spokeVault, "handoffs");
18350
- if (!fs31.existsSync(dir)) return null;
18351
- const candidates = fs31.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
18967
+ if (!fs32.existsSync(dir)) return null;
18968
+ const candidates = fs32.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
18352
18969
  const full = path34.join(dir, e.name);
18353
- return { name: e.name, full, mtime: fs31.statSync(full).mtime };
18970
+ return { name: e.name, full, mtime: fs32.statSync(full).mtime };
18354
18971
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
18355
18972
  for (const candidate of candidates) {
18356
18973
  let content;
18357
18974
  try {
18358
- content = JSON.parse(fs31.readFileSync(candidate.full, "utf-8"));
18975
+ content = JSON.parse(fs32.readFileSync(candidate.full, "utf-8"));
18359
18976
  } catch {
18360
18977
  continue;
18361
18978
  }
@@ -18425,15 +19042,15 @@ init_dist();
18425
19042
  init_dist();
18426
19043
  init_dist();
18427
19044
  var __dirname = dirname8(fileURLToPath6(import.meta.url));
18428
- var pkg = JSON.parse(readFileSync16(join27(__dirname, "..", "package.json"), "utf-8"));
19045
+ var pkg = JSON.parse(readFileSync17(join29(__dirname, "..", "package.json"), "utf-8"));
18429
19046
  ensureRuntimeSynced({
18430
- sourceRoot: join27(__dirname, ".."),
18431
- runtimeRoot: join27(homedir17(), ".config", "squadrant")
19047
+ sourceRoot: join29(__dirname, ".."),
19048
+ runtimeRoot: join29(homedir18(), ".config", "squadrant")
18432
19049
  });
18433
19050
  if (process.argv[2] !== "config") {
18434
19051
  try {
18435
- const cfgPath = join27(homedir17(), ".config", "squadrant", "config.json");
18436
- if (existsSync13(cfgPath)) {
19052
+ const cfgPath = join29(homedir18(), ".config", "squadrant", "config.json");
19053
+ if (existsSync14(cfgPath)) {
18437
19054
  const cfg = JSON.parse(readConfigFileSync(cfgPath));
18438
19055
  if (needsCheck(cfg, pkg.version)) {
18439
19056
  const items = detectDrift(cfg, getDefaultConfig());