diff --git a/.changeset/qualify-sub-skill-names.md b/.changeset/qualify-sub-skill-names.md new file mode 100644 index 000000000..f6f5823aa --- /dev/null +++ b/.changeset/qualify-sub-skill-names.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": minor +"@moonshot-ai/kimi-code": minor +--- + +Qualify sub-skill names with their parent prefix and expose sub-skills as dotted slash commands in the TUI. diff --git a/apps/kimi-code/src/tui/commands/skills.ts b/apps/kimi-code/src/tui/commands/skills.ts index 8ddfbda0e..2997a8b15 100644 --- a/apps/kimi-code/src/tui/commands/skills.ts +++ b/apps/kimi-code/src/tui/commands/skills.ts @@ -33,7 +33,10 @@ export function buildSkillSlashCommands(skills: readonly SkillSummary[]): SkillS const commandMap = new Map(); const sortedSkills = [...skills].toSorted(compareSkillSlashCommands); const commands = sortedSkills.filter(isUserActivatableSkill).map((skill) => { - const commandName = skill.source === 'builtin' ? skill.name : `skill:${skill.name}`; + const commandName = + skill.source === 'builtin' || skill.isSubSkill === true + ? skill.name + : `skill:${skill.name}`; commandMap.set(commandName, skill.name); return { name: commandName, diff --git a/apps/kimi-code/test/tui/commands/resolve.test.ts b/apps/kimi-code/test/tui/commands/resolve.test.ts index cf009b158..e25a1b984 100644 --- a/apps/kimi-code/test/tui/commands/resolve.test.ts +++ b/apps/kimi-code/test/tui/commands/resolve.test.ts @@ -211,6 +211,17 @@ describe('resolveSlashCommandInput', () => { }); }); + it('resolves unprefixed sub-skill commands with dotted names', () => { + const skillCommandMap = new Map([['outer.inner', 'outer.inner']]); + + expect(resolve('/outer.inner src/app.ts', { skillCommandMap })).toEqual({ + kind: 'skill', + commandName: 'outer.inner', + skillName: 'outer.inner', + args: 'src/app.ts', + }); + }); + it('returns message for unknown slash input', () => { expect(resolve('/does-not-exist arg')).toEqual({ kind: 'message', diff --git a/apps/kimi-code/test/tui/commands/skills.test.ts b/apps/kimi-code/test/tui/commands/skills.test.ts index cdb5c8a51..b9cf46bd6 100644 --- a/apps/kimi-code/test/tui/commands/skills.test.ts +++ b/apps/kimi-code/test/tui/commands/skills.test.ts @@ -90,4 +90,16 @@ describe('skill slash commands', () => { expect(built.commands.map((command) => command.name)).toEqual(['mcp-config']); expect(built.commandMap.get('mcp-config')).toBe('mcp-config'); }); + + it('keeps sub-skills slash-invocable', () => { + const built = buildSkillSlashCommands([ + skill('outer.inner', 'prompt', { + isSubSkill: true, + source: 'project', + }), + ]); + + expect(built.commands.map((command) => command.name)).toEqual(['outer.inner']); + expect(built.commandMap.get('outer.inner')).toBe('outer.inner'); + }); }); diff --git a/docs/en/guides/interaction.md b/docs/en/guides/interaction.md index 8a1c91531..d43448bad 100644 --- a/docs/en/guides/interaction.md +++ b/docs/en/guides/interaction.md @@ -25,7 +25,7 @@ After pasting, the input box shows a placeholder that you can edit like normal t Anything starting with `/` is treated as a slash command. Typing `/` opens a completion menu that filters in real time as you keep typing; press `Esc` to close the menu. If nothing matches, the input is sent to the agent as a regular message. -Active [Agent Skills](../customization/skills.md) are automatically registered as slash commands: external Skills are invoked with `/skill:`, while built-in Skills appear directly as `/` in the slash command panel. If an external skill name does not conflict with a system slash command, you can also drop the `skill:` prefix and type `/` directly. +Active [Agent Skills](../customization/skills.md) are automatically registered as slash commands: ordinary external Skills are invoked with `/skill:`, external sub-skills appear as dotted commands such as `/parent.child`, and built-in Skills appear directly as `/` in the slash command panel. If an external skill name does not conflict with a system slash command, you can also drop the `skill:` prefix and type `/` directly. Some commands are only available when the agent is idle — you need to press `Esc` to interrupt streaming output or context compression before using them. Mode-toggle and query commands like `/yolo`, `/plan`, `/help`, and `/btw` are always available. For the full list, see [Slash commands reference](../reference/slash-commands.md). diff --git a/docs/en/reference/slash-commands.md b/docs/en/reference/slash-commands.md index f6eb04cae..cbb99390a 100644 --- a/docs/en/reference/slash-commands.md +++ b/docs/en/reference/slash-commands.md @@ -128,7 +128,7 @@ All built-in Skill commands are only available in the idle state. ## Skill Dynamic Commands -Activated external Skills are automatically registered as slash commands with the `skill:` namespace prefix: +Activated external Skills are automatically registered as slash commands. Ordinary external Skills use the `skill:` namespace prefix: ``` /skill: [extra text] @@ -136,6 +136,14 @@ Activated external Skills are automatically registered as slash commands with th For example, `/skill:code-style` loads the Skill named `code-style` and sends it to the Agent; any text appended after the command is concatenated to the Skill prompt. +External sub-skills appear directly in the slash command panel with dotted names: + +``` +/. [extra text] +``` + +For example, a child Skill named `review` inside a parent Skill named `code-style` is shown as `/code-style.review`. The dotted command name is derived from the hierarchy; the child `SKILL.md` can keep its local `name`. + For convenience, external Skill commands also support a shorthand form that omits the `skill:` prefix — `/` — as long as the name is not taken by a system slash command. That is, `/code-style` falls back to matching `/skill:code-style`. Built-in Skills shipped with Kimi Code CLI appear directly as `/` in the slash command panel. For example, `/mcp-config` helps configure MCP servers and handle MCP OAuth login, and `/custom-theme [extra text]` invokes the custom-theme workflow to create or edit a TUI theme. diff --git a/docs/zh/guides/interaction.md b/docs/zh/guides/interaction.md index 6da8066a5..445f2c560 100644 --- a/docs/zh/guides/interaction.md +++ b/docs/zh/guides/interaction.md @@ -25,7 +25,7 @@ Kimi Code CLI 支持在输入框中直接粘贴图片和视频,让 AI 结合 以 `/` 开头的内容会被识别为斜杠命令。输入 `/` 后弹出补全菜单,随后续字符实时过滤;按 `Esc` 关闭菜单,匹配失败时内容会作为普通消息发送给 Agent。 -已激活的 [Agent Skills](../customization/skills.md) 会自动注册为斜杠命令:外部 Skill 以 `/skill:` 调用,内置 Skill 直接以 `/` 出现在斜杠命令面板中;若外部 Skill 名称与系统斜杠命令不冲突,也可以省略 `skill:` 前缀直接输入 `/`。 +已激活的 [Agent Skills](../customization/skills.md) 会自动注册为斜杠命令:普通外部 Skill 以 `/skill:` 调用,外部子 Skill 以 `/parent.child` 这样的点分命令显示,内置 Skill 直接以 `/` 出现在斜杠命令面板中;若外部 Skill 名称与系统斜杠命令不冲突,也可以省略 `skill:` 前缀直接输入 `/`。 部分命令仅在 Agent 空闲时可用,流式输出或上下文压缩期间需先按 `Esc` 中断。`/yolo`、`/plan`、`/help`、`/btw` 等模式切换和查询类命令则始终可用。全部命令说明见[斜杠命令参考](../reference/slash-commands.md)。 diff --git a/docs/zh/reference/slash-commands.md b/docs/zh/reference/slash-commands.md index e475ddf7d..48d5c8f5f 100644 --- a/docs/zh/reference/slash-commands.md +++ b/docs/zh/reference/slash-commands.md @@ -126,7 +126,7 @@ Kimi Code CLI 随包内置了一组 Skill,直接以 `/` 形式出现在 ## Skill 动态命令 -已激活的外部 Skill 会自动注册为斜杠命令,并以 `skill:` 作为命名空间前缀: +已激活的外部 Skill 会自动注册为斜杠命令。普通外部 Skill 以 `skill:` 作为命名空间前缀: ``` /skill: [附加文本] @@ -134,6 +134,14 @@ Kimi Code CLI 随包内置了一组 Skill,直接以 `/` 形式出现在 例如 `/skill:code-style` 加载名为 `code-style` 的 Skill 并发送给 Agent;命令后附带的文本拼接到 Skill 提示词之后。 +外部子 Skill 会直接以点分名称出现在斜杠命令面板中: + +``` +/. [附加文本] +``` + +例如,父 Skill 名为 `code-style`,其中子 Skill 的本地名称为 `review`,面板中显示为 `/code-style.review`。点分命令名由层级自动生成,子 Skill 的 `SKILL.md` 可以保留本地 `name`。 + 为方便输入,外部 Skill 命令同时支持省略 `skill:` 前缀的简写形式 `/`,前提是该名称未被系统斜杠命令占用——即 `/code-style` 会回退匹配到 `/skill:code-style`。 Kimi Code CLI 随包内置的 Skill 会直接以 `/` 形式出现在斜杠命令面板中。例如,`/mcp-config` 用于配置 MCP server 和处理 MCP OAuth 登录,`/custom-theme [附加文本]` 用于进入自定义主题流程,创建或编辑 TUI 主题。 diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index 8543b2596..af2e00a4d 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -213,6 +213,7 @@ export interface SkillSummary { readonly source: 'builtin' | 'user' | 'extra' | 'project'; readonly type?: string | undefined; readonly disableModelInvocation?: boolean | undefined; + readonly isSubSkill?: boolean | undefined; } export interface ActivateSkillPayload { diff --git a/packages/agent-core/src/skill/builtin/sub-skill.ts b/packages/agent-core/src/skill/builtin/sub-skill.ts index cd1c2ed81..3fe5fec02 100644 --- a/packages/agent-core/src/skill/builtin/sub-skill.ts +++ b/packages/agent-core/src/skill/builtin/sub-skill.ts @@ -18,6 +18,7 @@ function makeBuiltin( }); return { ...parsed, + name: dirName, path: pseudoPath, dir: pseudoPath, metadata: { @@ -39,12 +40,12 @@ export const SUB_SKILL_REVIEW = makeBuiltin( REVIEW_BODY, 'sub-skill.review', 'builtin://sub-skill/review', - { disableModelInvocation: true }, + { disableModelInvocation: true, isSubSkill: true }, ); export const SUB_SKILL_CONSOLIDATE = makeBuiltin( CONSOLIDATE_BODY, 'sub-skill.consolidate', 'builtin://sub-skill/consolidate', - { disableModelInvocation: true }, + { disableModelInvocation: true, isSubSkill: true }, ); diff --git a/packages/agent-core/src/skill/builtin/sub-skill/consolidate/SKILL.md b/packages/agent-core/src/skill/builtin/sub-skill/consolidate/SKILL.md index e495ac5fb..bf343b9c8 100644 --- a/packages/agent-core/src/skill/builtin/sub-skill/consolidate/SKILL.md +++ b/packages/agent-core/src/skill/builtin/sub-skill/consolidate/SKILL.md @@ -1,5 +1,5 @@ --- -name: sub-skill.consolidate +name: consolidate description: Apply an approved sub-skill grouping by moving user-specified skills into a parent bundle, with timestamped backups of every modified directory. disable-model-invocation: true --- @@ -27,12 +27,17 @@ Execute the reorganization by moving user-specified skills into a parent bundle, - If the parent already exists, ensure its frontmatter includes `has-sub-skill: true`. 4. **Move child skills into the parent.** Move each child skill's entire directory under the parent bundle. - Example: `web-search/` → `web-research/web-search/` -5. **Verify the result.** List the new directory structure and confirm each moved skill still has a valid `SKILL.md` with required frontmatter (`name` and `description`). -6. **Report the change.** Summarize what was moved, the new structure, and where backups are located. +5. **Keep documentation directory alignment.** When moving documentation, references, examples, assets, or other payload directories, align them with the new skill directory layout. + - Preserve relative links from `SKILL.md` to files such as `references/`, `assets/`, `examples/`, or templates. + - If a child skill moves from `//` to `///`, its documentation payload should move with that child unless the approved plan says otherwise. + - Do not leave documentation in the old location or merge unrelated documentation directories together. +6. **Verify the result.** List the new directory structure and confirm each moved skill still has a valid `SKILL.md` with required frontmatter (`name` and `description`). Check documentation directory alignment and relative links after the move. +7. **Report the change.** Summarize what was moved, the new structure, any documentation directories that moved, and where backups are located. ## Don'ts - **Never move skills without backing up first.** - **Never overwrite an existing backup** — always use a fresh timestamped suffix. - **Don't drop frontmatter or payload files** during the move; the entire directory must be preserved. +- **Don't break documentation directory alignment** — references, assets, examples, and templates must stay aligned with the skill directory that uses them. - **Don't create deeply nested hierarchies** (3+ levels) unless the user explicitly requests it. diff --git a/packages/agent-core/src/skill/builtin/sub-skill/review/SKILL.md b/packages/agent-core/src/skill/builtin/sub-skill/review/SKILL.md index fd3b6fefb..187c76948 100644 --- a/packages/agent-core/src/skill/builtin/sub-skill/review/SKILL.md +++ b/packages/agent-core/src/skill/builtin/sub-skill/review/SKILL.md @@ -1,5 +1,5 @@ --- -name: sub-skill.review +name: review description: Analyze the available skill set and recommend candidate groups that could be consolidated into sub-skill bundles. Read-only — proposes a plan, does not move files. disable-model-invocation: true --- @@ -23,6 +23,7 @@ Analyze the current skill inventory and identify candidate groups that could be 5. **Propose sub-skill structures.** For each candidate group, list: - The parent skill name and a one-line description. - The children that should move under it. + - Any documentation, reference, example, asset, or template directories that must move with each child so the final directory layout stays aligned. - Whether the parent needs `has-sub-skill: true` (it does, if children should be discovered). 6. **Output a summary report.** Present findings as a concise grouped list with rationale, and stop. Do **not** edit any file — that's `sub-skill.consolidate`'s job. @@ -41,5 +42,6 @@ Proposed sub-skill: web-research - Children: - web-search → move under web-research/search - fetch-url → move under web-research/fetch + - Documentation alignment: move each child's references/examples/assets with that child. - Rationale: Both deal with online information retrieval and are often chained together. ``` diff --git a/packages/agent-core/src/skill/registry.ts b/packages/agent-core/src/skill/registry.ts index 44f37d84d..00368e391 100644 --- a/packages/agent-core/src/skill/registry.ts +++ b/packages/agent-core/src/skill/registry.ts @@ -131,7 +131,10 @@ export class SkillRegistry { getModelSkillListing(): string { const lines = ['DISREGARD any earlier skill listings. Current available skills:']; - const listing = renderGroupedSkills(this.listInvocableSkills(), formatModelSkill); + const listing = renderGroupedSkills( + this.listInvocableSkills().filter((skill) => skill.metadata.isSubSkill !== true), + formatModelSkill, + ); if (listing.length > 0) { lines.push(listing); } diff --git a/packages/agent-core/src/skill/scanner.ts b/packages/agent-core/src/skill/scanner.ts index 6e60e8f0e..6c1ddb462 100644 --- a/packages/agent-core/src/skill/scanner.ts +++ b/packages/agent-core/src/skill/scanner.ts @@ -145,6 +145,7 @@ export async function discoverSkills( root: SkillRoot, isTopLevel: boolean, depth: number, + subSkillParentName?: string, ): Promise { if (depth > MAX_SKILL_SCAN_DEPTH) return; @@ -171,7 +172,7 @@ export async function discoverSkills( if (await isDir(entryPath)) subdirs.push(entry); } - const allowedSubSkillBundles = new Set(); + const allowedSubSkillBundles = new Map(); for (const entry of directorySkills) { const skill = await parseAndRegister({ parse, @@ -182,9 +183,10 @@ export async function discoverSkills( onDiscoveredSkill: options.onDiscoveredSkill, warn, skip, + subSkillParentName, }); if (skill !== undefined && hasSubSkillEnabled(skill)) { - allowedSubSkillBundles.add(entry); + allowedSubSkillBundles.set(entry, skill.name); } } @@ -237,7 +239,14 @@ export async function discoverSkills( for (const entry of subdirs) { if (directorySkills.has(entry) && !allowedSubSkillBundles.has(entry)) continue; - await walkSkillDir(path.join(dirPath, entry), root, false, depth + 1); + const allowedSubSkillParentName = allowedSubSkillBundles.get(entry); + await walkSkillDir( + path.join(dirPath, entry), + root, + false, + depth + 1, + allowedSubSkillParentName ?? subSkillParentName, + ); } } @@ -359,13 +368,26 @@ async function parseAndRegister(input: { readonly onDiscoveredSkill?: (skill: SkillDefinition) => void; readonly warn: (message: string, cause?: unknown) => void; readonly skip: (skill: SkippedSkill) => void; + readonly subSkillParentName?: string; }): Promise { try { - const skill = await input.parse({ + const parsed = await input.parse({ skillMdPath: input.skillMdPath, skillDirName: input.skillDirName, source: input.root.source, }); + const subSkillParentName = input.subSkillParentName; + const skill = + subSkillParentName !== undefined + ? { + ...parsed, + name: qualifySubSkillName(subSkillParentName, parsed.name), + metadata: { + ...parsed.metadata, + isSubSkill: true, + }, + } + : parsed; const discovered = input.root.plugin === undefined ? skill : { ...skill, plugin: input.root.plugin, @@ -375,7 +397,7 @@ async function parseAndRegister(input: { if (!input.byName.has(key)) { input.byName.set(key, discovered); } - return skill; + return discovered; } catch (error) { if (error instanceof UnsupportedSkillTypeError) { input.skip({ @@ -392,6 +414,11 @@ async function parseAndRegister(input: { } } +function qualifySubSkillName(parentName: string, skillName: string): string { + if (skillName === parentName || skillName.startsWith(`${parentName}.`)) return skillName; + return `${parentName}.${skillName}`; +} + function hasSubSkillEnabled(skill: SkillDefinition): boolean { const nested = skill.metadata['metadata']; const nestedFlag = diff --git a/packages/agent-core/src/skill/types.ts b/packages/agent-core/src/skill/types.ts index e7485fc16..7e030adf9 100644 --- a/packages/agent-core/src/skill/types.ts +++ b/packages/agent-core/src/skill/types.ts @@ -6,6 +6,7 @@ export interface SkillMetadata { readonly type?: string | undefined; readonly whenToUse?: string | undefined; readonly disableModelInvocation?: boolean | undefined; + readonly isSubSkill?: boolean | undefined; readonly safe?: boolean | undefined; readonly arguments?: readonly unknown[] | string | undefined; readonly [key: string]: unknown; @@ -31,6 +32,7 @@ export interface SkillSummary { readonly source: SkillSource; readonly type?: string | undefined; readonly disableModelInvocation?: boolean | undefined; + readonly isSubSkill?: boolean | undefined; } export interface SkillRoot { @@ -80,5 +82,6 @@ export function summarizeSkill(skill: SkillDefinition): SkillSummary { source: skill.source, type: skill.metadata.type, disableModelInvocation: skill.metadata.disableModelInvocation, + isSubSkill: skill.metadata.isSubSkill, }; } diff --git a/packages/agent-core/test/profile/agent-profile-loader.test.ts b/packages/agent-core/test/profile/agent-profile-loader.test.ts index 642daf55e..0d62a5579 100644 --- a/packages/agent-core/test/profile/agent-profile-loader.test.ts +++ b/packages/agent-core/test/profile/agent-profile-loader.test.ts @@ -189,7 +189,10 @@ describe('default agent profiles', () => { const skills = new SkillRegistry(); skills.register(skill('review', { whenToUse: 'When code review is requested.' })); skills.register({ - ...skill('nested-review', { whenToUse: 'When nested review is requested.' }), + ...skill('nested-review', { + isSubSkill: true, + whenToUse: 'When nested review is requested.', + }), path: '/skills/parent/nested-review/SKILL.md', dir: '/skills/parent/nested-review', content: 'Nested review body must not enter system prompt.', @@ -204,10 +207,10 @@ describe('default agent profiles', () => { expect(prompt).toContain('Current available skills:'); expect(prompt).toContain('- review:'); - expect(prompt).toContain('- nested-review:'); - expect(prompt).toContain('Path: /skills/parent/nested-review/SKILL.md'); expect(prompt).toContain('When to use: When code review is requested.'); - expect(prompt).toContain('When to use: When nested review is requested.'); + expect(prompt).not.toContain('- nested-review:'); + expect(prompt).not.toContain('Path: /skills/parent/nested-review/SKILL.md'); + expect(prompt).not.toContain('When to use: When nested review is requested.'); expect(prompt).not.toContain('private'); expect(prompt).not.toContain('flow-only'); expect(prompt).not.toContain('body of review'); diff --git a/packages/agent-core/test/session/init.test.ts b/packages/agent-core/test/session/init.test.ts index d6b844bfb..89657684a 100644 --- a/packages/agent-core/test/session/init.test.ts +++ b/packages/agent-core/test/session/init.test.ts @@ -581,7 +581,7 @@ describe('AgentAPI.startBtw', () => { try { const disabledSkills = await disabledSession.listSkills(); expect(disabledSkills.map((skill) => skill.name)).toContain('outer'); - expect(disabledSkills.map((skill) => skill.name)).toContain('inner'); + expect(disabledSkills.map((skill) => skill.name)).toContain('outer.inner'); expect(disabledSkills.map((skill) => skill.name)).toContain('sub-skill.consolidate'); } finally { await disabledSession.close(); @@ -598,7 +598,7 @@ describe('AgentAPI.startBtw', () => { try { const enabledSkills = await enabledSession.listSkills(); expect(enabledSkills.map((skill) => skill.name)).toContain('outer'); - expect(enabledSkills.map((skill) => skill.name)).toContain('inner'); + expect(enabledSkills.map((skill) => skill.name)).toContain('outer.inner'); expect(enabledSkills.map((skill) => skill.name)).toContain('sub-skill.consolidate'); } finally { await enabledSession.close(); diff --git a/packages/agent-core/test/skill/builtin-sub-skill.test.ts b/packages/agent-core/test/skill/builtin-sub-skill.test.ts index 3cee3dc99..797fead70 100644 --- a/packages/agent-core/test/skill/builtin-sub-skill.test.ts +++ b/packages/agent-core/test/skill/builtin-sub-skill.test.ts @@ -54,6 +54,7 @@ describe('builtin skill: sub-skill.review', () => { expect(SUB_SKILL_REVIEW.source).toBe('builtin'); expect(SUB_SKILL_REVIEW.description.length).toBeGreaterThan(0); expect(SUB_SKILL_REVIEW.metadata.type).toBe('inline'); + expect(SUB_SKILL_REVIEW.metadata.name).toBe('review'); }); it('is hidden from model invocation', () => { @@ -77,6 +78,7 @@ describe('builtin skill: sub-skill.consolidate', () => { expect(SUB_SKILL_CONSOLIDATE.source).toBe('builtin'); expect(SUB_SKILL_CONSOLIDATE.description.length).toBeGreaterThan(0); expect(SUB_SKILL_CONSOLIDATE.metadata.type).toBe('inline'); + expect(SUB_SKILL_CONSOLIDATE.metadata.name).toBe('consolidate'); }); it('is hidden from model invocation', () => { @@ -88,6 +90,11 @@ describe('builtin skill: sub-skill.consolidate', () => { expect(SUB_SKILL_CONSOLIDATE.content).toContain('timestamped'); }); + it('mentions documentation directory alignment in its content', () => { + expect(SUB_SKILL_CONSOLIDATE.content).toContain('documentation'); + expect(SUB_SKILL_CONSOLIDATE.content).toContain('directory alignment'); + }); + it('registers through registerBuiltinSkills', () => { const registry = new SkillRegistry(); registerBuiltinSkills(registry); diff --git a/packages/agent-core/test/skill/scanner.test.ts b/packages/agent-core/test/skill/scanner.test.ts index d6e67a9d1..19bf76605 100644 --- a/packages/agent-core/test/skill/scanner.test.ts +++ b/packages/agent-core/test/skill/scanner.test.ts @@ -363,7 +363,9 @@ describe('discoverSkills shape and ordering', () => { const skills = await discoverSkills({ roots: [{ path: root, source: 'user' }] }); - expect(skills.map((s) => s.name)).toEqual(['inner', 'outer']); + expect(skills.map((s) => s.name)).toEqual(['outer', 'outer.inner']); + expect(skills.find((s) => s.name === 'outer')?.metadata.isSubSkill).toBeUndefined(); + expect(skills.find((s) => s.name === 'outer.inner')?.metadata.isSubSkill).toBe(true); }); it('does not discover nested SKILL.md files when the parent bundle disables sub-skills', async () => {