diff --git a/crates/forge_domain/src/env.rs b/crates/forge_domain/src/env.rs index 324e3f70f5..7e6ee30601 100644 --- a/crates/forge_domain/src/env.rs +++ b/crates/forge_domain/src/env.rs @@ -119,6 +119,13 @@ impl Environment { self.base_path.join("skills") } + /// Returns the agents skills directory path (~/.agents/skills) + /// + /// Returns `None` when the home directory cannot be determined. + pub fn agents_skills_path(&self) -> Option { + self.home.as_ref().map(|home| home.join(".agents/skills")) + } + /// Returns the project-local skills directory path (.forge/skills) pub fn local_skills_path(&self) -> PathBuf { self.cwd.join(".forge/skills") @@ -233,6 +240,29 @@ mod tests { assert_eq!(actual, expected); } + #[test] + fn test_agents_skills_path_with_home() { + let fixture: Environment = Faker.fake(); + let fixture = fixture.home(PathBuf::from("/home/user")); + + let actual = fixture.agents_skills_path(); + let expected = Some(PathBuf::from("/home/user/.agents/skills")); + + assert_eq!(actual, expected); + } + + #[test] + fn test_agents_skills_path_without_home() { + let fixture: Environment = Faker.fake(); + // Explicitly clear the home field + let mut fixture = fixture; + fixture.home = None; + + let actual = fixture.agents_skills_path(); + + assert_eq!(actual, None); + } + #[test] fn test_local_skills_path() { let fixture: Environment = Faker.fake(); diff --git a/crates/forge_repo/src/skill.rs b/crates/forge_repo/src/skill.rs index efe80ab25f..c1dbd77a29 100644 --- a/crates/forge_repo/src/skill.rs +++ b/crates/forge_repo/src/skill.rs @@ -12,19 +12,22 @@ use serde::Deserialize; /// Repository implementation for loading skills from multiple sources: /// 1. Built-in skills (embedded in the application) /// 2. Global custom skills (from ~/forge/skills/ directory) -/// 3. Project-local skills (from .forge/skills/ directory in current working +/// 3. Agents skills (from ~/.agents/skills/ directory) +/// 4. Project-local skills (from .forge/skills/ directory in current working /// directory) /// /// ## Skill Precedence /// When skills have duplicate names across different sources, the precedence -/// order is: **CWD (project-local) > Global custom > Built-in** +/// order is: **CWD (project-local) > Agents (~/.agents/skills) > Global +/// custom > Built-in** /// -/// This means project-local skills can override global skills, and both can -/// override built-in skills. +/// This means project-local skills can override agents skills, which can +/// override global skills, which can override built-in skills. /// /// ## Directory Resolution /// - **Built-in skills**: Embedded in application binary /// - **Global skills**: `~/forge/skills//SKILL.md` +/// - **Agents skills**: `~/.agents/skills//SKILL.md` /// - **CWD skills**: `./.forge/skills//SKILL.md` (relative to /// current working directory) /// @@ -84,12 +87,19 @@ impl SkillR let global_skills = self.load_skills_from_dir(&global_dir).await?; skills.extend(global_skills); + // Load agents skills (~/.agents/skills) + if let Some(agents_dir) = env.agents_skills_path() { + let agents_skills = self.load_skills_from_dir(&agents_dir).await?; + skills.extend(agents_skills); + } + // Load project-local skills let cwd_dir = env.local_skills_path(); let cwd_skills = self.load_skills_from_dir(&cwd_dir).await?; skills.extend(cwd_skills); - // Resolve conflicts by keeping the last occurrence (CWD > Global > Built-in) + // Resolve conflicts by keeping the last occurrence (CWD > Agents > Global > + // Built-in) let skills = resolve_skill_conflicts(skills); // Render all skills with environment context @@ -220,11 +230,16 @@ impl ForgeS /// * `env` - The environment containing path informations fn render_skill(&self, skill: Skill, env: &forge_domain::Environment) -> Skill { let global = env.global_skills_path().display().to_string(); + let agents = env + .agents_skills_path() + .map(|p| p.display().to_string()) + .unwrap_or_default(); let local = env.local_skills_path().display().to_string(); let rendered = skill .command .replace("{{global_skills_path}}", &global) + .replace("{{agents_skills_path}}", &agents) .replace("{{local_skills_path}}", &local); skill.command(rendered)