diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs
index d48bda18f6..d8813e23d7 100644
--- a/codex-rs/core/src/config_loader/mod.rs
+++ b/codex-rs/core/src/config_loader/mod.rs
@@ -425,7 +425,9 @@ async fn load_requirements_from_legacy_scheme(
/// empty array, which indicates that root detection should be disabled).
/// - Returns an error if `project_root_markers` is specified but is not an
/// array of strings.
-fn project_root_markers_from_config(config: &TomlValue) -> io::Result>> {
+pub(crate) fn project_root_markers_from_config(
+ config: &TomlValue,
+) -> io::Result >> {
let Some(table) = config.as_table() else {
return Ok(None);
};
@@ -454,7 +456,7 @@ fn project_root_markers_from_config(config: &TomlValue) -> io::Result Vec {
+pub(crate) fn default_project_root_markers() -> Vec {
DEFAULT_PROJECT_ROOT_MARKERS
.iter()
.map(ToString::to_string)
diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs
index 32f1b8706e..a3c7731a6e 100644
--- a/codex-rs/core/src/skills/loader.rs
+++ b/codex-rs/core/src/skills/loader.rs
@@ -1,6 +1,9 @@
use crate::config::Config;
use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigLayerStackOrdering;
+use crate::config_loader::default_project_root_markers;
+use crate::config_loader::merge_toml_values;
+use crate::config_loader::project_root_markers_from_config;
use crate::skills::model::SkillDependencies;
use crate::skills::model::SkillError;
use crate::skills::model::SkillInterface;
@@ -20,6 +23,7 @@ use std::fs;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
+use toml::Value as TomlValue;
use tracing::error;
#[derive(Debug, Deserialize)]
@@ -72,6 +76,7 @@ struct DependencyTool {
}
const SKILLS_FILENAME: &str = "SKILL.md";
+const AGENTS_DIR_NAME: &str = ".agents";
const SKILLS_METADATA_DIR: &str = "agents";
const SKILLS_METADATA_FILENAME: &str = "openai.yaml";
const SKILLS_DIR_NAME: &str = "skills";
@@ -209,15 +214,104 @@ fn skill_roots_from_layer_stack_inner(config_layer_stack: &ConfigLayerStack) ->
}
fn skill_roots(config: &Config) -> Vec {
- skill_roots_from_layer_stack_inner(&config.config_layer_stack)
+ skill_roots_from_layer_stack_with_agents(&config.config_layer_stack, &config.cwd)
}
+#[cfg(test)]
pub(crate) fn skill_roots_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
) -> Vec {
skill_roots_from_layer_stack_inner(config_layer_stack)
}
+pub(crate) fn skill_roots_from_layer_stack_with_agents(
+ config_layer_stack: &ConfigLayerStack,
+ cwd: &Path,
+) -> Vec {
+ let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack);
+ roots.extend(repo_agents_skill_roots(config_layer_stack, cwd));
+ dedupe_skill_roots_by_path(&mut roots);
+ roots
+}
+
+fn dedupe_skill_roots_by_path(roots: &mut Vec) {
+ let mut seen: HashSet = HashSet::new();
+ roots.retain(|root| seen.insert(root.path.clone()));
+}
+
+fn repo_agents_skill_roots(config_layer_stack: &ConfigLayerStack, cwd: &Path) -> Vec {
+ let project_root_markers = project_root_markers_from_stack(config_layer_stack);
+ let project_root = find_project_root(cwd, &project_root_markers);
+ let dirs = dirs_between_project_root_and_cwd(cwd, &project_root);
+ let mut roots = Vec::new();
+ for dir in dirs {
+ let agents_skills = dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME);
+ if agents_skills.is_dir() {
+ roots.push(SkillRoot {
+ path: agents_skills,
+ scope: SkillScope::Repo,
+ });
+ }
+ }
+ roots
+}
+
+fn project_root_markers_from_stack(config_layer_stack: &ConfigLayerStack) -> Vec {
+ let mut merged = TomlValue::Table(toml::map::Map::new());
+ for layer in
+ config_layer_stack.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false)
+ {
+ if matches!(layer.name, ConfigLayerSource::Project { .. }) {
+ continue;
+ }
+ merge_toml_values(&mut merged, &layer.config);
+ }
+
+ match project_root_markers_from_config(&merged) {
+ Ok(Some(markers)) => markers,
+ Ok(None) => default_project_root_markers(),
+ Err(err) => {
+ tracing::warn!("invalid project_root_markers: {err}");
+ default_project_root_markers()
+ }
+ }
+}
+
+fn find_project_root(cwd: &Path, project_root_markers: &[String]) -> PathBuf {
+ if project_root_markers.is_empty() {
+ return cwd.to_path_buf();
+ }
+
+ for ancestor in cwd.ancestors() {
+ for marker in project_root_markers {
+ let marker_path = ancestor.join(marker);
+ if marker_path.exists() {
+ return ancestor.to_path_buf();
+ }
+ }
+ }
+
+ cwd.to_path_buf()
+}
+
+fn dirs_between_project_root_and_cwd(cwd: &Path, project_root: &Path) -> Vec {
+ let mut dirs = cwd
+ .ancestors()
+ .scan(false, |done, a| {
+ if *done {
+ None
+ } else {
+ if a == project_root {
+ *done = true;
+ }
+ Some(a.to_path_buf())
+ }
+ })
+ .collect::>();
+ dirs.reverse();
+ dirs
+}
+
fn discover_skills_under_root(root: &Path, scope: SkillScope, outcome: &mut SkillLoadOutcome) {
let Ok(root) = canonicalize_path(root) else {
return;
@@ -1615,6 +1709,40 @@ interface:
);
}
+ #[tokio::test]
+ async fn loads_skills_from_agents_dir_without_codex_dir() {
+ let codex_home = tempfile::tempdir().expect("tempdir");
+ let repo_dir = tempfile::tempdir().expect("tempdir");
+ mark_as_git_repo(repo_dir.path());
+
+ let skill_path = write_skill_at(
+ &repo_dir.path().join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME),
+ "agents",
+ "agents-skill",
+ "from agents",
+ );
+ let cfg = make_config_for_cwd(&codex_home, repo_dir.path().to_path_buf()).await;
+
+ let outcome = load_skills(&cfg);
+ assert!(
+ outcome.errors.is_empty(),
+ "unexpected errors: {:?}",
+ outcome.errors
+ );
+ assert_eq!(
+ outcome.skills,
+ vec![SkillMetadata {
+ name: "agents-skill".to_string(),
+ description: "from agents".to_string(),
+ short_description: None,
+ interface: None,
+ dependencies: None,
+ path: normalized(&skill_path),
+ scope: SkillScope::Repo,
+ }]
+ );
+ }
+
#[tokio::test]
async fn loads_skills_from_all_codex_dirs_under_project_root() {
let codex_home = tempfile::tempdir().expect("tempdir");
diff --git a/codex-rs/core/src/skills/manager.rs b/codex-rs/core/src/skills/manager.rs
index 3b100991f7..85e0bf20eb 100644
--- a/codex-rs/core/src/skills/manager.rs
+++ b/codex-rs/core/src/skills/manager.rs
@@ -15,7 +15,7 @@ use crate::config_loader::LoaderOverrides;
use crate::config_loader::load_config_layers_state;
use crate::skills::SkillLoadOutcome;
use crate::skills::loader::load_skills_from_roots;
-use crate::skills::loader::skill_roots_from_layer_stack;
+use crate::skills::loader::skill_roots_from_layer_stack_with_agents;
use crate::skills::system::install_system_skills;
pub struct SkillsManager {
@@ -47,7 +47,8 @@ impl SkillsManager {
return outcome;
}
- let roots = skill_roots_from_layer_stack(&config.config_layer_stack);
+ let roots =
+ skill_roots_from_layer_stack_with_agents(&config.config_layer_stack, &config.cwd);
let mut outcome = load_skills_from_roots(roots);
outcome.disabled_paths = disabled_paths_from_stack(&config.config_layer_stack);
match self.cache_by_cwd.write() {
@@ -105,7 +106,7 @@ impl SkillsManager {
}
};
- let roots = skill_roots_from_layer_stack(&config_layer_stack);
+ let roots = skill_roots_from_layer_stack_with_agents(&config_layer_stack, cwd);
let mut outcome = load_skills_from_roots(roots);
outcome.disabled_paths = disabled_paths_from_stack(&config_layer_stack);
match self.cache_by_cwd.write() {