Skip to main content

kiss_coding/
context_files.rs

1//! Project context file discovery: AGENTS.md / CLAUDE.md, with
2//! AGENTS.override.md taking precedence per directory. Load order: global
3//! file, then ancestor directories root->cwd, then cwd.
4
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, PartialEq)]
8pub struct ContextFile {
9    pub path: PathBuf,
10    pub content: String,
11}
12
13fn pick_context_file(dir: &Path) -> Option<PathBuf> {
14    for name in ["AGENTS.override.md", "AGENTS.md", "CLAUDE.md"] {
15        let candidate = dir.join(name);
16        if candidate.is_file() {
17            return Some(candidate);
18        }
19    }
20    None
21}
22
23pub fn discover(cwd: &Path) -> Vec<ContextFile> {
24    let mut paths: Vec<PathBuf> = Vec::new();
25
26    if let Some(home) = dirs::home_dir() {
27        let global = home.join(".kiss/agent/AGENTS.md");
28        if global.is_file() {
29            paths.push(global);
30        }
31    }
32
33    // Ancestors from root down to cwd (cwd last so it wins by proximity).
34    let mut chain: Vec<&Path> = cwd.ancestors().collect();
35    chain.reverse();
36    for dir in chain {
37        if let Some(file) = pick_context_file(dir) {
38            paths.push(file);
39        }
40    }
41
42    paths.dedup();
43    paths
44        .into_iter()
45        .filter_map(|path| {
46            let content = std::fs::read_to_string(&path).ok()?;
47            Some(ContextFile { path, content })
48        })
49        .collect()
50}
51
52/// System-prompt replacement/append files (project wins over global).
53pub struct SystemPromptFiles {
54    pub replace: Option<String>,
55    pub append: Option<String>,
56}
57
58pub fn system_prompt_files(cwd: &Path) -> SystemPromptFiles {
59    let read = |p: PathBuf| std::fs::read_to_string(p).ok();
60    let global = dirs::home_dir().map(|h| h.join(".kiss/agent"));
61    let project = cwd.join(".kiss");
62
63    let replace = read(project.join("SYSTEM.md"))
64        .or_else(|| global.as_ref().and_then(|g| read(g.join("SYSTEM.md"))));
65    let append = read(project.join("APPEND_SYSTEM.md")).or_else(|| {
66        global
67            .as_ref()
68            .and_then(|g| read(g.join("APPEND_SYSTEM.md")))
69    });
70    SystemPromptFiles { replace, append }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn override_beats_agents_beats_claude() {
79        let dir = tempfile::tempdir().unwrap();
80        std::fs::write(dir.path().join("CLAUDE.md"), "claude").unwrap();
81        assert!(
82            pick_context_file(dir.path())
83                .unwrap()
84                .ends_with("CLAUDE.md")
85        );
86        std::fs::write(dir.path().join("AGENTS.md"), "agents").unwrap();
87        assert!(
88            pick_context_file(dir.path())
89                .unwrap()
90                .ends_with("AGENTS.md")
91        );
92        std::fs::write(dir.path().join("AGENTS.override.md"), "override").unwrap();
93        assert!(
94            pick_context_file(dir.path())
95                .unwrap()
96                .ends_with("AGENTS.override.md")
97        );
98    }
99
100    #[test]
101    fn ancestors_ordered_root_to_cwd() {
102        let dir = tempfile::tempdir().unwrap();
103        let nested = dir.path().join("a/b");
104        std::fs::create_dir_all(&nested).unwrap();
105        std::fs::write(dir.path().join("AGENTS.md"), "root rules").unwrap();
106        std::fs::write(nested.join("AGENTS.md"), "leaf rules").unwrap();
107        let files = discover(&nested);
108        let contents: Vec<&str> = files.iter().map(|f| f.content.as_str()).collect();
109        let root_pos = contents.iter().position(|c| *c == "root rules").unwrap();
110        let leaf_pos = contents.iter().position(|c| *c == "leaf rules").unwrap();
111        assert!(root_pos < leaf_pos);
112    }
113}