Skip to content

Conversation

@YoungY620
Copy link
Collaborator

@YoungY620 YoungY620 commented Dec 24, 2025

Related Issue

Resolves #107

Description

This PR adds skill discovery from the filesystem and exposes available skills in the default agent prompt. Skills are loaded from ~/.kimi/skills by default, with a fallback to ~/.claude/skills when the Kimi directory is missing, and can be overridden with a CLI flag. Skill metadata is read from SKILL.md YAML frontmatter and surfaced to the system prompt via KIMI_SKILLS.

Key Changes

  • Skill discovery utilities:

    • New skill.py with Skill model, discover_skills(), and helpers for default skill directories.
    • New utils/frontmatter.py to parse YAML frontmatter from SKILL.md.
    • Invalid skills are skipped with a log entry; skills sorted by name.
  • Runtime and CLI integration:

    • Pass skills_dir into Runtime.create() and surface KIMI_SKILLS in built-in prompt args.
    • Add --skills-dir to the CLI with validation and default ~/.kimi/skills fallback.
  • Prompt updates:

    • Default system prompt now includes an "Agent Skills" section and renders ${KIMI_SKILLS}.
  • Tests and changelog:

    • Add coverage for frontmatter parsing and skill discovery, update default agent snapshots.
    • Add changelog entry for skill discovery in ~/.kimi/skills or ~/.claude/skills.

Usage

Users can now specify a custom skills directory:

kimi --skills-dir /path/to/skills

If not specified, the default ~/.kimi/skills is used; if it does not exist and ~/.claude/skills does, that directory is used instead. Discovered skills are included in the system prompt.

- Introduced a new `skillspec.py` module for managing skill specifications.
- Added `skill_folder` parameter to `KimiCLI` and `Runtime` for specifying skill locations.
- Implemented skill discovery logic to load skills from the specified folder.
- Updated CLI to support skill folder option.
- Enhanced system prompt to include discovered skills information.
- Added tests for skill discovery and formatting.
Copilot AI review requested due to automatic review settings December 24, 2025 05:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a filesystem-based skill system that enables Kimi CLI to discover and utilize custom skills defined in SKILL.md files with YAML frontmatter. Skills are discovered from a configurable folder (defaulting to ~/.kimi/skill) and automatically included in the agent's system prompt.

Key Changes:

  • New skillspec.py module for skill discovery and YAML frontmatter parsing
  • CLI option --skill-folder to specify custom skill directories
  • Integration of discovered skills into the system prompt via ${KIMI_SKILLS} placeholder

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/kimi_cli/skillspec.py New module implementing skill discovery, YAML frontmatter parsing, and skill formatting for system prompts
src/kimi_cli/soul/agent.py Integrates skill discovery into Runtime creation, determines skill folder location, and passes formatted skills to system prompt
src/kimi_cli/cli.py Adds --skill-folder CLI option with validation for directory existence and readability
src/kimi_cli/app.py Passes skill_folder parameter through to Runtime.create()
src/kimi_cli/agents/default/system.md Adds comprehensive documentation section explaining skills concept, usage, and directory structure
tests/test_skillspec.py Comprehensive test suite for skill discovery, validation, formatting, and edge cases
tests/test_default_agent.py Updates snapshot tests to include new skills section in system prompt
tests/conftest.py Adds KIMI_SKILLS to builtin_args fixture for testing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 52 to 84
def test_discover_skills_with_invalid_skill():
"""Test discovering a skill with invalid SKILL.md."""
with TemporaryDirectory() as tmpdir:
# Skill without frontmatter
skill_dir1 = Path(tmpdir) / "invalid-skill-1"
skill_dir1.mkdir()
(skill_dir1 / "SKILL.md").write_text("# No frontmatter", encoding="utf-8")

# Skill without name
skill_dir2 = Path(tmpdir) / "invalid-skill-2"
skill_dir2.mkdir()
(skill_dir2 / "SKILL.md").write_text(
"""---
description: Missing name
---
""",
encoding="utf-8",
)

# Skill without description
skill_dir3 = Path(tmpdir) / "invalid-skill-3"
skill_dir3.mkdir()
(skill_dir3 / "SKILL.md").write_text(
"""---
name: missing-description
---
""",
encoding="utf-8",
)

skills = discover_skills(Path(tmpdir))
assert skills == []

Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite doesn't include a test case for malformed YAML in SKILL.md files (e.g., invalid YAML syntax). Consider adding a test to verify that skills with YAML parsing errors are properly skipped without crashing the discovery process.

Copilot uses AI. Check for mistakes.
Comment on lines 52 to 84
def test_discover_skills_with_invalid_skill():
"""Test discovering a skill with invalid SKILL.md."""
with TemporaryDirectory() as tmpdir:
# Skill without frontmatter
skill_dir1 = Path(tmpdir) / "invalid-skill-1"
skill_dir1.mkdir()
(skill_dir1 / "SKILL.md").write_text("# No frontmatter", encoding="utf-8")

# Skill without name
skill_dir2 = Path(tmpdir) / "invalid-skill-2"
skill_dir2.mkdir()
(skill_dir2 / "SKILL.md").write_text(
"""---
description: Missing name
---
""",
encoding="utf-8",
)

# Skill without description
skill_dir3 = Path(tmpdir) / "invalid-skill-3"
skill_dir3.mkdir()
(skill_dir3 / "SKILL.md").write_text(
"""---
name: missing-description
---
""",
encoding="utf-8",
)

skills = discover_skills(Path(tmpdir))
assert skills == []

Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite doesn't cover edge cases where name or description fields in YAML frontmatter are non-string types (e.g., numbers, booleans, lists) or whitespace-only strings. Consider adding tests for these cases to verify proper validation and rejection of invalid skill metadata.

Copilot uses AI. Check for mistakes.
Comment on lines 123 to 124
lines.append(f"- **{skill.name}**: {skill.description}")
lines.append(f" - Path: `{skill.skill_md_path}`")
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skill name and description are inserted directly into markdown formatting without escaping. If a skill name or description contains markdown special characters (e.g., *, `, [, ]), it could break the markdown formatting or potentially be misinterpreted. Consider escaping or sanitizing these values before including them in the formatted output.

Copilot uses AI. Check for mistakes.
Returns:
SkillInfo object if valid, None otherwise.
"""
content = skill_md_path.read_text(encoding="utf-8")
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code reads file content without catching potential I/O exceptions (PermissionError, OSError, UnicodeDecodeError). While the outer try-except block in discover_skills catches these, the broad exception handler makes debugging difficult. Consider catching specific exceptions in _parse_skill_md to provide better error handling or logging for common file reading issues.

Suggested change
content = skill_md_path.read_text(encoding="utf-8")
try:
content = skill_md_path.read_text(encoding="utf-8")
except (PermissionError, OSError, UnicodeDecodeError):
# Treat unreadable or undecodable SKILL.md files as invalid skills.
return None

Copilot uses AI. Check for mistakes.
Comment on lines 51 to 53
except Exception:
# Skip invalid skills
continue
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The broad exception handler silently suppresses all errors during skill parsing, making it difficult to debug issues with invalid SKILL.md files. Consider logging the exception at debug or warning level to help users understand why a skill was skipped.

Copilot uses AI. Check for mistakes.
Comment on lines 76 to 80
end_idx = content.find("---", 3)
if end_idx == -1:
return None

frontmatter = content[3:end_idx].strip()
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frontmatter parsing has a subtle bug: it searches for the closing "---" starting at position 3, but it should start searching after the opening "---\n" sequence (position 4 or later). If a file starts with "----" (4 dashes), the parser would incorrectly find the closing delimiter at position 3, resulting in an empty frontmatter. The search should start at position 4 to skip past the newline after the opening delimiter.

Suggested change
end_idx = content.find("---", 3)
if end_idx == -1:
return None
frontmatter = content[3:end_idx].strip()
end_idx = content.find("---", 4)
if end_idx == -1:
return None
frontmatter = content[4:end_idx].strip()

Copilot uses AI. Check for mistakes.
Comment on lines 94 to 97
if not name or not description:
return None

if not isinstance(name, str) or not isinstance(description, str):
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation checks if name or description are falsy, which correctly rejects empty strings. However, it doesn't reject whitespace-only strings (e.g., " "). Consider using .strip() on the values and checking if they're non-empty after stripping to ensure skills have meaningful names and descriptions.

Suggested change
if not name or not description:
return None
if not isinstance(name, str) or not isinstance(description, str):
if not isinstance(name, str) or not isinstance(description, str):
return None
# Reject empty or whitespace-only names/descriptions
if not name.strip() or not description.strip():

Copilot uses AI. Check for mistakes.
YoungY620 and others added 9 commits December 24, 2025 14:02
…d validation

- Added logging for skipped invalid skills during discovery.
- Updated frontmatter parsing to handle edge cases and invalid formats.
- Implemented stricter validation for skill names and descriptions to reject non-string and whitespace-only values.
- Added tests for handling malformed YAML and non-string types in skill specifications.
- Consolidated logging statement for skipped invalid skills to improve readability and maintainability.
Signed-off-by: Richard Chien <stdrc@outlook.com>
Signed-off-by: Richard Chien <stdrc@outlook.com>
Signed-off-by: Richard Chien <stdrc@outlook.com>
Signed-off-by: Richard Chien <stdrc@outlook.com>
Signed-off-by: Richard Chien <stdrc@outlook.com>
Signed-off-by: Richard Chien <stdrc@outlook.com>
@stdrc stdrc merged commit ea8cf1b into MoonshotAI:main Dec 25, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kimi CLI Skill.md

2 participants