diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index f77749c07a..b559a935b2 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -461,40 +461,57 @@ impl DeveloperRouter { }, }; - // choose_app_strategy().config_dir() - // - macOS/Linux: ~/.config/goose/ - // - Windows: ~\AppData\Roaming\Block\goose\config\ - // keep previous behavior of expanding ~/.config in case this fails - let global_hints_path = choose_app_strategy(crate::APP_STRATEGY.clone()) - .map(|strategy| strategy.in_config_dir(".goosehints")) - .unwrap_or_else(|_| { - PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints").to_string()) - }); + let hints_filenames: Vec = std::env::var("CONTEXT_FILE_NAMES") + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_else(|| vec![".goosehints".to_string()]); - // Create the directory if it doesn't exist - let _ = std::fs::create_dir_all(global_hints_path.parent().unwrap()); + let mut global_hints_contents = Vec::with_capacity(hints_filenames.len()); + let mut local_hints_contents = Vec::with_capacity(hints_filenames.len()); - // Check for local hints in current directory - let local_hints_path = cwd.join(".goosehints"); + for hints_filename in &hints_filenames { + // Global hints + // choose_app_strategy().config_dir() + // - macOS/Linux: ~/.config/goose/ + // - Windows: ~\AppData\Roaming\Block\goose\config\ + // keep previous behavior of expanding ~/.config in case this fails + let global_hints_path = choose_app_strategy(crate::APP_STRATEGY.clone()) + .map(|strategy| strategy.in_config_dir(hints_filename)) + .unwrap_or_else(|_| { + let path_str = format!("~/.config/goose/{}", hints_filename); + PathBuf::from(shellexpand::tilde(&path_str).to_string()) + }); - // Read global hints if they exist - let mut hints = String::new(); - if global_hints_path.is_file() { - if let Ok(global_hints) = std::fs::read_to_string(&global_hints_path) { - hints.push_str("\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\n"); - hints.push_str(&global_hints); + if let Some(parent) = global_hints_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + + if global_hints_path.is_file() { + if let Ok(content) = std::fs::read_to_string(&global_hints_path) { + global_hints_contents.push(content); + } + } + + let local_hints_path = cwd.join(hints_filename); + if local_hints_path.is_file() { + if let Ok(content) = std::fs::read_to_string(&local_hints_path) { + local_hints_contents.push(content); + } } } - // Read local hints if they exist - if local_hints_path.is_file() { - if let Ok(local_hints) = std::fs::read_to_string(&local_hints_path) { - if !hints.is_empty() { - hints.push_str("\n\n"); - } - hints.push_str("### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n"); - hints.push_str(&local_hints); + let mut hints = String::new(); + if !global_hints_contents.is_empty() { + hints.push_str("\n### Global Hints\nThe developer extension includes some global hints that apply to all projects & directories.\n"); + hints.push_str(&global_hints_contents.join("\n")); + } + + if !local_hints_contents.is_empty() { + if !hints.is_empty() { + hints.push_str("\n\n"); } + hints.push_str("### Project Hints\nThe developer extension includes some hints for working on the project in this directory.\n"); + hints.push_str(&local_hints_contents.join("\n")); } // Return base instructions directly when no hints are found @@ -1749,6 +1766,39 @@ mod tests { temp_dir.close().unwrap(); } + #[test] + #[serial] + fn test_goosehints_multiple_filenames() { + let dir = TempDir::new().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + std::env::set_var("CONTEXT_FILE_NAMES", r#"["CLAUDE.md", ".goosehints"]"#); + + fs::write("CLAUDE.md", "Custom hints file content from CLAUDE.md").unwrap(); + fs::write(".goosehints", "Custom hints file content from .goosehints").unwrap(); + let router = DeveloperRouter::new(); + let instructions = router.instructions(); + + assert!(instructions.contains("Custom hints file content from CLAUDE.md")); + assert!(instructions.contains("Custom hints file content from .goosehints")); + std::env::remove_var("CONTEXT_FILE_NAMES"); + } + + #[test] + #[serial] + fn test_goosehints_configurable_filename() { + let dir = TempDir::new().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + std::env::set_var("CONTEXT_FILE_NAMES", r#"["CLAUDE.md"]"#); + + fs::write("CLAUDE.md", "Custom hints file content").unwrap(); + let router = DeveloperRouter::new(); + let instructions = router.instructions(); + + assert!(instructions.contains("Custom hints file content")); + assert!(!instructions.contains(".goosehints")); // Make sure it's not loading the default + std::env::remove_var("CONTEXT_FILE_NAMES"); + } + #[tokio::test] #[serial] #[cfg(windows)] diff --git a/documentation/docs/guides/tips.md b/documentation/docs/guides/tips.md index cc225e7c14..9232f48471 100644 --- a/documentation/docs/guides/tips.md +++ b/documentation/docs/guides/tips.md @@ -27,7 +27,7 @@ LLMs have context windows, which are limits on how much conversation history the Turning on too many extensions can degrade performance. Enable only essential [extensions and tools](/docs/guides/managing-tools/tool-permissions) to improve tool selection accuracy, save context window space, and stay within provider tool limits. ### Teach Goose your preferences -Help Goose remember how you like to work by using [`.goosehints`](/docs/guides/using-goosehints/) for permanent project preferences and the [Memory extension](/docs/mcp/memory-mcp) for things you want Goose to dynamically recall later. Both can help save valuable context window space while keeping your preferences available. +Help Goose remember how you like to work by using [`.goosehints` or other context files](/docs/guides/using-goosehints/) for permanent project preferences and the [Memory extension](/docs/mcp/memory-mcp) for things you want Goose to dynamically recall later. Both can help save valuable context window space while keeping your preferences available. ### Protect sensitive files Goose is often eager to make changes. You can stop it from changing specific files by creating a [.gooseignore](/docs/guides/using-gooseignore) file. In this file, you can list all the file paths you want it to avoid. @@ -45,4 +45,4 @@ You can turn a successful session into a reusable "[recipe](/docs/guides/recipes You don’t need to get it right the first time. Iterating on prompts and tools is part of the workflow. ### Keep Goose updated -Regularly [update](/docs/guides/updating-goose) Goose to benefit from the latest features, bug fixes, and performance improvements. \ No newline at end of file +Regularly [update](/docs/guides/updating-goose) Goose to benefit from the latest features, bug fixes, and performance improvements. diff --git a/documentation/docs/guides/using-goosehints.md b/documentation/docs/guides/using-goosehints.md index c85a69a4dc..75b2876ace 100644 --- a/documentation/docs/guides/using-goosehints.md +++ b/documentation/docs/guides/using-goosehints.md @@ -39,6 +39,10 @@ Goose supports two types of hint files: You can use both global and local hints at the same time. When both exist, Goose will consider both your global preferences and project-specific requirements. If the instructions in your local hints file conflict with your global preferences, Goose will prioritize the local hints. ::: +:::tip +You can name your context files differently -- e.g. `AGENTS.md` -- and Goose can still pick them up. Configure the `CONTEXT_FILE_NAMES` setting! +::: +