pi-subagents 0.13.3 → 0.14.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.
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import type { Message } from "@mariozechner/pi-ai";
6
7
  import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
7
8
  import { getPiSpawnCommand } from "./pi-spawn.ts";
8
9
  import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
@@ -27,6 +28,7 @@ import {
27
28
  } from "./parallel-utils.ts";
28
29
  import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
29
30
  import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
31
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
30
32
  import {
31
33
  cleanupWorktrees,
32
34
  createWorktrees,
@@ -53,6 +55,7 @@ interface SubagentRunConfig {
53
55
  asyncDir: string;
54
56
  sessionId?: string | null;
55
57
  piPackageRoot?: string;
58
+ piArgv1?: string;
56
59
  worktreeSetupHook?: string;
57
60
  worktreeSetupHookTimeoutMs?: number;
58
61
  }
@@ -122,32 +125,44 @@ function emptyUsage(): Usage {
122
125
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
123
126
  }
124
127
 
125
- function parseRunOutput(output: string): { usage: Usage; model?: string; error?: string } {
126
- const usage = emptyUsage();
127
- let model: string | undefined;
128
- let error: string | undefined;
129
- for (const line of output.split("\n")) {
130
- if (!line.trim()) continue;
131
- try {
132
- const evt = JSON.parse(line) as { type?: string; message?: { role?: string; model?: string; errorMessage?: string; usage?: any } };
133
- if (evt.type !== "message_end" || evt.message?.role !== "assistant") continue;
134
- const msg = evt.message;
135
- if (msg.model) model = msg.model;
136
- if (msg.errorMessage) error = msg.errorMessage;
137
- const u = msg.usage;
138
- if (u) {
139
- usage.turns++;
140
- usage.input += u.input ?? u.inputTokens ?? 0;
141
- usage.output += u.output ?? u.outputTokens ?? 0;
142
- usage.cacheRead += u.cacheRead ?? 0;
143
- usage.cacheWrite += u.cacheWrite ?? 0;
144
- usage.cost += u.cost?.total ?? 0;
145
- }
146
- } catch {
147
- // Ignore malformed stdout lines.
148
- }
149
- }
150
- return { usage, model, error };
128
+ interface ChildEventContext {
129
+ eventsPath: string;
130
+ runId: string;
131
+ stepIndex: number;
132
+ agent: string;
133
+ }
134
+
135
+ interface ChildUsage {
136
+ input?: number;
137
+ inputTokens?: number;
138
+ output?: number;
139
+ outputTokens?: number;
140
+ cacheRead?: number;
141
+ cacheWrite?: number;
142
+ cost?: { total?: number };
143
+ }
144
+
145
+ type ChildMessage = Message & {
146
+ model?: string;
147
+ errorMessage?: string;
148
+ usage?: ChildUsage;
149
+ };
150
+
151
+ interface ChildEvent {
152
+ type?: string;
153
+ message?: ChildMessage;
154
+ toolName?: string;
155
+ args?: Record<string, unknown>;
156
+ }
157
+
158
+ interface RunPiStreamingResult {
159
+ stderr: string;
160
+ exitCode: number | null;
161
+ messages: Message[];
162
+ usage: Usage;
163
+ model?: string;
164
+ error?: string;
165
+ finalOutput: string;
151
166
  }
152
167
 
153
168
  function runPiStreaming(
@@ -156,36 +171,131 @@ function runPiStreaming(
156
171
  outputFile: string,
157
172
  env?: Record<string, string | undefined>,
158
173
  piPackageRoot?: string,
174
+ piArgv1?: string,
159
175
  maxSubagentDepth?: number,
160
- ): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
176
+ childEventContext?: ChildEventContext,
177
+ ): Promise<RunPiStreamingResult> {
161
178
  return new Promise((resolve) => {
162
179
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
163
180
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
164
- const spawnSpec = getPiSpawnCommand(args, piPackageRoot ? { piPackageRoot } : undefined);
181
+ const spawnSpec = getPiSpawnCommand(args, {
182
+ ...(piPackageRoot ? { piPackageRoot } : {}),
183
+ ...(piArgv1 ? { argv1: piArgv1 } : {}),
184
+ });
165
185
  const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
166
- let stdout = "";
167
186
  let stderr = "";
187
+ let stdoutBuf = "";
188
+ let stderrBuf = "";
189
+ const messages: Message[] = [];
190
+ const usage = emptyUsage();
191
+ let model: string | undefined;
192
+ let error: string | undefined;
193
+ const rawStdoutLines: string[] = [];
194
+
195
+ const writeOutputLine = (line: string) => {
196
+ if (!line.trim()) return;
197
+ outputStream.write(`${line}\n`);
198
+ };
199
+
200
+ const writeOutputText = (text: string) => {
201
+ for (const line of text.split("\n")) {
202
+ writeOutputLine(line);
203
+ }
204
+ };
205
+
206
+ const appendChildEvent = (event: Record<string, unknown>) => {
207
+ if (!childEventContext) return;
208
+ appendJsonl(childEventContext.eventsPath, JSON.stringify({
209
+ ...event,
210
+ subagentSource: "child",
211
+ subagentRunId: childEventContext.runId,
212
+ subagentStepIndex: childEventContext.stepIndex,
213
+ subagentAgent: childEventContext.agent,
214
+ observedAt: Date.now(),
215
+ }));
216
+ };
217
+
218
+ const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
219
+ appendChildEvent({ type, line });
220
+ };
221
+
222
+ const processStdoutLine = (line: string) => {
223
+ if (!line.trim()) return;
224
+ let event: ChildEvent;
225
+ try {
226
+ event = JSON.parse(line) as ChildEvent;
227
+ } catch {
228
+ rawStdoutLines.push(line);
229
+ writeOutputLine(line);
230
+ appendChildLine("subagent.child.stdout", line);
231
+ return;
232
+ }
233
+
234
+ appendChildEvent(event);
235
+
236
+ if (event.type === "tool_execution_start" && event.toolName) {
237
+ const toolArgs = extractToolArgsPreview(event.args ?? {});
238
+ writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
239
+ return;
240
+ }
241
+
242
+ if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
243
+ messages.push(event.message);
244
+ const text = extractTextFromContent(event.message.content);
245
+ if (text) writeOutputText(text);
246
+
247
+ if (event.type !== "message_end" || event.message.role !== "assistant") return;
248
+ if (event.message.model) model = event.message.model;
249
+ if (event.message.errorMessage) error = event.message.errorMessage;
250
+ const eventUsage = event.message.usage;
251
+ if (!eventUsage) return;
252
+ usage.turns++;
253
+ usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
254
+ usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
255
+ usage.cacheRead += eventUsage.cacheRead ?? 0;
256
+ usage.cacheWrite += eventUsage.cacheWrite ?? 0;
257
+ usage.cost += eventUsage.cost?.total ?? 0;
258
+ }
259
+ };
260
+
261
+ const processStderrText = (text: string) => {
262
+ stderr += text;
263
+ stderrBuf += text;
264
+ outputStream.write(text);
265
+ if (!childEventContext) return;
266
+ const lines = stderrBuf.split("\n");
267
+ stderrBuf = lines.pop() || "";
268
+ for (const line of lines) {
269
+ if (!line.trim()) continue;
270
+ appendChildLine("subagent.child.stderr", line);
271
+ }
272
+ };
168
273
 
169
274
  child.stdout.on("data", (chunk: Buffer) => {
170
275
  const text = chunk.toString();
171
- stdout += text;
172
- outputStream.write(text);
276
+ stdoutBuf += text;
277
+ const lines = stdoutBuf.split("\n");
278
+ stdoutBuf = lines.pop() || "";
279
+ for (const line of lines) processStdoutLine(line);
173
280
  });
174
281
 
175
282
  child.stderr.on("data", (chunk: Buffer) => {
176
- const text = chunk.toString();
177
- stderr += text;
178
- outputStream.write(text);
283
+ processStderrText(chunk.toString());
179
284
  });
180
285
 
181
286
  child.on("close", (exitCode) => {
287
+ if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
288
+ if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
182
289
  outputStream.end();
183
- resolve({ stdout, stderr, exitCode });
290
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
291
+ resolve({ stderr, exitCode, messages, usage, model, error, finalOutput });
184
292
  });
185
293
 
186
- child.on("error", () => {
294
+ child.on("error", (spawnError) => {
187
295
  outputStream.end();
188
- resolve({ stdout, stderr, exitCode: 1 });
296
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
297
+ const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
298
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput });
189
299
  });
190
300
  });
191
301
  }
@@ -340,6 +450,7 @@ interface SingleStepContext {
340
450
  flatStepCount: number;
341
451
  outputFile: string;
342
452
  piPackageRoot?: string;
453
+ piArgv1?: string;
343
454
  }
344
455
 
345
456
  /** Run a single pi agent step, returning output and metadata */
@@ -380,14 +491,13 @@ async function runSingleStep(
380
491
  const attemptedModels: string[] = [];
381
492
  const modelAttempts: ModelAttempt[] = [];
382
493
  const attemptNotes: string[] = [];
383
- let finalResult:
384
- | { stdout: string; stderr: string; exitCode: number | null; usage: Usage; model?: string; error?: string }
385
- | undefined;
494
+ const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
495
+ let finalResult: RunPiStreamingResult | undefined;
386
496
 
387
497
  for (let index = 0; index < candidates.length; index++) {
388
498
  const candidate = candidates[index];
389
499
  const { args, env, tempDir } = buildPiArgs({
390
- baseArgs: ["-p"],
500
+ baseArgs: ["--mode", "json", "-p"],
391
501
  task,
392
502
  sessionEnabled,
393
503
  sessionDir,
@@ -400,28 +510,41 @@ async function runSingleStep(
400
510
  mcpDirectTools: step.mcpDirectTools,
401
511
  promptFileStem: step.agent,
402
512
  });
403
- const outputFile = index === 0 ? ctx.outputFile : `${ctx.outputFile}.attempt-${index + 1}`;
404
- const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, step.maxSubagentDepth);
513
+ const run = await runPiStreaming(
514
+ args,
515
+ step.cwd ?? ctx.cwd,
516
+ ctx.outputFile,
517
+ env,
518
+ ctx.piPackageRoot,
519
+ ctx.piArgv1,
520
+ step.maxSubagentDepth,
521
+ { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
522
+ );
405
523
  cleanupTempDir(tempDir);
406
524
 
407
- const parsed = parseRunOutput(run.stdout);
408
- const error = parsed.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
525
+ const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
526
+ const effectiveExitCode = hiddenError?.hasError ? (hiddenError.exitCode ?? 1) : run.exitCode;
527
+ const error = hiddenError?.hasError
528
+ ? hiddenError.details
529
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
530
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
531
+ : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
409
532
  const attempt: ModelAttempt = {
410
- model: candidate ?? parsed.model ?? step.model ?? "default",
411
- success: run.exitCode === 0 && !error,
412
- exitCode: run.exitCode,
533
+ model: candidate ?? run.model ?? step.model ?? "default",
534
+ success: effectiveExitCode === 0 && !error,
535
+ exitCode: effectiveExitCode,
413
536
  error,
414
- usage: parsed.usage,
537
+ usage: run.usage,
415
538
  };
416
539
  modelAttempts.push(attempt);
417
540
  if (candidate) attemptedModels.push(candidate);
418
- finalResult = { ...run, usage: parsed.usage, model: candidate ?? parsed.model, error };
541
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
419
542
  if (attempt.success) break;
420
543
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
421
544
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
422
545
  }
423
546
 
424
- const rawOutput = (finalResult?.stdout || "").trim();
547
+ const rawOutput = finalResult?.finalOutput ?? "";
425
548
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
426
549
  ? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
427
550
  : { fullOutput: rawOutput };
@@ -759,6 +882,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
759
882
  flatIndex: fi, flatStepCount: flatSteps.length,
760
883
  outputFile: path.join(asyncDir, `output-${fi}.log`),
761
884
  piPackageRoot: config.piPackageRoot,
885
+ piArgv1: config.piArgv1,
762
886
  });
763
887
  if (task.sessionFile) {
764
888
  latestSessionFile = task.sessionFile;
@@ -879,6 +1003,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
879
1003
  flatIndex, flatStepCount: flatSteps.length,
880
1004
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
881
1005
  piPackageRoot: config.piPackageRoot,
1006
+ piArgv1: config.piArgv1,
882
1007
  });
883
1008
  if (seqStep.sessionFile) {
884
1009
  latestSessionFile = seqStep.sessionFile;
package/types.ts CHANGED
@@ -40,17 +40,6 @@ export interface TokenUsage {
40
40
  total: number;
41
41
  }
42
42
 
43
- // ============================================================================
44
- // Skills
45
- // ============================================================================
46
-
47
- export interface ResolvedSkill {
48
- name: string;
49
- path: string;
50
- content: string;
51
- source: "project" | "user";
52
- }
53
-
54
43
  // ============================================================================
55
44
  // Progress Tracking
56
45
  // ============================================================================
@@ -271,6 +260,8 @@ export interface RunSyncOptions {
271
260
  modelOverride?: string;
272
261
  /** Registry models available for heuristic bare-model resolution */
273
262
  availableModels?: Array<{ provider: string; id: string; fullId: string }>;
263
+ /** Current parent-session provider to prefer for ambiguous bare model ids */
264
+ preferredModelProvider?: string;
274
265
  /** Skills to inject (overrides agent default if provided) */
275
266
  skills?: string[];
276
267
  }
@@ -309,10 +300,66 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
309
300
  cleanupDays: 7,
310
301
  };
311
302
 
303
+ function sanitizeTempScopeSegment(value: string): string {
304
+ const sanitized = value
305
+ .trim()
306
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
307
+ .replace(/^-+|-+$/g, "");
308
+ return sanitized || "unknown";
309
+ }
310
+
311
+ export function resolveTempScopeId(options?: {
312
+ env?: NodeJS.ProcessEnv;
313
+ getuid?: (() => number) | undefined;
314
+ userInfo?: (() => { username?: string | null }) | undefined;
315
+ homedir?: (() => string) | undefined;
316
+ }): string {
317
+ const env = options?.env ?? process.env;
318
+ const getuid = options && Object.hasOwn(options, "getuid")
319
+ ? options.getuid
320
+ : process.getuid?.bind(process);
321
+ if (typeof getuid === "function") {
322
+ return `uid-${getuid()}`;
323
+ }
324
+
325
+ for (const key of ["USERNAME", "USER", "LOGNAME"] as const) {
326
+ const value = env[key];
327
+ if (value) return `user-${sanitizeTempScopeSegment(value)}`;
328
+ }
329
+
330
+ const userInfo = options && Object.hasOwn(options, "userInfo")
331
+ ? options.userInfo
332
+ : os.userInfo;
333
+ try {
334
+ const username = userInfo?.().username;
335
+ if (username) return `user-${sanitizeTempScopeSegment(username)}`;
336
+ } catch {
337
+ // Fall through to home-directory-based scoping.
338
+ }
339
+
340
+ const homedir = env.USERPROFILE ?? env.HOME;
341
+ if (homedir) return `home-${sanitizeTempScopeSegment(homedir)}`;
342
+
343
+ const resolveHomedir = options && Object.hasOwn(options, "homedir")
344
+ ? options.homedir
345
+ : os.homedir;
346
+ try {
347
+ const fallbackHomedir = resolveHomedir?.();
348
+ if (fallbackHomedir) return `home-${sanitizeTempScopeSegment(fallbackHomedir)}`;
349
+ } catch {
350
+ // Fall through to the last-resort shared scope.
351
+ }
352
+
353
+ return "shared";
354
+ }
355
+
312
356
  export const MAX_PARALLEL = 8;
313
357
  export const MAX_CONCURRENCY = 4;
314
- export const RESULTS_DIR = path.join(os.tmpdir(), "pi-async-subagent-results");
315
- export const ASYNC_DIR = path.join(os.tmpdir(), "pi-async-subagent-runs");
358
+ export const TEMP_ROOT_DIR = path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
359
+ export const RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
360
+ export const ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
361
+ export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
362
+ export const TEMP_ARTIFACTS_DIR = path.join(TEMP_ROOT_DIR, "artifacts");
316
363
  export const WIDGET_KEY = "subagent-async";
317
364
  export const SLASH_RESULT_TYPE = "subagent-slash-result";
318
365
  export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
@@ -329,6 +376,10 @@ export const DEFAULT_FORK_PREAMBLE =
329
376
  "Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
330
377
  "— focus exclusively on completing this task using your tools.";
331
378
 
379
+ export function getAsyncConfigPath(suffix: string): string {
380
+ return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
381
+ }
382
+
332
383
  export function wrapForkTask(task: string, preamble?: string | false): string {
333
384
  if (preamble === false) return task;
334
385
  const effectivePreamble = preamble ?? DEFAULT_FORK_PREAMBLE;
package/utils.ts CHANGED
@@ -224,15 +224,12 @@ export function getDisplayItems(messages: Message[]): DisplayItem[] {
224
224
  * Detect errors in subagent execution from messages (only errors with no subsequent success)
225
225
  */
226
226
  export function detectSubagentError(messages: Message[]): ErrorInfo {
227
- // Step 1: Find the last assistant message with text content.
228
- // If the agent produced a text response after encountering errors,
229
- // it had a chance to recover — only errors AFTER this point matter.
230
227
  let lastAssistantTextIndex = -1;
231
228
  for (let i = messages.length - 1; i >= 0; i--) {
232
229
  const msg = messages[i];
233
230
  if (msg.role === "assistant") {
234
231
  const hasText = Array.isArray(msg.content) && msg.content.some(
235
- (c) => c.type === "text" && "text" in c && (c.text as string).trim().length > 0,
232
+ (c) => c.type === "text" && "text" in c && typeof c.text === "string" && c.text.trim().length > 0,
236
233
  );
237
234
  if (hasText) {
238
235
  lastAssistantTextIndex = i;
@@ -241,28 +238,26 @@ export function detectSubagentError(messages: Message[]): ErrorInfo {
241
238
  }
242
239
  }
243
240
 
244
- // Step 2: Only scan tool results AFTER the last assistant text message.
245
- // Errors before the agent's final response are implicitly recovered.
246
241
  const scanStart = lastAssistantTextIndex >= 0 ? lastAssistantTextIndex + 1 : 0;
247
242
 
248
- // Step 3: Check tool results in the post-response window
249
243
  for (let i = messages.length - 1; i >= scanStart; i--) {
250
244
  const msg = messages[i];
251
245
  if (msg.role !== "toolResult") continue;
246
+ const toolName = "toolName" in msg && typeof msg.toolName === "string" ? msg.toolName : undefined;
247
+ const isError = "isError" in msg && msg.isError === true;
252
248
 
253
- if ((msg as any).isError) {
249
+ if (isError) {
254
250
  const text = msg.content.find((c) => c.type === "text");
255
251
  const details = text && "text" in text ? text.text : undefined;
256
252
  const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
257
253
  return {
258
254
  hasError: true,
259
255
  exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
260
- errorType: (msg as any).toolName || "tool",
256
+ errorType: toolName || "tool",
261
257
  details: details?.slice(0, 200),
262
258
  };
263
259
  }
264
260
 
265
- const toolName = (msg as any).toolName;
266
261
  if (toolName !== "bash") continue;
267
262
 
268
263
  const text = msg.content.find((c) => c.type === "text");
package/worktree.ts CHANGED
@@ -106,9 +106,11 @@ function resolveRepoState(cwd: string): RepoState {
106
106
  }
107
107
 
108
108
  const toplevel = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
109
- const realCwd = fs.realpathSync(cwd);
110
- const realToplevel = fs.realpathSync(toplevel);
111
- const cwdRelative = path.relative(realToplevel, realCwd);
109
+ const rawPrefix = runGitChecked(cwd, ["rev-parse", "--show-prefix"]).trim();
110
+ const normalizedPrefix = rawPrefix
111
+ ? path.normalize(rawPrefix.replace(/[\\/]+$/, ""))
112
+ : "";
113
+ const cwdRelative = normalizedPrefix === "." ? "" : normalizedPrefix;
112
114
 
113
115
  const status = runGitChecked(toplevel, ["status", "--porcelain"]);
114
116
  if (status.trim().length > 0) {
@@ -124,6 +126,7 @@ function normalizeComparableCwd(cwd: string): string {
124
126
  try {
125
127
  return fs.realpathSync(resolved);
126
128
  } catch {
129
+ // Use the unresolved absolute path when realpath resolution is unavailable.
127
130
  return resolved;
128
131
  }
129
132
  }
@@ -169,6 +172,7 @@ function linkNodeModulesIfPresent(toplevel: string, worktreePath: string): boole
169
172
  fs.symlinkSync(nodeModulesPath, nodeModulesLinkPath);
170
173
  return true;
171
174
  } catch {
175
+ // Symlink creation is optional (e.g., unsupported filesystems on CI runners).
172
176
  return false;
173
177
  }
174
178
  }
@@ -344,8 +348,12 @@ function createSingleWorktree(
344
348
  syntheticPaths,
345
349
  };
346
350
  } catch (error) {
347
- try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {}
348
- try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch {}
351
+ try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
352
+ // Best-effort rollback; preserve the original setup failure.
353
+ }
354
+ try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch {
355
+ // Best-effort rollback; preserve the original setup failure.
356
+ }
349
357
  throw error;
350
358
  }
351
359
  }
@@ -453,12 +461,18 @@ function captureWorktreeDiff(
453
461
  function writeEmptyPatch(patchPath: string): void {
454
462
  try {
455
463
  fs.writeFileSync(patchPath, "", "utf-8");
456
- } catch {}
464
+ } catch {
465
+ // Diff artifact writing is best-effort in error paths.
466
+ }
457
467
  }
458
468
 
459
469
  function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): void {
460
- try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {}
461
- try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch {}
470
+ try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
471
+ // Cleanup is best-effort to avoid masking caller errors.
472
+ }
473
+ try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch {
474
+ // Cleanup is best-effort to avoid masking caller errors.
475
+ }
462
476
  }
463
477
 
464
478
  function hasWorktreeChanges(diff: WorktreeDiff): boolean {
@@ -502,6 +516,7 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
502
516
  try {
503
517
  fs.mkdirSync(diffsDir, { recursive: true });
504
518
  } catch {
519
+ // Returning no diffs is safer than failing the whole command on artifact-dir issues.
505
520
  return [];
506
521
  }
507
522
 
@@ -513,6 +528,7 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
513
528
  try {
514
529
  diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
515
530
  } catch {
531
+ // Preserve execution flow; failed diff capture maps to an empty per-task patch.
516
532
  writeEmptyPatch(patchPath);
517
533
  diffs.push(emptyDiff(index, agent, worktree.branch, patchPath));
518
534
  }
@@ -525,7 +541,9 @@ export function cleanupWorktrees(setup: WorktreeSetup): void {
525
541
  for (let index = setup.worktrees.length - 1; index >= 0; index--) {
526
542
  cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
527
543
  }
528
- try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {}
544
+ try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
545
+ // Pruning is best-effort cleanup.
546
+ }
529
547
  }
530
548
 
531
549
  export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {