/** * Agent discovery and configuration */ import { execSync } from "node:child_process"; import * as fs from "node:fs"; import { parse as parseYaml } from "yaml"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import type { AcceptanceInput, AcceptanceRole, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts"; import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts"; import { KNOWN_FIELDS } from "./agent-serializer.ts"; import { parseChain, parseJsonChain } from "./chain-serializer.ts"; import { mergeAgentsForScope } from "./agent-selection.ts"; import { parseFrontmatter, parseFrontmatterList } from "./frontmatter.ts"; import { buildRuntimeName, parsePackageName } from "./identity.ts"; import { parseModelScopeConfig, type ModelScopeConfig } from "../runs/shared/model-scope.ts"; import { parseMemoryFrontmatter } from "./agent-memory.ts"; import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts"; import { validateAcceptanceInput } from "../runs/shared/acceptance.ts"; import { EMPTY_SUBAGENT_SETTINGS, agentFrontmatterFields, arraysEqual, cloneOverrideBase, cloneOverrideValue, defaultInheritProjectContext, defaultInheritSkills, defaultSystemPromptMode, effectiveAgentMatch, joinToolList, normalizeAgentAliases, resolveAgentName, splitToolList, BUILTIN_AGENT_NAMES, type AgentConfig, type AgentScope, type AgentSource, type AgentDiscoveryResult, type BuiltinAgentOverrideBase, type BuiltinAgentOverrideConfig, type BuiltinAgentOverrideInfo, type ChainConfig, type ChainDiscoveryDiagnostic, type SubagentSettings, type ProjectRootResolution, } from "./agent-types.ts"; export function getUserChainDir(): string { return path.join(getAgentDir(), "chains"); } interface PackageSubagentPaths { agents: string[]; chains: string[]; } let cachedGlobalNpmRoot: string | null = null; function readJsonFileBestEffort(filePath: string): unknown { try { return JSON.parse(fs.readFileSync(filePath, "utf-8")); } catch { // Installed package scans are opportunistic; bad third-party manifests // should not break local agent discovery. return null; } } function readOptionalJsonFile(filePath: string): unknown { try { return JSON.parse(fs.readFileSync(filePath, "utf-8")); } catch (error) { const code = typeof error === "object" && error !== null && "code" in error ? (error as { code?: unknown }).code : undefined; if (code === "ENOENT") return null; throw error; } } function isSafePackagePath(value: string): boolean { return value.length > 0 && !path.isAbsolute(value) && value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== ".."); } function parseNpmPackageName(source: string): string | undefined { const spec = source.slice(4).trim(); if (!spec) return undefined; const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/); const packageName = match?.[1] ?? spec; return isSafePackagePath(packageName) ? packageName : undefined; } function stripGitRef(repoPath: string): string { const atIndex = repoPath.indexOf("@"); const hashIndex = repoPath.indexOf("#"); const refIndex = [atIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0]; return refIndex === undefined ? repoPath : repoPath.slice(0, refIndex); } function parseGitPackagePath(source: string): { host: string; repoPath: string } | undefined { const spec = source.slice(4).trim(); if (!spec) return undefined; let host = ""; let repoPath = ""; const scpLike = spec.match(/^git@([^:]+):(.+)$/); if (scpLike) { host = scpLike[1] ?? ""; repoPath = scpLike[2] ?? ""; } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(spec)) { try { const url = new URL(spec); host = url.hostname; repoPath = url.pathname.replace(/^\/+/, ""); } catch { return undefined; } } else { const slashIndex = spec.indexOf("/"); if (slashIndex < 0) return undefined; host = spec.slice(0, slashIndex); repoPath = spec.slice(slashIndex + 1); } const normalizedPath = stripGitRef(repoPath).replace(/\.git$/, "").replace(/^\/+/, ""); if (!host || !isSafePackagePath(host) || !isSafePackagePath(normalizedPath) || normalizedPath.split(/[\\/]/).length < 2) { return undefined; } return { host, repoPath: normalizedPath }; } function resolveSettingsPackageRoot(source: string, baseDir: string): string | undefined { const trimmed = source.trim(); if (!trimmed) return undefined; if (trimmed.startsWith("git:")) { const parsed = parseGitPackagePath(trimmed); return parsed ? path.join(baseDir, "git", parsed.host, parsed.repoPath) : undefined; } if (trimmed.startsWith("npm:")) { const packageName = parseNpmPackageName(trimmed); return packageName ? path.join(baseDir, "npm", "node_modules", packageName) : undefined; } const normalized = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed; if (normalized === "~") return os.homedir(); if (normalized.startsWith("~/")) return path.join(os.homedir(), normalized.slice(2)); if (path.isAbsolute(normalized)) return normalized; if (normalized === "." || normalized === ".." || normalized.startsWith("./") || normalized.startsWith("../")) { return path.resolve(baseDir, normalized); } return undefined; } function getGlobalNpmRoot(): string | null { const offline = process.env.PI_OFFLINE?.toLowerCase(); if (offline === "1" || offline === "true" || offline === "yes") return null; if (cachedGlobalNpmRoot !== null) return cachedGlobalNpmRoot; try { cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim()); return cachedGlobalNpmRoot; } catch { cachedGlobalNpmRoot = ""; return null; } } function stringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0); } function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubagentPaths { const packageJsonPath = path.join(packageRoot, "package.json"); const pkg = readJsonFileBestEffort(packageJsonPath); if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [] }; const roots: Record[] = []; const piSubagents = (pkg as { "pi-agents-flow"?: unknown })["pi-agents-flow"]; if (piSubagents && typeof piSubagents === "object" && !Array.isArray(piSubagents)) { roots.push(piSubagents as Record); } const pi = (pkg as { pi?: unknown }).pi; if (pi && typeof pi === "object" && !Array.isArray(pi)) { const subagents = (pi as { subagents?: unknown }).subagents; if (subagents && typeof subagents === "object" && !Array.isArray(subagents)) { roots.push(subagents as Record); } } const agents: string[] = []; const chains: string[] = []; for (const root of roots) { for (const entry of stringArray(root.agents)) agents.push(path.resolve(packageRoot, entry)); for (const entry of stringArray(root.chains)) chains.push(path.resolve(packageRoot, entry)); } return { agents, chains }; } function collectPackageRootsFromNodeModules(nodeModulesDir: string): string[] { const roots: string[] = []; if (!fs.existsSync(nodeModulesDir)) return roots; let entries: fs.Dirent[]; try { entries = fs.readdirSync(nodeModulesDir, { withFileTypes: true }); } catch { return roots; } for (const entry of entries) { if (entry.name.startsWith(".")) continue; if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; if (entry.name.startsWith("@")) { const scopeDir = path.join(nodeModulesDir, entry.name); let scopeEntries: fs.Dirent[]; try { scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true }); } catch { continue; } for (const scopeEntry of scopeEntries) { if (scopeEntry.name.startsWith(".")) continue; if (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink()) continue; roots.push(path.join(scopeDir, scopeEntry.name)); } continue; } roots.push(path.join(nodeModulesDir, entry.name)); } return roots; } function collectSettingsPackageRoots(settingsFile: string, baseDir: string): string[] { const settings = readOptionalJsonFile(settingsFile); if (!settings || typeof settings !== "object" || Array.isArray(settings)) return []; const packages = (settings as { packages?: unknown }).packages; if (!Array.isArray(packages)) return []; const roots: string[] = []; for (const entry of packages) { const packageSource = typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && typeof (entry as { source?: unknown }).source === "string" ? (entry as { source: string }).source : undefined; if (!packageSource) continue; const packageRoot = resolveSettingsPackageRoot(packageSource, baseDir); if (packageRoot) roots.push(packageRoot); } return roots; } export function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolean; includeProject: boolean } = { includeUser: true, includeProject: true }): PackageSubagentPaths { const agentDir = getAgentDir(); const projectRoot = findConfiguredProjectRoot(cwd) ?? cwd; const packageRoots = [ projectRoot, ]; if (options.includeProject) { const projectConfigDir = getProjectConfigDir(projectRoot); packageRoots.push( ...collectPackageRootsFromNodeModules(path.join(projectConfigDir, "npm", "node_modules")), ...collectSettingsPackageRoots(path.join(projectConfigDir, "settings.json"), projectConfigDir), ); } if (options.includeUser) { packageRoots.push( ...collectPackageRootsFromNodeModules(path.join(agentDir, "npm", "node_modules")), ...collectSettingsPackageRoots(path.join(agentDir, "settings.json"), agentDir), ); } if (options.includeUser) { const globalRoot = getGlobalNpmRoot(); if (globalRoot) packageRoots.push(...collectPackageRootsFromNodeModules(globalRoot)); } const seenRoots = new Set(); const seenAgents = new Set(); const seenChains = new Set(); const agents: string[] = []; const chains: string[] = []; for (const packageRoot of packageRoots) { const resolvedRoot = path.resolve(packageRoot); if (seenRoots.has(resolvedRoot)) continue; seenRoots.add(resolvedRoot); const paths = extractSubagentPathsFromPackageRoot(resolvedRoot); for (const agentDir of paths.agents) { if (seenAgents.has(agentDir)) continue; seenAgents.add(agentDir); agents.push(agentDir); } for (const chainDir of paths.chains) { if (seenChains.has(chainDir)) continue; seenChains.add(chainDir); chains.push(chainDir); } } return { agents, chains }; } function isProjectRootCandidate(dir: string): boolean { return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, ".agents")); } function findProjectRootCandidates(cwd: string): string[] { const roots: string[] = []; let currentDir = cwd; while (true) { if (isProjectRootCandidate(currentDir)) roots.push(currentDir); const parentDir = path.dirname(currentDir); if (parentDir === currentDir) return roots; currentDir = parentDir; } } function findNearestGitRoot(cwd: string): string | null { let currentDir = cwd; while (true) { if (fs.existsSync(path.join(currentDir, ".git"))) return currentDir; const parentDir = path.dirname(currentDir); if (parentDir === currentDir) return null; currentDir = parentDir; } } function readProjectRootResolution(projectRoot: string): ProjectRootResolution | undefined { const settingsPath = path.join(getProjectConfigDir(projectRoot), "settings.json"); if (!fs.existsSync(settingsPath)) return undefined; const settings = readSettingsFileStrict(settingsPath); const subagents = settings.subagents; if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return undefined; const value = (subagents as Record).projectRootResolution; if (value === undefined) return undefined; if (value === "nearest" || value === "git-root") return value; throw new Error(`Subagent settings in '${settingsPath}' have invalid 'projectRootResolution'; expected 'nearest' or 'git-root'.`); } export function findNearestProjectRoot(cwd: string): string | null { return findProjectRootCandidates(cwd)[0] ?? null; } function findConfiguredProjectRoot(cwd: string): string | null { const candidates = findProjectRootCandidates(cwd); const nearestRoot = candidates[0]; if (!nearestRoot) return null; const nearestMode = readProjectRootResolution(nearestRoot); if (nearestMode === "nearest") return nearestRoot; const gitRoot = findNearestGitRoot(cwd); const gitProjectRoot = gitRoot ? candidates.find((candidate) => path.resolve(candidate) === path.resolve(gitRoot)) : undefined; if (gitProjectRoot && (nearestMode === "git-root" || readProjectRootResolution(gitProjectRoot) === "git-root")) { return gitProjectRoot; } return nearestRoot; } export function getUserAgentSettingsPath(): string { return path.join(getAgentDir(), "settings.json"); } export function getProjectAgentSettingsPath(cwd: string): string | null { const projectRoot = findConfiguredProjectRoot(cwd); return projectRoot ? path.join(getProjectConfigDir(projectRoot), "settings.json") : null; } function readSettingsFileStrict(filePath: string): Record { if (!fs.existsSync(filePath)) return {}; let raw: string; try { raw = fs.readFileSync(filePath, "utf-8"); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Failed to read settings file '${filePath}': ${message}`, { cause: error }); } let parsed: unknown; try { parsed = JSON.parse(raw); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Failed to parse settings file '${filePath}': ${message}`, { cause: error }); } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error(`Settings file '${filePath}' must contain a JSON object.`); } return parsed as Record; } function writeSettingsFile(filePath: string, settings: Record): void { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n", "utf-8"); } function parseOverrideStringArrayOrFalse( value: unknown, meta: { filePath: string; name: string; field: string }, ): string[] | false | undefined { if (value === undefined) return undefined; if (value === false) return false; if (!Array.isArray(value)) { throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid '${meta.field}'; expected an array of strings or false.`); } const items: string[] = []; for (const item of value) { if (typeof item !== "string") { throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid '${meta.field}'; expected an array of strings or false.`); } const trimmed = item.trim(); if (trimmed) items.push(trimmed); } return items; } function parseBuiltinOverrideEntry( name: string, value: unknown, filePath: string, ): BuiltinAgentOverrideConfig | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`Builtin override '${name}' in '${filePath}' must be an object.`); } const input = value as Record; const override: BuiltinAgentOverrideConfig = {}; if ("description" in input) { if (typeof input.description === "string" && input.description.trim()) { override.description = input.description.trim(); } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'description'; expected a non-empty string.`); } } if ("model" in input) { if (typeof input.model === "string" || input.model === false) override.model = input.model; else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`); } if ("thinking" in input) { if (typeof input.thinking === "string" || input.thinking === false) override.thinking = input.thinking; else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected a string or false.`); } if ("systemPromptMode" in input) { if (input.systemPromptMode === "append" || input.systemPromptMode === "replace") { override.systemPromptMode = input.systemPromptMode; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPromptMode'; expected 'append' or 'replace'.`); } } if ("inheritProjectContext" in input) { if (typeof input.inheritProjectContext === "boolean") { override.inheritProjectContext = input.inheritProjectContext; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'inheritProjectContext'; expected a boolean.`); } } if ("inheritSkills" in input) { if (typeof input.inheritSkills === "boolean") { override.inheritSkills = input.inheritSkills; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'inheritSkills'; expected a boolean.`); } } if ("defaultContext" in input) { if (input.defaultContext === "fresh" || input.defaultContext === "fork" || input.defaultContext === false) { override.defaultContext = input.defaultContext; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'defaultContext'; expected 'fresh', 'fork', or false.`); } } if ("acceptanceRole" in input) { if (input.acceptanceRole === "read-only" || input.acceptanceRole === "writer" || input.acceptanceRole === false) { override.acceptanceRole = input.acceptanceRole; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'acceptanceRole'; expected 'read-only', 'writer', or false.`); } } if ("disabled" in input) { if (typeof input.disabled === "boolean") { override.disabled = input.disabled; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'disabled'; expected a boolean.`); } } if ("visibility" in input) { if (input.visibility === "default" || input.visibility === "hidden" || input.visibility === false) { override.visibility = input.visibility; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'visibility'; expected 'default', 'hidden', or false.`); } } if ("invocation" in input) { if (input.invocation === "both" || input.invocation === "model" || input.invocation === "user" || input.invocation === "disabled" || input.invocation === false) { override.invocation = input.invocation; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'invocation'; expected 'both', 'model', 'user', 'disabled', or false.`); } } if ("completionGuard" in input) { if (typeof input.completionGuard === "boolean") { override.completionGuard = input.completionGuard; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'completionGuard'; expected a boolean.`); } } if ("toolBudget" in input) { if (input.toolBudget === false) { override.toolBudget = false; } else if (input.toolBudget && typeof input.toolBudget === "object" && !Array.isArray(input.toolBudget)) { override.toolBudget = input.toolBudget as ToolBudgetConfig; } else { throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'toolBudget'; expected an object or false.`); } } if ("systemPrompt" in input) { if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt; else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPrompt'; expected a string.`); } const fallbackModels = parseOverrideStringArrayOrFalse(input.fallbackModels, { filePath, name, field: "fallbackModels" }); if (fallbackModels !== undefined) override.fallbackModels = fallbackModels; const skills = parseOverrideStringArrayOrFalse(input.skills, { filePath, name, field: "skills" }); if (skills !== undefined) override.skills = skills; const tools = parseOverrideStringArrayOrFalse(input.tools, { filePath, name, field: "tools" }); if (tools !== undefined) override.tools = tools; const extensions = parseOverrideStringArrayOrFalse(input.extensions, { filePath, name, field: "extensions" }); if (extensions !== undefined) override.extensions = extensions; const subagentOnlyExtensions = parseOverrideStringArrayOrFalse(input.subagentOnlyExtensions, { filePath, name, field: "subagentOnlyExtensions" }); if (subagentOnlyExtensions !== undefined) override.subagentOnlyExtensions = subagentOnlyExtensions; return Object.keys(override).length > 0 ? override : undefined; } export function readSubagentSettings(filePath: string | null): SubagentSettings { if (!filePath) return EMPTY_SUBAGENT_SETTINGS; const settings = readSettingsFileStrict(filePath); const subagents = settings.subagents; if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return EMPTY_SUBAGENT_SETTINGS; const subagentsObject = subagents as Record; let disableBuiltins: boolean | undefined; if ("disableBuiltins" in subagentsObject) { if (typeof subagentsObject.disableBuiltins === "boolean") { disableBuiltins = subagentsObject.disableBuiltins; } else { throw new Error(`Subagent settings in '${filePath}' have invalid 'disableBuiltins'; expected a boolean.`); } } let disableThinking: boolean | undefined; if ("disableThinking" in subagentsObject) { if (typeof subagentsObject.disableThinking === "boolean") { disableThinking = subagentsObject.disableThinking; } else { throw new Error(`Subagent settings in '${filePath}' have invalid 'disableThinking'; expected a boolean.`); } } let defaultModel: string | undefined; if ("defaultModel" in subagentsObject) { if (typeof subagentsObject.defaultModel === "string" && subagentsObject.defaultModel.trim()) { defaultModel = subagentsObject.defaultModel.trim(); } else { throw new Error(`Subagent settings in '${filePath}' have invalid 'defaultModel'; expected a non-empty string.`); } } let defaultThinking: string | undefined; if ("defaultThinking" in subagentsObject) { if (typeof subagentsObject.defaultThinking === "string" && subagentsObject.defaultThinking.trim()) { defaultThinking = subagentsObject.defaultThinking.trim(); } else { throw new Error(`Subagent settings in '${filePath}' have invalid 'defaultThinking'; expected a non-empty string.`); } } let defaultExtensions: string[] | undefined; if ("defaultExtensions" in subagentsObject) { if (!Array.isArray(subagentsObject.defaultExtensions) || subagentsObject.defaultExtensions.some((item) => typeof item !== "string" || !item.trim())) { throw new Error(`Subagent settings in '${filePath}' have invalid 'defaultExtensions'; expected an array of non-empty strings.`); } defaultExtensions = subagentsObject.defaultExtensions.map((item) => item.trim()); } const modelScope = parseModelScopeConfig(subagentsObject.modelScope, { filePath }); const parsed: Record = {}; const agentOverrides = subagentsObject.agentOverrides; if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) { return { overrides: parsed, defaultModel, defaultThinking, defaultExtensions, disableBuiltins, disableThinking, modelScope }; } for (const [name, value] of Object.entries(agentOverrides)) { const override = parseBuiltinOverrideEntry(name, value, filePath); if (override) parsed[name] = override; } return { overrides: parsed, defaultModel, defaultThinking, defaultExtensions, disableBuiltins, disableThinking, modelScope }; } export function resolveSubagentDefaultModel( userSettings: SubagentSettings, projectSettings: SubagentSettings, userSettingsPath: string, projectSettingsPath: string | null, ): AgentModelSourceInfo | undefined { if (projectSettingsPath && projectSettings.defaultModel !== undefined) { return { type: "subagents.defaultModel", scope: "project", path: projectSettingsPath, model: projectSettings.defaultModel }; } return userSettings.defaultModel !== undefined ? { type: "subagents.defaultModel", scope: "user", path: userSettingsPath, model: userSettings.defaultModel } : undefined; } function applySubagentDefaultModel(agents: AgentConfig[], defaultModel: AgentModelSourceInfo | undefined): AgentConfig[] { if (!defaultModel) return agents; return agents.map((agent) => { if (agent.model !== undefined) return agent; const next = { ...agent, model: defaultModel.model, modelSource: defaultModel }; const frontmatterFields = agentFrontmatterFields.get(agent); if (frontmatterFields) agentFrontmatterFields.set(next, frontmatterFields); return next; }); } export function resolveSubagentDefaultThinking( userSettings: SubagentSettings, projectSettings: SubagentSettings, projectSettingsPath: string | null, ): string | undefined { if (projectSettingsPath && projectSettings.defaultThinking !== undefined) return projectSettings.defaultThinking; return userSettings.defaultThinking; } function applySubagentDefaultThinking(agents: AgentConfig[], defaultThinking: string | undefined): AgentConfig[] { if (defaultThinking === undefined) return agents; return agents.map((agent) => { if (agent.thinking !== undefined) return agent; const next = { ...agent, thinking: defaultThinking }; const frontmatterFields = agentFrontmatterFields.get(agent); if (frontmatterFields) agentFrontmatterFields.set(next, frontmatterFields); return next; }); } export function resolveSubagentDefaultExtensions( userSettings: SubagentSettings, projectSettings: SubagentSettings, projectSettingsPath: string | null, ): string[] | undefined { if (projectSettingsPath && projectSettings.defaultExtensions !== undefined) return projectSettings.defaultExtensions; return userSettings.defaultExtensions; } function applySubagentDefaultExtensions(agents: AgentConfig[], defaultExtensions: string[] | undefined): AgentConfig[] { if (defaultExtensions === undefined) return agents; return agents.map((agent) => { if (agent.extensions !== undefined) return agent; const next = { ...agent, extensions: [...defaultExtensions], extensionsFromDefault: true }; const frontmatterFields = agentFrontmatterFields.get(agent); if (frontmatterFields) agentFrontmatterFields.set(next, frontmatterFields); return next; }); } export function applySubagentDefaults( agents: AgentConfig[], defaultModel: AgentModelSourceInfo | undefined, defaultThinking: string | undefined, defaultExtensions: string[] | undefined, ): AgentConfig[] { return applySubagentDefaultExtensions( applySubagentDefaultThinking(applySubagentDefaultModel(agents, defaultModel), defaultThinking), defaultExtensions, ); } function applyBuiltinOverride( agent: AgentConfig, override: BuiltinAgentOverrideConfig, meta: { scope: "user" | "project"; path: string }, ): AgentConfig { const next: AgentConfig = { ...agent, override: { ...meta, base: cloneOverrideBase(agent) }, }; if (override.description !== undefined) next.description = override.description; if (override.model !== undefined) next.model = override.model === false ? undefined : override.model; if (override.fallbackModels !== undefined) { next.fallbackModels = override.fallbackModels === false ? undefined : [...override.fallbackModels]; } if (override.thinking !== undefined) next.thinking = override.thinking === false ? undefined : override.thinking; if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode; if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext; if (override.inheritSkills !== undefined) next.inheritSkills = override.inheritSkills; if (override.defaultContext !== undefined) next.defaultContext = override.defaultContext === false ? undefined : override.defaultContext; if (override.acceptanceRole !== undefined) next.acceptanceRole = override.acceptanceRole === false ? undefined : override.acceptanceRole; if (override.disabled !== undefined) next.disabled = override.disabled; if (override.visibility !== undefined) next.visibility = override.visibility === false ? undefined : override.visibility; if (override.invocation !== undefined) next.invocation = override.invocation === false ? undefined : override.invocation; if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt; if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills]; if (override.tools !== undefined) { const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools); next.tools = tools; next.mcpDirectTools = mcpDirectTools; } if (override.extensions !== undefined) next.extensions = override.extensions === false ? undefined : [...override.extensions]; if (override.subagentOnlyExtensions !== undefined) { next.subagentOnlyExtensions = override.subagentOnlyExtensions === false ? undefined : [...override.subagentOnlyExtensions]; } if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard; if (override.toolBudget !== undefined) next.toolBudget = override.toolBudget === false ? undefined : override.toolBudget; return next; } function clearBuiltinThinking(agent: AgentConfig, meta: { scope: "user" | "project"; path: string }): AgentConfig { if (agent.thinking === undefined) return agent; return { ...agent, thinking: undefined, override: agent.override ?? { ...meta, base: cloneOverrideBase(agent) }, }; } export function applyBuiltinOverrides( builtinAgents: AgentConfig[], userSettings: SubagentSettings, projectSettings: SubagentSettings, userSettingsPath: string, projectSettingsPath: string | null, ): AgentConfig[] { const projectBulkDisabled = projectSettings.disableBuiltins === true && projectSettingsPath !== null; const userBulkDisabled = projectSettings.disableBuiltins === undefined && userSettings.disableBuiltins === true; const projectThinkingConfigured = projectSettings.disableThinking !== undefined && projectSettingsPath !== null; const disableThinking = projectThinkingConfigured ? projectSettings.disableThinking === true : userSettings.disableThinking === true; const disableThinkingMeta = projectThinkingConfigured ? { scope: "project" as const, path: projectSettingsPath! } : { scope: "user" as const, path: userSettingsPath }; const applyGlobalThinking = (agent: AgentConfig, hasExplicitThinkingOverride: boolean): AgentConfig => { if (!disableThinking || hasExplicitThinkingOverride) return agent; return clearBuiltinThinking(agent, disableThinkingMeta); }; return builtinAgents.map((agent) => { const projectOverride = projectSettings.overrides[agent.name]; if (projectOverride && projectSettingsPath) { return applyGlobalThinking( applyBuiltinOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath }), projectOverride.thinking !== undefined, ); } if (projectBulkDisabled && projectSettingsPath) { return applyGlobalThinking( applyBuiltinOverride(agent, { disabled: true }, { scope: "project", path: projectSettingsPath }), false, ); } const userOverride = userSettings.overrides[agent.name]; if (userOverride) { return applyGlobalThinking( applyBuiltinOverride(agent, userOverride, { scope: "user", path: userSettingsPath }), !projectThinkingConfigured && userOverride.thinking !== undefined, ); } if (userBulkDisabled) { return applyGlobalThinking( applyBuiltinOverride(agent, { disabled: true }, { scope: "user", path: userSettingsPath }), false, ); } return applyGlobalThinking(agent, false); }); } export function agentHasFrontmatterField(agent: AgentConfig, ...fields: string[]): boolean { const frontmatterFields = agentFrontmatterFields.get(agent); return frontmatterFields ? fields.some((field) => frontmatterFields.has(field)) : false; } function applyCustomAgentOverride( agent: AgentConfig, override: BuiltinAgentOverrideConfig, meta: { scope: "user" | "project"; path: string }, ): AgentConfig { let next: AgentConfig | undefined; let anyFilled = false; const mutable = (): AgentConfig => { next ??= { ...agent }; return next; }; const fill = ( field: K, frontmatterFields: string[], value: AgentConfig[K], ): void => { if (agentHasFrontmatterField(agent, ...frontmatterFields)) return; mutable()[field] = value; anyFilled = true; }; if (override.description !== undefined) { mutable().description = override.description; anyFilled = true; } if (override.model !== undefined) { fill("model", ["model"], override.model === false ? undefined : override.model); } if (override.fallbackModels !== undefined) { fill( "fallbackModels", ["fallbackModels"], override.fallbackModels === false ? undefined : [...override.fallbackModels], ); } if (override.thinking !== undefined) { fill("thinking", ["thinking"], override.thinking === false ? undefined : override.thinking); } if (override.systemPromptMode !== undefined) { fill("systemPromptMode", ["systemPromptMode"], override.systemPromptMode); } if (override.inheritProjectContext !== undefined) { fill("inheritProjectContext", ["inheritProjectContext"], override.inheritProjectContext); } if (override.inheritSkills !== undefined) { fill("inheritSkills", ["inheritSkills"], override.inheritSkills); } if (override.defaultContext !== undefined) { fill("defaultContext", ["defaultContext"], override.defaultContext === false ? undefined : override.defaultContext); } if (override.acceptanceRole !== undefined) { fill("acceptanceRole", ["acceptanceRole"], override.acceptanceRole === false ? undefined : override.acceptanceRole); } if (override.disabled !== undefined && agent.disabled === undefined) { mutable().disabled = override.disabled; anyFilled = true; } if (override.visibility !== undefined) { fill("visibility", ["visibility"], override.visibility === false ? undefined : override.visibility); } if (override.invocation !== undefined) { fill("invocation", ["invocation"], override.invocation === false ? undefined : override.invocation); } if (override.skills !== undefined) { fill("skills", ["skill", "skills"], override.skills === false ? undefined : [...override.skills]); } if (override.tools !== undefined && !agentHasFrontmatterField(agent, "tools")) { const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools); const target = mutable(); target.tools = tools; target.mcpDirectTools = mcpDirectTools; anyFilled = true; } if (override.extensions !== undefined) { fill("extensions", ["extensions"], override.extensions === false ? undefined : [...override.extensions]); } if (override.subagentOnlyExtensions !== undefined) { fill( "subagentOnlyExtensions", ["subagentOnlyExtensions"], override.subagentOnlyExtensions === false ? undefined : [...override.subagentOnlyExtensions], ); } if (override.completionGuard !== undefined) { fill("completionGuard", ["completionGuard"], override.completionGuard); } if (override.toolBudget !== undefined) { fill("toolBudget", ["toolBudget"], override.toolBudget === false ? undefined : override.toolBudget); } if (!anyFilled || !next) return agent; next.override = { ...meta, base: cloneOverrideBase(agent) }; const frontmatterFields = agentFrontmatterFields.get(agent); if (frontmatterFields) agentFrontmatterFields.set(next, frontmatterFields); return next; } export function applyCustomAgentOverrides( agents: AgentConfig[], userSettings: SubagentSettings, projectSettings: SubagentSettings, userSettingsPath: string, projectSettingsPath: string | null, ): AgentConfig[] { return agents.map((agent) => { const projectOverride = projectSettings.overrides[agent.name]; if (projectOverride && projectSettingsPath) { return applyCustomAgentOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath }); } const userOverride = userSettings.overrides[agent.name]; if (userOverride) { return applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath }); } return agent; }); } export function buildBuiltinOverrideConfig( base: BuiltinAgentOverrideBase, draft: Pick & Partial>, ): BuiltinAgentOverrideConfig | undefined { const override: BuiltinAgentOverrideConfig = {}; if (draft.description !== undefined) { const description = draft.description.trim(); if (description && description !== base.description) override.description = description; } if (draft.model !== base.model) override.model = draft.model ?? false; if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false; if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false; if (draft.systemPromptMode !== base.systemPromptMode) override.systemPromptMode = draft.systemPromptMode; if (draft.inheritProjectContext !== base.inheritProjectContext) override.inheritProjectContext = draft.inheritProjectContext; if (draft.inheritSkills !== base.inheritSkills) override.inheritSkills = draft.inheritSkills; if (draft.defaultContext !== base.defaultContext) override.defaultContext = draft.defaultContext ?? false; if (draft.acceptanceRole !== base.acceptanceRole) override.acceptanceRole = draft.acceptanceRole ?? false; if (draft.disabled !== base.disabled) override.disabled = draft.disabled ?? false; if (draft.visibility !== base.visibility) override.visibility = draft.visibility ?? false; if (draft.invocation !== base.invocation) override.invocation = draft.invocation ?? false; if (draft.systemPrompt !== base.systemPrompt) override.systemPrompt = draft.systemPrompt; if (!arraysEqual(draft.skills, base.skills)) override.skills = draft.skills ? [...draft.skills] : false; const baseTools = joinToolList(base); const draftTools = joinToolList(draft); if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false; if (!arraysEqual(draft.extensions, base.extensions)) override.extensions = draft.extensions ? [...draft.extensions] : false; if (!arraysEqual(draft.subagentOnlyExtensions, base.subagentOnlyExtensions)) { override.subagentOnlyExtensions = draft.subagentOnlyExtensions ? [...draft.subagentOnlyExtensions] : false; } if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) { override.completionGuard = draft.completionGuard !== false; } if (JSON.stringify(draft.toolBudget) !== JSON.stringify(base.toolBudget)) override.toolBudget = draft.toolBudget ?? false; return Object.keys(override).length > 0 ? override : undefined; } export function saveBuiltinAgentOverride( cwd: string, name: string, scope: "user" | "project", override: BuiltinAgentOverrideConfig, ): string { const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath(); if (!filePath) throw new Error("Project override is not available here. No project config root was found."); const settings = readSettingsFileStrict(filePath); const subagents = settings.subagents && typeof settings.subagents === "object" && !Array.isArray(settings.subagents) ? { ...(settings.subagents as Record) } : {}; const agentOverrides = subagents.agentOverrides && typeof subagents.agentOverrides === "object" && !Array.isArray(subagents.agentOverrides) ? { ...(subagents.agentOverrides as Record) } : {}; agentOverrides[name] = cloneOverrideValue(override); subagents.agentOverrides = agentOverrides; settings.subagents = subagents; writeSettingsFile(filePath, settings); return filePath; } export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "user" | "project"): { path: string; removed: boolean } { const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath(); if (!filePath) throw new Error("Project override is not available here. No project config root was found."); if (!fs.existsSync(filePath)) return { path: filePath, removed: false }; const settings = readSettingsFileStrict(filePath); const subagents = settings.subagents; if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return { path: filePath, removed: false }; const nextSubagents = { ...(subagents as Record) }; const agentOverrides = nextSubagents.agentOverrides; if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return { path: filePath, removed: false }; const nextOverrides = { ...(agentOverrides as Record) }; if (!Object.prototype.hasOwnProperty.call(nextOverrides, name)) return { path: filePath, removed: false }; delete nextOverrides[name]; if (Object.keys(nextOverrides).length > 0) nextSubagents.agentOverrides = nextOverrides; else delete nextSubagents.agentOverrides; if (Object.keys(nextSubagents).length > 0) settings.subagents = nextSubagents; else delete settings.subagents; writeSettingsFile(filePath, settings); return { path: filePath, removed: true }; } export function mergeBuiltinAgentOverride( cwd: string, name: string, scope: "user" | "project", fields: BuiltinAgentOverrideConfig, ): string { const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath(); if (!filePath) throw new Error("Project override is not available here. No project config root was found."); const settings = readSettingsFileStrict(filePath); const subagents = settings.subagents && typeof settings.subagents === "object" && !Array.isArray(settings.subagents) ? { ...(settings.subagents as Record) } : {}; const agentOverrides = subagents.agentOverrides && typeof subagents.agentOverrides === "object" && !Array.isArray(subagents.agentOverrides) ? { ...(subagents.agentOverrides as Record) } : {}; const existing = agentOverrides[name]; const base = existing && typeof existing === "object" && !Array.isArray(existing) ? existing as Record : {}; agentOverrides[name] = { ...base, ...cloneOverrideValue(fields) }; subagents.agentOverrides = agentOverrides; settings.subagents = subagents; writeSettingsFile(filePath, settings); return filePath; } export function removeBuiltinAgentOverrideFields( cwd: string, name: string, scope: "user" | "project", fields: string[], ): { path: string; removed: boolean } { const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath(); if (!filePath) throw new Error("Project override is not available here. No project config root was found."); if (!fs.existsSync(filePath)) return { path: filePath, removed: false }; const settings = readSettingsFileStrict(filePath); const subagents = settings.subagents; if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return { path: filePath, removed: false }; const agentOverrides = (subagents as Record).agentOverrides; if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return { path: filePath, removed: false }; const entry = (agentOverrides as Record)[name]; if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { path: filePath, removed: false }; const nextEntry: Record = { ...(entry as Record) }; let removed = false; for (const field of fields) { if (Object.prototype.hasOwnProperty.call(nextEntry, field)) { delete nextEntry[field]; removed = true; } } if (!removed) return { path: filePath, removed: false }; const nextSubagents = { ...(subagents as Record) }; if (Object.keys(nextEntry).length > 0) { (nextSubagents.agentOverrides as Record)[name] = nextEntry; } else { const nextOverrides = { ...(agentOverrides as Record) }; delete nextOverrides[name]; if (Object.keys(nextOverrides).length > 0) nextSubagents.agentOverrides = nextOverrides; else delete nextSubagents.agentOverrides; } if (Object.keys(nextSubagents).length > 0) settings.subagents = nextSubagents; else delete settings.subagents; writeSettingsFile(filePath, settings); return { path: filePath, removed: true }; } const DISCOVERY_PRUNED_DIR_NAMES = new Set([".git", "node_modules"]); function isDiscoveryNestedProjectRoot(dir: string): boolean { return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, ".agents")); } function shouldPruneDiscoveryDir(rootDir: string, dir: string, dirName: string): boolean { if (DISCOVERY_PRUNED_DIR_NAMES.has(dirName)) return true; if (fs.existsSync(path.join(dir, ".git"))) return true; return path.resolve(dir) !== path.resolve(rootDir) && isDiscoveryNestedProjectRoot(dir); } function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean, rootDir = dir): string[] { const files: string[] = []; if (!fs.existsSync(dir)) return files; let entries: fs.Dirent[]; try { entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); } catch { return files; } for (const entry of entries) { const filePath = path.join(dir, entry.name); if (entry.isDirectory()) { if (!shouldPruneDiscoveryDir(rootDir, filePath, entry.name)) { files.push(...listFilesRecursive(filePath, predicate, rootDir)); } continue; } if (!entry.isFile() && !entry.isSymbolicLink()) continue; if (!predicate(entry.name)) continue; files.push(filePath); } return files; } function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean { const relative = path.relative(rootDir, filePath); const parts = relative.split(path.sep).map((part) => part.toLowerCase()); if (path.basename(rootDir).toLowerCase() === ".agents") { parts.unshift(".agents"); } return parts.some((part, index) => part === ".agents" && parts[index + 1] === "skills"); } function parseAgentAcceptanceFrontmatter(raw: string | undefined, agentName: string): AcceptanceInput | undefined { if (raw === undefined || !raw.trim()) return undefined; let parsed: unknown; try { parsed = parseYaml(raw); } catch (error) { throw new Error(`Agent '${agentName}' has invalid acceptance frontmatter: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } const errors = validateAcceptanceInput(parsed, `Agent '${agentName}' acceptance frontmatter`); if (errors.length > 0) throw new Error(errors.join(" ")); return parsed as AcceptanceInput; } export function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] { const agents: AgentConfig[] = []; for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) { if (isLegacyAgentSkillPath(dir, filePath)) { continue; } let content: string; try { content = fs.readFileSync(filePath, "utf-8"); } catch { continue; } const { frontmatter, body } = parseFrontmatter(content); if (!frontmatter.name || !frontmatter.description) { continue; } const localName = frontmatter.name; const parsedPackage = parsePackageName(frontmatter.package, `Agent '${localName}' package`); if (parsedPackage.error) continue; const packageName = parsedPackage.packageName; const runtimeName = buildRuntimeName(localName, packageName); const rawTools = parseFrontmatterList(frontmatter.tools); const parsedTools = splitToolList(rawTools); const tools = parsedTools.tools ?? []; const mcpDirectTools = parsedTools.mcpDirectTools ?? []; const defaultReads = parseFrontmatterList(frontmatter.defaultReads); const aliases = normalizeAgentAliases(parseFrontmatterList(frontmatter.aliases ?? frontmatter.alias), runtimeName); const skillStr = frontmatter.skill || frontmatter.skills; const skills = parseFrontmatterList(skillStr); const skillPath = parseFrontmatterList(frontmatter.skillPath); const fallbackModels = parseFrontmatterList(frontmatter.fallbackModels); const systemPromptMode = frontmatter.systemPromptMode === "replace" ? "replace" : frontmatter.systemPromptMode === "append" ? "append" : defaultSystemPromptMode(localName); const inheritProjectContext = frontmatter.inheritProjectContext === "true" ? true : frontmatter.inheritProjectContext === "false" ? false : defaultInheritProjectContext(localName); const inheritSkills = frontmatter.inheritSkills === "true" ? true : frontmatter.inheritSkills === "false" ? false : defaultInheritSkills(); const defaultContext = frontmatter.defaultContext === "fork" ? "fork" as const : frontmatter.defaultContext === "fresh" ? "fresh" as const : undefined; let defaultAsync: boolean | undefined; if (frontmatter.async !== undefined) { if (frontmatter.async === "true") defaultAsync = true; else if (frontmatter.async === "false") defaultAsync = false; else throw new Error(`Agent '${localName}' has invalid async frontmatter; expected true or false.`); } let defaultTimeoutMs: number | undefined; if (frontmatter.timeoutMs !== undefined) { const parsed = Number(frontmatter.timeoutMs); if (!Number.isInteger(parsed) || parsed <= 0) { throw new Error(`Agent '${localName}' has invalid timeoutMs frontmatter; expected a positive integer.`); } defaultTimeoutMs = parsed; } let defaultTurnBudget: TurnBudgetConfig | undefined; if (frontmatter.turnBudget !== undefined && frontmatter.turnBudget.trim()) { const parsed = JSON.parse(frontmatter.turnBudget) as unknown; const resolved = resolveTurnBudgetConfig(parsed, `Agent '${localName}' turnBudget frontmatter`); if (resolved.error) throw new Error(resolved.error); defaultTurnBudget = resolved.turnBudget; } const defaultAcceptance = parseAgentAcceptanceFrontmatter(frontmatter.acceptance, localName); let acceptanceRole: AcceptanceRole | undefined; if (frontmatter.acceptanceRole !== undefined && frontmatter.acceptanceRole.trim()) { if (frontmatter.acceptanceRole === "read-only" || frontmatter.acceptanceRole === "writer") acceptanceRole = frontmatter.acceptanceRole; else throw new Error(`Agent '${localName}' has invalid acceptanceRole frontmatter; expected 'read-only' or 'writer'.`); } let visibility: AgentVisibility | undefined; if (frontmatter.visibility !== undefined && frontmatter.visibility.trim()) { if (frontmatter.visibility === "default" || frontmatter.visibility === "hidden") visibility = frontmatter.visibility; else throw new Error(`Agent '${localName}' has invalid visibility frontmatter; expected 'default' or 'hidden'.`); } let invocation: AgentInvocation | undefined; if (frontmatter.invocation !== undefined && frontmatter.invocation.trim()) { if (frontmatter.invocation === "both" || frontmatter.invocation === "model" || frontmatter.invocation === "user" || frontmatter.invocation === "disabled") invocation = frontmatter.invocation; else throw new Error(`Agent '${localName}' has invalid invocation frontmatter; expected 'both', 'model', 'user', or 'disabled'.`); } const extensions = parseFrontmatterList(frontmatter.extensions); const subagentOnlyExtensions = parseFrontmatterList(frontmatter.subagentOnlyExtensions); const extraFields: Record = {}; for (const [key, value] of Object.entries(frontmatter)) { if (!KNOWN_FIELDS.has(key)) extraFields[key] = value; } const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth); let toolBudget: ToolBudgetConfig | undefined; if (frontmatter.toolBudget !== undefined && frontmatter.toolBudget.trim()) { const parsed = JSON.parse(frontmatter.toolBudget) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error(`Agent '${localName}' has invalid toolBudget frontmatter; expected a JSON object.`); } toolBudget = parsed as ToolBudgetConfig; } const completionGuard = frontmatter.completionGuard === "false" ? false : frontmatter.completionGuard === "true" ? true : undefined; const agent: AgentConfig = { name: runtimeName, localName, packageName, description: frontmatter.description, aliases, tools: rawTools !== undefined ? tools : undefined, mcpDirectTools: mcpDirectTools.length > 0 ? mcpDirectTools : undefined, model: frontmatter.model, fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : undefined, thinking: frontmatter.thinking === "false" ? false : frontmatter.thinking, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, defaultAsync, defaultTimeoutMs, defaultTurnBudget, defaultAcceptance, acceptanceRole, visibility, invocation, systemPrompt: body, source, filePath, skills: skills && skills.length > 0 ? skills : undefined, skillPath: skillPath && skillPath.length > 0 ? skillPath : undefined, extensions, subagentOnlyExtensions, output: frontmatter.output, defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined, defaultProgress: frontmatter.defaultProgress === "true", interactive: frontmatter.interactive === "true", maxSubagentDepth: Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0 ? parsedMaxSubagentDepth : undefined, completionGuard, toolBudget, memory: parseMemoryFrontmatter(frontmatter.memory), extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined, }; agentFrontmatterFields.set(agent, new Set(Object.keys(frontmatter))); agents.push(agent); } return agents; } export function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } { const chains = new Map(); const diagnostics: ChainDiscoveryDiagnostic[] = []; for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".chain.md") || fileName.endsWith(".chain.json"))) { let content: string; try { content = fs.readFileSync(filePath, "utf-8"); } catch { continue; } try { const chain = filePath.endsWith(".chain.json") ? parseJsonChain(content, source, filePath) : parseChain(content, source, filePath); const existing = chains.get(chain.name); if (existing && existing.filePath.endsWith(".chain.json") && filePath.endsWith(".chain.md")) continue; chains.set(chain.name, chain); } catch (error) { diagnostics.push({ source, filePath, error: error instanceof Error ? error.message : String(error) }); continue; } } return { chains: Array.from(chains.values()), diagnostics }; } function isDirectory(p: string): boolean { try { return fs.statSync(p).isDirectory(); } catch { return false; } } export function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } { const projectRoot = findConfiguredProjectRoot(cwd); if (!projectRoot) return { readDirs: [], preferredDir: null }; const legacyDir = path.join(projectRoot, ".agents"); const preferredDir = path.join(getProjectConfigDir(projectRoot), "agents"); const readDirs: string[] = []; if (isDirectory(legacyDir)) readDirs.push(legacyDir); if (isDirectory(preferredDir)) readDirs.push(preferredDir); return { readDirs, preferredDir, }; } export function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } { const projectRoot = findConfiguredProjectRoot(cwd); if (!projectRoot) return { readDirs: [], preferredDir: null }; const preferredDir = path.join(getProjectConfigDir(projectRoot), "chains"); return { readDirs: isDirectory(preferredDir) ? [preferredDir] : [], preferredDir, }; }