pi-cohort 2.0.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 +1151 -0
- package/LICENSE +22 -0
- package/README.md +1220 -0
- package/agents/context-builder.md +45 -0
- package/agents/delegate.md +12 -0
- package/agents/oracle.md +73 -0
- package/agents/planner.md +55 -0
- package/agents/reviewer.md +91 -0
- package/agents/scout.md +50 -0
- package/agents/worker.md +67 -0
- package/package.json +87 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-context-build.md +55 -0
- package/prompts/parallel-handoff-plan.md +61 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +41 -0
- package/skills/pi-cohort/SKILL.md +818 -0
- package/src/agents/agent-management.ts +685 -0
- package/src/agents/agent-scope.ts +6 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +83 -0
- package/src/agents/agents.ts +1141 -0
- package/src/agents/chain-serializer.ts +251 -0
- package/src/agents/frontmatter.ts +29 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +632 -0
- package/src/extension/config.ts +16 -0
- package/src/extension/control-notices.ts +92 -0
- package/src/extension/doctor.ts +236 -0
- package/src/extension/fanout-child.ts +170 -0
- package/src/extension/grand-total.ts +109 -0
- package/src/extension/index.ts +630 -0
- package/src/extension/schemas.ts +306 -0
- package/src/intercom/intercom-bridge.ts +379 -0
- package/src/intercom/result-intercom.ts +377 -0
- package/src/runs/background/async-execution.ts +796 -0
- package/src/runs/background/async-job-tracker.ts +320 -0
- package/src/runs/background/async-resume.ts +345 -0
- package/src/runs/background/async-status.ts +335 -0
- package/src/runs/background/completion-dedupe.ts +63 -0
- package/src/runs/background/notify.ts +108 -0
- package/src/runs/background/parallel-groups.ts +45 -0
- package/src/runs/background/result-watcher.ts +307 -0
- package/src/runs/background/run-id-resolver.ts +83 -0
- package/src/runs/background/run-status.ts +272 -0
- package/src/runs/background/stale-run-reconciler.ts +336 -0
- package/src/runs/background/subagent-runner.ts +2326 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1333 -0
- package/src/runs/foreground/chain-execution.ts +1187 -0
- package/src/runs/foreground/execution.ts +1028 -0
- package/src/runs/foreground/subagent-executor.ts +2580 -0
- package/src/runs/shared/acceptance.ts +605 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +143 -0
- package/src/runs/shared/dynamic-fanout.ts +293 -0
- package/src/runs/shared/long-running-guard.ts +175 -0
- package/src/runs/shared/model-fallback.ts +103 -0
- package/src/runs/shared/nested-events.ts +822 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +115 -0
- package/src/runs/shared/parallel-utils.ts +136 -0
- package/src/runs/shared/pi-args.ts +221 -0
- package/src/runs/shared/pi-spawn.ts +115 -0
- package/src/runs/shared/run-history.ts +60 -0
- package/src/runs/shared/single-output.ts +164 -0
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-control.ts +287 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +220 -0
- package/src/runs/shared/workflow-graph.ts +206 -0
- package/src/runs/shared/worktree.ts +577 -0
- package/src/shared/artifacts.ts +98 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/file-coalescer.ts +40 -0
- package/src/shared/fork-context.ts +76 -0
- package/src/shared/formatters.ts +133 -0
- package/src/shared/jsonl-writer.ts +81 -0
- package/src/shared/model-info.ts +78 -0
- package/src/shared/post-exit-stdio-guard.ts +85 -0
- package/src/shared/session-identity.ts +10 -0
- package/src/shared/session-tokens.ts +46 -0
- package/src/shared/settings.ts +447 -0
- package/src/shared/status-format.ts +59 -0
- package/src/shared/types.ts +1072 -0
- package/src/shared/utils.ts +451 -0
- package/src/slash/prompt-template-bridge.ts +397 -0
- package/src/slash/slash-bridge.ts +174 -0
- package/src/slash/slash-commands.ts +567 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/render-helpers.ts +80 -0
- package/src/tui/render.ts +1476 -0
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery and configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import type { AcceptanceInput, OutputMode } from "../shared/types.ts";
|
|
10
|
+
import { getAgentDir } from "../shared/utils.ts";
|
|
11
|
+
import { KNOWN_FIELDS } from "./agent-serializer.ts";
|
|
12
|
+
import { parseChain, parseJsonChain } from "./chain-serializer.ts";
|
|
13
|
+
import { mergeAgentsForScope } from "./agent-selection.ts";
|
|
14
|
+
import { parseFrontmatter } from "./frontmatter.ts";
|
|
15
|
+
import { buildRuntimeName, parsePackageName } from "./identity.ts";
|
|
16
|
+
export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
|
|
17
|
+
|
|
18
|
+
export type AgentScope = "user" | "project" | "both";
|
|
19
|
+
|
|
20
|
+
export type AgentSource = "builtin" | "user" | "project";
|
|
21
|
+
type SystemPromptMode = "append" | "replace";
|
|
22
|
+
export type AgentDefaultContext = "fresh" | "fork";
|
|
23
|
+
|
|
24
|
+
export function defaultSystemPromptMode(name: string): SystemPromptMode {
|
|
25
|
+
return name === "delegate" ? "append" : "replace";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defaultInheritProjectContext(name: string): boolean {
|
|
29
|
+
return name === "delegate";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function defaultInheritSkills(): boolean {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function assertNoMcpDirectTools(tools: readonly string[], context: string): void {
|
|
37
|
+
const offending = tools.find((tool) => tool.startsWith("mcp:"));
|
|
38
|
+
if (offending) {
|
|
39
|
+
throw new Error(`${context}: MCP direct tools are no longer supported ('${offending}'). Remove the 'mcp:' entry.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BuiltinAgentOverrideBase {
|
|
44
|
+
model?: string;
|
|
45
|
+
fallbackModels?: string[];
|
|
46
|
+
thinking?: string;
|
|
47
|
+
systemPromptMode: SystemPromptMode;
|
|
48
|
+
inheritProjectContext: boolean;
|
|
49
|
+
inheritSkills: boolean;
|
|
50
|
+
defaultContext?: AgentDefaultContext;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
systemPrompt: string;
|
|
53
|
+
skills?: string[];
|
|
54
|
+
tools?: string[];
|
|
55
|
+
completionGuard?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface BuiltinAgentOverrideConfig {
|
|
59
|
+
model?: string | false;
|
|
60
|
+
fallbackModels?: string[] | false;
|
|
61
|
+
thinking?: string | false;
|
|
62
|
+
systemPromptMode?: SystemPromptMode;
|
|
63
|
+
inheritProjectContext?: boolean;
|
|
64
|
+
inheritSkills?: boolean;
|
|
65
|
+
defaultContext?: AgentDefaultContext | false;
|
|
66
|
+
disabled?: boolean;
|
|
67
|
+
systemPrompt?: string;
|
|
68
|
+
skills?: string[] | false;
|
|
69
|
+
tools?: string[] | false;
|
|
70
|
+
toolsPrepend?: string[];
|
|
71
|
+
toolsAppend?: string[];
|
|
72
|
+
completionGuard?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface BuiltinAgentOverrideInfo {
|
|
76
|
+
scope: "user" | "project";
|
|
77
|
+
path: string;
|
|
78
|
+
base: BuiltinAgentOverrideBase;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface AgentConfig {
|
|
82
|
+
name: string;
|
|
83
|
+
localName?: string;
|
|
84
|
+
packageName?: string;
|
|
85
|
+
description: string;
|
|
86
|
+
tools?: string[];
|
|
87
|
+
model?: string;
|
|
88
|
+
fallbackModels?: string[];
|
|
89
|
+
thinking?: string;
|
|
90
|
+
systemPromptMode: SystemPromptMode;
|
|
91
|
+
inheritProjectContext: boolean;
|
|
92
|
+
inheritSkills: boolean;
|
|
93
|
+
defaultContext?: AgentDefaultContext;
|
|
94
|
+
systemPrompt: string;
|
|
95
|
+
source: AgentSource;
|
|
96
|
+
filePath: string;
|
|
97
|
+
skills?: string[];
|
|
98
|
+
extensions?: string[];
|
|
99
|
+
output?: string;
|
|
100
|
+
defaultReads?: string[];
|
|
101
|
+
defaultProgress?: boolean;
|
|
102
|
+
interactive?: boolean;
|
|
103
|
+
maxSubagentDepth?: number;
|
|
104
|
+
completionGuard?: boolean;
|
|
105
|
+
disabled?: boolean;
|
|
106
|
+
extraFields?: Record<string, string>;
|
|
107
|
+
override?: BuiltinAgentOverrideInfo;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface SubagentSettings {
|
|
111
|
+
overrides: Record<string, BuiltinAgentOverrideConfig>;
|
|
112
|
+
disableBuiltins?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
|
|
116
|
+
|
|
117
|
+
export interface ChainStepConfig {
|
|
118
|
+
agent?: string;
|
|
119
|
+
task?: string;
|
|
120
|
+
phase?: string;
|
|
121
|
+
label?: string;
|
|
122
|
+
as?: string;
|
|
123
|
+
outputSchema?: string | Record<string, unknown>;
|
|
124
|
+
output?: string | false;
|
|
125
|
+
outputMode?: OutputMode;
|
|
126
|
+
reads?: string[] | false;
|
|
127
|
+
model?: string;
|
|
128
|
+
skills?: string[] | false;
|
|
129
|
+
progress?: boolean;
|
|
130
|
+
parallel?: unknown;
|
|
131
|
+
expand?: unknown;
|
|
132
|
+
collect?: unknown;
|
|
133
|
+
concurrency?: number;
|
|
134
|
+
failFast?: boolean;
|
|
135
|
+
worktree?: boolean;
|
|
136
|
+
acceptance?: AcceptanceInput;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ChainConfig {
|
|
140
|
+
name: string;
|
|
141
|
+
localName?: string;
|
|
142
|
+
packageName?: string;
|
|
143
|
+
description: string;
|
|
144
|
+
source: AgentSource;
|
|
145
|
+
filePath: string;
|
|
146
|
+
steps: ChainStepConfig[];
|
|
147
|
+
extraFields?: Record<string, string>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ChainDiscoveryDiagnostic {
|
|
151
|
+
source: "user" | "project";
|
|
152
|
+
filePath: string;
|
|
153
|
+
error: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface AgentDiscoveryResult {
|
|
157
|
+
agents: AgentConfig[];
|
|
158
|
+
projectAgentsDir: string | null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getUserChainDir(): string {
|
|
162
|
+
return path.join(getAgentDir(), "chains");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean {
|
|
166
|
+
if (!a && !b) return true;
|
|
167
|
+
if (!a || !b) return false;
|
|
168
|
+
if (a.length !== b.length) return false;
|
|
169
|
+
for (let i = 0; i < a.length; i++) {
|
|
170
|
+
if (a[i] !== b[i]) return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
176
|
+
return {
|
|
177
|
+
model: agent.model,
|
|
178
|
+
fallbackModels: agent.fallbackModels ? [...agent.fallbackModels] : undefined,
|
|
179
|
+
thinking: agent.thinking,
|
|
180
|
+
systemPromptMode: agent.systemPromptMode,
|
|
181
|
+
inheritProjectContext: agent.inheritProjectContext,
|
|
182
|
+
inheritSkills: agent.inheritSkills,
|
|
183
|
+
defaultContext: agent.defaultContext,
|
|
184
|
+
disabled: agent.disabled,
|
|
185
|
+
systemPrompt: agent.systemPrompt,
|
|
186
|
+
skills: agent.skills ? [...agent.skills] : undefined,
|
|
187
|
+
tools: agent.tools ? [...agent.tools] : undefined,
|
|
188
|
+
completionGuard: agent.completionGuard,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentOverrideConfig {
|
|
193
|
+
return {
|
|
194
|
+
...(override.model !== undefined ? { model: override.model } : {}),
|
|
195
|
+
...(override.fallbackModels !== undefined
|
|
196
|
+
? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
|
|
197
|
+
: {}),
|
|
198
|
+
...(override.thinking !== undefined ? { thinking: override.thinking } : {}),
|
|
199
|
+
...(override.systemPromptMode !== undefined ? { systemPromptMode: override.systemPromptMode } : {}),
|
|
200
|
+
...(override.inheritProjectContext !== undefined ? { inheritProjectContext: override.inheritProjectContext } : {}),
|
|
201
|
+
...(override.inheritSkills !== undefined ? { inheritSkills: override.inheritSkills } : {}),
|
|
202
|
+
...(override.defaultContext !== undefined ? { defaultContext: override.defaultContext } : {}),
|
|
203
|
+
...(override.disabled !== undefined ? { disabled: override.disabled } : {}),
|
|
204
|
+
...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
|
|
205
|
+
...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
|
|
206
|
+
...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
|
|
207
|
+
...(override.toolsPrepend !== undefined ? { toolsPrepend: [...override.toolsPrepend] } : {}),
|
|
208
|
+
...(override.toolsAppend !== undefined ? { toolsAppend: [...override.toolsAppend] } : {}),
|
|
209
|
+
...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resolveRealPath(p: string): string {
|
|
214
|
+
try {
|
|
215
|
+
return fs.realpathSync(p);
|
|
216
|
+
} catch {
|
|
217
|
+
return path.resolve(p);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function findGitRoot(startDir: string): string | null {
|
|
222
|
+
let currentDir = startDir;
|
|
223
|
+
while (true) {
|
|
224
|
+
if (fs.existsSync(path.join(currentDir, ".git"))) {
|
|
225
|
+
return resolveRealPath(currentDir);
|
|
226
|
+
}
|
|
227
|
+
const parentDir = path.dirname(currentDir);
|
|
228
|
+
if (parentDir === currentDir) return null;
|
|
229
|
+
currentDir = parentDir;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Project levels from cwd up to and including the git root, FARTHEST-FIRST.
|
|
234
|
+
// Marker predicate matches findNearestProjectRoot (.pi OR .agents), so a level
|
|
235
|
+
// carrying only .pi/settings.json or only .pi/chains still participates.
|
|
236
|
+
// Returns raw (non-realpath) paths so callers build file paths consistent with
|
|
237
|
+
// how they received cwd. Realpath is used only internally for dedup. Falls back
|
|
238
|
+
// to at most the single nearest project root when not inside a git repo.
|
|
239
|
+
function enumerateProjectLevels(cwd: string): string[] {
|
|
240
|
+
const gitRoot = findGitRoot(cwd);
|
|
241
|
+
if (!gitRoot) {
|
|
242
|
+
const nearest = findNearestProjectRoot(cwd);
|
|
243
|
+
return nearest ? [nearest] : [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const levels: string[] = [];
|
|
247
|
+
const seen = new Set<string>();
|
|
248
|
+
let currentDir = cwd;
|
|
249
|
+
while (true) {
|
|
250
|
+
const resolved = resolveRealPath(currentDir);
|
|
251
|
+
const hasMarker = isDirectory(path.join(currentDir, ".pi")) || isDirectory(path.join(currentDir, ".agents"));
|
|
252
|
+
if (hasMarker && !seen.has(resolved)) {
|
|
253
|
+
seen.add(resolved);
|
|
254
|
+
levels.push(currentDir);
|
|
255
|
+
}
|
|
256
|
+
if (resolved === gitRoot) break;
|
|
257
|
+
const parentDir = path.dirname(currentDir);
|
|
258
|
+
if (parentDir === currentDir) break;
|
|
259
|
+
currentDir = parentDir;
|
|
260
|
+
}
|
|
261
|
+
return levels.reverse();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function findNearestProjectRoot(cwd: string): string | null {
|
|
265
|
+
let currentDir = cwd;
|
|
266
|
+
while (true) {
|
|
267
|
+
if (isDirectory(path.join(currentDir, ".pi")) || isDirectory(path.join(currentDir, ".agents"))) {
|
|
268
|
+
return currentDir;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const parentDir = path.dirname(currentDir);
|
|
272
|
+
if (parentDir === currentDir) return null;
|
|
273
|
+
currentDir = parentDir;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getUserAgentSettingsPath(): string {
|
|
278
|
+
return path.join(getAgentDir(), "settings.json");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getProjectAgentSettingsPath(cwd: string): string | null {
|
|
282
|
+
const projectRoot = findNearestProjectRoot(cwd);
|
|
283
|
+
return projectRoot ? path.join(projectRoot, ".pi", "settings.json") : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function readSettingsFileStrict(filePath: string): Record<string, unknown> {
|
|
287
|
+
if (!fs.existsSync(filePath)) return {};
|
|
288
|
+
let raw: string;
|
|
289
|
+
try {
|
|
290
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
throw new Error(`Failed to read settings file '${filePath}': ${message}`, { cause: error });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let parsed: unknown;
|
|
297
|
+
try {
|
|
298
|
+
parsed = JSON.parse(raw);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
301
|
+
throw new Error(`Failed to parse settings file '${filePath}': ${message}`, { cause: error });
|
|
302
|
+
}
|
|
303
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
304
|
+
throw new Error(`Settings file '${filePath}' must contain a JSON object.`);
|
|
305
|
+
}
|
|
306
|
+
return parsed as Record<string, unknown>;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function writeSettingsFile(filePath: string, settings: Record<string, unknown>): void {
|
|
310
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
311
|
+
fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseOverrideStringArrayOrFalse(
|
|
315
|
+
value: unknown,
|
|
316
|
+
meta: { filePath: string; name: string; field: string },
|
|
317
|
+
): string[] | false | undefined {
|
|
318
|
+
if (value === undefined) return undefined;
|
|
319
|
+
if (value === false) return false;
|
|
320
|
+
if (!Array.isArray(value)) {
|
|
321
|
+
throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid '${meta.field}'; expected an array of strings or false.`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const items: string[] = [];
|
|
325
|
+
for (const item of value) {
|
|
326
|
+
if (typeof item !== "string") {
|
|
327
|
+
throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid '${meta.field}'; expected an array of strings or false.`);
|
|
328
|
+
}
|
|
329
|
+
const trimmed = item.trim();
|
|
330
|
+
if (trimmed) items.push(trimmed);
|
|
331
|
+
}
|
|
332
|
+
return items;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function parseBuiltinOverrideEntry(
|
|
336
|
+
name: string,
|
|
337
|
+
value: unknown,
|
|
338
|
+
filePath: string,
|
|
339
|
+
): BuiltinAgentOverrideConfig | undefined {
|
|
340
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
341
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' must be an object.`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const input = value as Record<string, unknown>;
|
|
345
|
+
const override: BuiltinAgentOverrideConfig = {};
|
|
346
|
+
|
|
347
|
+
if ("model" in input) {
|
|
348
|
+
if (typeof input.model === "string" || input.model === false) override.model = input.model;
|
|
349
|
+
else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if ("thinking" in input) {
|
|
353
|
+
if (typeof input.thinking === "string" || input.thinking === false) override.thinking = input.thinking;
|
|
354
|
+
else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected a string or false.`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if ("systemPromptMode" in input) {
|
|
358
|
+
if (input.systemPromptMode === "append" || input.systemPromptMode === "replace") {
|
|
359
|
+
override.systemPromptMode = input.systemPromptMode;
|
|
360
|
+
} else {
|
|
361
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPromptMode'; expected 'append' or 'replace'.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if ("inheritProjectContext" in input) {
|
|
366
|
+
if (typeof input.inheritProjectContext === "boolean") {
|
|
367
|
+
override.inheritProjectContext = input.inheritProjectContext;
|
|
368
|
+
} else {
|
|
369
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'inheritProjectContext'; expected a boolean.`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if ("inheritSkills" in input) {
|
|
374
|
+
if (typeof input.inheritSkills === "boolean") {
|
|
375
|
+
override.inheritSkills = input.inheritSkills;
|
|
376
|
+
} else {
|
|
377
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'inheritSkills'; expected a boolean.`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if ("defaultContext" in input) {
|
|
382
|
+
if (input.defaultContext === "fresh" || input.defaultContext === "fork" || input.defaultContext === false) {
|
|
383
|
+
override.defaultContext = input.defaultContext;
|
|
384
|
+
} else {
|
|
385
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'defaultContext'; expected 'fresh', 'fork', or false.`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if ("disabled" in input) {
|
|
390
|
+
if (typeof input.disabled === "boolean") {
|
|
391
|
+
override.disabled = input.disabled;
|
|
392
|
+
} else {
|
|
393
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'disabled'; expected a boolean.`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if ("completionGuard" in input) {
|
|
398
|
+
if (typeof input.completionGuard === "boolean") {
|
|
399
|
+
override.completionGuard = input.completionGuard;
|
|
400
|
+
} else {
|
|
401
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'completionGuard'; expected a boolean.`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if ("systemPrompt" in input) {
|
|
406
|
+
if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt;
|
|
407
|
+
else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPrompt'; expected a string.`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const fallbackModels = parseOverrideStringArrayOrFalse(input.fallbackModels, { filePath, name, field: "fallbackModels" });
|
|
411
|
+
if (fallbackModels !== undefined) override.fallbackModels = fallbackModels;
|
|
412
|
+
|
|
413
|
+
const skills = parseOverrideStringArrayOrFalse(input.skills, { filePath, name, field: "skills" });
|
|
414
|
+
if (skills !== undefined) override.skills = skills;
|
|
415
|
+
|
|
416
|
+
const tools = parseOverrideStringArrayOrFalse(input.tools, { filePath, name, field: "tools" });
|
|
417
|
+
if (Array.isArray(tools)) assertNoMcpDirectTools(tools, `Builtin override '${name}' in '${filePath}'`);
|
|
418
|
+
if (tools !== undefined) override.tools = tools;
|
|
419
|
+
|
|
420
|
+
const toolsPrepend = parseOverrideStringArrayOrFalse(input.toolsPrepend, { filePath, name, field: "toolsPrepend" });
|
|
421
|
+
if (toolsPrepend === false) {
|
|
422
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'toolsPrepend'; expected an array of strings.`);
|
|
423
|
+
}
|
|
424
|
+
if (toolsPrepend !== undefined) {
|
|
425
|
+
assertNoMcpDirectTools(toolsPrepend, `Builtin override '${name}' in '${filePath}'`);
|
|
426
|
+
override.toolsPrepend = toolsPrepend;
|
|
427
|
+
}
|
|
428
|
+
const toolsAppend = parseOverrideStringArrayOrFalse(input.toolsAppend, { filePath, name, field: "toolsAppend" });
|
|
429
|
+
if (toolsAppend === false) {
|
|
430
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'toolsAppend'; expected an array of strings.`);
|
|
431
|
+
}
|
|
432
|
+
if (toolsAppend !== undefined) {
|
|
433
|
+
assertNoMcpDirectTools(toolsAppend, `Builtin override '${name}' in '${filePath}'`);
|
|
434
|
+
override.toolsAppend = toolsAppend;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return Object.keys(override).length > 0 ? override : undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function readSubagentSettings(filePath: string | null): SubagentSettings {
|
|
441
|
+
if (!filePath) return EMPTY_SUBAGENT_SETTINGS;
|
|
442
|
+
const settings = readSettingsFileStrict(filePath);
|
|
443
|
+
const subagents = settings.subagents;
|
|
444
|
+
if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return EMPTY_SUBAGENT_SETTINGS;
|
|
445
|
+
|
|
446
|
+
const subagentsObject = subagents as Record<string, unknown>;
|
|
447
|
+
let disableBuiltins: boolean | undefined;
|
|
448
|
+
if ("disableBuiltins" in subagentsObject) {
|
|
449
|
+
if (typeof subagentsObject.disableBuiltins === "boolean") {
|
|
450
|
+
disableBuiltins = subagentsObject.disableBuiltins;
|
|
451
|
+
} else {
|
|
452
|
+
throw new Error(`Subagent settings in '${filePath}' have invalid 'disableBuiltins'; expected a boolean.`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
|
|
457
|
+
const agentOverrides = subagentsObject.agentOverrides;
|
|
458
|
+
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) {
|
|
459
|
+
return { overrides: parsed, disableBuiltins };
|
|
460
|
+
}
|
|
461
|
+
for (const [name, value] of Object.entries(agentOverrides)) {
|
|
462
|
+
const override = parseBuiltinOverrideEntry(name, value, filePath);
|
|
463
|
+
if (override) parsed[name] = override;
|
|
464
|
+
}
|
|
465
|
+
return { overrides: parsed, disableBuiltins };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Merge project .pi/settings.json across every enumerated level, farthest-first
|
|
469
|
+
// so the nearest level's values land last and win. agentOverrides merge per
|
|
470
|
+
// name with whole-object replacement (disjoint fields do NOT compose);
|
|
471
|
+
// disableBuiltins takes the nearest-defined value as-is. A malformed level
|
|
472
|
+
// (readSubagentSettings throws) is warned and skipped, never aborting the merge.
|
|
473
|
+
function readMergedProjectSubagentSettings(cwd: string): SubagentSettings {
|
|
474
|
+
const levels = enumerateProjectLevels(cwd);
|
|
475
|
+
if (levels.length === 0) return EMPTY_SUBAGENT_SETTINGS;
|
|
476
|
+
|
|
477
|
+
const overrides: Record<string, BuiltinAgentOverrideConfig> = {};
|
|
478
|
+
let disableBuiltins: boolean | undefined;
|
|
479
|
+
for (const level of levels) {
|
|
480
|
+
const settingsPath = path.join(level, ".pi", "settings.json");
|
|
481
|
+
let levelSettings: SubagentSettings;
|
|
482
|
+
try {
|
|
483
|
+
levelSettings = readSubagentSettings(settingsPath);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
486
|
+
console.warn(`Skipping malformed subagent settings at '${settingsPath}': ${message}`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
for (const [name, override] of Object.entries(levelSettings.overrides)) {
|
|
490
|
+
overrides[name] = override;
|
|
491
|
+
}
|
|
492
|
+
if (levelSettings.disableBuiltins !== undefined) {
|
|
493
|
+
disableBuiltins = levelSettings.disableBuiltins;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return { overrides, disableBuiltins };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function composeOverrideTools(
|
|
500
|
+
effective: string[],
|
|
501
|
+
prepend: string[] | undefined,
|
|
502
|
+
append: string[] | undefined,
|
|
503
|
+
): string[] | undefined {
|
|
504
|
+
const seen = new Set<string>();
|
|
505
|
+
const result: string[] = [];
|
|
506
|
+
for (const tool of [...(prepend ?? []), ...effective, ...(append ?? [])]) {
|
|
507
|
+
if (seen.has(tool)) continue;
|
|
508
|
+
seen.add(tool);
|
|
509
|
+
result.push(tool);
|
|
510
|
+
}
|
|
511
|
+
return result.length > 0 ? result : undefined;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function applyBuiltinOverride(
|
|
515
|
+
agent: AgentConfig,
|
|
516
|
+
override: BuiltinAgentOverrideConfig,
|
|
517
|
+
meta: { scope: "user" | "project"; path: string },
|
|
518
|
+
): AgentConfig {
|
|
519
|
+
const next: AgentConfig = {
|
|
520
|
+
...agent,
|
|
521
|
+
override: { ...meta, base: cloneOverrideBase(agent) },
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
if (override.model !== undefined) next.model = override.model === false ? undefined : override.model;
|
|
525
|
+
if (override.fallbackModels !== undefined) {
|
|
526
|
+
next.fallbackModels = override.fallbackModels === false ? undefined : [...override.fallbackModels];
|
|
527
|
+
}
|
|
528
|
+
if (override.thinking !== undefined) next.thinking = override.thinking === false ? undefined : override.thinking;
|
|
529
|
+
if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
|
|
530
|
+
if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext;
|
|
531
|
+
if (override.inheritSkills !== undefined) next.inheritSkills = override.inheritSkills;
|
|
532
|
+
if (override.defaultContext !== undefined) next.defaultContext = override.defaultContext === false ? undefined : override.defaultContext;
|
|
533
|
+
if (override.disabled !== undefined) next.disabled = override.disabled;
|
|
534
|
+
if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
|
|
535
|
+
if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills];
|
|
536
|
+
if (override.tools !== undefined || override.toolsPrepend !== undefined || override.toolsAppend !== undefined) {
|
|
537
|
+
const effective = override.tools === false
|
|
538
|
+
? []
|
|
539
|
+
: override.tools !== undefined
|
|
540
|
+
? override.tools
|
|
541
|
+
: (agent.tools ?? []);
|
|
542
|
+
next.tools = composeOverrideTools(effective, override.toolsPrepend, override.toolsAppend);
|
|
543
|
+
}
|
|
544
|
+
if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
|
|
545
|
+
|
|
546
|
+
return next;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function applyBuiltinOverrides(
|
|
550
|
+
builtinAgents: AgentConfig[],
|
|
551
|
+
userSettings: SubagentSettings,
|
|
552
|
+
projectSettings: SubagentSettings,
|
|
553
|
+
userSettingsPath: string,
|
|
554
|
+
projectSettingsPath: string | null,
|
|
555
|
+
): AgentConfig[] {
|
|
556
|
+
const projectBulkDisabled = projectSettings.disableBuiltins === true && projectSettingsPath !== null;
|
|
557
|
+
const userBulkDisabled = projectSettings.disableBuiltins === undefined && userSettings.disableBuiltins === true;
|
|
558
|
+
|
|
559
|
+
return builtinAgents.map((agent) => {
|
|
560
|
+
const projectOverride = projectSettings.overrides[agent.name];
|
|
561
|
+
if (projectOverride && projectSettingsPath) {
|
|
562
|
+
return applyBuiltinOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (projectBulkDisabled && projectSettingsPath) {
|
|
566
|
+
return applyBuiltinOverride(agent, { disabled: true }, { scope: "project", path: projectSettingsPath });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const userOverride = userSettings.overrides[agent.name];
|
|
570
|
+
if (userOverride) {
|
|
571
|
+
return applyBuiltinOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (userBulkDisabled) {
|
|
575
|
+
return applyBuiltinOverride(agent, { disabled: true }, { scope: "user", path: userSettingsPath });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return agent;
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Custom (user/project) agents pre-fill fields from their frontmatter, so
|
|
583
|
+
// agentOverrides[name] only fills fields the frontmatter left unset. This
|
|
584
|
+
// keeps frontmatter as the per-agent source of truth and lets settings.json
|
|
585
|
+
// supply per-harness defaults (model, thinking, ...) for shared personas.
|
|
586
|
+
// Bulk disableBuiltins intentionally does not touch custom agents.
|
|
587
|
+
function applyCustomAgentOverride(
|
|
588
|
+
agent: AgentConfig,
|
|
589
|
+
override: BuiltinAgentOverrideConfig,
|
|
590
|
+
meta: { scope: "user" | "project"; path: string },
|
|
591
|
+
): AgentConfig {
|
|
592
|
+
const next: AgentConfig = { ...agent };
|
|
593
|
+
let anyFilled = false;
|
|
594
|
+
|
|
595
|
+
const fill = <T>(field: keyof AgentConfig, currentlyUnset: boolean, value: T) => {
|
|
596
|
+
if (!currentlyUnset) return;
|
|
597
|
+
(next as Record<string, unknown>)[field as string] = value;
|
|
598
|
+
anyFilled = true;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
if (override.model !== undefined) {
|
|
602
|
+
fill("model", agent.model === undefined, override.model === false ? undefined : override.model);
|
|
603
|
+
}
|
|
604
|
+
if (override.fallbackModels !== undefined) {
|
|
605
|
+
fill(
|
|
606
|
+
"fallbackModels",
|
|
607
|
+
agent.fallbackModels === undefined,
|
|
608
|
+
override.fallbackModels === false ? undefined : [...override.fallbackModels],
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
if (override.thinking !== undefined) {
|
|
612
|
+
fill("thinking", agent.thinking === undefined, override.thinking === false ? undefined : override.thinking);
|
|
613
|
+
}
|
|
614
|
+
if (override.systemPromptMode !== undefined) {
|
|
615
|
+
fill("systemPromptMode", agent.systemPromptMode === undefined, override.systemPromptMode);
|
|
616
|
+
}
|
|
617
|
+
if (override.inheritProjectContext !== undefined) {
|
|
618
|
+
fill("inheritProjectContext", agent.inheritProjectContext === undefined, override.inheritProjectContext);
|
|
619
|
+
}
|
|
620
|
+
if (override.inheritSkills !== undefined) {
|
|
621
|
+
fill("inheritSkills", agent.inheritSkills === undefined, override.inheritSkills);
|
|
622
|
+
}
|
|
623
|
+
if (override.defaultContext !== undefined) {
|
|
624
|
+
fill(
|
|
625
|
+
"defaultContext",
|
|
626
|
+
agent.defaultContext === undefined,
|
|
627
|
+
override.defaultContext === false ? undefined : override.defaultContext,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
if (override.disabled !== undefined) {
|
|
631
|
+
fill("disabled", agent.disabled === undefined, override.disabled);
|
|
632
|
+
}
|
|
633
|
+
if (override.systemPrompt !== undefined) {
|
|
634
|
+
fill("systemPrompt", agent.systemPrompt === undefined, override.systemPrompt);
|
|
635
|
+
}
|
|
636
|
+
if (override.skills !== undefined) {
|
|
637
|
+
fill(
|
|
638
|
+
"skills",
|
|
639
|
+
agent.skills === undefined,
|
|
640
|
+
override.skills === false ? undefined : [...override.skills],
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (override.tools !== undefined || override.toolsPrepend !== undefined || override.toolsAppend !== undefined) {
|
|
644
|
+
const replacementApplies = agent.tools === undefined && override.tools !== undefined;
|
|
645
|
+
const effective = replacementApplies
|
|
646
|
+
? (override.tools === false ? [] : override.tools as string[])
|
|
647
|
+
: (agent.tools ?? []);
|
|
648
|
+
if (replacementApplies || override.toolsPrepend !== undefined || override.toolsAppend !== undefined) {
|
|
649
|
+
next.tools = composeOverrideTools(effective, override.toolsPrepend, override.toolsAppend);
|
|
650
|
+
anyFilled = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (override.completionGuard !== undefined) {
|
|
654
|
+
fill("completionGuard", agent.completionGuard === undefined, override.completionGuard);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (!anyFilled) return agent;
|
|
658
|
+
next.override = { ...meta, base: cloneOverrideBase(agent) };
|
|
659
|
+
return next;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function applyCustomAgentOverrides(
|
|
663
|
+
agents: AgentConfig[],
|
|
664
|
+
userSettings: SubagentSettings,
|
|
665
|
+
projectSettings: SubagentSettings,
|
|
666
|
+
userSettingsPath: string,
|
|
667
|
+
projectSettingsPath: string | null,
|
|
668
|
+
): AgentConfig[] {
|
|
669
|
+
return agents.map((agent) => {
|
|
670
|
+
const projectOverride = projectSettings.overrides[agent.name];
|
|
671
|
+
if (projectOverride && projectSettingsPath) {
|
|
672
|
+
return applyCustomAgentOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
|
|
673
|
+
}
|
|
674
|
+
const userOverride = userSettings.overrides[agent.name];
|
|
675
|
+
if (userOverride) {
|
|
676
|
+
return applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
|
|
677
|
+
}
|
|
678
|
+
return agent;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function buildBuiltinOverrideConfig(
|
|
683
|
+
base: BuiltinAgentOverrideBase,
|
|
684
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "completionGuard">,
|
|
685
|
+
): BuiltinAgentOverrideConfig | undefined {
|
|
686
|
+
const override: BuiltinAgentOverrideConfig = {};
|
|
687
|
+
|
|
688
|
+
if (draft.model !== base.model) override.model = draft.model ?? false;
|
|
689
|
+
if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
|
|
690
|
+
if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
|
|
691
|
+
if (draft.systemPromptMode !== base.systemPromptMode) override.systemPromptMode = draft.systemPromptMode;
|
|
692
|
+
if (draft.inheritProjectContext !== base.inheritProjectContext) override.inheritProjectContext = draft.inheritProjectContext;
|
|
693
|
+
if (draft.inheritSkills !== base.inheritSkills) override.inheritSkills = draft.inheritSkills;
|
|
694
|
+
if (draft.defaultContext !== base.defaultContext) override.defaultContext = draft.defaultContext ?? false;
|
|
695
|
+
if (draft.disabled !== base.disabled) override.disabled = draft.disabled ?? false;
|
|
696
|
+
if (draft.systemPrompt !== base.systemPrompt) override.systemPrompt = draft.systemPrompt;
|
|
697
|
+
if (!arraysEqual(draft.skills, base.skills)) override.skills = draft.skills ? [...draft.skills] : false;
|
|
698
|
+
if (!arraysEqual(draft.tools, base.tools)) override.tools = draft.tools ? [...draft.tools] : false;
|
|
699
|
+
if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) {
|
|
700
|
+
override.completionGuard = draft.completionGuard !== false;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return Object.keys(override).length > 0 ? override : undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export function saveBuiltinAgentOverride(
|
|
707
|
+
cwd: string,
|
|
708
|
+
name: string,
|
|
709
|
+
scope: "user" | "project",
|
|
710
|
+
override: BuiltinAgentOverrideConfig,
|
|
711
|
+
): string {
|
|
712
|
+
// Reads merge project settings across every level cwd->git root (see
|
|
713
|
+
// readMergedProjectSubagentSettings), but writes target only the NEAREST
|
|
714
|
+
// project root: edits from a subdir land at the closest writable anchor
|
|
715
|
+
// instead of silently mutating an ancestor's settings.
|
|
716
|
+
const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath();
|
|
717
|
+
if (!filePath) throw new Error("Project override is not available here. No project config root was found.");
|
|
718
|
+
|
|
719
|
+
const settings = readSettingsFileStrict(filePath);
|
|
720
|
+
const subagents = settings.subagents && typeof settings.subagents === "object" && !Array.isArray(settings.subagents)
|
|
721
|
+
? { ...(settings.subagents as Record<string, unknown>) }
|
|
722
|
+
: {};
|
|
723
|
+
const agentOverrides = subagents.agentOverrides && typeof subagents.agentOverrides === "object" && !Array.isArray(subagents.agentOverrides)
|
|
724
|
+
? { ...(subagents.agentOverrides as Record<string, unknown>) }
|
|
725
|
+
: {};
|
|
726
|
+
|
|
727
|
+
agentOverrides[name] = cloneOverrideValue(override);
|
|
728
|
+
subagents.agentOverrides = agentOverrides;
|
|
729
|
+
settings.subagents = subagents;
|
|
730
|
+
writeSettingsFile(filePath, settings);
|
|
731
|
+
return filePath;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "user" | "project"): string {
|
|
735
|
+
const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath();
|
|
736
|
+
if (!filePath) throw new Error("Project override is not available here. No project config root was found.");
|
|
737
|
+
if (!fs.existsSync(filePath)) return filePath;
|
|
738
|
+
|
|
739
|
+
const settings = readSettingsFileStrict(filePath);
|
|
740
|
+
const subagents = settings.subagents;
|
|
741
|
+
if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return filePath;
|
|
742
|
+
const nextSubagents = { ...(subagents as Record<string, unknown>) };
|
|
743
|
+
const agentOverrides = nextSubagents.agentOverrides;
|
|
744
|
+
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return filePath;
|
|
745
|
+
|
|
746
|
+
const nextOverrides = { ...(agentOverrides as Record<string, unknown>) };
|
|
747
|
+
delete nextOverrides[name];
|
|
748
|
+
if (Object.keys(nextOverrides).length > 0) nextSubagents.agentOverrides = nextOverrides;
|
|
749
|
+
else delete nextSubagents.agentOverrides;
|
|
750
|
+
|
|
751
|
+
if (Object.keys(nextSubagents).length > 0) settings.subagents = nextSubagents;
|
|
752
|
+
else delete settings.subagents;
|
|
753
|
+
|
|
754
|
+
writeSettingsFile(filePath, settings);
|
|
755
|
+
return filePath;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Subtrees never scanned for agents/chains. `skills/` holds skill packages
|
|
759
|
+
// (each with a `SKILL.md` carrying name+description frontmatter that would
|
|
760
|
+
// otherwise be mistaken for an agent persona).
|
|
761
|
+
// Discovery reads agent/chain roots FLAT (top-level entries only). Subdirectories
|
|
762
|
+
// are reserved for typed content (skills/, chains/) and are never scanned for
|
|
763
|
+
// personas, so a repo's skills/<name>/SKILL.md is never mistaken for an agent.
|
|
764
|
+
function listFilesFlat(dir: string, predicate: (fileName: string) => boolean): string[] {
|
|
765
|
+
if (!fs.existsSync(dir)) return [];
|
|
766
|
+
|
|
767
|
+
let entries: fs.Dirent[];
|
|
768
|
+
try {
|
|
769
|
+
entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
770
|
+
} catch {
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const files: string[] = [];
|
|
775
|
+
for (const entry of entries) {
|
|
776
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
777
|
+
if (!predicate(entry.name)) continue;
|
|
778
|
+
files.push(path.join(dir, entry.name));
|
|
779
|
+
}
|
|
780
|
+
return files;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// A persona file is a top-level *.md that is neither a chain definition nor a
|
|
784
|
+
// skill manifest. SKILL.md is excluded by name because it carries name+description
|
|
785
|
+
// frontmatter and would otherwise be loaded as an agent.
|
|
786
|
+
function isAgentFileName(fileName: string): boolean {
|
|
787
|
+
return fileName.endsWith(".md") && !fileName.endsWith(".chain.md") && fileName !== "SKILL.md";
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function isChainFileName(fileName: string): boolean {
|
|
791
|
+
return fileName.endsWith(".chain.md") || fileName.endsWith(".chain.json");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
795
|
+
const agents: AgentConfig[] = [];
|
|
796
|
+
|
|
797
|
+
for (const filePath of listFilesFlat(dir, isAgentFileName)) {
|
|
798
|
+
let content: string;
|
|
799
|
+
try {
|
|
800
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
801
|
+
} catch {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
806
|
+
|
|
807
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const localName = frontmatter.name;
|
|
812
|
+
const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`);
|
|
813
|
+
if (parsedPackage.error) continue;
|
|
814
|
+
const packageName = parsedPackage.packageName;
|
|
815
|
+
const runtimeName = buildRuntimeName(localName, packageName);
|
|
816
|
+
|
|
817
|
+
const tools = frontmatter.tools
|
|
818
|
+
?.split(",")
|
|
819
|
+
.map((t) => t.trim())
|
|
820
|
+
.filter(Boolean);
|
|
821
|
+
if (tools) assertNoMcpDirectTools(tools, `Agent '${localName}' in '${filePath}'`);
|
|
822
|
+
|
|
823
|
+
const defaultReads = frontmatter.defaultReads
|
|
824
|
+
?.split(",")
|
|
825
|
+
.map((f) => f.trim())
|
|
826
|
+
.filter(Boolean);
|
|
827
|
+
|
|
828
|
+
const skillStr = frontmatter.skill || frontmatter.skills;
|
|
829
|
+
const skills = skillStr
|
|
830
|
+
?.split(",")
|
|
831
|
+
.map((s) => s.trim())
|
|
832
|
+
.filter(Boolean);
|
|
833
|
+
const fallbackModels = frontmatter.fallbackModels
|
|
834
|
+
?.split(",")
|
|
835
|
+
.map((model) => model.trim())
|
|
836
|
+
.filter(Boolean);
|
|
837
|
+
const systemPromptMode = frontmatter.systemPromptMode === "replace"
|
|
838
|
+
? "replace"
|
|
839
|
+
: frontmatter.systemPromptMode === "append"
|
|
840
|
+
? "append"
|
|
841
|
+
: defaultSystemPromptMode(localName);
|
|
842
|
+
const inheritProjectContext = frontmatter.inheritProjectContext === "true"
|
|
843
|
+
? true
|
|
844
|
+
: frontmatter.inheritProjectContext === "false"
|
|
845
|
+
? false
|
|
846
|
+
: defaultInheritProjectContext(localName);
|
|
847
|
+
const inheritSkills = frontmatter.inheritSkills === "true"
|
|
848
|
+
? true
|
|
849
|
+
: frontmatter.inheritSkills === "false"
|
|
850
|
+
? false
|
|
851
|
+
: defaultInheritSkills();
|
|
852
|
+
const defaultContext = frontmatter.defaultContext === "fork"
|
|
853
|
+
? "fork" as const
|
|
854
|
+
: frontmatter.defaultContext === "fresh"
|
|
855
|
+
? "fresh" as const
|
|
856
|
+
: undefined;
|
|
857
|
+
|
|
858
|
+
let extensions: string[] | undefined;
|
|
859
|
+
if (frontmatter.extensions !== undefined) {
|
|
860
|
+
extensions = frontmatter.extensions
|
|
861
|
+
.split(",")
|
|
862
|
+
.map((e) => e.trim())
|
|
863
|
+
.filter(Boolean);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const extraFields: Record<string, string> = {};
|
|
867
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
868
|
+
if (!KNOWN_FIELDS.has(key)) extraFields[key] = value;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
|
|
872
|
+
const completionGuard = frontmatter.completionGuard === "false"
|
|
873
|
+
? false
|
|
874
|
+
: frontmatter.completionGuard === "true"
|
|
875
|
+
? true
|
|
876
|
+
: undefined;
|
|
877
|
+
|
|
878
|
+
agents.push({
|
|
879
|
+
name: runtimeName,
|
|
880
|
+
localName,
|
|
881
|
+
packageName,
|
|
882
|
+
description: frontmatter.description,
|
|
883
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
884
|
+
model: frontmatter.model,
|
|
885
|
+
fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : undefined,
|
|
886
|
+
thinking: frontmatter.thinking,
|
|
887
|
+
systemPromptMode,
|
|
888
|
+
inheritProjectContext,
|
|
889
|
+
inheritSkills,
|
|
890
|
+
defaultContext,
|
|
891
|
+
systemPrompt: body,
|
|
892
|
+
source,
|
|
893
|
+
filePath,
|
|
894
|
+
skills: skills && skills.length > 0 ? skills : undefined,
|
|
895
|
+
extensions,
|
|
896
|
+
output: frontmatter.output,
|
|
897
|
+
defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
|
|
898
|
+
defaultProgress: frontmatter.defaultProgress === "true",
|
|
899
|
+
interactive: frontmatter.interactive === "true",
|
|
900
|
+
maxSubagentDepth:
|
|
901
|
+
Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
|
|
902
|
+
? parsedMaxSubagentDepth
|
|
903
|
+
: undefined,
|
|
904
|
+
completionGuard,
|
|
905
|
+
extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return agents;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function loadChainsFromDir(dir: string, source: "user" | "project"): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
913
|
+
const chains = new Map<string, ChainConfig>();
|
|
914
|
+
const diagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
915
|
+
|
|
916
|
+
for (const filePath of listFilesFlat(dir, isChainFileName)) {
|
|
917
|
+
let content: string;
|
|
918
|
+
try {
|
|
919
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
920
|
+
} catch {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
try {
|
|
925
|
+
const chain = filePath.endsWith(".chain.json") ? parseJsonChain(content, source, filePath) : parseChain(content, source, filePath);
|
|
926
|
+
const existing = chains.get(chain.name);
|
|
927
|
+
if (existing && existing.filePath.endsWith(".chain.json") && filePath.endsWith(".chain.md")) continue;
|
|
928
|
+
chains.set(chain.name, chain);
|
|
929
|
+
} catch (error) {
|
|
930
|
+
diagnostics.push({ source, filePath, error: error instanceof Error ? error.message : String(error) });
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return { chains: Array.from(chains.values()), diagnostics };
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function isDirectory(p: string): boolean {
|
|
939
|
+
try {
|
|
940
|
+
return fs.statSync(p).isDirectory();
|
|
941
|
+
} catch {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Single source of truth for agent persona discovery precedence.
|
|
947
|
+
//
|
|
948
|
+
// Roots are listed LOWEST -> HIGHEST priority. Downstream merging is
|
|
949
|
+
// last-writer-wins (Map.set / mergeAgentsForScope), so a later root overrides an
|
|
950
|
+
// earlier one on name collision. Builtins are loaded separately from
|
|
951
|
+
// BUILTIN_AGENTS_DIR and sit BELOW every root here. Project discovery is NOT a
|
|
952
|
+
// single root: enumerateProjectLevels walks cwd -> git root and aggregates every
|
|
953
|
+
// level that has .pi or .agents (farthest-first), so each level contributes its
|
|
954
|
+
// own .agents / .pi/agents pair and nearest wins:
|
|
955
|
+
//
|
|
956
|
+
// builtin
|
|
957
|
+
// < ~/.agents global, cross-harness convention
|
|
958
|
+
// < <PI_CODING_AGENT_DIR>/agents pi profile (PI_CODING_AGENT_DIR defaults to ~/.pi/agent)
|
|
959
|
+
// < <farthest project level>/.agents project, legacy layout
|
|
960
|
+
// < <farthest project level>/.pi/agents project, preferred layout
|
|
961
|
+
// < ... intermediate levels
|
|
962
|
+
// < <nearest project level>/.agents
|
|
963
|
+
// < <nearest project level>/.pi/agents (highest)
|
|
964
|
+
//
|
|
965
|
+
// Any project level outranks all user roots; nearest project level wins; within a
|
|
966
|
+
// level .pi/agents beats .agents. With no git root, the walk falls back to the
|
|
967
|
+
// single nearest project root (findNearestProjectRoot). PI_CODING_AGENT_DIR
|
|
968
|
+
// relocates the pi profile root; it does NOT sandbox discovery. ~/.agents is
|
|
969
|
+
// always scanned regardless of PI_CODING_AGENT_DIR.
|
|
970
|
+
//
|
|
971
|
+
// All roots are read flat (see listFilesFlat): only top-level *.md personas are
|
|
972
|
+
// loaded; subdirectories such as skills/ are never scanned.
|
|
973
|
+
function resolveUserAgentDirs(): string[] {
|
|
974
|
+
return [
|
|
975
|
+
path.join(os.homedir(), ".agents"),
|
|
976
|
+
path.join(getAgentDir(), "agents"),
|
|
977
|
+
];
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Highest-priority user root: the canonical location for creating new user
|
|
981
|
+
// agents and the value surfaced as `userDir`.
|
|
982
|
+
function preferredUserAgentDir(): string {
|
|
983
|
+
const dirs = resolveUserAgentDirs();
|
|
984
|
+
return dirs[dirs.length - 1];
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Dedup expanded read dirs by realpath, keeping the NEAREST occurrence and
|
|
988
|
+
// repositioning it last among distinct dirs (the consuming Map is last-writer-
|
|
989
|
+
// wins). Walking from the end makes a nearest .pi that symlinks to a farther
|
|
990
|
+
// real dir win over a same-level .agents, which first-position dedup inverts.
|
|
991
|
+
function dedupeByRealPath(dirs: string[]): string[] {
|
|
992
|
+
const seen = new Set<string>();
|
|
993
|
+
const result: string[] = [];
|
|
994
|
+
for (let i = dirs.length - 1; i >= 0; i--) {
|
|
995
|
+
const real = resolveRealPath(dirs[i]);
|
|
996
|
+
if (seen.has(real)) continue;
|
|
997
|
+
seen.add(real);
|
|
998
|
+
result.push(dirs[i]);
|
|
999
|
+
}
|
|
1000
|
+
return result.reverse();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
1004
|
+
const levels = enumerateProjectLevels(cwd);
|
|
1005
|
+
if (levels.length === 0) return { readDirs: [], preferredDir: null };
|
|
1006
|
+
|
|
1007
|
+
const candidates: string[] = [];
|
|
1008
|
+
for (const level of levels) {
|
|
1009
|
+
const legacyDir = path.join(level, ".agents");
|
|
1010
|
+
const preferredDir = path.join(level, ".pi", "agents");
|
|
1011
|
+
if (isDirectory(legacyDir)) candidates.push(legacyDir);
|
|
1012
|
+
if (isDirectory(preferredDir)) candidates.push(preferredDir);
|
|
1013
|
+
}
|
|
1014
|
+
const nearestLevel = levels[levels.length - 1];
|
|
1015
|
+
return { readDirs: dedupeByRealPath(candidates), preferredDir: path.join(nearestLevel, ".pi", "agents") };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
1019
|
+
const levels = enumerateProjectLevels(cwd);
|
|
1020
|
+
if (levels.length === 0) return { readDirs: [], preferredDir: null };
|
|
1021
|
+
|
|
1022
|
+
const candidates: string[] = [];
|
|
1023
|
+
for (const level of levels) {
|
|
1024
|
+
const chainsDir = path.join(level, ".pi", "chains");
|
|
1025
|
+
if (isDirectory(chainsDir)) candidates.push(chainsDir);
|
|
1026
|
+
}
|
|
1027
|
+
const nearestLevel = levels[levels.length - 1];
|
|
1028
|
+
return { readDirs: dedupeByRealPath(candidates), preferredDir: path.join(nearestLevel, ".pi", "chains") };
|
|
1029
|
+
}
|
|
1030
|
+
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
1031
|
+
|
|
1032
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
1033
|
+
const userDirs = resolveUserAgentDirs();
|
|
1034
|
+
const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(cwd);
|
|
1035
|
+
const userSettingsPath = getUserAgentSettingsPath();
|
|
1036
|
+
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
1037
|
+
const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
|
|
1038
|
+
const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readMergedProjectSubagentSettings(cwd);
|
|
1039
|
+
|
|
1040
|
+
const builtinAgents = applyBuiltinOverrides(
|
|
1041
|
+
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
1042
|
+
userSettings,
|
|
1043
|
+
projectSettings,
|
|
1044
|
+
userSettingsPath,
|
|
1045
|
+
projectSettingsPath,
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
const userAgents = applyCustomAgentOverrides(
|
|
1049
|
+
scope === "project" ? [] : userDirs.flatMap((dir) => loadAgentsFromDir(dir, "user")),
|
|
1050
|
+
userSettings,
|
|
1051
|
+
projectSettings,
|
|
1052
|
+
userSettingsPath,
|
|
1053
|
+
projectSettingsPath,
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
const projectAgents = applyCustomAgentOverrides(
|
|
1057
|
+
scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project")),
|
|
1058
|
+
userSettings,
|
|
1059
|
+
projectSettings,
|
|
1060
|
+
userSettingsPath,
|
|
1061
|
+
projectSettingsPath,
|
|
1062
|
+
);
|
|
1063
|
+
const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents)
|
|
1064
|
+
.filter((agent) => agent.disabled !== true);
|
|
1065
|
+
|
|
1066
|
+
return { agents, projectAgentsDir };
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export function discoverAgentsAll(cwd: string): {
|
|
1070
|
+
builtin: AgentConfig[];
|
|
1071
|
+
user: AgentConfig[];
|
|
1072
|
+
project: AgentConfig[];
|
|
1073
|
+
chains: ChainConfig[];
|
|
1074
|
+
chainDiagnostics: ChainDiscoveryDiagnostic[];
|
|
1075
|
+
userDir: string;
|
|
1076
|
+
projectDir: string | null;
|
|
1077
|
+
userChainDir: string;
|
|
1078
|
+
projectChainDir: string | null;
|
|
1079
|
+
userSettingsPath: string;
|
|
1080
|
+
projectSettingsPath: string | null;
|
|
1081
|
+
} {
|
|
1082
|
+
const userDirs = resolveUserAgentDirs();
|
|
1083
|
+
const userChainDir = getUserChainDir();
|
|
1084
|
+
const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
|
|
1085
|
+
const { readDirs: projectChainDirs, preferredDir: projectChainDir } = resolveNearestProjectChainDirs(cwd);
|
|
1086
|
+
const userSettingsPath = getUserAgentSettingsPath();
|
|
1087
|
+
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
1088
|
+
const userSettings = readSubagentSettings(userSettingsPath);
|
|
1089
|
+
const projectSettings = readMergedProjectSubagentSettings(cwd);
|
|
1090
|
+
|
|
1091
|
+
const builtin = applyBuiltinOverrides(
|
|
1092
|
+
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
1093
|
+
userSettings,
|
|
1094
|
+
projectSettings,
|
|
1095
|
+
userSettingsPath,
|
|
1096
|
+
projectSettingsPath,
|
|
1097
|
+
);
|
|
1098
|
+
const user = applyCustomAgentOverrides(
|
|
1099
|
+
userDirs.flatMap((dir) => loadAgentsFromDir(dir, "user")),
|
|
1100
|
+
userSettings,
|
|
1101
|
+
projectSettings,
|
|
1102
|
+
userSettingsPath,
|
|
1103
|
+
projectSettingsPath,
|
|
1104
|
+
);
|
|
1105
|
+
const projectMap = new Map<string, AgentConfig>();
|
|
1106
|
+
for (const dir of projectDirs) {
|
|
1107
|
+
for (const agent of loadAgentsFromDir(dir, "project")) {
|
|
1108
|
+
projectMap.set(agent.name, agent);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const project = applyCustomAgentOverrides(
|
|
1112
|
+
Array.from(projectMap.values()),
|
|
1113
|
+
userSettings,
|
|
1114
|
+
projectSettings,
|
|
1115
|
+
userSettingsPath,
|
|
1116
|
+
projectSettingsPath,
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
const chainMap = new Map<string, ChainConfig>();
|
|
1120
|
+
const projectChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
1121
|
+
for (const dir of projectChainDirs) {
|
|
1122
|
+
const loaded = loadChainsFromDir(dir, "project");
|
|
1123
|
+
projectChainDiagnostics.push(...loaded.diagnostics);
|
|
1124
|
+
for (const chain of loaded.chains) {
|
|
1125
|
+
chainMap.set(chain.name, chain);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const userChains = loadChainsFromDir(userChainDir, "user");
|
|
1129
|
+
const chains = [
|
|
1130
|
+
...userChains.chains,
|
|
1131
|
+
...Array.from(chainMap.values()),
|
|
1132
|
+
];
|
|
1133
|
+
const chainDiagnostics = [
|
|
1134
|
+
...userChains.diagnostics,
|
|
1135
|
+
...projectChainDiagnostics,
|
|
1136
|
+
];
|
|
1137
|
+
|
|
1138
|
+
const userDir = preferredUserAgentDir();
|
|
1139
|
+
|
|
1140
|
+
return { builtin, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
1141
|
+
}
|