Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions crates/forge_domain/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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")
Expand Down Expand Up @@ -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();
Expand Down
25 changes: 20 additions & 5 deletions crates/forge_repo/src/skill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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-name>/SKILL.md`
/// - **Agents skills**: `~/.agents/skills/<skill-name>/SKILL.md`
/// - **CWD skills**: `./.forge/skills/<skill-name>/SKILL.md` (relative to
/// current working directory)
///
Expand Down Expand Up @@ -84,12 +87,19 @@ impl<I: FileInfoInfra + EnvironmentInfra + FileReaderInfra + WalkerInfra> 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
Expand Down Expand Up @@ -220,11 +230,16 @@ impl<I: FileInfoInfra + EnvironmentInfra + FileReaderInfra + WalkerInfra> 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)
Expand Down
Loading