mirror of
https://github.com/openai/codex.git
synced 2026-02-02 15:03:38 +00:00
Compare commits
1 Commits
exec-run-a
...
pr9012
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9307235f7 |
@@ -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<PathBu
|
||||
dir = canon;
|
||||
}
|
||||
|
||||
// Build chain from cwd upwards and detect git root.
|
||||
let mut chain: Vec<PathBuf> = vec![dir.clone()];
|
||||
let mut git_root: Option<PathBuf> = 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<PathBuf> = if let Some(root) = git_root {
|
||||
let mut dirs: Vec<PathBuf> = 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<PathBuf> = Vec::new();
|
||||
@@ -200,6 +181,60 @@ pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBu
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
fn project_root_markers_from_config(config: &Config) -> io::Result<Vec<String>> {
|
||||
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<String> {
|
||||
DEFAULT_PROJECT_ROOT_MARKERS
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn find_project_root(cwd: &std::path::Path, markers: &[String]) -> io::Result<Option<PathBuf>> {
|
||||
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());
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<ResponseMock> {
|
||||
async fn start_test_conversation(
|
||||
server: &MockServer,
|
||||
model: Option<&str>,
|
||||
) -> (TempDir, Config, ThreadManager, Arc<CodexThread>) {
|
||||
) -> (TempDir, TempDir, Config, ThreadManager, Arc<CodexThread>) {
|
||||
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<CodexThread>, text: &str) {
|
||||
|
||||
Reference in New Issue
Block a user