Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/relax-skill-frontmatter-requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code": patch
---

Fix silent skipping of user skills with missing frontmatter name or description by falling back to the directory name and body text.
19 changes: 19 additions & 0 deletions packages/agent-core/src/profile/default/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,25 @@ Identify the skills that are likely to be useful for the tasks you are currently

Only read skill details when needed to conserve the context window.

## Skill format

A directory skill (e.g. `my-skill/SKILL.md`) may begin with an optional YAML frontmatter block:

```yaml
---
name: my-skill
description: What this skill does and when to use it.
type: prompt
whenToUse: When the user asks for ...
---
```

- `name` defaults to the directory name if omitted.
- `description` defaults to the first non-empty line of the body if omitted.
- `type` defaults to `prompt`; other valid values are `inline` and `flow`.
- `whenToUse` helps the model decide when to invoke the skill automatically.
- `disableModelInvocation: true` prevents the model from auto-invoking the skill.

# Ultimate Reminders

At any time, you should be HELPFUL, CONCISE, and ACCURATE. Be thorough in your actions — test what you build, verify what you change — not in your explanations.
Expand Down
11 changes: 0 additions & 11 deletions packages/agent-core/src/skill/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,6 @@ export function parseFrontmatter(text: string): ParsedFrontmatter {
}

export function parseSkillText(options: ParseSkillTextOptions): SkillDefinition {
const isDirectorySkill = path.basename(options.skillMdPath) === 'SKILL.md';
if (isDirectorySkill && options.text.split(/\r?\n/, 1)[0]?.trim() !== FENCE) {
throw new SkillParseError(`Missing frontmatter in ${options.skillMdPath}`);
}

let parsed;
try {
parsed = parseFrontmatter(options.text);
Expand Down Expand Up @@ -137,12 +132,6 @@ export function parseSkillText(options: ParseSkillTextOptions): SkillDefinition

const name = nonEmptyString(metadata.name);
const description = nonEmptyString(metadata.description);
if (isDirectorySkill && (name === undefined || description === undefined)) {
const field = name === undefined ? '"name"' : '"description"';
throw new SkillParseError(
`Missing required frontmatter field ${field} in ${options.skillMdPath}`,
);
}

const skillPath = path.resolve(options.skillMdPath);
const content = parsed.body.trim();
Expand Down
21 changes: 15 additions & 6 deletions packages/agent-core/test/skill/scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('skill discovery', () => {
expect(registry.listInvocableSkills()).toEqual([]);
});

it('skips directory skills with missing frontmatter metadata', async () => {
it('falls back to directory name and body for missing directory skill metadata', async () => {
const { repoDir } = await makeWorkspace();
const projectRoot = path.join(repoDir, '.kimi-code', 'skills');
await writeSkill(projectRoot, path.join('valid', 'SKILL.md'), [
Expand Down Expand Up @@ -182,11 +182,20 @@ describe('skill discovery', () => {
onWarning: (message) => warnings.push(message),
});

expect(skills.map((skill) => skill.name)).toEqual(['valid']);
expect(warnings).toHaveLength(3);
expect(warnings.some((message) => message.includes('Missing frontmatter'))).toBe(true);
expect(warnings.some((message) => message.includes('"name"'))).toBe(true);
expect(warnings.some((message) => message.includes('"description"'))).toBe(true);
const names = skills.map((skill) => skill.name).sort();
expect(names).toEqual([
'missing-description',
'missing-name',
'no-frontmatter',
'valid',
]);
expect(warnings).toHaveLength(0);

const missingNameSkill = skills.find((s) => s.name === 'missing-name');
expect(missingNameSkill?.description).toBe('Missing name');

const noFrontmatterSkill = skills.find((s) => s.name === 'no-frontmatter');
expect(noFrontmatterSkill?.description).toBe('# Heading should not become a description');
});
});

Expand Down