rab/agent/
context_files.rs1use std::fs;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone)]
14pub struct ContextFile {
15 pub path: PathBuf,
16 pub content: String,
17}
18
19const CANDIDATES: &[&str] = &["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
21
22fn load_context_file_from_dir(dir: &Path) -> Option<ContextFile> {
24 for filename in CANDIDATES {
25 let file_path = dir.join(filename);
26 if file_path.exists() {
27 match fs::read_to_string(&file_path) {
28 Ok(content) => {
29 return Some(ContextFile {
30 path: fs::canonicalize(&file_path).unwrap_or(file_path),
31 content,
32 });
33 }
34 Err(_) => {
35 continue;
37 }
38 }
39 }
40 }
41 None
42}
43
44pub fn load_context_files(cwd: &Path, agent_dir: &Path) -> Vec<ContextFile> {
50 let resolved_cwd = if cwd.is_absolute() {
51 cwd.to_path_buf()
52 } else {
53 match fs::canonicalize(cwd) {
54 Ok(p) => p,
55 Err(_) => cwd.to_path_buf(),
56 }
57 };
58 let resolved_agent = if agent_dir.is_absolute() {
59 agent_dir.to_path_buf()
60 } else {
61 match fs::canonicalize(agent_dir) {
62 Ok(p) => p,
63 Err(_) => agent_dir.to_path_buf(),
64 }
65 };
66
67 let mut context_files: Vec<ContextFile> = Vec::new();
68 let mut seen_paths = std::collections::HashSet::new();
69
70 if let Some(cf) = load_context_file_from_dir(&resolved_agent) {
72 let canon = cf.path.clone();
73 if seen_paths.insert(canon) {
74 context_files.push(cf);
75 }
76 }
77
78 let root = Path::new("/");
80 let mut current = Some(resolved_cwd.as_path());
81
82 let mut ancestors: Vec<&Path> = Vec::new();
84 while let Some(dir) = current {
85 ancestors.push(dir);
86 if dir == root {
87 break;
88 }
89 let parent = dir.parent().unwrap_or(root);
90 if parent == dir {
91 break;
92 }
93 current = Some(parent);
94 }
95
96 for dir in ancestors.into_iter().rev() {
99 if let Some(cf) = load_context_file_from_dir(dir) {
100 let canon = cf.path.clone();
101 if seen_paths.insert(canon) {
102 context_files.push(cf);
103 }
104 }
105 }
106
107 context_files
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use std::fs;
114 use tempfile::TempDir;
115
116 fn create_file(dir: &Path, name: &str, content: &str) -> PathBuf {
117 let path = dir.join(name);
118 fs::write(&path, content).unwrap();
119 path
120 }
121
122 #[test]
123 fn test_load_from_agent_dir() {
124 let tmp = TempDir::new().unwrap();
125 let agent_dir = tmp.path().join("agent");
126 fs::create_dir_all(&agent_dir).unwrap();
127 create_file(&agent_dir, "AGENTS.md", "# Agent rules\n- be careful");
128
129 let cwd = tmp.path().join("project");
130 fs::create_dir_all(&cwd).unwrap();
131
132 let files = load_context_files(&cwd, &agent_dir);
133 assert_eq!(files.len(), 1);
134 assert!(files[0].content.contains("Agent rules"));
135 }
136
137 #[test]
138 fn test_load_from_cwd_preferred() {
139 let tmp = TempDir::new().unwrap();
140 let agent_dir = tmp.path().join("agent");
141 fs::create_dir_all(&agent_dir).unwrap();
142
143 let project = tmp.path().join("project");
144 fs::create_dir_all(&project).unwrap();
145 create_file(&project, "AGENTS.md", "# Project rules");
146
147 let files = load_context_files(&project, &agent_dir);
148 assert_eq!(files.len(), 1);
150 assert!(files[0].content.contains("Project rules"));
151 }
152
153 #[test]
154 fn test_both_global_and_project() {
155 let tmp = TempDir::new().unwrap();
156 let agent_dir = tmp.path().join("agent");
157 fs::create_dir_all(&agent_dir).unwrap();
158 create_file(&agent_dir, "AGENTS.md", "# Global rules");
159
160 let project = tmp.path().join("project");
161 fs::create_dir_all(&project).unwrap();
162 create_file(&project, "AGENTS.md", "# Project rules");
163
164 let files = load_context_files(&project, &agent_dir);
165 assert_eq!(files.len(), 2);
166 assert!(files[0].content.contains("Global rules"));
167 assert!(files[1].content.contains("Project rules"));
168 }
169
170 #[test]
171 fn test_claude_md_alternative() {
172 let tmp = TempDir::new().unwrap();
173 let agent_dir = tmp.path().join("agent");
174 fs::create_dir_all(&agent_dir).unwrap();
175
176 let project = tmp.path().join("project");
177 fs::create_dir_all(&project).unwrap();
178 create_file(&project, "CLAUDE.md", "# Claude instructions");
179
180 let files = load_context_files(&project, &agent_dir);
181 assert_eq!(files.len(), 1);
182 assert!(files[0].content.contains("Claude instructions"));
183 }
184
185 #[test]
186 fn test_agents_md_preferred_over_claude_md() {
187 let tmp = TempDir::new().unwrap();
188 let project = tmp.path().join("project");
189 fs::create_dir_all(&project).unwrap();
190 create_file(&project, "AGENTS.md", "# Agents first");
191 create_file(&project, "CLAUDE.md", "# Claude second");
192
193 let agent_dir = tmp.path().join("agent");
194 fs::create_dir_all(&agent_dir).unwrap();
195
196 let files = load_context_files(&project, &agent_dir);
197 assert_eq!(files.len(), 1);
199 assert!(files[0].content.contains("Agents first"));
200 }
201
202 #[test]
203 fn test_deduplicate_by_path() {
204 let tmp = TempDir::new().unwrap();
205 let agent_dir = tmp.path().join("agent");
206 fs::create_dir_all(&agent_dir).unwrap();
207
208 create_file(&agent_dir, "AGENTS.md", "# Shared file");
210
211 let files = load_context_files(&agent_dir, &agent_dir);
212 assert_eq!(files.len(), 1);
214 }
215
216 #[test]
217 fn test_no_context_files_returns_empty() {
218 let tmp = TempDir::new().unwrap();
219 let agent_dir = tmp.path().join("agent");
220 fs::create_dir_all(&agent_dir).unwrap();
221
222 let project = tmp.path().join("project");
223 fs::create_dir_all(&project).unwrap();
224
225 let files = load_context_files(&project, &agent_dir);
226 assert!(files.is_empty());
227 }
228
229 #[test]
230 fn test_ancestor_directories() {
231 let tmp = TempDir::new().unwrap();
232 let agent_dir = tmp.path().join("agent");
233 fs::create_dir_all(&agent_dir).unwrap();
234
235 let parent = tmp.path().join("parent");
237 fs::create_dir_all(&parent).unwrap();
238 create_file(&parent, "AGENTS.md", "# Parent rules");
239
240 let child = parent.join("child");
241 fs::create_dir_all(&child).unwrap();
242 create_file(&child, "AGENTS.md", "# Child rules");
243
244 let files = load_context_files(&child, &agent_dir);
245 assert_eq!(files.len(), 2);
246 assert!(files[0].content.contains("Parent rules"));
248 assert!(files[1].content.contains("Child rules"));
249 }
250
251 #[test]
252 fn test_ignores_non_context_files() {
253 let tmp = TempDir::new().unwrap();
254 let agent_dir = tmp.path().join("agent");
255 fs::create_dir_all(&agent_dir).unwrap();
256
257 let project = tmp.path().join("project");
258 fs::create_dir_all(&project).unwrap();
259 create_file(&project, "README.md", "# Not a context file");
260
261 let files = load_context_files(&project, &agent_dir);
262 assert!(files.is_empty());
263 }
264}