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 description: String,
pub path: PathBuf, pub path: PathBuf,
pub content: String, pub content: String,
pub supporting_files: Vec<PathBuf>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[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, description: metadata.description,
path, path,
content: body, content: body,
supporting_files: Vec::new(),
}) })
} }
@@ -195,9 +197,36 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option<Source> {
description, description,
path, path,
content: body, 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 { fn round_duration(d: Duration) -> String {
let secs = d.as_secs(); let secs = d.as_secs();
if secs < 60 { if secs < 60 {
@@ -614,6 +643,7 @@ impl SummonClient {
description, description,
path: PathBuf::from(&sr.path), path: PathBuf::from(&sr.path),
content: String::new(), content: String::new(),
supporting_files: Vec::new(),
}); });
} }
} }
@@ -684,6 +714,7 @@ impl SummonClient {
description: recipe.description.clone(), description: recipe.description.clone(),
path: path.clone(), path: path.clone(),
content: recipe.instructions.clone().unwrap_or_default(), content: recipe.instructions.clone().unwrap_or_default(),
supporting_files: Vec::new(),
}); });
} }
Err(e) => { 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) { if !seen.contains(&source.name) {
source.supporting_files = find_supporting_files(&skill_dir, &skill_file);
seen.insert(source.name.clone()); seen.insert(source.name.clone());
sources.push(source); sources.push(source);
} }
@@ -1016,11 +1048,28 @@ impl SummonClient {
Some(source) => { Some(source) => {
let content = source.to_load_text(); let content = source.to_load_text();
let output = format!( let mut output = format!(
"# Loaded: {} ({})\n\n{}\n\n---\nThis knowledge is now available in your context.", "# Loaded: {} ({})\n\n{}\n",
source.name, source.kind, content 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)]) Ok(vec![Content::text(output)])
} }
None => { None => {
@@ -1837,6 +1886,37 @@ You review code."#;
assert!(sources.iter().any(|s| s.kind == SourceKind::BuiltinSkill)); 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] #[tokio::test]
async fn test_client_tools_and_unknown_tool() { async fn test_client_tools_and_unknown_tool() {
let client = SummonClient::new(create_test_context()).unwrap(); let client = SummonClient::new(create_test_context()).unwrap();