//! Project-local agent prompt layer. //! //! Reads `.huskies/AGENT.md` from the project root and appends its content to //! the baked-in agent prompt at spawn time. This lets projects record //! non-obvious facts (directory conventions, known traps, etc.) that every //! agent should know without modifying the shared agent configuration. //! //! Behaviour contract: //! - If the file is missing or empty the caller receives `None`; agents spawn //! normally with no warnings or errors. //! - If the file exists and is non-empty, the content is returned and an //! INFO-level log line is emitted with the file path and byte count. //! - The file is read fresh on every agent spawn — no caching. use std::path::Path; /// Attempt to load the project-local agent prompt from `.huskies/AGENT.md`. /// /// Returns `Some(content)` when the file exists and is non-empty, or `None` /// when the file is absent or empty. Never returns an error; any I/O problem /// is silently treated as "no local prompt". pub fn read_project_local_prompt(project_root: &Path) -> Option { let path = project_root.join(".huskies/AGENT.md"); let content = std::fs::read_to_string(&path).ok()?; let trimmed = content.trim(); if trimmed.is_empty() { return None; } crate::slog!( "[agents] project-local prompt loaded: {} ({} bytes)", path.display(), trimmed.len() ); Some(trimmed.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn returns_none_when_file_absent() { let tmp = tempfile::tempdir().unwrap(); let result = read_project_local_prompt(tmp.path()); assert!(result.is_none(), "missing file must return None"); } #[test] fn returns_none_when_file_empty() { let tmp = tempfile::tempdir().unwrap(); let huskies_dir = tmp.path().join(".huskies"); std::fs::create_dir_all(&huskies_dir).unwrap(); std::fs::write(huskies_dir.join("AGENT.md"), "").unwrap(); let result = read_project_local_prompt(tmp.path()); assert!(result.is_none(), "empty file must return None"); } #[test] fn returns_none_when_file_whitespace_only() { let tmp = tempfile::tempdir().unwrap(); let huskies_dir = tmp.path().join(".huskies"); std::fs::create_dir_all(&huskies_dir).unwrap(); std::fs::write(huskies_dir.join("AGENT.md"), " \n\n ").unwrap(); let result = read_project_local_prompt(tmp.path()); assert!(result.is_none(), "whitespace-only file must return None"); } #[test] fn returns_content_when_file_non_empty() { let tmp = tempfile::tempdir().unwrap(); let huskies_dir = tmp.path().join(".huskies"); std::fs::create_dir_all(&huskies_dir).unwrap(); let marker = "DISTINCTIVE_MARKER_XYZ42"; std::fs::write(huskies_dir.join("AGENT.md"), format!("# Hints\n{marker}\n")).unwrap(); let result = read_project_local_prompt(tmp.path()); assert!(result.is_some(), "non-empty file must return Some"); let content = result.unwrap(); assert!( content.contains(marker), "returned content must include the marker: {content}" ); } #[test] fn appended_to_prompt_integration() { // Simulates the start.rs usage: marker appears in the constructed // system prompt when the file is present, absent when it is not. let tmp_with = tempfile::tempdir().unwrap(); let huskies_dir = tmp_with.path().join(".huskies"); std::fs::create_dir_all(&huskies_dir).unwrap(); let marker = "INTEGRATION_MARKER_601"; std::fs::write(huskies_dir.join("AGENT.md"), marker).unwrap(); let base_prompt = "You are a coder agent.".to_string(); let local = read_project_local_prompt(tmp_with.path()); let effective = match local { Some(ref extra) => format!("{base_prompt}\n\n{extra}"), None => base_prompt.clone(), }; assert!( effective.contains(marker), "marker must appear in effective prompt when file present: {effective}" ); // Without the file let tmp_without = tempfile::tempdir().unwrap(); let local2 = read_project_local_prompt(tmp_without.path()); assert!(local2.is_none(), "no marker when file absent"); let effective2 = match local2 { Some(ref extra) => format!("{base_prompt}\n\n{extra}"), None => base_prompt.clone(), }; assert!( !effective2.contains(marker), "marker must NOT appear in effective prompt when file absent: {effective2}" ); } }