pi-subagents 0.30.0 → 0.31.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 (39) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +116 -17
  3. package/agents/context-builder.md +3 -3
  4. package/agents/planner.md +1 -1
  5. package/agents/researcher.md +1 -1
  6. package/agents/scout.md +1 -1
  7. package/package.json +7 -7
  8. package/skills/pi-subagents/SKILL.md +5 -0
  9. package/src/agents/agent-management.ts +170 -6
  10. package/src/agents/agent-serializer.ts +31 -13
  11. package/src/agents/agents.ts +207 -23
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/skills.ts +117 -20
  14. package/src/extension/doctor.ts +20 -0
  15. package/src/extension/fanout-child.ts +1 -0
  16. package/src/extension/index.ts +47 -4
  17. package/src/extension/schemas.ts +10 -76
  18. package/src/intercom/intercom-bridge.ts +2 -3
  19. package/src/runs/background/async-execution.ts +14 -4
  20. package/src/runs/background/async-job-tracker.ts +56 -11
  21. package/src/runs/background/result-watcher.ts +11 -2
  22. package/src/runs/background/stale-run-reconciler.ts +9 -4
  23. package/src/runs/background/subagent-runner.ts +79 -3
  24. package/src/runs/foreground/chain-execution.ts +26 -2
  25. package/src/runs/foreground/execution.ts +113 -8
  26. package/src/runs/foreground/subagent-executor.ts +325 -77
  27. package/src/runs/shared/acceptance.ts +285 -34
  28. package/src/runs/shared/completion-guard.ts +1 -1
  29. package/src/runs/shared/dynamic-fanout.ts +4 -2
  30. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  31. package/src/runs/shared/parallel-utils.ts +6 -1
  32. package/src/runs/shared/pi-args.ts +9 -1
  33. package/src/runs/shared/single-output.ts +15 -1
  34. package/src/shared/settings.ts +1 -0
  35. package/src/shared/types.ts +8 -2
  36. package/src/shared/utils.ts +19 -1
  37. package/src/slash/prompt-template-bridge.ts +26 -3
  38. package/src/slash/slash-commands.ts +33 -3
  39. package/src/tui/render.ts +265 -13
@@ -30,8 +30,14 @@ function joinComma(values: string[] | undefined): string | undefined {
30
30
  return values.join(", ");
31
31
  }
32
32
 
33
- export function serializeAgent(config: AgentConfig): string {
33
+ interface SerializeAgentOptions {
34
+ preserveFrontmatterFields?: ReadonlySet<string>;
35
+ }
36
+
37
+ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptions = {}): string {
34
38
  const lines: string[] = [];
39
+ const preserve = (...fields: string[]) => fields.some((field) => options.preserveFrontmatterFields?.has(field));
40
+ const preservingExistingFrontmatter = options.preserveFrontmatterFields !== undefined;
35
41
  lines.push("---");
36
42
  lines.push(`name: ${frontmatterNameForConfig(config)}`);
37
43
  if (config.packageName) lines.push(`package: ${config.packageName}`);
@@ -42,25 +48,27 @@ export function serializeAgent(config: AgentConfig): string {
42
48
  ...(config.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`),
43
49
  ];
44
50
  const toolsValue = joinComma(tools);
45
- if (toolsValue) lines.push(`tools: ${toolsValue}`);
51
+ if (toolsValue || preserve("tools")) lines.push(`tools: ${toolsValue ?? ""}`);
46
52
 
47
- if (config.model) lines.push(`model: ${config.model}`);
53
+ if (config.model || preserve("model")) lines.push(`model: ${config.model ?? ""}`);
48
54
  const fallbackModelsValue = joinComma(config.fallbackModels);
49
- if (fallbackModelsValue) lines.push(`fallbackModels: ${fallbackModelsValue}`);
50
- if (config.thinking && config.thinking !== "off") lines.push(`thinking: ${config.thinking}`);
51
- lines.push(`systemPromptMode: ${config.systemPromptMode}`);
52
- lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
53
- lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
54
- if (config.defaultContext) lines.push(`defaultContext: ${config.defaultContext}`);
55
+ if (fallbackModelsValue || preserve("fallbackModels")) lines.push(`fallbackModels: ${fallbackModelsValue ?? ""}`);
56
+ if ((config.thinking && (config.thinking !== "off" || preserve("thinking"))) || (!config.thinking && preserve("thinking"))) {
57
+ lines.push(`thinking: ${config.thinking ?? ""}`);
58
+ }
59
+ if (!preservingExistingFrontmatter || preserve("systemPromptMode")) lines.push(`systemPromptMode: ${config.systemPromptMode}`);
60
+ if (!preservingExistingFrontmatter || preserve("inheritProjectContext")) lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
61
+ if (!preservingExistingFrontmatter || preserve("inheritSkills")) lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
62
+ if (config.defaultContext || preserve("defaultContext")) lines.push(`defaultContext: ${config.defaultContext ?? ""}`);
55
63
 
56
64
  const skillsValue = joinComma(config.skills);
57
- if (skillsValue) lines.push(`skills: ${skillsValue}`);
65
+ if (skillsValue || preserve("skill", "skills")) lines.push(`skills: ${skillsValue ?? ""}`);
58
66
 
59
67
  if (config.extensions !== undefined) {
60
68
  const extensionsValue = joinComma(config.extensions);
61
69
  lines.push(`extensions: ${extensionsValue ?? ""}`);
62
70
  }
63
- if (config.subagentOnlyExtensions !== undefined) {
71
+ if (config.subagentOnlyExtensions !== undefined || preserve("subagentOnlyExtensions")) {
64
72
  const subagentOnlyExtensionsValue = joinComma(config.subagentOnlyExtensions);
65
73
  lines.push(`subagentOnlyExtensions: ${subagentOnlyExtensionsValue ?? ""}`);
66
74
  }
@@ -76,12 +84,22 @@ export function serializeAgent(config: AgentConfig): string {
76
84
  if (typeof maxSubagentDepth === "number" && Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0) {
77
85
  lines.push(`maxSubagentDepth: ${maxSubagentDepth}`);
78
86
  }
79
- if (config.completionGuard === false) lines.push("completionGuard: false");
87
+ if (config.completionGuard === false || preserve("completionGuard")) {
88
+ lines.push(`completionGuard: ${config.completionGuard === undefined ? "" : config.completionGuard ? "true" : "false"}`);
89
+ }
80
90
 
81
91
  if (config.extraFields) {
82
92
  for (const [key, value] of Object.entries(config.extraFields)) {
83
93
  if (KNOWN_FIELDS.has(key)) continue;
84
- lines.push(`${key}: ${value}`);
94
+ if (typeof value === "string" && value.includes("\n")) {
95
+ // Multi-line block value (e.g. permission: nested YAML)
96
+ lines.push(`${key}:`);
97
+ for (const blockLine of value.split("\n")) {
98
+ lines.push(` ${blockLine}`);
99
+ }
100
+ } else {
101
+ lines.push(`${key}: ${value}`);
102
+ }
85
103
  }
86
104
  }
87
105
 
@@ -8,7 +8,7 @@ import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import type { AcceptanceInput, OutputMode } from "../shared/types.ts";
11
- import { getAgentDir } from "../shared/utils.ts";
11
+ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
12
12
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
13
13
  import { parseChain, parseJsonChain } from "./chain-serializer.ts";
14
14
  import { mergeAgentsForScope } from "./agent-selection.ts";
@@ -22,6 +22,17 @@ export type AgentSource = "builtin" | "package" | "user" | "project";
22
22
  type SystemPromptMode = "append" | "replace";
23
23
  export type AgentDefaultContext = "fresh" | "fork";
24
24
 
25
+ export const BUILTIN_AGENT_NAMES = [
26
+ "context-builder",
27
+ "delegate",
28
+ "oracle",
29
+ "planner",
30
+ "researcher",
31
+ "reviewer",
32
+ "scout",
33
+ "worker",
34
+ ] as const;
35
+
25
36
  export function defaultSystemPromptMode(name: string): SystemPromptMode {
26
37
  return name === "delegate" ? "append" : "replace";
27
38
  }
@@ -107,9 +118,11 @@ export interface AgentConfig {
107
118
  interface SubagentSettings {
108
119
  overrides: Record<string, BuiltinAgentOverrideConfig>;
109
120
  disableBuiltins?: boolean;
121
+ disableThinking?: boolean;
110
122
  }
111
123
 
112
124
  const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
125
+ const agentFrontmatterFields = new WeakMap<AgentConfig, Set<string>>();
113
126
 
114
127
  export interface ChainStepConfig {
115
128
  agent?: string;
@@ -370,9 +383,10 @@ function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolea
370
383
  ];
371
384
 
372
385
  if (options.includeProject) {
386
+ const projectConfigDir = getProjectConfigDir(projectRoot);
373
387
  packageRoots.push(
374
- ...collectPackageRootsFromNodeModules(path.join(projectRoot, ".pi", "npm", "node_modules")),
375
- ...collectSettingsPackageRoots(path.join(projectRoot, ".pi", "settings.json"), path.join(projectRoot, ".pi")),
388
+ ...collectPackageRootsFromNodeModules(path.join(projectConfigDir, "npm", "node_modules")),
389
+ ...collectSettingsPackageRoots(path.join(projectConfigDir, "settings.json"), projectConfigDir),
376
390
  );
377
391
  }
378
392
 
@@ -488,7 +502,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
488
502
  function findNearestProjectRoot(cwd: string): string | null {
489
503
  let currentDir = cwd;
490
504
  while (true) {
491
- if (isDirectory(path.join(currentDir, ".pi")) || isDirectory(path.join(currentDir, ".agents"))) {
505
+ if (isDirectory(getProjectConfigDir(currentDir)) || isDirectory(path.join(currentDir, ".agents"))) {
492
506
  return currentDir;
493
507
  }
494
508
 
@@ -504,7 +518,7 @@ function getUserAgentSettingsPath(): string {
504
518
 
505
519
  function getProjectAgentSettingsPath(cwd: string): string | null {
506
520
  const projectRoot = findNearestProjectRoot(cwd);
507
- return projectRoot ? path.join(projectRoot, ".pi", "settings.json") : null;
521
+ return projectRoot ? path.join(getProjectConfigDir(projectRoot), "settings.json") : null;
508
522
  }
509
523
 
510
524
  function readSettingsFileStrict(filePath: string): Record<string, unknown> {
@@ -661,17 +675,25 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
661
675
  throw new Error(`Subagent settings in '${filePath}' have invalid 'disableBuiltins'; expected a boolean.`);
662
676
  }
663
677
  }
678
+ let disableThinking: boolean | undefined;
679
+ if ("disableThinking" in subagentsObject) {
680
+ if (typeof subagentsObject.disableThinking === "boolean") {
681
+ disableThinking = subagentsObject.disableThinking;
682
+ } else {
683
+ throw new Error(`Subagent settings in '${filePath}' have invalid 'disableThinking'; expected a boolean.`);
684
+ }
685
+ }
664
686
 
665
687
  const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
666
688
  const agentOverrides = subagentsObject.agentOverrides;
667
689
  if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) {
668
- return { overrides: parsed, disableBuiltins };
690
+ return { overrides: parsed, disableBuiltins, disableThinking };
669
691
  }
670
692
  for (const [name, value] of Object.entries(agentOverrides)) {
671
693
  const override = parseBuiltinOverrideEntry(name, value, filePath);
672
694
  if (override) parsed[name] = override;
673
695
  }
674
- return { overrides: parsed, disableBuiltins };
696
+ return { overrides: parsed, disableBuiltins, disableThinking };
675
697
  }
676
698
 
677
699
  function applyBuiltinOverride(
@@ -709,6 +731,15 @@ function applyBuiltinOverride(
709
731
  return next;
710
732
  }
711
733
 
734
+ function clearBuiltinThinking(agent: AgentConfig, meta: { scope: "user" | "project"; path: string }): AgentConfig {
735
+ if (agent.thinking === undefined) return agent;
736
+ return {
737
+ ...agent,
738
+ thinking: undefined,
739
+ override: agent.override ?? { ...meta, base: cloneOverrideBase(agent) },
740
+ };
741
+ }
742
+
712
743
  function applyBuiltinOverrides(
713
744
  builtinAgents: AgentConfig[],
714
745
  userSettings: SubagentSettings,
@@ -718,24 +749,151 @@ function applyBuiltinOverrides(
718
749
  ): AgentConfig[] {
719
750
  const projectBulkDisabled = projectSettings.disableBuiltins === true && projectSettingsPath !== null;
720
751
  const userBulkDisabled = projectSettings.disableBuiltins === undefined && userSettings.disableBuiltins === true;
752
+ const projectThinkingConfigured = projectSettings.disableThinking !== undefined && projectSettingsPath !== null;
753
+ const disableThinking = projectThinkingConfigured ? projectSettings.disableThinking === true : userSettings.disableThinking === true;
754
+ const disableThinkingMeta = projectThinkingConfigured
755
+ ? { scope: "project" as const, path: projectSettingsPath! }
756
+ : { scope: "user" as const, path: userSettingsPath };
757
+
758
+ const applyGlobalThinking = (agent: AgentConfig, hasExplicitThinkingOverride: boolean): AgentConfig => {
759
+ if (!disableThinking || hasExplicitThinkingOverride) return agent;
760
+ return clearBuiltinThinking(agent, disableThinkingMeta);
761
+ };
721
762
 
722
763
  return builtinAgents.map((agent) => {
723
764
  const projectOverride = projectSettings.overrides[agent.name];
724
765
  if (projectOverride && projectSettingsPath) {
725
- return applyBuiltinOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
766
+ return applyGlobalThinking(
767
+ applyBuiltinOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath }),
768
+ projectOverride.thinking !== undefined,
769
+ );
726
770
  }
727
771
 
728
772
  if (projectBulkDisabled && projectSettingsPath) {
729
- return applyBuiltinOverride(agent, { disabled: true }, { scope: "project", path: projectSettingsPath });
773
+ return applyGlobalThinking(
774
+ applyBuiltinOverride(agent, { disabled: true }, { scope: "project", path: projectSettingsPath }),
775
+ false,
776
+ );
730
777
  }
731
778
 
732
779
  const userOverride = userSettings.overrides[agent.name];
733
780
  if (userOverride) {
734
- return applyBuiltinOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
781
+ return applyGlobalThinking(
782
+ applyBuiltinOverride(agent, userOverride, { scope: "user", path: userSettingsPath }),
783
+ !projectThinkingConfigured && userOverride.thinking !== undefined,
784
+ );
735
785
  }
736
786
 
737
787
  if (userBulkDisabled) {
738
- return applyBuiltinOverride(agent, { disabled: true }, { scope: "user", path: userSettingsPath });
788
+ return applyGlobalThinking(
789
+ applyBuiltinOverride(agent, { disabled: true }, { scope: "user", path: userSettingsPath }),
790
+ false,
791
+ );
792
+ }
793
+
794
+ return applyGlobalThinking(agent, false);
795
+ });
796
+ }
797
+
798
+ function customAgentHasFrontmatterField(agent: AgentConfig, ...fields: string[]): boolean {
799
+ const frontmatterFields = agentFrontmatterFields.get(agent);
800
+ return frontmatterFields ? fields.some((field) => frontmatterFields.has(field)) : false;
801
+ }
802
+
803
+ function applyCustomAgentOverride(
804
+ agent: AgentConfig,
805
+ override: BuiltinAgentOverrideConfig,
806
+ meta: { scope: "user" | "project"; path: string },
807
+ ): AgentConfig {
808
+ let next: AgentConfig | undefined;
809
+ let anyFilled = false;
810
+
811
+ const mutable = (): AgentConfig => {
812
+ next ??= { ...agent };
813
+ return next;
814
+ };
815
+
816
+ const fill = <K extends keyof AgentConfig>(
817
+ field: K,
818
+ frontmatterFields: string[],
819
+ value: AgentConfig[K],
820
+ ): void => {
821
+ if (customAgentHasFrontmatterField(agent, ...frontmatterFields)) return;
822
+ mutable()[field] = value;
823
+ anyFilled = true;
824
+ };
825
+
826
+ if (override.model !== undefined) {
827
+ fill("model", ["model"], override.model === false ? undefined : override.model);
828
+ }
829
+ if (override.fallbackModels !== undefined) {
830
+ fill(
831
+ "fallbackModels",
832
+ ["fallbackModels"],
833
+ override.fallbackModels === false ? undefined : [...override.fallbackModels],
834
+ );
835
+ }
836
+ if (override.thinking !== undefined) {
837
+ fill("thinking", ["thinking"], override.thinking === false ? undefined : override.thinking);
838
+ }
839
+ if (override.systemPromptMode !== undefined) {
840
+ fill("systemPromptMode", ["systemPromptMode"], override.systemPromptMode);
841
+ }
842
+ if (override.inheritProjectContext !== undefined) {
843
+ fill("inheritProjectContext", ["inheritProjectContext"], override.inheritProjectContext);
844
+ }
845
+ if (override.inheritSkills !== undefined) {
846
+ fill("inheritSkills", ["inheritSkills"], override.inheritSkills);
847
+ }
848
+ if (override.defaultContext !== undefined) {
849
+ fill("defaultContext", ["defaultContext"], override.defaultContext === false ? undefined : override.defaultContext);
850
+ }
851
+ if (override.disabled !== undefined && agent.disabled === undefined) {
852
+ mutable().disabled = override.disabled;
853
+ anyFilled = true;
854
+ }
855
+ if (override.skills !== undefined) {
856
+ fill("skills", ["skill", "skills"], override.skills === false ? undefined : [...override.skills]);
857
+ }
858
+ if (override.tools !== undefined && !customAgentHasFrontmatterField(agent, "tools")) {
859
+ const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools);
860
+ const target = mutable();
861
+ target.tools = tools;
862
+ target.mcpDirectTools = mcpDirectTools;
863
+ anyFilled = true;
864
+ }
865
+ if (override.subagentOnlyExtensions !== undefined) {
866
+ fill(
867
+ "subagentOnlyExtensions",
868
+ ["subagentOnlyExtensions"],
869
+ override.subagentOnlyExtensions === false ? undefined : [...override.subagentOnlyExtensions],
870
+ );
871
+ }
872
+ if (override.completionGuard !== undefined) {
873
+ fill("completionGuard", ["completionGuard"], override.completionGuard);
874
+ }
875
+
876
+ if (!anyFilled || !next) return agent;
877
+ next.override = { ...meta, base: cloneOverrideBase(agent) };
878
+ return next;
879
+ }
880
+
881
+ function applyCustomAgentOverrides(
882
+ agents: AgentConfig[],
883
+ userSettings: SubagentSettings,
884
+ projectSettings: SubagentSettings,
885
+ userSettingsPath: string,
886
+ projectSettingsPath: string | null,
887
+ ): AgentConfig[] {
888
+ return agents.map((agent) => {
889
+ const projectOverride = projectSettings.overrides[agent.name];
890
+ if (projectOverride && projectSettingsPath) {
891
+ return applyCustomAgentOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
892
+ }
893
+
894
+ const userOverride = userSettings.overrides[agent.name];
895
+ if (userOverride) {
896
+ return applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
739
897
  }
740
898
 
741
899
  return agent;
@@ -959,7 +1117,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
959
1117
  ? true
960
1118
  : undefined;
961
1119
 
962
- agents.push({
1120
+ const agent: AgentConfig = {
963
1121
  name: runtimeName,
964
1122
  localName,
965
1123
  packageName,
@@ -989,7 +1147,9 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
989
1147
  : undefined,
990
1148
  completionGuard,
991
1149
  extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
992
- });
1150
+ };
1151
+ agentFrontmatterFields.set(agent, new Set(Object.keys(frontmatter)));
1152
+ agents.push(agent);
993
1153
  }
994
1154
 
995
1155
  return agents;
@@ -1034,7 +1194,7 @@ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; pref
1034
1194
  if (!projectRoot) return { readDirs: [], preferredDir: null };
1035
1195
 
1036
1196
  const legacyDir = path.join(projectRoot, ".agents");
1037
- const preferredDir = path.join(projectRoot, ".pi", "agents");
1197
+ const preferredDir = path.join(getProjectConfigDir(projectRoot), "agents");
1038
1198
  const readDirs: string[] = [];
1039
1199
  if (isDirectory(legacyDir)) readDirs.push(legacyDir);
1040
1200
  if (isDirectory(preferredDir)) readDirs.push(preferredDir);
@@ -1049,7 +1209,7 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
1049
1209
  const projectRoot = findNearestProjectRoot(cwd);
1050
1210
  if (!projectRoot) return { readDirs: [], preferredDir: null };
1051
1211
 
1052
- const preferredDir = path.join(projectRoot, ".pi", "chains");
1212
+ const preferredDir = path.join(getProjectConfigDir(projectRoot), "chains");
1053
1213
  return {
1054
1214
  readDirs: isDirectory(preferredDir) ? [preferredDir] : [],
1055
1215
  preferredDir,
@@ -1097,9 +1257,21 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
1097
1257
  const userAgentsExtra = scope === "project" ? [] : extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user"));
1098
1258
  const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
1099
1259
  const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
1100
- const userAgents = [...userAgentsExtra, ...userAgentsOld, ...userAgentsNew];
1260
+ const userAgents = applyCustomAgentOverrides(
1261
+ [...userAgentsExtra, ...userAgentsOld, ...userAgentsNew],
1262
+ userSettings,
1263
+ projectSettings,
1264
+ userSettingsPath,
1265
+ projectSettingsPath,
1266
+ );
1101
1267
 
1102
- const projectAgents = scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project"));
1268
+ const projectAgents = applyCustomAgentOverrides(
1269
+ scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project")),
1270
+ userSettings,
1271
+ projectSettings,
1272
+ userSettingsPath,
1273
+ projectSettingsPath,
1274
+ );
1103
1275
  const packageAgents = packageSubagentPaths.agents.flatMap((dir) => loadAgentsFromDir(dir, "package"));
1104
1276
  const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
1105
1277
  .filter((agent) => agent.disabled !== true);
@@ -1139,11 +1311,17 @@ export function discoverAgentsAll(cwd: string): {
1139
1311
  userSettingsPath,
1140
1312
  projectSettingsPath,
1141
1313
  );
1142
- const user = [
1143
- ...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
1144
- ...loadAgentsFromDir(userDirOld, "user"),
1145
- ...loadAgentsFromDir(userDirNew, "user"),
1146
- ];
1314
+ const user = applyCustomAgentOverrides(
1315
+ [
1316
+ ...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
1317
+ ...loadAgentsFromDir(userDirOld, "user"),
1318
+ ...loadAgentsFromDir(userDirNew, "user"),
1319
+ ],
1320
+ userSettings,
1321
+ projectSettings,
1322
+ userSettingsPath,
1323
+ projectSettingsPath,
1324
+ );
1147
1325
  const packageMap = new Map<string, AgentConfig>();
1148
1326
  for (const dir of packageSubagentPaths.agents) {
1149
1327
  for (const agent of loadAgentsFromDir(dir, "package")) {
@@ -1157,7 +1335,13 @@ export function discoverAgentsAll(cwd: string): {
1157
1335
  projectMap.set(agent.name, agent);
1158
1336
  }
1159
1337
  }
1160
- const project = Array.from(projectMap.values());
1338
+ const project = applyCustomAgentOverrides(
1339
+ Array.from(projectMap.values()),
1340
+ userSettings,
1341
+ projectSettings,
1342
+ userSettingsPath,
1343
+ projectSettingsPath,
1344
+ );
1161
1345
 
1162
1346
  const chainMap = new Map<string, ChainConfig>();
1163
1347
  const packageChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Escape regex special characters for use in a RegExp constructor.
3
+ */
4
+ function escapeRegex(s: string): string {
5
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6
+ }
7
+
8
+ /**
9
+ * Parse YAML frontmatter from agent/chain files.
10
+ * Handles both flat (key: value) and nested block (key: \n sub: val) values.
11
+ * Block values are stored as single strings with embedded newlines.
12
+ * The indentation of the block content is preserved relative to the key.
13
+ */
1
14
  export function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
2
15
  const frontmatter: Record<string, string> = {};
3
16
  const normalized = content.replace(/\r\n/g, "\n");
@@ -14,15 +27,66 @@ export function parseFrontmatter(content: string): { frontmatter: Record<string,
14
27
  const frontmatterBlock = normalized.slice(4, endIndex);
15
28
  const body = normalized.slice(endIndex + 4).trim();
16
29
 
17
- for (const line of frontmatterBlock.split("\n")) {
30
+ const lines = frontmatterBlock.split("\n");
31
+ let currentKey: string | null = null;
32
+ let currentBlockLines: string[] | null = null;
33
+ let currentIndent: number | null = null;
34
+
35
+ for (const line of lines) {
36
+ const indent = line.search(/\S|$/); // position of first non-whitespace char
37
+ const trimmed = line.trim();
38
+
39
+ if (currentKey !== null && currentBlockLines !== null && indent > (currentIndent ?? 0)) {
40
+ // This line is part of the current block value
41
+ currentBlockLines.push(line);
42
+ continue;
43
+ }
44
+
45
+ // Flush any pending block value
46
+ if (currentKey !== null && currentBlockLines !== null) {
47
+ // Strip the common leading whitespace from the block so the
48
+ // serializer can add its own indentation level.
49
+ const rawBlock = currentBlockLines.join("\n");
50
+ const leadingSpaces = rawBlock.match(/^([ \t]+)/m);
51
+ const prefix = leadingSpaces?.[1] ?? "";
52
+ const stripped = prefix
53
+ ? rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "")
54
+ : rawBlock;
55
+ frontmatter[currentKey] = stripped;
56
+ currentKey = null;
57
+ currentBlockLines = null;
58
+ currentIndent = null;
59
+ }
60
+
18
61
  const match = line.match(/^([\w-]+):\s*(.*)$/);
19
62
  if (match) {
20
63
  let value = match[2].trim();
21
64
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
22
65
  value = value.slice(1, -1);
23
66
  }
24
- frontmatter[match[1]] = value;
67
+
68
+ if (value === "") {
69
+ // Key with empty value — might start a block; defer storing until we see indent
70
+ currentKey = match[1];
71
+ currentBlockLines = [];
72
+ currentIndent = indent;
73
+ } else {
74
+ // Simple key: value
75
+ frontmatter[match[1]] = value;
76
+ }
25
77
  }
78
+ // Lines that don't match a key pattern (e.g., comments, empty lines) are ignored
79
+ }
80
+
81
+ // Flush final block value
82
+ if (currentKey !== null && currentBlockLines !== null) {
83
+ const rawBlock = currentBlockLines.join("\n");
84
+ const leadingSpaces = rawBlock.match(/^([ \t]+)/m);
85
+ const prefix = leadingSpaces?.[1] ?? "";
86
+ const stripped = prefix
87
+ ? rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "")
88
+ : rawBlock;
89
+ frontmatter[currentKey] = stripped;
26
90
  }
27
91
 
28
92
  return { frontmatter, body };