use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub(crate) fn gather(cwd: &Path) -> Option<String> {
let mut seen = HashSet::new();
let mut sections = Vec::new();
for path in instruction_files(cwd) {
if !seen.insert(path.clone()) {
continue;
}
if let Ok(content) = std::fs::read_to_string(&path) {
let trimmed = content.trim();
if !trimmed.is_empty() {
sections.push(trimmed.to_owned());
}
}
}
if sections.is_empty() {
None
} else {
Some(sections.join("\n\n"))
}
}
fn instruction_files(cwd: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
files.push(home.join(".claude/CLAUDE.md"));
files.push(home.join(".claude/AGENTS.md"));
}
for dir in project_dirs(cwd) {
files.push(dir.join("AGENTS.md"));
files.push(dir.join("CLAUDE.md"));
}
files.into_iter().filter(|p| p.is_file()).collect()
}
fn project_dirs(cwd: &Path) -> Vec<PathBuf> {
let mut chain = Vec::new();
let mut cur = cwd;
loop {
chain.push(cur.to_path_buf());
if cur.join(".git").exists() {
chain.reverse(); return chain;
}
match cur.parent() {
Some(parent) => cur = parent,
None => return vec![cwd.to_path_buf()], }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("hl-instr-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn gathers_project_files_root_to_cwd() {
let root = scratch("proj");
std::fs::create_dir_all(root.join(".git")).unwrap();
std::fs::write(root.join("AGENTS.md"), "root rules").unwrap();
let sub = root.join("crate-a");
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(sub.join("CLAUDE.md"), "crate rules").unwrap();
let text = gather(&sub).expect("found instructions");
assert!(text.contains("root rules"));
assert!(text.contains("crate rules"));
assert!(text.find("root rules") < text.find("crate rules"));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn none_when_absent() {
let dir = scratch("empty");
std::fs::create_dir_all(dir.join(".git")).unwrap();
let text = gather(&dir);
assert!(text.as_deref().map_or(true, |t| !t.contains("crate rules")));
let _ = std::fs::remove_dir_all(&dir);
}
}