From fbd8310e7a0e488e9c22f93b498a6139bf0228f0 Mon Sep 17 00:00:00 2001 From: angiejones Date: Mon, 23 Feb 2026 18:31:52 -0600 Subject: [PATCH] fix(summon): restore skill supporting files and directory path in load output When the unified summon extension replaced skills_extension in v1.25.0, the skill directory path and supporting files (scripts, templates, etc.) were no longer communicated to the agent on load. This caused the agent to not know where bundled scripts live, leading it to search the filesystem. - Add supporting_files field to Source - Collect supporting files when scanning skill directories - Store skill_dir (not SKILL.md path) as the source path - Include directory and file listing in handle_load_source output --- .../src/agents/platform_extensions/summon.rs | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 9ec4f73b6d0b..de01e5960c77 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -45,6 +45,7 @@ pub struct Source { pub description: String, pub path: PathBuf, pub content: String, + pub supporting_files: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -174,6 +175,7 @@ fn parse_skill_content(content: &str, path: PathBuf) -> Option { description: metadata.description, path, content: body, + supporting_files: Vec::new(), }) } @@ -195,9 +197,36 @@ fn parse_agent_content(content: &str, path: PathBuf) -> Option { 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 { + 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 = 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();