From f28552d442005be71ae2891ff0286d218ddc8269 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Mon, 29 Jun 2026 19:09:10 +0530 Subject: [PATCH 1/6] fix(skills): discover standard agent skill paths --- src/skills.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/skills.ts | 20 ++++++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/skills.test.ts b/src/skills.test.ts index 1b0ebae7..787493c3 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -4,22 +4,51 @@ import { join } from "node:path"; import assert from "node:assert/strict"; import { loadConfig } from "./config.js"; import { + effectiveSkillPaths, formatPathForPrompt, loadWorkspaceSkills, resolveSkillReadPath, } from "./skills.js"; const root = await mkdtemp(join(tmpdir(), "devspace-skills-test-")); +const originalHome = process.env.HOME; try { + process.env.HOME = root; const projectRoot = join(root, "project"); const agentDir = join(root, "agent"); const explicitSkills = join(root, "explicit-skills"); + const globalAgentsSkills = join(root, ".agents", "skills"); + const projectAgentsSkills = join(projectRoot, ".agents", "skills"); + await mkdir(join(globalAgentsSkills, "agent-global-skill"), { recursive: true }); + await mkdir(join(projectAgentsSkills, "agent-project-skill"), { recursive: true }); await mkdir(join(projectRoot, ".pi", "skills", "project-skill"), { recursive: true }); await mkdir(join(agentDir, "skills", "global-skill"), { recursive: true }); await mkdir(join(explicitSkills, "duplicate"), { recursive: true }); await mkdir(join(explicitSkills, "disabled"), { recursive: true }); + await writeFile( + join(globalAgentsSkills, "agent-global-skill", "SKILL.md"), + [ + "---", + "name: agent-global-skill", + "description: Agent global skill description.", + "---", + "", + "# Agent Global Skill", + ].join("\n"), + ); + await writeFile( + join(projectAgentsSkills, "agent-project-skill", "SKILL.md"), + [ + "---", + "name: agent-project-skill", + "description: Agent project skill description.", + "---", + "", + "# Agent Project Skill", + ].join("\n"), + ); await writeFile( join(projectRoot, ".pi", "skills", "project-skill", "SKILL.md"), [ @@ -84,11 +113,25 @@ try { PORT: "1", }); const loaded = loadWorkspaceSkills(config, projectRoot); + assert.equal(loaded.skills.some((skill) => skill.name === "agent-global-skill"), true); + assert.equal(loaded.skills.some((skill) => skill.name === "agent-project-skill"), true); assert.equal(loaded.skills.some((skill) => skill.name === "project-skill"), true); assert.equal(loaded.skills.filter((skill) => skill.name === "duplicate-skill").length, 1); assert.equal(loaded.skills.some((skill) => skill.name === "hidden-skill"), true); assert.equal(loaded.diagnostics.some((diagnostic) => diagnostic.type === "collision"), true); + const duplicateConfig = loadConfig({ + DEVSPACE_ALLOWED_ROOTS: projectRoot, + DEVSPACE_AGENT_DIR: agentDir, + DEVSPACE_SKILL_PATHS: [explicitSkills, projectAgentsSkills].join(","), + DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", + PORT: "1", + }); + assert.equal( + effectiveSkillPaths(duplicateConfig, projectRoot).filter((path) => path === projectAgentsSkills).length, + 1, + ); + const projectSkill = loaded.skills.find((skill) => skill.name === "project-skill"); assert.ok(projectSkill); assert.match(formatPathForPrompt(projectSkill.filePath), /SKILL\.md$/); @@ -106,5 +149,7 @@ try { false, ); } finally { + if (originalHome === undefined) delete process.env.HOME; + else process.env.HOME = originalHome; await rm(root, { recursive: true, force: true }); } diff --git a/src/skills.ts b/src/skills.ts index 20a35205..1b1dbc58 100644 --- a/src/skills.ts +++ b/src/skills.ts @@ -1,5 +1,6 @@ +import { existsSync } from "node:fs"; import { homedir } from "node:os"; -import { resolve, sep } from "node:path"; +import { join, resolve, sep } from "node:path"; import { loadSkills, type Skill, @@ -19,13 +20,28 @@ export interface SkillReadResolution { isSkillFile: boolean; } +export function effectiveSkillPaths(config: ServerConfig, cwd: string): string[] { + const defaultPaths = [ + join(homedir(), ".agents", "skills"), + resolve(cwd, ".agents", "skills"), + ].filter((path) => existsSync(path)); + + const seen = new Set(); + return [...defaultPaths, ...config.skillPaths].filter((path) => { + const resolvedPath = resolve(expandHomePath(path)); + if (seen.has(resolvedPath)) return false; + seen.add(resolvedPath); + return true; + }); +} + export function loadWorkspaceSkills(config: ServerConfig, cwd: string): LoadedSkills { if (!config.skillsEnabled) return { skills: [], diagnostics: [] }; return loadSkills({ cwd, agentDir: config.agentDir, - skillPaths: config.skillPaths, + skillPaths: effectiveSkillPaths(config, cwd), includeDefaults: true, }); } From b1de12efc927327c623783b22295eb1ee0b17612 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Mon, 29 Jun 2026 19:10:25 +0530 Subject: [PATCH 2/6] docs(skills): document standard agent skill paths --- docs/chatgpt-coding-workflow.md | 11 ++++++++--- docs/configuration.md | 17 ++++++++++++++--- docs/gotchas.md | 11 ++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/chatgpt-coding-workflow.md b/docs/chatgpt-coding-workflow.md index 3b0efaf2..1def8a75 100644 --- a/docs/chatgpt-coding-workflow.md +++ b/docs/chatgpt-coding-workflow.md @@ -79,11 +79,16 @@ new context during later tool calls. Skills are enabled by default for coding-agent workflows. -DevSpace discovers skills from: +DevSpace discovers standard Agent Skills from: -- `DEVSPACE_AGENT_DIR`, which defaults to `~/.codex` +- `~/.agents/skills` +- project `.agents/skills` + +It also keeps compatibility with: + +- `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` - project `.pi/skills` -- optional paths from `DEVSPACE_SKILL_PATHS` +- additional paths from `DEVSPACE_SKILL_PATHS` When `open_workspace` returns matching skills, the model should read the advertised `SKILL.md` before following that skill. diff --git a/docs/configuration.md b/docs/configuration.md index fa3a61de..a731cba1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -99,13 +99,24 @@ sessions. | Variable | Purpose | | --- | --- | | `DEVSPACE_SKILLS` | Set to `0` to hide skills. Enabled by default. | -| `DEVSPACE_AGENT_DIR` | Defaults to `~/.codex`. | -| `DEVSPACE_SKILL_PATHS` | Optional comma-separated skill directories. | +| `DEVSPACE_AGENT_DIR` | Defaults to `~/.codex`; its `skills` child is loaded for compatibility. | +| `DEVSPACE_SKILL_PATHS` | Optional comma-separated additional skill directories. | + +DevSpace discovers standard Agent Skills from: + +- `~/.agents/skills` +- project `.agents/skills` + +It also keeps compatibility with: + +- `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` +- project `.pi/skills` +- additional paths from `DEVSPACE_SKILL_PATHS` Example: ```bash -DEVSPACE_SKILL_PATHS="$HOME/.codex/skills,$HOME/.claude/skills" \ +DEVSPACE_SKILL_PATHS="$HOME/.claude/skills,$HOME/company/skills" \ npx @waishnav/devspace serve ``` diff --git a/docs/gotchas.md b/docs/gotchas.md index 33823b5a..0ef8a7f4 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -193,11 +193,16 @@ Skills are enabled by default. Check: DEVSPACE_SKILLS=1 npx @waishnav/devspace serve ``` -DevSpace looks in: +DevSpace looks in standard Agent Skills locations: -- `DEVSPACE_AGENT_DIR`, defaulting to `~/.codex` +- `~/.agents/skills` +- project `.agents/skills` + +It also checks compatibility and custom paths: + +- `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` - project `.pi/skills` -- `DEVSPACE_SKILL_PATHS` +- additional paths from `DEVSPACE_SKILL_PATHS` If a skill appears in `open_workspace`, the model must read that skill's `SKILL.md` before reading other files inside the skill directory. From 2dfe62171247cd9154e862ccfec8d052893e4dcb Mon Sep 17 00:00:00 2001 From: Waishnav Date: Mon, 29 Jun 2026 19:23:30 +0530 Subject: [PATCH 3/6] fix(skills): make pi project skills opt-in --- src/skills.test.ts | 16 ++++++++++++++-- src/skills.ts | 3 ++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/skills.test.ts b/src/skills.test.ts index 787493c3..f8af6c1d 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -115,7 +115,7 @@ try { const loaded = loadWorkspaceSkills(config, projectRoot); assert.equal(loaded.skills.some((skill) => skill.name === "agent-global-skill"), true); assert.equal(loaded.skills.some((skill) => skill.name === "agent-project-skill"), true); - assert.equal(loaded.skills.some((skill) => skill.name === "project-skill"), true); + assert.equal(loaded.skills.some((skill) => skill.name === "project-skill"), false); assert.equal(loaded.skills.filter((skill) => skill.name === "duplicate-skill").length, 1); assert.equal(loaded.skills.some((skill) => skill.name === "hidden-skill"), true); assert.equal(loaded.diagnostics.some((diagnostic) => diagnostic.type === "collision"), true); @@ -132,7 +132,19 @@ try { 1, ); - const projectSkill = loaded.skills.find((skill) => skill.name === "project-skill"); + const legacyPiConfig = loadConfig({ + DEVSPACE_ALLOWED_ROOTS: projectRoot, + DEVSPACE_AGENT_DIR: agentDir, + DEVSPACE_SKILL_PATHS: [explicitSkills, join(projectRoot, ".pi", "skills")].join(","), + DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", + PORT: "1", + }); + assert.equal( + loadWorkspaceSkills(legacyPiConfig, projectRoot).skills.some((skill) => skill.name === "project-skill"), + true, + ); + + const projectSkill = loaded.skills.find((skill) => skill.name === "agent-project-skill"); assert.ok(projectSkill); assert.match(formatPathForPrompt(projectSkill.filePath), /SKILL\.md$/); diff --git a/src/skills.ts b/src/skills.ts index 1b1dbc58..9a1141a1 100644 --- a/src/skills.ts +++ b/src/skills.ts @@ -24,6 +24,7 @@ export function effectiveSkillPaths(config: ServerConfig, cwd: string): string[] const defaultPaths = [ join(homedir(), ".agents", "skills"), resolve(cwd, ".agents", "skills"), + join(config.agentDir, "skills"), ].filter((path) => existsSync(path)); const seen = new Set(); @@ -42,7 +43,7 @@ export function loadWorkspaceSkills(config: ServerConfig, cwd: string): LoadedSk cwd, agentDir: config.agentDir, skillPaths: effectiveSkillPaths(config, cwd), - includeDefaults: true, + includeDefaults: false, }); } From 7d360cc1f1074f59c90ecc93a374c65f436a801b Mon Sep 17 00:00:00 2001 From: Waishnav Date: Mon, 29 Jun 2026 19:24:30 +0530 Subject: [PATCH 4/6] docs(skills): mark pi project skills as opt-in --- docs/chatgpt-coding-workflow.md | 3 ++- docs/configuration.md | 3 ++- docs/gotchas.md | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/chatgpt-coding-workflow.md b/docs/chatgpt-coding-workflow.md index 1def8a75..13269f52 100644 --- a/docs/chatgpt-coding-workflow.md +++ b/docs/chatgpt-coding-workflow.md @@ -87,9 +87,10 @@ DevSpace discovers standard Agent Skills from: It also keeps compatibility with: - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` -- project `.pi/skills` - additional paths from `DEVSPACE_SKILL_PATHS` +Legacy project paths such as `.pi/skills` can be added through `DEVSPACE_SKILL_PATHS` when needed. + When `open_workspace` returns matching skills, the model should read the advertised `SKILL.md` before following that skill. diff --git a/docs/configuration.md b/docs/configuration.md index a731cba1..32293631 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -110,9 +110,10 @@ DevSpace discovers standard Agent Skills from: It also keeps compatibility with: - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` -- project `.pi/skills` - additional paths from `DEVSPACE_SKILL_PATHS` +Legacy project paths such as `.pi/skills` can be added through `DEVSPACE_SKILL_PATHS` when needed. + Example: ```bash diff --git a/docs/gotchas.md b/docs/gotchas.md index 0ef8a7f4..c6ef1d2e 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -201,9 +201,10 @@ DevSpace looks in standard Agent Skills locations: It also checks compatibility and custom paths: - `DEVSPACE_AGENT_DIR/skills`, defaulting to `~/.codex/skills` -- project `.pi/skills` - additional paths from `DEVSPACE_SKILL_PATHS` +Legacy project paths such as `.pi/skills` can be added through `DEVSPACE_SKILL_PATHS` when needed. + If a skill appears in `open_workspace`, the model must read that skill's `SKILL.md` before reading other files inside the skill directory. From 3ea42e105f807badff291e0de5f2a68b94a5d3ac Mon Sep 17 00:00:00 2001 From: Waishnav Date: Mon, 29 Jun 2026 19:28:06 +0530 Subject: [PATCH 5/6] test(skills): set windows home fixture --- src/skills.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/skills.test.ts b/src/skills.test.ts index f8af6c1d..669cb9fc 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -12,9 +12,11 @@ import { const root = await mkdtemp(join(tmpdir(), "devspace-skills-test-")); const originalHome = process.env.HOME; +const originalUserProfile = process.env.USERPROFILE; try { process.env.HOME = root; + process.env.USERPROFILE = root; const projectRoot = join(root, "project"); const agentDir = join(root, "agent"); const explicitSkills = join(root, "explicit-skills"); @@ -163,5 +165,7 @@ try { } finally { if (originalHome === undefined) delete process.env.HOME; else process.env.HOME = originalHome; + if (originalUserProfile === undefined) delete process.env.USERPROFILE; + else process.env.USERPROFILE = originalUserProfile; await rm(root, { recursive: true, force: true }); } From 4ab5e857c42a4234407c5c776b684bf47ab07bf4 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Mon, 29 Jun 2026 19:39:09 +0530 Subject: [PATCH 6/6] fix(skills): resolve configured paths per workspace --- src/config.ts | 3 +-- src/skills.test.ts | 32 ++++++++++++++++++++++++++++++-- src/skills.ts | 17 +++++++++++------ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/config.ts b/src/config.ts index 5bf96f87..d065b17c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -110,8 +110,7 @@ function parsePathList(value: string | undefined): string[] { value ?.split(",") .map((entry) => entry.trim()) - .filter(Boolean) - .map((entry) => resolve(expandHomePath(entry))) ?? [] + .filter(Boolean) ?? [] ); } diff --git a/src/skills.test.ts b/src/skills.test.ts index 669cb9fc..16a49c8a 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -22,8 +22,12 @@ try { const explicitSkills = join(root, "explicit-skills"); const globalAgentsSkills = join(root, ".agents", "skills"); const projectAgentsSkills = join(projectRoot, ".agents", "skills"); + const globalClaudeSkills = join(root, ".claude", "skills"); + const projectClaudeSkills = join(projectRoot, ".claude", "skills"); await mkdir(join(globalAgentsSkills, "agent-global-skill"), { recursive: true }); await mkdir(join(projectAgentsSkills, "agent-project-skill"), { recursive: true }); + await mkdir(join(globalClaudeSkills, "claude-global-skill"), { recursive: true }); + await mkdir(join(projectClaudeSkills, "claude-project-skill"), { recursive: true }); await mkdir(join(projectRoot, ".pi", "skills", "project-skill"), { recursive: true }); await mkdir(join(agentDir, "skills", "global-skill"), { recursive: true }); await mkdir(join(explicitSkills, "duplicate"), { recursive: true }); @@ -51,6 +55,28 @@ try { "# Agent Project Skill", ].join("\n"), ); + await writeFile( + join(globalClaudeSkills, "claude-global-skill", "SKILL.md"), + [ + "---", + "name: claude-global-skill", + "description: Claude global skill description.", + "---", + "", + "# Claude Global Skill", + ].join("\n"), + ); + await writeFile( + join(projectClaudeSkills, "claude-project-skill", "SKILL.md"), + [ + "---", + "name: claude-project-skill", + "description: Claude project skill description.", + "---", + "", + "# Claude Project Skill", + ].join("\n"), + ); await writeFile( join(projectRoot, ".pi", "skills", "project-skill", "SKILL.md"), [ @@ -110,13 +136,15 @@ try { const config = loadConfig({ DEVSPACE_ALLOWED_ROOTS: projectRoot, DEVSPACE_AGENT_DIR: agentDir, - DEVSPACE_SKILL_PATHS: explicitSkills, + DEVSPACE_SKILL_PATHS: [explicitSkills, "~/.claude/skills", "./.claude/skills"].join(","), DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", PORT: "1", }); const loaded = loadWorkspaceSkills(config, projectRoot); assert.equal(loaded.skills.some((skill) => skill.name === "agent-global-skill"), true); assert.equal(loaded.skills.some((skill) => skill.name === "agent-project-skill"), true); + assert.equal(loaded.skills.some((skill) => skill.name === "claude-global-skill"), true); + assert.equal(loaded.skills.some((skill) => skill.name === "claude-project-skill"), true); assert.equal(loaded.skills.some((skill) => skill.name === "project-skill"), false); assert.equal(loaded.skills.filter((skill) => skill.name === "duplicate-skill").length, 1); assert.equal(loaded.skills.some((skill) => skill.name === "hidden-skill"), true); @@ -125,7 +153,7 @@ try { const duplicateConfig = loadConfig({ DEVSPACE_ALLOWED_ROOTS: projectRoot, DEVSPACE_AGENT_DIR: agentDir, - DEVSPACE_SKILL_PATHS: [explicitSkills, projectAgentsSkills].join(","), + DEVSPACE_SKILL_PATHS: [explicitSkills, "./.agents/skills"].join(","), DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough", PORT: "1", }); diff --git a/src/skills.ts b/src/skills.ts index 9a1141a1..e3da62ff 100644 --- a/src/skills.ts +++ b/src/skills.ts @@ -28,12 +28,17 @@ export function effectiveSkillPaths(config: ServerConfig, cwd: string): string[] ].filter((path) => existsSync(path)); const seen = new Set(); - return [...defaultPaths, ...config.skillPaths].filter((path) => { - const resolvedPath = resolve(expandHomePath(path)); - if (seen.has(resolvedPath)) return false; - seen.add(resolvedPath); - return true; - }); + return [...defaultPaths, ...config.skillPaths] + .map((path) => resolveSkillPath(path, cwd)) + .filter((path) => { + if (seen.has(path)) return false; + seen.add(path); + return true; + }); +} + +function resolveSkillPath(path: string, cwd: string): string { + return resolve(cwd, expandHomePath(path)); } export function loadWorkspaceSkills(config: ServerConfig, cwd: string): LoadedSkills {