fix(skills): align skills API with TUI command multi-directory discovery#2285
fix(skills): align skills API with TUI command multi-directory discovery#2285gaord wants to merge 1 commit into
Conversation
- Use discover_for_workspace_and_dir() instead of SkillRegistry::discover() to search all skill directories (workspace + global), matching TUI /skills - Add is_bundled field to SkillEntry for built-in skill identification - Add directories field to SkillsResponse showing all search paths - Use skill.path instead of constructing path from skills_dir + name - Update set_skill_enabled to use the same discovery logic
There was a problem hiding this comment.
Code Review
This pull request updates the runtime API to support multiple skill directories and bundled skills. It adds a directories field and an is_bundled flag to the skills response, and updates skill discovery to search across the workspace. A review comment points out that the directories list returned in the API response might omit a custom skills_dir if it is not already part of the default workspace directories, and suggests appending it to ensure consistency with the actual search paths.
| let registry = | ||
| crate::skills::discover_for_workspace_and_dir(&state.workspace, &skills_dir); | ||
| let skill_state = state.skill_state.lock().await; | ||
| let directories = crate::skills::skills_directories(&state.workspace); |
There was a problem hiding this comment.
The directories list returned in SkillsResponse is populated using crate::skills::skills_directories(&state.workspace). However, discover_for_workspace_and_dir also searches skills_dir (the primary configured/resolved skills directory) and appends it to the search paths if it is not already present.
If a user has configured a custom skills_dir that is not in the default workspace/global candidate list, it will be searched for skills, but it will be missing from the directories field in the API response.
To ensure the returned directories list accurately reflects all directories that were actually searched, we should append skills_dir to directories if it is a valid directory and not already present, matching the behavior of discover_for_workspace_and_dir.
let mut directories = crate::skills::skills_directories(&state.workspace);
if skills_dir.is_dir() && !directories.contains(&skills_dir) {
directories.push(skills_dir.clone());
}| let registry = | ||
| crate::skills::discover_for_workspace_and_dir(&state.workspace, &skills_dir); | ||
| let skill_state = state.skill_state.lock().await; | ||
| let directories = crate::skills::skills_directories(&state.workspace); |
There was a problem hiding this comment.
directories omits the custom skills_dir
skills_directories(&state.workspace) returns only the standard workspace/global candidate directories, but discover_for_workspace_and_dir internally appends skills_dir when it is not already in that set (see discover_for_workspace_dirs_and_dir). When a user has configured a non-standard config.skills_dir() (e.g. a path outside the well-known ~/.codewhale/skills, ~/.agents/skills, etc. hierarchy), the registry will contain skills from that directory but directories will not list it. Clients relying on the directories field to know where a skill came from will have an incomplete picture, and the advertised contract ("returns all searched directories") will silently be broken.
A straightforward fix is to mirror the same append logic used in discover_for_workspace_dirs_and_dir when building the directories list:
let mut directories = crate::skills::skills_directories(&state.workspace);
if skills_dir.is_dir() && !directories.iter().any(|p| p == &skills_dir) {
directories.push(skills_dir.clone());
}| return Err(ApiError::not_found(format!( | ||
| "skill '{name}' not found under {}", | ||
| skills_dir.display() | ||
| "skill '{name}' not found" | ||
| ))); |
There was a problem hiding this comment.
404 message loses search-path context
The previous error text included the directory being searched ("skill '{name}' not found under {skills_dir}"), which was directly useful for debugging misconfigurations. Now that skills come from multiple directories, the directory context is even more valuable (the user can't know which of the several searched paths was checked). The simplified "skill '{name}' not found" message gives clients no actionable context.
Summary
The
/v1/skillsAPI endpoint only searched a single directory (resolve_skills_dir), while the TUI's/skillscommand usesdiscover_for_workspace_and_dir()to search multiple directories (workspace-local + global). This means the API would miss skills installed in~/.claude/skills,~/.agents/skills, workspace.agents/skills, etc.Changes
discover_for_workspace_and_dir()in bothlist_skillsandset_skill_enabledhandlers, matching the TUI/skillscommand behavioris_bundledfield toSkillEntry— identifies built-in skills viais_bundled_skill_name(), allowing clients to distinguish user skills from bundled skillsdirectoriesfield toSkillsResponse— returns all searched directories so clients know where skills come fromskill.pathinstead of constructing path fromskills_dir + name, since skills may come from different directoriesSkillRegistryimportBefore
{ "directory": "/Users/user/.codewhale/skills", "warnings": [], "skills": [ { "name": "delegate", "description": "...", "path": "/Users/user/.codewhale/skills/delegate/SKILL.md", "enabled": true } ] }Only skills under
~/.codewhale/skillswere discoverable.After
{ "directory": "/Users/user/.codewhale/skills", "directories": [ "/Users/user/project/.agents/skills", "/Users/user/.agents/skills", "/Users/user/.claude/skills", "/Users/user/.codewhale/skills", "/Users/user/.deepseek/skills" ], "warnings": [], "skills": [ { "name": "delegate", "description": "...", "path": "/Users/user/.codewhale/skills/delegate/SKILL.md", "enabled": true, "is_bundled": true }, { "name": "my-custom-skill", "description": "...", "path": "/Users/user/.claude/skills/my-custom-skill/SKILL.md", "enabled": true, "is_bundled": false } ] }Skills from all configured directories are now discoverable, matching TUI behavior.
Test plan
/v1/skillsreturns skills from~/.claude/skills,~/.agents/skills, etc.is_bundledistruefor built-in skills (delegate, documents, pdf, etc.)directorieslists all existing skill directoriesset_skill_enabledworks for skills from any directoryset_skill_enabledreturns 404 for non-existent skillsGreptile Summary
This PR aligns the
/v1/skillsAPI with the TUI/skillscommand by switching from single-directory discovery (SkillRegistry::discover) to multi-directory discovery (discover_for_workspace_and_dir), and enriches the response with anis_bundledfield per skill entry and adirectoriesfield listing all searched paths.list_skillsandset_skill_enabledboth adoptdiscover_for_workspace_and_dir, so the API and TUI now share the same multi-directory search logic.SkillEntrygainsis_bundled(viais_bundled_skill_name), andSkillsResponsegainsdirectories(viaskills_directories), butdirectoriesis computed independently and does not include the custom configuredskills_dirwhen it falls outside the standard hierarchy — meaning skills from a non-standard configured path can appear in the response while that path is missing fromdirectories.Confidence Score: 3/5
The change is largely correct and improves parity with the TUI, but the
directoriesresponse field can silently omit the custom configured skills directory while still surfacing skills from it, leaving clients with an inconsistent picture of where skills come from.The core discovery logic (
discover_for_workspace_and_dir,is_bundled_skill_name) is sound and well-tested elsewhere. The mismatch betweendiscover_for_workspace_and_dir's actual search set and thedirectoriesfield produced by the independentskills_directories()call is a present defect: in any deployment whereconfig.skills_dir()resolves to a path outside the standard hierarchy, the API response will contain skills whose origin directory is absent fromdirectories, which is the new field's sole purpose.crates/tui/src/runtime_api.rs — specifically the
list_skillshandler wheredirectoriesis built independently of the actual search path set used bydiscover_for_workspace_and_dir.Important Files Changed
list_skillsandset_skill_enabledtodiscover_for_workspace_and_dir, addsis_bundledper entry anddirectoriesto the response, butdirectoriesis computed fromskills_directoriesalone and will omit any customskills_dirthat is outside the standard hierarchy — creating a mismatch between the advertised directory list and the actual search paths used.Sequence Diagram
sequenceDiagram participant Client participant API as /v1/skills (list_skills) participant RSD as resolve_skills_dir() participant DWD as discover_for_workspace_and_dir() participant SD as skills_directories() participant SS as skill_state (lock) Client->>API: GET /v1/skills API->>RSD: config + workspace RSD-->>API: skills_dir (workspace-local or config default) API->>DWD: workspace + skills_dir DWD->>SD: workspace → standard dirs SD-->>DWD: [.agents/skills, .claude/skills, ~/.codewhale/skills, ...] DWD->>DWD: append skills_dir if not in standard dirs DWD-->>API: merged SkillRegistry API->>SS: lock() API->>SD: workspace → standard dirs only (⚠ missing custom skills_dir) SD-->>API: directories list (may be incomplete) API-->>Client: "SkillsResponse { directory, directories, skills[{is_bundled,...}], warnings }"Reviews (1): Last reviewed commit: "fix(skills): align skills API with TUI c..." | Re-trigger Greptile