fix(summon): restore skill supporting files and directory path in load output (#7457)

This commit is contained in:
Angie Jones
2026-02-23 20:45:22 -06:00
committed by GitHub
parent 716085ce09
commit 359c5fba7d
@@ -45,6 +45,7 @@ pub struct Source {
pub description: String,
pub path: PathBuf,
pub content: String,
pub supporting_files: Vec<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@@ -174,6 +175,7 @@ fn parse_skill_content(content: &str, path: PathBuf) -> Option<Source> {
description: metadata.description,
path,
content: body,
supporting_files: Vec::new(),
})
}
@@ -195,9 +197,36 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option<Source> {
description,
path,
content: body,
supporting_files: Vec::new(),
})
}
/// Collect all files in a skill directory (excluding SKILL.md itself),
/// recursing one level into subdirectories.
fn find_supporting_files(directory: &Path, skill_file: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
let entries = match std::fs::read_dir(directory) {
Ok(e) => e,
Err(_) => return files,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && path != skill_file {
files.push(path);
} else if path.is_dir() {
if let Ok(sub_entries) = std::fs::read_dir(&path) {
for sub_entry in sub_entries.flatten() {
let sub_path = sub_entry.path();
if sub_path.is_file() {
files.push(sub_path);
}
}
}
}
}
files
}
fn round_duration(d: Duration) -> String {
let secs = d.as_secs();
if secs < 60 {
@@ -614,6 +643,7 @@ impl SummonClient {
description,
path: PathBuf::from(&sr.path),
content: String::new(),
supporting_files: Vec::new(),
});
}
}
@@ -684,6 +714,7 @@ impl SummonClient {
description: recipe.description.clone(),
path: path.clone(),
content: recipe.instructions.clone().unwrap_or_default(),
supporting_files: Vec::new(),
});
}
Err(e) => {
@@ -723,8 +754,9 @@ impl SummonClient {
}
};
if let Some(source) = parse_skill_content(&content, skill_file) {
if let Some(mut source) = parse_skill_content(&content, skill_dir.clone()) {
if !seen.contains(&source.name) {
source.supporting_files = find_supporting_files(&skill_dir, &skill_file);
seen.insert(source.name.clone());
sources.push(source);
}
@@ -1016,11 +1048,28 @@ impl SummonClient {
Some(source) => {
let content = source.to_load_text();
let output = format!(
"# Loaded: {} ({})\n\n{}\n\n---\nThis knowledge is now available in your context.",
let mut output = format!(
"# Loaded: {} ({})\n\n{}\n",
source.name, source.kind, content
);
if !source.supporting_files.is_empty() {
output.push_str(&format!(
"\n## Supporting Files\n\nSkill directory: {}\n\nThe following supporting files are available:\n",
source.path.display()
));
for file in &source.supporting_files {
if let Ok(relative) = file.strip_prefix(&source.path) {
output.push_str(&format!("- {}\n", relative.display()));
}
}
output.push_str(
"\nUse the file tools to read these files or run scripts as directed.\n",
);
}
output.push_str("\n---\nThis knowledge is now available in your context.");
Ok(vec![Content::text(output)])
}
None => {
@@ -1837,6 +1886,37 @@ You review code."#;
assert!(sources.iter().any(|s| s.kind == SourceKind::BuiltinSkill));
}
#[tokio::test]
async fn test_skill_supporting_files_discovered() {
let temp_dir = TempDir::new().unwrap();
let skill_dir = temp_dir.path().join(".goose/skills/my-skill");
fs::create_dir_all(&skill_dir).unwrap();
fs::write(
skill_dir.join("SKILL.md"),
"---\nname: my-skill\ndescription: A skill with scripts\n---\nRun check_all.sh",
)
.unwrap();
fs::write(skill_dir.join("myscript.sh"), "#!/bin/bash\necho ok").unwrap();
fs::create_dir(skill_dir.join("templates")).unwrap();
fs::write(skill_dir.join("templates/report.txt"), "template content").unwrap();
let client = SummonClient::new(create_test_context()).unwrap();
let sources = client.discover_filesystem_sources(temp_dir.path());
let skill = sources.iter().find(|s| s.name == "my-skill").unwrap();
assert_eq!(skill.path, skill_dir);
assert_eq!(skill.supporting_files.len(), 2);
let file_names: Vec<String> = skill
.supporting_files
.iter()
.filter_map(|f| f.file_name().map(|n| n.to_string_lossy().to_string()))
.collect();
assert!(file_names.contains(&"myscript.sh".to_string()));
assert!(file_names.contains(&"report.txt".to_string()));
}
#[tokio::test]
async fn test_client_tools_and_unknown_tool() {
let client = SummonClient::new(create_test_context()).unwrap();