From d8ade52a88c26b54a88ab94f4114f1ab7071199f Mon Sep 17 00:00:00 2001 From: "Marcus R. Brown" Date: Thu, 26 Mar 2026 16:55:01 -0700 Subject: [PATCH] feat: register bundled skills via config.skills.paths for native discovery Add skills.paths registration in the config hook so OpenCode's built-in skill tool can discover Systematic skills natively, alongside the existing slash command registration. This follows the pattern established by the Superpowers plugin (obra/superpowers) and uses the skills.paths property from the OpenCode v2 SDK types. A local type extension bridges the gap until we upgrade from v1 imports. Changes: - config-handler.ts: add registerSkillsPaths() after command merging - opencode.test.ts: 3 new integration tests (path registered, existing paths preserved, no duplicates on repeated calls) Verification: - Build: exit 0 - Typecheck: exit 0 - Lint: 54 files, no fixes - Unit tests: 305/305 pass - Integration tests: 22/22 pass (3 new) --- src/lib/config-handler.ts | 18 ++++++++++ tests/integration/opencode.test.ts | 54 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/lib/config-handler.ts b/src/lib/config-handler.ts index 32f7bba..f4a639d 100644 --- a/src/lib/config-handler.ts +++ b/src/lib/config-handler.ts @@ -252,5 +252,23 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { ...bundledSkills, ...existingCommands, } + + // skills.paths exists at runtime (v2 SDK types) but not in our v1 import + registerSkillsPaths(config, bundledSkillsDir) + } +} + +// Config.skills exists in v2 SDK types but not v1 — bridge until import upgrade +type ConfigWithSkills = Config & { + skills?: { paths?: string[] } +} + +/** Register a directory for OpenCode's native skill discovery (`skill` tool). */ +export function registerSkillsPaths(config: Config, skillsDir: string): void { + const extended = config as ConfigWithSkills + extended.skills ??= {} + extended.skills.paths ??= [] + if (!extended.skills.paths.includes(skillsDir)) { + extended.skills.paths.push(skillsDir) } } diff --git a/tests/integration/opencode.test.ts b/tests/integration/opencode.test.ts index f05b3df..5d3cf57 100644 --- a/tests/integration/opencode.test.ts +++ b/tests/integration/opencode.test.ts @@ -263,6 +263,60 @@ Integration test content.`, 'description: A skill for integration testing', ) }) + + test('registers bundled skills dir in config.skills.paths', async () => { + const skillsDir = path.join(testEnv.bundledDir, 'skills') + const handler = createConfigHandler({ + directory: testEnv.projectDir, + bundledSkillsDir: skillsDir, + bundledAgentsDir: path.join(testEnv.bundledDir, 'agents'), + bundledCommandsDir: path.join(testEnv.bundledDir, 'commands'), + }) + + const config: Config = {} + await handler(config) + + const extended = config as Config & { skills?: { paths?: string[] } } + expect(extended.skills?.paths).toContain(skillsDir) + }) + + test('preserves existing skills.paths entries', async () => { + const skillsDir = path.join(testEnv.bundledDir, 'skills') + const handler = createConfigHandler({ + directory: testEnv.projectDir, + bundledSkillsDir: skillsDir, + bundledAgentsDir: path.join(testEnv.bundledDir, 'agents'), + bundledCommandsDir: path.join(testEnv.bundledDir, 'commands'), + }) + + const existingPath = '/some/other/skills' + const config = { skills: { paths: [existingPath] } } as Config & { + skills?: { paths?: string[] } + } + await handler(config as Config) + + const extended = config as Config & { skills?: { paths?: string[] } } + expect(extended.skills?.paths).toContain(existingPath) + expect(extended.skills?.paths).toContain(skillsDir) + }) + + test('does not duplicate skills.paths on repeated calls', async () => { + const skillsDir = path.join(testEnv.bundledDir, 'skills') + const handler = createConfigHandler({ + directory: testEnv.projectDir, + bundledSkillsDir: skillsDir, + bundledAgentsDir: path.join(testEnv.bundledDir, 'agents'), + bundledCommandsDir: path.join(testEnv.bundledDir, 'commands'), + }) + + const config: Config = {} + await handler(config) + await handler(config) + + const extended = config as Config & { skills?: { paths?: string[] } } + const count = extended.skills?.paths?.filter((p) => p === skillsDir).length + expect(count).toBe(1) + }) }) describe('opencode availability check', () => {