pi-subagents 0.59.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/docs/agents.md +2 -2
  3. package/docs/configuration.md +9 -5
  4. package/docs/extension-api.md +14 -7
  5. package/docs/models.md +1 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +15 -3
  8. package/docs/workflows.md +15 -14
  9. package/install.mjs +2 -1
  10. package/package.json +1 -1
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +89 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +30 -234
  15. package/skills/pi-subagents/references/execution-controls.md +79 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +35 -28
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +256 -88
  21. package/src/agents/agents.ts +527 -221
  22. package/src/api/background-work.ts +7 -2
  23. package/src/api/external-runs.ts +67 -4
  24. package/src/api/preflight.ts +13 -8
  25. package/src/api/shared-types.ts +3 -0
  26. package/src/extension/index.ts +7 -4
  27. package/src/extension/public-execution.ts +48 -4
  28. package/src/extension/rpc.ts +62 -4
  29. package/src/extension/schemas.ts +12 -7
  30. package/src/extension/tool-description.ts +16 -16
  31. package/src/runs/background/async-execution.ts +53 -37
  32. package/src/runs/background/async-job-tracker.ts +65 -3
  33. package/src/runs/background/async-resume.ts +3 -1
  34. package/src/runs/background/async-status.ts +104 -11
  35. package/src/runs/background/auto-drain.ts +1 -1
  36. package/src/runs/background/control-channel.ts +3 -2
  37. package/src/runs/background/fleet-view.ts +1 -1
  38. package/src/runs/background/result-watcher.ts +1 -1
  39. package/src/runs/background/resume-guidance.ts +1 -1
  40. package/src/runs/background/run-status.ts +15 -4
  41. package/src/runs/background/subagent-runner.ts +6 -3
  42. package/src/runs/background/subagent-wait.ts +30 -23
  43. package/src/runs/background/wait-completions.ts +4 -1
  44. package/src/runs/background/wait-tool.ts +24 -18
  45. package/src/runs/foreground/execution.ts +72 -4
  46. package/src/runs/foreground/subagent-executor.ts +276 -59
  47. package/src/runs/shared/acceptance.ts +43 -18
  48. package/src/runs/shared/async-status-projection.ts +138 -4
  49. package/src/runs/shared/background-process-options.ts +9 -0
  50. package/src/runs/shared/host-step-status.ts +1 -0
  51. package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  52. package/src/runs/shared/model-fallback.ts +61 -17
  53. package/src/runs/shared/mutation-evidence.ts +52 -3
  54. package/src/runs/shared/permissions.ts +1 -1
  55. package/src/runs/shared/pi-args.ts +47 -1
  56. package/src/runs/shared/single-output.ts +45 -18
  57. package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
  58. package/src/runs/shared/tool-timeout.ts +1 -1
  59. package/src/runs/shared/workflow-graph.ts +16 -0
  60. package/src/shared/types.ts +74 -4
  61. package/src/shared/workflow-child-permit.ts +91 -0
  62. package/src/slash/prompt-template-bridge.ts +37 -1
  63. package/src/slash/slash-commands.ts +18 -26
  64. package/src/tui/fleet-status.ts +11 -3
  65. package/src/tui/render-helpers.ts +31 -0
  66. package/src/tui/render.ts +673 -145
  67. package/src/watchdog/change-signature.ts +40 -1
  68. package/src/workflows/host-command.ts +6 -1
  69. package/src/workflows/scripted-workflow.ts +206 -6
  70. package/src/workflows/workflow-child-summary.ts +1 -1
  71. package/src/workflows/workflow-receipt.ts +41 -4
  72. package/src/workflows/workflow-resources.ts +150 -0
@@ -341,16 +341,30 @@ function getUserChainDir(): string {
341
341
  return path.join(getAgentDir(), "chains");
342
342
  }
343
343
 
344
+ type PackageScope = "root" | "user" | "project";
345
+ type PackageSettingsScope = Exclude<PackageScope, "root">;
346
+
344
347
  interface PackageSubagentPath {
345
348
  dir: string;
349
+ // A package can be reachable from more than one settings scope. Keep the
350
+ // actual memberships instead of promoting a mixed-scope path to root.
351
+ scope: Set<PackageScope>;
346
352
  packageName?: string;
347
353
  packageVersion?: string;
348
354
  packageRoot: string;
349
355
  }
350
356
 
357
+ interface PackageChainPath {
358
+ dir: string;
359
+ scope: Set<PackageScope>;
360
+ packageRoot: string;
361
+ }
362
+
351
363
  interface PackageSubagentPaths {
352
364
  agents: PackageSubagentPath[];
353
- chains: string[];
365
+ chains: PackageChainPath[];
366
+ watchPaths: string[];
367
+ settingsErrors: Partial<Record<PackageSettingsScope, Error>>;
354
368
  }
355
369
 
356
370
  let cachedGlobalNpmRoot: string | null = null;
@@ -365,18 +379,6 @@ function readJsonFileBestEffort(filePath: string): unknown {
365
379
  }
366
380
  }
367
381
 
368
- function readOptionalJsonFile(filePath: string): unknown {
369
- try {
370
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
371
- } catch (error) {
372
- const code = typeof error === "object" && error !== null && "code" in error
373
- ? (error as { code?: unknown }).code
374
- : undefined;
375
- if (code === "ENOENT") return null;
376
- throw error;
377
- }
378
- }
379
-
380
382
  function isSafePackagePath(value: string): boolean {
381
383
  return value.length > 0
382
384
  && !path.isAbsolute(value)
@@ -488,7 +490,7 @@ function stringArray(value: unknown): string[] {
488
490
  return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
489
491
  }
490
492
 
491
- function packageMetadata(pkg: Record<string, unknown>, packageRoot: string): Omit<PackageSubagentPath, "dir"> {
493
+ function packageMetadata(pkg: Record<string, unknown>, packageRoot: string): Omit<PackageSubagentPath, "dir" | "scope"> {
492
494
  const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : undefined;
493
495
  const version = typeof pkg.version === "string" && pkg.version.trim() ? pkg.version.trim() : undefined;
494
496
  return {
@@ -498,10 +500,10 @@ function packageMetadata(pkg: Record<string, unknown>, packageRoot: string): Omi
498
500
  };
499
501
  }
500
502
 
501
- function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubagentPaths {
503
+ function extractSubagentPathsFromPackageRoot(packageRoot: string, scope: Set<PackageScope>): PackageSubagentPaths {
502
504
  const packageJsonPath = path.join(packageRoot, "package.json");
503
505
  const pkg = readJsonFileBestEffort(packageJsonPath);
504
- if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [] };
506
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [], watchPaths: [packageJsonPath], settingsErrors: {} };
505
507
  const pkgRecord = pkg as Record<string, unknown>;
506
508
  const metadata = packageMetadata(pkgRecord, packageRoot);
507
509
 
@@ -520,16 +522,17 @@ function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubage
520
522
  }
521
523
 
522
524
  const agents: PackageSubagentPath[] = [];
523
- const chains: string[] = [];
525
+ const chains: PackageChainPath[] = [];
524
526
  for (const root of roots) {
525
- for (const entry of stringArray(root.agents)) agents.push({ dir: path.resolve(packageRoot, entry), ...metadata });
526
- for (const entry of stringArray(root.chains)) chains.push(path.resolve(packageRoot, entry));
527
+ for (const entry of stringArray(root.agents)) agents.push({ dir: path.resolve(packageRoot, entry), scope, ...metadata });
528
+ for (const entry of stringArray(root.chains)) chains.push({ dir: path.resolve(packageRoot, entry), scope, packageRoot });
527
529
  }
528
- return { agents, chains };
530
+ return { agents, chains, watchPaths: [packageJsonPath], settingsErrors: {} };
529
531
  }
530
532
 
531
- function collectPackageRootsFromNodeModules(nodeModulesDir: string): string[] {
533
+ function collectPackageRootsFromNodeModules(nodeModulesDir: string, watchPaths?: string[]): string[] {
532
534
  const roots: string[] = [];
535
+ watchPaths?.push(nodeModulesDir);
533
536
  if (!fs.existsSync(nodeModulesDir)) return roots;
534
537
 
535
538
  let entries: fs.Dirent[];
@@ -545,6 +548,7 @@ function collectPackageRootsFromNodeModules(nodeModulesDir: string): string[] {
545
548
 
546
549
  if (entry.name.startsWith("@")) {
547
550
  const scopeDir = path.join(nodeModulesDir, entry.name);
551
+ watchPaths?.push(scopeDir);
548
552
  let scopeEntries: fs.Dirent[];
549
553
  try {
550
554
  scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
@@ -565,8 +569,7 @@ function collectPackageRootsFromNodeModules(nodeModulesDir: string): string[] {
565
569
  }
566
570
 
567
571
  function collectSettingsPackageRoots(settingsFile: string, baseDir: string): string[] {
568
- const settings = readOptionalJsonFile(settingsFile);
569
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) return [];
572
+ const settings = readSettingsFileStrict(settingsFile);
570
573
  const packages = (settings as { packages?: unknown }).packages;
571
574
  if (!Array.isArray(packages)) return [];
572
575
 
@@ -587,52 +590,82 @@ function collectSettingsPackageRoots(settingsFile: string, baseDir: string): str
587
590
  function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolean; includeProject: boolean } = { includeUser: true, includeProject: true }): PackageSubagentPaths {
588
591
  const agentDir = getAgentDir();
589
592
  const projectRoot = findConfiguredProjectRoot(cwd) ?? cwd;
590
- const packageRoots = [
591
- projectRoot,
593
+ const packageRoots: Array<{ root: string; scope: PackageScope }> = [
594
+ { root: projectRoot, scope: "root" },
592
595
  ];
593
-
596
+ const watchPaths: string[] = [path.join(projectRoot, "package.json")];
597
+ const settingsErrors: Partial<Record<PackageSettingsScope, Error>> = {};
598
+ const collectScopedSettingsRoots = (scope: PackageSettingsScope, settingsFile: string, baseDir: string): string[] => {
599
+ try {
600
+ return collectSettingsPackageRoots(settingsFile, baseDir);
601
+ } catch (error) {
602
+ // The raw source snapshot includes both settings scopes. Defer a
603
+ // strict failure until a projection actually selects this scope so an
604
+ // unrelated malformed settings file stays out of scoped discovery.
605
+ settingsErrors[scope] = error instanceof Error
606
+ ? error
607
+ : new Error(`Failed to read settings file '${settingsFile}': ${String(error)}`, { cause: error });
608
+ return [];
609
+ }
610
+ };
594
611
  if (options.includeProject) {
595
612
  const projectConfigDir = getProjectConfigDir(projectRoot);
596
- packageRoots.push(
597
- ...collectPackageRootsFromNodeModules(path.join(projectConfigDir, "npm", "node_modules")),
598
- ...collectSettingsPackageRoots(path.join(projectConfigDir, "settings.json"), projectConfigDir),
599
- );
613
+ const nodeModulesDir = path.join(projectConfigDir, "npm", "node_modules");
614
+ packageRoots.push(...collectPackageRootsFromNodeModules(nodeModulesDir, watchPaths).map((root) => ({ root, scope: "project" as const })));
615
+ packageRoots.push(...collectScopedSettingsRoots("project", path.join(projectConfigDir, "settings.json"), projectConfigDir).map((root) => ({ root, scope: "project" as const })));
600
616
  }
601
617
 
602
618
  if (options.includeUser) {
603
- packageRoots.push(
604
- ...collectPackageRootsFromNodeModules(path.join(agentDir, "npm", "node_modules")),
605
- ...collectSettingsPackageRoots(path.join(agentDir, "settings.json"), agentDir),
606
- );
619
+ const nodeModulesDir = path.join(agentDir, "npm", "node_modules");
620
+ packageRoots.push(...collectPackageRootsFromNodeModules(nodeModulesDir, watchPaths).map((root) => ({ root, scope: "user" as const })));
621
+ packageRoots.push(...collectScopedSettingsRoots("user", path.join(agentDir, "settings.json"), agentDir).map((root) => ({ root, scope: "user" as const })));
607
622
  }
608
623
 
609
624
  if (options.includeUser) {
610
625
  const globalRoot = getGlobalNpmRoot();
611
- if (globalRoot) packageRoots.push(...collectPackageRootsFromNodeModules(globalRoot));
626
+ if (globalRoot) {
627
+ packageRoots.push(...collectPackageRootsFromNodeModules(globalRoot, watchPaths).map((root) => ({ root, scope: "user" as const })));
628
+ }
612
629
  }
613
630
 
614
- const seenRoots = new Set<string>();
615
- const seenAgents = new Set<string>();
616
- const seenChains = new Set<string>();
631
+ const seenRoots = new Map<string, Set<PackageScope>>();
632
+ const seenAgents = new Map<string, PackageSubagentPath>();
633
+ const seenChains = new Map<string, PackageChainPath>();
617
634
  const agents: PackageSubagentPath[] = [];
618
- const chains: string[] = [];
619
- for (const packageRoot of packageRoots) {
635
+ const chains: PackageChainPath[] = [];
636
+ for (const { root: packageRoot, scope } of packageRoots) {
620
637
  const resolvedRoot = path.resolve(packageRoot);
621
- if (seenRoots.has(resolvedRoot)) continue;
622
- seenRoots.add(resolvedRoot);
623
- const paths = extractSubagentPathsFromPackageRoot(resolvedRoot);
638
+ const scopes = seenRoots.get(resolvedRoot);
639
+ if (scopes !== undefined) {
640
+ scopes.add(scope);
641
+ continue;
642
+ }
643
+ const packageScopes = new Set<PackageScope>([scope]);
644
+ seenRoots.set(resolvedRoot, packageScopes);
645
+ const paths = extractSubagentPathsFromPackageRoot(resolvedRoot, packageScopes);
646
+ watchPaths.push(...paths.watchPaths);
624
647
  for (const agentPath of paths.agents) {
625
- if (seenAgents.has(agentPath.dir)) continue;
626
- seenAgents.add(agentPath.dir);
648
+ const agentKey = `${agentPath.dir}\u0000${agentPath.packageRoot}`;
649
+ const existing = seenAgents.get(agentKey);
650
+ if (existing) {
651
+ for (const packageScope of agentPath.scope) existing.scope.add(packageScope);
652
+ continue;
653
+ }
654
+ seenAgents.set(agentKey, agentPath);
627
655
  agents.push(agentPath);
628
656
  }
629
657
  for (const chainDir of paths.chains) {
630
- if (seenChains.has(chainDir)) continue;
631
- seenChains.add(chainDir);
658
+ const chainKey = `${chainDir.dir}\u0000${chainDir.packageRoot}`;
659
+ const existing = seenChains.get(chainKey);
660
+ if (existing) {
661
+ for (const packageScope of chainDir.scope) existing.scope.add(packageScope);
662
+ continue;
663
+ }
664
+ seenChains.set(chainKey, chainDir);
632
665
  chains.push(chainDir);
633
666
  }
634
667
  }
635
- return { agents, chains };
668
+ return { agents, chains, watchPaths, settingsErrors };
636
669
  }
637
670
 
638
671
  function normalizeAgentAliases(rawAliases: string[] | undefined, agentName: string): string[] | undefined {
@@ -1739,9 +1772,11 @@ function listFilesRecursive(
1739
1772
  predicate: (fileName: string) => boolean,
1740
1773
  rootDir = dir,
1741
1774
  visitedDirectories = new Set<string>(),
1775
+ inspectedDirectories = new Set<string>(),
1742
1776
  ): string[] {
1743
1777
  const files: string[] = [];
1744
1778
  if (!fs.existsSync(dir)) return files;
1779
+ inspectedDirectories.add(path.resolve(dir));
1745
1780
  let realDir: string;
1746
1781
  try {
1747
1782
  realDir = fs.realpathSync(dir);
@@ -1770,7 +1805,7 @@ function listFilesRecursive(
1770
1805
  }
1771
1806
  if (isDirectory) {
1772
1807
  if (!shouldPruneDiscoveryDir(rootDir, filePath, entry.name)) {
1773
- files.push(...listFilesRecursive(filePath, predicate, rootDir, visitedDirectories));
1808
+ files.push(...listFilesRecursive(filePath, predicate, rootDir, visitedDirectories, inspectedDirectories));
1774
1809
  }
1775
1810
  continue;
1776
1811
  }
@@ -1786,6 +1821,17 @@ export interface AgentDefinitionInspection {
1786
1821
  state: AgentDefinitionDirectoryState;
1787
1822
  }
1788
1823
 
1824
+ const agentDefinitionInspectionDirectories = new WeakMap<AgentDefinitionInspection, string[]>();
1825
+
1826
+ function rememberAgentDefinitionInspection(inspection: AgentDefinitionInspection, directories: Iterable<string>): AgentDefinitionInspection {
1827
+ agentDefinitionInspectionDirectories.set(inspection, [...new Set([...directories].map((directory) => path.resolve(directory)))]);
1828
+ return inspection;
1829
+ }
1830
+
1831
+ function inspectedAgentDefinitionDirectories(inspection: AgentDefinitionInspection, root: string): string[] {
1832
+ return agentDefinitionInspectionDirectories.get(inspection) ?? [path.resolve(root)];
1833
+ }
1834
+
1789
1835
  /** Narrow filesystem seam so unavailable paths can be tested without permissions. */
1790
1836
  export interface AgentDefinitionInspectionFs {
1791
1837
  existsSync(filePath: string): boolean;
@@ -1809,15 +1855,17 @@ const DEFAULT_AGENT_DEFINITION_INSPECTION_FS: AgentDefinitionInspectionFs = {
1809
1855
  export function inspectAgentDefinitionDirectory(dir: string, operations: AgentDefinitionInspectionFs = DEFAULT_AGENT_DEFINITION_INSPECTION_FS): AgentDefinitionInspection {
1810
1856
  const root = path.resolve(dir);
1811
1857
  try {
1812
- if (!operations.existsSync(root)) return { files: [], state: "absent" };
1813
- if (!operations.statSync(root).isDirectory()) return { files: [], state: "not-directory" };
1858
+ if (!operations.existsSync(root)) return rememberAgentDefinitionInspection({ files: [], state: "absent" }, [root]);
1859
+ if (!operations.statSync(root).isDirectory()) return rememberAgentDefinitionInspection({ files: [], state: "not-directory" }, [root]);
1814
1860
  } catch {
1815
- return { files: [], state: "unreadable" };
1861
+ return rememberAgentDefinitionInspection({ files: [], state: "unreadable" }, [root]);
1816
1862
  }
1817
1863
  const files: string[] = [];
1818
1864
  let unreadable = false;
1819
1865
  const visitedDirectories = new Set<string>();
1866
+ const inspectedDirectories = new Set<string>();
1820
1867
  const visit = (current: string): void => {
1868
+ inspectedDirectories.add(path.resolve(current));
1821
1869
  let realDir: string;
1822
1870
  try {
1823
1871
  realDir = operations.realpathSync?.(current) ?? path.resolve(current);
@@ -1853,7 +1901,7 @@ export function inspectAgentDefinitionDirectory(dir: string, operations: AgentDe
1853
1901
  }
1854
1902
  };
1855
1903
  visit(root);
1856
- return { files, state: unreadable ? "unreadable" : files.length ? "candidates" : "empty" };
1904
+ return rememberAgentDefinitionInspection({ files, state: unreadable ? "unreadable" : files.length ? "candidates" : "empty" }, inspectedDirectories);
1857
1905
  }
1858
1906
 
1859
1907
  function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
@@ -1989,7 +2037,7 @@ function resolveAgentRelativeExtensionPaths(paths: string[] | undefined, agentFi
1989
2037
  });
1990
2038
  }
1991
2039
 
1992
- function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
2040
+ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir" | "scope">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1993
2041
  const agents: AgentConfig[] = [];
1994
2042
  const diagnostics: AgentDiscoveryDiagnostic[] = [];
1995
2043
 
@@ -2186,7 +2234,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2186
2234
  return { agents, diagnostics };
2187
2235
  }
2188
2236
 
2189
- function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">, inspection = inspectAgentDefinitionDirectory(dir)): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
2237
+ function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir" | "scope">, inspection = inspectAgentDefinitionDirectory(dir)): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
2190
2238
  return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir, inspection), source, discoveryPriority, packageSource);
2191
2239
  }
2192
2240
 
@@ -2199,11 +2247,13 @@ function reportAgentDefinitionDirectory(source: AgentSource, dir: string, inspec
2199
2247
  };
2200
2248
  }
2201
2249
 
2202
- function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
2250
+ function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[]; files: string[]; directories: string[] } {
2203
2251
  const chains = new Map<string, ChainConfig>();
2204
2252
  const diagnostics: ChainDiscoveryDiagnostic[] = [];
2253
+ const directories = new Set<string>();
2254
+ const files = listFilesRecursive(dir, (fileName) => fileName.endsWith(".chain.md") || fileName.endsWith(".chain.json"), dir, new Set<string>(), directories);
2205
2255
 
2206
- for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".chain.md") || fileName.endsWith(".chain.json"))) {
2256
+ for (const filePath of files) {
2207
2257
  let content: string;
2208
2258
  try {
2209
2259
  content = fs.readFileSync(filePath, "utf-8");
@@ -2222,7 +2272,7 @@ function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainCon
2222
2272
  }
2223
2273
  }
2224
2274
 
2225
- return { chains: Array.from(chains.values()), diagnostics };
2275
+ return { chains: Array.from(chains.values()), diagnostics, files, directories: [...directories] };
2226
2276
  }
2227
2277
 
2228
2278
  function isDirectory(p: string): boolean {
@@ -2278,78 +2328,7 @@ function extraUserAgentDirs(): string[] {
2278
2328
  .filter((dir) => dir.length > 0);
2279
2329
  }
2280
2330
 
2281
- export function discoverAgents(cwd: string, scope: AgentScope, preferredModelProvider?: string): AgentDiscoveryResult {
2282
- const effectiveCwd = path.resolve(cwd);
2283
- const userDirOld = path.join(getAgentDir(), "agents");
2284
- const userDirNew = path.join(os.homedir(), ".agents");
2285
- const { readDirs: projectAgentDirs, candidateDirs: projectCandidateDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(effectiveCwd);
2286
- const userSettingsPath = getUserAgentSettingsPath();
2287
- const projectSettingsPath = getProjectAgentSettingsPath(effectiveCwd);
2288
- const userSettings = selectProviderOverrides(scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath), preferredModelProvider);
2289
- const projectSettings = selectProviderOverrides(scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath), preferredModelProvider);
2290
- const defaultProvider = resolveSubagentDefaultProvider(userSettings, projectSettings, projectSettingsPath);
2291
- const defaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath, defaultProvider);
2292
- const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath);
2293
- const maxThinking = resolveSubagentMaxThinking(userSettings, projectSettings, projectSettingsPath);
2294
- const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
2295
- const modelScope = projectSettings.modelScope ?? userSettings.modelScope;
2296
- const packageSubagentPaths = collectPackageSubagentPaths(effectiveCwd, {
2297
- includeUser: scope !== "project",
2298
- includeProject: scope !== "user",
2299
- });
2300
- const directories: AgentDefinitionDirectoryReport[] = [reportAgentDefinitionDirectory("builtin", BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION)];
2301
-
2302
- const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
2303
- const builtinAgents = applyBuiltinOverrides(
2304
- applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2305
- userSettings, projectSettings, userSettingsPath, projectSettingsPath,
2306
- );
2307
-
2308
- const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew].map((dir, discoveryPriority) => {
2309
- const inspection = inspectAgentDefinitionDirectory(dir);
2310
- directories.push(reportAgentDefinitionDirectory("user", dir, inspection));
2311
- return loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection);
2312
- });
2313
- const userAgents = applyCustomAgentOverrides(
2314
- applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2315
- userSettings, projectSettings, userSettingsPath, projectSettingsPath,
2316
- );
2317
-
2318
- // Report both legacy and preferred project definition paths only after the
2319
- // existing configured-root resolver found a root; never synthesize cwd paths.
2320
- const projectInspections = scope === "user" ? new Map<string, AgentDefinitionInspection>() : new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
2321
- if (scope !== "user") for (const dir of projectCandidateDirs) directories.push(reportAgentDefinitionDirectory("project", dir, projectInspections.get(dir)!));
2322
- const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0, undefined, projectInspections.get(dir)!));
2323
- const projectAgents = applyCustomAgentOverrides(
2324
- applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2325
- userSettings, projectSettings, userSettingsPath, projectSettingsPath,
2326
- );
2327
-
2328
- const packageLoaded = packageSubagentPaths.agents.map((entry, index) => {
2329
- const inspection = inspectAgentDefinitionDirectory(entry.dir);
2330
- directories.push(reportAgentDefinitionDirectory("package", entry.dir, inspection));
2331
- return loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry, inspection);
2332
- });
2333
- const packageMap = new Map<string, AgentConfig>();
2334
- for (const loaded of packageLoaded) for (const agent of loaded.agents) if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
2335
- const packageAgents = applyCustomAgentOverrides(
2336
- applySubagentDefaults(Array.from(packageMap.values()), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2337
- userSettings, projectSettings, userSettingsPath, projectSettingsPath,
2338
- );
2339
- const agents = applySubagentMaxThinking(
2340
- mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents).filter((agent) => agent.disabled !== true),
2341
- maxThinking,
2342
- );
2343
- const agentDiagnostics = [
2344
- ...builtinLoaded.diagnostics,
2345
- ...userLoaded.flatMap((loaded) => loaded.diagnostics),
2346
- ...projectLoaded.flatMap((loaded) => loaded.diagnostics),
2347
- ...packageLoaded.flatMap((loaded) => loaded.diagnostics),
2348
- ];
2349
- return { agents, agentDiagnostics, projectAgentsDir, cwd: effectiveCwd, scope, directories, ...(modelScope !== undefined ? { modelScope } : {}), ...(maxThinking !== undefined ? { maxThinking } : {}) };
2350
- }
2351
-
2352
- export function discoverAgentsAll(cwd: string, preferredModelProvider?: string): {
2331
+ export interface AgentDiscoveryAllResult {
2353
2332
  builtin: AgentConfig[];
2354
2333
  package: AgentConfig[];
2355
2334
  user: AgentConfig[];
@@ -2364,125 +2343,452 @@ export function discoverAgentsAll(cwd: string, preferredModelProvider?: string):
2364
2343
  userSettingsPath: string;
2365
2344
  projectSettingsPath: string | null;
2366
2345
  maxThinking?: ThinkingLevel;
2367
- } {
2346
+ }
2347
+
2348
+ /**
2349
+ * A single fresh source scan with both projections retained. The effective
2350
+ * projection intentionally remains separate from the all-source projection:
2351
+ * disabled and shadowed definitions are needed for diagnostics and runtime
2352
+ * collision checks even when they are not launchable.
2353
+ */
2354
+ export interface AgentDiscoverySnapshot {
2355
+ effective: AgentDiscoveryResult;
2356
+ all: AgentDiscoveryAllResult;
2357
+ }
2358
+
2359
+ interface LoadedAgentDirectory {
2360
+ dir: string;
2361
+ inspection: AgentDefinitionInspection;
2362
+ loaded: ReturnType<typeof loadAgentsFromDir>;
2363
+ packageEntry?: PackageSubagentPath;
2364
+ }
2365
+
2366
+ interface AgentDiscoverySources {
2367
+ cwd: string;
2368
+ preferredModelProvider?: string;
2369
+ userDir: string;
2370
+ userDirOld: string;
2371
+ userDirNew: string;
2372
+ userChainDir: string;
2373
+ projectAgentDirs: string[];
2374
+ projectCandidateDirs: string[];
2375
+ projectAgentsDir: string | null;
2376
+ projectChainDirs: string[];
2377
+ projectChainDir: string | null;
2378
+ userSettingsPath: string;
2379
+ projectSettingsPath: string | null;
2380
+ userSettings?: SubagentSettings;
2381
+ projectSettings?: SubagentSettings;
2382
+ packageSubagentPaths: PackageSubagentPaths;
2383
+ builtinLoaded: ReturnType<typeof loadAgentsFromDefinitionFiles>;
2384
+ userLoaded: LoadedAgentDirectory[];
2385
+ projectLoaded: LoadedAgentDirectory[];
2386
+ projectInspections: Map<string, AgentDefinitionInspection>;
2387
+ packageLoaded: LoadedAgentDirectory[];
2388
+ userChains?: ReturnType<typeof loadChainsFromDir>;
2389
+ projectChainLoaded?: Array<{ dir: string; loaded: ReturnType<typeof loadChainsFromDir> }>;
2390
+ packageChainLoaded?: Array<{ entry: PackageChainPath; loaded: ReturnType<typeof loadChainsFromDir> }>;
2391
+ watchPaths: string[];
2392
+ }
2393
+
2394
+ interface AgentDiscoveryCacheEntry {
2395
+ sources: AgentDiscoverySources;
2396
+ fingerprint: string;
2397
+ }
2398
+
2399
+ const agentDiscoveryCache = new Map<string, AgentDiscoveryCacheEntry>();
2400
+
2401
+ function isPathWithin(root: string, candidate: string): boolean {
2402
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
2403
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
2404
+ }
2405
+
2406
+ function addDirectoryWatchPaths(paths: Set<string>, root: string, files: readonly string[], directories: readonly string[] = []): void {
2407
+ const resolvedRoot = path.resolve(root);
2408
+ paths.add(resolvedRoot);
2409
+ // The initial recursive inspection is already bounded to this discovery
2410
+ // root. Keep every visited directory in the fingerprint so a file added to
2411
+ // an existing empty nested directory invalidates the snapshot too.
2412
+ for (const directory of directories) {
2413
+ if (isPathWithin(resolvedRoot, directory)) paths.add(path.resolve(directory));
2414
+ }
2415
+ for (const file of files) {
2416
+ paths.add(path.resolve(file));
2417
+ let current = path.resolve(path.dirname(file));
2418
+ while (isPathWithin(resolvedRoot, current)) {
2419
+ paths.add(current);
2420
+ if (current === resolvedRoot) break;
2421
+ current = path.dirname(current);
2422
+ }
2423
+ }
2424
+ }
2425
+
2426
+ function watchPathSignature(filePath: string): string {
2427
+ try {
2428
+ const stat = fs.statSync(filePath);
2429
+ if (stat.isDirectory()) {
2430
+ const entries = fs.readdirSync(filePath, { withFileTypes: true })
2431
+ .sort((left, right) => left.name.localeCompare(right.name))
2432
+ .map((entry) => `${entry.name}:${entry.isDirectory() ? "d" : entry.isSymbolicLink() ? "l" : "f"}`)
2433
+ .join("|");
2434
+ return `directory:${entries}`;
2435
+ }
2436
+ return `file:${stat.size}:${stat.mtimeMs}`;
2437
+ } catch {
2438
+ return "missing";
2439
+ }
2440
+ }
2441
+
2442
+ function discoveryFingerprint(paths: readonly string[]): string {
2443
+ return [...new Set(paths)]
2444
+ .sort((left, right) => left.localeCompare(right))
2445
+ .map((filePath) => `${filePath}:${watchPathSignature(filePath)}`)
2446
+ .join("\n");
2447
+ }
2448
+
2449
+ function discoveryCacheKey(cwd: string, preferredModelProvider: string | undefined): string {
2450
+ return JSON.stringify([
2451
+ path.resolve(cwd),
2452
+ preferredModelProvider ?? null,
2453
+ getProjectConfigDir(path.resolve(cwd)),
2454
+ getAgentDir(),
2455
+ os.homedir(),
2456
+ process.env.HOME ?? "",
2457
+ process.env.USERPROFILE ?? "",
2458
+ process.env[EXTRA_AGENT_DIRS_ENV] ?? "",
2459
+ process.env.PI_OFFLINE ?? "",
2460
+ process.env.APPDATA ?? "",
2461
+ ]);
2462
+ }
2463
+
2464
+ function projectDiscoveryWatchPaths(cwd: string): string[] {
2465
+ const paths: string[] = [];
2466
+ let current = path.resolve(cwd);
2467
+ while (true) {
2468
+ paths.push(getProjectConfigDir(current), path.join(current, ".agents"), path.join(current, ".git"));
2469
+ const parent = path.dirname(current);
2470
+ if (parent === current) break;
2471
+ current = parent;
2472
+ }
2473
+ return paths;
2474
+ }
2475
+
2476
+ function packageEntryIncluded(scope: AgentScope, packageScopes: PackageSubagentPath["scope"]): boolean {
2477
+ if (scope === "both" || packageScopes.has("root")) return true;
2478
+ return packageScopes.has(scope);
2479
+ }
2480
+
2481
+ function buildAgentDiscoverySources(cwd: string, preferredModelProvider?: string): AgentDiscoverySources {
2482
+ const effectiveCwd = path.resolve(cwd);
2368
2483
  const userDirOld = path.join(getAgentDir(), "agents");
2369
2484
  const userDirNew = path.join(os.homedir(), ".agents");
2370
2485
  const userChainDir = getUserChainDir();
2371
- const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
2372
- const { readDirs: projectChainDirs, preferredDir: projectChainDir } = resolveNearestProjectChainDirs(cwd);
2486
+ const { readDirs: projectAgentDirs, candidateDirs: projectCandidateDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(effectiveCwd);
2487
+ const { readDirs: projectChainDirs, preferredDir: projectChainDir } = resolveNearestProjectChainDirs(effectiveCwd);
2373
2488
  const userSettingsPath = getUserAgentSettingsPath();
2374
- const projectSettingsPath = getProjectAgentSettingsPath(cwd);
2375
- const userSettings = selectProviderOverrides(readSubagentSettings(userSettingsPath), preferredModelProvider);
2376
- const projectSettings = selectProviderOverrides(readSubagentSettings(projectSettingsPath), preferredModelProvider);
2377
- const defaultProvider = resolveSubagentDefaultProvider(userSettings, projectSettings, projectSettingsPath);
2378
- const defaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath, defaultProvider);
2379
- const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath);
2380
- const maxThinking = resolveSubagentMaxThinking(userSettings, projectSettings, projectSettingsPath);
2381
- const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
2382
- const packageSubagentPaths = collectPackageSubagentPaths(cwd);
2489
+ const projectSettingsPath = getProjectAgentSettingsPath(effectiveCwd);
2490
+ const packageSubagentPaths = collectPackageSubagentPaths(effectiveCwd);
2383
2491
 
2384
2492
  const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
2385
- const builtin = applyBuiltinOverrides(
2386
- applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2387
- userSettings,
2388
- projectSettings,
2493
+ const userLoaded = [...extraUserAgentDirs(), userDirOld, userDirNew].map((dir, discoveryPriority): LoadedAgentDirectory => {
2494
+ const inspection = inspectAgentDefinitionDirectory(dir);
2495
+ return { dir, inspection, loaded: loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection) };
2496
+ });
2497
+ const projectInspections = new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
2498
+ const projectLoaded = projectAgentDirs.map((dir): LoadedAgentDirectory => {
2499
+ const inspection = projectInspections.get(dir)!;
2500
+ return { dir, inspection, loaded: loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0, undefined, inspection) };
2501
+ });
2502
+ const packageLoaded = packageSubagentPaths.agents.map((entry, index): LoadedAgentDirectory => {
2503
+ const inspection = inspectAgentDefinitionDirectory(entry.dir);
2504
+ return { dir: entry.dir, inspection, packageEntry: entry, loaded: loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry, inspection) };
2505
+ });
2506
+
2507
+ const watchPaths = new Set<string>([
2389
2508
  userSettingsPath,
2390
- projectSettingsPath,
2391
- );
2392
- const userLoaded = [...extraUserAgentDirs(), userDirOld, userDirNew]
2393
- .map((dir, discoveryPriority) => loadAgentsFromDir(dir, "user", discoveryPriority));
2394
- const user = applyCustomAgentOverrides(
2395
- applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2396
- userSettings,
2397
- projectSettings,
2509
+ ...(projectSettingsPath ? [projectSettingsPath] : []),
2510
+ ...projectDiscoveryWatchPaths(effectiveCwd),
2511
+ ...findProjectRootCandidates(effectiveCwd).map((root) => path.join(getProjectConfigDir(root), "settings.json")),
2512
+ ...packageSubagentPaths.watchPaths,
2513
+ ]);
2514
+ for (const directory of userLoaded) addDirectoryWatchPaths(watchPaths, directory.dir, directory.inspection.files, inspectedAgentDefinitionDirectories(directory.inspection, directory.dir));
2515
+ for (const dir of projectCandidateDirs) {
2516
+ const inspection = projectInspections.get(dir);
2517
+ addDirectoryWatchPaths(watchPaths, dir, inspection?.files ?? [], inspection ? inspectedAgentDefinitionDirectories(inspection, dir) : []);
2518
+ }
2519
+ for (const directory of packageLoaded) addDirectoryWatchPaths(watchPaths, directory.dir, directory.inspection.files, inspectedAgentDefinitionDirectories(directory.inspection, directory.dir));
2520
+
2521
+ const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
2522
+ return {
2523
+ cwd: effectiveCwd,
2524
+ ...(preferredModelProvider !== undefined ? { preferredModelProvider } : {}),
2525
+ userDir,
2526
+ userDirOld,
2527
+ userDirNew,
2528
+ userChainDir,
2529
+ projectAgentDirs,
2530
+ projectCandidateDirs,
2531
+ projectAgentsDir,
2532
+ projectChainDirs,
2533
+ projectChainDir,
2398
2534
  userSettingsPath,
2399
2535
  projectSettingsPath,
2400
- );
2401
- const packageMap = new Map<string, AgentConfig>();
2402
- const packageAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
2403
- for (const [index, entry] of packageSubagentPaths.agents.entries()) {
2404
- const loaded = loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry);
2405
- packageAgentDiagnostics.push(...loaded.diagnostics);
2406
- for (const agent of loaded.agents) {
2407
- if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
2536
+ packageSubagentPaths,
2537
+ builtinLoaded,
2538
+ userLoaded,
2539
+ projectLoaded,
2540
+ projectInspections,
2541
+ packageLoaded,
2542
+ watchPaths: [...watchPaths],
2543
+ };
2544
+ }
2545
+
2546
+ function ensureDiscoveryChains(sources: AgentDiscoverySources): void {
2547
+ if (sources.userChains) return;
2548
+ sources.packageChainLoaded = sources.packageSubagentPaths.chains.map((entry) => ({ entry, loaded: loadChainsFromDir(entry.dir, "package") }));
2549
+ sources.userChains = loadChainsFromDir(sources.userChainDir, "user");
2550
+ sources.projectChainLoaded = sources.projectChainDirs.map((dir) => ({ dir, loaded: loadChainsFromDir(dir, "project") }));
2551
+ const watchPaths = new Set(sources.watchPaths);
2552
+ addDirectoryWatchPaths(watchPaths, sources.userChainDir, sources.userChains.files, sources.userChains.directories);
2553
+ for (const chain of sources.projectChainLoaded) addDirectoryWatchPaths(watchPaths, chain.dir, chain.loaded.files, chain.loaded.directories);
2554
+ for (const chain of sources.packageChainLoaded) addDirectoryWatchPaths(watchPaths, chain.entry.dir, chain.loaded.files, chain.loaded.directories);
2555
+ sources.watchPaths = [...watchPaths];
2556
+ }
2557
+
2558
+ function getAgentDiscoverySources(cwd: string, preferredModelProvider?: string, includeChains = false): AgentDiscoverySources {
2559
+ const key = discoveryCacheKey(cwd, preferredModelProvider);
2560
+ const cached = agentDiscoveryCache.get(key);
2561
+ if (cached && cached.fingerprint === discoveryFingerprint(cached.sources.watchPaths)) {
2562
+ if (includeChains) {
2563
+ ensureDiscoveryChains(cached.sources);
2564
+ cached.fingerprint = discoveryFingerprint(cached.sources.watchPaths);
2408
2565
  }
2566
+ return cached.sources;
2567
+ }
2568
+ const sources = buildAgentDiscoverySources(cwd, preferredModelProvider);
2569
+ const entry = { sources, fingerprint: discoveryFingerprint(sources.watchPaths) };
2570
+ agentDiscoveryCache.set(key, entry);
2571
+ if (includeChains) {
2572
+ ensureDiscoveryChains(sources);
2573
+ entry.fingerprint = discoveryFingerprint(sources.watchPaths);
2409
2574
  }
2410
- const packageAgents = applyCustomAgentOverrides(
2411
- applySubagentDefaults(Array.from(packageMap.values()), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2575
+ return sources;
2576
+ }
2577
+
2578
+ /** Clear process-local discovery snapshots, primarily for hosts that reload settings in place. */
2579
+ export function clearAgentDiscoveryCache(): void {
2580
+ agentDiscoveryCache.clear();
2581
+ }
2582
+
2583
+ function ensureSettingsForScope(sources: AgentDiscoverySources, scope: AgentScope, preferredModelProvider?: string): void {
2584
+ if (scope !== "project" && sources.userSettings === undefined) {
2585
+ if (sources.packageSubagentPaths.settingsErrors.user) throw sources.packageSubagentPaths.settingsErrors.user;
2586
+ sources.userSettings = selectProviderOverrides(readSubagentSettings(sources.userSettingsPath), preferredModelProvider);
2587
+ }
2588
+ if (scope !== "user" && sources.projectSettings === undefined) {
2589
+ if (sources.packageSubagentPaths.settingsErrors.project) throw sources.packageSubagentPaths.settingsErrors.project;
2590
+ sources.projectSettings = selectProviderOverrides(readSubagentSettings(sources.projectSettingsPath), preferredModelProvider);
2591
+ }
2592
+ }
2593
+
2594
+ function settingsForScope(sources: AgentDiscoverySources, scope: AgentScope): { user: SubagentSettings; project: SubagentSettings } {
2595
+ // Parse only settings that participate in this projection. The source cache
2596
+ // still retains both source trees, while malformed out-of-scope settings do
2597
+ // not make an otherwise valid scoped projection fail.
2598
+ ensureSettingsForScope(sources, scope, sources.preferredModelProvider);
2599
+ return {
2600
+ user: scope === "project" ? EMPTY_SUBAGENT_SETTINGS : sources.userSettings ?? EMPTY_SUBAGENT_SETTINGS,
2601
+ project: scope === "user" ? EMPTY_SUBAGENT_SETTINGS : sources.projectSettings ?? EMPTY_SUBAGENT_SETTINGS,
2602
+ };
2603
+ }
2604
+
2605
+ function configuredAgentsForScope(sources: AgentDiscoverySources, scope: AgentScope, settingsScope = scope): {
2606
+ builtin: AgentConfig[];
2607
+ package: AgentConfig[];
2608
+ user: AgentConfig[];
2609
+ project: AgentConfig[];
2610
+ userSettings: SubagentSettings;
2611
+ projectSettings: SubagentSettings;
2612
+ maxThinking?: ThinkingLevel;
2613
+ modelScope?: ModelScopeConfig;
2614
+ } {
2615
+ const { user: userSettings, project: projectSettings } = settingsForScope(sources, settingsScope);
2616
+ const defaultProvider = resolveSubagentDefaultProvider(userSettings, projectSettings, sources.projectSettingsPath);
2617
+ const defaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, sources.userSettingsPath, sources.projectSettingsPath, defaultProvider);
2618
+ const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, sources.projectSettingsPath);
2619
+ const maxThinking = resolveSubagentMaxThinking(userSettings, projectSettings, sources.projectSettingsPath);
2620
+ const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, sources.projectSettingsPath);
2621
+ const applyDefaults = (agents: AgentConfig[]): AgentConfig[] => applySubagentDefaults(agents, defaultModel, defaultProvider, defaultThinking, defaultExtensions);
2622
+ const builtin = applyBuiltinOverrides(applyDefaults(sources.builtinLoaded.agents), userSettings, projectSettings, sources.userSettingsPath, sources.projectSettingsPath);
2623
+ const user = applyCustomAgentOverrides(
2624
+ applyDefaults(scope === "project" ? [] : sources.userLoaded.flatMap((loaded) => loaded.loaded.agents)),
2412
2625
  userSettings,
2413
2626
  projectSettings,
2414
- userSettingsPath,
2415
- projectSettingsPath,
2627
+ sources.userSettingsPath,
2628
+ sources.projectSettingsPath,
2416
2629
  );
2417
2630
  const projectMap = new Map<string, AgentConfig>();
2418
- const projectAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
2419
- for (const dir of projectDirs) {
2420
- const loaded = loadAgentsFromDir(dir, "project", dir === projectDir ? 1 : 0);
2421
- projectAgentDiagnostics.push(...loaded.diagnostics);
2422
- for (const agent of loaded.agents) {
2423
- projectMap.set(agent.name, agent);
2424
- }
2631
+ if (scope !== "user") for (const loaded of sources.projectLoaded) for (const agent of loaded.loaded.agents) projectMap.set(agent.name, agent);
2632
+ const project = applyCustomAgentOverrides(applyDefaults(Array.from(projectMap.values())), userSettings, projectSettings, sources.userSettingsPath, sources.projectSettingsPath);
2633
+ const packageMap = new Map<string, AgentConfig>();
2634
+ for (const loaded of sources.packageLoaded) {
2635
+ if (!loaded.packageEntry || !packageEntryIncluded(scope, loaded.packageEntry.scope)) continue;
2636
+ for (const agent of loaded.loaded.agents) if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
2425
2637
  }
2426
- const project = applyCustomAgentOverrides(
2427
- applySubagentDefaults(Array.from(projectMap.values()), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2638
+ const packageAgents = applyCustomAgentOverrides(applyDefaults(Array.from(packageMap.values())), userSettings, projectSettings, sources.userSettingsPath, sources.projectSettingsPath);
2639
+ return {
2640
+ builtin,
2641
+ package: packageAgents,
2642
+ user,
2643
+ project,
2428
2644
  userSettings,
2429
2645
  projectSettings,
2430
- userSettingsPath,
2431
- projectSettingsPath,
2646
+ maxThinking,
2647
+ modelScope: projectSettings.modelScope ?? userSettings.modelScope,
2648
+ };
2649
+ }
2650
+
2651
+ function discoveryDirectories(sources: AgentDiscoverySources, scope: AgentScope): AgentDefinitionDirectoryReport[] {
2652
+ const directories: AgentDefinitionDirectoryReport[] = [reportAgentDefinitionDirectory("builtin", BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION)];
2653
+ if (scope !== "project") for (const directory of sources.userLoaded) directories.push(reportAgentDefinitionDirectory("user", directory.dir, directory.inspection));
2654
+ if (scope !== "user") for (const dir of sources.projectCandidateDirs) directories.push(reportAgentDefinitionDirectory("project", dir, sources.projectInspections.get(dir)!));
2655
+ for (const directory of sources.packageLoaded) {
2656
+ if (directory.packageEntry && packageEntryIncluded(scope, directory.packageEntry.scope)) directories.push(reportAgentDefinitionDirectory("package", directory.dir, directory.inspection));
2657
+ }
2658
+ return directories;
2659
+ }
2660
+
2661
+ function discoveryDiagnostics(sources: AgentDiscoverySources, scope: AgentScope): AgentDiscoveryDiagnostic[] {
2662
+ return [
2663
+ ...sources.builtinLoaded.diagnostics,
2664
+ ...(scope === "project" ? [] : sources.userLoaded.flatMap((loaded) => loaded.loaded.diagnostics)),
2665
+ ...(scope === "user" ? [] : sources.projectLoaded.flatMap((loaded) => loaded.loaded.diagnostics)),
2666
+ ...sources.packageLoaded
2667
+ .filter((loaded) => loaded.packageEntry && packageEntryIncluded(scope, loaded.packageEntry.scope))
2668
+ .flatMap((loaded) => loaded.loaded.diagnostics),
2669
+ ];
2670
+ }
2671
+
2672
+ function buildEffectiveDiscovery(sources: AgentDiscoverySources, scope: AgentScope): AgentDiscoveryResult {
2673
+ const configured = configuredAgentsForScope(sources, scope);
2674
+ const agents = applySubagentMaxThinking(
2675
+ mergeAgentsForScope(scope, configured.user, configured.project, configured.builtin, configured.package).filter((agent) => agent.disabled !== true),
2676
+ configured.maxThinking,
2432
2677
  );
2678
+ return {
2679
+ agents,
2680
+ agentDiagnostics: discoveryDiagnostics(sources, scope),
2681
+ projectAgentsDir: sources.projectAgentsDir,
2682
+ cwd: sources.cwd,
2683
+ scope,
2684
+ directories: discoveryDirectories(sources, scope),
2685
+ ...(configured.modelScope !== undefined ? { modelScope: configured.modelScope } : {}),
2686
+ ...(configured.maxThinking !== undefined ? { maxThinking: configured.maxThinking } : {}),
2687
+ };
2688
+ }
2433
2689
 
2434
- const chainMap = new Map<string, ChainConfig>();
2435
- const packageChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
2690
+ function buildAllDiscovery(sources: AgentDiscoverySources, includeChains: boolean, settingsScope: AgentScope): AgentDiscoveryAllResult {
2691
+ if (includeChains) ensureDiscoveryChains(sources);
2692
+ const configured = configuredAgentsForScope(sources, "both", settingsScope);
2436
2693
  const packageChainMap = new Map<string, ChainConfig>();
2437
- for (const dir of packageSubagentPaths.chains) {
2438
- const loaded = loadChainsFromDir(dir, "package");
2439
- packageChainDiagnostics.push(...loaded.diagnostics);
2440
- for (const chain of loaded.chains) {
2441
- if (!packageChainMap.has(chain.name)) packageChainMap.set(chain.name, chain);
2442
- }
2694
+ const packageChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
2695
+ for (const chain of sources.packageChainLoaded ?? []) {
2696
+ packageChainDiagnostics.push(...chain.loaded.diagnostics);
2697
+ for (const definition of chain.loaded.chains) if (!packageChainMap.has(definition.name)) packageChainMap.set(definition.name, definition);
2443
2698
  }
2699
+ const projectChainMap = new Map<string, ChainConfig>();
2444
2700
  const projectChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
2445
- for (const dir of projectChainDirs) {
2446
- const loaded = loadChainsFromDir(dir, "project");
2447
- projectChainDiagnostics.push(...loaded.diagnostics);
2448
- for (const chain of loaded.chains) {
2449
- chainMap.set(chain.name, chain);
2450
- }
2701
+ for (const chain of sources.projectChainLoaded ?? []) {
2702
+ projectChainDiagnostics.push(...chain.loaded.diagnostics);
2703
+ for (const definition of chain.loaded.chains) projectChainMap.set(definition.name, definition);
2451
2704
  }
2452
- const userChains = loadChainsFromDir(userChainDir, "user");
2453
2705
  const chains = [
2454
2706
  ...Array.from(packageChainMap.values()),
2455
- ...userChains.chains,
2456
- ...Array.from(chainMap.values()),
2457
- ];
2458
- const chainDiagnostics = [
2459
- ...packageChainDiagnostics,
2460
- ...userChains.diagnostics,
2461
- ...projectChainDiagnostics,
2707
+ ...(sources.userChains?.chains ?? []),
2708
+ ...Array.from(projectChainMap.values()),
2462
2709
  ];
2463
- const agentDiagnostics = [
2464
- ...builtinLoaded.diagnostics,
2465
- ...userLoaded.flatMap((loaded) => loaded.diagnostics),
2466
- ...packageAgentDiagnostics,
2467
- ...projectAgentDiagnostics,
2468
- ];
2469
-
2470
- const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
2471
-
2472
2710
  return {
2473
- builtin: applySubagentMaxThinking(builtin, maxThinking),
2474
- package: applySubagentMaxThinking(packageAgents, maxThinking),
2475
- user: applySubagentMaxThinking(user, maxThinking),
2476
- project: applySubagentMaxThinking(project, maxThinking),
2477
- agentDiagnostics,
2711
+ builtin: applySubagentMaxThinking(configured.builtin, configured.maxThinking),
2712
+ package: applySubagentMaxThinking(configured.package, configured.maxThinking),
2713
+ user: applySubagentMaxThinking(configured.user, configured.maxThinking),
2714
+ project: applySubagentMaxThinking(configured.project, configured.maxThinking),
2715
+ agentDiagnostics: [
2716
+ ...sources.builtinLoaded.diagnostics,
2717
+ ...sources.userLoaded.flatMap((loaded) => loaded.loaded.diagnostics),
2718
+ ...sources.packageLoaded.flatMap((loaded) => loaded.loaded.diagnostics),
2719
+ ...sources.projectLoaded.flatMap((loaded) => loaded.loaded.diagnostics),
2720
+ ],
2478
2721
  chains,
2479
- chainDiagnostics,
2480
- userDir,
2481
- projectDir,
2482
- userChainDir,
2483
- projectChainDir,
2484
- userSettingsPath,
2485
- projectSettingsPath,
2486
- ...(maxThinking !== undefined ? { maxThinking } : {}),
2722
+ chainDiagnostics: [...packageChainDiagnostics, ...(sources.userChains?.diagnostics ?? []), ...projectChainDiagnostics],
2723
+ userDir: sources.userDir,
2724
+ projectDir: sources.projectAgentsDir,
2725
+ userChainDir: sources.userChainDir,
2726
+ projectChainDir: sources.projectChainDir,
2727
+ userSettingsPath: sources.userSettingsPath,
2728
+ projectSettingsPath: sources.projectSettingsPath,
2729
+ ...(configured.maxThinking !== undefined ? { maxThinking: configured.maxThinking } : {}),
2487
2730
  };
2488
2731
  }
2732
+
2733
+ export function discoverAgentSnapshot(
2734
+ cwd: string,
2735
+ scope: AgentScope,
2736
+ preferredModelProvider?: string,
2737
+ options: { includeChains?: boolean } = {},
2738
+ ): AgentDiscoverySnapshot {
2739
+ const includeChains = options.includeChains !== false;
2740
+ const sources = getAgentDiscoverySources(cwd, preferredModelProvider, includeChains);
2741
+ return { effective: buildEffectiveDiscovery(sources, scope), all: buildAllDiscovery(sources, includeChains, scope) };
2742
+ }
2743
+
2744
+ function discoverAgentsUncached(cwd: string, scope: AgentScope, preferredModelProvider?: string): AgentDiscoveryResult {
2745
+ const effectiveCwd = path.resolve(cwd);
2746
+ const userDirOld = path.join(getAgentDir(), "agents");
2747
+ const userDirNew = path.join(os.homedir(), ".agents");
2748
+ const { readDirs: projectAgentDirs, candidateDirs: projectCandidateDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(effectiveCwd);
2749
+ const userSettingsPath = getUserAgentSettingsPath();
2750
+ const projectSettingsPath = getProjectAgentSettingsPath(effectiveCwd);
2751
+ const userSettings = selectProviderOverrides(scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath), preferredModelProvider);
2752
+ const projectSettings = selectProviderOverrides(scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath), preferredModelProvider);
2753
+ const defaultProvider = resolveSubagentDefaultProvider(userSettings, projectSettings, projectSettingsPath);
2754
+ const defaultModel = resolveSubagentDefaultModel(userSettings, projectSettings, userSettingsPath, projectSettingsPath, defaultProvider);
2755
+ const defaultThinking = resolveSubagentDefaultThinking(userSettings, projectSettings, projectSettingsPath);
2756
+ const maxThinking = resolveSubagentMaxThinking(userSettings, projectSettings, projectSettingsPath);
2757
+ const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
2758
+ const modelScope = projectSettings.modelScope ?? userSettings.modelScope;
2759
+ const packageSubagentPaths = collectPackageSubagentPaths(effectiveCwd, { includeUser: scope !== "project", includeProject: scope !== "user" });
2760
+ const directories: AgentDefinitionDirectoryReport[] = [reportAgentDefinitionDirectory("builtin", BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION)];
2761
+ const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
2762
+ const builtinAgents = applyBuiltinOverrides(applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2763
+ const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew].map((dir, discoveryPriority) => {
2764
+ const inspection = inspectAgentDefinitionDirectory(dir);
2765
+ directories.push(reportAgentDefinitionDirectory("user", dir, inspection));
2766
+ return loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection);
2767
+ });
2768
+ const userAgents = applyCustomAgentOverrides(applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2769
+ const projectInspections = scope === "user" ? new Map<string, AgentDefinitionInspection>() : new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
2770
+ if (scope !== "user") for (const dir of projectCandidateDirs) directories.push(reportAgentDefinitionDirectory("project", dir, projectInspections.get(dir)!));
2771
+ const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0, undefined, projectInspections.get(dir)!));
2772
+ const projectAgents = applyCustomAgentOverrides(applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2773
+ const packageLoaded = packageSubagentPaths.agents.map((entry, index) => {
2774
+ const inspection = inspectAgentDefinitionDirectory(entry.dir);
2775
+ directories.push(reportAgentDefinitionDirectory("package", entry.dir, inspection));
2776
+ return loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry, inspection);
2777
+ });
2778
+ const packageMap = new Map<string, AgentConfig>();
2779
+ for (const loaded of packageLoaded) for (const agent of loaded.agents) if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
2780
+ const packageAgents = applyCustomAgentOverrides(applySubagentDefaults(Array.from(packageMap.values()), defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2781
+ const agents = applySubagentMaxThinking(mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents).filter((agent) => agent.disabled !== true), maxThinking);
2782
+ const agentDiagnostics = [...builtinLoaded.diagnostics, ...userLoaded.flatMap((loaded) => loaded.diagnostics), ...projectLoaded.flatMap((loaded) => loaded.diagnostics), ...packageLoaded.flatMap((loaded) => loaded.diagnostics)];
2783
+ return { agents, agentDiagnostics, projectAgentsDir, cwd: effectiveCwd, scope, directories, ...(modelScope !== undefined ? { modelScope } : {}), ...(maxThinking !== undefined ? { maxThinking } : {}) };
2784
+ }
2785
+
2786
+ export function discoverAgents(cwd: string, scope: AgentScope, preferredModelProvider?: string): AgentDiscoveryResult {
2787
+ if (scope !== "both") return discoverAgentsUncached(cwd, scope, preferredModelProvider);
2788
+ const sources = getAgentDiscoverySources(cwd, preferredModelProvider);
2789
+ return buildEffectiveDiscovery(sources, scope);
2790
+ }
2791
+
2792
+ export function discoverAgentsAll(cwd: string, preferredModelProvider?: string): AgentDiscoveryAllResult {
2793
+ return discoverAgentSnapshot(cwd, "both", preferredModelProvider).all;
2794
+ }