@melihmucuk/pi-crew 1.0.17 → 1.0.19

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/agents/code-reviewer.md +16 -11
  2. package/agents/quality-reviewer.md +8 -17
  3. package/extension/catalog.ts +543 -0
  4. package/extension/crew.ts +377 -0
  5. package/extension/index.ts +35 -18
  6. package/extension/subagent-session.ts +257 -0
  7. package/extension/tools.ts +323 -0
  8. package/extension/ui.ts +291 -0
  9. package/package.json +6 -6
  10. package/prompts/pi-crew-review.md +25 -16
  11. package/skills/pi-crew/SKILL.md +3 -1
  12. package/extension/agent-catalog.ts +0 -369
  13. package/extension/agent-config-fields.ts +0 -359
  14. package/extension/agent-discovery.ts +0 -123
  15. package/extension/bootstrap-session.ts +0 -131
  16. package/extension/integration/crew-tool-actions.ts +0 -306
  17. package/extension/integration/crew-tool-executor.ts +0 -109
  18. package/extension/integration/register-renderers.ts +0 -77
  19. package/extension/integration/register-tools.ts +0 -47
  20. package/extension/integration/tool-presentation.ts +0 -30
  21. package/extension/integration/tools/crew-abort.ts +0 -56
  22. package/extension/integration/tools/crew-done.ts +0 -27
  23. package/extension/integration/tools/crew-list.ts +0 -36
  24. package/extension/integration/tools/crew-respond.ts +0 -38
  25. package/extension/integration/tools/crew-spawn.ts +0 -46
  26. package/extension/message-delivery-policy.ts +0 -22
  27. package/extension/runtime/crew-runtime.ts +0 -263
  28. package/extension/runtime/overflow-recovery.ts +0 -211
  29. package/extension/runtime/owner-session-coordinator.ts +0 -138
  30. package/extension/runtime/subagent-lifecycle.ts +0 -203
  31. package/extension/runtime/subagent-registry.ts +0 -122
  32. package/extension/runtime/subagent-transitions.ts +0 -100
  33. package/extension/status-widget.ts +0 -107
  34. package/extension/subagent-messages.ts +0 -116
  35. package/extension/tool-registry.ts +0 -19
@@ -1,26 +1,32 @@
1
1
  ---
2
2
  name: code-reviewer
3
- description: Reviews changed code for actionable bugs. Read-only.
3
+ description: Reviews scoped code for actionable bugs. Read-only.
4
4
  model: openai-codex/gpt-5.2
5
5
  thinking: high
6
6
  tools: read, grep, find, ls, bash
7
7
  ---
8
8
 
9
- You are a read-only code reviewer. Your goal is not to find something; it is to decide whether the changed code contains realistic, actionable bugs. An empty review is a valid successful outcome. Reply in the user's language.
9
+ You are a read-only code reviewer. Your goal is not to find something; it is to decide whether the reviewed scope contains realistic, actionable bugs. An empty review is a valid successful outcome. Reply in the user's language.
10
10
 
11
11
  Do not modify files. Use bash only for read-only inspection. Do not run builds, tests, typechecks, formatters, installers, or commands that may change project state.
12
12
 
13
13
  ## Scope
14
14
 
15
- Review the provided scope. If none is provided, review uncommitted changes. For commits, branches, PRs, files, or "latest" requests, inspect the corresponding diff. If "latest" is requested, review the last 5 commits unless a count is given.
15
+ Review the provided scope. If none is provided, review uncommitted changes.
16
16
 
17
- For large or broad diffs, summarize coverage by area with brief risk notes, then deeply review only the highest-risk changed files: business logic, auth, data mutation, error handling, and public APIs. Avoid exhaustive file inventories.
17
+ For commits, branches, PRs, files, directories, modules, or "latest" requests, inspect the corresponding diff or code. If "latest" is requested, review the last 5 commits unless a count is given.
18
18
 
19
- Review changed-code issues only. Pre-existing code is reportable only when the change triggers it or makes it relevant.
19
+ If "full", "codebase", or whole-repo review is requested, perform a bounded bug audit: map the highest-risk areas, deeply inspect selected files, state coverage/skipped areas briefly, and do not imply exhaustive coverage.
20
+
21
+ For large or broad scopes, prioritize highest-risk areas: business logic, auth/security, data mutation, persistence, external integrations, concurrency/async, error handling, and public APIs.
22
+
23
+ For changed-code scopes, report pre-existing issues only when the change triggers or makes them relevant. For full-codebase scopes, report existing issues only when directly evidenced, realistically triggerable, and worth acting on now.
20
24
 
21
25
  ## Method
22
26
 
23
- Diffs are not enough. Before reporting a finding, read the full changed file involved. Trace direct callers/callees or nearby patterns only when needed. Check local conventions only when relevant. Stop expanding context when it stops adding evidence.
27
+ Diffs are not enough. Before reporting a finding, read the full relevant file involved. Trace direct callers/callees or nearby patterns only when needed. Check local conventions only when relevant. Stop expanding context when it stops adding evidence.
28
+
29
+ For full-codebase scopes, make findings only from files and paths you directly inspected; verify any caller, route, config, schema, or runtime assumption the finding depends on.
24
30
 
25
31
  Do not report findings from skipped or unreviewed files. A finding requires direct inspection of the relevant file or diff context; if a file was skipped, only mention it as skipped, not as evidence for a finding.
26
32
 
@@ -40,17 +46,15 @@ Report the same finding pattern at most twice, then list other affected location
40
46
 
41
47
  ## Severity
42
48
 
43
- - Critical: proven realistic security, data loss, or severe breakage.
44
- - Major: realistic bug likely to affect users, developers, or operations.
45
- - Minor: real non-blocking bug or high-risk coverage gap.
49
+ - Critical: urgent, high-impact issue within this reviewer's scope that can cause severe user, data, security, operational, or near-term development breakage.
50
+ - Major: realistic issue within this reviewer's scope likely to affect users, developers, operations, or maintainability enough to act on soon.
51
+ - Minor: real but non-blocking issue within this reviewer's scope, localized maintenance friction, or high-risk coverage gap.
46
52
 
47
53
  ## Output
48
54
 
49
55
  If no findings:
50
56
 
51
57
  **No issues found.**
52
- Reviewed: [files]
53
- Overall confidence: [high/medium]
54
58
 
55
59
  For each finding:
56
60
 
@@ -58,6 +62,7 @@ For each finding:
58
62
  File: `path:line`
59
63
  Issue: what is wrong
60
64
  Evidence: what you verified
65
+ Impact: concrete consequence
61
66
  Fix: suggested correction
62
67
 
63
68
  Be direct, concise, and unpadded.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: quality-reviewer
3
- description: Reviews changed code for maintainability, duplication, and complexity. Read-only.
3
+ description: Reviews scoped code for maintainability, duplication, and complexity. Read-only.
4
4
  model: openai-codex/gpt-5.2
5
5
  thinking: high
6
6
  tools: read, grep, find, ls, bash
@@ -16,7 +16,7 @@ Do not modify files. Use bash only for read-only inspection. Do not run builds,
16
16
 
17
17
  Review the provided scope. If none is provided, review uncommitted changes. For files, directories, modules, commits, branches, PRs, or "latest" requests, inspect the corresponding code or diff. If "latest" is requested, review the last 5 commits unless a count is given.
18
18
 
19
- If "full" or "codebase" is requested, first produce a structural risk map, then deeply review only the highest-risk areas.
19
+ If "full", "codebase", or whole-repo review is requested, first produce a structural risk map, then deeply review only the highest-risk areas, state coverage/skipped areas briefly, and do not imply exhaustive coverage.
20
20
 
21
21
  For large or broad scopes, summarize coverage by area with brief structural notes, then deeply review the highest-risk areas/files: large files, dependency-heavy files, widely imported files, or files crossing module boundaries. Avoid exhaustive file inventories; state skipped areas briefly.
22
22
 
@@ -48,32 +48,23 @@ Default stance: no new abstraction unless it reduces present-day duplication or
48
48
 
49
49
  ## Severity
50
50
 
51
- - High: structure will materially hinder near-term changes or debugging.
52
- - Medium: noticeable maintenance friction with concrete evidence.
53
- - Minor: small structural friction on a realistic future change/debug path.
51
+ - Critical: urgent, high-impact issue within this reviewer's scope that can cause severe user, data, security, operational, or near-term development breakage.
52
+ - Major: realistic issue within this reviewer's scope likely to affect users, developers, operations, or maintainability enough to act on soon.
53
+ - Minor: real but non-blocking issue within this reviewer's scope, localized maintenance friction, or high-risk coverage gap.
54
54
 
55
55
  ## Output
56
56
 
57
57
  If no findings:
58
58
 
59
59
  **No issues found.**
60
- Reviewed: [files]
61
- Overall health: [brief assessment]
62
60
 
63
61
  For each finding:
64
62
 
65
63
  **[SEVERITY] Category: Title**
66
64
  File: `path:line`
67
- Issue: structural problem
68
- Impact: concrete future change/debug task made harder
65
+ Issue: what is wrong
69
66
  Evidence: what you verified
70
- Fix: specific refactoring approach
71
-
72
- End with:
73
-
74
- **Quality Review Summary**
75
- Files reviewed: [count]
76
- Findings: [count by severity]
77
- Overall health: [one sentence]
67
+ Impact: concrete consequence
68
+ Fix: suggested correction
78
69
 
79
70
  Be direct, concise, and unpadded.
@@ -0,0 +1,543 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
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
+
7
+ const SUPPORTED_TOOL_NAMES_LITERAL = [
8
+ "read",
9
+ "bash",
10
+ "edit",
11
+ "write",
12
+ "grep",
13
+ "find",
14
+ "ls",
15
+ ] as const;
16
+
17
+ export type SupportedToolName = (typeof SUPPORTED_TOOL_NAMES_LITERAL)[number];
18
+ export const SUPPORTED_TOOL_NAMES = Object.freeze([...SUPPORTED_TOOL_NAMES_LITERAL] as SupportedToolName[]);
19
+
20
+ function isSupportedToolName(name: string): name is SupportedToolName {
21
+ return SUPPORTED_TOOL_NAMES.includes(name as SupportedToolName);
22
+ }
23
+
24
+ export interface ParsedModel {
25
+ provider: string;
26
+ modelId: string;
27
+ }
28
+
29
+ interface AgentConfigFields {
30
+ model?: string;
31
+ parsedModel?: ParsedModel;
32
+ thinking?: ThinkingLevel;
33
+ tools?: SupportedToolName[];
34
+ skills?: string[];
35
+ compaction?: boolean;
36
+ interactive?: boolean;
37
+ }
38
+
39
+ export interface AgentConfig extends AgentConfigFields {
40
+ name: string;
41
+ description: string;
42
+ systemPrompt: string;
43
+ filePath: string;
44
+ }
45
+
46
+ export interface AgentDiscoveryWarning {
47
+ filePath: string;
48
+ message: string;
49
+ }
50
+
51
+ export interface AgentDiscoveryResult {
52
+ agents: AgentConfig[];
53
+ warnings: AgentDiscoveryWarning[];
54
+ }
55
+
56
+ export interface AgentDefinitionFile {
57
+ filePath: string;
58
+ content: string | null;
59
+ warnings?: AgentDiscoveryWarning[];
60
+ }
61
+
62
+ export interface AgentDefinitionSourceGroup {
63
+ agentsDir: string;
64
+ files: AgentDefinitionFile[];
65
+ warnings?: AgentDiscoveryWarning[];
66
+ }
67
+
68
+ export interface AgentConfigFile {
69
+ filePath: string;
70
+ content: string | null;
71
+ warnings?: AgentDiscoveryWarning[];
72
+ }
73
+
74
+ export interface AgentCatalogSource {
75
+ loadAgentDefinitionGroups(cwd: string): AgentDefinitionSourceGroup[];
76
+ loadConfigFiles(cwd: string): AgentConfigFile[];
77
+ }
78
+
79
+ type AgentConfigOverride = AgentConfigFields;
80
+ type ParsedFieldName = "model" | "thinking" | "tools" | "skills" | "compaction" | "interactive";
81
+ type ParsedListFieldName = "tools" | "skills";
82
+ type ParsedBooleanFieldName = "compaction" | "interactive";
83
+ type WarningSubject = "subagent" | "subagent override";
84
+
85
+ type ParsedFieldWarning =
86
+ | { code: "invalid-list-format"; fieldName: ParsedListFieldName }
87
+ | { code: "invalid-type"; fieldName: ParsedFieldName; expected: "string" | "boolean" }
88
+ | { code: "invalid-model-format"; model: string }
89
+ | { code: "invalid-thinking-level"; thinking: string }
90
+ | { code: "unknown-tools"; tools: string[] };
91
+
92
+ interface ParsedFieldSet extends AgentConfigFields {
93
+ warnings: ParsedFieldWarning[];
94
+ }
95
+
96
+ interface ParseFieldOptions {
97
+ warnOnInvalidType: boolean;
98
+ setValueOnInvalidType: boolean;
99
+ }
100
+
101
+ interface ParseResult {
102
+ agent: AgentConfig | null;
103
+ warnings: AgentDiscoveryWarning[];
104
+ }
105
+
106
+ interface ConfigParseResult {
107
+ overrides: Record<string, AgentConfigOverride>;
108
+ overrideSources: Record<string, string>;
109
+ warnings: AgentDiscoveryWarning[];
110
+ }
111
+
112
+ const VALID_THINKING_LEVELS: readonly string[] = [
113
+ "off",
114
+ "minimal",
115
+ "low",
116
+ "medium",
117
+ "high",
118
+ "xhigh",
119
+ ];
120
+
121
+ const ALLOWED_OVERRIDE_FIELDS = new Set([
122
+ "model",
123
+ "thinking",
124
+ "tools",
125
+ "skills",
126
+ "compaction",
127
+ "interactive",
128
+ ]);
129
+
130
+ function createDiscoveryWarning(filePath: string, message: string): AgentDiscoveryWarning {
131
+ return { filePath, message };
132
+ }
133
+
134
+ function parseCommaSeparated(value: unknown): string[] | undefined {
135
+ if (value == null) return undefined;
136
+ if (Array.isArray(value)) return value.map((v) => String(v).trim()).filter(Boolean);
137
+ if (typeof value === "string") return value.split(",").map((s) => s.trim()).filter(Boolean);
138
+ return undefined;
139
+ }
140
+
141
+ function formatFieldWarning(subject: WarningSubject, name: string, warning: ParsedFieldWarning): string {
142
+ const prefix = `${subject === "subagent" ? "Subagent" : "Subagent override"} "${name}"`;
143
+
144
+ switch (warning.code) {
145
+ case "invalid-list-format":
146
+ return `${prefix}: invalid ${warning.fieldName} field, expected a comma-separated string or YAML array`;
147
+ case "invalid-type":
148
+ return `${prefix}: field "${warning.fieldName}" must be a ${warning.expected}, ignoring`;
149
+ case "invalid-model-format":
150
+ return `${prefix}: invalid model format "${warning.model}" (expected "provider/model-id"), ignoring model field`;
151
+ case "invalid-thinking-level":
152
+ return `${prefix}: invalid thinking level "${warning.thinking}", ignoring`;
153
+ case "unknown-tools":
154
+ return `${prefix}: unknown tools ${warning.tools.map((toolName) => `"${toolName}"`).join(", ")}, ignoring`;
155
+ }
156
+ }
157
+
158
+ function toParseWarnings(
159
+ filePath: string,
160
+ subject: WarningSubject,
161
+ name: string,
162
+ warnings: ParsedFieldWarning[],
163
+ ): AgentDiscoveryWarning[] {
164
+ return warnings.map((warning) => createDiscoveryWarning(filePath, formatFieldWarning(subject, name, warning)));
165
+ }
166
+
167
+ function parseListField(value: unknown, fieldName: ParsedListFieldName): { values: string[]; warnings: ParsedFieldWarning[] } {
168
+ if (value == null) return { values: [], warnings: [] };
169
+ const parsed = parseCommaSeparated(value);
170
+ if (parsed !== undefined) return { values: parsed, warnings: [] };
171
+ return { values: [], warnings: [{ code: "invalid-list-format", fieldName }] };
172
+ }
173
+
174
+ function parseModel(value: unknown): ParsedModel | null {
175
+ if (typeof value !== "string" || !value.includes("/")) return null;
176
+ const slashIndex = value.indexOf("/");
177
+ const provider = value.slice(0, slashIndex).trim();
178
+ const modelId = value.slice(slashIndex + 1).trim();
179
+ return provider && modelId ? { provider, modelId } : null;
180
+ }
181
+
182
+ function validateThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
183
+ if (!value) return undefined;
184
+ return VALID_THINKING_LEVELS.includes(value) ? value as ThinkingLevel : undefined;
185
+ }
186
+
187
+ function parseModelField(value: unknown, options: ParseFieldOptions): Pick<ParsedFieldSet, "model" | "parsedModel" | "warnings"> {
188
+ if (typeof value === "string") {
189
+ const parsedModel = parseModel(value);
190
+ if (!parsedModel) {
191
+ return {
192
+ ...(options.setValueOnInvalidType ? { model: value } : {}),
193
+ warnings: [{ code: "invalid-model-format", model: value }],
194
+ };
195
+ }
196
+ return { model: value, parsedModel, warnings: [] };
197
+ }
198
+ if (value !== undefined && options.warnOnInvalidType) {
199
+ return { warnings: [{ code: "invalid-type", fieldName: "model", expected: "string" }] };
200
+ }
201
+ return { warnings: [] };
202
+ }
203
+
204
+ function parseThinkingField(value: unknown, options: ParseFieldOptions): Pick<ParsedFieldSet, "thinking" | "warnings"> {
205
+ if (typeof value === "string") {
206
+ const thinking = validateThinkingLevel(value);
207
+ return thinking ? { thinking, warnings: [] } : { warnings: [{ code: "invalid-thinking-level", thinking: value }] };
208
+ }
209
+ if (value !== undefined && options.warnOnInvalidType) {
210
+ return { warnings: [{ code: "invalid-type", fieldName: "thinking", expected: "string" }] };
211
+ }
212
+ return { warnings: [] };
213
+ }
214
+
215
+ function parseToolsField(value: unknown, options: ParseFieldOptions): Pick<ParsedFieldSet, "tools" | "warnings"> {
216
+ const parsedTools = parseListField(value, "tools");
217
+ const validTools = parsedTools.values.filter(isSupportedToolName);
218
+ const invalidTools = parsedTools.values.filter((toolName) => !isSupportedToolName(toolName));
219
+ const warnings: ParsedFieldWarning[] = [...parsedTools.warnings];
220
+ if (invalidTools.length > 0) warnings.push({ code: "unknown-tools", tools: invalidTools });
221
+ if (invalidTools.length > 0 && validTools.length === 0 && !options.setValueOnInvalidType) return { warnings };
222
+ if (parsedTools.warnings.length > 0 && !options.setValueOnInvalidType) return { warnings };
223
+ return { tools: validTools, warnings };
224
+ }
225
+
226
+ function parseSkillsField(value: unknown, options: ParseFieldOptions): Pick<ParsedFieldSet, "skills" | "warnings"> {
227
+ const parsedSkills = parseListField(value, "skills");
228
+ if (parsedSkills.warnings.length > 0 && !options.setValueOnInvalidType) return { warnings: parsedSkills.warnings };
229
+ return { skills: parsedSkills.values, warnings: parsedSkills.warnings };
230
+ }
231
+
232
+ function parseBooleanField(
233
+ fieldName: ParsedBooleanFieldName,
234
+ value: unknown,
235
+ options: ParseFieldOptions,
236
+ ): Pick<ParsedFieldSet, ParsedBooleanFieldName | "warnings"> {
237
+ if (typeof value === "boolean") return { [fieldName]: value, warnings: [] };
238
+ if (value !== undefined && options.warnOnInvalidType) {
239
+ return { warnings: [{ code: "invalid-type", fieldName, expected: "boolean" }] };
240
+ }
241
+ return { warnings: [] };
242
+ }
243
+
244
+ function parseSharedFields(record: Record<string, unknown>, options: ParseFieldOptions): ParsedFieldSet {
245
+ const model = parseModelField(record.model, options);
246
+ const thinking = parseThinkingField(record.thinking, options);
247
+ const tools = Object.prototype.hasOwnProperty.call(record, "tools") ? parseToolsField(record.tools, options) : { warnings: [] };
248
+ const skills = Object.prototype.hasOwnProperty.call(record, "skills") ? parseSkillsField(record.skills, options) : { warnings: [] };
249
+ const compaction = parseBooleanField("compaction", record.compaction, options);
250
+ const interactive = parseBooleanField("interactive", record.interactive, options);
251
+
252
+ return {
253
+ ...("model" in model ? { model: model.model } : {}),
254
+ ...("parsedModel" in model ? { parsedModel: model.parsedModel } : {}),
255
+ ...(thinking.thinking !== undefined ? { thinking: thinking.thinking } : {}),
256
+ ...(tools.tools !== undefined ? { tools: tools.tools } : {}),
257
+ ...(skills.skills !== undefined ? { skills: skills.skills } : {}),
258
+ ...(compaction.compaction !== undefined ? { compaction: compaction.compaction } : {}),
259
+ ...(interactive.interactive !== undefined ? { interactive: interactive.interactive } : {}),
260
+ warnings: [
261
+ ...model.warnings,
262
+ ...thinking.warnings,
263
+ ...tools.warnings,
264
+ ...skills.warnings,
265
+ ...compaction.warnings,
266
+ ...interactive.warnings,
267
+ ],
268
+ };
269
+ }
270
+
271
+ function parseDefinitionFields(
272
+ record: Record<string, unknown>,
273
+ filePath: string,
274
+ agentName: string,
275
+ ): { fields: AgentConfigFields; warnings: AgentDiscoveryWarning[] } {
276
+ const parsed = parseSharedFields(record, { warnOnInvalidType: false, setValueOnInvalidType: true });
277
+ const { warnings, ...fields } = parsed;
278
+ return { fields, warnings: toParseWarnings(filePath, "subagent", agentName, warnings) };
279
+ }
280
+
281
+ function parseOverrideFields(
282
+ record: Record<string, unknown>,
283
+ filePath: string,
284
+ agentName: string,
285
+ ): { fields: AgentConfigFields; warnings: AgentDiscoveryWarning[] } {
286
+ const warnings: AgentDiscoveryWarning[] = [];
287
+ for (const fieldName of Object.keys(record)) {
288
+ if (fieldName === "name" || fieldName === "description") {
289
+ warnings.push(createDiscoveryWarning(filePath, `Subagent override "${agentName}": field "${fieldName}" is not overridable, ignoring`));
290
+ continue;
291
+ }
292
+ if (!ALLOWED_OVERRIDE_FIELDS.has(fieldName)) {
293
+ warnings.push(createDiscoveryWarning(filePath, `Subagent override "${agentName}": unknown field "${fieldName}", ignoring`));
294
+ }
295
+ }
296
+
297
+ const parsed = parseSharedFields(record, { warnOnInvalidType: true, setValueOnInvalidType: false });
298
+ const { warnings: fieldWarnings, ...fields } = parsed;
299
+ warnings.push(...toParseWarnings(filePath, "subagent override", agentName, fieldWarnings));
300
+ return { fields, warnings };
301
+ }
302
+
303
+ function parseAgentDefinition(content: string, filePath: string): ParseResult {
304
+ let frontmatter: Record<string, unknown>;
305
+ let body: string;
306
+ try {
307
+ const parsed = parseFrontmatter<Record<string, unknown>>(content);
308
+ frontmatter = parsed.frontmatter;
309
+ body = parsed.body;
310
+ } catch (error) {
311
+ const reason = error instanceof Error ? error.message : String(error);
312
+ return { agent: null, warnings: [createDiscoveryWarning(filePath, `Ignored invalid subagent definition. Frontmatter could not be parsed: ${reason}`)] };
313
+ }
314
+
315
+ const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : undefined;
316
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : undefined;
317
+ if (!name || !description) {
318
+ return { agent: null, warnings: [createDiscoveryWarning(filePath, 'Ignored invalid subagent definition. Required frontmatter fields "name" and "description" must be non-empty strings.')] };
319
+ }
320
+ if (/\s/.test(name)) {
321
+ return { agent: null, warnings: [createDiscoveryWarning(filePath, `Ignored subagent definition "${name}". Subagent names cannot contain whitespace. Use "-" instead.`)] };
322
+ }
323
+
324
+ const parsedFields = parseDefinitionFields(frontmatter, filePath, name);
325
+ return {
326
+ agent: { name, description, ...parsedFields.fields, systemPrompt: body, filePath },
327
+ warnings: parsedFields.warnings,
328
+ };
329
+ }
330
+
331
+ function parseAgentOverride(
332
+ agentName: string,
333
+ value: unknown,
334
+ filePath: string,
335
+ ): { override: AgentConfigOverride | null; warnings: AgentDiscoveryWarning[] } {
336
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
337
+ return { override: null, warnings: [createDiscoveryWarning(filePath, `Subagent override "${agentName}" must be a JSON object, ignoring`)] };
338
+ }
339
+ const parsedFields = parseOverrideFields(value as Record<string, unknown>, filePath, agentName);
340
+ return { override: parsedFields.fields, warnings: parsedFields.warnings };
341
+ }
342
+
343
+ function parseConfigFile(content: string, filePath: string): ConfigParseResult {
344
+ let parsed: unknown;
345
+ try {
346
+ parsed = JSON.parse(content);
347
+ } catch (error) {
348
+ const reason = error instanceof Error ? error.message : String(error);
349
+ return { overrides: {}, overrideSources: {}, warnings: [createDiscoveryWarning(filePath, `Ignored pi-crew config. JSON could not be parsed: ${reason}`)] };
350
+ }
351
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
352
+ return { overrides: {}, overrideSources: {}, warnings: [createDiscoveryWarning(filePath, "Ignored pi-crew config. Root value must be a JSON object.")] };
353
+ }
354
+
355
+ const root = parsed as Record<string, unknown>;
356
+ if (root.agents === undefined) return { overrides: {}, overrideSources: {}, warnings: [] };
357
+ if (!root.agents || typeof root.agents !== "object" || Array.isArray(root.agents)) {
358
+ return { overrides: {}, overrideSources: {}, warnings: [createDiscoveryWarning(filePath, 'Ignored pi-crew config. Field "agents" must be a JSON object.')] };
359
+ }
360
+
361
+ const overrides: Record<string, AgentConfigOverride> = {};
362
+ const overrideSources: Record<string, string> = {};
363
+ const warnings: AgentDiscoveryWarning[] = [];
364
+ for (const [agentName, value] of Object.entries(root.agents)) {
365
+ if (!agentName.trim()) {
366
+ warnings.push(createDiscoveryWarning(filePath, "Ignored pi-crew config entry with empty subagent name."));
367
+ continue;
368
+ }
369
+ const parsedOverride = parseAgentOverride(agentName, value, filePath);
370
+ warnings.push(...parsedOverride.warnings);
371
+ if (parsedOverride.override) {
372
+ overrides[agentName] = parsedOverride.override;
373
+ overrideSources[agentName] = filePath;
374
+ }
375
+ }
376
+ return { overrides, overrideSources, warnings };
377
+ }
378
+
379
+ function mergeConfigOverrides(
380
+ base: Record<string, AgentConfigOverride>,
381
+ override: Record<string, AgentConfigOverride>,
382
+ ): Record<string, AgentConfigOverride> {
383
+ const merged: Record<string, AgentConfigOverride> = { ...base };
384
+ for (const [agentName, agentOverride] of Object.entries(override)) {
385
+ merged[agentName] = { ...(merged[agentName] ?? {}), ...agentOverride };
386
+ }
387
+ return merged;
388
+ }
389
+
390
+ function applyAgentOverride(agent: AgentConfig, override: AgentConfigOverride): AgentConfig {
391
+ return {
392
+ ...agent,
393
+ ...(override.model !== undefined ? { model: override.model, parsedModel: override.parsedModel } : {}),
394
+ ...(override.thinking !== undefined ? { thinking: override.thinking } : {}),
395
+ ...(override.tools !== undefined ? { tools: override.tools } : {}),
396
+ ...(override.skills !== undefined ? { skills: override.skills } : {}),
397
+ ...(override.compaction !== undefined ? { compaction: override.compaction } : {}),
398
+ ...(override.interactive !== undefined ? { interactive: override.interactive } : {}),
399
+ };
400
+ }
401
+
402
+ function loadAgentDefinitionFromFile(file: AgentDefinitionFile): ParseResult {
403
+ if (!file.content) return { agent: null, warnings: file.warnings ?? [] };
404
+ const parsed = parseAgentDefinition(file.content, file.filePath);
405
+ return { agent: parsed.agent, warnings: [...(file.warnings ?? []), ...parsed.warnings] };
406
+ }
407
+
408
+ function mergeConfigFiles(configFiles: AgentConfigFile[]): ConfigParseResult {
409
+ let overrides: Record<string, AgentConfigOverride> = {};
410
+ let overrideSources: Record<string, string> = {};
411
+ const warnings: AgentDiscoveryWarning[] = [];
412
+
413
+ for (const configFile of configFiles) {
414
+ warnings.push(...(configFile.warnings ?? []));
415
+ if (!configFile.content) continue;
416
+ const parsed = parseConfigFile(configFile.content, configFile.filePath);
417
+ overrides = mergeConfigOverrides(overrides, parsed.overrides);
418
+ overrideSources = { ...overrideSources, ...parsed.overrideSources };
419
+ warnings.push(...parsed.warnings);
420
+ }
421
+
422
+ return { overrides, overrideSources, warnings };
423
+ }
424
+
425
+ export class AgentCatalog {
426
+ constructor(private readonly source: AgentCatalogSource) {}
427
+
428
+ discover(cwd: string = process.cwd()): AgentDiscoveryResult {
429
+ const agents: AgentConfig[] = [];
430
+ const warnings: AgentDiscoveryWarning[] = [];
431
+ const seenNames = new Map<string, string>();
432
+
433
+ for (const group of this.source.loadAgentDefinitionGroups(cwd)) {
434
+ this.loadAgentsFromGroup(group, seenNames, agents, warnings);
435
+ }
436
+
437
+ const configOverrides = mergeConfigFiles(this.source.loadConfigFiles(cwd));
438
+ warnings.push(...configOverrides.warnings);
439
+
440
+ const finalAgents = agents.map((agent) => {
441
+ const override = configOverrides.overrides[agent.name];
442
+ return override ? applyAgentOverride(agent, override) : agent;
443
+ });
444
+
445
+ for (const agentName of Object.keys(configOverrides.overrides)) {
446
+ if (!seenNames.has(agentName)) {
447
+ warnings.push(createDiscoveryWarning(
448
+ configOverrides.overrideSources[agentName] ?? "pi-crew.json",
449
+ `Subagent override "${agentName}" does not match any discovered subagent, ignoring`,
450
+ ));
451
+ }
452
+ }
453
+
454
+ return { agents: finalAgents, warnings };
455
+ }
456
+
457
+ private loadAgentsFromGroup(
458
+ group: AgentDefinitionSourceGroup,
459
+ seenNames: Map<string, string>,
460
+ agents: AgentConfig[],
461
+ warnings: AgentDiscoveryWarning[],
462
+ ): void {
463
+ warnings.push(...(group.warnings ?? []));
464
+ const groupNames = new Set<string>();
465
+
466
+ for (const file of group.files) {
467
+ const loaded = loadAgentDefinitionFromFile(file);
468
+ warnings.push(...loaded.warnings);
469
+ if (!loaded.agent) continue;
470
+
471
+ const { name } = loaded.agent;
472
+ if (groupNames.has(name)) {
473
+ warnings.push(createDiscoveryWarning(file.filePath, `Duplicate subagent name "${name}" in ${group.agentsDir}, skipping`));
474
+ continue;
475
+ }
476
+ groupNames.add(name);
477
+ if (seenNames.has(name)) continue;
478
+ seenNames.set(name, file.filePath);
479
+ agents.push(loaded.agent);
480
+ }
481
+ }
482
+ }
483
+
484
+ function loadAgentFile(filePath: string): AgentDefinitionFile {
485
+ try {
486
+ return { filePath, content: fs.readFileSync(filePath, "utf-8") };
487
+ } catch (error) {
488
+ const reason = error instanceof Error ? error.message : String(error);
489
+ return {
490
+ filePath,
491
+ content: null,
492
+ warnings: [createDiscoveryWarning(filePath, `Ignored subagent definition. File could not be read: ${reason}`)],
493
+ };
494
+ }
495
+ }
496
+
497
+ function loadAgentDefinitionGroup(agentsDir: string): AgentDefinitionSourceGroup | null {
498
+ if (!fs.existsSync(agentsDir)) return null;
499
+ let entries: fs.Dirent[];
500
+ try {
501
+ entries = fs.readdirSync(agentsDir, { withFileTypes: true });
502
+ } catch (error) {
503
+ const reason = error instanceof Error ? error.message : String(error);
504
+ return { agentsDir, files: [], warnings: [createDiscoveryWarning(agentsDir, `Subagent directory could not be read: ${reason}`)] };
505
+ }
506
+ return {
507
+ agentsDir,
508
+ files: entries
509
+ .filter((entry) => entry.name.endsWith(".md"))
510
+ .filter((entry) => entry.isFile() || entry.isSymbolicLink())
511
+ .map((entry) => loadAgentFile(path.join(agentsDir, entry.name))),
512
+ };
513
+ }
514
+
515
+ function loadConfigFile(filePath: string): AgentConfigFile | null {
516
+ if (!fs.existsSync(filePath)) return null;
517
+ try {
518
+ return { filePath, content: fs.readFileSync(filePath, "utf-8") };
519
+ } catch (error) {
520
+ const reason = error instanceof Error ? error.message : String(error);
521
+ return { filePath, content: null, warnings: [createDiscoveryWarning(filePath, `Ignored pi-crew config. File could not be read: ${reason}`)] };
522
+ }
523
+ }
524
+
525
+ const bundledAgentsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
526
+
527
+ class FilesystemAgentCatalogSource implements AgentCatalogSource {
528
+ loadAgentDefinitionGroups(cwd: string): AgentDefinitionSourceGroup[] {
529
+ return [path.join(cwd, ".pi", "agents"), path.join(getAgentDir(), "agents"), bundledAgentsDir]
530
+ .map(loadAgentDefinitionGroup)
531
+ .filter((group): group is AgentDefinitionSourceGroup => group !== null);
532
+ }
533
+
534
+ loadConfigFiles(cwd: string): AgentConfigFile[] {
535
+ return [path.join(getAgentDir(), "pi-crew.json"), path.join(cwd, ".pi", "pi-crew.json")]
536
+ .map(loadConfigFile)
537
+ .filter((file): file is AgentConfigFile => file !== null);
538
+ }
539
+ }
540
+
541
+ export function discoverAgents(cwd: string = process.cwd()): AgentDiscoveryResult {
542
+ return new AgentCatalog(new FilesystemAgentCatalogSource()).discover(cwd);
543
+ }