Skip to main content

vtcode_core/skills/
loader.rs

1use crate::skills::cli_bridge::discover_cli_tools;
2use crate::skills::model::{SkillLoadOutcome, SkillScope};
3use hashbrown::HashSet;
4use std::path::{Path, PathBuf};
5
6// Config for loader
7#[derive(Debug, Clone)]
8pub struct SkillLoaderConfig {
9    pub codex_home: PathBuf,
10    pub cwd: PathBuf,
11    pub project_root: Option<PathBuf>,
12    pub include_bundled_system_skills: bool,
13}
14
15pub struct SkillRoot {
16    pub path: PathBuf,
17    pub scope: SkillScope,
18    pub is_tool_root: bool,
19    pub is_plugin_root: bool,
20}
21
22mod cache;
23mod codex;
24mod discovery;
25mod enhanced;
26mod mentions;
27mod resources;
28
29pub use self::enhanced::{EnhancedSkill, EnhancedSkillLoader};
30
31#[cfg(test)]
32pub(crate) use self::discovery::find_git_root;
33use self::discovery::tool_config_to_metadata;
34use self::discovery::{discover_metadata_under_root, discover_skills_under_root, skill_roots_with_home_dir};
35#[cfg(test)]
36pub(crate) use self::enhanced::{discovery_config_for_codex_home, select_traditional_skill_ctx};
37
38pub use self::resources::load_skill_resources;
39
40use self::codex::filter_disabled_skills;
41
42pub use self::mentions::{SkillMentionDetectionOptions, detect_skill_mentions, detect_skill_mentions_with_options};
43
44pub(crate) use self::cache::clear_lightweight_skill_metadata_cache;
45use self::cache::{LightweightSkillCacheKey, cache_lightweight_skill_outcome, get_cached_lightweight_skill_outcome};
46
47pub fn load_skills(config: &SkillLoaderConfig) -> SkillLoadOutcome {
48    let home_dir = dirs::home_dir();
49    load_skills_with_home_dir(config, home_dir.as_deref())
50}
51
52/// Lightweight metadata discovery that avoids parsing SKILL.md files.
53/// Returns skill stubs with only name, description, and path (no manifest parsing).
54/// This is much faster for listing available skills.
55pub fn discover_skill_metadata_lightweight(config: &SkillLoaderConfig) -> SkillLoadOutcome {
56    let home_dir = dirs::home_dir();
57    discover_skill_metadata_lightweight_with_home_dir(config, home_dir.as_deref())
58}
59
60/// Internal helper that allows specifying an explicit home directory.
61/// This is useful for testing to avoid picking up real user skills from ~/.agents/skills.
62pub(crate) fn load_skills_with_home_dir(config: &SkillLoaderConfig, home_dir: Option<&Path>) -> SkillLoadOutcome {
63    let mut outcome = SkillLoadOutcome::default();
64    let roots = skill_roots_with_home_dir(config, home_dir);
65
66    for root in roots {
67        discover_skills_under_root(&root, &mut outcome);
68    }
69
70    add_system_cli_tools(&mut outcome);
71    dedup_and_sort(&mut outcome);
72    filter_disabled_skills(&mut outcome, home_dir);
73    outcome
74}
75
76/// Internal helper for lightweight discovery with explicit home directory.
77/// Useful for hermetic tests.
78fn discover_skill_metadata_lightweight_with_home_dir(
79    config: &SkillLoaderConfig,
80    home_dir: Option<&Path>,
81) -> SkillLoadOutcome {
82    let cache_key = LightweightSkillCacheKey::new(config, home_dir);
83    if let Some(cached) = get_cached_lightweight_skill_outcome(&cache_key) {
84        return cached;
85    }
86
87    let outcome = discover_skill_metadata_lightweight_uncached(config, home_dir);
88    cache_lightweight_skill_outcome(cache_key, &outcome);
89    outcome
90}
91
92fn discover_skill_metadata_lightweight_uncached(
93    config: &SkillLoaderConfig,
94    home_dir: Option<&Path>,
95) -> SkillLoadOutcome {
96    let mut outcome = SkillLoadOutcome::default();
97    let roots = skill_roots_with_home_dir(config, home_dir);
98
99    for root in roots {
100        discover_metadata_under_root(&root, &mut outcome);
101    }
102
103    add_system_cli_tools(&mut outcome);
104    dedup_and_sort(&mut outcome);
105    filter_disabled_skills(&mut outcome, home_dir);
106    outcome
107}
108
109fn add_system_cli_tools(outcome: &mut SkillLoadOutcome) {
110    if let Ok(system_tools) = discover_cli_tools() {
111        for tool in system_tools {
112            if let Ok(skill) = tool_config_to_metadata(&tool, SkillScope::System) {
113                outcome.skills.push(skill);
114            }
115        }
116    }
117}
118
119fn dedup_and_sort(outcome: &mut SkillLoadOutcome) {
120    let mut seen: HashSet<String> = HashSet::new();
121    let mut shadowed: Vec<String> = Vec::new();
122    outcome.skills.retain(|skill| {
123        if seen.insert(skill.name.clone()) {
124            return true;
125        }
126        // Agent Skills interop: same-name skills must not silently replace one
127        // another. First discovery wins (project roots precede user roots);
128        // warn so the shadowing is visible instead of silent.
129        shadowed.push(format!("{} ({})", skill.name, skill.path.display()));
130        false
131    });
132    if !shadowed.is_empty() {
133        tracing::warn!(
134            shadowed = ?shadowed,
135            "duplicate skill names shadowed by earlier discovery; only the first occurrence is loaded"
136        );
137    }
138    outcome.skills.sort_by(|a, b| a.name.cmp(&b.name));
139}
140
141/// Test helper for hermetic skill loading that does not pick up user skills.
142/// Use this in tests to avoid failures when ~/.agents/skills contains skills.
143#[cfg(test)]
144pub fn load_skills_hermetic(config: &SkillLoaderConfig) -> SkillLoadOutcome {
145    load_skills_with_home_dir(config, None)
146}
147
148/// Test helper for hermetic lightweight skill discovery.
149#[cfg(test)]
150pub fn discover_skill_metadata_lightweight_hermetic(config: &SkillLoaderConfig) -> SkillLoadOutcome {
151    discover_skill_metadata_lightweight_with_home_dir(config, None)
152}
153
154#[cfg(test)]
155mod tests;