squadrant 0.19.4 → 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",
@@ -9219,6 +9228,10 @@ function mapSubToLifecycle(sub) {
9219
9228
  return "needsInput";
9220
9229
  case "ask-question":
9221
9230
  return "needsInput";
9231
+ case "permission-request":
9232
+ return "needsInput";
9233
+ case "stop-failure":
9234
+ return "idle";
9222
9235
  case "session-end":
9223
9236
  return "session-end";
9224
9237
  default:
@@ -9260,7 +9273,13 @@ var init_native_hook_source = __esm({
9260
9273
  ["Stop", "stop"],
9261
9274
  ["Notification", "notification"],
9262
9275
  ["PreToolUse", "ask-question", "AskUserQuestion"],
9263
- ["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"]
9264
9283
  ];
9265
9284
  DEFAULT_HOOK_CMD = "squadrant hooks";
9266
9285
  NativeHookSource = class {
@@ -9625,12 +9644,12 @@ function createClaudeDriver() {
9625
9644
  if (opts.settingsPath) {
9626
9645
  cmd += ` --settings ${opts.settingsPath}`;
9627
9646
  }
9628
- if (opts.messagingSocketPath) {
9629
- cmd += ` --messaging-socket-path ${opts.messagingSocketPath}`;
9630
- }
9631
9647
  const pluginSubdir = opts.role === "crew" ? "plugin-crew" : "plugin";
9632
9648
  const pluginDir = `${process.env.HOME}/.config/squadrant/${pluginSubdir}`;
9633
9649
  cmd += ` --plugin-dir ${pluginDir}`;
9650
+ if (opts.messagingSocketPath) {
9651
+ cmd += ` --messaging-socket-path ${opts.messagingSocketPath}`;
9652
+ }
9634
9653
  if (!opts.interactive) {
9635
9654
  cmd += ` -p "${opts.prompt.replace(/"/g, '\\"')}"`;
9636
9655
  }
@@ -11389,6 +11408,70 @@ function decideCaptainMemoryWrite(toolName, toolInput, env, homeDir) {
11389
11408
  return { decision: "allow" };
11390
11409
  return { decision: "deny", reason: DENY_REASON };
11391
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
+ }
11392
11475
  function mapClaudeHookToEvent(event, payload, taskId) {
11393
11476
  switch (event) {
11394
11477
  case "PreToolUse": {
@@ -11396,7 +11479,7 @@ function mapClaudeHookToEvent(event, payload, taskId) {
11396
11479
  if (toolName !== "AskUserQuestion")
11397
11480
  return null;
11398
11481
  const question = formatAskUserQuestionPrompt(payload?.tool_input) ?? "crew opened an AskUserQuestion prompt (options unavailable)";
11399
- return { type: "task.input.requested", id: taskId, requestId: nextAskUserQuestionRequestId++, question };
11482
+ return { type: "task.input.requested", id: taskId, requestId: nextHookRequestId++, question };
11400
11483
  }
11401
11484
  case "Stop": {
11402
11485
  const text = resolveLastAssistantText(payload);
@@ -11404,15 +11487,36 @@ function mapClaudeHookToEvent(event, payload, taskId) {
11404
11487
  if (question) {
11405
11488
  return { type: "task.blocked", id: taskId, reason: "crew asked a question (auto-detected)", question };
11406
11489
  }
11407
- 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) };
11408
11495
  }
11496
+ case "StopFailure":
11497
+ return {
11498
+ type: "task.turn.failed",
11499
+ id: taskId,
11500
+ turnId: resolveTurnId(payload),
11501
+ error: redactApiError(payload?.error)
11502
+ };
11409
11503
  case "Notification": {
11410
- const msg = payload?.message;
11411
- if (typeof msg === "string" && isPermissionNotification(msg)) {
11412
- 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 };
11413
11510
  }
11414
11511
  return { type: "task.progress", id: taskId, note: "notification" };
11415
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
+ };
11416
11520
  case "SessionEnd":
11417
11521
  return { type: "task.session.ended", id: taskId };
11418
11522
  case "SubagentStop":
@@ -11424,14 +11528,14 @@ function mapClaudeHookToEvent(event, payload, taskId) {
11424
11528
  return null;
11425
11529
  }
11426
11530
  }
11427
- 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;
11428
11532
  var init_claude3 = __esm({
11429
11533
  "packages/agents/dist/interactive/claude.js"() {
11430
- EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit"];
11534
+ EVENTS = ["Stop", "SubagentStop", "SessionEnd", "PostToolUse", "Notification", "UserPromptSubmit", "PermissionRequest", "StopFailure"];
11431
11535
  MATCHED_EVENTS = [
11432
11536
  ["PreToolUse", "AskUserQuestion"]
11433
11537
  ];
11434
- nextAskUserQuestionRequestId = Date.now();
11538
+ nextHookRequestId = Date.now();
11435
11539
  CAPTAIN_MEMORY_PATH_RE = /\.claude\/projects\/.*\/memory\//;
11436
11540
  DENY_REASON = "Crews do not write captain memory. Put the finding in your done/blocked message; the captain decides what is durable (#556).";
11437
11541
  FILE_PATH_FIELD_BY_TOOL = {
@@ -11440,6 +11544,16 @@ var init_claude3 = __esm({
11440
11544
  MultiEdit: "file_path",
11441
11545
  NotebookEdit: "notebook_path"
11442
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;
11443
11557
  claudeInteractive = {
11444
11558
  provider: "claude",
11445
11559
  tier: "strong",
@@ -12403,6 +12517,7 @@ __export(dist_exports4, {
12403
12517
  buildAgentCmd: () => buildAgentCmd,
12404
12518
  buildCodexDeveloperInstructions: () => buildCodexDeveloperInstructions,
12405
12519
  buildUserEnvelope: () => buildUserEnvelope,
12520
+ classifyNotification: () => classifyNotification,
12406
12521
  classifyPaneTail: () => classifyPaneTail2,
12407
12522
  claudeInteractive: () => claudeInteractive,
12408
12523
  createClaudeDriver: () => createClaudeDriver,
@@ -12419,8 +12534,10 @@ __export(dist_exports4, {
12419
12534
  deriveTranscriptPath: () => deriveTranscriptPath,
12420
12535
  detectTrailingQuestion: () => detectTrailingQuestion2,
12421
12536
  formatAskUserQuestionPrompt: () => formatAskUserQuestionPrompt,
12537
+ formatPermissionRequestQuestion: () => formatPermissionRequestQuestion,
12422
12538
  getHeadlessAdapter: () => getHeadlessAdapter,
12423
12539
  getInteractiveAdapter: () => getInteractiveAdapter,
12540
+ hasActiveBackgroundWork: () => hasActiveBackgroundWork,
12424
12541
  isPermissionNotification: () => isPermissionNotification,
12425
12542
  mapClaudeHookToEvent: () => mapClaudeHookToEvent,
12426
12543
  mergeClaudeHooks: () => mergeClaudeHooks,
@@ -12431,6 +12548,8 @@ __export(dist_exports4, {
12431
12548
  readClaudeStatus: () => readClaudeStatus,
12432
12549
  readClaudeStatusByCwd: () => readClaudeStatusByCwd,
12433
12550
  readClaudeStatusBySocketPath: () => readClaudeStatusBySocketPath,
12551
+ redactApiError: () => redactApiError,
12552
+ resolveTurnId: () => resolveTurnId,
12434
12553
  runHeadless: () => runHeadless,
12435
12554
  shouldReattachCodex: () => shouldReattachCodex,
12436
12555
  toLifecycleSnapshot: () => toLifecycleSnapshot,
@@ -18921,10 +19040,14 @@ function mapHookSub(sub, payload, taskId) {
18921
19040
  return { type: "task.first-turn.confirmed", id: taskId };
18922
19041
  case "stop":
18923
19042
  return mapClaudeHookToEvent("Stop", payload, taskId);
19043
+ case "stop-failure":
19044
+ return mapClaudeHookToEvent("StopFailure", payload, taskId);
18924
19045
  case "notification":
18925
19046
  return mapClaudeHookToEvent("Notification", payload, taskId);
18926
19047
  case "ask-question":
18927
19048
  return mapClaudeHookToEvent("PreToolUse", payload, taskId);
19049
+ case "permission-request":
19050
+ return mapClaudeHookToEvent("PermissionRequest", payload, taskId);
18928
19051
  case "session-end":
18929
19052
  return mapClaudeHookToEvent("SessionEnd", payload, taskId);
18930
19053
  default: