squadrant 0.19.3 → 0.19.5

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.
@@ -887,6 +887,11 @@ function reduce(rec, ev, now) {
887
887
  return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
888
888
  case "task.turn.started":
889
889
  return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
890
+ case "task.turn.failed":
891
+ // #763: a crew's turn died on an API error (StopFailure hook). Anti-#2576:
892
+ // NOT task.failed — a hook may never terminalize a task. Falls through to
893
+ // the exact same turn-boundary transition as task.turn.completed below;
894
+ // only the notify message (reduce.ts formatMessage) differs.
890
895
  case "task.turn.completed":
891
896
  if (isStickyAttention(rec.state))
892
897
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
@@ -1002,6 +1007,9 @@ function formatMessage(rec, event) {
1002
1007
  return `CREW STALLED ${tag}: no heartbeat in ${rec.heartbeatBudgetMs}ms`;
1003
1008
  }
1004
1009
  case "awaiting-input":
1010
+ if (event?.type === "task.turn.failed") {
1011
+ return `CREW TURN FAILED ${tag}: API error ended the turn (not a stall) \u2014 ${event.error}`;
1012
+ }
1005
1013
  return `CREW IDLE ${tag}: turn ended, awaiting your reply.`;
1006
1014
  default:
1007
1015
  return null;
@@ -1411,6 +1419,7 @@ var init_reduce = __esm({
1411
1419
  "task.session",
1412
1420
  "task.turn.started",
1413
1421
  "task.turn.completed",
1422
+ "task.turn.failed",
1414
1423
  "task.delta",
1415
1424
  "task.input.requested",
1416
1425
  "task.approval.requested",
@@ -2322,6 +2331,26 @@ function parseProgramArgs(plistXml) {
2322
2331
  return null;
2323
2332
  return { nodeBin: xmlUnescape(m[1]), daemonEntry: xmlUnescape(m[2]) };
2324
2333
  }
2334
+ function isVersionUpgrade(registeredEntry, thisEntry) {
2335
+ const a = registeredEntry.split("/");
2336
+ const b = thisEntry.split("/");
2337
+ if (a.length !== b.length)
2338
+ return false;
2339
+ let diffIndex = -1;
2340
+ for (let i = 0; i < a.length; i++) {
2341
+ if (a[i] !== b[i]) {
2342
+ if (diffIndex !== -1)
2343
+ return false;
2344
+ diffIndex = i;
2345
+ }
2346
+ }
2347
+ if (diffIndex === -1)
2348
+ return false;
2349
+ const versionedSegment = /^(.+)@([^@]+)$/;
2350
+ const ma = a[diffIndex].match(versionedSegment);
2351
+ const mb = b[diffIndex].match(versionedSegment);
2352
+ return ma !== null && mb !== null && ma[1] === mb[1] && ma[2] !== mb[2];
2353
+ }
2325
2354
  function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
2326
2355
  if (!parsed)
2327
2356
  return null;
@@ -2329,7 +2358,11 @@ function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
2329
2358
  return null;
2330
2359
  if (!registeredEntryExists)
2331
2360
  return null;
2332
- return { registeredEntry: parsed.daemonEntry, thisEntry };
2361
+ return {
2362
+ registeredEntry: parsed.daemonEntry,
2363
+ thisEntry,
2364
+ isUpgrade: isVersionUpgrade(parsed.daemonEntry, thisEntry)
2365
+ };
2333
2366
  }
2334
2367
  function sanitizePathForPlist(path21) {
2335
2368
  const seen = /* @__PURE__ */ new Set();
@@ -2496,7 +2529,7 @@ function forceKickstartAndVerify(target, opts = {}) {
2496
2529
  const pollDelayMs = opts.pollDelayMs ?? 300;
2497
2530
  const kickstartRetries = opts.kickstartRetries ?? 10;
2498
2531
  const kickstartRetryDelayMs = opts.kickstartRetryDelayMs ?? 300;
2499
- const pidBefore = getDaemonPid(target);
2532
+ const pidBefore = opts.pidBefore !== void 0 ? opts.pidBefore : getDaemonPid(target);
2500
2533
  let kickstartError = null;
2501
2534
  for (let i = 0; i < kickstartRetries; i++) {
2502
2535
  try {
@@ -2533,6 +2566,9 @@ function forceKickstartAndVerify(target, opts = {}) {
2533
2566
  function isOperatorInitiatedCommand(topLevelArg) {
2534
2567
  return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
2535
2568
  }
2569
+ function isReadOnlyCrewCommand(argv) {
2570
+ return argv[2] === "crew" && argv[3] !== void 0 && READ_ONLY_CREW_SUBCOMMANDS.has(argv[3]);
2571
+ }
2536
2572
  function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2537
2573
  if (restartInFlight)
2538
2574
  return;
@@ -2553,8 +2589,10 @@ function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2553
2589
  try {
2554
2590
  const drift = computeDaemonDrift(nodeBin);
2555
2591
  if (drift.foreignInstall) {
2556
- process.stderr.write(printForeignInstallError(drift.foreignInstall));
2557
- return;
2592
+ const decision = decideForeignInstall(drift.foreignInstall);
2593
+ process.stderr.write(decision.message);
2594
+ if (!decision.proceed)
2595
+ return;
2558
2596
  }
2559
2597
  applyDaemonDrift(drift);
2560
2598
  } catch (e) {
@@ -2564,6 +2602,12 @@ function ensureDaemon(nodeBin = process.execPath, opts = {}) {
2564
2602
  releaseDaemonLock();
2565
2603
  }
2566
2604
  }
2605
+ function decideForeignInstall(foreign) {
2606
+ if (foreign.isUpgrade) {
2607
+ return { proceed: true, message: printVersionUpgradeNotice(foreign) };
2608
+ }
2609
+ return { proceed: false, message: printForeignInstallError(foreign) };
2610
+ }
2567
2611
  function printForeignInstallError(foreign) {
2568
2612
  return `[squadrant] refusing to restart the daemon: the registered launchd config belongs to a DIFFERENT squadrant install than this one.
2569
2613
  registered install: ${foreign.registeredEntry}
@@ -2571,14 +2615,19 @@ function printForeignInstallError(foreign) {
2571
2615
  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.
2572
2616
  `;
2573
2617
  }
2618
+ function printVersionUpgradeNotice(foreign) {
2619
+ return `[squadrant] upgraded plist: ${foreign.registeredEntry} -> ${foreign.thisEntry}
2620
+ `;
2621
+ }
2574
2622
  function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
2575
2623
  if (!tryAcquireDaemonLock()) {
2576
2624
  throw new Error("could not acquire the daemon lock \u2014 another squadrant process is already restarting the daemon");
2577
2625
  }
2578
2626
  try {
2579
2627
  const drift = computeDaemonDrift(nodeBin);
2628
+ const pidBeforeReconcile = getDaemonPid(drift.target);
2580
2629
  reconcilePlistAndService(drift);
2581
- const result = forceKickstartAndVerify(drift.target, kickstartOpts);
2630
+ const result = forceKickstartAndVerify(drift.target, { ...kickstartOpts, pidBefore: pidBeforeReconcile });
2582
2631
  if (!result.restarted) {
2583
2632
  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
2633
  }
@@ -2587,13 +2636,14 @@ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
2587
2636
  releaseDaemonLock();
2588
2637
  }
2589
2638
  }
2590
- var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS;
2639
+ var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS, READ_ONLY_CREW_SUBCOMMANDS;
2591
2640
  var init_launchd = __esm({
2592
2641
  "packages/core/dist/launchd.js"() {
2593
2642
  LABEL = "com.squadrant.daemon";
2594
2643
  AGENT_BINS = ["cmux", "claude", "opencode", "codex", "gemini", "node"];
2595
2644
  restartInFlight = false;
2596
2645
  OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
2646
+ READ_ONLY_CREW_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "read", "tasks"]);
2597
2647
  }
2598
2648
  });
2599
2649
 
@@ -3290,6 +3340,51 @@ var init_captain_delivery = __esm({
3290
3340
  }
3291
3341
  });
3292
3342
 
3343
+ // packages/core/dist/daemon/down-alert.js
3344
+ function pad2(n) {
3345
+ return String(n).padStart(2, "0");
3346
+ }
3347
+ function formatLocalDateTime(d) {
3348
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
3349
+ }
3350
+ function formatTzOffset(d) {
3351
+ const offMin = -d.getTimezoneOffset();
3352
+ const sign = offMin >= 0 ? "+" : "-";
3353
+ const abs = Math.abs(offMin);
3354
+ const hh = pad2(Math.floor(abs / 60));
3355
+ const mm = abs % 60;
3356
+ return mm === 0 ? `${sign}${hh}` : `${sign}${hh}:${pad2(mm)}`;
3357
+ }
3358
+ function formatDownAlertText(opts) {
3359
+ const start = new Date(opts.startTs);
3360
+ const end = new Date(opts.endTs);
3361
+ return `\u26A0\uFE0F daemon was down ${formatLocalDateTime(start)} \u2192 ${formatLocalDateTime(end)} (${formatTzOffset(end)}), ${opts.minutes} min (last exit reason=${opts.reasonText})`;
3362
+ }
3363
+ function formatRelative(ms) {
3364
+ const minutes = Math.round(ms / 6e4);
3365
+ if (minutes < 60)
3366
+ return `${minutes} min`;
3367
+ const hours = Math.floor(minutes / 60);
3368
+ const remMinutes = minutes % 60;
3369
+ if (hours < 24)
3370
+ return remMinutes > 0 ? `${hours}h ${remMinutes}m` : `${hours}h`;
3371
+ const days = Math.floor(hours / 24);
3372
+ const remHours = hours % 24;
3373
+ return remHours > 0 ? `${days}d ${remHours}h` : `${days}d`;
3374
+ }
3375
+ function stalePrefix(generatedAtMs, deliveredAtMs) {
3376
+ const ageMs = deliveredAtMs - generatedAtMs;
3377
+ if (ageMs <= STALE_ALERT_THRESHOLD_MS)
3378
+ return null;
3379
+ return `[stale \u2014 generated ${formatRelative(ageMs)} ago] `;
3380
+ }
3381
+ var STALE_ALERT_THRESHOLD_MS;
3382
+ var init_down_alert = __esm({
3383
+ "packages/core/dist/daemon/down-alert.js"() {
3384
+ STALE_ALERT_THRESHOLD_MS = 60 * 60 * 1e3;
3385
+ }
3386
+ });
3387
+
3293
3388
  // packages/core/dist/control-channel.js
3294
3389
  function fallsBackToPane(o) {
3295
3390
  return o.status === "gone" || o.status === "unsupported";
@@ -3442,12 +3537,20 @@ function ensureSocksDir(dir = CC_SOCKS_DIR) {
3442
3537
  if ((fs9.statSync(dir).mode & 511) !== 448)
3443
3538
  fs9.chmodSync(dir, 448);
3444
3539
  }
3445
- async function pollFirstTurnConfirmedAt(getTaskRecord, project, id) {
3446
- const deadline = Date.now() + FIRST_TURN_HOOK_CONFIRM_WINDOW_MS;
3540
+ async function pollFirstTurnConfirmedAt(getTaskRecord, project, id, scrapeSettled, cancel) {
3541
+ let scrapeSettledAt;
3542
+ const markSettled = () => {
3543
+ scrapeSettledAt ??= Date.now();
3544
+ };
3545
+ scrapeSettled.then(markSettled, markSettled);
3546
+ const hardDeadline = Date.now() + FIRST_TURN_HOOK_CONFIRM_MAX_MS;
3447
3547
  for (; ; ) {
3548
+ if (cancel.stopped)
3549
+ return false;
3448
3550
  const rec = await getTaskRecord(project, id).catch(() => void 0);
3449
3551
  if (rec?.firstTurnConfirmedAt)
3450
3552
  return true;
3553
+ const deadline = scrapeSettledAt !== void 0 ? Math.min(hardDeadline, scrapeSettledAt + FIRST_TURN_HOOK_CONFIRM_GRACE_MS) : hardDeadline;
3451
3554
  if (Date.now() >= deadline)
3452
3555
  return false;
3453
3556
  await new Promise((r) => setTimeout(r, FIRST_TURN_HOOK_POLL_INTERVAL_MS));
@@ -3629,7 +3732,9 @@ async function runCrewSpawn(input, config, deps) {
3629
3732
 
3630
3733
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
3631
3734
  const scrapeDelivered = sendPromise.then((r) => r.delivered).catch(() => false);
3632
- const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id)) : await scrapeDelivered;
3735
+ const cancelHookPoll = { stopped: false };
3736
+ const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id, scrapeDelivered, cancelHookPoll)) : await scrapeDelivered;
3737
+ cancelHookPoll.stopped = true;
3633
3738
  if (!delivered) {
3634
3739
  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.
3635
3740
  `);
@@ -3911,7 +4016,7 @@ async function runCrewList(project, runtime, workspaceId) {
3911
4016
  surfaceId: c.surfaceId
3912
4017
  }));
3913
4018
  }
3914
- 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;
4019
+ var CC_SOCKS_DIR, FIRST_TURN_INLINE_MAX_BYTES, TEMPLATES_DIR, STATE_ROOT, FIRST_TURN_HOOK_CONFIRM_GRACE_MS, FIRST_TURN_HOOK_CONFIRM_MAX_MS, FIRST_TURN_HOOK_POLL_INTERVAL_MS, CLOSE_LOOKUP_RETRIES, CLOSE_LOOKUP_RETRY_DELAY_MS;
3915
4020
  var init_crew_spawn = __esm({
3916
4021
  "packages/core/dist/crew-spawn.js"() {
3917
4022
  init_control_channel();
@@ -3923,7 +4028,8 @@ var init_crew_spawn = __esm({
3923
4028
  FIRST_TURN_INLINE_MAX_BYTES = 1200;
3924
4029
  TEMPLATES_DIR = path9.join(os4.homedir(), ".config", "squadrant", "templates");
3925
4030
  STATE_ROOT = path9.join(os4.homedir(), ".config", "squadrant", "state");
3926
- FIRST_TURN_HOOK_CONFIRM_WINDOW_MS = 1e5;
4031
+ FIRST_TURN_HOOK_CONFIRM_GRACE_MS = 15e3;
4032
+ FIRST_TURN_HOOK_CONFIRM_MAX_MS = 18e4;
3927
4033
  FIRST_TURN_HOOK_POLL_INTERVAL_MS = 2e3;
3928
4034
  CLOSE_LOOKUP_RETRIES = 3;
3929
4035
  CLOSE_LOOKUP_RETRY_DELAY_MS = 150;
@@ -4169,7 +4275,13 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4169
4275
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4170
4276
  }
4171
4277
  }
4172
- const result = await d.deliver(entry, async (text, sendOpts) => {
4278
+ let deliverEntry = entry;
4279
+ if (entry.kind === "captain.message" && entry.payload?.source === "daemon" && entry.message) {
4280
+ const prefix = stalePrefix(new Date(entry.ts).getTime(), Date.now());
4281
+ if (prefix)
4282
+ deliverEntry = { ...entry, message: `${prefix}${entry.message}` };
4283
+ }
4284
+ const result = await d.deliver(deliverEntry, async (text, sendOpts) => {
4173
4285
  let handledByChannel = false;
4174
4286
  try {
4175
4287
  const mode = ctx.captainChannelMode?.() ?? "off";
@@ -4261,6 +4373,7 @@ var init_delivery_loop = __esm({
4261
4373
  init_defer_delivery();
4262
4374
  init_dist();
4263
4375
  init_interactive_probe();
4376
+ init_down_alert();
4264
4377
  init_liveness2();
4265
4378
  init_captain_channel();
4266
4379
  CURSOR_SUBSCRIBER = "captain";
@@ -4695,8 +4808,8 @@ function startDaemon(ctx, opts, pkgVersion) {
4695
4808
  log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
4696
4809
  const bootTs = (/* @__PURE__ */ new Date()).toISOString();
4697
4810
  {
4698
- const sendDownAlert = (minutes, reasonText) => {
4699
- const text = `\u26A0\uFE0F daemon was down for ${minutes} min (last exit reason=${reasonText})`;
4811
+ const sendDownAlert = (startTs, endTs, minutes, reasonText) => {
4812
+ const text = formatDownAlertText({ startTs, endTs, minutes, reasonText });
4700
4813
  const alertProjects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
4701
4814
  for (const project of alertProjects) {
4702
4815
  appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`boot-gap alert failed project=${project}: ${e.message}`));
@@ -4707,13 +4820,13 @@ function startDaemon(ctx, opts, pkgVersion) {
4707
4820
  if (marker) {
4708
4821
  log(`previous exit ts=${marker.ts} reason=${marker.reason} gap=${((gapMs ?? 0) / 1e3).toFixed(1)}s`);
4709
4822
  if ((gapMs ?? 0) > 6e4)
4710
- sendDownAlert(Math.round((gapMs ?? 0) / 6e4), marker.reason);
4823
+ sendDownAlert(marker.ts, bootTs, Math.round((gapMs ?? 0) / 6e4), marker.reason);
4711
4824
  } else if (prevRunning) {
4712
4825
  const lastHeartbeatMs = new Date(prevRunning.lastHeartbeatTs).getTime();
4713
4826
  const uncleanGapMs = Math.max(0, Date.now() - lastHeartbeatMs);
4714
4827
  log(`previous exit: UNCLEAN (no marker; last heartbeat ${prevRunning.lastHeartbeatTs}, gap=${(uncleanGapMs / 1e3).toFixed(1)}s)`);
4715
4828
  if (uncleanGapMs > 6e4) {
4716
- sendDownAlert(Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
4829
+ sendDownAlert(prevRunning.lastHeartbeatTs, bootTs, Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
4717
4830
  }
4718
4831
  } else {
4719
4832
  log("previous exit: none (clean or first boot)");
@@ -4835,6 +4948,7 @@ var init_start = __esm({
4835
4948
  init_server();
4836
4949
  init_mailbox();
4837
4950
  init_exit_marker();
4951
+ init_down_alert();
4838
4952
  init_liveness2();
4839
4953
  init_dist();
4840
4954
  init_snapshot_gather();
@@ -6804,6 +6918,7 @@ __export(dist_exports, {
6804
6918
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
6805
6919
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
6806
6920
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
6921
+ READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
6807
6922
  STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
6808
6923
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
6809
6924
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
@@ -6852,6 +6967,7 @@ __export(dist_exports, {
6852
6967
  crewTag: () => crewTag,
6853
6968
  daemonEntryPath: () => daemonEntryPath,
6854
6969
  daemonLockPath: () => daemonLockPath,
6970
+ decideForeignInstall: () => decideForeignInstall,
6855
6971
  decodeFrames: () => decodeFrames,
6856
6972
  defaultIsPidAlive: () => defaultIsPidAlive,
6857
6973
  defaultListenError: () => defaultListenError,
@@ -6893,9 +7009,11 @@ __export(dist_exports, {
6893
7009
  isDaemonSocketLive: () => isDaemonSocketLive,
6894
7010
  isNotifyActive: () => isNotifyActive,
6895
7011
  isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
7012
+ isReadOnlyCrewCommand: () => isReadOnlyCrewCommand,
6896
7013
  isSideTitle: () => isSideTitle,
6897
7014
  isStickyAttention: () => isStickyAttention,
6898
7015
  isTurnAccepted: () => isTurnAccepted,
7016
+ isVersionUpgrade: () => isVersionUpgrade,
6899
7017
  kickstartArgv: () => kickstartArgv,
6900
7018
  launchOneWorkspace: () => launchOneWorkspace,
6901
7019
  loadSessions: () => loadSessions,
@@ -6912,6 +7030,7 @@ __export(dist_exports, {
6912
7030
  parseProgramArgs: () => parseProgramArgs,
6913
7031
  plistPath: () => plistPath,
6914
7032
  printForeignInstallError: () => printForeignInstallError,
7033
+ printVersionUpgradeNotice: () => printVersionUpgradeNotice,
6915
7034
  programArgsBlock: () => programArgsBlock,
6916
7035
  projectHealth: () => projectHealth,
6917
7036
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
@@ -7887,7 +8006,7 @@ import { execSync as execSync6 } from "child_process";
7887
8006
  import { readFileSync as readFileSync10 } from "fs";
7888
8007
  import { homedir as homedir8 } from "os";
7889
8008
  import { join as join15 } from "path";
7890
- var nextAskUserQuestionRequestId = Date.now();
8009
+ var nextHookRequestId = Date.now();
7891
8010
 
7892
8011
  // packages/agents/dist/headless/types.js
7893
8012
  var HEADLESS_ERROR_TAIL = 2e3;
@@ -9593,7 +9712,13 @@ var CLAUDE_HOOK_EVENTS = [
9593
9712
  ["Stop", "stop"],
9594
9713
  ["Notification", "notification"],
9595
9714
  ["PreToolUse", "ask-question", "AskUserQuestion"],
9596
- ["SessionEnd", "session-end"]
9715
+ ["SessionEnd", "session-end"],
9716
+ // #760: fires ~6s before the matching Notification, carrying tool_name +
9717
+ // tool_input directly — a strictly better task.blocked source.
9718
+ ["PermissionRequest", "permission-request"],
9719
+ // #763: the turn died on an API error — today squadrant has no source for
9720
+ // this at all; without it the watchdog reports a plain stall (wrong story).
9721
+ ["StopFailure", "stop-failure"]
9597
9722
  ];
9598
9723
  var DEFAULT_HOOK_CMD = "squadrant hooks";
9599
9724
  function installClaudeHooks(opts = {}) {
@@ -9671,6 +9796,10 @@ function mapSubToLifecycle(sub) {
9671
9796
  return "needsInput";
9672
9797
  case "ask-question":
9673
9798
  return "needsInput";
9799
+ case "permission-request":
9800
+ return "needsInput";
9801
+ case "stop-failure":
9802
+ return "idle";
9674
9803
  case "session-end":
9675
9804
  return "session-end";
9676
9805
  default:
@@ -9809,9 +9938,12 @@ async function confirmedSendToPane(runtime, pane, message) {
9809
9938
  let sawDraft = await settleInputBox(runtime, pane);
9810
9939
  await runtime.sendKeyToPane(pane, "Enter");
9811
9940
  let repasted = false;
9941
+ let screenMoved = false;
9812
9942
  for (let attempt = 0; attempt < SUBMIT_RETRY_LIMIT; attempt++) {
9813
9943
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
9814
9944
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
9945
+ if (afterScreen !== preSendScreen)
9946
+ screenMoved = true;
9815
9947
  const draft = parseDraftFromScreen(afterScreen);
9816
9948
  if (draft !== "" && draft !== null)
9817
9949
  sawDraft = true;
@@ -9822,7 +9954,7 @@ async function confirmedSendToPane(runtime, pane, message) {
9822
9954
  const settled = await settleInputBox(runtime, pane);
9823
9955
  if (settled)
9824
9956
  sawDraft = true;
9825
- if (!sawDraft && !repasted) {
9957
+ if (!sawDraft && !repasted && !screenMoved) {
9826
9958
  repasted = true;
9827
9959
  await runtime.pasteToPane(pane, message);
9828
9960
  }