From 3b2b9abffcdceadc446153dcd664b8b5c0296855 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 12 Dec 2025 23:23:17 +0100 Subject: [PATCH] coding-agent: change Pi skills to use SKILL.md convention Breaking change: Pi skills must now be named SKILL.md inside a directory, matching Codex CLI format. Previously any *.md file was treated as a skill. Migrate by renaming ~/.pi/agent/skills/foo.md to ~/.pi/agent/skills/foo/SKILL.md --- packages/ai/src/models.generated.ts | 177 ++++++++++------------- packages/coding-agent/CHANGELOG.md | 16 +- packages/coding-agent/docs/skills.md | 14 +- packages/coding-agent/src/core/skills.ts | 63 ++++---- pi-mono.code-workspace | 6 + 5 files changed, 123 insertions(+), 153 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 37f3ecf7..8035ce11 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3608,23 +3608,6 @@ export const MODELS = { contextWindow: 262144, maxTokens: 4096, } satisfies Model<"openai-completions">, - "meituan/longcat-flash-chat:free": { - id: "meituan/longcat-flash-chat:free", - name: "Meituan: LongCat Flash Chat (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "qwen/qwen-plus-2025-07-28": { id: "qwen/qwen-plus-2025-07-28", name: "Qwen: Qwen Plus 0728", @@ -5750,23 +5733,6 @@ export const MODELS = { contextWindow: 32768, maxTokens: 4096, } satisfies Model<"openai-completions">, - "cohere/command-r-08-2024": { - id: "cohere/command-r-08-2024", - name: "Cohere: Command R (08-2024)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4000, - } satisfies Model<"openai-completions">, "cohere/command-r-plus-08-2024": { id: "cohere/command-r-plus-08-2024", name: "Cohere: Command R+ (08-2024)", @@ -5784,6 +5750,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4000, } satisfies Model<"openai-completions">, + "cohere/command-r-08-2024": { + id: "cohere/command-r-08-2024", + name: "Cohere: Command R (08-2024)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4000, + } satisfies Model<"openai-completions">, "sao10k/l3.1-euryale-70b": { id: "sao10k/l3.1-euryale-70b", name: "Sao10K: Llama 3.1 Euryale 70B v2.2", @@ -5852,23 +5835,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, - "meta-llama/llama-3.1-405b-instruct": { - id: "meta-llama/llama-3.1-405b-instruct", - name: "Meta: Llama 3.1 405B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 3.5, - output: 3.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 130815, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "meta-llama/llama-3.1-70b-instruct": { id: "meta-llama/llama-3.1-70b-instruct", name: "Meta: Llama 3.1 70B Instruct", @@ -5886,6 +5852,23 @@ export const MODELS = { contextWindow: 131072, maxTokens: 4096, } satisfies Model<"openai-completions">, + "meta-llama/llama-3.1-405b-instruct": { + id: "meta-llama/llama-3.1-405b-instruct", + name: "Meta: Llama 3.1 405B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 3.5, + output: 3.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 130815, + maxTokens: 4096, + } satisfies Model<"openai-completions">, "mistralai/mistral-nemo": { id: "mistralai/mistral-nemo", name: "Mistral: Mistral Nemo", @@ -5903,9 +5886,9 @@ export const MODELS = { contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, - "openai/gpt-4o-mini-2024-07-18": { - id: "openai/gpt-4o-mini-2024-07-18", - name: "OpenAI: GPT-4o-mini (2024-07-18)", + "openai/gpt-4o-mini": { + id: "openai/gpt-4o-mini", + name: "OpenAI: GPT-4o-mini", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -5920,9 +5903,9 @@ export const MODELS = { contextWindow: 128000, maxTokens: 16384, } satisfies Model<"openai-completions">, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "OpenAI: GPT-4o-mini", + "openai/gpt-4o-mini-2024-07-18": { + id: "openai/gpt-4o-mini-2024-07-18", + name: "OpenAI: GPT-4o-mini (2024-07-18)", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -6073,23 +6056,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 64000, } satisfies Model<"openai-completions">, - "meta-llama/llama-3-70b-instruct": { - id: "meta-llama/llama-3-70b-instruct", - name: "Meta: Llama 3 70B Instruct", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.3, - output: 0.39999999999999997, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "meta-llama/llama-3-8b-instruct": { id: "meta-llama/llama-3-8b-instruct", name: "Meta: Llama 3 8B Instruct", @@ -6107,6 +6073,23 @@ export const MODELS = { contextWindow: 8192, maxTokens: 16384, } satisfies Model<"openai-completions">, + "meta-llama/llama-3-70b-instruct": { + id: "meta-llama/llama-3-70b-instruct", + name: "Meta: Llama 3 70B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.3, + output: 0.39999999999999997, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 16384, + } satisfies Model<"openai-completions">, "mistralai/mixtral-8x22b-instruct": { id: "mistralai/mixtral-8x22b-instruct", name: "Mistral: Mixtral 8x22B Instruct", @@ -6192,23 +6175,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"openai-completions">, - "openai/gpt-3.5-turbo-0613": { - id: "openai/gpt-3.5-turbo-0613", - name: "OpenAI: GPT-3.5 Turbo (older v0613)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 4095, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "openai/gpt-4-turbo-preview": { id: "openai/gpt-4-turbo-preview", name: "OpenAI: GPT-4 Turbo Preview", @@ -6226,6 +6192,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } satisfies Model<"openai-completions">, + "openai/gpt-3.5-turbo-0613": { + id: "openai/gpt-3.5-turbo-0613", + name: "OpenAI: GPT-3.5 Turbo (older v0613)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 1, + output: 2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 4095, + maxTokens: 4096, + } satisfies Model<"openai-completions">, "mistralai/mistral-tiny": { id: "mistralai/mistral-tiny", name: "Mistral Tiny", @@ -6294,9 +6277,9 @@ export const MODELS = { contextWindow: 16385, maxTokens: 4096, } satisfies Model<"openai-completions">, - "openai/gpt-4-0314": { - id: "openai/gpt-4-0314", - name: "OpenAI: GPT-4 (older v0314)", + "openai/gpt-4": { + id: "openai/gpt-4", + name: "OpenAI: GPT-4", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -6311,9 +6294,9 @@ export const MODELS = { contextWindow: 8191, maxTokens: 4096, } satisfies Model<"openai-completions">, - "openai/gpt-4": { - id: "openai/gpt-4", - name: "OpenAI: GPT-4", + "openai/gpt-4-0314": { + id: "openai/gpt-4-0314", + name: "OpenAI: GPT-4 (older v0314)", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1f8657e3..bf6174b7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,22 +8,14 @@ ## [Unreleased] +### Breaking Changes + +- **Pi skills now use `SKILL.md` convention**: Pi skills must now be named `SKILL.md` inside a directory, matching Codex CLI format. Previously any `*.md` file was treated as a skill. Migrate by renaming `~/.pi/agent/skills/foo.md` to `~/.pi/agent/skills/foo/SKILL.md`. + ### Added - Display loaded skills on startup in interactive mode -### Fixed - -### Changed - - -### Added - -### Fixed - -### Changed - - ## [0.19.0] - 2025-12-12 ### Added diff --git a/packages/coding-agent/docs/skills.md b/packages/coding-agent/docs/skills.md index 91ba9f79..210886eb 100644 --- a/packages/coding-agent/docs/skills.md +++ b/packages/coding-agent/docs/skills.md @@ -9,8 +9,8 @@ Skills are discovered from these locations (in order of priority, later wins on 1. `~/.codex/skills/**/SKILL.md` (Codex CLI user skills, recursive) 2. `~/.claude/skills/*/SKILL.md` (Claude Code user skills) 3. `/.claude/skills/*/SKILL.md` (Claude Code project skills) -4. `~/.pi/agent/skills/**/*.md` (Pi user skills, recursive) -5. `/.pi/skills/**/*.md` (Pi project skills, recursive) +4. `~/.pi/agent/skills/**/SKILL.md` (Pi user skills, recursive) +5. `/.pi/skills/**/SKILL.md` (Pi project skills, recursive) Skill names and descriptions are listed in the system prompt. When a task matches a skill's description, the agent uses the `read` tool to load it. @@ -43,13 +43,13 @@ The parser only supports single-line `key: value` syntax. Multiline YAML blocks ### Variables -`{baseDir}` is replaced with the directory containing the skill file. Use it to reference bundled scripts or resources. +Use `{baseDir}` as a placeholder for the skill's directory. The agent is told each skill's base directory and will substitute it when following the instructions. -### Subdirectories (Pi Skills) +### Subdirectories -Pi skills in subdirectories use colon-separated names: -- `~/.pi/agent/skills/db/migrate.md` → `db:migrate` -- `/.pi/skills/aws/s3/upload.md` → `aws:s3:upload` +Pi and Codex skills in subdirectories use colon-separated names: +- `~/.pi/agent/skills/db/migrate/SKILL.md` → `db:migrate` +- `/.pi/skills/aws/s3/upload/SKILL.md` → `aws:s3:upload` ## Claude Code Compatibility diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index de1cf68e..f0252516 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -18,7 +18,7 @@ export interface Skill { source: SkillSource; } -type SkillFormat = "pi" | "claude" | "codex"; +type SkillFormat = "recursive" | "claude"; function stripQuotes(value: string): string { if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { @@ -60,7 +60,13 @@ function parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; bod return { frontmatter, body }; } -function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat, subdir: string = ""): Skill[] { +function loadSkillsFromDir( + dir: string, + source: SkillSource, + format: SkillFormat, + useColonPath: boolean = false, + subdir: string = "", +): Skill[] { const skills: Skill[] = []; if (!existsSync(dir)) { @@ -81,11 +87,12 @@ function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat const fullPath = join(dir, entry.name); - if (format === "pi") { + if (format === "recursive") { + // Recursive format: scan directories, look for SKILL.md files if (entry.isDirectory()) { const newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name; - skills.push(...loadSkillsFromDir(fullPath, source, format, newSubdir)); - } else if (entry.isFile() && entry.name.endsWith(".md")) { + skills.push(...loadSkillsFromDir(fullPath, source, format, useColonPath, newSubdir)); + } else if (entry.isFile() && entry.name === "SKILL.md") { try { const rawContent = readFileSync(fullPath, "utf-8"); const { frontmatter } = parseFrontmatter(rawContent); @@ -94,19 +101,22 @@ function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat continue; } - const nameFromFile = entry.name.slice(0, -3); - const name = frontmatter.name || (subdir ? `${subdir}:${nameFromFile}` : nameFromFile); + const skillDir = dirname(fullPath); + // useColonPath: db:migrate (pi), otherwise just: migrate (codex) + const nameFromPath = useColonPath ? subdir || basename(skillDir) : basename(skillDir); + const name = frontmatter.name || nameFromPath; skills.push({ name, description: frontmatter.description, filePath: fullPath, - baseDir: dirname(fullPath), + baseDir: skillDir, source, }); } catch {} } } else if (format === "claude") { + // Claude format: only one level deep, each directory must contain SKILL.md if (!entry.isDirectory()) { continue; } @@ -136,30 +146,6 @@ function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat source, }); } catch {} - } else if (format === "codex") { - if (entry.isDirectory()) { - skills.push(...loadSkillsFromDir(fullPath, source, format)); - } else if (entry.isFile() && entry.name === "SKILL.md") { - try { - const rawContent = readFileSync(fullPath, "utf-8"); - const { frontmatter } = parseFrontmatter(rawContent); - - if (!frontmatter.description) { - continue; - } - - const skillDir = dirname(fullPath); - const name = frontmatter.name || basename(skillDir); - - skills.push({ - name, - description: frontmatter.description, - filePath: fullPath, - baseDir: skillDir, - source, - }); - } catch {} - } } } } catch {} @@ -170,28 +156,31 @@ function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat export function loadSkills(): Skill[] { const skillMap = new Map(); + // Codex: recursive, simple directory name const codexUserDir = join(homedir(), ".codex", "skills"); - for (const skill of loadSkillsFromDir(codexUserDir, "codex-user", "codex")) { + for (const skill of loadSkillsFromDir(codexUserDir, "codex-user", "recursive", false)) { skillMap.set(skill.name, skill); } + // Claude: single level only const claudeUserDir = join(homedir(), ".claude", "skills"); - for (const skill of loadSkillsFromDir(claudeUserDir, "claude-user", "claude")) { + for (const skill of loadSkillsFromDir(claudeUserDir, "claude-user", "claude", false)) { skillMap.set(skill.name, skill); } const claudeProjectDir = resolve(process.cwd(), ".claude", "skills"); - for (const skill of loadSkillsFromDir(claudeProjectDir, "claude-project", "claude")) { + for (const skill of loadSkillsFromDir(claudeProjectDir, "claude-project", "claude", false)) { skillMap.set(skill.name, skill); } + // Pi: recursive, colon-separated path names const globalSkillsDir = join(homedir(), CONFIG_DIR_NAME, "agent", "skills"); - for (const skill of loadSkillsFromDir(globalSkillsDir, "user", "pi")) { + for (const skill of loadSkillsFromDir(globalSkillsDir, "user", "recursive", true)) { skillMap.set(skill.name, skill); } const projectSkillsDir = resolve(process.cwd(), CONFIG_DIR_NAME, "skills"); - for (const skill of loadSkillsFromDir(projectSkillsDir, "project", "pi")) { + for (const skill of loadSkillsFromDir(projectSkillsDir, "project", "recursive", true)) { skillMap.set(skill.name, skill); } diff --git a/pi-mono.code-workspace b/pi-mono.code-workspace index 7cfaae8b..525d385c 100644 --- a/pi-mono.code-workspace +++ b/pi-mono.code-workspace @@ -6,6 +6,12 @@ }, { "path": "../../moms" + }, + { + "path": "../../.pi/agent" + }, + { + "path": "../pi-skills" } ], "settings": {}