feat: load global hints from ~/.agents/AGENTS.md (#9736)

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Douwe M Osinga <douwe@sidewalklabs.com>
Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
Michael Neale
2026-06-17 12:25:27 +10:00
committed by GitHub
parent 0b95199896
commit b0d8ea055c
2 changed files with 120 additions and 4 deletions
+11
View File
@@ -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,
}
+109 -4
View File
@@ -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<PathBuf> = 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();