Skip to content

Commit 7482f1f

Browse files
Apply PR #11842: feat: add support for reading skills from .agents/skills directories
2 parents c635a8b + 2bee316 commit 7482f1f

File tree

3 files changed

+131
-29
lines changed

3 files changed

+131
-29
lines changed

packages/opencode/src/flag/flag.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export namespace Flag {
2323
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT")
2424
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
2525
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
26+
export const OPENCODE_DISABLE_EXTERNAL_SKILLS =
27+
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS || truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS")
2628
export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean
2729
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
2830
export declare const OPENCODE_CLIENT: string

packages/opencode/src/skill/skill.ts

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ export namespace Skill {
4040
}),
4141
)
4242

43+
// External skill patterns to search for (project-level and global)
44+
const EXTERNAL_PATTERNS = [".claude/skills/**/SKILL.md", ".agents/skills/**/SKILL.md"]
45+
4346
const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
44-
const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
4547
const SKILL_GLOB = new Bun.Glob("**/SKILL.md")
4648

4749
export const state = Instance.state(async () => {
@@ -79,36 +81,27 @@ export namespace Skill {
7981
}
8082
}
8183

82-
// Scan .claude/skills/ directories (project-level)
83-
const claudeDirs = await Array.fromAsync(
84-
Filesystem.up({
85-
targets: [".claude"],
86-
start: Instance.directory,
87-
stop: Instance.worktree,
88-
}),
89-
)
90-
// Also include global ~/.claude/skills/
91-
const globalClaude = `${Global.Path.home}/.claude`
92-
if (await Filesystem.isDir(globalClaude)) {
93-
claudeDirs.push(globalClaude)
94-
}
84+
// Scan external skill directories (.claude/skills/, .agents/skills/, etc.)
85+
// Load global (home) first, then project-level (so project-level overwrites)
86+
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
87+
for (const pattern of EXTERNAL_PATTERNS) {
88+
// Scan global home directory for external skills first
89+
const glob = new Bun.Glob(pattern)
90+
for await (const match of glob.scan({
91+
cwd: Global.Path.home,
92+
absolute: true,
93+
onlyFiles: true,
94+
followSymlinks: true,
95+
dot: true,
96+
})) {
97+
await addSkill(match)
98+
}
9599

96-
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS) {
97-
for (const dir of claudeDirs) {
98-
const matches = await Array.fromAsync(
99-
CLAUDE_SKILL_GLOB.scan({
100-
cwd: dir,
101-
absolute: true,
102-
onlyFiles: true,
103-
followSymlinks: true,
104-
dot: true,
105-
}),
106-
).catch((error) => {
107-
log.error("failed .claude directory scan for skills", { dir, error })
100+
// Then walk up from current directory to find project-level skills (overwrites globals)
101+
for (const match of await Filesystem.globUp(pattern, Instance.directory, Instance.worktree).catch((error) => {
102+
log.error("failed to scan project directories for skills", { pattern, error })
108103
return []
109-
})
110-
111-
for (const match of matches) {
104+
})) {
112105
await addSkill(match)
113106
}
114107
}

packages/opencode/test/skill/skill.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,110 @@ test("returns empty array when no skills exist", async () => {
183183
},
184184
})
185185
})
186+
187+
test("discovers skills from .agents/skills/ directory", async () => {
188+
await using tmp = await tmpdir({
189+
git: true,
190+
init: async (dir) => {
191+
const skillDir = path.join(dir, ".agents", "skills", "agent-skill")
192+
await Bun.write(
193+
path.join(skillDir, "SKILL.md"),
194+
`---
195+
name: agent-skill
196+
description: A skill in the .agents/skills directory.
197+
---
198+
199+
# Agent Skill
200+
`,
201+
)
202+
},
203+
})
204+
205+
await Instance.provide({
206+
directory: tmp.path,
207+
fn: async () => {
208+
const skills = await Skill.all()
209+
expect(skills.length).toBe(1)
210+
const agentSkill = skills.find((s) => s.name === "agent-skill")
211+
expect(agentSkill).toBeDefined()
212+
expect(agentSkill!.location).toContain(".agents/skills/agent-skill/SKILL.md")
213+
},
214+
})
215+
})
216+
217+
test("discovers global skills from ~/.agents/skills/ directory", async () => {
218+
await using tmp = await tmpdir({ git: true })
219+
220+
const originalHome = process.env.OPENCODE_TEST_HOME
221+
process.env.OPENCODE_TEST_HOME = tmp.path
222+
223+
try {
224+
const skillDir = path.join(tmp.path, ".agents", "skills", "global-agent-skill")
225+
await fs.mkdir(skillDir, { recursive: true })
226+
await Bun.write(
227+
path.join(skillDir, "SKILL.md"),
228+
`---
229+
name: global-agent-skill
230+
description: A global skill from ~/.agents/skills for testing.
231+
---
232+
233+
# Global Agent Skill
234+
235+
This skill is loaded from the global home directory.
236+
`,
237+
)
238+
239+
await Instance.provide({
240+
directory: tmp.path,
241+
fn: async () => {
242+
const skills = await Skill.all()
243+
expect(skills.length).toBe(1)
244+
expect(skills[0].name).toBe("global-agent-skill")
245+
expect(skills[0].description).toBe("A global skill from ~/.agents/skills for testing.")
246+
expect(skills[0].location).toContain(".agents/skills/global-agent-skill/SKILL.md")
247+
},
248+
})
249+
} finally {
250+
process.env.OPENCODE_TEST_HOME = originalHome
251+
}
252+
})
253+
254+
test("discovers skills from both .claude/skills/ and .agents/skills/", async () => {
255+
await using tmp = await tmpdir({
256+
git: true,
257+
init: async (dir) => {
258+
const claudeDir = path.join(dir, ".claude", "skills", "claude-skill")
259+
const agentDir = path.join(dir, ".agents", "skills", "agent-skill")
260+
await Bun.write(
261+
path.join(claudeDir, "SKILL.md"),
262+
`---
263+
name: claude-skill
264+
description: A skill in the .claude/skills directory.
265+
---
266+
267+
# Claude Skill
268+
`,
269+
)
270+
await Bun.write(
271+
path.join(agentDir, "SKILL.md"),
272+
`---
273+
name: agent-skill
274+
description: A skill in the .agents/skills directory.
275+
---
276+
277+
# Agent Skill
278+
`,
279+
)
280+
},
281+
})
282+
283+
await Instance.provide({
284+
directory: tmp.path,
285+
fn: async () => {
286+
const skills = await Skill.all()
287+
expect(skills.length).toBe(2)
288+
expect(skills.find((s) => s.name === "claude-skill")).toBeDefined()
289+
expect(skills.find((s) => s.name === "agent-skill")).toBeDefined()
290+
},
291+
})
292+
})

0 commit comments

Comments
 (0)