diff --git a/codex-rs/core/src/project_doc.rs b/codex-rs/core/src/project_doc.rs index cb2499cbbb..13db01910c 100644 --- a/codex-rs/core/src/project_doc.rs +++ b/codex-rs/core/src/project_doc.rs @@ -234,6 +234,7 @@ mod tests { use super::*; use crate::config::ConfigBuilder; use crate::skills::load_skills; + use pretty_assertions::assert_eq; use std::fs; use std::path::PathBuf; use tempfile::TempDir; @@ -544,10 +545,82 @@ mod tests { assert_eq!(res, expected); } + #[tokio::test] + async fn system_plan_skill_is_excluded_from_project_doc() { + let tmp = tempfile::tempdir().expect("tempdir"); + fs::write(tmp.path().join("AGENTS.md"), "base doc").unwrap(); + + let cfg = make_config(&tmp, 4096, None).await; + create_system_skill(cfg.codex_home.clone(), "plan", "Generate a plan"); + create_skill(cfg.codex_home.clone(), "linting", "run clippy"); + + let skills = load_skills(&cfg); + assert!( + skills.skills.iter().any(|skill| { + skill.scope == codex_protocol::protocol::SkillScope::System && skill.name == "plan" + }), + "expected system plan skill to load, loaded: {:?}", + skills.skills + ); + let res = get_user_instructions( + &cfg, + skills.errors.is_empty().then_some(skills.skills.as_slice()), + ) + .await + .expect("instructions expected"); + + assert!( + res.contains("- linting: run clippy"), + "expected linting skill to render, got: {res}" + ); + assert!( + !res.contains("- plan: Generate a plan"), + "expected system plan skill to be excluded, got: {res}" + ); + } + + #[tokio::test] + async fn user_plan_skill_is_included_in_project_doc() { + let tmp = tempfile::tempdir().expect("tempdir"); + fs::write(tmp.path().join("AGENTS.md"), "base doc").unwrap(); + + let cfg = make_config(&tmp, 4096, None).await; + create_skill(cfg.codex_home.clone(), "plan", "Generate a plan"); + + let skills = load_skills(&cfg); + assert!( + skills.skills.iter().any(|skill| { + skill.scope == codex_protocol::protocol::SkillScope::User && skill.name == "plan" + }), + "expected user plan skill to load, loaded: {:?}", + skills.skills + ); + + let res = get_user_instructions( + &cfg, + skills.errors.is_empty().then_some(skills.skills.as_slice()), + ) + .await + .expect("instructions expected"); + + assert!( + res.contains("- plan: Generate a plan"), + "expected user plan skill to render, got: {res}" + ); + } + fn create_skill(codex_home: PathBuf, name: &str, description: &str) { let skill_dir = codex_home.join(format!("skills/{name}")); fs::create_dir_all(&skill_dir).unwrap(); let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n"); fs::write(skill_dir.join("SKILL.md"), content).unwrap(); } + + fn create_system_skill(codex_home: PathBuf, name: &str, description: &str) { + let system_root = crate::skills::system::system_cache_root_dir(&codex_home); + let skill_dir = system_root.join(name); + fs::create_dir_all(&skill_dir).unwrap(); + let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n"); + fs::write(skill_dir.join("SKILL.md"), content).unwrap(); + } } diff --git a/codex-rs/core/src/skills/render.rs b/codex-rs/core/src/skills/render.rs index f767849b24..49769ee6b4 100644 --- a/codex-rs/core/src/skills/render.rs +++ b/codex-rs/core/src/skills/render.rs @@ -1,7 +1,20 @@ use crate::skills::model::SkillMetadata; +use codex_protocol::protocol::SkillScope; + +fn excluded_from_prompt_context(skill: &SkillMetadata) -> bool { + matches!( + (skill.name.as_str(), skill.scope), + ("plan", SkillScope::System) + ) +} pub fn render_skills_section(skills: &[SkillMetadata]) -> Option { - if skills.is_empty() { + let visible_skills: Vec<&SkillMetadata> = skills + .iter() + .filter(|skill| !excluded_from_prompt_context(skill)) + .collect(); + + if visible_skills.is_empty() { return None; } @@ -9,7 +22,7 @@ pub fn render_skills_section(skills: &[SkillMetadata]) -> Option { lines.push("## Skills".to_string()); lines.push("These skills are discovered at startup from multiple local sources. Each entry includes a name, description, and file path so you can open the source for full instructions.".to_string()); - for skill in skills { + for skill in visible_skills { let path_str = skill.path.to_string_lossy().replace('\\', "/"); let name = skill.name.as_str(); let description = skill.description.as_str();