taskplane 0.29.2 → 0.30.0

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.
Files changed (41) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/extensions/reviewer-extension.ts +17 -11
  5. package/extensions/taskplane/abort.ts +50 -18
  6. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  7. package/extensions/taskplane/agent-host.ts +224 -97
  8. package/extensions/taskplane/cleanup.ts +71 -42
  9. package/extensions/taskplane/config-loader.ts +142 -58
  10. package/extensions/taskplane/config-schema.ts +6 -13
  11. package/extensions/taskplane/config.ts +10 -2
  12. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  13. package/extensions/taskplane/diagnostics.ts +13 -13
  14. package/extensions/taskplane/discovery.ts +35 -61
  15. package/extensions/taskplane/engine-worker.ts +53 -46
  16. package/extensions/taskplane/engine.ts +1760 -602
  17. package/extensions/taskplane/execution.ts +426 -206
  18. package/extensions/taskplane/extension.ts +1073 -598
  19. package/extensions/taskplane/formatting.ts +136 -124
  20. package/extensions/taskplane/git.ts +0 -2
  21. package/extensions/taskplane/lane-runner.ts +542 -311
  22. package/extensions/taskplane/mailbox.ts +57 -49
  23. package/extensions/taskplane/merge.ts +662 -383
  24. package/extensions/taskplane/messages.ts +109 -51
  25. package/extensions/taskplane/migrations.ts +1 -1
  26. package/extensions/taskplane/path-resolver.ts +8 -9
  27. package/extensions/taskplane/persistence.ts +425 -262
  28. package/extensions/taskplane/process-registry.ts +36 -7
  29. package/extensions/taskplane/quality-gate.ts +107 -55
  30. package/extensions/taskplane/resume.ts +774 -267
  31. package/extensions/taskplane/sessions.ts +1 -1
  32. package/extensions/taskplane/settings-tui.ts +505 -164
  33. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  34. package/extensions/taskplane/supervisor.ts +477 -270
  35. package/extensions/taskplane/task-executor-core.ts +178 -53
  36. package/extensions/taskplane/types.ts +186 -108
  37. package/extensions/taskplane/verification.ts +27 -22
  38. package/extensions/taskplane/waves.ts +59 -43
  39. package/extensions/taskplane/workspace.ts +14 -12
  40. package/extensions/taskplane/worktree.ts +218 -196
  41. package/package.json +14 -2
@@ -22,8 +22,13 @@
22
22
 
23
23
  import { spawn, type ChildProcess } from "child_process";
24
24
  import {
25
- readFileSync, writeFileSync, appendFileSync, mkdirSync,
26
- existsSync, readdirSync, renameSync,
25
+ readFileSync,
26
+ writeFileSync,
27
+ appendFileSync,
28
+ mkdirSync,
29
+ existsSync,
30
+ readdirSync,
31
+ renameSync,
27
32
  } from "fs";
28
33
  import { join, dirname, basename, resolve } from "path";
29
34
  import { StringDecoder } from "string_decoder";
@@ -121,13 +126,19 @@ import { DEFAULT_WORKER_USER_TOOLS } from "./tool-allowlist-constants.ts";
121
126
  */
122
127
  export function buildWorkerToolsAllowlist(userTools: string | undefined | null): string {
123
128
  const userPart = (userTools && userTools.trim()) || DEFAULT_WORKER_USER_TOOLS;
124
- const rawUserList = userPart.split(",").map((s) => s.trim()).filter(Boolean);
129
+ const rawUserList = userPart
130
+ .split(",")
131
+ .map((s) => s.trim())
132
+ .filter(Boolean);
125
133
  // Guard against delimiter-only / whitespace-only inputs (e.g. ",", " , ")
126
134
  // that would otherwise parse to an empty list and yield bridge-tools-only
127
135
  // workers with no file/shell capabilities.
128
- const userList = rawUserList.length > 0
129
- ? rawUserList
130
- : DEFAULT_WORKER_USER_TOOLS.split(",").map((s) => s.trim()).filter(Boolean);
136
+ const userList =
137
+ rawUserList.length > 0
138
+ ? rawUserList
139
+ : DEFAULT_WORKER_USER_TOOLS.split(",")
140
+ .map((s) => s.trim())
141
+ .filter(Boolean);
131
142
  const merged = new Set<string>(userList);
132
143
  for (const t of ENGINE_BRIDGE_TOOLS) merged.add(t);
133
144
  return Array.from(merged).join(",");
@@ -155,9 +166,13 @@ function extractAssistantText(message: Record<string, unknown>): string {
155
166
  // Guard: skip null/non-object entries to prevent TypeError on malformed streams
156
167
  if (Array.isArray(message.content)) {
157
168
  const textBlocks = message.content
158
- .filter((b: unknown): b is { type: string; text: string } =>
159
- typeof b === "object" && b !== null &&
160
- (b as any).type === "text" && typeof (b as any).text === "string")
169
+ .filter(
170
+ (b: unknown): b is { type: string; text: string } =>
171
+ typeof b === "object" &&
172
+ b !== null &&
173
+ (b as any).type === "text" &&
174
+ typeof (b as any).text === "string",
175
+ )
161
176
  .map((b) => b.text);
162
177
  if (textBlocks.length > 0) return textBlocks.join("\n");
163
178
  }
@@ -304,8 +319,10 @@ function isValidMailboxMessage(obj: any): boolean {
304
319
  typeof obj.batchId === "string" &&
305
320
  typeof obj.from === "string" &&
306
321
  typeof obj.to === "string" &&
307
- typeof obj.timestamp === "number" && Number.isFinite(obj.timestamp) &&
308
- typeof obj.type === "string" && MAILBOX_MESSAGE_TYPES.has(obj.type) &&
322
+ typeof obj.timestamp === "number" &&
323
+ Number.isFinite(obj.timestamp) &&
324
+ typeof obj.type === "string" &&
325
+ MAILBOX_MESSAGE_TYPES.has(obj.type) &&
309
326
  typeof obj.content === "string"
310
327
  );
311
328
  }
@@ -330,7 +347,6 @@ export function spawnAgent(
330
347
  onEvent?: AgentEventCallback,
331
348
  onTelemetry?: AgentTelemetryCallback,
332
349
  ): { promise: Promise<AgentHostResult>; kill: () => void } {
333
-
334
350
  const cliPath = resolvePiCliPath();
335
351
  const closeDelayMs = opts.closeDelayMs ?? 100;
336
352
  const timeoutMs = opts.timeoutMs ?? 0;
@@ -369,9 +385,16 @@ export function spawnAgent(
369
385
  let stdinClosed = false;
370
386
  let assistantMessageEnds = 0;
371
387
  const STATS_REFRESH_EVERY_ASSISTANT_MESSAGES = 5;
372
- let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0;
373
- let costUsd = 0, toolCalls = 0, retries = 0, compactions = 0;
374
- let lastTool = "", error: string | null = null;
388
+ let inputTokens = 0,
389
+ outputTokens = 0,
390
+ cacheReadTokens = 0,
391
+ cacheWriteTokens = 0;
392
+ let costUsd = 0,
393
+ toolCalls = 0,
394
+ retries = 0,
395
+ compactions = 0;
396
+ let lastTool = "",
397
+ error: string | null = null;
375
398
  let contextUsage: AgentHostResult["contextUsage"] = null;
376
399
  let stderrBuffer = "";
377
400
  const STDERR_MAX = 2048;
@@ -388,7 +411,11 @@ export function spawnAgent(
388
411
  timeoutHandle = setTimeout(() => {
389
412
  timedOut = true;
390
413
  killed = true;
391
- try { proc.kill("SIGTERM"); } catch { /* ignore */ }
414
+ try {
415
+ proc.kill("SIGTERM");
416
+ } catch {
417
+ /* ignore */
418
+ }
392
419
  }, timeoutMs);
393
420
  }
394
421
 
@@ -397,12 +424,14 @@ export function spawnAgent(
397
424
  const refreshRegistrySnapshot = (force: boolean = false) => {
398
425
  if (!opts.stateRoot) return;
399
426
  const now = Date.now();
400
- if (!force && (now - lastRegistryRefreshAt) < REGISTRY_REFRESH_INTERVAL_MS) return;
427
+ if (!force && now - lastRegistryRefreshAt < REGISTRY_REFRESH_INTERVAL_MS) return;
401
428
  try {
402
429
  const snapshot = buildRegistrySnapshot(opts.stateRoot, opts.batchId);
403
430
  writeRegistrySnapshot(opts.stateRoot, snapshot);
404
431
  lastRegistryRefreshAt = now;
405
- } catch { /* best effort */ }
432
+ } catch {
433
+ /* best effort */
434
+ }
406
435
  };
407
436
 
408
437
  // Registry integration: write manifest before process is considered visible
@@ -430,10 +459,18 @@ export function spawnAgent(
430
459
  stdinClosed = true;
431
460
  if (closeDelayMs > 0) {
432
461
  setTimeout(() => {
433
- try { proc.stdin?.end(); } catch { /* ignore */ }
462
+ try {
463
+ proc.stdin?.end();
464
+ } catch {
465
+ /* ignore */
466
+ }
434
467
  }, closeDelayMs);
435
468
  } else {
436
- try { proc.stdin?.end(); } catch { /* ignore */ }
469
+ try {
470
+ proc.stdin?.end();
471
+ } catch {
472
+ /* ignore */
473
+ }
437
474
  }
438
475
  }
439
476
 
@@ -456,7 +493,9 @@ export function spawnAgent(
456
493
  try {
457
494
  mkdirSync(dirname(opts.eventsPath), { recursive: true });
458
495
  appendFileSync(opts.eventsPath, JSON.stringify(event) + "\n", "utf-8");
459
- } catch { /* best effort */ }
496
+ } catch {
497
+ /* best effort */
498
+ }
460
499
  }
461
500
  }
462
501
 
@@ -481,9 +520,15 @@ export function spawnAgent(
481
520
  if (!existsSync(inboxDir)) continue;
482
521
 
483
522
  let entries: string[];
484
- try { entries = readdirSync(inboxDir); } catch { continue; }
523
+ try {
524
+ entries = readdirSync(inboxDir);
525
+ } catch {
526
+ continue;
527
+ }
485
528
 
486
- const msgFiles = entries.filter(f => f.endsWith(".msg.json") && !f.endsWith(".msg.json.tmp")).sort();
529
+ const msgFiles = entries
530
+ .filter((f) => f.endsWith(".msg.json") && !f.endsWith(".msg.json.tmp"))
531
+ .sort();
487
532
  if (msgFiles.length === 0) continue;
488
533
 
489
534
  const ackDir = join(opts.mailboxDir, "ack");
@@ -509,12 +554,24 @@ export function spawnAgent(
509
554
  if (isBroadcast) {
510
555
  // Do NOT remove the shared broadcast inbox file. Persist a per-agent
511
556
  // ack marker so all agents can consume the same broadcast exactly once.
512
- try { writeFileSync(ackPath, raw, "utf-8"); } catch { /* best effort */ }
557
+ try {
558
+ writeFileSync(ackPath, raw, "utf-8");
559
+ } catch {
560
+ /* best effort */
561
+ }
513
562
  } else {
514
- try { renameSync(join(inboxDir, filename), ackPath); } catch { /* race ok */ }
563
+ try {
564
+ renameSync(join(inboxDir, filename), ackPath);
565
+ } catch {
566
+ /* race ok */
567
+ }
515
568
  }
516
569
 
517
- emitEvent("message_delivered", { messageId: msg.id, content: msg.content, broadcast: isBroadcast });
570
+ emitEvent("message_delivered", {
571
+ messageId: msg.id,
572
+ content: msg.content,
573
+ broadcast: isBroadcast,
574
+ });
518
575
  if (opts.stateRoot) {
519
576
  appendMailboxAuditEvent(opts.stateRoot, expectedBatchId, {
520
577
  type: "message_delivered",
@@ -530,11 +587,18 @@ export function spawnAgent(
530
587
  // TP-090: steering-pending flag
531
588
  if (opts.steeringPendingPath) {
532
589
  try {
533
- appendFileSync(opts.steeringPendingPath,
534
- JSON.stringify({ ts: msg.timestamp, content: msg.content, id: msg.id }) + "\n", "utf-8");
535
- } catch { /* best effort */ }
590
+ appendFileSync(
591
+ opts.steeringPendingPath,
592
+ JSON.stringify({ ts: msg.timestamp, content: msg.content, id: msg.id }) + "\n",
593
+ "utf-8",
594
+ );
595
+ } catch {
596
+ /* best effort */
597
+ }
536
598
  }
537
- } catch { /* skip malformed */ }
599
+ } catch {
600
+ /* skip malformed */
601
+ }
538
602
  }
539
603
  }
540
604
  }
@@ -576,9 +640,15 @@ export function spawnAgent(
576
640
  const summary = {
577
641
  exitCode: result.exitCode,
578
642
  exitSignal: result.signal,
579
- tokens: (inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens) > 0
580
- ? { input: inputTokens, output: outputTokens, cacheRead: cacheReadTokens, cacheWrite: cacheWriteTokens }
581
- : null,
643
+ tokens:
644
+ inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens > 0
645
+ ? {
646
+ input: inputTokens,
647
+ output: outputTokens,
648
+ cacheRead: cacheReadTokens,
649
+ cacheWrite: cacheWriteTokens,
650
+ }
651
+ : null,
582
652
  cost: costUsd > 0 ? costUsd : null,
583
653
  toolCalls,
584
654
  retries,
@@ -589,23 +659,29 @@ export function spawnAgent(
589
659
  contextUsage: contextUsage || null,
590
660
  };
591
661
  writeFileSync(opts.exitSummaryPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
592
- } catch { /* best effort */ }
662
+ } catch {
663
+ /* best effort */
664
+ }
593
665
  }
594
666
 
595
- const exitEventType: RuntimeAgentEventType =
596
- timedOut ? "agent_timeout" :
597
- killed ? "agent_killed" :
598
- (exitCode === 0 && agentEnded) ? "agent_exited" :
599
- "agent_crashed";
667
+ const exitEventType: RuntimeAgentEventType = timedOut
668
+ ? "agent_timeout"
669
+ : killed
670
+ ? "agent_killed"
671
+ : exitCode === 0 && agentEnded
672
+ ? "agent_exited"
673
+ : "agent_crashed";
600
674
  emitEvent(exitEventType, { exitCode, signal, durationMs: result.durationMs, timedOut });
601
675
 
602
676
  // Registry integration: update manifest to terminal status
603
677
  if (opts.stateRoot) {
604
- const terminalStatus =
605
- timedOut ? "timed_out" as const :
606
- killed ? "killed" as const :
607
- (exitCode === 0 && agentEnded) ? "exited" as const :
608
- "crashed" as const;
678
+ const terminalStatus = timedOut
679
+ ? ("timed_out" as const)
680
+ : killed
681
+ ? ("killed" as const)
682
+ : exitCode === 0 && agentEnded
683
+ ? ("exited" as const)
684
+ : ("crashed" as const);
609
685
  updateManifestStatus(opts.stateRoot, opts.batchId, opts.agentId, terminalStatus);
610
686
  refreshRegistrySnapshot(true);
611
687
  }
@@ -623,7 +699,11 @@ export function spawnAgent(
623
699
  if (!line.trim()) continue;
624
700
 
625
701
  let event: any;
626
- try { event = JSON.parse(line); } catch { continue; }
702
+ try {
703
+ event = JSON.parse(line);
704
+ } catch {
705
+ continue;
706
+ }
627
707
  if (!event || !event.type) continue;
628
708
 
629
709
  // Accumulate telemetry
@@ -636,7 +716,12 @@ export function spawnAgent(
636
716
  cacheReadTokens += usage.cacheRead || 0;
637
717
  cacheWriteTokens += usage.cacheWrite || 0;
638
718
  if (usage.cost) {
639
- costUsd += typeof usage.cost === "object" ? (usage.cost.total || 0) : (typeof usage.cost === "number" ? usage.cost : 0);
719
+ costUsd +=
720
+ typeof usage.cost === "object"
721
+ ? usage.cost.total || 0
722
+ : typeof usage.cost === "number"
723
+ ? usage.cost
724
+ : 0;
640
725
  }
641
726
  }
642
727
  // TP-111: Emit assistant_message with bounded content
@@ -652,8 +737,15 @@ export function spawnAgent(
652
737
  // then periodically at a bounded cadence to refresh context usage.
653
738
  if (event.message?.role === "assistant") {
654
739
  assistantMessageEnds += 1;
655
- if (assistantMessageEnds === 1 || assistantMessageEnds % STATS_REFRESH_EVERY_ASSISTANT_MESSAGES === 0) {
656
- try { proc.stdin?.write(JSON.stringify({ type: "get_session_stats" }) + "\n"); } catch { /* ignore */ }
740
+ if (
741
+ assistantMessageEnds === 1 ||
742
+ assistantMessageEnds % STATS_REFRESH_EVERY_ASSISTANT_MESSAGES === 0
743
+ ) {
744
+ try {
745
+ proc.stdin?.write(JSON.stringify({ type: "get_session_stats" }) + "\n");
746
+ } catch {
747
+ /* ignore */
748
+ }
657
749
  }
658
750
  }
659
751
  // Check mailbox
@@ -662,7 +754,16 @@ export function spawnAgent(
662
754
  refreshRegistrySnapshot(false);
663
755
  // Emit telemetry update
664
756
  if (onTelemetry) {
665
- onTelemetry({ inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, costUsd, toolCalls, lastTool, contextUsage });
757
+ onTelemetry({
758
+ inputTokens,
759
+ outputTokens,
760
+ cacheReadTokens,
761
+ cacheWriteTokens,
762
+ costUsd,
763
+ toolCalls,
764
+ lastTool,
765
+ contextUsage,
766
+ });
666
767
  }
667
768
  break;
668
769
  }
@@ -670,8 +771,12 @@ export function spawnAgent(
670
771
  toolCalls++;
671
772
  currentTurnHadToolCalls = true;
672
773
  const toolName = event.toolName || "tool";
673
- const argPreview = typeof event.args === "string" ? event.args.slice(0, 300) :
674
- (event.args && typeof Object.values(event.args)[0] === "string" ? String(Object.values(event.args)[0]).slice(0, 300) : "");
774
+ const argPreview =
775
+ typeof event.args === "string"
776
+ ? event.args.slice(0, 300)
777
+ : event.args && typeof Object.values(event.args)[0] === "string"
778
+ ? String(Object.values(event.args)[0]).slice(0, 300)
779
+ : "";
675
780
  lastTool = argPreview ? `${toolName}: ${argPreview}` : toolName;
676
781
  // TP-111: Bounded payload only — no raw args in durable event log
677
782
  const toolPath = event.args?.path ? String(event.args.path).slice(0, 200) : "";
@@ -680,14 +785,21 @@ export function spawnAgent(
680
785
  }
681
786
  case "tool_execution_end": {
682
787
  // TP-111: Include bounded result summary for dashboard display
683
- const toolResultSummary = typeof event.result === "string" ? event.result.slice(0, 200)
684
- : event.output ? String(event.output).slice(0, 200) : "";
788
+ const toolResultSummary =
789
+ typeof event.result === "string"
790
+ ? event.result.slice(0, 200)
791
+ : event.output
792
+ ? String(event.output).slice(0, 200)
793
+ : "";
685
794
  emitEvent("tool_result", { tool: event.toolName, summary: toolResultSummary });
686
795
  break;
687
796
  }
688
797
  case "auto_retry_start": {
689
798
  retries++;
690
- emitEvent("retry_started", { attempt: event.attempt, error: event.errorMessage || event.error });
799
+ emitEvent("retry_started", {
800
+ attempt: event.attempt,
801
+ error: event.errorMessage || event.error,
802
+ });
691
803
  break;
692
804
  }
693
805
  case "auto_compaction_start": {
@@ -704,7 +816,16 @@ export function spawnAgent(
704
816
  emitEvent("context_usage", { ...event.data.contextUsage });
705
817
  // Emit telemetry immediately so context % is live in dashboard
706
818
  if (onTelemetry) {
707
- onTelemetry({ inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, costUsd, toolCalls, lastTool, contextUsage });
819
+ onTelemetry({
820
+ inputTokens,
821
+ outputTokens,
822
+ cacheReadTokens,
823
+ cacheWriteTokens,
824
+ costUsd,
825
+ toolCalls,
826
+ lastTool,
827
+ contextUsage,
828
+ });
708
829
  }
709
830
  }
710
831
  break;
@@ -717,60 +838,62 @@ export function spawnAgent(
717
838
  // because workers commonly use tools (reads/greps) then exit
718
839
  // with a text declaration ("Now let me fix this:") without
719
840
  // actually making the edit.
720
- const shouldIntercept = opts.onPrematureExit
721
- && exitInterceptionCount < maxExitInterceptions;
841
+ const shouldIntercept = opts.onPrematureExit && exitInterceptionCount < maxExitInterceptions;
722
842
  if (shouldIntercept) {
723
843
  exitInterceptionCount++;
724
844
  const INTERCEPTION_TIMEOUT_MS = 120_000; // 2 minute safety timeout
725
845
  // Wrap in Promise.resolve().then() to catch synchronous throws
726
846
  const interceptPromise = Promise.resolve().then(() =>
727
- opts.onPrematureExit!(lastAssistantMessage));
847
+ opts.onPrematureExit!(lastAssistantMessage),
848
+ );
728
849
  const timeoutPromise = new Promise<null>((res) =>
729
- setTimeout(() => res(null), INTERCEPTION_TIMEOUT_MS));
730
- Promise.race([interceptPromise, timeoutPromise])
731
- .then(
732
- (newPrompt: string | null) => {
733
- if (newPrompt && !stdinClosed && proc.stdin && !proc.stdin.destroyed) {
734
- // Re-prompt the agent with supervisor guidance
735
- agentEnded = false; // Reset for the new turn
736
- currentTurnHadToolCalls = false; // Reset for new turn
737
- proc.stdin.write(JSON.stringify({ type: "prompt", message: newPrompt }) + "\n");
738
- emitEvent("exit_intercepted", {
739
- interceptionCount: exitInterceptionCount,
740
- assistantMessage: truncatePayload(lastAssistantMessage, 500),
741
- supervisorConsulted: true,
742
- action: "reprompt",
743
- newPromptPreview: truncatePayload(newPrompt, MAX_CONV_PAYLOAD_CHARS),
744
- });
745
- } else {
746
- // Callback returned null or stdin already closed — close session
747
- const reason = stdinClosed ? "stdin_closed"
748
- : newPrompt === null ? "callback_returned_null"
850
+ setTimeout(() => res(null), INTERCEPTION_TIMEOUT_MS),
851
+ );
852
+ Promise.race([interceptPromise, timeoutPromise]).then(
853
+ (newPrompt: string | null) => {
854
+ if (newPrompt && !stdinClosed && proc.stdin && !proc.stdin.destroyed) {
855
+ // Re-prompt the agent with supervisor guidance
856
+ agentEnded = false; // Reset for the new turn
857
+ currentTurnHadToolCalls = false; // Reset for new turn
858
+ proc.stdin.write(JSON.stringify({ type: "prompt", message: newPrompt }) + "\n");
859
+ emitEvent("exit_intercepted", {
860
+ interceptionCount: exitInterceptionCount,
861
+ assistantMessage: truncatePayload(lastAssistantMessage, 500),
862
+ supervisorConsulted: true,
863
+ action: "reprompt",
864
+ newPromptPreview: truncatePayload(newPrompt, MAX_CONV_PAYLOAD_CHARS),
865
+ });
866
+ } else {
867
+ // Callback returned null or stdin already closed — close session
868
+ const reason = stdinClosed
869
+ ? "stdin_closed"
870
+ : newPrompt === null
871
+ ? "callback_returned_null"
749
872
  : "unknown";
750
- emitEvent("exit_intercepted", {
751
- interceptionCount: exitInterceptionCount,
752
- assistantMessage: truncatePayload(lastAssistantMessage, 500),
753
- supervisorConsulted: true,
754
- action: "close",
755
- reason,
756
- });
757
- closeStdin();
758
- }
759
- },
760
- (err: unknown) => {
761
- // Callback rejected — emit single diagnostic event and close
762
- const msg = err instanceof Error ? err.message : String(err);
763
873
  emitEvent("exit_intercepted", {
764
874
  interceptionCount: exitInterceptionCount,
765
875
  assistantMessage: truncatePayload(lastAssistantMessage, 500),
766
- supervisorConsulted: false,
876
+ supervisorConsulted: true,
767
877
  action: "close",
768
- reason: "callback_error",
769
- error: msg,
878
+ reason,
770
879
  });
771
880
  closeStdin();
772
- },
773
- );
881
+ }
882
+ },
883
+ (err: unknown) => {
884
+ // Callback rejected — emit single diagnostic event and close
885
+ const msg = err instanceof Error ? err.message : String(err);
886
+ emitEvent("exit_intercepted", {
887
+ interceptionCount: exitInterceptionCount,
888
+ assistantMessage: truncatePayload(lastAssistantMessage, 500),
889
+ supervisorConsulted: false,
890
+ action: "close",
891
+ reason: "callback_error",
892
+ error: msg,
893
+ });
894
+ closeStdin();
895
+ },
896
+ );
774
897
  } else {
775
898
  // No callback, had tool calls, or interception limit reached — close normally
776
899
  if (opts.onPrematureExit && exitInterceptionCount >= maxExitInterceptions) {
@@ -820,7 +943,11 @@ export function spawnAgent(
820
943
 
821
944
  const kill = () => {
822
945
  killed = true;
823
- try { proc.kill("SIGTERM"); } catch { /* ignore */ }
946
+ try {
947
+ proc.kill("SIGTERM");
948
+ } catch {
949
+ /* ignore */
950
+ }
824
951
  };
825
952
 
826
953
  return { promise, kill };