diff --git a/apps/web/src/app/docs/page.tsx b/apps/web/src/app/docs/page.tsx
index 87f8c24..2655381 100644
--- a/apps/web/src/app/docs/page.tsx
+++ b/apps/web/src/app/docs/page.tsx
@@ -232,7 +232,8 @@ export default function DocsPage() {
rules/, for skills{" "}
.agents/skills then{" "}
skills/ then a root{" "}
- SKILL.md fallback.
+ SKILL.md fallback, then root{" "}
+ <name>/SKILL.md directories.
diff --git a/packages/cli/README.md b/packages/cli/README.md
index 5c8106c..14a6ae4 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -72,7 +72,7 @@ Source path resolution is additive and priority-ordered:
- Agents: `.agents/agents` -> `agents`
- Commands: `.agents/commands` -> `commands` -> `prompts` -> provider fallbacks `.github/prompts` + `.gemini/commands`
- Rules: `.agents/rules` -> `rules`
-- Skills: `.agents/skills` -> `skills` -> root `SKILL.md` fallback
+- Skills: `.agents/skills` -> `skills` -> root `SKILL.md` -> root `/SKILL.md` fallback
- MCP: `.agents/mcp.json` -> `mcp.json`
Aggregate `agentloom add ` can import command/skill/MCP-only repositories even when no `agents/` directory exists.
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 30cf751..a5ce4cd 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "agentloom",
- "version": "0.1.10",
+ "version": "0.1.11",
"description": "Unified agent and MCP sync CLI for multi-provider AI tooling",
"type": "module",
"bin": {
diff --git a/packages/cli/src/core/copy.ts b/packages/cli/src/core/copy.ts
index a5131f7..5480df3 100644
--- a/packages/cli/src/core/copy.ts
+++ b/packages/cli/src/core/copy.ts
@@ -86,7 +86,7 @@ Source discovery:
agents: .agents/agents -> agents
commands: .agents/commands -> commands -> prompts -> (.github/prompts + .gemini/commands fallback)
rules: .agents/rules -> rules
- skills: .agents/skills -> skills -> root SKILL.md
+ skills: .agents/skills -> skills -> root SKILL.md -> root /SKILL.md
Usage:
agentloom add [options]
diff --git a/packages/cli/src/core/importer.ts b/packages/cli/src/core/importer.ts
index f266365..50236e7 100644
--- a/packages/cli/src/core/importer.ts
+++ b/packages/cli/src/core/importer.ts
@@ -293,7 +293,7 @@ export async function importSource(
sourceSkillsDirs.length === 0
) {
throw new Error(
- `No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, or root SKILL.md, including plugin sources declared in .claude-plugin/marketplace.json).`,
+ `No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, root SKILL.md, or root /SKILL.md directories, including plugin sources declared in .claude-plugin/marketplace.json).`,
);
}
if (
@@ -313,7 +313,7 @@ export async function importSource(
Object.keys(sourceMcp?.mcpServers ?? {}).length === 0
) {
throw new Error(
- `No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, root SKILL.md, or plugin sources from .claude-plugin/marketplace.json.`,
+ `No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, root SKILL.md, root /SKILL.md directories, or plugin sources from .claude-plugin/marketplace.json.`,
);
}
diff --git a/packages/cli/src/core/sources.ts b/packages/cli/src/core/sources.ts
index 8422477..ff69a83 100644
--- a/packages/cli/src/core/sources.ts
+++ b/packages/cli/src/core/sources.ts
@@ -313,6 +313,10 @@ function discoverSourceSkillsDirsForRoot(importRoot: string): string[] {
return [importRoot];
}
+ if (hasImmediateRootSkillDirs(importRoot)) {
+ return [importRoot];
+ }
+
return [];
}
@@ -334,6 +338,21 @@ function dedupePaths(paths: string[]): string[] {
return [...new Set(paths)];
}
+function hasImmediateRootSkillDirs(importRoot: string): boolean {
+ for (const entry of fs.readdirSync(importRoot, { withFileTypes: true })) {
+ if (!entry.isDirectory()) {
+ continue;
+ }
+
+ const skillFile = path.join(importRoot, entry.name, "SKILL.md");
+ if (fs.existsSync(skillFile) && fs.statSync(skillFile).isFile()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
function isPathWithinRoot(rootPath: string, targetPath: string): boolean {
const relative = path.relative(rootPath, targetPath);
return (
diff --git a/packages/cli/tests/unit/copy.test.ts b/packages/cli/tests/unit/copy.test.ts
index 0498658..ccf3382 100644
--- a/packages/cli/tests/unit/copy.test.ts
+++ b/packages/cli/tests/unit/copy.test.ts
@@ -70,6 +70,9 @@ describe("copy helpers", () => {
it("includes targeted add import help", () => {
const help = getAddHelpText();
+ expect(help).toContain(
+ "skills: .agents/skills -> skills -> root SKILL.md -> root /SKILL.md",
+ );
expect(help).toContain("--agents ");
expect(help).toContain("--rule ");
expect(help).toContain("--rules ");
diff --git a/packages/cli/tests/unit/importer-conflict.test.ts b/packages/cli/tests/unit/importer-conflict.test.ts
index 4ae3a50..13ba3de 100644
--- a/packages/cli/tests/unit/importer-conflict.test.ts
+++ b/packages/cli/tests/unit/importer-conflict.test.ts
@@ -1153,6 +1153,47 @@ name: visual-explainer
description: Explain visuals
---
+Skill body.
+`,
+ );
+
+ const paths = buildScopePaths(workspaceRoot, "local");
+ const summary = await importSource({
+ source: sourceRoot,
+ paths,
+ yes: true,
+ nonInteractive: true,
+ importAgents: false,
+ importCommands: false,
+ importMcp: false,
+ importSkills: true,
+ requireSkills: true,
+ skillSelectors: ["visual-explainer"],
+ });
+
+ expect(summary.importedSkills).toEqual(["visual-explainer"]);
+ expect(
+ fs.existsSync(path.join(paths.skillsDir, "visual-explainer", "SKILL.md")),
+ ).toBe(true);
+ });
+
+ it("recognizes root /SKILL.md directories and imports selected skills natively", async () => {
+ const sourceRoot = fs.mkdtempSync(
+ path.join(os.tmpdir(), "agentloom-source-"),
+ );
+ const workspaceRoot = fs.mkdtempSync(
+ path.join(os.tmpdir(), "agentloom-workspace-"),
+ );
+ tempDirs.push(sourceRoot, workspaceRoot);
+
+ ensureDir(path.join(sourceRoot, "visual-explainer"));
+ writeTextAtomic(
+ path.join(sourceRoot, "visual-explainer", "SKILL.md"),
+ `---
+name: visual-explainer
+description: Explain visuals
+---
+
Skill body.
`,
);
diff --git a/packages/cli/tests/unit/sources.test.ts b/packages/cli/tests/unit/sources.test.ts
index e6a410b..998fd85 100644
--- a/packages/cli/tests/unit/sources.test.ts
+++ b/packages/cli/tests/unit/sources.test.ts
@@ -148,6 +148,20 @@ describe("source parsing and revision", () => {
expect(discoverSourceSkillsDir(root)).toBe(path.join(root, "skills"));
});
+ it("falls back to root /SKILL.md directories when canonical skill paths are absent", () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "agentloom-sources-"));
+ tempDirs.push(root);
+
+ ensureDir(path.join(root, "reviewer"));
+ writeTextAtomic(path.join(root, "reviewer", "SKILL.md"), "# reviewer\n");
+
+ expect(discoverSourceSkillsDirs(root)).toEqual([root]);
+ expect(discoverSourceSkillsDir(root)).toBe(root);
+
+ ensureDir(path.join(root, "skills"));
+ expect(discoverSourceSkillsDirs(root)).toEqual([path.join(root, "skills")]);
+ });
+
it("discovers plugin roots from .claude-plugin marketplace metadata", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "agentloom-sources-"));
tempDirs.push(root);