@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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 (158) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/package.json +5 -5
  3. package/src/cli/args.ts +4 -0
  4. package/src/core/agent-session.ts +29 -2
  5. package/src/core/bash-executor.ts +2 -1
  6. package/src/core/custom-commands/bundled/review/index.ts +367 -14
  7. package/src/core/custom-commands/bundled/wt/index.ts +1 -1
  8. package/src/core/sdk.ts +10 -2
  9. package/src/core/session-manager.ts +158 -246
  10. package/src/core/session-storage.ts +379 -0
  11. package/src/core/settings-manager.ts +155 -4
  12. package/src/core/slash-commands.ts +39 -13
  13. package/src/core/system-prompt.ts +62 -64
  14. package/src/core/tools/ask.ts +5 -4
  15. package/src/core/tools/bash-interceptor.ts +26 -61
  16. package/src/core/tools/bash.ts +13 -8
  17. package/src/core/tools/edit-diff.ts +11 -4
  18. package/src/core/tools/edit.ts +7 -13
  19. package/src/core/tools/find.ts +111 -50
  20. package/src/core/tools/gemini-image.ts +128 -147
  21. package/src/core/tools/grep.ts +397 -415
  22. package/src/core/tools/index.test.ts +5 -1
  23. package/src/core/tools/index.ts +8 -4
  24. package/src/core/tools/ls.ts +12 -10
  25. package/src/core/tools/lsp/client.ts +84 -19
  26. package/src/core/tools/lsp/config.ts +205 -656
  27. package/src/core/tools/lsp/defaults.json +465 -0
  28. package/src/core/tools/lsp/index.ts +72 -35
  29. package/src/core/tools/lsp/rust-analyzer.ts +49 -10
  30. package/src/core/tools/lsp/types.ts +1 -0
  31. package/src/core/tools/lsp/utils.ts +1 -1
  32. package/src/core/tools/read.ts +150 -74
  33. package/src/core/tools/render-utils.ts +70 -10
  34. package/src/core/tools/review.ts +38 -126
  35. package/src/core/tools/task/artifacts.ts +5 -4
  36. package/src/core/tools/task/commands.ts +4 -0
  37. package/src/core/tools/task/executor.ts +94 -83
  38. package/src/core/tools/task/index.ts +130 -92
  39. package/src/core/tools/task/parallel.ts +30 -3
  40. package/src/core/tools/task/render.ts +85 -39
  41. package/src/core/tools/task/types.ts +15 -6
  42. package/src/core/tools/task/worker.ts +124 -89
  43. package/src/core/tools/web-fetch.ts +112 -377
  44. package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
  45. package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
  46. package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
  47. package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
  48. package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
  49. package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
  50. package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
  51. package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
  52. package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
  53. package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
  54. package/src/core/tools/web-scrapers/clojars.ts +180 -0
  55. package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
  56. package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
  57. package/src/core/tools/web-scrapers/crossref.ts +149 -0
  58. package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
  59. package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
  60. package/src/core/tools/web-scrapers/discourse.ts +221 -0
  61. package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
  62. package/src/core/tools/web-scrapers/fdroid.ts +158 -0
  63. package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
  64. package/src/core/tools/web-scrapers/flathub.ts +239 -0
  65. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
  66. package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
  67. package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
  68. package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
  69. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
  70. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
  71. package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
  72. package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
  73. package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
  74. package/src/core/tools/web-scrapers/index.ts +250 -0
  75. package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
  76. package/src/core/tools/web-scrapers/lemmy.ts +220 -0
  77. package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
  78. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
  79. package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
  80. package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
  81. package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
  82. package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
  83. package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
  84. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
  85. package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
  86. package/src/core/tools/web-scrapers/ollama.ts +267 -0
  87. package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
  88. package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
  89. package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
  90. package/src/core/tools/web-scrapers/orcid.ts +299 -0
  91. package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
  92. package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
  93. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
  94. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
  95. package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
  96. package/src/core/tools/web-scrapers/rawg.ts +124 -0
  97. package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
  98. package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
  99. package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
  100. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
  101. package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
  102. package/src/core/tools/web-scrapers/searchcode.ts +217 -0
  103. package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
  104. package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
  105. package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
  106. package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
  107. package/src/core/tools/web-scrapers/spdx.ts +121 -0
  108. package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
  109. package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
  110. package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
  111. package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
  112. package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
  113. package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
  114. package/src/core/tools/web-scrapers/utils.ts +162 -0
  115. package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
  116. package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
  117. package/src/core/tools/web-scrapers/w3c.ts +163 -0
  118. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
  119. package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
  120. package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
  121. package/src/core/tools/write.ts +21 -18
  122. package/src/core/voice.ts +3 -2
  123. package/src/lib/worktree/collapse.ts +2 -1
  124. package/src/lib/worktree/git.ts +2 -18
  125. package/src/main.ts +59 -3
  126. package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
  127. package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
  128. package/src/modes/interactive/components/hook-editor.ts +2 -1
  129. package/src/modes/interactive/components/model-selector.ts +19 -4
  130. package/src/modes/interactive/interactive-mode.ts +41 -63
  131. package/src/modes/interactive/theme/theme.ts +58 -58
  132. package/src/modes/rpc/rpc-mode.ts +10 -9
  133. package/src/prompts/review-request.md +27 -0
  134. package/src/prompts/reviewer.md +64 -68
  135. package/src/prompts/tools/output.md +22 -3
  136. package/src/prompts/tools/task.md +32 -33
  137. package/src/utils/clipboard.ts +2 -1
  138. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  139. package/src/core/tools/web-fetch-handlers/index.ts +0 -69
  140. package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
  141. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
  142. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
  143. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
  144. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
  145. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
  146. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
  147. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
  148. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
  149. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
  150. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
  151. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
  152. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
  153. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
  154. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
  155. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
  156. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
  157. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
  158. /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
@@ -21,7 +21,6 @@ import { formatDuration } from "../render-utils";
21
21
  import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
22
22
  import { discoverAgents, getAgent } from "./discovery";
23
23
  import { runSubprocess } from "./executor";
24
- import { generateTaskName } from "./name-generator";
25
24
  import { mapWithConcurrencyLimit } from "./parallel";
26
25
  import { renderCall, renderResult } from "./render";
27
26
  import {
@@ -135,17 +134,35 @@ export async function createTaskTool(
135
134
  execute: async (_toolCallId, params, signal, onUpdate) => {
136
135
  const startTime = Date.now();
137
136
  const { agents, projectAgentsDir } = await discoverAgents(session.cwd);
138
- const context = params.context;
139
- const outputSchema = params.output_schema;
137
+ const { agent: agentName, context, model, output: outputSchema } = params;
138
+ const modelOverride = model ?? session.getModelString?.();
139
+
140
+ // Validate agent exists
141
+ const agent = getAgent(agents, agentName);
142
+ if (!agent) {
143
+ const available = agents.map((a) => a.name).join(", ") || "none";
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: `Unknown agent "${agentName}". Available: ${available}`,
149
+ },
150
+ ],
151
+ details: {
152
+ projectAgentsDir,
153
+ results: [],
154
+ totalDurationMs: 0,
155
+ },
156
+ };
157
+ }
140
158
 
141
159
  // Handle empty or missing tasks
142
160
  if (!params.tasks || params.tasks.length === 0) {
143
- const available = agents.map((a) => a.name).join(", ") || "none";
144
161
  return {
145
162
  content: [
146
163
  {
147
164
  type: "text",
148
- text: `No tasks provided. Use: { tasks: [{agent, task}, ...] }\nAvailable agents: ${available}`,
165
+ text: `No tasks provided. Use: { agent, context, tasks: [{id, task, description}, ...] }`,
149
166
  },
150
167
  ],
151
168
  details: {
@@ -173,6 +190,56 @@ export async function createTaskTool(
173
190
  };
174
191
  }
175
192
 
193
+ const tasks = params.tasks;
194
+ const missingTaskIndexes: number[] = [];
195
+ const idIndexes = new Map<string, number[]>();
196
+
197
+ for (let i = 0; i < tasks.length; i++) {
198
+ const id = tasks[i]?.id;
199
+ if (typeof id !== "string" || id.trim() === "") {
200
+ missingTaskIndexes.push(i);
201
+ continue;
202
+ }
203
+ const normalizedId = id.toLowerCase();
204
+ const indexes = idIndexes.get(normalizedId);
205
+ if (indexes) {
206
+ indexes.push(i);
207
+ } else {
208
+ idIndexes.set(normalizedId, [i]);
209
+ }
210
+ }
211
+
212
+ const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
213
+ for (const [normalizedId, indexes] of idIndexes.entries()) {
214
+ if (indexes.length > 1) {
215
+ duplicateIds.push({
216
+ id: tasks[indexes[0]]?.id ?? normalizedId,
217
+ indexes,
218
+ });
219
+ }
220
+ }
221
+
222
+ if (missingTaskIndexes.length > 0 || duplicateIds.length > 0) {
223
+ const problems: string[] = [];
224
+ if (missingTaskIndexes.length > 0) {
225
+ problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
226
+ }
227
+ if (duplicateIds.length > 0) {
228
+ const details = duplicateIds
229
+ .map((entry) => `${entry.id} (indexes ${entry.indexes.join(", ")})`)
230
+ .join("; ");
231
+ problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
232
+ }
233
+ return {
234
+ content: [{ type: "text", text: `Invalid tasks: ${problems.join(". ")}` }],
235
+ details: {
236
+ projectAgentsDir,
237
+ results: [],
238
+ totalDurationMs: 0,
239
+ },
240
+ };
241
+ }
242
+
176
243
  // Derive artifacts directory
177
244
  const sessionFile = session.getSessionFile();
178
245
  const artifactsDir = sessionFile ? getArtifactsDir(sessionFile) : null;
@@ -197,88 +264,59 @@ export async function createTaskTool(
197
264
  };
198
265
 
199
266
  try {
200
- let tasks = params.tasks;
201
- let skippedSelfRecursion = 0;
202
-
203
- // Filter out blocked agent (self-recursion prevention)
204
- if (blockedAgent) {
205
- const blockedTasks = tasks.filter((t) => t.agent === blockedAgent);
206
- tasks = tasks.filter((t) => t.agent !== blockedAgent);
207
- skippedSelfRecursion = blockedTasks.length;
208
-
209
- if (skippedSelfRecursion > 0 && tasks.length === 0) {
210
- return {
211
- content: [
212
- {
213
- type: "text",
214
- text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
215
- },
216
- ],
217
- details: {
218
- projectAgentsDir,
219
- results: [],
220
- totalDurationMs: Date.now() - startTime,
267
+ // Check self-recursion prevention
268
+ if (blockedAgent && agentName === blockedAgent) {
269
+ return {
270
+ content: [
271
+ {
272
+ type: "text",
273
+ text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
221
274
  },
222
- };
223
- }
224
- }
225
-
226
- // Validate all agents exist
227
- for (const task of tasks) {
228
- if (!getAgent(agents, task.agent)) {
229
- const available = agents.map((a) => a.name).join(", ");
230
- return {
231
- content: [{ type: "text", text: `Unknown agent: ${task.agent}. Available: ${available}` }],
232
- details: {
233
- projectAgentsDir,
234
- results: [],
235
- totalDurationMs: Date.now() - startTime,
236
- },
237
- };
238
- }
275
+ ],
276
+ details: {
277
+ projectAgentsDir,
278
+ results: [],
279
+ totalDurationMs: Date.now() - startTime,
280
+ },
281
+ };
239
282
  }
240
283
 
241
284
  // Check spawn restrictions from parent
242
285
  const parentSpawns = session.getSessionSpawns() ?? "*";
243
286
  const allowedSpawns = parentSpawns.split(",").map((s) => s.trim());
244
- const isSpawnAllowed = (agentName: string): boolean => {
287
+ const isSpawnAllowed = (): boolean => {
245
288
  if (parentSpawns === "") return false; // Empty = deny all
246
289
  if (parentSpawns === "*") return true; // Wildcard = allow all
247
290
  return allowedSpawns.includes(agentName);
248
291
  };
249
292
 
250
- for (const task of tasks) {
251
- if (!isSpawnAllowed(task.agent)) {
252
- const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
253
- return {
254
- content: [{ type: "text", text: `Cannot spawn '${task.agent}'. Allowed: ${allowed}` }],
255
- details: {
256
- projectAgentsDir,
257
- results: [],
258
- totalDurationMs: Date.now() - startTime,
259
- },
260
- };
261
- }
293
+ if (!isSpawnAllowed()) {
294
+ const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
295
+ return {
296
+ content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
297
+ details: {
298
+ projectAgentsDir,
299
+ results: [],
300
+ totalDurationMs: Date.now() - startTime,
301
+ },
302
+ };
262
303
  }
263
304
 
264
- // Build full prompts with context prepended and generate task IDs
305
+ // Build full prompts with context prepended
265
306
  const tasksWithContext = tasks.map((t) => ({
266
- agent: t.agent,
267
307
  task: context ? `${context}\n\n${t.task}` : t.task,
268
- model: t.model,
269
308
  description: t.description,
270
- taskId: generateTaskName(),
309
+ taskId: t.id,
271
310
  }));
272
311
 
273
312
  // Initialize progress for all tasks
274
313
  for (let i = 0; i < tasksWithContext.length; i++) {
275
314
  const t = tasksWithContext[i];
276
- const agentCfg = getAgent(agents, t.agent);
277
315
  progressMap.set(i, {
278
316
  index: i,
279
317
  taskId: t.taskId,
280
- agent: t.agent,
281
- agentSource: agentCfg?.source ?? "user",
318
+ agent: agentName,
319
+ agentSource: agent.source,
282
320
  status: "pending",
283
321
  task: t.task,
284
322
  recentTools: [],
@@ -286,36 +324,40 @@ export async function createTaskTool(
286
324
  toolCount: 0,
287
325
  tokens: 0,
288
326
  durationMs: 0,
289
- modelOverride: t.model,
327
+ modelOverride,
290
328
  description: t.description,
291
329
  });
292
330
  }
293
331
  emitProgress();
294
332
 
295
333
  // Execute in parallel with concurrency limit
296
- const results = await mapWithConcurrencyLimit(tasksWithContext, MAX_CONCURRENCY, async (task, index) => {
297
- const agent = getAgent(agents, task.agent)!;
298
- return runSubprocess({
299
- cwd: session.cwd,
300
- agent,
301
- task: task.task,
302
- description: task.description,
303
- index,
304
- taskId: task.taskId,
305
- context: undefined, // Already prepended above
306
- modelOverride: task.model,
307
- outputSchema,
308
- sessionFile,
309
- persistArtifacts: !!artifactsDir,
310
- artifactsDir: effectiveArtifactsDir,
311
- signal,
312
- eventBus: undefined,
313
- onProgress: (progress) => {
314
- progressMap.set(index, structuredClone(progress));
315
- emitProgress();
316
- },
317
- });
318
- });
334
+ const results = await mapWithConcurrencyLimit(
335
+ tasksWithContext,
336
+ MAX_CONCURRENCY,
337
+ async (task, index) => {
338
+ return runSubprocess({
339
+ cwd: session.cwd,
340
+ agent,
341
+ task: task.task,
342
+ description: task.description,
343
+ index,
344
+ taskId: task.taskId,
345
+ context: undefined, // Already prepended above
346
+ modelOverride,
347
+ outputSchema,
348
+ sessionFile,
349
+ persistArtifacts: !!artifactsDir,
350
+ artifactsDir: effectiveArtifactsDir,
351
+ signal,
352
+ eventBus: undefined,
353
+ onProgress: (progress) => {
354
+ progressMap.set(index, structuredClone(progress));
355
+ emitProgress();
356
+ },
357
+ });
358
+ },
359
+ signal,
360
+ );
319
361
 
320
362
  // Aggregate usage from executor results (already accumulated incrementally)
321
363
  const aggregatedUsage = createUsageTotals();
@@ -349,14 +391,10 @@ export async function createTaskTool(
349
391
  return `[${r.agent}] ${status}${meta} ${r.taskId}\n${preview}`;
350
392
  });
351
393
 
352
- const skippedNote =
353
- skippedSelfRecursion > 0
354
- ? ` (${skippedSelfRecursion} ${blockedAgent} task${skippedSelfRecursion > 1 ? "s" : ""} skipped - self-recursion blocked)`
355
- : "";
356
394
  const outputIds = results.map((r) => r.taskId);
357
395
  const outputHint =
358
396
  outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
359
- const summary = `${successCount}/${results.length} succeeded${skippedNote} [${formatDuration(
397
+ const summary = `${successCount}/${results.length} succeeded [${formatDuration(
360
398
  totalDuration,
361
399
  )}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}`;
362
400
 
@@ -7,24 +7,45 @@ import { MAX_CONCURRENCY } from "./types";
7
7
  /**
8
8
  * Execute items with a concurrency limit using a worker pool pattern.
9
9
  * Results are returned in the same order as input items.
10
+ * Fails fast on first error - does not wait for other workers to complete.
10
11
  *
11
12
  * @param items - Items to process
12
13
  * @param concurrency - Maximum concurrent operations
13
14
  * @param fn - Async function to execute for each item
15
+ * @param signal - Optional abort signal to stop scheduling work
14
16
  */
15
17
  export async function mapWithConcurrencyLimit<T, R>(
16
18
  items: T[],
17
19
  concurrency: number,
18
20
  fn: (item: T, index: number) => Promise<R>,
21
+ signal?: AbortSignal,
19
22
  ): Promise<R[]> {
20
23
  const limit = Math.max(1, Math.min(concurrency, items.length, MAX_CONCURRENCY));
21
24
  const results: R[] = new Array(items.length);
22
25
  let nextIndex = 0;
23
26
 
27
+ // Create internal abort controller to cancel workers on any rejection
28
+ const abortController = new AbortController();
29
+ const workerSignal = signal ? AbortSignal.any([signal, abortController.signal]) : abortController.signal;
30
+
31
+ // Promise that rejects on first error - used to fail fast
32
+ let rejectFirst: (error: unknown) => void;
33
+ const firstErrorPromise = new Promise<never>((_, reject) => {
34
+ rejectFirst = reject;
35
+ });
36
+
24
37
  const worker = async (): Promise<void> => {
25
- while (nextIndex < items.length) {
38
+ while (true) {
39
+ workerSignal.throwIfAborted();
26
40
  const index = nextIndex++;
27
- results[index] = await fn(items[index], index);
41
+ if (index >= items.length) return;
42
+ try {
43
+ results[index] = await fn(items[index], index);
44
+ } catch (error) {
45
+ abortController.abort();
46
+ rejectFirst(error);
47
+ throw error;
48
+ }
28
49
  }
29
50
  };
30
51
 
@@ -32,7 +53,13 @@ export async function mapWithConcurrencyLimit<T, R>(
32
53
  const workers = Array(limit)
33
54
  .fill(null)
34
55
  .map(() => worker());
56
+ await Promise.race([Promise.all(workers), firstErrorPromise]);
57
+
58
+ // Check external abort
59
+ if (signal?.aborted) {
60
+ const reason = signal.reason instanceof Error ? signal.reason : new Error("Aborted");
61
+ throw reason;
62
+ }
35
63
 
36
- await Promise.all(workers);
37
64
  return results;
38
65
  }
@@ -58,12 +58,20 @@ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): s
58
58
  counts.set(finding.priority, (counts.get(finding.priority) ?? 0) + 1);
59
59
  }
60
60
 
61
+ const priorityMeta: Record<number, { icon: string; color: "error" | "warning" | "muted" | "accent" }> = {
62
+ 0: { icon: theme.styledSymbol("status.error", "error"), color: "error" },
63
+ 1: { icon: theme.styledSymbol("status.warning", "warning"), color: "warning" },
64
+ 2: { icon: theme.styledSymbol("status.warning", "muted"), color: "muted" },
65
+ 3: { icon: theme.styledSymbol("status.info", "accent"), color: "accent" },
66
+ };
67
+
61
68
  const parts: string[] = [];
62
69
  for (const priority of [0, 1, 2, 3]) {
63
70
  const label = PRIORITY_LABELS[priority] ?? "P?";
64
- const color = priority === 0 ? "error" : priority === 1 ? "warning" : "muted";
71
+ const meta = priorityMeta[priority] ?? { icon: "", color: "muted" as const };
65
72
  const count = counts.get(priority) ?? 0;
66
- parts.push(theme.fg(color, `${label}:${count}`));
73
+ const text = theme.fg(meta.color, `${label}:${count}`);
74
+ parts.push(meta.icon ? `${meta.icon} ${text}` : text);
67
75
  }
68
76
 
69
77
  return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
@@ -123,13 +131,19 @@ function renderJsonTreeLines(
123
131
  pushLine(`${prefix}${iconArray} ${header}`);
124
132
  if (val.length === 0) {
125
133
  pushLine(
126
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", "[]")}`,
134
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
135
+ "dim",
136
+ "[]",
137
+ )}`,
127
138
  );
128
139
  return;
129
140
  }
130
141
  if (depth >= maxDepth) {
131
142
  pushLine(
132
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", theme.format.ellipsis)}`,
143
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
144
+ "dim",
145
+ theme.format.ellipsis,
146
+ )}`,
133
147
  );
134
148
  return;
135
149
  }
@@ -150,13 +164,19 @@ function renderJsonTreeLines(
150
164
  const entries = Object.entries(val as Record<string, unknown>);
151
165
  if (entries.length === 0) {
152
166
  pushLine(
153
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", "{}")}`,
167
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
168
+ "dim",
169
+ "{}",
170
+ )}`,
154
171
  );
155
172
  return;
156
173
  }
157
174
  if (depth >= maxDepth) {
158
175
  pushLine(
159
- `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", theme.format.ellipsis)}`,
176
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg(
177
+ "dim",
178
+ theme.format.ellipsis,
179
+ )}`,
160
180
  );
161
181
  return;
162
182
  }
@@ -233,19 +253,25 @@ function renderOutputSection(
233
253
  */
234
254
  export function renderCall(args: TaskParams, theme: Theme): Component {
235
255
  const label = theme.fg("toolTitle", theme.bold("Task"));
256
+ const agentTag = theme.italic(
257
+ theme.fg("dim", `${theme.format.bracketLeft}${args.agent}${theme.format.bracketRight}`),
258
+ );
236
259
 
237
260
  if (args.tasks.length === 1) {
238
- // Single task - show agent and task preview
261
+ // Single task - show description preview
239
262
  const task = args.tasks[0];
240
- const summary = task.description?.trim() || task.task;
241
- const taskPreview = truncate(summary, 60, theme.format.ellipsis);
242
- return new Text(`${label} ${theme.fg("accent", task.agent)}: ${theme.fg("muted", taskPreview)}`, 0, 0);
263
+ const summary = task.description.trim() || task.task;
264
+ const taskPreview = truncate(summary, 50, theme.format.ellipsis);
265
+ return new Text(`${label} ${agentTag} ${theme.fg("muted", taskPreview)}`, 0, 0);
243
266
  }
244
267
 
245
- // Multiple tasks - show count and descriptions (or agent names as fallback)
246
- const agents = args.tasks.map((t) => t.description?.trim() || t.agent).join(", ");
268
+ // Multiple tasks - show count and descriptions
269
+ const descriptions = args.tasks.map((t) => t.description.trim()).join(", ");
247
270
  return new Text(
248
- `${label} ${theme.fg("muted", `${args.tasks.length} agents: ${truncate(agents, 50, theme.format.ellipsis)}`)}`,
271
+ `${label} ${agentTag} ${args.tasks.length} agents: ${theme.fg(
272
+ "muted",
273
+ truncate(descriptions, 50, theme.format.ellipsis),
274
+ )}`,
249
275
  0,
250
276
  0,
251
277
  );
@@ -275,23 +301,14 @@ function renderAgentProgress(
275
301
  ? "error"
276
302
  : "accent";
277
303
 
278
- // Main status line - use taskId for Output tool
279
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", progress.taskId)}`;
304
+ // Main status line: taskId: description [status] · stats · ⟨agent⟩
280
305
  const description = progress.description?.trim();
281
- if (description) {
282
- statusLine += ` ${theme.fg("muted", truncate(description, 40, theme.format.ellipsis))}`;
283
- }
306
+ const titlePart = description ? `${theme.bold(progress.taskId)}: ${description}` : progress.taskId;
307
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
284
308
 
285
309
  // Only show badge for non-running states (spinner already indicates running)
286
- if (progress.status !== "running") {
287
- const statusLabel =
288
- progress.status === "completed"
289
- ? "done"
290
- : progress.status === "failed"
291
- ? "failed"
292
- : progress.status === "aborted"
293
- ? "aborted"
294
- : "pending";
310
+ if (progress.status === "failed" || progress.status === "aborted") {
311
+ const statusLabel = progress.status === "failed" ? "failed" : "aborted";
295
312
  statusLine += ` ${formatBadge(statusLabel, iconColor, theme)}`;
296
313
  }
297
314
 
@@ -338,6 +355,21 @@ function renderAgentProgress(
338
355
 
339
356
  // Render extracted tool data inline (e.g., review findings)
340
357
  if (progress.extractedToolData) {
358
+ // For completed tasks, check for review verdict from complete tool
359
+ if (progress.status === "completed") {
360
+ const completeData = progress.extractedToolData.complete as Array<{ data: unknown }> | undefined;
361
+ const reportFindingData = progress.extractedToolData.report_finding as ReportFindingDetails[] | undefined;
362
+ const reviewData = completeData
363
+ ?.map((c) => c.data as SubmitReviewDetails)
364
+ .filter((d) => d && typeof d === "object" && "overall_correctness" in d);
365
+ if (reviewData && reviewData.length > 0) {
366
+ const summary = reviewData[reviewData.length - 1];
367
+ const findings = reportFindingData ?? [];
368
+ lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
369
+ return lines; // Review result handles its own rendering
370
+ }
371
+ }
372
+
341
373
  for (const [toolName, dataArray] of Object.entries(progress.extractedToolData)) {
342
374
  const handler = subprocessToolRegistry.getHandler(toolName);
343
375
  if (handler?.renderInline) {
@@ -381,7 +413,10 @@ function renderReviewResult(
381
413
  const verdictColor = summary.overall_correctness === "correct" ? "success" : "error";
382
414
  const verdictIcon = summary.overall_correctness === "correct" ? theme.status.success : theme.status.error;
383
415
  lines.push(
384
- `${continuePrefix}${theme.fg(verdictColor, verdictIcon)} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${theme.fg("dim", `(${(summary.confidence * 100).toFixed(0)}% confidence)`)}`,
416
+ `${continuePrefix} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${theme.fg(
417
+ verdictColor,
418
+ verdictIcon,
419
+ )} ${theme.fg("dim", `(${(summary.confidence * 100).toFixed(0)}% confidence)`)}`,
385
420
  );
386
421
 
387
422
  // Explanation preview (first ~80 chars when collapsed, full when expanded)
@@ -395,7 +430,7 @@ function renderReviewResult(
395
430
  } else {
396
431
  // Preview: first sentence or ~100 chars
397
432
  const preview = truncate(`${summary.explanation.split(/[.!?]/)[0]}.`, 100, theme.format.ellipsis);
398
- lines.push(`${continuePrefix}${theme.fg("dim", `Summary: ${preview}`)}`);
433
+ lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
399
434
  }
400
435
  }
401
436
 
@@ -411,7 +446,7 @@ function renderReviewResult(
411
446
  }
412
447
 
413
448
  /**
414
- * Render review findings list (used with and without submit_review).
449
+ * Render review findings list.
415
450
  */
416
451
  function renderFindings(
417
452
  findings: ReportFindingDetails[],
@@ -472,12 +507,14 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
472
507
  const iconColor = success ? "success" : "error";
473
508
  const statusText = aborted ? "aborted" : success ? "done" : "failed";
474
509
 
475
- // Main status line - use taskId for Output tool
476
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", result.taskId)} ${formatBadge(statusText, iconColor, theme)}`;
510
+ // Main status line: taskId: description [status] · stats · ⟨agent⟩
477
511
  const description = result.description?.trim();
478
- if (description) {
479
- statusLine += ` ${theme.fg("muted", truncate(description, 40, theme.format.ellipsis))}`;
480
- }
512
+ const titlePart = description ? `${theme.bold(result.taskId)}: ${description}` : result.taskId;
513
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)} ${formatBadge(
514
+ statusText,
515
+ iconColor,
516
+ theme,
517
+ )}`;
481
518
  if (result.tokens > 0) {
482
519
  statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatTokens(result.tokens)} tokens`)}`;
483
520
  }
@@ -489,10 +526,16 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
489
526
 
490
527
  lines.push(statusLine);
491
528
 
492
- // Check for review result (submit_review + report_finding)
493
- const submitReviewData = result.extractedToolData?.submit_review as SubmitReviewDetails[] | undefined;
529
+ // Check for review result (complete with review schema + report_finding)
530
+ const completeData = result.extractedToolData?.complete as Array<{ data: unknown }> | undefined;
494
531
  const reportFindingData = result.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
495
532
 
533
+ // Extract review verdict from complete tool's data field if it matches SubmitReviewDetails
534
+ const reviewData = completeData
535
+ ?.map((c) => c.data as SubmitReviewDetails)
536
+ .filter((d) => d && typeof d === "object" && "overall_correctness" in d);
537
+ const submitReviewData = reviewData && reviewData.length > 0 ? reviewData : undefined;
538
+
496
539
  if (submitReviewData && submitReviewData.length > 0) {
497
540
  // Use combined review renderer
498
541
  const summary = submitReviewData[submitReviewData.length - 1];
@@ -502,7 +545,10 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
502
545
  }
503
546
  if (reportFindingData && reportFindingData.length > 0) {
504
547
  lines.push(
505
- `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", "Review summary missing (submit_review not called)")}`,
548
+ `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
549
+ "dim",
550
+ "Review summary missing (complete not called)",
551
+ )}`,
506
552
  );
507
553
  lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
508
554
  lines.push(`${continuePrefix}`); // Spacing
@@ -515,7 +561,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
515
561
  if (result.extractedToolData) {
516
562
  for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
517
563
  // Skip review tools - handled above
518
- if (toolName === "submit_review" || toolName === "report_finding") continue;
564
+ if (toolName === "complete" || toolName === "report_finding") continue;
519
565
 
520
566
  const handler = subprocessToolRegistry.getHandler(toolName);
521
567
  if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
@@ -41,20 +41,29 @@ export const TASK_SUBAGENT_PROGRESS_CHANNEL = "task:subagent:progress";
41
41
 
42
42
  /** Single task item for parallel execution */
43
43
  export const taskItemSchema = Type.Object({
44
- agent: Type.String({ description: "Agent name" }),
44
+ id: Type.String({
45
+ description: "Short task identifier for display (max 32 chars, CamelCase, e.g. 'SessionStore', 'WebFetchFix')",
46
+ maxLength: 32,
47
+ pattern: "^[A-Za-z][A-Za-z0-9]*$",
48
+ }),
45
49
  task: Type.String({ description: "Task description for the agent" }),
46
- description: Type.Optional(Type.String({ description: "Short description for UI display" })),
47
- model: Type.Optional(Type.String({ description: "Model override for this task" })),
50
+ description: Type.String({ description: "Short description for UI display" }),
48
51
  });
49
52
 
50
53
  export type TaskItem = Static<typeof taskItemSchema>;
51
54
 
52
55
  /** Task tool parameters */
53
56
  export const taskSchema = Type.Object({
54
- context: Type.Optional(Type.String({ description: "Shared context prepended to all task prompts" })),
55
- output_schema: Type.Optional(
57
+ agent: Type.String({ description: "Agent type to use for all tasks" }),
58
+ context: Type.String({ description: "Shared context prepended to all task prompts" }),
59
+ model: Type.Optional(
60
+ Type.String({
61
+ description: "Model override for all tasks (fuzzy matching, e.g. 'sonnet', 'opus')",
62
+ }),
63
+ ),
64
+ output: Type.Optional(
56
65
  Type.Any({
57
- description: "JSON schema for structured subagent output (used by the complete tool)",
66
+ description: "JTD schema for structured subagent output (used by the complete tool)",
58
67
  }),
59
68
  ),
60
69
  tasks: Type.Array(taskItemSchema, {