{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../../src/agents/skills.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH,MAAM,MAAM,WAAW,GACpB,SAAS,GACT,MAAM,GACN,iBAAiB,GACjB,cAAc,GACd,kBAAkB,GAClB,eAAe,GACf,WAAW,GACX,SAAS,GACT,SAAS,CAAC;AAEb,UAAU,aAAa;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;CACpB;AA8kBD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG,SAAS,CAKlH;AAkCD,wBAAgB,aAAa,CAC5B,UAAU,EAAE,MAAM,EAAE,EACpB,GAAG,EAAE,MAAM,EACX,eAAe,CAAC,EAAE,MAAM,EAAE,EAC1B,YAAY,CAAC,EAAE,MAAM,GACnB;IAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAsClD;AAED,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,eAAe,CAAC,EAAE,MAAM,EAAE,EAC1B,YAAY,CAAC,EAAE,MAAM,GACnB;IAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAUlD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAmBnE;AAMD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,EAAE,GAAG,KAAK,GAAG,SAAS,CA6BhH;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC,CAUD;AAED,wBAAgB,eAAe,IAAI,IAAI,CAGtC","sourcesContent":["/**\n * Skill resolution and caching for subagent extension\n */\n\nimport { execSync } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { getAgentDir, getProjectConfigDir } from \"../shared/utils.ts\";\n\nexport type SkillSource =\n\t| \"project\"\n\t| \"user\"\n\t| \"project-package\"\n\t| \"user-package\"\n\t| \"project-settings\"\n\t| \"user-settings\"\n\t| \"extension\"\n\t| \"builtin\"\n\t| \"unknown\";\n\ninterface ResolvedSkill {\n\tname: string;\n\tpath: string;\n\tcontent: string;\n\tdescription?: string;\n\tsource: SkillSource;\n}\n\ninterface SkillCacheEntry {\n\tmtime: number;\n\tskill: ResolvedSkill;\n}\n\ninterface CachedSkillEntry {\n\tname: string;\n\tfilePath: string;\n\tsource: SkillSource;\n\tdescription?: string;\n\torder: number;\n}\n\ninterface SkillSearchPath {\n\tpath: string;\n\tsource: SkillSource;\n}\n\nconst skillCache = new Map<string, SkillCacheEntry>();\nconst MAX_CACHE_SIZE = 50;\n\nlet loadSkillsCache: { cwd: string; agentDir: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;\nconst LOAD_SKILLS_CACHE_TTL_MS = 5000;\n\nconst SUBAGENT_ORCHESTRATION_SKILL = \"pi-subagents\";\n\nconst SOURCE_PRIORITY: Record<SkillSource, number> = {\n\tproject: 700,\n\t\"project-settings\": 650,\n\t\"project-package\": 600,\n\tuser: 300,\n\t\"user-settings\": 250,\n\t\"user-package\": 200,\n\textension: 150,\n\tbuiltin: 100,\n\tunknown: 0,\n};\n\nfunction stripSkillFrontmatter(content: string): string {\n\tconst normalized = content.replace(/\\r\\n/g, \"\\n\");\n\tif (!normalized.startsWith(\"---\")) return normalized;\n\n\tconst endIndex = normalized.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) return normalized;\n\n\treturn normalized.slice(endIndex + 4).trim();\n}\n\nfunction isWithinPath(filePath: string, dir: string): boolean {\n\tconst relative = path.relative(dir, filePath);\n\treturn relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n\nfunction readOptionalJsonFile(filePath: string, label: string): unknown {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(filePath, \"utf-8\"));\n\t} catch (error) {\n\t\tconst code =\n\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t? (error as { code?: unknown }).code\n\t\t\t\t: undefined;\n\t\tif (code === \"ENOENT\") return null;\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthrow new Error(`Failed to read ${label} '${filePath}': ${message}`, {\n\t\t\tcause: error instanceof Error ? error : undefined,\n\t\t});\n\t}\n}\n\nfunction readJsonFileBestEffort(filePath: string): unknown {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(filePath, \"utf-8\"));\n\t} catch {\n\t\t// Package scans over installed dependencies are opportunistic.\n\t\treturn null;\n\t}\n}\n\nfunction extractSkillPathsFromPackageRoot(\n\tpackageRoot: string,\n\tsource: SkillSource,\n\tbestEffort = false,\n): SkillSearchPath[] {\n\tconst packageJsonPath = path.join(packageRoot, \"package.json\");\n\tconst pkg = bestEffort\n\t\t? readJsonFileBestEffort(packageJsonPath)\n\t\t: readOptionalJsonFile(packageJsonPath, \"package manifest\");\n\tif (!pkg || typeof pkg !== \"object\" || Array.isArray(pkg)) return [];\n\tconst pi = (pkg as { pi?: unknown }).pi;\n\tif (!pi || typeof pi !== \"object\" || Array.isArray(pi)) return [];\n\tconst skills = (pi as { skills?: unknown }).skills;\n\tif (!Array.isArray(skills)) return [];\n\treturn skills\n\t\t.filter((entry): entry is string => typeof entry === \"string\")\n\t\t.map((entry) => ({ path: path.resolve(packageRoot, entry), source }));\n}\n\nlet cachedGlobalNpmRoot: string | null = null;\n\nfunction getGlobalNpmRoot(): string | null {\n\tconst offline = process.env.PI_OFFLINE?.toLowerCase();\n\tif (offline === \"1\" || offline === \"true\" || offline === \"yes\") return null;\n\tif (cachedGlobalNpmRoot !== null) return cachedGlobalNpmRoot;\n\n\tconst windowsGlobalRoot =\n\t\tprocess.platform === \"win32\" && process.env.APPDATA\n\t\t\t? path.join(process.env.APPDATA, \"npm\", \"node_modules\")\n\t\t\t: undefined;\n\tif (windowsGlobalRoot) {\n\t\ttry {\n\t\t\tif (fs.statSync(windowsGlobalRoot).isDirectory()) {\n\t\t\t\tcachedGlobalNpmRoot = fs.realpathSync(windowsGlobalRoot);\n\t\t\t\treturn cachedGlobalNpmRoot;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through if the directory disappears while resolving it.\n\t\t}\n\t}\n\n\ttry {\n\t\tcachedGlobalNpmRoot = fs.realpathSync(execSync(\"npm root -g\", { encoding: \"utf-8\", timeout: 15_000 }).trim());\n\t\treturn cachedGlobalNpmRoot;\n\t} catch {\n\t\t// Global npm root is optional in constrained environments.\n\t\tcachedGlobalNpmRoot = \"\"; // Empty string means \"tried but failed\"\n\t\treturn null;\n\t}\n}\n\nfunction collectInstalledPackageSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {\n\tconst projectConfigDir = getProjectConfigDir(cwd);\n\tconst dirs: SkillSearchPath[] = [\n\t\t{ path: path.join(projectConfigDir, \"npm\", \"node_modules\"), source: \"project-package\" },\n\t\t{ path: path.join(agentDir, \"npm\", \"node_modules\"), source: \"user-package\" },\n\t];\n\n\tconst globalRoot = getGlobalNpmRoot();\n\tif (globalRoot) {\n\t\tdirs.push({ path: globalRoot, source: \"user-package\" });\n\t}\n\n\tconst results: SkillSearchPath[] = [];\n\n\tfor (const dir of dirs) {\n\t\tif (!fs.existsSync(dir.path)) continue;\n\t\tlet entries: fs.Dirent[];\n\t\ttry {\n\t\t\tentries = fs.readdirSync(dir.path, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) continue;\n\t\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\n\t\t\tif (entry.name.startsWith(\"@\")) {\n\t\t\t\tconst scopeDir = path.join(dir.path, entry.name);\n\t\t\t\tlet scopeEntries: fs.Dirent[];\n\t\t\t\ttry {\n\t\t\t\t\tscopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tfor (const scopeEntry of scopeEntries) {\n\t\t\t\t\tif (scopeEntry.name.startsWith(\".\")) continue;\n\t\t\t\t\tif (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink()) continue;\n\t\t\t\t\tconst pkgRoot = path.join(scopeDir, scopeEntry.name);\n\t\t\t\t\tresults.push(...extractSkillPathsFromPackageRoot(pkgRoot, dir.source, true));\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst pkgRoot = path.join(dir.path, entry.name);\n\t\t\tresults.push(...extractSkillPathsFromPackageRoot(pkgRoot, dir.source, true));\n\t\t}\n\t}\n\n\treturn results;\n}\n\nfunction collectSettingsSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {\n\tconst results: SkillSearchPath[] = [];\n\tconst projectConfigDir = getProjectConfigDir(cwd);\n\tconst settingsFiles = [\n\t\t{\n\t\t\tfile: path.join(projectConfigDir, \"settings.json\"),\n\t\t\tbase: projectConfigDir,\n\t\t\tsource: \"project-settings\" as const,\n\t\t},\n\t\t{ file: path.join(agentDir, \"settings.json\"), base: agentDir, source: \"user-settings\" as const },\n\t];\n\n\tfor (const { file, base, source } of settingsFiles) {\n\t\tconst settings = readOptionalJsonFile(file, \"skills settings file\");\n\t\tif (!settings || typeof settings !== \"object\" || Array.isArray(settings)) continue;\n\t\tconst skills = (settings as { skills?: unknown }).skills;\n\t\tif (!Array.isArray(skills)) continue;\n\t\tfor (const entry of skills) {\n\t\t\tif (typeof entry !== \"string\") continue;\n\t\t\tlet resolved = entry;\n\t\t\tif (resolved.startsWith(\"~/\")) {\n\t\t\t\tresolved = path.join(os.homedir(), resolved.slice(2));\n\t\t\t} else if (!path.isAbsolute(resolved)) {\n\t\t\t\tresolved = path.resolve(base, resolved);\n\t\t\t}\n\t\t\tresults.push({ path: resolved, source });\n\t\t}\n\t}\n\n\treturn results;\n}\n\nfunction isSafePackagePath(value: string): boolean {\n\treturn (\n\t\tvalue.length > 0 &&\n\t\t!path.isAbsolute(value) &&\n\t\tvalue.split(/[\\\\/]/).every((part) => part.length > 0 && part !== \".\" && part !== \"..\")\n\t);\n}\n\nfunction parseNpmPackageName(source: string): string | undefined {\n\tconst spec = source.slice(4).trim();\n\tif (!spec) return undefined;\n\tconst match = spec.match(/^(@?[^@]+(?:\\/[^@]+)?)(?:@(.+))?$/);\n\tconst packageName = match?.[1] ?? spec;\n\treturn isSafePackagePath(packageName) ? packageName : undefined;\n}\n\nfunction stripGitRef(repoPath: string): string {\n\tconst atIndex = repoPath.indexOf(\"@\");\n\tconst hashIndex = repoPath.indexOf(\"#\");\n\tconst refIndex = [atIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];\n\treturn refIndex === undefined ? repoPath : repoPath.slice(0, refIndex);\n}\n\nfunction parseGitPackagePath(source: string): { host: string; repoPath: string } | undefined {\n\tconst spec = source.slice(4).trim();\n\tif (!spec) return undefined;\n\n\tlet host = \"\";\n\tlet repoPath = \"\";\n\tconst scpLike = spec.match(/^git@([^:]+):(.+)$/);\n\tif (scpLike) {\n\t\thost = scpLike[1] ?? \"\";\n\t\trepoPath = scpLike[2] ?? \"\";\n\t} else if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(spec)) {\n\t\ttry {\n\t\t\tconst url = new URL(spec);\n\t\t\thost = url.hostname;\n\t\t\trepoPath = url.pathname.replace(/^\\/+/, \"\");\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t} else {\n\t\tconst slashIndex = spec.indexOf(\"/\");\n\t\tif (slashIndex < 0) return undefined;\n\t\thost = spec.slice(0, slashIndex);\n\t\trepoPath = spec.slice(slashIndex + 1);\n\t}\n\n\tconst normalizedPath = stripGitRef(repoPath)\n\t\t.replace(/\\.git$/, \"\")\n\t\t.replace(/^\\/+/, \"\");\n\tif (\n\t\t!host ||\n\t\t!isSafePackagePath(host) ||\n\t\t!isSafePackagePath(normalizedPath) ||\n\t\tnormalizedPath.split(/[\\\\/]/).length < 2\n\t) {\n\t\treturn undefined;\n\t}\n\treturn { host, repoPath: normalizedPath };\n}\n\nfunction resolveSettingsPackageRoot(source: string, baseDir: string): string | undefined {\n\tconst trimmed = source.trim();\n\tif (!trimmed) return undefined;\n\tif (trimmed.startsWith(\"git:\")) {\n\t\tconst parsed = parseGitPackagePath(trimmed);\n\t\treturn parsed ? path.join(baseDir, \"git\", parsed.host, parsed.repoPath) : undefined;\n\t}\n\tif (trimmed.startsWith(\"npm:\")) {\n\t\tconst packageName = parseNpmPackageName(trimmed);\n\t\treturn packageName ? path.join(baseDir, \"npm\", \"node_modules\", packageName) : undefined;\n\t}\n\tconst normalized = trimmed.startsWith(\"file:\") ? trimmed.slice(5) : trimmed;\n\tif (normalized === \"~\") return os.homedir();\n\tif (normalized.startsWith(\"~/\")) return path.join(os.homedir(), normalized.slice(2));\n\tif (path.isAbsolute(normalized)) return normalized;\n\tif (normalized === \".\" || normalized === \"..\" || normalized.startsWith(\"./\") || normalized.startsWith(\"../\")) {\n\t\treturn path.resolve(baseDir, normalized);\n\t}\n\treturn undefined;\n}\n\nfunction collectSettingsPackageSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {\n\tconst projectConfigDir = getProjectConfigDir(cwd);\n\tconst settingsFiles = [\n\t\t{\n\t\t\tfile: path.join(projectConfigDir, \"settings.json\"),\n\t\t\tbase: projectConfigDir,\n\t\t\tsource: \"project-package\" as const,\n\t\t},\n\t\t{ file: path.join(agentDir, \"settings.json\"), base: agentDir, source: \"user-package\" as const },\n\t];\n\tconst results: SkillSearchPath[] = [];\n\n\tfor (const { file, base, source } of settingsFiles) {\n\t\tconst settings = readOptionalJsonFile(file, \"skills settings file\");\n\t\tif (!settings || typeof settings !== \"object\" || Array.isArray(settings)) continue;\n\t\tconst packages = (settings as { packages?: unknown }).packages;\n\t\tif (!Array.isArray(packages)) continue;\n\n\t\tfor (const entry of packages) {\n\t\t\tconst packageSource =\n\t\t\t\ttypeof entry === \"string\"\n\t\t\t\t\t? entry\n\t\t\t\t\t: typeof entry === \"object\" &&\n\t\t\t\t\t\t\tentry !== null &&\n\t\t\t\t\t\t\ttypeof (entry as { source?: unknown }).source === \"string\"\n\t\t\t\t\t\t? (entry as { source: string }).source\n\t\t\t\t\t\t: undefined;\n\t\t\tif (!packageSource) continue;\n\n\t\t\tconst packageRoot = resolveSettingsPackageRoot(packageSource, base);\n\t\t\tif (!packageRoot) continue;\n\t\t\tresults.push(...extractSkillPathsFromPackageRoot(packageRoot, source));\n\t\t}\n\t}\n\n\treturn results;\n}\n\nfunction buildSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {\n\tconst projectConfigDir = getProjectConfigDir(cwd);\n\tconst skillPaths: SkillSearchPath[] = [\n\t\t{ path: path.join(projectConfigDir, \"skills\"), source: \"project\" },\n\t\t{ path: path.join(cwd, \".agents\", \"skills\"), source: \"project\" },\n\t\t{ path: path.join(agentDir, \"skills\"), source: \"user\" },\n\t\t{ path: path.join(os.homedir(), \".agents\", \"skills\"), source: \"user\" },\n\t\t...collectInstalledPackageSkillPaths(cwd, agentDir),\n\t\t...collectSettingsPackageSkillPaths(cwd, agentDir),\n\t\t...extractSkillPathsFromPackageRoot(cwd, \"project-package\"),\n\t\t...collectSettingsSkillPaths(cwd, agentDir),\n\t];\n\n\tconst deduped = new Map<string, SkillSearchPath>();\n\tfor (const entry of skillPaths) {\n\t\tconst resolvedPath = path.resolve(entry.path);\n\t\tconst existing = deduped.get(resolvedPath);\n\t\tif (!existing || (SOURCE_PRIORITY[entry.source] ?? 0) > (SOURCE_PRIORITY[existing.source] ?? 0)) {\n\t\t\tdeduped.set(resolvedPath, { path: resolvedPath, source: entry.source });\n\t\t}\n\t}\n\treturn [...deduped.values()];\n}\n\nfunction inferSkillSource(filePath: string, cwd: string, agentDir: string, sourceHint?: SkillSource): SkillSource {\n\tif (sourceHint) return sourceHint;\n\n\tconst projectConfigRoot = path.resolve(getProjectConfigDir(cwd));\n\tconst projectSkillsRoot = path.resolve(projectConfigRoot, \"skills\");\n\tconst projectPackagesRoot = path.resolve(projectConfigRoot, \"npm\", \"node_modules\");\n\tconst projectAgentsRoot = path.resolve(cwd, \".agents\");\n\tconst userSkillsRoot = path.resolve(agentDir, \"skills\");\n\tconst userPackagesRoot = path.resolve(agentDir, \"npm\", \"node_modules\");\n\tconst userAgentRoot = path.resolve(agentDir);\n\tconst userAgentsRoot = path.resolve(os.homedir(), \".agents\");\n\n\tif (isWithinPath(filePath, projectPackagesRoot)) return \"project-package\";\n\tif (isWithinPath(filePath, projectSkillsRoot) || isWithinPath(filePath, projectAgentsRoot)) return \"project\";\n\tif (isWithinPath(filePath, projectConfigRoot)) return \"project-settings\";\n\n\tif (isWithinPath(filePath, userPackagesRoot)) return \"user-package\";\n\tif (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return \"user\";\n\tif (isWithinPath(filePath, userAgentRoot)) return \"user-settings\";\n\n\tconst globalRoot = getGlobalNpmRoot();\n\tif (globalRoot && isWithinPath(filePath, globalRoot)) return \"user-package\";\n\n\treturn \"unknown\";\n}\n\nfunction chooseHigherPrioritySkill(\n\texisting: CachedSkillEntry | undefined,\n\tcandidate: CachedSkillEntry,\n): CachedSkillEntry {\n\tif (!existing) return candidate;\n\tconst existingPriority = SOURCE_PRIORITY[existing.source] ?? 0;\n\tconst candidatePriority = SOURCE_PRIORITY[candidate.source] ?? 0;\n\tif (candidatePriority > existingPriority) return candidate;\n\tif (candidatePriority < existingPriority) return existing;\n\treturn candidate.order < existing.order ? candidate : existing;\n}\n\nfunction parseSkillDescription(content: string): string | undefined {\n\tconst normalized = content.replace(/\\r\\n/g, \"\\n\");\n\tif (!normalized.startsWith(\"---\")) return undefined;\n\n\tconst endIndex = normalized.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) return undefined;\n\n\tconst frontmatter = normalized.slice(3, endIndex).trim();\n\tconst match = frontmatter.match(/^description:\\s*(.+)$/m);\n\treturn match?.[1]?.trim().replace(/^['\"]|['\"]$/g, \"\");\n}\n\nfunction maybeReadSkillDescription(filePath: string): string | undefined {\n\ttry {\n\t\treturn parseSkillDescription(fs.readFileSync(filePath, \"utf-8\"));\n\t} catch {\n\t\t// Description parsing is best-effort metadata extraction.\n\t\treturn undefined;\n\t}\n}\n\nfunction collectFilesystemSkills(cwd: string, agentDir: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {\n\tconst entries: CachedSkillEntry[] = [];\n\tconst seen = new Map<string, number>();\n\tconst visitedDirectories = new Map<string, number>();\n\tlet order = 0;\n\n\tconst pushEntry = (name: string, filePath: string, sourceHint?: SkillSource) => {\n\t\tconst resolvedFile = path.resolve(filePath);\n\t\tif (!fs.existsSync(resolvedFile)) return;\n\t\tconst source = inferSkillSource(resolvedFile, cwd, agentDir, sourceHint);\n\t\tconst description = maybeReadSkillDescription(resolvedFile);\n\t\tconst existingIndex = seen.get(resolvedFile);\n\t\tif (existingIndex !== undefined) {\n\t\t\tconst existing = entries[existingIndex];\n\t\t\tif (existing && (SOURCE_PRIORITY[source] ?? 0) > (SOURCE_PRIORITY[existing.source] ?? 0)) {\n\t\t\t\tconst { description: _description, ...existingWithoutDescription } = existing;\n\t\t\t\tentries[existingIndex] = {\n\t\t\t\t\t...existingWithoutDescription,\n\t\t\t\t\tname,\n\t\t\t\t\tsource,\n\t\t\t\t\t...(description !== undefined ? { description } : {}),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tseen.set(resolvedFile, entries.length);\n\t\tentries.push({\n\t\t\tname,\n\t\t\tfilePath: resolvedFile,\n\t\t\tsource,\n\t\t\t...(description !== undefined ? { description } : {}),\n\t\t\torder: order++,\n\t\t});\n\t};\n\n\tconst shouldSkipDirectory = (name: string) => name.startsWith(\".\") || name === \"node_modules\";\n\n\tconst markDirectoryVisited = (dirPath: string, sourceHint?: SkillSource): boolean => {\n\t\tlet resolvedDir: string;\n\t\ttry {\n\t\t\tresolvedDir = fs.realpathSync(dirPath);\n\t\t} catch {\n\t\t\tresolvedDir = path.resolve(dirPath);\n\t\t}\n\t\tconst priority = sourceHint ? (SOURCE_PRIORITY[sourceHint] ?? 0) : SOURCE_PRIORITY.unknown;\n\t\tconst previousPriority = visitedDirectories.get(resolvedDir);\n\t\tif (previousPriority !== undefined && previousPriority >= priority) return false;\n\t\tvisitedDirectories.set(resolvedDir, priority);\n\t\treturn true;\n\t};\n\n\tconst walkSkillDirectories = (dirPath: string, sourceHint?: SkillSource) => {\n\t\tif (!markDirectoryVisited(dirPath, sourceHint)) return;\n\n\t\tconst skillFile = path.join(dirPath, \"SKILL.md\");\n\t\tif (fs.existsSync(skillFile)) {\n\t\t\tpushEntry(path.basename(dirPath), skillFile, sourceHint);\n\t\t\treturn;\n\t\t}\n\n\t\tlet entriesInDir: fs.Dirent[];\n\t\ttry {\n\t\t\tentriesInDir = fs.readdirSync(dirPath, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const entry of entriesInDir) {\n\t\t\tif (shouldSkipDirectory(entry.name)) continue;\n\t\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\n\t\t\tconst entryPath = path.join(dirPath, entry.name);\n\t\t\tlet stat: fs.Stats;\n\t\t\ttry {\n\t\t\t\tstat = fs.statSync(entryPath);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (stat.isDirectory()) {\n\t\t\t\twalkSkillDirectories(entryPath, sourceHint);\n\t\t\t}\n\t\t}\n\t};\n\n\tfor (const skillPath of skillPaths) {\n\t\tif (!fs.existsSync(skillPath.path)) continue;\n\n\t\tlet stat: fs.Stats;\n\t\ttry {\n\t\t\tstat = fs.statSync(skillPath.path);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (stat.isFile()) {\n\t\t\tconst fileName = path.basename(skillPath.path);\n\t\t\tif (!fileName.toLowerCase().endsWith(\".md\")) continue;\n\t\t\tconst skillName =\n\t\t\t\tfileName.toLowerCase() === \"skill.md\"\n\t\t\t\t\t? path.basename(path.dirname(skillPath.path))\n\t\t\t\t\t: path.basename(fileName, path.extname(fileName));\n\t\t\tpushEntry(skillName, skillPath.path, skillPath.source);\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!stat.isDirectory()) continue;\n\n\t\tconst rootSkillFile = path.join(skillPath.path, \"SKILL.md\");\n\t\tif (fs.existsSync(rootSkillFile)) {\n\t\t\tpushEntry(path.basename(skillPath.path), rootSkillFile, skillPath.source);\n\t\t\tcontinue;\n\t\t}\n\n\t\tmarkDirectoryVisited(skillPath.path, skillPath.source);\n\n\t\tlet childEntries: fs.Dirent[];\n\t\ttry {\n\t\t\tchildEntries = fs.readdirSync(skillPath.path, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const child of childEntries) {\n\t\t\tif (child.name.startsWith(\".\")) continue;\n\t\t\tconst childPath = path.join(skillPath.path, child.name);\n\t\t\tif (child.isDirectory() || child.isSymbolicLink()) {\n\t\t\t\tif (shouldSkipDirectory(child.name)) continue;\n\t\t\t\tlet childStat: fs.Stats;\n\t\t\t\ttry {\n\t\t\t\t\tchildStat = fs.statSync(childPath);\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (childStat.isDirectory()) walkSkillDirectories(childPath, skillPath.source);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (child.isFile() && child.name.toLowerCase().endsWith(\".md\")) {\n\t\t\t\tpushEntry(path.basename(child.name, path.extname(child.name)), childPath, skillPath.source);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn entries;\n}\n\nfunction getCachedSkills(cwd: string): CachedSkillEntry[] {\n\tconst now = Date.now();\n\tconst agentDir = getAgentDir();\n\tif (\n\t\tloadSkillsCache &&\n\t\tloadSkillsCache.cwd === cwd &&\n\t\tloadSkillsCache.agentDir === agentDir &&\n\t\tnow - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS\n\t) {\n\t\treturn loadSkillsCache.skills;\n\t}\n\n\tconst skillPaths = buildSkillPaths(cwd, agentDir);\n\tconst loaded = collectFilesystemSkills(cwd, agentDir, skillPaths);\n\tconst dedupedByName = new Map<string, CachedSkillEntry>();\n\n\tfor (const entry of loaded) {\n\t\tconst current = dedupedByName.get(entry.name);\n\t\tdedupedByName.set(entry.name, chooseHigherPrioritySkill(current, entry));\n\t}\n\n\tconst skills = [...dedupedByName.values()].sort((a, b) => a.order - b.order);\n\tloadSkillsCache = { cwd, agentDir, skills, timestamp: now };\n\treturn skills;\n}\n\nexport function resolveSkillPath(skillName: string, cwd: string): { path: string; source: SkillSource } | undefined {\n\tconst skills = getCachedSkills(cwd);\n\tconst skill = skills.find((s) => s.name === skillName);\n\tif (!skill) return undefined;\n\treturn { path: skill.filePath, source: skill.source };\n}\n\nfunction readSkill(skillName: string, skillPath: string, source: SkillSource): ResolvedSkill | undefined {\n\ttry {\n\t\tconst stat = fs.statSync(skillPath);\n\t\tconst cached = skillCache.get(skillPath);\n\t\tif (cached && cached.mtime === stat.mtimeMs) {\n\t\t\treturn cached.skill;\n\t\t}\n\n\t\tconst raw = fs.readFileSync(skillPath, \"utf-8\");\n\t\tconst content = stripSkillFrontmatter(raw);\n\t\tconst description = parseSkillDescription(raw);\n\t\tconst skill: ResolvedSkill = {\n\t\t\tname: skillName,\n\t\t\tpath: skillPath,\n\t\t\tcontent,\n\t\t\t...(description !== undefined ? { description } : {}),\n\t\t\tsource,\n\t\t};\n\n\t\tskillCache.set(skillPath, { mtime: stat.mtimeMs, skill });\n\t\tif (skillCache.size > MAX_CACHE_SIZE) {\n\t\t\tconst firstKey = skillCache.keys().next().value;\n\t\t\tif (firstKey) skillCache.delete(firstKey);\n\t\t}\n\n\t\treturn skill;\n\t} catch {\n\t\t// Treat unreadable skill files as unresolved so callers can surface as missing.\n\t\treturn undefined;\n\t}\n}\n\nexport function resolveSkills(\n\tskillNames: string[],\n\tcwd: string,\n\tlocalSkillPaths?: string[],\n\tlocalBaseDir?: string,\n): { resolved: ResolvedSkill[]; missing: string[] } {\n\tconst resolved: ResolvedSkill[] = [];\n\tconst missing: string[] = [];\n\tconst localByName = new Map<string, CachedSkillEntry>();\n\tif (localSkillPaths?.length) {\n\t\tconst agentDir = getAgentDir();\n\t\tconst localEntries = collectFilesystemSkills(\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tlocalSkillPaths.map((entry) => ({\n\t\t\t\tpath: path.resolve(localBaseDir ?? cwd, entry),\n\t\t\t\tsource: \"unknown\" as const,\n\t\t\t})),\n\t\t);\n\t\tfor (const entry of localEntries) {\n\t\t\tif (!localByName.has(entry.name)) localByName.set(entry.name, entry);\n\t\t}\n\t}\n\n\tfor (const name of skillNames) {\n\t\tconst trimmed = name.trim();\n\t\tif (!trimmed) continue;\n\t\tif (trimmed === SUBAGENT_ORCHESTRATION_SKILL) {\n\t\t\tmissing.push(trimmed);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst local = localByName.get(trimmed);\n\t\tlet skill = local ? readSkill(trimmed, local.filePath, local.source) : undefined;\n\t\tif (!skill) {\n\t\t\tconst location = resolveSkillPath(trimmed, cwd);\n\t\t\tif (location) skill = readSkill(trimmed, location.path, location.source);\n\t\t}\n\t\tif (skill) resolved.push(skill);\n\t\telse missing.push(trimmed);\n\t}\n\n\treturn { resolved, missing };\n}\n\nexport function resolveSkillsWithFallback(\n\tskillNames: string[],\n\tprimaryCwd: string,\n\tfallbackCwd?: string,\n\tlocalSkillPaths?: string[],\n\tlocalBaseDir?: string,\n): { resolved: ResolvedSkill[]; missing: string[] } {\n\tconst primary = resolveSkills(skillNames, primaryCwd, localSkillPaths, localBaseDir);\n\tif (!fallbackCwd || primary.missing.length === 0) return primary;\n\tif (path.resolve(primaryCwd) === path.resolve(fallbackCwd)) return primary;\n\n\tconst fallback = resolveSkills(primary.missing, fallbackCwd);\n\treturn {\n\t\tresolved: [...primary.resolved, ...fallback.resolved],\n\t\tmissing: fallback.missing,\n\t};\n}\n\nexport function buildSkillInjection(skills: ResolvedSkill[]): string {\n\tif (skills.length === 0) return \"\";\n\n\tconst lines = [\n\t\t\"The following configured skills are available to this subagent.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t];\n\tfor (const skill of skills) {\n\t\tlines.push(\"  <skill>\");\n\t\tlines.push(`    <name>${escapeXmlText(skill.name)}</name>`);\n\t\tlines.push(`    <description>${escapeXmlText(skill.description ?? \"\")}</description>`);\n\t\tlines.push(`    <location>${escapeXmlText(skill.path)}</location>`);\n\t\tlines.push(\"  </skill>\");\n\t}\n\tlines.push(\"</available_skills>\");\n\treturn lines.join(\"\\n\");\n}\n\nfunction escapeXmlText(value: string): string {\n\treturn value.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nexport function normalizeSkillInput(input: string | string[] | boolean | undefined): string[] | false | undefined {\n\tif (input === false) return false;\n\tif (input === true || input === undefined) return undefined;\n\tif (Array.isArray(input)) {\n\t\treturn [...new Set(input.map((s) => s.trim()).filter((s) => s.length > 0))];\n\t}\n\t// Guard against JSON-encoded arrays arriving as strings (e.g. '[\"a\",\"b\"]').\n\t// Models sometimes serialise the skill parameter as a JSON string instead of\n\t// a native array, and naively splitting on \",\" would embed brackets/quotes\n\t// into the skill names, causing resolution to silently fail.\n\tconst trimmed = input.trim();\n\tif (trimmed.startsWith(\"[\")) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(trimmed);\n\t\t\tif (Array.isArray(parsed)) {\n\t\t\t\treturn normalizeSkillInput(parsed);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not valid JSON – fall through to comma-split\n\t\t}\n\t}\n\treturn [\n\t\t...new Set(\n\t\t\tinput\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter((s) => s.length > 0),\n\t\t),\n\t];\n}\n\nexport function discoverAvailableSkills(cwd: string): Array<{\n\tname: string;\n\tsource: SkillSource;\n\tdescription?: string;\n}> {\n\tconst skills = getCachedSkills(cwd);\n\treturn skills\n\t\t.filter((s) => s.name !== SUBAGENT_ORCHESTRATION_SKILL)\n\t\t.map((s) => ({\n\t\t\tname: s.name,\n\t\t\tsource: s.source,\n\t\t\t...(s.description !== undefined ? { description: s.description } : {}),\n\t\t}))\n\t\t.sort((a, b) => a.name.localeCompare(b.name));\n}\n\nexport function clearSkillCache(): void {\n\tskillCache.clear();\n\tloadSkillsCache = null;\n}\n"]}