feat: read global AGENTS.md from ~/.agents/ as vendor-neutral fallback

This commit is contained in:
Matt Van Horn
2026-05-26 17:13:54 -07:00
committed by Hunter Bown
parent e2b58e6b22
commit f3def32ef9
+68 -10
View File
@@ -35,10 +35,13 @@ const PROJECT_CONTEXT_FILES: &[&str] = &[
/// User-level project instructions loaded as a fallback when the workspace and
/// its parents do not define project context. `.codewhale/` takes priority
/// over `.deepseek/` for both WHALE.md and AGENTS.md.
/// over vendor-neutral `.agents/`, which takes priority over legacy
/// `.deepseek/`, for both WHALE.md and AGENTS.md.
const GLOBAL_AGENTS_RELATIVE_PATH: &[&str] = &[".codewhale", "AGENTS.md"];
const GLOBAL_AGENTS_VENDOR_NEUTRAL_PATH: &[&str] = &[".agents", "AGENTS.md"];
const GLOBAL_AGENTS_LEGACY_PATH: &[&str] = &[".deepseek", "AGENTS.md"];
const GLOBAL_WHALE_RELATIVE_PATH: &[&str] = &[".codewhale", "WHALE.md"];
const GLOBAL_WHALE_VENDOR_NEUTRAL_PATH: &[&str] = &[".agents", "WHALE.md"];
const GLOBAL_WHALE_LEGACY_PATH: &[&str] = &[".deepseek", "WHALE.md"];
/// Maximum size for project context files (to prevent loading huge files)
@@ -437,7 +440,7 @@ fn load_project_context_with_parents_and_home(
}
}
// Always check `~/.deepseek/AGENTS.md` so user-wide preferences
// Always check global instruction files so user-wide preferences
// travel into every session (#1157). When both global and project
// instructions exist, the global block prepends the project's so
// workspace overrides win the last word; when only global exists,
@@ -487,12 +490,11 @@ fn load_project_context_with_parents_and_home(
ctx
}
/// Combine `~/.deepseek/AGENTS.md` (global, user-wide preferences) with a
/// project-local AGENTS.md/CLAUDE.md/instructions.md. Global comes first
/// so workspace-specific rules can override it — the model reads in
/// declared order. Each block is wrapped in a labelled fence so the
/// model can tell which level any rule comes from when the two sets
/// disagree (#1157).
/// Combine global user-wide preferences with a project-local
/// AGENTS.md/CLAUDE.md/instructions.md. Global comes first so
/// workspace-specific rules can override it — the model reads in declared
/// order. Each block is wrapped in a labelled fence so the model can tell
/// which level any rule comes from when the two sets disagree (#1157).
fn merge_global_and_project_instructions(
global: &str,
global_source: Option<&Path>,
@@ -514,11 +516,15 @@ fn load_global_agents_context(workspace: &Path, home_dir: Option<&Path>) -> Opti
// Priority order:
// 1. ~/.codewhale/WHALE.md (CodeWhale-native)
// 2. ~/.codewhale/AGENTS.md (new config directory)
// 3. ~/.deepseek/WHALE.md (legacy fallback)
// 4. ~/.deepseek/AGENTS.md (legacy fallback)
// 3. ~/.agents/WHALE.md (vendor-neutral fallback)
// 4. ~/.agents/AGENTS.md (vendor-neutral fallback)
// 5. ~/.deepseek/WHALE.md (legacy fallback)
// 6. ~/.deepseek/AGENTS.md (legacy fallback)
let candidates: &[&[&str]] = &[
GLOBAL_WHALE_RELATIVE_PATH,
GLOBAL_AGENTS_RELATIVE_PATH,
GLOBAL_WHALE_VENDOR_NEUTRAL_PATH,
GLOBAL_AGENTS_VENDOR_NEUTRAL_PATH,
GLOBAL_WHALE_LEGACY_PATH,
GLOBAL_AGENTS_LEGACY_PATH,
];
@@ -1044,6 +1050,58 @@ mod tests {
assert_eq!(ctx.source_path, Some(global_agents));
}
#[test]
fn test_load_global_agents_falls_back_to_vendor_neutral_path() {
let workspace = tempdir().expect("workspace tempdir");
let home = tempdir().expect("home tempdir");
let global_dir = home.path().join(".agents");
fs::create_dir(&global_dir).expect("mkdir .agents");
let global_agents = global_dir.join("AGENTS.md");
fs::write(&global_agents, "Vendor-neutral instructions").expect("write global agents");
let ctx = load_project_context_with_parents_and_home(workspace.path(), Some(home.path()));
assert!(ctx.has_instructions());
assert!(
ctx.instructions
.as_ref()
.unwrap()
.contains("Vendor-neutral instructions")
);
assert_eq!(ctx.source_path, Some(global_agents));
}
#[test]
fn test_codewhale_specific_path_wins_over_agents_path() {
let workspace = tempdir().expect("workspace tempdir");
let home = tempdir().expect("home tempdir");
let codewhale_dir = home.path().join(".codewhale");
fs::create_dir(&codewhale_dir).expect("mkdir .codewhale");
let codewhale_agents = codewhale_dir.join("AGENTS.md");
fs::write(&codewhale_agents, "CodeWhale-specific instructions")
.expect("write codewhale agents");
let agents_dir = home.path().join(".agents");
fs::create_dir(&agents_dir).expect("mkdir .agents");
fs::write(agents_dir.join("AGENTS.md"), "Vendor-neutral instructions")
.expect("write vendor-neutral agents");
let ctx = load_project_context_with_parents_and_home(workspace.path(), Some(home.path()));
assert!(ctx.has_instructions());
let instructions = ctx.instructions.as_ref().unwrap();
assert!(
instructions.contains("CodeWhale-specific instructions"),
"CodeWhale-specific global file should win:\n{instructions}"
);
assert!(
!instructions.contains("Vendor-neutral instructions"),
"lower-priority .agents file should be skipped:\n{instructions}"
);
assert_eq!(ctx.source_path, Some(codewhale_agents));
}
#[test]
fn test_local_and_global_agents_merge_when_both_exist() {
// #1157: when both `~/.deepseek/AGENTS.md` and a project AGENTS.md