(
dirPath: string,
root: SkillRoot,
isTopLevel: boolean,
depth: number,
subSkillParentName?: string,
)
| 141 | const byName = new Map<string, SkillDefinition>(); |
| 142 | |
| 143 | async function walkSkillDir( |
| 144 | dirPath: string, |
| 145 | root: SkillRoot, |
| 146 | isTopLevel: boolean, |
| 147 | depth: number, |
| 148 | subSkillParentName?: string, |
| 149 | ): Promise<void> { |
| 150 | if (depth > MAX_SKILL_SCAN_DEPTH) return; |
| 151 | |
| 152 | let entries: readonly string[]; |
| 153 | try { |
| 154 | // Sorted so first-wins collision resolution across sibling directories |
| 155 | // is deterministic rather than dependent on filesystem readdir order. |
| 156 | entries = [...(await readdir(dirPath))].toSorted(); |
| 157 | } catch (error) { |
| 158 | warn(`Failed to read skill directory ${dirPath}`, error); |
| 159 | return; |
| 160 | } |
| 161 | |
| 162 | const directorySkills = new Set<string>(); |
| 163 | const subdirs: string[] = []; |
| 164 | for (const entry of entries) { |
| 165 | const entryPath = path.join(dirPath, entry); |
| 166 | // A directory holding SKILL.md is a skill bundle: register it, then keep |
| 167 | // descending so nested SKILL.md bundles remain discoverable as sub-skills. |
| 168 | if (await isFile(path.join(entryPath, 'SKILL.md'))) { |
| 169 | directorySkills.add(entry); |
| 170 | } |
| 171 | if (entry === 'node_modules' || entry.startsWith('.')) continue; |
| 172 | if (await isDir(entryPath)) subdirs.push(entry); |
| 173 | } |
| 174 | |
| 175 | const allowedSubSkillBundles = new Map<string, string>(); |
| 176 | for (const entry of directorySkills) { |
| 177 | const skill = await parseAndRegister({ |
| 178 | parse, |
| 179 | byName, |
| 180 | skillMdPath: path.join(dirPath, entry, 'SKILL.md'), |
| 181 | skillDirName: entry, |
| 182 | root, |
| 183 | onDiscoveredSkill: options.onDiscoveredSkill, |
| 184 | warn, |
| 185 | skip, |
| 186 | subSkillParentName, |
| 187 | }); |
| 188 | if (skill !== undefined && hasSubSkillEnabled(skill)) { |
| 189 | allowedSubSkillBundles.set(entry, skill.name); |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | // Flat .md skills count only at a root's top level; deeper .md files are |
| 194 | // skill payload (e.g. references/foo.md), not skills. |
| 195 | if (isTopLevel) { |
| 196 | // A SKILL.md placed directly at a plugin skill root (e.g. plugin root fallback) |
| 197 | // is treated as a single skill bundle. This only applies to plugin-derived roots, |
| 198 | // not to user/project skill directories. |
| 199 | if (root.plugin !== undefined) { |
| 200 | const rootSkillMd = path.join(dirPath, 'SKILL.md'); |
no test coverage detected