@oh-my-pi/pi-coding-agent 3.30.0 → 3.31.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.
- package/CHANGELOG.md +71 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +6 -8
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +55 -32
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -38
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /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,34 @@ 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
|
|
139
|
-
|
|
137
|
+
const { agent: agentName, context, model, output: outputSchema } = params;
|
|
138
|
+
|
|
139
|
+
// Validate agent exists
|
|
140
|
+
const agent = getAgent(agents, agentName);
|
|
141
|
+
if (!agent) {
|
|
142
|
+
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: `Unknown agent "${agentName}". Available: ${available}`,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
details: {
|
|
151
|
+
projectAgentsDir,
|
|
152
|
+
results: [],
|
|
153
|
+
totalDurationMs: 0,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
140
157
|
|
|
141
158
|
// Handle empty or missing tasks
|
|
142
159
|
if (!params.tasks || params.tasks.length === 0) {
|
|
143
|
-
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
144
160
|
return {
|
|
145
161
|
content: [
|
|
146
162
|
{
|
|
147
163
|
type: "text",
|
|
148
|
-
text: `No tasks provided. Use: { tasks: [{
|
|
164
|
+
text: `No tasks provided. Use: { agent, context, tasks: [{id, task, description}, ...] }`,
|
|
149
165
|
},
|
|
150
166
|
],
|
|
151
167
|
details: {
|
|
@@ -173,6 +189,56 @@ export async function createTaskTool(
|
|
|
173
189
|
};
|
|
174
190
|
}
|
|
175
191
|
|
|
192
|
+
const tasks = params.tasks;
|
|
193
|
+
const missingTaskIndexes: number[] = [];
|
|
194
|
+
const idIndexes = new Map<string, number[]>();
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
197
|
+
const id = tasks[i]?.id;
|
|
198
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
199
|
+
missingTaskIndexes.push(i);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const normalizedId = id.toLowerCase();
|
|
203
|
+
const indexes = idIndexes.get(normalizedId);
|
|
204
|
+
if (indexes) {
|
|
205
|
+
indexes.push(i);
|
|
206
|
+
} else {
|
|
207
|
+
idIndexes.set(normalizedId, [i]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const duplicateIds: Array<{ id: string; indexes: number[] }> = [];
|
|
212
|
+
for (const [normalizedId, indexes] of idIndexes.entries()) {
|
|
213
|
+
if (indexes.length > 1) {
|
|
214
|
+
duplicateIds.push({
|
|
215
|
+
id: tasks[indexes[0]]?.id ?? normalizedId,
|
|
216
|
+
indexes,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (missingTaskIndexes.length > 0 || duplicateIds.length > 0) {
|
|
222
|
+
const problems: string[] = [];
|
|
223
|
+
if (missingTaskIndexes.length > 0) {
|
|
224
|
+
problems.push(`Missing task ids at indexes: ${missingTaskIndexes.join(", ")}`);
|
|
225
|
+
}
|
|
226
|
+
if (duplicateIds.length > 0) {
|
|
227
|
+
const details = duplicateIds
|
|
228
|
+
.map((entry) => `${entry.id} (indexes ${entry.indexes.join(", ")})`)
|
|
229
|
+
.join("; ");
|
|
230
|
+
problems.push(`Duplicate task ids detected (case-insensitive): ${details}`);
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: `Invalid tasks: ${problems.join(". ")}` }],
|
|
234
|
+
details: {
|
|
235
|
+
projectAgentsDir,
|
|
236
|
+
results: [],
|
|
237
|
+
totalDurationMs: 0,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
176
242
|
// Derive artifacts directory
|
|
177
243
|
const sessionFile = session.getSessionFile();
|
|
178
244
|
const artifactsDir = sessionFile ? getArtifactsDir(sessionFile) : null;
|
|
@@ -197,88 +263,59 @@ export async function createTaskTool(
|
|
|
197
263
|
};
|
|
198
264
|
|
|
199
265
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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,
|
|
266
|
+
// Check self-recursion prevention
|
|
267
|
+
if (blockedAgent && agentName === blockedAgent) {
|
|
268
|
+
return {
|
|
269
|
+
content: [
|
|
270
|
+
{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: `Cannot spawn ${blockedAgent} agent from within itself (recursion prevention). Use a different agent type.`,
|
|
221
273
|
},
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
}
|
|
274
|
+
],
|
|
275
|
+
details: {
|
|
276
|
+
projectAgentsDir,
|
|
277
|
+
results: [],
|
|
278
|
+
totalDurationMs: Date.now() - startTime,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
239
281
|
}
|
|
240
282
|
|
|
241
283
|
// Check spawn restrictions from parent
|
|
242
284
|
const parentSpawns = session.getSessionSpawns() ?? "*";
|
|
243
285
|
const allowedSpawns = parentSpawns.split(",").map((s) => s.trim());
|
|
244
|
-
const isSpawnAllowed = (
|
|
286
|
+
const isSpawnAllowed = (): boolean => {
|
|
245
287
|
if (parentSpawns === "") return false; // Empty = deny all
|
|
246
288
|
if (parentSpawns === "*") return true; // Wildcard = allow all
|
|
247
289
|
return allowedSpawns.includes(agentName);
|
|
248
290
|
};
|
|
249
291
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
};
|
|
261
|
-
}
|
|
292
|
+
if (!isSpawnAllowed()) {
|
|
293
|
+
const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
|
|
296
|
+
details: {
|
|
297
|
+
projectAgentsDir,
|
|
298
|
+
results: [],
|
|
299
|
+
totalDurationMs: Date.now() - startTime,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
262
302
|
}
|
|
263
303
|
|
|
264
|
-
// Build full prompts with context prepended
|
|
304
|
+
// Build full prompts with context prepended
|
|
265
305
|
const tasksWithContext = tasks.map((t) => ({
|
|
266
|
-
agent: t.agent,
|
|
267
306
|
task: context ? `${context}\n\n${t.task}` : t.task,
|
|
268
|
-
model: t.model,
|
|
269
307
|
description: t.description,
|
|
270
|
-
taskId:
|
|
308
|
+
taskId: t.id,
|
|
271
309
|
}));
|
|
272
310
|
|
|
273
311
|
// Initialize progress for all tasks
|
|
274
312
|
for (let i = 0; i < tasksWithContext.length; i++) {
|
|
275
313
|
const t = tasksWithContext[i];
|
|
276
|
-
const agentCfg = getAgent(agents, t.agent);
|
|
277
314
|
progressMap.set(i, {
|
|
278
315
|
index: i,
|
|
279
316
|
taskId: t.taskId,
|
|
280
|
-
agent:
|
|
281
|
-
agentSource:
|
|
317
|
+
agent: agentName,
|
|
318
|
+
agentSource: agent.source,
|
|
282
319
|
status: "pending",
|
|
283
320
|
task: t.task,
|
|
284
321
|
recentTools: [],
|
|
@@ -286,36 +323,40 @@ export async function createTaskTool(
|
|
|
286
323
|
toolCount: 0,
|
|
287
324
|
tokens: 0,
|
|
288
325
|
durationMs: 0,
|
|
289
|
-
modelOverride:
|
|
326
|
+
modelOverride: model,
|
|
290
327
|
description: t.description,
|
|
291
328
|
});
|
|
292
329
|
}
|
|
293
330
|
emitProgress();
|
|
294
331
|
|
|
295
332
|
// Execute in parallel with concurrency limit
|
|
296
|
-
const results = await mapWithConcurrencyLimit(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
333
|
+
const results = await mapWithConcurrencyLimit(
|
|
334
|
+
tasksWithContext,
|
|
335
|
+
MAX_CONCURRENCY,
|
|
336
|
+
async (task, index) => {
|
|
337
|
+
return runSubprocess({
|
|
338
|
+
cwd: session.cwd,
|
|
339
|
+
agent,
|
|
340
|
+
task: task.task,
|
|
341
|
+
description: task.description,
|
|
342
|
+
index,
|
|
343
|
+
taskId: task.taskId,
|
|
344
|
+
context: undefined, // Already prepended above
|
|
345
|
+
modelOverride: model,
|
|
346
|
+
outputSchema,
|
|
347
|
+
sessionFile,
|
|
348
|
+
persistArtifacts: !!artifactsDir,
|
|
349
|
+
artifactsDir: effectiveArtifactsDir,
|
|
350
|
+
signal,
|
|
351
|
+
eventBus: undefined,
|
|
352
|
+
onProgress: (progress) => {
|
|
353
|
+
progressMap.set(index, structuredClone(progress));
|
|
354
|
+
emitProgress();
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
signal,
|
|
359
|
+
);
|
|
319
360
|
|
|
320
361
|
// Aggregate usage from executor results (already accumulated incrementally)
|
|
321
362
|
const aggregatedUsage = createUsageTotals();
|
|
@@ -349,14 +390,10 @@ export async function createTaskTool(
|
|
|
349
390
|
return `[${r.agent}] ${status}${meta} ${r.taskId}\n${preview}`;
|
|
350
391
|
});
|
|
351
392
|
|
|
352
|
-
const skippedNote =
|
|
353
|
-
skippedSelfRecursion > 0
|
|
354
|
-
? ` (${skippedSelfRecursion} ${blockedAgent} task${skippedSelfRecursion > 1 ? "s" : ""} skipped - self-recursion blocked)`
|
|
355
|
-
: "";
|
|
356
393
|
const outputIds = results.map((r) => r.taskId);
|
|
357
394
|
const outputHint =
|
|
358
395
|
outputIds.length > 0 ? `\n\nUse output tool for full logs: output ids ${outputIds.join(", ")}` : "";
|
|
359
|
-
const summary = `${successCount}/${results.length} succeeded
|
|
396
|
+
const summary = `${successCount}/${results.length} succeeded [${formatDuration(
|
|
360
397
|
totalDuration,
|
|
361
398
|
)}]\n\n${summaries.join("\n\n---\n\n")}${outputHint}`;
|
|
362
399
|
|
|
@@ -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 (
|
|
38
|
+
while (true) {
|
|
39
|
+
workerSignal.throwIfAborted();
|
|
26
40
|
const index = nextIndex++;
|
|
27
|
-
|
|
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
|
|
71
|
+
const meta = priorityMeta[priority] ?? { icon: "", color: "muted" as const };
|
|
65
72
|
const count = counts.get(priority) ?? 0;
|
|
66
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
261
|
+
// Single task - show description preview
|
|
239
262
|
const task = args.tasks[0];
|
|
240
|
-
const summary = task.description
|
|
241
|
-
const taskPreview = truncate(summary,
|
|
242
|
-
return new Text(`${label} ${
|
|
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
|
|
246
|
-
const
|
|
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} ${
|
|
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
|
|
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
|
-
|
|
282
|
-
|
|
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
|
|
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}
|
|
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",
|
|
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
|
|
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
|
|
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
|
-
|
|
479
|
-
|
|
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 (
|
|
493
|
-
const
|
|
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(
|
|
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 === "
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
55
|
-
|
|
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: "
|
|
66
|
+
description: "JTD schema for structured subagent output (used by the complete tool)",
|
|
58
67
|
}),
|
|
59
68
|
),
|
|
60
69
|
tasks: Type.Array(taskItemSchema, {
|