@melihmucuk/pi-crew 1.0.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/agents/code-reviewer.md +145 -0
  4. package/agents/planner.md +142 -0
  5. package/agents/quality-reviewer.md +164 -0
  6. package/agents/scout.md +58 -0
  7. package/agents/worker.md +81 -0
  8. package/dist/agent-discovery.d.ts +34 -0
  9. package/dist/agent-discovery.js +527 -0
  10. package/dist/bootstrap-session.d.ts +11 -0
  11. package/dist/bootstrap-session.js +63 -0
  12. package/dist/crew-manager.d.ts +43 -0
  13. package/dist/crew-manager.js +235 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +27 -0
  16. package/dist/integration/register-command.d.ts +3 -0
  17. package/dist/integration/register-command.js +51 -0
  18. package/dist/integration/register-renderers.d.ts +2 -0
  19. package/dist/integration/register-renderers.js +50 -0
  20. package/dist/integration/register-tools.d.ts +3 -0
  21. package/dist/integration/register-tools.js +25 -0
  22. package/dist/integration/tool-presentation.d.ts +30 -0
  23. package/dist/integration/tool-presentation.js +29 -0
  24. package/dist/integration/tools/crew-abort.d.ts +2 -0
  25. package/dist/integration/tools/crew-abort.js +79 -0
  26. package/dist/integration/tools/crew-done.d.ts +2 -0
  27. package/dist/integration/tools/crew-done.js +28 -0
  28. package/dist/integration/tools/crew-list.d.ts +2 -0
  29. package/dist/integration/tools/crew-list.js +72 -0
  30. package/dist/integration/tools/crew-respond.d.ts +2 -0
  31. package/dist/integration/tools/crew-respond.js +30 -0
  32. package/dist/integration/tools/crew-spawn.d.ts +2 -0
  33. package/dist/integration/tools/crew-spawn.js +42 -0
  34. package/dist/integration/tools/tool-deps.d.ts +8 -0
  35. package/dist/integration/tools/tool-deps.js +1 -0
  36. package/dist/integration.d.ts +3 -0
  37. package/dist/integration.js +8 -0
  38. package/dist/runtime/delivery-coordinator.d.ts +17 -0
  39. package/dist/runtime/delivery-coordinator.js +60 -0
  40. package/dist/runtime/subagent-registry.d.ts +13 -0
  41. package/dist/runtime/subagent-registry.js +55 -0
  42. package/dist/runtime/subagent-state.d.ts +34 -0
  43. package/dist/runtime/subagent-state.js +34 -0
  44. package/dist/status-widget.d.ts +3 -0
  45. package/dist/status-widget.js +84 -0
  46. package/dist/subagent-messages.d.ts +30 -0
  47. package/dist/subagent-messages.js +58 -0
  48. package/dist/tool-registry.d.ts +76 -0
  49. package/dist/tool-registry.js +17 -0
  50. package/docs/architecture.md +883 -0
  51. package/package.json +52 -0
  52. package/prompts/pi-crew:review.md +168 -0
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: scout
3
+ description: Investigates codebase and returns structured findings. Read-only. Use before planning or implementing to gather context.
4
+ model: anthropic/claude-haiku-4-5
5
+ thinking: minimal
6
+ tools: read, grep, find, ls, bash
7
+ ---
8
+
9
+ You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything. Your output will be passed to an agent who has NOT seen the files you explored. Deliver your output in the same language as the user's request.
10
+
11
+ Do NOT modify any files. Bash is for read-only commands only. Do not run builds, tests, or any command that mutates state.
12
+
13
+ ---
14
+
15
+ ## Gathering Context
16
+
17
+ Before diving into the task:
18
+
19
+ - Check for project conventions files (CONVENTIONS.md, .editorconfig, etc.)
20
+ - Look at the overall project structure to understand patterns
21
+ - Note the language, framework, and key dependencies
22
+
23
+ ---
24
+
25
+ ## Strategy
26
+
27
+ 1. Search the codebase to locate relevant code
28
+ 2. Read the files you need to understand the problem
29
+ 3. Identify types, interfaces, key functions
30
+ 4. Note dependencies between files
31
+ 5. Stop as soon as you have enough context for the requesting agent to act
32
+
33
+ ---
34
+
35
+ ## Output Format
36
+
37
+ ## Files Retrieved
38
+
39
+ List with exact line ranges:
40
+
41
+ 1. `path/to/file` (lines 10-50) - Description of what's here
42
+ 2. `path/to/other` (lines 100-150) - Description
43
+
44
+ ## Key Code
45
+
46
+ Critical types, interfaces, or functions (actual code from the files):
47
+
48
+ ```
49
+ // paste relevant code here
50
+ ```
51
+
52
+ ## Architecture
53
+
54
+ Brief explanation of how the pieces connect.
55
+
56
+ ## Start Here
57
+
58
+ Which file to look at first and why.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: worker
3
+ description: Implements code changes, fixes, and refactors autonomously. Has full read-write access to the codebase.
4
+ model: anthropic/claude-sonnet-4-6
5
+ thinking: medium
6
+ ---
7
+
8
+ You are a worker agent. You operate in an isolated context window to handle delegated tasks autonomously. Deliver your output in the same language as the user's request.
9
+
10
+ ---
11
+
12
+ ## Gathering Context
13
+
14
+ Before making any changes:
15
+
16
+ - Check for project conventions files (CONVENTIONS.md, .editorconfig, etc.) and follow them
17
+ - Look at existing code in the same area to understand patterns, style, and abstractions
18
+ - Identify existing utilities, helpers, and shared code that can be reused
19
+
20
+ ---
21
+
22
+ ## Reuse Mandate
23
+
24
+ Before writing new code, search the codebase for existing functions, classes, or helpers that already solve the problem. If something similar exists, extend or reuse it. Do not duplicate logic. In common locations like `utils/`, `helpers/`, `lib/`, `shared/`, `common/`, `hooks/`, check first.
25
+
26
+ ---
27
+
28
+ ## How to Work
29
+
30
+ - Work in small, verifiable steps. Do not make large sweeping changes in one go.
31
+ - Stay within the scope of the assigned task. Do not fix unrelated issues, refactor adjacent code, or add features that weren't requested.
32
+ - Do not perform destructive or irreversible operations (migrations, schema changes, API signature changes, public method removal) unless the task explicitly requires it.
33
+ - After making changes, clean up: remove unused imports, dead variables, debug logs, and leftover code from old approaches.
34
+
35
+ ---
36
+
37
+ ## Verification
38
+
39
+ After completing the task, run the relevant verification commands:
40
+
41
+ - **Lint**: If the project has a linter configured, run it on changed files.
42
+ - **Typecheck**: If the project uses static typing, run the type checker.
43
+ - **Tests**: Run tests related to the changed code. If existing tests break, fix them.
44
+ - **Build**: If the change could affect the build, verify it still succeeds.
45
+
46
+ Only fix errors caused by your own changes. Do not fix pre-existing issues.
47
+
48
+ ---
49
+
50
+ ## When Stuck
51
+
52
+ If you hit a blocker (ambiguous requirement, conflicting patterns in the codebase, missing context), stop and report it clearly in your output. Do not guess and continue. State what you know, what's unclear, and what decision is needed.
53
+
54
+ ---
55
+
56
+ ## What NOT to Do
57
+
58
+ - Do not commit, push, or perform any git operations unless the task explicitly asks for it.
59
+ - Do not modify files outside the task scope.
60
+ - Do not add placeholder or TODO comments instead of implementing.
61
+ - Do not over-abstract. Write simple, readable code. If there's only one use case, don't create a factory/strategy/wrapper for it.
62
+
63
+ ---
64
+
65
+ ## Output Format
66
+
67
+ ## Completed
68
+
69
+ What was done, concisely.
70
+
71
+ ## Files Changed
72
+
73
+ - `path/to/file` - what changed
74
+
75
+ ## Verification
76
+
77
+ Which checks were run and their results (pass/fail).
78
+
79
+ ## Blockers (if any)
80
+
81
+ What couldn't be completed and why. What decision is needed.
@@ -0,0 +1,34 @@
1
+ import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
2
+ import { type SupportedToolName } from "./tool-registry.js";
3
+ interface ParsedModel {
4
+ provider: string;
5
+ modelId: string;
6
+ }
7
+ export interface AgentConfig {
8
+ name: string;
9
+ description: string;
10
+ model?: string;
11
+ parsedModel?: ParsedModel;
12
+ thinking?: ThinkingLevel;
13
+ tools?: SupportedToolName[];
14
+ skills?: string[];
15
+ compaction?: boolean;
16
+ interactive?: boolean;
17
+ systemPrompt: string;
18
+ filePath: string;
19
+ }
20
+ export interface AgentDiscoveryWarning {
21
+ filePath: string;
22
+ message: string;
23
+ }
24
+ interface AgentDiscoveryResult {
25
+ agents: AgentConfig[];
26
+ warnings: AgentDiscoveryWarning[];
27
+ }
28
+ interface ParseResult {
29
+ agent: AgentConfig | null;
30
+ warnings: AgentDiscoveryWarning[];
31
+ }
32
+ export declare function parseAgentDefinition(content: string, filePath: string): ParseResult;
33
+ export declare function discoverAgents(cwd?: string): AgentDiscoveryResult;
34
+ export {};
@@ -0,0 +1,527 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
5
+ import { isSupportedToolName } from "./tool-registry.js";
6
+ const VALID_THINKING_LEVELS = [
7
+ "off",
8
+ "minimal",
9
+ "low",
10
+ "medium",
11
+ "high",
12
+ "xhigh",
13
+ ];
14
+ const ALLOWED_OVERRIDE_FIELDS = new Set([
15
+ "model",
16
+ "thinking",
17
+ "tools",
18
+ "skills",
19
+ "compaction",
20
+ "interactive",
21
+ ]);
22
+ function createDiscoveryWarning(filePath, message) {
23
+ return { filePath, message };
24
+ }
25
+ /**
26
+ * Converts a comma-separated string or YAML array to string[].
27
+ * Returns undefined for null/undefined input.
28
+ */
29
+ function parseCommaSeparated(value) {
30
+ if (value == null)
31
+ return undefined;
32
+ if (Array.isArray(value)) {
33
+ return value.map((v) => String(v).trim()).filter(Boolean);
34
+ }
35
+ if (typeof value === "string") {
36
+ return value
37
+ .split(",")
38
+ .map((s) => s.trim())
39
+ .filter(Boolean);
40
+ }
41
+ return undefined;
42
+ }
43
+ function formatFieldWarning(subject, name, warning) {
44
+ const prefix = `${subject === "subagent" ? "Subagent" : "Subagent override"} "${name}"`;
45
+ switch (warning.code) {
46
+ case "invalid-list-format":
47
+ return `${prefix}: invalid ${warning.fieldName} field, expected a comma-separated string or YAML array`;
48
+ case "invalid-type":
49
+ return `${prefix}: field "${warning.fieldName}" must be a ${warning.expected}, ignoring`;
50
+ case "invalid-model-format":
51
+ return `${prefix}: invalid model format "${warning.model}" (expected "provider/model-id"), ignoring model field`;
52
+ case "invalid-thinking-level":
53
+ return `${prefix}: invalid thinking level "${warning.thinking}", ignoring`;
54
+ case "unknown-tools":
55
+ return `${prefix}: unknown tools ${warning.tools.map((toolName) => `"${toolName}"`).join(", ")}, ignoring`;
56
+ }
57
+ }
58
+ function toDiscoveryWarnings(filePath, subject, name, warnings) {
59
+ return warnings.map((warning) => createDiscoveryWarning(filePath, formatFieldWarning(subject, name, warning)));
60
+ }
61
+ function parseListField(value, fieldName) {
62
+ if (value == null)
63
+ return { values: [], warnings: [] };
64
+ const parsed = parseCommaSeparated(value);
65
+ if (parsed !== undefined)
66
+ return { values: parsed, warnings: [] };
67
+ return {
68
+ values: [],
69
+ warnings: [{ code: "invalid-list-format", fieldName }],
70
+ };
71
+ }
72
+ /**
73
+ * Parses "provider/model-id" format.
74
+ * Returns null if "/" is missing.
75
+ */
76
+ function parseModel(value) {
77
+ if (typeof value !== "string" || !value.includes("/")) {
78
+ return null;
79
+ }
80
+ const slashIndex = value.indexOf("/");
81
+ const provider = value.slice(0, slashIndex).trim();
82
+ const modelId = value.slice(slashIndex + 1).trim();
83
+ if (!provider || !modelId)
84
+ return null;
85
+ return { provider, modelId };
86
+ }
87
+ function validateThinkingLevel(value) {
88
+ if (!value)
89
+ return undefined;
90
+ if (VALID_THINKING_LEVELS.includes(value))
91
+ return value;
92
+ return undefined;
93
+ }
94
+ function parseModelField(value, options) {
95
+ if (typeof value === "string") {
96
+ const parsedModel = parseModel(value);
97
+ if (!parsedModel) {
98
+ return {
99
+ ...(options.setValueOnInvalidType ? { model: value } : {}),
100
+ warnings: [{ code: "invalid-model-format", model: value }],
101
+ };
102
+ }
103
+ return {
104
+ model: value,
105
+ parsedModel,
106
+ warnings: [],
107
+ };
108
+ }
109
+ if (value !== undefined && options.warnOnInvalidType) {
110
+ return {
111
+ warnings: [{ code: "invalid-type", fieldName: "model", expected: "string" }],
112
+ };
113
+ }
114
+ return { warnings: [] };
115
+ }
116
+ function parseThinkingField(value, options) {
117
+ if (typeof value === "string") {
118
+ const thinking = validateThinkingLevel(value);
119
+ if (!thinking) {
120
+ return {
121
+ warnings: [{ code: "invalid-thinking-level", thinking: value }],
122
+ };
123
+ }
124
+ return { thinking, warnings: [] };
125
+ }
126
+ if (value !== undefined && options.warnOnInvalidType) {
127
+ return {
128
+ warnings: [{ code: "invalid-type", fieldName: "thinking", expected: "string" }],
129
+ };
130
+ }
131
+ return { warnings: [] };
132
+ }
133
+ function parseToolsField(value, options) {
134
+ const parsedTools = parseListField(value, "tools");
135
+ const validTools = parsedTools.values.filter(isSupportedToolName);
136
+ const invalidTools = parsedTools.values.filter((toolName) => !isSupportedToolName(toolName));
137
+ const warnings = [...parsedTools.warnings];
138
+ if (invalidTools.length > 0) {
139
+ warnings.push({ code: "unknown-tools", tools: invalidTools });
140
+ }
141
+ if (invalidTools.length > 0 && validTools.length === 0 && !options.setValueOnInvalidType) {
142
+ return { warnings };
143
+ }
144
+ if (parsedTools.warnings.length > 0 && !options.setValueOnInvalidType) {
145
+ return { warnings };
146
+ }
147
+ return {
148
+ tools: validTools,
149
+ warnings,
150
+ };
151
+ }
152
+ function parseSkillsField(value, options) {
153
+ const parsedSkills = parseListField(value, "skills");
154
+ if (parsedSkills.warnings.length > 0 && !options.setValueOnInvalidType) {
155
+ return { warnings: parsedSkills.warnings };
156
+ }
157
+ return {
158
+ skills: parsedSkills.values,
159
+ warnings: parsedSkills.warnings,
160
+ };
161
+ }
162
+ function parseBooleanField(fieldName, value, options) {
163
+ if (typeof value === "boolean") {
164
+ return {
165
+ [fieldName]: value,
166
+ warnings: [],
167
+ };
168
+ }
169
+ if (value !== undefined && options.warnOnInvalidType) {
170
+ return {
171
+ warnings: [{ code: "invalid-type", fieldName, expected: "boolean" }],
172
+ };
173
+ }
174
+ return { warnings: [] };
175
+ }
176
+ function parseSharedFields(record, options) {
177
+ const model = parseModelField(record.model, options);
178
+ const thinking = parseThinkingField(record.thinking, options);
179
+ const tools = Object.prototype.hasOwnProperty.call(record, "tools")
180
+ ? parseToolsField(record.tools, options)
181
+ : { warnings: [] };
182
+ const skills = Object.prototype.hasOwnProperty.call(record, "skills")
183
+ ? parseSkillsField(record.skills, options)
184
+ : { warnings: [] };
185
+ const compaction = parseBooleanField("compaction", record.compaction, options);
186
+ const interactive = parseBooleanField("interactive", record.interactive, options);
187
+ return {
188
+ ...("model" in model ? { model: model.model } : {}),
189
+ ...("parsedModel" in model ? { parsedModel: model.parsedModel } : {}),
190
+ ...(thinking.thinking !== undefined ? { thinking: thinking.thinking } : {}),
191
+ ...(tools.tools !== undefined ? { tools: tools.tools } : {}),
192
+ ...(skills.skills !== undefined ? { skills: skills.skills } : {}),
193
+ ...(compaction.compaction !== undefined ? { compaction: compaction.compaction } : {}),
194
+ ...(interactive.interactive !== undefined ? { interactive: interactive.interactive } : {}),
195
+ warnings: [
196
+ ...model.warnings,
197
+ ...thinking.warnings,
198
+ ...tools.warnings,
199
+ ...skills.warnings,
200
+ ...compaction.warnings,
201
+ ...interactive.warnings,
202
+ ],
203
+ };
204
+ }
205
+ export function parseAgentDefinition(content, filePath) {
206
+ const warnings = [];
207
+ let frontmatter;
208
+ let body;
209
+ try {
210
+ const parsed = parseFrontmatter(content);
211
+ frontmatter = parsed.frontmatter;
212
+ body = parsed.body;
213
+ }
214
+ catch (error) {
215
+ const reason = error instanceof Error ? error.message : String(error);
216
+ return {
217
+ agent: null,
218
+ warnings: [
219
+ createDiscoveryWarning(filePath, `Ignored invalid subagent definition. Frontmatter could not be parsed: ${reason}`),
220
+ ],
221
+ };
222
+ }
223
+ const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : undefined;
224
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : undefined;
225
+ if (!name || !description) {
226
+ return {
227
+ agent: null,
228
+ warnings: [
229
+ createDiscoveryWarning(filePath, 'Ignored invalid subagent definition. Required frontmatter fields "name" and "description" must be non-empty strings.'),
230
+ ],
231
+ };
232
+ }
233
+ if (/\s/.test(name)) {
234
+ return {
235
+ agent: null,
236
+ warnings: [
237
+ createDiscoveryWarning(filePath, `Ignored subagent definition "${name}". Subagent names cannot contain whitespace. Use "-" instead.`),
238
+ ],
239
+ };
240
+ }
241
+ const parsedFields = parseSharedFields(frontmatter, {
242
+ warnOnInvalidType: false,
243
+ setValueOnInvalidType: true,
244
+ });
245
+ warnings.push(...toDiscoveryWarnings(filePath, "subagent", name, parsedFields.warnings));
246
+ const { model, parsedModel, thinking, tools, skills, compaction, interactive } = parsedFields;
247
+ return {
248
+ agent: {
249
+ name,
250
+ description,
251
+ model,
252
+ parsedModel: parsedModel ?? undefined,
253
+ thinking,
254
+ tools,
255
+ skills,
256
+ compaction,
257
+ interactive,
258
+ systemPrompt: body,
259
+ filePath,
260
+ },
261
+ warnings,
262
+ };
263
+ }
264
+ function loadAgentFile(filePath) {
265
+ try {
266
+ return {
267
+ content: fs.readFileSync(filePath, "utf-8"),
268
+ warnings: [],
269
+ };
270
+ }
271
+ catch (error) {
272
+ const reason = error instanceof Error ? error.message : String(error);
273
+ return {
274
+ content: null,
275
+ warnings: [
276
+ createDiscoveryWarning(filePath, `Ignored subagent definition. File could not be read: ${reason}`),
277
+ ],
278
+ };
279
+ }
280
+ }
281
+ function loadAgentDefinitionFromFile(filePath) {
282
+ const file = loadAgentFile(filePath);
283
+ if (!file.content) {
284
+ return { agent: null, warnings: file.warnings };
285
+ }
286
+ const parsed = parseAgentDefinition(file.content, filePath);
287
+ return {
288
+ agent: parsed.agent,
289
+ warnings: [...file.warnings, ...parsed.warnings],
290
+ };
291
+ }
292
+ function loadAgentDefinitionFiles(agentsDir) {
293
+ let entries;
294
+ try {
295
+ entries = fs.readdirSync(agentsDir, { withFileTypes: true });
296
+ }
297
+ catch (error) {
298
+ const reason = error instanceof Error ? error.message : String(error);
299
+ return {
300
+ filePaths: [],
301
+ warnings: [
302
+ createDiscoveryWarning(agentsDir, `Subagent directory could not be read: ${reason}`),
303
+ ],
304
+ };
305
+ }
306
+ return {
307
+ filePaths: entries
308
+ .filter((entry) => entry.name.endsWith(".md"))
309
+ .filter((entry) => entry.isFile() || entry.isSymbolicLink())
310
+ .map((entry) => path.join(agentsDir, entry.name)),
311
+ warnings: [],
312
+ };
313
+ }
314
+ function parseOverrideFields(agentName, value, filePath) {
315
+ const warnings = [];
316
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
317
+ return {
318
+ override: null,
319
+ warnings: [
320
+ createDiscoveryWarning(filePath, `Subagent override "${agentName}" must be a JSON object, ignoring`),
321
+ ],
322
+ };
323
+ }
324
+ const record = value;
325
+ for (const fieldName of Object.keys(record)) {
326
+ if (fieldName === "name" || fieldName === "description") {
327
+ warnings.push(createDiscoveryWarning(filePath, `Subagent override "${agentName}": field "${fieldName}" is not overridable, ignoring`));
328
+ continue;
329
+ }
330
+ if (!ALLOWED_OVERRIDE_FIELDS.has(fieldName)) {
331
+ warnings.push(createDiscoveryWarning(filePath, `Subagent override "${agentName}": unknown field "${fieldName}", ignoring`));
332
+ }
333
+ }
334
+ const parsedFields = parseSharedFields(record, {
335
+ warnOnInvalidType: true,
336
+ setValueOnInvalidType: false,
337
+ });
338
+ warnings.push(...toDiscoveryWarnings(filePath, "subagent override", agentName, parsedFields.warnings));
339
+ const override = {};
340
+ if (parsedFields.model !== undefined) {
341
+ override.model = parsedFields.model;
342
+ }
343
+ if (parsedFields.parsedModel !== undefined) {
344
+ override.parsedModel = parsedFields.parsedModel;
345
+ }
346
+ if (parsedFields.thinking !== undefined) {
347
+ override.thinking = parsedFields.thinking;
348
+ }
349
+ if (parsedFields.tools !== undefined) {
350
+ override.tools = parsedFields.tools;
351
+ }
352
+ if (parsedFields.skills !== undefined) {
353
+ override.skills = parsedFields.skills;
354
+ }
355
+ if (parsedFields.compaction !== undefined) {
356
+ override.compaction = parsedFields.compaction;
357
+ }
358
+ if (parsedFields.interactive !== undefined) {
359
+ override.interactive = parsedFields.interactive;
360
+ }
361
+ return { override, warnings };
362
+ }
363
+ function parseConfigFile(content, filePath) {
364
+ let parsed;
365
+ try {
366
+ parsed = JSON.parse(content);
367
+ }
368
+ catch (error) {
369
+ const reason = error instanceof Error ? error.message : String(error);
370
+ return {
371
+ overrides: {},
372
+ overrideSources: {},
373
+ warnings: [
374
+ createDiscoveryWarning(filePath, `Ignored pi-crew config. JSON could not be parsed: ${reason}`),
375
+ ],
376
+ };
377
+ }
378
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
379
+ return {
380
+ overrides: {},
381
+ overrideSources: {},
382
+ warnings: [
383
+ createDiscoveryWarning(filePath, "Ignored pi-crew config. Root value must be a JSON object."),
384
+ ],
385
+ };
386
+ }
387
+ const root = parsed;
388
+ if (root.agents === undefined) {
389
+ return { overrides: {}, overrideSources: {}, warnings: [] };
390
+ }
391
+ if (!root.agents || typeof root.agents !== "object" || Array.isArray(root.agents)) {
392
+ return {
393
+ overrides: {},
394
+ overrideSources: {},
395
+ warnings: [
396
+ createDiscoveryWarning(filePath, 'Ignored pi-crew config. Field "agents" must be a JSON object.'),
397
+ ],
398
+ };
399
+ }
400
+ const overrides = {};
401
+ const overrideSources = {};
402
+ const warnings = [];
403
+ for (const [agentName, value] of Object.entries(root.agents)) {
404
+ if (!agentName.trim()) {
405
+ warnings.push(createDiscoveryWarning(filePath, "Ignored pi-crew config entry with empty subagent name."));
406
+ continue;
407
+ }
408
+ const parsedOverride = parseOverrideFields(agentName, value, filePath);
409
+ warnings.push(...parsedOverride.warnings);
410
+ if (parsedOverride.override) {
411
+ overrides[agentName] = parsedOverride.override;
412
+ overrideSources[agentName] = filePath;
413
+ }
414
+ }
415
+ return { overrides, overrideSources, warnings };
416
+ }
417
+ function loadConfigOverridesFromFile(filePath) {
418
+ if (!fs.existsSync(filePath)) {
419
+ return { overrides: {}, overrideSources: {}, warnings: [] };
420
+ }
421
+ try {
422
+ const content = fs.readFileSync(filePath, "utf-8");
423
+ return parseConfigFile(content, filePath);
424
+ }
425
+ catch (error) {
426
+ const reason = error instanceof Error ? error.message : String(error);
427
+ return {
428
+ overrides: {},
429
+ overrideSources: {},
430
+ warnings: [
431
+ createDiscoveryWarning(filePath, `Ignored pi-crew config. File could not be read: ${reason}`),
432
+ ],
433
+ };
434
+ }
435
+ }
436
+ function mergeConfigOverrides(base, override) {
437
+ const merged = { ...base };
438
+ for (const [agentName, agentOverride] of Object.entries(override)) {
439
+ merged[agentName] = {
440
+ ...(merged[agentName] ?? {}),
441
+ ...agentOverride,
442
+ };
443
+ }
444
+ return merged;
445
+ }
446
+ function mergeOverrideSources(base, override) {
447
+ return {
448
+ ...base,
449
+ ...override,
450
+ };
451
+ }
452
+ function loadConfigOverrides(cwd) {
453
+ const globalPath = path.join(getAgentDir(), "pi-crew.json");
454
+ const projectPath = path.join(cwd, ".pi", "pi-crew.json");
455
+ const globalConfig = loadConfigOverridesFromFile(globalPath);
456
+ const projectConfig = loadConfigOverridesFromFile(projectPath);
457
+ return {
458
+ overrides: mergeConfigOverrides(globalConfig.overrides, projectConfig.overrides),
459
+ overrideSources: mergeOverrideSources(globalConfig.overrideSources, projectConfig.overrideSources),
460
+ warnings: [...globalConfig.warnings, ...projectConfig.warnings],
461
+ };
462
+ }
463
+ function applyAgentOverride(agent, override) {
464
+ return {
465
+ ...agent,
466
+ ...(override.model !== undefined ? { model: override.model, parsedModel: override.parsedModel } : {}),
467
+ ...(override.thinking !== undefined ? { thinking: override.thinking } : {}),
468
+ ...(override.tools !== undefined ? { tools: override.tools } : {}),
469
+ ...(override.skills !== undefined ? { skills: override.skills } : {}),
470
+ ...(override.compaction !== undefined ? { compaction: override.compaction } : {}),
471
+ ...(override.interactive !== undefined ? { interactive: override.interactive } : {}),
472
+ };
473
+ }
474
+ const bundledAgentsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
475
+ /**
476
+ * Loads agents from a single directory into the agents list.
477
+ * Skips agents whose name already exists in seenNames (higher-priority source wins).
478
+ * Within the same directory, duplicate names produce a warning.
479
+ */
480
+ function loadAgentsFromDir(agentsDir, seenNames, agents, warnings) {
481
+ if (!fs.existsSync(agentsDir))
482
+ return;
483
+ const fileLoad = loadAgentDefinitionFiles(agentsDir);
484
+ warnings.push(...fileLoad.warnings);
485
+ const dirNames = new Set();
486
+ for (const filePath of fileLoad.filePaths) {
487
+ const loaded = loadAgentDefinitionFromFile(filePath);
488
+ warnings.push(...loaded.warnings);
489
+ if (!loaded.agent)
490
+ continue;
491
+ const { name } = loaded.agent;
492
+ // Higher-priority source already registered this name
493
+ if (seenNames.has(name))
494
+ continue;
495
+ // Duplicate within the same directory
496
+ if (dirNames.has(name)) {
497
+ warnings.push(createDiscoveryWarning(filePath, `Duplicate subagent name "${name}" in ${agentsDir}, skipping`));
498
+ continue;
499
+ }
500
+ dirNames.add(name);
501
+ seenNames.set(name, filePath);
502
+ agents.push(loaded.agent);
503
+ }
504
+ }
505
+ export function discoverAgents(cwd = process.cwd()) {
506
+ const agents = [];
507
+ const warnings = [];
508
+ const seenNames = new Map();
509
+ // Priority 1: project-level agents
510
+ loadAgentsFromDir(path.join(cwd, ".pi", "agents"), seenNames, agents, warnings);
511
+ // Priority 2: user global agents
512
+ loadAgentsFromDir(path.join(getAgentDir(), "agents"), seenNames, agents, warnings);
513
+ // Priority 3: bundled agents
514
+ loadAgentsFromDir(bundledAgentsDir, seenNames, agents, warnings);
515
+ const configOverrides = loadConfigOverrides(cwd);
516
+ warnings.push(...configOverrides.warnings);
517
+ const finalAgents = agents.map((agent) => {
518
+ const override = configOverrides.overrides[agent.name];
519
+ return override ? applyAgentOverride(agent, override) : agent;
520
+ });
521
+ for (const agentName of Object.keys(configOverrides.overrides)) {
522
+ if (!seenNames.has(agentName)) {
523
+ warnings.push(createDiscoveryWarning(configOverrides.overrideSources[agentName] ?? path.join(cwd, ".pi", "pi-crew.json"), `Subagent override "${agentName}" does not match any discovered subagent, ignoring`));
524
+ }
525
+ }
526
+ return { agents: finalAgents, warnings };
527
+ }