From a8a3de5773c0e230262fe2d68cbc7bf43f346a90 Mon Sep 17 00:00:00 2001 From: morgmart <98432065+morgmart@users.noreply.github.com> Date: Mon, 4 May 2026 19:49:34 -0700 Subject: [PATCH] Allow sharing nested skill files Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> --- ui/goose2/src-tauri/src/commands/system.rs | 30 +++++++++++++---- .../src-tauri/src/commands/system_tests.rs | 33 +++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/ui/goose2/src-tauri/src/commands/system.rs b/ui/goose2/src-tauri/src/commands/system.rs index 19369fe75f..44748f79b5 100644 --- a/ui/goose2/src-tauri/src/commands/system.rs +++ b/ui/goose2/src-tauri/src/commands/system.rs @@ -103,6 +103,24 @@ fn has_sequence_with_remaining(path: &Path, sequence: &[&str], remaining: usize) }) } +fn has_sequence_with_min_remaining( + path: &Path, + sequence: &[&str], + minimum_remaining: usize, +) -> bool { + let components = path_components(path); + components + .windows(sequence.len()) + .enumerate() + .any(|(index, window)| { + window + .iter() + .map(String::as_str) + .eq(sequence.iter().copied()) + && components.len() >= index + sequence.len() + minimum_remaining + }) +} + fn is_shareable_agent_file(path: &Path) -> bool { path.extension().and_then(|extension| extension.to_str()) == Some("md") && (has_sequence_with_remaining(path, &[".agents", "agents"], 1) @@ -111,11 +129,11 @@ fn is_shareable_agent_file(path: &Path) -> bool { fn is_shareable_skill_file(path: &Path) -> bool { path.file_name().and_then(|name| name.to_str()) == Some("SKILL.md") - && (has_sequence_with_remaining(path, &[".agents", "skills"], 2) - || has_sequence_with_remaining(path, &[".goose", "skills"], 2) - || has_sequence_with_remaining(path, &[".claude", "skills"], 2) - || has_sequence_with_remaining(path, &[".config", "agents", "skills"], 2) - || has_sequence_with_remaining(path, &["goose", "skills"], 2) + && (has_sequence_with_min_remaining(path, &[".agents", "skills"], 2) + || has_sequence_with_min_remaining(path, &[".goose", "skills"], 2) + || has_sequence_with_min_remaining(path, &[".claude", "skills"], 2) + || has_sequence_with_min_remaining(path, &[".config", "agents", "skills"], 2) + || has_sequence_with_min_remaining(path, &["goose", "skills"], 2) || is_plugin_skill_file(path)) } @@ -125,7 +143,7 @@ fn is_plugin_skill_file(path: &Path) -> bool { window[0] == "goose" && window[1] == "plugins" && window[3] == "skills" - && components.len() == index + 6 + && components.len() >= index + 6 }) } diff --git a/ui/goose2/src-tauri/src/commands/system_tests.rs b/ui/goose2/src-tauri/src/commands/system_tests.rs index ecf5a4469b..e8ddba52e4 100644 --- a/ui/goose2/src-tauri/src/commands/system_tests.rs +++ b/ui/goose2/src-tauri/src/commands/system_tests.rs @@ -281,6 +281,13 @@ fn shareable_source_validation_accepts_agent_and_skill_files() { .join("skills") .join("review") .join("SKILL.md"); + let nested_skill_file = dir + .path() + .join(".agents") + .join("skills") + .join("team") + .join("review") + .join("SKILL.md"); let config_skill_file = dir .path() .join("Block") @@ -297,21 +304,47 @@ fn shareable_source_validation_accepts_agent_and_skill_files() { .join("skills") .join("plugin-review") .join("SKILL.md"); + let nested_plugin_skill_file = dir + .path() + .join("Block") + .join("goose") + .join("plugins") + .join("plugin-a") + .join("skills") + .join("team") + .join("plugin-review") + .join("SKILL.md"); fs::create_dir_all(agent_file.parent().expect("agent parent")).expect("agent dir"); fs::create_dir_all(skill_file.parent().expect("skill parent")).expect("skill dir"); + fs::create_dir_all(nested_skill_file.parent().expect("nested skill parent")) + .expect("nested skill dir"); fs::create_dir_all(config_skill_file.parent().expect("config skill parent")) .expect("config skill dir"); fs::create_dir_all(plugin_skill_file.parent().expect("plugin skill parent")) .expect("plugin skill dir"); + fs::create_dir_all( + nested_plugin_skill_file + .parent() + .expect("nested plugin skill parent"), + ) + .expect("nested plugin skill dir"); fs::write(&agent_file, "---\nname: Helper\n---\n").expect("agent file"); fs::write(&skill_file, "---\nname: review\n---\n").expect("skill file"); + fs::write(&nested_skill_file, "---\nname: team-review\n---\n").expect("nested skill file"); fs::write(&config_skill_file, "---\nname: legacy\n---\n").expect("config skill file"); fs::write(&plugin_skill_file, "---\nname: plugin-review\n---\n").expect("plugin skill file"); + fs::write( + &nested_plugin_skill_file, + "---\nname: nested-plugin-review\n---\n", + ) + .expect("nested plugin skill file"); assert!(validate_shareable_source_file(&agent_file).is_ok()); assert!(validate_shareable_source_file(&skill_file).is_ok()); + assert!(validate_shareable_source_file(&nested_skill_file).is_ok()); assert!(validate_shareable_source_file(&config_skill_file).is_ok()); assert!(validate_shareable_source_file(&plugin_skill_file).is_ok()); + assert!(validate_shareable_source_file(&nested_plugin_skill_file).is_ok()); } #[test]