@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.
- package/README.md +8 -8
- package/agents/code-reviewer.md +2 -2
- package/agents/oracle.md +1 -1
- package/agents/planner.md +5 -1
- package/agents/quality-reviewer.md +2 -2
- package/agents/scout.md +2 -2
- package/agents/worker.md +3 -3
- package/extension/agent-catalog.ts +369 -0
- package/extension/agent-config-fields.ts +359 -0
- package/extension/agent-discovery.ts +49 -717
- package/extension/index.ts +4 -2
- package/extension/integration/crew-tool-actions.ts +306 -0
- package/extension/integration/crew-tool-executor.ts +109 -0
- package/extension/integration/register-tools.ts +10 -2
- package/extension/integration/tool-presentation.ts +0 -20
- package/extension/integration/tools/crew-abort.ts +14 -84
- package/extension/integration/tools/crew-done.ts +7 -26
- package/extension/integration/tools/crew-list.ts +4 -60
- package/extension/integration/tools/crew-respond.ts +8 -29
- package/extension/integration/tools/crew-spawn.ts +15 -56
- package/extension/message-delivery-policy.ts +22 -0
- package/extension/runtime/crew-runtime.ts +60 -223
- package/extension/runtime/{delivery-coordinator.ts → owner-session-coordinator.ts} +44 -37
- package/extension/runtime/subagent-lifecycle.ts +203 -0
- package/extension/runtime/subagent-registry.ts +50 -6
- package/extension/runtime/subagent-transitions.ts +100 -0
- package/extension/subagent-messages.ts +9 -17
- package/package.json +8 -6
- package/prompts/pi-crew-plan.md +14 -13
- package/prompts/pi-crew-review.md +20 -16
- package/skills/pi-crew/REFERENCE.md +32 -20
- package/skills/pi-crew/SKILL.md +13 -10
- package/extension/integration/tools/tool-deps.ts +0 -16
- package/extension/integration.ts +0 -13
- 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
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
487
|
-
|
|
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
|
-
|
|
599
|
-
|
|
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
|
-
|
|
647
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
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
|
}
|