diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 9fc0c8a05..da11b8346 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -128,6 +128,56 @@ fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> { Ok(()) } +#[test] +fn load_config_records_global_agents_path() -> std::io::Result<()> { + let codex_home = tempdir()?; + let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME); + std::fs::write(&global_agents_path, "\n global instructions \n")?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides::default(), + codex_home.abs().into_path_buf(), + )?; + + assert_eq!( + config.user_instructions.as_deref(), + Some("global instructions") + ); + assert_eq!( + config.user_instructions_path.as_deref(), + Some(global_agents_path.as_path()) + ); + Ok(()) +} + +#[test] +fn load_config_records_preferred_global_agents_override_path() -> std::io::Result<()> { + let codex_home = tempdir()?; + std::fs::write( + codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME), + "global instructions", + )?; + let global_agents_override_path = codex_home.path().join(LOCAL_PROJECT_DOC_FILENAME); + std::fs::write(&global_agents_override_path, "local override instructions")?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides::default(), + codex_home.abs().into_path_buf(), + )?; + + assert_eq!( + config.user_instructions.as_deref(), + Some("local override instructions") + ); + assert_eq!( + config.user_instructions_path.as_deref(), + Some(global_agents_override_path.as_path()) + ); + Ok(()) +} + #[test] fn test_toml_parsing() { let history_with_persistence = r#" @@ -4457,6 +4507,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { approvals_reviewer: ApprovalsReviewer::User, enforce_residency: Constrained::allow_any(/*initial_value*/ None), user_instructions: None, + user_instructions_path: None, notify: None, cwd: fixture.cwd(), cli_auth_credentials_store_mode: Default::default(), @@ -4603,6 +4654,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { approvals_reviewer: ApprovalsReviewer::User, enforce_residency: Constrained::allow_any(/*initial_value*/ None), user_instructions: None, + user_instructions_path: None, notify: None, cwd: fixture.cwd(), cli_auth_credentials_store_mode: Default::default(), @@ -4747,6 +4799,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { approvals_reviewer: ApprovalsReviewer::User, enforce_residency: Constrained::allow_any(/*initial_value*/ None), user_instructions: None, + user_instructions_path: None, notify: None, cwd: fixture.cwd(), cli_auth_credentials_store_mode: Default::default(), @@ -4877,6 +4930,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { approvals_reviewer: ApprovalsReviewer::User, enforce_residency: Constrained::allow_any(/*initial_value*/ None), user_instructions: None, + user_instructions_path: None, notify: None, cwd: fixture.cwd(), cli_auth_credentials_store_mode: Default::default(), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 7783c3046..289d3a0d6 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -245,6 +245,9 @@ pub struct Config { /// User-provided instructions from AGENTS.md. pub user_instructions: Option, + /// Path to the global AGENTS file loaded into `user_instructions`. + pub user_instructions_path: Option, + /// Base instructions override. pub base_instructions: Option, @@ -1408,7 +1411,10 @@ impl Config { network: network_requirements, } = config_layer_stack.requirements().clone(); - let user_instructions = Self::load_instructions(Some(&codex_home)); + let (user_instructions, user_instructions_path) = + Self::load_instructions(Some(&codex_home)) + .map(|loaded| (Some(loaded.contents), Some(loaded.path))) + .unwrap_or((None, None)); let mut startup_warnings = Vec::new(); // Destructure ConfigOverrides fully to ensure all overrides are applied. @@ -2001,6 +2007,7 @@ impl Config { enforce_residency: enforce_residency.value, notify: cfg.notify, user_instructions, + user_instructions_path, base_instructions, personality, developer_instructions, @@ -2168,7 +2175,7 @@ impl Config { Ok(config) } - fn load_instructions(codex_dir: Option<&Path>) -> Option { + fn load_instructions(codex_dir: Option<&Path>) -> Option { let base = codex_dir?; for candidate in [LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME] { let mut path = base.to_path_buf(); @@ -2176,7 +2183,10 @@ impl Config { if let Ok(contents) = std::fs::read_to_string(&path) { let trimmed = contents.trim(); if !trimmed.is_empty() { - return Some(trimmed.to_string()); + return Some(LoadedUserInstructions { + contents: trimmed.to_string(), + path, + }); } } } @@ -2250,6 +2260,11 @@ impl Config { } } +struct LoadedUserInstructions { + contents: String, + path: PathBuf, +} + pub(crate) fn uses_deprecated_instructions_file(config_layer_stack: &ConfigLayerStack) -> bool { config_layer_stack .layers_high_to_low() diff --git a/codex-rs/tui/src/status/helpers.rs b/codex-rs/tui/src/status/helpers.rs index 76aaa3da1..338f50346 100644 --- a/codex-rs/tui/src/status/helpers.rs +++ b/codex-rs/tui/src/status/helpers.rs @@ -43,6 +43,10 @@ pub(crate) async fn discover_agents_summary(config: &Config) -> io::Result String { let mut rels: Vec = Vec::new(); + if let Some(path) = config.user_instructions_path.as_deref() { + rels.push(format_directory_display(path, /*max_width*/ None)); + } + for p in paths { let file_name = p .file_name() @@ -189,7 +193,21 @@ fn title_case(s: &str) -> String { #[cfg(test)] mod tests { use super::*; + use codex_core::DEFAULT_PROJECT_DOC_FILENAME; + use codex_core::LOCAL_PROJECT_DOC_FILENAME; + use codex_core::config::ConfigBuilder; use pretty_assertions::assert_eq; + use std::fs; + use tempfile::TempDir; + + async fn test_config(codex_home: &TempDir, cwd: &TempDir) -> Config { + ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(cwd.path().to_path_buf())) + .build() + .await + .expect("load config") + } #[test] fn plan_type_display_name_remaps_display_labels() { @@ -211,4 +229,61 @@ mod tests { assert_eq!(plan_type_display_name(plan_type), expected); } } + + #[tokio::test] + async fn discover_agents_summary_includes_global_agents_path() { + let codex_home = TempDir::new().expect("temp codex home"); + let cwd = TempDir::new().expect("temp cwd"); + let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME); + fs::write(&global_agents_path, "global instructions").expect("write global agents"); + let config = test_config(&codex_home, &cwd).await; + + assert_eq!( + discover_agents_summary(&config).await.expect("summary"), + format_directory_display(&global_agents_path, /*max_width*/ None) + ); + } + + #[tokio::test] + async fn discover_agents_summary_names_global_agents_override() { + let codex_home = TempDir::new().expect("temp codex home"); + let cwd = TempDir::new().expect("temp cwd"); + fs::write( + codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME), + "global instructions", + ) + .expect("write global agents"); + let override_path = codex_home.path().join(LOCAL_PROJECT_DOC_FILENAME); + fs::write(&override_path, "override instructions").expect("write global override"); + let config = test_config(&codex_home, &cwd).await; + + assert_eq!( + discover_agents_summary(&config).await.expect("summary"), + format_directory_display(&override_path, /*max_width*/ None) + ); + } + + #[tokio::test] + async fn discover_agents_summary_orders_global_before_project_agents() { + let codex_home = TempDir::new().expect("temp codex home"); + let cwd = TempDir::new().expect("temp cwd"); + let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME); + fs::write(&global_agents_path, "global instructions").expect("write global agents"); + fs::write( + cwd.path().join(DEFAULT_PROJECT_DOC_FILENAME), + "project instructions", + ) + .expect("write project agents"); + let config = test_config(&codex_home, &cwd).await; + + let summary = discover_agents_summary(&config).await.expect("summary"); + let mut paths = summary.split(", "); + assert_eq!( + paths.next(), + Some(format_directory_display(&global_agents_path, /*max_width*/ None).as_str()) + ); + let project_path = paths.next().expect("project agents path"); + assert!(project_path.ends_with(DEFAULT_PROJECT_DOC_FILENAME)); + assert_eq!(paths.next(), None); + } }