From 897d4d5f17845962382c87c66cf609e50d3bed8d Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 15 Oct 2025 17:46:01 +0100 Subject: [PATCH] feat: agent override file (#5215) Add a file that overrides `AGENTS.md` but is not versioned (for local devs) --- .gitignore | 1 + codex-rs/core/src/config.rs | 26 +++++++++++++------------- codex-rs/core/src/project_doc.rs | 28 +++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index a264d918..178239c0 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ result # cli tools CLAUDE.md .claude/ +AGENTS.override.md # caches .cache/ diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 47db1a88..f0652c0c 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -28,6 +28,8 @@ use crate::model_family::find_family_for_model; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::built_in_model_providers; use crate::openai_model_info::get_model_info; +use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME; +use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; use anyhow::Context; @@ -1217,20 +1219,18 @@ impl Config { } fn load_instructions(codex_dir: Option<&Path>) -> Option { - let mut p = match codex_dir { - Some(p) => p.to_path_buf(), - None => return None, - }; - - p.push("AGENTS.md"); - std::fs::read_to_string(&p).ok().and_then(|s| { - let s = s.trim(); - if s.is_empty() { - None - } else { - Some(s.to_string()) + let base = codex_dir?; + for candidate in [LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME] { + let mut path = base.to_path_buf(); + path.push(candidate); + if let Ok(contents) = std::fs::read_to_string(&path) { + let trimmed = contents.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } } - }) + } + None } fn get_base_instructions( diff --git a/codex-rs/core/src/project_doc.rs b/codex-rs/core/src/project_doc.rs index c33b1a5d..c8076aa8 100644 --- a/codex-rs/core/src/project_doc.rs +++ b/codex-rs/core/src/project_doc.rs @@ -21,6 +21,8 @@ use tracing::error; /// Default filename scanned for project-level docs. pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md"; +/// Preferred local override for project-level docs. +pub const LOCAL_PROJECT_DOC_FILENAME: &str = "AGENTS.override.md"; /// When both `Config::instructions` and the project doc are present, they will /// be concatenated with the following separator. @@ -178,7 +180,8 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result(config: &'a Config) -> Vec<&'a str> { let mut names: Vec<&'a str> = - Vec::with_capacity(1 + config.project_doc_fallback_filenames.len()); + Vec::with_capacity(2 + config.project_doc_fallback_filenames.len()); + names.push(LOCAL_PROJECT_DOC_FILENAME); names.push(DEFAULT_PROJECT_DOC_FILENAME); for candidate in &config.project_doc_fallback_filenames { let candidate = candidate.as_str(); @@ -381,6 +384,29 @@ mod tests { assert_eq!(res, "root doc\n\ncrate doc"); } + /// AGENTS.override.md is preferred over AGENTS.md when both are present. + #[tokio::test] + async fn agents_local_md_preferred() { + let tmp = tempfile::tempdir().expect("tempdir"); + fs::write(tmp.path().join(DEFAULT_PROJECT_DOC_FILENAME), "versioned").unwrap(); + fs::write(tmp.path().join(LOCAL_PROJECT_DOC_FILENAME), "local").unwrap(); + + let cfg = make_config(&tmp, 4096, None); + + let res = get_user_instructions(&cfg) + .await + .expect("local doc expected"); + + assert_eq!(res, "local"); + + let discovery = discover_project_doc_paths(&cfg).expect("discover paths"); + assert_eq!(discovery.len(), 1); + assert_eq!( + discovery[0].file_name().unwrap().to_string_lossy(), + LOCAL_PROJECT_DOC_FILENAME + ); + } + /// When AGENTS.md is absent but a configured fallback exists, the fallback is used. #[tokio::test] async fn uses_configured_fallback_when_agents_missing() {