@jwilger/pi-development-system 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/strict-lints/SKILL.md +2 -1
- package/src/core/spawn-overrides.ts +2 -2
- package/src/core/test-runner.ts +8 -2
- package/src/review/digest.ts +7 -1
- package/src/subagents/VENDORED.md +1 -1
- package/src/subagents/orch/mailbox.ts +11 -7
- package/src/subagents/orch/manager.ts +7 -1
- package/src/subagents/orch/runtime.ts +38 -33
- package/src/subagents/orch/tools.ts +12 -10
- package/src/subagents/prefs/agent-import.ts +15 -15
- package/src/subagents/prefs/config.ts +85 -185
- package/src/subagents/prefs/models.ts +6 -10
- package/src/subagents/prefs/settings.ts +38 -38
- package/src/subagents/types.ts +22 -19
- package/src/subagents/ui/dialog.ts +99 -84
- package/src/subagents/ui/import-picker.ts +28 -11
- package/src/subagents/ui/model-picker.ts +26 -17
- package/src/subagents/ui/settings-ui.ts +16 -34
- package/src/subagents/ui/status-ui.ts +140 -51
- package/src/subagents/ui/ui.ts +162 -265
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
//
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: agent-file parsing and layer merging are long validators kept close to upstream; only parseAgentType is covered (test/agents/agents.test.ts), so splitting the rest would move untested behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import {
|
|
3
4
|
lstatSync,
|
|
4
5
|
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
6
|
readdirSync,
|
|
7
|
+
readFileSync,
|
|
7
8
|
renameSync,
|
|
8
9
|
unlinkSync,
|
|
9
10
|
writeFileSync,
|
|
10
11
|
} from "node:fs";
|
|
11
12
|
import { dirname, join, resolve } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { isMap, isScalar, parseDocument, stringify } from "yaml";
|
|
15
|
+
import { type AgentType, THINKING_LEVELS } from "../types.ts";
|
|
15
16
|
import { getModelPreferences } from "./models.ts";
|
|
16
|
-
import { THINKING_LEVELS, type AgentType } from "../types.ts";
|
|
17
17
|
import type { ToolFilteringMode } from "./settings.ts";
|
|
18
18
|
|
|
19
19
|
// Pi semantic color tokens, resolved as concrete colors for agent name backgrounds.
|
|
@@ -94,9 +94,7 @@ function errorCode(error: unknown): string | undefined {
|
|
|
94
94
|
: undefined;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function lstatIfPresent(
|
|
98
|
-
path: string,
|
|
99
|
-
): ReturnType<typeof lstatSync> | undefined {
|
|
97
|
+
function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
100
98
|
try {
|
|
101
99
|
return lstatSync(path);
|
|
102
100
|
} catch (error) {
|
|
@@ -105,9 +103,7 @@ function lstatIfPresent(
|
|
|
105
103
|
}
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
function assertNotSymlinkPath(
|
|
109
|
-
path: string,
|
|
110
|
-
): ReturnType<typeof lstatSync> | undefined {
|
|
106
|
+
function assertNotSymlinkPath(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
111
107
|
const stat = lstatIfPresent(path);
|
|
112
108
|
if (stat?.isSymbolicLink()) throw new Error(`Unsafe symlink path: ${path}`);
|
|
113
109
|
return stat;
|
|
@@ -130,12 +126,9 @@ function validatePreservedDestination(
|
|
|
130
126
|
}
|
|
131
127
|
|
|
132
128
|
function frontmatter(content: string): { yaml: string; body: string } {
|
|
133
|
-
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)^---[ \t]*(?:\r?\n|$)/m.exec(
|
|
134
|
-
|
|
135
|
-
);
|
|
136
|
-
if (!match || match.index !== 0)
|
|
137
|
-
throw new Error("Expected YAML frontmatter enclosed by --- lines");
|
|
138
|
-
return { yaml: match[1], body: content.slice(match[0].length) };
|
|
129
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)^---[ \t]*(?:\r?\n|$)/m.exec(content);
|
|
130
|
+
if (match?.index !== 0) throw new Error("Expected YAML frontmatter enclosed by --- lines");
|
|
131
|
+
return { yaml: match[1] ?? "", body: content.slice(match[0].length) };
|
|
139
132
|
}
|
|
140
133
|
|
|
141
134
|
function record(value: unknown, label: string): Record<string, unknown> {
|
|
@@ -161,13 +154,8 @@ function modelSuggestionNames(value: unknown): string[] {
|
|
|
161
154
|
}
|
|
162
155
|
|
|
163
156
|
function toolNames(value: unknown, label: string): string[] {
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
value.some((name) => typeof name !== "string" || !TOOL.test(name))
|
|
167
|
-
) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`${label} must be an array of exact tool names (no wildcards or patterns)`,
|
|
170
|
-
);
|
|
157
|
+
if (!Array.isArray(value) || value.some((name) => typeof name !== "string" || !TOOL.test(name))) {
|
|
158
|
+
throw new Error(`${label} must be an array of exact tool names (no wildcards or patterns)`);
|
|
171
159
|
}
|
|
172
160
|
if (new Set(value).size !== value.length)
|
|
173
161
|
throw new Error(`${label} contains duplicate tool names`);
|
|
@@ -177,13 +165,10 @@ function toolNames(value: unknown, label: string): string[] {
|
|
|
177
165
|
function toolPolicy(value: unknown): NonNullable<AgentType["tools"]> {
|
|
178
166
|
const mapping = record(value, "tools");
|
|
179
167
|
for (const key of Object.keys(mapping))
|
|
180
|
-
if (key !== "allow" && key !== "block")
|
|
181
|
-
throw new Error(`Unknown tools field: ${key}`);
|
|
168
|
+
if (key !== "allow" && key !== "block") throw new Error(`Unknown tools field: ${key}`);
|
|
182
169
|
const result: NonNullable<AgentType["tools"]> = {};
|
|
183
|
-
if (Object.hasOwn(mapping, "allow"))
|
|
184
|
-
|
|
185
|
-
if (Object.hasOwn(mapping, "block"))
|
|
186
|
-
result.block = toolNames(mapping.block, "tools.block");
|
|
170
|
+
if (Object.hasOwn(mapping, "allow")) result.allow = toolNames(mapping.allow, "tools.allow");
|
|
171
|
+
if (Object.hasOwn(mapping, "block")) result.block = toolNames(mapping.block, "tools.block");
|
|
187
172
|
return result;
|
|
188
173
|
}
|
|
189
174
|
|
|
@@ -191,12 +176,10 @@ export function parseAgentType(content: string, filePath?: string): AgentType {
|
|
|
191
176
|
try {
|
|
192
177
|
const { yaml, body } = frontmatter(content);
|
|
193
178
|
const doc = parseDocument(yaml, { uniqueKeys: true });
|
|
194
|
-
if (doc.errors.length)
|
|
195
|
-
throw new Error(doc.errors.map((error) => error.message).join("; "));
|
|
179
|
+
if (doc.errors.length > 0) throw new Error(doc.errors.map((error) => error.message).join("; "));
|
|
196
180
|
const data = record(doc.toJS({ maxAliasCount: 0 }), "Frontmatter");
|
|
197
181
|
for (const key of Object.keys(data))
|
|
198
|
-
if (!FIELDS.has(key))
|
|
199
|
-
throw new Error(`Unknown frontmatter field: ${key}`);
|
|
182
|
+
if (!FIELDS.has(key)) throw new Error(`Unknown frontmatter field: ${key}`);
|
|
200
183
|
if (typeof data.name !== "string" || !NAME.test(data.name))
|
|
201
184
|
throw new Error(
|
|
202
185
|
"name must be a safe identifier using letters, numbers, underscores or hyphens",
|
|
@@ -216,26 +199,20 @@ export function parseAgentType(content: string, filePath?: string): AgentType {
|
|
|
216
199
|
if (Object.hasOwn(data, "modelSuggestions"))
|
|
217
200
|
result.modelSuggestions = modelSuggestionNames(data.modelSuggestions);
|
|
218
201
|
if (Object.hasOwn(data, "thinkingLevel")) {
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
data.thinkingLevel as AgentType["thinkingLevel"] & string,
|
|
222
|
-
)
|
|
223
|
-
)
|
|
224
|
-
throw new Error(
|
|
225
|
-
`thinkingLevel must be one of: ${THINKING_LEVELS.join(", ")}`,
|
|
226
|
-
);
|
|
202
|
+
if (!THINKING_LEVELS.includes(data.thinkingLevel as AgentType["thinkingLevel"] & string))
|
|
203
|
+
throw new Error(`thinkingLevel must be one of: ${THINKING_LEVELS.join(", ")}`);
|
|
227
204
|
result.thinkingLevel = data.thinkingLevel as AgentType["thinkingLevel"];
|
|
228
205
|
}
|
|
229
206
|
if (Object.hasOwn(data, "color")) {
|
|
230
207
|
if (!AGENT_COLORS.includes(data.color as (typeof AGENT_COLORS)[number]))
|
|
231
|
-
throw new Error(
|
|
232
|
-
`color must be a Pi foreground token: ${AGENT_COLORS.join(", ")}`,
|
|
233
|
-
);
|
|
208
|
+
throw new Error(`color must be a Pi foreground token: ${AGENT_COLORS.join(", ")}`);
|
|
234
209
|
result.color = data.color as string;
|
|
235
210
|
}
|
|
236
211
|
if (Object.hasOwn(data, "icon")) {
|
|
237
212
|
if (typeof data.icon !== "string" || !/^\p{Co}$/u.test(data.icon))
|
|
238
|
-
throw new Error(
|
|
213
|
+
throw new Error(
|
|
214
|
+
"icon must be a single literal Nerd Font glyph (Unicode private-use character)",
|
|
215
|
+
);
|
|
239
216
|
result.icon = data.icon;
|
|
240
217
|
}
|
|
241
218
|
if (Object.hasOwn(data, "tools")) result.tools = toolPolicy(data.tools);
|
|
@@ -244,13 +221,13 @@ export function parseAgentType(content: string, filePath?: string): AgentType {
|
|
|
244
221
|
} catch (error) {
|
|
245
222
|
throw new Error(
|
|
246
223
|
`${filePath ?? "Agent definition"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
224
|
+
{ cause: error },
|
|
247
225
|
);
|
|
248
226
|
}
|
|
249
227
|
}
|
|
250
228
|
|
|
251
229
|
export function serializeAgentType(type: AgentType): string {
|
|
252
|
-
if (typeof type.systemPrompt !== "string")
|
|
253
|
-
throw new Error("systemPrompt must be a string");
|
|
230
|
+
if (typeof type.systemPrompt !== "string") throw new Error("systemPrompt must be a string");
|
|
254
231
|
const data: Record<string, unknown> = {
|
|
255
232
|
name: type.name,
|
|
256
233
|
description: type.description,
|
|
@@ -317,12 +294,9 @@ export function parseAgentSettings(
|
|
|
317
294
|
const label = filePath ?? "Agent settings override";
|
|
318
295
|
try {
|
|
319
296
|
if (/^\uFEFF?---[ \t]*\r?\n/m.test(content) || content.includes("\n---"))
|
|
320
|
-
throw new Error(
|
|
321
|
-
"Settings overrides are plain YAML with no frontmatter and no Markdown body",
|
|
322
|
-
);
|
|
297
|
+
throw new Error("Settings overrides are plain YAML with no frontmatter and no Markdown body");
|
|
323
298
|
const doc = parseDocument(content, { uniqueKeys: true });
|
|
324
|
-
if (doc.errors.length)
|
|
325
|
-
throw new Error(doc.errors.map((error) => error.message).join("; "));
|
|
299
|
+
if (doc.errors.length > 0) throw new Error(doc.errors.map((error) => error.message).join("; "));
|
|
326
300
|
const data = record(doc.toJS({ maxAliasCount: 0 }) ?? {}, "Settings override");
|
|
327
301
|
for (const key of Object.keys(data)) {
|
|
328
302
|
if (key === "systemPrompt" || key === "prompt" || key === "body")
|
|
@@ -350,16 +324,14 @@ export function parseAgentSettings(
|
|
|
350
324
|
}
|
|
351
325
|
const hasModels = Object.hasOwn(data, "models");
|
|
352
326
|
const hasModel = Object.hasOwn(data, "model");
|
|
353
|
-
if (hasModels && hasModel)
|
|
354
|
-
throw new Error("Specify either models or model, not both");
|
|
327
|
+
if (hasModels && hasModel) throw new Error("Specify either models or model, not both");
|
|
355
328
|
if (hasModels)
|
|
356
329
|
result.models = nullableField(data.models, (value) =>
|
|
357
330
|
getModelPreferences({ models: value }),
|
|
358
|
-
) as string[] | null
|
|
331
|
+
) as string[] | null;
|
|
359
332
|
if (hasModel) {
|
|
360
333
|
if (data.model === null) result.model = null;
|
|
361
|
-
else
|
|
362
|
-
result.models = getModelPreferences({ model: data.model }) as string[];
|
|
334
|
+
else result.models = getModelPreferences({ model: data.model }) as string[];
|
|
363
335
|
}
|
|
364
336
|
if (Object.hasOwn(data, "modelSuggestions"))
|
|
365
337
|
result.modelSuggestions = nullableField(data.modelSuggestions, (value) =>
|
|
@@ -368,17 +340,13 @@ export function parseAgentSettings(
|
|
|
368
340
|
if (Object.hasOwn(data, "thinkingLevel"))
|
|
369
341
|
result.thinkingLevel = nullableField(data.thinkingLevel, (value) => {
|
|
370
342
|
if (!THINKING_LEVELS.includes(value as AgentType["thinkingLevel"] & string))
|
|
371
|
-
throw new Error(
|
|
372
|
-
`thinkingLevel must be one of: ${THINKING_LEVELS.join(", ")}`,
|
|
373
|
-
);
|
|
343
|
+
throw new Error(`thinkingLevel must be one of: ${THINKING_LEVELS.join(", ")}`);
|
|
374
344
|
return value as AgentType["thinkingLevel"];
|
|
375
345
|
});
|
|
376
346
|
if (Object.hasOwn(data, "color"))
|
|
377
347
|
result.color = nullableField(data.color, (value) => {
|
|
378
348
|
if (!AGENT_COLORS.includes(value as (typeof AGENT_COLORS)[number]))
|
|
379
|
-
throw new Error(
|
|
380
|
-
`color must be a Pi foreground token: ${AGENT_COLORS.join(", ")}`,
|
|
381
|
-
);
|
|
349
|
+
throw new Error(`color must be a Pi foreground token: ${AGENT_COLORS.join(", ")}`);
|
|
382
350
|
return value as string;
|
|
383
351
|
});
|
|
384
352
|
if (Object.hasOwn(data, "icon"))
|
|
@@ -393,9 +361,9 @@ export function parseAgentSettings(
|
|
|
393
361
|
result.tools = data.tools === null ? null : toolPolicy(data.tools);
|
|
394
362
|
return result;
|
|
395
363
|
} catch (error) {
|
|
396
|
-
throw new Error(
|
|
397
|
-
|
|
398
|
-
);
|
|
364
|
+
throw new Error(`${label}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
365
|
+
cause: error,
|
|
366
|
+
});
|
|
399
367
|
}
|
|
400
368
|
}
|
|
401
369
|
|
|
@@ -411,9 +379,7 @@ export function serializeAgentSettings(override: AgentSettingsOverride): string
|
|
|
411
379
|
else if (models !== undefined) data.models = models;
|
|
412
380
|
if (override.modelSuggestions !== undefined)
|
|
413
381
|
data.modelSuggestions =
|
|
414
|
-
override.modelSuggestions === null
|
|
415
|
-
? null
|
|
416
|
-
: modelSuggestionNames(override.modelSuggestions);
|
|
382
|
+
override.modelSuggestions === null ? null : modelSuggestionNames(override.modelSuggestions);
|
|
417
383
|
for (const key of ["thinkingLevel", "color", "icon", "tools"] as const)
|
|
418
384
|
if (override[key] !== undefined) data[key] = override[key];
|
|
419
385
|
const content = `${stringify(data)}`;
|
|
@@ -421,25 +387,12 @@ export function serializeAgentSettings(override: AgentSettingsOverride): string
|
|
|
421
387
|
return content;
|
|
422
388
|
}
|
|
423
389
|
|
|
424
|
-
/** Fields an override may clear back to the base/inherit state with null. */
|
|
425
|
-
const CLEARABLE_OVERRIDE_FIELDS = new Set([
|
|
426
|
-
"models",
|
|
427
|
-
"modelSuggestions",
|
|
428
|
-
"thinkingLevel",
|
|
429
|
-
"color",
|
|
430
|
-
"icon",
|
|
431
|
-
"tools",
|
|
432
|
-
]);
|
|
433
|
-
|
|
434
390
|
/**
|
|
435
391
|
* Merge a sparse settings override on top of a base definition. Unset fields
|
|
436
392
|
* follow the base; explicit null clears the field. The prompt body always
|
|
437
393
|
* comes from the base.
|
|
438
394
|
*/
|
|
439
|
-
export function mergeAgentSettings(
|
|
440
|
-
base: AgentType,
|
|
441
|
-
override: AgentSettingsOverride,
|
|
442
|
-
): AgentType {
|
|
395
|
+
export function mergeAgentSettings(base: AgentType, override: AgentSettingsOverride): AgentType {
|
|
443
396
|
const merged: AgentType = structuredClone(base);
|
|
444
397
|
if (override.description !== undefined) merged.description = override.description;
|
|
445
398
|
const models = getModelPreferences({
|
|
@@ -447,19 +400,13 @@ export function mergeAgentSettings(
|
|
|
447
400
|
model: override.model ?? undefined,
|
|
448
401
|
} as { models?: unknown; model?: unknown });
|
|
449
402
|
if (override.models === null || override.model === null) {
|
|
450
|
-
|
|
451
|
-
|
|
403
|
+
merged.models = undefined;
|
|
404
|
+
merged.model = undefined;
|
|
452
405
|
} else if (models !== undefined) {
|
|
453
406
|
merged.models = [...models];
|
|
454
|
-
|
|
407
|
+
merged.model = undefined;
|
|
455
408
|
}
|
|
456
|
-
for (const key of [
|
|
457
|
-
"modelSuggestions",
|
|
458
|
-
"thinkingLevel",
|
|
459
|
-
"color",
|
|
460
|
-
"icon",
|
|
461
|
-
"tools",
|
|
462
|
-
] as const) {
|
|
409
|
+
for (const key of ["modelSuggestions", "thinkingLevel", "color", "icon", "tools"] as const) {
|
|
463
410
|
const value = override[key];
|
|
464
411
|
if (value === undefined) continue;
|
|
465
412
|
if (value === null) delete merged[key];
|
|
@@ -489,20 +436,13 @@ export function diffAgentSettings(base: AgentType, draft: AgentType): AgentSetti
|
|
|
489
436
|
if (draftModels === undefined) override.models = null;
|
|
490
437
|
else override.models = [...draftModels];
|
|
491
438
|
}
|
|
492
|
-
for (const key of [
|
|
493
|
-
"modelSuggestions",
|
|
494
|
-
"thinkingLevel",
|
|
495
|
-
"color",
|
|
496
|
-
"icon",
|
|
497
|
-
"tools",
|
|
498
|
-
] as const) {
|
|
439
|
+
for (const key of ["modelSuggestions", "thinkingLevel", "color", "icon", "tools"] as const) {
|
|
499
440
|
const before = base[key];
|
|
500
441
|
const after = draft[key];
|
|
501
442
|
if (JSON.stringify(before ?? null) === JSON.stringify(after ?? null)) continue;
|
|
502
443
|
if (after === undefined) override[key] = null as never;
|
|
503
444
|
else Object.assign(override, { [key]: structuredClone(after) });
|
|
504
445
|
}
|
|
505
|
-
void CLEARABLE_OVERRIDE_FIELDS;
|
|
506
446
|
return override;
|
|
507
447
|
}
|
|
508
448
|
|
|
@@ -514,12 +454,7 @@ export function selectTools(
|
|
|
514
454
|
const names = new Set(available);
|
|
515
455
|
if (mode === "all") return [...names];
|
|
516
456
|
// Ignored lists do not participate in validation or tool selection.
|
|
517
|
-
const relevantPolicy =
|
|
518
|
-
mode === "all-except-blocked"
|
|
519
|
-
? policy?.block === undefined
|
|
520
|
-
? {}
|
|
521
|
-
: { block: policy.block }
|
|
522
|
-
: policy;
|
|
457
|
+
const relevantPolicy = blockedOnly(mode, policy);
|
|
523
458
|
const validated = relevantPolicy === undefined ? {} : toolPolicy(relevantPolicy);
|
|
524
459
|
for (const name of [...(validated.allow ?? []), ...(validated.block ?? [])]) {
|
|
525
460
|
if (!names.has(name)) throw new Error(`Unavailable tool name: ${name}`);
|
|
@@ -553,24 +488,17 @@ export class ConfigStore {
|
|
|
553
488
|
this.diagnostics = [];
|
|
554
489
|
const layers: [string, NonNullable<AgentType["source"]>][] = [
|
|
555
490
|
[
|
|
556
|
-
this.options.bundledDir ??
|
|
557
|
-
fileURLToPath(new URL("../../../agents/", import.meta.url)),
|
|
491
|
+
this.options.bundledDir ?? fileURLToPath(new URL("../../../agents/", import.meta.url)),
|
|
558
492
|
"bundled",
|
|
559
493
|
],
|
|
560
494
|
...this.scopeDirectories("user").map(
|
|
561
|
-
(directory): [string, NonNullable<AgentType["source"]>] => [
|
|
562
|
-
directory,
|
|
563
|
-
"user",
|
|
564
|
-
],
|
|
495
|
+
(directory): [string, NonNullable<AgentType["source"]>] => [directory, "user"],
|
|
565
496
|
),
|
|
566
497
|
];
|
|
567
498
|
if (this.options.includeProject)
|
|
568
499
|
layers.push(
|
|
569
500
|
...this.scopeDirectories("project").map(
|
|
570
|
-
(directory): [string, NonNullable<AgentType["source"]>] => [
|
|
571
|
-
directory,
|
|
572
|
-
"project",
|
|
573
|
-
],
|
|
501
|
+
(directory): [string, NonNullable<AgentType["source"]>] => [directory, "project"],
|
|
574
502
|
),
|
|
575
503
|
);
|
|
576
504
|
for (const [directory, source] of layers) {
|
|
@@ -581,9 +509,7 @@ export class ConfigStore {
|
|
|
581
509
|
if (!stat.isDirectory())
|
|
582
510
|
throw new Error(`Agent directory must be a directory: ${directory}`);
|
|
583
511
|
files = readdirSync(directory)
|
|
584
|
-
.filter(
|
|
585
|
-
(file) => file.endsWith(".md") || isAgentOverrideFile(file),
|
|
586
|
-
)
|
|
512
|
+
.filter((file) => file.endsWith(".md") || isAgentOverrideFile(file))
|
|
587
513
|
.sort();
|
|
588
514
|
} catch (error) {
|
|
589
515
|
this.diagnostics.push(`${directory}: ${String(error)}`);
|
|
@@ -607,7 +533,8 @@ export class ConfigStore {
|
|
|
607
533
|
for (const file of files) {
|
|
608
534
|
const filePath = join(directory, file);
|
|
609
535
|
if (isAgentOverrideFile(file)) {
|
|
610
|
-
|
|
536
|
+
const base = overrideBaseName(file);
|
|
537
|
+
if (base !== undefined) blocked.add(base);
|
|
611
538
|
this.diagnostics.push(
|
|
612
539
|
`${filePath}: Settings overrides (<name>.yml) are not allowed in the bundled layer`,
|
|
613
540
|
);
|
|
@@ -616,16 +543,12 @@ export class ConfigStore {
|
|
|
616
543
|
let content = "";
|
|
617
544
|
try {
|
|
618
545
|
if (!lstatSync(filePath).isFile())
|
|
619
|
-
throw new Error(
|
|
620
|
-
"Agent definition must be a regular file, not a symlink",
|
|
621
|
-
);
|
|
546
|
+
throw new Error("Agent definition must be a regular file, not a symlink");
|
|
622
547
|
content = readFileSync(filePath, "utf8");
|
|
623
548
|
const type = parseAgentType(content, filePath);
|
|
624
549
|
if (seenNames.has(type.name)) {
|
|
625
550
|
blocked.add(type.name);
|
|
626
|
-
this.diagnostics.push(
|
|
627
|
-
`${filePath}: Duplicate agent name in bundled layer: ${type.name}`,
|
|
628
|
-
);
|
|
551
|
+
this.diagnostics.push(`${filePath}: Duplicate agent name in bundled layer: ${type.name}`);
|
|
629
552
|
continue;
|
|
630
553
|
}
|
|
631
554
|
seenNames.add(type.name);
|
|
@@ -663,17 +586,10 @@ export class ConfigStore {
|
|
|
663
586
|
* Having both for one name, duplicate declarations, malformed files, or an
|
|
664
587
|
* override without a base fails closed for that agent name.
|
|
665
588
|
*/
|
|
666
|
-
private applyScopeLayer(
|
|
667
|
-
directory: string,
|
|
668
|
-
scope: AgentSettingsScope,
|
|
669
|
-
files: string[],
|
|
670
|
-
): void {
|
|
589
|
+
private applyScopeLayer(directory: string, scope: AgentSettingsScope, files: string[]): void {
|
|
671
590
|
const blocked = new Set<string>();
|
|
672
591
|
const forks = new Map<string, { type: AgentType; file: string }>();
|
|
673
|
-
const overrides = new Map<
|
|
674
|
-
string,
|
|
675
|
-
{ override: AgentSettingsOverride; file: string }
|
|
676
|
-
>();
|
|
592
|
+
const overrides = new Map<string, { override: AgentSettingsOverride; file: string }>();
|
|
677
593
|
const forkNames = new Set<string>();
|
|
678
594
|
const declareBlocked = (name: string) => {
|
|
679
595
|
blocked.add(name);
|
|
@@ -690,9 +606,7 @@ export class ConfigStore {
|
|
|
690
606
|
let content = "";
|
|
691
607
|
try {
|
|
692
608
|
if (!lstatSync(filePath).isFile())
|
|
693
|
-
throw new Error(
|
|
694
|
-
"Agent settings override must be a regular file, not a symlink",
|
|
695
|
-
);
|
|
609
|
+
throw new Error("Agent settings override must be a regular file, not a symlink");
|
|
696
610
|
content = readFileSync(filePath, "utf8");
|
|
697
611
|
const override = parseAgentSettings(content, filePath, overrideName);
|
|
698
612
|
const key = override.name ?? overrideName;
|
|
@@ -735,9 +649,7 @@ export class ConfigStore {
|
|
|
735
649
|
let content = "";
|
|
736
650
|
try {
|
|
737
651
|
if (!lstatSync(filePath).isFile())
|
|
738
|
-
throw new Error(
|
|
739
|
-
"Agent definition must be a regular file, not a symlink",
|
|
740
|
-
);
|
|
652
|
+
throw new Error("Agent definition must be a regular file, not a symlink");
|
|
741
653
|
content = readFileSync(filePath, "utf8");
|
|
742
654
|
const type = parseAgentType(content, filePath);
|
|
743
655
|
if (overrides.has(type.name)) {
|
|
@@ -776,10 +688,13 @@ export class ConfigStore {
|
|
|
776
688
|
this.diagnostics.push(`${filePath}: ${String(error)}`);
|
|
777
689
|
}
|
|
778
690
|
}
|
|
779
|
-
for (const name of [...forks.keys()].filter((
|
|
691
|
+
for (const name of [...forks.keys()].filter((key) => overrides.has(key))) {
|
|
780
692
|
declareBlocked(name);
|
|
693
|
+
const fork = forks.get(name);
|
|
694
|
+
const override = overrides.get(name);
|
|
695
|
+
if (fork === undefined || override === undefined) continue;
|
|
781
696
|
this.diagnostics.push(
|
|
782
|
-
`${join(directory,
|
|
697
|
+
`${join(directory, fork.file)} and ${join(directory, override.file)}: ` +
|
|
783
698
|
`Use either ${name}.md (full fork) or ${name}.yml (settings override), not both`,
|
|
784
699
|
);
|
|
785
700
|
}
|
|
@@ -834,9 +749,7 @@ export class ConfigStore {
|
|
|
834
749
|
}
|
|
835
750
|
|
|
836
751
|
list(): AgentType[] {
|
|
837
|
-
return structuredClone(
|
|
838
|
-
[...this.types.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
839
|
-
);
|
|
752
|
+
return structuredClone([...this.types.values()].sort((a, b) => a.name.localeCompare(b.name)));
|
|
840
753
|
}
|
|
841
754
|
|
|
842
755
|
get(name: string): AgentType {
|
|
@@ -850,19 +763,12 @@ export class ConfigStore {
|
|
|
850
763
|
}
|
|
851
764
|
|
|
852
765
|
private preferredScopeDirectory(scope: AgentScope): string {
|
|
853
|
-
if (scope !== "user" && scope !== "project")
|
|
854
|
-
throw new Error("Invalid agent scope");
|
|
766
|
+
if (scope !== "user" && scope !== "project") throw new Error("Invalid agent scope");
|
|
855
767
|
if (scope === "project" && !this.options.includeProject)
|
|
856
768
|
throw new Error("Project agents are not enabled/trusted");
|
|
857
769
|
return scope === "user"
|
|
858
770
|
? resolve(this.options.agentDir, SUBAGENT_MANAGER_DIR, AGENTS_DIR)
|
|
859
|
-
: resolve(
|
|
860
|
-
this.options.cwd,
|
|
861
|
-
".pi",
|
|
862
|
-
"agent",
|
|
863
|
-
SUBAGENT_MANAGER_DIR,
|
|
864
|
-
AGENTS_DIR,
|
|
865
|
-
);
|
|
771
|
+
: resolve(this.options.cwd, ".pi", "agent", SUBAGENT_MANAGER_DIR, AGENTS_DIR);
|
|
866
772
|
}
|
|
867
773
|
|
|
868
774
|
// Manager-owned storage only. Do not scan legacy Pi agent packages.
|
|
@@ -878,13 +784,7 @@ export class ConfigStore {
|
|
|
878
784
|
const base = resolve(this.options.cwd);
|
|
879
785
|
const projectPi = join(base, ".pi");
|
|
880
786
|
const projectAgent = join(projectPi, "agent");
|
|
881
|
-
return [
|
|
882
|
-
base,
|
|
883
|
-
projectPi,
|
|
884
|
-
projectAgent,
|
|
885
|
-
join(projectAgent, SUBAGENT_MANAGER_DIR),
|
|
886
|
-
directory,
|
|
887
|
-
];
|
|
787
|
+
return [base, projectPi, projectAgent, join(projectAgent, SUBAGENT_MANAGER_DIR), directory];
|
|
888
788
|
}
|
|
889
789
|
|
|
890
790
|
/** Base definition under the top customization, for diffing and read-only display. */
|
|
@@ -903,15 +803,12 @@ export class ConfigStore {
|
|
|
903
803
|
original?.name !== name ||
|
|
904
804
|
original.source !== scope ||
|
|
905
805
|
typeof original.filePath !== "string" ||
|
|
906
|
-
dirname(resolve(original.filePath)) !==
|
|
907
|
-
this.preferredScopeDirectory(scope)
|
|
806
|
+
dirname(resolve(original.filePath)) !== this.preferredScopeDirectory(scope)
|
|
908
807
|
)
|
|
909
808
|
return false;
|
|
910
809
|
const preserved = resolve(original.filePath);
|
|
911
810
|
if (kind === "fork") return preserved.endsWith(".md");
|
|
912
|
-
return AGENT_OVERRIDE_EXTENSIONS.some((extension) =>
|
|
913
|
-
preserved.endsWith(extension),
|
|
914
|
-
);
|
|
811
|
+
return AGENT_OVERRIDE_EXTENSIONS.some((extension) => preserved.endsWith(extension));
|
|
915
812
|
}
|
|
916
813
|
|
|
917
814
|
destination(
|
|
@@ -938,10 +835,7 @@ export class ConfigStore {
|
|
|
938
835
|
private writeContent(filePath: string, name: string, content: string): void {
|
|
939
836
|
const directory = dirname(filePath);
|
|
940
837
|
mkdirSync(directory, { recursive: true });
|
|
941
|
-
const temporary = join(
|
|
942
|
-
dirname(filePath),
|
|
943
|
-
`.${name}.${randomUUID()}.tmp`,
|
|
944
|
-
);
|
|
838
|
+
const temporary = join(dirname(filePath), `.${name}.${randomUUID()}.tmp`);
|
|
945
839
|
try {
|
|
946
840
|
writeFileSync(temporary, content, { flag: "wx", mode: 0o600 });
|
|
947
841
|
renameSync(temporary, filePath);
|
|
@@ -953,10 +847,7 @@ export class ConfigStore {
|
|
|
953
847
|
private assertWritableDestination(filePath: string, scope: AgentScope): void {
|
|
954
848
|
const directory = dirname(filePath);
|
|
955
849
|
// Reject redirected destination directories and files before writing.
|
|
956
|
-
for (const path of [
|
|
957
|
-
...this.destinationBasePaths(scope, directory),
|
|
958
|
-
filePath,
|
|
959
|
-
]) {
|
|
850
|
+
for (const path of [...this.destinationBasePaths(scope, directory), filePath]) {
|
|
960
851
|
const stat = assertNotSymlinkPath(path);
|
|
961
852
|
if (path === filePath && stat && !stat.isFile())
|
|
962
853
|
throw new Error(`Unsafe agent destination: ${filePath}`);
|
|
@@ -988,13 +879,15 @@ export class ConfigStore {
|
|
|
988
879
|
if (!NAME.test(name)) throw new Error("Unsafe agent name");
|
|
989
880
|
if (draft.name !== name)
|
|
990
881
|
throw new Error("Settings overrides cannot rename the agent; fork it instead");
|
|
991
|
-
const base =
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
882
|
+
const base =
|
|
883
|
+
this.getBase(name) ??
|
|
884
|
+
(() => {
|
|
885
|
+
try {
|
|
886
|
+
return this.get(name);
|
|
887
|
+
} catch {
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
})();
|
|
998
891
|
if (!base) throw new Error(`Settings override has no base agent named ${JSON.stringify(name)}`);
|
|
999
892
|
if (base.name !== name)
|
|
1000
893
|
throw new Error(`Settings override has no base agent named ${JSON.stringify(name)}`);
|
|
@@ -1017,8 +910,10 @@ export class ConfigStore {
|
|
|
1017
910
|
const resolved = resolve(customization.filePath);
|
|
1018
911
|
if (
|
|
1019
912
|
dirname(resolved) !== this.preferredScopeDirectory(scope) ||
|
|
1020
|
-
|
|
1021
|
-
|
|
913
|
+
!(
|
|
914
|
+
resolved.endsWith(".md") ||
|
|
915
|
+
AGENT_OVERRIDE_EXTENSIONS.some((extension) => resolved.endsWith(extension))
|
|
916
|
+
)
|
|
1022
917
|
)
|
|
1023
918
|
throw new Error(`Unsafe agent destination: ${resolved}`);
|
|
1024
919
|
const stat = lstatIfPresent(resolved);
|
|
@@ -1027,3 +922,8 @@ export class ConfigStore {
|
|
|
1027
922
|
this.reload();
|
|
1028
923
|
}
|
|
1029
924
|
}
|
|
925
|
+
|
|
926
|
+
function blockedOnly(mode: ToolFilteringMode, policy: AgentType["tools"]): AgentType["tools"] {
|
|
927
|
+
if (mode !== "all-except-blocked") return policy;
|
|
928
|
+
return policy?.block === undefined ? {} : { block: policy.block };
|
|
929
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
1
|
import { resolveCandidate } from "../../core/models.ts";
|
|
3
2
|
|
|
4
3
|
const MODEL_IDENTITY = /^[^\s/]+\/[^\s]+$/;
|
|
@@ -15,15 +14,12 @@ export function getModelPreferences(type: {
|
|
|
15
14
|
}): string[] | undefined {
|
|
16
15
|
const hasModels = type.models !== undefined;
|
|
17
16
|
const hasModel = type.model !== undefined;
|
|
18
|
-
if (hasModels && hasModel)
|
|
19
|
-
throw new Error("Specify either models or model, not both");
|
|
17
|
+
if (hasModels && hasModel) throw new Error("Specify either models or model, not both");
|
|
20
18
|
if (hasModels) {
|
|
21
19
|
if (!Array.isArray(type.models))
|
|
22
20
|
throw new Error("models must be an array of provider/model-id strings");
|
|
23
21
|
if (type.models.length === 0)
|
|
24
|
-
throw new Error(
|
|
25
|
-
"models must not be empty; omit models to inherit the parent/default model",
|
|
26
|
-
);
|
|
22
|
+
throw new Error("models must not be empty; omit models to inherit the parent/default model");
|
|
27
23
|
const models = type.models.map((value, index) =>
|
|
28
24
|
validateModelPreference(value, `models[${index}]`),
|
|
29
25
|
);
|
|
@@ -40,11 +36,11 @@ export function modelIdentity(model: { provider: string; id: string }): string {
|
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
export class ModelPreferenceError extends Error {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
) {
|
|
39
|
+
readonly scopedModelFiltering: boolean;
|
|
40
|
+
|
|
41
|
+
constructor(message: string, scopedModelFiltering: boolean) {
|
|
47
42
|
super(message);
|
|
43
|
+
this.scopedModelFiltering = scopedModelFiltering;
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
get summary(): string {
|