diff --git a/Cargo.lock b/Cargo.lock index 02008bf051..0e41d18a7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2597,9 +2597,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -2632,6 +2632,7 @@ dependencies = [ "etcetera", "fs2", "futures", + "ignore", "include_dir", "indexmap 2.12.0", "indoc", @@ -3415,9 +3416,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", diff --git a/crates/goose-mcp/src/developer/goose_hints/mod.rs b/crates/goose-mcp/src/developer/goose_hints/mod.rs deleted file mode 100644 index 818255dbc6..0000000000 --- a/crates/goose-mcp/src/developer/goose_hints/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod import_files; -pub mod load_hints; diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index 5eac4ff0ee..609f88bcc5 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -1,6 +1,5 @@ pub mod analyze; mod editor_models; -mod goose_hints; mod lang; mod shell; mod text_editor; diff --git a/crates/goose-mcp/src/developer/rmcp_developer.rs b/crates/goose-mcp/src/developer/rmcp_developer.rs index c2257c2876..c249d2aef7 100644 --- a/crates/goose-mcp/src/developer/rmcp_developer.rs +++ b/crates/goose-mcp/src/developer/rmcp_developer.rs @@ -33,7 +33,6 @@ use tokio_util::sync::CancellationToken; use super::analyze::{types::AnalyzeParams, CodeAnalyzer}; use super::editor_models::{create_editor_model, EditorModel}; -use super::goose_hints::load_hints::{load_hint_files, GOOSE_HINTS_FILENAME}; use super::shell::{ configure_shell_command, expand_path, get_shell_config, is_absolute_path, kill_process_group, }; @@ -246,17 +245,6 @@ impl ServerHandler for DeveloperServer { } }; - let hints_filenames: Vec = std::env::var("CONTEXT_FILE_NAMES") - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_else(|| vec!["AGENTS.md".to_string(), GOOSE_HINTS_FILENAME.to_string()]); - - // Build ignore patterns for file reference processing - let ignore_patterns = Self::build_ignore_patterns(&cwd); - - // Load hints using the centralized function - let hints = load_hint_files(&cwd, &hints_filenames, &ignore_patterns); - // Check if editor model exists and augment with custom llm editor tool description let editor_description = if let Some(ref editor) = self.editor_model { formatdoc! {r#" @@ -373,12 +361,7 @@ impl ServerHandler for DeveloperServer { _ => format!("{}{}", common_shell_instructions, unix_specific), }; - // Return base instructions directly when no hints are found - let instructions = if hints.is_empty() { - format!("{base_instructions}{editor_description}\n{shell_tool_desc}") - } else { - format!("{base_instructions}\n{editor_description}\n{shell_tool_desc}\n{hints}") - }; + let instructions = format!("{base_instructions}{editor_description}\n{shell_tool_desc}"); ServerInfo { server_info: Implementation { @@ -3397,161 +3380,6 @@ mod tests { ); } - #[tokio::test] - #[serial] - async fn test_global_goosehints() { - // Note: This test checks if ~/.config/goose/.goosehints exists and includes it in instructions - // Since RMCP version uses get_info() instead of instructions(), we test that method - let global_hints_path = - PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints").to_string()); - let global_hints_bak_path = - PathBuf::from(shellexpand::tilde("~/.config/goose/.goosehints.bak").to_string()); - let mut globalhints_existed = false; - - if global_hints_path.is_file() { - globalhints_existed = true; - fs::copy(&global_hints_path, &global_hints_bak_path).unwrap(); - } - - fs::write(&global_hints_path, "These are my global goose hints.").unwrap(); - - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - assert!(instructions.contains("my global goose hints.")); - - // restore backup if globalhints previously existed - if globalhints_existed { - fs::copy(&global_hints_bak_path, &global_hints_path).unwrap(); - fs::remove_file(&global_hints_bak_path).unwrap(); - } else { - fs::remove_file(&global_hints_path).unwrap(); - } - } - - #[tokio::test] - #[serial] - async fn test_goosehints_with_file_references() { - let temp_dir = tempfile::tempdir().unwrap(); - std::env::set_current_dir(&temp_dir).unwrap(); - - // Create referenced files - let readme_path = temp_dir.path().join("README.md"); - std::fs::write( - &readme_path, - "# Project README\n\nThis is the project documentation.", - ) - .unwrap(); - - let guide_path = temp_dir.path().join("guide.md"); - std::fs::write(&guide_path, "# Development Guide\n\nFollow these steps...").unwrap(); - - // Create .goosehints with references - let hints_content = r#"# Project Information - -Please refer to: -@README.md -@guide.md - -Additional instructions here. -"#; - let hints_path = temp_dir.path().join(".goosehints"); - std::fs::write(&hints_path, hints_content).unwrap(); - - // Create server and check instructions - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - - // Should contain the .goosehints content - assert!(instructions.contains("Project Information")); - assert!(instructions.contains("Additional instructions here")); - - // Should contain the referenced files' content - assert!(instructions.contains("# Project README")); - assert!(instructions.contains("This is the project documentation")); - assert!(instructions.contains("# Development Guide")); - assert!(instructions.contains("Follow these steps")); - - // Should have attribution markers - assert!(instructions.contains("--- Content from")); - assert!(instructions.contains("--- End of")); - } - - #[tokio::test] - #[serial] - async fn test_goosehints_when_present() { - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - fs::write(".goosehints", "Test hint content").unwrap(); - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - assert!(instructions.contains("Test hint content")); - } - - #[tokio::test] - #[serial] - async fn test_goosehints_when_missing() { - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - // When no hints are present, instructions should not contain hint content - assert!(!instructions.contains("AGENTS.md:") && !instructions.contains(".goosehints:")); - } - - #[tokio::test] - #[serial] - async 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 server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - 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"); - } - - #[tokio::test] - #[serial] - async 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 server = create_test_server(); - let server_info = server.get_info(); - - assert!(server_info.instructions.is_some()); - let instructions = server_info.instructions.unwrap(); - 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"); - } - #[test] #[serial] fn test_resolve_path_absolute() { diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index b3617556b0..d5a175892f 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -109,6 +109,7 @@ insta = "1.43.2" paste = "1.0.0" shellexpand = "3.1.1" indexmap = "2.12.0" +ignore = "0.4.25" [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 0d28297b06..1d13ec1f53 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -245,6 +245,7 @@ impl Agent { async fn prepare_reply_context( &self, unfixed_conversation: Conversation, + working_dir: &std::path::Path, ) -> Result { let unfixed_messages = unfixed_conversation.messages().clone(); let (conversation, issues) = fix_conversation(unfixed_conversation.clone()); @@ -261,7 +262,8 @@ impl Agent { let initial_messages = conversation.messages().clone(); let config = Config::global(); - let (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt().await?; + let (tools, toolshim_tools, system_prompt) = + self.prepare_tools_and_prompt(working_dir).await?; let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); self.tool_inspection_manager @@ -830,7 +832,9 @@ impl Agent { session: Session, cancel_token: Option, ) -> Result>> { - let context = self.prepare_reply_context(conversation).await?; + let context = self + .prepare_reply_context(conversation, &session.working_dir) + .await?; let ReplyContext { mut conversation, mut tools, @@ -844,6 +848,7 @@ impl Agent { let provider = self.provider().await?; let session_id = session_config.id.clone(); + let working_dir = session.working_dir.clone(); tokio::spawn(async move { if let Err(e) = SessionManager::maybe_update_name(&session_id, provider).await { warn!("Failed to generate session description: {}", e); @@ -1137,7 +1142,8 @@ impl Agent { } } if tools_updated { - (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt().await?; + (tools, toolshim_tools, system_prompt) = + self.prepare_tools_and_prompt(&working_dir).await?; } let mut exit_chat = false; if no_tools_called { @@ -1307,6 +1313,7 @@ impl Agent { .with_extensions(extensions_info.into_iter()) .with_frontend_instructions(self.frontend_instructions.lock().await.clone()) .with_extension_and_tool_counts(extension_count, tool_count) + .with_hints(&std::env::current_dir()?) .build(); let recipe_prompt = prompt_manager.get_recipe_prompt().await; diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index 8f8ca85f2c..af31418acd 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -8,11 +8,13 @@ use std::collections::HashMap; use crate::agents::extension::ExtensionInfo; use crate::agents::recipe_tools::dynamic_task_tools::should_enabled_subagents; use crate::agents::router_tools::llm_search_tool_prompt; +use crate::hints::load_hints::{load_hint_files, AGENTS_MD_FILENAME, GOOSE_HINTS_FILENAME}; use crate::{ config::{Config, GooseMode}, prompt_template, utils::sanitize_unicode_tags, }; +use std::path::Path; const MAX_EXTENSIONS: usize = 5; const MAX_TOOLS: usize = 50; @@ -52,6 +54,7 @@ pub struct SystemPromptBuilder<'a, M> { frontend_instructions: Option, extension_tool_count: Option<(usize, usize)>, router_enabled: bool, + hints: Option, } impl<'a> SystemPromptBuilder<'a, PromptManager> { @@ -86,6 +89,33 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> { self } + pub fn with_hints(mut self, working_dir: &Path) -> Self { + let config = Config::global(); + let hints_filenames = config + .get_param::>("CONTEXT_FILE_NAMES") + .unwrap_or_else(|_| { + vec![ + GOOSE_HINTS_FILENAME.to_string(), + AGENTS_MD_FILENAME.to_string(), + ] + }); + let ignore_patterns = { + let builder = ignore::gitignore::GitignoreBuilder::new(working_dir); + builder.build().unwrap_or_else(|_| { + ignore::gitignore::GitignoreBuilder::new(working_dir) + .build() + .expect("Failed to build default gitignore") + }) + }; + + let hints = load_hint_files(working_dir, &hints_filenames, &ignore_patterns); + + if !hints.is_empty() { + self.hints = Some(hints); + } + self + } + pub fn build(self) -> String { let mut extensions_info = self.extensions_info; @@ -138,6 +168,12 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> { }); let mut system_prompt_extras = self.manager.system_prompt_extras.clone(); + + // Add hints if provided + if let Some(hints) = self.hints { + system_prompt_extras.push(hints); + } + if goose_mode == GooseMode::Chat { system_prompt_extras.push( "Right now you are in the chat only mode, no access to any tool use and system." @@ -201,6 +237,7 @@ impl PromptManager { frontend_instructions: None, extension_tool_count: None, router_enabled: false, + hints: None, } } diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 99fa94806b..f9755c7167 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -107,7 +107,10 @@ async fn toolshim_postprocess( } impl Agent { - pub async fn prepare_tools_and_prompt(&self) -> Result<(Vec, Vec, String)> { + pub async fn prepare_tools_and_prompt( + &self, + working_dir: &std::path::Path, + ) -> Result<(Vec, Vec, String)> { // Get router enabled status let router_enabled = self.tool_route_manager.is_router_enabled().await; @@ -156,6 +159,7 @@ impl Agent { .with_frontend_instructions(self.frontend_instructions.lock().await.clone()) .with_extension_and_tool_counts(extension_count, tool_count) .with_router_enabled(router_enabled) + .with_hints(working_dir) .build(); // Handle toolshim if enabled @@ -468,7 +472,9 @@ mod tests { .await .unwrap(); - let (tools, _toolshim_tools, _system_prompt) = agent.prepare_tools_and_prompt().await?; + let working_dir = std::env::current_dir()?; + let (tools, _toolshim_tools, _system_prompt) = + agent.prepare_tools_and_prompt(&working_dir).await?; // Ensure both platform and frontend tools are present let names: Vec = tools.iter().map(|t| t.name.clone().into_owned()).collect(); diff --git a/crates/goose-mcp/src/developer/goose_hints/import_files.rs b/crates/goose/src/hints/import_files.rs similarity index 100% rename from crates/goose-mcp/src/developer/goose_hints/import_files.rs rename to crates/goose/src/hints/import_files.rs diff --git a/crates/goose-mcp/src/developer/goose_hints/load_hints.rs b/crates/goose/src/hints/load_hints.rs similarity index 82% rename from crates/goose-mcp/src/developer/goose_hints/load_hints.rs rename to crates/goose/src/hints/load_hints.rs index b2074c318e..ec4b4375f5 100644 --- a/crates/goose-mcp/src/developer/goose_hints/load_hints.rs +++ b/crates/goose/src/hints/load_hints.rs @@ -1,13 +1,14 @@ -use etcetera::{choose_app_strategy, AppStrategy}; use ignore::gitignore::Gitignore; use std::{ collections::HashSet, path::{Path, PathBuf}, }; -use crate::developer::goose_hints::import_files::read_referenced_files; +use crate::config::paths::Paths; +use crate::hints::import_files::read_referenced_files; pub const GOOSE_HINTS_FILENAME: &str = ".goosehints"; +pub const AGENTS_MD_FILENAME: &str = "AGENTS.md"; fn find_git_root(start_dir: &Path) -> Option<&Path> { let mut check_dir = start_dir; @@ -59,22 +60,7 @@ pub fn load_hint_files( let mut local_hints_contents = Vec::with_capacity(hints_filenames.len()); 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()) - }); - - if let Some(parent) = global_hints_path.parent() { - let _ = std::fs::create_dir_all(parent); - } - + let global_hints_path = Paths::in_config_dir(hints_filename); if global_hints_path.is_file() { let mut visited = HashSet::new(); let hints_dir = global_hints_path.parent().unwrap(); @@ -116,7 +102,7 @@ pub fn load_hint_files( 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("\n### Global Hints\nThese are my global goose hints.\n"); hints.push_str(&global_hints_contents.join("\n")); } @@ -124,7 +110,9 @@ pub fn load_hint_files( 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( + "### Project Hints\nThese are hints for working on the project in this directory.\n", + ); hints.push_str(&local_hints_contents.join("\n")); } @@ -135,7 +123,6 @@ pub fn load_hint_files( mod tests { use super::*; use ignore::gitignore::GitignoreBuilder; - use serial_test::serial; use std::fs::{self}; use tempfile::TempDir; @@ -146,51 +133,8 @@ mod tests { } #[test] - #[serial] - fn test_global_goosehints() { - // if ~/.config/goose/.goosehints exists, it should be included in the instructions - // copy the existing global hints file to a .bak file - let global_hints_path = PathBuf::from( - shellexpand::tilde(format!("~/.config/goose/{}", GOOSE_HINTS_FILENAME).as_str()) - .to_string(), - ); - let global_hints_bak_path = PathBuf::from( - shellexpand::tilde(format!("~/.config/goose/{}.bak", GOOSE_HINTS_FILENAME).as_str()) - .to_string(), - ); - let mut globalhints_existed = false; - - if global_hints_path.is_file() { - globalhints_existed = true; - fs::copy(&global_hints_path, &global_hints_bak_path).unwrap(); - } - - fs::write(&global_hints_path, "These are my global goose hints.").unwrap(); - - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let gitignore = create_dummy_gitignore(); - let hints = load_hint_files(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()], &gitignore); - - assert!(hints.contains("### Global Hints")); - assert!(hints.contains("my global goose hints.")); - - // restore backup if globalhints previously existed - if globalhints_existed { - fs::copy(&global_hints_bak_path, &global_hints_path).unwrap(); - fs::remove_file(&global_hints_bak_path).unwrap(); - } else { - // Clean up the test file we created - let _ = fs::remove_file(&global_hints_path); - } - } - - #[test] - #[serial] fn test_goosehints_when_present() { let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); fs::write(dir.path().join(GOOSE_HINTS_FILENAME), "Test hint content").unwrap(); let gitignore = create_dummy_gitignore(); @@ -200,10 +144,8 @@ mod tests { } #[test] - #[serial] fn test_goosehints_when_missing() { let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); let gitignore = create_dummy_gitignore(); let hints = load_hint_files(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()], &gitignore); @@ -212,10 +154,8 @@ mod tests { } #[test] - #[serial] fn test_goosehints_multiple_filenames() { let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); fs::write( dir.path().join("CLAUDE.md"), @@ -240,10 +180,8 @@ mod tests { } #[test] - #[serial] fn test_goosehints_configurable_filename() { let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); fs::write(dir.path().join("CLAUDE.md"), "Custom hints file content").unwrap(); let gitignore = create_dummy_gitignore(); @@ -357,11 +295,7 @@ mod tests { fs::create_dir(project_root.join(".git")).unwrap(); - fs::write( - project_root.join("README.md"), - "# Project README\nProject overview content", - ) - .unwrap(); + fs::write(project_root.join("README.md"), "# Project README").unwrap(); fs::write(project_root.join("config.md"), "Configuration details").unwrap(); let hints_content = r#"Project hints content @@ -382,7 +316,6 @@ Additional instructions here."#; assert!(hints.contains("--- Content from README.md ---")); assert!(hints.contains("# Project README")); - assert!(hints.contains("Project overview content")); assert!(hints.contains("--- End of README.md ---")); assert!(hints.contains("--- Content from config.md ---")); diff --git a/crates/goose/src/hints/mod.rs b/crates/goose/src/hints/mod.rs new file mode 100644 index 0000000000..b8c0ddb996 --- /dev/null +++ b/crates/goose/src/hints/mod.rs @@ -0,0 +1,4 @@ +mod import_files; +pub mod load_hints; + +pub use load_hints::{load_hint_files, AGENTS_MD_FILENAME, GOOSE_HINTS_FILENAME}; diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index f5383c8be5..80da2e839c 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -3,6 +3,7 @@ pub mod config; pub mod context_mgmt; pub mod conversation; pub mod execution; +pub mod hints; pub mod logging; pub mod mcp_utils; pub mod model;