Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/goose/src/config/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"),
}
}
}
Expand All @@ -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)
}
Expand All @@ -74,4 +84,5 @@ enum DirType {
State,
Plugins,
Agents,
AgentsHome,
}
113 changes: 109 additions & 4 deletions crates/goose/src/hints/load_hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines +252 to 255

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a non-project matcher for global AGENTS imports

When the newly added global path is ~/.agents/AGENTS.md, this call still expands its @... references with the ignore_patterns built from the current project. ignore::Gitignore matches patterns such as *.md against absolute paths outside the repo too, so in any project that ignores Markdown files, references like @policy.md from the global AGENTS file are left unexpanded and the global hints silently lose content. Use an empty/global matcher for global hint expansion, or apply the project matcher only to project hint files.

Useful? React with 👍 / 👎.

ignore_patterns,
&global_ignore_patterns,
);
if !expanded_content.is_empty() {
global_hints_contents.push(expanded_content);
Expand Down Expand Up @@ -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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard GOOSE_PATH_ROOT mutations with env_lock

When the unit suite runs with default parallelism, this mutates the process-global GOOSE_PATH_ROOT without taking the crate's env_lock guard. #[serial_test::serial] only coordinates with other serial tests, so unit tests that use env_lock::lock_env([("GOOSE_PATH_ROOT", ...)]) can still run concurrently and observe this temporary root or have it removed by these new tests, causing nondeterministic failures. Use the same env_lock pattern for these new GOOSE_PATH_ROOT tests instead of raw set/remove.

Useful? React with 👍 / 👎.


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();
Expand Down
Loading