@dreki-gg/pi-subagent 0.6.0 → 0.8.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 (40) hide show
  1. package/.fallowrc.json +6 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +82 -70
  4. package/docs/orchestration-principles.md +132 -0
  5. package/docs/plans/01_structured-handoffs-and-synthesis.plan.md +376 -0
  6. package/docs/plans/02_clean-context-review-loop.plan.md +240 -0
  7. package/docs/plans/03_parallel-recon-single-writer-docs.plan.md +199 -0
  8. package/docs/plans/04_manager-workflow.plan.md +248 -0
  9. package/docs/plans/05_advisor-routing.plan.md +244 -0
  10. package/extensions/subagent/agent-result-utils.ts +12 -0
  11. package/extensions/subagent/agent-runner-types.ts +23 -0
  12. package/extensions/subagent/agent-runner.ts +90 -0
  13. package/extensions/subagent/agents.ts +31 -77
  14. package/extensions/subagent/handoffs.ts +273 -0
  15. package/extensions/subagent/index.ts +359 -581
  16. package/extensions/subagent/run-agent-args.ts +90 -0
  17. package/extensions/subagent/{delegate-executor.ts → spawn-utils.ts} +64 -87
  18. package/extensions/subagent/synthesis.ts +1 -51
  19. package/package.json +7 -10
  20. package/prompts/advisor.md +37 -0
  21. package/prompts/bug-prover.md +42 -0
  22. package/{agents → prompts}/docs-scout.md +2 -2
  23. package/prompts/manager.md +52 -0
  24. package/{agents → prompts}/planner.md +16 -1
  25. package/prompts/reviewer.md +93 -0
  26. package/{agents → prompts}/scout.md +5 -1
  27. package/prompts/validator.md +42 -0
  28. package/prompts/worker.md +61 -0
  29. package/skills/spawn-subagents/SKILL.md +80 -8
  30. package/skills/write-an-agent/SKILL.md +5 -5
  31. package/agents/reviewer.md +0 -37
  32. package/agents/worker.md +0 -26
  33. package/extensions/subagent/delegate-args.ts +0 -145
  34. package/extensions/subagent/delegate-types.ts +0 -62
  35. package/extensions/subagent/delegate-ui.ts +0 -132
  36. package/extensions/subagent/workflows.ts +0 -150
  37. package/prompts/implement-and-review.md +0 -10
  38. package/prompts/implement.md +0 -10
  39. package/prompts/scout-and-plan.md +0 -9
  40. /package/{agents → prompts}/ux-designer.md +0 -0
@@ -12,61 +12,36 @@
12
12
  * Uses JSON mode to capture structured output from subagents.
13
13
  */
14
14
 
15
- import { spawn } from 'node:child_process';
16
- import { existsSync } from 'node:fs';
17
- import * as fs from 'node:fs';
18
- import { copyFile, mkdir, readdir, unlink } from 'node:fs/promises';
19
15
  import * as os from 'node:os';
20
- import { join } from 'node:path';
21
- import * as path from 'node:path';
22
- import type { AgentToolResult } from '@mariozechner/pi-agent-core';
23
- import type { Message } from '@mariozechner/pi-ai';
24
- import { StringEnum } from '@mariozechner/pi-ai';
16
+ import type { AgentToolResult } from '@earendil-works/pi-agent-core';
17
+ import type { Message } from '@earendil-works/pi-ai';
18
+ import { StringEnum } from '@earendil-works/pi-ai';
25
19
  import {
26
20
  type ExtensionAPI,
27
21
  type ExtensionCommandContext,
22
+ AuthStorage,
28
23
  DefaultPackageManager,
29
24
  getAgentDir,
30
25
  getMarkdownTheme,
26
+ ModelRegistry,
31
27
  SettingsManager,
32
- withFileMutationQueue,
33
- } from '@mariozechner/pi-coding-agent';
34
- import type { ResolvedPaths } from '@mariozechner/pi-coding-agent';
35
- import { Box, Container, Markdown, Spacer, Text } from '@mariozechner/pi-tui';
36
- import { Type } from '@sinclair/typebox';
28
+ } from '@earendil-works/pi-coding-agent';
29
+ import type { ResolvedPaths } from '@earendil-works/pi-coding-agent';
30
+ import { Box, Container, Markdown, Spacer, Text, type AutocompleteItem } from '@earendil-works/pi-tui';
31
+ import { Type } from 'typebox';
37
32
  import {
38
33
  type AgentConfig,
39
34
  type AgentScope,
40
35
  type AgentSource,
41
- bundledAgentsDir,
42
36
  discoverAgents,
43
- discoverAgentsWithPackages,
44
37
  } from './agents.js';
45
- import {
46
- formatDelegateUsage,
47
- formatRunAgentUsage,
48
- parseDelegateArgs,
49
- parseRunAgentArgs,
50
- } from './delegate-args.js';
51
- import { runAgent, runParallel } from './delegate-executor.js';
52
- import { buildSynthesisPrompt, extractRecentConversation } from './synthesis.js';
53
- import type {
54
- AgentResult,
55
- DelegateState,
56
- PhaseDefinition,
57
- PhaseResult,
58
- UsageStats as DelegateUsageStats,
59
- } from './delegate-types.js';
60
- import {
61
- aggregateUsage,
62
- confirmPlan,
63
- confirmSynthesis,
64
- formatFullSummary,
65
- formatPhaseHeader,
66
- getFinalText,
67
- pickWorkflow,
68
- } from './delegate-ui.js';
69
- import { getWorkflow, suggestWorkflow } from './workflows.js';
38
+ import { formatRunAgentUsage, parseRunAgentArgs } from './run-agent-args.js';
39
+ import { runAgent } from './agent-runner.js';
40
+ import { extractRecentConversation } from './synthesis.js';
41
+ import type { AgentResult } from './agent-runner-types.js';
42
+ import { getFinalText } from './agent-result-utils.js';
43
+ import { buildHandoffFromResult, renderHandoffForPrompt } from './handoffs.js';
44
+ import { emptyUsage, spawnPiAgent } from './spawn-utils.js';
70
45
 
71
46
  const MAX_PARALLEL_TASKS = 8;
72
47
  const MAX_CONCURRENCY = 4;
@@ -218,7 +193,7 @@ interface SingleResult {
218
193
  interface SubagentDetails {
219
194
  mode: 'single' | 'parallel' | 'chain';
220
195
  agentScope: AgentScope;
221
- projectAgentsDir: string | null;
196
+ projectPromptsDir: string | null;
222
197
  results: SingleResult[];
223
198
  }
224
199
 
@@ -272,39 +247,6 @@ async function mapWithConcurrencyLimit<TIn, TOut>(
272
247
  return results;
273
248
  }
274
249
 
275
- async function writePromptToTempFile(
276
- agentName: string,
277
- prompt: string,
278
- ): Promise<{ dir: string; filePath: string }> {
279
- const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pi-subagent-'));
280
- const safeName = agentName.replace(/[^\w.-]+/g, '_');
281
- const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
282
- await withFileMutationQueue(filePath, async () => {
283
- await fs.promises.writeFile(filePath, prompt, { encoding: 'utf-8', mode: 0o600 });
284
- });
285
- return { dir: tmpDir, filePath };
286
- }
287
-
288
- function getPiInvocation(args: string[]): { command: string; args: string[] } {
289
- const currentScript = process.argv[1];
290
-
291
- // Bun standalone binaries set argv[1] to a virtual FS path (e.g. /$bunfs/root/pi)
292
- // that only resolves inside the running Bun process. Skip it — the binary IS the entry point.
293
- const isBunVirtualPath = currentScript?.startsWith('/$bunfs/');
294
-
295
- if (currentScript && !isBunVirtualPath && fs.existsSync(currentScript)) {
296
- return { command: process.execPath, args: [currentScript, ...args] };
297
- }
298
-
299
- const execName = path.basename(process.execPath).toLowerCase();
300
- const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
301
- if (!isGenericRuntime) {
302
- // Standalone binary (e.g. pi compiled with Bun) — invoke directly
303
- return { command: process.execPath, args };
304
- }
305
-
306
- return { command: 'pi', args };
307
- }
308
250
 
309
251
  type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
310
252
 
@@ -314,6 +256,8 @@ async function runSingleAgent(
314
256
  agentName: string,
315
257
  task: string,
316
258
  cwd: string | undefined,
259
+ modelOverride: string | undefined,
260
+ thinkingOverride: string | undefined,
317
261
  step: number | undefined,
318
262
  signal: AbortSignal | undefined,
319
263
  onUpdate: OnUpdateCallback | undefined,
@@ -330,26 +274,13 @@ async function runSingleAgent(
330
274
  exitCode: 1,
331
275
  messages: [],
332
276
  stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
333
- usage: {
334
- input: 0,
335
- output: 0,
336
- cacheRead: 0,
337
- cacheWrite: 0,
338
- cost: 0,
339
- contextTokens: 0,
340
- turns: 0,
341
- },
277
+ usage: emptyUsage(),
342
278
  step,
343
279
  };
344
280
  }
345
281
 
346
- const args: string[] = ['--mode', 'json', '-p', '--no-session'];
347
- if (agent.model) args.push('--model', agent.model);
348
- if (agent.thinking) args.push('--thinking', agent.thinking);
349
- if (agent.tools && agent.tools.length > 0) args.push('--tools', agent.tools.join(','));
350
-
351
- let tmpPromptDir: string | null = null;
352
- let tmpPromptPath: string | null = null;
282
+ const selectedModel = modelOverride ?? agent.model;
283
+ const selectedThinking = thinkingOverride ?? agent.thinking;
353
284
 
354
285
  const currentResult: SingleResult = {
355
286
  agent: agentName,
@@ -358,16 +289,8 @@ async function runSingleAgent(
358
289
  exitCode: 0,
359
290
  messages: [],
360
291
  stderr: '',
361
- usage: {
362
- input: 0,
363
- output: 0,
364
- cacheRead: 0,
365
- cacheWrite: 0,
366
- cost: 0,
367
- contextTokens: 0,
368
- turns: 0,
369
- },
370
- model: agent.model,
292
+ usage: emptyUsage(),
293
+ model: selectedModel,
371
294
  step,
372
295
  };
373
296
 
@@ -380,124 +303,58 @@ async function runSingleAgent(
380
303
  }
381
304
  };
382
305
 
383
- try {
384
- if (agent.systemPrompt.trim()) {
385
- const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
386
- tmpPromptDir = tmp.dir;
387
- tmpPromptPath = tmp.filePath;
388
- args.push('--append-system-prompt', tmpPromptPath);
389
- }
390
-
391
- args.push(`Task: ${task}`);
392
- let wasAborted = false;
393
-
394
- const exitCode = await new Promise<number>((resolve) => {
395
- const invocation = getPiInvocation(args);
396
- const proc = spawn(invocation.command, invocation.args, {
397
- cwd: cwd ?? defaultCwd,
398
- shell: false,
399
- stdio: ['ignore', 'pipe', 'pipe'],
400
- });
401
- let buffer = '';
402
-
403
- const processLine = (line: string) => {
404
- if (!line.trim()) return;
405
- let event: any;
406
- try {
407
- event = JSON.parse(line);
408
- } catch {
409
- return;
410
- }
411
-
412
- if (event.type === 'message_end' && event.message) {
413
- const msg = event.message as Message;
414
- currentResult.messages.push(msg);
415
-
416
- if (msg.role === 'assistant') {
417
- currentResult.usage.turns++;
418
- const usage = msg.usage;
419
- if (usage) {
420
- currentResult.usage.input += usage.input || 0;
421
- currentResult.usage.output += usage.output || 0;
422
- currentResult.usage.cacheRead += usage.cacheRead || 0;
423
- currentResult.usage.cacheWrite += usage.cacheWrite || 0;
424
- currentResult.usage.cost += usage.cost?.total || 0;
425
- currentResult.usage.contextTokens = usage.totalTokens || 0;
426
- }
427
- if (!currentResult.model && msg.model) currentResult.model = msg.model;
428
- if (msg.stopReason) currentResult.stopReason = msg.stopReason;
429
- if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
430
- }
431
- emitUpdate();
432
- }
433
-
434
- if (event.type === 'tool_result_end' && event.message) {
435
- currentResult.messages.push(event.message as Message);
436
- emitUpdate();
437
- }
438
- };
439
-
440
- proc.stdout.on('data', (data) => {
441
- buffer += data.toString();
442
- const lines = buffer.split('\n');
443
- buffer = lines.pop() || '';
444
- for (const line of lines) processLine(line);
445
- });
446
-
447
- proc.stderr.on('data', (data) => {
448
- currentResult.stderr += data.toString();
449
- });
450
-
451
- proc.on('close', (code) => {
452
- if (buffer.trim()) processLine(buffer);
453
- resolve(code ?? 0);
454
- });
455
-
456
- proc.on('error', () => {
457
- resolve(1);
458
- });
306
+ const spawnResult = await spawnPiAgent({
307
+ cwd: cwd ?? defaultCwd,
308
+ agentName: agent.name,
309
+ task,
310
+ systemPrompt: agent.systemPrompt,
311
+ model: selectedModel,
312
+ thinking: selectedThinking,
313
+ tools: agent.tools,
314
+ signal,
315
+ onMessage: (msg) => {
316
+ currentResult.messages = spawnResult.messages;
317
+ currentResult.usage = spawnResult.usage;
318
+ currentResult.model = spawnResult.model ?? currentResult.model;
319
+ currentResult.stopReason = spawnResult.stopReason;
320
+ currentResult.errorMessage = spawnResult.errorMessage;
321
+ emitUpdate();
322
+ },
323
+ onToolResult: () => {
324
+ currentResult.messages = spawnResult.messages;
325
+ emitUpdate();
326
+ },
327
+ });
459
328
 
460
- if (signal) {
461
- const killProc = () => {
462
- wasAborted = true;
463
- proc.kill('SIGTERM');
464
- setTimeout(() => {
465
- if (!proc.killed) proc.kill('SIGKILL');
466
- }, 5000);
467
- };
468
- if (signal.aborted) killProc();
469
- else signal.addEventListener('abort', killProc, { once: true });
470
- }
471
- });
329
+ currentResult.exitCode = spawnResult.exitCode;
330
+ currentResult.messages = spawnResult.messages;
331
+ currentResult.stderr = spawnResult.stderr;
332
+ currentResult.usage = spawnResult.usage;
333
+ currentResult.model = spawnResult.model ?? currentResult.model;
334
+ currentResult.stopReason = spawnResult.stopReason;
335
+ currentResult.errorMessage = spawnResult.errorMessage;
472
336
 
473
- currentResult.exitCode = exitCode;
474
- if (wasAborted) throw new Error('Subagent was aborted');
475
- return currentResult;
476
- } finally {
477
- if (tmpPromptPath)
478
- try {
479
- fs.unlinkSync(tmpPromptPath);
480
- } catch {
481
- /* ignore */
482
- }
483
- if (tmpPromptDir)
484
- try {
485
- fs.rmdirSync(tmpPromptDir);
486
- } catch {
487
- /* ignore */
488
- }
489
- }
337
+ if (spawnResult.wasAborted) throw new Error('Subagent was aborted');
338
+ return currentResult;
490
339
  }
491
340
 
492
341
  const TaskItem = Type.Object({
493
342
  agent: Type.String({ description: 'Name of the agent to invoke' }),
494
343
  task: Type.String({ description: 'Task to delegate to the agent' }),
344
+ model: Type.Optional(Type.String({ description: 'Optional model override for this task' })),
345
+ thinking: Type.Optional(
346
+ Type.String({ description: 'Optional reasoning level override for this task' }),
347
+ ),
495
348
  cwd: Type.Optional(Type.String({ description: 'Working directory for the agent process' })),
496
349
  });
497
350
 
498
351
  const ChainItem = Type.Object({
499
352
  agent: Type.String({ description: 'Name of the agent to invoke' }),
500
353
  task: Type.String({ description: 'Task with optional {previous} placeholder for prior output' }),
354
+ model: Type.Optional(Type.String({ description: 'Optional model override for this step' })),
355
+ thinking: Type.Optional(
356
+ Type.String({ description: 'Optional reasoning level override for this step' }),
357
+ ),
501
358
  cwd: Type.Optional(Type.String({ description: 'Working directory for the agent process' })),
502
359
  });
503
360
 
@@ -512,11 +369,17 @@ const SubagentParams = Type.Object({
512
369
  Type.String({ description: 'Name of the agent to invoke (for single mode)' }),
513
370
  ),
514
371
  task: Type.Optional(Type.String({ description: 'Task to delegate (for single mode)' })),
372
+ model: Type.Optional(
373
+ Type.String({ description: 'Optional default model override for the run or all steps/tasks' }),
374
+ ),
375
+ thinking: Type.Optional(
376
+ Type.String({ description: 'Optional default reasoning level override for the run or all steps/tasks' }),
377
+ ),
515
378
  tasks: Type.Optional(
516
- Type.Array(TaskItem, { description: 'Array of {agent, task} for parallel execution' }),
379
+ Type.Array(TaskItem, { description: 'Array of {agent, task, model?, thinking?} for parallel execution' }),
517
380
  ),
518
381
  chain: Type.Optional(
519
- Type.Array(ChainItem, { description: 'Array of {agent, task} for sequential execution' }),
382
+ Type.Array(ChainItem, { description: 'Array of {agent, task, model?, thinking?} for sequential execution' }),
520
383
  ),
521
384
  agentScope: Type.Optional(AgentScopeSchema),
522
385
  confirmProjectAgents: Type.Optional(
@@ -530,42 +393,120 @@ const SubagentParams = Type.Object({
530
393
  ),
531
394
  });
532
395
 
533
- /**
534
- * Try to resolve package paths including the agents resource.
535
- * Returns null if the pi version does not support ResolvedPaths.agents.
536
- */
537
- async function tryResolvePackagePaths(cwd: string): Promise<ResolvedPaths | undefined> {
396
+ async function resolvePackagePaths(cwd: string): Promise<ResolvedPaths | undefined> {
538
397
  try {
539
398
  const agentDir = getAgentDir();
540
399
  const settingsManager = SettingsManager.create(cwd, agentDir);
541
400
  const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
542
- const resolved = await packageManager.resolve();
543
- // Check if this pi version has agents support
544
- if ('agents' in resolved && Array.isArray((resolved as Record<string, unknown>).agents)) {
545
- return resolved;
546
- }
401
+ return await packageManager.resolve();
547
402
  } catch {
548
- // Incompatible pi version or missing API
403
+ return undefined;
549
404
  }
550
- return undefined;
551
405
  }
552
406
 
553
407
  export default function (pi: ExtensionAPI) {
408
+ let autocompleteCwd = process.cwd();
409
+
410
+ function parseArgumentText(argumentText: string): {
411
+ completedTokens: string[];
412
+ currentToken: string;
413
+ } {
414
+ const endsWithSpace = argumentText.length > 0 && /\s$/.test(argumentText);
415
+ const tokens = argumentText.trim().length > 0 ? argumentText.trim().split(/\s+/) : [];
416
+ return {
417
+ completedTokens: endsWithSpace ? tokens : tokens.slice(0, -1),
418
+ currentToken: endsWithSpace ? '' : (tokens.at(-1) ?? ''),
419
+ };
420
+ }
421
+
422
+ function replaceCurrentToken(argumentText: string, replacement: string): string {
423
+ const trimmedEnd = argumentText.replace(/\s+$/, '');
424
+ if (!trimmedEnd) return replacement;
425
+ if (/\s$/.test(argumentText)) return `${trimmedEnd} ${replacement}`;
426
+ const lastSpace = trimmedEnd.lastIndexOf(' ');
427
+ return lastSpace === -1
428
+ ? replacement
429
+ : `${trimmedEnd.slice(0, lastSpace + 1)}${replacement}`;
430
+ }
431
+
432
+ function buildArgumentCompletions(
433
+ argumentText: string,
434
+ items: Array<{ value: string; label?: string; description?: string }>,
435
+ ): AutocompleteItem[] | null {
436
+ const { currentToken } = parseArgumentText(argumentText);
437
+ const query = currentToken.toLowerCase();
438
+ const filtered = items.filter((item) => {
439
+ if (!query) return true;
440
+ return (
441
+ item.value.toLowerCase().startsWith(query) ||
442
+ item.label?.toLowerCase().includes(query) ||
443
+ item.description?.toLowerCase().includes(query)
444
+ );
445
+ });
446
+ if (filtered.length === 0) return null;
447
+ return filtered.map((item) => ({
448
+ value: replaceCurrentToken(argumentText, item.value),
449
+ label: item.label ?? item.value,
450
+ description: item.description,
451
+ }));
452
+ }
453
+
454
+ async function getAutocompleteAgents(scope: AgentScope): Promise<AgentConfig[]> {
455
+ const resolvedPaths = await resolvePackagePaths(autocompleteCwd);
456
+ return discoverAgents(autocompleteCwd, scope, resolvedPaths).agents;
457
+ }
458
+
459
+ async function getAutocompleteModels(scope: AgentScope): Promise<
460
+ Array<{ value: string; label?: string; description?: string }>
461
+ > {
462
+ const items = new Map<string, { value: string; label?: string; description?: string }>();
463
+
464
+ try {
465
+ const authStorage = AuthStorage.create();
466
+ const modelRegistry = ModelRegistry.create(authStorage);
467
+ const available = await modelRegistry.getAvailable();
468
+ for (const model of available) {
469
+ const value = `${model.provider}/${model.id}`;
470
+ items.set(value, {
471
+ value,
472
+ label: value,
473
+ description: 'Configured model with available credentials',
474
+ });
475
+ }
476
+ } catch {
477
+ // Fall back to models referenced by discovered agents.
478
+ }
479
+
480
+ const agents = await getAutocompleteAgents(scope);
481
+ for (const agent of agents) {
482
+ if (!agent.model) continue;
483
+ if (items.has(agent.model)) continue;
484
+ items.set(agent.model, {
485
+ value: agent.model,
486
+ label: agent.model,
487
+ description: `Default model on agent ${agent.name}`,
488
+ });
489
+ }
490
+
491
+ return [...items.values()];
492
+ }
493
+
554
494
  pi.registerTool({
555
495
  name: 'subagent',
556
496
  label: 'Subagent',
557
497
  description: [
558
498
  'Delegate tasks to specialized subagents with isolated context.',
559
499
  'Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).',
560
- 'Default agent scope is "user" (from ~/.pi/agent/agents).',
561
- 'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").',
500
+ 'Optional per-run or per-step model and reasoning-level overrides are supported via model/thinking.',
501
+ 'Default agent scope is "user" (from ~/.pi/agent/prompts).',
502
+ 'To enable project-local agents in .pi/prompts, set agentScope: "both" (or "project").',
562
503
  ].join(' '),
563
504
  parameters: SubagentParams,
564
505
 
565
506
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
566
507
  const agentScope: AgentScope = params.agentScope ?? 'user';
567
- const resolvedPaths = await tryResolvePackagePaths(ctx.cwd);
568
- const discovery = discoverAgentsWithPackages(ctx.cwd, agentScope, resolvedPaths);
508
+ const resolvedPaths = await resolvePackagePaths(ctx.cwd);
509
+ const discovery = discoverAgents(ctx.cwd, agentScope, resolvedPaths);
569
510
  const agents = discovery.agents;
570
511
  const confirmProjectAgents = params.confirmProjectAgents ?? true;
571
512
 
@@ -579,7 +520,7 @@ export default function (pi: ExtensionAPI) {
579
520
  (results: SingleResult[]): SubagentDetails => ({
580
521
  mode,
581
522
  agentScope,
582
- projectAgentsDir: discovery.projectAgentsDir,
523
+ projectPromptsDir: discovery.projectPromptsDir,
583
524
  results,
584
525
  });
585
526
 
@@ -612,7 +553,7 @@ export default function (pi: ExtensionAPI) {
612
553
 
613
554
  if (projectAgentsRequested.length > 0) {
614
555
  const names = projectAgentsRequested.map((a) => a.name).join(', ');
615
- const dir = discovery.projectAgentsDir ?? '(unknown)';
556
+ const dir = discovery.projectPromptsDir ?? '(unknown)';
616
557
  const ok = await ctx.ui.confirm(
617
558
  'Run project-local agents?',
618
559
  `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
@@ -648,12 +589,18 @@ export default function (pi: ExtensionAPI) {
648
589
  }
649
590
  : undefined;
650
591
 
592
+ if (ctx.hasUI) {
593
+ ctx.ui.setWorkingMessage(`Chain step ${i + 1}/${params.chain.length}: ${step.agent}`);
594
+ }
595
+
651
596
  const result = await runSingleAgent(
652
597
  ctx.cwd,
653
598
  agents,
654
599
  step.agent,
655
600
  taskWithContext,
656
601
  step.cwd,
602
+ step.model ?? params.model,
603
+ step.thinking ?? params.thinking,
657
604
  i + 1,
658
605
  signal,
659
606
  chainUpdate,
@@ -682,7 +629,18 @@ export default function (pi: ExtensionAPI) {
682
629
  isError: true,
683
630
  };
684
631
  }
685
- previousOutput = getFinalOutput(result.messages);
632
+ const finalOutput = getFinalOutput(result.messages) || '(no output)';
633
+ previousOutput = renderHandoffForPrompt(
634
+ buildHandoffFromResult({
635
+ agent: step.agent,
636
+ step: i + 1,
637
+ task: step.task,
638
+ output: finalOutput,
639
+ }),
640
+ );
641
+ }
642
+ if (ctx.hasUI) {
643
+ ctx.ui.setWorkingMessage();
686
644
  }
687
645
  return {
688
646
  content: [
@@ -747,6 +705,10 @@ export default function (pi: ExtensionAPI) {
747
705
  }
748
706
  };
749
707
 
708
+ if (ctx.hasUI) {
709
+ ctx.ui.setWorkingMessage(`Running ${params.tasks.length} agents in parallel`);
710
+ }
711
+
750
712
  const results = await mapWithConcurrencyLimit(
751
713
  params.tasks,
752
714
  MAX_CONCURRENCY,
@@ -757,6 +719,8 @@ export default function (pi: ExtensionAPI) {
757
719
  t.agent,
758
720
  t.task,
759
721
  t.cwd,
722
+ t.model ?? params.model,
723
+ t.thinking ?? params.thinking,
760
724
  undefined,
761
725
  signal,
762
726
  // Per-task update callback
@@ -774,6 +738,10 @@ export default function (pi: ExtensionAPI) {
774
738
  },
775
739
  );
776
740
 
741
+ if (ctx.hasUI) {
742
+ ctx.ui.setWorkingMessage();
743
+ }
744
+
777
745
  const successCount = results.filter((r) => r.exitCode === 0).length;
778
746
  const summaries = results.map((r) => {
779
747
  const output = getFinalOutput(r.messages);
@@ -791,17 +759,27 @@ export default function (pi: ExtensionAPI) {
791
759
  }
792
760
 
793
761
  if (params.agent && params.task) {
762
+ if (ctx.hasUI) {
763
+ ctx.ui.setWorkingMessage(`Running agent: ${params.agent}`);
764
+ }
765
+
794
766
  const result = await runSingleAgent(
795
767
  ctx.cwd,
796
768
  agents,
797
769
  params.agent,
798
770
  params.task,
799
771
  params.cwd,
772
+ params.model,
773
+ params.thinking,
800
774
  undefined,
801
775
  signal,
802
776
  onUpdate,
803
777
  makeDetails('single'),
804
778
  );
779
+ if (ctx.hasUI) {
780
+ ctx.ui.setWorkingMessage();
781
+ }
782
+
805
783
  const isError =
806
784
  result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted';
807
785
  if (isError) {
@@ -1164,31 +1142,18 @@ export default function (pi: ExtensionAPI) {
1164
1142
  },
1165
1143
  });
1166
1144
 
1167
- // ── Delegate commands ──────────────────────────────────────────────────────
1168
-
1169
- function emptyDelegateUsage(): DelegateUsageStats {
1170
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
1171
- }
1172
-
1173
- function combineParallelOutputs(results: AgentResult[]): string {
1174
- return results
1175
- .map((r) => {
1176
- const output = getFinalText(r);
1177
- return `## ${r.agent}\n${output}`;
1178
- })
1179
- .join('\n\n');
1180
- }
1145
+ // ── Command helpers ───────────────────────────────────────────────────────
1181
1146
 
1182
1147
  async function confirmProjectAgentsIfNeeded(
1183
1148
  ctx: ExtensionCommandContext,
1184
1149
  agents: AgentConfig[],
1185
- projectAgentsDir: string | null,
1150
+ projectPromptsDir: string | null,
1186
1151
  confirmProjectAgents: boolean,
1187
1152
  ): Promise<boolean> {
1188
1153
  if (!ctx.hasUI || !confirmProjectAgents || agents.length === 0) return true;
1189
1154
 
1190
1155
  const names = Array.from(new Set(agents.map((agent) => agent.name))).join(', ');
1191
- const dir = projectAgentsDir ?? '(unknown)';
1156
+ const dir = projectPromptsDir ?? '(unknown)';
1192
1157
  return ctx.ui.confirm(
1193
1158
  'Run project-local agents?',
1194
1159
  `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
@@ -1303,192 +1268,109 @@ export default function (pi: ExtensionAPI) {
1303
1268
  return box;
1304
1269
  });
1305
1270
 
1306
- async function executePhase(
1307
- cwd: string,
1308
- phase: PhaseDefinition,
1309
- synthesis: string,
1310
- previousOutput: string,
1311
- ctx: ExtensionCommandContext,
1312
- agentScope: AgentScope,
1313
- resolvedPaths?: ResolvedPaths,
1314
- ): Promise<{ results: AgentResult[]; output: string; aborted: boolean }> {
1315
- const task = phase.taskTemplate
1316
- .replace(/\{synthesis\}/g, synthesis)
1317
- .replace(/\{previous\}/g, previousOutput);
1318
-
1319
- ctx.ui.notify(
1320
- `${formatPhaseHeader(phase.name, 'running')} ${phase.kind === 'parallel' ? `(${phase.agents.length} agents)` : phase.agents[0]}`,
1321
- 'info',
1322
- );
1323
-
1324
- let results: AgentResult[];
1325
- if (phase.kind === 'parallel') {
1326
- results = await runParallel(cwd, phase.agents, task, {
1327
- agentScope,
1328
- onUpdate: (_phaseName, _agentName, _result) => {
1329
- // Streaming updates happen per-message
1330
- },
1331
- phaseName: phase.name,
1332
- resolvedPaths,
1333
- });
1334
- } else {
1335
- const result = await runAgent(cwd, phase.agents[0], task, {
1336
- agentScope,
1337
- phaseName: phase.name,
1338
- resolvedPaths,
1339
- });
1340
- results = [result];
1341
- }
1342
-
1343
- const hasError = results.some((r) => r.exitCode !== 0 || r.stopReason === 'error');
1344
- if (hasError) {
1345
- const failedAgents = results
1346
- .filter((r) => r.exitCode !== 0 || r.stopReason === 'error')
1347
- .map((r) => `${r.agent}: ${r.errorMessage || r.stderr || '(unknown error)'}`)
1348
- .join('\n');
1349
-
1350
- ctx.ui.notify(`${formatPhaseHeader(phase.name, 'failed')}\n${failedAgents}`, 'error');
1351
- return { results, output: combineParallelOutputs(results), aborted: true };
1352
- }
1353
-
1354
- const output = combineParallelOutputs(results);
1355
- ctx.ui.notify(formatPhaseHeader(phase.name, 'done'), 'info');
1356
-
1357
- if (phase.requiresConfirmation && ctx.hasUI) {
1358
- const planText = output;
1359
- const proceed = await confirmPlan(ctx, planText);
1360
- if (!proceed) {
1361
- ctx.ui.notify('Delegate aborted by user after plan review.', 'warning');
1362
- return { results, output, aborted: true };
1363
- }
1364
- }
1365
-
1366
- return { results, output, aborted: false };
1367
- }
1368
-
1369
- async function listMdFiles(dir: string): Promise<Set<string>> {
1370
- if (!existsSync(dir)) return new Set();
1371
- try {
1372
- const entries = await readdir(dir);
1373
- return new Set(entries.filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, '')));
1374
- } catch {
1375
- return new Set();
1376
- }
1377
- }
1271
+ pi.on('session_start', async (_event, ctx) => {
1272
+ autocompleteCwd = ctx.cwd;
1273
+ });
1378
1274
 
1379
- pi.registerCommand('delegate-agents', {
1380
- description: 'List, customize, or reset delegate agents',
1381
- handler: async (args, ctx) => {
1382
- const userDir = join(getAgentDir(), 'agents');
1383
- const parts = args?.trim().split(/\s+/) || [];
1384
- const action = parts[0] || 'list';
1385
-
1386
- if (action === 'list') {
1387
- const bundled = await listMdFiles(bundledAgentsDir);
1388
- const user = await listMdFiles(userDir);
1389
- const allNames = new Set([...bundled, ...user]);
1390
-
1391
- const lines: string[] = ['## Delegate Agents\n'];
1392
- for (const name of [...allNames].sort()) {
1393
- const isBundled = bundled.has(name);
1394
- const isUser = user.has(name);
1395
- if (isUser && isBundled) {
1396
- lines.push(`- **${name}** — user override (bundled version available, \`/delegate-agents reset ${name}\` to restore)`);
1397
- } else if (isUser) {
1398
- lines.push(`- **${name}** — user-only`);
1399
- } else {
1400
- lines.push(`- **${name}** — bundled`);
1275
+ pi.registerCommand('run-agent', {
1276
+ description:
1277
+ 'Run a single agent directly — honors agent sessionStrategy metadata such as fork-at',
1278
+ getArgumentCompletions: async (argumentText) => {
1279
+ const { completedTokens } = parseArgumentText(argumentText);
1280
+ let agentScope: AgentScope = 'user';
1281
+ let expectsScopeValue = false;
1282
+ let expectsThinkingValue = false;
1283
+ let expectsModelValue = false;
1284
+ let agentName: string | undefined;
1285
+
1286
+ for (const token of completedTokens) {
1287
+ if (expectsScopeValue) {
1288
+ if (token === 'user' || token === 'project' || token === 'both') {
1289
+ agentScope = token;
1401
1290
  }
1291
+ expectsScopeValue = false;
1292
+ continue;
1402
1293
  }
1403
- pi.sendMessage({ customType: 'delegate-agents-list', content: lines.join('\n'), display: true });
1404
- return;
1405
- }
1406
-
1407
- if (action === 'reset') {
1408
- const name = parts[1];
1409
- if (!name) {
1410
- ctx.ui.notify('Usage: /delegate-agents reset <name|--all>', 'warning');
1411
- return;
1294
+ if (expectsThinkingValue) {
1295
+ expectsThinkingValue = false;
1296
+ continue;
1412
1297
  }
1413
-
1414
- if (name === '--all') {
1415
- const user = await listMdFiles(userDir);
1416
- const bundled = await listMdFiles(bundledAgentsDir);
1417
- let count = 0;
1418
- for (const n of user) {
1419
- if (bundled.has(n)) {
1420
- await unlink(join(userDir, `${n}.md`));
1421
- count++;
1422
- }
1423
- }
1424
- ctx.ui.notify(count > 0 ? `Reset ${count} agent(s) to bundled versions.` : 'No user overrides to reset.', 'info');
1425
- return;
1298
+ if (expectsModelValue) {
1299
+ expectsModelValue = false;
1300
+ continue;
1426
1301
  }
1427
-
1428
- const userFile = join(userDir, `${name}.md`);
1429
- const bundledFile = join(bundledAgentsDir, `${name}.md`);
1430
-
1431
- if (!existsSync(userFile)) {
1432
- ctx.ui.notify(`No user override for "${name}" — already using bundled version.`, 'info');
1433
- return;
1302
+ if (token === '--scope') {
1303
+ expectsScopeValue = true;
1304
+ continue;
1434
1305
  }
1435
- if (!existsSync(bundledFile)) {
1436
- ctx.ui.notify(`"${name}" is user-only (no bundled version). Delete manually if needed.`, 'warning');
1437
- return;
1306
+ if (token === '--thinking' || token === '--reasoning-level') {
1307
+ expectsThinkingValue = true;
1308
+ continue;
1438
1309
  }
1439
-
1440
- await unlink(userFile);
1441
- ctx.ui.notify(`Reset "${name}" — now using bundled version.`, 'info');
1442
- return;
1443
- }
1444
-
1445
- if (action === 'edit') {
1446
- const name = parts[1];
1447
- if (!name) {
1448
- ctx.ui.notify('Usage: /delegate-agents edit <name>', 'warning');
1449
- return;
1310
+ if (token === '--model') {
1311
+ expectsModelValue = true;
1312
+ continue;
1450
1313
  }
1314
+ if (token === '--yes-project-agents') continue;
1315
+ if (token.startsWith('--')) continue;
1316
+ if (!agentName) agentName = token;
1317
+ }
1451
1318
 
1452
- const bundledFile = join(bundledAgentsDir, `${name}.md`);
1453
- const userFile = join(userDir, `${name}.md`);
1454
-
1455
- if (existsSync(userFile)) {
1456
- ctx.ui.notify(`User override already exists: ${userFile}`, 'info');
1457
- return;
1458
- }
1459
- if (!existsSync(bundledFile)) {
1460
- ctx.ui.notify(`No bundled agent named "${name}".`, 'warning');
1461
- return;
1462
- }
1319
+ if (expectsScopeValue) {
1320
+ return buildArgumentCompletions(argumentText, [
1321
+ { value: 'user', description: 'Only user/global agents from ~/.pi/agent/prompts' },
1322
+ { value: 'project', description: 'Only project-local agents from .pi/prompts' },
1323
+ { value: 'both', description: 'User/global agents plus project-local agents' },
1324
+ ]);
1325
+ }
1463
1326
 
1464
- await mkdir(userDir, { recursive: true });
1465
- await copyFile(bundledFile, userFile);
1466
- ctx.ui.notify(`Copied "${name}" to ${userFile} edit it there to customize.`, 'info');
1467
- return;
1327
+ if (expectsThinkingValue) {
1328
+ return buildArgumentCompletions(argumentText, [
1329
+ { value: 'off', description: 'Disable reasoning if the model supports it' },
1330
+ { value: 'minimal', description: 'Minimal reasoning effort' },
1331
+ { value: 'low', description: 'Lower reasoning effort' },
1332
+ { value: 'medium', description: 'Balanced reasoning effort' },
1333
+ { value: 'high', description: 'Higher reasoning effort' },
1334
+ { value: 'xhigh', description: 'Maximum reasoning effort on supported models' },
1335
+ ]);
1468
1336
  }
1469
1337
 
1470
- ctx.ui.notify('Unknown action. Usage: /delegate-agents [list|reset <name|--all>|edit <name>]', 'warning');
1338
+ if (expectsModelValue) {
1339
+ const models = await getAutocompleteModels(agentScope);
1340
+ return buildArgumentCompletions(argumentText, models);
1341
+ }
1342
+ if (agentName) return null;
1343
+
1344
+ const agents = await getAutocompleteAgents(agentScope);
1345
+ return buildArgumentCompletions(argumentText, [
1346
+ { value: '--scope', description: 'Choose which agent directories are searched' },
1347
+ { value: '--model', description: 'Override the agent model for this run' },
1348
+ { value: '--thinking', description: 'Override the reasoning level for this run' },
1349
+ { value: '--reasoning-level', description: 'Alias for --thinking' },
1350
+ { value: '--yes-project-agents', description: 'Skip the project-agent trust confirmation prompt' },
1351
+ ...agents.map((agent) => ({
1352
+ value: agent.name,
1353
+ label: agent.name,
1354
+ description: `${agent.source} — ${agent.description}`,
1355
+ })),
1356
+ ]);
1471
1357
  },
1472
- });
1473
-
1474
- pi.registerCommand('run-agent', {
1475
- description:
1476
- 'Run a single agent directly — honors agent sessionStrategy metadata such as fork-at',
1477
1358
  handler: async (args, ctx) => {
1359
+ autocompleteCwd = ctx.cwd;
1478
1360
  const parsed = parseRunAgentArgs(args);
1479
1361
  if (!parsed.ok) {
1480
1362
  ctx.ui.notify(parsed.error, 'warning');
1481
1363
  return;
1482
1364
  }
1483
1365
 
1484
- const { agentName, explicitTask, agentScope, confirmProjectAgents } = parsed.options;
1366
+ const { agentName, explicitTask, agentScope, confirmProjectAgents, model, thinking } = parsed.options;
1485
1367
  if (!agentName) {
1486
1368
  ctx.ui.notify(formatRunAgentUsage(), 'warning');
1487
1369
  return;
1488
1370
  }
1489
1371
 
1490
- const resolvedPaths = await tryResolvePackagePaths(ctx.cwd);
1491
- const discovery = discoverAgentsWithPackages(ctx.cwd, agentScope, resolvedPaths);
1372
+ const resolvedPaths = await resolvePackagePaths(ctx.cwd);
1373
+ const discovery = discoverAgents(ctx.cwd, agentScope, resolvedPaths);
1492
1374
  const agent = discovery.agents.find((candidate) => candidate.name === agentName);
1493
1375
 
1494
1376
  if (!agent) {
@@ -1501,7 +1383,7 @@ export default function (pi: ExtensionAPI) {
1501
1383
  const approved = await confirmProjectAgentsIfNeeded(
1502
1384
  ctx,
1503
1385
  requestedProjectAgents,
1504
- discovery.projectAgentsDir,
1386
+ discovery.projectPromptsDir,
1505
1387
  confirmProjectAgents,
1506
1388
  );
1507
1389
  if (!approved) {
@@ -1516,7 +1398,65 @@ export default function (pi: ExtensionAPI) {
1516
1398
  return;
1517
1399
  }
1518
1400
 
1519
- let forked = false;
1401
+ const runAndReport = async (
1402
+ commandCwd: string,
1403
+ commandUi: typeof ctx.ui,
1404
+ sendSummary: (message: {
1405
+ customType: 'run-agent-summary';
1406
+ content: string;
1407
+ display: true;
1408
+ details: RunAgentSummaryDetails;
1409
+ }) => Promise<void> | void,
1410
+ forked: boolean,
1411
+ ) => {
1412
+ const selectedModel = model ?? agent.model;
1413
+ const selectedThinking = thinking ?? agent.thinking;
1414
+
1415
+ commandUi.notify(
1416
+ `Running agent: ${agent.name} [${agent.source}${forked ? ', forked' : ', inline'}${selectedModel ? `, model=${selectedModel}` : ''}${selectedThinking ? `, thinking=${selectedThinking}` : ''}]`,
1417
+ 'info',
1418
+ );
1419
+
1420
+ const result = await runAgent(commandCwd, agent.name, task, {
1421
+ agentScope,
1422
+ resolvedPaths,
1423
+ model,
1424
+ thinking,
1425
+ });
1426
+ const failed = result.exitCode !== 0 || result.stopReason === 'error';
1427
+ const output = getFinalText(result).trim();
1428
+
1429
+ commandUi.notify(
1430
+ failed ? `Agent "${agent.name}" failed.` : `Agent "${agent.name}" complete.`,
1431
+ failed ? 'error' : 'info',
1432
+ );
1433
+
1434
+ await sendSummary({
1435
+ customType: 'run-agent-summary',
1436
+ content: formatRunAgentSummary({
1437
+ agent,
1438
+ agentScope,
1439
+ task,
1440
+ result,
1441
+ forked,
1442
+ }),
1443
+ display: true,
1444
+ details: {
1445
+ agent: agent.name,
1446
+ agentSource: agent.source,
1447
+ agentScope,
1448
+ sessionStrategy: agent.sessionStrategy ?? 'inline',
1449
+ forked,
1450
+ exitCode: result.exitCode,
1451
+ stopReason: result.stopReason,
1452
+ errorMessage: result.errorMessage,
1453
+ usage: result.usage,
1454
+ task,
1455
+ output,
1456
+ } satisfies RunAgentSummaryDetails,
1457
+ });
1458
+ };
1459
+
1520
1460
  if (agent.sessionStrategy === 'fork-at') {
1521
1461
  const leafId = ctx.sessionManager.getLeafId?.();
1522
1462
  if (!leafId) {
@@ -1525,188 +1465,26 @@ export default function (pi: ExtensionAPI) {
1525
1465
  'warning',
1526
1466
  );
1527
1467
  } else {
1528
- const forkCompat = ctx.fork as unknown as (
1529
- entryId: string,
1530
- options?: { position: 'at' },
1531
- ) => Promise<{ cancelled: boolean }>;
1532
- const forkResult = await forkCompat(leafId, { position: 'at' });
1468
+ const forkResult = await ctx.fork(leafId, {
1469
+ position: 'at',
1470
+ withSession: async (replacementCtx) => {
1471
+ await runAndReport(
1472
+ replacementCtx.cwd,
1473
+ replacementCtx.ui,
1474
+ (message) => replacementCtx.sendMessage(message),
1475
+ true,
1476
+ );
1477
+ },
1478
+ });
1533
1479
  if (forkResult.cancelled) {
1534
1480
  ctx.ui.notify(`Run cancelled — unable to fork session for "${agent.name}".`, 'info');
1535
- return;
1536
1481
  }
1537
- forked = true;
1538
- }
1539
- }
1540
-
1541
- ctx.ui.notify(
1542
- `Running agent: ${agent.name} [${agent.source}${forked ? ', forked' : ', inline'}]`,
1543
- 'info',
1544
- );
1545
-
1546
- const result = await runAgent(ctx.cwd, agent.name, task, { agentScope, resolvedPaths });
1547
- const failed = result.exitCode !== 0 || result.stopReason === 'error';
1548
- const output = getFinalText(result).trim();
1549
-
1550
- ctx.ui.notify(
1551
- failed ? `Agent "${agent.name}" failed.` : `Agent "${agent.name}" complete.`,
1552
- failed ? 'error' : 'info',
1553
- );
1554
-
1555
- pi.sendMessage({
1556
- customType: 'run-agent-summary',
1557
- content: formatRunAgentSummary({
1558
- agent,
1559
- agentScope,
1560
- task,
1561
- result,
1562
- forked,
1563
- }),
1564
- display: true,
1565
- details: {
1566
- agent: agent.name,
1567
- agentSource: agent.source,
1568
- agentScope,
1569
- sessionStrategy: agent.sessionStrategy ?? 'inline',
1570
- forked,
1571
- exitCode: result.exitCode,
1572
- stopReason: result.stopReason,
1573
- errorMessage: result.errorMessage,
1574
- usage: result.usage,
1575
- task,
1576
- output,
1577
- } satisfies RunAgentSummaryDetails,
1578
- });
1579
- },
1580
- });
1581
-
1582
- pi.registerCommand('delegate', {
1583
- description:
1584
- 'Orchestrate subagent workflows — supports --scope, --workflow, and project-local agents',
1585
- handler: async (args, ctx) => {
1586
- const parsed = parseDelegateArgs(args);
1587
- if (!parsed.ok) {
1588
- ctx.ui.notify(parsed.error, 'warning');
1589
- return;
1590
- }
1591
-
1592
- const { explicitTask, agentScope, workflowId, confirmProjectAgents } = parsed.options;
1593
- const resolvedPaths = await tryResolvePackagePaths(ctx.cwd);
1594
-
1595
- // Step 1: Synthesize
1596
- const conversation = extractRecentConversation(ctx);
1597
- if (!conversation.trim() && !explicitTask) {
1598
- ctx.ui.notify(`No conversation context and no task specified.\n\n${formatDelegateUsage()}`, 'warning');
1599
- return;
1600
- }
1601
-
1602
- const prompt = buildSynthesisPrompt(conversation, explicitTask);
1603
- ctx.ui.notify('⏳ Synthesizing task from conversation...', 'info');
1604
-
1605
- const synthResult = await runAgent(ctx.cwd, 'planner', prompt, { agentScope, resolvedPaths });
1606
- const synthesis = getFinalText(synthResult);
1607
-
1608
- if (!synthesis.trim()) {
1609
- ctx.ui.notify('Failed to synthesize task from conversation.', 'error');
1610
- return;
1611
- }
1612
-
1613
- // Step 2: Confirm synthesis
1614
- if (ctx.hasUI) {
1615
- const synthOk = await confirmSynthesis(ctx, synthesis);
1616
- if (!synthOk) {
1617
- ctx.ui.notify('Delegate cancelled.', 'info');
1618
- return;
1619
- }
1620
- }
1621
-
1622
- // Step 3: Pick workflow
1623
- const workflow = workflowId
1624
- ? getWorkflow(workflowId)
1625
- : ctx.hasUI
1626
- ? await pickWorkflow(ctx, synthesis)
1627
- : getWorkflow(suggestWorkflow(synthesis));
1628
- if (!workflow) {
1629
- ctx.ui.notify('Delegate cancelled — no workflow selected.', 'info');
1630
- return;
1631
- }
1632
-
1633
- if (agentScope === 'project' || agentScope === 'both') {
1634
- const discovery = discoverAgentsWithPackages(ctx.cwd, agentScope, resolvedPaths);
1635
- const requestedProjectAgents = workflow.phases
1636
- .flatMap((phase) => phase.agents)
1637
- .map((name) => discovery.agents.find((agent) => agent.name === name))
1638
- .filter((agent): agent is AgentConfig => agent?.source === 'project');
1639
-
1640
- const ok = await confirmProjectAgentsIfNeeded(
1641
- ctx,
1642
- requestedProjectAgents,
1643
- discovery.projectAgentsDir,
1644
- confirmProjectAgents,
1645
- );
1646
- if (!ok) {
1647
- ctx.ui.notify('Delegate cancelled — project-local agents not approved.', 'info');
1648
1482
  return;
1649
1483
  }
1650
1484
  }
1651
1485
 
1652
- // Step 4: Execute phases
1653
- const state: DelegateState = {
1654
- synthesis,
1655
- workflow,
1656
- phases: [],
1657
- totalUsage: emptyDelegateUsage(),
1658
- };
1659
-
1660
- let previousOutput = '';
1661
-
1662
- ctx.ui.notify(`Starting workflow: ${workflow.label} [${agentScope}]`, 'info');
1663
-
1664
- for (const phaseDef of workflow.phases) {
1665
- const phaseResult = await executePhase(
1666
- ctx.cwd,
1667
- phaseDef,
1668
- synthesis,
1669
- previousOutput,
1670
- ctx,
1671
- agentScope,
1672
- resolvedPaths,
1673
- );
1674
-
1675
- const phase: PhaseResult = {
1676
- name: phaseDef.name,
1677
- kind: phaseDef.kind,
1678
- agents: phaseResult.results,
1679
- };
1680
-
1681
- state.phases.push(phase);
1682
- const phaseUsage = aggregateUsage(phaseResult.results);
1683
- state.totalUsage.input += phaseUsage.input;
1684
- state.totalUsage.output += phaseUsage.output;
1685
- state.totalUsage.cacheRead += phaseUsage.cacheRead;
1686
- state.totalUsage.cacheWrite += phaseUsage.cacheWrite;
1687
- state.totalUsage.cost += phaseUsage.cost;
1688
- state.totalUsage.turns += phaseUsage.turns;
1689
-
1690
- if (phaseResult.aborted) break;
1691
-
1692
- previousOutput = phaseResult.output;
1693
- }
1694
-
1695
- // Step 5: Show full summary
1696
- const summary = formatFullSummary(state);
1697
- ctx.ui.notify(`Delegate complete — ${workflow.label}`, 'info');
1698
-
1699
- pi.sendMessage({
1700
- customType: 'delegate-summary',
1701
- content: summary,
1702
- display: true,
1703
- details: {
1704
- workflow: workflow.id,
1705
- agentScope,
1706
- phaseCount: state.phases.length,
1707
- totalUsage: state.totalUsage,
1708
- },
1709
- });
1486
+ await runAndReport(ctx.cwd, ctx.ui, (message) => pi.sendMessage(message), false);
1710
1487
  },
1711
1488
  });
1489
+
1712
1490
  }