use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ContextFile {
pub path: PathBuf,
pub content: String,
}
const CANDIDATES: &[&str] = &["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
fn load_context_file_from_dir(dir: &Path) -> Option<ContextFile> {
for filename in CANDIDATES {
let file_path = dir.join(filename);
if file_path.exists() {
match fs::read_to_string(&file_path) {
Ok(content) => {
return Some(ContextFile {
path: fs::canonicalize(&file_path).unwrap_or(file_path),
content,
});
}
Err(_) => {
continue;
}
}
}
}
None
}
pub fn load_context_files(cwd: &Path, agent_dir: &Path) -> Vec<ContextFile> {
let resolved_cwd = if cwd.is_absolute() {
cwd.to_path_buf()
} else {
match fs::canonicalize(cwd) {
Ok(p) => p,
Err(_) => cwd.to_path_buf(),
}
};
let resolved_agent = if agent_dir.is_absolute() {
agent_dir.to_path_buf()
} else {
match fs::canonicalize(agent_dir) {
Ok(p) => p,
Err(_) => agent_dir.to_path_buf(),
}
};
let mut context_files: Vec<ContextFile> = Vec::new();
let mut seen_paths = std::collections::HashSet::new();
if let Some(cf) = load_context_file_from_dir(&resolved_agent) {
let canon = cf.path.clone();
if seen_paths.insert(canon) {
context_files.push(cf);
}
}
let root = Path::new("/");
let mut current = Some(resolved_cwd.as_path());
let mut ancestors: Vec<&Path> = Vec::new();
while let Some(dir) = current {
ancestors.push(dir);
if dir == root {
break;
}
let parent = dir.parent().unwrap_or(root);
if parent == dir {
break;
}
current = Some(parent);
}
for dir in ancestors.into_iter().rev() {
if let Some(cf) = load_context_file_from_dir(dir) {
let canon = cf.path.clone();
if seen_paths.insert(canon) {
context_files.push(cf);
}
}
}
context_files
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_file(dir: &Path, name: &str, content: &str) -> PathBuf {
let path = dir.join(name);
fs::write(&path, content).unwrap();
path
}
#[test]
fn test_load_from_agent_dir() {
let tmp = TempDir::new().unwrap();
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
create_file(&agent_dir, "AGENTS.md", "# Agent rules\n- be careful");
let cwd = tmp.path().join("project");
fs::create_dir_all(&cwd).unwrap();
let files = load_context_files(&cwd, &agent_dir);
assert_eq!(files.len(), 1);
assert!(files[0].content.contains("Agent rules"));
}
#[test]
fn test_load_from_cwd_preferred() {
let tmp = TempDir::new().unwrap();
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
let project = tmp.path().join("project");
fs::create_dir_all(&project).unwrap();
create_file(&project, "AGENTS.md", "# Project rules");
let files = load_context_files(&project, &agent_dir);
assert_eq!(files.len(), 1);
assert!(files[0].content.contains("Project rules"));
}
#[test]
fn test_both_global_and_project() {
let tmp = TempDir::new().unwrap();
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
create_file(&agent_dir, "AGENTS.md", "# Global rules");
let project = tmp.path().join("project");
fs::create_dir_all(&project).unwrap();
create_file(&project, "AGENTS.md", "# Project rules");
let files = load_context_files(&project, &agent_dir);
assert_eq!(files.len(), 2);
assert!(files[0].content.contains("Global rules"));
assert!(files[1].content.contains("Project rules"));
}
#[test]
fn test_claude_md_alternative() {
let tmp = TempDir::new().unwrap();
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
let project = tmp.path().join("project");
fs::create_dir_all(&project).unwrap();
create_file(&project, "CLAUDE.md", "# Claude instructions");
let files = load_context_files(&project, &agent_dir);
assert_eq!(files.len(), 1);
assert!(files[0].content.contains("Claude instructions"));
}
#[test]
fn test_agents_md_preferred_over_claude_md() {
let tmp = TempDir::new().unwrap();
let project = tmp.path().join("project");
fs::create_dir_all(&project).unwrap();
create_file(&project, "AGENTS.md", "# Agents first");
create_file(&project, "CLAUDE.md", "# Claude second");
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
let files = load_context_files(&project, &agent_dir);
assert_eq!(files.len(), 1);
assert!(files[0].content.contains("Agents first"));
}
#[test]
fn test_deduplicate_by_path() {
let tmp = TempDir::new().unwrap();
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
create_file(&agent_dir, "AGENTS.md", "# Shared file");
let files = load_context_files(&agent_dir, &agent_dir);
assert_eq!(files.len(), 1);
}
#[test]
fn test_no_context_files_returns_empty() {
let tmp = TempDir::new().unwrap();
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
let project = tmp.path().join("project");
fs::create_dir_all(&project).unwrap();
let files = load_context_files(&project, &agent_dir);
assert!(files.is_empty());
}
#[test]
fn test_ancestor_directories() {
let tmp = TempDir::new().unwrap();
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
let parent = tmp.path().join("parent");
fs::create_dir_all(&parent).unwrap();
create_file(&parent, "AGENTS.md", "# Parent rules");
let child = parent.join("child");
fs::create_dir_all(&child).unwrap();
create_file(&child, "AGENTS.md", "# Child rules");
let files = load_context_files(&child, &agent_dir);
assert_eq!(files.len(), 2);
assert!(files[0].content.contains("Parent rules"));
assert!(files[1].content.contains("Child rules"));
}
#[test]
fn test_ignores_non_context_files() {
let tmp = TempDir::new().unwrap();
let agent_dir = tmp.path().join("agent");
fs::create_dir_all(&agent_dir).unwrap();
let project = tmp.path().join("project");
fs::create_dir_all(&project).unwrap();
create_file(&project, "README.md", "# Not a context file");
let files = load_context_files(&project, &agent_dir);
assert!(files.is_empty());
}
}