@melihmucuk/pi-crew 1.0.16 → 1.0.17

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 (35) hide show
  1. package/README.md +8 -8
  2. package/agents/code-reviewer.md +2 -2
  3. package/agents/oracle.md +1 -1
  4. package/agents/planner.md +5 -1
  5. package/agents/quality-reviewer.md +2 -2
  6. package/agents/scout.md +2 -2
  7. package/agents/worker.md +3 -3
  8. package/extension/agent-catalog.ts +369 -0
  9. package/extension/agent-config-fields.ts +359 -0
  10. package/extension/agent-discovery.ts +49 -717
  11. package/extension/index.ts +4 -2
  12. package/extension/integration/crew-tool-actions.ts +306 -0
  13. package/extension/integration/crew-tool-executor.ts +109 -0
  14. package/extension/integration/register-tools.ts +10 -2
  15. package/extension/integration/tool-presentation.ts +0 -20
  16. package/extension/integration/tools/crew-abort.ts +14 -84
  17. package/extension/integration/tools/crew-done.ts +7 -26
  18. package/extension/integration/tools/crew-list.ts +4 -60
  19. package/extension/integration/tools/crew-respond.ts +8 -29
  20. package/extension/integration/tools/crew-spawn.ts +15 -56
  21. package/extension/message-delivery-policy.ts +22 -0
  22. package/extension/runtime/crew-runtime.ts +60 -223
  23. package/extension/runtime/{delivery-coordinator.ts → owner-session-coordinator.ts} +44 -37
  24. package/extension/runtime/subagent-lifecycle.ts +203 -0
  25. package/extension/runtime/subagent-registry.ts +50 -6
  26. package/extension/runtime/subagent-transitions.ts +100 -0
  27. package/extension/subagent-messages.ts +9 -17
  28. package/package.json +8 -6
  29. package/prompts/pi-crew-plan.md +14 -13
  30. package/prompts/pi-crew-review.md +20 -16
  31. package/skills/pi-crew/REFERENCE.md +32 -20
  32. package/skills/pi-crew/SKILL.md +13 -10
  33. package/extension/integration/tools/tool-deps.ts +0 -16
  34. package/extension/integration.ts +0 -13
  35. package/extension/runtime/subagent-state.ts +0 -59
@@ -1,438 +1,37 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
5
- import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
6
- import { type SupportedToolName, isSupportedToolName } from "./tool-registry.js";
7
-
8
- interface ParsedModel {
9
- provider: string;
10
- modelId: string;
11
- }
12
-
13
- export interface AgentConfig {
14
- name: string;
15
- description: string;
16
- model?: string;
17
- parsedModel?: ParsedModel;
18
- thinking?: ThinkingLevel;
19
- tools?: SupportedToolName[];
20
- skills?: string[];
21
- compaction?: boolean;
22
- interactive?: boolean;
23
- systemPrompt: string;
24
- filePath: string;
25
- }
26
-
27
- interface AgentConfigOverride {
28
- model?: string;
29
- parsedModel?: ParsedModel;
30
- thinking?: ThinkingLevel;
31
- tools?: SupportedToolName[];
32
- skills?: string[];
33
- compaction?: boolean;
34
- interactive?: boolean;
35
- }
36
-
37
- export interface AgentDiscoveryWarning {
38
- filePath: string;
39
- message: string;
40
- }
41
-
42
- interface AgentDiscoveryResult {
43
- agents: AgentConfig[];
44
- warnings: AgentDiscoveryWarning[];
45
- }
46
-
47
- interface ParseResult {
48
- agent: AgentConfig | null;
49
- warnings: AgentDiscoveryWarning[];
50
- }
51
-
52
- interface FileLoadResult {
53
- content: string | null;
54
- warnings: AgentDiscoveryWarning[];
55
- }
56
-
57
- interface DirectoryLoadResult {
58
- filePaths: string[];
59
- warnings: AgentDiscoveryWarning[];
60
- }
61
-
62
- interface ConfigParseResult {
63
- overrides: Record<string, AgentConfigOverride>;
64
- overrideSources: Record<string, string>;
65
- warnings: AgentDiscoveryWarning[];
66
- }
67
-
68
- const VALID_THINKING_LEVELS: readonly string[] = [
69
- "off",
70
- "minimal",
71
- "low",
72
- "medium",
73
- "high",
74
- "xhigh",
75
- ];
76
-
77
- const ALLOWED_OVERRIDE_FIELDS = new Set([
78
- "model",
79
- "thinking",
80
- "tools",
81
- "skills",
82
- "compaction",
83
- "interactive",
84
- ]);
4
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ AgentCatalog,
7
+ type AgentCatalogSource,
8
+ type AgentConfigFile,
9
+ type AgentDefinitionFile,
10
+ type AgentDefinitionSourceGroup,
11
+ type AgentDiscoveryResult,
12
+ type AgentDiscoveryWarning,
13
+ } from "./agent-catalog.js";
14
+
15
+ export type {
16
+ AgentConfig,
17
+ AgentDiscoveryResult,
18
+ AgentDiscoveryWarning,
19
+ } from "./agent-catalog.js";
85
20
 
86
21
  function createDiscoveryWarning(filePath: string, message: string): AgentDiscoveryWarning {
87
22
  return { filePath, message };
88
23
  }
89
24
 
90
- /**
91
- * Converts a comma-separated string or YAML array to string[].
92
- * Returns undefined for null/undefined input.
93
- */
94
- function parseCommaSeparated(value: unknown): string[] | undefined {
95
- if (value == null) return undefined;
96
-
97
- if (Array.isArray(value)) {
98
- return value.map((v) => String(v).trim()).filter(Boolean);
99
- }
100
-
101
- if (typeof value === "string") {
102
- return value
103
- .split(",")
104
- .map((s) => s.trim())
105
- .filter(Boolean);
106
- }
107
-
108
- return undefined;
109
- }
110
-
111
- type ParsedFieldName = "model" | "thinking" | "tools" | "skills" | "compaction" | "interactive";
112
- type ParsedListFieldName = "tools" | "skills";
113
- type ParsedBooleanFieldName = "compaction" | "interactive";
114
- type WarningSubject = "subagent" | "subagent override";
115
-
116
- type ParsedFieldWarning =
117
- | {
118
- code: "invalid-list-format";
119
- fieldName: ParsedListFieldName;
120
- }
121
- | {
122
- code: "invalid-type";
123
- fieldName: ParsedFieldName;
124
- expected: "string" | "boolean";
125
- }
126
- | {
127
- code: "invalid-model-format";
128
- model: string;
129
- }
130
- | {
131
- code: "invalid-thinking-level";
132
- thinking: string;
133
- }
134
- | {
135
- code: "unknown-tools";
136
- tools: string[];
137
- };
138
-
139
- interface ParseFieldOptions {
140
- warnOnInvalidType: boolean;
141
- setValueOnInvalidType: boolean;
142
- }
143
-
144
- interface ParsedFieldSet {
145
- model?: string;
146
- parsedModel?: ParsedModel;
147
- thinking?: ThinkingLevel;
148
- tools?: SupportedToolName[];
149
- skills?: string[];
150
- compaction?: boolean;
151
- interactive?: boolean;
152
- warnings: ParsedFieldWarning[];
153
- }
154
-
155
- function formatFieldWarning(subject: WarningSubject, name: string, warning: ParsedFieldWarning): string {
156
- const prefix = `${subject === "subagent" ? "Subagent" : "Subagent override"} "${name}"`;
157
-
158
- switch (warning.code) {
159
- case "invalid-list-format":
160
- return `${prefix}: invalid ${warning.fieldName} field, expected a comma-separated string or YAML array`;
161
- case "invalid-type":
162
- return `${prefix}: field "${warning.fieldName}" must be a ${warning.expected}, ignoring`;
163
- case "invalid-model-format":
164
- return `${prefix}: invalid model format "${warning.model}" (expected "provider/model-id"), ignoring model field`;
165
- case "invalid-thinking-level":
166
- return `${prefix}: invalid thinking level "${warning.thinking}", ignoring`;
167
- case "unknown-tools":
168
- return `${prefix}: unknown tools ${warning.tools.map((toolName) => `"${toolName}"`).join(", ")}, ignoring`;
169
- }
170
- }
171
-
172
- function toDiscoveryWarnings(
173
- filePath: string,
174
- subject: WarningSubject,
175
- name: string,
176
- warnings: ParsedFieldWarning[],
177
- ): AgentDiscoveryWarning[] {
178
- return warnings.map((warning) => createDiscoveryWarning(filePath, formatFieldWarning(subject, name, warning)));
179
- }
180
-
181
- function parseListField(value: unknown, fieldName: ParsedListFieldName): { values: string[]; warnings: ParsedFieldWarning[] } {
182
- if (value == null) return { values: [], warnings: [] };
183
-
184
- const parsed = parseCommaSeparated(value);
185
- if (parsed !== undefined) return { values: parsed, warnings: [] };
186
-
187
- return {
188
- values: [],
189
- warnings: [{ code: "invalid-list-format", fieldName }],
190
- };
191
- }
192
-
193
- /**
194
- * Parses "provider/model-id" format.
195
- * Returns null if "/" is missing.
196
- */
197
- function parseModel(value: unknown): ParsedModel | null {
198
- if (typeof value !== "string" || !value.includes("/")) {
199
- return null;
200
- }
201
-
202
- const slashIndex = value.indexOf("/");
203
- const provider = value.slice(0, slashIndex).trim();
204
- const modelId = value.slice(slashIndex + 1).trim();
205
-
206
- if (!provider || !modelId) return null;
207
-
208
- return { provider, modelId };
209
- }
210
-
211
- function validateThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
212
- if (!value) return undefined;
213
- if (VALID_THINKING_LEVELS.includes(value)) return value as ThinkingLevel;
214
- return undefined;
215
- }
216
-
217
- function parseModelField(value: unknown, options: ParseFieldOptions): Pick<ParsedFieldSet, "model" | "parsedModel" | "warnings"> {
218
- if (typeof value === "string") {
219
- const parsedModel = parseModel(value);
220
- if (!parsedModel) {
221
- return {
222
- ...(options.setValueOnInvalidType ? { model: value } : {}),
223
- warnings: [{ code: "invalid-model-format", model: value }],
224
- };
225
- }
226
-
227
- return {
228
- model: value,
229
- parsedModel,
230
- warnings: [],
231
- };
232
- }
233
-
234
- if (value !== undefined && options.warnOnInvalidType) {
235
- return {
236
- warnings: [{ code: "invalid-type", fieldName: "model", expected: "string" }],
237
- };
238
- }
239
-
240
- return { warnings: [] };
241
- }
242
-
243
- function parseThinkingField(value: unknown, options: ParseFieldOptions): Pick<ParsedFieldSet, "thinking" | "warnings"> {
244
- if (typeof value === "string") {
245
- const thinking = validateThinkingLevel(value);
246
- if (!thinking) {
247
- return {
248
- warnings: [{ code: "invalid-thinking-level", thinking: value }],
249
- };
250
- }
251
-
252
- return { thinking, warnings: [] };
253
- }
254
-
255
- if (value !== undefined && options.warnOnInvalidType) {
256
- return {
257
- warnings: [{ code: "invalid-type", fieldName: "thinking", expected: "string" }],
258
- };
259
- }
260
-
261
- return { warnings: [] };
262
- }
263
-
264
- function parseToolsField(value: unknown, options: ParseFieldOptions): Pick<ParsedFieldSet, "tools" | "warnings"> {
265
- const parsedTools = parseListField(value, "tools");
266
- const validTools = parsedTools.values.filter(isSupportedToolName);
267
- const invalidTools = parsedTools.values.filter((toolName) => !isSupportedToolName(toolName));
268
- const warnings: ParsedFieldWarning[] = [...parsedTools.warnings];
269
-
270
- if (invalidTools.length > 0) {
271
- warnings.push({ code: "unknown-tools", tools: invalidTools });
272
- }
273
-
274
- if (invalidTools.length > 0 && validTools.length === 0 && !options.setValueOnInvalidType) {
275
- return { warnings };
276
- }
277
-
278
- if (parsedTools.warnings.length > 0 && !options.setValueOnInvalidType) {
279
- return { warnings };
280
- }
281
-
282
- return {
283
- tools: validTools,
284
- warnings,
285
- };
286
- }
287
-
288
- function parseSkillsField(value: unknown, options: ParseFieldOptions): Pick<ParsedFieldSet, "skills" | "warnings"> {
289
- const parsedSkills = parseListField(value, "skills");
290
- if (parsedSkills.warnings.length > 0 && !options.setValueOnInvalidType) {
291
- return { warnings: parsedSkills.warnings };
292
- }
293
-
294
- return {
295
- skills: parsedSkills.values,
296
- warnings: parsedSkills.warnings,
297
- };
298
- }
299
-
300
- function parseBooleanField(
301
- fieldName: ParsedBooleanFieldName,
302
- value: unknown,
303
- options: ParseFieldOptions,
304
- ): Pick<ParsedFieldSet, ParsedBooleanFieldName | "warnings"> {
305
- if (typeof value === "boolean") {
306
- return {
307
- [fieldName]: value,
308
- warnings: [],
309
- };
310
- }
311
-
312
- if (value !== undefined && options.warnOnInvalidType) {
313
- return {
314
- warnings: [{ code: "invalid-type", fieldName, expected: "boolean" }],
315
- };
316
- }
317
-
318
- return { warnings: [] };
319
- }
320
-
321
- function parseSharedFields(record: Record<string, unknown>, options: ParseFieldOptions): ParsedFieldSet {
322
- const model = parseModelField(record.model, options);
323
- const thinking = parseThinkingField(record.thinking, options);
324
- const tools = Object.prototype.hasOwnProperty.call(record, "tools")
325
- ? parseToolsField(record.tools, options)
326
- : { warnings: [] };
327
- const skills = Object.prototype.hasOwnProperty.call(record, "skills")
328
- ? parseSkillsField(record.skills, options)
329
- : { warnings: [] };
330
- const compaction = parseBooleanField("compaction", record.compaction, options);
331
- const interactive = parseBooleanField("interactive", record.interactive, options);
332
-
333
- return {
334
- ...("model" in model ? { model: model.model } : {}),
335
- ...("parsedModel" in model ? { parsedModel: model.parsedModel } : {}),
336
- ...(thinking.thinking !== undefined ? { thinking: thinking.thinking } : {}),
337
- ...(tools.tools !== undefined ? { tools: tools.tools } : {}),
338
- ...(skills.skills !== undefined ? { skills: skills.skills } : {}),
339
- ...(compaction.compaction !== undefined ? { compaction: compaction.compaction } : {}),
340
- ...(interactive.interactive !== undefined ? { interactive: interactive.interactive } : {}),
341
- warnings: [
342
- ...model.warnings,
343
- ...thinking.warnings,
344
- ...tools.warnings,
345
- ...skills.warnings,
346
- ...compaction.warnings,
347
- ...interactive.warnings,
348
- ],
349
- };
350
- }
351
-
352
- function parseAgentDefinition(content: string, filePath: string): ParseResult {
353
- const warnings: AgentDiscoveryWarning[] = [];
354
-
355
- let frontmatter: Record<string, unknown>;
356
- let body: string;
25
+ function loadAgentFile(filePath: string): AgentDefinitionFile {
357
26
  try {
358
- const parsed = parseFrontmatter<Record<string, unknown>>(content);
359
- frontmatter = parsed.frontmatter;
360
- body = parsed.body;
361
- } catch (error) {
362
- const reason = error instanceof Error ? error.message : String(error);
363
27
  return {
364
- agent: null,
365
- warnings: [
366
- createDiscoveryWarning(
367
- filePath,
368
- `Ignored invalid subagent definition. Frontmatter could not be parsed: ${reason}`,
369
- ),
370
- ],
371
- };
372
- }
373
-
374
- const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : undefined;
375
- const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : undefined;
376
-
377
- if (!name || !description) {
378
- return {
379
- agent: null,
380
- warnings: [
381
- createDiscoveryWarning(
382
- filePath,
383
- 'Ignored invalid subagent definition. Required frontmatter fields "name" and "description" must be non-empty strings.',
384
- ),
385
- ],
386
- };
387
- }
388
-
389
- if (/\s/.test(name)) {
390
- return {
391
- agent: null,
392
- warnings: [
393
- createDiscoveryWarning(
394
- filePath,
395
- `Ignored subagent definition "${name}". Subagent names cannot contain whitespace. Use "-" instead.`,
396
- ),
397
- ],
398
- };
399
- }
400
-
401
- const parsedFields = parseSharedFields(frontmatter, {
402
- warnOnInvalidType: false,
403
- setValueOnInvalidType: true,
404
- });
405
- warnings.push(...toDiscoveryWarnings(filePath, "subagent", name, parsedFields.warnings));
406
-
407
- const { model, parsedModel, thinking, tools, skills, compaction, interactive } = parsedFields;
408
-
409
- return {
410
- agent: {
411
- name,
412
- description,
413
- model,
414
- parsedModel: parsedModel ?? undefined,
415
- thinking,
416
- tools,
417
- skills,
418
- compaction,
419
- interactive,
420
- systemPrompt: body,
421
28
  filePath,
422
- },
423
- warnings,
424
- };
425
- }
426
-
427
- function loadAgentFile(filePath: string): FileLoadResult {
428
- try {
429
- return {
430
29
  content: fs.readFileSync(filePath, "utf-8"),
431
- warnings: [],
432
30
  };
433
31
  } catch (error) {
434
32
  const reason = error instanceof Error ? error.message : String(error);
435
33
  return {
34
+ filePath,
436
35
  content: null,
437
36
  warnings: [
438
37
  createDiscoveryWarning(
@@ -444,27 +43,17 @@ function loadAgentFile(filePath: string): FileLoadResult {
444
43
  }
445
44
  }
446
45
 
447
- function loadAgentDefinitionFromFile(filePath: string): ParseResult {
448
- const file = loadAgentFile(filePath);
449
- if (!file.content) {
450
- return { agent: null, warnings: file.warnings };
451
- }
452
-
453
- const parsed = parseAgentDefinition(file.content, filePath);
454
- return {
455
- agent: parsed.agent,
456
- warnings: [...file.warnings, ...parsed.warnings],
457
- };
458
- }
46
+ function loadAgentDefinitionGroup(agentsDir: string): AgentDefinitionSourceGroup | null {
47
+ if (!fs.existsSync(agentsDir)) return null;
459
48
 
460
- function loadAgentDefinitionFiles(agentsDir: string): DirectoryLoadResult {
461
49
  let entries: fs.Dirent[];
462
50
  try {
463
51
  entries = fs.readdirSync(agentsDir, { withFileTypes: true });
464
52
  } catch (error) {
465
53
  const reason = error instanceof Error ? error.message : String(error);
466
54
  return {
467
- filePaths: [],
55
+ agentsDir,
56
+ files: [],
468
57
  warnings: [
469
58
  createDiscoveryWarning(
470
59
  agentsDir,
@@ -475,176 +64,27 @@ function loadAgentDefinitionFiles(agentsDir: string): DirectoryLoadResult {
475
64
  }
476
65
 
477
66
  return {
478
- filePaths: entries
67
+ agentsDir,
68
+ files: entries
479
69
  .filter((entry) => entry.name.endsWith(".md"))
480
70
  .filter((entry) => entry.isFile() || entry.isSymbolicLink())
481
- .map((entry) => path.join(agentsDir, entry.name)),
482
- warnings: [],
71
+ .map((entry) => loadAgentFile(path.join(agentsDir, entry.name))),
483
72
  };
484
73
  }
485
74
 
486
- function parseOverrideFields(
487
- agentName: string,
488
- value: unknown,
489
- filePath: string,
490
- ): { override: AgentConfigOverride | null; warnings: AgentDiscoveryWarning[] } {
491
- const warnings: AgentDiscoveryWarning[] = [];
492
-
493
- if (!value || typeof value !== "object" || Array.isArray(value)) {
494
- return {
495
- override: null,
496
- warnings: [
497
- createDiscoveryWarning(
498
- filePath,
499
- `Subagent override "${agentName}" must be a JSON object, ignoring`,
500
- ),
501
- ],
502
- };
503
- }
504
-
505
- const record = value as Record<string, unknown>;
506
-
507
- for (const fieldName of Object.keys(record)) {
508
- if (fieldName === "name" || fieldName === "description") {
509
- warnings.push(
510
- createDiscoveryWarning(
511
- filePath,
512
- `Subagent override "${agentName}": field "${fieldName}" is not overridable, ignoring`,
513
- ),
514
- );
515
- continue;
516
- }
517
-
518
- if (!ALLOWED_OVERRIDE_FIELDS.has(fieldName)) {
519
- warnings.push(
520
- createDiscoveryWarning(
521
- filePath,
522
- `Subagent override "${agentName}": unknown field "${fieldName}", ignoring`,
523
- ),
524
- );
525
- }
526
- }
527
-
528
- const parsedFields = parseSharedFields(record, {
529
- warnOnInvalidType: true,
530
- setValueOnInvalidType: false,
531
- });
532
- warnings.push(...toDiscoveryWarnings(filePath, "subagent override", agentName, parsedFields.warnings));
75
+ function loadConfigFile(filePath: string): AgentConfigFile | null {
76
+ if (!fs.existsSync(filePath)) return null;
533
77
 
534
- const override: AgentConfigOverride = {};
535
- if (parsedFields.model !== undefined) {
536
- override.model = parsedFields.model;
537
- }
538
- if (parsedFields.parsedModel !== undefined) {
539
- override.parsedModel = parsedFields.parsedModel;
540
- }
541
- if (parsedFields.thinking !== undefined) {
542
- override.thinking = parsedFields.thinking;
543
- }
544
- if (parsedFields.tools !== undefined) {
545
- override.tools = parsedFields.tools;
546
- }
547
- if (parsedFields.skills !== undefined) {
548
- override.skills = parsedFields.skills;
549
- }
550
- if (parsedFields.compaction !== undefined) {
551
- override.compaction = parsedFields.compaction;
552
- }
553
- if (parsedFields.interactive !== undefined) {
554
- override.interactive = parsedFields.interactive;
555
- }
556
-
557
- return { override, warnings };
558
- }
559
-
560
- function parseConfigFile(content: string, filePath: string): ConfigParseResult {
561
- let parsed: unknown;
562
78
  try {
563
- parsed = JSON.parse(content);
564
- } catch (error) {
565
- const reason = error instanceof Error ? error.message : String(error);
566
- return {
567
- overrides: {},
568
- overrideSources: {},
569
- warnings: [
570
- createDiscoveryWarning(
571
- filePath,
572
- `Ignored pi-crew config. JSON could not be parsed: ${reason}`,
573
- ),
574
- ],
575
- };
576
- }
577
-
578
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
579
- return {
580
- overrides: {},
581
- overrideSources: {},
582
- warnings: [
583
- createDiscoveryWarning(
584
- filePath,
585
- "Ignored pi-crew config. Root value must be a JSON object.",
586
- ),
587
- ],
588
- };
589
- }
590
-
591
- const root = parsed as Record<string, unknown>;
592
- if (root.agents === undefined) {
593
- return { overrides: {}, overrideSources: {}, warnings: [] };
594
- }
595
-
596
- if (!root.agents || typeof root.agents !== "object" || Array.isArray(root.agents)) {
597
79
  return {
598
- overrides: {},
599
- overrideSources: {},
600
- warnings: [
601
- createDiscoveryWarning(
602
- filePath,
603
- 'Ignored pi-crew config. Field "agents" must be a JSON object.',
604
- ),
605
- ],
80
+ filePath,
81
+ content: fs.readFileSync(filePath, "utf-8"),
606
82
  };
607
- }
608
-
609
- const overrides: Record<string, AgentConfigOverride> = {};
610
- const overrideSources: Record<string, string> = {};
611
- const warnings: AgentDiscoveryWarning[] = [];
612
-
613
- for (const [agentName, value] of Object.entries(root.agents)) {
614
- if (!agentName.trim()) {
615
- warnings.push(
616
- createDiscoveryWarning(
617
- filePath,
618
- "Ignored pi-crew config entry with empty subagent name.",
619
- ),
620
- );
621
- continue;
622
- }
623
-
624
- const parsedOverride = parseOverrideFields(agentName, value, filePath);
625
- warnings.push(...parsedOverride.warnings);
626
- if (parsedOverride.override) {
627
- overrides[agentName] = parsedOverride.override;
628
- overrideSources[agentName] = filePath;
629
- }
630
- }
631
-
632
- return { overrides, overrideSources, warnings };
633
- }
634
-
635
- function loadConfigOverridesFromFile(filePath: string): ConfigParseResult {
636
- if (!fs.existsSync(filePath)) {
637
- return { overrides: {}, overrideSources: {}, warnings: [] };
638
- }
639
-
640
- try {
641
- const content = fs.readFileSync(filePath, "utf-8");
642
- return parseConfigFile(content, filePath);
643
83
  } catch (error) {
644
84
  const reason = error instanceof Error ? error.message : String(error);
645
85
  return {
646
- overrides: {},
647
- overrideSources: {},
86
+ filePath,
87
+ content: null,
648
88
  warnings: [
649
89
  createDiscoveryWarning(
650
90
  filePath,
@@ -655,137 +95,29 @@ function loadConfigOverridesFromFile(filePath: string): ConfigParseResult {
655
95
  }
656
96
  }
657
97
 
658
- function mergeConfigOverrides(
659
- base: Record<string, AgentConfigOverride>,
660
- override: Record<string, AgentConfigOverride>,
661
- ): Record<string, AgentConfigOverride> {
662
- const merged: Record<string, AgentConfigOverride> = { ...base };
663
-
664
- for (const [agentName, agentOverride] of Object.entries(override)) {
665
- merged[agentName] = {
666
- ...(merged[agentName] ?? {}),
667
- ...agentOverride,
668
- };
669
- }
670
-
671
- return merged;
672
- }
673
-
674
- function mergeOverrideSources(
675
- base: Record<string, string>,
676
- override: Record<string, string>,
677
- ): Record<string, string> {
678
- return {
679
- ...base,
680
- ...override,
681
- };
682
- }
683
-
684
- function loadConfigOverrides(cwd: string): ConfigParseResult {
685
- const globalPath = path.join(getAgentDir(), "pi-crew.json");
686
- const projectPath = path.join(cwd, ".pi", "pi-crew.json");
687
-
688
- const globalConfig = loadConfigOverridesFromFile(globalPath);
689
- const projectConfig = loadConfigOverridesFromFile(projectPath);
690
-
691
- return {
692
- overrides: mergeConfigOverrides(globalConfig.overrides, projectConfig.overrides),
693
- overrideSources: mergeOverrideSources(globalConfig.overrideSources, projectConfig.overrideSources),
694
- warnings: [...globalConfig.warnings, ...projectConfig.warnings],
695
- };
696
- }
697
-
698
- function applyAgentOverride(agent: AgentConfig, override: AgentConfigOverride): AgentConfig {
699
- return {
700
- ...agent,
701
- ...(override.model !== undefined ? { model: override.model, parsedModel: override.parsedModel } : {}),
702
- ...(override.thinking !== undefined ? { thinking: override.thinking } : {}),
703
- ...(override.tools !== undefined ? { tools: override.tools } : {}),
704
- ...(override.skills !== undefined ? { skills: override.skills } : {}),
705
- ...(override.compaction !== undefined ? { compaction: override.compaction } : {}),
706
- ...(override.interactive !== undefined ? { interactive: override.interactive } : {}),
707
- };
708
- }
709
-
710
98
  const bundledAgentsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
711
99
 
712
- /**
713
- * Loads agents from a single directory into the agents list.
714
- * Skips agents whose name already exists in seenNames (higher-priority source wins).
715
- * Within the same directory, duplicate names produce a warning.
716
- */
717
- function loadAgentsFromDir(
718
- agentsDir: string,
719
- seenNames: Map<string, string>,
720
- agents: AgentConfig[],
721
- warnings: AgentDiscoveryWarning[],
722
- ): void {
723
- if (!fs.existsSync(agentsDir)) return;
724
-
725
- const fileLoad = loadAgentDefinitionFiles(agentsDir);
726
- warnings.push(...fileLoad.warnings);
727
-
728
- const dirNames = new Set<string>();
729
-
730
- for (const filePath of fileLoad.filePaths) {
731
- const loaded = loadAgentDefinitionFromFile(filePath);
732
- warnings.push(...loaded.warnings);
733
- if (!loaded.agent) continue;
734
-
735
- const { name } = loaded.agent;
736
-
737
- // Higher-priority source already registered this name
738
- if (seenNames.has(name)) continue;
739
-
740
- // Duplicate within the same directory
741
- if (dirNames.has(name)) {
742
- warnings.push(
743
- createDiscoveryWarning(
744
- filePath,
745
- `Duplicate subagent name "${name}" in ${agentsDir}, skipping`,
746
- ),
747
- );
748
- continue;
749
- }
100
+ class FilesystemAgentCatalogSource implements AgentCatalogSource {
101
+ loadAgentDefinitionGroups(cwd: string): AgentDefinitionSourceGroup[] {
102
+ return [
103
+ path.join(cwd, ".pi", "agents"),
104
+ path.join(getAgentDir(), "agents"),
105
+ bundledAgentsDir,
106
+ ]
107
+ .map(loadAgentDefinitionGroup)
108
+ .filter((group): group is AgentDefinitionSourceGroup => group !== null);
109
+ }
750
110
 
751
- dirNames.add(name);
752
- seenNames.set(name, filePath);
753
- agents.push(loaded.agent);
111
+ loadConfigFiles(cwd: string): AgentConfigFile[] {
112
+ return [
113
+ path.join(getAgentDir(), "pi-crew.json"),
114
+ path.join(cwd, ".pi", "pi-crew.json"),
115
+ ]
116
+ .map(loadConfigFile)
117
+ .filter((file): file is AgentConfigFile => file !== null);
754
118
  }
755
119
  }
756
120
 
757
121
  export function discoverAgents(cwd: string = process.cwd()): AgentDiscoveryResult {
758
- const agents: AgentConfig[] = [];
759
- const warnings: AgentDiscoveryWarning[] = [];
760
- const seenNames = new Map<string, string>();
761
-
762
- // Priority 1: project-level agents
763
- loadAgentsFromDir(path.join(cwd, ".pi", "agents"), seenNames, agents, warnings);
764
-
765
- // Priority 2: user global agents
766
- loadAgentsFromDir(path.join(getAgentDir(), "agents"), seenNames, agents, warnings);
767
-
768
- // Priority 3: bundled agents
769
- loadAgentsFromDir(bundledAgentsDir, seenNames, agents, warnings);
770
-
771
- const configOverrides = loadConfigOverrides(cwd);
772
- warnings.push(...configOverrides.warnings);
773
-
774
- const finalAgents = agents.map((agent) => {
775
- const override = configOverrides.overrides[agent.name];
776
- return override ? applyAgentOverride(agent, override) : agent;
777
- });
778
-
779
- for (const agentName of Object.keys(configOverrides.overrides)) {
780
- if (!seenNames.has(agentName)) {
781
- warnings.push(
782
- createDiscoveryWarning(
783
- configOverrides.overrideSources[agentName] ?? path.join(cwd, ".pi", "pi-crew.json"),
784
- `Subagent override "${agentName}" does not match any discovered subagent, ignoring`,
785
- ),
786
- );
787
- }
788
- }
789
-
790
- return { agents: finalAgents, warnings };
122
+ return new AgentCatalog(new FilesystemAgentCatalogSource()).discover(cwd);
791
123
  }