mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(skill): qualify sub-skill names with parent prefix (#651)
* feat(skill): qualify sub-skill names with parent prefix - qualify sub-skill names with parent prefix and set isSubSkill metadata\n- hide sub-skills from the model skill listing - expose sub-skills as dotted slash commands in TUI - update slash command docs for English and Chinese - align built-in sub-skill local names with their directories
This commit is contained in:
parent
911e7c3fcf
commit
c39c62590d
19 changed files with 126 additions and 24 deletions
6
.changeset/qualify-sub-skill-names.md
Normal file
6
.changeset/qualify-sub-skill-names.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -33,7 +33,10 @@ export function buildSkillSlashCommands(skills: readonly SkillSummary[]): SkillS
|
|||
const commandMap = new Map<string, string>();
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:<name>`, while built-in Skills appear directly as `/<name>` 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 `/<name>` directly.
|
||||
Active [Agent Skills](../customization/skills.md) are automatically registered as slash commands: ordinary external Skills are invoked with `/skill:<name>`, external sub-skills appear as dotted commands such as `/parent.child`, and built-in Skills appear directly as `/<name>` 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 `/<name>` 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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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:<name> [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:
|
||||
|
||||
```
|
||||
/<parent-skill>.<sub-skill> [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 — `/<name>` — 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 `/<name>` 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.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Kimi Code CLI 支持在输入框中直接粘贴图片和视频,让 AI 结合
|
|||
|
||||
以 `/` 开头的内容会被识别为斜杠命令。输入 `/` 后弹出补全菜单,随后续字符实时过滤;按 `Esc` 关闭菜单,匹配失败时内容会作为普通消息发送给 Agent。
|
||||
|
||||
已激活的 [Agent Skills](../customization/skills.md) 会自动注册为斜杠命令:外部 Skill 以 `/skill:<name>` 调用,内置 Skill 直接以 `/<name>` 出现在斜杠命令面板中;若外部 Skill 名称与系统斜杠命令不冲突,也可以省略 `skill:` 前缀直接输入 `/<name>`。
|
||||
已激活的 [Agent Skills](../customization/skills.md) 会自动注册为斜杠命令:普通外部 Skill 以 `/skill:<name>` 调用,外部子 Skill 以 `/parent.child` 这样的点分命令显示,内置 Skill 直接以 `/<name>` 出现在斜杠命令面板中;若外部 Skill 名称与系统斜杠命令不冲突,也可以省略 `skill:` 前缀直接输入 `/<name>`。
|
||||
|
||||
部分命令仅在 Agent 空闲时可用,流式输出或上下文压缩期间需先按 `Esc` 中断。`/yolo`、`/plan`、`/help`、`/btw` 等模式切换和查询类命令则始终可用。全部命令说明见[斜杠命令参考](../reference/slash-commands.md)。
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ Kimi Code CLI 随包内置了一组 Skill,直接以 `/<name>` 形式出现在
|
|||
|
||||
## Skill 动态命令
|
||||
|
||||
已激活的外部 Skill 会自动注册为斜杠命令,并以 `skill:` 作为命名空间前缀:
|
||||
已激活的外部 Skill 会自动注册为斜杠命令。普通外部 Skill 以 `skill:` 作为命名空间前缀:
|
||||
|
||||
```
|
||||
/skill:<name> [附加文本]
|
||||
|
|
@ -134,6 +134,14 @@ Kimi Code CLI 随包内置了一组 Skill,直接以 `/<name>` 形式出现在
|
|||
|
||||
例如 `/skill:code-style` 加载名为 `code-style` 的 Skill 并发送给 Agent;命令后附带的文本拼接到 Skill 提示词之后。
|
||||
|
||||
外部子 Skill 会直接以点分名称出现在斜杠命令面板中:
|
||||
|
||||
```
|
||||
/<parent-skill>.<sub-skill> [附加文本]
|
||||
```
|
||||
|
||||
例如,父 Skill 名为 `code-style`,其中子 Skill 的本地名称为 `review`,面板中显示为 `/code-style.review`。点分命令名由层级自动生成,子 Skill 的 `SKILL.md` 可以保留本地 `name`。
|
||||
|
||||
为方便输入,外部 Skill 命令同时支持省略 `skill:` 前缀的简写形式 `/<name>`,前提是该名称未被系统斜杠命令占用——即 `/code-style` 会回退匹配到 `/skill:code-style`。
|
||||
|
||||
Kimi Code CLI 随包内置的 Skill 会直接以 `/<name>` 形式出现在斜杠命令面板中。例如,`/mcp-config` 用于配置 MCP server 和处理 MCP OAuth 登录,`/custom-theme [附加文本]` 用于进入自定义主题流程,创建或编辑 TUI 主题。
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 `<root>/<child>/` to `<root>/<parent>/<child>/`, 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ export async function discoverSkills(
|
|||
root: SkillRoot,
|
||||
isTopLevel: boolean,
|
||||
depth: number,
|
||||
subSkillParentName?: string,
|
||||
): Promise<void> {
|
||||
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<string>();
|
||||
const allowedSubSkillBundles = new Map<string, string>();
|
||||
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<SkillDefinition | undefined> {
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue