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.
- package/CHANGELOG.md +43 -0
- package/README.md +125 -19
- 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 +30 -0
- package/src/agents/agent-management.ts +189 -8
- package/src/agents/agent-serializer.ts +35 -12
- package/src/agents/agents.ts +243 -24
- package/src/agents/frontmatter.ts +66 -2
- package/src/agents/proactive-skills.ts +191 -0
- package/src/agents/skills.ts +117 -20
- package/src/extension/doctor.ts +20 -0
- package/src/extension/fanout-child.ts +2 -1
- package/src/extension/index.ts +50 -5
- package/src/extension/schemas.ts +40 -79
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/runs/background/async-execution.ts +180 -67
- package/src/runs/background/async-job-tracker.ts +56 -11
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/result-watcher.ts +11 -2
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +9 -4
- package/src/runs/background/subagent-runner.ts +158 -11
- package/src/runs/foreground/chain-execution.ts +26 -2
- package/src/runs/foreground/execution.ts +114 -8
- package/src/runs/foreground/subagent-executor.ts +611 -87
- package/src/runs/shared/acceptance.ts +285 -34
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +5 -3
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +13 -1
- package/src/runs/shared/pi-args.ts +12 -3
- package/src/runs/shared/single-output.ts +15 -1
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +17 -2
- package/src/shared/utils.ts +19 -1
- package/src/slash/prompt-template-bridge.ts +26 -3
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +34 -4
- package/src/tui/render.ts +265 -13
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
|
}
|
|
@@ -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(
|
|
372
|
-
...collectSettingsPackageRoots(path.join(
|
|
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(
|
|
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, "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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, "
|
|
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, "
|
|
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 =
|
|
1260
|
+
const userAgents = applyCustomAgentOverrides(
|
|
1261
|
+
[...userAgentsExtra, ...userAgentsOld, ...userAgentsNew],
|
|
1262
|
+
userSettings,
|
|
1263
|
+
projectSettings,
|
|
1264
|
+
userSettingsPath,
|
|
1265
|
+
projectSettingsPath,
|
|
1266
|
+
);
|
|
1066
1267
|
|
|
1067
|
-
const projectAgents =
|
|
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
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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 =
|
|
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
|
-
|
|
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 };
|