From 0d0ddefbfe1254123eeda2cdef0e2bcf82a8d083 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 5 Jan 2026 04:50:01 +0900 Subject: [PATCH 1/5] fix: implement proper version-aware permission format for OpenCode v1.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite permission-compat.ts with runtime version detection - createAgentToolRestrictions() returns correct format per version - v1.1.1+ uses permission format, older uses tools format - Add migrateToolsToPermission/migratePermissionToTools helpers - Update test suite for new API 🤖 Generated with assistance of OhMyOpenCode https://github.com/code-yeongyu/oh-my-opencode --- src/shared/permission-compat.test.ts | 211 +++++++++++++-------------- src/shared/permission-compat.ts | 94 ++++++------ 2 files changed, 154 insertions(+), 151 deletions(-) diff --git a/src/shared/permission-compat.test.ts b/src/shared/permission-compat.test.ts index 057da4c..c277ca7 100644 --- a/src/shared/permission-compat.test.ts +++ b/src/shared/permission-compat.test.ts @@ -1,161 +1,158 @@ -import { describe, test, expect } from "bun:test" +import { describe, test, expect, beforeEach, afterEach } from "bun:test" import { - createToolDenyList, - permissionValueToBoolean, - booleanToPermissionValue, - convertToolsToPermission, - convertPermissionToTools, - createAgentRestrictions, + createAgentToolRestrictions, + migrateToolsToPermission, + migratePermissionToTools, + migrateAgentConfig, } from "./permission-compat" +import { setVersionCache, resetVersionCache } from "./opencode-version" describe("permission-compat", () => { - describe("createToolDenyList", () => { - test("creates tools config with all values false", () => { - // #given a list of tool names - const tools = ["write", "edit", "task"] + beforeEach(() => { + resetVersionCache() + }) - // #when creating deny list - const result = createToolDenyList(tools) + afterEach(() => { + resetVersionCache() + }) - // #then all values are false - expect(result).toEqual({ write: false, edit: false, task: false }) + describe("createAgentToolRestrictions", () => { + test("returns permission format for v1.1.1+", () => { + // #given version is 1.1.1 + setVersionCache("1.1.1") + + // #when creating restrictions + const result = createAgentToolRestrictions(["write", "edit"]) + + // #then returns permission format + expect(result).toEqual({ + permission: { write: "deny", edit: "deny" }, + }) }) - test("returns empty object for empty array", () => { - // #given empty array - // #when creating deny list - const result = createToolDenyList([]) + test("returns tools format for versions below 1.1.1", () => { + // #given version is below 1.1.1 + setVersionCache("1.0.150") - // #then returns empty object - expect(result).toEqual({}) + // #when creating restrictions + const result = createAgentToolRestrictions(["write", "edit"]) + + // #then returns tools format + expect(result).toEqual({ + tools: { write: false, edit: false }, + }) + }) + + test("assumes new format when version unknown", () => { + // #given version is null + setVersionCache(null) + + // #when creating restrictions + const result = createAgentToolRestrictions(["write"]) + + // #then returns permission format (assumes new version) + expect(result).toEqual({ + permission: { write: "deny" }, + }) }) }) - describe("permissionValueToBoolean", () => { - test("converts allow to true", () => { - expect(permissionValueToBoolean("allow")).toBe(true) - }) - - test("converts deny to false", () => { - expect(permissionValueToBoolean("deny")).toBe(false) - }) - - test("converts ask to false", () => { - expect(permissionValueToBoolean("ask")).toBe(false) - }) - }) - - describe("booleanToPermissionValue", () => { - test("converts true to allow", () => { - expect(booleanToPermissionValue(true)).toBe("allow") - }) - - test("converts false to deny", () => { - expect(booleanToPermissionValue(false)).toBe("deny") - }) - }) - - describe("convertToolsToPermission", () => { - test("converts boolean tools config to permission format", () => { - // #given tools config with booleans + describe("migrateToolsToPermission", () => { + test("converts boolean tools to permission values", () => { + // #given tools config const tools = { write: false, edit: true, bash: false } - // #when converting to permission - const result = convertToolsToPermission(tools) + // #when migrating + const result = migrateToolsToPermission(tools) - // #then converts to permission values - expect(result).toEqual({ write: "deny", edit: "allow", bash: "deny" }) - }) - - test("handles empty tools config", () => { - // #given empty config - // #when converting - const result = convertToolsToPermission({}) - - // #then returns empty object - expect(result).toEqual({}) + // #then converts correctly + expect(result).toEqual({ + write: "deny", + edit: "allow", + bash: "deny", + }) }) }) - describe("convertPermissionToTools", () => { - test("converts permission to boolean tools config", () => { + describe("migratePermissionToTools", () => { + test("converts permission to boolean tools", () => { // #given permission config const permission = { write: "deny" as const, edit: "allow" as const } - // #when converting to tools - const result = convertPermissionToTools(permission) + // #when migrating + const result = migratePermissionToTools(permission) - // #then converts to boolean values + // #then converts correctly expect(result).toEqual({ write: false, edit: true }) }) test("excludes ask values", () => { - // #given permission with ask value + // #given permission with ask const permission = { write: "deny" as const, edit: "ask" as const, bash: "allow" as const, } - // #when converting - const result = convertPermissionToTools(permission) + // #when migrating + const result = migratePermissionToTools(permission) // #then ask is excluded expect(result).toEqual({ write: false, bash: true }) }) }) - describe("createAgentRestrictions", () => { - test("creates restrictions with denied tools", () => { - // #given deny tools list - const config = { denyTools: ["write", "task"] } - - // #when creating restrictions - const result = createAgentRestrictions(config) - - // #then creates tools config - expect(result).toEqual({ tools: { write: false, task: false } }) - }) - - test("creates restrictions with permission", () => { - // #given permission config + describe("migrateAgentConfig", () => { + test("migrates tools to permission for v1.1.1+", () => { + // #given v1.1.1 and config with tools + setVersionCache("1.1.1") const config = { - permission: { edit: "deny" as const, bash: "ask" as const }, + model: "test", + tools: { write: false, edit: false }, } - // #when creating restrictions - const result = createAgentRestrictions(config) + // #when migrating + const result = migrateAgentConfig(config) - // #then creates permission config - expect(result).toEqual({ - permission: { edit: "deny", bash: "ask" }, - }) + // #then converts to permission + expect(result.tools).toBeUndefined() + expect(result.permission).toEqual({ write: "deny", edit: "deny" }) + expect(result.model).toBe("test") }) - test("combines tools and permission", () => { - // #given both deny tools and permission + test("migrates permission to tools for old versions", () => { + // #given old version and config with permission + setVersionCache("1.0.150") const config = { - denyTools: ["task"], - permission: { edit: "deny" as const }, + model: "test", + permission: { write: "deny" as const, edit: "deny" as const }, } - // #when creating restrictions - const result = createAgentRestrictions(config) + // #when migrating + const result = migrateAgentConfig(config) - // #then includes both - expect(result).toEqual({ - tools: { task: false }, - permission: { edit: "deny" }, - }) + // #then converts to tools + expect(result.permission).toBeUndefined() + expect(result.tools).toEqual({ write: false, edit: false }) }) - test("returns empty object when no config provided", () => { - // #given empty config - // #when creating restrictions - const result = createAgentRestrictions({}) + test("preserves other config fields", () => { + // #given config with other fields + setVersionCache("1.1.1") + const config = { + model: "test", + temperature: 0.5, + prompt: "hello", + tools: { write: false }, + } - // #then returns empty object - expect(result).toEqual({}) + // #when migrating + const result = migrateAgentConfig(config) + + // #then preserves other fields + expect(result.model).toBe("test") + expect(result.temperature).toBe(0.5) + expect(result.prompt).toBe("hello") }) }) }) diff --git a/src/shared/permission-compat.ts b/src/shared/permission-compat.ts index 1fb2255..f29df34 100644 --- a/src/shared/permission-compat.ts +++ b/src/shared/permission-compat.ts @@ -1,72 +1,78 @@ -import { supportsNewPermissionSystem as checkNewPermissionSystem } from "./opencode-version" +import { supportsNewPermissionSystem } from "./opencode-version" export type PermissionValue = "ask" | "allow" | "deny" -export type BashPermission = PermissionValue | Record -export interface StandardPermission { - edit?: PermissionValue - bash?: BashPermission - webfetch?: PermissionValue - doom_loop?: PermissionValue - external_directory?: PermissionValue +export interface LegacyToolsFormat { + tools: Record } -export interface ToolsConfig { - [toolName: string]: boolean +export interface NewPermissionFormat { + permission: Record } -export interface AgentPermissionConfig { - permission?: StandardPermission - tools?: ToolsConfig +export type VersionAwareRestrictions = LegacyToolsFormat | NewPermissionFormat + +export function createAgentToolRestrictions( + denyTools: string[] +): VersionAwareRestrictions { + if (supportsNewPermissionSystem()) { + return { + permission: Object.fromEntries( + denyTools.map((tool) => [tool, "deny" as const]) + ), + } + } + + return { + tools: Object.fromEntries(denyTools.map((tool) => [tool, false])), + } } -export { checkNewPermissionSystem as supportsNewPermissionSystemFromCompat } - -export function createToolDenyList(toolNames: string[]): ToolsConfig { - return Object.fromEntries(toolNames.map((name) => [name, false])) -} - -export function permissionValueToBoolean(value: PermissionValue): boolean { - return value === "allow" -} - -export function booleanToPermissionValue(value: boolean): PermissionValue { - return value ? "allow" : "deny" -} - -export function convertToolsToPermission( - tools: ToolsConfig +export function migrateToolsToPermission( + tools: Record ): Record { return Object.fromEntries( Object.entries(tools).map(([key, value]) => [ key, - booleanToPermissionValue(value), + value ? ("allow" as const) : ("deny" as const), ]) ) } -export function convertPermissionToTools( +export function migratePermissionToTools( permission: Record -): ToolsConfig { +): Record { return Object.fromEntries( Object.entries(permission) .filter(([, value]) => value !== "ask") - .map(([key, value]) => [key, permissionValueToBoolean(value)]) + .map(([key, value]) => [key, value === "allow"]) ) } -export function createAgentRestrictions(config: { - denyTools?: string[] - permission?: StandardPermission -}): AgentPermissionConfig { - const result: AgentPermissionConfig = {} +export function migrateAgentConfig( + config: Record +): Record { + const result = { ...config } - if (config.denyTools && config.denyTools.length > 0) { - result.tools = createToolDenyList(config.denyTools) - } - - if (config.permission) { - result.permission = config.permission + if (supportsNewPermissionSystem()) { + if (result.tools && typeof result.tools === "object") { + const existingPermission = + (result.permission as Record) || {} + const migratedPermission = migrateToolsToPermission( + result.tools as Record + ) + result.permission = { ...migratedPermission, ...existingPermission } + delete result.tools + } + } else { + if (result.permission && typeof result.permission === "object") { + const existingTools = (result.tools as Record) || {} + const migratedTools = migratePermissionToTools( + result.permission as Record + ) + result.tools = { ...migratedTools, ...existingTools } + delete result.permission + } } return result From e1e8b249411d641f8a9d651ef67f98d8617dd4c1 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 5 Jan 2026 04:50:13 +0900 Subject: [PATCH 2/5] fix: update all agents to use createAgentToolRestrictions() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace hardcoded tools: { X: false } format with version-aware utility - All agents now use createAgentToolRestrictions([...]) - Ensures compatibility with both old and new OpenCode versions 🤖 Generated with assistance of OhMyOpenCode https://github.com/code-yeongyu/oh-my-opencode --- src/agents/document-writer.ts | 5 ++++- src/agents/explore.ts | 10 ++++++++-- src/agents/frontend-ui-ux-engineer.ts | 5 ++++- src/agents/librarian.ts | 9 ++++++++- src/agents/multimodal-looker.ts | 10 +++++++++- src/agents/oracle.ts | 17 ++++++++++++----- 6 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/agents/document-writer.ts b/src/agents/document-writer.ts index 753173b..6de0929 100644 --- a/src/agents/document-writer.ts +++ b/src/agents/document-writer.ts @@ -1,5 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentPromptMetadata } from "./types" +import { createAgentToolRestrictions } from "../shared/permission-compat" const DEFAULT_MODEL = "google/gemini-3-flash-preview" @@ -15,12 +16,14 @@ export const DOCUMENT_WRITER_PROMPT_METADATA: AgentPromptMetadata = { export function createDocumentWriterAgent( model: string = DEFAULT_MODEL ): AgentConfig { + const restrictions = createAgentToolRestrictions(["background_task"]) + return { description: "A technical writer who crafts clear, comprehensive documentation. Specializes in README files, API docs, architecture docs, and user guides. MUST BE USED when executing documentation tasks from ai-todo list plans.", mode: "subagent" as const, model, - tools: { background_task: false }, + ...restrictions, prompt: ` You are a TECHNICAL WRITER with deep engineering background who transforms complex codebases into crystal-clear documentation. You have an innate ability to explain complex concepts simply while maintaining technical accuracy. diff --git a/src/agents/explore.ts b/src/agents/explore.ts index 4f8cd12..fd0478d 100644 --- a/src/agents/explore.ts +++ b/src/agents/explore.ts @@ -1,5 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentPromptMetadata } from "./types" +import { createAgentToolRestrictions } from "../shared/permission-compat" const DEFAULT_MODEL = "opencode/grok-code" @@ -24,14 +25,19 @@ export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = { } export function createExploreAgent(model: string = DEFAULT_MODEL): AgentConfig { + const restrictions = createAgentToolRestrictions([ + "write", + "edit", + "background_task", + ]) + return { description: 'Contextual grep for codebases. Answers "Where is X?", "Which file has Y?", "Find the code that does Z". Fire multiple in parallel for broad searches. Specify thoroughness: "quick" for basic, "medium" for moderate, "very thorough" for comprehensive analysis.', mode: "subagent" as const, model, temperature: 0.1, - tools: { write: false, background_task: false }, - permission: { edit: "deny" as const }, + ...restrictions, prompt: `You are a codebase search specialist. Your job: find files and code, return actionable results. ## Your Mission diff --git a/src/agents/frontend-ui-ux-engineer.ts b/src/agents/frontend-ui-ux-engineer.ts index f7ff5bd..6778ef3 100644 --- a/src/agents/frontend-ui-ux-engineer.ts +++ b/src/agents/frontend-ui-ux-engineer.ts @@ -1,5 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentPromptMetadata } from "./types" +import { createAgentToolRestrictions } from "../shared/permission-compat" const DEFAULT_MODEL = "google/gemini-3-pro-preview" @@ -21,12 +22,14 @@ export const FRONTEND_PROMPT_METADATA: AgentPromptMetadata = { export function createFrontendUiUxEngineerAgent( model: string = DEFAULT_MODEL ): AgentConfig { + const restrictions = createAgentToolRestrictions(["background_task"]) + return { description: "A designer-turned-developer who crafts stunning UI/UX even without design mockups. Code may be a bit messy, but the visual output is always fire.", mode: "subagent" as const, model, - tools: { background_task: false }, + ...restrictions, prompt: `# Role: Designer-Turned-Developer You are a designer who learned to code. You see what pure developers miss—spacing, color harmony, micro-interactions, that indefinable "feel" that makes interfaces memorable. Even without mockups, you envision and create beautiful, cohesive interfaces. diff --git a/src/agents/librarian.ts b/src/agents/librarian.ts index 7c0f2e3..9418426 100644 --- a/src/agents/librarian.ts +++ b/src/agents/librarian.ts @@ -1,5 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentPromptMetadata } from "./types" +import { createAgentToolRestrictions } from "../shared/permission-compat" const DEFAULT_MODEL = "anthropic/claude-sonnet-4-5" @@ -21,13 +22,19 @@ export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = { } export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig { + const restrictions = createAgentToolRestrictions([ + "write", + "edit", + "background_task", + ]) + return { description: "Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source.", mode: "subagent" as const, model, temperature: 0.1, - tools: { write: false, edit: false, background_task: false }, + ...restrictions, prompt: `# THE LIBRARIAN You are **THE LIBRARIAN**, a specialized open-source codebase understanding agent. diff --git a/src/agents/multimodal-looker.ts b/src/agents/multimodal-looker.ts index 2625853..71b104f 100644 --- a/src/agents/multimodal-looker.ts +++ b/src/agents/multimodal-looker.ts @@ -1,5 +1,6 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentPromptMetadata } from "./types" +import { createAgentToolRestrictions } from "../shared/permission-compat" const DEFAULT_MODEL = "google/gemini-3-flash" @@ -13,13 +14,20 @@ export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = { export function createMultimodalLookerAgent( model: string = DEFAULT_MODEL ): AgentConfig { + const restrictions = createAgentToolRestrictions([ + "write", + "edit", + "bash", + "background_task", + ]) + return { description: "Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text. Extracts specific information or summaries from documents, describes visual content. Use when you need analyzed/extracted data rather than literal file contents.", mode: "subagent" as const, model, temperature: 0.1, - tools: { write: false, edit: false, bash: false, background_task: false }, + ...restrictions, prompt: `You interpret media files that cannot be read as plain text. Your job: examine the attached file and extract ONLY what was requested. diff --git a/src/agents/oracle.ts b/src/agents/oracle.ts index 26b8652..7d067a7 100644 --- a/src/agents/oracle.ts +++ b/src/agents/oracle.ts @@ -1,6 +1,7 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentPromptMetadata } from "./types" import { isGptModel } from "./types" +import { createAgentToolRestrictions } from "../shared/permission-compat" const DEFAULT_MODEL = "openai/gpt-5.2" @@ -97,22 +98,28 @@ Organize your final answer in three tiers: Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why.` export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig { + const restrictions = createAgentToolRestrictions([ + "write", + "edit", + "task", + "background_task", + ]) + const base = { description: "Expert technical advisor with deep reasoning for architecture decisions, code analysis, and engineering guidance.", mode: "subagent" as const, model, temperature: 0.1, - tools: { write: false, task: false, background_task: false }, - permission: { edit: "deny" as const }, + ...restrictions, prompt: ORACLE_SYSTEM_PROMPT, - } + } as AgentConfig if (isGptModel(model)) { - return { ...base, reasoningEffort: "medium", textVerbosity: "high" } + return { ...base, reasoningEffort: "medium", textVerbosity: "high" } as AgentConfig } - return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } + return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig } export const oracleAgent = createOracleAgent() From 6c3ef65aed7c57686e6c2a6b21b5bc4e7e40e390 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 5 Jan 2026 04:56:47 +0900 Subject: [PATCH 3/5] fix: add runtime migration for user agent configs in config-handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate tools/permission format in user/project/plugin agent configs based on detected OpenCode version at load time. 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/plugin-handlers/config-handler.ts | 41 +++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index 9feb8ad..32b071a 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -21,6 +21,7 @@ import { loadAllPluginComponents } from "../features/claude-code-plugin-loader"; import { createBuiltinMcps } from "../mcp"; import type { OhMyOpenCodeConfig } from "../config"; import { log } from "../shared"; +import { migrateAgentConfig } from "../shared/permission-compat"; import { PLAN_SYSTEM_PROMPT, PLAN_PERMISSION } from "../agents/plan-prompt"; import type { ModelCacheState } from "../plugin-state"; @@ -95,13 +96,32 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { config.model as string | undefined ); - const userAgents = (pluginConfig.claude_code?.agents ?? true) + const rawUserAgents = (pluginConfig.claude_code?.agents ?? true) ? loadUserAgents() : {}; - const projectAgents = (pluginConfig.claude_code?.agents ?? true) + const rawProjectAgents = (pluginConfig.claude_code?.agents ?? true) ? loadProjectAgents() : {}; - const pluginAgents = pluginComponents.agents; + const rawPluginAgents = pluginComponents.agents; + + const userAgents = Object.fromEntries( + Object.entries(rawUserAgents).map(([k, v]) => [ + k, + v ? migrateAgentConfig(v as Record) : v, + ]) + ); + const projectAgents = Object.fromEntries( + Object.entries(rawProjectAgents).map(([k, v]) => [ + k, + v ? migrateAgentConfig(v as Record) : v, + ]) + ); + const pluginAgents = Object.fromEntries( + Object.entries(rawPluginAgents).map(([k, v]) => [ + k, + v ? migrateAgentConfig(v as Record) : v, + ]) + ); const isSisyphusEnabled = pluginConfig.sisyphus_agent?.disabled !== true; const builderEnabled = @@ -162,15 +182,20 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { : plannerSisyphusBase; } - const filteredConfigAgents = configAgent - ? Object.fromEntries( - Object.entries(configAgent).filter(([key]) => { + const filteredConfigAgents = configAgent + ? Object.fromEntries( + Object.entries(configAgent) + .filter(([key]) => { if (key === "build") return false; if (key === "plan" && replacePlan) return false; return true; }) - ) - : {}; + .map(([key, value]) => [ + key, + value ? migrateAgentConfig(value as Record) : value, + ]) + ) + : {}; config.agent = { ...agentConfig, From b66c8dc1d12a659b06b9f7d0923ceba8eaa8d8ab Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 5 Jan 2026 05:38:17 +0900 Subject: [PATCH 4/5] feat(frontmatter): track parsing errors and frontmatter existence in result type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add hadFrontmatter and parseError flags to FrontmatterResult interface to enable error handling in skill loading. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/shared/frontmatter.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shared/frontmatter.ts b/src/shared/frontmatter.ts index 674b03d..db16420 100644 --- a/src/shared/frontmatter.ts +++ b/src/shared/frontmatter.ts @@ -3,6 +3,8 @@ import yaml from "js-yaml" export interface FrontmatterResult> { data: T body: string + hadFrontmatter: boolean + parseError: boolean } export function parseFrontmatter>( @@ -12,7 +14,7 @@ export function parseFrontmatter>( const match = content.match(frontmatterRegex) if (!match) { - return { data: {} as T, body: content } + return { data: {} as T, body: content, hadFrontmatter: false, parseError: false } } const yamlContent = match[1] @@ -22,8 +24,8 @@ export function parseFrontmatter>( // Use JSON_SCHEMA for security - prevents code execution via YAML tags const parsed = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA }) const data = (parsed ?? {}) as T - return { data, body } + return { data, body, hadFrontmatter: true, parseError: false } } catch { - return { data: {} as T, body } + return { data: {} as T, body, hadFrontmatter: true, parseError: true } } } From 2992902283060461634b88273fe6a4385c07ef88 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 5 Jan 2026 05:38:28 +0900 Subject: [PATCH 5/5] fix: skip invalid YAML skills and enable Planner-Sisyphus in Tab selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Skip skills with invalid YAML frontmatter using new parseError flag - Add mode: "primary" to Planner-Sisyphus agent config for visibility - Prevents silent failures when loading skills with malformed YAML 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- .../opencode-skill-loader/async-loader.ts | 4 ++- src/plugin-handlers/config-handler.ts | 36 +++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/features/opencode-skill-loader/async-loader.ts b/src/features/opencode-skill-loader/async-loader.ts index 74f1a8e..bfb1e7f 100644 --- a/src/features/opencode-skill-loader/async-loader.ts +++ b/src/features/opencode-skill-loader/async-loader.ts @@ -78,7 +78,9 @@ export async function loadSkillFromPathAsync( ): Promise { try { const content = await readFile(skillPath, "utf-8") - const { data, body } = parseFrontmatter(content) + const { data, body, parseError } = parseFrontmatter(content) + if (parseError) return null + const frontmatterMcp = parseSkillMcpConfigFromFrontmatter(content) const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index 32b071a..d3831b3 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -152,10 +152,13 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { if (builderEnabled) { const { name: _buildName, ...buildConfigWithoutName } = configAgent?.build ?? {}; + const migratedBuildConfig = migrateAgentConfig( + buildConfigWithoutName as Record + ); const openCodeBuilderOverride = pluginConfig.agents?.["OpenCode-Builder"]; const openCodeBuilderBase = { - ...buildConfigWithoutName, + ...migratedBuildConfig, description: `${configAgent?.build?.description ?? "Build agent"} (OpenCode default)`, }; @@ -167,10 +170,14 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { if (plannerEnabled) { const { name: _planName, ...planConfigWithoutName } = configAgent?.plan ?? {}; + const migratedPlanConfig = migrateAgentConfig( + planConfigWithoutName as Record + ); const plannerSisyphusOverride = pluginConfig.agents?.["Planner-Sisyphus"]; const plannerSisyphusBase = { - ...planConfigWithoutName, + ...migratedPlanConfig, + mode: "primary", prompt: PLAN_SYSTEM_PROMPT, permission: PLAN_PERMISSION, description: `${configAgent?.plan?.description ?? "Plan agent"} (OhMyOpenCode version)`, @@ -197,6 +204,25 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { ) : {}; + const migratedBuild = configAgent?.build + ? migrateAgentConfig(configAgent.build as Record) + : {}; + const migratedPlan = configAgent?.plan + ? migrateAgentConfig(configAgent.plan as Record) + : {}; + + const planDemoteConfig = replacePlan + ? { disable: true } + : undefined; + + log("DEBUG plan demotion", { + replacePlan, + plannerEnabled, + hasPlannerSisyphus: !!agentConfig["Planner-Sisyphus"], + planDemoteConfig, + configAgentPlan: configAgent?.plan, + }); + config.agent = { ...agentConfig, ...Object.fromEntries( @@ -206,10 +232,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { ...projectAgents, ...pluginAgents, ...filteredConfigAgents, - build: { ...configAgent?.build, mode: "subagent" }, - ...(replacePlan - ? { plan: { ...configAgent?.plan, mode: "subagent" } } - : {}), + build: { ...migratedBuild, mode: "subagent", hidden: true }, + ...(planDemoteConfig ? { plan: planDemoteConfig } : {}), }; } else { config.agent = {