@dreki-gg/pi-subagent 0.7.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.
- package/.fallowrc.json +6 -0
- package/CHANGELOG.md +16 -0
- package/README.md +4 -13
- package/docs/plans/01_structured-handoffs-and-synthesis.plan.md +14 -14
- package/docs/plans/02_clean-context-review-loop.plan.md +12 -12
- package/docs/plans/03_parallel-recon-single-writer-docs.plan.md +4 -4
- package/docs/plans/04_manager-workflow.plan.md +10 -10
- package/docs/plans/05_advisor-routing.plan.md +22 -22
- package/extensions/subagent/agent-runner-types.ts +1 -1
- package/extensions/subagent/agent-runner.ts +40 -152
- package/extensions/subagent/agents.ts +31 -77
- package/extensions/subagent/index.ts +83 -354
- package/extensions/subagent/spawn-utils.ts +198 -0
- package/extensions/subagent/synthesis.ts +1 -51
- package/package.json +8 -8
- package/skills/write-an-agent/SKILL.md +5 -5
- /package/{agents → prompts}/advisor.md +0 -0
- /package/{agents → prompts}/bug-prover.md +0 -0
- /package/{agents → prompts}/docs-scout.md +0 -0
- /package/{agents → prompts}/manager.md +0 -0
- /package/{agents → prompts}/planner.md +0 -0
- /package/{agents → prompts}/reviewer.md +0 -0
- /package/{agents → prompts}/scout.md +0 -0
- /package/{agents → prompts}/ux-designer.md +0 -0
- /package/{agents → prompts}/validator.md +0 -0
- /package/{agents → prompts}/worker.md +0 -0
|
@@ -12,16 +12,10 @@
|
|
|
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 {
|
|
21
|
-
import
|
|
22
|
-
import
|
|
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,
|
|
@@ -31,17 +25,15 @@ import {
|
|
|
31
25
|
getMarkdownTheme,
|
|
32
26
|
ModelRegistry,
|
|
33
27
|
SettingsManager,
|
|
34
|
-
|
|
35
|
-
} from '@
|
|
36
|
-
import type
|
|
37
|
-
import { Box, Container, Markdown, Spacer, Text, type AutocompleteItem } from '@mariozechner/pi-tui';
|
|
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';
|
|
38
31
|
import { Type } from 'typebox';
|
|
39
32
|
import {
|
|
40
33
|
type AgentConfig,
|
|
41
34
|
type AgentScope,
|
|
42
35
|
type AgentSource,
|
|
43
|
-
|
|
44
|
-
discoverAgentsWithPackages,
|
|
36
|
+
discoverAgents,
|
|
45
37
|
} from './agents.js';
|
|
46
38
|
import { formatRunAgentUsage, parseRunAgentArgs } from './run-agent-args.js';
|
|
47
39
|
import { runAgent } from './agent-runner.js';
|
|
@@ -49,6 +41,7 @@ import { extractRecentConversation } from './synthesis.js';
|
|
|
49
41
|
import type { AgentResult } from './agent-runner-types.js';
|
|
50
42
|
import { getFinalText } from './agent-result-utils.js';
|
|
51
43
|
import { buildHandoffFromResult, renderHandoffForPrompt } from './handoffs.js';
|
|
44
|
+
import { emptyUsage, spawnPiAgent } from './spawn-utils.js';
|
|
52
45
|
|
|
53
46
|
const MAX_PARALLEL_TASKS = 8;
|
|
54
47
|
const MAX_CONCURRENCY = 4;
|
|
@@ -200,7 +193,7 @@ interface SingleResult {
|
|
|
200
193
|
interface SubagentDetails {
|
|
201
194
|
mode: 'single' | 'parallel' | 'chain';
|
|
202
195
|
agentScope: AgentScope;
|
|
203
|
-
|
|
196
|
+
projectPromptsDir: string | null;
|
|
204
197
|
results: SingleResult[];
|
|
205
198
|
}
|
|
206
199
|
|
|
@@ -254,39 +247,6 @@ async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
|
254
247
|
return results;
|
|
255
248
|
}
|
|
256
249
|
|
|
257
|
-
async function writePromptToTempFile(
|
|
258
|
-
agentName: string,
|
|
259
|
-
prompt: string,
|
|
260
|
-
): Promise<{ dir: string; filePath: string }> {
|
|
261
|
-
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pi-subagent-'));
|
|
262
|
-
const safeName = agentName.replace(/[^\w.-]+/g, '_');
|
|
263
|
-
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
264
|
-
await withFileMutationQueue(filePath, async () => {
|
|
265
|
-
await fs.promises.writeFile(filePath, prompt, { encoding: 'utf-8', mode: 0o600 });
|
|
266
|
-
});
|
|
267
|
-
return { dir: tmpDir, filePath };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
271
|
-
const currentScript = process.argv[1];
|
|
272
|
-
|
|
273
|
-
// Bun standalone binaries set argv[1] to a virtual FS path (e.g. /$bunfs/root/pi)
|
|
274
|
-
// that only resolves inside the running Bun process. Skip it — the binary IS the entry point.
|
|
275
|
-
const isBunVirtualPath = currentScript?.startsWith('/$bunfs/');
|
|
276
|
-
|
|
277
|
-
if (currentScript && !isBunVirtualPath && fs.existsSync(currentScript)) {
|
|
278
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
282
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
283
|
-
if (!isGenericRuntime) {
|
|
284
|
-
// Standalone binary (e.g. pi compiled with Bun) — invoke directly
|
|
285
|
-
return { command: process.execPath, args };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return { command: 'pi', args };
|
|
289
|
-
}
|
|
290
250
|
|
|
291
251
|
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
292
252
|
|
|
@@ -314,15 +274,7 @@ async function runSingleAgent(
|
|
|
314
274
|
exitCode: 1,
|
|
315
275
|
messages: [],
|
|
316
276
|
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
317
|
-
usage:
|
|
318
|
-
input: 0,
|
|
319
|
-
output: 0,
|
|
320
|
-
cacheRead: 0,
|
|
321
|
-
cacheWrite: 0,
|
|
322
|
-
cost: 0,
|
|
323
|
-
contextTokens: 0,
|
|
324
|
-
turns: 0,
|
|
325
|
-
},
|
|
277
|
+
usage: emptyUsage(),
|
|
326
278
|
step,
|
|
327
279
|
};
|
|
328
280
|
}
|
|
@@ -330,14 +282,6 @@ async function runSingleAgent(
|
|
|
330
282
|
const selectedModel = modelOverride ?? agent.model;
|
|
331
283
|
const selectedThinking = thinkingOverride ?? agent.thinking;
|
|
332
284
|
|
|
333
|
-
const args: string[] = ['--mode', 'json', '-p', '--no-session'];
|
|
334
|
-
if (selectedModel) args.push('--model', selectedModel);
|
|
335
|
-
if (selectedThinking) args.push('--thinking', selectedThinking);
|
|
336
|
-
if (agent.tools && agent.tools.length > 0) args.push('--tools', agent.tools.join(','));
|
|
337
|
-
|
|
338
|
-
let tmpPromptDir: string | null = null;
|
|
339
|
-
let tmpPromptPath: string | null = null;
|
|
340
|
-
|
|
341
285
|
const currentResult: SingleResult = {
|
|
342
286
|
agent: agentName,
|
|
343
287
|
agentSource: agent.source,
|
|
@@ -345,15 +289,7 @@ async function runSingleAgent(
|
|
|
345
289
|
exitCode: 0,
|
|
346
290
|
messages: [],
|
|
347
291
|
stderr: '',
|
|
348
|
-
usage:
|
|
349
|
-
input: 0,
|
|
350
|
-
output: 0,
|
|
351
|
-
cacheRead: 0,
|
|
352
|
-
cacheWrite: 0,
|
|
353
|
-
cost: 0,
|
|
354
|
-
contextTokens: 0,
|
|
355
|
-
turns: 0,
|
|
356
|
-
},
|
|
292
|
+
usage: emptyUsage(),
|
|
357
293
|
model: selectedModel,
|
|
358
294
|
step,
|
|
359
295
|
};
|
|
@@ -367,113 +303,39 @@ async function runSingleAgent(
|
|
|
367
303
|
}
|
|
368
304
|
};
|
|
369
305
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
let event: any;
|
|
393
|
-
try {
|
|
394
|
-
event = JSON.parse(line);
|
|
395
|
-
} catch {
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (event.type === 'message_end' && event.message) {
|
|
400
|
-
const msg = event.message as Message;
|
|
401
|
-
currentResult.messages.push(msg);
|
|
402
|
-
|
|
403
|
-
if (msg.role === 'assistant') {
|
|
404
|
-
currentResult.usage.turns++;
|
|
405
|
-
const usage = msg.usage;
|
|
406
|
-
if (usage) {
|
|
407
|
-
currentResult.usage.input += usage.input || 0;
|
|
408
|
-
currentResult.usage.output += usage.output || 0;
|
|
409
|
-
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
410
|
-
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
411
|
-
currentResult.usage.cost += usage.cost?.total || 0;
|
|
412
|
-
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
413
|
-
}
|
|
414
|
-
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
|
415
|
-
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
416
|
-
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
417
|
-
}
|
|
418
|
-
emitUpdate();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (event.type === 'tool_result_end' && event.message) {
|
|
422
|
-
currentResult.messages.push(event.message as Message);
|
|
423
|
-
emitUpdate();
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
proc.stdout.on('data', (data) => {
|
|
428
|
-
buffer += data.toString();
|
|
429
|
-
const lines = buffer.split('\n');
|
|
430
|
-
buffer = lines.pop() || '';
|
|
431
|
-
for (const line of lines) processLine(line);
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
proc.stderr.on('data', (data) => {
|
|
435
|
-
currentResult.stderr += data.toString();
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
proc.on('close', (code) => {
|
|
439
|
-
if (buffer.trim()) processLine(buffer);
|
|
440
|
-
resolve(code ?? 0);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
proc.on('error', () => {
|
|
444
|
-
resolve(1);
|
|
445
|
-
});
|
|
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
|
+
});
|
|
446
328
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
};
|
|
455
|
-
if (signal.aborted) killProc();
|
|
456
|
-
else signal.addEventListener('abort', killProc, { once: true });
|
|
457
|
-
}
|
|
458
|
-
});
|
|
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;
|
|
459
336
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
return currentResult;
|
|
463
|
-
} finally {
|
|
464
|
-
if (tmpPromptPath)
|
|
465
|
-
try {
|
|
466
|
-
fs.unlinkSync(tmpPromptPath);
|
|
467
|
-
} catch {
|
|
468
|
-
/* ignore */
|
|
469
|
-
}
|
|
470
|
-
if (tmpPromptDir)
|
|
471
|
-
try {
|
|
472
|
-
fs.rmdirSync(tmpPromptDir);
|
|
473
|
-
} catch {
|
|
474
|
-
/* ignore */
|
|
475
|
-
}
|
|
476
|
-
}
|
|
337
|
+
if (spawnResult.wasAborted) throw new Error('Subagent was aborted');
|
|
338
|
+
return currentResult;
|
|
477
339
|
}
|
|
478
340
|
|
|
479
341
|
const TaskItem = Type.Object({
|
|
@@ -531,24 +393,15 @@ const SubagentParams = Type.Object({
|
|
|
531
393
|
),
|
|
532
394
|
});
|
|
533
395
|
|
|
534
|
-
|
|
535
|
-
* Try to resolve package paths including the agents resource.
|
|
536
|
-
* Returns null if the pi version does not support ResolvedPaths.agents.
|
|
537
|
-
*/
|
|
538
|
-
async function tryResolvePackagePaths(cwd: string): Promise<ResolvedPaths | undefined> {
|
|
396
|
+
async function resolvePackagePaths(cwd: string): Promise<ResolvedPaths | undefined> {
|
|
539
397
|
try {
|
|
540
398
|
const agentDir = getAgentDir();
|
|
541
399
|
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
542
400
|
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
|
543
|
-
|
|
544
|
-
// Check if this pi version has agents support
|
|
545
|
-
if ('agents' in resolved && Array.isArray((resolved as Record<string, unknown>).agents)) {
|
|
546
|
-
return resolved;
|
|
547
|
-
}
|
|
401
|
+
return await packageManager.resolve();
|
|
548
402
|
} catch {
|
|
549
|
-
|
|
403
|
+
return undefined;
|
|
550
404
|
}
|
|
551
|
-
return undefined;
|
|
552
405
|
}
|
|
553
406
|
|
|
554
407
|
export default function (pi: ExtensionAPI) {
|
|
@@ -599,8 +452,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
599
452
|
}
|
|
600
453
|
|
|
601
454
|
async function getAutocompleteAgents(scope: AgentScope): Promise<AgentConfig[]> {
|
|
602
|
-
const resolvedPaths = await
|
|
603
|
-
return
|
|
455
|
+
const resolvedPaths = await resolvePackagePaths(autocompleteCwd);
|
|
456
|
+
return discoverAgents(autocompleteCwd, scope, resolvedPaths).agents;
|
|
604
457
|
}
|
|
605
458
|
|
|
606
459
|
async function getAutocompleteModels(scope: AgentScope): Promise<
|
|
@@ -645,15 +498,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
645
498
|
'Delegate tasks to specialized subagents with isolated context.',
|
|
646
499
|
'Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).',
|
|
647
500
|
'Optional per-run or per-step model and reasoning-level overrides are supported via model/thinking.',
|
|
648
|
-
'Default agent scope is "user" (from ~/.pi/agent/
|
|
649
|
-
'To enable project-local agents in .pi/
|
|
501
|
+
'Default agent scope is "user" (from ~/.pi/agent/prompts).',
|
|
502
|
+
'To enable project-local agents in .pi/prompts, set agentScope: "both" (or "project").',
|
|
650
503
|
].join(' '),
|
|
651
504
|
parameters: SubagentParams,
|
|
652
505
|
|
|
653
506
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
654
507
|
const agentScope: AgentScope = params.agentScope ?? 'user';
|
|
655
|
-
const resolvedPaths = await
|
|
656
|
-
const discovery =
|
|
508
|
+
const resolvedPaths = await resolvePackagePaths(ctx.cwd);
|
|
509
|
+
const discovery = discoverAgents(ctx.cwd, agentScope, resolvedPaths);
|
|
657
510
|
const agents = discovery.agents;
|
|
658
511
|
const confirmProjectAgents = params.confirmProjectAgents ?? true;
|
|
659
512
|
|
|
@@ -667,7 +520,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
667
520
|
(results: SingleResult[]): SubagentDetails => ({
|
|
668
521
|
mode,
|
|
669
522
|
agentScope,
|
|
670
|
-
|
|
523
|
+
projectPromptsDir: discovery.projectPromptsDir,
|
|
671
524
|
results,
|
|
672
525
|
});
|
|
673
526
|
|
|
@@ -700,7 +553,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
700
553
|
|
|
701
554
|
if (projectAgentsRequested.length > 0) {
|
|
702
555
|
const names = projectAgentsRequested.map((a) => a.name).join(', ');
|
|
703
|
-
const dir = discovery.
|
|
556
|
+
const dir = discovery.projectPromptsDir ?? '(unknown)';
|
|
704
557
|
const ok = await ctx.ui.confirm(
|
|
705
558
|
'Run project-local agents?',
|
|
706
559
|
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
@@ -736,6 +589,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
736
589
|
}
|
|
737
590
|
: undefined;
|
|
738
591
|
|
|
592
|
+
if (ctx.hasUI) {
|
|
593
|
+
ctx.ui.setWorkingMessage(`Chain step ${i + 1}/${params.chain.length}: ${step.agent}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
739
596
|
const result = await runSingleAgent(
|
|
740
597
|
ctx.cwd,
|
|
741
598
|
agents,
|
|
@@ -782,6 +639,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
782
639
|
}),
|
|
783
640
|
);
|
|
784
641
|
}
|
|
642
|
+
if (ctx.hasUI) {
|
|
643
|
+
ctx.ui.setWorkingMessage();
|
|
644
|
+
}
|
|
785
645
|
return {
|
|
786
646
|
content: [
|
|
787
647
|
{
|
|
@@ -845,6 +705,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
845
705
|
}
|
|
846
706
|
};
|
|
847
707
|
|
|
708
|
+
if (ctx.hasUI) {
|
|
709
|
+
ctx.ui.setWorkingMessage(`Running ${params.tasks.length} agents in parallel`);
|
|
710
|
+
}
|
|
711
|
+
|
|
848
712
|
const results = await mapWithConcurrencyLimit(
|
|
849
713
|
params.tasks,
|
|
850
714
|
MAX_CONCURRENCY,
|
|
@@ -874,6 +738,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
874
738
|
},
|
|
875
739
|
);
|
|
876
740
|
|
|
741
|
+
if (ctx.hasUI) {
|
|
742
|
+
ctx.ui.setWorkingMessage();
|
|
743
|
+
}
|
|
744
|
+
|
|
877
745
|
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
878
746
|
const summaries = results.map((r) => {
|
|
879
747
|
const output = getFinalOutput(r.messages);
|
|
@@ -891,6 +759,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
891
759
|
}
|
|
892
760
|
|
|
893
761
|
if (params.agent && params.task) {
|
|
762
|
+
if (ctx.hasUI) {
|
|
763
|
+
ctx.ui.setWorkingMessage(`Running agent: ${params.agent}`);
|
|
764
|
+
}
|
|
765
|
+
|
|
894
766
|
const result = await runSingleAgent(
|
|
895
767
|
ctx.cwd,
|
|
896
768
|
agents,
|
|
@@ -904,6 +776,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
904
776
|
onUpdate,
|
|
905
777
|
makeDetails('single'),
|
|
906
778
|
);
|
|
779
|
+
if (ctx.hasUI) {
|
|
780
|
+
ctx.ui.setWorkingMessage();
|
|
781
|
+
}
|
|
782
|
+
|
|
907
783
|
const isError =
|
|
908
784
|
result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted';
|
|
909
785
|
if (isError) {
|
|
@@ -1271,13 +1147,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1271
1147
|
async function confirmProjectAgentsIfNeeded(
|
|
1272
1148
|
ctx: ExtensionCommandContext,
|
|
1273
1149
|
agents: AgentConfig[],
|
|
1274
|
-
|
|
1150
|
+
projectPromptsDir: string | null,
|
|
1275
1151
|
confirmProjectAgents: boolean,
|
|
1276
1152
|
): Promise<boolean> {
|
|
1277
1153
|
if (!ctx.hasUI || !confirmProjectAgents || agents.length === 0) return true;
|
|
1278
1154
|
|
|
1279
1155
|
const names = Array.from(new Set(agents.map((agent) => agent.name))).join(', ');
|
|
1280
|
-
const dir =
|
|
1156
|
+
const dir = projectPromptsDir ?? '(unknown)';
|
|
1281
1157
|
return ctx.ui.confirm(
|
|
1282
1158
|
'Run project-local agents?',
|
|
1283
1159
|
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
@@ -1392,157 +1268,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1392
1268
|
return box;
|
|
1393
1269
|
});
|
|
1394
1270
|
|
|
1395
|
-
async function listMdFiles(dir: string): Promise<Set<string>> {
|
|
1396
|
-
if (!existsSync(dir)) return new Set();
|
|
1397
|
-
try {
|
|
1398
|
-
const entries = await readdir(dir);
|
|
1399
|
-
return new Set(entries.filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, '')));
|
|
1400
|
-
} catch {
|
|
1401
|
-
return new Set();
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
1271
|
pi.on('session_start', async (_event, ctx) => {
|
|
1406
1272
|
autocompleteCwd = ctx.cwd;
|
|
1407
1273
|
});
|
|
1408
1274
|
|
|
1409
|
-
pi.registerCommand('delegate-agents', {
|
|
1410
|
-
description: 'List, customize, or reset delegate agents',
|
|
1411
|
-
getArgumentCompletions: async (argumentText) => {
|
|
1412
|
-
const userDir = join(getAgentDir(), 'agents');
|
|
1413
|
-
const { completedTokens } = parseArgumentText(argumentText);
|
|
1414
|
-
const action = completedTokens[0];
|
|
1415
|
-
|
|
1416
|
-
if (!action) {
|
|
1417
|
-
return buildArgumentCompletions(argumentText, [
|
|
1418
|
-
{ value: 'list', description: 'Show bundled and overridden agents' },
|
|
1419
|
-
{ value: 'reset', description: 'Restore bundled agents by removing user overrides' },
|
|
1420
|
-
{ value: 'edit', description: 'Copy a bundled agent into your user agents directory' },
|
|
1421
|
-
]);
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
if (action === 'reset') {
|
|
1425
|
-
if (completedTokens.length > 1) return null;
|
|
1426
|
-
const bundled = await listMdFiles(bundledAgentsDir);
|
|
1427
|
-
const user = await listMdFiles(userDir);
|
|
1428
|
-
const resettable = [...user].filter((name) => bundled.has(name)).sort();
|
|
1429
|
-
return buildArgumentCompletions(argumentText, [
|
|
1430
|
-
{ value: '--all', description: 'Reset all user overrides that have bundled versions' },
|
|
1431
|
-
...resettable.map((name) => ({
|
|
1432
|
-
value: name,
|
|
1433
|
-
description: 'Reset this user override to the bundled version',
|
|
1434
|
-
})),
|
|
1435
|
-
]);
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
if (action === 'edit') {
|
|
1439
|
-
if (completedTokens.length > 1) return null;
|
|
1440
|
-
const bundled = [...(await listMdFiles(bundledAgentsDir))].sort();
|
|
1441
|
-
return buildArgumentCompletions(
|
|
1442
|
-
argumentText,
|
|
1443
|
-
bundled.map((name) => ({
|
|
1444
|
-
value: name,
|
|
1445
|
-
description: 'Copy this bundled agent into your user agents directory',
|
|
1446
|
-
})),
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
return null;
|
|
1451
|
-
},
|
|
1452
|
-
handler: async (args, ctx) => {
|
|
1453
|
-
autocompleteCwd = ctx.cwd;
|
|
1454
|
-
const userDir = join(getAgentDir(), 'agents');
|
|
1455
|
-
const parts = args?.trim().split(/\s+/) || [];
|
|
1456
|
-
const action = parts[0] || 'list';
|
|
1457
|
-
|
|
1458
|
-
if (action === 'list') {
|
|
1459
|
-
const bundled = await listMdFiles(bundledAgentsDir);
|
|
1460
|
-
const user = await listMdFiles(userDir);
|
|
1461
|
-
const allNames = new Set([...bundled, ...user]);
|
|
1462
|
-
|
|
1463
|
-
const lines: string[] = ['## Delegate Agents\n'];
|
|
1464
|
-
for (const name of [...allNames].sort()) {
|
|
1465
|
-
const isBundled = bundled.has(name);
|
|
1466
|
-
const isUser = user.has(name);
|
|
1467
|
-
if (isUser && isBundled) {
|
|
1468
|
-
lines.push(`- **${name}** — user override (bundled version available, \`/delegate-agents reset ${name}\` to restore)`);
|
|
1469
|
-
} else if (isUser) {
|
|
1470
|
-
lines.push(`- **${name}** — user-only`);
|
|
1471
|
-
} else {
|
|
1472
|
-
lines.push(`- **${name}** — bundled`);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
pi.sendMessage({ customType: 'delegate-agents-list', content: lines.join('\n'), display: true });
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
if (action === 'reset') {
|
|
1480
|
-
const name = parts[1];
|
|
1481
|
-
if (!name) {
|
|
1482
|
-
ctx.ui.notify('Usage: /delegate-agents reset <name|--all>', 'warning');
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
if (name === '--all') {
|
|
1487
|
-
const user = await listMdFiles(userDir);
|
|
1488
|
-
const bundled = await listMdFiles(bundledAgentsDir);
|
|
1489
|
-
let count = 0;
|
|
1490
|
-
for (const n of user) {
|
|
1491
|
-
if (bundled.has(n)) {
|
|
1492
|
-
await unlink(join(userDir, `${n}.md`));
|
|
1493
|
-
count++;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
ctx.ui.notify(count > 0 ? `Reset ${count} agent(s) to bundled versions.` : 'No user overrides to reset.', 'info');
|
|
1497
|
-
return;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const userFile = join(userDir, `${name}.md`);
|
|
1501
|
-
const bundledFile = join(bundledAgentsDir, `${name}.md`);
|
|
1502
|
-
|
|
1503
|
-
if (!existsSync(userFile)) {
|
|
1504
|
-
ctx.ui.notify(`No user override for "${name}" — already using bundled version.`, 'info');
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
if (!existsSync(bundledFile)) {
|
|
1508
|
-
ctx.ui.notify(`"${name}" is user-only (no bundled version). Delete manually if needed.`, 'warning');
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
await unlink(userFile);
|
|
1513
|
-
ctx.ui.notify(`Reset "${name}" — now using bundled version.`, 'info');
|
|
1514
|
-
return;
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
if (action === 'edit') {
|
|
1518
|
-
const name = parts[1];
|
|
1519
|
-
if (!name) {
|
|
1520
|
-
ctx.ui.notify('Usage: /delegate-agents edit <name>', 'warning');
|
|
1521
|
-
return;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
const bundledFile = join(bundledAgentsDir, `${name}.md`);
|
|
1525
|
-
const userFile = join(userDir, `${name}.md`);
|
|
1526
|
-
|
|
1527
|
-
if (existsSync(userFile)) {
|
|
1528
|
-
ctx.ui.notify(`User override already exists: ${userFile}`, 'info');
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
if (!existsSync(bundledFile)) {
|
|
1532
|
-
ctx.ui.notify(`No bundled agent named "${name}".`, 'warning');
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
await mkdir(userDir, { recursive: true });
|
|
1537
|
-
await copyFile(bundledFile, userFile);
|
|
1538
|
-
ctx.ui.notify(`Copied "${name}" to ${userFile} — edit it there to customize.`, 'info');
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
ctx.ui.notify('Unknown action. Usage: /delegate-agents [list|reset <name|--all>|edit <name>]', 'warning');
|
|
1543
|
-
},
|
|
1544
|
-
});
|
|
1545
|
-
|
|
1546
1275
|
pi.registerCommand('run-agent', {
|
|
1547
1276
|
description:
|
|
1548
1277
|
'Run a single agent directly — honors agent sessionStrategy metadata such as fork-at',
|
|
@@ -1589,8 +1318,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1589
1318
|
|
|
1590
1319
|
if (expectsScopeValue) {
|
|
1591
1320
|
return buildArgumentCompletions(argumentText, [
|
|
1592
|
-
{ value: 'user', description: 'Only user/global agents from ~/.pi/agent/
|
|
1593
|
-
{ value: 'project', description: 'Only project-local agents from .pi/
|
|
1321
|
+
{ value: 'user', description: 'Only user/global agents from ~/.pi/agent/prompts' },
|
|
1322
|
+
{ value: 'project', description: 'Only project-local agents from .pi/prompts' },
|
|
1594
1323
|
{ value: 'both', description: 'User/global agents plus project-local agents' },
|
|
1595
1324
|
]);
|
|
1596
1325
|
}
|
|
@@ -1640,8 +1369,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1640
1369
|
return;
|
|
1641
1370
|
}
|
|
1642
1371
|
|
|
1643
|
-
const resolvedPaths = await
|
|
1644
|
-
const discovery =
|
|
1372
|
+
const resolvedPaths = await resolvePackagePaths(ctx.cwd);
|
|
1373
|
+
const discovery = discoverAgents(ctx.cwd, agentScope, resolvedPaths);
|
|
1645
1374
|
const agent = discovery.agents.find((candidate) => candidate.name === agentName);
|
|
1646
1375
|
|
|
1647
1376
|
if (!agent) {
|
|
@@ -1654,7 +1383,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1654
1383
|
const approved = await confirmProjectAgentsIfNeeded(
|
|
1655
1384
|
ctx,
|
|
1656
1385
|
requestedProjectAgents,
|
|
1657
|
-
discovery.
|
|
1386
|
+
discovery.projectPromptsDir,
|
|
1658
1387
|
confirmProjectAgents,
|
|
1659
1388
|
);
|
|
1660
1389
|
if (!approved) {
|