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.
@@ -199,8 +199,8 @@ function deepMerge(base, patch) {
199
199
  if (patch === null || typeof patch !== "object" || Array.isArray(patch))
200
200
  return patch ?? base;
201
201
  const out = { ...base };
202
- for (const [k, v] of Object.entries(patch)) {
203
- out[k] = deepMerge(out[k], v);
202
+ for (const [k, v2] of Object.entries(patch)) {
203
+ out[k] = deepMerge(out[k], v2);
204
204
  }
205
205
  return out;
206
206
  }
@@ -676,8 +676,8 @@ var init_runtime_sync = __esm({
676
676
  });
677
677
 
678
678
  // packages/shared/dist/lib/tool-compat.js
679
- function parseSemVer(v) {
680
- const m = v.match(/(\d+)\.(\d+)\.(\d+)/);
679
+ function parseSemVer(v2) {
680
+ const m = v2.match(/(\d+)\.(\d+)\.(\d+)/);
681
681
  if (!m)
682
682
  return null;
683
683
  return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
@@ -804,8 +804,9 @@ function isStickyAttention(state) {
804
804
  function nextPendingTool(current, ev, now) {
805
805
  if (ev.note === "agent.hook.PreToolUse")
806
806
  return { name: ev.tool ?? "tool", since: now };
807
- if (ev.note === "posttooluse" || ev.note === "agent.hook.UserPromptSubmit")
807
+ if (ev.note === "posttooluse" || ev.note === "agent.hook.PostToolUse" || ev.note === "agent.hook.UserPromptSubmit") {
808
808
  return void 0;
809
+ }
809
810
  return current;
810
811
  }
811
812
  function nextPendingMonitor(current, ev, now) {
@@ -908,6 +909,7 @@ function reduce(rec, ev, now) {
908
909
  case "task.stalled":
909
910
  case "task.idle":
910
911
  case "task.quiet":
912
+ case "task.warn":
911
913
  case "task.timeout":
912
914
  case "task.reconcile-failed":
913
915
  return rec;
@@ -929,6 +931,15 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStal
929
931
  if (rec.pendingTool) {
930
932
  if (now - rec.pendingTool.since <= toolStallMs)
931
933
  return null;
934
+ if (rec.lastEvent === "task.turn.completed") {
935
+ return {
936
+ ...rec,
937
+ state: "awaiting-input",
938
+ pendingTool: void 0,
939
+ pendingMonitor: void 0,
940
+ lastEvent: "watchdog.tool-stall-recovered"
941
+ };
942
+ }
932
943
  return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
933
944
  }
934
945
  if (rec.pendingMonitor) {
@@ -1250,7 +1261,7 @@ function createDaemon(deps) {
1250
1261
  }
1251
1262
  }
1252
1263
  }
1253
- if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
1264
+ if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state) && r.state !== "awaiting-input") {
1254
1265
  const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
1255
1266
  const refTime = r.workingStretchStartedAt ?? r.createdAt;
1256
1267
  if (t - refTime > ceiling) {
@@ -1296,6 +1307,11 @@ function createDaemon(deps) {
1296
1307
  const idle = evaluateStall(r, t);
1297
1308
  if (idle) {
1298
1309
  store.put(idle);
1310
+ if (idle.state === "awaiting-input") {
1311
+ const recoveredEvent = { type: "task.turn.completed", id: r.id, turnId: "watchdog-recover" };
1312
+ firePush(deps, r.project, r.state, idle, recoveredEvent, lastCaptainTurnAt.get(r.id));
1313
+ continue;
1314
+ }
1299
1315
  const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : idle.pendingMonitor ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: "Monitor", elapsedMs: t - idle.pendingMonitor.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
1300
1316
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
1301
1317
  continue;
@@ -1403,6 +1419,7 @@ var init_reduce = __esm({
1403
1419
  "task.stalled",
1404
1420
  "task.idle",
1405
1421
  "task.quiet",
1422
+ "task.warn",
1406
1423
  "task.timeout",
1407
1424
  "task.reconcile-failed",
1408
1425
  "task.cancelled",
@@ -1806,12 +1823,12 @@ function isDaemonSocketLive(sockPath, timeoutMs = 500) {
1806
1823
  return;
1807
1824
  }
1808
1825
  const conn = createConnection(sockPath);
1809
- const finish = (v) => {
1826
+ const finish = (v2) => {
1810
1827
  try {
1811
1828
  conn.destroy();
1812
1829
  } catch {
1813
1830
  }
1814
- resolve4(v);
1831
+ resolve4(v2);
1815
1832
  };
1816
1833
  const timer = setTimeout(() => finish(false), timeoutMs);
1817
1834
  conn.on("connect", () => {
@@ -1901,7 +1918,18 @@ function projectHealth(input) {
1901
1918
  ref: captainName,
1902
1919
  state: captainState,
1903
1920
  lastSeenMs: null,
1904
- detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries) \u2014 draft/ghost text blocking captain pane; input never touched, delivers automatically once cleared` : void 0
1921
+ detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries, reason: ${deferral.reason ?? "unknown"})` : void 0
1922
+ });
1923
+ const deliveryState = captainState === "stopped" ? "stopped" : deferral?.stuck || deferral && deferral.maxDeferCount > 0 ? "stale" : "alive";
1924
+ const deliveryDetail = captainState === "stopped" ? "delivery paused (captain stopped)" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries, reason: ${deferral.reason ?? "unknown"})` : deferral && deferral.maxDeferCount > 0 ? `delivery deferred (${deferral.maxDeferCount} retries, reason: ${deferral.reason ?? "unknown"})` : void 0;
1925
+ out.push({
1926
+ kind: "delivery",
1927
+ project,
1928
+ ref: "delivery",
1929
+ state: deliveryState,
1930
+ lastSeenMs: null,
1931
+ detail: deliveryDetail,
1932
+ stuck: deferral?.stuck
1905
1933
  });
1906
1934
  if (commandPresent !== null) {
1907
1935
  out.push({
@@ -2003,8 +2031,23 @@ function createStore(root) {
2003
2031
  };
2004
2032
  const projDir = (p) => assertUnderRoot(join6(root, safeSegment("project", p)));
2005
2033
  const taskFile = (p, id) => assertUnderRoot(join6(projDir(p), `${safeSegment("id", id)}.json`));
2034
+ const readRecord = (project, id) => {
2035
+ const f = taskFile(project, id);
2036
+ if (!existsSync6(f))
2037
+ return void 0;
2038
+ try {
2039
+ return JSON.parse(readFileSync4(f, "utf-8"));
2040
+ } catch {
2041
+ return void 0;
2042
+ }
2043
+ };
2006
2044
  return {
2007
2045
  put(rec) {
2046
+ const existing = readRecord(rec.project, rec.id);
2047
+ if (existing && TERMINAL_STATES.has(existing.state) && TERMINAL_STATES.has(rec.state) && (existing.state !== rec.state || existing.lastEvent !== rec.lastEvent)) {
2048
+ console.error(`[squadrant] REJECTED terminal\u2192terminal overwrite of ${rec.project}/${rec.id}: already ${existing.state}/${existing.lastEvent} \u2014 refusing ${rec.state}/${rec.lastEvent}; original terminal record preserved (#595)`);
2049
+ return;
2050
+ }
2008
2051
  mkdirSync2(projDir(rec.project), { recursive: true });
2009
2052
  const dest = taskFile(rec.project, rec.id);
2010
2053
  const tmp = `${dest}.tmp`;
@@ -2012,14 +2055,7 @@ function createStore(root) {
2012
2055
  renameSync(tmp, dest);
2013
2056
  },
2014
2057
  get(project, id) {
2015
- const f = taskFile(project, id);
2016
- if (!existsSync6(f))
2017
- return void 0;
2018
- try {
2019
- return JSON.parse(readFileSync4(f, "utf-8"));
2020
- } catch {
2021
- return void 0;
2022
- }
2058
+ return readRecord(project, id);
2023
2059
  },
2024
2060
  list(project) {
2025
2061
  const d = projDir(project);
@@ -2058,6 +2094,7 @@ function createStore(root) {
2058
2094
  }
2059
2095
  var init_store = __esm({
2060
2096
  "packages/core/dist/store.js"() {
2097
+ init_dist();
2061
2098
  }
2062
2099
  });
2063
2100
 
@@ -2424,7 +2461,7 @@ function computeDaemonDrift(nodeBin) {
2424
2461
  const foreignInstall = detectForeignInstall(parsedCurrent, entry, parsedCurrent !== null && existsSync8(parsedCurrent.daemonEntry));
2425
2462
  return { plistPath: p, target, desired, current, changed, programChanged, foreignInstall };
2426
2463
  }
2427
- function applyDaemonDrift(drift) {
2464
+ function reconcilePlistAndService(drift) {
2428
2465
  if (drift.changed) {
2429
2466
  mkdirSync4(dirname2(drift.plistPath), { recursive: true });
2430
2467
  writeFileSync5(drift.plistPath, drift.desired);
@@ -2440,8 +2477,59 @@ function applyDaemonDrift(drift) {
2440
2477
  execFileSync3("launchctl", ["bootstrap", `gui/${uid}`, drift.plistPath], { stdio: "ignore" });
2441
2478
  } catch {
2442
2479
  }
2480
+ }
2481
+ function applyDaemonDrift(drift) {
2482
+ reconcilePlistAndService(drift);
2443
2483
  execFileSync3("launchctl", ["kickstart", drift.target], { stdio: "ignore" });
2444
2484
  }
2485
+ function getDaemonPid(target) {
2486
+ try {
2487
+ const out = execFileSync3("launchctl", ["print", target], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
2488
+ const m = out.match(/\bpid\s*=\s*(\d+)/);
2489
+ return m ? parseInt(m[1], 10) : null;
2490
+ } catch {
2491
+ return null;
2492
+ }
2493
+ }
2494
+ function forceKickstartAndVerify(target, opts = {}) {
2495
+ const pollAttempts = opts.pollAttempts ?? 15;
2496
+ const pollDelayMs = opts.pollDelayMs ?? 300;
2497
+ const kickstartRetries = opts.kickstartRetries ?? 10;
2498
+ const kickstartRetryDelayMs = opts.kickstartRetryDelayMs ?? 300;
2499
+ const pidBefore = getDaemonPid(target);
2500
+ let kickstartError = null;
2501
+ for (let i = 0; i < kickstartRetries; i++) {
2502
+ try {
2503
+ execFileSync3("launchctl", ["kickstart", "-k", target], { stdio: "ignore" });
2504
+ kickstartError = null;
2505
+ break;
2506
+ } catch (e) {
2507
+ kickstartError = e;
2508
+ if (i < kickstartRetries - 1) {
2509
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, kickstartRetryDelayMs);
2510
+ }
2511
+ }
2512
+ }
2513
+ let pidAfter = null;
2514
+ for (let i = 0; i < pollAttempts; i++) {
2515
+ pidAfter = getDaemonPid(target);
2516
+ if (pidAfter !== null && pidAfter !== pidBefore)
2517
+ break;
2518
+ if (i < pollAttempts - 1) {
2519
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollDelayMs);
2520
+ }
2521
+ }
2522
+ const restarted = pidAfter !== null && pidAfter !== pidBefore;
2523
+ if (kickstartError && !restarted)
2524
+ throw kickstartError;
2525
+ return {
2526
+ target,
2527
+ pidBefore,
2528
+ pidAfter,
2529
+ restarted,
2530
+ ...kickstartError ? { note: "kickstart -k refused; daemon restarted by bootstrap" } : {}
2531
+ };
2532
+ }
2445
2533
  function isOperatorInitiatedCommand(topLevelArg) {
2446
2534
  return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
2447
2535
  }
@@ -2483,11 +2571,18 @@ function printForeignInstallError(foreign) {
2483
2571
  Two squadrant installs on this machine will keep fighting over the daemon (#670). Uninstall the one you don't use, then run \`squadrant heal daemon\` to reconcile.
2484
2572
  `;
2485
2573
  }
2486
- function reregisterDaemon(nodeBin = process.execPath) {
2487
- if (!tryAcquireDaemonLock())
2488
- return;
2574
+ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
2575
+ if (!tryAcquireDaemonLock()) {
2576
+ throw new Error("could not acquire the daemon lock \u2014 another squadrant process is already restarting the daemon");
2577
+ }
2489
2578
  try {
2490
- applyDaemonDrift(computeDaemonDrift(nodeBin));
2579
+ const drift = computeDaemonDrift(nodeBin);
2580
+ reconcilePlistAndService(drift);
2581
+ const result = forceKickstartAndVerify(drift.target, kickstartOpts);
2582
+ if (!result.restarted) {
2583
+ 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`);
2584
+ }
2585
+ return result;
2491
2586
  } finally {
2492
2587
  releaseDaemonLock();
2493
2588
  }
@@ -2875,6 +2970,10 @@ function detectTrailingQuestion(text) {
2875
2970
  return lastLine;
2876
2971
  return null;
2877
2972
  }
2973
+ function isQuotedLine(raw) {
2974
+ const noAnsi = raw.replace(/\[[0-9;]*m/g, "");
2975
+ return QUOTED_PREFIX_RE.test(noAnsi);
2976
+ }
2878
2977
  function stripChrome(raw) {
2879
2978
  let line = raw.replace(/\[[0-9;]*m/g, "");
2880
2979
  line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
@@ -2933,8 +3032,11 @@ function classifyPaneTail(tail) {
2933
3032
  if (q)
2934
3033
  return { kind: "question", text: q };
2935
3034
  let errLine = null;
2936
- for (const c of cleaned) {
2937
- if (c != null && ERROR_BANNER_RE.some((re) => re.test(c)))
3035
+ for (let i = 0; i < cleaned.length; i++) {
3036
+ const c = cleaned[i];
3037
+ if (c == null || isQuotedLine(raw[i]))
3038
+ continue;
3039
+ if (ERROR_BANNER_RE.some((re) => re.test(c)))
2938
3040
  errLine = c;
2939
3041
  }
2940
3042
  if (errLine)
@@ -2977,6 +3079,22 @@ function createInteractiveProbe(deps) {
2977
3079
  const verdict = classifyPaneTail(tail);
2978
3080
  if (!verdict)
2979
3081
  continue;
3082
+ if (verdict.kind === "error") {
3083
+ const alive = deps.checkAlive ? await deps.checkAlive(rec) : "unknown";
3084
+ if (alive !== "gone") {
3085
+ const message = `CREW WARN ${rec.name}: pane shows an error string \u2014 crew still ${alive}, not terminalized (pane-detected): ${verdict.text}`;
3086
+ deps.log(`probe -> ${message}`);
3087
+ if (deps.notify) {
3088
+ const warnEvent = { type: "task.warn", id: rec.id, message };
3089
+ try {
3090
+ await deps.notify({ project: rec.project, message, record: rec, event: warnEvent });
3091
+ } catch (e) {
3092
+ deps.log(`probe notify failed for ${rec.id}: ${e.message}`);
3093
+ }
3094
+ }
3095
+ continue;
3096
+ }
3097
+ }
2980
3098
  const event = verdict.kind === "error" ? {
2981
3099
  type: "task.failed",
2982
3100
  id: rec.id,
@@ -2998,7 +3116,7 @@ function createInteractiveProbe(deps) {
2998
3116
  }
2999
3117
  return { tick };
3000
3118
  }
3001
- var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE;
3119
+ var STALE_THRESHOLD_MS, PROBE_QUIET_MS, ERROR_BANNER_RE, OPTION_RE, PICKER_FOOTER_RE, PURE_CHROME_RE, STATUS_LINE_RE, QUOTED_PREFIX_RE;
3002
3120
  var init_interactive_probe = __esm({
3003
3121
  "packages/core/dist/daemon/interactive-probe.js"() {
3004
3122
  STALE_THRESHOLD_MS = 5 * 60 * 1e3;
@@ -3014,6 +3132,7 @@ var init_interactive_probe = __esm({
3014
3132
  PICKER_FOOTER_RE = /↑↓\s*select|enter\s+submit|esc\s+dismiss/i;
3015
3133
  PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
3016
3134
  STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
3135
+ QUOTED_PREFIX_RE = /^\s*(?:[┃│▏▕]|>|[+-]|\d+[\t:→])\s/;
3017
3136
  }
3018
3137
  });
3019
3138
 
@@ -3029,6 +3148,7 @@ function createProbes(ctx) {
3029
3148
  };
3030
3149
  function buildInteractiveProbe(deps) {
3031
3150
  const directPaneReader = createDirectCrewPaneReader(deps.cmux, captainNameForProject);
3151
+ const checkAlive = createDirectSurfaceLivenessProbe(deps.cmux, captainNameForProject);
3032
3152
  const probe = createInteractiveProbe({
3033
3153
  project: "_all_",
3034
3154
  listTasks: async () => store.listAll(),
@@ -3040,7 +3160,9 @@ function createProbes(ctx) {
3040
3160
  }
3041
3161
  },
3042
3162
  now: () => Date.now(),
3043
- log
3163
+ log,
3164
+ checkAlive,
3165
+ notify: ctx.notify
3044
3166
  });
3045
3167
  let probing = false;
3046
3168
  return async () => {
@@ -3315,6 +3437,33 @@ import fs9 from "fs";
3315
3437
  import os4 from "os";
3316
3438
  import path9 from "path";
3317
3439
  import { randomUUID as randomUUID3 } from "crypto";
3440
+ async function pollFirstTurnConfirmedAt(getTaskRecord, project, id) {
3441
+ const deadline = Date.now() + FIRST_TURN_HOOK_CONFIRM_WINDOW_MS;
3442
+ for (; ; ) {
3443
+ const rec = await getTaskRecord(project, id).catch(() => void 0);
3444
+ if (rec?.firstTurnConfirmedAt)
3445
+ return true;
3446
+ if (Date.now() >= deadline)
3447
+ return false;
3448
+ await new Promise((r) => setTimeout(r, FIRST_TURN_HOOK_POLL_INTERVAL_MS));
3449
+ }
3450
+ }
3451
+ function firstTrueOrBothFalse(a, b) {
3452
+ return new Promise((resolve4) => {
3453
+ let settledFalseCount = 0;
3454
+ const onSettle = (ok2) => {
3455
+ if (ok2) {
3456
+ resolve4(true);
3457
+ return;
3458
+ }
3459
+ settledFalseCount++;
3460
+ if (settledFalseCount === 2)
3461
+ resolve4(false);
3462
+ };
3463
+ a.then(onSettle, () => onSettle(false));
3464
+ b.then(onSettle, () => onSettle(false));
3465
+ });
3466
+ }
3318
3467
  async function listCrewPanes(runtime, workspaceId, project) {
3319
3468
  const surfaces = await runtime.listSurfaces(workspaceId);
3320
3469
  return surfaces.filter((s) => s.title && isCrewTitle(project, s.title));
@@ -3420,6 +3569,7 @@ async function runCrewSpawn(input, config, deps) {
3420
3569
  const crewRole = config.defaults.roles?.crew;
3421
3570
  const configModel = crewRole && crewRole.agent === agent.name ? crewRole.model : void 0;
3422
3571
  const crewModel = input.model ?? route?.model ?? configModel;
3572
+ const crewThinking = input.thinking ?? config.defaults.roles?.crew?.thinking;
3423
3573
  if (agentName !== "claude") {
3424
3574
  deps.onModelResolved?.({ agentName, model: crewModel });
3425
3575
  }
@@ -3455,7 +3605,8 @@ async function runCrewSpawn(input, config, deps) {
3455
3605
  // crew apart from an unrelated session instead of an auto-derived cwd
3456
3606
  // basename (only the claude driver reads this — other agents ignore it).
3457
3607
  sessionName: crewSessionName(input.project, name),
3458
- ...crewModel ? { model: crewModel } : {}
3608
+ ...crewModel ? { model: crewModel } : {},
3609
+ ...crewThinking ? { thinking: crewThinking } : {}
3459
3610
  });
3460
3611
  const direction2 = input.direction ?? "tab";
3461
3612
  const title2 = titleFor(input.project, name);
@@ -3463,10 +3614,18 @@ async function runCrewSpawn(input, config, deps) {
3463
3614
  const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
3464
3615
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix} ${niceCrewCommand(cliCommand2)}`);
3465
3616
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
3466
- const claudeResult = await deps.sendFirstTurn(pane2, `${firstTurnTask}
3617
+ let claudeFirstTurn = firstTurnTask;
3618
+ if (Buffer.byteLength(claudeFirstTurn, "utf8") > FIRST_TURN_INLINE_MAX_BYTES) {
3619
+ const spillFile = path9.join(os4.tmpdir(), `squadrant-task-${rec.id}.md`);
3620
+ fs9.writeFileSync(spillFile, claudeFirstTurn, "utf8");
3621
+ claudeFirstTurn = `Full task is at ${spillFile} \u2014 cat it and follow it exactly.`;
3622
+ }
3623
+ const sendPromise = deps.sendFirstTurn(pane2, `${claudeFirstTurn}
3467
3624
 
3468
3625
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
3469
- if (!claudeResult.delivered) {
3626
+ const scrapeDelivered = sendPromise.then((r) => r.delivered).catch(() => false);
3627
+ const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id)) : await scrapeDelivered;
3628
+ if (!delivered) {
3470
3629
  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.
3471
3630
  `);
3472
3631
  } else if (!hooksInstalled) {
@@ -3563,7 +3722,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3563
3722
  if (!crew) {
3564
3723
  throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
3565
3724
  }
3566
- const blockedByModalMessage = () => `Crew '${name}' has an interactive prompt open (AskUserQuestion/permission) \u2014 message NOT delivered, to avoid confirming its default option. Wait for the prompt to close, then re-send with 'squadrant crew send ${project} ${name}'.`;
3725
+ const blockedByModalMessage = () => `Crew '${name}' has an interactive prompt open (AskUserQuestion/permission) \u2014 message NOT delivered, to avoid confirming its default option. To answer it deliberately: squadrant crew read ${project} ${name} to see the options, then squadrant crew answer ${project} ${name} <n>.`;
3567
3726
  if (deps.isBlockedByModal && await deps.isBlockedByModal(crew)) {
3568
3727
  throw new Error(blockedByModalMessage());
3569
3728
  }
@@ -3577,10 +3736,16 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3577
3736
  const heldForMin = Math.round((Date.now() - task.operatorHold.since) / 6e4);
3578
3737
  throw new Error(`Crew '${name}' is under operator takeover (held ${heldForMin}m${task.operatorHold.note ? `: ${task.operatorHold.note}` : ""}). The operator is working in that tab \u2014 sending a message disrupts their conversation. Ask them to run 'squadrant crew handback ${project} ${name}', or pass --force if they told you to.`);
3579
3738
  }
3739
+ const isAttentionState = task?.state === "blocked" || task?.state === "awaiting-input" || task?.state === "review";
3740
+ if (task && !isAttentionState && task.firstTurnConfirmedAt && task.task === message) {
3741
+ 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.`);
3742
+ }
3743
+ let reopened = false;
3580
3744
  try {
3581
3745
  if (task) {
3582
3746
  if (TERMINAL_STATES.has(task.state)) {
3583
3747
  await deps.emitEvent(project, { type: "task.reopened", id: task.id });
3748
+ reopened = true;
3584
3749
  } else if (task.state === "blocked" || task.state === "awaiting-input" || task.state === "review") {
3585
3750
  await deps.emitEvent(project, { type: "task.started", id: task.id });
3586
3751
  }
@@ -3606,7 +3771,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3606
3771
  throw new Error(`Message to crew '${name}' is held: ${outcome.reason}. Resolve it in the crew's session, then re-send.`);
3607
3772
  }
3608
3773
  if (!fallsBackToPane(outcome)) {
3609
- return;
3774
+ return { reopened };
3610
3775
  }
3611
3776
  }
3612
3777
  if (mode === "shadow" && channel && task) {
@@ -3629,7 +3794,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3629
3794
  if (!paneOk) {
3630
3795
  throw new Error(`Message not delivered to crew '${name}' \u2014 the paste/submit could not be confirmed. Re-send with 'squadrant crew send ${project} ${name}'.`);
3631
3796
  }
3632
- return;
3797
+ return { reopened };
3633
3798
  }
3634
3799
  const { delivered, blockedByModal } = await deliver(crew, message);
3635
3800
  if (blockedByModal) {
@@ -3638,6 +3803,7 @@ async function runCrewSend(project, name, message, runtime, workspaceId, deps, o
3638
3803
  if (!delivered) {
3639
3804
  throw new Error(`Message not delivered to crew '${name}' \u2014 the paste/submit could not be confirmed. Re-send with 'squadrant crew send ${project} ${name}'.`);
3640
3805
  }
3806
+ return { reopened };
3641
3807
  }
3642
3808
  async function runCrewRead(project, name, runtime, workspaceId) {
3643
3809
  const crew = await findCrewPane(runtime, workspaceId, project, name);
@@ -3740,7 +3906,7 @@ async function runCrewList(project, runtime, workspaceId) {
3740
3906
  surfaceId: c.surfaceId
3741
3907
  }));
3742
3908
  }
3743
- var CC_SOCKS_DIR, TEMPLATES_DIR, STATE_ROOT, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
3909
+ 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;
3744
3910
  var init_crew_spawn = __esm({
3745
3911
  "packages/core/dist/crew-spawn.js"() {
3746
3912
  init_control_channel();
@@ -3749,8 +3915,11 @@ var init_crew_spawn = __esm({
3749
3915
  init_crew_protocol();
3750
3916
  init_crew_lifecycle();
3751
3917
  CC_SOCKS_DIR = "/tmp/cc-socks";
3918
+ FIRST_TURN_INLINE_MAX_BYTES = 1200;
3752
3919
  TEMPLATES_DIR = path9.join(os4.homedir(), ".config", "squadrant", "templates");
3753
3920
  STATE_ROOT = path9.join(os4.homedir(), ".config", "squadrant", "state");
3921
+ FIRST_TURN_HOOK_CONFIRM_WINDOW_MS = 1e5;
3922
+ FIRST_TURN_HOOK_POLL_INTERVAL_MS = 2e3;
3754
3923
  CLOSE_LOOKUP_RETRIES = 3;
3755
3924
  CLOSE_LOOKUP_RETRY_DELAY_MS = 150;
3756
3925
  }
@@ -3795,13 +3964,16 @@ var init_captain_channel = __esm({
3795
3964
  function discoverCaptainSurface(surfaces, captainTitle) {
3796
3965
  return surfaces.find((s) => s.title === captainTitle) ?? null;
3797
3966
  }
3798
- function reapOrphanedCrews(store, project) {
3967
+ async function reapOrphanedCrews(store, project, isSurfaceAlive) {
3799
3968
  let reaped = 0;
3800
3969
  for (const r of store.list(project)) {
3801
3970
  if (TERMINAL_STATES.has(r.state))
3802
3971
  continue;
3803
3972
  if (r.mode !== "interactive")
3804
3973
  continue;
3974
+ const liveness = await isSurfaceAlive(r);
3975
+ if (liveness !== "gone")
3976
+ continue;
3805
3977
  store.put({ ...r, state: "cancelled", lastEvent: "captain-stopped" });
3806
3978
  reaped++;
3807
3979
  }
@@ -3855,10 +4027,13 @@ async function runLivenessTick(deps) {
3855
4027
  const prev = deps.registry.get(project);
3856
4028
  if (prev && prev.lastState === "start")
3857
4029
  entry.startedAt = prev.startedAt;
4030
+ const prevState = deriveCaptainState(prev);
3858
4031
  deps.registry.apply(entry);
3859
4032
  if (winner.pid != null)
3860
4033
  deps.registry.setPidAlive(project, deps.isPidAlive(winner.pid), now);
3861
- logEntry(deps.log, project, deps.registry.get(project));
4034
+ const updated = deps.registry.get(project);
4035
+ if (deriveCaptainState(updated) !== prevState)
4036
+ logEntry(deps.log, project, updated);
3862
4037
  }
3863
4038
  for (const e of deps.registry.all()) {
3864
4039
  if (e.role !== "captain" || e.lastState !== "start" || seen.has(e.project))
@@ -3876,12 +4051,13 @@ async function runLivenessTick(deps) {
3876
4051
  continue;
3877
4052
  const state = deriveCaptainState(e);
3878
4053
  if (state === "stopped" || state === "gone")
3879
- deps.reap(e.project);
4054
+ await deps.reap(e.project);
3880
4055
  }
3881
4056
  }
3882
4057
  }
3883
- function createDelivery(ctx, daemonCmux) {
4058
+ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
3884
4059
  const { stateRoot, store, log, livenessRegistry, isPidAlive, opts, telegramBridge } = ctx;
4060
+ const surfaceProbe = isSurfaceAlive ?? (async () => "unknown");
3885
4061
  const notifyFault = ctx.notifyFault ?? (() => {
3886
4062
  });
3887
4063
  const defaultNotify = async (args) => {
@@ -3904,12 +4080,22 @@ function createDelivery(ctx, daemonCmux) {
3904
4080
  }
3905
4081
  };
3906
4082
  if (!daemonCmux) {
3907
- return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0 };
4083
+ return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0, inFlightDelivery: () => null };
3908
4084
  }
3909
4085
  const cmux2 = daemonCmux;
3910
4086
  const cfg = loadConfig();
3911
4087
  const deliveries = /* @__PURE__ */ new Map();
3912
4088
  const deliveryStats = (project) => deliveries.get(project)?.stats();
4089
+ const lastDeferred = /* @__PURE__ */ new Map();
4090
+ const inFlightDelivery = () => {
4091
+ let worst = null;
4092
+ for (const [project, v2] of lastDeferred) {
4093
+ if (!worst || v2.deferCount > worst.deferCount)
4094
+ worst = { project, ...v2 };
4095
+ }
4096
+ return worst;
4097
+ };
4098
+ const projectBackoff = /* @__PURE__ */ new Map();
3913
4099
  const stuckNotified = /* @__PURE__ */ new Set();
3914
4100
  const sessionStartMs = Date.now();
3915
4101
  let delivering = false;
@@ -3920,8 +4106,8 @@ function createDelivery(ctx, daemonCmux) {
3920
4106
  isPidAlive,
3921
4107
  now: () => Date.now(),
3922
4108
  log,
3923
- reap: (project) => {
3924
- const reaped = reapOrphanedCrews(store, project);
4109
+ reap: async (project) => {
4110
+ const reaped = await reapOrphanedCrews(store, project, surfaceProbe);
3925
4111
  if (reaped > 0) {
3926
4112
  const title = cfg.projects?.[project]?.captainName ?? `${project}-captain`;
3927
4113
  log(`captain ${title}: reaped ${reaped} orphaned crew(s)`);
@@ -3937,82 +4123,116 @@ function createDelivery(ctx, daemonCmux) {
3937
4123
  cfg.commandName
3938
4124
  ])];
3939
4125
  for (const project of allProjects) {
3940
- const projCfg = cfg.projects?.[project];
3941
- const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
3942
- const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
3943
- let surface = null;
3944
- if (wsId) {
3945
- const surfaces = await cmux2.listSurfaces(wsId);
3946
- surface = discoverCaptainSurface(surfaces, captainTitle);
3947
- }
3948
- if (!surface)
3949
- surface = injectedSurfaces[project] ?? null;
3950
- if (!surface)
3951
- continue;
3952
- const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
3953
- const lastAcked = cursor?.lastAckedSeq ?? 0;
3954
- let d = deliveries.get(project);
3955
- if (!d) {
3956
- d = new CaptainDelivery({
3957
- maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
3958
- stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
3959
- });
3960
- deliveries.set(project, d);
3961
- }
3962
- for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
3963
- if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
3964
- if (!TERMINAL_KINDS.has(entry.kind)) {
3965
- const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
3966
- if (!isExemptMessage) {
3967
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
3968
- await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
3969
- continue;
4126
+ try {
4127
+ const backoff = projectBackoff.get(project);
4128
+ if (backoff && Date.now() < backoff.nextAttemptAt)
4129
+ continue;
4130
+ const projCfg = cfg.projects?.[project];
4131
+ const captainTitle = project === cfg.commandName ? cfg.commandName : projCfg?.captainName ?? `${project}-captain`;
4132
+ let surface = null;
4133
+ const resolveCaptainSurface = async () => {
4134
+ const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
4135
+ if (!wsId)
4136
+ return injectedSurfaces[project] ?? null;
4137
+ const surfaces = await cmux2.listSurfaces(wsId);
4138
+ return discoverCaptainSurface(surfaces, captainTitle) ?? injectedSurfaces[project] ?? null;
4139
+ };
4140
+ surface = await resolveCaptainSurface();
4141
+ if (!surface)
4142
+ continue;
4143
+ const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
4144
+ const lastAcked = cursor?.lastAckedSeq ?? 0;
4145
+ let d = deliveries.get(project);
4146
+ if (!d) {
4147
+ d = new CaptainDelivery({
4148
+ maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
4149
+ stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
4150
+ });
4151
+ deliveries.set(project, d);
4152
+ }
4153
+ for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
4154
+ if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
4155
+ if (!TERMINAL_KINDS.has(entry.kind)) {
4156
+ const isExemptMessage = entry.kind === "captain.message" && entry.payload?.source !== "daemon";
4157
+ if (!isExemptMessage) {
4158
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-skipped`);
4159
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4160
+ continue;
4161
+ }
4162
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
4163
+ } else {
4164
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4165
+ }
4166
+ }
4167
+ const result = await d.deliver(entry, async (text, sendOpts) => {
4168
+ let handledByChannel = false;
4169
+ try {
4170
+ const mode = ctx.captainChannelMode?.() ?? "off";
4171
+ const r = await deliverToCaptain(project, text, {
4172
+ channel: ctx.captainChannel,
4173
+ mode,
4174
+ log
4175
+ });
4176
+ handledByChannel = r.handled;
4177
+ } catch (e) {
4178
+ log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4179
+ }
4180
+ if (handledByChannel) {
4181
+ return;
4182
+ }
4183
+ try {
4184
+ return await cmux2.send(surface, text, sendOpts);
4185
+ } catch (e) {
4186
+ if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
4187
+ throw e;
4188
+ const next = await resolveCaptainSurface();
4189
+ const same = next !== null && next.workspaceId === surface.workspaceId && next.surfaceId === surface.surfaceId;
4190
+ if (!next || same) {
4191
+ log(`delivery project=${project}: probe-failed but surface re-resolution found ${next ? "the same dead surface" : "no captain surface"} \u2014 deferring`);
4192
+ throw e;
4193
+ }
4194
+ log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
4195
+ surface = next;
4196
+ return cmux2.send(next, text, sendOpts);
3970
4197
  }
3971
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-exempt-deliver`);
4198
+ });
4199
+ if ("delivered" in result) {
4200
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
4201
+ await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
4202
+ lastDeferred.delete(project);
4203
+ projectBackoff.delete(project);
3972
4204
  } else {
3973
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4205
+ const { maxDeferCount, stuck: stuck2 } = d.stats();
4206
+ if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4207
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4208
+ }
4209
+ lastDeferred.set(project, { seq: entry.seq, deferCount: maxDeferCount });
4210
+ if (stuck2) {
4211
+ const streak = (projectBackoff.get(project)?.streak ?? 0) + 1;
4212
+ const backoffMs = Math.min(6e4, 1e3 * 2 ** streak);
4213
+ projectBackoff.set(project, { nextAttemptAt: Date.now() + backoffMs, streak });
4214
+ }
4215
+ break;
3974
4216
  }
3975
4217
  }
3976
- const result = await d.deliver(entry, async (text, sendOpts) => {
3977
- let handledByChannel = false;
4218
+ const stuck = d.stats().stuck;
4219
+ if (stuck && !stuckNotified.has(project)) {
4220
+ stuckNotified.add(project);
4221
+ const { maxDeferCount, reason } = d.stats();
4222
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
4223
+ const text = STUCK_ALERT_TEXT[reason ?? "unknown"](maxDeferCount);
3978
4224
  try {
3979
- const mode = ctx.captainChannelMode?.() ?? "off";
3980
- const r = await deliverToCaptain(project, text, {
3981
- channel: ctx.captainChannel,
3982
- mode,
3983
- log
3984
- });
3985
- handledByChannel = r.handled;
4225
+ await appendCaptainMessage({ stateRoot, project, text, source: "daemon" });
3986
4226
  } catch (e) {
3987
- log(`captain-channel ${project}: threw, falling back to pane \u2014 ${e.message}`);
4227
+ log(`delivery stuck alert failed project=${project}: ${e.message}`);
3988
4228
  }
3989
- if (handledByChannel) {
3990
- return;
3991
- }
3992
- return cmux2.send(surface, text, sendOpts);
3993
- });
3994
- if ("delivered" in result) {
3995
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
3996
- await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
3997
- } else {
3998
- const { maxDeferCount } = d.stats();
3999
- if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
4000
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
4001
- }
4002
- break;
4229
+ Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
4230
+ telegramBridge?.pushRaw(project, text);
4231
+ } else if (!stuck && stuckNotified.has(project)) {
4232
+ stuckNotified.delete(project);
4003
4233
  }
4004
- }
4005
- const stuck = d.stats().stuck;
4006
- if (stuck && !stuckNotified.has(project)) {
4007
- stuckNotified.add(project);
4008
- const { maxDeferCount, reason } = d.stats();
4009
- log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
4010
- const text = reason === "modal" ? `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${maxDeferCount}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.` : `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
4011
- appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
4012
- Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
4013
- telegramBridge?.pushRaw(project, text);
4014
- } else if (!stuck && stuckNotified.has(project)) {
4015
- stuckNotified.delete(project);
4234
+ } catch (e) {
4235
+ log(`delivery project=${project}: unhandled error \u2014 ${e.message}`);
4016
4236
  }
4017
4237
  }
4018
4238
  };
@@ -4026,19 +4246,28 @@ function createDelivery(ctx, daemonCmux) {
4026
4246
  delivering = false;
4027
4247
  }
4028
4248
  };
4029
- return { defaultNotify, deliveryTick, deliveryStats };
4249
+ return { defaultNotify, deliveryTick, deliveryStats, inFlightDelivery };
4030
4250
  }
4031
- var CURSOR_SUBSCRIBER, TERMINAL_KINDS;
4251
+ var CURSOR_SUBSCRIBER, TERMINAL_KINDS, STUCK_ALERT_TEXT;
4032
4252
  var init_delivery_loop = __esm({
4033
4253
  "packages/core/dist/daemon/delivery-loop.js"() {
4034
4254
  init_mailbox();
4035
4255
  init_captain_delivery();
4256
+ init_defer_delivery();
4036
4257
  init_dist();
4037
4258
  init_interactive_probe();
4038
4259
  init_liveness2();
4039
4260
  init_captain_channel();
4040
4261
  CURSOR_SUBSCRIBER = "captain";
4041
4262
  TERMINAL_KINDS = /* @__PURE__ */ new Set(["task.done", "task.failed", "task.cancelled", "task.blocked"]);
4263
+ STUCK_ALERT_TEXT = {
4264
+ "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.`,
4265
+ 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.`,
4266
+ 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.`,
4267
+ "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.`,
4268
+ 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.`,
4269
+ 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.`
4270
+ };
4042
4271
  }
4043
4272
  });
4044
4273
 
@@ -4126,10 +4355,72 @@ var init_server = __esm({
4126
4355
  }
4127
4356
  });
4128
4357
 
4358
+ // packages/core/dist/daemon/exit-marker.js
4359
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
4360
+ import { join as join11 } from "path";
4361
+ function exitMarkerPath(stateRoot) {
4362
+ return join11(stateRoot, "exit-marker.json");
4363
+ }
4364
+ function writeExitMarker(stateRoot, marker, log) {
4365
+ try {
4366
+ writeFileSync8(exitMarkerPath(stateRoot), JSON.stringify(marker));
4367
+ } catch (e) {
4368
+ log(`exit marker write failed: ${e.message}`);
4369
+ }
4370
+ }
4371
+ function consumeExitMarker(stateRoot, now = Date.now) {
4372
+ const p = exitMarkerPath(stateRoot);
4373
+ if (!existsSync9(p))
4374
+ return { marker: null };
4375
+ let marker = null;
4376
+ try {
4377
+ marker = JSON.parse(readFileSync8(p, "utf-8"));
4378
+ } catch {
4379
+ marker = null;
4380
+ }
4381
+ try {
4382
+ unlinkSync3(p);
4383
+ } catch {
4384
+ }
4385
+ if (!marker)
4386
+ return { marker: null };
4387
+ const gapMs = Math.max(0, now() - new Date(marker.ts).getTime());
4388
+ return { marker, gapMs };
4389
+ }
4390
+ function runningMarkerPath(stateRoot) {
4391
+ return join11(stateRoot, "running-marker.json");
4392
+ }
4393
+ function writeRunningMarker(stateRoot, marker, log) {
4394
+ try {
4395
+ writeFileSync8(runningMarkerPath(stateRoot), JSON.stringify(marker));
4396
+ } catch (e) {
4397
+ log(`running marker write failed: ${e.message}`);
4398
+ }
4399
+ }
4400
+ function readRunningMarker(stateRoot) {
4401
+ try {
4402
+ return JSON.parse(readFileSync8(runningMarkerPath(stateRoot), "utf-8"));
4403
+ } catch {
4404
+ return null;
4405
+ }
4406
+ }
4407
+ function removeRunningMarker(stateRoot, log) {
4408
+ try {
4409
+ unlinkSync3(runningMarkerPath(stateRoot));
4410
+ } catch (e) {
4411
+ if (e.code !== "ENOENT")
4412
+ log(`running marker remove failed: ${e.message}`);
4413
+ }
4414
+ }
4415
+ var init_exit_marker = __esm({
4416
+ "packages/core/dist/daemon/exit-marker.js"() {
4417
+ }
4418
+ });
4419
+
4129
4420
  // packages/core/dist/daemon/snapshot-gather.js
4130
4421
  import { fileURLToPath as fileURLToPath2 } from "url";
4131
- import { join as join11 } from "path";
4132
- import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
4422
+ import { join as join12 } from "path";
4423
+ import { statSync as statSync3, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
4133
4424
  function distBuiltAt() {
4134
4425
  try {
4135
4426
  return statSync3(SELF_PATH).mtimeMs;
@@ -4182,7 +4473,7 @@ function gatherStoreStats(store, stateRoot, project) {
4182
4473
  for (const r of store.list(project))
4183
4474
  byState[r.state] = (byState[r.state] ?? 0) + 1;
4184
4475
  let corruptCount = 0;
4185
- const dir = join11(stateRoot, project);
4476
+ const dir = join12(stateRoot, project);
4186
4477
  try {
4187
4478
  for (const n of readdirSync3(dir)) {
4188
4479
  if (n.includes(".corrupt.")) {
@@ -4192,7 +4483,7 @@ function gatherStoreStats(store, stateRoot, project) {
4192
4483
  if (!n.endsWith(".json"))
4193
4484
  continue;
4194
4485
  try {
4195
- JSON.parse(readFileSync8(join11(dir, n), "utf-8"));
4486
+ JSON.parse(readFileSync9(join12(dir, n), "utf-8"));
4196
4487
  } catch {
4197
4488
  corruptCount++;
4198
4489
  }
@@ -4207,7 +4498,7 @@ function gatherResults(resultsDir) {
4207
4498
  try {
4208
4499
  for (const n of readdirSync3(resultsDir)) {
4209
4500
  try {
4210
- const s = statSync3(join11(resultsDir, n));
4501
+ const s = statSync3(join12(resultsDir, n));
4211
4502
  if (s.isFile()) {
4212
4503
  fileCount++;
4213
4504
  totalBytes += s.size;
@@ -4227,19 +4518,20 @@ var init_snapshot_gather = __esm({
4227
4518
  });
4228
4519
 
4229
4520
  // packages/core/dist/daemon/start.js
4230
- import { join as join12, dirname as dirname3 } from "path";
4521
+ import { join as join13, dirname as dirname3 } from "path";
4231
4522
  import { readdir } from "fs/promises";
4232
4523
  function startDaemon(ctx, opts, pkgVersion) {
4233
4524
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
4234
4525
  const { daemonCmux } = ctx;
4235
4526
  const probes = createProbes(ctx);
4236
- const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats } = createDelivery(ctx, daemonCmux);
4527
+ const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
4528
+ const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats, inFlightDelivery } = createDelivery(ctx, daemonCmux, surfaceProbe);
4237
4529
  const baseNotify = opts.notify ?? defaultNotify;
4238
4530
  const notify = ctx.telegramBridge ? async (args) => {
4239
4531
  await baseNotify(args);
4240
4532
  ctx.telegramBridge.pushLifecycle(args.project, args.event);
4241
4533
  } : baseNotify;
4242
- const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
4534
+ ctx.notify = notify;
4243
4535
  const ingest = (project) => (e) => void ctx.d.handle({ kind: "event", project, event: e });
4244
4536
  const d = createDaemon({
4245
4537
  store,
@@ -4303,7 +4595,7 @@ function startDaemon(ctx, opts, pkgVersion) {
4303
4595
  return out;
4304
4596
  }
4305
4597
  async function gatherSnapshotInputs(now) {
4306
- const logPath2 = join12(dirname3(stateRoot), "squadrantd.log");
4598
+ const logPath2 = join13(dirname3(stateRoot), "squadrantd.log");
4307
4599
  const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4308
4600
  const projects = await Promise.all(tier2Projects.map(async (project) => {
4309
4601
  const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
@@ -4396,6 +4688,33 @@ function startDaemon(ctx, opts, pkgVersion) {
4396
4688
  })();
4397
4689
  const server = createServer2(ctx, { buildHealth, gatherSnapshotInputs, cancelPromotionsFor, broadcast });
4398
4690
  log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
4691
+ const bootTs = (/* @__PURE__ */ new Date()).toISOString();
4692
+ {
4693
+ const sendDownAlert = (minutes, reasonText) => {
4694
+ const text = `\u26A0\uFE0F daemon was down for ${minutes} min (last exit reason=${reasonText})`;
4695
+ const alertProjects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4696
+ for (const project of alertProjects) {
4697
+ appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`boot-gap alert failed project=${project}: ${e.message}`));
4698
+ }
4699
+ };
4700
+ const { marker, gapMs } = consumeExitMarker(stateRoot);
4701
+ const prevRunning = readRunningMarker(stateRoot);
4702
+ if (marker) {
4703
+ log(`previous exit ts=${marker.ts} reason=${marker.reason} gap=${((gapMs ?? 0) / 1e3).toFixed(1)}s`);
4704
+ if ((gapMs ?? 0) > 6e4)
4705
+ sendDownAlert(Math.round((gapMs ?? 0) / 6e4), marker.reason);
4706
+ } else if (prevRunning) {
4707
+ const lastHeartbeatMs = new Date(prevRunning.lastHeartbeatTs).getTime();
4708
+ const uncleanGapMs = Math.max(0, Date.now() - lastHeartbeatMs);
4709
+ log(`previous exit: UNCLEAN (no marker; last heartbeat ${prevRunning.lastHeartbeatTs}, gap=${(uncleanGapMs / 1e3).toFixed(1)}s)`);
4710
+ if (uncleanGapMs > 6e4) {
4711
+ sendDownAlert(Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
4712
+ }
4713
+ } else {
4714
+ log("previous exit: none (clean or first boot)");
4715
+ }
4716
+ writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: bootTs }, log);
4717
+ }
4399
4718
  let deliveryTick = initialDeliveryTick;
4400
4719
  let probeTick;
4401
4720
  if (daemonCmux) {
@@ -4436,9 +4755,10 @@ function startDaemon(ctx, opts, pkgVersion) {
4436
4755
  keepCount: opts.mailboxConfig?.keepCount ?? 3
4437
4756
  };
4438
4757
  let rotationTimer;
4758
+ let rotationTick;
4439
4759
  if (rotationInterval > 0) {
4440
- const inboxPath = join12(stateRoot, "inbox");
4441
- rotationTimer = setInterval(async () => {
4760
+ const inboxPath = join13(stateRoot, "inbox");
4761
+ rotationTick = async () => {
4442
4762
  try {
4443
4763
  let entries;
4444
4764
  try {
@@ -4451,13 +4771,23 @@ function startDaemon(ctx, opts, pkgVersion) {
4451
4771
  await rotateIfNeeded({ stateRoot, project, ...mboxCfg });
4452
4772
  } catch (e) {
4453
4773
  log(`rotation timer error: ${e.message}`);
4774
+ } finally {
4775
+ writeRunningMarker(stateRoot, { pid: process.pid, bootTs, lastHeartbeatTs: (/* @__PURE__ */ new Date()).toISOString() }, log);
4454
4776
  }
4777
+ };
4778
+ rotationTimer = setInterval(() => {
4779
+ void rotationTick();
4455
4780
  }, rotationInterval);
4456
4781
  rotationTimer.unref?.();
4457
4782
  }
4458
4783
  return {
4459
4784
  stop(reason = "requested") {
4460
- log(`exit pid=${process.pid} reason=${reason}`);
4785
+ const ppid = process.ppid;
4786
+ const uptimeMs = Math.round(process.uptime() * 1e3);
4787
+ const inFlight = inFlightDelivery();
4788
+ log(`exit pid=${process.pid} reason=${reason} ppid=${ppid} launchd=${ppid === 1} uptimeMs=${uptimeMs} inFlightDelivery=${inFlight ? `${inFlight.project}#${inFlight.seq}(defers=${inFlight.deferCount})` : "none"}`);
4789
+ writeExitMarker(stateRoot, { ts: (/* @__PURE__ */ new Date()).toISOString(), pid: process.pid, reason, ppid, uptimeMs, inFlightDelivery: inFlight }, log);
4790
+ removeRunningMarker(stateRoot, log);
4461
4791
  if (deliveryTimer)
4462
4792
  clearInterval(deliveryTimer);
4463
4793
  if (probeTimer)
@@ -4486,7 +4816,8 @@ function startDaemon(ctx, opts, pkgVersion) {
4486
4816
  }));
4487
4817
  },
4488
4818
  tickDelivery: deliveryTick,
4489
- tickProbe: probeTick
4819
+ tickProbe: probeTick,
4820
+ tickRotation: rotationTick
4490
4821
  };
4491
4822
  }
4492
4823
  var CURSOR_SUBSCRIBER2, SNAPSHOT_LOG_WINDOW_MS;
@@ -4498,6 +4829,7 @@ var init_start = __esm({
4498
4829
  init_gates();
4499
4830
  init_server();
4500
4831
  init_mailbox();
4832
+ init_exit_marker();
4501
4833
  init_liveness2();
4502
4834
  init_dist();
4503
4835
  init_snapshot_gather();
@@ -5492,9 +5824,9 @@ var init_bridge = __esm({
5492
5824
 
5493
5825
  // packages/core/dist/restart-daemon.js
5494
5826
  import { execFileSync as execFileSync4 } from "child_process";
5495
- import { existsSync as existsSync9 } from "fs";
5827
+ import { existsSync as existsSync10 } from "fs";
5496
5828
  function defaultIsRunning() {
5497
- return existsSync9(DAEMON_SOCK_PATH);
5829
+ return existsSync10(DAEMON_SOCK_PATH);
5498
5830
  }
5499
5831
  function defaultRunKickstart() {
5500
5832
  const uid = process.getuid?.() ?? 0;
@@ -6032,6 +6364,76 @@ var init_side_session = __esm({
6032
6364
  }
6033
6365
  });
6034
6366
 
6367
+ // packages/core/dist/crew-answer.js
6368
+ function describeOptions(options) {
6369
+ return options.map((o) => ` ${o.highlighted ? "\u276F" : " "} ${o.index}. ${o.label}`).join("\n");
6370
+ }
6371
+ function resolveOption(options, selector) {
6372
+ const trimmed = selector.trim();
6373
+ if (/^\d+$/.test(trimmed)) {
6374
+ const byIndex = options.find((o) => o.index === Number(trimmed));
6375
+ if (!byIndex) {
6376
+ throw new Error(`No option ${trimmed} in the visible prompt. Visible options:
6377
+ ${describeOptions(options)}`);
6378
+ }
6379
+ return byIndex;
6380
+ }
6381
+ const lower = trimmed.toLowerCase();
6382
+ const exact = options.filter((o) => o.label.toLowerCase() === lower);
6383
+ if (exact.length === 1)
6384
+ return exact[0];
6385
+ if (exact.length > 1) {
6386
+ throw new Error(`Option text "${selector}" matches multiple options ambiguously:
6387
+ ${describeOptions(exact)}`);
6388
+ }
6389
+ const prefix = options.filter((o) => o.label.toLowerCase().startsWith(lower));
6390
+ if (prefix.length === 1)
6391
+ return prefix[0];
6392
+ if (prefix.length > 1) {
6393
+ throw new Error(`Option text "${selector}" matches multiple options ambiguously:
6394
+ ${describeOptions(prefix)}`);
6395
+ }
6396
+ throw new Error(`No option matches "${selector}". Visible options:
6397
+ ${describeOptions(options)}`);
6398
+ }
6399
+ async function runCrewAnswer(project, name, option, runtime, workspaceId, deps, opts) {
6400
+ const crew = await findCrewPane(runtime, workspaceId, project, name);
6401
+ if (!crew) {
6402
+ throw new Error(`Crew '${name}' not found for ${project}. Run 'squadrant crew list ${project}'.`);
6403
+ }
6404
+ const options = await deps.readModalOptions(crew);
6405
+ if (!options) {
6406
+ 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.`);
6407
+ }
6408
+ const target = resolveOption(options, option);
6409
+ if (opts?.expect && !target.label.toLowerCase().includes(opts.expect.toLowerCase())) {
6410
+ 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}'.
6411
+ Visible options:
6412
+ ${describeOptions(options)}`);
6413
+ }
6414
+ const log = deps.log ?? (() => {
6415
+ });
6416
+ log(`\u2192 selecting ${target.index}. "${target.label}"`);
6417
+ const current = options.find((o) => o.highlighted) ?? options[0];
6418
+ const steps = target.index - current.index;
6419
+ const key = steps >= 0 ? "Down" : "Up";
6420
+ for (let i = 0; i < Math.abs(steps); i++) {
6421
+ await runtime.sendKeyToPane(crew, key);
6422
+ }
6423
+ await runtime.sendKeyToPane(crew, "Enter");
6424
+ if (opts?.text) {
6425
+ await runtime.pasteToPane(crew, opts.text);
6426
+ await runtime.sendKeyToPane(crew, "Enter");
6427
+ }
6428
+ const after = await deps.readModalOptions(crew);
6429
+ return { selected: target, closed: after === null };
6430
+ }
6431
+ var init_crew_answer = __esm({
6432
+ "packages/core/dist/crew-answer.js"() {
6433
+ init_crew_spawn();
6434
+ }
6435
+ });
6436
+
6035
6437
  // packages/core/dist/lifecycle-source.js
6036
6438
  function reduceLifecycle(prev, next) {
6037
6439
  if (next.origin === "agent") {
@@ -6053,6 +6455,326 @@ var init_lifecycle_source = __esm({
6053
6455
  }
6054
6456
  });
6055
6457
 
6458
+ // packages/core/dist/events/fact.js
6459
+ function stampFact(raw, id) {
6460
+ return { ...raw, ...id };
6461
+ }
6462
+ var init_fact = __esm({
6463
+ "packages/core/dist/events/fact.js"() {
6464
+ }
6465
+ });
6466
+
6467
+ // packages/core/dist/events/log.js
6468
+ var FactLog;
6469
+ var init_log = __esm({
6470
+ "packages/core/dist/events/log.js"() {
6471
+ FactLog = class {
6472
+ capacity;
6473
+ buffers = /* @__PURE__ */ new Map();
6474
+ constructor(opts = {}) {
6475
+ this.capacity = opts.capacity ?? 256;
6476
+ }
6477
+ push(fact) {
6478
+ let buf = this.buffers.get(fact.taskId);
6479
+ if (!buf) {
6480
+ buf = [];
6481
+ this.buffers.set(fact.taskId, buf);
6482
+ }
6483
+ buf.push(fact);
6484
+ while (buf.length > this.capacity)
6485
+ buf.shift();
6486
+ }
6487
+ /** Oldest-first snapshot. A fresh array; later pushes never grow it. */
6488
+ recent(taskId) {
6489
+ return [...this.buffers.get(taskId) ?? []];
6490
+ }
6491
+ /** Newline-delimited JSON, one fact per line, oldest first. */
6492
+ serialize(taskId) {
6493
+ return this.recent(taskId).map((f) => JSON.stringify(f)).join("\n") + "\n";
6494
+ }
6495
+ /** Release a finished crew's buffer. */
6496
+ drop(taskId) {
6497
+ this.buffers.delete(taskId);
6498
+ }
6499
+ };
6500
+ }
6501
+ });
6502
+
6503
+ // packages/core/dist/events/invariant.js
6504
+ function freshTrace() {
6505
+ return {
6506
+ depth: 0,
6507
+ oldestOpenAt: null,
6508
+ stallReported: false,
6509
+ unknownSeen: 0,
6510
+ liveness: /* @__PURE__ */ new Map()
6511
+ };
6512
+ }
6513
+ function checkFact(trace, fact, opts) {
6514
+ const out = [];
6515
+ if (fact.origin === "inferred" && TERMINALISING.has(fact.kind)) {
6516
+ out.push(v("I4", `inferred fact "${fact.kind}" from ${fact.source} cannot terminalise alone`, fact));
6517
+ }
6518
+ switch (fact.kind) {
6519
+ case "tool.opened":
6520
+ if (trace.depth === 0) {
6521
+ trace.oldestOpenAt = fact.at;
6522
+ trace.stallReported = false;
6523
+ }
6524
+ trace.depth += 1;
6525
+ break;
6526
+ case "tool.closed":
6527
+ if (trace.depth === 0) {
6528
+ out.push(v("I1", `tool.closed from ${fact.source} with no open tool`, fact));
6529
+ } else {
6530
+ trace.depth -= 1;
6531
+ if (trace.depth === 0) {
6532
+ trace.oldestOpenAt = null;
6533
+ trace.stallReported = false;
6534
+ }
6535
+ }
6536
+ break;
6537
+ case "turn.ended":
6538
+ if (trace.depth > 0) {
6539
+ out.push(v("I2", `turn.ended with ${trace.depth} tool call(s) still open`, fact));
6540
+ trace.depth = 0;
6541
+ trace.oldestOpenAt = null;
6542
+ trace.stallReported = false;
6543
+ }
6544
+ break;
6545
+ case "unknown":
6546
+ trace.unknownSeen += 1;
6547
+ out.push(v("I5", `unrecognised frame "${fact.name}" from ${fact.source}`, fact));
6548
+ break;
6549
+ case "process.observed": {
6550
+ const prior = [...trace.liveness.entries()].find(([src, s]) => src !== fact.source && s.alive !== fact.alive && fact.at - s.at <= (opts.disagreeWindowMs ?? -1));
6551
+ if (prior) {
6552
+ out.push(v("I6", `liveness disagreement: ${prior[0]} said alive=${prior[1].alive}, ${fact.source} says alive=${fact.alive}`, fact));
6553
+ }
6554
+ trace.liveness.set(fact.source, { alive: fact.alive, at: fact.at });
6555
+ break;
6556
+ }
6557
+ default:
6558
+ break;
6559
+ }
6560
+ if (opts.stallBudgetMs !== void 0 && trace.depth > 0 && trace.oldestOpenAt !== null && !trace.stallReported && fact.at - trace.oldestOpenAt > opts.stallBudgetMs) {
6561
+ trace.stallReported = true;
6562
+ out.push(v("I3", `tool open for ${fact.at - trace.oldestOpenAt}ms, past the stall budget`, fact));
6563
+ }
6564
+ return out;
6565
+ }
6566
+ var v, TERMINALISING;
6567
+ var init_invariant = __esm({
6568
+ "packages/core/dist/events/invariant.js"() {
6569
+ v = (code, message, f) => ({ code, message, taskId: f.taskId, at: f.at });
6570
+ TERMINALISING = /* @__PURE__ */ new Set(["session.ended"]);
6571
+ }
6572
+ });
6573
+
6574
+ // packages/core/dist/events/to-control-event.js
6575
+ function toControlEvent(fact) {
6576
+ if (fact.origin === "inferred" && TERMINALISING2.has(fact.kind))
6577
+ return [];
6578
+ switch (fact.kind) {
6579
+ case "turn.ended":
6580
+ return [{ type: "task.turn.completed", id: fact.taskId, turnId: fact.turnId ?? fact.source }];
6581
+ case "permission.requested":
6582
+ return [{
6583
+ type: "task.approval.requested",
6584
+ id: fact.taskId,
6585
+ requestId: fact.requestId,
6586
+ question: fact.question,
6587
+ kind: fact.tool
6588
+ }];
6589
+ case "input.requested":
6590
+ return [{
6591
+ type: "task.input.requested",
6592
+ id: fact.taskId,
6593
+ requestId: fact.requestId,
6594
+ question: fact.question
6595
+ }];
6596
+ case "session.ended":
6597
+ return [{ type: "task.session.ended", id: fact.taskId }];
6598
+ case "session.started":
6599
+ return [{
6600
+ type: "task.started",
6601
+ id: fact.taskId,
6602
+ ...fact.pid === void 0 ? {} : { pid: fact.pid },
6603
+ ...fact.sessionId === void 0 ? {} : { sessionId: fact.sessionId }
6604
+ }];
6605
+ case "prompt.submitted":
6606
+ return [{ type: "task.first-turn.confirmed", id: fact.taskId }];
6607
+ // Liveness-only. The facade still feeds these to reduceLifecycle; they
6608
+ // simply carry no ControlEvent of their own.
6609
+ case "tool.opened":
6610
+ case "tool.closed":
6611
+ case "activity":
6612
+ case "process.observed":
6613
+ case "unknown":
6614
+ return [];
6615
+ }
6616
+ }
6617
+ var TERMINALISING2;
6618
+ var init_to_control_event = __esm({
6619
+ "packages/core/dist/events/to-control-event.js"() {
6620
+ TERMINALISING2 = /* @__PURE__ */ new Set(["session.ended"]);
6621
+ }
6622
+ });
6623
+
6624
+ // packages/core/dist/events/conformance.js
6625
+ function assert(cond, msg) {
6626
+ if (!cond)
6627
+ throw new Error(`conformance: ${msg}`);
6628
+ }
6629
+ function runAdapterConformance(adapter, samples) {
6630
+ const call = (raw) => adapter.translate(raw);
6631
+ return [
6632
+ {
6633
+ name: `${adapter.name}: never throws on garbage`,
6634
+ run: () => {
6635
+ for (const g of GARBAGE) {
6636
+ try {
6637
+ call(g);
6638
+ } catch (e) {
6639
+ throw new Error(`threw on ${JSON.stringify(g)}: ${String(e)}`);
6640
+ }
6641
+ }
6642
+ }
6643
+ },
6644
+ {
6645
+ name: `${adapter.name}: never returns null or undefined`,
6646
+ run: () => {
6647
+ for (const g of [...GARBAGE, ...samples]) {
6648
+ const out = call(g);
6649
+ assert(Array.isArray(out), `returned a non-array for ${JSON.stringify(g)}`);
6650
+ }
6651
+ }
6652
+ },
6653
+ {
6654
+ name: `${adapter.name}: an unrecognised frame yields unknown, not an empty array`,
6655
+ run: () => {
6656
+ const out = call({ type: "definitely-not-a-real-event-name" });
6657
+ assert(out.length > 0, "silently dropped an unrecognised frame (the #542 shape)");
6658
+ assert(out.every((f) => f.kind === "unknown"), "an unrecognised frame must translate to kind 'unknown'");
6659
+ }
6660
+ },
6661
+ {
6662
+ name: `${adapter.name}: recognises its own samples`,
6663
+ run: () => {
6664
+ for (const s of samples) {
6665
+ const out = call(s);
6666
+ assert(out.length > 0, `produced nothing for its own sample ${JSON.stringify(s)}`);
6667
+ assert(out.some((f) => f.kind !== "unknown"), `failed to recognise its own sample ${JSON.stringify(s)}`);
6668
+ }
6669
+ }
6670
+ },
6671
+ {
6672
+ name: `${adapter.name}: declares a constant origin`,
6673
+ run: () => {
6674
+ assert(adapter.origin === "agent" || adapter.origin === "scan" || adapter.origin === "inferred", `invalid origin "${String(adapter.origin)}"`);
6675
+ }
6676
+ }
6677
+ ];
6678
+ }
6679
+ var GARBAGE;
6680
+ var init_conformance = __esm({
6681
+ "packages/core/dist/events/conformance.js"() {
6682
+ GARBAGE = [
6683
+ null,
6684
+ void 0,
6685
+ 0,
6686
+ "",
6687
+ "not json",
6688
+ [],
6689
+ {},
6690
+ { type: 42 },
6691
+ { type: "definitely-not-a-real-event-name" }
6692
+ ];
6693
+ }
6694
+ });
6695
+
6696
+ // packages/core/dist/events/source.js
6697
+ function createEventsSource(opts) {
6698
+ const now = opts.now ?? (() => Date.now());
6699
+ const log = new FactLog({ capacity: opts.capacity });
6700
+ const adapters = new Map(opts.adapters.map((a) => [a.name, a]));
6701
+ const traces = /* @__PURE__ */ new Map();
6702
+ const seqs = /* @__PURE__ */ new Map();
6703
+ let deps;
6704
+ const traceFor = (taskId) => {
6705
+ let t = traces.get(taskId);
6706
+ if (!t) {
6707
+ t = freshTrace();
6708
+ traces.set(taskId, t);
6709
+ }
6710
+ return t;
6711
+ };
6712
+ const nextSeq = (taskId) => {
6713
+ const n = seqs.get(taskId) ?? 0;
6714
+ seqs.set(taskId, n + 1);
6715
+ return n;
6716
+ };
6717
+ return {
6718
+ name: "events",
6719
+ start(d) {
6720
+ deps = d;
6721
+ },
6722
+ stop() {
6723
+ deps = void 0;
6724
+ },
6725
+ health() {
6726
+ return { active: deps !== void 0, error: null };
6727
+ },
6728
+ recent(taskId) {
6729
+ return log.recent(taskId);
6730
+ },
6731
+ dump(taskId) {
6732
+ return log.serialize(taskId);
6733
+ },
6734
+ ingest(source, raw, hint) {
6735
+ const adapter = adapters.get(source);
6736
+ if (!adapter || !deps)
6737
+ return;
6738
+ const rec = deps.resolve(hint);
6739
+ if (!rec)
6740
+ return;
6741
+ const taskId = rec.id;
6742
+ const at = now();
6743
+ let produced;
6744
+ try {
6745
+ const out = adapter.translate(raw);
6746
+ produced = Array.isArray(out) ? out : [{ kind: "unknown", name: `${source} returned non-array` }];
6747
+ } catch (e) {
6748
+ opts.log?.(`events: adapter ${source} threw: ${String(e)}`);
6749
+ produced = [{ kind: "unknown", name: `${source} threw` }];
6750
+ }
6751
+ for (const rawFact of produced) {
6752
+ const fact = stampFact(rawFact, {
6753
+ seq: nextSeq(taskId),
6754
+ taskId,
6755
+ at,
6756
+ source,
6757
+ origin: adapter.origin
6758
+ });
6759
+ log.push(fact);
6760
+ for (const v2 of checkFact(traceFor(taskId), fact, opts.check ?? {})) {
6761
+ opts.onViolation(v2);
6762
+ }
6763
+ for (const ev of toControlEvent(fact))
6764
+ opts.emit(ev);
6765
+ }
6766
+ }
6767
+ };
6768
+ }
6769
+ var init_source = __esm({
6770
+ "packages/core/dist/events/source.js"() {
6771
+ init_fact();
6772
+ init_log();
6773
+ init_invariant();
6774
+ init_to_control_event();
6775
+ }
6776
+ });
6777
+
6056
6778
  // packages/core/dist/index.js
6057
6779
  var dist_exports = {};
6058
6780
  __export(dist_exports, {
@@ -6066,6 +6788,8 @@ __export(dist_exports, {
6066
6788
  DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS: () => DEFAULT_FIRST_TURN_UNDELIVERED_BUDGET_MS,
6067
6789
  DEFAULT_TASK_TIMEOUT_MS: () => DEFAULT_TASK_TIMEOUT_MS,
6068
6790
  DeferDelivery: () => DeferDelivery,
6791
+ FIRST_TURN_INLINE_MAX_BYTES: () => FIRST_TURN_INLINE_MAX_BYTES,
6792
+ FactLog: () => FactLog,
6069
6793
  GROUP_DISPATCH_WARMUP_POLL_MS: () => GROUP_DISPATCH_WARMUP_POLL_MS,
6070
6794
  GROUP_DISPATCH_WARMUP_TIMEOUT_MS: () => GROUP_DISPATCH_WARMUP_TIMEOUT_MS,
6071
6795
  IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
@@ -6095,9 +6819,11 @@ __export(dist_exports, {
6095
6819
  capAllowed: () => capAllowed,
6096
6820
  capOutput: () => capOutput,
6097
6821
  captainSocketPath: () => captainSocketPath,
6822
+ checkFact: () => checkFact,
6098
6823
  classifyHealth: () => classifyHealth,
6099
6824
  closeWorkItem: () => closeWorkItem,
6100
6825
  computeTemplateHash: () => computeTemplateHash,
6826
+ consumeExitMarker: () => consumeExitMarker,
6101
6827
  createAttach: () => createAttach,
6102
6828
  createCrewPaneReader: () => createCrewPaneReader,
6103
6829
  createDaemon: () => createDaemon,
@@ -6106,6 +6832,7 @@ __export(dist_exports, {
6106
6832
  createDirectCrewPaneReader: () => createDirectCrewPaneReader,
6107
6833
  createDirectSurfaceLivenessProbe: () => createDirectSurfaceLivenessProbe,
6108
6834
  createEnsureCaptainAlive: () => createEnsureCaptainAlive,
6835
+ createEventsSource: () => createEventsSource,
6109
6836
  createInteractiveProbe: () => createInteractiveProbe,
6110
6837
  createIsCaptainAlive: () => createIsCaptainAlive,
6111
6838
  createLaunch: () => createLaunch,
@@ -6138,13 +6865,18 @@ __export(dist_exports, {
6138
6865
  encodeMsg: () => encodeMsg,
6139
6866
  ensureDaemon: () => ensureDaemon,
6140
6867
  evaluateStall: () => evaluateStall,
6868
+ exitMarkerPath: () => exitMarkerPath,
6141
6869
  fallsBackToPane: () => fallsBackToPane,
6870
+ findCrewPane: () => findCrewPane,
6142
6871
  findOpenChildren: () => findOpenChildren,
6143
6872
  findProjectByThread: () => findProjectByThread,
6144
6873
  findWorkItemById: () => findWorkItemById,
6874
+ forceKickstartAndVerify: () => forceKickstartAndVerify,
6145
6875
  formatInbound: () => formatInbound,
6146
6876
  formatInboundReceipt: () => formatInboundReceipt,
6147
6877
  formatLifecycle: () => formatLifecycle,
6878
+ freshTrace: () => freshTrace,
6879
+ getDaemonPid: () => getDaemonPid,
6148
6880
  healCmdFor: () => healCmdFor,
6149
6881
  isAuthorized: () => isAuthorized,
6150
6882
  isBareSpawn: () => isBareSpawn,
@@ -6179,6 +6911,7 @@ __export(dist_exports, {
6179
6911
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
6180
6912
  readCursor: () => readCursor,
6181
6913
  readFromCursor: () => readFromCursor,
6914
+ readRunningMarker: () => readRunningMarker,
6182
6915
  reapCrewChildren: () => reapCrewChildren,
6183
6916
  reapOrphanedCrews: () => reapOrphanedCrews,
6184
6917
  reconcileLiveness: () => reconcileLiveness,
@@ -6187,6 +6920,7 @@ __export(dist_exports, {
6187
6920
  reduce: () => reduce,
6188
6921
  reduceLifecycle: () => reduceLifecycle,
6189
6922
  releaseDaemonLock: () => releaseDaemonLock,
6923
+ removeRunningMarker: () => removeRunningMarker,
6190
6924
  renderPlist: () => renderPlist,
6191
6925
  reregisterDaemon: () => reregisterDaemon,
6192
6926
  resolveAgentBinDirs: () => resolveAgentBinDirs,
@@ -6198,6 +6932,8 @@ __export(dist_exports, {
6198
6932
  resolveSetupUserId: () => resolveSetupUserId,
6199
6933
  restartDaemonIfRunning: () => restartDaemonIfRunning,
6200
6934
  rotateIfNeeded: () => rotateIfNeeded,
6935
+ runAdapterConformance: () => runAdapterConformance,
6936
+ runCrewAnswer: () => runCrewAnswer,
6201
6937
  runCrewClose: () => runCrewClose,
6202
6938
  runCrewList: () => runCrewList,
6203
6939
  runCrewRead: () => runCrewRead,
@@ -6217,6 +6953,7 @@ __export(dist_exports, {
6217
6953
  runTelegramPostSetup: () => runTelegramPostSetup,
6218
6954
  runTelegramSend: () => runTelegramSend,
6219
6955
  runTelegramStatus: () => runTelegramStatus,
6956
+ runningMarkerPath: () => runningMarkerPath,
6220
6957
  sanitizePathForPlist: () => sanitizePathForPlist,
6221
6958
  saveSessions: () => saveSessions,
6222
6959
  saveState: () => saveState,
@@ -6230,18 +6967,22 @@ __export(dist_exports, {
6230
6967
  sideNameFromTitle: () => sideNameFromTitle,
6231
6968
  sideNextAutoName: () => sideNextAutoName,
6232
6969
  sideTitleFor: () => sideTitleFor,
6970
+ stampFact: () => stampFact,
6233
6971
  startDaemon: () => startDaemon,
6234
6972
  startServer: () => startServer,
6235
6973
  stripBotMention: () => stripBotMention,
6236
6974
  surfaceVerdict: () => surfaceVerdict,
6237
6975
  timeoutGate: () => timeoutGate,
6238
6976
  titleFor: () => titleFor,
6977
+ toControlEvent: () => toControlEvent,
6239
6978
  topicKey: () => topicKey,
6240
6979
  topicName: () => topicName,
6241
6980
  tryAcquireDaemonLock: () => tryAcquireDaemonLock,
6242
6981
  waitForCaptainDelivery: () => waitForCaptainDelivery,
6243
6982
  waitForWarmup: () => waitForWarmup,
6244
6983
  writeCursor: () => writeCursor,
6984
+ writeExitMarker: () => writeExitMarker,
6985
+ writeRunningMarker: () => writeRunningMarker,
6245
6986
  writeTelegramConfig: () => writeTelegramConfig
6246
6987
  });
6247
6988
  var init_dist2 = __esm({
@@ -6264,6 +7005,7 @@ var init_dist2 = __esm({
6264
7005
  init_attach();
6265
7006
  init_start();
6266
7007
  init_delivery_loop();
7008
+ init_exit_marker();
6267
7009
  init_interactive_probe();
6268
7010
  init_captain_delivery();
6269
7011
  init_defer_delivery();
@@ -6277,7 +7019,14 @@ var init_dist2 = __esm({
6277
7019
  init_launch_workspace();
6278
7020
  init_side_session();
6279
7021
  init_crew_spawn();
7022
+ init_crew_answer();
6280
7023
  init_lifecycle_source();
7024
+ init_fact();
7025
+ init_log();
7026
+ init_invariant();
7027
+ init_to_control_event();
7028
+ init_conformance();
7029
+ init_source();
6281
7030
  init_control_channel();
6282
7031
  init_captain_channel();
6283
7032
  }
@@ -6294,9 +7043,11 @@ init_dist2();
6294
7043
  init_dist2();
6295
7044
  init_dist2();
6296
7045
  init_dist2();
6297
- import { join as join20, dirname as dirname4, resolve as resolve3 } from "path";
7046
+ init_dist2();
7047
+ import { join as join22, dirname as dirname4, resolve as resolve3 } from "path";
7048
+ import { homedir as homedir13 } from "os";
6298
7049
  import { fileURLToPath as fileURLToPath3 } from "url";
6299
- import { readFileSync as readFileSync14, statSync as statSync4, existsSync as existsSync12 } from "fs";
7050
+ import { readFileSync as readFileSync15, statSync as statSync4, existsSync as existsSync13 } from "fs";
6300
7051
 
6301
7052
  // packages/agents/dist/drivers/claude.js
6302
7053
  import { execSync as execSync2 } from "child_process";
@@ -6612,10 +7363,10 @@ function toSnapshot(ev) {
6612
7363
  // packages/agents/dist/codex/config.js
6613
7364
  import { readFile as readFile6 } from "fs/promises";
6614
7365
  import { homedir as homedir7 } from "os";
6615
- import { join as join13 } from "path";
7366
+ import { join as join14 } from "path";
6616
7367
  async function resolveCodexModel() {
6617
- const home = process.env["CODEX_HOME"] ?? join13(homedir7(), ".codex");
6618
- const configPath = join13(home, "config.toml");
7368
+ const home = process.env["CODEX_HOME"] ?? join14(homedir7(), ".codex");
7369
+ const configPath = join14(home, "config.toml");
6619
7370
  let text;
6620
7371
  try {
6621
7372
  text = await readFile6(configPath, "utf8");
@@ -6931,9 +7682,9 @@ ${directive}` : directive;
6931
7682
  function withTimeout(p, ms, msg) {
6932
7683
  return new Promise((resolve4, reject) => {
6933
7684
  const t = setTimeout(() => reject(new Error(msg)), ms);
6934
- p.then((v) => {
7685
+ p.then((v2) => {
6935
7686
  clearTimeout(t);
6936
- resolve4(v);
7687
+ resolve4(v2);
6937
7688
  }, (e) => {
6938
7689
  clearTimeout(t);
6939
7690
  reject(e);
@@ -6942,6 +7693,7 @@ function withTimeout(p, ms, msg) {
6942
7693
  }
6943
7694
 
6944
7695
  // packages/agents/dist/opencode/sse-bridge.js
7696
+ var IGNORED_FRAME = /^(message|storage|file|lsp|installation)\./;
6945
7697
  var OpencodeSseBridge = class {
6946
7698
  controllers = /* @__PURE__ */ new Map();
6947
7699
  /** taskId → the crew's opencode server port (for permission-reply POSTs). */
@@ -7078,6 +7830,10 @@ var OpencodeSseBridge = class {
7078
7830
  return;
7079
7831
  }
7080
7832
  if (json?.type === "session.idle") {
7833
+ if (this.deps.ingest) {
7834
+ this.deps.ingest(json, taskId);
7835
+ return;
7836
+ }
7081
7837
  this.deps.emit({
7082
7838
  type: "task.turn.completed",
7083
7839
  id: taskId,
@@ -7087,6 +7843,10 @@ var OpencodeSseBridge = class {
7087
7843
  const p = json.properties;
7088
7844
  if (p?.id && p?.sessionID) {
7089
7845
  this.pendingPermByTask.set(taskId, { permID: p.id, sessionID: p.sessionID });
7846
+ if (this.deps.ingest) {
7847
+ this.deps.ingest(json, taskId);
7848
+ return;
7849
+ }
7090
7850
  const tool = p.permission ?? "a tool";
7091
7851
  const cmd = Array.isArray(p.patterns) && p.patterns.length ? `: ${p.patterns.join(" ")}` : "";
7092
7852
  this.deps.emit({
@@ -7096,18 +7856,31 @@ var OpencodeSseBridge = class {
7096
7856
  question: `opencode requests permission to run ${tool}${cmd}`,
7097
7857
  kind: tool
7098
7858
  });
7859
+ } else if (this.deps.ingest) {
7860
+ this.deps.ingest(json, taskId);
7099
7861
  }
7100
7862
  } else if (json?.type === "permission.replied") {
7101
7863
  this.pendingPermByTask.delete(taskId);
7864
+ this.deps.ingest?.(json, taskId);
7865
+ } else if (!IGNORED_FRAME.test(json?.type ?? "")) {
7866
+ this.deps.ingest?.(json, taskId);
7102
7867
  }
7103
7868
  }
7869
+ /** Test seam: exercise handleLine without an SSE stream. */
7870
+ handleLineForTest(rawLine, taskId) {
7871
+ this.handleLine(taskId, rawLine);
7872
+ }
7873
+ /** Test seam: read pendingPermByTask without exposing it publicly. */
7874
+ pendingPermForTest(taskId) {
7875
+ return this.pendingPermByTask.get(taskId);
7876
+ }
7104
7877
  };
7105
7878
 
7106
7879
  // packages/agents/dist/interactive/claude.js
7107
7880
  import { execSync as execSync6 } from "child_process";
7108
- import { readFileSync as readFileSync9 } from "fs";
7881
+ import { readFileSync as readFileSync10 } from "fs";
7109
7882
  import { homedir as homedir8 } from "os";
7110
- import { join as join14 } from "path";
7883
+ import { join as join15 } from "path";
7111
7884
  var nextAskUserQuestionRequestId = Date.now();
7112
7885
 
7113
7886
  // packages/agents/dist/headless/types.js
@@ -7271,14 +8044,14 @@ function runHeadless(opts) {
7271
8044
  }
7272
8045
 
7273
8046
  // packages/agents/dist/claude/peer-registry-source.js
7274
- import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "fs";
7275
- import { join as join16 } from "path";
8047
+ import { readdirSync as readdirSync4, readFileSync as readFileSync11 } from "fs";
8048
+ import { join as join17 } from "path";
7276
8049
 
7277
8050
  // packages/agents/dist/claude/registry.js
7278
8051
  import fs14 from "fs";
7279
- import { join as join15 } from "path";
8052
+ import { join as join16 } from "path";
7280
8053
  import { homedir as homedir9 } from "os";
7281
- var CLAUDE_SESSIONS_DIR = join15(homedir9(), ".claude", "sessions");
8054
+ var CLAUDE_SESSIONS_DIR = join16(homedir9(), ".claude", "sessions");
7282
8055
  var PID_JSON = /^(\d+)\.json$/;
7283
8056
  function parseRegistryDir(files, readFile7) {
7284
8057
  const out = [];
@@ -7324,7 +8097,7 @@ function readClaudeStatusBySocketPath(socketPath) {
7324
8097
  } catch {
7325
8098
  return void 0;
7326
8099
  }
7327
- const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join15(CLAUDE_SESSIONS_DIR, name), "utf8"));
8100
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join16(CLAUDE_SESSIONS_DIR, name), "utf8"));
7328
8101
  const entry = entries.find((e) => e.messagingSocketPath === socketPath);
7329
8102
  if (!entry)
7330
8103
  return void 0;
@@ -7356,7 +8129,7 @@ var ClaudePeerRegistrySource = class {
7356
8129
  log;
7357
8130
  constructor(o = {}) {
7358
8131
  this.readdir = o.readdir ?? (() => readdirSync4(CLAUDE_SESSIONS_DIR));
7359
- this.readFile = o.readFile ?? ((n) => readFileSync10(join16(CLAUDE_SESSIONS_DIR, n), "utf8"));
8132
+ this.readFile = o.readFile ?? ((n) => readFileSync11(join17(CLAUDE_SESSIONS_DIR, n), "utf8"));
7360
8133
  this.isAlive = o.isAlive ?? defaultIsAlive;
7361
8134
  this.now = o.now ?? Date.now;
7362
8135
  this.pollMs = o.pollMs ?? 2e3;
@@ -7668,70 +8441,41 @@ var ClaudeReceiptListener = class {
7668
8441
  }
7669
8442
  };
7670
8443
 
7671
- // packages/agents/dist/opencode/control-source.js
7672
- var OpencodeControlSource = class {
7673
- name = "opencode-control";
7674
- deps;
7675
- active = false;
7676
- cache = /* @__PURE__ */ new Map();
7677
- start(deps) {
7678
- this.deps = deps;
7679
- this.active = true;
7680
- }
7681
- stop() {
7682
- this.deps = void 0;
7683
- this.active = false;
7684
- this.cache.clear();
7685
- }
7686
- /** Push-only source — no fallible startup of its own. */
7687
- health() {
7688
- return { active: this.active, error: null };
7689
- }
7690
- /** Liveness floor: origin must be "scan" and must not assert needsInput. */
7691
- snapshot(taskId) {
7692
- const s = this.cache.get(taskId);
7693
- if (!s)
7694
- return void 0;
7695
- return { ...s, origin: "scan", state: s.state === "needsInput" ? "running" : s.state };
7696
- }
7697
- /**
7698
- * Feed one ControlEvent from OpencodeSseBridge into the port.
7699
- * Wired in squadrantd.ts as: emit = (ev) => { source.observe(ev); …existing… }
7700
- */
7701
- observe(ev) {
7702
- if (!this.deps)
7703
- return;
7704
- const snap = toSnapshot2(ev);
7705
- if (!snap)
7706
- return;
7707
- this.cache.set(snap.taskId, snap);
7708
- this.deps.report(snap);
7709
- }
7710
- };
7711
- function toSnapshot2(ev) {
7712
- const now = Date.now();
7713
- switch (ev.type) {
7714
- // A permission was answered on the bus and the turn resumed.
7715
- case "task.started":
7716
- return { taskId: ev.id, state: "running", alive: true, origin: "agent", at: now };
7717
- // session.idle — the turn finished. Liveness, NOT completion (anti-#2576).
7718
- case "task.turn.completed":
7719
- return { taskId: ev.id, state: "idle", alive: true, origin: "agent", at: now };
7720
- // permission.asked — opencode STATES it is gated. No guessing from pixels.
7721
- case "task.approval.requested":
7722
- return {
7723
- taskId: ev.id,
7724
- state: "needsInput",
7725
- alive: true,
7726
- origin: "agent",
7727
- at: now,
7728
- detail: { note: ev.question, reason: ev.kind }
7729
- };
7730
- // Terminal (task.done/blocked/cancelled) and notify-only events are ignored:
7731
- // terminal state comes exclusively from `squadrant crew signal`.
7732
- default:
7733
- return null;
7734
- }
8444
+ // packages/agents/dist/opencode/fact-adapter.js
8445
+ function createOpencodeFactAdapter(deps) {
8446
+ return {
8447
+ name: "opencode-sse",
8448
+ origin: "agent",
8449
+ translate(raw) {
8450
+ const f = typeof raw === "object" && raw !== null ? raw : {};
8451
+ const type = typeof f.type === "string" ? f.type : void 0;
8452
+ if (type === void 0)
8453
+ return [{ kind: "unknown", name: "non-object" }];
8454
+ const p = f.properties ?? {};
8455
+ if (type === "session.idle") {
8456
+ return [{
8457
+ kind: "turn.ended",
8458
+ turnId: typeof p.sessionID === "string" ? p.sessionID : void 0
8459
+ }];
8460
+ }
8461
+ if (type === "permission.asked") {
8462
+ if (typeof p.id !== "string" || typeof p.sessionID !== "string") {
8463
+ return [{ kind: "unknown", name: "permission.asked:incomplete" }];
8464
+ }
8465
+ const tool = typeof p.permission === "string" ? p.permission : "a tool";
8466
+ const cmd = Array.isArray(p.patterns) && p.patterns.length ? `: ${p.patterns.join(" ")}` : "";
8467
+ return [{
8468
+ kind: "permission.requested",
8469
+ question: `opencode requests permission to run ${tool}${cmd}`,
8470
+ requestId: deps.nextRequestId(),
8471
+ tool
8472
+ }];
8473
+ }
8474
+ if (type === "permission.replied")
8475
+ return [{ kind: "activity" }];
8476
+ return [{ kind: "unknown", name: type }];
8477
+ }
8478
+ };
7735
8479
  }
7736
8480
 
7737
8481
  // packages/workspaces/dist/runtimes/cmux.js
@@ -8133,7 +8877,10 @@ function createCmuxDriver() {
8133
8877
  let screen = "";
8134
8878
  try {
8135
8879
  screen = await cmux(["read-screen", "--workspace", ws, "--surface", sf]);
8136
- } catch {
8880
+ } catch (e) {
8881
+ process.stderr.write(`[squadrant] read-screen failed for ${ws}/${sf}: ${e.message}
8882
+ `);
8883
+ throw new DeferDelivery(null, "probe-failed");
8137
8884
  }
8138
8885
  const draft = parseDraftFromScreen(screen);
8139
8886
  if (draft === null)
@@ -8333,7 +9080,7 @@ var NotifierRegistry = class {
8333
9080
 
8334
9081
  // packages/workspaces/dist/workspaces/obsidian.js
8335
9082
  import fs15 from "fs/promises";
8336
- import { existsSync as existsSync10 } from "fs";
9083
+ import { existsSync as existsSync11 } from "fs";
8337
9084
  import path19 from "path";
8338
9085
 
8339
9086
  // packages/workspaces/dist/workspaces/registry.js
@@ -8449,6 +9196,16 @@ var CmuxEventsBridge = class {
8449
9196
  }
8450
9197
  if (f?.type !== "event" || f.category !== "agent")
8451
9198
  return;
9199
+ if (f.name === "agent.hook.PostToolUse") {
9200
+ const p2 = f.payload ?? {};
9201
+ if (p2.phase === "received")
9202
+ return;
9203
+ const rec2 = this.deps.resolve({ cwd: p2.cwd, source: p2._source ?? f.source, sessionId: p2.session_id });
9204
+ if (!rec2)
9205
+ return;
9206
+ this.deps.emit({ type: "task.progress", id: rec2.id, note: f.name });
9207
+ return;
9208
+ }
8452
9209
  const runState = f.name ? deriveRunState(f.name) : null;
8453
9210
  if (!runState)
8454
9211
  return;
@@ -8477,8 +9234,8 @@ var CmuxEventsBridge = class {
8477
9234
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
8478
9235
  init_dist2();
8479
9236
  init_dist();
8480
- import { readdirSync as readdirSync5, readFileSync as readFileSync11 } from "fs";
8481
- import { join as join17 } from "path";
9237
+ import { readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
9238
+ import { join as join18 } from "path";
8482
9239
  import { homedir as homedir10 } from "os";
8483
9240
 
8484
9241
  // packages/workspaces/dist/cmux-daemon/store-fingerprint.js
@@ -8576,7 +9333,7 @@ function readLivenessSnapshot(files, readFile7, projects, argvRecovery = {}) {
8576
9333
 
8577
9334
  // packages/workspaces/dist/cmux-daemon/daemon-cmux.js
8578
9335
  async function readCmuxLiveness() {
8579
- const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join17(homedir10(), ".cmuxterm");
9336
+ const dir = process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join18(homedir10(), ".cmuxterm");
8580
9337
  const projects = loadConfig().projects;
8581
9338
  let files;
8582
9339
  try {
@@ -8584,7 +9341,7 @@ async function readCmuxLiveness() {
8584
9341
  } catch (e) {
8585
9342
  throw new Error(`liveness: could not read cmux state dir ${dir}: ${e.message}`);
8586
9343
  }
8587
- return readLivenessSnapshot(files, (f) => readFileSync11(join17(dir, f), "utf-8"), projects);
9344
+ return readLivenessSnapshot(files, (f) => readFileSync12(join18(dir, f), "utf-8"), projects);
8588
9345
  }
8589
9346
  var DaemonCmux = class {
8590
9347
  driver;
@@ -8647,9 +9404,9 @@ var DaemonCmux = class {
8647
9404
  };
8648
9405
 
8649
9406
  // packages/workspaces/dist/cmux-daemon/cmux-store-source.js
8650
- import { join as join18 } from "path";
9407
+ import { join as join19 } from "path";
8651
9408
  import { homedir as homedir11 } from "os";
8652
- import { watch, readdirSync as readdirSync6, readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
9409
+ import { watch, readdirSync as readdirSync6, readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
8653
9410
  var CmuxStoreSource = class {
8654
9411
  name = "cmux-store";
8655
9412
  stateDir;
@@ -8670,12 +9427,12 @@ var CmuxStoreSource = class {
8670
9427
  active = false;
8671
9428
  lastError = null;
8672
9429
  constructor(opts = {}) {
8673
- this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join18(homedir11(), ".cmuxterm");
9430
+ this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join19(homedir11(), ".cmuxterm");
8674
9431
  this.debounceMs = opts.debounceMs ?? 50;
8675
9432
  this.isPidAlive = opts.isPidAlive ?? defaultIsPidAlive2;
8676
9433
  this.listFiles = opts.listFiles ?? defaultListFiles;
8677
9434
  this.readFile = opts.readFile ?? defaultReadFile;
8678
- this.fileExists = opts.fileExists ?? existsSync11;
9435
+ this.fileExists = opts.fileExists ?? existsSync12;
8679
9436
  this.watchDir = opts.watchDir ?? defaultWatchDir;
8680
9437
  this.scheduleTimer = opts.scheduleTimer ?? setTimeout;
8681
9438
  this.cancelTimer = opts.cancelTimer ?? clearTimeout;
@@ -8732,7 +9489,7 @@ var CmuxStoreSource = class {
8732
9489
  }
8733
9490
  scanFile(filename) {
8734
9491
  const deps = this.deps;
8735
- const filePath = join18(this.stateDir, filename);
9492
+ const filePath = join19(this.stateDir, filename);
8736
9493
  const lockPath = `${filePath}.lock`;
8737
9494
  if (this.fileExists(lockPath)) {
8738
9495
  this.log(`cmux-store: skipping ${filename} (locked)`);
@@ -8805,7 +9562,7 @@ function defaultListFiles(dir) {
8805
9562
  }
8806
9563
  function defaultReadFile(path21) {
8807
9564
  try {
8808
- return readFileSync12(path21, "utf-8");
9565
+ return readFileSync13(path21, "utf-8");
8809
9566
  } catch {
8810
9567
  return void 0;
8811
9568
  }
@@ -8820,9 +9577,9 @@ function defaultWatchDir(dir, cb) {
8820
9577
  }
8821
9578
 
8822
9579
  // packages/workspaces/dist/native-hooks/native-hook-source.js
8823
- import { join as join19 } from "path";
9580
+ import { join as join20 } from "path";
8824
9581
  import { homedir as homedir12 } from "os";
8825
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
9582
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
8826
9583
  var CLAUDE_HOOK_EVENTS = [
8827
9584
  ["SessionStart", "session-start"],
8828
9585
  ["UserPromptSubmit", "prompt-submit"],
@@ -8834,7 +9591,7 @@ var CLAUDE_HOOK_EVENTS = [
8834
9591
  ];
8835
9592
  var DEFAULT_HOOK_CMD = "squadrant hooks";
8836
9593
  function installClaudeHooks(opts = {}) {
8837
- const settingsPath = opts.settingsPath ?? join19(homedir12(), ".claude", "settings.json");
9594
+ const settingsPath = opts.settingsPath ?? join20(homedir12(), ".claude", "settings.json");
8838
9595
  const hookCmd = opts.hookCmd ?? DEFAULT_HOOK_CMD;
8839
9596
  const readFile7 = opts.readFile ?? defaultReadFile2;
8840
9597
  const writeFile6 = opts.writeFile ?? defaultWriteFile;
@@ -9004,14 +9761,14 @@ function extractDetail(sub, payload) {
9004
9761
  }
9005
9762
  function defaultReadFile2(path21) {
9006
9763
  try {
9007
- return readFileSync13(path21, "utf-8");
9764
+ return readFileSync14(path21, "utf-8");
9008
9765
  } catch {
9009
9766
  return void 0;
9010
9767
  }
9011
9768
  }
9012
9769
  function defaultWriteFile(path21, content) {
9013
9770
  mkdirSync6(path21.replace(/\/[^/]+$/, ""), { recursive: true });
9014
- writeFileSync8(path21, content, "utf-8");
9771
+ writeFileSync9(path21, content, "utf-8");
9015
9772
  }
9016
9773
 
9017
9774
  // packages/workspaces/dist/crew-pane.js
@@ -9139,27 +9896,56 @@ async function maybeBroadcastDaemonRestart(opts) {
9139
9896
 
9140
9897
  // packages/cli/src/lib/captain-channel-factory.ts
9141
9898
  import { createServer as createServer3, connect as netConnect } from "net";
9142
- import { unlinkSync as unlinkSync3 } from "fs";
9899
+ import fs16 from "fs";
9900
+ import { join as join21 } from "path";
9143
9901
  import { randomUUID as randomUUID5 } from "crypto";
9144
9902
  import chalk2 from "chalk";
9145
9903
  init_dist2();
9146
9904
  var shared;
9905
+ var registryEntryPath = () => join21(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
9906
+ function unregisterSenderIdentity() {
9907
+ try {
9908
+ fs16.unlinkSync(registryEntryPath());
9909
+ } catch {
9910
+ }
9911
+ }
9912
+ function registerSenderIdentity(socketPath) {
9913
+ try {
9914
+ unregisterSenderIdentity();
9915
+ fs16.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
9916
+ fs16.writeFileSync(
9917
+ registryEntryPath(),
9918
+ JSON.stringify({
9919
+ pid: process.pid,
9920
+ sessionId: randomUUID5(),
9921
+ name: "squadrantd",
9922
+ messagingSocketPath: socketPath,
9923
+ kind: "daemon",
9924
+ peerProtocol: 1
9925
+ })
9926
+ );
9927
+ process.on("exit", unregisterSenderIdentity);
9928
+ } catch {
9929
+ }
9930
+ }
9147
9931
  async function sharedReceiptListener() {
9148
9932
  if (shared) return shared;
9933
+ const socketPath = `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`;
9149
9934
  const listener = new ClaudeReceiptListener({
9150
- socketPath: `${CC_SOCKS_DIR}/squadrantd-${process.pid}.sock`,
9935
+ socketPath,
9151
9936
  createServer: (h) => createServer3(h),
9152
9937
  // A UDS path is not cleaned up when a process is killed, so our own leftover
9153
9938
  // must never be the reason we refuse to start.
9154
9939
  unlinkStale: (p) => {
9155
9940
  try {
9156
- unlinkSync3(p);
9941
+ fs16.unlinkSync(p);
9157
9942
  } catch {
9158
9943
  }
9159
9944
  },
9160
9945
  log: (m) => console.error(chalk2.dim(m))
9161
9946
  });
9162
9947
  await listener.start();
9948
+ registerSenderIdentity(socketPath);
9163
9949
  shared = listener;
9164
9950
  return shared;
9165
9951
  }
@@ -9207,11 +9993,11 @@ async function buildCaptainChannelWithRetry(opts = {}) {
9207
9993
 
9208
9994
  // packages/cli/src/squadrantd.ts
9209
9995
  var SELF_PATH2 = fileURLToPath3(import.meta.url);
9210
- var CLI_BIN = join20(dirname4(SELF_PATH2), "index.js");
9996
+ var CLI_BIN = join22(dirname4(SELF_PATH2), "index.js");
9211
9997
  function readPkgVersion() {
9212
9998
  try {
9213
- const pkgPath = join20(dirname4(SELF_PATH2), "..", "package.json");
9214
- return JSON.parse(readFileSync14(pkgPath, "utf-8")).version ?? "unknown";
9999
+ const pkgPath = join22(dirname4(SELF_PATH2), "..", "package.json");
10000
+ return JSON.parse(readFileSync15(pkgPath, "utf-8")).version ?? "unknown";
9215
10001
  } catch {
9216
10002
  return "unknown";
9217
10003
  }
@@ -9304,7 +10090,37 @@ function startSquadrantd(opts = {}) {
9304
10090
  codexAppServerSource.observe(ev);
9305
10091
  }
9306
10092
  });
9307
- const opencodeControlSource = new OpencodeControlSource();
10093
+ const violationLogState = /* @__PURE__ */ new Map();
10094
+ const VIOLATION_LOG_SUMMARY_MS = 6e4;
10095
+ const onEventsViolation = (v2) => {
10096
+ const key = `${v2.taskId}:${v2.code}`;
10097
+ const prev = violationLogState.get(key);
10098
+ if (!prev) {
10099
+ violationLogState.set(key, { count: 1, loggedAt: Date.now() });
10100
+ log(`[events] ${v2.code} ${v2.taskId}: ${v2.message}`);
10101
+ return;
10102
+ }
10103
+ prev.count++;
10104
+ const now = Date.now();
10105
+ if (now - prev.loggedAt > VIOLATION_LOG_SUMMARY_MS) {
10106
+ log(`[events] ${v2.code} ${v2.taskId}: seen ${prev.count}x since last log (latest: ${v2.message})`);
10107
+ prev.count = 0;
10108
+ prev.loggedAt = now;
10109
+ }
10110
+ };
10111
+ const opencodeRequestIds = { n: 1 };
10112
+ const eventsSource = createEventsSource({
10113
+ adapters: [createOpencodeFactAdapter({ nextRequestId: () => opencodeRequestIds.n++ })],
10114
+ emit: (ev) => {
10115
+ const found = store.listAll().find((r) => r.id === ev.id);
10116
+ if (!found) return;
10117
+ void ctx.d.handle({ kind: "event", project: found.project, event: ev });
10118
+ if (ev.type === "task.approval.requested")
10119
+ ctx.schedulePromotion(ev.id, ev.requestId, "approval", ev.question);
10120
+ },
10121
+ onViolation: onEventsViolation,
10122
+ check: { stallBudgetMs: 6e4, disagreeWindowMs: 5e3 }
10123
+ });
9308
10124
  const opencodeBridge = opts.opencodeBridge ?? new OpencodeSseBridge({
9309
10125
  emit: (ev) => {
9310
10126
  const found = store.listAll().find((r) => r.id === ev.id);
@@ -9312,8 +10128,8 @@ function startSquadrantd(opts = {}) {
9312
10128
  void ctx.d.handle({ kind: "event", project: found.project, event: ev });
9313
10129
  if (ev.type === "task.approval.requested")
9314
10130
  ctx.schedulePromotion(ev.id, ev.requestId, "approval", ev.question);
9315
- opencodeControlSource.observe(ev);
9316
10131
  },
10132
+ ingest: (raw, taskId) => eventsSource.ingest("opencode-sse", raw, { taskId }),
9317
10133
  log
9318
10134
  });
9319
10135
  const cmuxEventsBridge = opts.cmuxEventsBridge ?? new CmuxEventsBridge({
@@ -9328,7 +10144,7 @@ function startSquadrantd(opts = {}) {
9328
10144
  (r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
9329
10145
  );
9330
10146
  },
9331
- cursorFile: join20(stateRoot, "cmux-events.seq"),
10147
+ cursorFile: join22(stateRoot, "cmux-events.seq"),
9332
10148
  log
9333
10149
  });
9334
10150
  const cmuxStoreSource = new CmuxStoreSource({ log });
@@ -9338,11 +10154,11 @@ function startSquadrantd(opts = {}) {
9338
10154
  ctx.cmuxEventsBridge = cmuxEventsBridge;
9339
10155
  const claudePeerRegistrySource = new ClaudePeerRegistrySource({ log });
9340
10156
  ctx.lifecycleSources = [
10157
+ eventsSource,
9341
10158
  cmuxStoreSource,
9342
10159
  nativeHookSource,
9343
10160
  codexAppServerSource,
9344
- claudePeerRegistrySource,
9345
- opencodeControlSource
10161
+ claudePeerRegistrySource
9346
10162
  ];
9347
10163
  const tgCfg = loadConfig().telegram;
9348
10164
  ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(
@@ -9398,6 +10214,37 @@ ${buildCompletionProtocol(rec.id, rec.project)}`;
9398
10214
  }
9399
10215
  });
9400
10216
  const h = startDaemon(ctx, { ...opts, launchHeadless }, PKG_VERSION);
10217
+ const startEventsLifecycleSource = () => {
10218
+ let eventsTaskIndex;
10219
+ let eventsTaskIndexAt = 0;
10220
+ const EVENTS_TASK_INDEX_TTL_MS = 500;
10221
+ const eventsSourceDeps = {
10222
+ resolve: (hint) => {
10223
+ if (!hint.taskId) return void 0;
10224
+ const now = Date.now();
10225
+ if (!eventsTaskIndex || now - eventsTaskIndexAt > EVENTS_TASK_INDEX_TTL_MS) {
10226
+ eventsTaskIndex = /* @__PURE__ */ new Map();
10227
+ for (const r of store.listAll()) {
10228
+ if (!TERMINAL_STATES.has(r.state)) eventsTaskIndex.set(r.id, { id: r.id });
10229
+ }
10230
+ eventsTaskIndexAt = now;
10231
+ }
10232
+ const cached = eventsTaskIndex.get(hint.taskId);
10233
+ if (cached) return cached;
10234
+ return store.listAll().find(
10235
+ (r) => r.id === hint.taskId && !TERMINAL_STATES.has(r.state)
10236
+ );
10237
+ },
10238
+ report: () => {
10239
+ },
10240
+ log
10241
+ };
10242
+ try {
10243
+ eventsSource.start(eventsSourceDeps);
10244
+ } catch (e) {
10245
+ log(`events source start failed: ${e.message}`);
10246
+ }
10247
+ };
9401
10248
  if (!process.env.VITEST) {
9402
10249
  const prevSnaps = /* @__PURE__ */ new Map();
9403
10250
  const storeDeps = {
@@ -9524,19 +10371,9 @@ ${buildCompletionProtocol(rec.id, rec.project)}`;
9524
10371
  } catch (e) {
9525
10372
  log(`claude peer registry source start failed: ${e.message}`);
9526
10373
  }
9527
- const opencodeSourceDeps = {
9528
- resolve: () => void 0,
9529
- report: () => {
9530
- },
9531
- // read-only slice: caching is internal to the source
9532
- log
9533
- };
9534
- try {
9535
- opencodeControlSource.start(opencodeSourceDeps);
9536
- } catch (e) {
9537
- log(`opencode control source start failed: ${e.message}`);
9538
- }
10374
+ startEventsLifecycleSource();
9539
10375
  }
10376
+ if (opts.forceStartEventsSource) startEventsLifecycleSource();
9540
10377
  if (!process.env.VITEST) {
9541
10378
  try {
9542
10379
  const buildMtimeMs = statSync4(SELF_PATH2).mtimeMs;
@@ -9573,7 +10410,7 @@ ${buildCompletionProtocol(rec.id, rec.project)}`;
9573
10410
  } catch {
9574
10411
  }
9575
10412
  try {
9576
- opencodeControlSource.stop();
10413
+ eventsSource.stop();
9577
10414
  } catch {
9578
10415
  }
9579
10416
  return origStop(reason);
@@ -9584,9 +10421,20 @@ function logCrashMarker(kind, err) {
9584
10421
  const message = err instanceof Error ? err.stack ?? err.message : String(err);
9585
10422
  process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${kind} pid=${process.pid} error=${message}
9586
10423
  `);
9587
- }
9588
- function isMonorepoCheckout(scriptPath, dirExists = existsSync12) {
9589
- return dirExists(join20(dirname4(resolve3(scriptPath)), "..", "packages"));
10424
+ const stateRoot = join22(homedir13(), ".config", "squadrant", "state");
10425
+ writeExitMarker(stateRoot, {
10426
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10427
+ pid: process.pid,
10428
+ reason: kind,
10429
+ ppid: process.ppid,
10430
+ uptimeMs: Math.round(process.uptime() * 1e3),
10431
+ inFlightDelivery: null
10432
+ // crash path has no access to the delivery loop's live state
10433
+ }, (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
10434
+ `));
10435
+ }
10436
+ function isMonorepoCheckout(scriptPath, dirExists = existsSync13) {
10437
+ return dirExists(join22(dirname4(resolve3(scriptPath)), "..", "packages"));
9590
10438
  }
9591
10439
  function isLinkedWorktree(scriptPath, statFile = (p) => {
9592
10440
  try {
@@ -9595,7 +10443,7 @@ function isLinkedWorktree(scriptPath, statFile = (p) => {
9595
10443
  return void 0;
9596
10444
  }
9597
10445
  }) {
9598
- const dotGit = join20(dirname4(resolve3(scriptPath)), "..", ".git");
10446
+ const dotGit = join22(dirname4(resolve3(scriptPath)), "..", ".git");
9599
10447
  return statFile(dotGit)?.isFile === true;
9600
10448
  }
9601
10449
  if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {