mirror of
https://github.com/instructkr/claw-code.git
synced 2026-11-07 09:37:54 +01:00
feat(skills): implement bundled skills registry and skill chaining support (#3324)
- Add SkillOrigin::Bundled variant to support bundled skills registry - Implement bundled_skill_root() function for discovering bundled skills - Update skill discovery to include bundled skills from standard install paths - Create example bundled skills (example-skill, chain-example) - Add comprehensive tests for bundled skills discovery and metadata - Bundled skills follow same directory structure as plugins - Skills can now chain/invoke other skills using standard dispatch mechanisms - Maintains backward compatibility with existing SkillsDir and LegacyCommandsDir
This commit is contained in:
1 parent
09f3b366ba
commit
cd04270a17
4 files changed
+253
-5
No files matched your search
@@ -0,0 +1,106 @@
|
|||||||
|
# Bundled Skills Registry and Chaining
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The bundled skills registry enables claw-code to include built-in skills with each release, similar to how bundled plugins work. This feature supports skill chaining, allowing skills to invoke and compose with other skills.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
**Bundled Skills Root Discovery**: Skills are discovered from the following locations (in order of precedence):
|
||||||
|
|
||||||
|
1. `<exe_dir>/../share/claw/skills/bundled` — standard FHS install layout
|
||||||
|
2. `<exe_dir>/bundled-skills` — simple relocated layout
|
||||||
|
3. `CARGO_MANIFEST_DIR/bundled` — dev/source-tree fallback
|
||||||
|
4. Default fallback if above paths don't exist
|
||||||
|
|
||||||
|
The directory structure mirrors the plugin system:
|
||||||
|
```
|
||||||
|
rust/crates/commands/bundled/
|
||||||
|
├── example-skill/
|
||||||
|
│ └── SKILL.md
|
||||||
|
└── chain-example/
|
||||||
|
└── SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### SkillOrigin Enum
|
||||||
|
|
||||||
|
A new `Bundled` variant was added to the `SkillOrigin` enum:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
enum SkillOrigin {
|
||||||
|
SkillsDir, // User/project skills directories
|
||||||
|
LegacyCommandsDir, // Legacy /commands directories
|
||||||
|
Bundled, // Built-in bundled skills [NEW]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skill Discovery
|
||||||
|
|
||||||
|
The `discover_skill_roots()` function was updated to include bundled skills:
|
||||||
|
- Bundled skills are added to the discovery roots with `SkillOrigin::Bundled`
|
||||||
|
- They're loaded using the same mechanism as regular skills
|
||||||
|
- Bundled skills have origin metadata ("bundled") in JSON output
|
||||||
|
|
||||||
|
### Skill Chaining
|
||||||
|
|
||||||
|
Skills can invoke other skills through standard mechanisms:
|
||||||
|
|
||||||
|
1. **Slash Command Invocation**: A skill can document that it chains to another skill
|
||||||
|
```
|
||||||
|
/skill chain-example
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Tool-based Invocation**: Via the Skill tool or similar mechanisms
|
||||||
|
|
||||||
|
Skills marked as "bundled" indicate they're managed by the claw-code release and persist across upgrades.
|
||||||
|
|
||||||
|
## Example Bundled Skills
|
||||||
|
|
||||||
|
### `example-skill`
|
||||||
|
A simple demonstration skill showing the bundled skills feature.
|
||||||
|
|
||||||
|
### `chain-example`
|
||||||
|
An example showing how one skill can reference and chain to other skills (e.g., `example-skill`).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Three new tests were added:
|
||||||
|
|
||||||
|
1. **`discovers_bundled_skills_from_binary_layout`**
|
||||||
|
- Verifies that `discover_skill_roots()` includes bundled skills
|
||||||
|
- Tests directory discovery logic
|
||||||
|
|
||||||
|
2. **`bundled_skills_have_correct_origin_metadata`**
|
||||||
|
- Confirms `SkillOrigin::Bundled` generates correct metadata
|
||||||
|
- Tests JSON output format
|
||||||
|
|
||||||
|
3. **`bundled_skills_are_included_in_json_output`**
|
||||||
|
- Verifies loaded bundled skills appear in JSON output
|
||||||
|
- Tests end-to-end loading flow
|
||||||
|
|
||||||
|
All tests pass with:
|
||||||
|
```bash
|
||||||
|
cargo test -p commands --lib
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backward Compatibility
|
||||||
|
|
||||||
|
- Existing skill discovery paths unchanged
|
||||||
|
- `SkillsDir` and `LegacyCommandsDir` origins fully preserved
|
||||||
|
- No changes to skill file format or content
|
||||||
|
- Bundled skills are additive; they don't replace user/project skills
|
||||||
|
|
||||||
|
## Future Enhancement Points
|
||||||
|
|
||||||
|
1. **Bundled Skills Registry Sync**: Auto-sync bundled skills into a system registry (similar to plugins)
|
||||||
|
2. **Skill Dependency Resolution**: Automatic dependency discovery and loading
|
||||||
|
3. **Skill Versioning**: Track versions of bundled skills
|
||||||
|
4. **Skill Auto-Update**: Sync bundled skills on claw-code updates
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- **Issue**: #3324 [Slice 2.2] Skills bundled registry & chaining
|
||||||
|
- **Plugin System**: `rust/crates/plugins/src/lib.rs` (bundled plugins reference implementation)
|
||||||
|
- **Commands Crate**: `rust/crates/commands/src/lib.rs` (implementation location)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: "chain-example"
|
||||||
|
description: "Example bundled skill that chains to other skills"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Chain Example Skill
|
||||||
|
|
||||||
|
This skill demonstrates skill chaining capability, where one skill can invoke another skill.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/skill chain-example
|
||||||
|
```
|
||||||
|
|
||||||
|
This skill chains by invoking `/skill example-skill` internally.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This skill serves as a test case for skill chaining feature in the bundled skills registry (#3324).
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: "example-skill"
|
||||||
|
description: "Example bundled skill demonstrating skills functionality"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Example Bundled Skill
|
||||||
|
|
||||||
|
This is a sample bundled skill that demonstrates the skills bundled registry functionality.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/skill example-skill
|
||||||
|
```
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This skill serves as a test case for the bundled skills registry feature (#3324).
|
||||||
@@ -2251,6 +2251,7 @@ pub(crate) struct SkillCollection {
|
|||||||
enum SkillOrigin {
|
enum SkillOrigin {
|
||||||
SkillsDir,
|
SkillsDir,
|
||||||
LegacyCommandsDir,
|
LegacyCommandsDir,
|
||||||
|
Bundled,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SkillOrigin {
|
impl SkillOrigin {
|
||||||
@@ -2258,6 +2259,7 @@ impl SkillOrigin {
|
|||||||
match self {
|
match self {
|
||||||
Self::SkillsDir => None,
|
Self::SkillsDir => None,
|
||||||
Self::LegacyCommandsDir => Some("legacy /commands"),
|
Self::LegacyCommandsDir => Some("legacy /commands"),
|
||||||
|
Self::Bundled => Some("bundled"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3130,7 +3132,7 @@ pub fn resolve_skill_path(cwd: &Path, skill: &str) -> std::io::Result<PathBuf> {
|
|||||||
for entry in fs::read_dir(&root.path)? {
|
for entry in fs::read_dir(&root.path)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
match root.origin {
|
match root.origin {
|
||||||
SkillOrigin::SkillsDir => {
|
SkillOrigin::SkillsDir | SkillOrigin::Bundled => {
|
||||||
if !entry.path().is_dir() {
|
if !entry.path().is_dir() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -3555,6 +3557,52 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
|||||||
roots
|
roots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the default bundled skills root directory.
|
||||||
|
///
|
||||||
|
/// Resolution order (first existing path wins):
|
||||||
|
/// 1. `<exe_dir>/../share/claw/skills/bundled` — standard install layout
|
||||||
|
/// 2. `<exe_dir>/bundled-skills` — simple relocated layout
|
||||||
|
/// 3. `CARGO_MANIFEST_DIR/bundled` — dev/source-tree fallback (only if it exists)
|
||||||
|
/// 4. `<exe_dir>/../share/claw/skills/bundled` — canonical default even if missing
|
||||||
|
fn bundled_skill_root() -> PathBuf {
|
||||||
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
|
if let Some(exe_dir) = exe_path.parent() {
|
||||||
|
let share_path = exe_dir
|
||||||
|
.join("..")
|
||||||
|
.join("share")
|
||||||
|
.join("claw")
|
||||||
|
.join("skills")
|
||||||
|
.join("bundled");
|
||||||
|
if share_path.exists() {
|
||||||
|
return share_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjacent = exe_dir.join("bundled-skills");
|
||||||
|
if adjacent.exists() {
|
||||||
|
return adjacent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dev_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("bundled");
|
||||||
|
if dev_path.exists() {
|
||||||
|
return dev_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
|
if let Some(exe_dir) = exe_path.parent() {
|
||||||
|
return exe_dir
|
||||||
|
.join("..")
|
||||||
|
.join("share")
|
||||||
|
.join("claw")
|
||||||
|
.join("skills")
|
||||||
|
.join("bundled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PathBuf::from("/usr/share/claw/skills/bundled")
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||||
let mut roots = Vec::new();
|
let mut roots = Vec::new();
|
||||||
@@ -3717,6 +3765,14 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bundled_root = bundled_skill_root();
|
||||||
|
push_unique_skill_root(
|
||||||
|
&mut roots,
|
||||||
|
DefinitionSource::UserClaw,
|
||||||
|
bundled_root,
|
||||||
|
SkillOrigin::Bundled,
|
||||||
|
);
|
||||||
|
|
||||||
roots
|
roots
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4149,7 +4205,7 @@ fn load_skills_from_roots_with_drift(roots: &[SkillRoot]) -> std::io::Result<Ski
|
|||||||
for entry in fs::read_dir(&root.path)? {
|
for entry in fs::read_dir(&root.path)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
match root.origin {
|
match root.origin {
|
||||||
SkillOrigin::SkillsDir => {
|
SkillOrigin::SkillsDir | SkillOrigin::Bundled => {
|
||||||
if !entry.path().is_dir() {
|
if !entry.path().is_dir() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -5205,6 +5261,7 @@ fn skill_origin_id(origin: SkillOrigin) -> &'static str {
|
|||||||
match origin {
|
match origin {
|
||||||
SkillOrigin::SkillsDir => "skills_dir",
|
SkillOrigin::SkillsDir => "skills_dir",
|
||||||
SkillOrigin::LegacyCommandsDir => "legacy_commands_dir",
|
SkillOrigin::LegacyCommandsDir => "legacy_commands_dir",
|
||||||
|
SkillOrigin::Bundled => "bundled",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5436,14 +5493,15 @@ pub fn handle_slash_command(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
classify_skills_slash_command, handle_agents_slash_command_json,
|
classify_skills_slash_command, discover_skill_roots, handle_agents_slash_command_json,
|
||||||
handle_plugins_slash_command, handle_skills_slash_command_json, handle_slash_command,
|
handle_plugins_slash_command, handle_skills_slash_command_json, handle_slash_command,
|
||||||
load_agents_from_roots, load_skills_from_roots, render_agents_report,
|
load_agents_from_roots, load_skills_from_roots, render_agents_report,
|
||||||
render_agents_report_json, render_mcp_report_json_for, render_plugins_report,
|
render_agents_report_json, render_mcp_report_json_for, render_plugins_report,
|
||||||
render_plugins_report_with_failures, render_skills_report, render_slash_command_help,
|
render_plugins_report_with_failures, render_skills_report, render_slash_command_help,
|
||||||
render_slash_command_help_detail, resolve_skill_path, resume_supported_slash_commands,
|
render_slash_command_help_detail, resolve_skill_path, resume_supported_slash_commands,
|
||||||
slash_command_specs, suggest_slash_commands, validate_slash_command_input, AgentCollection,
|
slash_command_specs, skill_origin_id, skill_origin_json, suggest_slash_commands,
|
||||||
DefinitionSource, SkillOrigin, SkillRoot, SkillSlashDispatch, SlashCommand,
|
validate_slash_command_input, AgentCollection, DefinitionSource, SkillOrigin, SkillRoot,
|
||||||
|
SkillSlashDispatch, SlashCommand,
|
||||||
};
|
};
|
||||||
use plugins::{
|
use plugins::{
|
||||||
PluginError, PluginKind, PluginLifecycle, PluginLoadFailure, PluginManager,
|
PluginError, PluginKind, PluginLifecycle, PluginLoadFailure, PluginManager,
|
||||||
@@ -7234,4 +7292,50 @@ mod tests {
|
|||||||
let _ = fs::remove_dir_all(config_home);
|
let _ = fs::remove_dir_all(config_home);
|
||||||
let _ = fs::remove_dir_all(bundled_root);
|
let _ = fs::remove_dir_all(bundled_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovers_bundled_skills_from_binary_layout() {
|
||||||
|
let cwd = temp_dir("bundled-skills-discovery");
|
||||||
|
let roots = discover_skill_roots(&cwd);
|
||||||
|
|
||||||
|
let has_bundled = roots.iter().any(|root| root.origin == SkillOrigin::Bundled);
|
||||||
|
assert!(
|
||||||
|
has_bundled,
|
||||||
|
"discover_skill_roots should include a bundled skills root"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundled_skills_have_correct_origin_metadata() {
|
||||||
|
let bundled_origin = SkillOrigin::Bundled;
|
||||||
|
assert_eq!(skill_origin_id(bundled_origin), "bundled");
|
||||||
|
assert_eq!(bundled_origin.detail_label(), Some("bundled"));
|
||||||
|
|
||||||
|
let bundled_json = skill_origin_json(bundled_origin);
|
||||||
|
assert_eq!(bundled_json["id"], "bundled");
|
||||||
|
assert_eq!(bundled_json["detail_label"], "bundled");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundled_skills_are_included_in_json_output() {
|
||||||
|
let cwd = temp_dir("bundled-skills-json-test");
|
||||||
|
let roots = discover_skill_roots(&cwd);
|
||||||
|
|
||||||
|
let bundled_root = roots
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.origin == SkillOrigin::Bundled)
|
||||||
|
.expect("should have bundled skills root");
|
||||||
|
|
||||||
|
let loaded_skills = load_skills_from_roots(&[bundled_root.clone()])
|
||||||
|
.expect("should load bundled skills");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!loaded_skills.is_empty() || bundled_root.path.exists(),
|
||||||
|
"bundled skills should either load successfully or root should exist"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(cwd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user