@jwilger/pi-development-system 0.50.0 → 0.51.1

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.
@@ -1,19 +1,19 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
1
+ // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks 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
- content,
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
- !Array.isArray(value) ||
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
- result.allow = toolNames(mapping.allow, "tools.allow");
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
- !THINKING_LEVELS.includes(
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("icon must be a single literal Nerd Font glyph (Unicode private-use character)");
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 | undefined;
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
- `${label}: ${error instanceof Error ? error.message : String(error)}`,
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
- delete merged.models;
451
- delete merged.model;
403
+ merged.models = undefined;
404
+ merged.model = undefined;
452
405
  } else if (models !== undefined) {
453
406
  merged.models = [...models];
454
- delete merged.model;
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
- blocked.add(overrideBaseName(file)!);
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((name) => overrides.has(name))) {
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, forks.get(name)!.file)} and ${join(directory, overrides.get(name)!.file)}: ` +
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 = this.getBase(name) ?? (() => {
992
- try {
993
- return this.get(name);
994
- } catch {
995
- return undefined;
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
- (!resolved.endsWith(".md") &&
1021
- !AGENT_OVERRIDE_EXTENSIONS.some((extension) => resolved.endsWith(extension)))
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,5 +1,5 @@
1
1
  // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
- import { lstatSync, readdirSync, readFileSync, type Dirent, type Stats } from "node:fs";
2
+ import { type Dirent, lstatSync, readdirSync, readFileSync, type Stats } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
5
5
  import { parseDocument } from "yaml";
@@ -17,8 +17,8 @@ export interface DiscoverImportOptions {
17
17
  cwd: string;
18
18
  agentDir: string;
19
19
  includeProject: boolean;
20
- homeDir?: string;
21
- extraAgentDirs?: string;
20
+ homeDir?: string | undefined;
21
+ extraAgentDirs?: string | undefined;
22
22
  }
23
23
 
24
24
  const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
@@ -227,7 +227,7 @@ function walk(
227
227
  continue;
228
228
  }
229
229
  if (isSkillMarkdownName(entry.name)) continue;
230
- if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.endsWith(".chain.md")) {
230
+ if (!(entry.isFile() && entry.name.endsWith(".md")) || entry.name.endsWith(".chain.md")) {
231
231
  continue;
232
232
  }
233
233
  if (isExcluded(child, excludes) || isManagerOwned(child, managerDirs)) continue;
@@ -270,7 +270,7 @@ function considerFile(
270
270
  const rawName = fields.name?.trim() ?? "";
271
271
  const name = rawName || (permissive ? filenameName(resolved) : "");
272
272
  const description = fields.description ?? (permissive ? "" : undefined);
273
- if (!name || description === undefined || (!permissive && !description.trim())) return;
273
+ if (!name || description === undefined || !(permissive || description.trim())) return;
274
274
  const packageName = fields.packageName?.trim() ?? "";
275
275
  candidates.push({
276
276
  id: resolved,
@@ -303,7 +303,7 @@ function looseDisplayFields(content: string): DisplayFields | undefined {
303
303
  /* Ambiguous display metadata is not selectable. */
304
304
  }
305
305
  }
306
- if (!fields.name || !fields.description) return undefined;
306
+ if (!(fields.name && fields.description)) return undefined;
307
307
  return { name: fields.name, description: fields.description, packageName: fields.package };
308
308
  }
309
309
 
@@ -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
- constructor(
44
- message: string,
45
- readonly scopedModelFiltering: boolean,
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 {