@oh-my-pi/pi-coding-agent 16.1.14 → 16.1.16

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 (111) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/cli.js +3705 -4015
  3. package/dist/types/advisor/runtime.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +2 -5
  5. package/dist/types/cli/flag-tables.d.ts +2 -2
  6. package/dist/types/cli/session-picker.d.ts +4 -2
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/settings-schema.d.ts +32 -1
  9. package/dist/types/eval/agent-bridge.d.ts +19 -0
  10. package/dist/types/eval/js/shared/helpers.d.ts +1 -13
  11. package/dist/types/eval/js/shared/types.d.ts +1 -1
  12. package/dist/types/eval/js/worker-protocol.d.ts +1 -1
  13. package/dist/types/eval/py/executor.d.ts +1 -1
  14. package/dist/types/export/share.d.ts +8 -1
  15. package/dist/types/internal-urls/local-protocol.d.ts +18 -1
  16. package/dist/types/main.d.ts +2 -0
  17. package/dist/types/mcp/transports/stdio.d.ts +12 -1
  18. package/dist/types/modes/components/plugin-settings.d.ts +5 -0
  19. package/dist/types/modes/components/session-selector.d.ts +25 -0
  20. package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
  21. package/dist/types/modes/components/status-line/types.d.ts +1 -0
  22. package/dist/types/secrets/obfuscator.d.ts +3 -3
  23. package/dist/types/task/isolation-runner.d.ts +128 -0
  24. package/dist/types/task/worktree.d.ts +14 -1
  25. package/dist/types/thinking.d.ts +15 -0
  26. package/dist/types/tools/eval-render.d.ts +3 -0
  27. package/dist/types/tools/eval.d.ts +11 -17
  28. package/dist/types/tools/todo.d.ts +26 -28
  29. package/dist/types/tui/output-block.d.ts +8 -0
  30. package/dist/types/utils/image-resize.d.ts +2 -0
  31. package/dist/types/utils/shell-snapshot.d.ts +10 -0
  32. package/dist/types/web/search/providers/exa.d.ts +2 -0
  33. package/dist/types/web/search/providers/perplexity.d.ts +17 -3
  34. package/package.json +12 -12
  35. package/scripts/build-binary.ts +18 -4
  36. package/src/advisor/__tests__/advisor.test.ts +114 -0
  37. package/src/advisor/runtime.ts +129 -1
  38. package/src/cli/args.ts +4 -5
  39. package/src/cli/flag-tables.ts +3 -3
  40. package/src/cli/gallery-fixtures/interaction.ts +6 -9
  41. package/src/cli/gallery-fixtures/shell.ts +15 -23
  42. package/src/cli/session-picker.ts +17 -3
  43. package/src/commands/launch.ts +3 -3
  44. package/src/config/model-registry.ts +12 -4
  45. package/src/config/settings-schema.ts +37 -1
  46. package/src/edit/renderer.ts +34 -12
  47. package/src/eval/__tests__/agent-bridge.test.ts +462 -3
  48. package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
  49. package/src/eval/__tests__/julia-prelude.test.ts +1 -30
  50. package/src/eval/__tests__/prelude-agent.test.ts +42 -8
  51. package/src/eval/agent-bridge.ts +301 -71
  52. package/src/eval/jl/prelude.jl +32 -227
  53. package/src/eval/jl/runner.jl +38 -12
  54. package/src/eval/js/shared/helpers.ts +1 -114
  55. package/src/eval/js/shared/prelude.txt +13 -27
  56. package/src/eval/js/shared/runtime.ts +0 -6
  57. package/src/eval/js/shared/types.ts +1 -1
  58. package/src/eval/js/worker-protocol.ts +1 -1
  59. package/src/eval/py/__tests__/prelude.test.ts +13 -0
  60. package/src/eval/py/executor.ts +1 -1
  61. package/src/eval/py/prelude.py +47 -105
  62. package/src/eval/py/runner.py +0 -6
  63. package/src/eval/rb/prelude.rb +21 -189
  64. package/src/eval/rb/runner.rb +116 -9
  65. package/src/exec/bash-executor.ts +44 -0
  66. package/src/export/html/tool-views.generated.js +29 -29
  67. package/src/export/share.ts +51 -28
  68. package/src/internal-urls/docs-index.generated.txt +1 -1
  69. package/src/internal-urls/local-protocol.ts +100 -53
  70. package/src/main.ts +15 -4
  71. package/src/mcp/transports/stdio.ts +20 -4
  72. package/src/modes/acp/acp-event-mapper.ts +7 -2
  73. package/src/modes/components/custom-editor.test.ts +22 -0
  74. package/src/modes/components/custom-editor.ts +10 -1
  75. package/src/modes/components/footer.ts +4 -3
  76. package/src/modes/components/plugin-settings.ts +7 -1
  77. package/src/modes/components/session-selector.ts +143 -29
  78. package/src/modes/components/status-line/component.ts +5 -1
  79. package/src/modes/components/status-line/context-thresholds.ts +11 -3
  80. package/src/modes/components/status-line/segments.ts +1 -1
  81. package/src/modes/components/status-line/types.ts +1 -0
  82. package/src/modes/controllers/command-controller.ts +6 -0
  83. package/src/modes/rpc/rpc-mode.ts +6 -0
  84. package/src/modes/utils/copy-targets.ts +7 -2
  85. package/src/prompts/system/system-prompt.md +6 -6
  86. package/src/prompts/system/workflow-notice.md +3 -3
  87. package/src/prompts/tools/bash.md +18 -2
  88. package/src/prompts/tools/eval.md +19 -19
  89. package/src/prompts/tools/search.md +1 -0
  90. package/src/prompts/tools/todo.md +1 -1
  91. package/src/secrets/obfuscator.ts +3 -9
  92. package/src/session/agent-session.ts +264 -52
  93. package/src/slash-commands/builtin-registry.ts +2 -1
  94. package/src/task/index.ts +61 -207
  95. package/src/task/isolation-runner.ts +354 -0
  96. package/src/task/worktree.ts +46 -9
  97. package/src/thinking.ts +20 -0
  98. package/src/tools/ask.ts +44 -38
  99. package/src/tools/bash.ts +9 -2
  100. package/src/tools/browser/tab-worker.ts +1 -1
  101. package/src/tools/eval-render.ts +34 -27
  102. package/src/tools/eval.ts +100 -103
  103. package/src/tools/index.ts +8 -1
  104. package/src/tools/read.ts +136 -60
  105. package/src/tools/todo.ts +60 -64
  106. package/src/tui/code-cell.ts +1 -1
  107. package/src/tui/output-block.ts +11 -0
  108. package/src/utils/image-resize.ts +30 -0
  109. package/src/utils/shell-snapshot.ts +63 -1
  110. package/src/web/search/providers/exa.ts +85 -1
  111. package/src/web/search/providers/perplexity.ts +18 -6
@@ -12,9 +12,19 @@ import { MCPManager } from "../mcp/manager";
12
12
  import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
13
13
  import { MAIN_AGENT_ID } from "../registry/agent-registry";
14
14
  import * as taskDiscovery from "../task/discovery";
15
+ import type { ExecutorOptions } from "../task/executor";
15
16
  import * as taskExecutor from "../task/executor";
17
+ import {
18
+ applyEligibleNestedPatches,
19
+ type IsolationContext,
20
+ makeIsolationCommitMessage,
21
+ mergeIsolatedChanges,
22
+ prepareIsolationContext,
23
+ runIsolatedSubprocess,
24
+ } from "../task/isolation-runner";
16
25
  import { AgentOutputManager } from "../task/output-manager";
17
26
  import type { AgentDefinition, AgentProgress, SingleResult } from "../task/types";
27
+ import { type NestedRepoPatch, parseIsolationMode } from "../task/worktree";
18
28
  import type { ToolSession } from "../tools";
19
29
  import { ToolError } from "../tools/tool-errors";
20
30
  import { withBridgeTimeoutPause } from "./bridge-timeout";
@@ -33,18 +43,47 @@ const DEFAULT_AGENT_LABEL = "EvalAgent";
33
43
 
34
44
  const agentArgsSchema = type({
35
45
  prompt: "string>0",
36
- "agentType?": "string>0",
46
+ "agent?": "string>0",
37
47
  "model?": "string>0|string>0[]",
38
48
  "label?": "string",
39
49
  "schema?": "unknown",
50
+ "isolated?": "boolean",
51
+ "apply?": "boolean",
52
+ "merge?": "boolean",
53
+ "handle?": "boolean",
40
54
  });
41
55
 
42
56
  interface EvalAgentArgs {
43
57
  prompt: string;
44
- agentType?: string;
58
+ agent?: string;
45
59
  model?: string | string[];
46
60
  label?: string;
47
61
  schema?: unknown;
62
+ /**
63
+ * Run this subagent inside an isolation worktree (copy-on-write of the
64
+ * parent repo). Strict opt-in: defaults to `false` regardless of the
65
+ * session's `task.isolation.mode`, mirroring the `task` tool. Passing
66
+ * `true` while `task.isolation.mode === "none"` errors out instead of
67
+ * silently downgrading.
68
+ */
69
+ isolated?: boolean;
70
+ /**
71
+ * When isolated, apply the captured patch / merge the captured branch back
72
+ * to the parent repo (default `true`). Pass `false` to keep changes in the
73
+ * isolation worktree only — the patch artifact path / branch name lands in
74
+ * the result so the caller can inspect or apply manually.
75
+ */
76
+ apply?: boolean;
77
+ /**
78
+ * When isolated, allow branch-merge mode (cherry-pick onto HEAD). Defaults
79
+ * to `true`, in which case the active `task.isolation.merge` setting picks
80
+ * patch vs branch. Pass `false` to force patch mode even when the setting
81
+ * is `"branch"` — useful when a fan-out cannot tolerate the per-call git
82
+ * lock + repo mutation that branch mode performs.
83
+ */
84
+ merge?: boolean;
85
+ /** True when a runtime helper will return an `agent://` handle backed by the output artifacts. */
86
+ handle?: boolean;
48
87
  }
49
88
 
50
89
  export interface EvalAgentBridgeOptions {
@@ -60,6 +99,24 @@ export interface EvalAgentResult {
60
99
  id: string;
61
100
  model?: string | string[];
62
101
  structured: boolean;
102
+ /** True iff this run executed inside an isolation worktree. */
103
+ isolated?: boolean;
104
+ /** Captured patch artifact (patch mode) — surfaced regardless of `apply`. */
105
+ patchPath?: string;
106
+ /** Captured branch (branch mode) — surfaced regardless of `apply`. */
107
+ branchName?: string;
108
+ /** Captured nested repository patches — surfaced for isolated `apply=false` manual application. */
109
+ nestedPatches?: NestedRepoPatch[];
110
+ /**
111
+ * Tri-state apply outcome for isolated runs:
112
+ * - `true` — apply ran (or had nothing to do) and left the repo clean.
113
+ * - `false` — apply attempted and failed; artifacts preserved.
114
+ * - `null` — caller opted out via `apply=false`.
115
+ * Omitted for non-isolated runs.
116
+ */
117
+ changesApplied?: boolean | null;
118
+ /** Human-readable isolation apply/merge summary; kept out of schema-backed `text`. */
119
+ isolationSummary?: string;
63
120
  };
64
121
  }
65
122
 
@@ -129,15 +186,47 @@ function getOutputManager(session: ToolSession): AgentOutputManager {
129
186
  return manager;
130
187
  }
131
188
 
132
- async function getArtifacts(session: ToolSession): Promise<{
189
+ interface ArtifactPaths {
133
190
  sessionFile: string | null;
134
191
  artifactsDir: string;
135
- }> {
192
+ /**
193
+ * True when `artifactsDir` was created off the session path (no session
194
+ * file). Caller is then free to `rm -rf` it once all isolated patch
195
+ * artifacts have been consumed or applied.
196
+ */
197
+ tempArtifactsDir: boolean;
198
+ }
199
+
200
+ async function getArtifacts(session: ToolSession): Promise<ArtifactPaths> {
136
201
  const sessionFile = session.getSessionFile();
137
202
  const sessionArtifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
203
+ const tempArtifactsDir = sessionArtifactsDir === null;
138
204
  const artifactsDir = sessionArtifactsDir ?? path.join(os.tmpdir(), `omp-eval-agent-${Snowflake.next()}`);
139
205
  await fs.mkdir(artifactsDir, { recursive: true });
140
- return { sessionFile, artifactsDir };
206
+ return { sessionFile, artifactsDir, tempArtifactsDir };
207
+ }
208
+
209
+ /**
210
+ * Persist nested-repo patches to the per-call artifacts dir so an isolated
211
+ * apply failure can surface their paths in the thrown ToolError. The
212
+ * isolation worktree is already gone by the time we run, so without this the
213
+ * captured nested patches would be unrecoverable.
214
+ */
215
+ async function persistNestedPatches(
216
+ artifactsDir: string,
217
+ agentId: string,
218
+ nestedPatches: NestedRepoPatch[],
219
+ ): Promise<string[]> {
220
+ const written: string[] = [];
221
+ for (let index = 0; index < nestedPatches.length; index++) {
222
+ const patch = nestedPatches[index];
223
+ if (!patch) continue;
224
+ const slug = patch.relativePath.replace(/[^A-Za-z0-9._-]+/g, "_") || `nested-${index}`;
225
+ const out = path.join(artifactsDir, `${agentId}.nested-${index}-${slug}.patch`);
226
+ await Bun.write(out, patch.patch);
227
+ written.push(out);
228
+ }
229
+ return written;
141
230
  }
142
231
 
143
232
  function emitProgressStatus(emitStatus: ((event: JsStatusEvent) => void) | undefined, progress: AgentProgress): void {
@@ -187,7 +276,7 @@ function buildSubagentFailureMessage(agentName: string, result: SingleResult): s
187
276
  */
188
277
  export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOptions): Promise<EvalAgentResult> {
189
278
  const parsed = parseAgentArgs(args);
190
- const agentName = parsed.agentType ?? DEFAULT_AGENT_TYPE;
279
+ const agentName = parsed.agent ?? DEFAULT_AGENT_TYPE;
191
280
  const structured = Object.hasOwn(parsed, "schema");
192
281
 
193
282
  assertNotPlanMode(options.session);
@@ -235,85 +324,226 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
235
324
  };
236
325
  const parentArtifactManager = options.session.getArtifactManager?.() ?? undefined;
237
326
  const mcpManager = options.session.mcpManager ?? MCPManager.instance();
238
- const { sessionFile, artifactsDir } = await getArtifacts(options.session);
327
+ const { sessionFile, artifactsDir, tempArtifactsDir } = await getArtifacts(options.session);
239
328
  const outputManager = getOutputManager(options.session);
240
329
  const id = await outputManager.allocate(outputIdBase(parsed.label, agentName));
241
330
  const assignment = parsed.prompt.trim();
242
- // Suspend eval timeout accounting while the subagent owns control. The
243
- // timeout clock restarts once the bridge returns to the cell runtime.
244
- const result = await withBridgeTimeoutPause(options.emitStatus, () =>
245
- taskExecutor.runSubprocess({
246
- cwd: options.session.cwd,
247
- agent: effectiveAgent,
248
- task: renderSubagentPrompt(assignment),
249
- assignment,
250
- description: trimToUndefined(parsed.label),
251
- index: 0,
252
- id,
253
- taskDepth: options.session.taskDepth ?? 0,
254
- modelOverride,
255
- parentActiveModelPattern,
256
- thinkingLevel: effectiveAgent.thinkingLevel,
257
- outputSchema: structured ? parsed.schema : undefined,
258
- sessionFile,
259
- persistArtifacts: Boolean(sessionFile),
260
- artifactsDir,
261
- // Eval `agent()` subagents are short-lived programmatic helpers (data
262
- // collection, structured output, parallel() fan-out). LSP server
263
- // cold-start costs tens of seconds and is pure overhead here, so it is
264
- // forced off regardless of the `task.enableLsp` setting — that knob only
265
- // governs LSP-aware delegation through the `task` tool.
266
- enableLsp: false,
267
- signal: options.signal,
268
- eventBus: options.session.eventBus,
269
- onProgress: progress => emitProgressStatus(options.emitStatus, progress),
270
- authStorage: options.session.authStorage,
271
- modelRegistry: options.session.modelRegistry,
272
- settings: options.session.settings,
273
- // Eval `agent()` subagents are never wall-clock capped: the parent
274
- // cell's idle watchdog is suspended for the whole bridge call
275
- // (withBridgeTimeoutPause), so a long-running phase/recovery workflow
276
- // must not be killed by `task.maxRuntimeMs`. Force the limit off
277
- // regardless of the inherited session setting.
278
- maxRuntimeMs: 0,
279
- mcpManager,
280
- contextFiles,
281
- skills: availableSkills,
282
- autoloadSkills: resolvedAutoloadSkills,
283
- workspaceTree: options.session.workspaceTree,
284
- promptTemplates: options.session.promptTemplates,
285
- localProtocolOptions,
286
- parentArtifactManager,
287
- parentHindsightSessionState: options.session.getHindsightSessionState?.(),
288
- parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
289
- parentTelemetry: options.session.getTelemetry?.(),
290
- parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
291
- // Deliberately omit parentEvalSessionId: the parent's Python kernel is
292
- // blocked on this bridge call, so sharing the eval session would deadlock
293
- // (subagent queues behind the parent's in-flight execution, parent waits
294
- // for subagent → circular). Each bridge-spawned subagent gets its own
295
- // eval session with an independent kernel.
296
- }),
297
- );
298
331
 
299
- if (result.exitCode !== 0 || result.error || result.aborted) {
300
- throw new ToolError(buildSubagentFailureMessage(agentName, result));
332
+ // Isolation gating. Strict opt-in: only the explicit `isolated=true`
333
+ // argument turns it on; `task.isolation.mode` no longer drives the
334
+ // default. Mirrors the `task` tool so eval `agent()` and `task` callers
335
+ // see the same semantic. `isolated=true` while the mode is `"none"`
336
+ // surfaces a clear error instead of silently downgrading.
337
+ const isolationMode = options.session.settings.get("task.isolation.mode");
338
+ const isolationEnabledInSettings = isolationMode !== "none";
339
+ if (parsed.isolated === true && !isolationEnabledInSettings) {
340
+ throw new ToolError(`agent(isolated=True) requires task.isolation.mode to be set; current mode is "none".`);
301
341
  }
342
+ const isIsolated = parsed.isolated === true;
343
+ const settingsMergeMode = options.session.settings.get("task.isolation.merge");
344
+ const mergeMode: "patch" | "branch" = parsed.merge === false ? "patch" : settingsMergeMode;
345
+ const applyChanges = parsed.apply !== false;
346
+
347
+ // Isolation context capture (prepareIsolationContext → captureBaseline)
348
+ // happens inside the timeout-pause closure below; on dirty/large repos the
349
+ // baseline walk can run long and must stay covered by the eval idle
350
+ // suspension.
351
+
352
+ const buildCommitMessage = makeIsolationCommitMessage(options.session);
302
353
 
303
- options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
354
+ const baseRunOptions: ExecutorOptions = {
355
+ cwd: options.session.cwd,
356
+ agent: effectiveAgent,
357
+ task: renderSubagentPrompt(assignment),
358
+ assignment,
359
+ description: trimToUndefined(parsed.label),
360
+ index: 0,
361
+ id,
362
+ taskDepth: options.session.taskDepth ?? 0,
363
+ modelOverride,
364
+ parentActiveModelPattern,
365
+ thinkingLevel: effectiveAgent.thinkingLevel,
366
+ outputSchema: structured ? parsed.schema : undefined,
367
+ sessionFile,
368
+ persistArtifacts: Boolean(sessionFile),
369
+ artifactsDir,
370
+ // Eval `agent()` subagents are short-lived programmatic helpers (data
371
+ // collection, structured output, parallel() fan-out). LSP server
372
+ // cold-start costs tens of seconds and is pure overhead here, so it is
373
+ // forced off regardless of the `task.enableLsp` setting — that knob only
374
+ // governs LSP-aware delegation through the `task` tool.
375
+ enableLsp: false,
376
+ signal: options.signal,
377
+ eventBus: options.session.eventBus,
378
+ onProgress: progress => emitProgressStatus(options.emitStatus, progress),
379
+ authStorage: options.session.authStorage,
380
+ modelRegistry: options.session.modelRegistry,
381
+ settings: options.session.settings,
382
+ // Eval `agent()` subagents are never wall-clock capped: the parent
383
+ // cell's idle watchdog is suspended for the whole bridge call
384
+ // (withBridgeTimeoutPause), so a long-running phase/recovery workflow
385
+ // must not be killed by `task.maxRuntimeMs`. Force the limit off
386
+ // regardless of the inherited session setting.
387
+ maxRuntimeMs: 0,
388
+ mcpManager,
389
+ contextFiles,
390
+ skills: availableSkills,
391
+ autoloadSkills: resolvedAutoloadSkills,
392
+ workspaceTree: options.session.workspaceTree,
393
+ promptTemplates: options.session.promptTemplates,
394
+ localProtocolOptions,
395
+ parentArtifactManager,
396
+ parentHindsightSessionState: options.session.getHindsightSessionState?.(),
397
+ parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
398
+ parentTelemetry: options.session.getTelemetry?.(),
399
+ parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
400
+ // Deliberately omit parentEvalSessionId: the parent's Python kernel is
401
+ // blocked on this bridge call, so sharing the eval session would deadlock
402
+ // (subagent queues behind the parent's in-flight execution, parent waits
403
+ // for subagent → circular). Each bridge-spawned subagent gets its own
404
+ // eval session with an independent kernel.
405
+ };
406
+
407
+ // Suspend eval timeout accounting through the WHOLE bridge call: the
408
+ // subagent subprocess plus any isolation post-processing (merge,
409
+ // nested-patch apply, cleanup). All of that is host-side work while the
410
+ // runtime is parked waiting for the result, and the cell timeout must
411
+ // not abort us mid-cherry-pick or mid-nested-commit. The clock restarts
412
+ // only after we hand control back to the runtime.
413
+ const { result, mergeSummary, changesApplied } = await withBridgeTimeoutPause(options.emitStatus, async () => {
414
+ let isolationContext: IsolationContext | null = null;
415
+ if (isIsolated) {
416
+ try {
417
+ isolationContext = await prepareIsolationContext(options.session.cwd);
418
+ } catch (err) {
419
+ const message = err instanceof Error ? err.message : String(err);
420
+ throw new ToolError(`Isolated agent() execution requires a git repository. ${message}`);
421
+ }
422
+ }
423
+ const preferredBackend = isIsolated ? parseIsolationMode(isolationMode) : undefined;
424
+
425
+ const result = await (async () => {
426
+ if (!isolationContext) {
427
+ return taskExecutor.runSubprocess(baseRunOptions);
428
+ }
429
+ const taskStart = Date.now();
430
+ return runIsolatedSubprocess({
431
+ baseOptions: baseRunOptions,
432
+ context: isolationContext,
433
+ preferredBackend,
434
+ agentId: id,
435
+ mergeMode,
436
+ artifactsDir,
437
+ description: trimToUndefined(parsed.label),
438
+ buildCommitMessage,
439
+ buildFailureResult: err => {
440
+ const message = err instanceof Error ? err.message : String(err);
441
+ return {
442
+ index: 0,
443
+ id,
444
+ agent: effectiveAgent.name,
445
+ agentSource: effectiveAgent.source,
446
+ task: renderSubagentPrompt(assignment),
447
+ assignment,
448
+ description: trimToUndefined(parsed.label),
449
+ exitCode: 1,
450
+ output: "",
451
+ stderr: message,
452
+ truncated: false,
453
+ durationMs: Date.now() - taskStart,
454
+ tokens: 0,
455
+ requests: 0,
456
+ modelOverride,
457
+ error: message,
458
+ };
459
+ },
460
+ });
461
+ })();
462
+
463
+ if (result.exitCode !== 0 || result.error || result.aborted) {
464
+ throw new ToolError(buildSubagentFailureMessage(agentName, result));
465
+ }
304
466
 
305
- // The final `onProgress` flush from `runSubprocess` already emits a
306
- // status:"completed" event carrying full stats (toolCount, cost, context),
307
- // so we don't emit a second, sparser completion event here — it would
308
- // coalesce over the richer one and drop those stats.
467
+ let mergeSummary = "";
468
+ let changesApplied: boolean | null = null;
469
+ if (isIsolated && isolationContext) {
470
+ if (applyChanges) {
471
+ const outcome = await mergeIsolatedChanges({
472
+ result,
473
+ repoRoot: isolationContext.repoRoot,
474
+ mergeMode,
475
+ });
476
+ mergeSummary = outcome.summary;
477
+ changesApplied = outcome.changesApplied;
478
+ if (outcome.changesApplied === false) {
479
+ const summaryText = outcome.summary.trim();
480
+ const recoveryParts: string[] = [];
481
+ if (result.patchPath) recoveryParts.push(`Captured patch preserved at ${result.patchPath}.`);
482
+ if (result.branchName) recoveryParts.push(`Captured branch preserved as ${result.branchName}.`);
483
+ if (result.nestedPatches?.length) {
484
+ const nestedPaths = await persistNestedPatches(artifactsDir, result.id, result.nestedPatches);
485
+ recoveryParts.push(
486
+ `Captured nested repository patches (${result.nestedPatches.length}) preserved at: ${nestedPaths.join(", ")}.`,
487
+ );
488
+ }
489
+ const recoveryHint = recoveryParts.length > 0 ? ` ${recoveryParts.join(" ")}` : "";
490
+ throw new ToolError(
491
+ `agent() isolated apply failed for ${result.id}${summaryText ? `: ${summaryText}` : ""}${recoveryHint}`,
492
+ );
493
+ }
494
+
495
+ mergeSummary += await applyEligibleNestedPatches({
496
+ result,
497
+ repoRoot: isolationContext.repoRoot,
498
+ mergeMode,
499
+ changesApplied: outcome.changesApplied,
500
+ mergedBranchForNestedPatches: outcome.mergedBranchForNestedPatches,
501
+ commitMessage: buildCommitMessage(),
502
+ });
503
+ } else if (result.branchName) {
504
+ mergeSummary = `\n\nIsolation: changes captured on branch \`${result.branchName}\` (apply=false). Not merged.`;
505
+ } else if (result.patchPath) {
506
+ mergeSummary = `\n\nIsolation: changes captured at \`${result.patchPath}\` (apply=false). Not applied.`;
507
+ } else {
508
+ const nestedPatches = result.nestedPatches ?? [];
509
+ if (nestedPatches.length > 0) {
510
+ mergeSummary = `\n\nIsolation: changes captured for ${nestedPatches.length} nested repositor${nestedPatches.length === 1 ? "y" : "ies"} (apply=false). Not applied.`;
511
+ } else {
512
+ mergeSummary = "\n\nIsolation: no changes captured.";
513
+ }
514
+ }
515
+ }
516
+
517
+ // Clean up the temp artifacts dir we created for this call only when the
518
+ // caller will not need files from it later. Keep it when the runtime helper
519
+ // will return an `agent://` handle (the `.md`/`.jsonl` backing files live
520
+ // here) and on `apply=false` (`changesApplied === null`) where the caller
521
+ // consumes `details.patchPath` / `details.branchName` /
522
+ // `details.nestedPatches` out of band. Failed isolated applies throw
523
+ // earlier with a recovery hint, so they never reach this gate.
524
+ const shouldCleanupTempArtifacts = tempArtifactsDir && !parsed.handle && (!isIsolated || changesApplied === true);
525
+ if (shouldCleanupTempArtifacts) {
526
+ await fs.rm(artifactsDir, { recursive: true, force: true });
527
+ }
528
+
529
+ options.session.recordEvalSubagentUsage?.(result.usage?.output ?? 0);
530
+
531
+ return { result, mergeSummary, changesApplied };
532
+ });
309
533
 
310
534
  return {
311
- text: result.output,
535
+ text: structured ? result.output : result.output + mergeSummary,
312
536
  details: {
313
537
  agent: result.agent,
314
538
  id: result.id,
315
539
  model: result.resolvedModel ?? modelOverride,
316
540
  structured,
541
+ isolated: isIsolated || undefined,
542
+ patchPath: result.patchPath,
543
+ branchName: result.branchName,
544
+ nestedPatches: result.nestedPatches?.length ? result.nestedPatches : undefined,
545
+ changesApplied: isIsolated ? changesApplied : undefined,
546
+ isolationSummary: mergeSummary ? mergeSummary.trim() : undefined,
317
547
  },
318
548
  };
319
549
  }