@oh-my-pi/pi-coding-agent 11.0.3 → 11.2.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 (143) hide show
  1. package/CHANGELOG.md +199 -49
  2. package/README.md +1 -1
  3. package/docs/config-usage.md +3 -4
  4. package/docs/sdk.md +6 -5
  5. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  6. package/examples/sdk/README.md +1 -1
  7. package/package.json +19 -11
  8. package/src/cli/args.ts +11 -94
  9. package/src/cli/config-cli.ts +1 -1
  10. package/src/cli/file-processor.ts +3 -3
  11. package/src/cli/oclif-help.ts +26 -0
  12. package/src/cli/web-search-cli.ts +148 -0
  13. package/src/cli.ts +8 -2
  14. package/src/commands/commit.ts +36 -0
  15. package/src/commands/config.ts +51 -0
  16. package/src/commands/grep.ts +41 -0
  17. package/src/commands/index/index.ts +136 -0
  18. package/src/commands/jupyter.ts +32 -0
  19. package/src/commands/plugin.ts +70 -0
  20. package/src/commands/setup.ts +39 -0
  21. package/src/commands/shell.ts +29 -0
  22. package/src/commands/stats.ts +29 -0
  23. package/src/commands/update.ts +21 -0
  24. package/src/commands/web-search.ts +50 -0
  25. package/src/commit/agentic/index.ts +3 -2
  26. package/src/commit/agentic/tools/analyze-file.ts +1 -3
  27. package/src/commit/git/errors.ts +4 -6
  28. package/src/commit/pipeline.ts +3 -2
  29. package/src/config/keybindings.ts +1 -3
  30. package/src/config/model-registry.ts +89 -162
  31. package/src/config/settings-schema.ts +10 -0
  32. package/src/config.ts +202 -132
  33. package/src/exa/mcp-client.ts +8 -41
  34. package/src/export/html/index.ts +1 -1
  35. package/src/extensibility/extensions/loader.ts +7 -10
  36. package/src/extensibility/extensions/runner.ts +5 -15
  37. package/src/extensibility/extensions/types.ts +1 -1
  38. package/src/extensibility/hooks/runner.ts +6 -9
  39. package/src/index.ts +0 -1
  40. package/src/ipy/kernel.ts +10 -22
  41. package/src/lsp/clients/biome-client.ts +4 -7
  42. package/src/lsp/clients/lsp-linter-client.ts +4 -6
  43. package/src/lsp/index.ts +5 -4
  44. package/src/lsp/utils.ts +18 -0
  45. package/src/main.ts +86 -181
  46. package/src/mcp/json-rpc.ts +2 -2
  47. package/src/mcp/transports/http.ts +12 -49
  48. package/src/modes/components/armin.ts +1 -3
  49. package/src/modes/components/assistant-message.ts +4 -4
  50. package/src/modes/components/bash-execution.ts +5 -3
  51. package/src/modes/components/branch-summary-message.ts +1 -3
  52. package/src/modes/components/compaction-summary-message.ts +1 -3
  53. package/src/modes/components/custom-message.ts +4 -5
  54. package/src/modes/components/extensions/extension-dashboard.ts +10 -16
  55. package/src/modes/components/extensions/extension-list.ts +5 -5
  56. package/src/modes/components/footer.ts +1 -4
  57. package/src/modes/components/hook-editor.ts +7 -32
  58. package/src/modes/components/hook-message.ts +4 -5
  59. package/src/modes/components/model-selector.ts +2 -2
  60. package/src/modes/components/plugin-settings.ts +16 -20
  61. package/src/modes/components/python-execution.ts +5 -5
  62. package/src/modes/components/session-selector.ts +6 -7
  63. package/src/modes/components/settings-defs.ts +49 -40
  64. package/src/modes/components/settings-selector.ts +8 -17
  65. package/src/modes/components/skill-message.ts +1 -3
  66. package/src/modes/components/status-line-segment-editor.ts +1 -3
  67. package/src/modes/components/status-line.ts +1 -3
  68. package/src/modes/components/todo-reminder.ts +5 -7
  69. package/src/modes/components/tree-selector.ts +10 -12
  70. package/src/modes/components/ttsr-notification.ts +1 -3
  71. package/src/modes/components/user-message-selector.ts +2 -4
  72. package/src/modes/components/welcome.ts +6 -18
  73. package/src/modes/controllers/event-controller.ts +1 -0
  74. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  75. package/src/modes/controllers/input-controller.ts +7 -34
  76. package/src/modes/controllers/selector-controller.ts +3 -3
  77. package/src/modes/interactive-mode.ts +27 -1
  78. package/src/modes/rpc/rpc-client.ts +2 -5
  79. package/src/modes/rpc/rpc-mode.ts +2 -2
  80. package/src/modes/theme/theme.ts +2 -6
  81. package/src/modes/types.ts +1 -0
  82. package/src/modes/utils/ui-helpers.ts +6 -1
  83. package/src/patch/index.ts +1 -4
  84. package/src/prompts/agents/explore.md +1 -0
  85. package/src/prompts/agents/frontmatter.md +2 -1
  86. package/src/prompts/agents/init.md +1 -0
  87. package/src/prompts/agents/plan.md +1 -0
  88. package/src/prompts/agents/reviewer.md +1 -0
  89. package/src/prompts/system/subagent-submit-reminder.md +2 -0
  90. package/src/prompts/system/subagent-system-prompt.md +2 -0
  91. package/src/prompts/system/subagent-user-prompt.md +8 -0
  92. package/src/prompts/system/system-prompt.md +5 -3
  93. package/src/prompts/system/web-search.md +6 -4
  94. package/src/prompts/tools/task.md +216 -163
  95. package/src/sdk.ts +11 -110
  96. package/src/session/agent-session.ts +117 -83
  97. package/src/session/auth-storage.ts +10 -51
  98. package/src/session/messages.ts +17 -3
  99. package/src/session/session-manager.ts +30 -30
  100. package/src/session/streaming-output.ts +1 -1
  101. package/src/ssh/ssh-executor.ts +6 -3
  102. package/src/task/agents.ts +2 -0
  103. package/src/task/discovery.ts +1 -1
  104. package/src/task/executor.ts +5 -10
  105. package/src/task/index.ts +43 -23
  106. package/src/task/render.ts +67 -64
  107. package/src/task/template.ts +17 -34
  108. package/src/task/types.ts +49 -22
  109. package/src/tools/ask.ts +1 -3
  110. package/src/tools/bash.ts +1 -4
  111. package/src/tools/browser.ts +5 -7
  112. package/src/tools/exit-plan-mode.ts +1 -4
  113. package/src/tools/fetch.ts +1 -3
  114. package/src/tools/find.ts +4 -3
  115. package/src/tools/gemini-image.ts +24 -55
  116. package/src/tools/grep.ts +4 -4
  117. package/src/tools/index.ts +12 -14
  118. package/src/tools/notebook.ts +1 -5
  119. package/src/tools/python.ts +4 -3
  120. package/src/tools/read.ts +2 -4
  121. package/src/tools/render-utils.ts +23 -0
  122. package/src/tools/ssh.ts +8 -12
  123. package/src/tools/todo-write.ts +1 -4
  124. package/src/tools/tool-errors.ts +1 -4
  125. package/src/tools/write.ts +1 -3
  126. package/src/utils/external-editor.ts +59 -0
  127. package/src/utils/file-mentions.ts +39 -1
  128. package/src/utils/image-convert.ts +1 -1
  129. package/src/utils/image-resize.ts +4 -4
  130. package/src/web/search/auth.ts +3 -33
  131. package/src/web/search/index.ts +73 -139
  132. package/src/web/search/provider.ts +58 -0
  133. package/src/web/search/providers/anthropic.ts +53 -14
  134. package/src/web/search/providers/base.ts +22 -0
  135. package/src/web/search/providers/codex.ts +38 -16
  136. package/src/web/search/providers/exa.ts +30 -6
  137. package/src/web/search/providers/gemini.ts +56 -20
  138. package/src/web/search/providers/jina.ts +28 -5
  139. package/src/web/search/providers/perplexity.ts +103 -36
  140. package/src/web/search/render.ts +84 -74
  141. package/src/web/search/types.ts +285 -59
  142. package/src/migrations.ts +0 -175
  143. package/src/session/storage-migration.ts +0 -173
@@ -9,7 +9,7 @@ import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
9
9
  import { logger, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import type { TSchema } from "@sinclair/typebox";
11
11
  import Ajv, { type ValidateFunction } from "ajv";
12
- import type { ModelRegistry } from "../config/model-registry";
12
+ import { ModelRegistry } from "../config/model-registry";
13
13
  import { resolveModelOverride } from "../config/model-resolver";
14
14
  import { type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
15
15
  import { Settings } from "../config/settings";
@@ -19,7 +19,7 @@ import { callTool } from "../mcp/client";
19
19
  import type { MCPManager } from "../mcp/manager";
20
20
  import submitReminderTemplate from "../prompts/system/subagent-submit-reminder.md" with { type: "text" };
21
21
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
22
- import { createAgentSession, discoverAuthStorage, discoverModels } from "../sdk";
22
+ import { createAgentSession, discoverAuthStorage } from "../sdk";
23
23
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
24
24
  import type { AuthStorage } from "../session/auth-storage";
25
25
  import { SessionManager } from "../session/session-manager";
@@ -153,7 +153,6 @@ export interface ExecutorOptions {
153
153
  description?: string;
154
154
  index: number;
155
155
  id: string;
156
- context?: string;
157
156
  modelOverride?: string | string[];
158
157
  thinkingLevel?: ThinkingLevel;
159
158
  outputSchema?: unknown;
@@ -374,7 +373,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
374
373
  index,
375
374
  id,
376
375
  worktree,
377
- context,
378
376
  modelOverride,
379
377
  thinkingLevel,
380
378
  outputSchema,
@@ -421,9 +419,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
421
419
  };
422
420
  }
423
421
 
424
- // Build full task with context
425
- const fullTask = context ? `${context}\n\n${task}` : task;
426
-
427
422
  // Set up artifact paths and write input file upfront if artifacts dir provided
428
423
  let subtaskSessionFile: string | undefined;
429
424
  if (options.artifactsDir) {
@@ -849,7 +844,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
849
844
  checkAbort();
850
845
  const authStorage = options.authStorage ?? (await discoverAuthStorage());
851
846
  checkAbort();
852
- const modelRegistry = options.modelRegistry ?? discoverModels(authStorage);
847
+ const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
853
848
  checkAbort();
854
849
 
855
850
  const { model, thinkingLevel: resolvedThinkingLevel } = resolveModelOverride(
@@ -905,7 +900,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
905
900
 
906
901
  session.sessionManager.appendSessionInit({
907
902
  systemPrompt: session.agent.state.systemPrompt,
908
- task: fullTask,
903
+ task,
909
904
  tools: session.getAllToolNames(),
910
905
  outputSchema,
911
906
  });
@@ -994,7 +989,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
994
989
  }
995
990
  });
996
991
 
997
- await session.prompt(fullTask);
992
+ await session.prompt(task);
998
993
 
999
994
  const reminderToolChoice = buildSubmitResultToolChoice(session.model);
1000
995
 
package/src/task/index.ts CHANGED
@@ -35,7 +35,15 @@ import { AgentOutputManager } from "./output-manager";
35
35
  import { mapWithConcurrencyLimit } from "./parallel";
36
36
  import { renderCall, renderResult } from "./render";
37
37
  import { renderTemplate } from "./template";
38
- import { type AgentProgress, type SingleResult, type TaskParams, type TaskToolDetails, taskSchema } from "./types";
38
+ import {
39
+ type AgentProgress,
40
+ type SingleResult,
41
+ type TaskParams,
42
+ type TaskSchema,
43
+ type TaskToolDetails,
44
+ taskSchema,
45
+ taskSchemaNoIsolation,
46
+ } from "./types";
39
47
  import {
40
48
  applyBaseline,
41
49
  captureBaseline,
@@ -102,12 +110,13 @@ export { taskSchema } from "./types";
102
110
  /**
103
111
  * Build dynamic tool description listing available agents.
104
112
  */
105
- async function buildDescription(cwd: string, maxConcurrency: number): Promise<string> {
113
+ async function buildDescription(cwd: string, maxConcurrency: number, isolationEnabled: boolean): Promise<string> {
106
114
  const { agents } = await discoverAgents(cwd);
107
115
 
108
116
  return renderPromptTemplate(taskDescriptionTemplate, {
109
117
  agents,
110
118
  MAX_CONCURRENCY: maxConcurrency,
119
+ isolationEnabled,
111
120
  });
112
121
  }
113
122
 
@@ -121,20 +130,21 @@ async function buildDescription(cwd: string, maxConcurrency: number): Promise<st
121
130
  * Requires async initialization to discover available agents.
122
131
  * Use `TaskTool.create(session)` to instantiate.
123
132
  */
124
- export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, Theme> {
133
+ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
125
134
  public readonly name = "task";
126
135
  public readonly label = "Task";
127
- public readonly description: string;
128
- public readonly parameters = taskSchema;
136
+ public readonly parameters: TaskSchema;
129
137
  public readonly renderCall = renderCall;
130
138
  public readonly renderResult = renderResult;
131
139
 
132
- private readonly session: ToolSession;
133
140
  private readonly blockedAgent: string | undefined;
134
141
 
135
- private constructor(session: ToolSession, description: string) {
136
- this.session = session;
137
- this.description = description;
142
+ private constructor(
143
+ private readonly session: ToolSession,
144
+ public readonly description: string,
145
+ isolationEnabled: boolean,
146
+ ) {
147
+ this.parameters = isolationEnabled ? taskSchema : taskSchemaNoIsolation;
138
148
  this.blockedAgent = $env.PI_BLOCKED_AGENT;
139
149
  }
140
150
 
@@ -143,8 +153,9 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
143
153
  */
144
154
  public static async create(session: ToolSession): Promise<TaskTool> {
145
155
  const maxConcurrency = session.settings.get("task.maxConcurrency");
146
- const description = await buildDescription(session.cwd, maxConcurrency);
147
- return new TaskTool(session, description);
156
+ const isolationEnabled = session.settings.get("task.isolation.enabled");
157
+ const description = await buildDescription(session.cwd, maxConcurrency, isolationEnabled);
158
+ return new TaskTool(session, description, isolationEnabled);
148
159
  }
149
160
 
150
161
  public async execute(
@@ -155,11 +166,29 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
155
166
  ): Promise<AgentToolResult<TaskToolDetails>> {
156
167
  const startTime = Date.now();
157
168
  const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
158
- const { agent: agentName, context, schema: outputSchema, isolated } = params;
159
- const isIsolated = isolated === true;
169
+ const { agent: agentName, context, schema: outputSchema } = params;
170
+ const isolationEnabled = this.session.settings.get("task.isolation.enabled");
171
+ const isolationRequested = "isolated" in params ? params.isolated === true : false;
172
+ const isIsolated = isolationEnabled && isolationRequested;
160
173
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
161
174
  const taskDepth = this.session.taskDepth ?? 0;
162
175
 
176
+ if (!isolationEnabled && "isolated" in params) {
177
+ return {
178
+ content: [
179
+ {
180
+ type: "text",
181
+ text: "Task isolation is disabled. Remove the isolated argument to run subagents.",
182
+ },
183
+ ],
184
+ details: {
185
+ projectAgentsDir,
186
+ results: [],
187
+ totalDurationMs: 0,
188
+ },
189
+ };
190
+ }
191
+
163
192
  // Validate agent exists
164
193
  const agent = getAgent(agents, agentName);
165
194
  if (!agent) {
@@ -426,7 +455,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
426
455
  agentSource: agent.source,
427
456
  status: "pending",
428
457
  task: t.task,
429
- args: t.args,
430
458
  recentTools: [],
431
459
  recentOutput: [],
432
460
  toolCount: 0,
@@ -447,7 +475,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
447
475
  description: task.description,
448
476
  index,
449
477
  id: task.id,
450
- context: undefined, // Already prepended above
451
478
  taskDepth,
452
479
  modelOverride,
453
480
  thinkingLevel: thinkingLevelOverride,
@@ -462,7 +489,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
462
489
  onProgress: progress => {
463
490
  progressMap.set(index, {
464
491
  ...structuredClone(progress),
465
- args: tasksWithSkills[index]?.args,
466
492
  });
467
493
  emitProgress();
468
494
  },
@@ -493,7 +519,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
493
519
  description: task.description,
494
520
  index,
495
521
  id: task.id,
496
- context: undefined, // Already prepended above
497
522
  taskDepth,
498
523
  modelOverride,
499
524
  thinkingLevel: thinkingLevelOverride,
@@ -508,7 +533,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
508
533
  onProgress: progress => {
509
534
  progressMap.set(index, {
510
535
  ...structuredClone(progress),
511
- args: tasksWithSkills[index]?.args,
512
536
  });
513
537
  emitProgress();
514
538
  },
@@ -564,10 +588,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
564
588
  // Fill in skipped tasks (undefined entries from abort) with placeholder results
565
589
  const results: SingleResult[] = partialResults.map((result, index) => {
566
590
  if (result !== undefined) {
567
- return {
568
- ...result,
569
- args: tasksWithSkills[index]?.args,
570
- };
591
+ return result;
571
592
  }
572
593
  const task = tasksWithSkills[index];
573
594
  return {
@@ -576,7 +597,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
576
597
  agent: agentName,
577
598
  agentSource: agent.source,
578
599
  task: task.task,
579
- args: task.args,
580
600
  description: task.description,
581
601
  exitCode: 1,
582
602
  output: "",
@@ -354,25 +354,27 @@ function renderOutputSection(
354
354
  return lines;
355
355
  }
356
356
 
357
- function formatArgsInline(args: Record<string, string>, _theme: Theme): string {
358
- const entries = Object.entries(args);
359
- if (entries.length === 0) return "No arguments";
357
+ function renderTaskSection(
358
+ task: string,
359
+ continuePrefix: string,
360
+ expanded: boolean,
361
+ theme: Theme,
362
+ maxExpanded = 20,
363
+ ): string[] {
364
+ const lines: string[] = [];
365
+ const trimmed = task.trimEnd();
366
+ if (!expanded || !trimmed) return lines;
360
367
 
361
- // Single variable: show inline as "Key: value" without tree structure
362
- if (entries.length === 1) {
363
- const [key, value] = entries[0];
364
- const humanKey = humanizeKey(key);
365
- const displayValue = `"${truncateToWidth(value, 32)}"`;
366
- return `${humanKey}: ${displayValue}`;
368
+ lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
369
+ const taskLines = trimmed.split("\n");
370
+ for (const line of taskLines.slice(0, maxExpanded)) {
371
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(line, 70))}`);
372
+ }
373
+ if (taskLines.length > maxExpanded) {
374
+ lines.push(`${continuePrefix} ${theme.fg("dim", formatMoreItems(taskLines.length - maxExpanded, "line"))}`);
367
375
  }
368
376
 
369
- const pairs = entries.map(([key, value]) => `${key}=${truncateToWidth(value, 24)}`);
370
- return `Args: ${pairs.join(", ")}`;
371
- }
372
-
373
- /** Convert snake_case or kebab-case to Title Case */
374
- function humanizeKey(key: string): string {
375
- return key.replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
377
+ return lines;
376
378
  }
377
379
 
378
380
  function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): string {
@@ -428,47 +430,6 @@ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string
428
430
  return `Output: ${pairs.join(", ")}`;
429
431
  }
430
432
 
431
- function renderArgsSection(
432
- args: Record<string, string> | undefined,
433
- continuePrefix: string,
434
- expanded: boolean,
435
- theme: Theme,
436
- ): string[] {
437
- if (!args) return [];
438
- // Filter out auto-injected id and description
439
- const filteredArgs = Object.fromEntries(
440
- Object.entries(args).filter(([key]) => key !== "id" && key !== "description"),
441
- );
442
- if (Object.keys(filteredArgs).length === 0) return [];
443
- const lines: string[] = [];
444
- const entries = Object.entries(filteredArgs);
445
-
446
- if (!expanded) {
447
- lines.push(`${continuePrefix}${theme.fg("dim", formatArgsInline(filteredArgs, theme))}`);
448
- return lines;
449
- }
450
-
451
- // Single variable: show inline as "Key: value" without tree structure
452
- if (entries.length === 1) {
453
- const [key, value] = entries[0];
454
- const humanKey = humanizeKey(key);
455
- const displayValue = `"${truncateToWidth(value, 60)}"`;
456
- lines.push(`${continuePrefix}${theme.fg("dim", `${humanKey}: ${displayValue}`)}`);
457
- return lines;
458
- }
459
-
460
- lines.push(`${continuePrefix}${theme.fg("dim", "Args")}`);
461
- const tree = renderJsonTreeLines(filteredArgs, theme, 4, 16);
462
- for (const line of tree.lines) {
463
- lines.push(`${continuePrefix} ${line}`);
464
- }
465
- if (tree.truncated) {
466
- lines.push(`${continuePrefix} ${theme.fg("dim", "…")}`);
467
- }
468
-
469
- return lines;
470
- }
471
-
472
433
  /**
473
434
  * Render the tool call arguments.
474
435
  */
@@ -482,7 +443,7 @@ export function renderCall(args: TaskParams, theme: Theme): Component {
482
443
  const branch = theme.fg("dim", theme.tree.branch);
483
444
  const last = theme.fg("dim", theme.tree.last);
484
445
  const vertical = theme.fg("dim", theme.tree.vertical);
485
- const showIsolated = args.isolated === true;
446
+ const showIsolated = "isolated" in args && args.isolated === true;
486
447
 
487
448
  if (hasContext) {
488
449
  lines.push(` ${branch} ${theme.fg("dim", "Context")}`);
@@ -556,7 +517,7 @@ function renderAgentProgress(
556
517
 
557
518
  lines.push(statusLine);
558
519
 
559
- lines.push(...renderArgsSection(progress.args, continuePrefix, expanded, theme));
520
+ lines.push(...renderTaskSection(progress.task, continuePrefix, expanded, theme));
560
521
 
561
522
  // Current tool (if running) or most recent completed tool
562
523
  if (progress.status === "running") {
@@ -778,7 +739,8 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
778
739
  }
779
740
 
780
741
  lines.push(statusLine);
781
- lines.push(...renderArgsSection(result.args, continuePrefix, expanded, theme));
742
+
743
+ lines.push(...renderTaskSection(result.task, continuePrefix, expanded, theme));
782
744
 
783
745
  // Check for review result (submit_result with review schema + report_finding)
784
746
  const completeData = result.extractedToolData?.submit_result as Array<{ data: unknown }> | undefined;
@@ -810,6 +772,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
810
772
 
811
773
  // Check for extracted tool data with custom renderers (skip review tools)
812
774
  let hasCustomRendering = false;
775
+ const deferredToolLines: string[] = [];
813
776
  if (result.extractedToolData) {
814
777
  for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
815
778
  // Skip review tools - handled above
@@ -817,20 +780,24 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
817
780
 
818
781
  const handler = subprocessToolRegistry.getHandler(toolName);
819
782
  if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
820
- hasCustomRendering = true;
783
+ const isTaskTool = toolName === "task";
821
784
  const component = handler.renderFinal(dataArray as unknown[], theme, expanded);
822
- lines.push(`${continuePrefix}${theme.fg("dim", `Tool: ${toolName}`)}`);
785
+ const target = isTaskTool ? deferredToolLines : lines;
786
+ if (!isTaskTool) {
787
+ hasCustomRendering = true;
788
+ target.push(`${continuePrefix}${theme.fg("dim", `Tool: ${toolName}`)}`);
789
+ }
823
790
  if (component instanceof Text) {
824
791
  // Prefix each line with continuePrefix
825
792
  const text = component.getText();
826
793
  for (const line of text.split("\n")) {
827
- lines.push(`${continuePrefix}${line}`);
794
+ target.push(`${continuePrefix}${line}`);
828
795
  }
829
796
  } else if (component instanceof Container) {
830
797
  // For containers, render each child
831
798
  for (const child of (component as Container).children) {
832
799
  if (child instanceof Text) {
833
- lines.push(`${continuePrefix}${child.getText()}`);
800
+ target.push(`${continuePrefix}${child.getText()}`);
834
801
  }
835
802
  }
836
803
  }
@@ -854,6 +821,10 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
854
821
  );
855
822
  }
856
823
 
824
+ if (deferredToolLines.length > 0) {
825
+ lines.push(...deferredToolLines);
826
+ }
827
+
857
828
  if (result.patchPath && !aborted && result.exitCode === 0) {
858
829
  lines.push(`${continuePrefix}${theme.fg("dim", `Patch: ${result.patchPath}`)}`);
859
830
  }
@@ -944,6 +915,38 @@ export function renderResult(
944
915
  return new Text(indented.join("\n"), 0, 0);
945
916
  }
946
917
 
918
+ function isTaskToolDetails(value: unknown): value is TaskToolDetails {
919
+ return (
920
+ Boolean(value) &&
921
+ typeof value === "object" &&
922
+ "results" in (value as TaskToolDetails) &&
923
+ Array.isArray((value as TaskToolDetails).results)
924
+ );
925
+ }
926
+
927
+ function renderNestedTaskResults(detailsList: TaskToolDetails[], expanded: boolean, theme: Theme): string[] {
928
+ const lines: string[] = [];
929
+ for (const details of detailsList) {
930
+ if (!details.results || details.results.length === 0) continue;
931
+ details.results.forEach((result, index) => {
932
+ const isLast = index === details.results.length - 1;
933
+ lines.push(...renderAgentResult(result, isLast, expanded, theme));
934
+ });
935
+ }
936
+ return lines;
937
+ }
938
+
939
+ subprocessToolRegistry.register<TaskToolDetails>("task", {
940
+ extractData: event => {
941
+ const details = event.result?.details;
942
+ return isTaskToolDetails(details) ? details : undefined;
943
+ },
944
+ renderFinal: (allData, theme, expanded) => {
945
+ const lines = renderNestedTaskResults(allData, expanded, theme);
946
+ return new Text(lines.join("\n"), 0, 0);
947
+ },
948
+ });
949
+
947
950
  export const taskToolRenderer = {
948
951
  renderCall,
949
952
  renderResult,
@@ -1,47 +1,30 @@
1
+ import { renderPromptTemplate } from "../config/prompt-templates";
2
+ import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
1
3
  import type { TaskItem } from "./types";
2
4
 
3
- type RenderResult = {
5
+ interface RenderResult {
6
+ /** Full task text sent to the subagent */
4
7
  task: string;
5
- args: Record<string, string>;
6
8
  id: string;
7
9
  description: string;
8
10
  skills?: string[];
9
- };
10
-
11
- export function renderTemplate(template: string, task: TaskItem): RenderResult {
12
- const { id, description, args, skills } = task;
13
-
14
- let usedPlaceholder = false;
15
- const unknownArguments: string[] = [];
16
- let renderedTask = template.replace(/\{\{(\w+)\}\}/g, (_match, key: string) => {
17
- const value = args?.[key];
18
- if (value) {
19
- usedPlaceholder = true;
20
- return value;
21
- }
22
- switch (key) {
23
- case "id":
24
- usedPlaceholder = true;
25
- return id;
26
- case "description":
27
- usedPlaceholder = true;
28
- return description;
29
- default:
30
- unknownArguments.push(key);
31
- return `{{${key}}}`;
32
- }
33
- });
11
+ }
34
12
 
35
- if (unknownArguments.length > 0) {
36
- throw new Error(`Task "${id}" has unknown arguments: ${unknownArguments.join(", ")}`);
37
- }
13
+ /**
14
+ * Build the full task text from shared context and per-task assignment.
15
+ *
16
+ * If context is provided, it is prepended with a separator.
17
+ */
18
+ export function renderTemplate(context: string | undefined, task: TaskItem): RenderResult {
19
+ let { id, description, assignment, skills } = task;
20
+ assignment = assignment.trim();
21
+ context = context?.trim();
38
22
 
39
- if (!usedPlaceholder) {
40
- renderedTask += `\n----------------------\n# ${id}\n${description}`;
23
+ if (!context || !assignment) {
24
+ return { task: assignment || context!, id, description, skills };
41
25
  }
42
26
  return {
43
- task: renderedTask,
44
- args: { id, description, ...args },
27
+ task: renderPromptTemplate(subagentUserPromptTemplate, { context, assignment }),
45
28
  id,
46
29
  description,
47
30
  skills,
package/src/task/types.ts CHANGED
@@ -33,36 +33,65 @@ export const TASK_SUBAGENT_PROGRESS_CHANNEL = "task:subagent:progress";
33
33
  /** Single task item for parallel execution */
34
34
  export const taskItemSchema = Type.Object({
35
35
  id: Type.String({
36
- description: "Task ID, CamelCase, max 32 chars",
36
+ description: "CamelCase identifier, max 32 chars",
37
37
  maxLength: 32,
38
38
  }),
39
- description: Type.String({ description: "Short description for display" }),
40
- args: Type.Optional(
41
- Type.Record(Type.String(), Type.String(), {
42
- description: "Arguments to fill {{placeholders}} in context",
43
- }),
44
- ),
39
+ description: Type.String({
40
+ description: "Short one-liner for UI display only — not seen by the subagent",
41
+ }),
42
+ assignment: Type.String({
43
+ description:
44
+ "Complete per-task instructions the subagent executes. Must follow the Target/Change/Edge Cases/Acceptance structure. Only include per-task deltas — shared background belongs in `context`.",
45
+ }),
45
46
  skills: Type.Optional(
46
47
  Type.Array(Type.String(), {
47
- description: "Skill names to preload into the subagent system prompt",
48
+ description: "Skill names to preload into the subagent. Use only where it changes correctness.",
48
49
  }),
49
50
  ),
50
51
  });
51
-
52
52
  export type TaskItem = Static<typeof taskItemSchema>;
53
53
 
54
- /** Task tool parameters */
55
- export const taskSchema = Type.Object({
56
- agent: Type.String({ description: "Agent type for all tasks" }),
57
- context: Type.String({ description: "Template with {{placeholders}} for args" }),
58
- isolated: Type.Optional(Type.Boolean({ description: "Run in isolated git worktree" })),
59
- schema: Type.Optional(
60
- Type.Record(Type.String(), Type.Unknown(), { description: "JTD schema defining expected response structure" }),
61
- ),
62
- tasks: Type.Array(taskItemSchema, { description: "Tasks to run in parallel" }),
63
- });
54
+ const createTaskSchema = (options: { isolationEnabled: boolean }) => {
55
+ const properties = {
56
+ agent: Type.String({ description: "Agent type for all tasks in this batch" }),
57
+ context: Type.Optional(
58
+ Type.String({
59
+ description:
60
+ "Shared background prepended to every task's assignment. Put goal, non-goals, constraints, conventions, reference paths, API contracts, and global acceptance commands here once — instead of duplicating across assignments.",
61
+ }),
62
+ ),
63
+ schema: Type.Optional(
64
+ Type.Record(Type.String(), Type.Unknown(), {
65
+ description:
66
+ "JTD schema defining expected response structure. Use typed properties. Output format belongs here — never in context or assignment.",
67
+ }),
68
+ ),
69
+ tasks: Type.Array(taskItemSchema, {
70
+ description:
71
+ "Tasks to execute in parallel. Each must be small-scoped (3-5 files max) and self-contained given context + assignment.",
72
+ }),
73
+ };
74
+
75
+ if (options.isolationEnabled) {
76
+ return Type.Object({
77
+ ...properties,
78
+ isolated: Type.Optional(
79
+ Type.Boolean({
80
+ description: "Run in isolated git worktree; returns patches. Use when tasks edit overlapping files.",
81
+ }),
82
+ ),
83
+ });
84
+ }
85
+
86
+ return Type.Object(properties);
87
+ };
88
+
89
+ export const taskSchema = createTaskSchema({ isolationEnabled: true });
90
+ export const taskSchemaNoIsolation = createTaskSchema({ isolationEnabled: false });
91
+
92
+ export type TaskSchema = typeof taskSchema | typeof taskSchemaNoIsolation;
64
93
 
65
- export type TaskParams = Static<typeof taskSchema>;
94
+ export type TaskParams = Static<TaskSchema>;
66
95
 
67
96
  /** A code review finding reported by the reviewer agent */
68
97
  export interface ReviewFinding {
@@ -110,7 +139,6 @@ export interface AgentProgress {
110
139
  agentSource: AgentSource;
111
140
  status: "pending" | "running" | "completed" | "failed" | "aborted";
112
141
  task: string;
113
- args?: Record<string, string>;
114
142
  description?: string;
115
143
  currentTool?: string;
116
144
  currentToolArgs?: string;
@@ -132,7 +160,6 @@ export interface SingleResult {
132
160
  agent: string;
133
161
  agentSource: AgentSource;
134
162
  task: string;
135
- args?: Record<string, string>;
136
163
  description?: string;
137
164
  exitCode: number;
138
165
  output: string;
package/src/tools/ask.ts CHANGED
@@ -254,10 +254,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
254
254
  public readonly label = "Ask";
255
255
  public readonly description: string;
256
256
  public readonly parameters = askSchema;
257
- private readonly session: ToolSession;
258
257
 
259
- constructor(session: ToolSession) {
260
- this.session = session;
258
+ constructor(private readonly session: ToolSession) {
261
259
  this.description = renderPromptTemplate(askDescription);
262
260
  }
263
261
 
package/src/tools/bash.ts CHANGED
@@ -49,10 +49,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
49
49
  public readonly parameters = bashSchema;
50
50
  public readonly concurrency = "exclusive";
51
51
 
52
- private readonly session: ToolSession;
53
-
54
- constructor(session: ToolSession) {
55
- this.session = session;
52
+ constructor(private readonly session: ToolSession) {
56
53
  this.description = renderPromptTemplate(bashDescription);
57
54
  }
58
55
 
@@ -257,7 +257,6 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
257
257
  public readonly label = "Puppeteer";
258
258
  public readonly description: string;
259
259
  public readonly parameters = browserSchema;
260
- private readonly session: ToolSession;
261
260
  private browser: Browser | null = null;
262
261
  private page: Page | null = null;
263
262
  private currentHeadless: boolean | null = null;
@@ -267,8 +266,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
267
266
  private readonly elementCache = new Map<number, ElementHandle>();
268
267
  private readonly patchedClients = new WeakSet<object>();
269
268
 
270
- constructor(session: ToolSession) {
271
- this.session = session;
269
+ constructor(private readonly session: ToolSession) {
272
270
  this.description = renderPromptTemplate(browserDescription, {});
273
271
  }
274
272
 
@@ -1033,10 +1031,10 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
1033
1031
  const html = (await untilAborted(signal, () => page.content())) as string;
1034
1032
  const url = page.url();
1035
1033
  const virtualConsole = new VirtualConsole();
1036
- virtualConsole.on("jsdomError", error => {
1037
- if (error?.message?.includes("Could not parse CSS stylesheet")) return;
1034
+ virtualConsole.on("jsdomError", err => {
1035
+ if (err?.message?.includes("Could not parse CSS stylesheet")) return;
1038
1036
  logger.debug("JSDOM error during readable extraction", {
1039
- error: error instanceof Error ? error.message : String(error),
1037
+ error: err instanceof Error ? err.message : String(err),
1040
1038
  });
1041
1039
  });
1042
1040
  const dom = new JSDOM(html, { url, virtualConsole });
@@ -1094,7 +1092,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
1094
1092
  }
1095
1093
 
1096
1094
  const mimeType = imageFormat === "png" ? "image/png" : "image/jpeg";
1097
- const base64 = buffer.toString("base64");
1095
+ const base64 = buffer.toBase64();
1098
1096
  let savedPath: string | undefined;
1099
1097
  if (params.path) {
1100
1098
  const resolved = resolveToCwd(params.path, this.session.cwd);