From c9307235f74df5903a680a8b61f0785fc6ddd39e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 9 Jan 2026 16:52:15 -0800 Subject: [PATCH] try to fix :AGENTS.md bazel hack --- codex-rs/core/src/project_doc.rs | 111 ++++++++++++------ codex-rs/core/tests/common/lib.rs | 18 +++ codex-rs/core/tests/suite/compact.rs | 11 ++ .../core/tests/suite/compact_resume_fork.rs | 12 +- 4 files changed, 110 insertions(+), 42 deletions(-) diff --git a/codex-rs/core/src/project_doc.rs b/codex-rs/core/src/project_doc.rs index 79f82c4598..11fe6caec5 100644 --- a/codex-rs/core/src/project_doc.rs +++ b/codex-rs/core/src/project_doc.rs @@ -5,26 +5,30 @@ //! We include the concatenation of all files found along the path from the //! repository root to the current working directory as follows: //! -//! 1. Determine the Git repository root by walking upwards from the current -//! working directory until a `.git` directory or file is found. If no Git -//! root is found, only the current working directory is considered. +//! 1. Determine the project root by walking upwards from the current working +//! directory until any marker in `project_root_markers` (default: `.git`) +//! is found. If no root is found (or markers are empty), only the current +//! working directory is considered. //! 2. Collect every `AGENTS.md` found from the repository root down to the //! current working directory (inclusive) and concatenate their contents in //! that order. -//! 3. We do **not** walk past the Git root. +//! 3. We do **not** walk past the detected project root. use crate::config::Config; use crate::skills::SkillMetadata; use crate::skills::render_skills_section; use dunce::canonicalize as normalize_path; +use std::io; use std::path::PathBuf; use tokio::io::AsyncReadExt; +use toml::Value as TomlValue; 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"; +const DEFAULT_PROJECT_ROOT_MARKERS: &[&str] = &[".git"]; /// When both `Config::instructions` and the project doc are present, they will /// be concatenated with the following separator. @@ -138,43 +142,20 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result = vec![dir.clone()]; - let mut git_root: Option = None; - let mut cursor = dir; - while let Some(parent) = cursor.parent() { - let git_marker = cursor.join(".git"); - let git_exists = match std::fs::metadata(&git_marker) { - Ok(_) => true, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => false, - Err(e) => return Err(e), - }; - - if git_exists { - git_root = Some(cursor.clone()); - break; - } - - chain.push(parent.to_path_buf()); - cursor = parent.to_path_buf(); - } - - let search_dirs: Vec = if let Some(root) = git_root { - let mut dirs: Vec = Vec::new(); - let mut saw_root = false; - for p in chain.iter().rev() { - if !saw_root { - if p == &root { - saw_root = true; - } else { - continue; + let markers = project_root_markers_from_config(config)?; + let search_dirs = match find_project_root(&dir, &markers)? { + Some(root) => { + let mut dirs = Vec::new(); + for ancestor in dir.as_path().ancestors() { + dirs.push(ancestor.to_path_buf()); + if ancestor == root { + break; } } - dirs.push(p.clone()); + dirs.reverse(); + dirs } - dirs - } else { - vec![config.cwd.clone()] + None => vec![dir.clone()], }; let mut found: Vec = Vec::new(); @@ -200,6 +181,60 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result io::Result> { + let merged = config.config_layer_stack.effective_config(); + let Some(table) = merged.as_table() else { + return Ok(default_project_root_markers()); + }; + let Some(markers_value) = table.get("project_root_markers") else { + return Ok(default_project_root_markers()); + }; + let TomlValue::Array(entries) = markers_value else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "project_root_markers must be an array of strings", + )); + }; + if entries.is_empty() { + return Ok(Vec::new()); + } + let mut markers = Vec::new(); + for entry in entries { + let Some(marker) = entry.as_str() else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "project_root_markers must be an array of strings", + )); + }; + markers.push(marker.to_string()); + } + Ok(markers) +} + +fn default_project_root_markers() -> Vec { + DEFAULT_PROJECT_ROOT_MARKERS + .iter() + .map(ToString::to_string) + .collect() +} + +fn find_project_root(cwd: &std::path::Path, markers: &[String]) -> io::Result> { + if markers.is_empty() { + return Ok(None); + } + for ancestor in cwd.ancestors() { + for marker in markers { + let marker_path = ancestor.join(marker); + match std::fs::metadata(&marker_path) { + Ok(_) => return Ok(Some(ancestor.to_path_buf())), + Err(e) if e.kind() == io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + } + } + } + Ok(None) +} + fn candidate_filenames<'a>(config: &'a Config) -> Vec<&'a str> { let mut names: Vec<&'a str> = Vec::with_capacity(2 + config.project_doc_fallback_filenames.len()); diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 5b80f80ba3..b8931a1719 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -5,6 +5,7 @@ use codex_utils_cargo_bin::find_resource; use tempfile::TempDir; use codex_core::CodexThread; +use codex_core::config::CONFIG_TOML_FILE; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; @@ -18,6 +19,23 @@ pub mod streaming_sse; pub mod test_codex; pub mod test_codex_exec; +pub fn prepare_test_project_root(codex_home: &TempDir) -> (TempDir, PathBuf) { + const MARKER: &str = ".codex-test-root"; + let config_path = codex_home.path().join(CONFIG_TOML_FILE); + let config_contents = format!("project_root_markers = [\"{MARKER}\"]\n"); + std::fs::write(config_path, config_contents).expect("write config.toml"); + + let root = TempDir::new().expect("tempdir"); + let root_path = root.path().to_path_buf(); + std::fs::write(root_path.join(MARKER), "").expect("write project root marker"); + std::fs::write(root_path.join("AGENTS.md"), "Test project instructions.\n") + .expect("write AGENTS.md"); + + let cwd = root_path.join("workspace"); + std::fs::create_dir_all(&cwd).expect("create workspace"); + (root, cwd) +} + #[track_caller] pub fn assert_regex_match<'s>(pattern: &str, actual: &'s str) -> regex_lite::Captures<'s> { let regex = Regex::new(pattern).unwrap_or_else(|err| { diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index ee58399708..43c9c8ebc3 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -18,6 +18,7 @@ use codex_core::protocol::WarningEvent; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::user_input::UserInput; use core_test_support::load_default_config_for_test; +use core_test_support::prepare_test_project_root; use core_test_support::responses::ev_local_shell_call; use core_test_support::responses::ev_reasoning_item; use core_test_support::skip_if_no_network; @@ -139,9 +140,11 @@ async fn summarize_context_three_requests_and_instructions() { // Build config pointing to the mock server and spawn Codex. let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); + let (_project_root, project_cwd) = prepare_test_project_root(&home); let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); + config.cwd = project_cwd; config.model_auto_compact_token_limit = Some(200_000); let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), @@ -411,9 +414,11 @@ async fn manual_compact_emits_api_and_local_token_usage_events() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); + let (_project_root, project_cwd) = prepare_test_project_root(&home); let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); + config.cwd = project_cwd; let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), @@ -1029,9 +1034,11 @@ async fn auto_compact_runs_after_token_limit_hit() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); + let (_project_root, project_cwd) = prepare_test_project_root(&home); let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); + config.cwd = project_cwd; config.model_auto_compact_token_limit = Some(200_000); let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), @@ -1358,9 +1365,11 @@ async fn auto_compact_persists_rollout_entries() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); + let (_project_root, project_cwd) = prepare_test_project_root(&home); let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); + config.cwd = project_cwd; config.model_auto_compact_token_limit = Some(200_000); let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), @@ -1607,9 +1616,11 @@ async fn manual_compact_twice_preserves_latest_user_messages() { let model_provider = non_openai_model_provider(&server); let home = TempDir::new().unwrap(); + let (_project_root, project_cwd) = prepare_test_project_root(&home); let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); + config.cwd = project_cwd; let codex = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index ad2e0e65ad..cde2cf1d53 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -24,6 +24,7 @@ use codex_core::protocol::WarningEvent; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_protocol::user_input::UserInput; use core_test_support::load_default_config_for_test; +use core_test_support::prepare_test_project_root; use core_test_support::responses::ResponseMock; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -151,7 +152,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { let request_log = mount_initial_flow(&server).await; let expected_model = "gpt-5.1-codex"; // 2. Start a new conversation and drive it through the compact/resume/fork steps. - let (_home, config, manager, base) = + let (_home, _project_root, config, manager, base) = start_test_conversation(&server, Some(expected_model)).await; user_turn(&base, "hello world").await; @@ -604,7 +605,8 @@ async fn compact_resume_after_second_compaction_preserves_history() { request_log.extend(mount_second_compact_flow(&server).await); // 2. Drive the conversation through compact -> resume -> fork -> compact -> resume. - let (_home, config, manager, base) = start_test_conversation(&server, None).await; + let (_home, _project_root, config, manager, base) = + start_test_conversation(&server, None).await; user_turn(&base, "hello world").await; compact_conversation(&base).await; @@ -863,16 +865,18 @@ async fn mount_second_compact_flow(server: &MockServer) -> Vec { async fn start_test_conversation( server: &MockServer, model: Option<&str>, -) -> (TempDir, Config, ThreadManager, Arc) { +) -> (TempDir, TempDir, Config, ThreadManager, Arc) { let model_provider = ModelProviderInfo { name: "Non-OpenAI Model provider".into(), base_url: Some(format!("{}/v1", server.uri())), ..built_in_model_providers()["openai"].clone() }; let home = TempDir::new().expect("create temp dir"); + let (project_root, project_cwd) = prepare_test_project_root(&home); let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; config.compact_prompt = Some(SUMMARIZATION_PROMPT.to_string()); + config.cwd = project_cwd; if let Some(model) = model { config.model = Some(model.to_string()); } @@ -885,7 +889,7 @@ async fn start_test_conversation( .await .expect("create conversation"); - (home, config, manager, thread) + (home, project_root, config, manager, thread) } async fn user_turn(conversation: &Arc, text: &str) {