diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index af595b49..afc9ff0f 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -141,6 +141,9 @@ pub struct Config { /// Maximum number of bytes to include from an AGENTS.md project doc file. pub project_doc_max_bytes: usize, + /// Additional filenames to try when looking for project-level docs. + pub project_doc_fallback_filenames: Vec, + /// Directory containing all Codex state (defaults to `~/.codex` but can be /// overridden by the `CODEX_HOME` environment variable). pub codex_home: PathBuf, @@ -670,6 +673,9 @@ pub struct ConfigToml { /// Maximum number of bytes to include from an AGENTS.md project doc file. pub project_doc_max_bytes: Option, + /// Ordered list of fallback filenames to look for when AGENTS.md is missing. + pub project_doc_fallback_filenames: Option>, + /// Profile to use from the `profiles` map. pub profile: Option, @@ -1038,6 +1044,19 @@ impl Config { mcp_servers: cfg.mcp_servers, model_providers, project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES), + project_doc_fallback_filenames: cfg + .project_doc_fallback_filenames + .unwrap_or_default() + .into_iter() + .filter_map(|name| { + let trimmed = name.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + }) + .collect(), codex_home, history, file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode), @@ -1811,6 +1830,7 @@ model_verbosity = "high" mcp_servers: HashMap::new(), model_providers: fixture.model_provider_map.clone(), project_doc_max_bytes: PROJECT_DOC_MAX_BYTES, + project_doc_fallback_filenames: Vec::new(), codex_home: fixture.codex_home(), history: History::default(), file_opener: UriBasedFileOpener::VsCode, @@ -1871,6 +1891,7 @@ model_verbosity = "high" mcp_servers: HashMap::new(), model_providers: fixture.model_provider_map.clone(), project_doc_max_bytes: PROJECT_DOC_MAX_BYTES, + project_doc_fallback_filenames: Vec::new(), codex_home: fixture.codex_home(), history: History::default(), file_opener: UriBasedFileOpener::VsCode, @@ -1946,6 +1967,7 @@ model_verbosity = "high" mcp_servers: HashMap::new(), model_providers: fixture.model_provider_map.clone(), project_doc_max_bytes: PROJECT_DOC_MAX_BYTES, + project_doc_fallback_filenames: Vec::new(), codex_home: fixture.codex_home(), history: History::default(), file_opener: UriBasedFileOpener::VsCode, @@ -2007,6 +2029,7 @@ model_verbosity = "high" mcp_servers: HashMap::new(), model_providers: fixture.model_provider_map.clone(), project_doc_max_bytes: PROJECT_DOC_MAX_BYTES, + project_doc_fallback_filenames: Vec::new(), codex_home: fixture.codex_home(), history: History::default(), file_opener: UriBasedFileOpener::VsCode, diff --git a/codex-rs/core/src/project_doc.rs b/codex-rs/core/src/project_doc.rs index 549248fc..476f0e1f 100644 --- a/codex-rs/core/src/project_doc.rs +++ b/codex-rs/core/src/project_doc.rs @@ -1,6 +1,7 @@ //! Project-level documentation discovery. //! -//! Project-level documentation can be stored in files named `AGENTS.md`. +//! Project-level documentation is primarily stored in files named `AGENTS.md`. +//! Additional fallback filenames can be configured via `project_doc_fallback_filenames`. //! We include the concatenation of all files found along the path from the //! repository root to the current working directory as follows: //! @@ -17,8 +18,8 @@ use std::path::PathBuf; use tokio::io::AsyncReadExt; use tracing::error; -/// Currently, we only match the filename `AGENTS.md` exactly. -const CANDIDATE_FILENAMES: &[&str] = &["AGENTS.md"]; +/// Default filename scanned for project-level docs. +pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md"; /// When both `Config::instructions` and the project doc are present, they will /// be concatenated with the following separator. @@ -152,8 +153,9 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result = Vec::new(); + let candidate_filenames = candidate_filenames(config); for d in search_dirs { - for name in CANDIDATE_FILENAMES { + for name in &candidate_filenames { let candidate = d.join(name); match std::fs::symlink_metadata(&candidate) { Ok(md) => { @@ -173,6 +175,22 @@ 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()); + names.push(DEFAULT_PROJECT_DOC_FILENAME); + for candidate in &config.project_doc_fallback_filenames { + let candidate = candidate.as_str(); + if candidate.is_empty() { + continue; + } + if !names.contains(&candidate) { + names.push(candidate); + } + } + names +} + #[cfg(test)] mod tests { use super::*; @@ -202,6 +220,20 @@ mod tests { config } + fn make_config_with_fallback( + root: &TempDir, + limit: usize, + instructions: Option<&str>, + fallbacks: &[&str], + ) -> Config { + let mut config = make_config(root, limit, instructions); + config.project_doc_fallback_filenames = fallbacks + .iter() + .map(std::string::ToString::to_string) + .collect(); + config + } + /// AGENTS.md missing – should yield `None`. #[tokio::test] async fn no_doc_file_returns_none() { @@ -347,4 +379,45 @@ mod tests { let res = get_user_instructions(&cfg).await.expect("doc expected"); assert_eq!(res, "root doc\n\ncrate doc"); } + + /// When AGENTS.md is absent but a configured fallback exists, the fallback is used. + #[tokio::test] + async fn uses_configured_fallback_when_agents_missing() { + let tmp = tempfile::tempdir().expect("tempdir"); + fs::write(tmp.path().join("EXAMPLE.md"), "example instructions").unwrap(); + + let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md"]); + + let res = get_user_instructions(&cfg) + .await + .expect("fallback doc expected"); + + assert_eq!(res, "example instructions"); + } + + /// AGENTS.md remains preferred when both AGENTS.md and fallbacks are present. + #[tokio::test] + async fn agents_md_preferred_over_fallbacks() { + let tmp = tempfile::tempdir().expect("tempdir"); + fs::write(tmp.path().join("AGENTS.md"), "primary").unwrap(); + fs::write(tmp.path().join("EXAMPLE.md"), "secondary").unwrap(); + + let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md", ".example.md"]); + + let res = get_user_instructions(&cfg) + .await + .expect("AGENTS.md should win"); + + assert_eq!(res, "primary"); + + let discovery = discover_project_doc_paths(&cfg).expect("discover paths"); + assert_eq!(discovery.len(), 1); + assert!( + discovery[0] + .file_name() + .unwrap() + .to_string_lossy() + .eq(DEFAULT_PROJECT_DOC_FILENAME) + ); + } } diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 34e8e3bc..ed43172d 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::test_backend::VT100Backend; +use crate::tui::FrameRequester; use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::config::Config; diff --git a/codex-rs/tui/src/status/helpers.rs b/codex-rs/tui/src/status/helpers.rs index 43dfd820..47faae27 100644 --- a/codex-rs/tui/src/status/helpers.rs +++ b/codex-rs/tui/src/status/helpers.rs @@ -36,9 +36,13 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String { Ok(paths) => { let mut rels: Vec = Vec::new(); for p in paths { + let file_name = p + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_else(|| "".to_string()); let display = if let Some(parent) = p.parent() { if parent == config.cwd { - "AGENTS.md".to_string() + file_name.clone() } else { let mut cur = config.cwd.as_path(); let mut ups = 0usize; @@ -53,7 +57,7 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String { } if reached { let up = format!("..{}", std::path::MAIN_SEPARATOR); - format!("{}AGENTS.md", up.repeat(ups)) + format!("{}{}", up.repeat(ups), file_name) } else if let Ok(stripped) = p.strip_prefix(&config.cwd) { stripped.display().to_string() } else { diff --git a/docs/config.md b/docs/config.md index c8ebf94b..9c4e6575 100644 --- a/docs/config.md +++ b/docs/config.md @@ -705,6 +705,16 @@ This is analogous to `model_context_window`, but for the maximum number of outpu Maximum number of bytes to read from an `AGENTS.md` file to include in the instructions sent with the first turn of a session. Defaults to 32 KiB. +## project_doc_fallback_filenames + +Ordered list of additional filenames to look for when `AGENTS.md` is missing at a given directory level. The CLI always checks `AGENTS.md` first; the configured fallbacks are tried in the order provided. This lets monorepos that already use alternate instruction files (for example, `CLAUDE.md`) work out of the box while you migrate to `AGENTS.md` over time. + +```toml +project_doc_fallback_filenames = ["CLAUDE.md", ".exampleagentrules.md"] +``` + +We recommend migrating instructions to AGENTS.md; other filenames may reduce model performance. + ## tui Options that are specific to the TUI.