pi-subagents 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.
Files changed (109) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. package/src/runs/foreground/chain-execution.ts +0 -1581
@@ -137,6 +137,7 @@ export interface AgentConfig {
137
137
  systemPrompt: string;
138
138
  source: AgentSource;
139
139
  filePath: string;
140
+ discoveryPriority?: number;
140
141
  skills?: string[];
141
142
  skillPath?: string[];
142
143
  extensions?: string[];
@@ -212,8 +213,45 @@ export interface ChainDiscoveryDiagnostic {
212
213
  error: string;
213
214
  }
214
215
 
216
+ export interface AgentDiscoveryDiagnostic extends ChainDiscoveryDiagnostic {
217
+ name?: string;
218
+ runtimeName?: string;
219
+ packageSpecified?: boolean;
220
+ discoveryPriority?: number;
221
+ }
222
+
223
+ const AGENT_SOURCE_PRIORITY: Record<AgentSource, number> = {
224
+ builtin: 0,
225
+ package: 1,
226
+ user: 2,
227
+ project: 3,
228
+ };
229
+
230
+ function agentDefinitionPriority(definition: Pick<AgentConfig | AgentDiscoveryDiagnostic, "source" | "discoveryPriority">): number {
231
+ return AGENT_SOURCE_PRIORITY[definition.source] * 1_000_000
232
+ + (definition.discoveryPriority ?? 0);
233
+ }
234
+
235
+ export function findBlockingAgentDiagnostic(name: string, agent: AgentConfig | readonly AgentConfig[] | undefined, diagnostics: AgentDiscoveryDiagnostic[] | undefined): AgentDiscoveryDiagnostic | undefined {
236
+ const normalizedName = name.trim();
237
+ const agents = Array.isArray(agent) ? agent : agent ? [agent] : [];
238
+ let match: AgentDiscoveryDiagnostic | undefined;
239
+ for (const diagnostic of diagnostics ?? []) {
240
+ if ((diagnostic.runtimeName === normalizedName
241
+ || (diagnostic.name === normalizedName && (!diagnostic.packageSpecified
242
+ || diagnostic.runtimeName === undefined
243
+ || agents.some((agent) => agent.name === diagnostic.runtimeName && agent.localName === diagnostic.name))))
244
+ && (!match || agentDefinitionPriority(diagnostic) > agentDefinitionPriority(match))) {
245
+ match = diagnostic;
246
+ }
247
+ }
248
+ const highestPriority = Math.max(...agents.map(agentDefinitionPriority), -Infinity);
249
+ return !agents.length || (match && agentDefinitionPriority(match) > highestPriority) ? match : undefined;
250
+ }
251
+
215
252
  interface AgentDiscoveryResult {
216
253
  agents: AgentConfig[];
254
+ agentDiagnostics?: AgentDiscoveryDiagnostic[];
217
255
  projectAgentsDir: string | null;
218
256
  modelScope?: ModelScopeConfig;
219
257
  }
@@ -345,7 +383,7 @@ function getGlobalNpmRoot(): string | null {
345
383
  }
346
384
 
347
385
  try {
348
- cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
386
+ cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000, windowsHide: true }).trim());
349
387
  return cachedGlobalNpmRoot;
350
388
  } catch {
351
389
  cachedGlobalNpmRoot = "";
@@ -1448,6 +1486,14 @@ function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
1448
1486
  return parts.some((part, index) => part === ".agents" && parts[index + 1] === "skills");
1449
1487
  }
1450
1488
 
1489
+ function isJsonSerializable(value: unknown): boolean {
1490
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1491
+ if (typeof value === "number") return Number.isFinite(value);
1492
+ if (Array.isArray(value)) return value.every(isJsonSerializable);
1493
+ if (value && typeof value === "object") return Object.values(value).every(isJsonSerializable);
1494
+ return false;
1495
+ }
1496
+
1451
1497
  function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string): AgentRunnerConfig | undefined {
1452
1498
  if (raw === undefined || !raw.trim()) return undefined;
1453
1499
  let parsed: unknown;
@@ -1464,8 +1510,24 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
1464
1510
  if (Object.keys(runner).some((key) => key !== "type")) throw new Error(`Agent '${agentName}' has invalid Pi runner frontmatter; only 'type' is supported.`);
1465
1511
  return { type: "pi" };
1466
1512
  }
1513
+ if (runner.type === "external-job") {
1514
+ if (typeof runner.provider !== "string" || !runner.provider.trim() || runner.provider.trim() !== runner.provider) {
1515
+ throw new Error(`Agent '${agentName}' external-job runner requires a non-empty trimmed provider string.`);
1516
+ }
1517
+ if (runner.options !== undefined && (!runner.options || typeof runner.options !== "object" || Array.isArray(runner.options) || !isJsonSerializable(runner.options))) {
1518
+ throw new Error(`Agent '${agentName}' external-job runner options must be a JSON-serializable object.`);
1519
+ }
1520
+ const supported = new Set(["type", "provider", "options"]);
1521
+ const unknown = Object.keys(runner).filter((key) => !supported.has(key));
1522
+ if (unknown.length > 0) throw new Error(`Agent '${agentName}' external-job runner has unsupported fields: ${unknown.join(", ")}.`);
1523
+ return {
1524
+ type: "external-job",
1525
+ provider: runner.provider,
1526
+ ...(runner.options ? { options: runner.options as Record<string, unknown> } : {}),
1527
+ };
1528
+ }
1467
1529
  if (runner.type !== "external-cli") {
1468
- throw new Error(`Agent '${agentName}' has invalid runner.type; expected 'pi' or 'external-cli'.`);
1530
+ throw new Error(`Agent '${agentName}' has invalid runner.type; expected 'pi', 'external-cli', or 'external-job'.`);
1469
1531
  }
1470
1532
  if (typeof runner.command !== "string" || !runner.command.trim()) {
1471
1533
  throw new Error(`Agent '${agentName}' external-cli runner requires a non-empty command string.`);
@@ -1489,11 +1551,11 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
1489
1551
  }
1490
1552
 
1491
1553
  function validateExternalRunnerProfile(frontmatter: Record<string, string>, agentName: string, runner: AgentRunnerConfig | undefined): void {
1492
- if (runner?.type !== "external-cli") return;
1554
+ if (runner?.type !== "external-cli" && runner?.type !== "external-job") return;
1493
1555
  const unsupported = ["tools", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
1494
1556
  .filter((field) => frontmatter[field] !== undefined);
1495
1557
  if (unsupported.length > 0) {
1496
- throw new Error(`Agent '${agentName}' uses runner.type='external-cli' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
1558
+ throw new Error(`Agent '${agentName}' uses runner.type='${runner.type}' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
1497
1559
  }
1498
1560
  }
1499
1561
 
@@ -1510,21 +1572,48 @@ function parseAgentAcceptanceFrontmatter(raw: string | undefined, agentName: str
1510
1572
  return parsed as AcceptanceInput;
1511
1573
  }
1512
1574
 
1513
- function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1514
- const agents: AgentConfig[] = [];
1575
+ interface AgentDefinitionFile {
1576
+ filePath: string;
1577
+ content: string;
1578
+ }
1515
1579
 
1580
+ function readAgentDefinitionFiles(dir: string): AgentDefinitionFile[] {
1581
+ const files: AgentDefinitionFile[] = [];
1516
1582
  for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
1517
1583
  if (isLegacyAgentSkillPath(dir, filePath)) {
1518
1584
  continue;
1519
1585
  }
1520
1586
 
1521
- let content: string;
1522
1587
  try {
1523
- content = fs.readFileSync(filePath, "utf-8");
1588
+ files.push({ filePath, content: fs.readFileSync(filePath, "utf-8") });
1524
1589
  } catch {
1525
1590
  continue;
1526
1591
  }
1592
+ }
1593
+ return files;
1594
+ }
1527
1595
 
1596
+ function resolveAgentRelativeExtensionPaths(paths: string[] | undefined, agentFilePath: string): string[] | undefined {
1597
+ if (paths === undefined) return undefined;
1598
+ const baseDir = path.dirname(agentFilePath);
1599
+ return paths.map((entry) => {
1600
+ const trimmed = entry.trim();
1601
+ if (trimmed === "." || trimmed === ".." || trimmed.startsWith("./") || trimmed.startsWith("../")) {
1602
+ return path.resolve(baseDir, trimmed);
1603
+ }
1604
+ return entry;
1605
+ });
1606
+ }
1607
+
1608
+ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1609
+ const agents: AgentConfig[] = [];
1610
+ const diagnostics: AgentDiscoveryDiagnostic[] = [];
1611
+
1612
+ for (const { filePath, content } of files) {
1613
+ let name: string | undefined;
1614
+ let runtimeName: string | undefined;
1615
+ let packageSpecified = false;
1616
+ try {
1528
1617
  const { frontmatter, body } = parseFrontmatter(content);
1529
1618
 
1530
1619
  if (!frontmatter.name || !frontmatter.description) {
@@ -1532,10 +1621,12 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1532
1621
  }
1533
1622
 
1534
1623
  const localName = frontmatter.name;
1624
+ name = localName;
1535
1625
  const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`);
1536
- if (parsedPackage.error) continue;
1626
+ packageSpecified = parsedPackage.packageName !== undefined || parsedPackage.error !== undefined;
1627
+ if (parsedPackage.error) throw new Error(parsedPackage.error);
1537
1628
  const packageName = parsedPackage.packageName;
1538
- const runtimeName = buildRuntimeName(localName, packageName);
1629
+ runtimeName = buildRuntimeName(localName, packageName);
1539
1630
 
1540
1631
  const runner = parseAgentRunnerFrontmatter(frontmatter.runner, localName);
1541
1632
  validateExternalRunnerProfile(frontmatter, localName, runner);
@@ -1605,8 +1696,8 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1605
1696
  else throw new Error(`Agent '${localName}' has invalid acceptanceRole frontmatter; expected 'read-only' or 'writer'.`);
1606
1697
  }
1607
1698
 
1608
- const extensions = parseFrontmatterList(frontmatter.extensions);
1609
- const subagentOnlyExtensions = parseFrontmatterList(frontmatter.subagentOnlyExtensions);
1699
+ const extensions = resolveAgentRelativeExtensionPaths(parseFrontmatterList(frontmatter.extensions), filePath);
1700
+ const subagentOnlyExtensions = resolveAgentRelativeExtensionPaths(parseFrontmatterList(frontmatter.subagentOnlyExtensions), filePath);
1610
1701
 
1611
1702
  const extraFields: Record<string, string> = {};
1612
1703
  for (const [key, value] of Object.entries(frontmatter)) {
@@ -1664,6 +1755,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1664
1755
  systemPrompt: body,
1665
1756
  source,
1666
1757
  filePath,
1758
+ ...(discoveryPriority !== undefined ? { discoveryPriority } : {}),
1667
1759
  ...(skills?.length ? { skills } : {}),
1668
1760
  ...(skillPath?.length ? { skillPath } : {}),
1669
1761
  ...(extensions !== undefined ? { extensions } : {}),
@@ -1681,9 +1773,16 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1681
1773
  };
1682
1774
  agentFrontmatterFields.set(agent, new Set(Object.keys(frontmatter)));
1683
1775
  agents.push(agent);
1776
+ } catch (error) {
1777
+ diagnostics.push({ source, filePath, ...(name ? { name } : {}), ...(runtimeName && runtimeName !== name ? { runtimeName } : {}), ...(packageSpecified ? { packageSpecified: true } : {}), ...(discoveryPriority !== undefined ? { discoveryPriority } : {}), error: error instanceof Error ? error.message : String(error) });
1778
+ }
1684
1779
  }
1685
1780
 
1686
- return agents;
1781
+ return { agents, diagnostics };
1782
+ }
1783
+
1784
+ function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1785
+ return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority);
1687
1786
  }
1688
1787
 
1689
1788
  function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
@@ -1747,6 +1846,7 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
1747
1846
  };
1748
1847
  }
1749
1848
  const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
1849
+ const BUILTIN_AGENT_DEFINITION_FILES = readAgentDefinitionFiles(BUILTIN_AGENTS_DIR);
1750
1850
 
1751
1851
  export const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
1752
1852
 
@@ -1781,34 +1881,42 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
1781
1881
  includeProject: scope !== "user",
1782
1882
  });
1783
1883
 
1884
+ const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
1784
1885
  const builtinAgents = applyBuiltinOverrides(
1785
- applySubagentDefaults(loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"), defaultModel, defaultThinking, defaultExtensions),
1886
+ applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultThinking, defaultExtensions),
1786
1887
  userSettings,
1787
1888
  projectSettings,
1788
1889
  userSettingsPath,
1789
1890
  projectSettingsPath,
1790
1891
  );
1791
1892
 
1792
- const userAgentsExtra = scope === "project" ? [] : extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user"));
1793
- const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
1794
- const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
1893
+ const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew]
1894
+ .map((dir, discoveryPriority) => loadAgentsFromDir(dir, "user", discoveryPriority));
1795
1895
  const userAgents = applyCustomAgentOverrides(
1796
- applySubagentDefaults([...userAgentsExtra, ...userAgentsOld, ...userAgentsNew], defaultModel, defaultThinking, defaultExtensions),
1896
+ applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
1797
1897
  userSettings,
1798
1898
  projectSettings,
1799
1899
  userSettingsPath,
1800
1900
  projectSettingsPath,
1801
1901
  );
1802
1902
 
1903
+ const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0));
1803
1904
  const projectAgents = applyCustomAgentOverrides(
1804
- applySubagentDefaults(scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project")), defaultModel, defaultThinking, defaultExtensions),
1905
+ applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
1805
1906
  userSettings,
1806
1907
  projectSettings,
1807
1908
  userSettingsPath,
1808
1909
  projectSettingsPath,
1809
1910
  );
1911
+ const packageLoaded = packageSubagentPaths.agents.map((dir, index) => loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index));
1912
+ const packageMap = new Map<string, AgentConfig>();
1913
+ for (const loaded of packageLoaded) {
1914
+ for (const agent of loaded.agents) {
1915
+ if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
1916
+ }
1917
+ }
1810
1918
  const packageAgents = applyCustomAgentOverrides(
1811
- applySubagentDefaults(packageSubagentPaths.agents.flatMap((dir) => loadAgentsFromDir(dir, "package")), defaultModel, defaultThinking, defaultExtensions),
1919
+ applySubagentDefaults(Array.from(packageMap.values()), defaultModel, defaultThinking, defaultExtensions),
1812
1920
  userSettings,
1813
1921
  projectSettings,
1814
1922
  userSettingsPath,
@@ -1817,7 +1925,13 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
1817
1925
  const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
1818
1926
  .filter((agent) => agent.disabled !== true);
1819
1927
 
1820
- return { agents, projectAgentsDir, ...(modelScope !== undefined ? { modelScope } : {}) };
1928
+ const agentDiagnostics = [
1929
+ ...builtinLoaded.diagnostics,
1930
+ ...userLoaded.flatMap((loaded) => loaded.diagnostics),
1931
+ ...projectLoaded.flatMap((loaded) => loaded.diagnostics),
1932
+ ...packageLoaded.flatMap((loaded) => loaded.diagnostics),
1933
+ ];
1934
+ return { agents, agentDiagnostics, projectAgentsDir, ...(modelScope !== undefined ? { modelScope } : {}) };
1821
1935
  }
1822
1936
 
1823
1937
  export function discoverAgentsAll(cwd: string): {
@@ -1825,6 +1939,7 @@ export function discoverAgentsAll(cwd: string): {
1825
1939
  package: AgentConfig[];
1826
1940
  user: AgentConfig[];
1827
1941
  project: AgentConfig[];
1942
+ agentDiagnostics?: AgentDiscoveryDiagnostic[];
1828
1943
  chains: ChainConfig[];
1829
1944
  chainDiagnostics: ChainDiscoveryDiagnostic[];
1830
1945
  userDir: string;
@@ -1848,27 +1963,29 @@ export function discoverAgentsAll(cwd: string): {
1848
1963
  const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
1849
1964
  const packageSubagentPaths = collectPackageSubagentPaths(cwd);
1850
1965
 
1966
+ const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
1851
1967
  const builtin = applyBuiltinOverrides(
1852
- applySubagentDefaults(loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"), defaultModel, defaultThinking, defaultExtensions),
1968
+ applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultThinking, defaultExtensions),
1853
1969
  userSettings,
1854
1970
  projectSettings,
1855
1971
  userSettingsPath,
1856
1972
  projectSettingsPath,
1857
1973
  );
1974
+ const userLoaded = [...extraUserAgentDirs(), userDirOld, userDirNew]
1975
+ .map((dir, discoveryPriority) => loadAgentsFromDir(dir, "user", discoveryPriority));
1858
1976
  const user = applyCustomAgentOverrides(
1859
- applySubagentDefaults([
1860
- ...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
1861
- ...loadAgentsFromDir(userDirOld, "user"),
1862
- ...loadAgentsFromDir(userDirNew, "user"),
1863
- ], defaultModel, defaultThinking, defaultExtensions),
1977
+ applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultThinking, defaultExtensions),
1864
1978
  userSettings,
1865
1979
  projectSettings,
1866
1980
  userSettingsPath,
1867
1981
  projectSettingsPath,
1868
1982
  );
1869
1983
  const packageMap = new Map<string, AgentConfig>();
1870
- for (const dir of packageSubagentPaths.agents) {
1871
- for (const agent of loadAgentsFromDir(dir, "package")) {
1984
+ const packageAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
1985
+ for (const [index, dir] of packageSubagentPaths.agents.entries()) {
1986
+ const loaded = loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index);
1987
+ packageAgentDiagnostics.push(...loaded.diagnostics);
1988
+ for (const agent of loaded.agents) {
1872
1989
  if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
1873
1990
  }
1874
1991
  }
@@ -1880,8 +1997,11 @@ export function discoverAgentsAll(cwd: string): {
1880
1997
  projectSettingsPath,
1881
1998
  );
1882
1999
  const projectMap = new Map<string, AgentConfig>();
2000
+ const projectAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
1883
2001
  for (const dir of projectDirs) {
1884
- for (const agent of loadAgentsFromDir(dir, "project")) {
2002
+ const loaded = loadAgentsFromDir(dir, "project", dir === projectDir ? 1 : 0);
2003
+ projectAgentDiagnostics.push(...loaded.diagnostics);
2004
+ for (const agent of loaded.agents) {
1885
2005
  projectMap.set(agent.name, agent);
1886
2006
  }
1887
2007
  }
@@ -1922,8 +2042,14 @@ export function discoverAgentsAll(cwd: string): {
1922
2042
  ...userChains.diagnostics,
1923
2043
  ...projectChainDiagnostics,
1924
2044
  ];
2045
+ const agentDiagnostics = [
2046
+ ...builtinLoaded.diagnostics,
2047
+ ...userLoaded.flatMap((loaded) => loaded.diagnostics),
2048
+ ...packageAgentDiagnostics,
2049
+ ...projectAgentDiagnostics,
2050
+ ];
1925
2051
 
1926
2052
  const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
1927
2053
 
1928
- return { builtin, package: packageAgents, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
2054
+ return { builtin, package: packageAgents, user, project, agentDiagnostics, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
1929
2055
  }
@@ -142,7 +142,7 @@ function getGlobalNpmRoot(): string | null {
142
142
  }
143
143
 
144
144
  try {
145
- cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
145
+ cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000, windowsHide: true }).trim());
146
146
  return cachedGlobalNpmRoot;
147
147
  } catch {
148
148
  // Global npm root is optional in constrained environments.
@@ -0,0 +1,185 @@
1
+ export const EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION = 1;
2
+ export const EXTERNAL_JOB_PROVIDER_REGISTRY_KEY = "pi-subagents.external-job-providers.v1";
3
+
4
+ const MAX_PROVIDER_NAME_LENGTH = 128;
5
+ const MAX_PROVIDERS = 100;
6
+ const MAX_JOB_ID_LENGTH = 256;
7
+ const MAX_FAILURE_CODE_LENGTH = 128;
8
+ const MAX_FAILURE_MESSAGE_LENGTH = 4_096;
9
+ const MAX_URL_LENGTH = 4_096;
10
+
11
+ export type ExternalJobState = "queued" | "running" | "completed" | "failed" | "stopped" | "blocked";
12
+ export type ExternalJobOperation = "start" | "status" | "result" | "reattach";
13
+
14
+ export type ExternalJobOptions = Record<string, unknown>;
15
+
16
+ export interface ExternalJobStartInput {
17
+ prompt: string;
18
+ promptDigest: string;
19
+ cwd: string;
20
+ runId: string;
21
+ stepIndex: number;
22
+ agent: string;
23
+ options: ExternalJobOptions;
24
+ sessionId?: string;
25
+ }
26
+
27
+ export interface ExternalJobHandle {
28
+ providerJobId: string;
29
+ state: ExternalJobState;
30
+ handleUrl?: string;
31
+ conversationUrl?: string;
32
+ failureCode?: string;
33
+ failureMessage?: string;
34
+ blockingJobId?: string;
35
+ }
36
+
37
+ export interface ExternalJobResult extends ExternalJobHandle {
38
+ output?: string;
39
+ artifactPath?: string;
40
+ }
41
+
42
+ export interface ExternalJobProvider {
43
+ name: string;
44
+ start(input: ExternalJobStartInput): Promise<ExternalJobHandle> | ExternalJobHandle;
45
+ status(providerJobId: string): Promise<ExternalJobHandle> | ExternalJobHandle;
46
+ result(providerJobId: string): Promise<ExternalJobResult> | ExternalJobResult;
47
+ reattach(providerJobId: string): Promise<ExternalJobHandle> | ExternalJobHandle;
48
+ }
49
+
50
+ export class ExternalJobProviderError extends Error {
51
+ readonly code: string;
52
+ readonly blockingJobId?: string;
53
+
54
+ constructor(message: string, options: { code: string; blockingJobId?: string; cause?: unknown }) {
55
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
56
+ this.name = "ExternalJobProviderError";
57
+ this.code = options.code;
58
+ this.blockingJobId = options.blockingJobId;
59
+ }
60
+ }
61
+
62
+ interface ExternalJobProviderRegistry {
63
+ version: typeof EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION;
64
+ providers: Map<string, ExternalJobProvider>;
65
+ }
66
+
67
+ function registry(): ExternalJobProviderRegistry {
68
+ const key = Symbol.for(EXTERNAL_JOB_PROVIDER_REGISTRY_KEY);
69
+ const globalObject = globalThis as Record<PropertyKey, unknown>;
70
+ const existing = globalObject[key];
71
+ if (existing === undefined) {
72
+ const created: ExternalJobProviderRegistry = {
73
+ version: EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION,
74
+ providers: new Map(),
75
+ };
76
+ globalObject[key] = created;
77
+ return created;
78
+ }
79
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
80
+ throw new Error(`Malformed external-job provider registry at Symbol.for("${EXTERNAL_JOB_PROVIDER_REGISTRY_KEY}").`);
81
+ }
82
+ const candidate = existing as Partial<ExternalJobProviderRegistry>;
83
+ if (candidate.version !== EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION || !(candidate.providers instanceof Map)) {
84
+ throw new Error(`Unsupported external-job provider registry at Symbol.for("${EXTERNAL_JOB_PROVIDER_REGISTRY_KEY}").`);
85
+ }
86
+ return candidate as ExternalJobProviderRegistry;
87
+ }
88
+
89
+ function validateString(value: unknown, field: string, maxLength: number): string {
90
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
91
+ throw new Error(`${field} must be a non-empty string without leading or trailing whitespace.`);
92
+ }
93
+ if (value.length > maxLength) throw new Error(`${field} must be at most ${maxLength} characters.`);
94
+ if (value.includes("\0")) throw new Error(`${field} must not contain NUL characters.`);
95
+ return value;
96
+ }
97
+
98
+ function validateOptionalString(value: unknown, field: string, maxLength: number): string | undefined {
99
+ if (value === undefined) return undefined;
100
+ return validateString(value, field, maxLength);
101
+ }
102
+
103
+ function validateState(value: unknown, field: string): ExternalJobState {
104
+ if (value === "queued" || value === "running" || value === "completed" || value === "failed" || value === "stopped" || value === "blocked") return value;
105
+ throw new Error(`${field} must be queued, running, completed, failed, stopped, or blocked.`);
106
+ }
107
+
108
+ function validateHandle(provider: string, value: unknown, field: string, extraFields: readonly string[] = []): ExternalJobHandle {
109
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} from external-job provider '${provider}' must be an object.`);
110
+ const handle = value as Record<string, unknown>;
111
+ const supported = new Set(["providerJobId", "state", "handleUrl", "conversationUrl", "failureCode", "failureMessage", "blockingJobId", ...extraFields]);
112
+ const unknown = Object.keys(handle).filter((key) => !supported.has(key));
113
+ if (unknown.length > 0) throw new Error(`${field} from external-job provider '${provider}' has unknown fields: ${unknown.join(", ")}.`);
114
+ const handleUrl = validateOptionalString(handle.handleUrl, `${field}.handleUrl`, MAX_URL_LENGTH);
115
+ const conversationUrl = validateOptionalString(handle.conversationUrl, `${field}.conversationUrl`, MAX_URL_LENGTH);
116
+ const failureCode = validateOptionalString(handle.failureCode, `${field}.failureCode`, MAX_FAILURE_CODE_LENGTH);
117
+ const failureMessage = validateOptionalString(handle.failureMessage, `${field}.failureMessage`, MAX_FAILURE_MESSAGE_LENGTH);
118
+ const blockingJobId = validateOptionalString(handle.blockingJobId, `${field}.blockingJobId`, MAX_JOB_ID_LENGTH);
119
+ return {
120
+ providerJobId: validateString(handle.providerJobId, `${field}.providerJobId`, MAX_JOB_ID_LENGTH),
121
+ state: validateState(handle.state, `${field}.state`),
122
+ ...(handleUrl ? { handleUrl } : {}),
123
+ ...(conversationUrl ? { conversationUrl } : {}),
124
+ ...(failureCode ? { failureCode } : {}),
125
+ ...(failureMessage ? { failureMessage } : {}),
126
+ ...(blockingJobId ? { blockingJobId } : {}),
127
+ };
128
+ }
129
+
130
+ export function validateExternalJobHandle(provider: string, value: unknown, field = "External-job handle"): ExternalJobHandle {
131
+ return validateHandle(provider, value, field);
132
+ }
133
+
134
+ export function validateExternalJobResult(provider: string, value: unknown, field = "External-job result"): ExternalJobResult {
135
+ const result = validateHandle(provider, value, field, ["output", "artifactPath"]) as ExternalJobResult;
136
+ const record = value as Record<string, unknown>;
137
+ const output = validateOptionalString(record.output, `${field}.output`, 1024 * 1024);
138
+ const artifactPath = validateOptionalString(record.artifactPath, `${field}.artifactPath`, MAX_URL_LENGTH);
139
+ return {
140
+ ...result,
141
+ ...(output !== undefined ? { output } : {}),
142
+ ...(artifactPath !== undefined ? { artifactPath } : {}),
143
+ };
144
+ }
145
+
146
+ function validateProvider(value: unknown): ExternalJobProvider {
147
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("External-job provider must be an object.");
148
+ const provider = value as Record<string, unknown>;
149
+ const unknownFields = Object.keys(provider).filter((key) => !["name", "start", "status", "result", "reattach"].includes(key));
150
+ if (unknownFields.length > 0) throw new Error(`External-job provider has unknown fields: ${unknownFields.join(", ")}.`);
151
+ const name = validateString(provider.name, "External-job provider name", MAX_PROVIDER_NAME_LENGTH);
152
+ for (const op of ["start", "status", "result", "reattach"] as const) {
153
+ if (typeof provider[op] !== "function") throw new Error(`External-job provider '${name}' must expose ${op}().`);
154
+ }
155
+ return value as ExternalJobProvider;
156
+ }
157
+
158
+ export function registerExternalJobProvider(provider: ExternalJobProvider): () => void {
159
+ const validated = validateProvider(provider);
160
+ const current = registry();
161
+ if (!current.providers.has(validated.name) && current.providers.size >= MAX_PROVIDERS) {
162
+ throw new Error(`External-job provider registry supports at most ${MAX_PROVIDERS} providers.`);
163
+ }
164
+ current.providers.set(validated.name, validated);
165
+ return () => {
166
+ if (current.providers.get(validated.name) === validated) current.providers.delete(validated.name);
167
+ };
168
+ }
169
+
170
+ export function listExternalJobProviders(): readonly ExternalJobProvider[] {
171
+ const current = registry();
172
+ if (current.providers.size > MAX_PROVIDERS) throw new Error(`External-job provider registry contains more than ${MAX_PROVIDERS} providers.`);
173
+ const providers: ExternalJobProvider[] = [];
174
+ for (const [key, value] of current.providers) {
175
+ const provider = validateProvider(value);
176
+ if (key !== provider.name) throw new Error(`External-job provider registry key '${key}' does not match provider name '${provider.name}'.`);
177
+ providers.push(provider);
178
+ }
179
+ return providers;
180
+ }
181
+
182
+ export function getExternalJobProvider(name: string): ExternalJobProvider | undefined {
183
+ const safeName = validateString(name, "External-job provider name", MAX_PROVIDER_NAME_LENGTH);
184
+ return listExternalJobProviders().find((provider) => provider.name === safeName);
185
+ }