diff --git a/crates/goose/src/config/paths.rs b/crates/goose/src/config/paths.rs index be142869be..4a79cbf306 100644 --- a/crates/goose/src/config/paths.rs +++ b/crates/goose/src/config/paths.rs @@ -13,6 +13,7 @@ impl Paths { DirType::State => base.join("state"), DirType::Plugins => base.join(".agents").join("plugins"), DirType::Agents => base.join(".agents").join("agents"), + DirType::AgentsHome => base.join(".agents"), } } else { // NOTE: "Block" is kept here for backwards compatibility with existing @@ -31,6 +32,7 @@ impl Paths { DirType::State => strategy.state_dir().unwrap_or(strategy.data_dir()), DirType::Plugins => strategy.home_dir().join(".agents").join("plugins"), DirType::Agents => strategy.home_dir().join(".agents").join("agents"), + DirType::AgentsHome => strategy.home_dir().join(".agents"), } } } @@ -55,6 +57,14 @@ impl Paths { Self::get_dir(DirType::Agents) } + pub fn agents_home_dir() -> PathBuf { + Self::get_dir(DirType::AgentsHome) + } + + pub fn in_agents_home_dir(subpath: &str) -> PathBuf { + Self::agents_home_dir().join(subpath) + } + pub fn in_state_dir(subpath: &str) -> PathBuf { Self::state_dir().join(subpath) } @@ -74,4 +84,5 @@ enum DirType { State, Plugins, Agents, + AgentsHome, } diff --git a/crates/goose/src/hints/load_hints.rs b/crates/goose/src/hints/load_hints.rs index 4050b530bd..6cf553cbe3 100644 --- a/crates/goose/src/hints/load_hints.rs +++ b/crates/goose/src/hints/load_hints.rs @@ -230,17 +230,30 @@ pub fn load_hint_files( let mut global_hints_contents = Vec::with_capacity(hints_filenames.len()); let mut local_hints_contents = Vec::with_capacity(hints_filenames.len()); - for hints_filename in hints_filenames { - let global_hints_path = Paths::in_config_dir(hints_filename); + let mut global_hints_paths: Vec = hints_filenames + .iter() + .map(|name| Paths::in_config_dir(name)) + .collect(); + if hints_filenames + .iter() + .any(|name| name == AGENTS_MD_FILENAME) + { + global_hints_paths.push(Paths::in_agents_home_dir(AGENTS_MD_FILENAME)); + } + + for global_hints_path in &global_hints_paths { if global_hints_path.is_file() { let mut visited = HashSet::new(); let hints_dir = global_hints_path.parent().unwrap(); + let global_ignore_patterns = GitignoreBuilder::new(hints_dir) + .build() + .unwrap_or_else(|_| Gitignore::empty()); let expanded_content = read_referenced_files( - &global_hints_path, + global_hints_path, hints_dir, &mut visited, 0, - ignore_patterns, + &global_ignore_patterns, ); if !expanded_content.is_empty() { global_hints_contents.push(expanded_content); @@ -314,6 +327,98 @@ mod tests { assert!(hints.contains("Test hint content")); } + #[test] + #[serial_test::serial] + fn test_global_agents_md_in_agents_home() { + let root = TempDir::new().unwrap(); + std::env::set_var("GOOSE_PATH_ROOT", root.path()); + + let agents_home = root.path().join(".agents"); + fs::create_dir_all(&agents_home).unwrap(); + fs::write( + agents_home.join(AGENTS_MD_FILENAME), + "Global agents home instructions", + ) + .unwrap(); + + let project = TempDir::new().unwrap(); + let gitignore = create_dummy_gitignore(); + let hints = load_hint_files( + project.path(), + &[ + GOOSE_HINTS_FILENAME.to_string(), + AGENTS_MD_FILENAME.to_string(), + ], + &gitignore, + ); + + std::env::remove_var("GOOSE_PATH_ROOT"); + + assert!(hints.contains("Global Hints")); + assert!(hints.contains("Global agents home instructions")); + } + + #[test] + #[serial_test::serial] + fn test_global_agents_md_imports_not_filtered_by_project_gitignore() { + let root = TempDir::new().unwrap(); + std::env::set_var("GOOSE_PATH_ROOT", root.path()); + + let agents_home = root.path().join(".agents"); + fs::create_dir_all(&agents_home).unwrap(); + fs::write(agents_home.join("policy.md"), "Imported policy content").unwrap(); + fs::write( + agents_home.join(AGENTS_MD_FILENAME), + "Global header\n@policy.md\n", + ) + .unwrap(); + + let project = TempDir::new().unwrap(); + let mut builder = GitignoreBuilder::new(project.path()); + builder.add_line(None, "*.md").unwrap(); + let gitignore = builder.build().unwrap(); + + let hints = load_hint_files( + project.path(), + &[ + GOOSE_HINTS_FILENAME.to_string(), + AGENTS_MD_FILENAME.to_string(), + ], + &gitignore, + ); + + std::env::remove_var("GOOSE_PATH_ROOT"); + + assert!(hints.contains("Imported policy content")); + } + + #[test] + #[serial_test::serial] + fn test_global_agents_md_skipped_when_not_in_context_file_names() { + let root = TempDir::new().unwrap(); + std::env::set_var("GOOSE_PATH_ROOT", root.path()); + + let agents_home = root.path().join(".agents"); + fs::create_dir_all(&agents_home).unwrap(); + fs::write( + agents_home.join(AGENTS_MD_FILENAME), + "Global agents home instructions", + ) + .unwrap(); + + let project = TempDir::new().unwrap(); + let gitignore = create_dummy_gitignore(); + let hints = load_hint_files( + project.path(), + &[GOOSE_HINTS_FILENAME.to_string()], + &gitignore, + ); + + std::env::remove_var("GOOSE_PATH_ROOT"); + + assert!(!hints.contains("Global agents home instructions")); + } + #[test] fn test_goosehints_when_missing() { let dir = TempDir::new().unwrap();