pi-subagents 0.3.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/install.mjs ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * pi-subagents installer
5
+ *
6
+ * Usage:
7
+ * npx pi-subagents # Install to ~/.pi/agent/extensions/subagent
8
+ * npx pi-subagents --remove # Remove the extension
9
+ */
10
+
11
+ import { execSync } from "node:child_process";
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import * as os from "node:os";
15
+
16
+ const EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
17
+ const REPO_URL = "https://github.com/nicobailon/pi-subagents.git";
18
+
19
+ const args = process.argv.slice(2);
20
+ const isRemove = args.includes("--remove") || args.includes("-r");
21
+ const isHelp = args.includes("--help") || args.includes("-h");
22
+
23
+ if (isHelp) {
24
+ console.log(`
25
+ pi-subagents - Pi extension for delegating tasks to subagents
26
+
27
+ Usage:
28
+ npx pi-subagents Install the extension
29
+ npx pi-subagents --remove Remove the extension
30
+ npx pi-subagents --help Show this help
31
+
32
+ Installation directory: ${EXTENSION_DIR}
33
+ `);
34
+ process.exit(0);
35
+ }
36
+
37
+ if (isRemove) {
38
+ if (fs.existsSync(EXTENSION_DIR)) {
39
+ console.log(`Removing ${EXTENSION_DIR}...`);
40
+ fs.rmSync(EXTENSION_DIR, { recursive: true });
41
+ console.log("✓ pi-subagents removed");
42
+ } else {
43
+ console.log("pi-subagents is not installed");
44
+ }
45
+ process.exit(0);
46
+ }
47
+
48
+ // Install
49
+ console.log("Installing pi-subagents...\n");
50
+
51
+ // Ensure parent directory exists
52
+ const parentDir = path.dirname(EXTENSION_DIR);
53
+ if (!fs.existsSync(parentDir)) {
54
+ fs.mkdirSync(parentDir, { recursive: true });
55
+ }
56
+
57
+ // Check if already installed
58
+ if (fs.existsSync(EXTENSION_DIR)) {
59
+ const isGitRepo = fs.existsSync(path.join(EXTENSION_DIR, ".git"));
60
+ if (isGitRepo) {
61
+ console.log("Updating existing installation...");
62
+ try {
63
+ execSync("git pull", { cwd: EXTENSION_DIR, stdio: "inherit" });
64
+ console.log("\n✓ pi-subagents updated");
65
+ } catch (err) {
66
+ console.error("Failed to update. Try removing and reinstalling:");
67
+ console.error(" npx pi-subagents --remove && npx pi-subagents");
68
+ process.exit(1);
69
+ }
70
+ } else {
71
+ console.log(`Directory exists but is not a git repo: ${EXTENSION_DIR}`);
72
+ console.log("Remove it first with: npx pi-subagents --remove");
73
+ process.exit(1);
74
+ }
75
+ } else {
76
+ // Fresh install
77
+ console.log(`Cloning to ${EXTENSION_DIR}...`);
78
+ try {
79
+ execSync(`git clone ${REPO_URL} "${EXTENSION_DIR}"`, { stdio: "inherit" });
80
+ console.log("\n✓ pi-subagents installed");
81
+ } catch (err) {
82
+ console.error("Failed to clone repository");
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ console.log(`
88
+ The extension is now available in pi. Tools added:
89
+ • subagent - Delegate tasks to agents (single, chain, parallel)
90
+ • subagent_status - Check async run status
91
+
92
+ Documentation: ${EXTENSION_DIR}/README.md
93
+ `);
package/notify.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Subagent completion notifications (extension)
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+
7
+ interface ChainStepResult {
8
+ agent: string;
9
+ output: string;
10
+ success: boolean;
11
+ }
12
+
13
+ interface SubagentResult {
14
+ id: string | null;
15
+ agent: string | null;
16
+ success: boolean;
17
+ summary: string;
18
+ exitCode: number;
19
+ timestamp: number;
20
+ sessionFile?: string;
21
+ shareUrl?: string;
22
+ gistUrl?: string;
23
+ shareError?: string;
24
+ results?: ChainStepResult[];
25
+ taskIndex?: number;
26
+ totalTasks?: number;
27
+ }
28
+
29
+ export default function registerSubagentNotify(pi: ExtensionAPI): void {
30
+ const seen = new Map<string, number>();
31
+ const ttlMs = 10 * 60 * 1000;
32
+
33
+ const prune = (now: number) => {
34
+ for (const [key, ts] of seen.entries()) {
35
+ if (now - ts > ttlMs) seen.delete(key);
36
+ }
37
+ };
38
+
39
+ const handleComplete = (data: unknown) => {
40
+ const result = data as SubagentResult;
41
+ const now = Date.now();
42
+ const key = `${result.id ?? "no-id"}:${result.agent ?? "unknown"}:${result.timestamp ?? now}`;
43
+ prune(now);
44
+ if (seen.has(key)) return;
45
+ seen.set(key, now);
46
+
47
+ const agent = result.agent ?? "unknown";
48
+ const status = result.success ? "completed" : "failed";
49
+
50
+ const taskInfo =
51
+ result.taskIndex !== undefined && result.totalTasks !== undefined
52
+ ? ` (${result.taskIndex + 1}/${result.totalTasks})`
53
+ : "";
54
+
55
+ const extra: string[] = [];
56
+ if (result.shareUrl) {
57
+ extra.push(`Session: ${result.shareUrl}`);
58
+ } else if (result.shareError) {
59
+ extra.push(`Session share error: ${result.shareError}`);
60
+ } else if (result.sessionFile) {
61
+ extra.push(`Session file: ${result.sessionFile}`);
62
+ }
63
+
64
+ const content = [
65
+ `Background task ${status}: **${agent}**${taskInfo}`,
66
+ "",
67
+ result.summary,
68
+ extra.length ? "" : undefined,
69
+ extra.length ? extra.join("\n") : undefined,
70
+ ]
71
+ .filter((line) => line !== undefined)
72
+ .join("\n");
73
+
74
+ pi.sendMessage(
75
+ {
76
+ customType: "subagent-notify",
77
+ content,
78
+ display: true,
79
+ },
80
+ { triggerTurn: true },
81
+ );
82
+ };
83
+
84
+ pi.events.on("subagent:complete", handleComplete);
85
+ pi.events.on("subagent_enhanced:complete", handleComplete);
86
+ pi.events.on("async_subagent:complete", handleComplete);
87
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "pi-subagents",
3
+ "version": "0.3.0",
4
+ "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
+ "author": "Nico Bailon",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/nicobailon/pi-subagents.git"
10
+ },
11
+ "homepage": "https://github.com/nicobailon/pi-subagents#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/nicobailon/pi-subagents/issues"
14
+ },
15
+ "keywords": [
16
+ "pi",
17
+ "pi-coding-agent",
18
+ "subagents",
19
+ "ai",
20
+ "agents",
21
+ "cli"
22
+ ],
23
+ "bin": {
24
+ "pi-subagents": "./install.mjs"
25
+ },
26
+ "files": [
27
+ "*.ts",
28
+ "*.mjs",
29
+ "README.md",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "pi": {
33
+ "extensions": [
34
+ "./index.ts",
35
+ "./notify.ts"
36
+ ]
37
+ }
38
+ }
package/settings.ts ADDED
@@ -0,0 +1,492 @@
1
+ /**
2
+ * Subagent settings, chain behavior, and template management
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import type { AgentConfig } from "./agents.js";
9
+
10
+ const SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
11
+ const CHAIN_RUNS_DIR = "/tmp/pi-chain-runs";
12
+ const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
13
+
14
+ // =============================================================================
15
+ // Settings Types
16
+ // =============================================================================
17
+
18
+ export interface ChainTemplates {
19
+ [chainKey: string]: {
20
+ [agentName: string]: string;
21
+ };
22
+ }
23
+
24
+ export interface SubagentSettings {
25
+ chains?: ChainTemplates;
26
+ }
27
+
28
+ // =============================================================================
29
+ // Behavior Resolution Types
30
+ // =============================================================================
31
+
32
+ export interface ResolvedStepBehavior {
33
+ output: string | false;
34
+ reads: string[] | false;
35
+ progress: boolean;
36
+ }
37
+
38
+ export interface StepOverrides {
39
+ output?: string | false;
40
+ reads?: string[] | false;
41
+ progress?: boolean;
42
+ }
43
+
44
+ // =============================================================================
45
+ // Chain Step Types
46
+ // =============================================================================
47
+
48
+ /** Sequential step: single agent execution */
49
+ export interface SequentialStep {
50
+ agent: string;
51
+ task?: string;
52
+ cwd?: string;
53
+ output?: string | false;
54
+ reads?: string[] | false;
55
+ progress?: boolean;
56
+ }
57
+
58
+ /** Parallel task item within a parallel step */
59
+ export interface ParallelTaskItem {
60
+ agent: string;
61
+ task?: string;
62
+ cwd?: string;
63
+ output?: string | false;
64
+ reads?: string[] | false;
65
+ progress?: boolean;
66
+ }
67
+
68
+ /** Parallel step: multiple agents running concurrently */
69
+ export interface ParallelStep {
70
+ parallel: ParallelTaskItem[];
71
+ concurrency?: number;
72
+ failFast?: boolean;
73
+ }
74
+
75
+ /** Union type for chain steps */
76
+ export type ChainStep = SequentialStep | ParallelStep;
77
+
78
+ // =============================================================================
79
+ // Type Guards
80
+ // =============================================================================
81
+
82
+ export function isParallelStep(step: ChainStep): step is ParallelStep {
83
+ return "parallel" in step && Array.isArray((step as ParallelStep).parallel);
84
+ }
85
+
86
+ export function isSequentialStep(step: ChainStep): step is SequentialStep {
87
+ return "agent" in step && !("parallel" in step);
88
+ }
89
+
90
+ /** Get all agent names in a step (single for sequential, multiple for parallel) */
91
+ export function getStepAgents(step: ChainStep): string[] {
92
+ if (isParallelStep(step)) {
93
+ return step.parallel.map((t) => t.agent);
94
+ }
95
+ return [step.agent];
96
+ }
97
+
98
+ /** Get total task count in a step */
99
+ export function getStepTaskCount(step: ChainStep): number {
100
+ if (isParallelStep(step)) {
101
+ return step.parallel.length;
102
+ }
103
+ return 1;
104
+ }
105
+
106
+ // =============================================================================
107
+ // Settings Management
108
+ // =============================================================================
109
+
110
+ export function loadSubagentSettings(): SubagentSettings {
111
+ try {
112
+ const data = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
113
+ return (data.subagent as SubagentSettings) ?? {};
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+
119
+ export function saveChainTemplate(chainKey: string, templates: Record<string, string>): void {
120
+ let settings: Record<string, unknown> = {};
121
+ try {
122
+ settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
123
+ } catch {}
124
+
125
+ if (!settings.subagent) settings.subagent = {};
126
+ const subagent = settings.subagent as Record<string, unknown>;
127
+ if (!subagent.chains) subagent.chains = {};
128
+ const chains = subagent.chains as Record<string, unknown>;
129
+
130
+ chains[chainKey] = templates;
131
+
132
+ const dir = path.dirname(SETTINGS_PATH);
133
+ if (!fs.existsSync(dir)) {
134
+ fs.mkdirSync(dir, { recursive: true });
135
+ }
136
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
137
+ }
138
+
139
+ export function getChainKey(agents: string[]): string {
140
+ return agents.join("->");
141
+ }
142
+
143
+ // =============================================================================
144
+ // Chain Directory Management
145
+ // =============================================================================
146
+
147
+ export function createChainDir(runId: string): string {
148
+ const chainDir = path.join(CHAIN_RUNS_DIR, runId);
149
+ fs.mkdirSync(chainDir, { recursive: true });
150
+ return chainDir;
151
+ }
152
+
153
+ export function removeChainDir(chainDir: string): void {
154
+ try {
155
+ fs.rmSync(chainDir, { recursive: true });
156
+ } catch {}
157
+ }
158
+
159
+ export function cleanupOldChainDirs(): void {
160
+ if (!fs.existsSync(CHAIN_RUNS_DIR)) return;
161
+ const now = Date.now();
162
+ let dirs: string[];
163
+ try {
164
+ dirs = fs.readdirSync(CHAIN_RUNS_DIR);
165
+ } catch {
166
+ return;
167
+ }
168
+
169
+ for (const dir of dirs) {
170
+ try {
171
+ const dirPath = path.join(CHAIN_RUNS_DIR, dir);
172
+ const stat = fs.statSync(dirPath);
173
+ if (stat.isDirectory() && now - stat.mtimeMs > CHAIN_DIR_MAX_AGE_MS) {
174
+ fs.rmSync(dirPath, { recursive: true });
175
+ }
176
+ } catch {
177
+ // Skip directories that can't be processed; continue with others
178
+ }
179
+ }
180
+ }
181
+
182
+ // =============================================================================
183
+ // Template Resolution
184
+ // =============================================================================
185
+
186
+ /**
187
+ * Resolve templates for each step in a chain.
188
+ * Priority: inline task > saved template > default
189
+ * Default for step 0: "{task}", for others: "{previous}"
190
+ */
191
+ export function resolveChainTemplates(
192
+ agentNames: string[],
193
+ inlineTasks: (string | undefined)[],
194
+ settings: SubagentSettings,
195
+ ): string[] {
196
+ const chainKey = getChainKey(agentNames);
197
+ const savedTemplates = settings.chains?.[chainKey] ?? {};
198
+
199
+ return agentNames.map((agent, i) => {
200
+ // Priority: inline > saved > default
201
+ const inline = inlineTasks[i];
202
+ if (inline) return inline;
203
+
204
+ const saved = savedTemplates[agent];
205
+ if (saved) return saved;
206
+
207
+ // Default: first step uses {task}, others use {previous}
208
+ return i === 0 ? "{task}" : "{previous}";
209
+ });
210
+ }
211
+
212
+ // =============================================================================
213
+ // Parallel-Aware Template Resolution
214
+ // =============================================================================
215
+
216
+ /** Resolved templates for a chain - string for sequential, string[] for parallel */
217
+ export type ResolvedTemplates = (string | string[])[];
218
+
219
+ /**
220
+ * Resolve templates for a chain with parallel step support.
221
+ * Returns string for sequential steps, string[] for parallel steps.
222
+ */
223
+ export function resolveChainTemplatesV2(
224
+ steps: ChainStep[],
225
+ settings: SubagentSettings,
226
+ ): ResolvedTemplates {
227
+ return steps.map((step, i) => {
228
+ if (isParallelStep(step)) {
229
+ // Parallel step: resolve each task's template
230
+ return step.parallel.map((task) => {
231
+ if (task.task) return task.task;
232
+ // Default for parallel tasks is {previous}
233
+ return "{previous}";
234
+ });
235
+ }
236
+ // Sequential step: existing logic
237
+ const seq = step as SequentialStep;
238
+ if (seq.task) return seq.task;
239
+ // Default: first step uses {task}, others use {previous}
240
+ return i === 0 ? "{task}" : "{previous}";
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Flatten templates for display (TUI navigation needs flat list)
246
+ */
247
+ export function flattenTemplates(templates: ResolvedTemplates): string[] {
248
+ const result: string[] = [];
249
+ for (const t of templates) {
250
+ if (Array.isArray(t)) {
251
+ result.push(...t);
252
+ } else {
253
+ result.push(t);
254
+ }
255
+ }
256
+ return result;
257
+ }
258
+
259
+ /**
260
+ * Unflatten templates back to structured form
261
+ */
262
+ export function unflattenTemplates(
263
+ flat: string[],
264
+ steps: ChainStep[],
265
+ ): ResolvedTemplates {
266
+ const result: ResolvedTemplates = [];
267
+ let idx = 0;
268
+ for (const step of steps) {
269
+ if (isParallelStep(step)) {
270
+ const count = step.parallel.length;
271
+ result.push(flat.slice(idx, idx + count));
272
+ idx += count;
273
+ } else {
274
+ result.push(flat[idx]!);
275
+ idx++;
276
+ }
277
+ }
278
+ return result;
279
+ }
280
+
281
+ // =============================================================================
282
+ // Behavior Resolution
283
+ // =============================================================================
284
+
285
+ /**
286
+ * Resolve effective chain behavior per step.
287
+ * Priority: step override > agent frontmatter > false (disabled)
288
+ */
289
+ export function resolveStepBehavior(
290
+ agentConfig: AgentConfig,
291
+ stepOverrides: StepOverrides,
292
+ ): ResolvedStepBehavior {
293
+ // Output: step override > frontmatter > false (no output)
294
+ const output =
295
+ stepOverrides.output !== undefined
296
+ ? stepOverrides.output
297
+ : agentConfig.output ?? false;
298
+
299
+ // Reads: step override > frontmatter defaultReads > false (no reads)
300
+ const reads =
301
+ stepOverrides.reads !== undefined
302
+ ? stepOverrides.reads
303
+ : agentConfig.defaultReads ?? false;
304
+
305
+ // Progress: step override > frontmatter defaultProgress > false
306
+ const progress =
307
+ stepOverrides.progress !== undefined
308
+ ? stepOverrides.progress
309
+ : agentConfig.defaultProgress ?? false;
310
+
311
+ return { output, reads, progress };
312
+ }
313
+
314
+ /**
315
+ * Find index of first agent in chain that has progress enabled
316
+ */
317
+ export function findFirstProgressAgentIndex(
318
+ agentConfigs: AgentConfig[],
319
+ stepOverrides: StepOverrides[],
320
+ ): number {
321
+ return agentConfigs.findIndex((config, i) => {
322
+ const override = stepOverrides[i];
323
+ if (override?.progress !== undefined) return override.progress;
324
+ return config.defaultProgress ?? false;
325
+ });
326
+ }
327
+
328
+ // =============================================================================
329
+ // Chain Instruction Injection
330
+ // =============================================================================
331
+
332
+ /**
333
+ * Resolve a file path: absolute paths pass through, relative paths get chainDir prepended.
334
+ */
335
+ function resolveChainPath(filePath: string, chainDir: string): string {
336
+ return path.isAbsolute(filePath) ? filePath : `${chainDir}/${filePath}`;
337
+ }
338
+
339
+ /**
340
+ * Build chain instructions from resolved behavior.
341
+ * These are appended to the task to tell the agent what to read/write.
342
+ */
343
+ export function buildChainInstructions(
344
+ behavior: ResolvedStepBehavior,
345
+ chainDir: string,
346
+ isFirstProgressAgent: boolean,
347
+ previousSummary?: string,
348
+ ): string {
349
+ const instructions: string[] = [];
350
+
351
+ // Include previous step's summary if available (prose output from prior agent)
352
+ if (previousSummary && previousSummary.trim()) {
353
+ instructions.push(`Previous step summary:\n\n${previousSummary.trim()}`);
354
+ }
355
+
356
+ // Reads (supports both absolute and relative paths)
357
+ if (behavior.reads && behavior.reads.length > 0) {
358
+ const files = behavior.reads.map((f) => resolveChainPath(f, chainDir)).join(", ");
359
+ instructions.push(`Read these files: ${files}`);
360
+ }
361
+
362
+ // Output (supports both absolute and relative paths)
363
+ if (behavior.output) {
364
+ const outputPath = resolveChainPath(behavior.output, chainDir);
365
+ instructions.push(`Write your output to: ${outputPath}`);
366
+ }
367
+
368
+ // Progress
369
+ if (behavior.progress) {
370
+ const progressPath = `${chainDir}/progress.md`;
371
+ if (isFirstProgressAgent) {
372
+ instructions.push(`Create and maintain: ${progressPath}`);
373
+ instructions.push("Format: Status, Tasks (checkboxes), Files Changed, Notes");
374
+ } else {
375
+ instructions.push(`Read and update: ${progressPath}`);
376
+ }
377
+ }
378
+
379
+ if (instructions.length === 0) return "";
380
+
381
+ return (
382
+ "\n\n---\n**Chain Instructions:**\n" + instructions.map((i) => `- ${i}`).join("\n")
383
+ );
384
+ }
385
+
386
+ // =============================================================================
387
+ // Parallel Step Support
388
+ // =============================================================================
389
+
390
+ /**
391
+ * Resolve behaviors for all tasks in a parallel step.
392
+ * Creates namespaced output paths to avoid collisions.
393
+ */
394
+ export function resolveParallelBehaviors(
395
+ tasks: ParallelTaskItem[],
396
+ agentConfigs: AgentConfig[],
397
+ stepIndex: number,
398
+ ): ResolvedStepBehavior[] {
399
+ return tasks.map((task, taskIndex) => {
400
+ const config = agentConfigs.find((a) => a.name === task.agent);
401
+ if (!config) {
402
+ throw new Error(`Unknown agent: ${task.agent}`);
403
+ }
404
+
405
+ // Build subdirectory path for this parallel task
406
+ const subdir = `parallel-${stepIndex}/${taskIndex}-${task.agent}`;
407
+
408
+ // Output: task override > agent default (namespaced) > false
409
+ // Absolute paths pass through unchanged; relative paths get namespaced under subdir
410
+ let output: string | false = false;
411
+ if (task.output !== undefined) {
412
+ if (task.output === false) {
413
+ output = false;
414
+ } else if (path.isAbsolute(task.output)) {
415
+ output = task.output; // Absolute path: use as-is
416
+ } else {
417
+ output = `${subdir}/${task.output}`; // Relative: namespace under subdir
418
+ }
419
+ } else if (config.output) {
420
+ // Agent defaults are always relative, so namespace them
421
+ output = `${subdir}/${config.output}`;
422
+ }
423
+
424
+ // Reads: task override > agent default > false
425
+ const reads =
426
+ task.reads !== undefined ? task.reads : config.defaultReads ?? false;
427
+
428
+ // Progress: task override > agent default > false
429
+ const progress =
430
+ task.progress !== undefined
431
+ ? task.progress
432
+ : config.defaultProgress ?? false;
433
+
434
+ return { output, reads, progress };
435
+ });
436
+ }
437
+
438
+ /**
439
+ * Create subdirectories for parallel step outputs
440
+ */
441
+ export function createParallelDirs(
442
+ chainDir: string,
443
+ stepIndex: number,
444
+ taskCount: number,
445
+ agentNames: string[],
446
+ ): void {
447
+ for (let i = 0; i < taskCount; i++) {
448
+ const subdir = path.join(chainDir, `parallel-${stepIndex}`, `${i}-${agentNames[i]}`);
449
+ fs.mkdirSync(subdir, { recursive: true });
450
+ }
451
+ }
452
+
453
+ /** Result from a parallel task (simplified for aggregation) */
454
+ export interface ParallelTaskResult {
455
+ agent: string;
456
+ taskIndex: number;
457
+ output: string;
458
+ exitCode: number;
459
+ error?: string;
460
+ }
461
+
462
+ /**
463
+ * Aggregate outputs from parallel tasks into a single string for {previous}.
464
+ * Uses clear separators so the next agent can parse all outputs.
465
+ */
466
+ export function aggregateParallelOutputs(results: ParallelTaskResult[]): string {
467
+ return results
468
+ .map((r, i) => {
469
+ const header = `=== Parallel Task ${i + 1} (${r.agent}) ===`;
470
+ return `${header}\n${r.output}`;
471
+ })
472
+ .join("\n\n");
473
+ }
474
+
475
+ /**
476
+ * Check if any parallel task failed
477
+ */
478
+ export function hasParallelFailures(results: ParallelTaskResult[]): boolean {
479
+ return results.some((r) => r.exitCode !== 0);
480
+ }
481
+
482
+ /**
483
+ * Get failure summary for parallel step
484
+ */
485
+ export function getParallelFailureSummary(results: ParallelTaskResult[]): string {
486
+ const failures = results.filter((r) => r.exitCode !== 0);
487
+ if (failures.length === 0) return "";
488
+
489
+ return failures
490
+ .map((f) => `- Task ${f.taskIndex + 1} (${f.agent}): ${f.error || "failed"}`)
491
+ .join("\n");
492
+ }