fix: move goosehints/AGENTS.md handling to goose, and out of developer extension (#5575)

This commit is contained in:
Alex Hancock
2025-11-06 17:04:46 -05:00
committed by GitHub
parent e0f0898781
commit c62c61cc07
12 changed files with 76 additions and 261 deletions
Generated
+5 -4
View File
@@ -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",
@@ -1,2 +0,0 @@
mod import_files;
pub mod load_hints;
-1
View File
@@ -1,6 +1,5 @@
pub mod analyze;
mod editor_models;
mod goose_hints;
mod lang;
mod shell;
mod text_editor;
@@ -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<String> = 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() {
+1
View File
@@ -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]
+10 -3
View File
@@ -245,6 +245,7 @@ impl Agent {
async fn prepare_reply_context(
&self,
unfixed_conversation: Conversation,
working_dir: &std::path::Path,
) -> Result<ReplyContext> {
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<CancellationToken>,
) -> Result<BoxStream<'_, Result<AgentEvent>>> {
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;
+37
View File
@@ -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<String>,
extension_tool_count: Option<(usize, usize)>,
router_enabled: bool,
hints: Option<String>,
}
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::<Vec<String>>("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,
}
}
+8 -2
View File
@@ -107,7 +107,10 @@ async fn toolshim_postprocess(
}
impl Agent {
pub async fn prepare_tools_and_prompt(&self) -> Result<(Vec<Tool>, Vec<Tool>, String)> {
pub async fn prepare_tools_and_prompt(
&self,
working_dir: &std::path::Path,
) -> Result<(Vec<Tool>, Vec<Tool>, 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<String> = tools.iter().map(|t| t.name.clone().into_owned()).collect();
@@ -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 ---"));
+4
View File
@@ -0,0 +1,4 @@
mod import_files;
pub mod load_hints;
pub use load_hints::{load_hint_files, AGENTS_MD_FILENAME, GOOSE_HINTS_FILENAME};
+1
View File
@@ -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;