Skip to content

Commit 400a5a9

Browse files
Fall back to configured instruction files if AGENTS.md isn't available (#4544)
Allow users to configure an agents.md alternative to consume, but warn the user it may degrade model performance. Fixes #4376
1 parent 2f370e9 commit 400a5a9

File tree

5 files changed

+117
-6
lines changed

5 files changed

+117
-6
lines changed

codex-rs/core/src/config.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ pub struct Config {
141141
/// Maximum number of bytes to include from an AGENTS.md project doc file.
142142
pub project_doc_max_bytes: usize,
143143

144+
/// Additional filenames to try when looking for project-level docs.
145+
pub project_doc_fallback_filenames: Vec<String>,
146+
144147
/// Directory containing all Codex state (defaults to `~/.codex` but can be
145148
/// overridden by the `CODEX_HOME` environment variable).
146149
pub codex_home: PathBuf,
@@ -670,6 +673,9 @@ pub struct ConfigToml {
670673
/// Maximum number of bytes to include from an AGENTS.md project doc file.
671674
pub project_doc_max_bytes: Option<usize>,
672675

676+
/// Ordered list of fallback filenames to look for when AGENTS.md is missing.
677+
pub project_doc_fallback_filenames: Option<Vec<String>>,
678+
673679
/// Profile to use from the `profiles` map.
674680
pub profile: Option<String>,
675681

@@ -1038,6 +1044,19 @@ impl Config {
10381044
mcp_servers: cfg.mcp_servers,
10391045
model_providers,
10401046
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
1047+
project_doc_fallback_filenames: cfg
1048+
.project_doc_fallback_filenames
1049+
.unwrap_or_default()
1050+
.into_iter()
1051+
.filter_map(|name| {
1052+
let trimmed = name.trim();
1053+
if trimmed.is_empty() {
1054+
None
1055+
} else {
1056+
Some(trimmed.to_string())
1057+
}
1058+
})
1059+
.collect(),
10411060
codex_home,
10421061
history,
10431062
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
@@ -1811,6 +1830,7 @@ model_verbosity = "high"
18111830
mcp_servers: HashMap::new(),
18121831
model_providers: fixture.model_provider_map.clone(),
18131832
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
1833+
project_doc_fallback_filenames: Vec::new(),
18141834
codex_home: fixture.codex_home(),
18151835
history: History::default(),
18161836
file_opener: UriBasedFileOpener::VsCode,
@@ -1871,6 +1891,7 @@ model_verbosity = "high"
18711891
mcp_servers: HashMap::new(),
18721892
model_providers: fixture.model_provider_map.clone(),
18731893
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
1894+
project_doc_fallback_filenames: Vec::new(),
18741895
codex_home: fixture.codex_home(),
18751896
history: History::default(),
18761897
file_opener: UriBasedFileOpener::VsCode,
@@ -1946,6 +1967,7 @@ model_verbosity = "high"
19461967
mcp_servers: HashMap::new(),
19471968
model_providers: fixture.model_provider_map.clone(),
19481969
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
1970+
project_doc_fallback_filenames: Vec::new(),
19491971
codex_home: fixture.codex_home(),
19501972
history: History::default(),
19511973
file_opener: UriBasedFileOpener::VsCode,
@@ -2007,6 +2029,7 @@ model_verbosity = "high"
20072029
mcp_servers: HashMap::new(),
20082030
model_providers: fixture.model_provider_map.clone(),
20092031
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
2032+
project_doc_fallback_filenames: Vec::new(),
20102033
codex_home: fixture.codex_home(),
20112034
history: History::default(),
20122035
file_opener: UriBasedFileOpener::VsCode,

codex-rs/core/src/project_doc.rs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Project-level documentation discovery.
22
//!
3-
//! Project-level documentation can be stored in files named `AGENTS.md`.
3+
//! Project-level documentation is primarily stored in files named `AGENTS.md`.
4+
//! Additional fallback filenames can be configured via `project_doc_fallback_filenames`.
45
//! We include the concatenation of all files found along the path from the
56
//! repository root to the current working directory as follows:
67
//!
@@ -17,8 +18,8 @@ use std::path::PathBuf;
1718
use tokio::io::AsyncReadExt;
1819
use tracing::error;
1920

20-
/// Currently, we only match the filename `AGENTS.md` exactly.
21-
const CANDIDATE_FILENAMES: &[&str] = &["AGENTS.md"];
21+
/// Default filename scanned for project-level docs.
22+
pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md";
2223

2324
/// When both `Config::instructions` and the project doc are present, they will
2425
/// be concatenated with the following separator.
@@ -152,8 +153,9 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBu
152153
};
153154

154155
let mut found: Vec<PathBuf> = Vec::new();
156+
let candidate_filenames = candidate_filenames(config);
155157
for d in search_dirs {
156-
for name in CANDIDATE_FILENAMES {
158+
for name in &candidate_filenames {
157159
let candidate = d.join(name);
158160
match std::fs::symlink_metadata(&candidate) {
159161
Ok(md) => {
@@ -173,6 +175,22 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBu
173175
Ok(found)
174176
}
175177

178+
fn candidate_filenames<'a>(config: &'a Config) -> Vec<&'a str> {
179+
let mut names: Vec<&'a str> =
180+
Vec::with_capacity(1 + config.project_doc_fallback_filenames.len());
181+
names.push(DEFAULT_PROJECT_DOC_FILENAME);
182+
for candidate in &config.project_doc_fallback_filenames {
183+
let candidate = candidate.as_str();
184+
if candidate.is_empty() {
185+
continue;
186+
}
187+
if !names.contains(&candidate) {
188+
names.push(candidate);
189+
}
190+
}
191+
names
192+
}
193+
176194
#[cfg(test)]
177195
mod tests {
178196
use super::*;
@@ -202,6 +220,20 @@ mod tests {
202220
config
203221
}
204222

223+
fn make_config_with_fallback(
224+
root: &TempDir,
225+
limit: usize,
226+
instructions: Option<&str>,
227+
fallbacks: &[&str],
228+
) -> Config {
229+
let mut config = make_config(root, limit, instructions);
230+
config.project_doc_fallback_filenames = fallbacks
231+
.iter()
232+
.map(std::string::ToString::to_string)
233+
.collect();
234+
config
235+
}
236+
205237
/// AGENTS.md missing – should yield `None`.
206238
#[tokio::test]
207239
async fn no_doc_file_returns_none() {
@@ -347,4 +379,45 @@ mod tests {
347379
let res = get_user_instructions(&cfg).await.expect("doc expected");
348380
assert_eq!(res, "root doc\n\ncrate doc");
349381
}
382+
383+
/// When AGENTS.md is absent but a configured fallback exists, the fallback is used.
384+
#[tokio::test]
385+
async fn uses_configured_fallback_when_agents_missing() {
386+
let tmp = tempfile::tempdir().expect("tempdir");
387+
fs::write(tmp.path().join("EXAMPLE.md"), "example instructions").unwrap();
388+
389+
let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md"]);
390+
391+
let res = get_user_instructions(&cfg)
392+
.await
393+
.expect("fallback doc expected");
394+
395+
assert_eq!(res, "example instructions");
396+
}
397+
398+
/// AGENTS.md remains preferred when both AGENTS.md and fallbacks are present.
399+
#[tokio::test]
400+
async fn agents_md_preferred_over_fallbacks() {
401+
let tmp = tempfile::tempdir().expect("tempdir");
402+
fs::write(tmp.path().join("AGENTS.md"), "primary").unwrap();
403+
fs::write(tmp.path().join("EXAMPLE.md"), "secondary").unwrap();
404+
405+
let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md", ".example.md"]);
406+
407+
let res = get_user_instructions(&cfg)
408+
.await
409+
.expect("AGENTS.md should win");
410+
411+
assert_eq!(res, "primary");
412+
413+
let discovery = discover_project_doc_paths(&cfg).expect("discover paths");
414+
assert_eq!(discovery.len(), 1);
415+
assert!(
416+
discovery[0]
417+
.file_name()
418+
.unwrap()
419+
.to_string_lossy()
420+
.eq(DEFAULT_PROJECT_DOC_FILENAME)
421+
);
422+
}
350423
}

codex-rs/tui/src/chatwidget/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::*;
22
use crate::app_event::AppEvent;
33
use crate::app_event_sender::AppEventSender;
44
use crate::test_backend::VT100Backend;
5+
use crate::tui::FrameRequester;
56
use codex_core::AuthManager;
67
use codex_core::CodexAuth;
78
use codex_core::config::Config;

codex-rs/tui/src/status/helpers.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String {
3636
Ok(paths) => {
3737
let mut rels: Vec<String> = Vec::new();
3838
for p in paths {
39+
let file_name = p
40+
.file_name()
41+
.map(|name| name.to_string_lossy().to_string())
42+
.unwrap_or_else(|| "<unknown>".to_string());
3943
let display = if let Some(parent) = p.parent() {
4044
if parent == config.cwd {
41-
"AGENTS.md".to_string()
45+
file_name.clone()
4246
} else {
4347
let mut cur = config.cwd.as_path();
4448
let mut ups = 0usize;
@@ -53,7 +57,7 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String {
5357
}
5458
if reached {
5559
let up = format!("..{}", std::path::MAIN_SEPARATOR);
56-
format!("{}AGENTS.md", up.repeat(ups))
60+
format!("{}{}", up.repeat(ups), file_name)
5761
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
5862
stripped.display().to_string()
5963
} else {

docs/config.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,16 @@ This is analogous to `model_context_window`, but for the maximum number of outpu
705705

706706
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.
707707

708+
## project_doc_fallback_filenames
709+
710+
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.
711+
712+
```toml
713+
project_doc_fallback_filenames = ["CLAUDE.md", ".exampleagentrules.md"]
714+
```
715+
716+
We recommend migrating instructions to AGENTS.md; other filenames may reduce model performance.
717+
708718
## tui
709719

710720
Options that are specific to the TUI.

0 commit comments

Comments
 (0)