diff --git a/agentscope-core/src/main/java/io/agentscope/core/skill/util/SkillFileSystemHelper.java b/agentscope-core/src/main/java/io/agentscope/core/skill/util/SkillFileSystemHelper.java
index daccd99a6f..39d3b586ff 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/skill/util/SkillFileSystemHelper.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/skill/util/SkillFileSystemHelper.java
@@ -126,10 +126,22 @@ public static AgentSkill loadSkillFromDirectory(
/**
* Retrieves all skill names by parsing SKILL.md metadata in each skill folder.
*
+ *
If {@code baseDir} itself contains a {@code SKILL.md} file, it is treated as a single
+ * skill directory and only that skill's name is returned. Otherwise, subdirectories are
+ * scanned for skills.
+ *
* @param baseDir The base directory containing skill folders
* @return A list of skill names sorted alphabetically
*/
public static List getAllSkillNames(Path baseDir) {
+ // If baseDir itself is a skill directory, return only that skill
+ if (hasSkillFile(baseDir)) {
+ List names = new ArrayList<>();
+ readSkillName(baseDir).ifPresent(names::add);
+ names.sort(String::compareTo);
+ return names;
+ }
+
List skillNames = new ArrayList<>();
try (Stream subdirs = Files.list(baseDir)) {
@@ -160,6 +172,10 @@ public static List getAllSkills(Path baseDir, String source) {
/**
* Retrieves all skills from the base directory.
*
+ * If {@code baseDir} itself contains a {@code SKILL.md} file, it is treated as a single
+ * skill directory and only that skill is returned. Otherwise, subdirectories are scanned for
+ * skills.
+ *
* @param baseDir The base directory containing skill folders
* @param source The source identifier for created skills
* @param includeResources when {@code false}, each skill's resource map is left empty and
@@ -168,6 +184,17 @@ public static List getAllSkills(Path baseDir, String source) {
*/
public static List getAllSkills(
Path baseDir, String source, boolean includeResources) {
+ // If baseDir itself is a skill directory, return only that skill
+ if (hasSkillFile(baseDir)) {
+ List skills = new ArrayList<>();
+ try {
+ skills.add(loadSkillFromDirectory(baseDir, source, includeResources));
+ } catch (Exception e) {
+ logger.warn("Failed to load skill from '{}': {}", baseDir, e.getMessage(), e);
+ }
+ return skills;
+ }
+
List skills = new ArrayList<>();
try (Stream subdirs = Files.list(baseDir)) {
@@ -463,6 +490,16 @@ private static Optional findSkillDirectoryByName(Path baseDir, String skil
return Optional.empty();
}
+ // Check if baseDir itself is the skill
+ if (hasSkillFile(baseDir)) {
+ String rootName = readSkillName(baseDir).orElse(null);
+ if (skillName.equals(rootName)) {
+ return Optional.of(baseDir);
+ }
+ // baseDir is a single skill directory; no subdirectory skills to scan
+ return Optional.empty();
+ }
+
try (Stream subdirs = Files.list(baseDir)) {
return subdirs.filter(Files::isDirectory)
.filter(SkillFileSystemHelper::hasSkillFile)
diff --git a/agentscope-core/src/test/java/io/agentscope/core/skill/util/SkillFileSystemHelperTest.java b/agentscope-core/src/test/java/io/agentscope/core/skill/util/SkillFileSystemHelperTest.java
index 21c4da5e0c..c8be4e3201 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/skill/util/SkillFileSystemHelperTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/skill/util/SkillFileSystemHelperTest.java
@@ -494,6 +494,88 @@ void shouldFilterOsHiddenFilesOnWindows() throws IOException {
"OS hidden file should be filtered out on Windows");
}
+ @Test
+ @DisplayName("Should return root skill name when baseDir itself has SKILL.md")
+ void testGetAllSkillNames_RootLevelSkill() throws IOException {
+ Path rootSkillDir = tempDir.resolve("root-skill-dir");
+ Files.createDirectories(rootSkillDir);
+ Files.writeString(
+ rootSkillDir.resolve("SKILL.md"),
+ "---\nname: root-skill\ndescription: Root Skill\n---\nRoot content",
+ StandardCharsets.UTF_8);
+ // Add a subdirectory (e.g. references/) that should be ignored
+ Files.createDirectories(rootSkillDir.resolve("references"));
+ Files.writeString(rootSkillDir.resolve("references/doc.md"), "doc");
+
+ List names = SkillFileSystemHelper.getAllSkillNames(rootSkillDir);
+ assertEquals(1, names.size());
+ assertEquals("root-skill", names.get(0));
+ }
+
+ @Test
+ @DisplayName("Should return root skill when baseDir itself has SKILL.md")
+ void testGetAllSkills_RootLevelSkill() throws IOException {
+ Path rootSkillDir = tempDir.resolve("root-skill-dir2");
+ Files.createDirectories(rootSkillDir);
+ Files.writeString(
+ rootSkillDir.resolve("SKILL.md"),
+ "---\nname: root-skill2\ndescription: Root Skill 2\n---\nRoot content 2",
+ StandardCharsets.UTF_8);
+ Files.writeString(rootSkillDir.resolve("extra.txt"), "extra resource");
+
+ List skills = SkillFileSystemHelper.getAllSkills(rootSkillDir, "git-source");
+ assertEquals(1, skills.size());
+ assertEquals("root-skill2", skills.get(0).getName());
+ assertEquals("git-source", skills.get(0).getSource());
+ assertTrue(skills.get(0).getResources().containsKey("extra.txt"));
+ }
+
+ @Test
+ @DisplayName("Should load root skill by name when baseDir itself has SKILL.md")
+ void testLoadSkill_RootLevelSkill() throws IOException {
+ Path rootSkillDir = tempDir.resolve("root-skill-dir3");
+ Files.createDirectories(rootSkillDir);
+ Files.writeString(
+ rootSkillDir.resolve("SKILL.md"),
+ "---\nname: root-skill3\ndescription: Root Skill 3\n---\nRoot content 3",
+ StandardCharsets.UTF_8);
+
+ AgentSkill skill =
+ SkillFileSystemHelper.loadSkill(rootSkillDir, "root-skill3", "git-source");
+ assertNotNull(skill);
+ assertEquals("root-skill3", skill.getName());
+ assertEquals("Root Skill 3", skill.getDescription());
+ }
+
+ @Test
+ @DisplayName("Should find root skill via skillExists when baseDir itself has SKILL.md")
+ void testSkillExists_RootLevelSkill() throws IOException {
+ Path rootSkillDir = tempDir.resolve("root-skill-dir4");
+ Files.createDirectories(rootSkillDir);
+ Files.writeString(
+ rootSkillDir.resolve("SKILL.md"),
+ "---\nname: root-skill4\ndescription: Root Skill 4\n---\nContent",
+ StandardCharsets.UTF_8);
+
+ assertTrue(SkillFileSystemHelper.skillExists(rootSkillDir, "root-skill4"));
+ assertFalse(SkillFileSystemHelper.skillExists(rootSkillDir, "nonexistent"));
+ }
+
+ @Test
+ @DisplayName("Should throw when root skill name does not match requested name")
+ void testLoadSkill_RootLevelSkillNameMismatch() throws IOException {
+ Path rootSkillDir = tempDir.resolve("root-skill-dir5");
+ Files.createDirectories(rootSkillDir);
+ Files.writeString(
+ rootSkillDir.resolve("SKILL.md"),
+ "---\nname: actual-name\ndescription: Actual\n---\nContent",
+ StandardCharsets.UTF_8);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> SkillFileSystemHelper.loadSkill(rootSkillDir, "wrong-name", "source"));
+ }
+
private void createSampleSkill(String name, String description, String content)
throws IOException {
Path skillDir = skillsBaseDir.resolve(name);