Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/agents/agent-skills.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ npx skills add https://github.com/anthropics/skills --skill frontend-design

## Where skills live

Mux discovers skills from four roots:
Mux discovers skills from five roots:

- **Workspace-local**: `.mux/skills/<skill-name>/SKILL.md` (in the workspace working directory)
- **Global (Mux-specific)**: `~/.mux/skills/<skill-name>/SKILL.md`
- **Workspace universal**: `.agent/skills/<skill-name>/SKILL.md` (in the workspace working directory)
- **Universal (cross-tool)**: `~/.agents/skills/<skill-name>/SKILL.md`
- **Built-in**: shipped with Mux

If a skill exists in multiple locations, the precedence order is:
**workspace-local > global (`~/.mux/skills`) > universal (`~/.agents/skills`) > built-in**.
**workspace-local > global (`~/.mux/skills`) > workspace universal (`.agent/skills`) > universal (`~/.agents/skills`) > built-in**.

<Info>
Mux reads skills using the active workspace runtime. For SSH workspaces, skills are read from the
Expand Down
2 changes: 1 addition & 1 deletion src/common/utils/tools/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ export const TOOL_DEFINITIONS = {
agent_skill_read: {
description:
"Load an Agent Skill's SKILL.md (YAML frontmatter + markdown body) by name. " +
"Skills are discovered from <projectRoot>/.mux/skills/<name>/SKILL.md, ~/.mux/skills/<name>/SKILL.md, and ~/.agents/skills/<name>/SKILL.md.",
"Skills are discovered from <projectRoot>/.mux/skills/<name>/SKILL.md, <projectRoot>/.agent/skills/<name>/SKILL.md, ~/.mux/skills/<name>/SKILL.md, and ~/.agents/skills/<name>/SKILL.md.",
schema: z
.object({
name: SkillNameSchema.describe("Skill name (directory name under the skills root)"),
Expand Down
88 changes: 76 additions & 12 deletions src/node/services/agentSkills/agentSkillsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DisposableTempDir } from "@/node/services/tempDir";
import {
discoverAgentSkills,
discoverAgentSkillsDiagnostics,
getDefaultAgentSkillsRoots,
readAgentSkill,
} from "./agentSkillsService";

Expand Down Expand Up @@ -55,22 +56,66 @@ describe("agentSkillsService", () => {
expect(bar!.scope).toBe("global");
});

test("scans universal root after mux global root", async () => {
test("falls back to lower-precedence root when higher-precedence skill is invalid", async () => {
using project = new DisposableTempDir("agent-skills-project-invalid");
using global = new DisposableTempDir("agent-skills-global-valid");

const projectSkillsRoot = path.join(project.path, ".mux", "skills");
const globalSkillsRoot = global.path;

const invalidProjectSkillDir = path.join(projectSkillsRoot, "foo");
await fs.mkdir(invalidProjectSkillDir, { recursive: true });
await fs.writeFile(
path.join(invalidProjectSkillDir, "SKILL.md"),
"---\nname: foo\n---\n",
"utf-8"
);

await writeSkill(globalSkillsRoot, "foo", "from global");

const roots = { projectRoot: projectSkillsRoot, globalRoot: globalSkillsRoot };
const runtime = new LocalRuntime(project.path);

const skills = await discoverAgentSkills(runtime, project.path, { roots });
const foo = skills.find((s) => s.name === "foo");

expect(foo).toBeDefined();
expect(foo!.scope).toBe("global");
expect(foo!.description).toBe("from global");
});

test("default roots include workspace and home universal paths", () => {
using project = new DisposableTempDir("agent-skills-default-roots");
const runtime = new LocalRuntime(project.path);

const roots = getDefaultAgentSkillsRoots(runtime, project.path);

expect(roots.projectRoot).toBe(path.join(project.path, ".mux", "skills"));
expect(roots.workspaceUniversalRoot).toBe(path.join(project.path, ".agent", "skills"));
expect(roots.globalRoot).toBe("~/.mux/skills");
expect(roots.universalRoot).toBe("~/.agents/skills");
});

test("scans workspace/home universal roots after mux global root", async () => {
using project = new DisposableTempDir("agent-skills-project");
using global = new DisposableTempDir("agent-skills-global");
using universal = new DisposableTempDir("agent-skills-universal");

const projectSkillsRoot = path.join(project.path, ".mux", "skills");
const workspaceUniversalSkillsRoot = path.join(project.path, ".agent", "skills");
const globalSkillsRoot = global.path;
const universalSkillsRoot = universal.path;

await writeSkill(globalSkillsRoot, "shared", "from global");
await writeSkill(universalSkillsRoot, "shared", "from universal");
await writeSkill(universalSkillsRoot, "universal-only", "from universal only");
await writeSkill(workspaceUniversalSkillsRoot, "shared", "from workspace universal");
await writeSkill(universalSkillsRoot, "shared", "from home universal");
await writeSkill(workspaceUniversalSkillsRoot, "workspace-universal-only", "workspace only");
await writeSkill(universalSkillsRoot, "home-universal-only", "home only");

const roots = {
projectRoot: projectSkillsRoot,
globalRoot: globalSkillsRoot,
workspaceUniversalRoot: workspaceUniversalSkillsRoot,
universalRoot: universalSkillsRoot,
};
const runtime = new LocalRuntime(project.path);
Expand All @@ -82,15 +127,34 @@ describe("agentSkillsService", () => {
expect(shared!.scope).toBe("global");
expect(shared!.description).toBe("from global");

const universalOnly = skills.find((s) => s.name === "universal-only");
expect(universalOnly).toBeDefined();
expect(universalOnly!.scope).toBe("global");
expect(universalOnly!.description).toBe("from universal only");

const universalOnlyName = SkillNameSchema.parse("universal-only");
const resolved = await readAgentSkill(runtime, project.path, universalOnlyName, { roots });
expect(resolved.package.scope).toBe("global");
expect(resolved.package.frontmatter.description).toBe("from universal only");
const workspaceUniversalOnly = skills.find((s) => s.name === "workspace-universal-only");
expect(workspaceUniversalOnly).toBeDefined();
expect(workspaceUniversalOnly!.scope).toBe("global");
expect(workspaceUniversalOnly!.description).toBe("workspace only");

const homeUniversalOnly = skills.find((s) => s.name === "home-universal-only");
expect(homeUniversalOnly).toBeDefined();
expect(homeUniversalOnly!.scope).toBe("global");
expect(homeUniversalOnly!.description).toBe("home only");

const workspaceUniversalOnlyName = SkillNameSchema.parse("workspace-universal-only");
const resolvedWorkspace = await readAgentSkill(
runtime,
project.path,
workspaceUniversalOnlyName,
{
roots,
}
);
expect(resolvedWorkspace.package.scope).toBe("global");
expect(resolvedWorkspace.package.frontmatter.description).toBe("workspace only");

const homeUniversalOnlyName = SkillNameSchema.parse("home-universal-only");
const resolvedHome = await readAgentSkill(runtime, project.path, homeUniversalOnlyName, {
roots,
});
expect(resolvedHome.package.scope).toBe("global");
expect(resolvedHome.package.frontmatter.description).toBe("home only");
});

test("readAgentSkill resolves project before global", async () => {
Expand Down
Loading
Loading