From aeb4419172af9ef51fc3fdb4629fe6e00b099897 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 17:18:37 +0900 Subject: [PATCH 1/2] fix(agents): resolve skills after agent overrides Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/agents/agent-builder.ts | 17 ++------------ src/agents/agent-skill-resolution.ts | 26 +++++++++++++++++++++ src/agents/builtin-agents/general-agents.ts | 4 +++- src/agents/types.ts | 3 +++ 4 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 src/agents/agent-skill-resolution.ts diff --git a/src/agents/agent-builder.ts b/src/agents/agent-builder.ts index f60f8137b..5747bb841 100644 --- a/src/agents/agent-builder.ts +++ b/src/agents/agent-builder.ts @@ -1,9 +1,7 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentFactory } from "./types" -import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../config/schema" -import type { BrowserAutomationProvider } from "../config/schema" +import type { CategoriesConfig, CategoryConfig } from "../config/schema" import { mergeCategories } from "../shared/merge-categories" -import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content" export type AgentSource = AgentFactory | AgentConfig @@ -14,10 +12,7 @@ export function isFactory(source: AgentSource): source is AgentFactory { export function buildAgent( source: AgentSource, model: string, - categories?: CategoriesConfig, - gitMasterConfig?: GitMasterConfig, - browserProvider?: BrowserAutomationProvider, - disabledSkills?: Set + categories?: CategoriesConfig ): AgentConfig { const base = isFactory(source) ? source(model) : { ...source } const categoryConfigs: Record = mergeCategories(categories) @@ -38,13 +33,5 @@ export function buildAgent( } } - if (agentWithCategory.skills?.length) { - const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider, disabledSkills }) - if (resolved.size > 0) { - const skillContent = Array.from(resolved.values()).join("\n\n") - base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "") - } - } - return base } diff --git a/src/agents/agent-skill-resolution.ts b/src/agents/agent-skill-resolution.ts new file mode 100644 index 000000000..3713cca0f --- /dev/null +++ b/src/agents/agent-skill-resolution.ts @@ -0,0 +1,26 @@ +import type { AgentConfig } from "@opencode-ai/sdk" +import type { BrowserAutomationProvider, GitMasterConfig } from "../config/schema" +import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content" + +type AgentConfigWithSkills = AgentConfig & { skills?: string[] } + +export function resolveAgentSkills( + config: AgentConfig, + options: { + gitMasterConfig?: GitMasterConfig + browserProvider?: BrowserAutomationProvider + disabledSkills?: Set + } = {} +): AgentConfig { + const { skills, ...configWithoutSkills } = config as AgentConfigWithSkills + if (!skills?.length) return configWithoutSkills + + const { resolved } = resolveMultipleSkills(skills, options) + if (resolved.size === 0) return configWithoutSkills + + const skillContent = Array.from(resolved.values()).join("\n\n") + return { + ...configWithoutSkills, + prompt: skillContent + (configWithoutSkills.prompt ? "\n\n" + configWithoutSkills.prompt : ""), + } +} diff --git a/src/agents/builtin-agents/general-agents.ts b/src/agents/builtin-agents/general-agents.ts index 7d9d52979..fd05402a1 100644 --- a/src/agents/builtin-agents/general-agents.ts +++ b/src/agents/builtin-agents/general-agents.ts @@ -5,6 +5,7 @@ import type { BrowserAutomationProvider } from "../../config/schema" import type { AvailableAgent } from "../dynamic-agent-prompt-builder" import { AGENT_MODEL_REQUIREMENTS, isModelAvailable } from "../../shared" import { buildAgent, isFactory } from "../agent-builder" +import { resolveAgentSkills } from "../agent-skill-resolution" import { applyOverrides } from "./agent-overrides" import { applyEnvironmentContext } from "./environment-context" import { applyModelResolution, getFirstFallbackModel } from "./model-resolution" @@ -92,7 +93,7 @@ export function collectPendingBuiltinAgents(input: { if (!resolution) continue const { model, variant: resolvedVariant } = resolution - let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider, disabledSkills) + let config = buildAgent(source, model, mergedCategories) // Apply resolved variant from model fallback chain if (resolvedVariant) { @@ -104,6 +105,7 @@ export function collectPendingBuiltinAgents(input: { } config = applyOverrides(config, override, mergedCategories, directory) + config = resolveAgentSkills(config, { gitMasterConfig, browserProvider, disabledSkills }) // Store for later - will be added after sisyphus and hephaestus pendingAgentConfigs.set(name, config) diff --git a/src/agents/types.ts b/src/agents/types.ts index 79d3d7cd9..8afe7453d 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -138,7 +138,10 @@ export type OverridableAgentName = "build" | BuiltinAgentName; export type AgentName = BuiltinAgentName; export type AgentOverrideConfig = Partial & { + category?: string; prompt_append?: string; + skills?: string[]; + tools?: Record; variant?: string; fallback_models?: string | (string | import("../config/schema/fallback-models").FallbackModelObject)[]; }; From dbf0bb9b4ccea766921fcc68f9e318e5495c3c7d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 27 Apr 2026 17:18:37 +0900 Subject: [PATCH 2/2] test(agents): cover skills override prompt injection Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/agents/utils.test.ts | 66 +++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 192fec2dd..69ada729b 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -2,6 +2,8 @@ import { describe, test, expect, beforeEach, afterEach, spyOn, mock } from "bun:test" import type { AgentConfig } from "@opencode-ai/sdk" +import type { AgentOverrides } from "./types" +import { resolveAgentSkills } from "./agent-skill-resolution" import { clearSkillCache } from "../features/opencode-skill-loader/skill-content" import * as connectedProvidersCache from "../shared/connected-providers-cache" import * as modelAvailability from "../shared/model-availability" @@ -1060,7 +1062,7 @@ describe("buildAgent with category and skills", () => { } // #when - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then - category's built-in model is applied expect(agent.model).toBe("google/gemini-3.1-pro") @@ -1078,7 +1080,7 @@ describe("buildAgent with category and skills", () => { } // #when - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then - explicit model takes precedence over category expect(agent.model).toBe("custom/model") @@ -1121,7 +1123,7 @@ describe("buildAgent with category and skills", () => { } // #when - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then expect(agent.prompt).toContain("Role: Designer-Turned-Developer") @@ -1141,7 +1143,7 @@ describe("buildAgent with category and skills", () => { } // #when - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then expect(agent.prompt).toContain("Role: Designer-Turned-Developer") @@ -1161,7 +1163,7 @@ describe("buildAgent with category and skills", () => { } // #when - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then expect(agent.model).toBe("custom/model") @@ -1182,7 +1184,7 @@ describe("buildAgent with category and skills", () => { } // #when - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then - category's built-in model and skills are applied expect(agent.model).toBe("openai/gpt-5.5") @@ -1203,7 +1205,7 @@ describe("buildAgent with category and skills", () => { } // #when - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then // Note: The factory receives model, but if category doesn't exist, it's not applied @@ -1224,7 +1226,7 @@ describe("buildAgent with category and skills", () => { } // #when - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then expect(agent.prompt).toContain("Role: Designer-Turned-Developer") @@ -1261,7 +1263,7 @@ describe("buildAgent with category and skills", () => { } // #when - browserProvider is "agent-browser" - const agent = buildAgent(source["test-agent"], TEST_MODEL, undefined, undefined, "agent-browser") + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL), { browserProvider: "agent-browser" }) // #then - agent-browser skill content should be in prompt expect(agent.prompt).toContain("agent-browser") @@ -1280,7 +1282,7 @@ describe("buildAgent with category and skills", () => { } // #when - no browserProvider (defaults to playwright) - const agent = buildAgent(source["test-agent"], TEST_MODEL) + const agent = resolveAgentSkills(buildAgent(source["test-agent"], TEST_MODEL)) // #then - agent-browser skill not found, only base prompt remains expect(agent.prompt).toBe("Base prompt") @@ -1288,6 +1290,28 @@ describe("buildAgent with category and skills", () => { }) }) +describe("createBuiltinAgents with skill overrides", () => { + test("injects user configured skills into standard agent prompt", async () => { + // #given + const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set()) + const overrides = { + librarian: { skills: ["frontend-ui-ux"] }, + } as AgentOverrides + + try { + // #when + const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL) + + // #then + expect(agents.librarian.prompt).toContain("Role: Designer-Turned-Developer") + expect(agents.librarian.prompt).toContain("THE LIBRARIAN") + expect("skills" in agents.librarian).toBe(false) + } finally { + fetchSpy.mockRestore() + } + }) +}) + describe("override.category expansion in createBuiltinAgents", () => { let providerModelsSpy: ReturnType let fetchSpy: ReturnType @@ -1303,7 +1327,7 @@ describe("override.category expansion in createBuiltinAgents", () => { test("standard agent override with category expands category properties", async () => { // #given const overrides = { - oracle: { category: "ultrabrain" } as any, + oracle: { category: "ultrabrain" }, } // #when @@ -1318,7 +1342,7 @@ describe("override.category expansion in createBuiltinAgents", () => { test("standard agent override with category AND direct variant - direct wins", async () => { // #given - ultrabrain has variant=xhigh, but direct override says "max" const overrides = { - oracle: { category: "ultrabrain", variant: "max" } as any, + oracle: { category: "ultrabrain", variant: "max" }, } // #when @@ -1338,7 +1362,7 @@ describe("override.category expansion in createBuiltinAgents", () => { }, } const overrides = { - oracle: { category: "test-cat", reasoningEffort: "low" } as any, + oracle: { category: "test-cat", reasoningEffort: "low" as const }, } // #when @@ -1358,7 +1382,7 @@ describe("override.category expansion in createBuiltinAgents", () => { }, } const overrides = { - oracle: { category: "reasoning-cat" } as any, + oracle: { category: "reasoning-cat" }, } // #when @@ -1372,7 +1396,7 @@ describe("override.category expansion in createBuiltinAgents", () => { test("sisyphus override with category expands category properties", async () => { // #given const overrides = { - sisyphus: { category: "ultrabrain" } as any, + sisyphus: { category: "ultrabrain" }, } // #when @@ -1387,7 +1411,7 @@ describe("override.category expansion in createBuiltinAgents", () => { test("atlas override with category expands category properties", async () => { // #given const overrides = { - atlas: { category: "ultrabrain" } as any, + atlas: { category: "ultrabrain" }, } // #when @@ -1402,7 +1426,7 @@ describe("override.category expansion in createBuiltinAgents", () => { test("override with non-existent category has no effect on config", async () => { // #given const overrides = { - oracle: { category: "non-existent-category" } as any, + oracle: { category: "non-existent-category" }, } // #when @@ -1430,7 +1454,7 @@ describe("agent override tools migration", () => { test("tools: { x: false } is migrated to permission: { x: deny }", async () => { // #given const overrides = { - explore: { tools: { "jetbrains_*": false } } as any, + explore: { tools: { "jetbrains_*": false } }, } // #when @@ -1445,7 +1469,7 @@ describe("agent override tools migration", () => { test("tools: { x: true } is migrated to permission: { x: allow }", async () => { // #given const overrides = { - librarian: { tools: { "jetbrains_get_*": true } } as any, + librarian: { tools: { "jetbrains_get_*": true } }, } // #when @@ -1460,7 +1484,7 @@ describe("agent override tools migration", () => { test("tools config is removed after migration", async () => { // #given const overrides = { - explore: { tools: { "some_tool": false } } as any, + explore: { tools: { "some_tool": false } }, } // #when @@ -1468,7 +1492,7 @@ describe("agent override tools migration", () => { // #then expect(agents.explore).toBeDefined() - expect((agents.explore as any).tools).toBeUndefined() + expect("tools" in agents.explore).toBe(false) }) })