pi-subagents 0.67.0 → 0.69.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 (122) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +41 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +38 -14
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +59 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +6 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +185 -72
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/agents/skills.ts +1 -1
  30. package/src/api/preflight.ts +20 -16
  31. package/src/api/required-child-extensions.ts +6 -0
  32. package/src/extension/config.ts +10 -37
  33. package/src/extension/fanout-child.ts +3 -0
  34. package/src/extension/herdr-pi-bridge.ts +160 -0
  35. package/src/extension/index.ts +42 -31
  36. package/src/extension/public-execution.ts +3 -3
  37. package/src/extension/schemas.ts +23 -6
  38. package/src/extension/tool-description.ts +8 -7
  39. package/src/inspectors/ghostty/plugin.ts +13 -1
  40. package/src/intercom/native-supervisor-channel.ts +22 -18
  41. package/src/policy/authority.ts +4 -0
  42. package/src/profiles/profiles.ts +12 -6
  43. package/src/runs/background/active-run-index.ts +17 -1
  44. package/src/runs/background/async-execution.ts +309 -126
  45. package/src/runs/background/async-job-tracker.ts +8 -6
  46. package/src/runs/background/async-resume.ts +13 -4
  47. package/src/runs/background/async-status.ts +15 -4
  48. package/src/runs/background/auto-drain.ts +20 -10
  49. package/src/runs/background/binary-bootstrap.ts +5 -0
  50. package/src/runs/background/chain-append.ts +1 -1
  51. package/src/runs/background/chain-root-attachment.ts +14 -33
  52. package/src/runs/background/notify.ts +74 -6
  53. package/src/runs/background/result-files.ts +8 -4
  54. package/src/runs/background/result-watcher.ts +19 -2
  55. package/src/runs/background/run-child-session.ts +20 -29
  56. package/src/runs/background/runner-aliases.ts +4 -33
  57. package/src/runs/background/runner-child-launch.ts +4 -1
  58. package/src/runs/background/runner-child-sessions.ts +2 -2
  59. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  60. package/src/runs/background/scheduled-runs.ts +11 -5
  61. package/src/runs/background/stale-run-reconciler.ts +35 -11
  62. package/src/runs/background/subagent-runner.ts +413 -276
  63. package/src/runs/background/subagent-wait.ts +128 -23
  64. package/src/runs/background/wait-completions.ts +75 -27
  65. package/src/runs/background/wait-subscriptions.ts +9 -3
  66. package/src/runs/background/wait-tool.ts +4 -2
  67. package/src/runs/foreground/async-stop-action.ts +93 -3
  68. package/src/runs/foreground/execution.ts +115 -219
  69. package/src/runs/foreground/foreground-history.ts +2 -1
  70. package/src/runs/foreground/subagent-executor.ts +281 -80
  71. package/src/runs/shared/acceptance.ts +194 -37
  72. package/src/runs/shared/async-status-projection.ts +123 -33
  73. package/src/runs/shared/child-launch-plan.ts +15 -3
  74. package/src/runs/shared/child-launch.ts +19 -6
  75. package/src/runs/shared/child-runtime-config.ts +5 -0
  76. package/src/runs/shared/child-session.ts +94 -50
  77. package/src/runs/shared/child-tool-plan.ts +28 -16
  78. package/src/runs/shared/dynamic-fanout.ts +2 -2
  79. package/src/runs/shared/external-cli-contract.ts +11 -1
  80. package/src/runs/shared/external-cli-preflight.ts +6 -2
  81. package/src/runs/shared/herdr-connection.ts +134 -0
  82. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  83. package/src/runs/shared/herdr-machine.ts +279 -0
  84. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  85. package/src/runs/shared/herdr-placed-run.ts +263 -0
  86. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  87. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  88. package/src/runs/shared/model-scope.ts +1 -1
  89. package/src/runs/shared/nested-events.ts +11 -2
  90. package/src/runs/shared/parallel-utils.ts +7 -2
  91. package/src/runs/shared/pi-spawn.ts +1 -1
  92. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  93. package/src/runs/shared/worktree-setup-command.ts +27 -4
  94. package/src/runs/shared/worktree.ts +30 -8
  95. package/src/shared/child-cache-retention.ts +43 -0
  96. package/src/shared/launch-contract.ts +6 -9
  97. package/src/shared/pruned-fork.ts +1 -1
  98. package/src/shared/required-child-extensions.ts +81 -0
  99. package/src/shared/settings.ts +5 -2
  100. package/src/shared/shortcuts.ts +0 -4
  101. package/src/shared/types.ts +81 -29
  102. package/src/slash/slash-commands.ts +0 -6
  103. package/src/slash/subagents-admin.ts +13 -9
  104. package/src/tui/render.ts +20 -10
  105. package/src/watchdog/child-status.ts +28 -36
  106. package/src/watchdog/lsp-diagnostics.ts +1 -1
  107. package/src/watchdog/model-selection.ts +1 -1
  108. package/src/watchdog/register-child.ts +10 -3
  109. package/src/watchdog/register-main.ts +20 -20
  110. package/src/watchdog/render.ts +1 -1
  111. package/src/watchdog/review.ts +14 -30
  112. package/src/watchdog/rules.ts +1 -1
  113. package/src/watchdog/runtime.ts +23 -12
  114. package/src/watchdog/settings.ts +3 -6
  115. package/src/watchdog/types.ts +3 -5
  116. package/src/watchdog/warning-format.ts +1 -1
  117. package/src/workflows/scripted-workflow.ts +68 -7
  118. package/src/workflows/workflow-receipt.ts +21 -3
  119. package/src/workflows/workflow-resources.ts +13 -2
  120. package/src/runs/shared/model-exclusions.ts +0 -374
  121. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  122. package/src/runs/shared/readonly-session-evidence.ts +0 -307
@@ -8,7 +8,7 @@ import { parse as parseYaml } from "yaml";
8
8
  import * as os from "node:os";
9
9
  import * as path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
- import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig } from "../shared/types.ts";
11
+ import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, JsonSchemaObject, OutputMode, ToolBudgetConfig } from "../shared/types.ts";
12
12
  import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
13
13
  import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
14
14
  import { expandHomePath } from "../shared/settings.ts";
@@ -24,6 +24,7 @@ import { parseMemoryFrontmatter } from "./agent-memory.ts";
24
24
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
25
25
  import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
26
26
  import { parseThinkingLevel, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
27
+ import { assertJsonSchemaObject } from "../runs/shared/structured-output.ts";
27
28
 
28
29
  export type AgentScope = "user" | "project" | "both";
29
30
 
@@ -52,12 +53,12 @@ export function defaultInheritSkills(): boolean {
52
53
 
53
54
  export interface BuiltinAgentOverrideBase {
54
55
  description?: string;
56
+ machine?: string;
55
57
  output?: string;
56
58
  outputMode?: OutputMode;
57
59
  defaultReads?: string[];
58
60
  model?: string;
59
61
  modelProvider?: string;
60
- fallbackModels?: string[];
61
62
  fast?: boolean;
62
63
  thinking?: string | false;
63
64
  systemPromptMode: SystemPromptMode;
@@ -83,12 +84,12 @@ export interface BuiltinAgentOverrideBase {
83
84
 
84
85
  interface BuiltinAgentOverrideConfig {
85
86
  description?: string;
87
+ machine?: string | false;
86
88
  output?: string | false;
87
89
  outputMode?: OutputMode;
88
90
  defaultReads?: string[] | false;
89
91
  model?: string | false;
90
92
  defaultProvider?: string | false;
91
- fallbackModels?: string[] | false;
92
93
  fast?: boolean;
93
94
  thinking?: string | false;
94
95
  systemPromptMode?: SystemPromptMode;
@@ -143,7 +144,6 @@ export interface AgentConfig {
143
144
  mcpDirectTools?: string[];
144
145
  model?: string;
145
146
  modelProvider?: string;
146
- fallbackModels?: string[];
147
147
  fast?: boolean;
148
148
  thinking?: string | false;
149
149
  systemPromptMode: SystemPromptMode;
@@ -168,6 +168,7 @@ export interface AgentConfig {
168
168
  mutationTools?: string[];
169
169
  output?: string;
170
170
  outputMode?: OutputMode;
171
+ outputSchema?: JsonSchemaObject;
171
172
  defaultReads?: string[];
172
173
  defaultProgress?: boolean;
173
174
  interactive?: boolean;
@@ -176,6 +177,7 @@ export interface AgentConfig {
176
177
  toolBudget?: ToolBudgetConfig;
177
178
  permissions?: PermissionRules;
178
179
  memory?: AgentMemoryConfig;
180
+ machine?: string;
179
181
  disabled?: boolean;
180
182
  extraFields?: Record<string, string>;
181
183
  override?: BuiltinAgentOverrideInfo;
@@ -195,6 +197,7 @@ interface SubagentSettings {
195
197
  overrides: Record<string, BuiltinAgentOverrideConfig>;
196
198
  providerOverrides: Record<string, Record<string, BuiltinAgentOverrideConfig>>;
197
199
  agentScanDirs?: string[];
200
+ agentExcludeDirs?: string[];
198
201
  defaultModel?: string;
199
202
  defaultProvider?: string;
200
203
  defaultThinking?: string;
@@ -215,6 +218,7 @@ export interface ChainStepConfig {
215
218
  label?: string;
216
219
  as?: string;
217
220
  outputSchema?: string | Record<string, unknown>;
221
+ machine?: string;
218
222
  output?: string | false;
219
223
  outputMode?: OutputMode;
220
224
  reads?: string[] | false;
@@ -491,7 +495,7 @@ function getGlobalNpmRoot(): string | null {
491
495
  }
492
496
 
493
497
  try {
494
- cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000, windowsHide: true }).trim());
498
+ cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }).trim());
495
499
  return cachedGlobalNpmRoot;
496
500
  } catch {
497
501
  cachedGlobalNpmRoot = "";
@@ -591,8 +595,8 @@ function collectSettingsPackageRoots(settingsFile: string, baseDir: string): str
591
595
  for (const entry of packages) {
592
596
  const packageSource = typeof entry === "string"
593
597
  ? entry
594
- : typeof entry === "object" && entry !== null && typeof (entry as { source?: unknown }).source === "string"
595
- ? (entry as { source: string }).source
598
+ : typeof entry === "object" && entry !== null && "source" in entry && typeof entry.source === "string"
599
+ ? entry.source
596
600
  : undefined;
597
601
  if (!packageSource) continue;
598
602
  const packageRoot = resolveSettingsPackageRoot(packageSource, baseDir);
@@ -700,12 +704,20 @@ function effectiveAgentMatch(matches: AgentConfig[]): { agent?: AgentConfig; err
700
704
 
701
705
  export function resolveAgentName(name: string, agents: AgentConfig[]): { agent?: AgentConfig; error?: string } {
702
706
  const raw = name.trim();
703
- const exact = agents.filter((agent) => agent.name === raw || agent.localName === raw);
704
- if (exact.length === 1) return exact[0] ? { agent: exact[0] } : {};
705
- if (exact.length > 1) {
706
- const effective = effectiveAgentMatch(exact);
707
+ const canonical = agents.filter((agent) => agent.name === raw);
708
+ if (canonical.length === 1) return canonical[0] ? { agent: canonical[0] } : {};
709
+ if (canonical.length > 1) {
710
+ const effective = effectiveAgentMatch(canonical);
707
711
  if (effective.agent) return effective;
708
- return { error: `Ambiguous agent name '${name}': ${exact.map((agent) => agent.name).join(", ")}` };
712
+ return { error: `Ambiguous agent name '${name}': ${canonical.map((agent) => agent.name).join(", ")}` };
713
+ }
714
+
715
+ const local = agents.filter((agent) => agent.localName === raw);
716
+ if (local.length === 1) return local[0] ? { agent: local[0] } : {};
717
+ if (local.length > 1) {
718
+ const effective = effectiveAgentMatch(local);
719
+ if (effective.agent) return effective;
720
+ return { error: `Ambiguous local agent name '${name}': ${local.map((agent) => agent.name).join(", ")}` };
709
721
  }
710
722
 
711
723
  const aliases = agents.filter((agent) => agent.aliases?.includes(raw));
@@ -755,12 +767,12 @@ function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean
755
767
  function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
756
768
  return {
757
769
  description: agent.description,
770
+ ...(agent.machine !== undefined ? { machine: agent.machine } : {}),
758
771
  ...(agent.output !== undefined ? { output: agent.output } : {}),
759
772
  ...(agent.outputMode !== undefined ? { outputMode: agent.outputMode } : {}),
760
773
  ...(agent.defaultReads !== undefined ? { defaultReads: [...agent.defaultReads] } : {}),
761
774
  ...(agent.model !== undefined ? { model: agent.model } : {}),
762
775
  ...(agent.modelProvider !== undefined ? { modelProvider: agent.modelProvider } : {}),
763
- ...(agent.fallbackModels ? { fallbackModels: [...agent.fallbackModels] } : {}),
764
776
  ...(agent.fast !== undefined ? { fast: agent.fast } : {}),
765
777
  ...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
766
778
  systemPromptMode: agent.systemPromptMode,
@@ -788,14 +800,12 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
788
800
  function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentOverrideConfig {
789
801
  return {
790
802
  ...(override.description !== undefined ? { description: override.description } : {}),
803
+ ...(override.machine !== undefined ? { machine: override.machine } : {}),
791
804
  ...(override.output !== undefined ? { output: override.output } : {}),
792
805
  ...(override.outputMode !== undefined ? { outputMode: override.outputMode } : {}),
793
806
  ...(override.defaultReads !== undefined ? { defaultReads: override.defaultReads === false ? false : [...override.defaultReads] } : {}),
794
807
  ...(override.model !== undefined ? { model: override.model } : {}),
795
808
  ...(override.defaultProvider !== undefined ? { defaultProvider: override.defaultProvider } : {}),
796
- ...(override.fallbackModels !== undefined
797
- ? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
798
- : {}),
799
809
  ...(override.fast !== undefined ? { fast: override.fast } : {}),
800
810
  ...(override.thinking !== undefined ? { thinking: override.thinking } : {}),
801
811
  ...(override.systemPromptMode !== undefined ? { systemPromptMode: override.systemPromptMode } : {}),
@@ -824,8 +834,17 @@ function isProjectRootCandidate(dir: string): boolean {
824
834
 
825
835
  function findProjectRootCandidates(cwd: string): string[] {
826
836
  const roots: string[] = [];
837
+ const windowsProfile = process.env.HOMEDRIVE && process.env.HOMEPATH
838
+ ? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`
839
+ : undefined;
840
+ const homeDirs = new Set([os.homedir(), process.env.HOME, process.env.USERPROFILE, windowsProfile]
841
+ .filter((value): value is string => Boolean(value?.trim()))
842
+ .filter(isDirectory)
843
+ .map((value) => fs.realpathSync.native(value)));
827
844
  let currentDir = cwd;
828
845
  while (true) {
846
+ // ~/.pi and ~/.agents are user configuration, never an implicit project.
847
+ if (isDirectory(currentDir) && homeDirs.has(fs.realpathSync.native(currentDir))) return roots;
829
848
  if (isProjectRootCandidate(currentDir)) roots.push(currentDir);
830
849
 
831
850
  const parentDir = path.dirname(currentDir);
@@ -957,6 +976,15 @@ function parseToolsOverride(
957
976
  throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid 'tools'; expected an array of strings, "inherit", or false.`);
958
977
  }
959
978
 
979
+ function validateOptionalMachine(value: unknown, label: string): string | undefined {
980
+ if (value === undefined || value === false) return undefined;
981
+ if (typeof value !== "string" || !value.trim()) throw new Error(label + " must be a non-empty string or false.");
982
+ const machine = value.trim();
983
+ if (machine.length > 128) throw new Error(label + " must be 128 characters or fewer.");
984
+ if (/[\u0000-\u001f\u007f]/u.test(machine)) throw new Error(label + " contains control characters.");
985
+ return machine;
986
+ }
987
+
960
988
  function parseBuiltinOverrideEntry(
961
989
  name: string,
962
990
  value: unknown,
@@ -967,6 +995,9 @@ function parseBuiltinOverrideEntry(
967
995
  }
968
996
 
969
997
  const input = value as Record<string, unknown>;
998
+ if (Object.hasOwn(input, "fallbackModels")) {
999
+ throw new Error(`Builtin override '${name}' in '${filePath}' uses removed field 'fallbackModels'; configure one model instead.`);
1000
+ }
970
1001
  const override: BuiltinAgentOverrideConfig = {};
971
1002
 
972
1003
  if ("description" in input) {
@@ -1084,11 +1115,15 @@ function parseBuiltinOverrideEntry(
1084
1115
  else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPrompt'; expected a string.`);
1085
1116
  }
1086
1117
 
1118
+ if (input.machine === false) override.machine = false;
1119
+ else {
1120
+ const machine = validateOptionalMachine(input.machine, `Builtin override '${name}' in '${filePath}' field 'machine'`);
1121
+ if (machine !== undefined) override.machine = machine;
1122
+ }
1123
+
1087
1124
  const defaultReads = parseOverrideStringArrayOrFalse(input.defaultReads, { filePath, name, field: "defaultReads" });
1088
1125
  if (defaultReads !== undefined) override.defaultReads = defaultReads;
1089
1126
 
1090
- const fallbackModels = parseOverrideStringArrayOrFalse(input.fallbackModels, { filePath, name, field: "fallbackModels" });
1091
- if (fallbackModels !== undefined) override.fallbackModels = fallbackModels;
1092
1127
 
1093
1128
  if ("defaultProvider" in input) {
1094
1129
  if (input.defaultProvider === false) override.defaultProvider = false;
@@ -1191,6 +1226,14 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
1191
1226
  }
1192
1227
  agentScanDirs = subagentsObject.agentScanDirs.map((item) => item.trim());
1193
1228
  }
1229
+ let agentExcludeDirs: string[] | undefined;
1230
+ if ("agentExcludeDirs" in subagentsObject) {
1231
+ if (!Array.isArray(subagentsObject.agentExcludeDirs)
1232
+ || subagentsObject.agentExcludeDirs.some((item) => typeof item !== "string" || !item.trim())) {
1233
+ throw new Error(`Subagent settings in '${filePath}' have invalid 'agentExcludeDirs'; expected an array of non-empty strings.`);
1234
+ }
1235
+ agentExcludeDirs = subagentsObject.agentExcludeDirs.map((item) => item.trim());
1236
+ }
1194
1237
  const modelScope = parseModelScopeConfig(subagentsObject.modelScope, { filePath });
1195
1238
 
1196
1239
  const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
@@ -1206,6 +1249,7 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
1206
1249
  ...(maxThinking !== undefined ? { maxThinking } : {}),
1207
1250
  ...(defaultExtensions !== undefined ? { defaultExtensions } : {}),
1208
1251
  ...(agentScanDirs !== undefined ? { agentScanDirs } : {}),
1252
+ ...(agentExcludeDirs !== undefined ? { agentExcludeDirs } : {}),
1209
1253
  ...(disableBuiltins !== undefined ? { disableBuiltins } : {}),
1210
1254
  ...(disableThinking !== undefined ? { disableThinking } : {}),
1211
1255
  ...(modelScope !== undefined ? { modelScope } : {}),
@@ -1384,6 +1428,7 @@ function applyBuiltinOverride(
1384
1428
  };
1385
1429
 
1386
1430
  if (override.description !== undefined) next.description = override.description;
1431
+ if (override.machine !== undefined) { if (override.machine === false) delete next.machine; else next.machine = override.machine; }
1387
1432
  if (override.output !== undefined) { if (override.output === false) delete next.output; else next.output = override.output; }
1388
1433
  if (override.outputMode !== undefined) next.outputMode = override.outputMode;
1389
1434
  if (override.defaultReads !== undefined) { if (override.defaultReads === false) delete next.defaultReads; else next.defaultReads = [...override.defaultReads]; }
@@ -1395,7 +1440,6 @@ function applyBuiltinOverride(
1395
1440
  if (override.defaultProvider === false) delete next.modelProvider;
1396
1441
  else next.modelProvider = override.defaultProvider;
1397
1442
  }
1398
- if (override.fallbackModels !== undefined) { if (override.fallbackModels === false) delete next.fallbackModels; else next.fallbackModels = [...override.fallbackModels]; }
1399
1443
  if (override.fast !== undefined) next.fast = override.fast;
1400
1444
  if (override.thinking !== undefined) { if (override.thinking === false) delete next.thinking; else next.thinking = override.thinking; }
1401
1445
  if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
@@ -1516,9 +1560,10 @@ function applyCustomAgentOverrides(
1516
1560
 
1517
1561
  export function buildBuiltinOverrideConfig(
1518
1562
  base: BuiltinAgentOverrideBase,
1519
- draft: Pick<AgentConfig, "model" | "modelProvider" | "fallbackModels" | "fast" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "allowNestedSubagents" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "mutationTools" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "output" | "outputMode" | "defaultReads" | "excludeTools">>,
1563
+ draft: Pick<AgentConfig, "model" | "modelProvider" | "fast" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "allowNestedSubagents" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "mutationTools" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "machine" | "output" | "outputMode" | "defaultReads" | "excludeTools">>,
1520
1564
  ): BuiltinAgentOverrideConfig | undefined {
1521
1565
  const override: BuiltinAgentOverrideConfig = {};
1566
+ if (draft.machine !== base.machine) override.machine = draft.machine ?? false;
1522
1567
 
1523
1568
  if (draft.description !== undefined) {
1524
1569
  const description = draft.description.trim();
@@ -1529,7 +1574,6 @@ export function buildBuiltinOverrideConfig(
1529
1574
  if (!arraysEqual(draft.defaultReads, base.defaultReads)) override.defaultReads = draft.defaultReads ? [...draft.defaultReads] : false;
1530
1575
  if (draft.model !== base.model) override.model = draft.model ?? false;
1531
1576
  if (draft.modelProvider !== base.modelProvider) override.defaultProvider = draft.modelProvider ?? false;
1532
- if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
1533
1577
  if (draft.fast !== base.fast) override.fast = draft.fast === true;
1534
1578
  if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
1535
1579
  if (draft.systemPromptMode !== base.systemPromptMode) override.systemPromptMode = draft.systemPromptMode;
@@ -1584,21 +1628,25 @@ export function saveBuiltinAgentOverride(
1584
1628
  return filePath;
1585
1629
  }
1586
1630
 
1587
- export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "user" | "project"): { path: string; removed: boolean } {
1631
+ export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "user" | "project", options?: { preserveMachine?: boolean }): { path: string; removed: boolean; machinePreserved: boolean } {
1588
1632
  const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath();
1589
1633
  if (!filePath) throw new Error("Project override is not available here. No project config root was found.");
1590
- if (!fs.existsSync(filePath)) return { path: filePath, removed: false };
1634
+ if (!fs.existsSync(filePath)) return { path: filePath, removed: false, machinePreserved: false };
1591
1635
 
1592
1636
  const settings = readSettingsFileStrict(filePath);
1593
1637
  const subagents = settings.subagents;
1594
- if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return { path: filePath, removed: false };
1638
+ if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return { path: filePath, removed: false, machinePreserved: false };
1595
1639
  const nextSubagents = { ...(subagents as Record<string, unknown>) };
1596
1640
  const agentOverrides = nextSubagents.agentOverrides;
1597
- if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return { path: filePath, removed: false };
1641
+ if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return { path: filePath, removed: false, machinePreserved: false };
1598
1642
 
1599
1643
  const nextOverrides = { ...(agentOverrides as Record<string, unknown>) };
1600
- if (!Object.prototype.hasOwnProperty.call(nextOverrides, name)) return { path: filePath, removed: false };
1601
- delete nextOverrides[name];
1644
+ const current = nextOverrides[name];
1645
+ if (!Object.prototype.hasOwnProperty.call(nextOverrides, name)) return { path: filePath, removed: false, machinePreserved: false };
1646
+ const configuredMachine = current && typeof current === "object" && !Array.isArray(current) ? (current as Record<string, unknown>).machine : undefined;
1647
+ const machine = configuredMachine === false || (typeof configuredMachine === "string" && configuredMachine.trim()) ? configuredMachine : undefined;
1648
+ if (options?.preserveMachine && machine !== undefined) nextOverrides[name] = { machine };
1649
+ else delete nextOverrides[name];
1602
1650
  if (Object.keys(nextOverrides).length > 0) nextSubagents.agentOverrides = nextOverrides;
1603
1651
  else delete nextSubagents.agentOverrides;
1604
1652
 
@@ -1606,7 +1654,7 @@ export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "us
1606
1654
  else delete settings.subagents;
1607
1655
 
1608
1656
  writeSettingsFile(filePath, settings);
1609
- return { path: filePath, removed: true };
1657
+ return { path: filePath, removed: true, machinePreserved: options?.preserveMachine === true && machine !== undefined };
1610
1658
  }
1611
1659
 
1612
1660
  export function mergeBuiltinAgentOverride(
@@ -1778,8 +1826,9 @@ const DEFAULT_AGENT_DEFINITION_INSPECTION_FS: AgentDefinitionInspectionFs = {
1778
1826
  * state. A nested unreadable directory makes the whole inspection unavailable,
1779
1827
  * preventing a partial traversal from being reported as empty or complete.
1780
1828
  */
1781
- export function inspectAgentDefinitionDirectory(dir: string, operations: AgentDefinitionInspectionFs = DEFAULT_AGENT_DEFINITION_INSPECTION_FS): AgentDefinitionInspection {
1829
+ export function inspectAgentDefinitionDirectory(dir: string, operations: AgentDefinitionInspectionFs = DEFAULT_AGENT_DEFINITION_INSPECTION_FS, isExcluded: (filePath: string) => boolean = () => false): AgentDefinitionInspection {
1782
1830
  const root = path.resolve(dir);
1831
+ if (isExcluded(root)) return rememberAgentDefinitionInspection({ files: [], state: "empty" }, []);
1783
1832
  try {
1784
1833
  if (!operations.existsSync(root)) return rememberAgentDefinitionInspection({ files: [], state: "absent" }, [root]);
1785
1834
  if (!operations.statSync(root).isDirectory()) return rememberAgentDefinitionInspection({ files: [], state: "not-directory" }, [root]);
@@ -1811,6 +1860,7 @@ export function inspectAgentDefinitionDirectory(dir: string, operations: AgentDe
1811
1860
  }
1812
1861
  for (const entry of entries) {
1813
1862
  const filePath = path.join(current, entry.name);
1863
+ if (isExcluded(filePath)) continue;
1814
1864
  let isDirectory = entry.isDirectory();
1815
1865
  if (entry.isSymbolicLink()) {
1816
1866
  try {
@@ -1910,7 +1960,7 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
1910
1960
 
1911
1961
  function validateExternalRunnerProfile(frontmatter: Record<string, string>, agentName: string, runner: AgentRunnerConfig | undefined): void {
1912
1962
  if (runner?.type !== "external-cli" && runner?.type !== "external-job") return;
1913
- const unsupported = ["tools", "excludeTools", "allowNestedSubagents", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "mutationTools", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
1963
+ const unsupported = ["tools", "excludeTools", "allowNestedSubagents", "model", "thinking", "extensions", "subagentOnlyExtensions", "mutationTools", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
1914
1964
  .filter((field) => frontmatter[field] !== undefined);
1915
1965
  if (unsupported.length > 0) {
1916
1966
  throw new Error(`Agent '${agentName}' uses runner.type='${runner.type}' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
@@ -2006,7 +2056,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2006
2056
  const skillStr = frontmatter.skill || frontmatter.skills;
2007
2057
  const skills = parseFrontmatterList(skillStr);
2008
2058
  const skillPath = parseFrontmatterList(frontmatter.skillPath);
2009
- const fallbackModels = parseFrontmatterList(frontmatter.fallbackModels);
2059
+ if (frontmatter.fallbackModels !== undefined) throw new Error(`Agent '${filePath}' uses removed frontmatter field 'fallbackModels'. Configure one model instead.`);
2010
2060
  const systemPromptMode = frontmatter.systemPromptMode === "replace"
2011
2061
  ? "replace"
2012
2062
  : frontmatter.systemPromptMode === "append"
@@ -2099,6 +2149,12 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2099
2149
  }
2100
2150
  toolBudget = parsed as ToolBudgetConfig;
2101
2151
  }
2152
+ let outputSchema: JsonSchemaObject | undefined;
2153
+ if (frontmatter.outputSchema !== undefined && frontmatter.outputSchema.trim()) {
2154
+ const parsed: unknown = JSON.parse(frontmatter.outputSchema);
2155
+ assertJsonSchemaObject(parsed, `Agent '${localName}' outputSchema`);
2156
+ outputSchema = parsed;
2157
+ }
2102
2158
  const completionGuard = frontmatter.completionGuard === "false"
2103
2159
  ? false
2104
2160
  : frontmatter.completionGuard === "true"
@@ -2109,6 +2165,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2109
2165
  ? parsedMaxSubagentDepth
2110
2166
  : undefined;
2111
2167
  const memory = parseMemoryFrontmatter(frontmatter.memory);
2168
+ const machine = validateOptionalMachine(frontmatter.machine, `Agent '${runtimeName}' frontmatter 'machine'`);
2112
2169
  const agent: AgentConfig = {
2113
2170
  name: runtimeName,
2114
2171
  ...(runner !== undefined ? { runner } : {}),
@@ -2125,7 +2182,6 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2125
2182
  ...(allowNestedSubagents !== undefined ? { allowNestedSubagents } : {}),
2126
2183
  ...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),
2127
2184
  ...(frontmatter.model !== undefined ? { model: frontmatter.model } : {}),
2128
- ...(fallbackModels?.length ? { fallbackModels } : {}),
2129
2185
  ...(fast !== undefined ? { fast } : {}),
2130
2186
  ...(frontmatter.thinking !== undefined ? { thinking: frontmatter.thinking === "false" ? false : frontmatter.thinking } : {}),
2131
2187
  systemPromptMode,
@@ -2147,8 +2203,10 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
2147
2203
  ...(extensions !== undefined ? { extensions } : {}),
2148
2204
  ...(subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions } : {}),
2149
2205
  ...(mutationTools?.length ? { mutationTools } : {}),
2206
+ ...(machine !== undefined ? { machine } : {}),
2150
2207
  ...(frontmatter.output !== undefined ? { output: frontmatter.output } : {}),
2151
2208
  ...(outputMode !== undefined ? { outputMode } : {}),
2209
+ ...(outputSchema !== undefined ? { outputSchema } : {}),
2152
2210
  ...(defaultReads?.length ? { defaultReads } : {}),
2153
2211
  defaultProgress: frontmatter.defaultProgress === "true",
2154
2212
  interactive: frontmatter.interactive === "true",
@@ -2268,31 +2326,60 @@ interface AgentScanDirs {
2268
2326
  watchPaths: string[];
2269
2327
  }
2270
2328
 
2271
- function readConfiguredAgentScanDirs(filePath: string | null): string[] {
2329
+ function readConfiguredAgentScanDirs(filePath: string | null, key: "agentScanDirs" | "agentExcludeDirs" = "agentScanDirs"): string[] {
2272
2330
  if (!filePath) return [];
2273
2331
  try {
2274
2332
  const settings = readSettingsFileStrict(filePath);
2275
2333
  const subagents = settings.subagents;
2276
2334
  if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return [];
2277
- const dirs = (subagents as { agentScanDirs?: unknown }).agentScanDirs;
2335
+ const dirs = (subagents as Record<string, unknown>)[key];
2278
2336
  return Array.isArray(dirs) ? dirs.filter((dir): dir is string => typeof dir === "string" && dir.trim().length > 0) : [];
2279
2337
  } catch {
2280
2338
  return [];
2281
2339
  }
2282
2340
  }
2283
2341
 
2284
- function expandAgentScanDirPattern(pattern: string): AgentScanDirs {
2342
+ // Resolve existing ancestors to canonicalize missing descendants of symlinks.
2343
+ function canonicalAgentPath(filePath: string): string {
2344
+ const resolved = path.resolve(filePath);
2345
+ try {
2346
+ return fs.realpathSync(resolved);
2347
+ } catch {
2348
+ const parent = path.dirname(resolved);
2349
+ return parent === resolved ? resolved : path.join(canonicalAgentPath(parent), path.basename(resolved));
2350
+ }
2351
+ }
2352
+
2353
+ function agentExclusionRoots(userSettingsPath: string, projectSettingsPath: string | null): Array<{ resolved: string; real: string }> {
2354
+ return [userSettingsPath, projectSettingsPath].flatMap((settingsPath) => settingsPath
2355
+ ? readConfiguredAgentScanDirs(settingsPath, "agentExcludeDirs").map((entry) => {
2356
+ const resolved = path.resolve(path.dirname(settingsPath), expandHomePath(entry.trim()).replace(/[\\/]+/g, path.sep));
2357
+ return { resolved, real: canonicalAgentPath(resolved) };
2358
+ }) : []);
2359
+ }
2360
+
2361
+ function agentExclusions(roots: Array<{ resolved: string; real: string }>): (filePath: string) => boolean {
2362
+ return (filePath) => {
2363
+ if (!roots.length) return false;
2364
+ if (roots.some((root) => isPathWithin(root.resolved, filePath))) return true;
2365
+ const real = canonicalAgentPath(filePath);
2366
+ return roots.some((root) => isPathWithin(root.real, real));
2367
+ };
2368
+ }
2369
+
2370
+ function expandAgentScanDirPattern(pattern: string, isExcluded: (filePath: string) => boolean): AgentScanDirs {
2285
2371
  const expanded = expandHomePath(pattern.trim()).replace(/[\\/]+/g, path.sep);
2286
2372
  if (!expanded) return { dirs: [], watchPaths: [] };
2287
2373
  const wildcardMatches = [...expanded.matchAll(/\*/g)];
2288
2374
  if (wildcardMatches.length === 0) {
2289
2375
  const dir = path.resolve(expanded);
2290
- return { dirs: fs.existsSync(dir) ? [dir] : [], watchPaths: [dir] };
2376
+ return { dirs: !isExcluded(dir) && fs.existsSync(dir) ? [dir] : [], watchPaths: [dir] };
2291
2377
  }
2292
2378
  const parts = expanded.split(path.sep);
2293
2379
  const wildcardIndex = parts.findIndex((part) => part.includes("*"));
2294
2380
  if (wildcardMatches.length !== 1 || wildcardIndex === -1 || parts[wildcardIndex] !== "*") return { dirs: [], watchPaths: [] };
2295
2381
  const base = path.resolve(parts.slice(0, wildcardIndex).join(path.sep) || path.sep);
2382
+ if (isExcluded(base)) return { dirs: [], watchPaths: [base] };
2296
2383
  const rest = parts.slice(wildcardIndex + 1);
2297
2384
  let entries: fs.Dirent[];
2298
2385
  try {
@@ -2302,16 +2389,16 @@ function expandAgentScanDirPattern(pattern: string): AgentScanDirs {
2302
2389
  }
2303
2390
  const candidateDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => path.join(base, entry.name, ...rest));
2304
2391
  return {
2305
- dirs: candidateDirs.filter((dir) => fs.existsSync(dir)),
2392
+ dirs: candidateDirs.filter((dir) => !isExcluded(dir) && fs.existsSync(dir)),
2306
2393
  watchPaths: [base, ...candidateDirs],
2307
2394
  };
2308
2395
  }
2309
2396
 
2310
- function settingsAgentScanDirs(entries: string[]): AgentScanDirs {
2397
+ function settingsAgentScanDirs(entries: string[], isExcluded: (filePath: string) => boolean): AgentScanDirs {
2311
2398
  const dirs = new Set<string>();
2312
2399
  const watchPaths = new Set<string>();
2313
2400
  for (const entry of entries) {
2314
- const expanded = expandAgentScanDirPattern(entry);
2401
+ const expanded = expandAgentScanDirPattern(entry, isExcluded);
2315
2402
  for (const dir of expanded.dirs) dirs.add(dir);
2316
2403
  for (const watchPath of expanded.watchPaths) watchPaths.add(watchPath);
2317
2404
  }
@@ -2378,6 +2465,10 @@ interface AgentDiscoverySources {
2378
2465
  userChains?: ReturnType<typeof loadChainsFromDir>;
2379
2466
  projectChainLoaded?: Array<{ dir: string; loaded: ReturnType<typeof loadChainsFromDir> }>;
2380
2467
  packageChainLoaded?: Array<{ entry: PackageChainPath; loaded: ReturnType<typeof loadChainsFromDir> }>;
2468
+ isExcluded: (filePath: string) => boolean;
2469
+ exclusionRoots: Array<{ resolved: string; real: string }>;
2470
+ identityWatchPaths: string[];
2471
+ chainWatchPaths?: Set<string>;
2381
2472
  watchPaths: string[];
2382
2473
  }
2383
2474
 
@@ -2390,7 +2481,7 @@ const agentDiscoveryCache = new Map<string, AgentDiscoveryCacheEntry>();
2390
2481
 
2391
2482
  function isPathWithin(root: string, candidate: string): boolean {
2392
2483
  const relative = path.relative(path.resolve(root), path.resolve(candidate));
2393
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
2484
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
2394
2485
  }
2395
2486
 
2396
2487
  function addDirectoryWatchPaths(paths: Set<string>, root: string, files: readonly string[], directories: readonly string[] = []): void {
@@ -2413,13 +2504,14 @@ function addDirectoryWatchPaths(paths: Set<string>, root: string, files: readonl
2413
2504
  }
2414
2505
  }
2415
2506
 
2416
- function watchPathSignature(filePath: string): string {
2507
+ function watchPathSignature(filePath: string, isExcluded?: (filePath: string) => boolean): string {
2417
2508
  try {
2418
2509
  const stat = fs.statSync(filePath);
2419
2510
  if (stat.isDirectory()) {
2420
2511
  const entries = fs.readdirSync(filePath, { withFileTypes: true })
2512
+ .filter((entry) => !isExcluded || !isExcluded(path.join(filePath, entry.name)))
2421
2513
  .sort((left, right) => left.name.localeCompare(right.name))
2422
- .map((entry) => `${entry.name}:${entry.isDirectory() ? "d" : entry.isSymbolicLink() ? "l" : "f"}`)
2514
+ .map((entry) => `${entry.name}:${entry.isDirectory() ? "d" : entry.isSymbolicLink() ? (isExcluded ? `l:${canonicalAgentPath(path.join(filePath, entry.name))}` : "l") : "f"}`)
2423
2515
  .join("|");
2424
2516
  return `directory:${entries}`;
2425
2517
  }
@@ -2429,10 +2521,17 @@ function watchPathSignature(filePath: string): string {
2429
2521
  }
2430
2522
  }
2431
2523
 
2432
- function discoveryFingerprint(paths: readonly string[]): string {
2433
- return [...new Set(paths)]
2524
+ function discoveryFingerprint(sources: AgentDiscoverySources): string {
2525
+ const exclusionIdentity = JSON.stringify(sources.exclusionRoots.map((root) => ({ resolved: root.resolved, real: canonicalAgentPath(root.resolved) })));
2526
+ // A retargeted exclusion needs a fresh scan before fingerprinting old roots.
2527
+ if (exclusionIdentity !== JSON.stringify(sources.exclusionRoots)) return exclusionIdentity;
2528
+ const rootIdentities = JSON.stringify(sources.identityWatchPaths.map((root) => [root, canonicalAgentPath(root)]));
2529
+ // Package metadata can redirect agents outside an excluded tree without chains.
2530
+ const unfilteredPaths = new Set([...sources.packageSubagentPaths.watchPaths, ...(sources.chainWatchPaths ?? [])]);
2531
+ return exclusionIdentity + "\n" + rootIdentities + "\n" + [...new Set([...sources.watchPaths, ...unfilteredPaths])]
2532
+ .filter((filePath) => unfilteredPaths.has(filePath) || filePath === sources.userSettingsPath || filePath === sources.projectSettingsPath || !sources.isExcluded(filePath))
2434
2533
  .sort((left, right) => left.localeCompare(right))
2435
- .map((filePath) => `${filePath}:${watchPathSignature(filePath)}`)
2534
+ .map((filePath) => `${filePath}:${watchPathSignature(filePath, sources.exclusionRoots.length && !unfilteredPaths.has(filePath) ? sources.isExcluded : undefined)}`)
2436
2535
  .join("\n");
2437
2536
  }
2438
2537
 
@@ -2478,21 +2577,23 @@ function buildAgentDiscoverySources(cwd: string, preferredModelProvider?: string
2478
2577
  const userSettingsPath = getUserAgentSettingsPath();
2479
2578
  const projectSettingsPath = getProjectAgentSettingsPath(effectiveCwd);
2480
2579
  const packageSubagentPaths = collectPackageSubagentPaths(effectiveCwd);
2481
- const userScanDirs = settingsAgentScanDirs(readConfiguredAgentScanDirs(userSettingsPath));
2482
- const projectScanDirs = settingsAgentScanDirs(readConfiguredAgentScanDirs(projectSettingsPath));
2580
+ const exclusionRoots = agentExclusionRoots(userSettingsPath, projectSettingsPath);
2581
+ const isExcluded = agentExclusions(exclusionRoots);
2582
+ const userScanDirs = settingsAgentScanDirs(readConfiguredAgentScanDirs(userSettingsPath), isExcluded);
2583
+ const projectScanDirs = settingsAgentScanDirs(readConfiguredAgentScanDirs(projectSettingsPath), isExcluded);
2483
2584
 
2484
2585
  const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
2485
- const userLoaded = [...extraUserAgentDirs(), ...userScanDirs.dirs, userDirOld, userDirNew].map((dir, discoveryPriority): LoadedAgentDirectory => {
2486
- const inspection = inspectAgentDefinitionDirectory(dir);
2586
+ const userLoaded = [...extraUserAgentDirs(), ...userScanDirs.dirs, userDirOld, userDirNew].filter((dir) => !isExcluded(dir)).map((dir, discoveryPriority): LoadedAgentDirectory => {
2587
+ const inspection = inspectAgentDefinitionDirectory(dir, undefined, isExcluded);
2487
2588
  return { dir, inspection, loaded: loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection) };
2488
2589
  });
2489
- const projectInspections = new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
2490
- const projectLoaded = [...projectScanDirs.dirs, ...projectAgentDirs].map((dir, discoveryPriority): LoadedAgentDirectory => {
2491
- const inspection = projectInspections.get(dir) ?? inspectAgentDefinitionDirectory(dir);
2590
+ const projectInspections = new Map(projectCandidateDirs.filter((dir) => !isExcluded(dir)).map((dir) => [dir, inspectAgentDefinitionDirectory(dir, undefined, isExcluded)]));
2591
+ const projectLoaded = [...projectScanDirs.dirs, ...projectAgentDirs].filter((dir) => !isExcluded(dir)).map((dir, discoveryPriority): LoadedAgentDirectory => {
2592
+ const inspection = projectInspections.get(dir) ?? inspectAgentDefinitionDirectory(dir, undefined, isExcluded);
2492
2593
  return { dir, inspection, loaded: loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : discoveryPriority, undefined, inspection) };
2493
2594
  });
2494
- const packageLoaded = packageSubagentPaths.agents.map((entry, index): LoadedAgentDirectory => {
2495
- const inspection = inspectAgentDefinitionDirectory(entry.dir);
2595
+ const packageLoaded = packageSubagentPaths.agents.filter((entry) => !isExcluded(entry.dir)).map((entry, index): LoadedAgentDirectory => {
2596
+ const inspection = inspectAgentDefinitionDirectory(entry.dir, undefined, isExcluded);
2496
2597
  return { dir: entry.dir, inspection, packageEntry: entry, loaded: loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry, inspection) };
2497
2598
  });
2498
2599
 
@@ -2522,7 +2623,7 @@ function buildAgentDiscoverySources(cwd: string, preferredModelProvider?: string
2522
2623
  userDirNew,
2523
2624
  userChainDir,
2524
2625
  projectAgentDirs,
2525
- projectCandidateDirs,
2626
+ projectCandidateDirs: projectCandidateDirs.filter((dir) => !isExcluded(dir)),
2526
2627
  projectAgentsDir,
2527
2628
  projectChainDirs,
2528
2629
  projectChainDir,
@@ -2534,7 +2635,17 @@ function buildAgentDiscoverySources(cwd: string, preferredModelProvider?: string
2534
2635
  projectLoaded,
2535
2636
  projectInspections,
2536
2637
  packageLoaded,
2537
- watchPaths: [...watchPaths],
2638
+ isExcluded,
2639
+ exclusionRoots,
2640
+ // Aliases outside excluded lexical subtrees can be retargeted into allowed
2641
+ // storage. Watch only their identity, never the excluded target's contents.
2642
+ identityWatchPaths: [...new Set([
2643
+ ...userScanDirs.watchPaths, ...projectScanDirs.watchPaths,
2644
+ ...extraUserAgentDirs(), userDirOld, userDirNew, ...projectCandidateDirs,
2645
+ ...packageSubagentPaths.agents.map((entry) => entry.dir),
2646
+ ])]
2647
+ .filter((dir) => !exclusionRoots.some((root) => isPathWithin(root.resolved, dir)) && isExcluded(dir)),
2648
+ watchPaths: [...watchPaths].filter((filePath) => !isExcluded(filePath) || filePath === userSettingsPath || filePath === projectSettingsPath),
2538
2649
  };
2539
2650
  }
2540
2651
 
@@ -2543,29 +2654,30 @@ function ensureDiscoveryChains(sources: AgentDiscoverySources): void {
2543
2654
  sources.packageChainLoaded = sources.packageSubagentPaths.chains.map((entry) => ({ entry, loaded: loadChainsFromDir(entry.dir, "package") }));
2544
2655
  sources.userChains = loadChainsFromDir(sources.userChainDir, "user");
2545
2656
  sources.projectChainLoaded = sources.projectChainDirs.map((dir) => ({ dir, loaded: loadChainsFromDir(dir, "project") }));
2546
- const watchPaths = new Set(sources.watchPaths);
2657
+ const watchPaths = new Set(sources.packageSubagentPaths.watchPaths);
2547
2658
  addDirectoryWatchPaths(watchPaths, sources.userChainDir, sources.userChains.files, sources.userChains.directories);
2548
2659
  for (const chain of sources.projectChainLoaded) addDirectoryWatchPaths(watchPaths, chain.dir, chain.loaded.files, chain.loaded.directories);
2549
2660
  for (const chain of sources.packageChainLoaded) addDirectoryWatchPaths(watchPaths, chain.entry.dir, chain.loaded.files, chain.loaded.directories);
2550
- sources.watchPaths = [...watchPaths];
2661
+ sources.chainWatchPaths = watchPaths;
2662
+ sources.watchPaths = [...new Set([...sources.watchPaths, ...watchPaths])];
2551
2663
  }
2552
2664
 
2553
2665
  function getAgentDiscoverySources(cwd: string, preferredModelProvider?: string, includeChains = false): AgentDiscoverySources {
2554
2666
  const key = discoveryCacheKey(cwd, preferredModelProvider);
2555
2667
  const cached = agentDiscoveryCache.get(key);
2556
- if (cached && cached.fingerprint === discoveryFingerprint(cached.sources.watchPaths)) {
2668
+ if (cached && cached.fingerprint === discoveryFingerprint(cached.sources)) {
2557
2669
  if (includeChains) {
2558
2670
  ensureDiscoveryChains(cached.sources);
2559
- cached.fingerprint = discoveryFingerprint(cached.sources.watchPaths);
2671
+ cached.fingerprint = discoveryFingerprint(cached.sources);
2560
2672
  }
2561
2673
  return cached.sources;
2562
2674
  }
2563
2675
  const sources = buildAgentDiscoverySources(cwd, preferredModelProvider);
2564
- const entry = { sources, fingerprint: discoveryFingerprint(sources.watchPaths) };
2676
+ const entry = { sources, fingerprint: discoveryFingerprint(sources) };
2565
2677
  agentDiscoveryCache.set(key, entry);
2566
2678
  if (includeChains) {
2567
2679
  ensureDiscoveryChains(sources);
2568
- entry.fingerprint = discoveryFingerprint(sources.watchPaths);
2680
+ entry.fingerprint = discoveryFingerprint(sources);
2569
2681
  }
2570
2682
  return sources;
2571
2683
  }
@@ -2761,27 +2873,28 @@ function discoverAgentsUncached(cwd: string, scope: AgentScope, preferredModelPr
2761
2873
  const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
2762
2874
  const modelScope = projectSettings.modelScope ?? userSettings.modelScope;
2763
2875
  const packageSubagentPaths = collectPackageSubagentPaths(effectiveCwd, { includeUser: scope !== "project", includeProject: scope !== "user" });
2876
+ const isExcluded = agentExclusions(agentExclusionRoots(userSettingsPath, projectSettingsPath));
2764
2877
  const directories: AgentDefinitionDirectoryReport[] = [reportAgentDefinitionDirectory("builtin", BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION)];
2765
2878
  const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
2766
2879
  const builtinAgents = applyBuiltinOverrides(applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2767
- const userScanDirs = settingsAgentScanDirs(userSettings.agentScanDirs ?? []);
2768
- const projectScanDirs = settingsAgentScanDirs(projectSettings.agentScanDirs ?? []);
2769
- const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), ...userScanDirs.dirs, userDirOld, userDirNew].map((dir, discoveryPriority) => {
2770
- const inspection = inspectAgentDefinitionDirectory(dir);
2880
+ const userScanDirs = settingsAgentScanDirs(userSettings.agentScanDirs ?? [], isExcluded);
2881
+ const projectScanDirs = settingsAgentScanDirs(projectSettings.agentScanDirs ?? [], isExcluded);
2882
+ const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), ...userScanDirs.dirs, userDirOld, userDirNew].filter((dir) => !isExcluded(dir)).map((dir, discoveryPriority) => {
2883
+ const inspection = inspectAgentDefinitionDirectory(dir, undefined, isExcluded);
2771
2884
  directories.push(reportAgentDefinitionDirectory("user", dir, inspection));
2772
2885
  return loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection);
2773
2886
  });
2774
2887
  const userAgents = applyCustomAgentOverrides(applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2775
- const projectInspections = scope === "user" ? new Map<string, AgentDefinitionInspection>() : new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
2776
- if (scope !== "user") for (const dir of projectCandidateDirs) directories.push(reportAgentDefinitionDirectory("project", dir, projectInspections.get(dir)!));
2777
- const projectLoaded = scope === "user" ? [] : [...projectScanDirs.dirs, ...projectAgentDirs].map((dir, discoveryPriority) => {
2778
- const inspection = projectInspections.get(dir) ?? inspectAgentDefinitionDirectory(dir);
2888
+ const projectInspections = scope === "user" ? new Map<string, AgentDefinitionInspection>() : new Map(projectCandidateDirs.filter((dir) => !isExcluded(dir)).map((dir) => [dir, inspectAgentDefinitionDirectory(dir, undefined, isExcluded)]));
2889
+ if (scope !== "user") for (const [dir, inspection] of projectInspections) directories.push(reportAgentDefinitionDirectory("project", dir, inspection));
2890
+ const projectLoaded = scope === "user" ? [] : [...projectScanDirs.dirs, ...projectAgentDirs].filter((dir) => !isExcluded(dir)).map((dir, discoveryPriority) => {
2891
+ const inspection = projectInspections.get(dir) ?? inspectAgentDefinitionDirectory(dir, undefined, isExcluded);
2779
2892
  if (!projectInspections.has(dir)) directories.push(reportAgentDefinitionDirectory("project", dir, inspection));
2780
2893
  return loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : discoveryPriority, undefined, inspection);
2781
2894
  });
2782
2895
  const projectAgents = applyCustomAgentOverrides(applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions), userSettings, projectSettings, userSettingsPath, projectSettingsPath);
2783
- const packageLoaded = packageSubagentPaths.agents.map((entry, index) => {
2784
- const inspection = inspectAgentDefinitionDirectory(entry.dir);
2896
+ const packageLoaded = packageSubagentPaths.agents.filter((entry) => !isExcluded(entry.dir)).map((entry, index) => {
2897
+ const inspection = inspectAgentDefinitionDirectory(entry.dir, undefined, isExcluded);
2785
2898
  directories.push(reportAgentDefinitionDirectory("package", entry.dir, inspection));
2786
2899
  return loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry, inspection);
2787
2900
  });