Compare commits

...

1 Commits

Author SHA1 Message Date
Gav Verma
6c052ed809 Exclude plan skill from prompt context 2025-12-20 16:33:40 -08:00
2 changed files with 88 additions and 2 deletions

View File

@@ -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();
}
}

View File

@@ -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<String> {
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<String> {
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();