pi-subagents 0.29.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 (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +125 -19
  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 +30 -0
  9. package/src/agents/agent-management.ts +189 -8
  10. package/src/agents/agent-serializer.ts +35 -12
  11. package/src/agents/agents.ts +243 -24
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/proactive-skills.ts +191 -0
  14. package/src/agents/skills.ts +117 -20
  15. package/src/extension/doctor.ts +20 -0
  16. package/src/extension/fanout-child.ts +2 -1
  17. package/src/extension/index.ts +50 -5
  18. package/src/extension/schemas.ts +40 -79
  19. package/src/intercom/intercom-bridge.ts +2 -3
  20. package/src/runs/background/async-execution.ts +180 -67
  21. package/src/runs/background/async-job-tracker.ts +56 -11
  22. package/src/runs/background/async-resume.ts +53 -5
  23. package/src/runs/background/async-status.ts +4 -1
  24. package/src/runs/background/chain-append.ts +282 -0
  25. package/src/runs/background/chain-root-attachment.ts +161 -0
  26. package/src/runs/background/result-watcher.ts +11 -2
  27. package/src/runs/background/run-status.ts +1 -0
  28. package/src/runs/background/stale-run-reconciler.ts +9 -4
  29. package/src/runs/background/subagent-runner.ts +158 -11
  30. package/src/runs/foreground/chain-execution.ts +26 -2
  31. package/src/runs/foreground/execution.ts +114 -8
  32. package/src/runs/foreground/subagent-executor.ts +611 -87
  33. package/src/runs/shared/acceptance.ts +285 -34
  34. package/src/runs/shared/chain-outputs.ts +23 -8
  35. package/src/runs/shared/completion-guard.ts +1 -1
  36. package/src/runs/shared/dynamic-fanout.ts +5 -3
  37. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  38. package/src/runs/shared/parallel-utils.ts +13 -1
  39. package/src/runs/shared/pi-args.ts +12 -3
  40. package/src/runs/shared/single-output.ts +15 -1
  41. package/src/runs/shared/subagent-control.ts +8 -11
  42. package/src/shared/settings.ts +1 -0
  43. package/src/shared/types.ts +17 -2
  44. package/src/shared/utils.ts +19 -1
  45. package/src/slash/prompt-template-bridge.ts +26 -3
  46. package/src/slash/slash-bridge.ts +3 -1
  47. package/src/slash/slash-commands.ts +34 -4
  48. package/src/tui/render.ts +265 -13
@@ -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
  }
@@ -47,6 +58,7 @@ export interface BuiltinAgentOverrideBase {
47
58
  skills?: string[];
48
59
  tools?: string[];
49
60
  mcpDirectTools?: string[];
61
+ subagentOnlyExtensions?: string[];
50
62
  completionGuard?: boolean;
51
63
  }
52
64
 
@@ -62,6 +74,7 @@ interface BuiltinAgentOverrideConfig {
62
74
  systemPrompt?: string;
63
75
  skills?: string[] | false;
64
76
  tools?: string[] | false;
77
+ subagentOnlyExtensions?: string[] | false;
65
78
  completionGuard?: boolean;
66
79
  }
67
80
 
@@ -90,6 +103,7 @@ export interface AgentConfig {
90
103
  filePath: string;
91
104
  skills?: string[];
92
105
  extensions?: string[];
106
+ subagentOnlyExtensions?: string[];
93
107
  output?: string;
94
108
  defaultReads?: string[];
95
109
  defaultProgress?: boolean;
@@ -104,9 +118,11 @@ export interface AgentConfig {
104
118
  interface SubagentSettings {
105
119
  overrides: Record<string, BuiltinAgentOverrideConfig>;
106
120
  disableBuiltins?: boolean;
121
+ disableThinking?: boolean;
107
122
  }
108
123
 
109
124
  const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
125
+ const agentFrontmatterFields = new WeakMap<AgentConfig, Set<string>>();
110
126
 
111
127
  export interface ChainStepConfig {
112
128
  agent?: string;
@@ -367,9 +383,10 @@ function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolea
367
383
  ];
368
384
 
369
385
  if (options.includeProject) {
386
+ const projectConfigDir = getProjectConfigDir(projectRoot);
370
387
  packageRoots.push(
371
- ...collectPackageRootsFromNodeModules(path.join(projectRoot, ".pi", "npm", "node_modules")),
372
- ...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),
373
390
  );
374
391
  }
375
392
 
@@ -457,6 +474,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
457
474
  skills: agent.skills ? [...agent.skills] : undefined,
458
475
  tools: agent.tools ? [...agent.tools] : undefined,
459
476
  mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
477
+ subagentOnlyExtensions: agent.subagentOnlyExtensions ? [...agent.subagentOnlyExtensions] : undefined,
460
478
  completionGuard: agent.completionGuard,
461
479
  };
462
480
  }
@@ -476,6 +494,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
476
494
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
477
495
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
478
496
  ...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
497
+ ...(override.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: override.subagentOnlyExtensions === false ? false : [...override.subagentOnlyExtensions] } : {}),
479
498
  ...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
480
499
  };
481
500
  }
@@ -483,7 +502,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
483
502
  function findNearestProjectRoot(cwd: string): string | null {
484
503
  let currentDir = cwd;
485
504
  while (true) {
486
- if (isDirectory(path.join(currentDir, ".pi")) || isDirectory(path.join(currentDir, ".agents"))) {
505
+ if (isDirectory(getProjectConfigDir(currentDir)) || isDirectory(path.join(currentDir, ".agents"))) {
487
506
  return currentDir;
488
507
  }
489
508
 
@@ -499,7 +518,7 @@ function getUserAgentSettingsPath(): string {
499
518
 
500
519
  function getProjectAgentSettingsPath(cwd: string): string | null {
501
520
  const projectRoot = findNearestProjectRoot(cwd);
502
- return projectRoot ? path.join(projectRoot, ".pi", "settings.json") : null;
521
+ return projectRoot ? path.join(getProjectConfigDir(projectRoot), "settings.json") : null;
503
522
  }
504
523
 
505
524
  function readSettingsFileStrict(filePath: string): Record<string, unknown> {
@@ -635,6 +654,9 @@ function parseBuiltinOverrideEntry(
635
654
  const tools = parseOverrideStringArrayOrFalse(input.tools, { filePath, name, field: "tools" });
636
655
  if (tools !== undefined) override.tools = tools;
637
656
 
657
+ const subagentOnlyExtensions = parseOverrideStringArrayOrFalse(input.subagentOnlyExtensions, { filePath, name, field: "subagentOnlyExtensions" });
658
+ if (subagentOnlyExtensions !== undefined) override.subagentOnlyExtensions = subagentOnlyExtensions;
659
+
638
660
  return Object.keys(override).length > 0 ? override : undefined;
639
661
  }
640
662
 
@@ -653,17 +675,25 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
653
675
  throw new Error(`Subagent settings in '${filePath}' have invalid 'disableBuiltins'; expected a boolean.`);
654
676
  }
655
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
+ }
656
686
 
657
687
  const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
658
688
  const agentOverrides = subagentsObject.agentOverrides;
659
689
  if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) {
660
- return { overrides: parsed, disableBuiltins };
690
+ return { overrides: parsed, disableBuiltins, disableThinking };
661
691
  }
662
692
  for (const [name, value] of Object.entries(agentOverrides)) {
663
693
  const override = parseBuiltinOverrideEntry(name, value, filePath);
664
694
  if (override) parsed[name] = override;
665
695
  }
666
- return { overrides: parsed, disableBuiltins };
696
+ return { overrides: parsed, disableBuiltins, disableThinking };
667
697
  }
668
698
 
669
699
  function applyBuiltinOverride(
@@ -693,11 +723,23 @@ function applyBuiltinOverride(
693
723
  next.tools = tools;
694
724
  next.mcpDirectTools = mcpDirectTools;
695
725
  }
726
+ if (override.subagentOnlyExtensions !== undefined) {
727
+ next.subagentOnlyExtensions = override.subagentOnlyExtensions === false ? undefined : [...override.subagentOnlyExtensions];
728
+ }
696
729
  if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
697
730
 
698
731
  return next;
699
732
  }
700
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
+
701
743
  function applyBuiltinOverrides(
702
744
  builtinAgents: AgentConfig[],
703
745
  userSettings: SubagentSettings,
@@ -707,24 +749,151 @@ function applyBuiltinOverrides(
707
749
  ): AgentConfig[] {
708
750
  const projectBulkDisabled = projectSettings.disableBuiltins === true && projectSettingsPath !== null;
709
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
+ };
710
762
 
711
763
  return builtinAgents.map((agent) => {
712
764
  const projectOverride = projectSettings.overrides[agent.name];
713
765
  if (projectOverride && projectSettingsPath) {
714
- return applyBuiltinOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
766
+ return applyGlobalThinking(
767
+ applyBuiltinOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath }),
768
+ projectOverride.thinking !== undefined,
769
+ );
715
770
  }
716
771
 
717
772
  if (projectBulkDisabled && projectSettingsPath) {
718
- return applyBuiltinOverride(agent, { disabled: true }, { scope: "project", path: projectSettingsPath });
773
+ return applyGlobalThinking(
774
+ applyBuiltinOverride(agent, { disabled: true }, { scope: "project", path: projectSettingsPath }),
775
+ false,
776
+ );
719
777
  }
720
778
 
721
779
  const userOverride = userSettings.overrides[agent.name];
722
780
  if (userOverride) {
723
- 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
+ );
724
785
  }
725
786
 
726
787
  if (userBulkDisabled) {
727
- 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 });
728
897
  }
729
898
 
730
899
  return agent;
@@ -733,7 +902,7 @@ function applyBuiltinOverrides(
733
902
 
734
903
  export function buildBuiltinOverrideConfig(
735
904
  base: BuiltinAgentOverrideBase,
736
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "completionGuard">,
905
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "subagentOnlyExtensions" | "completionGuard">,
737
906
  ): BuiltinAgentOverrideConfig | undefined {
738
907
  const override: BuiltinAgentOverrideConfig = {};
739
908
 
@@ -751,6 +920,9 @@ export function buildBuiltinOverrideConfig(
751
920
  const baseTools = joinToolList(base);
752
921
  const draftTools = joinToolList(draft);
753
922
  if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false;
923
+ if (!arraysEqual(draft.subagentOnlyExtensions, base.subagentOnlyExtensions)) {
924
+ override.subagentOnlyExtensions = draft.subagentOnlyExtensions ? [...draft.subagentOnlyExtensions] : false;
925
+ }
754
926
  if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) {
755
927
  override.completionGuard = draft.completionGuard !== false;
756
928
  }
@@ -830,10 +1002,23 @@ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolea
830
1002
  return files;
831
1003
  }
832
1004
 
1005
+ function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
1006
+ const relative = path.relative(rootDir, filePath);
1007
+ const parts = relative.split(path.sep).map((part) => part.toLowerCase());
1008
+ if (path.basename(rootDir).toLowerCase() === ".agents") {
1009
+ parts.unshift(".agents");
1010
+ }
1011
+ return parts.some((part, index) => part === ".agents" && parts[index + 1] === "skills");
1012
+ }
1013
+
833
1014
  function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
834
1015
  const agents: AgentConfig[] = [];
835
1016
 
836
1017
  for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
1018
+ if (isLegacyAgentSkillPath(dir, filePath)) {
1019
+ continue;
1020
+ }
1021
+
837
1022
  let content: string;
838
1023
  try {
839
1024
  content = fs.readFileSync(filePath, "utf-8");
@@ -912,6 +1097,13 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
912
1097
  .map((e) => e.trim())
913
1098
  .filter(Boolean);
914
1099
  }
1100
+ let subagentOnlyExtensions: string[] | undefined;
1101
+ if (frontmatter.subagentOnlyExtensions !== undefined) {
1102
+ subagentOnlyExtensions = frontmatter.subagentOnlyExtensions
1103
+ .split(",")
1104
+ .map((e) => e.trim())
1105
+ .filter(Boolean);
1106
+ }
915
1107
 
916
1108
  const extraFields: Record<string, string> = {};
917
1109
  for (const [key, value] of Object.entries(frontmatter)) {
@@ -925,7 +1117,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
925
1117
  ? true
926
1118
  : undefined;
927
1119
 
928
- agents.push({
1120
+ const agent: AgentConfig = {
929
1121
  name: runtimeName,
930
1122
  localName,
931
1123
  packageName,
@@ -944,6 +1136,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
944
1136
  filePath,
945
1137
  skills: skills && skills.length > 0 ? skills : undefined,
946
1138
  extensions,
1139
+ subagentOnlyExtensions,
947
1140
  output: frontmatter.output,
948
1141
  defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
949
1142
  defaultProgress: frontmatter.defaultProgress === "true",
@@ -954,7 +1147,9 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
954
1147
  : undefined,
955
1148
  completionGuard,
956
1149
  extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
957
- });
1150
+ };
1151
+ agentFrontmatterFields.set(agent, new Set(Object.keys(frontmatter)));
1152
+ agents.push(agent);
958
1153
  }
959
1154
 
960
1155
  return agents;
@@ -999,7 +1194,7 @@ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; pref
999
1194
  if (!projectRoot) return { readDirs: [], preferredDir: null };
1000
1195
 
1001
1196
  const legacyDir = path.join(projectRoot, ".agents");
1002
- const preferredDir = path.join(projectRoot, ".pi", "agents");
1197
+ const preferredDir = path.join(getProjectConfigDir(projectRoot), "agents");
1003
1198
  const readDirs: string[] = [];
1004
1199
  if (isDirectory(legacyDir)) readDirs.push(legacyDir);
1005
1200
  if (isDirectory(preferredDir)) readDirs.push(preferredDir);
@@ -1014,7 +1209,7 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
1014
1209
  const projectRoot = findNearestProjectRoot(cwd);
1015
1210
  if (!projectRoot) return { readDirs: [], preferredDir: null };
1016
1211
 
1017
- const preferredDir = path.join(projectRoot, ".pi", "chains");
1212
+ const preferredDir = path.join(getProjectConfigDir(projectRoot), "chains");
1018
1213
  return {
1019
1214
  readDirs: isDirectory(preferredDir) ? [preferredDir] : [],
1020
1215
  preferredDir,
@@ -1062,9 +1257,21 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
1062
1257
  const userAgentsExtra = scope === "project" ? [] : extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user"));
1063
1258
  const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
1064
1259
  const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
1065
- const userAgents = [...userAgentsExtra, ...userAgentsOld, ...userAgentsNew];
1260
+ const userAgents = applyCustomAgentOverrides(
1261
+ [...userAgentsExtra, ...userAgentsOld, ...userAgentsNew],
1262
+ userSettings,
1263
+ projectSettings,
1264
+ userSettingsPath,
1265
+ projectSettingsPath,
1266
+ );
1066
1267
 
1067
- 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
+ );
1068
1275
  const packageAgents = packageSubagentPaths.agents.flatMap((dir) => loadAgentsFromDir(dir, "package"));
1069
1276
  const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
1070
1277
  .filter((agent) => agent.disabled !== true);
@@ -1104,11 +1311,17 @@ export function discoverAgentsAll(cwd: string): {
1104
1311
  userSettingsPath,
1105
1312
  projectSettingsPath,
1106
1313
  );
1107
- const user = [
1108
- ...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
1109
- ...loadAgentsFromDir(userDirOld, "user"),
1110
- ...loadAgentsFromDir(userDirNew, "user"),
1111
- ];
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
+ );
1112
1325
  const packageMap = new Map<string, AgentConfig>();
1113
1326
  for (const dir of packageSubagentPaths.agents) {
1114
1327
  for (const agent of loadAgentsFromDir(dir, "package")) {
@@ -1122,7 +1335,13 @@ export function discoverAgentsAll(cwd: string): {
1122
1335
  projectMap.set(agent.name, agent);
1123
1336
  }
1124
1337
  }
1125
- const project = Array.from(projectMap.values());
1338
+ const project = applyCustomAgentOverrides(
1339
+ Array.from(projectMap.values()),
1340
+ userSettings,
1341
+ projectSettings,
1342
+ userSettingsPath,
1343
+ projectSettingsPath,
1344
+ );
1126
1345
 
1127
1346
  const chainMap = new Map<string, ChainConfig>();
1128
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 };