-
Notifications
You must be signed in to change notification settings - Fork 381
feat(skills): add skill discovery and loading functionality #524
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- 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.
There was a problem hiding this 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.pymodule for skill discovery and YAML frontmatter parsing - CLI option
--skill-folderto 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.
tests/test_skillspec.py
Outdated
| 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 == [] | ||
|
|
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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.
tests/test_skillspec.py
Outdated
| 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 == [] | ||
|
|
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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.
src/kimi_cli/skillspec.py
Outdated
| lines.append(f"- **{skill.name}**: {skill.description}") | ||
| lines.append(f" - Path: `{skill.skill_md_path}`") |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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.
src/kimi_cli/skillspec.py
Outdated
| Returns: | ||
| SkillInfo object if valid, None otherwise. | ||
| """ | ||
| content = skill_md_path.read_text(encoding="utf-8") |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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.
| 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 |
src/kimi_cli/skillspec.py
Outdated
| except Exception: | ||
| # Skip invalid skills | ||
| continue |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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.
src/kimi_cli/skillspec.py
Outdated
| end_idx = content.find("---", 3) | ||
| if end_idx == -1: | ||
| return None | ||
|
|
||
| frontmatter = content[3:end_idx].strip() |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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.
| 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() |
src/kimi_cli/skillspec.py
Outdated
| if not name or not description: | ||
| return None | ||
|
|
||
| if not isinstance(name, str) or not isinstance(description, str): |
Copilot
AI
Dec 24, 2025
There was a problem hiding this comment.
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.
| 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(): |
…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>
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/skillsby default, with a fallback to~/.claude/skillswhen the Kimi directory is missing, and can be overridden with a CLI flag. Skill metadata is read fromSKILL.mdYAML frontmatter and surfaced to the system prompt viaKIMI_SKILLS.Key Changes
Skill discovery utilities:
skill.pywithSkillmodel,discover_skills(), and helpers for default skill directories.utils/frontmatter.pyto parse YAML frontmatter fromSKILL.md.Runtime and CLI integration:
skills_dirintoRuntime.create()and surfaceKIMI_SKILLSin built-in prompt args.--skills-dirto the CLI with validation and default~/.kimi/skillsfallback.Prompt updates:
${KIMI_SKILLS}.Tests and changelog:
~/.kimi/skillsor~/.claude/skills.Usage
Users can now specify a custom skills directory:
If not specified, the default
~/.kimi/skillsis used; if it does not exist and~/.claude/skillsdoes, that directory is used instead. Discovered skills are included in the system prompt.