Skip to content
Merged
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
4 changes: 3 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "mux",
Expand Down Expand Up @@ -75,6 +74,7 @@
"write-file-atomic": "^6.0.0",
"ws": "^8.18.3",
"xxhash-wasm": "^1.1.0",
"yaml": "^2.8.2",
"zod": "^4.1.11",
"zod-to-json-schema": "^3.24.6",
},
Expand Down Expand Up @@ -3674,6 +3674,8 @@

"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],

"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],

"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],

"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
Expand Down
10 changes: 10 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ gh pr view <number> --json mergeable,mergeStateStatus | jq '.'
./scripts/wait_pr_checks.sh <pr_number>
```

- When posting multi-line comments with `gh` (e.g., `@codex review`), **do not** rely on `\n` escapes inside quoted `--body` strings (they will be sent as literal text). Prefer `--body-file -` with a heredoc to preserve real newlines:

```bash
gh pr comment <pr_number> --body-file - <<'EOF'
@codex review

<message>
EOF
```
- If Codex left review comments and you addressed them, push your fixes and then comment `@codex review` to re-request review. After that, re-run `./scripts/wait_pr_checks.sh <pr_number>` and `./scripts/check_codex_comments.sh <pr_number>`.
- Generally run `wait_pr_checks` after submitting a PR to ensure CI passes.
- Status decoding: `mergeable=MERGEABLE` clean; `CONFLICTING` needs resolution. `mergeStateStatus=CLEAN` ready, `BLOCKED` waiting for CI, `BEHIND` rebase, `DIRTY` conflicts.
- If behind: `git fetch origin && git rebase origin/main && git push --force-with-lease`.
Expand Down
119 changes: 119 additions & 0 deletions docs/agent-skills.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: Agent Skills
description: Share reusable workflows and references with skills
---

## Overview

Agent Skills are reusable, file-based “playbooks” that you can share across projects or keep project-local.

Mux follows the Agent Skills specification and exposes skills to models in two steps:

1. **Index in the system prompt**: mux lists available skills (name + description).
2. **Tool-based loading**: the agent calls tools to load a full skill when needed.

This keeps the system prompt small while still making skills discoverable.

## Where skills live

Mux discovers skills from two roots:

- **Project-local**: `<projectRoot>/.mux/skills/<skill-name>/SKILL.md`
- **Global**: `~/.mux/skills/<skill-name>/SKILL.md`

If a skill exists in both locations, **project-local overrides global**.

<Info>
Mux reads skills using the active workspace runtime. For SSH workspaces, skills are read from the
remote host.
</Info>

## Skill layout

A skill is a directory named after the skill:

```text
.mux/skills/
my-skill/
SKILL.md
references/
...
```

Skill directory names must match `^[a-z0-9]+(?:-[a-z0-9]+)*$` (1–64 chars).

## `SKILL.md` format

`SKILL.md` must start with YAML frontmatter delimited by `---` on its own line.
Mux enforces a 1MB maximum file size for `SKILL.md`.

Required fields:

- `name`: must match the directory name
- `description`: short summary shown in mux’s skills index

Optional fields:

- `license`
- `compatibility`
- `metadata` (string key/value map)

Mux ignores unknown frontmatter keys (for example `allowed-tools`).

Example:

```md
---
name: my-skill
description: Build and validate a release branch.
license: MIT
metadata:
owner: platform
---

# My Skill

1. Do the thing...
```

## Using skills in mux

Mux injects an `<agent-skills>` block into the system prompt listing the available skills.

To load a skill, the agent calls:

```ts
agent_skill_read({ name: "my-skill" });
```

If your skill references additional files (cheatsheets, templates, etc.), the agent can read them with:

```ts
agent_skill_read_file({ name: "my-skill", filePath: "references/template.md" });
```

`agent_skill_read_file` supports `offset` / `limit` (like `file_read`) and rejects absolute paths and `..` traversal.

<Info>
`agent_skill_read_file` uses the same output limits as `file_read` (roughly 16KB per call,
numbered lines). Files are limited to 1MB. Read large files in chunks with `offset`/`limit`.
</Info>

## Current limitations

- There is no `/skill` command or UI activation flow yet; skills are loaded on-demand via tools.
- `allowed-tools` is not enforced by mux (it is tolerated in frontmatter, but ignored).

## Further reading

- [Agent Skills overview](https://agentskills.io/home)
- [What are skills?](https://agentskills.io/what-are-skills) (progressive disclosure)
- [Agent Skills specification](https://agentskills.io/specification)
- [Directory structure](https://agentskills.io/specification#directory-structure)
- [`SKILL.md` format](https://agentskills.io/specification#skill-md-format)
- [Frontmatter fields](https://agentskills.io/specification#frontmatter-required)
- [Optional directories](https://agentskills.io/specification#optional-directories)
- [Progressive disclosure](https://agentskills.io/specification#progressive-disclosure)
- [Integrate skills into your agent](https://agentskills.io/integrate-skills) (tool-based vs filesystem-based)
- [Example skills (GitHub)](https://github.com/anthropics/skills)
- [skills-ref validation library (GitHub)](https://github.com/agentskills/agentskills/tree/main/skills-ref)
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
},
"context-management",
"instruction-files",
"agent-skills",
"mcp-servers",
{
"group": "Project Secrets",
Expand Down
35 changes: 35 additions & 0 deletions docs/system-prompt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,41 @@ You are in a git worktree at ${workspacePath}
* Only included when at least one MCP server is configured.
* Note: We only expose server names, not commands, to avoid leaking secrets.
*/

async function buildAgentSkillsContext(runtime: Runtime, workspacePath: string): Promise<string> {
try {
const skills = await discoverAgentSkills(runtime, workspacePath);
if (skills.length === 0) return "";

const MAX_SKILLS = 50;
const shown = skills.slice(0, MAX_SKILLS);
const omitted = skills.length - shown.length;

const lines: string[] = [];
lines.push("Available agent skills (call tools to load):");
for (const skill of shown) {
lines.push(`- ${skill.name}: ${skill.description} (scope: ${skill.scope})`);
}
if (omitted > 0) {
lines.push(`(+${omitted} more not shown)`);
}

lines.push("");
lines.push("To load a skill:");
lines.push('- agent_skill_read({ name: "<skill-name>" })');

lines.push("");
lines.push("To read referenced files inside a skill directory:");
lines.push(
'- agent_skill_read_file({ name: "<skill-name>", filePath: "references/whatever.txt" })'
);

return `\n\n<agent-skills>\n${lines.join("\n")}\n</agent-skills>`;
} catch (error) {
log.warn("Failed to build agent skills context", { workspacePath, error });
return "";
}
}
function buildMCPContext(mcpServers: MCPServerMap): string {
const names = Object.keys(mcpServers);
if (names.length === 0) return "";
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@
"parse-duration": "^2.1.4",
"posthog-node": "^5.17.0",
"quickjs-emscripten": "^0.31.0",
"typescript": "^5.1.3",
"quickjs-emscripten-core": "^0.31.0",
"rehype-harden": "^1.1.5",
"rehype-sanitize": "^6.0.0",
Expand All @@ -111,10 +110,12 @@
"streamdown": "1.6.10",
"trpc-cli": "^0.12.1",
"turndown": "^7.2.2",
"typescript": "^5.1.3",
"undici": "^7.16.0",
"write-file-atomic": "^6.0.0",
"ws": "^8.18.3",
"xxhash-wasm": "^1.1.0",
"yaml": "^2.8.2",
"zod": "^4.1.11",
"zod-to-json-schema": "^3.24.6"
},
Expand Down
9 changes: 9 additions & 0 deletions src/common/orpc/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export {
TokenConsumerSchema,
} from "./schemas/chatStats";

// Agent Skill schemas
export {
AgentSkillDescriptorSchema,
AgentSkillFrontmatterSchema,
AgentSkillPackageSchema,
AgentSkillScopeSchema,
SkillNameSchema,
} from "./schemas/agentSkill";

// Error schemas
export { SendMessageErrorSchema, StreamErrorTypeSchema } from "./schemas/errors";

Expand Down
42 changes: 42 additions & 0 deletions src/common/orpc/schemas/agentSkill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { z } from "zod";

export const AgentSkillScopeSchema = z.enum(["project", "global"]);

/**
* Skill name per agentskills.io
* - 1–64 chars
* - lowercase letters/numbers/hyphens
* - no leading/trailing hyphen
* - no consecutive hyphens
*/
export const SkillNameSchema = z
.string()
.min(1)
.max(64)
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);

export const AgentSkillFrontmatterSchema = z.object({
name: SkillNameSchema,
description: z.string().min(1).max(1024),
license: z.string().optional(),
compatibility: z.string().min(1).max(500).optional(),
metadata: z.record(z.string(), z.string()).optional(),
});

export const AgentSkillDescriptorSchema = z.object({
name: SkillNameSchema,
description: z.string().min(1).max(1024),
scope: AgentSkillScopeSchema,
});

export const AgentSkillPackageSchema = z
.object({
scope: AgentSkillScopeSchema,
directoryName: SkillNameSchema,
frontmatter: AgentSkillFrontmatterSchema,
body: z.string(),
})
.refine((value) => value.directoryName === value.frontmatter.name, {
message: "SKILL.md frontmatter.name must match the parent directory name",
path: ["frontmatter", "name"],
});
18 changes: 18 additions & 0 deletions src/common/types/agentSkill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { z } from "zod";
import type {
AgentSkillDescriptorSchema,
AgentSkillFrontmatterSchema,
AgentSkillPackageSchema,
AgentSkillScopeSchema,
SkillNameSchema,
} from "@/common/orpc/schemas";

export type SkillName = z.infer<typeof SkillNameSchema>;

export type AgentSkillScope = z.infer<typeof AgentSkillScopeSchema>;

export type AgentSkillFrontmatter = z.infer<typeof AgentSkillFrontmatterSchema>;

export type AgentSkillDescriptor = z.infer<typeof AgentSkillDescriptorSchema>;

export type AgentSkillPackage = z.infer<typeof AgentSkillPackageSchema>;
12 changes: 12 additions & 0 deletions src/common/types/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import type { z } from "zod";
import type {
AgentReportToolResultSchema,
AgentSkillReadFileToolResultSchema,
AgentSkillReadToolResultSchema,
AskUserQuestionOptionSchema,
AskUserQuestionQuestionSchema,
AskUserQuestionToolResultSchema,
Expand Down Expand Up @@ -42,6 +44,16 @@ export interface FileReadToolArgs {
limit?: number; // number of lines to return from offset (optional)
}

// Agent Skill Tool Types
// Args derived from schema (avoid drift)
export type AgentSkillReadToolArgs = z.infer<typeof TOOL_DEFINITIONS.agent_skill_read.schema>;
export type AgentSkillReadToolResult = z.infer<typeof AgentSkillReadToolResultSchema>;

export type AgentSkillReadFileToolArgs = z.infer<
typeof TOOL_DEFINITIONS.agent_skill_read_file.schema
>;
export type AgentSkillReadFileToolResult = z.infer<typeof AgentSkillReadFileToolResultSchema>;

// FileReadToolResult derived from Zod schema (single source of truth)
export type FileReadToolResult = z.infer<typeof FileReadToolResultSchema>;

Expand Down
Loading