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
This commit is contained in:
@@ -141,6 +141,9 @@ pub struct Config {
|
|||||||
/// Maximum number of bytes to include from an AGENTS.md project doc file.
|
/// Maximum number of bytes to include from an AGENTS.md project doc file.
|
||||||
pub project_doc_max_bytes: usize,
|
pub project_doc_max_bytes: usize,
|
||||||
|
|
||||||
|
/// Additional filenames to try when looking for project-level docs.
|
||||||
|
pub project_doc_fallback_filenames: Vec<String>,
|
||||||
|
|
||||||
/// Directory containing all Codex state (defaults to `~/.codex` but can be
|
/// Directory containing all Codex state (defaults to `~/.codex` but can be
|
||||||
/// overridden by the `CODEX_HOME` environment variable).
|
/// overridden by the `CODEX_HOME` environment variable).
|
||||||
pub codex_home: PathBuf,
|
pub codex_home: PathBuf,
|
||||||
@@ -670,6 +673,9 @@ pub struct ConfigToml {
|
|||||||
/// Maximum number of bytes to include from an AGENTS.md project doc file.
|
/// Maximum number of bytes to include from an AGENTS.md project doc file.
|
||||||
pub project_doc_max_bytes: Option<usize>,
|
pub project_doc_max_bytes: Option<usize>,
|
||||||
|
|
||||||
|
/// Ordered list of fallback filenames to look for when AGENTS.md is missing.
|
||||||
|
pub project_doc_fallback_filenames: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Profile to use from the `profiles` map.
|
/// Profile to use from the `profiles` map.
|
||||||
pub profile: Option<String>,
|
pub profile: Option<String>,
|
||||||
|
|
||||||
@@ -1038,6 +1044,19 @@ impl Config {
|
|||||||
mcp_servers: cfg.mcp_servers,
|
mcp_servers: cfg.mcp_servers,
|
||||||
model_providers,
|
model_providers,
|
||||||
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
|
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,
|
codex_home,
|
||||||
history,
|
history,
|
||||||
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
|
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
|
||||||
@@ -1811,6 +1830,7 @@ model_verbosity = "high"
|
|||||||
mcp_servers: HashMap::new(),
|
mcp_servers: HashMap::new(),
|
||||||
model_providers: fixture.model_provider_map.clone(),
|
model_providers: fixture.model_provider_map.clone(),
|
||||||
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
||||||
|
project_doc_fallback_filenames: Vec::new(),
|
||||||
codex_home: fixture.codex_home(),
|
codex_home: fixture.codex_home(),
|
||||||
history: History::default(),
|
history: History::default(),
|
||||||
file_opener: UriBasedFileOpener::VsCode,
|
file_opener: UriBasedFileOpener::VsCode,
|
||||||
@@ -1871,6 +1891,7 @@ model_verbosity = "high"
|
|||||||
mcp_servers: HashMap::new(),
|
mcp_servers: HashMap::new(),
|
||||||
model_providers: fixture.model_provider_map.clone(),
|
model_providers: fixture.model_provider_map.clone(),
|
||||||
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
||||||
|
project_doc_fallback_filenames: Vec::new(),
|
||||||
codex_home: fixture.codex_home(),
|
codex_home: fixture.codex_home(),
|
||||||
history: History::default(),
|
history: History::default(),
|
||||||
file_opener: UriBasedFileOpener::VsCode,
|
file_opener: UriBasedFileOpener::VsCode,
|
||||||
@@ -1946,6 +1967,7 @@ model_verbosity = "high"
|
|||||||
mcp_servers: HashMap::new(),
|
mcp_servers: HashMap::new(),
|
||||||
model_providers: fixture.model_provider_map.clone(),
|
model_providers: fixture.model_provider_map.clone(),
|
||||||
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
||||||
|
project_doc_fallback_filenames: Vec::new(),
|
||||||
codex_home: fixture.codex_home(),
|
codex_home: fixture.codex_home(),
|
||||||
history: History::default(),
|
history: History::default(),
|
||||||
file_opener: UriBasedFileOpener::VsCode,
|
file_opener: UriBasedFileOpener::VsCode,
|
||||||
@@ -2007,6 +2029,7 @@ model_verbosity = "high"
|
|||||||
mcp_servers: HashMap::new(),
|
mcp_servers: HashMap::new(),
|
||||||
model_providers: fixture.model_provider_map.clone(),
|
model_providers: fixture.model_provider_map.clone(),
|
||||||
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
|
||||||
|
project_doc_fallback_filenames: Vec::new(),
|
||||||
codex_home: fixture.codex_home(),
|
codex_home: fixture.codex_home(),
|
||||||
history: History::default(),
|
history: History::default(),
|
||||||
file_opener: UriBasedFileOpener::VsCode,
|
file_opener: UriBasedFileOpener::VsCode,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Project-level documentation discovery.
|
//! 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
|
//! We include the concatenation of all files found along the path from the
|
||||||
//! repository root to the current working directory as follows:
|
//! repository root to the current working directory as follows:
|
||||||
//!
|
//!
|
||||||
@@ -17,8 +18,8 @@ use std::path::PathBuf;
|
|||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
/// Currently, we only match the filename `AGENTS.md` exactly.
|
/// Default filename scanned for project-level docs.
|
||||||
const CANDIDATE_FILENAMES: &[&str] = &["AGENTS.md"];
|
pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md";
|
||||||
|
|
||||||
/// When both `Config::instructions` and the project doc are present, they will
|
/// When both `Config::instructions` and the project doc are present, they will
|
||||||
/// be concatenated with the following separator.
|
/// be concatenated with the following separator.
|
||||||
@@ -152,8 +153,9 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBu
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut found: Vec<PathBuf> = Vec::new();
|
let mut found: Vec<PathBuf> = Vec::new();
|
||||||
|
let candidate_filenames = candidate_filenames(config);
|
||||||
for d in search_dirs {
|
for d in search_dirs {
|
||||||
for name in CANDIDATE_FILENAMES {
|
for name in &candidate_filenames {
|
||||||
let candidate = d.join(name);
|
let candidate = d.join(name);
|
||||||
match std::fs::symlink_metadata(&candidate) {
|
match std::fs::symlink_metadata(&candidate) {
|
||||||
Ok(md) => {
|
Ok(md) => {
|
||||||
@@ -173,6 +175,22 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBu
|
|||||||
Ok(found)
|
Ok(found)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn candidate_filenames<'a>(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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -202,6 +220,20 @@ mod tests {
|
|||||||
config
|
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`.
|
/// AGENTS.md missing – should yield `None`.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn no_doc_file_returns_none() {
|
async fn no_doc_file_returns_none() {
|
||||||
@@ -347,4 +379,45 @@ mod tests {
|
|||||||
let res = get_user_instructions(&cfg).await.expect("doc expected");
|
let res = get_user_instructions(&cfg).await.expect("doc expected");
|
||||||
assert_eq!(res, "root doc\n\ncrate doc");
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::*;
|
|||||||
use crate::app_event::AppEvent;
|
use crate::app_event::AppEvent;
|
||||||
use crate::app_event_sender::AppEventSender;
|
use crate::app_event_sender::AppEventSender;
|
||||||
use crate::test_backend::VT100Backend;
|
use crate::test_backend::VT100Backend;
|
||||||
|
use crate::tui::FrameRequester;
|
||||||
use codex_core::AuthManager;
|
use codex_core::AuthManager;
|
||||||
use codex_core::CodexAuth;
|
use codex_core::CodexAuth;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
|
|||||||
@@ -36,9 +36,13 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String {
|
|||||||
Ok(paths) => {
|
Ok(paths) => {
|
||||||
let mut rels: Vec<String> = Vec::new();
|
let mut rels: Vec<String> = Vec::new();
|
||||||
for p in paths {
|
for p in paths {
|
||||||
|
let file_name = p
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "<unknown>".to_string());
|
||||||
let display = if let Some(parent) = p.parent() {
|
let display = if let Some(parent) = p.parent() {
|
||||||
if parent == config.cwd {
|
if parent == config.cwd {
|
||||||
"AGENTS.md".to_string()
|
file_name.clone()
|
||||||
} else {
|
} else {
|
||||||
let mut cur = config.cwd.as_path();
|
let mut cur = config.cwd.as_path();
|
||||||
let mut ups = 0usize;
|
let mut ups = 0usize;
|
||||||
@@ -53,7 +57,7 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String {
|
|||||||
}
|
}
|
||||||
if reached {
|
if reached {
|
||||||
let up = format!("..{}", std::path::MAIN_SEPARATOR);
|
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) {
|
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
|
||||||
stripped.display().to_string()
|
stripped.display().to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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.
|
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
|
## tui
|
||||||
|
|
||||||
Options that are specific to the TUI.
|
Options that are specific to the TUI.
|
||||||
|
|||||||
Reference in New Issue
Block a user