squadrant 0.16.5 → 0.16.6

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
@@ -1323,6 +1323,11 @@ function nextPendingTool(current, ev, now) {
1323
1323
  return void 0;
1324
1324
  return current;
1325
1325
  }
1326
+ function nextPendingMonitor(current, ev, now) {
1327
+ if (ev.note === "agent.hook.PreToolUse" && ev.tool === "Monitor")
1328
+ return { since: now };
1329
+ return current;
1330
+ }
1326
1331
  function reduce(rec, ev, now) {
1327
1332
  if (ev.type === "task.reopened") {
1328
1333
  return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type };
@@ -1339,14 +1344,17 @@ function reduce(rec, ev, now) {
1339
1344
  sessionId: ev.sessionId ?? rec.sessionId,
1340
1345
  question: void 0,
1341
1346
  // resuming after a blocked→reply clears the question
1342
- pendingTool: void 0
1347
+ pendingTool: void 0,
1343
1348
  // #354: a new turn closes any prior tool window
1349
+ pendingMonitor: void 0
1350
+ // #594a: same reset — a new turn moots any prior watch
1344
1351
  };
1345
1352
  case "task.progress": {
1346
1353
  const pendingTool = nextPendingTool(rec.pendingTool, ev, now);
1354
+ const pendingMonitor = nextPendingMonitor(rec.pendingMonitor, ev, now);
1347
1355
  if (isStickyAttention(rec.state))
1348
- return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool };
1349
- const b = { ...base, pendingTool };
1356
+ return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool, pendingMonitor };
1357
+ const b = { ...base, pendingTool, pendingMonitor };
1350
1358
  if (rec.state === "awaiting-input" || rec.state === "stalled")
1351
1359
  return { ...stampAttempt(b, {}, now), state: "working" };
1352
1360
  return stampAttempt(b, {}, now);
@@ -1360,9 +1368,9 @@ function reduce(rec, ev, now) {
1360
1368
  case "task.blocked":
1361
1369
  if (rec.state === "blocked")
1362
1370
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
1363
- return { ...base, state: "blocked", question: ev.question, pendingTool: void 0 };
1371
+ return { ...base, state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
1364
1372
  case "task.review":
1365
- return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0 };
1373
+ return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0, pendingMonitor: void 0 };
1366
1374
  case "task.done":
1367
1375
  if (rec.state === "review" && ev.source !== "approve") {
1368
1376
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
@@ -1377,19 +1385,19 @@ function reduce(rec, ev, now) {
1377
1385
  case "task.session":
1378
1386
  return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
1379
1387
  case "task.turn.started":
1380
- return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0 };
1388
+ return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
1381
1389
  case "task.turn.completed":
1382
1390
  if (isStickyAttention(rec.state))
1383
1391
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
1384
- if (rec.pendingTool)
1392
+ if (rec.pendingTool || rec.pendingMonitor)
1385
1393
  return stampAttempt(base, {}, now);
1386
- return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
1394
+ return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0, pendingMonitor: void 0 };
1387
1395
  case "task.delta":
1388
1396
  return stampAttempt(base, {}, now);
1389
1397
  // heartbeat-only
1390
1398
  case "task.input.requested":
1391
1399
  case "task.approval.requested":
1392
- return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0 };
1400
+ return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
1393
1401
  case "task.reattached":
1394
1402
  return stampAttempt(base, {}, now);
1395
1403
  case "task.first-turn.confirmed":
@@ -1414,15 +1422,21 @@ var init_state_machine = __esm({
1414
1422
  });
1415
1423
 
1416
1424
  // packages/core/dist/watchdog.js
1417
- function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
1425
+ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStallMs = MONITOR_STALL_BUDGET_MS) {
1418
1426
  if (rec.state !== "working")
1419
1427
  return null;
1420
1428
  if (rec.mode === "interactive") {
1421
- if (!rec.pendingTool)
1422
- return null;
1423
- if (now - rec.pendingTool.since <= toolStallMs)
1424
- return null;
1425
- return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
1429
+ if (rec.pendingTool) {
1430
+ if (now - rec.pendingTool.since <= toolStallMs)
1431
+ return null;
1432
+ return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
1433
+ }
1434
+ if (rec.pendingMonitor) {
1435
+ if (now - rec.pendingMonitor.since <= monitorStallMs)
1436
+ return null;
1437
+ return { ...rec, state: "stalled", lastEvent: "watchdog.monitor-stall" };
1438
+ }
1439
+ return null;
1426
1440
  }
1427
1441
  const liveness = rec.attempts.at(-1)?.lastHeartbeatAt ?? rec.lastHeartbeat;
1428
1442
  if (now - liveness <= rec.heartbeatBudgetMs)
@@ -1432,12 +1446,13 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
1432
1446
  function recoverStall(rec, now) {
1433
1447
  if (rec.state !== "stalled")
1434
1448
  return null;
1435
- return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0 };
1449
+ return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0, pendingMonitor: void 0 };
1436
1450
  }
1437
- var TOOL_STALL_BUDGET_MS;
1451
+ var TOOL_STALL_BUDGET_MS, MONITOR_STALL_BUDGET_MS;
1438
1452
  var init_watchdog = __esm({
1439
1453
  "packages/core/dist/watchdog.js"() {
1440
1454
  TOOL_STALL_BUDGET_MS = 10 * 60 * 1e3;
1455
+ MONITOR_STALL_BUDGET_MS = 60 * 60 * 1e3;
1441
1456
  }
1442
1457
  });
1443
1458
 
@@ -1711,7 +1726,7 @@ function createDaemon(deps) {
1711
1726
  store.delete(r.project, r.id);
1712
1727
  continue;
1713
1728
  }
1714
- if (!TERMINAL_STATES.has(r.state)) {
1729
+ if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
1715
1730
  const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
1716
1731
  if (t - r.createdAt > ceiling) {
1717
1732
  const prevState = r.state;
@@ -1756,7 +1771,7 @@ function createDaemon(deps) {
1756
1771
  const idle = evaluateStall(r, t);
1757
1772
  if (idle) {
1758
1773
  store.put(idle);
1759
- const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
1774
+ 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 };
1760
1775
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
1761
1776
  continue;
1762
1777
  }
@@ -3319,9 +3334,11 @@ var init_defer_delivery = __esm({
3319
3334
  "packages/core/dist/delivery/defer-delivery.js"() {
3320
3335
  DeferDelivery = class extends Error {
3321
3336
  draft;
3322
- constructor(draft = null) {
3337
+ reason;
3338
+ constructor(draft = null, reason = "draft") {
3323
3339
  super("deferred: captain composing");
3324
3340
  this.draft = draft;
3341
+ this.reason = reason;
3325
3342
  this.name = "DeferDelivery";
3326
3343
  }
3327
3344
  };
@@ -3345,6 +3362,7 @@ var init_captain_delivery = __esm({
3345
3362
  deferCounts = /* @__PURE__ */ new Map();
3346
3363
  lastContent = /* @__PURE__ */ new Map();
3347
3364
  stableCounts = /* @__PURE__ */ new Map();
3365
+ lastReason = /* @__PURE__ */ new Map();
3348
3366
  constructor(opts) {
3349
3367
  this.opts = opts;
3350
3368
  }
@@ -3367,29 +3385,40 @@ var init_captain_delivery = __esm({
3367
3385
  this.deferCounts.delete(seq);
3368
3386
  this.stableCounts.delete(seq);
3369
3387
  this.lastContent.delete(seq);
3388
+ this.lastReason.delete(seq);
3370
3389
  return { delivered: true };
3371
3390
  } catch (e) {
3372
3391
  if (e instanceof DeferDelivery) {
3373
3392
  this.deferCounts.set(seq, deferCount + 1);
3374
3393
  const content = e.draft;
3394
+ let stableCount;
3375
3395
  if (content && content === this.lastContent.get(seq)) {
3376
- this.stableCounts.set(seq, (this.stableCounts.get(seq) ?? 0) + 1);
3396
+ stableCount = (this.stableCounts.get(seq) ?? 0) + 1;
3397
+ this.stableCounts.set(seq, stableCount);
3377
3398
  } else {
3399
+ stableCount = 0;
3378
3400
  this.stableCounts.set(seq, 0);
3379
3401
  }
3380
3402
  this.lastContent.set(seq, content);
3381
- return { deferred: true };
3403
+ const reason = e.reason !== "draft" ? e.reason : stableCount >= this.opts.stableProbePolls ? "stable" : "draft";
3404
+ this.lastReason.set(seq, reason);
3405
+ return { deferred: true, reason };
3382
3406
  }
3383
- return { deferred: true };
3407
+ this.lastReason.set(seq, "unknown");
3408
+ return { deferred: true, reason: "unknown" };
3384
3409
  }
3385
3410
  }
3386
3411
  /** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
3387
3412
  stats() {
3388
3413
  let maxDeferCount = 0;
3389
- for (const c of this.deferCounts.values())
3390
- if (c > maxDeferCount)
3414
+ let reason;
3415
+ for (const [seq, c] of this.deferCounts) {
3416
+ if (c > maxDeferCount) {
3391
3417
  maxDeferCount = c;
3392
- return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers };
3418
+ reason = this.lastReason.get(seq);
3419
+ }
3420
+ }
3421
+ return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers, reason };
3393
3422
  }
3394
3423
  };
3395
3424
  }
@@ -3489,6 +3518,10 @@ function createDelivery(ctx, daemonCmux) {
3489
3518
  const notifyFault = ctx.notifyFault ?? (() => {
3490
3519
  });
3491
3520
  const defaultNotify = async (args) => {
3521
+ const fresh = store.get(args.project, args.record.id);
3522
+ if (fresh && TERMINAL_STATES.has(fresh.state) && fresh.state !== args.record.state) {
3523
+ return;
3524
+ }
3492
3525
  try {
3493
3526
  await appendToMailbox({
3494
3527
  stateRoot,
@@ -3578,16 +3611,19 @@ function createDelivery(ctx, daemonCmux) {
3578
3611
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
3579
3612
  await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
3580
3613
  } else {
3581
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred`);
3614
+ const { maxDeferCount } = d.stats();
3615
+ if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
3616
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
3617
+ }
3582
3618
  break;
3583
3619
  }
3584
3620
  }
3585
3621
  const stuck = d.stats().stuck;
3586
3622
  if (stuck && !stuckNotified.has(project)) {
3587
3623
  stuckNotified.add(project);
3588
- const { maxDeferCount } = d.stats();
3589
- log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
3590
- const text = `\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.`;
3624
+ const { maxDeferCount, reason } = d.stats();
3625
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
3626
+ 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.`;
3591
3627
  appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
3592
3628
  Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
3593
3629
  telegramBridge?.pushRaw(project, text);
@@ -6079,6 +6115,7 @@ __export(dist_exports2, {
6079
6115
  GROUP_DISPATCH_WARMUP_TIMEOUT_MS: () => GROUP_DISPATCH_WARMUP_TIMEOUT_MS,
6080
6116
  IDLE_DEBOUNCE_MS: () => IDLE_DEBOUNCE_MS,
6081
6117
  LABEL: () => LABEL,
6118
+ MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
6082
6119
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
6083
6120
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
6084
6121
  STALE_THRESHOLD_MS: () => STALE_THRESHOLD_MS,
@@ -6148,6 +6185,7 @@ __export(dist_exports2, {
6148
6185
  isDaemonSocketLive: () => isDaemonSocketLive,
6149
6186
  isNotifyActive: () => isNotifyActive,
6150
6187
  isSideTitle: () => isSideTitle,
6188
+ isStickyAttention: () => isStickyAttention,
6151
6189
  isTurnAccepted: () => isTurnAccepted,
6152
6190
  kickstartArgv: () => kickstartArgv,
6153
6191
  launchOneWorkspace: () => launchOneWorkspace,
@@ -6666,9 +6704,9 @@ function createCmuxDriver() {
6666
6704
  }
6667
6705
  const draft = parseDraftFromScreen(screen);
6668
6706
  if (draft === null)
6669
- throw new DeferDelivery(null);
6707
+ throw new DeferDelivery(null, "no-box");
6670
6708
  if (hasModalOptionList(screen))
6671
- throw new DeferDelivery(null);
6709
+ throw new DeferDelivery(null, "modal");
6672
6710
  if (draft === "") {
6673
6711
  await deliver();
6674
6712
  return;
@@ -7493,6 +7531,7 @@ function installClaudeHooks(opts = {}) {
7493
7531
  });
7494
7532
  let settings = {};
7495
7533
  const raw = readFile6(settingsPath);
7534
+ const hadExistingSettings = raw !== void 0;
7496
7535
  if (raw) {
7497
7536
  try {
7498
7537
  settings = JSON.parse(raw);
@@ -7505,6 +7544,7 @@ function installClaudeHooks(opts = {}) {
7505
7544
  }
7506
7545
  const hooks = settings.hooks;
7507
7546
  let changed = false;
7547
+ const repaired = [];
7508
7548
  for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
7509
7549
  if (!Array.isArray(hooks[eventName])) {
7510
7550
  hooks[eventName] = [];
@@ -7516,6 +7556,26 @@ function installClaudeHooks(opts = {}) {
7516
7556
  if (!alreadyPresent) {
7517
7557
  entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
7518
7558
  changed = true;
7559
+ repaired.push(`${eventName}/${sub}`);
7560
+ }
7561
+ }
7562
+ if (repaired.length > 0 && hadExistingSettings) {
7563
+ log(`native-hook: repaired ${repaired.length} missing squadrant hook(s) in ${settingsPath} [${repaired.join(", ")}] \u2014 WARNING: blocked-signalling or lifecycle tracking may have been broken until this run`);
7564
+ }
7565
+ if (opts.claudeEnv && Object.keys(opts.claudeEnv).length > 0) {
7566
+ if (typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env)) {
7567
+ settings.env = {};
7568
+ }
7569
+ const env = settings.env;
7570
+ for (const [key, value] of Object.entries(opts.claudeEnv)) {
7571
+ if (key in env) {
7572
+ if (env[key] !== value) {
7573
+ log(`native-hook: claudeEnv key '${key}' already set to '${String(env[key])}' in ${settingsPath} \u2014 not overwriting with '${value}'`);
7574
+ }
7575
+ continue;
7576
+ }
7577
+ env[key] = value;
7578
+ changed = true;
7519
7579
  }
7520
7580
  }
7521
7581
  if (changed) {
@@ -7590,9 +7650,9 @@ var init_native_hook_source = __esm({
7590
7650
  cache = /* @__PURE__ */ new Map();
7591
7651
  active = false;
7592
7652
  constructor(opts = {}) {
7593
- this.hookInstall = opts.hookInstall ?? {};
7594
7653
  this.log = opts.log ?? (() => {
7595
7654
  });
7655
+ this.hookInstall = { log: this.log, ...opts.hookInstall };
7596
7656
  }
7597
7657
  start(deps) {
7598
7658
  this.deps = deps;
@@ -11484,6 +11544,16 @@ function defaultCreatePr(cwd, o) {
11484
11544
  { cwd, encoding: "utf-8" }
11485
11545
  ).trim();
11486
11546
  }
11547
+ function defaultGetCommitSubject(cwd) {
11548
+ try {
11549
+ return execFileSync6("git", ["-C", cwd, "log", "-1", "--format=%s"], {
11550
+ encoding: "utf-8",
11551
+ stdio: ["ignore", "pipe", "pipe"]
11552
+ }).trim();
11553
+ } catch {
11554
+ return "";
11555
+ }
11556
+ }
11487
11557
  async function runCrewApprove(project, crew, deps) {
11488
11558
  const config = loadConfig();
11489
11559
  const proj = config.projects[project];
@@ -11499,8 +11569,12 @@ async function runCrewApprove(project, crew, deps) {
11499
11569
  const cwd = task.cwd ?? proj.path;
11500
11570
  const base = resolveWorktreeBase(proj.path);
11501
11571
  const branch = crewBranch(crew);
11502
- const title = (task.task ?? branch).split(/\r?\n/)[0].trim().slice(0, 100);
11503
11572
  const body = (task.reviewNote ?? task.task ?? "").trim();
11573
+ const getCommitSubject = deps.getCommitSubject ?? defaultGetCommitSubject;
11574
+ const commitSubject = getCommitSubject(cwd).trim();
11575
+ const reviewNoteFirstLine = (task.reviewNote ?? "").split(/\r?\n/)[0].trim();
11576
+ const taskFirstLine = (task.task ?? branch).split(/\r?\n/)[0].trim();
11577
+ const title = (commitSubject || reviewNoteFirstLine || taskFirstLine).slice(0, 100);
11504
11578
  const pushBranch = deps.pushBranch ?? defaultPushBranch;
11505
11579
  const createPr = deps.createPr ?? defaultCreatePr;
11506
11580
  pushBranch(cwd, branch);