squadrant 0.19.0 → 0.19.2

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,13 +86,22 @@ 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(v2) {
90
+ return THINKING_LEVELS.includes(v2);
91
+ }
92
+ function parseThinkingLevel(v2) {
93
+ if (!isThinkingLevel(v2)) {
94
+ throw new Error(`Invalid --thinking value '${v2}'. Valid values: ${THINKING_LEVELS.join(", ")}`);
95
+ }
96
+ return v2;
97
+ }
89
98
  function resolveControlChannelMode(cfg, agent) {
90
- const v = cfg?.[agent];
91
- return v && CONTROL_CHANNEL_MODES.has(v) ? v : "off";
99
+ const v2 = cfg?.[agent];
100
+ return v2 && CONTROL_CHANNEL_MODES.has(v2) ? v2 : "off";
92
101
  }
93
102
  function resolveCaptainChannelMode(defaults) {
94
- const v = defaults?.captainChannel;
95
- return v && CONTROL_CHANNEL_MODES.has(v) ? v : "off";
103
+ const v2 = defaults?.captainChannel;
104
+ return v2 && CONTROL_CHANNEL_MODES.has(v2) ? v2 : "off";
96
105
  }
97
106
  function getDefaultConfig() {
98
107
  return {
@@ -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);
@@ -209,8 +219,8 @@ function deepMerge(base, patch) {
209
219
  if (patch === null || typeof patch !== "object" || Array.isArray(patch))
210
220
  return patch ?? base;
211
221
  const out = { ...base };
212
- for (const [k, v] of Object.entries(patch)) {
213
- out[k] = deepMerge(out[k], v);
222
+ for (const [k, v2] of Object.entries(patch)) {
223
+ out[k] = deepMerge(out[k], v2);
214
224
  }
215
225
  return out;
216
226
  }
@@ -720,7 +730,7 @@ function isCacheStale(state, now, intervalMs = CHECK_INTERVAL_MS, failureInterva
720
730
  return now - state.lastChecked >= (state.lastCheckFailed ? failureIntervalMs : intervalMs);
721
731
  }
722
732
  function isNewerVersion(latest, current) {
723
- const parse2 = (v) => v.trim().replace(/^v/, "").split("-")[0].split(".").map((n) => Number(n) || 0);
733
+ const parse2 = (v2) => v2.trim().replace(/^v/, "").split("-")[0].split(".").map((n) => Number(n) || 0);
724
734
  const [la = 0, lb = 0, lc = 0] = parse2(latest);
725
735
  const [ca = 0, cb = 0, cc = 0] = parse2(current);
726
736
  if (la !== ca)
@@ -1101,8 +1111,8 @@ var init_runtime_sync = __esm({
1101
1111
  });
1102
1112
 
1103
1113
  // packages/shared/dist/lib/tool-compat.js
1104
- function parseSemVer(v) {
1105
- const m = v.match(/(\d+)\.(\d+)\.(\d+)/);
1114
+ function parseSemVer(v2) {
1115
+ const m = v2.match(/(\d+)\.(\d+)\.(\d+)/);
1106
1116
  if (!m)
1107
1117
  return null;
1108
1118
  return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
@@ -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",
@@ -2493,12 +2523,12 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
2493
2523
  return;
2494
2524
  }
2495
2525
  const conn = createConnection(sockPath);
2496
- const finish = (v) => {
2526
+ const finish = (v2) => {
2497
2527
  try {
2498
2528
  conn.destroy();
2499
2529
  } catch {
2500
2530
  }
2501
- resolve4(v);
2531
+ resolve4(v2);
2502
2532
  };
2503
2533
  const timer = setTimeout(() => finish(false), timeoutMs);
2504
2534
  conn.on("connect", () => {
@@ -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,59 @@ 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 ?? 10;
3198
+ const kickstartRetryDelayMs = opts.kickstartRetryDelayMs ?? 300;
3199
+ const pidBefore = getDaemonPid(target);
3200
+ let kickstartError = null;
3201
+ for (let i = 0; i < kickstartRetries; i++) {
3202
+ try {
3203
+ execFileSync3("launchctl", ["kickstart", "-k", target], { stdio: "ignore" });
3204
+ kickstartError = null;
3205
+ break;
3206
+ } catch (e) {
3207
+ kickstartError = e;
3208
+ if (i < kickstartRetries - 1) {
3209
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, kickstartRetryDelayMs);
3210
+ }
3211
+ }
3212
+ }
3213
+ let pidAfter = null;
3214
+ for (let i = 0; i < pollAttempts; i++) {
3215
+ pidAfter = getDaemonPid(target);
3216
+ if (pidAfter !== null && pidAfter !== pidBefore)
3217
+ break;
3218
+ if (i < pollAttempts - 1) {
3219
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollDelayMs);
3220
+ }
3221
+ }
3222
+ const restarted = pidAfter !== null && pidAfter !== pidBefore;
3223
+ if (kickstartError && !restarted)
3224
+ throw kickstartError;
3225
+ return {
3226
+ target,
3227
+ pidBefore,
3228
+ pidAfter,
3229
+ restarted,
3230
+ ...kickstartError ? { note: "kickstart -k refused; daemon restarted by bootstrap" } : {}
3231
+ };
3232
+ }
3132
3233
  function isOperatorInitiatedCommand(topLevelArg) {
3133
3234
  return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
3134
3235
  }
@@ -3170,11 +3271,18 @@ function printForeignInstallError(foreign) {
3170
3271
  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
3272
  `;
3172
3273
  }
3173
- function reregisterDaemon(nodeBin = process.execPath) {
3174
- if (!tryAcquireDaemonLock())
3175
- return;
3274
+ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
3275
+ if (!tryAcquireDaemonLock()) {
3276
+ throw new Error("could not acquire the daemon lock \u2014 another squadrant process is already restarting the daemon");
3277
+ }
3176
3278
  try {
3177
- applyDaemonDrift(computeDaemonDrift(nodeBin));
3279
+ const drift = computeDaemonDrift(nodeBin);
3280
+ reconcilePlistAndService(drift);
3281
+ const result = forceKickstartAndVerify(drift.target, kickstartOpts);
3282
+ if (!result.restarted) {
3283
+ 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`);
3284
+ }
3285
+ return result;
3178
3286
  } finally {
3179
3287
  releaseDaemonLock();
3180
3288
  }
@@ -3562,6 +3670,10 @@ function detectTrailingQuestion(text) {
3562
3670
  return lastLine;
3563
3671
  return null;
3564
3672
  }
3673
+ function isQuotedLine(raw) {
3674
+ const noAnsi = raw.replace(/\[[0-9;]*m/g, "");
3675
+ return QUOTED_PREFIX_RE.test(noAnsi);
3676
+ }
3565
3677
  function stripChrome(raw) {
3566
3678
  let line = raw.replace(/\[[0-9;]*m/g, "");
3567
3679
  line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
@@ -3620,8 +3732,11 @@ function classifyPaneTail(tail) {
3620
3732
  if (q)
3621
3733
  return { kind: "question", text: q };
3622
3734
  let errLine = null;
3623
- for (const c of cleaned) {
3624
- if (c != null && ERROR_BANNER_RE.some((re) => re.test(c)))
3735
+ for (let i = 0; i < cleaned.length; i++) {
3736
+ const c = cleaned[i];
3737
+ if (c == null || isQuotedLine(raw[i]))
3738
+ continue;
3739
+ if (ERROR_BANNER_RE.some((re) => re.test(c)))
3625
3740
  errLine = c;
3626
3741
  }
3627
3742
  if (errLine)
@@ -3664,6 +3779,22 @@ function createInteractiveProbe(deps) {
3664
3779
  const verdict = classifyPaneTail(tail);
3665
3780
  if (!verdict)
3666
3781
  continue;
3782
+ if (verdict.kind === "error") {
3783
+ const alive = deps.checkAlive ? await deps.checkAlive(rec) : "unknown";
3784
+ if (alive !== "gone") {
3785
+ const message = `CREW WARN ${rec.name}: pane shows an error string \u2014 crew still ${alive}, not terminalized (pane-detected): ${verdict.text}`;
3786
+ deps.log(`probe -> ${message}`);
3787
+ if (deps.notify) {
3788
+ const warnEvent = { type: "task.warn", id: rec.id, message };
3789
+ try {
3790
+ await deps.notify({ project: rec.project, message, record: rec, event: warnEvent });
3791
+ } catch (e) {
3792
+ deps.log(`probe notify failed for ${rec.id}: ${e.message}`);
3793
+ }
3794
+ }
3795
+ continue;
3796
+ }
3797
+ }
3667
3798
  const event = verdict.kind === "error" ? {
3668
3799
  type: "task.failed",
3669
3800
  id: rec.id,
@@ -3685,7 +3816,7 @@ function createInteractiveProbe(deps) {
3685
3816
  }
3686
3817
  return { tick };
3687
3818
  }
3688
- var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE;
3819
+ 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
3820
  var init_interactive_probe = __esm({
3690
3821
  "packages/core/dist/daemon/interactive-probe.js"() {
3691
3822
  STALE_THRESHOLD_MS = 5 * 60 * 1e3;
@@ -3701,6 +3832,7 @@ var init_interactive_probe = __esm({
3701
3832
  PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
3702
3833
  PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
3703
3834
  STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
3835
+ QUOTED_PREFIX_RE = /^\s*(?:[┃│▏▕]|>|[+-]|\d+[\t:→])\s/;
3704
3836
  }
3705
3837
  });
3706
3838
 
@@ -3716,6 +3848,7 @@ function createProbes(ctx) {
3716
3848
  };
3717
3849
  function buildInteractiveProbe(deps) {
3718
3850
  const directPaneReader = createDirectCrewPaneReader(deps.cmux, captainNameForProject);
3851
+ const checkAlive = createDirectSurfaceLivenessProbe(deps.cmux, captainNameForProject);
3719
3852
  const probe = createInteractiveProbe({
3720
3853
  project: "_all_",
3721
3854
  listTasks: async () => store.listAll(),
@@ -3727,7 +3860,9 @@ function createProbes(ctx) {
3727
3860
  }
3728
3861
  },
3729
3862
  now: () => Date.now(),
3730
- log
3863
+ log,
3864
+ checkAlive,
3865
+ notify: ctx.notify
3731
3866
  });
3732
3867
  let probing = false;
3733
3868
  return async () => {
@@ -4002,6 +4137,33 @@ import fs9 from "fs";
4002
4137
  import os4 from "os";
4003
4138
  import path9 from "path";
4004
4139
  import { randomUUID as randomUUID3 } from "crypto";
4140
+ async function pollFirstTurnConfirmedAt(getTaskRecord, project, id) {
4141
+ const deadline = Date.now() + FIRST_TURN_HOOK_CONFIRM_WINDOW_MS;
4142
+ for (; ; ) {
4143
+ const rec = await getTaskRecord(project, id).catch(() => void 0);
4144
+ if (rec?.firstTurnConfirmedAt)
4145
+ return true;
4146
+ if (Date.now() >= deadline)
4147
+ return false;
4148
+ await new Promise((r) => setTimeout(r, FIRST_TURN_HOOK_POLL_INTERVAL_MS));
4149
+ }
4150
+ }
4151
+ function firstTrueOrBothFalse(a, b) {
4152
+ return new Promise((resolve4) => {
4153
+ let settledFalseCount = 0;
4154
+ const onSettle = (ok2) => {
4155
+ if (ok2) {
4156
+ resolve4(true);
4157
+ return;
4158
+ }
4159
+ settledFalseCount++;
4160
+ if (settledFalseCount === 2)
4161
+ resolve4(false);
4162
+ };
4163
+ a.then(onSettle, () => onSettle(false));
4164
+ b.then(onSettle, () => onSettle(false));
4165
+ });
4166
+ }
4005
4167
  async function listCrewPanes(runtime, workspaceId, project) {
4006
4168
  const surfaces = await runtime.listSurfaces(workspaceId);
4007
4169
  return surfaces.filter((s) => s.title && isCrewTitle(project, s.title));
@@ -4107,6 +4269,7 @@ async function runCrewSpawn(input, config, deps) {
4107
4269
  const crewRole = config.defaults.roles?.crew;
4108
4270
  const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
4109
4271
  const crewModel = input.model ?? route?.model ?? configModel;
4272
+ const crewThinking = input.thinking ?? config.defaults.roles?.crew?.thinking;
4110
4273
  if (agentName !== "claude") {
4111
4274
  deps.onModelResolved?.({ agentName, model: crewModel });
4112
4275
  }
@@ -4142,7 +4305,8 @@ async function runCrewSpawn(input, config, deps) {
4142
4305
  // crew apart from an unrelated session instead of an auto-derived cwd
4143
4306
  // basename (only the claude driver reads this — other agents ignore it).
4144
4307
  sessionName: crewSessionName(input.project, name),
4145
- ...crewModel ? { model: crewModel } : {}
4308
+ ...crewModel ? { model: crewModel } : {},
4309
+ ...crewThinking ? { thinking: crewThinking } : {}
4146
4310
  });
4147
4311
  const direction2 = input.direction ?? "tab";
4148
4312
  const title2 = titleFor(input.project, name);
@@ -4150,10 +4314,18 @@ async function runCrewSpawn(input, config, deps) {
4150
4314
  const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
4151
4315
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${niceCrewCommand(cliCommand2)}`);
4152
4316
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
4153
- const claudeResult = await deps.sendFirstTurn(pane2, `${firstTurnTask}
4317
+ let claudeFirstTurn = firstTurnTask;
4318
+ if (Buffer.byteLength(claudeFirstTurn, "utf8") > FIRST_TURN_INLINE_MAX_BYTES) {
4319
+ const spillFile = path9.join(os4.tmpdir(), `squadrant-task-${rec.id}.md`);
4320
+ fs9.writeFileSync(spillFile, claudeFirstTurn, "utf8");
4321
+ claudeFirstTurn = `Full task is at ${spillFile} \u2014 cat it and follow it exactly.`;
4322
+ }
4323
+ const sendPromise = deps.sendFirstTurn(pane2, `${claudeFirstTurn}
4154
4324
 
4155
4325
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
4156
- if (!claudeResult.delivered) {
4326
+ const scrapeDelivered = sendPromise.then((r) => r.delivered).catch(() => false);
4327
+ const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id)) : await scrapeDelivered;
4328
+ if (!delivered) {
4157
4329
  process.stderr.write(`\u26A0\uFE0F First turn not delivered for crew '${name}' \u2014 use 'squadrant crew send ${input.project} ${name}' to re-send the task.
4158
4330
  `);
4159
4331
  } else if (!hooksInstalled) {
@@ -4250,7 +4422,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4250
4422
  if (!crew) {
4251
4423
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
4252
4424
  }
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}'.`;
4425
+ 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
4426
  if (deps.isBlockedByModal && await deps.isBlockedByModal(crew)) {
4255
4427
  throw new Error(blockedByModalMessage());
4256
4428
  }
@@ -4264,10 +4436,16 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4264
4436
  const heldForMin = Math.round((Date.now() - task.operatorHold.since) / 6e4);
4265
4437
  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
4438
  }
4439
+ const isAttentionState = task?.state === "blocked" || task?.state === "awaiting-input" || task?.state === "review";
4440
+ if (task && !isAttentionState && task.firstTurnConfirmedAt && task.task === message) {
4441
+ throw new Error(`Crew '${name}' already confirmed receipt of this task \u2014 its first turn was delivered and is not being re-sent to avoid running it twice. If you have new instructions, send different text.`);
4442
+ }
4443
+ let reopened = false;
4267
4444
  try {
4268
4445
  if (task) {
4269
4446
  if (TERMINAL_STATES.has(task.state)) {
4270
4447
  await deps.emitEvent(project, { type: "task.reopened", id: task.id });
4448
+ reopened = true;
4271
4449
  } else if (task.state === "blocked" || task.state === "awaiting-input" || task.state === "review") {
4272
4450
  await deps.emitEvent(project, { type: "task.started", id: task.id });
4273
4451
  }
@@ -4293,7 +4471,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4293
4471
  throw new Error(`Message to crew '${name}' is held: ${outcome.reason}. Resolve it in the crew's session, then re-send.`);
4294
4472
  }
4295
4473
  if (!fallsBackToPane(outcome)) {
4296
- return;
4474
+ return { reopened };
4297
4475
  }
4298
4476
  }
4299
4477
  if (mode === "shadow" && channel && task) {
@@ -4316,7 +4494,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4316
4494
  if (!paneOk) {
4317
4495
  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
4496
  }
4319
- return;
4497
+ return { reopened };
4320
4498
  }
4321
4499
  const { delivered, blockedByModal } = await deliver(crew, message);
4322
4500
  if (blockedByModal) {
@@ -4325,6 +4503,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
4325
4503
  if (!delivered) {
4326
4504
  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
4505
  }
4506
+ return { reopened };
4328
4507
  }
4329
4508
  async function runCrewRead(project, name, runtime, workspaceId) {
4330
4509
  const crew = await findCrewPane(runtime, workspaceId, project, name);
@@ -4427,7 +4606,7 @@ async function runCrewList(project, runtime, workspaceId) {
4427
4606
  surfaceId: c.surfaceId
4428
4607
  }));
4429
4608
  }
4430
- var CC_SOCKS_DIR, TEMPLATES_DIR, STATE_ROOT, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
4609
+ var CC_SOCKS_DIR, FIRST_TURN_INLINE_MAX_BYTES, TEMPLATES_DIR, STATE_ROOT, FIRST_TURN_HOOK_CONFIRM_WINDOW_MS, FIRST_TURN_HOOK_POLL_INTERVAL_MS, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
4431
4610
  var init_crew_spawn = __esm({
4432
4611
  "packages/core/dist/crew-spawn.js"() {
4433
4612
  init_control_channel();
@@ -4436,8 +4615,11 @@ var init_crew_spawn = __esm({
4436
4615
  init_crew_protocol();
4437
4616
  init_crew_lifecycle();
4438
4617
  CC_SOCKS_DIR = "/tmp/cc-socks";
4618
+ FIRST_TURN_INLINE_MAX_BYTES = 1200;
4439
4619
  TEMPLATES_DIR = path9.join(os4.homedir(), ".config", "squadrant", "templates");
4440
4620
  STATE_ROOT = path9.join(os4.homedir(), ".config", "squadrant", "state");
4621
+ FIRST_TURN_HOOK_CONFIRM_WINDOW_MS = 1e5;
4622
+ FIRST_TURN_HOOK_POLL_INTERVAL_MS = 2e3;
4441
4623
  CLOSE_LOOKUP_RETRIES = 3;
4442
4624
  CLOSE_LOOKUP_RETRY_DELAY_MS = 150;
4443
4625
  }
@@ -4482,13 +4664,16 @@ var init_captain_channel = __esm({
4482
4664
  function discoverCaptainSurface(surfaces, captainTitle) {
4483
4665
  return surfaces.find((s) => s.title === captainTitle) ?? null;
4484
4666
  }
4485
- function reapOrphanedCrews(store, project) {
4667
+ async function reapOrphanedCrews(store, project, isSurfaceAlive) {
4486
4668
  let reaped = 0;
4487
4669
  for (const r of store.list(project)) {
4488
4670
  if (TERMINAL_STATES.has(r.state))
4489
4671
  continue;
4490
4672
  if (r.mode !== "interactive")
4491
4673
  continue;
4674
+ const liveness = await isSurfaceAlive(r);
4675
+ if (liveness !== "gone")
4676
+ continue;
4492
4677
  store.put({ ...r, state: "cancelled", lastEvent: "captain-stopped" });
4493
4678
  reaped++;
4494
4679
  }
@@ -4542,10 +4727,13 @@ async function runLivenessTick(deps) {
4542
4727
  const prev = deps.registry.get(project);
4543
4728
  if (prev && prev.lastState === "start")
4544
4729
  entry.startedAt = prev.startedAt;
4730
+ const prevState = deriveCaptainState(prev);
4545
4731
  deps.registry.apply(entry);
4546
4732
  if (winner.pid != null)
4547
4733
  deps.registry.setPidAlive(project, deps.isPidAlive(winner.pid), now);
4548
- logEntry(deps.log, project, deps.registry.get(project));
4734
+ const updated = deps.registry.get(project);
4735
+ if (deriveCaptainState(updated) !== prevState)
4736
+ logEntry(deps.log, project, updated);
4549
4737
  }
4550
4738
  for (const e of deps.registry.all()) {
4551
4739
  if (e.role !== "captain" || e.lastState !== "start" || seen.has(e.project))
@@ -4563,12 +4751,13 @@ async function runLivenessTick(deps) {
4563
4751
  continue;
4564
4752
  const state = deriveCaptainState(e);
4565
4753
  if (state === "stopped" || state === "gone")
4566
- deps.reap(e.project);
4754
+ await deps.reap(e.project);
4567
4755
  }
4568
4756
  }
4569
4757
  }
4570
- function createDelivery(ctx, daemonCmux) {
4758
+ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4571
4759
  const { stateRoot, store, log, livenessRegistry, isPidAlive, opts, telegramBridge } = ctx;
4760
+ const surfaceProbe = isSurfaceAlive ?? (async () => "unknown");
4572
4761
  const notifyFault = ctx.notifyFault ?? (() => {
4573
4762
  });
4574
4763
  const defaultNotify = async (args) => {
@@ -4591,12 +4780,22 @@ function createDelivery(ctx, daemonCmux) {
4591
4780
  }
4592
4781
  };
4593
4782
  if (!daemonCmux) {
4594
- return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0 };
4783
+ return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0, inFlightDelivery: () => null };
4595
4784
  }
4596
4785
  const cmux2 = daemonCmux;
4597
4786
  const cfg = loadConfig();
4598
4787
  const deliveries = /* @__PURE__ */ new Map();
4599
4788
  const deliveryStats = (project) => deliveries.get(project)?.stats();
4789
+ const lastDeferred = /* @__PURE__ */ new Map();
4790
+ const inFlightDelivery = () => {
4791
+ let worst2 = null;
4792
+ for (const [project, v2] of lastDeferred) {
4793
+ if (!worst2 || v2.deferCount > worst2.deferCount)
4794
+ worst2 = { project, ...v2 };
4795
+ }
4796
+ return worst2;
4797
+ };
4798
+ const projectBackoff = /* @__PURE__ */ new Map();
4600
4799
  const stuckNotified = /* @__PURE__ */ new Set();
4601
4800
  const sessionStartMs = Date.now();
4602
4801
  let delivering = false;
@@ -4607,8 +4806,8 @@ function createDelivery(ctx, daemonCmux) {
4607
4806
  isPidAlive,
4608
4807
  now: () => Date.now(),
4609
4808
  log,
4610
- reap: (project) => {
4611
- const reaped = reapOrphanedCrews(store, project);
4809
+ reap: async (project) => {
4810
+ const reaped = await reapOrphanedCrews(store, project, surfaceProbe);
4612
4811
  if (reaped > 0) {
4613
4812
  const title = cfg.projects?.[project]?.captainName ?? `${project}-captain`;
4614
4813
  log(`captain ${title}: reaped ${reaped} orphaned crew(s)`);
@@ -4624,82 +4823,116 @@ function createDelivery(ctx, daemonCmux) {
4624
4823
  cfg.commandName
4625
4824
  ])];
4626
4825
  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;
4826
+ try {
4827
+ const backoff = projectBackoff.get(project);
4828
+ if (backoff && Date.now() < backoff.nextAttemptAt)
4829
+ continue;
4830
+ const projCfg = cfg.projects?.[project];
4831
+ const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
4832
+ let surface = null;
4833
+ const resolveCaptainSurface = async () => {
4834
+ const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
4835
+ if (!wsId)
4836
+ return injectedSurfaces[project] ?? null;
4837
+ const surfaces = await cmux2.listSurfaces(wsId);
4838
+ return discoverCaptainSurface(surfaces, captainTitle) ?? injectedSurfaces[project] ?? null;
4839
+ };
4840
+ surface = await resolveCaptainSurface();
4841
+ if (!surface)
4842
+ continue;
4843
+ const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
4844
+ const lastAcked = cursor?.lastAckedSeq ?? 0;
4845
+ let d = deliveries.get(project);
4846
+ if (!d) {
4847
+ d = new CaptainDelivery({
4848
+ maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
4849
+ stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
4850
+ });
4851
+ deliveries.set(project, d);
4852
+ }
4853
+ for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
4854
+ if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
4855
+ if (!TERMINAL_KINDS.has(entry.kind)) {
4856
+ const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
4857
+ if (!isExemptMessage) {
4858
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
4859
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4860
+ continue;
4861
+ }
4862
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
4863
+ } else {
4864
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4657
4865
  }
4658
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
4866
+ }
4867
+ const result = await d.deliver(entry, async (text, sendOpts) => {
4868
+ let handledByChannel = false;
4869
+ try {
4870
+ const mode = ctx.captainChannelMode?.() ?? "off";
4871
+ const r = await deliverToCaptain(project, text, {
4872
+ channel: ctx.captainChannel,
4873
+ mode,
4874
+ log
4875
+ });
4876
+ handledByChannel = r.handled;
4877
+ } catch (e) {
4878
+ log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4879
+ }
4880
+ if (handledByChannel) {
4881
+ return;
4882
+ }
4883
+ try {
4884
+ return await cmux2.send(surface, text, sendOpts);
4885
+ } catch (e) {
4886
+ if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
4887
+ throw e;
4888
+ const next = await resolveCaptainSurface();
4889
+ const same = next !== null && next.workspaceId === surface.workspaceId && next.surfaceId === surface.surfaceId;
4890
+ if (!next || same) {
4891
+ log(`delivery project=${project}: probe-failed but surface re-resolution found ${next ? "the same dead surface" : "no captain surface"} \u2014 deferring`);
4892
+ throw e;
4893
+ }
4894
+ log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
4895
+ surface = next;
4896
+ return cmux2.send(next, text, sendOpts);
4897
+ }
4898
+ });
4899
+ if ("delivered" in result) {
4900
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
4901
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4902
+ lastDeferred.delete(project);
4903
+ projectBackoff.delete(project);
4659
4904
  } else {
4660
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4905
+ const { maxDeferCount, stuck: stuck2 } = d.stats();
4906
+ if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4907
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4908
+ }
4909
+ lastDeferred.set(project, { seq: entry.seq, deferCount: maxDeferCount });
4910
+ if (stuck2) {
4911
+ const streak = (projectBackoff.get(project)?.streak ?? 0) + 1;
4912
+ const backoffMs = Math.min(6e4, 1e3 * 2 ** streak);
4913
+ projectBackoff.set(project, { nextAttemptAt: Date.now() + backoffMs, streak });
4914
+ }
4915
+ break;
4661
4916
  }
4662
4917
  }
4663
- const result = await d.deliver(entry, async (text, sendOpts) => {
4664
- let handledByChannel = false;
4918
+ const stuck = d.stats().stuck;
4919
+ if (stuck && !stuckNotified.has(project)) {
4920
+ stuckNotified.add(project);
4921
+ const { maxDeferCount, reason } = d.stats();
4922
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
4923
+ const text = STUCK_ALERT_TEXT[reason ?? "unknown"](maxDeferCount);
4665
4924
  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;
4925
+ await appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
4673
4926
  } 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}`);
4927
+ log(`delivery stuck alert failed project=${project}: ${e.message}`);
4688
4928
  }
4689
- break;
4929
+ Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
4930
+ telegramBridge?.pushRaw(project, text);
4931
+ } else if (!stuck && stuckNotified.has(project)) {
4932
+ stuckNotified.delete(project);
4690
4933
  }
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);
4934
+ } catch (e) {
4935
+ log(`delivery project=${project}: unhandled error \u2014 ${e.message}`);
4703
4936
  }
4704
4937
  }
4705
4938
  };
@@ -4713,19 +4946,28 @@ function createDelivery(ctx, daemonCmux) {
4713
4946
  delivering = false;
4714
4947
  }
4715
4948
  };
4716
- return { defaultNotify, deliveryTick, deliveryStats };
4949
+ return { defaultNotify, deliveryTick, deliveryStats, inFlightDelivery };
4717
4950
  }
4718
- var CURSOR_SUBSCRIBER, TERMINAL_KINDS;
4951
+ var CURSOR_SUBSCRIBER, TERMINAL_KINDS, STUCK_ALERT_TEXT;
4719
4952
  var init_delivery_loop = __esm({
4720
4953
  "packages/core/dist/daemon/delivery-loop.js"() {
4721
4954
  init_mailbox();
4722
4955
  init_captain_delivery();
4956
+ init_defer_delivery();
4723
4957
  init_dist();
4724
4958
  init_interactive_probe();
4725
4959
  init_liveness2();
4726
4960
  init_captain_channel();
4727
4961
  CURSOR_SUBSCRIBER = "captain";
4728
4962
  TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
4963
+ STUCK_ALERT_TEXT = {
4964
+ "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.`,
4965
+ 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.`,
4966
+ 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.`,
4967
+ "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.`,
4968
+ 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.`,
4969
+ 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.`
4970
+ };
4729
4971
  }
4730
4972
  });
4731
4973
 
@@ -4813,10 +5055,72 @@ var init_server = __esm({
4813
5055
  }
4814
5056
  });
4815
5057
 
5058
+ // packages/core/dist/daemon/exit-marker.js
5059
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
5060
+ import { join as join11 } from "path";
5061
+ function exitMarkerPath(stateRoot) {
5062
+ return join11(stateRoot, "exit-marker.json");
5063
+ }
5064
+ function writeExitMarker(stateRoot, marker, log) {
5065
+ try {
5066
+ writeFileSync8(exitMarkerPath(stateRoot), JSON.stringify(marker));
5067
+ } catch (e) {
5068
+ log(`exit marker write failed: ${e.message}`);
5069
+ }
5070
+ }
5071
+ function consumeExitMarker(stateRoot, now = Date.now) {
5072
+ const p = exitMarkerPath(stateRoot);
5073
+ if (!existsSync9(p))
5074
+ return { marker: null };
5075
+ let marker = null;
5076
+ try {
5077
+ marker = JSON.parse(readFileSync8(p, "utf-8"));
5078
+ } catch {
5079
+ marker = null;
5080
+ }
5081
+ try {
5082
+ unlinkSync3(p);
5083
+ } catch {
5084
+ }
5085
+ if (!marker)
5086
+ return { marker: null };
5087
+ const gapMs = Math.max(0, now() - new Date(marker.ts).getTime());
5088
+ return { marker, gapMs };
5089
+ }
5090
+ function runningMarkerPath(stateRoot) {
5091
+ return join11(stateRoot, "running-marker.json");
5092
+ }
5093
+ function writeRunningMarker(stateRoot, marker, log) {
5094
+ try {
5095
+ writeFileSync8(runningMarkerPath(stateRoot), JSON.stringify(marker));
5096
+ } catch (e) {
5097
+ log(`running marker write failed: ${e.message}`);
5098
+ }
5099
+ }
5100
+ function readRunningMarker(stateRoot) {
5101
+ try {
5102
+ return JSON.parse(readFileSync8(runningMarkerPath(stateRoot), "utf-8"));
5103
+ } catch {
5104
+ return null;
5105
+ }
5106
+ }
5107
+ function removeRunningMarker(stateRoot, log) {
5108
+ try {
5109
+ unlinkSync3(runningMarkerPath(stateRoot));
5110
+ } catch (e) {
5111
+ if (e.code !== "ENOENT")
5112
+ log(`running marker remove failed: ${e.message}`);
5113
+ }
5114
+ }
5115
+ var init_exit_marker = __esm({
5116
+ "packages/core/dist/daemon/exit-marker.js"() {
5117
+ }
5118
+ });
5119
+
4816
5120
  // packages/core/dist/daemon/snapshot-gather.js
4817
5121
  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";
5122
+ import { join as join12 } from "path";
5123
+ import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
4820
5124
  function distBuiltAt() {
4821
5125
  try {
4822
5126
  return statSync3(SELF_PATH).mtimeMs;
@@ -4869,7 +5173,7 @@ function gatherStoreStats(store, stateRoot, project) {
4869
5173
  for (const r of store.list(project))
4870
5174
  byState[r.state] = (byState[r.state] ?? 0) + 1;
4871
5175
  let corruptCount = 0;
4872
- const dir = join11(stateRoot, project);
5176
+ const dir = join12(stateRoot, project);
4873
5177
  try {
4874
5178
  for (const n of readdirSync3(dir)) {
4875
5179
  if (n.includes(".corrupt.")) {
@@ -4879,7 +5183,7 @@ function gatherStoreStats(store, stateRoot, project) {
4879
5183
  if (!n.endsWith(".json"))
4880
5184
  continue;
4881
5185
  try {
4882
- JSON.parse(readFileSync8(join11(dir, n), "utf-8"));
5186
+ JSON.parse(readFileSync9(join12(dir, n), "utf-8"));
4883
5187
  } catch {
4884
5188
  corruptCount++;
4885
5189
  }
@@ -4894,7 +5198,7 @@ function gatherResults(resultsDir) {
4894
5198
  try {
4895
5199
  for (const n of readdirSync3(resultsDir)) {
4896
5200
  try {
4897
- const s = statSync3(join11(resultsDir, n));
5201
+ const s = statSync3(join12(resultsDir, n));
4898
5202
  if (s.isFile()) {
4899
5203
  fileCount++;
4900
5204
  totalBytes += s.size;
@@ -4914,19 +5218,20 @@ var init_snapshot_gather = __esm({
4914
5218
  });
4915
5219
 
4916
5220
  // packages/core/dist/daemon/start.js
4917
- import { join as join12, dirname as dirname3 } from "path";
5221
+ import { join as join13, dirname as dirname3 } from "path";
4918
5222
  import { readdir } from "fs/promises";
4919
5223
  function startDaemon(ctx, opts, pkgVersion) {
4920
5224
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
4921
5225
  const { daemonCmux } = ctx;
4922
5226
  const probes = createProbes(ctx);
4923
- const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats } = createDelivery(ctx, daemonCmux);
5227
+ const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
5228
+ const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats, inFlightDelivery } = createDelivery(ctx, daemonCmux, surfaceProbe);
4924
5229
  const baseNotify = opts.notify ?? defaultNotify;
4925
5230
  const notify = ctx.telegramBridge ? async (args) => {
4926
5231
  await baseNotify(args);
4927
5232
  ctx.telegramBridge.pushLifecycle(args.project, args.event);
4928
5233
  } : baseNotify;
4929
- const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
5234
+ ctx.notify = notify;
4930
5235
  const ingest = (project) => (e) => void ctx.d.handle({ kind: "event", project, event: e });
4931
5236
  const d = createDaemon({
4932
5237
  store,
@@ -4990,7 +5295,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4990
5295
  return out;
4991
5296
  }
4992
5297
  async function gatherSnapshotInputs(now) {
4993
- const logPath2 = join12(dirname3(stateRoot), "squadrantd.log");
5298
+ const logPath2 = join13(dirname3(stateRoot), "squadrantd.log");
4994
5299
  const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4995
5300
  const projects = await Promise.all(tier2Projects.map(async (project) => {
4996
5301
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
@@ -5083,6 +5388,33 @@ function startDaemon(ctx, opts, pkgVersion) {
5083
5388
  })();
5084
5389
  const server = createServer2(ctx, { buildHealth, gatherSnapshotInputs, cancelPromotionsFor, broadcast });
5085
5390
  log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
5391
+ const bootTs = (/* @__PURE__ */ new Date()).toISOString();
5392
+ {
5393
+ const sendDownAlert = (minutes, reasonText) => {
5394
+ const text = `\u26A0\uFE0F daemon was down for ${minutes} min (last exit reason=${reasonText})`;
5395
+ const alertProjects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
5396
+ for (const project of alertProjects) {
5397
+ appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`boot-gap alert failed project=${project}: ${e.message}`));
5398
+ }
5399
+ };
5400
+ const { marker, gapMs } = consumeExitMarker(stateRoot);
5401
+ const prevRunning = readRunningMarker(stateRoot);
5402
+ if (marker) {
5403
+ log(`previous exit ts=${marker.ts} reason=${marker.reason} gap=${((gapMs ?? 0) / 1e3).toFixed(1)}s`);
5404
+ if ((gapMs ?? 0) > 6e4)
5405
+ sendDownAlert(Math.round((gapMs ?? 0) / 6e4), marker.reason);
5406
+ } else if (prevRunning) {
5407
+ const lastHeartbeatMs = new Date(prevRunning.lastHeartbeatTs).getTime();
5408
+ const uncleanGapMs = Math.max(0, Date.now() - lastHeartbeatMs);
5409
+ log(`previous exit: UNCLEAN (no marker; last heartbeat ${prevRunning.lastHeartbeatTs}, gap=${(uncleanGapMs / 1e3).toFixed(1)}s)`);
5410
+ if (uncleanGapMs > 6e4) {
5411
+ sendDownAlert(Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
5412
+ }
5413
+ } else {
5414
+ log("previous exit: none (clean or first boot)");
5415
+ }
5416
+ writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: bootTs }, log);
5417
+ }
5086
5418
  let deliveryTick = initialDeliveryTick;
5087
5419
  let probeTick;
5088
5420
  if (daemonCmux) {
@@ -5123,9 +5455,10 @@ function startDaemon(ctx, opts, pkgVersion) {
5123
5455
  keepCount: opts.mailboxConfig?.keepCount ?? 3
5124
5456
  };
5125
5457
  let rotationTimer;
5458
+ let rotationTick;
5126
5459
  if (rotationInterval > 0) {
5127
- const inboxPath = join12(stateRoot, "inbox");
5128
- rotationTimer = setInterval(async () => {
5460
+ const inboxPath = join13(stateRoot, "inbox");
5461
+ rotationTick = async () => {
5129
5462
  try {
5130
5463
  let entries;
5131
5464
  try {
@@ -5138,13 +5471,23 @@ function startDaemon(ctx, opts, pkgVersion) {
5138
5471
  await rotateIfNeeded({ stateRoot, project, ...mboxCfg });
5139
5472
  } catch (e) {
5140
5473
  log(`rotation timer error: ${e.message}`);
5474
+ } finally {
5475
+ writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: (/* @__PURE__ */ new Date()).toISOString() }, log);
5141
5476
  }
5477
+ };
5478
+ rotationTimer = setInterval(() => {
5479
+ void rotationTick();
5142
5480
  }, rotationInterval);
5143
5481
  rotationTimer.unref?.();
5144
5482
  }
5145
5483
  return {
5146
5484
  stop(reason = "requested") {
5147
- log(`exit pid=${process.pid} reason=${reason}`);
5485
+ const ppid = process.ppid;
5486
+ const uptimeMs = Math.round(process.uptime() * 1e3);
5487
+ const inFlight = inFlightDelivery();
5488
+ log(`exit pid=${process.pid} reason=${reason} ppid=${ppid} launchd=${ppid === 1} uptimeMs=${uptimeMs} inFlightDelivery=${inFlight ? `${inFlight.project}#${inFlight.seq}(defers=${inFlight.deferCount})` : "none"}`);
5489
+ writeExitMarker(stateRoot, { ts: (/* @__PURE__ */ new Date()).toISOString(), pid: process.pid, reason, ppid, uptimeMs, inFlightDelivery: inFlight }, log);
5490
+ removeRunningMarker(stateRoot, log);
5148
5491
  if (deliveryTimer)
5149
5492
  clearInterval(deliveryTimer);
5150
5493
  if (probeTimer)
@@ -5173,7 +5516,8 @@ function startDaemon(ctx, opts, pkgVersion) {
5173
5516
  }));
5174
5517
  },
5175
5518
  tickDelivery: deliveryTick,
5176
- tickProbe: probeTick
5519
+ tickProbe: probeTick,
5520
+ tickRotation: rotationTick
5177
5521
  };
5178
5522
  }
5179
5523
  var CURSOR_SUBSCRIBER2, SNAPSHOT_LOG_WINDOW_MS;
@@ -5185,6 +5529,7 @@ var init_start = __esm({
5185
5529
  init_gates();
5186
5530
  init_server();
5187
5531
  init_mailbox();
5532
+ init_exit_marker();
5188
5533
  init_liveness2();
5189
5534
  init_dist();
5190
5535
  init_snapshot_gather();
@@ -6179,9 +6524,9 @@ var init_bridge = __esm({
6179
6524
 
6180
6525
  // packages/core/dist/restart-daemon.js
6181
6526
  import { execFileSync as execFileSync4 } from "child_process";
6182
- import { existsSync as existsSync9 } from "fs";
6527
+ import { existsSync as existsSync10 } from "fs";
6183
6528
  function defaultIsRunning() {
6184
- return existsSync9(DAEMON_SOCK_PATH);
6529
+ return existsSync10(DAEMON_SOCK_PATH);
6185
6530
  }
6186
6531
  function defaultRunKickstart() {
6187
6532
  const uid = process.getuid?.() ?? 0;
@@ -6719,6 +7064,76 @@ var init_side_session = __esm({
6719
7064
  }
6720
7065
  });
6721
7066
 
7067
+ // packages/core/dist/crew-answer.js
7068
+ function describeOptions(options) {
7069
+ return options.map((o) => ` ${o.highlighted ? "\u276F" : " "} ${o.index}. ${o.label}`).join("\n");
7070
+ }
7071
+ function resolveOption(options, selector) {
7072
+ const trimmed = selector.trim();
7073
+ if (/^\d+$/.test(trimmed)) {
7074
+ const byIndex = options.find((o) => o.index === Number(trimmed));
7075
+ if (!byIndex) {
7076
+ throw new Error(`No option ${trimmed} in the visible prompt. Visible options:
7077
+ ${describeOptions(options)}`);
7078
+ }
7079
+ return byIndex;
7080
+ }
7081
+ const lower = trimmed.toLowerCase();
7082
+ const exact = options.filter((o) => o.label.toLowerCase() === lower);
7083
+ if (exact.length === 1)
7084
+ return exact[0];
7085
+ if (exact.length > 1) {
7086
+ throw new Error(`Option text "${selector}" matches multiple options ambiguously:
7087
+ ${describeOptions(exact)}`);
7088
+ }
7089
+ const prefix = options.filter((o) => o.label.toLowerCase().startsWith(lower));
7090
+ if (prefix.length === 1)
7091
+ return prefix[0];
7092
+ if (prefix.length > 1) {
7093
+ throw new Error(`Option text "${selector}" matches multiple options ambiguously:
7094
+ ${describeOptions(prefix)}`);
7095
+ }
7096
+ throw new Error(`No option matches "${selector}". Visible options:
7097
+ ${describeOptions(options)}`);
7098
+ }
7099
+ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps, opts) {
7100
+ const crew = await findCrewPane(runtime, workspaceId, project, name);
7101
+ if (!crew) {
7102
+ throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
7103
+ }
7104
+ const options = await deps.readModalOptions(crew);
7105
+ if (!options) {
7106
+ 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.`);
7107
+ }
7108
+ const target = resolveOption(options, option);
7109
+ if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
7110
+ 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}'.
7111
+ Visible options:
7112
+ ${describeOptions(options)}`);
7113
+ }
7114
+ const log = deps.log ?? (() => {
7115
+ });
7116
+ log(`\u2192 selecting ${target.index}. "${target.label}"`);
7117
+ const current = options.find((o) => o.highlighted) ?? options[0];
7118
+ const steps = target.index - current.index;
7119
+ const key = steps >= 0 ? "Down" : "Up";
7120
+ for (let i = 0; i < Math.abs(steps); i++) {
7121
+ await runtime.sendKeyToPane(crew, key);
7122
+ }
7123
+ await runtime.sendKeyToPane(crew, "Enter");
7124
+ if (opts?.text) {
7125
+ await runtime.pasteToPane(crew, opts.text);
7126
+ await runtime.sendKeyToPane(crew, "Enter");
7127
+ }
7128
+ const after = await deps.readModalOptions(crew);
7129
+ return { selected: target, closed: after === null };
7130
+ }
7131
+ var init_crew_answer = __esm({
7132
+ "packages/core/dist/crew-answer.js"() {
7133
+ init_crew_spawn();
7134
+ }
7135
+ });
7136
+
6722
7137
  // packages/core/dist/lifecycle-source.js
6723
7138
  function reduceLifecycle(prev, next) {
6724
7139
  if (next.origin === "agent") {
@@ -6740,6 +7155,326 @@ var init_lifecycle_source = __esm({
6740
7155
  }
6741
7156
  });
6742
7157
 
7158
+ // packages/core/dist/events/fact.js
7159
+ function stampFact(raw, id) {
7160
+ return { ...raw, ...id };
7161
+ }
7162
+ var init_fact = __esm({
7163
+ "packages/core/dist/events/fact.js"() {
7164
+ }
7165
+ });
7166
+
7167
+ // packages/core/dist/events/log.js
7168
+ var FactLog;
7169
+ var init_log = __esm({
7170
+ "packages/core/dist/events/log.js"() {
7171
+ FactLog = class {
7172
+ capacity;
7173
+ buffers = /* @__PURE__ */ new Map();
7174
+ constructor(opts = {}) {
7175
+ this.capacity = opts.capacity ?? 256;
7176
+ }
7177
+ push(fact) {
7178
+ let buf = this.buffers.get(fact.taskId);
7179
+ if (!buf) {
7180
+ buf = [];
7181
+ this.buffers.set(fact.taskId, buf);
7182
+ }
7183
+ buf.push(fact);
7184
+ while (buf.length > this.capacity)
7185
+ buf.shift();
7186
+ }
7187
+ /** Oldest-first snapshot. A fresh array; later pushes never grow it. */
7188
+ recent(taskId) {
7189
+ return [...this.buffers.get(taskId) ?? []];
7190
+ }
7191
+ /** Newline-delimited JSON, one fact per line, oldest first. */
7192
+ serialize(taskId) {
7193
+ return this.recent(taskId).map((f) => JSON.stringify(f)).join("\n") + "\n";
7194
+ }
7195
+ /** Release a finished crew's buffer. */
7196
+ drop(taskId) {
7197
+ this.buffers.delete(taskId);
7198
+ }
7199
+ };
7200
+ }
7201
+ });
7202
+
7203
+ // packages/core/dist/events/invariant.js
7204
+ function freshTrace() {
7205
+ return {
7206
+ depth: 0,
7207
+ oldestOpenAt: null,
7208
+ stallReported: false,
7209
+ unknownSeen: 0,
7210
+ liveness: /* @__PURE__ */ new Map()
7211
+ };
7212
+ }
7213
+ function checkFact(trace, fact, opts) {
7214
+ const out = [];
7215
+ if (fact.origin === "inferred" && TERMINALISING.has(fact.kind)) {
7216
+ out.push(v("I4", `inferred fact "${fact.kind}" from ${fact.source} cannot terminalise alone`, fact));
7217
+ }
7218
+ switch (fact.kind) {
7219
+ case "tool.opened":
7220
+ if (trace.depth === 0) {
7221
+ trace.oldestOpenAt = fact.at;
7222
+ trace.stallReported = false;
7223
+ }
7224
+ trace.depth += 1;
7225
+ break;
7226
+ case "tool.closed":
7227
+ if (trace.depth === 0) {
7228
+ out.push(v("I1", `tool.closed from ${fact.source} with no open tool`, fact));
7229
+ } else {
7230
+ trace.depth -= 1;
7231
+ if (trace.depth === 0) {
7232
+ trace.oldestOpenAt = null;
7233
+ trace.stallReported = false;
7234
+ }
7235
+ }
7236
+ break;
7237
+ case "turn.ended":
7238
+ if (trace.depth > 0) {
7239
+ out.push(v("I2", `turn.ended with ${trace.depth} tool call(s) still open`, fact));
7240
+ trace.depth = 0;
7241
+ trace.oldestOpenAt = null;
7242
+ trace.stallReported = false;
7243
+ }
7244
+ break;
7245
+ case "unknown":
7246
+ trace.unknownSeen += 1;
7247
+ out.push(v("I5", `unrecognised frame "${fact.name}" from ${fact.source}`, fact));
7248
+ break;
7249
+ case "process.observed": {
7250
+ const prior = [...trace.liveness.entries()].find(([src, s]) => src !== fact.source && s.alive !== fact.alive && fact.at - s.at <= (opts.disagreeWindowMs ?? -1));
7251
+ if (prior) {
7252
+ out.push(v("I6", `liveness disagreement: ${prior[0]} said alive=${prior[1].alive}, ${fact.source} says alive=${fact.alive}`, fact));
7253
+ }
7254
+ trace.liveness.set(fact.source, { alive: fact.alive, at: fact.at });
7255
+ break;
7256
+ }
7257
+ default:
7258
+ break;
7259
+ }
7260
+ if (opts.stallBudgetMs !== void 0 && trace.depth > 0 && trace.oldestOpenAt !== null && !trace.stallReported && fact.at - trace.oldestOpenAt > opts.stallBudgetMs) {
7261
+ trace.stallReported = true;
7262
+ out.push(v("I3", `tool open for ${fact.at - trace.oldestOpenAt}ms, past the stall budget`, fact));
7263
+ }
7264
+ return out;
7265
+ }
7266
+ var v, TERMINALISING;
7267
+ var init_invariant = __esm({
7268
+ "packages/core/dist/events/invariant.js"() {
7269
+ v = (code, message, f) => ({ code, message, taskId: f.taskId, at: f.at });
7270
+ TERMINALISING = /* @__PURE__ */ new Set(["session.ended"]);
7271
+ }
7272
+ });
7273
+
7274
+ // packages/core/dist/events/to-control-event.js
7275
+ function toControlEvent(fact) {
7276
+ if (fact.origin === "inferred" && TERMINALISING2.has(fact.kind))
7277
+ return [];
7278
+ switch (fact.kind) {
7279
+ case "turn.ended":
7280
+ return [{ type: "task.turn.completed", id: fact.taskId, turnId: fact.turnId ?? fact.source }];
7281
+ case "permission.requested":
7282
+ return [{
7283
+ type: "task.approval.requested",
7284
+ id: fact.taskId,
7285
+ requestId: fact.requestId,
7286
+ question: fact.question,
7287
+ kind: fact.tool
7288
+ }];
7289
+ case "input.requested":
7290
+ return [{
7291
+ type: "task.input.requested",
7292
+ id: fact.taskId,
7293
+ requestId: fact.requestId,
7294
+ question: fact.question
7295
+ }];
7296
+ case "session.ended":
7297
+ return [{ type: "task.session.ended", id: fact.taskId }];
7298
+ case "session.started":
7299
+ return [{
7300
+ type: "task.started",
7301
+ id: fact.taskId,
7302
+ ...fact.pid === void 0 ? {} : { pid: fact.pid },
7303
+ ...fact.sessionId === void 0 ? {} : { sessionId: fact.sessionId }
7304
+ }];
7305
+ case "prompt.submitted":
7306
+ return [{ type: "task.first-turn.confirmed", id: fact.taskId }];
7307
+ // Liveness-only. The facade still feeds these to reduceLifecycle; they
7308
+ // simply carry no ControlEvent of their own.
7309
+ case "tool.opened":
7310
+ case "tool.closed":
7311
+ case "activity":
7312
+ case "process.observed":
7313
+ case "unknown":
7314
+ return [];
7315
+ }
7316
+ }
7317
+ var TERMINALISING2;
7318
+ var init_to_control_event = __esm({
7319
+ "packages/core/dist/events/to-control-event.js"() {
7320
+ TERMINALISING2 = /* @__PURE__ */ new Set(["session.ended"]);
7321
+ }
7322
+ });
7323
+
7324
+ // packages/core/dist/events/conformance.js
7325
+ function assert(cond, msg) {
7326
+ if (!cond)
7327
+ throw new Error(`conformance: ${msg}`);
7328
+ }
7329
+ function runAdapterConformance(adapter, samples) {
7330
+ const call = (raw) => adapter.translate(raw);
7331
+ return [
7332
+ {
7333
+ name: `${adapter.name}: never throws on garbage`,
7334
+ run: () => {
7335
+ for (const g of GARBAGE) {
7336
+ try {
7337
+ call(g);
7338
+ } catch (e) {
7339
+ throw new Error(`threw on ${JSON.stringify(g)}: ${String(e)}`);
7340
+ }
7341
+ }
7342
+ }
7343
+ },
7344
+ {
7345
+ name: `${adapter.name}: never returns null or undefined`,
7346
+ run: () => {
7347
+ for (const g of [...GARBAGE, ...samples]) {
7348
+ const out = call(g);
7349
+ assert(Array.isArray(out), `returned a non-array for ${JSON.stringify(g)}`);
7350
+ }
7351
+ }
7352
+ },
7353
+ {
7354
+ name: `${adapter.name}: an unrecognised frame yields unknown, not an empty array`,
7355
+ run: () => {
7356
+ const out = call({ type: "definitely-not-a-real-event-name" });
7357
+ assert(out.length > 0, "silently dropped an unrecognised frame (the #542 shape)");
7358
+ assert(out.every((f) => f.kind === "unknown"), "an unrecognised frame must translate to kind 'unknown'");
7359
+ }
7360
+ },
7361
+ {
7362
+ name: `${adapter.name}: recognises its own samples`,
7363
+ run: () => {
7364
+ for (const s of samples) {
7365
+ const out = call(s);
7366
+ assert(out.length > 0, `produced nothing for its own sample ${JSON.stringify(s)}`);
7367
+ assert(out.some((f) => f.kind !== "unknown"), `failed to recognise its own sample ${JSON.stringify(s)}`);
7368
+ }
7369
+ }
7370
+ },
7371
+ {
7372
+ name: `${adapter.name}: declares a constant origin`,
7373
+ run: () => {
7374
+ assert(adapter.origin === "agent" || adapter.origin === "scan" || adapter.origin === "inferred", `invalid origin "${String(adapter.origin)}"`);
7375
+ }
7376
+ }
7377
+ ];
7378
+ }
7379
+ var GARBAGE;
7380
+ var init_conformance = __esm({
7381
+ "packages/core/dist/events/conformance.js"() {
7382
+ GARBAGE = [
7383
+ null,
7384
+ void 0,
7385
+ 0,
7386
+ "",
7387
+ "not json",
7388
+ [],
7389
+ {},
7390
+ { type: 42 },
7391
+ { type: "definitely-not-a-real-event-name" }
7392
+ ];
7393
+ }
7394
+ });
7395
+
7396
+ // packages/core/dist/events/source.js
7397
+ function createEventsSource(opts) {
7398
+ const now = opts.now ?? (() => Date.now());
7399
+ const log = new FactLog({ capacity: opts.capacity });
7400
+ const adapters = new Map(opts.adapters.map((a) => [a.name, a]));
7401
+ const traces = /* @__PURE__ */ new Map();
7402
+ const seqs = /* @__PURE__ */ new Map();
7403
+ let deps;
7404
+ const traceFor = (taskId) => {
7405
+ let t = traces.get(taskId);
7406
+ if (!t) {
7407
+ t = freshTrace();
7408
+ traces.set(taskId, t);
7409
+ }
7410
+ return t;
7411
+ };
7412
+ const nextSeq = (taskId) => {
7413
+ const n = seqs.get(taskId) ?? 0;
7414
+ seqs.set(taskId, n + 1);
7415
+ return n;
7416
+ };
7417
+ return {
7418
+ name: "events",
7419
+ start(d) {
7420
+ deps = d;
7421
+ },
7422
+ stop() {
7423
+ deps = void 0;
7424
+ },
7425
+ health() {
7426
+ return { active: deps !== void 0, error: null };
7427
+ },
7428
+ recent(taskId) {
7429
+ return log.recent(taskId);
7430
+ },
7431
+ dump(taskId) {
7432
+ return log.serialize(taskId);
7433
+ },
7434
+ ingest(source, raw, hint) {
7435
+ const adapter = adapters.get(source);
7436
+ if (!adapter || !deps)
7437
+ return;
7438
+ const rec = deps.resolve(hint);
7439
+ if (!rec)
7440
+ return;
7441
+ const taskId = rec.id;
7442
+ const at = now();
7443
+ let produced;
7444
+ try {
7445
+ const out = adapter.translate(raw);
7446
+ produced = Array.isArray(out) ? out : [{ kind: "unknown", name: `${source} returned non-array` }];
7447
+ } catch (e) {
7448
+ opts.log?.(`events: adapter ${source} threw: ${String(e)}`);
7449
+ produced = [{ kind: "unknown", name: `${source} threw` }];
7450
+ }
7451
+ for (const rawFact of produced) {
7452
+ const fact = stampFact(rawFact, {
7453
+ seq: nextSeq(taskId),
7454
+ taskId,
7455
+ at,
7456
+ source,
7457
+ origin: adapter.origin
7458
+ });
7459
+ log.push(fact);
7460
+ for (const v2 of checkFact(traceFor(taskId), fact, opts.check ?? {})) {
7461
+ opts.onViolation(v2);
7462
+ }
7463
+ for (const ev of toControlEvent(fact))
7464
+ opts.emit(ev);
7465
+ }
7466
+ }
7467
+ };
7468
+ }
7469
+ var init_source = __esm({
7470
+ "packages/core/dist/events/source.js"() {
7471
+ init_fact();
7472
+ init_log();
7473
+ init_invariant();
7474
+ init_to_control_event();
7475
+ }
7476
+ });
7477
+
6743
7478
  // packages/core/dist/index.js
6744
7479
  var dist_exports2 = {};
6745
7480
  __export(dist_exports2, {
@@ -6753,6 +7488,8 @@ __export(dist_exports2, {
6753
7488
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
6754
7489
  DEFAULT_TASK_TIMEOUT_MS: () => DEFAULT_TASK_TIMEOUT_MS,
6755
7490
  DeferDelivery: () => DeferDelivery,
7491
+ FIRST_TURN_INLINE_MAX_BYTES: () => FIRST_TURN_INLINE_MAX_BYTES,
7492
+ FactLog: () => FactLog,
6756
7493
  GROUP_DISPATCH_WARMUP_POLL_MS: () => GROUP_DISPATCH_WARMUP_POLL_MS,
6757
7494
  GROUP_DISPATCH_WARMUP_TIMEOUT_MS: () => GROUP_DISPATCH_WARMUP_TIMEOUT_MS,
6758
7495
  IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
@@ -6782,9 +7519,11 @@ __export(dist_exports2, {
6782
7519
  capAllowed: () => capAllowed,
6783
7520
  capOutput: () => capOutput,
6784
7521
  captainSocketPath: () => captainSocketPath,
7522
+ checkFact: () => checkFact,
6785
7523
  classifyHealth: () => classifyHealth,
6786
7524
  closeWorkItem: () => closeWorkItem,
6787
7525
  computeTemplateHash: () => computeTemplateHash,
7526
+ consumeExitMarker: () => consumeExitMarker,
6788
7527
  createAttach: () => createAttach,
6789
7528
  createCrewPaneReader: () => createCrewPaneReader,
6790
7529
  createDaemon: () => createDaemon,
@@ -6793,6 +7532,7 @@ __export(dist_exports2, {
6793
7532
  createDirectCrewPaneReader: () => createDirectCrewPaneReader,
6794
7533
  createDirectSurfaceLivenessProbe: () => createDirectSurfaceLivenessProbe,
6795
7534
  createEnsureCaptainAlive: () => createEnsureCaptainAlive,
7535
+ createEventsSource: () => createEventsSource,
6796
7536
  createInteractiveProbe: () => createInteractiveProbe,
6797
7537
  createIsCaptainAlive: () => createIsCaptainAlive,
6798
7538
  createLaunch: () => createLaunch,
@@ -6825,13 +7565,18 @@ __export(dist_exports2, {
6825
7565
  encodeMsg: () => encodeMsg,
6826
7566
  ensureDaemon: () => ensureDaemon,
6827
7567
  evaluateStall: () => evaluateStall,
7568
+ exitMarkerPath: () => exitMarkerPath,
6828
7569
  fallsBackToPane: () => fallsBackToPane,
7570
+ findCrewPane: () => findCrewPane,
6829
7571
  findOpenChildren: () => findOpenChildren,
6830
7572
  findProjectByThread: () => findProjectByThread,
6831
7573
  findWorkItemById: () => findWorkItemById,
7574
+ forceKickstartAndVerify: () => forceKickstartAndVerify,
6832
7575
  formatInbound: () => formatInbound,
6833
7576
  formatInboundReceipt: () => formatInboundReceipt,
6834
7577
  formatLifecycle: () => formatLifecycle,
7578
+ freshTrace: () => freshTrace,
7579
+ getDaemonPid: () => getDaemonPid,
6835
7580
  healCmdFor: () => healCmdFor,
6836
7581
  isAuthorized: () => isAuthorized,
6837
7582
  isBareSpawn: () => isBareSpawn,
@@ -6866,6 +7611,7 @@ __export(dist_exports2, {
6866
7611
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
6867
7612
  readCursor: () => readCursor,
6868
7613
  readFromCursor: () => readFromCursor,
7614
+ readRunningMarker: () => readRunningMarker,
6869
7615
  reapCrewChildren: () => reapCrewChildren,
6870
7616
  reapOrphanedCrews: () => reapOrphanedCrews,
6871
7617
  reconcileLiveness: () => reconcileLiveness,
@@ -6874,6 +7620,7 @@ __export(dist_exports2, {
6874
7620
  reduce: () => reduce,
6875
7621
  reduceLifecycle: () => reduceLifecycle,
6876
7622
  releaseDaemonLock: () => releaseDaemonLock,
7623
+ removeRunningMarker: () => removeRunningMarker,
6877
7624
  renderPlist: () => renderPlist,
6878
7625
  reregisterDaemon: () => reregisterDaemon,
6879
7626
  resolveAgentBinDirs: () => resolveAgentBinDirs,
@@ -6885,6 +7632,8 @@ __export(dist_exports2, {
6885
7632
  resolveSetupUserId: () => resolveSetupUserId,
6886
7633
  restartDaemonIfRunning: () => restartDaemonIfRunning,
6887
7634
  rotateIfNeeded: () => rotateIfNeeded,
7635
+ runAdapterConformance: () => runAdapterConformance,
7636
+ runCrewAnswer: () => runCrewAnswer,
6888
7637
  runCrewClose: () => runCrewClose,
6889
7638
  runCrewList: () => runCrewList,
6890
7639
  runCrewRead: () => runCrewRead,
@@ -6904,6 +7653,7 @@ __export(dist_exports2, {
6904
7653
  runTelegramPostSetup: () => runTelegramPostSetup,
6905
7654
  runTelegramSend: () => runTelegramSend,
6906
7655
  runTelegramStatus: () => runTelegramStatus,
7656
+ runningMarkerPath: () => runningMarkerPath,
6907
7657
  sanitizePathForPlist: () => sanitizePathForPlist,
6908
7658
  saveSessions: () => saveSessions,
6909
7659
  saveState: () => saveState,
@@ -6917,18 +7667,22 @@ __export(dist_exports2, {
6917
7667
  sideNameFromTitle: () => sideNameFromTitle,
6918
7668
  sideNextAutoName: () => sideNextAutoName,
6919
7669
  sideTitleFor: () => sideTitleFor,
7670
+ stampFact: () => stampFact,
6920
7671
  startDaemon: () => startDaemon,
6921
7672
  startServer: () => startServer,
6922
7673
  stripBotMention: () => stripBotMention,
6923
7674
  surfaceVerdict: () => surfaceVerdict,
6924
7675
  timeoutGate: () => timeoutGate,
6925
7676
  titleFor: () => titleFor,
7677
+ toControlEvent: () => toControlEvent,
6926
7678
  topicKey: () => topicKey,
6927
7679
  topicName: () => topicName,
6928
7680
  tryAcquireDaemonLock: () => tryAcquireDaemonLock,
6929
7681
  waitForCaptainDelivery: () => waitForCaptainDelivery,
6930
7682
  waitForWarmup: () => waitForWarmup,
6931
7683
  writeCursor: () => writeCursor,
7684
+ writeExitMarker: () => writeExitMarker,
7685
+ writeRunningMarker: () => writeRunningMarker,
6932
7686
  writeTelegramConfig: () => writeTelegramConfig
6933
7687
  });
6934
7688
  var init_dist2 = __esm({
@@ -6951,6 +7705,7 @@ var init_dist2 = __esm({
6951
7705
  init_attach();
6952
7706
  init_start();
6953
7707
  init_delivery_loop();
7708
+ init_exit_marker();
6954
7709
  init_interactive_probe();
6955
7710
  init_captain_delivery();
6956
7711
  init_defer_delivery();
@@ -6964,7 +7719,14 @@ var init_dist2 = __esm({
6964
7719
  init_launch_workspace();
6965
7720
  init_side_session();
6966
7721
  init_crew_spawn();
7722
+ init_crew_answer();
6967
7723
  init_lifecycle_source();
7724
+ init_fact();
7725
+ init_log();
7726
+ init_invariant();
7727
+ init_to_control_event();
7728
+ init_conformance();
7729
+ init_source();
6968
7730
  init_control_channel();
6969
7731
  init_captain_channel();
6970
7732
  }
@@ -7120,6 +7882,35 @@ function hasModalOptionList(screen) {
7120
7882
  return false;
7121
7883
  return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
7122
7884
  }
7885
+ function parseModalOptions(screen) {
7886
+ if (!hasModalOptionList(screen))
7887
+ return null;
7888
+ const lines = screen.split(/\r?\n/);
7889
+ const HR_RE = /^\s*─{10,}\s*$/;
7890
+ let bottomHR = -1;
7891
+ let topHR = -1;
7892
+ for (let i = lines.length - 1; i >= 0; i--) {
7893
+ if (HR_RE.test(lines[i])) {
7894
+ if (bottomHR === -1)
7895
+ bottomHR = i;
7896
+ else {
7897
+ topHR = i;
7898
+ break;
7899
+ }
7900
+ }
7901
+ }
7902
+ if (topHR === -1)
7903
+ return null;
7904
+ const OPTION_RE3 = /^\s*(❯)?\s*(\d+)\.\s*(.*?)\s*$/;
7905
+ const options = [];
7906
+ for (const line of lines.slice(topHR + 1, bottomHR)) {
7907
+ const m = line.match(OPTION_RE3);
7908
+ if (!m)
7909
+ continue;
7910
+ options.push({ index: Number(m[2]), label: m[3], highlighted: m[1] === "\u276F" });
7911
+ }
7912
+ return options.length > 0 ? options : null;
7913
+ }
7123
7914
  function readInputBoxRaw(screen, opts) {
7124
7915
  if (!screen)
7125
7916
  return null;
@@ -7366,7 +8157,10 @@ function createCmuxDriver() {
7366
8157
  let screen = "";
7367
8158
  try {
7368
8159
  screen = await cmux(["read-screen", "--workspace", ws, "--surface", sf]);
7369
- } catch {
8160
+ } catch (e) {
8161
+ process.stderr.write(`[squadrant] read-screen failed for ${ws}/${sf}: ${e.message}
8162
+ `);
8163
+ throw new DeferDelivery(null, "probe-failed");
7370
8164
  }
7371
8165
  const draft = parseDraftFromScreen(screen);
7372
8166
  if (draft === null)
@@ -7616,7 +8410,7 @@ var init_notifiers = __esm({
7616
8410
 
7617
8411
  // packages/workspaces/dist/workspaces/obsidian.js
7618
8412
  import fs13 from "fs/promises";
7619
- import { existsSync as existsSync10 } from "fs";
8413
+ import { existsSync as existsSync11 } from "fs";
7620
8414
  import path13 from "path";
7621
8415
  function resolveInRoot(root, relative) {
7622
8416
  const joined = path13.resolve(root, relative);
@@ -7636,7 +8430,7 @@ function createObsidianDriver(scope) {
7636
8430
  async probe() {
7637
8431
  return {
7638
8432
  installed: true,
7639
- rootExists: existsSync10(root)
8433
+ rootExists: existsSync11(root)
7640
8434
  };
7641
8435
  },
7642
8436
  async read(rel) {
@@ -7834,6 +8628,16 @@ var init_events_bridge = __esm({
7834
8628
  }
7835
8629
  if (f?.type !== "event" || f.category !== "agent")
7836
8630
  return;
8631
+ if (f.name === "agent.hook.PostToolUse") {
8632
+ const p2 = f.payload ?? {};
8633
+ if (p2.phase === "received")
8634
+ return;
8635
+ const rec2 = this.deps.resolve({ cwd: p2.cwd, source: p2._source ?? f.source, sessionId: p2.session_id });
8636
+ if (!rec2)
8637
+ return;
8638
+ this.deps.emit({ type: "task.progress", id: rec2.id, note: f.name });
8639
+ return;
8640
+ }
7837
8641
  const runState = f.name ? deriveRunState(f.name) : null;
7838
8642
  if (!runState)
7839
8643
  return;
@@ -7960,11 +8764,11 @@ var init_store_fingerprint = __esm({
7960
8764
  });
7961
8765
 
7962
8766
  // 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";
8767
+ import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
8768
+ import { join as join14 } from "path";
7965
8769
  import { homedir as homedir7 } from "os";
7966
8770
  async function readCmuxLiveness() {
7967
- const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join13(homedir7(), ".cmuxterm");
8771
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir7(), ".cmuxterm");
7968
8772
  const projects = loadConfig().projects;
7969
8773
  let files;
7970
8774
  try {
@@ -7972,7 +8776,7 @@ async function readCmuxLiveness() {
7972
8776
  } catch (e) {
7973
8777
  throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
7974
8778
  }
7975
- return readLivenessSnapshot(files, (f) => readFileSync9(join13(dir, f), "utf-8"), projects);
8779
+ return readLivenessSnapshot(files, (f) => readFileSync10(join14(dir, f), "utf-8"), projects);
7976
8780
  }
7977
8781
  var DaemonCmux;
7978
8782
  var init_daemon_cmux = __esm({
@@ -8043,9 +8847,9 @@ var init_daemon_cmux = __esm({
8043
8847
  });
8044
8848
 
8045
8849
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
8046
- import { join as join14 } from "path";
8850
+ import { join as join15 } from "path";
8047
8851
  import { homedir as homedir8 } from "os";
8048
- import { watch, readdirSync as readdirSync5, readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
8852
+ import { watch, readdirSync as readdirSync5, readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
8049
8853
  function parseLifecycleState(s) {
8050
8854
  if (s === "running" || s === "idle" || s === "needsInput" || s === "unknown") {
8051
8855
  return s;
@@ -8069,7 +8873,7 @@ function defaultListFiles(dir) {
8069
8873
  }
8070
8874
  function defaultReadFile(path36) {
8071
8875
  try {
8072
- return readFileSync10(path36, "utf-8");
8876
+ return readFileSync11(path36, "utf-8");
8073
8877
  } catch {
8074
8878
  return void 0;
8075
8879
  }
@@ -8105,12 +8909,12 @@ var init_cmux_store_source = __esm({
8105
8909
  active = false;
8106
8910
  lastError = null;
8107
8911
  constructor(opts = {}) {
8108
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join14(homedir8(), ".cmuxterm");
8912
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir8(), ".cmuxterm");
8109
8913
  this.debounceMs = opts.debounceMs ?? 50;
8110
8914
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
8111
8915
  this.listFiles = opts.listFiles ?? defaultListFiles;
8112
8916
  this.readFile = opts.readFile ?? defaultReadFile;
8113
- this.fileExists = opts.fileExists ?? existsSync11;
8917
+ this.fileExists = opts.fileExists ?? existsSync12;
8114
8918
  this.watchDir = opts.watchDir ?? defaultWatchDir;
8115
8919
  this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
8116
8920
  this.cancelTimer = opts.cancelTimer ?? clearTimeout;
@@ -8167,7 +8971,7 @@ var init_cmux_store_source = __esm({
8167
8971
  }
8168
8972
  scanFile(filename) {
8169
8973
  const deps = this.deps;
8170
- const filePath = join14(this.stateDir, filename);
8974
+ const filePath = join15(this.stateDir, filename);
8171
8975
  const lockPath = `${filePath}.lock`;
8172
8976
  if (this.fileExists(lockPath)) {
8173
8977
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -8221,11 +9025,11 @@ var init_cmux_store_source = __esm({
8221
9025
  });
8222
9026
 
8223
9027
  // packages/workspaces/dist/native-hooks/native-hook-source.js
8224
- import { join as join15 } from "path";
9028
+ import { join as join16 } from "path";
8225
9029
  import { homedir as homedir9 } from "os";
8226
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
9030
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
8227
9031
  function installClaudeHooks(opts = {}) {
8228
- const settingsPath = opts.settingsPath ?? join15(homedir9(), ".claude", "settings.json");
9032
+ const settingsPath = opts.settingsPath ?? join16(homedir9(), ".claude", "settings.json");
8229
9033
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
8230
9034
  const readFile7 = opts.readFile ?? defaultReadFile2;
8231
9035
  const writeFile6 = opts.writeFile ?? defaultWriteFile;
@@ -8321,14 +9125,14 @@ function extractDetail(sub, payload) {
8321
9125
  }
8322
9126
  function defaultReadFile2(path36) {
8323
9127
  try {
8324
- return readFileSync11(path36, "utf-8");
9128
+ return readFileSync12(path36, "utf-8");
8325
9129
  } catch {
8326
9130
  return void 0;
8327
9131
  }
8328
9132
  }
8329
9133
  function defaultWriteFile(path36, content) {
8330
9134
  mkdirSync6(path36.replace(/\/[^/]+$/, ""), { recursive: true });
8331
- writeFileSync8(path36, content, "utf-8");
9135
+ writeFileSync9(path36, content, "utf-8");
8332
9136
  }
8333
9137
  var CLAUDE_HOOK_EVENTS, DEFAULT_HOOK_CMD, NativeHookSource;
8334
9138
  var init_native_hook_source = __esm({
@@ -8474,6 +9278,10 @@ async function paneHasOpenModal(runtime, pane) {
8474
9278
  const screen = await runtime.readPaneScreen(pane) ?? "";
8475
9279
  return hasModalOptionList(screen);
8476
9280
  }
9281
+ async function readModalOptions(runtime, pane) {
9282
+ const screen = await runtime.readPaneScreen(pane) ?? "";
9283
+ return parseModalOptions(screen);
9284
+ }
8477
9285
  async function confirmedSendToPane(runtime, pane, message) {
8478
9286
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
8479
9287
  if (hasModalOptionList(preSendScreen)) {
@@ -8629,6 +9437,7 @@ __export(dist_exports3, {
8629
9437
  mapSubToLifecycle: () => mapSubToLifecycle,
8630
9438
  paneHasOpenModal: () => paneHasOpenModal,
8631
9439
  readCmuxLiveness: () => readCmuxLiveness,
9440
+ readModalOptions: () => readModalOptions,
8632
9441
  resendCrewFirstTurn: () => resendCrewFirstTurn,
8633
9442
  resolveCaptainWorkspace: () => resolveCaptainWorkspace,
8634
9443
  sendFirstTurnWhenReady: () => sendFirstTurnWhenReady
@@ -8680,6 +9489,9 @@ function createClaudeDriver() {
8680
9489
  if (opts.model) {
8681
9490
  cmd += ` --model ${opts.model}`;
8682
9491
  }
9492
+ if (opts.thinking) {
9493
+ cmd += ` --effort ${opts.thinking}`;
9494
+ }
8683
9495
  if (opts.autoApprove) {
8684
9496
  cmd += " --dangerously-skip-permissions";
8685
9497
  } else if (opts.permissionMode) {
@@ -8956,7 +9768,7 @@ var init_registry4 = __esm({
8956
9768
  // packages/agents/dist/drivers/launch-cmd.js
8957
9769
  import fs14 from "fs";
8958
9770
  import path14 from "path";
8959
- function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName) {
9771
+ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model, templatesDir, messagingSocketPath, sessionName, thinking) {
8960
9772
  const driver = registry.getDriver(agentName);
8961
9773
  if (driver.name === "claude") {
8962
9774
  let cmd = fresh ? "claude" : "claude -c";
@@ -8973,6 +9785,9 @@ function buildAgentCmd(agentName, registry, role, fresh, permissionMode, model,
8973
9785
  if (model) {
8974
9786
  cmd += ` --model ${model}`;
8975
9787
  }
9788
+ if (thinking) {
9789
+ cmd += ` --effort ${thinking}`;
9790
+ }
8976
9791
  if (templatesDir) {
8977
9792
  const roleFile2 = path14.join(templatesDir, `${role}.claude.md`);
8978
9793
  const legacyRoleFile = path14.join(templatesDir, `${role}.CLAUDE.md`);
@@ -9753,10 +10568,10 @@ var init_codex_app_server_source = __esm({
9753
10568
  // packages/agents/dist/codex/config.js
9754
10569
  import { readFile as readFile6 } from "fs/promises";
9755
10570
  import { homedir as homedir10 } from "os";
9756
- import { join as join16 } from "path";
10571
+ import { join as join17 } from "path";
9757
10572
  async function resolveCodexModel() {
9758
- const home = process.env["CODEX_HOME"] ?? join16(homedir10(), ".codex");
9759
- const configPath = join16(home, "config.toml");
10573
+ const home = process.env["CODEX_HOME"] ?? join17(homedir10(), ".codex");
10574
+ const configPath = join17(home, "config.toml");
9760
10575
  let text;
9761
10576
  try {
9762
10577
  text = await readFile6(configPath, "utf8");
@@ -9873,9 +10688,9 @@ ${directive}` : directive;
9873
10688
  function withTimeout(p, ms, msg) {
9874
10689
  return new Promise((resolve4, reject) => {
9875
10690
  const t = setTimeout(() => reject(new Error(msg)), ms);
9876
- p.then((v) => {
10691
+ p.then((v2) => {
9877
10692
  clearTimeout(t);
9878
- resolve4(v);
10693
+ resolve4(v2);
9879
10694
  }, (e) => {
9880
10695
  clearTimeout(t);
9881
10696
  reject(e);
@@ -10109,9 +10924,10 @@ var init_driver = __esm({
10109
10924
  });
10110
10925
 
10111
10926
  // packages/agents/dist/opencode/sse-bridge.js
10112
- var OpencodeSseBridge;
10927
+ var IGNORED_FRAME, OpencodeSseBridge;
10113
10928
  var init_sse_bridge = __esm({
10114
10929
  "packages/agents/dist/opencode/sse-bridge.js"() {
10930
+ IGNORED_FRAME = /^(message|storage|file|lsp|installation)\./;
10115
10931
  OpencodeSseBridge = class {
10116
10932
  controllers = /* @__PURE__ */ new Map();
10117
10933
  /** taskId → the crew's opencode server port (for permission-reply POSTs). */
@@ -10248,6 +11064,10 @@ var init_sse_bridge = __esm({
10248
11064
  return;
10249
11065
  }
10250
11066
  if (json?.type === "session.idle") {
11067
+ if (this.deps.ingest) {
11068
+ this.deps.ingest(json, taskId);
11069
+ return;
11070
+ }
10251
11071
  this.deps.emit({
10252
11072
  type: "task.turn.completed",
10253
11073
  id: taskId,
@@ -10257,6 +11077,10 @@ var init_sse_bridge = __esm({
10257
11077
  const p = json.properties;
10258
11078
  if (p?.id && p?.sessionID) {
10259
11079
  this.pendingPermByTask.set(taskId, { permID: p.id, sessionID: p.sessionID });
11080
+ if (this.deps.ingest) {
11081
+ this.deps.ingest(json, taskId);
11082
+ return;
11083
+ }
10260
11084
  const tool = p.permission ?? "a tool";
10261
11085
  const cmd = Array.isArray(p.patterns) && p.patterns.length ? `: ${p.patterns.join(" ")}` : "";
10262
11086
  this.deps.emit({
@@ -10266,20 +11090,33 @@ var init_sse_bridge = __esm({
10266
11090
  question: `opencode requests permission to run ${tool}${cmd}`,
10267
11091
  kind: tool
10268
11092
  });
11093
+ } else if (this.deps.ingest) {
11094
+ this.deps.ingest(json, taskId);
10269
11095
  }
10270
11096
  } else if (json?.type === "permission.replied") {
10271
11097
  this.pendingPermByTask.delete(taskId);
11098
+ this.deps.ingest?.(json, taskId);
11099
+ } else if (!IGNORED_FRAME.test(json?.type ?? "")) {
11100
+ this.deps.ingest?.(json, taskId);
10272
11101
  }
10273
11102
  }
11103
+ /** Test seam: exercise handleLine without an SSE stream. */
11104
+ handleLineForTest(rawLine, taskId) {
11105
+ this.handleLine(taskId, rawLine);
11106
+ }
11107
+ /** Test seam: read pendingPermByTask without exposing it publicly. */
11108
+ pendingPermForTest(taskId) {
11109
+ return this.pendingPermByTask.get(taskId);
11110
+ }
10274
11111
  };
10275
11112
  }
10276
11113
  });
10277
11114
 
10278
11115
  // packages/agents/dist/interactive/claude.js
10279
11116
  import { execSync as execSync7 } from "child_process";
10280
- import { readFileSync as readFileSync12 } from "fs";
11117
+ import { readFileSync as readFileSync13 } from "fs";
10281
11118
  import { homedir as homedir11 } from "os";
10282
- import { join as join17 } from "path";
11119
+ import { join as join18 } from "path";
10283
11120
  function probeClaudeSettingsFlag() {
10284
11121
  try {
10285
11122
  const help = execSync7("claude --help", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -10337,11 +11174,11 @@ function deriveTranscriptPath(sessionId, cwd) {
10337
11174
  if (!sessionId || !cwd)
10338
11175
  return null;
10339
11176
  const escaped = cwd.replace(/[^a-zA-Z0-9]/g, "-");
10340
- return join17(homedir11(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
11177
+ return join18(homedir11(), ".claude", "projects", escaped, `${sessionId}.jsonl`);
10341
11178
  }
10342
11179
  function readLastAssistantText(transcriptPath) {
10343
11180
  try {
10344
- const raw = readFileSync12(transcriptPath, "utf-8");
11181
+ const raw = readFileSync13(transcriptPath, "utf-8");
10345
11182
  const lines = raw.split(/\r?\n/);
10346
11183
  for (let i = lines.length - 1; i >= 0; i--) {
10347
11184
  const line = lines[i].trim();
@@ -10407,6 +11244,29 @@ function formatAskUserQuestionPrompt(toolInput) {
10407
11244
  }
10408
11245
  return parts.length > 0 ? parts.join(" | ") : null;
10409
11246
  }
11247
+ function decideCaptainMemoryWrite(toolName, toolInput, env, homeDir) {
11248
+ if (!env.SQUADRANT_CREW_TASK_ID)
11249
+ return { decision: "allow" };
11250
+ if (toolName === "Bash") {
11251
+ const command = toolInput?.command;
11252
+ if (typeof command === "string" && CAPTAIN_MEMORY_PATH_RE.test(command)) {
11253
+ return { decision: "deny", reason: DENY_REASON };
11254
+ }
11255
+ return { decision: "allow" };
11256
+ }
11257
+ const field = FILE_PATH_FIELD_BY_TOOL[toolName];
11258
+ if (!field)
11259
+ return { decision: "allow" };
11260
+ const filePath = toolInput?.[field];
11261
+ if (typeof filePath !== "string" || !filePath)
11262
+ return { decision: "allow" };
11263
+ const home = homeDir.endsWith("/") ? homeDir.slice(0, -1) : homeDir;
11264
+ if (!filePath.startsWith(home))
11265
+ return { decision: "allow" };
11266
+ if (!CAPTAIN_MEMORY_PATH_RE.test(filePath))
11267
+ return { decision: "allow" };
11268
+ return { decision: "deny", reason: DENY_REASON };
11269
+ }
10410
11270
  function mapClaudeHookToEvent(event, payload, taskId) {
10411
11271
  switch (event) {
10412
11272
  case "PreToolUse": {
@@ -10442,7 +11302,7 @@ function mapClaudeHookToEvent(event, payload, taskId) {
10442
11302
  return null;
10443
11303
  }
10444
11304
  }
10445
- var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, claudeInteractive;
11305
+ var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, CAPTAIN_MEMORY_PATH_RE, DENY_REASON, FILE_PATH_FIELD_BY_TOOL, claudeInteractive;
10446
11306
  var init_claude3 = __esm({
10447
11307
  "packages/agents/dist/interactive/claude.js"() {
10448
11308
  EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit"];
@@ -10450,6 +11310,14 @@ var init_claude3 = __esm({
10450
11310
  ["PreToolUse", "AskUserQuestion"]
10451
11311
  ];
10452
11312
  nextAskUserQuestionRequestId = Date.now();
11313
+ CAPTAIN_MEMORY_PATH_RE = /\.claude\/projects\/.*\/memory\//;
11314
+ DENY_REASON = "Crews do not write captain memory. Put the finding in your done/blocked message; the captain decides what is durable (#556).";
11315
+ FILE_PATH_FIELD_BY_TOOL = {
11316
+ Write: "file_path",
11317
+ Edit: "file_path",
11318
+ MultiEdit: "file_path",
11319
+ NotebookEdit: "notebook_path"
11320
+ };
10453
11321
  claudeInteractive = {
10454
11322
  provider: "claude",
10455
11323
  tier: "strong",
@@ -10505,8 +11373,11 @@ function classifyPaneTail2(tail) {
10505
11373
  if (q)
10506
11374
  return { kind: "question", text: q };
10507
11375
  let errLine = null;
10508
- for (const c of cleaned) {
10509
- if (c != null && ERROR_BANNER_RE2.some((re) => re.test(c)))
11376
+ for (let i = 0; i < cleaned.length; i++) {
11377
+ const c = cleaned[i];
11378
+ if (c == null || isQuotedLine2(raw[i]))
11379
+ continue;
11380
+ if (ERROR_BANNER_RE2.some((re) => re.test(c)))
10510
11381
  errLine = c;
10511
11382
  }
10512
11383
  if (errLine) {
@@ -10518,6 +11389,10 @@ function classifyPaneTail2(tail) {
10518
11389
  }
10519
11390
  return null;
10520
11391
  }
11392
+ function isQuotedLine2(raw) {
11393
+ const noAnsi = raw.replace(/\[[0-9;]*m/g, "");
11394
+ return QUOTED_PREFIX_RE2.test(noAnsi);
11395
+ }
10521
11396
  function stripChrome2(raw) {
10522
11397
  let line = raw.replace(/\[[0-9;]*m/g, "");
10523
11398
  line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
@@ -10532,7 +11407,7 @@ function stripChrome2(raw) {
10532
11407
  return null;
10533
11408
  return trimmed;
10534
11409
  }
10535
- var ERROR_BANNER_RE2, RETRYING_RE, EXHAUSTED_RE, OPTION_RE2, PICKER_FOOTER_RE2, PURE_CHROME_RE2, STATUS_LINE_RE2;
11410
+ var ERROR_BANNER_RE2, RETRYING_RE, EXHAUSTED_RE, QUOTED_PREFIX_RE2, OPTION_RE2, PICKER_FOOTER_RE2, PURE_CHROME_RE2, STATUS_LINE_RE2;
10536
11411
  var init_pane_classifier = __esm({
10537
11412
  "packages/agents/dist/interactive/pane-classifier.js"() {
10538
11413
  init_claude3();
@@ -10547,6 +11422,7 @@ var init_pane_classifier = __esm({
10547
11422
  ];
10548
11423
  RETRYING_RE = /\bRetrying\b|\battempt\s+\d+\s*\/\s*\d+/i;
10549
11424
  EXHAUSTED_RE = /\bretr(?:y|ies)\s+(?:exhausted|limit\s+(?:reached|exceeded))\b|\bmaximum\s+retries\b/i;
11425
+ QUOTED_PREFIX_RE2 = /^\s*(?:[┃│▏▕]|>|[+-]|\d+[\t:→])\s/;
10550
11426
  OPTION_RE2 = /^[❯>›]?\s*(\d+)\.\s+(.*\S)\s*$/;
10551
11427
  PICKER_FOOTER_RE2 = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
10552
11428
  PURE_CHROME_RE2 = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
@@ -10776,7 +11652,7 @@ var init_headless_launcher = __esm({
10776
11652
 
10777
11653
  // packages/agents/dist/claude/registry.js
10778
11654
  import fs15 from "fs";
10779
- import { join as join18 } from "path";
11655
+ import { join as join19 } from "path";
10780
11656
  import { homedir as homedir12 } from "os";
10781
11657
  function parseRegistryDir(files, readFile7) {
10782
11658
  const out = [];
@@ -10824,7 +11700,7 @@ function readClaudeStatus(task) {
10824
11700
  } catch {
10825
11701
  return void 0;
10826
11702
  }
10827
- const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join18(CLAUDE_SESSIONS_DIR, name), "utf8"));
11703
+ const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
10828
11704
  let entry;
10829
11705
  if (task.messagingSocketPath) {
10830
11706
  entry = entries.find((e) => e.messagingSocketPath === task.messagingSocketPath);
@@ -10843,7 +11719,7 @@ function readClaudeStatusByCwd(cwd) {
10843
11719
  } catch {
10844
11720
  return void 0;
10845
11721
  }
10846
- const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join18(CLAUDE_SESSIONS_DIR, name), "utf8"));
11722
+ const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
10847
11723
  const entry = entries.find((e) => e.cwd === cwd);
10848
11724
  if (!entry)
10849
11725
  return void 0;
@@ -10856,7 +11732,7 @@ function readClaudeStatusBySocketPath(socketPath2) {
10856
11732
  } catch {
10857
11733
  return void 0;
10858
11734
  }
10859
- const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join18(CLAUDE_SESSIONS_DIR, name), "utf8"));
11735
+ const entries = parseRegistryDir(files, (name) => fs15.readFileSync(join19(CLAUDE_SESSIONS_DIR, name), "utf8"));
10860
11736
  const entry = entries.find((e) => e.messagingSocketPath === socketPath2);
10861
11737
  if (!entry)
10862
11738
  return void 0;
@@ -10865,14 +11741,14 @@ function readClaudeStatusBySocketPath(socketPath2) {
10865
11741
  var CLAUDE_SESSIONS_DIR, PID_JSON;
10866
11742
  var init_registry8 = __esm({
10867
11743
  "packages/agents/dist/claude/registry.js"() {
10868
- CLAUDE_SESSIONS_DIR = join18(homedir12(), ".claude", "sessions");
11744
+ CLAUDE_SESSIONS_DIR = join19(homedir12(), ".claude", "sessions");
10869
11745
  PID_JSON = /^(\d+)\.json$/;
10870
11746
  }
10871
11747
  });
10872
11748
 
10873
11749
  // 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";
11750
+ import { readdirSync as readdirSync6, readFileSync as readFileSync14 } from "fs";
11751
+ import { join as join20 } from "path";
10876
11752
  function defaultIsAlive(pid) {
10877
11753
  try {
10878
11754
  process.kill(pid, 0);
@@ -10901,7 +11777,7 @@ var init_peer_registry_source = __esm({
10901
11777
  log;
10902
11778
  constructor(o = {}) {
10903
11779
  this.readdir = o.readdir ?? (() => readdirSync6(CLAUDE_SESSIONS_DIR));
10904
- this.readFile = o.readFile ?? ((n) => readFileSync13(join19(CLAUDE_SESSIONS_DIR, n), "utf8"));
11780
+ this.readFile = o.readFile ?? ((n) => readFileSync14(join20(CLAUDE_SESSIONS_DIR, n), "utf8"));
10905
11781
  this.isAlive = o.isAlive ?? defaultIsAlive;
10906
11782
  this.now = o.now ?? Date.now;
10907
11783
  this.pollMs = o.pollMs ?? 2e3;
@@ -11231,77 +12107,6 @@ var init_receipt_listener = __esm({
11231
12107
  }
11232
12108
  });
11233
12109
 
11234
- // packages/agents/dist/opencode/control-source.js
11235
- function toSnapshot2(ev) {
11236
- const now = Date.now();
11237
- switch (ev.type) {
11238
- // A permission was answered on the bus and the turn resumed.
11239
- case "task.started":
11240
- return { taskId: ev.id, state: "running", alive: true, origin: "agent", at: now };
11241
- // session.idle — the turn finished. Liveness, NOT completion (anti-#2576).
11242
- case "task.turn.completed":
11243
- return { taskId: ev.id, state: "idle", alive: true, origin: "agent", at: now };
11244
- // permission.asked — opencode STATES it is gated. No guessing from pixels.
11245
- case "task.approval.requested":
11246
- return {
11247
- taskId: ev.id,
11248
- state: "needsInput",
11249
- alive: true,
11250
- origin: "agent",
11251
- at: now,
11252
- detail: { note: ev.question, reason: ev.kind }
11253
- };
11254
- // Terminal (task.done/blocked/cancelled) and notify-only events are ignored:
11255
- // terminal state comes exclusively from `squadrant crew signal`.
11256
- default:
11257
- return null;
11258
- }
11259
- }
11260
- var OpencodeControlSource;
11261
- var init_control_source = __esm({
11262
- "packages/agents/dist/opencode/control-source.js"() {
11263
- OpencodeControlSource = class {
11264
- name = "opencode-control";
11265
- deps;
11266
- active = false;
11267
- cache = /* @__PURE__ */ new Map();
11268
- start(deps) {
11269
- this.deps = deps;
11270
- this.active = true;
11271
- }
11272
- stop() {
11273
- this.deps = void 0;
11274
- this.active = false;
11275
- this.cache.clear();
11276
- }
11277
- /** Push-only source — no fallible startup of its own. */
11278
- health() {
11279
- return { active: this.active, error: null };
11280
- }
11281
- /** Liveness floor: origin must be "scan" and must not assert needsInput. */
11282
- snapshot(taskId) {
11283
- const s = this.cache.get(taskId);
11284
- if (!s)
11285
- return void 0;
11286
- return { ...s, origin: "scan", state: s.state === "needsInput" ? "running" : s.state };
11287
- }
11288
- /**
11289
- * Feed one ControlEvent from OpencodeSseBridge into the port.
11290
- * Wired in squadrantd.ts as: emit = (ev) => { source.observe(ev); …existing… }
11291
- */
11292
- observe(ev) {
11293
- if (!this.deps)
11294
- return;
11295
- const snap = toSnapshot2(ev);
11296
- if (!snap)
11297
- return;
11298
- this.cache.set(snap.taskId, snap);
11299
- this.deps.report(snap);
11300
- }
11301
- };
11302
- }
11303
- });
11304
-
11305
12110
  // packages/agents/dist/opencode/http-channel.js
11306
12111
  var OpencodeHttpChannel;
11307
12112
  var init_http_channel = __esm({
@@ -11413,6 +12218,47 @@ var init_http_channel = __esm({
11413
12218
  }
11414
12219
  });
11415
12220
 
12221
+ // packages/agents/dist/opencode/fact-adapter.js
12222
+ function createOpencodeFactAdapter(deps) {
12223
+ return {
12224
+ name: "opencode-sse",
12225
+ origin: "agent",
12226
+ translate(raw) {
12227
+ const f = typeof raw === "object" && raw !== null ? raw : {};
12228
+ const type = typeof f.type === "string" ? f.type : void 0;
12229
+ if (type === void 0)
12230
+ return [{ kind: "unknown", name: "non-object" }];
12231
+ const p = f.properties ?? {};
12232
+ if (type === "session.idle") {
12233
+ return [{
12234
+ kind: "turn.ended",
12235
+ turnId: typeof p.sessionID === "string" ? p.sessionID : void 0
12236
+ }];
12237
+ }
12238
+ if (type === "permission.asked") {
12239
+ if (typeof p.id !== "string" || typeof p.sessionID !== "string") {
12240
+ return [{ kind: "unknown", name: "permission.asked:incomplete" }];
12241
+ }
12242
+ const tool = typeof p.permission === "string" ? p.permission : "a tool";
12243
+ const cmd = Array.isArray(p.patterns) && p.patterns.length ? `: ${p.patterns.join(" ")}` : "";
12244
+ return [{
12245
+ kind: "permission.requested",
12246
+ question: `opencode requests permission to run ${tool}${cmd}`,
12247
+ requestId: deps.nextRequestId(),
12248
+ tool
12249
+ }];
12250
+ }
12251
+ if (type === "permission.replied")
12252
+ return [{ kind: "activity" }];
12253
+ return [{ kind: "unknown", name: type }];
12254
+ }
12255
+ };
12256
+ }
12257
+ var init_fact_adapter = __esm({
12258
+ "packages/agents/dist/opencode/fact-adapter.js"() {
12259
+ }
12260
+ });
12261
+
11416
12262
  // packages/agents/dist/index.js
11417
12263
  var dist_exports4 = {};
11418
12264
  __export(dist_exports4, {
@@ -11427,7 +12273,6 @@ __export(dist_exports4, {
11427
12273
  HEADLESS_ERROR_TAIL: () => HEADLESS_ERROR_TAIL,
11428
12274
  MARKER_END: () => MARKER_END,
11429
12275
  MARKER_START: () => MARKER_START,
11430
- OpencodeControlSource: () => OpencodeControlSource,
11431
12276
  OpencodeHttpChannel: () => OpencodeHttpChannel,
11432
12277
  OpencodeSseBridge: () => OpencodeSseBridge,
11433
12278
  ProjectionRegistry: () => ProjectionRegistry,
@@ -11447,6 +12292,8 @@ __export(dist_exports4, {
11447
12292
  createGeminiEmitter: () => createGeminiEmitter,
11448
12293
  createOpencodeDriver: () => createOpencodeDriver,
11449
12294
  createOpencodeEmitter: () => createOpencodeEmitter,
12295
+ createOpencodeFactAdapter: () => createOpencodeFactAdapter,
12296
+ decideCaptainMemoryWrite: () => decideCaptainMemoryWrite,
11450
12297
  deriveTranscriptPath: () => deriveTranscriptPath,
11451
12298
  detectTrailingQuestion: () => detectTrailingQuestion2,
11452
12299
  formatAskUserQuestionPrompt: () => formatAskUserQuestionPrompt,
@@ -11488,8 +12335,8 @@ var init_dist4 = __esm({
11488
12335
  init_receipt_listener();
11489
12336
  init_peer_wire();
11490
12337
  init_registry8();
11491
- init_control_source();
11492
12338
  init_http_channel();
12339
+ init_fact_adapter();
11493
12340
  }
11494
12341
  });
11495
12342
 
@@ -11568,9 +12415,9 @@ async function runRuntimeSend(arg1, arg2, opts, confirmOpts) {
11568
12415
  const resolved = resolveTarget(registry, config, target, !!opts.command);
11569
12416
  await needRef(resolved);
11570
12417
  const finalProject = opts.command ? config.commandName : target;
11571
- const { join: join28, dirname: dirname9 } = await import("path");
12418
+ const { join: join30, dirname: dirname9 } = await import("path");
11572
12419
  const { DEFAULT_CONFIG_PATH: DEFAULT_CONFIG_PATH2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
11573
- const stateRoot = join28(dirname9(DEFAULT_CONFIG_PATH2), "state");
12420
+ const stateRoot = join30(dirname9(DEFAULT_CONFIG_PATH2), "state");
11574
12421
  const seq = await appendCaptainMessage2({
11575
12422
  stateRoot,
11576
12423
  project: finalProject,
@@ -11676,10 +12523,10 @@ var init_runtime2 = __esm({
11676
12523
  init_dist();
11677
12524
  init_dist2();
11678
12525
  import { Command as Command35 } from "commander";
11679
- import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
12526
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
11680
12527
  import { fileURLToPath as fileURLToPath6 } from "url";
11681
- import { dirname as dirname8, join as join27 } from "path";
11682
- import { homedir as homedir17 } from "os";
12528
+ import { dirname as dirname8, join as join29 } from "path";
12529
+ import { homedir as homedir18 } from "os";
11683
12530
 
11684
12531
  // packages/cli/src/commands/doctor.ts
11685
12532
  init_dist();
@@ -12085,8 +12932,8 @@ import chalk4 from "chalk";
12085
12932
 
12086
12933
  // packages/cli/src/lib/per-crew-settings.ts
12087
12934
  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";
12935
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
12936
+ import { dirname as dirname4, join as join21 } from "path";
12090
12937
  import { homedir as homedir13 } from "os";
12091
12938
  var CREW_PERMISSION_ALLOWLIST = [
12092
12939
  // git — read + safe mutations (reset/clean/config intentionally excluded)
@@ -12178,21 +13025,21 @@ function mergeCrewPermissions(settings) {
12178
13025
  return next;
12179
13026
  }
12180
13027
  function writePerCrewSettingsLocal(o) {
12181
- const dir = join20(o.projectCwd, ".claude");
13028
+ const dir = join21(o.projectCwd, ".claude");
12182
13029
  mkdirSync7(dir, { recursive: true });
12183
- const file = join20(dir, "settings.local.json");
13030
+ const file = join21(dir, "settings.local.json");
12184
13031
  let existing = {};
12185
13032
  try {
12186
- const raw = healStaleCockpitRefs(readFileSync14(file, "utf-8"));
13033
+ const raw = healStaleCockpitRefs(readFileSync15(file, "utf-8"));
12187
13034
  existing = JSON.parse(raw);
12188
13035
  } catch {
12189
13036
  }
12190
13037
  const withHooks = mergeClaudeHooks(existing, o.hookCmd ?? "squadrant crew _hook");
12191
13038
  const merged = mergeCrewPermissions(withHooks);
12192
- writeFileSync9(file, JSON.stringify(merged, null, 2));
13039
+ writeFileSync10(file, JSON.stringify(merged, null, 2));
12193
13040
  return file;
12194
13041
  }
12195
- var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join20(homedir13(), ".config", "opencode", "opencode.json");
13042
+ var DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH = join21(homedir13(), ".config", "opencode", "opencode.json");
12196
13043
  function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
12197
13044
  mkdirSync7(dirname4(configPath), { recursive: true });
12198
13045
  const defaultConfig = {
@@ -12200,7 +13047,7 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
12200
13047
  model: "anthropic/claude-sonnet-4-5"
12201
13048
  };
12202
13049
  try {
12203
- writeFileSync9(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
13050
+ writeFileSync10(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", { flag: "wx" });
12204
13051
  return configPath;
12205
13052
  } catch (err) {
12206
13053
  if (err.code === "EEXIST") return null;
@@ -12209,16 +13056,16 @@ function ensureGlobalOpencodeConfig(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_
12209
13056
  }
12210
13057
  function readGlobalOpencodeModel(configPath = DEFAULT_GLOBAL_OPENCODE_CONFIG_PATH) {
12211
13058
  try {
12212
- const parsed = JSON.parse(readFileSync14(configPath, "utf-8"));
13059
+ const parsed = JSON.parse(readFileSync15(configPath, "utf-8"));
12213
13060
  return typeof parsed.model === "string" ? parsed.model : void 0;
12214
13061
  } catch {
12215
13062
  return void 0;
12216
13063
  }
12217
13064
  }
12218
13065
  function writePerCrewOpencodeConfig(o) {
12219
- const dir = join20(o.stateRoot, o.project, o.taskId);
13066
+ const dir = join21(o.stateRoot, o.project, o.taskId);
12220
13067
  mkdirSync7(dir, { recursive: true });
12221
- const file = join20(dir, "opencode.json");
13068
+ const file = join21(dir, "opencode.json");
12222
13069
  const config = {
12223
13070
  permission: {
12224
13071
  read: "allow",
@@ -12233,7 +13080,7 @@ function writePerCrewOpencodeConfig(o) {
12233
13080
  external_directory: { "**": "allow" }
12234
13081
  }
12235
13082
  };
12236
- writeFileSync9(file, JSON.stringify(config, null, 2));
13083
+ writeFileSync10(file, JSON.stringify(config, null, 2));
12237
13084
  return file;
12238
13085
  }
12239
13086
 
@@ -12721,8 +13568,8 @@ import { createConnection as createConnection3 } from "net";
12721
13568
  import { randomUUID as randomUUID5 } from "crypto";
12722
13569
  import { execFileSync as execFileSync7 } from "child_process";
12723
13570
  import { homedir as homedir14 } from "os";
12724
- import { join as join21 } from "path";
12725
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
13571
+ import { join as join22 } from "path";
13572
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
12726
13573
 
12727
13574
  // packages/cli/src/commands/crew-output.ts
12728
13575
  function tailLines(text, maxLines = 40, maxBytes = 4096) {
@@ -13180,10 +14027,10 @@ function buildSignalRequest(signal, o) {
13180
14027
  return { kind: "event", project, event };
13181
14028
  }
13182
14029
  function defaultWriteResult(id, payload) {
13183
- const dir = join21(homedir14(), ".config", "squadrant", "state", "_results");
14030
+ const dir = join22(homedir14(), ".config", "squadrant", "state", "_results");
13184
14031
  mkdirSync8(dir, { recursive: true });
13185
- const file = join21(dir, `${id}.txt`);
13186
- writeFileSync10(file, payload);
14032
+ const file = join22(dir, `${id}.txt`);
14033
+ writeFileSync11(file, payload);
13187
14034
  return file;
13188
14035
  }
13189
14036
  async function runCrewSignal(signal, o, deps) {
@@ -13196,7 +14043,7 @@ async function runCrewSignal(signal, o, deps) {
13196
14043
  const current = await deps.call(buildStatusRequest(project, taskId));
13197
14044
  if (current && TERMINAL_STATES.has(current.state)) {
13198
14045
  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.`
14046
+ `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
14047
  );
13201
14048
  }
13202
14049
  const req = buildSignalRequest(signal, { ...o, writeResult: o.writeResult ?? defaultWriteResult });
@@ -13259,6 +14106,20 @@ async function runCrewApprove(project, crew, deps) {
13259
14106
  });
13260
14107
  return prUrl;
13261
14108
  }
14109
+ async function runCrewReply(project, id, message, deps) {
14110
+ const tasks = await deps.listTasks(project);
14111
+ const matches = tasks.filter((t) => t.id === id || t.id.startsWith(id));
14112
+ if (matches.length === 0) throw new Error(`unknown task ${id}`);
14113
+ if (matches.length > 1) {
14114
+ throw new Error(`task id '${id}' is ambiguous \u2014 matches ${matches.map((t) => t.id).join(", ")}`);
14115
+ }
14116
+ const target = matches[0];
14117
+ if (!target.name) {
14118
+ throw new Error(`task ${id} has no crew name on record \u2014 cannot deliver via 'crew reply'`);
14119
+ }
14120
+ await deps.sendCrew(project, target.name, message);
14121
+ return deps.getStatus(project, target.id);
14122
+ }
13262
14123
  function addControlPlaneCrewCommands(crew) {
13263
14124
  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
14125
  const req = buildDispatchRequest({ project, task, provider: opts.provider, mode: opts.mode, cwd: opts.cwd });
@@ -13295,14 +14156,18 @@ function addControlPlaneCrewCommands(crew) {
13295
14156
  const compact = opts.json !== true;
13296
14157
  process.stdout.write(formatCompactTasks(records, { compact }) + "\n");
13297
14158
  });
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) => {
14159
+ 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) => {
14160
+ const resolvedMessage = await resolveTextInput({ positional: message, filePath: opts.messageFile, label: "message" });
13299
14161
  if (opts.gate) {
13300
- const r2 = await squadrantdCall(buildGateResolveRequest({ project, gateId: opts.gate, message }));
14162
+ const r2 = await squadrantdCall(buildGateResolveRequest({ project, gateId: opts.gate, message: resolvedMessage }));
13301
14163
  process.stdout.write(JSON.stringify(r2) + "\n");
13302
14164
  return;
13303
14165
  }
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 });
14166
+ const r = await runCrewReply(project, id, resolvedMessage, {
14167
+ listTasks: async (p) => await squadrantdCall({ kind: "list", project: p }),
14168
+ sendCrew: (p, name, msg) => runCrewSend2(p, name, msg),
14169
+ getStatus: (p, taskId) => squadrantdCall(buildStatusRequest(p, taskId))
14170
+ });
13306
14171
  process.stdout.write(JSON.stringify(r) + "\n");
13307
14172
  });
13308
14173
  crew.command("_hook <event>", { hidden: true }).description("internal: bridge from claude Stop/SubagentStop/SessionEnd hooks to squadrantd").action(async (event) => {
@@ -13470,6 +14335,10 @@ async function runCrewSpawn2(input) {
13470
14335
  emitEvent: async (p, event) => {
13471
14336
  await squadrantdCall({ kind: "event", project: p, event });
13472
14337
  },
14338
+ // #745: check the daemon's hook-confirmed state before reporting a false
14339
+ // "first turn not delivered" — swallow errors (offline/unreachable daemon)
14340
+ // so this optional check never itself breaks the spawn.
14341
+ getTaskRecord: async (p, id) => await squadrantdCall(buildStatusRequest(p, id)).catch(() => void 0),
13473
14342
  onRouted: (route) => console.log(
13474
14343
  chalk10.dim(
13475
14344
  `routed: tier=${route.tier} \u2192 ${route.agent}${route.model ? `/${route.model}` : ""} (rule: "${route.matchedRule}")`
@@ -13556,14 +14425,30 @@ async function runCrewList2(project) {
13556
14425
  const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
13557
14426
  return runCrewList(project, runtime, workspaceId);
13558
14427
  }
14428
+ async function runCrewAnswer2(project, name, option, opts) {
14429
+ const { runtime, workspaceId } = await resolveCaptainWorkspace(project);
14430
+ return runCrewAnswer(
14431
+ project,
14432
+ name,
14433
+ option,
14434
+ runtime,
14435
+ workspaceId,
14436
+ {
14437
+ readModalOptions: (pane) => readModalOptions(runtime, pane),
14438
+ log: (m) => console.log(chalk10.dim(m))
14439
+ },
14440
+ opts
14441
+ );
14442
+ }
13559
14443
  var crewCommand = new Command10("crew").description(
13560
14444
  "Spawn and manage interactive crew sessions next to the project's captain"
13561
14445
  );
13562
14446
  crewCommand.command("spawn").description(
13563
14447
  "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(
14448
+ ).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
14449
  async (project, task, opts, cmd) => {
13566
14450
  try {
14451
+ const thinking = opts.thinking ? parseThinkingLevel(opts.thinking) : void 0;
13567
14452
  const resolvedTask = await resolveTextInput({ positional: task, filePath: opts.taskFile, label: "task" });
13568
14453
  const agentExplicit = cmd.getOptionValueSource("agent") === "cli";
13569
14454
  const pane = await runCrewSpawn2({
@@ -13578,6 +14463,7 @@ crewCommand.command("spawn").description(
13578
14463
  ...opts.approval ? { approvalPolicy: "untrusted", approval: true } : {},
13579
14464
  ...opts.shared ? { shared: true } : {},
13580
14465
  ...opts.model ? { model: opts.model } : {},
14466
+ ...thinking ? { thinking } : {},
13581
14467
  // #458: pass the raw file path (not stdin) so runCrewSpawn can copy it
13582
14468
  // into the isolated worktree root for relative-path access.
13583
14469
  ...opts.taskFile && opts.taskFile !== "-" ? { taskFile: opts.taskFile } : {}
@@ -13631,13 +14517,29 @@ crewCommand.command("list").description("List live crew sessions for a project")
13631
14517
  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
14518
  try {
13633
14519
  const resolvedMessage = await resolveTextInput({ positional: message, filePath: opts.messageFile, label: "message" });
13634
- await runCrewSend2(project, name, resolvedMessage, opts);
14520
+ const { reopened } = await runCrewSend2(project, name, resolvedMessage, opts);
14521
+ if (reopened) console.log(chalk10.cyan(`\u21BB Task was terminal \u2014 reopened to working`));
13635
14522
  console.log(chalk10.green(`\u2714 Sent to ${project}:${name}`));
13636
14523
  } catch (e) {
13637
14524
  console.error(chalk10.red(e.message));
13638
14525
  process.exit(1);
13639
14526
  }
13640
14527
  });
14528
+ crewCommand.command("answer").description(
14529
+ "Deliberately answer a crew's open AskUserQuestion/permission prompt (#592) \u2014 never an implicit default"
14530
+ ).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) => {
14531
+ try {
14532
+ const { selected, closed } = await runCrewAnswer2(project, name, option, opts);
14533
+ if (closed) {
14534
+ console.log(chalk10.green(`\u2714 Answered ${project}:${name} with ${selected.index}. "${selected.label}" \u2014 prompt closed`));
14535
+ } else {
14536
+ 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}'`));
14537
+ }
14538
+ } catch (err) {
14539
+ console.error(chalk10.red(err.message));
14540
+ process.exit(1);
14541
+ }
14542
+ });
13641
14543
  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
14544
  try {
13643
14545
  const screen = await runCrewRead2(project, name);
@@ -14214,9 +15116,9 @@ function mergeSnapshot(daemon, external, now) {
14214
15116
  // packages/web/dist/probes.js
14215
15117
  init_dist();
14216
15118
  init_dist();
14217
- import { join as join22 } from "path";
15119
+ import { join as join23 } from "path";
14218
15120
  import { homedir as homedir15 } from "os";
14219
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
15121
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
14220
15122
  import { execFile as execFile4 } from "child_process";
14221
15123
  var DEFAULT_TIMEOUT_MS2 = 2e3;
14222
15124
  var AGENT_CLIS = ["claude", "codex", "gemini", "opencode"];
@@ -14252,7 +15154,7 @@ function vaultProbe(run, dir) {
14252
15154
  return { state: "unknown", detail: "no vault configured" };
14253
15155
  if (!run.pathExists(dir))
14254
15156
  return { state: "gone", detail: "vault directory missing" };
14255
- if (!run.pathExists(join22(dir, ".obsidian")))
15157
+ if (!run.pathExists(join23(dir, ".obsidian")))
14256
15158
  return { state: "gone", detail: "no .obsidian/ (not a vault)" };
14257
15159
  return { state: "alive" };
14258
15160
  } catch {
@@ -14320,13 +15222,13 @@ async function runExternalProbes(run, timeoutMs = DEFAULT_TIMEOUT_MS2) {
14320
15222
  const sessions = probeSessions(run);
14321
15223
  return { cmux: cmux2, agentClis, vaults, config: { parseable, projectPaths, sessions } };
14322
15224
  }
14323
- var SESSIONS_PATH = join22(homedir15(), ".config", "squadrant", "sessions.json");
15225
+ var SESSIONS_PATH = join23(homedir15(), ".config", "squadrant", "sessions.json");
14324
15226
  function onPath(cli) {
14325
15227
  const dirs = (process.env.PATH ?? "").split(":").filter(Boolean);
14326
- return dirs.some((d) => existsSync12(join22(d, cli)));
15228
+ return dirs.some((d) => existsSync13(join23(d, cli)));
14327
15229
  }
14328
15230
  function readSessionsHashes() {
14329
- const raw = JSON.parse(readFileSync15(SESSIONS_PATH, "utf-8"));
15231
+ const raw = JSON.parse(readFileSync16(SESSIONS_PATH, "utf-8"));
14330
15232
  const hashes = Object.values(raw.workspaces ?? {}).map((w) => w.templateHash).filter((h) => typeof h === "string" && h.length > 0);
14331
15233
  return [...new Set(hashes)];
14332
15234
  }
@@ -14340,7 +15242,7 @@ function defaultProbeRunners() {
14340
15242
  }
14341
15243
  }),
14342
15244
  probeOnPath: async (cli) => onPath(cli),
14343
- pathExists: (p) => existsSync12(p),
15245
+ pathExists: (p) => existsSync13(p),
14344
15246
  loadConfig: () => loadConfig(),
14345
15247
  loadSessionsHashes: () => readSessionsHashes()
14346
15248
  };
@@ -14415,12 +15317,12 @@ function donut(t) {
14415
15317
  const C = 2 * Math.PI * r;
14416
15318
  let acc = 0;
14417
15319
  const segs = DONUT_ORDER.map((k) => {
14418
- const v = t[k];
14419
- if (v <= 0)
15320
+ const v2 = t[k];
15321
+ if (v2 <= 0)
14420
15322
  return "";
14421
- const len = t.total ? v / t.total * C : 0;
15323
+ const len = t.total ? v2 / t.total * C : 0;
14422
15324
  const rot = t.total ? acc / t.total * 360 : 0;
14423
- acc += v;
15325
+ acc += v2;
14424
15326
  return `<circle class="seg s-${k}" cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke-width="${w}" stroke-dasharray="${len.toFixed(2)} ${(C - len).toFixed(2)}" transform="rotate(${(rot - 90).toFixed(2)} ${cx} ${cy})"></circle>`;
14425
15327
  }).join("");
14426
15328
  const track = `<circle class="donut-track" cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke-width="${w}"></circle>`;
@@ -14724,7 +15626,7 @@ function renderLiveGrid(snap, now) {
14724
15626
  const headerLabels = STATE_ORDER.map((s) => `${STATE_LABEL[s]}`);
14725
15627
  out.push(`<div class="live-header" data-live-header="">`);
14726
15628
  out.push(headerLabels.map((l) => {
14727
- const raw = Object.entries(STATE_LABEL).find(([, v]) => v === l)[0];
15629
+ const raw = Object.entries(STATE_LABEL).find(([, v2]) => v2 === l)[0];
14728
15630
  const c = stateCounts[raw];
14729
15631
  const cls = c === 0 ? "zero" : "";
14730
15632
  return `<span class="live-stat ${cls}"><span class="pdot ${STATE_CLS[raw]}"></span>${c} ${l}</span>`;
@@ -15276,7 +16178,7 @@ async function runDashboardWeb(input) {
15276
16178
  console.log(chalk14.green(`\u2714 Squadrant system dashboard \u2192 http://127.0.0.1:${handle.port}`));
15277
16179
  console.log(chalk14.dim(` polling the daemon every ${input.interval}s \xB7 localhost only \xB7 read-only \xB7 Ctrl-C to stop`));
15278
16180
  }
15279
- var dashboardCommand = new Command13("dashboard").description("Live status grid of all projects (derived from daemon task state)").option("--once", "Print one snapshot and exit (used by --pane's refresh loop)").option("--pane", "Open a refreshing sidebar pane in the current cmux workspace").option("--web", "Serve the live system-health web dashboard on 127.0.0.1 (HTTP + SSE)").option("--port <port>", "Port for --web (default 7878)", (v) => parseInt(v, 10), 7878).option("--direction <dir>", "Pane split direction (right|left|up|down)", "right").option("--interval <seconds>", "Daemon poll interval for --web (default 5); refresh interval for --pane (default 10)", (v) => parseInt(v, 10)).action(async (opts) => {
16181
+ var dashboardCommand = new Command13("dashboard").description("Live status grid of all projects (derived from daemon task state)").option("--once", "Print one snapshot and exit (used by --pane's refresh loop)").option("--pane", "Open a refreshing sidebar pane in the current cmux workspace").option("--web", "Serve the live system-health web dashboard on 127.0.0.1 (HTTP + SSE)").option("--port <port>", "Port for --web (default 7878)", (v2) => parseInt(v2, 10), 7878).option("--direction <dir>", "Pane split direction (right|left|up|down)", "right").option("--interval <seconds>", "Daemon poll interval for --web (default 5); refresh interval for --pane (default 10)", (v2) => parseInt(v2, 10)).action(async (opts) => {
15280
16182
  try {
15281
16183
  if (opts.web) {
15282
16184
  await runDashboardWeb({ port: opts.port, interval: opts.interval ?? 5 });
@@ -15398,7 +16300,9 @@ async function selectCaptainsInteractive(entries, yesterday = getYesterday()) {
15398
16300
  function resolveLaunchAgent(overrides, roleConfig, roleModelDefault) {
15399
16301
  return {
15400
16302
  agentName: overrides.agent ?? roleConfig?.agent ?? "claude",
15401
- model: overrides.model ?? roleConfig?.model ?? roleModelDefault
16303
+ model: overrides.model ?? roleConfig?.model ?? roleModelDefault,
16304
+ // No built-in default: unset ⇒ flag omitted ⇒ the agent's own effort.
16305
+ thinking: overrides.thinking ?? roleConfig?.thinking
15402
16306
  };
15403
16307
  }
15404
16308
 
@@ -15435,11 +16339,20 @@ function ensureCmuxReady(headless) {
15435
16339
  }
15436
16340
  var launchCommand = new Command14("launch").description(
15437
16341
  "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) => {
16342
+ ).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
16343
  if (opts.fresh && opts.keep) {
15440
16344
  console.error(chalk15.red("\n \u2718 --fresh and --keep are mutually exclusive\n"));
15441
16345
  process.exit(1);
15442
16346
  }
16347
+ let thinkingOverride;
16348
+ try {
16349
+ thinkingOverride = opts.thinking ? parseThinkingLevel(opts.thinking) : void 0;
16350
+ } catch (err) {
16351
+ console.error(chalk15.red(`
16352
+ \u2718 ${err.message}
16353
+ `));
16354
+ process.exit(1);
16355
+ }
15443
16356
  const config = loadConfig();
15444
16357
  let hadFailure = false;
15445
16358
  const drivers = {
@@ -15452,8 +16365,8 @@ var launchCommand = new Command14("launch").description(
15452
16365
  const runtimes = new RuntimeRegistry({ cmux: createCmuxDriver() });
15453
16366
  async function launchOne(workspaceName, role, cwd, permissionMode, navigate, pinToTop = false, projectName) {
15454
16367
  const roleConfig = config.defaults.roles?.[role];
15455
- const { agentName, model } = resolveLaunchAgent(
15456
- { agent: opts.agent, model: opts.model },
16368
+ const { agentName, model, thinking } = resolveLaunchAgent(
16369
+ { agent: opts.agent, model: opts.model, thinking: thinkingOverride },
15457
16370
  roleConfig,
15458
16371
  config.defaults.models?.[role]
15459
16372
  );
@@ -15496,7 +16409,8 @@ var launchCommand = new Command14("launch").description(
15496
16409
  model,
15497
16410
  TEMPLATES_DIR4,
15498
16411
  resolveCaptainSocketPath(captainChannelEnabled, projectName, workspaceName),
15499
- resolveCaptainSessionName(agentName, projectName)
16412
+ resolveCaptainSessionName(agentName, projectName),
16413
+ thinking
15500
16414
  );
15501
16415
  },
15502
16416
  initialPrompt,
@@ -16195,11 +17109,11 @@ import chalk22 from "chalk";
16195
17109
  import fs23 from "fs";
16196
17110
  import path28 from "path";
16197
17111
  import { fileURLToPath as fileURLToPath4 } from "url";
16198
- function parseScope(v) {
16199
- if (v !== "user" && v !== "project") {
17112
+ function parseScope(v2) {
17113
+ if (v2 !== "user" && v2 !== "project") {
16200
17114
  throw new Error("--scope must be 'user' or 'project'");
16201
17115
  }
16202
- return v;
17116
+ return v2;
16203
17117
  }
16204
17118
  function findPackageRoot3() {
16205
17119
  let dir = path28.dirname(fileURLToPath4(import.meta.url));
@@ -16403,7 +17317,7 @@ init_dist2();
16403
17317
  import { Command as Command23 } from "commander";
16404
17318
  import fs24 from "fs";
16405
17319
  import { fileURLToPath as fileURLToPath5 } from "url";
16406
- import { dirname as dirname5, join as join23 } from "path";
17320
+ import { dirname as dirname5, join as join24 } from "path";
16407
17321
  import chalk23 from "chalk";
16408
17322
  function runConfigCheck(opts) {
16409
17323
  const raw = JSON.parse(fs24.readFileSync(opts.configPath, "utf-8"));
@@ -16539,14 +17453,14 @@ configCommand.command("set").description("Write a config value by dotted key (e.
16539
17453
  }
16540
17454
  });
16541
17455
  function readPkgVersion2() {
16542
- const pkgPath = join23(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
17456
+ const pkgPath = join24(dirname5(fileURLToPath5(import.meta.url)), "..", "package.json");
16543
17457
  return JSON.parse(fs24.readFileSync(pkgPath, "utf-8")).version;
16544
17458
  }
16545
17459
 
16546
17460
  // packages/cli/src/commands/heal.ts
16547
17461
  init_dist();
16548
17462
  import { execFileSync as execFileSync9 } from "child_process";
16549
- import { join as join24 } from "path";
17463
+ import { join as join25 } from "path";
16550
17464
  import { homedir as homedir16 } from "os";
16551
17465
  import { Command as Command24 } from "commander";
16552
17466
  import chalk24 from "chalk";
@@ -16562,9 +17476,16 @@ function buildHealStatus(components) {
16562
17476
  project: c.project,
16563
17477
  ref: c.ref,
16564
17478
  state: c.state,
17479
+ detail: c.detail,
17480
+ stuck: c.stuck,
16565
17481
  healCmd: healCmdFor(c)
16566
17482
  }));
16567
- const healthy = out.every((c) => c.healCmd === null);
17483
+ const healthy = out.every((c) => {
17484
+ if (c.kind === "delivery") {
17485
+ return c.stuck !== true;
17486
+ }
17487
+ return c.healCmd === null;
17488
+ });
16568
17489
  return { healthy, components: out };
16569
17490
  }
16570
17491
  async function runHealStatus(opts) {
@@ -16601,15 +17522,37 @@ async function runHealStatus(opts) {
16601
17522
  }
16602
17523
  if (result.healthy) {
16603
17524
  stdout.write(chalk24.green("\u2714 all components healthy\n"));
17525
+ for (const c of result.components) {
17526
+ if (c.kind === "delivery" && c.state === "stale") {
17527
+ const glyph = chalk24.yellow("\u2022");
17528
+ const stateColor2 = chalk24.yellow;
17529
+ stdout.write(` ${glyph} ${c.kind.padEnd(8)} ${c.ref.padEnd(16)} ${stateColor2(c.state.padEnd(8))} ${c.project}
17530
+ `);
17531
+ if (c.detail) {
17532
+ stdout.write(` detail: ${c.detail}
17533
+ `);
17534
+ }
17535
+ }
17536
+ }
16604
17537
  return 0;
16605
17538
  }
16606
17539
  stdout.write(chalk24.bold("Unhealthy components:\n\n"));
16607
17540
  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}
17541
+ const isUnhealthy = c.kind === "delivery" ? c.stuck === true : c.healCmd !== null;
17542
+ const isAdvisoryDelivery = c.kind === "delivery" && c.state === "stale" && c.stuck !== true;
17543
+ if (isUnhealthy || isAdvisoryDelivery) {
17544
+ const glyph = c.state === "stale" || isAdvisoryDelivery ? chalk24.yellow("\u2022") : chalk24.red("\u2718");
17545
+ const stateColor2 = c.state === "stale" || isAdvisoryDelivery ? chalk24.yellow : chalk24.red;
17546
+ stdout.write(` ${glyph} ${c.kind.padEnd(8)} ${c.ref.padEnd(16)} ${stateColor2(c.state.padEnd(8))} ${c.project}
16610
17547
  `);
16611
- stdout.write(` heal: ${chalk24.cyan(c.healCmd)}
17548
+ if (c.detail) {
17549
+ stdout.write(` detail: ${c.detail}
16612
17550
  `);
17551
+ }
17552
+ if (c.healCmd) {
17553
+ stdout.write(` heal: ${chalk24.cyan(c.healCmd)}
17554
+ `);
17555
+ }
16613
17556
  }
16614
17557
  }
16615
17558
  return 2;
@@ -16618,8 +17561,10 @@ async function runHealDaemon(opts) {
16618
17561
  const { stdout, stderr } = opts;
16619
17562
  stdout.write("restarting squadrantd via launchd kickstart...\n");
16620
17563
  try {
16621
- opts.ensureDaemon();
16622
- stdout.write(chalk24.green("\u2714 daemon kickstart complete\n"));
17564
+ const result = opts.ensureDaemon();
17565
+ const noteSuffix = result?.note ? ` (${result.note})` : "";
17566
+ stdout.write(chalk24.green(`\u2714 daemon kickstart complete${noteSuffix}
17567
+ `));
16623
17568
  return 0;
16624
17569
  } catch (e) {
16625
17570
  stderr.write(`heal daemon failed: ${e.message}
@@ -16755,8 +17700,8 @@ var healCommand = new Command24("heal").description("Targeted, idempotent remedi
16755
17700
  }
16756
17701
  const config = loadConfig();
16757
17702
  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") });
17703
+ const stateRoot = join25(homedir16(), ".config", "squadrant", "state");
17704
+ const registry = new LivenessRegistry({ path: join25(stateRoot, "liveness.json") });
16760
17705
  registry.load();
16761
17706
  const code = await runHealCaptain(projects, {
16762
17707
  liveness: readCmuxLiveness,
@@ -16822,12 +17767,12 @@ async function dispatchAction(toProject, task, opts) {
16822
17767
  process.exit(1);
16823
17768
  }
16824
17769
  }
16825
- var dispatchCommand = new Command25("dispatch").description("Dispatch a task to any registered project (tracked, reports back on settle)").argument("<project>", "Target project name").argument("<task>", "Task description to dispatch").option("--provider <p>", "claude|opencode|codex", "claude").option("--mode <m>", "headless|interactive", "headless").option("--warmup-timeout <s>", "seconds to wait for target captain to boot (same-group only; default: 120)", (v) => parseInt(v, 10) * 1e3).action(dispatchAction);
17770
+ var dispatchCommand = new Command25("dispatch").description("Dispatch a task to any registered project (tracked, reports back on settle)").argument("<project>", "Target project name").argument("<task>", "Task description to dispatch").option("--provider <p>", "claude|opencode|codex", "claude").option("--mode <m>", "headless|interactive", "headless").option("--warmup-timeout <s>", "seconds to wait for target captain to boot (same-group only; default: 120)", (v2) => parseInt(v2, 10) * 1e3).action(dispatchAction);
16826
17771
 
16827
17772
  // packages/cli/src/commands/group.ts
16828
17773
  init_dist2();
16829
17774
  var groupCommand = new Command26("group").description("Cross-project intra-group operations (Phase 1: dispatch)").addCommand(
16830
- new Command26("dispatch").description("[DEPRECATED \u2014 use 'squadrant dispatch'] Dispatch a task to a sibling project in the same group").argument("<to-project>", "Target project name").argument("<task>", "Task description to dispatch").option("--provider <p>", "claude|opencode|codex", "claude").option("--mode <m>", "headless|interactive", "headless").option("--warmup-timeout <s>", "seconds to wait for target captain to boot (default: 120)", (v) => parseInt(v, 10) * 1e3).action(async (toProject, task, opts) => {
17775
+ new Command26("dispatch").description("[DEPRECATED \u2014 use 'squadrant dispatch'] Dispatch a task to a sibling project in the same group").argument("<to-project>", "Target project name").argument("<task>", "Task description to dispatch").option("--provider <p>", "claude|opencode|codex", "claude").option("--mode <m>", "headless|interactive", "headless").option("--warmup-timeout <s>", "seconds to wait for target captain to boot (default: 120)", (v2) => parseInt(v2, 10) * 1e3).action(async (toProject, task, opts) => {
16831
17776
  console.error(chalk26.yellow(
16832
17777
  `\u26A0 'squadrant group dispatch' is deprecated \u2014 use 'squadrant dispatch <project> "<task>"' instead.`
16833
17778
  ));
@@ -16840,7 +17785,7 @@ init_dist();
16840
17785
  init_dist2();
16841
17786
  init_runtime2();
16842
17787
  init_require_daemon();
16843
- import { join as join25, dirname as dirname6 } from "path";
17788
+ import { join as join27, dirname as dirname6 } from "path";
16844
17789
  import { Command as Command27 } from "commander";
16845
17790
  import chalk28 from "chalk";
16846
17791
 
@@ -16848,26 +17793,55 @@ import chalk28 from "chalk";
16848
17793
  init_dist4();
16849
17794
  init_dist2();
16850
17795
  import { createServer as createServer5, connect as netConnect2 } from "net";
16851
- import { unlinkSync as unlinkSync3 } from "fs";
17796
+ import fs25 from "fs";
17797
+ import { join as join26 } from "path";
16852
17798
  import { randomUUID as randomUUID7 } from "crypto";
16853
17799
  import chalk27 from "chalk";
16854
17800
  var shared;
17801
+ var registryEntryPath = () => join26(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
17802
+ function unregisterSenderIdentity() {
17803
+ try {
17804
+ fs25.unlinkSync(registryEntryPath());
17805
+ } catch {
17806
+ }
17807
+ }
17808
+ function registerSenderIdentity(socketPath2) {
17809
+ try {
17810
+ unregisterSenderIdentity();
17811
+ fs25.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
17812
+ fs25.writeFileSync(
17813
+ registryEntryPath(),
17814
+ JSON.stringify({
17815
+ pid: process.pid,
17816
+ sessionId: randomUUID7(),
17817
+ name: "squadrantd",
17818
+ messagingSocketPath: socketPath2,
17819
+ kind: "daemon",
17820
+ peerProtocol: 1
17821
+ })
17822
+ );
17823
+ process.on("exit", unregisterSenderIdentity);
17824
+ } catch {
17825
+ }
17826
+ }
16855
17827
  async function sharedReceiptListener() {
16856
17828
  if (shared) return shared;
17829
+ const socketPath2 = `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`;
16857
17830
  const listener = new ClaudeReceiptListener({
16858
- socketPath: `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`,
17831
+ socketPath: socketPath2,
16859
17832
  createServer: (h) => createServer5(h),
16860
17833
  // A UDS path is not cleaned up when a process is killed, so our own leftover
16861
17834
  // must never be the reason we refuse to start.
16862
17835
  unlinkStale: (p) => {
16863
17836
  try {
16864
- unlinkSync3(p);
17837
+ fs25.unlinkSync(p);
16865
17838
  } catch {
16866
17839
  }
16867
17840
  },
16868
17841
  log: (m) => console.error(chalk27.dim(m))
16869
17842
  });
16870
17843
  await listener.start();
17844
+ registerSenderIdentity(socketPath2);
16871
17845
  shared = listener;
16872
17846
  return shared;
16873
17847
  }
@@ -16922,7 +17896,7 @@ async function runPing(project, message) {
16922
17896
  log: (m) => console.error(chalk28.dim(m))
16923
17897
  });
16924
17898
  if (!handled) {
16925
- const stateRoot = join25(dirname6(DEFAULT_CONFIG_PATH), "state");
17899
+ const stateRoot = join27(dirname6(DEFAULT_CONFIG_PATH), "state");
16926
17900
  await appendCaptainMessage({
16927
17901
  stateRoot,
16928
17902
  project,
@@ -17001,7 +17975,7 @@ var cmuxCommand = new Command28("cmux").description("cmux integration helpers").
17001
17975
  // packages/cli/src/commands/effort.ts
17002
17976
  init_dist();
17003
17977
  init_dist2();
17004
- import fs25 from "fs";
17978
+ import fs26 from "fs";
17005
17979
  import path29 from "path";
17006
17980
  import { Command as Command29 } from "commander";
17007
17981
  import chalk30 from "chalk";
@@ -17045,7 +18019,7 @@ function effortScopeLabel(projectName) {
17045
18019
  }
17046
18020
  function canonical(p) {
17047
18021
  try {
17048
- return fs25.realpathSync(p);
18022
+ return fs26.realpathSync(p);
17049
18023
  } catch {
17050
18024
  return path29.resolve(p);
17051
18025
  }
@@ -17107,7 +18081,7 @@ var effortCommand = new Command29("effort").description("Get or set the crew tok
17107
18081
 
17108
18082
  // packages/cli/src/commands/tokens.ts
17109
18083
  init_dist();
17110
- import fs26 from "fs";
18084
+ import fs27 from "fs";
17111
18085
  import path30 from "path";
17112
18086
  import os16 from "os";
17113
18087
  import readline3 from "readline";
@@ -17163,7 +18137,7 @@ function foldTranscriptLine(agg, rawLine, state) {
17163
18137
  async function aggregateTranscriptFile(filePath) {
17164
18138
  const agg = emptySessionAggregate();
17165
18139
  const state = { lastCacheRead: null };
17166
- const rl = readline3.createInterface({ input: fs26.createReadStream(filePath), crlfDelay: Infinity });
18140
+ const rl = readline3.createInterface({ input: fs27.createReadStream(filePath), crlfDelay: Infinity });
17167
18141
  for await (const line of rl) {
17168
18142
  foldTranscriptLine(agg, line, state);
17169
18143
  }
@@ -17231,7 +18205,7 @@ function buildRoleReport(role, sessions) {
17231
18205
  }
17232
18206
  async function readdirSafe(dir) {
17233
18207
  try {
17234
- return await fs26.promises.readdir(dir);
18208
+ return await fs27.promises.readdir(dir);
17235
18209
  } catch {
17236
18210
  return [];
17237
18211
  }
@@ -17398,12 +18372,12 @@ var tokensCommand = new Command30("tokens").description(
17398
18372
  // packages/cli/src/commands/telegram.ts
17399
18373
  init_dist();
17400
18374
  init_dist2();
17401
- import { join as join26, dirname as dirname7 } from "path";
18375
+ import { join as join28, dirname as dirname7 } from "path";
17402
18376
  import { emitKeypressEvents } from "readline";
17403
18377
  import { Command as Command31 } from "commander";
17404
18378
  import chalk32 from "chalk";
17405
18379
  function defaultStateRoot() {
17406
- return join26(dirname7(DEFAULT_CONFIG_PATH), "state");
18380
+ return join28(dirname7(DEFAULT_CONFIG_PATH), "state");
17407
18381
  }
17408
18382
  async function questionMasked() {
17409
18383
  return new Promise((resolve4) => {
@@ -17474,7 +18448,7 @@ telegramCommand.command("link").argument("<project>", "project to bind to a Tele
17474
18448
  const { topicId, created } = await runTelegramLink({ project, cfg, client, stateRoot: defaultStateRoot() });
17475
18449
  console.log(chalk32.green(`${created ? "linked" : "already linked"}: ${project} \u2192 topic ${topicId}`));
17476
18450
  });
17477
- telegramCommand.command("setup").description("Interactive wizard \u2014 bot token, validate, auto-detect supergroup, write config").option("--reset-token", "force re-entry of the bot token even if one already exists").option("--redetect", "force group re-detection even when a supergroup is already configured").option("--user-id <id>", "allowlist user-id \u2014 enables remote control on a re-run without getUpdates detection", (v) => parseInt(v, 10)).action(async (opts) => {
18451
+ telegramCommand.command("setup").description("Interactive wizard \u2014 bot token, validate, auto-detect supergroup, write config").option("--reset-token", "force re-entry of the bot token even if one already exists").option("--redetect", "force group re-detection even when a supergroup is already configured").option("--user-id <id>", "allowlist user-id \u2014 enables remote control on a re-run without getUpdates detection", (v2) => parseInt(v2, 10)).action(async (opts) => {
17478
18452
  if (!process.stdin.isTTY) {
17479
18453
  console.error(chalk32.red("setup requires a TTY \u2014 pipe input is not supported"));
17480
18454
  process.exit(1);
@@ -17729,9 +18703,10 @@ init_dist2();
17729
18703
  init_dist4();
17730
18704
  init_dist();
17731
18705
  import { Command as Command32 } from "commander";
18706
+ import { homedir as homedir17 } from "os";
17732
18707
 
17733
18708
  // packages/cli/src/lib/captain-session-registry.ts
17734
- import fs27 from "fs";
18709
+ import fs28 from "fs";
17735
18710
  import path31 from "path";
17736
18711
 
17737
18712
  // packages/cli/src/lib/handoff-facts.ts
@@ -17783,15 +18758,15 @@ function assembleHandoffFacts(live, claudeMem, gapSessions, checkpoint, now, ext
17783
18758
  // packages/cli/src/lib/captain-session-registry.ts
17784
18759
  var CAPTAIN_SESSION_REGISTRY_FILE = "captain-sessions.jsonl";
17785
18760
  function appendCaptainSession(spokeVault, record) {
17786
- fs27.mkdirSync(spokeVault, { recursive: true });
18761
+ fs28.mkdirSync(spokeVault, { recursive: true });
17787
18762
  const file = path31.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
17788
- fs27.appendFileSync(file, JSON.stringify(record) + "\n");
18763
+ fs28.appendFileSync(file, JSON.stringify(record) + "\n");
17789
18764
  }
17790
18765
  function readCaptainSessionRegistry(spokeVault) {
17791
18766
  const file = path31.join(spokeVault, CAPTAIN_SESSION_REGISTRY_FILE);
17792
- if (!fs27.existsSync(file)) return [];
18767
+ if (!fs28.existsSync(file)) return [];
17793
18768
  const records = [];
17794
- for (const line of fs27.readFileSync(file, "utf-8").split("\n")) {
18769
+ for (const line of fs28.readFileSync(file, "utf-8").split("\n")) {
17795
18770
  if (!line.trim()) continue;
17796
18771
  try {
17797
18772
  records.push(JSON.parse(line));
@@ -17878,6 +18853,26 @@ function hooksCommand() {
17878
18853
  if (!taskId || !project) {
17879
18854
  process.exit(0);
17880
18855
  }
18856
+ if (sub === "pre-tool-use") {
18857
+ const p = payload;
18858
+ const guard = decideCaptainMemoryWrite(
18859
+ typeof p?.tool_name === "string" ? p.tool_name : "",
18860
+ p?.tool_input,
18861
+ process.env,
18862
+ homedir17()
18863
+ );
18864
+ if (guard.decision === "deny") {
18865
+ console.error(`[squadrant] denied crew write into captain memory (task ${taskId}): ${guard.reason}`);
18866
+ process.stdout.write(JSON.stringify({
18867
+ hookSpecificOutput: {
18868
+ hookEventName: "PreToolUse",
18869
+ permissionDecision: "deny",
18870
+ permissionDecisionReason: guard.reason
18871
+ }
18872
+ }));
18873
+ process.exit(0);
18874
+ }
18875
+ }
17881
18876
  const ev = mapHookSub(sub, payload, taskId);
17882
18877
  if (!ev) {
17883
18878
  process.exit(0);
@@ -17963,7 +18958,7 @@ function printTree(items) {
17963
18958
  function printFlat(items) {
17964
18959
  for (const item of items) printItem(item, 0);
17965
18960
  }
17966
- var startCmd = new Command33("start").description("Start a new work item").argument("<title>", "what you're doing").option("--project <name>", "project this work belongs to (defaults to the current registered project)").option("--parent <id>", "id of the wave/parent item this nests under").option("--tag <tag>", "attach a tag (repeatable)", (v, prev) => [...prev, v], []).action((title, opts) => {
18961
+ var startCmd = new Command33("start").description("Start a new work item").argument("<title>", "what you're doing").option("--project <name>", "project this work belongs to (defaults to the current registered project)").option("--parent <id>", "id of the wave/parent item this nests under").option("--tag <tag>", "attach a tag (repeatable)", (v2, prev) => [...prev, v2], []).action((title, opts) => {
17967
18962
  const config = loadConfig();
17968
18963
  const store = createWorkStore();
17969
18964
  purgeExpiredWorkItems(store);
@@ -18038,7 +19033,7 @@ import os17 from "os";
18038
19033
  // packages/cli/src/lib/handoff-live-repo.ts
18039
19034
  init_dist();
18040
19035
  import { execFileSync as execFileSync10 } from "child_process";
18041
- import fs28 from "fs";
19036
+ import fs29 from "fs";
18042
19037
  import path33 from "path";
18043
19038
 
18044
19039
  // packages/cli/src/lib/handoff-branch-state.ts
@@ -18178,7 +19173,7 @@ function localAheadOfBase(runner, projectPath, base) {
18178
19173
  }
18179
19174
  function readFetchAgeMs(projectPath, now) {
18180
19175
  try {
18181
- const stat2 = fs28.statSync(path33.join(projectPath, ".git", "FETCH_HEAD"));
19176
+ const stat2 = fs29.statSync(path33.join(projectPath, ".git", "FETCH_HEAD"));
18182
19177
  return Math.max(0, now - stat2.mtime.getTime());
18183
19178
  } catch {
18184
19179
  return null;
@@ -18249,7 +19244,7 @@ function gatherLiveRepoState(projectPath, fallbackBaseBranch, tasks, runner = de
18249
19244
 
18250
19245
  // packages/cli/src/lib/handoff-claude-mem.ts
18251
19246
  import { createRequire } from "module";
18252
- import fs29 from "fs";
19247
+ import fs30 from "fs";
18253
19248
  var { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
18254
19249
  var CLAUDE_MEM_RECENCY_LIMIT = 20;
18255
19250
  function decisionText(row) {
@@ -18263,7 +19258,7 @@ function decisionText(row) {
18263
19258
  return row.narrative ?? "";
18264
19259
  }
18265
19260
  function queryClaudeMem(dbPath, project) {
18266
- if (!fs29.existsSync(dbPath)) return null;
19261
+ if (!fs30.existsSync(dbPath)) return null;
18267
19262
  let db;
18268
19263
  try {
18269
19264
  db = new DatabaseSync(dbPath, { readOnly: true });
@@ -18285,7 +19280,7 @@ function queryClaudeMem(dbPath, project) {
18285
19280
  createdAt: r.created_at
18286
19281
  }));
18287
19282
  const candidates = [summaryRow?.created_at, ...decisionRows.map((r) => r.created_at)].filter(
18288
- (v) => !!v
19283
+ (v2) => !!v2
18289
19284
  );
18290
19285
  const oldestCreatedAt = candidates.length > 0 ? candidates.reduce((a, b) => a < b ? a : b) : null;
18291
19286
  return {
@@ -18306,7 +19301,7 @@ function queryClaudeMem(dbPath, project) {
18306
19301
  }
18307
19302
 
18308
19303
  // packages/cli/src/lib/handoff-transcript.ts
18309
- import fs30 from "fs";
19304
+ import fs31 from "fs";
18310
19305
  var TRANSCRIPT_BYTE_CAP = 2e5;
18311
19306
  function tailOf(content, byteCap) {
18312
19307
  const buf = Buffer.from(content, "utf-8");
@@ -18335,27 +19330,27 @@ function extractMessages(tailText) {
18335
19330
  return { lastUserMessage, lastAssistantText };
18336
19331
  }
18337
19332
  function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
18338
- if (!fs30.existsSync(transcriptPath)) return null;
18339
- const content = fs30.readFileSync(transcriptPath, "utf-8");
19333
+ if (!fs31.existsSync(transcriptPath)) return null;
19334
+ const content = fs31.readFileSync(transcriptPath, "utf-8");
18340
19335
  const { lastUserMessage, lastAssistantText } = extractMessages(tailOf(content, byteCap));
18341
- const mtimeIso = fs30.statSync(transcriptPath).mtime.toISOString();
19336
+ const mtimeIso = fs31.statSync(transcriptPath).mtime.toISOString();
18342
19337
  return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText };
18343
19338
  }
18344
19339
 
18345
19340
  // packages/cli/src/lib/handoff-archive.ts
18346
- import fs31 from "fs";
19341
+ import fs32 from "fs";
18347
19342
  import path34 from "path";
18348
19343
  function readNewestArchivedHandoff(spokeVault, now) {
18349
19344
  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) => {
19345
+ if (!fs32.existsSync(dir)) return null;
19346
+ const candidates = fs32.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => {
18352
19347
  const full = path34.join(dir, e.name);
18353
- return { name: e.name, full, mtime: fs31.statSync(full).mtime };
19348
+ return { name: e.name, full, mtime: fs32.statSync(full).mtime };
18354
19349
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
18355
19350
  for (const candidate of candidates) {
18356
19351
  let content;
18357
19352
  try {
18358
- content = JSON.parse(fs31.readFileSync(candidate.full, "utf-8"));
19353
+ content = JSON.parse(fs32.readFileSync(candidate.full, "utf-8"));
18359
19354
  } catch {
18360
19355
  continue;
18361
19356
  }
@@ -18425,15 +19420,15 @@ init_dist();
18425
19420
  init_dist();
18426
19421
  init_dist();
18427
19422
  var __dirname = dirname8(fileURLToPath6(import.meta.url));
18428
- var pkg = JSON.parse(readFileSync16(join27(__dirname, "..", "package.json"), "utf-8"));
19423
+ var pkg = JSON.parse(readFileSync17(join29(__dirname, "..", "package.json"), "utf-8"));
18429
19424
  ensureRuntimeSynced({
18430
- sourceRoot: join27(__dirname, ".."),
18431
- runtimeRoot: join27(homedir17(), ".config", "squadrant")
19425
+ sourceRoot: join29(__dirname, ".."),
19426
+ runtimeRoot: join29(homedir18(), ".config", "squadrant")
18432
19427
  });
18433
19428
  if (process.argv[2] !== "config") {
18434
19429
  try {
18435
- const cfgPath = join27(homedir17(), ".config", "squadrant", "config.json");
18436
- if (existsSync13(cfgPath)) {
19430
+ const cfgPath = join29(homedir18(), ".config", "squadrant", "config.json");
19431
+ if (existsSync14(cfgPath)) {
18437
19432
  const cfg = JSON.parse(readConfigFileSync(cfgPath));
18438
19433
  if (needsCheck(cfg, pkg.version)) {
18439
19434
  const items = detectDrift(cfg, getDefaultConfig());