From 043e84be56460977997c495242a87e5ae5d4c0d0 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 13:43:08 +0900 Subject: [PATCH 1/3] fix: add adaptHostSkillConfig utility for host config.skills.paths Converts the host OpenCode config.skills object (with paths/urls arrays set by other plugins like superpowers) into the SkillsConfig format used by discoverConfigSourceSkills. Filters blank/whitespace entries and non-string values. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/host-skill-config.test.ts | 80 ++++++++++++++++++++++++++++ src/shared/host-skill-config.ts | 28 ++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/shared/host-skill-config.test.ts create mode 100644 src/shared/host-skill-config.ts diff --git a/src/shared/host-skill-config.test.ts b/src/shared/host-skill-config.test.ts new file mode 100644 index 000000000..68564dc4a --- /dev/null +++ b/src/shared/host-skill-config.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, test } from "bun:test" + +import { adaptHostSkillConfig } from "./host-skill-config" + +describe("adaptHostSkillConfig", () => { + test("converts paths and urls into SkillsConfig sources", () => { + // given + const hostConfig = { + paths: ["/host/skills", "/other/skills"], + urls: ["https://example.com/skills/"], + } + + // when + const result = adaptHostSkillConfig(hostConfig) + + // then + expect(result).toEqual({ + sources: ["/host/skills", "/other/skills", "https://example.com/skills/"], + }) + }) + + test("filters blank and whitespace-only entries", () => { + // given + const hostConfig = { + paths: ["", " ", "/real/skills"], + urls: ["\n", "https://example.com/skills/"], + } + + // when + const result = adaptHostSkillConfig(hostConfig) + + // then + expect(result).toEqual({ + sources: ["/real/skills", "https://example.com/skills/"], + }) + }) + + test("returns undefined when no usable sources remain", () => { + // when + const result = adaptHostSkillConfig({ paths: ["", " "], urls: ["\t"] }) + + // then + expect(result).toBeUndefined() + }) + + test("returns undefined for null input", () => { + expect(adaptHostSkillConfig(null)).toBeUndefined() + }) + + test("returns undefined for undefined input", () => { + expect(adaptHostSkillConfig(undefined)).toBeUndefined() + }) + + test("returns undefined for non-object input", () => { + expect(adaptHostSkillConfig("string")).toBeUndefined() + }) + + test("handles missing paths or urls gracefully", () => { + // when - only paths + const pathsOnly = adaptHostSkillConfig({ paths: ["/skills"] }) + expect(pathsOnly).toEqual({ sources: ["/skills"] }) + + // when - only urls + const urlsOnly = adaptHostSkillConfig({ urls: ["https://example.com/skills/"] }) + expect(urlsOnly).toEqual({ sources: ["https://example.com/skills/"] }) + }) + + test("ignores non-string array elements", () => { + // given + const hostConfig = { + paths: ["/valid", 42, null, true, "/also-valid"], + } + + // when + const result = adaptHostSkillConfig(hostConfig) + + // then + expect(result).toEqual({ sources: ["/valid", "/also-valid"] }) + }) +}) diff --git a/src/shared/host-skill-config.ts b/src/shared/host-skill-config.ts new file mode 100644 index 000000000..9ab72ea3a --- /dev/null +++ b/src/shared/host-skill-config.ts @@ -0,0 +1,28 @@ +import type { SkillsConfig } from "../config/schema/skills" + +type HostSkillConfig = { + paths?: unknown + urls?: unknown +} + +function toStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value + .filter((item): item is string => typeof item === "string") + .map((item) => item.trim()) + .filter((item) => item.length > 0) +} + +export function adaptHostSkillConfig(value: unknown): SkillsConfig | undefined { + if (!value || typeof value !== "object") return undefined + + const hostSkillConfig = value as HostSkillConfig + const sources = [ + ...toStringArray(hostSkillConfig.paths), + ...toStringArray(hostSkillConfig.urls), + ] + + if (sources.length === 0) return undefined + + return { sources } as SkillsConfig +} From 1412795825efc3aec5c08c58c9410909dccf116a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 13:43:38 +0900 Subject: [PATCH 2/3] fix: wire host config.skills.paths into agent skill discovery When another plugin (e.g. superpowers) injects skill directories via config.skills.paths in the config hook, the agent-config-handler now discovers those skills via a second discoverConfigSourceSkills call using the adapted host config. Fixes #3396. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...agent-config-handler-agents-skills.test.ts | 46 +++++++++++++++++++ src/plugin-handlers/agent-config-handler.ts | 8 ++++ 2 files changed, 54 insertions(+) diff --git a/src/plugin-handlers/agent-config-handler-agents-skills.test.ts b/src/plugin-handlers/agent-config-handler-agents-skills.test.ts index 4bb94ce41..603cfb761 100644 --- a/src/plugin-handlers/agent-config-handler-agents-skills.test.ts +++ b/src/plugin-handlers/agent-config-handler-agents-skills.test.ts @@ -122,4 +122,50 @@ describe("applyAgentConfig .agents skills", () => { expect(discoveredSkills.map(skill => skill.name)).toContain("project-agent-skill") expect(discoveredSkills.map(skill => skill.name)).toContain("global-agent-skill") }) + + test("discovers skills from host config.skills.paths set by other plugins", async () => { + // given - second call to discoverConfigSourceSkills returns host config skills + discoverConfigSourceSkillsSpy + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { + name: "host-config-skill", + definition: { name: "host-config-skill", template: "host-template" }, + scope: "config", + }, + ]) + + // when + await applyAgentConfig({ + config: { + model: "anthropic/claude-opus-4-6", + agent: {}, + skills: { paths: ["/host/skills"] }, + }, + pluginConfig: createPluginConfig(), + ctx: { directory: "/tmp/project" }, + pluginComponents: createPluginComponents(), + }) + + // then + const discoveredSkills = createBuiltinAgentsSpy.mock.calls[0]?.[6] as Array<{ name: string }> + expect(discoveredSkills.map(skill => skill.name)).toContain("host-config-skill") + }) + + test("calls discoverConfigSourceSkills twice when host config has skills", async () => { + // when + await applyAgentConfig({ + config: { + model: "anthropic/claude-opus-4-6", + agent: {}, + skills: { paths: ["/host/skills"] }, + }, + pluginConfig: createPluginConfig(), + ctx: { directory: "/tmp/project" }, + pluginComponents: createPluginComponents(), + }) + + // then - called twice: once for pluginConfig.skills, once for host config.skills + expect(discoverConfigSourceSkillsSpy).toHaveBeenCalledTimes(2) + }) }) diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index 9d5c3b2ca..ff83d6900 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -35,6 +35,7 @@ import { } from "./agent-override-protection"; import { buildPrometheusAgentConfig } from "./prometheus-agent-config-builder"; import { buildPlanDemoteConfig } from "./plan-model-inheritance"; +import { adaptHostSkillConfig } from "../shared/host-skill-config"; type AgentConfigRecord = Record | undefined> & { build?: Record; @@ -61,8 +62,10 @@ export async function applyAgentConfig(params: { ) as typeof params.pluginConfig.disabled_agents; const includeClaudeSkillsForAwareness = params.pluginConfig.claude_code?.skills ?? true; + const hostSkillConfig = adaptHostSkillConfig(params.config.skills); const [ discoveredConfigSourceSkills, + discoveredHostConfigSkills, discoveredUserSkills, discoveredProjectSkills, discoveredProjectAgentsSkills, @@ -74,6 +77,10 @@ export async function applyAgentConfig(params: { config: params.pluginConfig.skills, configDir: params.ctx.directory, }), + discoverConfigSourceSkills({ + config: hostSkillConfig, + configDir: params.ctx.directory, + }), includeClaudeSkillsForAwareness ? discoverUserClaudeSkills() : Promise.resolve([]), includeClaudeSkillsForAwareness ? discoverProjectClaudeSkills(params.ctx.directory) @@ -88,6 +95,7 @@ export async function applyAgentConfig(params: { const allDiscoveredSkills = [ ...discoveredConfigSourceSkills, + ...discoveredHostConfigSkills, ...discoveredOpencodeProjectSkills, ...discoveredProjectSkills, ...discoveredProjectAgentsSkills, From ecb87608e683b11a460af400cf064dc947db5644 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 18 May 2026 13:43:56 +0900 Subject: [PATCH 3/3] fix: wire host config.skills.paths into command skill discovery Mirrors the agent-config-handler change: command-config-handler now also discovers skills from host config.skills.paths set by other plugins, making them available as slash commands. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../command-config-handler.test.ts | 33 +++++++++++++++++++ src/plugin-handlers/command-config-handler.ts | 8 +++++ 2 files changed, 41 insertions(+) diff --git a/src/plugin-handlers/command-config-handler.test.ts b/src/plugin-handlers/command-config-handler.test.ts index fa39cea1b..63837559e 100644 --- a/src/plugin-handlers/command-config-handler.test.ts +++ b/src/plugin-handlers/command-config-handler.test.ts @@ -157,4 +157,37 @@ describe("applyCommandConfig", () => { const commandConfig = config.command as Record; expect(commandConfig["start-work"]?.agent).toBe(getAgentListDisplayName("atlas")); }); + + test("includes host config skills declared in config.skills.paths by other plugins", async () => { + // given - second call to discoverConfigSourceSkills returns host config skills + discoverConfigSourceSkillsSpy + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { + name: "host-config-skill", + definition: { + name: "host-config-skill", + description: "Host config skill", + template: "template", + }, + scope: "config", + }, + ]); + const config: Record = { + command: {}, + skills: { paths: ["/host/skills"] }, + }; + + // when + await applyCommandConfig({ + config, + pluginConfig: createPluginConfig(), + ctx: { directory: "/tmp" }, + pluginComponents: createPluginComponents(), + }); + + // then + const commandConfig = config.command as Record; + expect(commandConfig["host-config-skill"]?.description).toContain("Host config skill"); + }); }); diff --git a/src/plugin-handlers/command-config-handler.ts b/src/plugin-handlers/command-config-handler.ts index 3d5fafd2c..c0189ba78 100644 --- a/src/plugin-handlers/command-config-handler.ts +++ b/src/plugin-handlers/command-config-handler.ts @@ -26,6 +26,7 @@ import { log, } from "../shared"; import type { PluginComponents } from "./plugin-components-loader"; +import { adaptHostSkillConfig } from "../shared/host-skill-config"; export async function applyCommandConfig(params: { config: Record; @@ -47,8 +48,10 @@ export async function applyCommandConfig(params: { log(getSkillPluginConflictWarning(externalSkillPlugin.pluginName)); } + const hostSkillConfig = adaptHostSkillConfig(params.config.skills); const [ configSourceSkills, + hostConfigSkills, userCommands, projectCommands, opencodeGlobalCommands, @@ -64,6 +67,10 @@ export async function applyCommandConfig(params: { config: params.pluginConfig.skills, configDir: params.ctx.directory, }), + discoverConfigSourceSkills({ + config: hostSkillConfig, + configDir: params.ctx.directory, + }), includeClaudeCommands ? loadUserCommands() : Promise.resolve({}), includeClaudeCommands ? loadProjectCommands(params.ctx.directory) : Promise.resolve({}), loadOpencodeGlobalCommands(), @@ -79,6 +86,7 @@ export async function applyCommandConfig(params: { params.config.command = { ...builtinCommands, ...skillsToCommandDefinitionRecord(configSourceSkills), + ...skillsToCommandDefinitionRecord(hostConfigSkills), ...userCommands, ...userSkills, ...globalAgentsSkills,