-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Enable load nested hint files #4002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
91c528d
refactor to extract load goosehints to a separate file
lifeizhou-ap 2b164e3
traverse upwards to get all the hints
lifeizhou-ap 6bedbeb
add the configuration for nested hints
lifeizhou-ap d9f7983
Merge branch 'main' into lifei/read-upper-goosehints
lifeizhou-ap 5c31ea8
resolved merge conflicts and refactor
lifeizhou-ap 31be8dd
moved tests
lifeizhou-ap 51ace7d
changed the logic that only traverse to project git root
lifeizhou-ap 3af6ba6
fixed the lint
lifeizhou-ap 7d38c5e
Merge branch 'main' into lifei/read-upper-goosehints
lifeizhou-ap db64e9e
added agent.md back
lifeizhou-ap 533913d
fixed compilation error
lifeizhou-ap b2b4a3f
fixed lint check
lifeizhou-ap 3b7f6b6
increased max depth to 6 to fit mono repo use case
lifeizhou-ap 3129deb
Revert "increased max depth to 6 to fit mono repo use case"
lifeizhou-ap 04ef6df
removed configuration for nested hints. use it directly
lifeizhou-ap File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| use cliclack; | ||
| use console::style; | ||
| use goose::config::Config; | ||
| use serde_json::Value; | ||
| use std::error::Error; | ||
|
|
||
| pub fn configure_nested_hints_dialog() -> Result<(), Box<dyn Error>> { | ||
| let config = Config::global(); | ||
|
|
||
| if std::env::var("GOOSE_NESTED_HINTS").is_ok() { | ||
| let _ = cliclack::log::info("Notice: GOOSE_NESTED_HINTS environment variable is set and will override the configuration here."); | ||
| } | ||
|
|
||
| let current_enabled: bool = config.get_param("NESTED_GOOSE_HINTS").unwrap_or(false); | ||
|
|
||
| println!( | ||
| "Current nested hints setting: {}", | ||
| style(if current_enabled { | ||
| "enabled" | ||
| } else { | ||
| "disabled" | ||
| }) | ||
| .cyan() | ||
| ); | ||
|
|
||
| let enable = cliclack::confirm("Enable nested hint files loading (eg: .goosehints)?") | ||
| .initial_value(current_enabled) | ||
| .interact()?; | ||
|
|
||
| config.set_param("NESTED_GOOSE_HINTS", Value::Bool(enable))?; | ||
|
|
||
| if enable { | ||
| cliclack::outro("✓ Nested hints enabled - Goose will load hint files from current directory upwards to project root (.git) or filesystem root.")?; | ||
| } else { | ||
| cliclack::outro("✓ Nested hints disabled - Goose will only load hint files from the current working directory")?; | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| pub mod bench; | ||
| pub mod configure; | ||
| pub mod configure_settings; | ||
| pub mod info; | ||
| pub mod mcp; | ||
| pub mod project; | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,295 @@ | ||
| use etcetera::{choose_app_strategy, AppStrategy}; | ||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| pub const GOOSE_HINTS_FILENAME: &str = ".goosehints"; | ||
|
|
||
| fn traverse_directories_upward(start_dir: &Path) -> Vec<PathBuf> { | ||
| let mut directories = Vec::new(); | ||
| let mut current_dir = start_dir; | ||
|
|
||
| loop { | ||
| directories.push(current_dir.to_path_buf()); | ||
| if current_dir.join(".git").exists() { | ||
| break; | ||
| } | ||
| if let Some(parent) = current_dir.parent() { | ||
| current_dir = parent; | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| directories.reverse(); | ||
| directories | ||
| } | ||
|
|
||
| fn is_nested_enabled() -> bool { | ||
| std::env::var("NESTED_GOOSE_HINTS") | ||
| .ok() | ||
| .and_then(|v| v.parse().ok()) | ||
| .unwrap_or(false) | ||
| } | ||
|
|
||
| pub fn load_hints(cwd: &Path, hints_filenames: &[String]) -> String { | ||
| 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 { | ||
| // 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); | ||
| } | ||
|
|
||
| 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_directories = if is_nested_enabled() { | ||
| traverse_directories_upward(cwd) | ||
| } else { | ||
| vec![cwd.to_path_buf()] | ||
| }; | ||
|
|
||
| for directory in &local_directories { | ||
| for hints_filename in hints_filenames { | ||
| let hints_path = directory.join(hints_filename); | ||
| if hints_path.is_file() { | ||
| if let Ok(content) = std::fs::read_to_string(&hints_path) { | ||
| local_hints_contents.push(content); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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")); | ||
| } | ||
|
|
||
| hints | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use serial_test::serial; | ||
| use std::fs; | ||
| use tempfile::TempDir; | ||
|
|
||
| #[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 hints = load_hints(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()]); | ||
|
|
||
| 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 hints = load_hints(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()]); | ||
|
|
||
| assert!(hints.contains("Test hint content")); | ||
| } | ||
|
|
||
| #[test] | ||
| #[serial] | ||
| fn test_goosehints_when_missing() { | ||
| let dir = TempDir::new().unwrap(); | ||
| std::env::set_current_dir(dir.path()).unwrap(); | ||
|
|
||
| let hints = load_hints(dir.path(), &[GOOSE_HINTS_FILENAME.to_string()]); | ||
|
|
||
| assert!(!hints.contains("Project Hints")); | ||
| } | ||
|
|
||
| #[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"), | ||
| "Custom hints file content from CLAUDE.md", | ||
| ) | ||
| .unwrap(); | ||
| fs::write( | ||
| dir.path().join(GOOSE_HINTS_FILENAME), | ||
| "Custom hints file content from .goosehints", | ||
| ) | ||
| .unwrap(); | ||
|
|
||
| let hints = load_hints( | ||
| dir.path(), | ||
| &["CLAUDE.md".to_string(), GOOSE_HINTS_FILENAME.to_string()], | ||
| ); | ||
|
|
||
| assert!(hints.contains("Custom hints file content from CLAUDE.md")); | ||
| assert!(hints.contains("Custom hints file content from .goosehints")); | ||
| } | ||
|
|
||
| #[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 hints = load_hints(dir.path(), &["CLAUDE.md".to_string()]); | ||
|
|
||
| assert!(hints.contains("Custom hints file content")); | ||
| assert!(!hints.contains(".goosehints")); // Make sure it's not loading the default | ||
| } | ||
|
|
||
| #[test] | ||
| #[serial] | ||
| fn test_nested_goosehints_with_git_root() { | ||
| std::env::set_var("NESTED_GOOSE_HINTS", "true"); | ||
|
|
||
| let temp_dir = TempDir::new().unwrap(); | ||
| let project_root = temp_dir.path(); | ||
|
|
||
| fs::create_dir(project_root.join(".git")).unwrap(); | ||
| fs::write( | ||
| project_root.join(GOOSE_HINTS_FILENAME), | ||
| "Root hints content", | ||
| ) | ||
| .unwrap(); | ||
|
|
||
| let subdir = project_root.join("subdir"); | ||
| fs::create_dir(&subdir).unwrap(); | ||
| fs::write(subdir.join(GOOSE_HINTS_FILENAME), "Subdir hints content").unwrap(); | ||
| let current_dir = subdir.join("current_dir"); | ||
| fs::create_dir(¤t_dir).unwrap(); | ||
| fs::write( | ||
| current_dir.join(GOOSE_HINTS_FILENAME), | ||
| "current_dir hints content", | ||
| ) | ||
| .unwrap(); | ||
|
|
||
| let hints = load_hints(¤t_dir, &[GOOSE_HINTS_FILENAME.to_string()]); | ||
|
|
||
| assert!( | ||
| hints.contains("Root hints content\nSubdir hints content\ncurrent_dir hints content") | ||
| ); | ||
|
|
||
| std::env::remove_var("NESTED_GOOSE_HINTS"); | ||
| } | ||
|
|
||
| #[test] | ||
| #[serial] | ||
| fn test_nested_goosehints_without_git_root() { | ||
| std::env::set_var("NESTED_GOOSE_HINTS", "true"); | ||
|
|
||
| let temp_dir = TempDir::new().unwrap(); | ||
| let base_dir = temp_dir.path(); | ||
|
|
||
| fs::write(base_dir.join(GOOSE_HINTS_FILENAME), "Base hints content").unwrap(); | ||
|
|
||
| let subdir = base_dir.join("subdir"); | ||
| fs::create_dir(&subdir).unwrap(); | ||
| fs::write(subdir.join(GOOSE_HINTS_FILENAME), "Subdir hints content").unwrap(); | ||
|
|
||
| let current_dir = subdir.join("current_dir"); | ||
| fs::create_dir(¤t_dir).unwrap(); | ||
|
|
||
| let hints = load_hints(¤t_dir, &[GOOSE_HINTS_FILENAME.to_string()]); | ||
|
|
||
| assert!(hints.contains("Base hints content")); | ||
| assert!(hints.contains("Subdir hints content")); | ||
|
|
||
| std::env::remove_var("NESTED_GOOSE_HINTS"); | ||
| } | ||
|
|
||
| #[test] | ||
| #[serial] | ||
| fn test_nested_goosehints_mixed_filenames() { | ||
| std::env::set_var("NESTED_GOOSE_HINTS", "true"); | ||
|
|
||
| let temp_dir = TempDir::new().unwrap(); | ||
| let project_root = temp_dir.path(); | ||
|
|
||
| fs::create_dir(project_root.join(".git")).unwrap(); | ||
| fs::write(project_root.join("CLAUDE.md"), "Root CLAUDE.md content").unwrap(); | ||
|
|
||
| let subdir = project_root.join("subdir"); | ||
| fs::create_dir(&subdir).unwrap(); | ||
| fs::write( | ||
| subdir.join(GOOSE_HINTS_FILENAME), | ||
| "Subdir .goosehints content", | ||
| ) | ||
| .unwrap(); | ||
|
|
||
| let current_dir = subdir.join("current_dir"); | ||
| fs::create_dir(¤t_dir).unwrap(); | ||
|
|
||
| let hints = load_hints( | ||
| ¤t_dir, | ||
| &["CLAUDE.md".to_string(), GOOSE_HINTS_FILENAME.to_string()], | ||
| ); | ||
|
|
||
| assert!(hints.contains("Root CLAUDE.md content")); | ||
| assert!(hints.contains("Subdir .goosehints content")); | ||
|
|
||
| std::env::remove_var("NESTED_GOOSE_HINTS"); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think enabled could be a better default here, and people would only need to seek out the config option if they didn't want it for some reason.