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.
- package/CHANGELOG.md +26 -0
- package/README.md +116 -17
- package/agents/context-builder.md +3 -3
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/scout.md +1 -1
- package/package.json +7 -7
- package/skills/pi-subagents/SKILL.md +5 -0
- package/src/agents/agent-management.ts +170 -6
- package/src/agents/agent-serializer.ts +31 -13
- package/src/agents/agents.ts +207 -23
- package/src/agents/frontmatter.ts +66 -2
- package/src/agents/skills.ts +117 -20
- package/src/extension/doctor.ts +20 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +47 -4
- package/src/extension/schemas.ts +10 -76
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/runs/background/async-execution.ts +14 -4
- package/src/runs/background/async-job-tracker.ts +56 -11
- package/src/runs/background/result-watcher.ts +11 -2
- package/src/runs/background/stale-run-reconciler.ts +9 -4
- package/src/runs/background/subagent-runner.ts +79 -3
- package/src/runs/foreground/chain-execution.ts +26 -2
- package/src/runs/foreground/execution.ts +113 -8
- package/src/runs/foreground/subagent-executor.ts +325 -77
- package/src/runs/shared/acceptance.ts +285 -34
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +4 -2
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +6 -1
- package/src/runs/shared/pi-args.ts +9 -1
- package/src/runs/shared/single-output.ts +15 -1
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +8 -2
- package/src/shared/utils.ts +19 -1
- package/src/slash/prompt-template-bridge.ts +26 -3
- package/src/slash/slash-commands.ts +33 -3
- 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
|
-
|
|
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"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
lines.push(`
|
|
54
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
|
package/src/agents/agents.ts
CHANGED
|
@@ -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(
|
|
375
|
-
...collectSettingsPackageRoots(path.join(
|
|
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(
|
|
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, "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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, "
|
|
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, "
|
|
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 =
|
|
1260
|
+
const userAgents = applyCustomAgentOverrides(
|
|
1261
|
+
[...userAgentsExtra, ...userAgentsOld, ...userAgentsNew],
|
|
1262
|
+
userSettings,
|
|
1263
|
+
projectSettings,
|
|
1264
|
+
userSettingsPath,
|
|
1265
|
+
projectSettingsPath,
|
|
1266
|
+
);
|
|
1101
1267
|
|
|
1102
|
-
const projectAgents =
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|