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.
package/dist/index.js CHANGED
@@ -1587,6 +1587,11 @@ function reduce(rec, ev, now) {
1587
1587
  return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
1588
1588
  case "task.turn.started":
1589
1589
  return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
1590
+ case "task.turn.failed":
1591
+ // #763: a crew's turn died on an API error (StopFailure hook). Anti-#2576:
1592
+ // NOT task.failed — a hook may never terminalize a task. Falls through to
1593
+ // the exact same turn-boundary transition as task.turn.completed below;
1594
+ // only the notify message (reduce.ts formatMessage) differs.
1590
1595
  case "task.turn.completed":
1591
1596
  if (isStickyAttention(rec.state))
1592
1597
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
@@ -1702,6 +1707,9 @@ function formatMessage(rec, event) {
1702
1707
  return `CREW STALLED ${tag}: no heartbeat in ${rec.heartbeatBudgetMs}ms`;
1703
1708
  }
1704
1709
  case "awaiting-input":
1710
+ if (event?.type === "task.turn.failed") {
1711
+ return `CREW TURN FAILED ${tag}: API error ended the turn (not a stall) \u2014 ${event.error}`;
1712
+ }
1705
1713
  return `CREW IDLE ${tag}: turn ended, awaiting your reply.`;
1706
1714
  default:
1707
1715
  return null;
@@ -2111,6 +2119,7 @@ var init_reduce = __esm({
2111
2119
  "task.session",
2112
2120
  "task.turn.started",
2113
2121
  "task.turn.completed",
2122
+ "task.turn.failed",
2114
2123
  "task.delta",
2115
2124
  "task.input.requested",
2116
2125
  "task.approval.requested",
@@ -3022,6 +3031,26 @@ function parseProgramArgs(plistXml) {
3022
3031
  return null;
3023
3032
  return { nodeBin: xmlUnescape(m[1]), daemonEntry: xmlUnescape(m[2]) };
3024
3033
  }
3034
+ function isVersionUpgrade(registeredEntry, thisEntry) {
3035
+ const a = registeredEntry.split("/");
3036
+ const b = thisEntry.split("/");
3037
+ if (a.length !== b.length)
3038
+ return false;
3039
+ let diffIndex = -1;
3040
+ for (let i = 0; i < a.length; i++) {
3041
+ if (a[i] !== b[i]) {
3042
+ if (diffIndex !== -1)
3043
+ return false;
3044
+ diffIndex = i;
3045
+ }
3046
+ }
3047
+ if (diffIndex === -1)
3048
+ return false;
3049
+ const versionedSegment = /^(.+)@([^@]+)$/;
3050
+ const ma = a[diffIndex].match(versionedSegment);
3051
+ const mb = b[diffIndex].match(versionedSegment);
3052
+ return ma !== null && mb !== null && ma[1] === mb[1] && ma[2] !== mb[2];
3053
+ }
3025
3054
  function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
3026
3055
  if (!parsed)
3027
3056
  return null;
@@ -3029,7 +3058,11 @@ function detectForeignInstall(parsed, thisEntry, registeredEntryExists) {
3029
3058
  return null;
3030
3059
  if (!registeredEntryExists)
3031
3060
  return null;
3032
- return { registeredEntry: parsed.daemonEntry, thisEntry };
3061
+ return {
3062
+ registeredEntry: parsed.daemonEntry,
3063
+ thisEntry,
3064
+ isUpgrade: isVersionUpgrade(parsed.daemonEntry, thisEntry)
3065
+ };
3033
3066
  }
3034
3067
  function sanitizePathForPlist(path36) {
3035
3068
  const seen = /* @__PURE__ */ new Set();
@@ -3196,7 +3229,7 @@ function forceKickstartAndVerify(target, opts = {}) {
3196
3229
  const pollDelayMs = opts.pollDelayMs ?? 300;
3197
3230
  const kickstartRetries = opts.kickstartRetries ?? 10;
3198
3231
  const kickstartRetryDelayMs = opts.kickstartRetryDelayMs ?? 300;
3199
- const pidBefore = getDaemonPid(target);
3232
+ const pidBefore = opts.pidBefore !== void 0 ? opts.pidBefore : getDaemonPid(target);
3200
3233
  let kickstartError = null;
3201
3234
  for (let i = 0; i < kickstartRetries; i++) {
3202
3235
  try {
@@ -3233,6 +3266,9 @@ function forceKickstartAndVerify(target, opts = {}) {
3233
3266
  function isOperatorInitiatedCommand(topLevelArg) {
3234
3267
  return topLevelArg !== void 0 && OPERATOR_INITIATED_COMMANDS.has(topLevelArg);
3235
3268
  }
3269
+ function isReadOnlyCrewCommand(argv) {
3270
+ return argv[2] === "crew" && argv[3] !== void 0 && READ_ONLY_CREW_SUBCOMMANDS.has(argv[3]);
3271
+ }
3236
3272
  function ensureDaemon(nodeBin = process.execPath, opts = {}) {
3237
3273
  if (restartInFlight)
3238
3274
  return;
@@ -3253,8 +3289,10 @@ function ensureDaemon(nodeBin = process.execPath, opts = {}) {
3253
3289
  try {
3254
3290
  const drift = computeDaemonDrift(nodeBin);
3255
3291
  if (drift.foreignInstall) {
3256
- process.stderr.write(printForeignInstallError(drift.foreignInstall));
3257
- return;
3292
+ const decision = decideForeignInstall(drift.foreignInstall);
3293
+ process.stderr.write(decision.message);
3294
+ if (!decision.proceed)
3295
+ return;
3258
3296
  }
3259
3297
  applyDaemonDrift(drift);
3260
3298
  } catch (e) {
@@ -3264,6 +3302,12 @@ function ensureDaemon(nodeBin = process.execPath, opts = {}) {
3264
3302
  releaseDaemonLock();
3265
3303
  }
3266
3304
  }
3305
+ function decideForeignInstall(foreign) {
3306
+ if (foreign.isUpgrade) {
3307
+ return { proceed: true, message: printVersionUpgradeNotice(foreign) };
3308
+ }
3309
+ return { proceed: false, message: printForeignInstallError(foreign) };
3310
+ }
3267
3311
  function printForeignInstallError(foreign) {
3268
3312
  return `[squadrant] refusing to restart the daemon: the registered launchd config belongs to a DIFFERENT squadrant install than this one.
3269
3313
  registered install: ${foreign.registeredEntry}
@@ -3271,14 +3315,19 @@ function printForeignInstallError(foreign) {
3271
3315
  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.
3272
3316
  `;
3273
3317
  }
3318
+ function printVersionUpgradeNotice(foreign) {
3319
+ return `[squadrant] upgraded plist: ${foreign.registeredEntry} -> ${foreign.thisEntry}
3320
+ `;
3321
+ }
3274
3322
  function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
3275
3323
  if (!tryAcquireDaemonLock()) {
3276
3324
  throw new Error("could not acquire the daemon lock \u2014 another squadrant process is already restarting the daemon");
3277
3325
  }
3278
3326
  try {
3279
3327
  const drift = computeDaemonDrift(nodeBin);
3328
+ const pidBeforeReconcile = getDaemonPid(drift.target);
3280
3329
  reconcilePlistAndService(drift);
3281
- const result = forceKickstartAndVerify(drift.target, kickstartOpts);
3330
+ const result = forceKickstartAndVerify(drift.target, { ...kickstartOpts, pidBefore: pidBeforeReconcile });
3282
3331
  if (!result.restarted) {
3283
3332
  throw new Error(`daemon did not restart (pid before=${result.pidBefore ?? "none"}, after=${result.pidAfter ?? "none"}) \u2014 \`launchctl kickstart -k ${drift.target}\` ran but the pid never changed`);
3284
3333
  }
@@ -3287,13 +3336,14 @@ function reregisterDaemon(nodeBin = process.execPath, kickstartOpts = {}) {
3287
3336
  releaseDaemonLock();
3288
3337
  }
3289
3338
  }
3290
- var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS;
3339
+ var LABEL, AGENT_BINS, restartInFlight, OPERATOR_INITIATED_COMMANDS, READ_ONLY_CREW_SUBCOMMANDS;
3291
3340
  var init_launchd = __esm({
3292
3341
  "packages/core/dist/launchd.js"() {
3293
3342
  LABEL = "com.squadrant.daemon";
3294
3343
  AGENT_BINS = ["cmux", "claude", "opencode", "codex", "gemini", "node"];
3295
3344
  restartInFlight = false;
3296
3345
  OPERATOR_INITIATED_COMMANDS = /* @__PURE__ */ new Set(["launch", "init"]);
3346
+ READ_ONLY_CREW_SUBCOMMANDS = /* @__PURE__ */ new Set(["list", "read", "tasks"]);
3297
3347
  }
3298
3348
  });
3299
3349
 
@@ -3990,6 +4040,51 @@ var init_captain_delivery = __esm({
3990
4040
  }
3991
4041
  });
3992
4042
 
4043
+ // packages/core/dist/daemon/down-alert.js
4044
+ function pad2(n) {
4045
+ return String(n).padStart(2, "0");
4046
+ }
4047
+ function formatLocalDateTime(d) {
4048
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
4049
+ }
4050
+ function formatTzOffset(d) {
4051
+ const offMin = -d.getTimezoneOffset();
4052
+ const sign = offMin >= 0 ? "+" : "-";
4053
+ const abs = Math.abs(offMin);
4054
+ const hh = pad2(Math.floor(abs / 60));
4055
+ const mm = abs % 60;
4056
+ return mm === 0 ? `${sign}${hh}` : `${sign}${hh}:${pad2(mm)}`;
4057
+ }
4058
+ function formatDownAlertText(opts) {
4059
+ const start = new Date(opts.startTs);
4060
+ const end = new Date(opts.endTs);
4061
+ return `\u26A0\uFE0F daemon was down ${formatLocalDateTime(start)} \u2192 ${formatLocalDateTime(end)} (${formatTzOffset(end)}), ${opts.minutes} min (last exit reason=${opts.reasonText})`;
4062
+ }
4063
+ function formatRelative(ms) {
4064
+ const minutes = Math.round(ms / 6e4);
4065
+ if (minutes < 60)
4066
+ return `${minutes} min`;
4067
+ const hours = Math.floor(minutes / 60);
4068
+ const remMinutes = minutes % 60;
4069
+ if (hours < 24)
4070
+ return remMinutes > 0 ? `${hours}h ${remMinutes}m` : `${hours}h`;
4071
+ const days = Math.floor(hours / 24);
4072
+ const remHours = hours % 24;
4073
+ return remHours > 0 ? `${days}d ${remHours}h` : `${days}d`;
4074
+ }
4075
+ function stalePrefix(generatedAtMs, deliveredAtMs) {
4076
+ const ageMs = deliveredAtMs - generatedAtMs;
4077
+ if (ageMs <= STALE_ALERT_THRESHOLD_MS)
4078
+ return null;
4079
+ return `[stale \u2014 generated ${formatRelative(ageMs)} ago] `;
4080
+ }
4081
+ var STALE_ALERT_THRESHOLD_MS;
4082
+ var init_down_alert = __esm({
4083
+ "packages/core/dist/daemon/down-alert.js"() {
4084
+ STALE_ALERT_THRESHOLD_MS = 60 * 60 * 1e3;
4085
+ }
4086
+ });
4087
+
3993
4088
  // packages/core/dist/control-channel.js
3994
4089
  function fallsBackToPane(o) {
3995
4090
  return o.status === "gone" || o.status === "unsupported";
@@ -4142,12 +4237,20 @@ function ensureSocksDir(dir = CC_SOCKS_DIR) {
4142
4237
  if ((fs9.statSync(dir).mode & 511) !== 448)
4143
4238
  fs9.chmodSync(dir, 448);
4144
4239
  }
4145
- async function pollFirstTurnConfirmedAt(getTaskRecord, project, id) {
4146
- const deadline = Date.now() + FIRST_TURN_HOOK_CONFIRM_WINDOW_MS;
4240
+ async function pollFirstTurnConfirmedAt(getTaskRecord, project, id, scrapeSettled, cancel) {
4241
+ let scrapeSettledAt;
4242
+ const markSettled = () => {
4243
+ scrapeSettledAt ??= Date.now();
4244
+ };
4245
+ scrapeSettled.then(markSettled, markSettled);
4246
+ const hardDeadline = Date.now() + FIRST_TURN_HOOK_CONFIRM_MAX_MS;
4147
4247
  for (; ; ) {
4248
+ if (cancel.stopped)
4249
+ return false;
4148
4250
  const rec = await getTaskRecord(project, id).catch(() => void 0);
4149
4251
  if (rec?.firstTurnConfirmedAt)
4150
4252
  return true;
4253
+ const deadline = scrapeSettledAt !== void 0 ? Math.min(hardDeadline, scrapeSettledAt + FIRST_TURN_HOOK_CONFIRM_GRACE_MS) : hardDeadline;
4151
4254
  if (Date.now() >= deadline)
4152
4255
  return false;
4153
4256
  await new Promise((r) => setTimeout(r, FIRST_TURN_HOOK_POLL_INTERVAL_MS));
@@ -4329,7 +4432,9 @@ async function runCrewSpawn(input, config, deps) {
4329
4432
 
4330
4433
  ${buildCompletionProtocol(rec.id, input.project)}`, preLaunchScreen);
4331
4434
  const scrapeDelivered = sendPromise.then((r) => r.delivered).catch(() => false);
4332
- const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id)) : await scrapeDelivered;
4435
+ const cancelHookPoll = { stopped: false };
4436
+ const delivered = hooksInstalled && deps.getTaskRecord ? await firstTrueOrBothFalse(scrapeDelivered, pollFirstTurnConfirmedAt(deps.getTaskRecord, input.project, rec.id, scrapeDelivered, cancelHookPoll)) : await scrapeDelivered;
4437
+ cancelHookPoll.stopped = true;
4333
4438
  if (!delivered) {
4334
4439
  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.
4335
4440
  `);
@@ -4611,7 +4716,7 @@ async function runCrewList(project, runtime, workspaceId) {
4611
4716
  surfaceId: c.surfaceId
4612
4717
  }));
4613
4718
  }
4614
- 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;
4719
+ 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;
4615
4720
  var init_crew_spawn = __esm({
4616
4721
  "packages/core/dist/crew-spawn.js"() {
4617
4722
  init_control_channel();
@@ -4623,7 +4728,8 @@ var init_crew_spawn = __esm({
4623
4728
  FIRST_TURN_INLINE_MAX_BYTES = 1200;
4624
4729
  TEMPLATES_DIR = path9.join(os4.homedir(), ".config", "squadrant", "templates");
4625
4730
  STATE_ROOT = path9.join(os4.homedir(), ".config", "squadrant", "state");
4626
- FIRST_TURN_HOOK_CONFIRM_WINDOW_MS = 1e5;
4731
+ FIRST_TURN_HOOK_CONFIRM_GRACE_MS = 15e3;
4732
+ FIRST_TURN_HOOK_CONFIRM_MAX_MS = 18e4;
4627
4733
  FIRST_TURN_HOOK_POLL_INTERVAL_MS = 2e3;
4628
4734
  CLOSE_LOOKUP_RETRIES = 3;
4629
4735
  CLOSE_LOOKUP_RETRY_DELAY_MS = 150;
@@ -4869,7 +4975,13 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4869
4975
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=stale-terminal-deliver`);
4870
4976
  }
4871
4977
  }
4872
- const result = await d.deliver(entry, async (text, sendOpts) => {
4978
+ let deliverEntry = entry;
4979
+ if (entry.kind === "captain.message" && entry.payload?.source === "daemon" && entry.message) {
4980
+ const prefix = stalePrefix(new Date(entry.ts).getTime(), Date.now());
4981
+ if (prefix)
4982
+ deliverEntry = { ...entry, message: `${prefix}${entry.message}` };
4983
+ }
4984
+ const result = await d.deliver(deliverEntry, async (text, sendOpts) => {
4873
4985
  let handledByChannel = false;
4874
4986
  try {
4875
4987
  const mode = ctx.captainChannelMode?.() ?? "off";
@@ -4961,6 +5073,7 @@ var init_delivery_loop = __esm({
4961
5073
  init_defer_delivery();
4962
5074
  init_dist();
4963
5075
  init_interactive_probe();
5076
+ init_down_alert();
4964
5077
  init_liveness2();
4965
5078
  init_captain_channel();
4966
5079
  CURSOR_SUBSCRIBER = "captain";
@@ -5395,8 +5508,8 @@ function startDaemon(ctx, opts, pkgVersion) {
5395
5508
  log(`boot pid=${process.pid} version=${pkgVersion} socket=${ctx.sockPath} stateRoot=${stateRoot}`);
5396
5509
  const bootTs = (/* @__PURE__ */ new Date()).toISOString();
5397
5510
  {
5398
- const sendDownAlert = (minutes, reasonText) => {
5399
- const text = `\u26A0\uFE0F daemon was down for ${minutes} min (last exit reason=${reasonText})`;
5511
+ const sendDownAlert = (startTs, endTs, minutes, reasonText) => {
5512
+ const text = formatDownAlertText({ startTs, endTs, minutes, reasonText });
5400
5513
  const alertProjects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
5401
5514
  for (const project of alertProjects) {
5402
5515
  appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`boot-gap alert failed project=${project}: ${e.message}`));
@@ -5407,13 +5520,13 @@ function startDaemon(ctx, opts, pkgVersion) {
5407
5520
  if (marker) {
5408
5521
  log(`previous exit ts=${marker.ts} reason=${marker.reason} gap=${((gapMs ?? 0) / 1e3).toFixed(1)}s`);
5409
5522
  if ((gapMs ?? 0) > 6e4)
5410
- sendDownAlert(Math.round((gapMs ?? 0) / 6e4), marker.reason);
5523
+ sendDownAlert(marker.ts, bootTs, Math.round((gapMs ?? 0) / 6e4), marker.reason);
5411
5524
  } else if (prevRunning) {
5412
5525
  const lastHeartbeatMs = new Date(prevRunning.lastHeartbeatTs).getTime();
5413
5526
  const uncleanGapMs = Math.max(0, Date.now() - lastHeartbeatMs);
5414
5527
  log(`previous exit: UNCLEAN (no marker; last heartbeat ${prevRunning.lastHeartbeatTs}, gap=${(uncleanGapMs / 1e3).toFixed(1)}s)`);
5415
5528
  if (uncleanGapMs > 6e4) {
5416
- sendDownAlert(Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
5529
+ sendDownAlert(prevRunning.lastHeartbeatTs, bootTs, Math.round(uncleanGapMs / 6e4), "unclean/unknown \u2014 no exit marker, likely SIGKILL/OOM/power-loss");
5417
5530
  }
5418
5531
  } else {
5419
5532
  log("previous exit: none (clean or first boot)");
@@ -5535,6 +5648,7 @@ var init_start = __esm({
5535
5648
  init_server();
5536
5649
  init_mailbox();
5537
5650
  init_exit_marker();
5651
+ init_down_alert();
5538
5652
  init_liveness2();
5539
5653
  init_dist();
5540
5654
  init_snapshot_gather();
@@ -7504,6 +7618,7 @@ __export(dist_exports2, {
7504
7618
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
7505
7619
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
7506
7620
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
7621
+ READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
7507
7622
  STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
7508
7623
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
7509
7624
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
@@ -7552,6 +7667,7 @@ __export(dist_exports2, {
7552
7667
  crewTag: () => crewTag,
7553
7668
  daemonEntryPath: () => daemonEntryPath,
7554
7669
  daemonLockPath: () => daemonLockPath,
7670
+ decideForeignInstall: () => decideForeignInstall,
7555
7671
  decodeFrames: () => decodeFrames,
7556
7672
  defaultIsPidAlive: () => defaultIsPidAlive,
7557
7673
  defaultListenError: () => defaultListenError,
@@ -7593,9 +7709,11 @@ __export(dist_exports2, {
7593
7709
  isDaemonSocketLive: () => isDaemonSocketLive,
7594
7710
  isNotifyActive: () => isNotifyActive,
7595
7711
  isOperatorInitiatedCommand: () => isOperatorInitiatedCommand,
7712
+ isReadOnlyCrewCommand: () => isReadOnlyCrewCommand,
7596
7713
  isSideTitle: () => isSideTitle,
7597
7714
  isStickyAttention: () => isStickyAttention,
7598
7715
  isTurnAccepted: () => isTurnAccepted,
7716
+ isVersionUpgrade: () => isVersionUpgrade,
7599
7717
  kickstartArgv: () => kickstartArgv,
7600
7718
  launchOneWorkspace: () => launchOneWorkspace,
7601
7719
  loadSessions: () => loadSessions,
@@ -7612,6 +7730,7 @@ __export(dist_exports2, {
7612
7730
  parseProgramArgs: () => parseProgramArgs,
7613
7731
  plistPath: () => plistPath,
7614
7732
  printForeignInstallError: () => printForeignInstallError,
7733
+ printVersionUpgradeNotice: () => printVersionUpgradeNotice,
7615
7734
  programArgsBlock: () => programArgsBlock,
7616
7735
  projectHealth: () => projectHealth,
7617
7736
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
@@ -9109,6 +9228,10 @@ function mapSubToLifecycle(sub) {
9109
9228
  return "needsInput";
9110
9229
  case "ask-question":
9111
9230
  return "needsInput";
9231
+ case "permission-request":
9232
+ return "needsInput";
9233
+ case "stop-failure":
9234
+ return "idle";
9112
9235
  case "session-end":
9113
9236
  return "session-end";
9114
9237
  default:
@@ -9150,7 +9273,13 @@ var init_native_hook_source = __esm({
9150
9273
  ["Stop", "stop"],
9151
9274
  ["Notification", "notification"],
9152
9275
  ["PreToolUse", "ask-question", "AskUserQuestion"],
9153
- ["SessionEnd", "session-end"]
9276
+ ["SessionEnd", "session-end"],
9277
+ // #760: fires ~6s before the matching Notification, carrying tool_name +
9278
+ // tool_input directly — a strictly better task.blocked source.
9279
+ ["PermissionRequest", "permission-request"],
9280
+ // #763: the turn died on an API error — today squadrant has no source for
9281
+ // this at all; without it the watchdog reports a plain stall (wrong story).
9282
+ ["StopFailure", "stop-failure"]
9154
9283
  ];
9155
9284
  DEFAULT_HOOK_CMD = "squadrant hooks";
9156
9285
  NativeHookSource = class {
@@ -9297,9 +9426,12 @@ async function confirmedSendToPane(runtime, pane, message) {
9297
9426
  let sawDraft = await settleInputBox(runtime, pane);
9298
9427
  await runtime.sendKeyToPane(pane, "Enter");
9299
9428
  let repasted = false;
9429
+ let screenMoved = false;
9300
9430
  for (let attempt = 0; attempt < SUBMIT_RETRY_LIMIT; attempt++) {
9301
9431
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
9302
9432
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
9433
+ if (afterScreen !== preSendScreen)
9434
+ screenMoved = true;
9303
9435
  const draft = parseDraftFromScreen(afterScreen);
9304
9436
  if (draft !== "" && draft !== null)
9305
9437
  sawDraft = true;
@@ -9310,7 +9442,7 @@ async function confirmedSendToPane(runtime, pane, message) {
9310
9442
  const settled = await settleInputBox(runtime, pane);
9311
9443
  if (settled)
9312
9444
  sawDraft = true;
9313
- if (!sawDraft && !repasted) {
9445
+ if (!sawDraft && !repasted && !screenMoved) {
9314
9446
  repasted = true;
9315
9447
  await runtime.pasteToPane(pane, message);
9316
9448
  }
@@ -9374,9 +9506,12 @@ async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acce
9374
9506
  await runtime.sendKeyToPane(pane, "Enter");
9375
9507
  const retryLimit = acceptanceConfig?.retryLimit ?? SUBMIT_RETRY_LIMIT;
9376
9508
  let repasted = false;
9509
+ let screenMoved = false;
9377
9510
  for (let attempt = 0; attempt < retryLimit; attempt++) {
9378
9511
  await new Promise((r) => setTimeout(r, POST_SEND_CHECK_MS));
9379
9512
  const afterScreen = await runtime.readPaneScreen(pane) ?? "";
9513
+ if (afterScreen !== preSendScreen)
9514
+ screenMoved = true;
9380
9515
  const draft = parseDraftFromScreen(afterScreen);
9381
9516
  if (draft !== "" && draft !== null)
9382
9517
  sawDraft = true;
@@ -9387,13 +9522,13 @@ async function sendFirstTurnWhenReady(runtime, pane, task, preLaunchScreen, acce
9387
9522
  const settled = await settleInputBox(runtime, pane);
9388
9523
  if (settled)
9389
9524
  sawDraft = true;
9390
- if (!sawDraft && !repasted) {
9525
+ if (!sawDraft && !repasted && !screenMoved) {
9391
9526
  repasted = true;
9392
9527
  await runtime.pasteToPane(pane, task);
9393
9528
  }
9394
9529
  await runtime.sendKeyToPane(pane, "Enter");
9395
9530
  }
9396
- if (!sawDraft) {
9531
+ if (!sawDraft && !screenMoved) {
9397
9532
  return confirmedSendToPane(runtime, pane, task);
9398
9533
  }
9399
9534
  return { delivered: false };
@@ -9509,12 +9644,12 @@ function createClaudeDriver() {
9509
9644
  if (opts.settingsPath) {
9510
9645
  cmd += ` --settings ${opts.settingsPath}`;
9511
9646
  }
9512
- if (opts.messagingSocketPath) {
9513
- cmd += ` --messaging-socket-path ${opts.messagingSocketPath}`;
9514
- }
9515
9647
  const pluginSubdir = opts.role === "crew" ? "plugin-crew" : "plugin";
9516
9648
  const pluginDir = `${process.env.HOME}/.config/squadrant/${pluginSubdir}`;
9517
9649
  cmd += ` --plugin-dir ${pluginDir}`;
9650
+ if (opts.messagingSocketPath) {
9651
+ cmd += ` --messaging-socket-path ${opts.messagingSocketPath}`;
9652
+ }
9518
9653
  if (!opts.interactive) {
9519
9654
  cmd += ` -p "${opts.prompt.replace(/"/g, '\\"')}"`;
9520
9655
  }
@@ -11273,6 +11408,70 @@ function decideCaptainMemoryWrite(toolName, toolInput, env, homeDir) {
11273
11408
  return { decision: "allow" };
11274
11409
  return { decision: "deny", reason: DENY_REASON };
11275
11410
  }
11411
+ function resolveTurnId(payload) {
11412
+ const id = payload?.prompt_id;
11413
+ return typeof id === "string" && id.trim() ? id : "hook-stop";
11414
+ }
11415
+ function logStopPermissionMode(payload, taskId) {
11416
+ const mode = payload?.permission_mode;
11417
+ if (typeof mode === "string" && mode) {
11418
+ console.error(`[squadrant] hook: Stop permission_mode=${mode} task=${taskId}`);
11419
+ }
11420
+ }
11421
+ function classifyNotification(payload) {
11422
+ const p = payload;
11423
+ const msg = typeof p?.message === "string" ? p.message : "";
11424
+ const notificationType = typeof p?.notification_type === "string" ? p.notification_type : null;
11425
+ if (notificationType === "permission_prompt" || notificationType === "worker_permission_prompt") {
11426
+ return { kind: "blocked", question: msg || "crew awaiting permission" };
11427
+ }
11428
+ if (notificationType === "agent_needs_input") {
11429
+ return { kind: "input-requested", question: msg || "crew needs input" };
11430
+ }
11431
+ if (notificationType != null) {
11432
+ return { kind: "progress" };
11433
+ }
11434
+ if (msg && isPermissionNotification(msg)) {
11435
+ return { kind: "blocked", question: msg };
11436
+ }
11437
+ return { kind: "progress" };
11438
+ }
11439
+ function summarizeToolInput(toolName, toolInput) {
11440
+ if (typeof toolName !== "string" || !toolName)
11441
+ return "a tool call";
11442
+ const input = toolInput;
11443
+ if (!input || typeof input !== "object")
11444
+ return toolName;
11445
+ const hintField = FILE_PATH_FIELD_BY_TOOL[toolName] ?? (toolName === "Bash" ? "command" : void 0);
11446
+ const raw = hintField ? input[hintField] : void 0;
11447
+ if (typeof raw === "string" && raw) {
11448
+ const short = raw.length > TOOL_INPUT_HINT_LEN ? `${raw.slice(0, TOOL_INPUT_HINT_LEN)}\u2026` : raw;
11449
+ return `${toolName}(${short})`;
11450
+ }
11451
+ return toolName;
11452
+ }
11453
+ function formatPermissionRequestQuestion(payload) {
11454
+ const p = payload;
11455
+ return `crew needs permission to run ${summarizeToolInput(p?.tool_name, p?.tool_input)}`;
11456
+ }
11457
+ function hasActiveBackgroundWork(payload) {
11458
+ const p = payload;
11459
+ const tasks = p?.background_tasks;
11460
+ if (Array.isArray(tasks) && tasks.length > 0)
11461
+ return true;
11462
+ const crons = p?.session_crons;
11463
+ if (Array.isArray(crons) && crons.length > 0)
11464
+ return true;
11465
+ return false;
11466
+ }
11467
+ function redactApiError(error) {
11468
+ if (typeof error !== "string" || !error.trim())
11469
+ return "unknown API error";
11470
+ let scrubbed = error.trim();
11471
+ for (const re of SECRET_PATTERNS)
11472
+ scrubbed = scrubbed.replace(re, "[redacted]");
11473
+ return scrubbed.length > MAX_API_ERROR_LEN ? `${scrubbed.slice(0, MAX_API_ERROR_LEN)}\u2026` : scrubbed;
11474
+ }
11276
11475
  function mapClaudeHookToEvent(event, payload, taskId) {
11277
11476
  switch (event) {
11278
11477
  case "PreToolUse": {
@@ -11280,7 +11479,7 @@ function mapClaudeHookToEvent(event, payload, taskId) {
11280
11479
  if (toolName !== "AskUserQuestion")
11281
11480
  return null;
11282
11481
  const question = formatAskUserQuestionPrompt(payload?.tool_input) ?? "crew opened an AskUserQuestion prompt (options unavailable)";
11283
- return { type: "task.input.requested", id: taskId, requestId: nextAskUserQuestionRequestId++, question };
11482
+ return { type: "task.input.requested", id: taskId, requestId: nextHookRequestId++, question };
11284
11483
  }
11285
11484
  case "Stop": {
11286
11485
  const text = resolveLastAssistantText(payload);
@@ -11288,15 +11487,36 @@ function mapClaudeHookToEvent(event, payload, taskId) {
11288
11487
  if (question) {
11289
11488
  return { type: "task.blocked", id: taskId, reason: "crew asked a question (auto-detected)", question };
11290
11489
  }
11291
- return { type: "task.turn.completed", id: taskId, turnId: "hook-stop" };
11490
+ if (hasActiveBackgroundWork(payload)) {
11491
+ return { type: "task.progress", id: taskId, note: "stop-background-work" };
11492
+ }
11493
+ logStopPermissionMode(payload, taskId);
11494
+ return { type: "task.turn.completed", id: taskId, turnId: resolveTurnId(payload) };
11292
11495
  }
11496
+ case "StopFailure":
11497
+ return {
11498
+ type: "task.turn.failed",
11499
+ id: taskId,
11500
+ turnId: resolveTurnId(payload),
11501
+ error: redactApiError(payload?.error)
11502
+ };
11293
11503
  case "Notification": {
11294
- const msg = payload?.message;
11295
- if (typeof msg === "string" && isPermissionNotification(msg)) {
11296
- return { type: "task.blocked", id: taskId, reason: "crew awaiting permission (notification hook)", question: msg };
11504
+ const cls = classifyNotification(payload);
11505
+ if (cls.kind === "blocked") {
11506
+ return { type: "task.blocked", id: taskId, reason: "crew awaiting permission (notification hook)", question: cls.question };
11507
+ }
11508
+ if (cls.kind === "input-requested") {
11509
+ return { type: "task.input.requested", id: taskId, requestId: nextHookRequestId++, question: cls.question };
11297
11510
  }
11298
11511
  return { type: "task.progress", id: taskId, note: "notification" };
11299
11512
  }
11513
+ case "PermissionRequest":
11514
+ return {
11515
+ type: "task.blocked",
11516
+ id: taskId,
11517
+ reason: "crew awaiting permission (permission-request hook)",
11518
+ question: formatPermissionRequestQuestion(payload)
11519
+ };
11300
11520
  case "SessionEnd":
11301
11521
  return { type: "task.session.ended", id: taskId };
11302
11522
  case "SubagentStop":
@@ -11308,14 +11528,14 @@ function mapClaudeHookToEvent(event, payload, taskId) {
11308
11528
  return null;
11309
11529
  }
11310
11530
  }
11311
- var EVENTS, MATCHED_EVENTS, nextAskUserQuestionRequestId, CAPTAIN_MEMORY_PATH_RE, DENY_REASON, FILE_PATH_FIELD_BY_TOOL, claudeInteractive;
11531
+ var EVENTS, MATCHED_EVENTS, nextHookRequestId, CAPTAIN_MEMORY_PATH_RE, DENY_REASON, FILE_PATH_FIELD_BY_TOOL, TOOL_INPUT_HINT_LEN, SECRET_PATTERNS, MAX_API_ERROR_LEN, claudeInteractive;
11312
11532
  var init_claude3 = __esm({
11313
11533
  "packages/agents/dist/interactive/claude.js"() {
11314
- EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit"];
11534
+ EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit", "PermissionRequest", "StopFailure"];
11315
11535
  MATCHED_EVENTS = [
11316
11536
  ["PreToolUse", "AskUserQuestion"]
11317
11537
  ];
11318
- nextAskUserQuestionRequestId = Date.now();
11538
+ nextHookRequestId = Date.now();
11319
11539
  CAPTAIN_MEMORY_PATH_RE = /\.claude\/projects\/.*\/memory\//;
11320
11540
  DENY_REASON = "Crews do not write captain memory. Put the finding in your done/blocked message; the captain decides what is durable (#556).";
11321
11541
  FILE_PATH_FIELD_BY_TOOL = {
@@ -11324,6 +11544,16 @@ var init_claude3 = __esm({
11324
11544
  MultiEdit: "file_path",
11325
11545
  NotebookEdit: "notebook_path"
11326
11546
  };
11547
+ TOOL_INPUT_HINT_LEN = 80;
11548
+ SECRET_PATTERNS = [
11549
+ /sk-ant-[A-Za-z0-9_-]{10,}/g,
11550
+ // Anthropic API key
11551
+ /gh[pousr]_[A-Za-z0-9]{10,}/g,
11552
+ // GitHub token
11553
+ /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g
11554
+ // Telegram bot token shape
11555
+ ];
11556
+ MAX_API_ERROR_LEN = 300;
11327
11557
  claudeInteractive = {
11328
11558
  provider: "claude",
11329
11559
  tier: "strong",
@@ -12287,6 +12517,7 @@ __export(dist_exports4, {
12287
12517
  buildAgentCmd: () => buildAgentCmd,
12288
12518
  buildCodexDeveloperInstructions: () => buildCodexDeveloperInstructions,
12289
12519
  buildUserEnvelope: () => buildUserEnvelope,
12520
+ classifyNotification: () => classifyNotification,
12290
12521
  classifyPaneTail: () => classifyPaneTail2,
12291
12522
  claudeInteractive: () => claudeInteractive,
12292
12523
  createClaudeDriver: () => createClaudeDriver,
@@ -12303,8 +12534,10 @@ __export(dist_exports4, {
12303
12534
  deriveTranscriptPath: () => deriveTranscriptPath,
12304
12535
  detectTrailingQuestion: () => detectTrailingQuestion2,
12305
12536
  formatAskUserQuestionPrompt: () => formatAskUserQuestionPrompt,
12537
+ formatPermissionRequestQuestion: () => formatPermissionRequestQuestion,
12306
12538
  getHeadlessAdapter: () => getHeadlessAdapter,
12307
12539
  getInteractiveAdapter: () => getInteractiveAdapter,
12540
+ hasActiveBackgroundWork: () => hasActiveBackgroundWork,
12308
12541
  isPermissionNotification: () => isPermissionNotification,
12309
12542
  mapClaudeHookToEvent: () => mapClaudeHookToEvent,
12310
12543
  mergeClaudeHooks: () => mergeClaudeHooks,
@@ -12315,6 +12548,8 @@ __export(dist_exports4, {
12315
12548
  readClaudeStatus: () => readClaudeStatus,
12316
12549
  readClaudeStatusByCwd: () => readClaudeStatusByCwd,
12317
12550
  readClaudeStatusBySocketPath: () => readClaudeStatusBySocketPath,
12551
+ redactApiError: () => redactApiError,
12552
+ resolveTurnId: () => resolveTurnId,
12318
12553
  runHeadless: () => runHeadless,
12319
12554
  shouldReattachCodex: () => shouldReattachCodex,
12320
12555
  toLifecycleSnapshot: () => toLifecycleSnapshot,
@@ -18805,10 +19040,14 @@ function mapHookSub(sub, payload, taskId) {
18805
19040
  return { type: "task.first-turn.confirmed", id: taskId };
18806
19041
  case "stop":
18807
19042
  return mapClaudeHookToEvent("Stop", payload, taskId);
19043
+ case "stop-failure":
19044
+ return mapClaudeHookToEvent("StopFailure", payload, taskId);
18808
19045
  case "notification":
18809
19046
  return mapClaudeHookToEvent("Notification", payload, taskId);
18810
19047
  case "ask-question":
18811
19048
  return mapClaudeHookToEvent("PreToolUse", payload, taskId);
19049
+ case "permission-request":
19050
+ return mapClaudeHookToEvent("PermissionRequest", payload, taskId);
18812
19051
  case "session-end":
18813
19052
  return mapClaudeHookToEvent("SessionEnd", payload, taskId);
18814
19053
  default:
@@ -19310,6 +19549,49 @@ function queryClaudeMem(dbPath, project) {
19310
19549
  // packages/cli/src/lib/handoff-transcript.ts
19311
19550
  import fs31 from "fs";
19312
19551
  var TRANSCRIPT_BYTE_CAP = 2e5;
19552
+ var DIGEST_BYTE_CAP = 8 * 1024;
19553
+ var PROMPT_TRUNCATE_CHARS = 300;
19554
+ var ASSISTANT_TRUNCATE_CHARS = 300;
19555
+ function truncate2(text, maxChars) {
19556
+ const trimmed = text.trim();
19557
+ return trimmed.length <= maxChars ? trimmed : `${trimmed.slice(0, maxChars)}\u2026`;
19558
+ }
19559
+ var ISSUE_OR_PR_REF_RE = /#\d+/g;
19560
+ var COMMIT_REF_RE = /\b[0-9a-f]{7,40}\b/gi;
19561
+ function findRefs(text) {
19562
+ const refs = [];
19563
+ for (const m of text.matchAll(ISSUE_OR_PR_REF_RE)) refs.push(m[0]);
19564
+ for (const m of text.matchAll(COMMIT_REF_RE)) {
19565
+ const token = m[0];
19566
+ if (/[0-9]/.test(token) && /[a-f]/i.test(token)) refs.push(token.toLowerCase());
19567
+ }
19568
+ return refs;
19569
+ }
19570
+ function byteSize(value) {
19571
+ return Buffer.byteLength(JSON.stringify(value), "utf-8");
19572
+ }
19573
+ function capDigest(digest, capBytes = DIGEST_BYTE_CAP) {
19574
+ if (byteSize(digest) <= capBytes) return digest;
19575
+ const capped = {
19576
+ ...digest,
19577
+ userPrompts: [...digest.userPrompts],
19578
+ assistantTexts: [...digest.assistantTexts],
19579
+ filesTouched: [...digest.filesTouched],
19580
+ refs: [...digest.refs],
19581
+ truncated: true
19582
+ };
19583
+ let dropFromPrompts = true;
19584
+ while (byteSize(capped) > capBytes && (capped.userPrompts.length > 0 || capped.assistantTexts.length > 0)) {
19585
+ const target = dropFromPrompts && capped.userPrompts.length > 0 ? capped.userPrompts : capped.assistantTexts;
19586
+ if (target.length > 0) target.shift();
19587
+ dropFromPrompts = !dropFromPrompts;
19588
+ }
19589
+ while (byteSize(capped) > capBytes && (capped.filesTouched.length > 0 || capped.refs.length > 0)) {
19590
+ if (capped.filesTouched.length > 0) capped.filesTouched.shift();
19591
+ else capped.refs.shift();
19592
+ }
19593
+ return capped;
19594
+ }
19313
19595
  function tailOf(content, byteCap) {
19314
19596
  const buf = Buffer.from(content, "utf-8");
19315
19597
  if (buf.length <= byteCap) return content;
@@ -19319,6 +19601,15 @@ function tailOf(content, byteCap) {
19319
19601
  function extractMessages(tailText) {
19320
19602
  let lastUserMessage = null;
19321
19603
  let lastAssistantText = null;
19604
+ const userPrompts = [];
19605
+ const assistantTexts = [];
19606
+ const toolCalls = {};
19607
+ const filesTouched = /* @__PURE__ */ new Set();
19608
+ const refs = /* @__PURE__ */ new Set();
19609
+ let currentTurnText = null;
19610
+ const collectRefs = (text) => {
19611
+ for (const ref of findRefs(text)) refs.add(ref);
19612
+ };
19322
19613
  for (const line of tailText.split("\n")) {
19323
19614
  if (!line.trim()) continue;
19324
19615
  let obj;
@@ -19328,20 +19619,48 @@ function extractMessages(tailText) {
19328
19619
  continue;
19329
19620
  }
19330
19621
  if (obj.type === "user" && typeof obj.message?.content === "string") {
19622
+ if (currentTurnText !== null) {
19623
+ assistantTexts.push(truncate2(currentTurnText, ASSISTANT_TRUNCATE_CHARS));
19624
+ currentTurnText = null;
19625
+ }
19331
19626
  lastUserMessage = obj.message.content;
19627
+ userPrompts.push(truncate2(obj.message.content, PROMPT_TRUNCATE_CHARS));
19628
+ collectRefs(obj.message.content);
19332
19629
  } else if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
19333
- const texts = obj.message.content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
19334
- if (texts.length > 0) lastAssistantText = texts.join("\n");
19335
- }
19336
- }
19337
- return { lastUserMessage, lastAssistantText };
19630
+ const blocks = obj.message.content;
19631
+ const texts = blocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text);
19632
+ if (texts.length > 0) {
19633
+ currentTurnText = texts.join("\n");
19634
+ lastAssistantText = currentTurnText;
19635
+ collectRefs(currentTurnText);
19636
+ }
19637
+ for (const block of blocks) {
19638
+ if (block.type !== "tool_use" || typeof block.name !== "string") continue;
19639
+ toolCalls[block.name] = (toolCalls[block.name] ?? 0) + 1;
19640
+ const filePath = block.input?.file_path;
19641
+ if (typeof filePath === "string") filesTouched.add(filePath);
19642
+ }
19643
+ }
19644
+ }
19645
+ if (currentTurnText !== null) {
19646
+ assistantTexts.push(truncate2(currentTurnText, ASSISTANT_TRUNCATE_CHARS));
19647
+ }
19648
+ const digest = capDigest({
19649
+ userPrompts,
19650
+ assistantTexts,
19651
+ toolCalls,
19652
+ filesTouched: [...filesTouched],
19653
+ refs: [...refs],
19654
+ truncated: false
19655
+ });
19656
+ return { lastUserMessage, lastAssistantText, digest };
19338
19657
  }
19339
19658
  function extractTranscriptTail(transcriptPath, byteCap = TRANSCRIPT_BYTE_CAP) {
19340
19659
  if (!fs31.existsSync(transcriptPath)) return null;
19341
19660
  const content = fs31.readFileSync(transcriptPath, "utf-8");
19342
- const { lastUserMessage, lastAssistantText } = extractMessages(tailOf(content, byteCap));
19661
+ const { lastUserMessage, lastAssistantText, digest } = extractMessages(tailOf(content, byteCap));
19343
19662
  const mtimeIso = fs31.statSync(transcriptPath).mtime.toISOString();
19344
- return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText };
19663
+ return { path: transcriptPath, mtimeIso, lastUserMessage, lastAssistantText, digest };
19345
19664
  }
19346
19665
 
19347
19666
  // packages/cli/src/lib/handoff-archive.ts
@@ -19457,7 +19776,7 @@ if (process.argv[2] !== "config") {
19457
19776
  } catch {
19458
19777
  }
19459
19778
  }
19460
- if (!process.env.SQUADRANT_DAEMON_SKIP) {
19779
+ if (!process.env.SQUADRANT_DAEMON_SKIP && !isReadOnlyCrewCommand(process.argv)) {
19461
19780
  ensureDaemon(void 0, { operatorInitiated: isOperatorInitiatedCommand(process.argv[2]) });
19462
19781
  }
19463
19782
  var program = new Command35();