From 326e4e8615ca613f3058f672f54dad3f615ef8ae Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Sun, 19 Apr 2026 17:32:51 -0700 Subject: [PATCH] refactor(opencode): align output with current permissions Stop emitting deprecated tools config for OpenCode permission modes. Update the OpenCode schema notes and add a regression that the current CE plugin outputs skills plus subagents, not command files. --- docs/specs/opencode.md | 16 +++++++++++----- src/converters/claude-to-opencode.ts | 6 ------ src/types/opencode.ts | 22 ++++++++++++++++++++-- tests/converter.test.ts | 20 ++++++++++++++++++++ 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/docs/specs/opencode.md b/docs/specs/opencode.md index dde203ed..1ea1aa2f 100644 --- a/docs/specs/opencode.md +++ b/docs/specs/opencode.md @@ -26,10 +26,11 @@ https://opencode.ai/config.json ## Core config keys - `model` and `small_model` set the primary and lightweight models; `provider` configures provider options. -- `tools` is still supported but deprecated; permissions are now the canonical control surface. +- `tools` is still supported but deprecated as of OpenCode v1.1.1; permissions are now the canonical control surface. - `permission` controls tool approvals and can be configured globally or per tool, including pattern-based rules. - `mcp`, `instructions`, `disabled_providers`, `enabled_providers`, and `plugin` are supported config sections. - `plugin` can list npm packages to load at startup. +- `skills.paths` and `skills.urls` can add extra skill discovery locations, but CE should not depend on them until the layout is smoke-tested locally with OpenCode. ## Tools @@ -45,27 +46,31 @@ https://opencode.ai/config.json ## Agents - Agents can be configured in `opencode.json` or as markdown files in `~/.config/opencode/agents/` or `.opencode/agents/`. -- Agent config supports `mode`, `model`, `temperature`, `tools`, and `permission`, and agent configs override global settings. +- Agent config supports `mode`, `model`, `variant`, `temperature`, `top_p`, `hidden`, `steps`, `options`, `permission`, and other schema fields. `tools` still exists but is deprecated. - `mode` can be `primary`, `subagent`, or `all`; omitted mode defaults to `all`. +- `hidden: true` hides subagents from the `@` autocomplete menu. +- `permission.task` controls which subagents an agent may invoke. - Model IDs use the `provider/model-id` format. ## Skills - Skills are reusable `SKILL.md` definitions loaded on demand through OpenCode's native `skill` tool. -- OpenCode searches direct child skill directories only: +- OpenCode searches direct child skill directories in its built-in roots: - `.opencode/skills//SKILL.md` - `~/.config/opencode/skills//SKILL.md` - `.claude/skills//SKILL.md` - `~/.claude/skills//SKILL.md` - `.agents/skills//SKILL.md` - `~/.agents/skills//SKILL.md` +- The config schema also exposes `skills.paths` and `skills.urls` for extra skill sources. Do not switch CE to those until tested against a local OpenCode install; direct `~/.config/opencode/skills//SKILL.md` remains the stable writer shape. - Skill frontmatter recognizes `name`, `description`, `license`, `compatibility`, and `metadata`; unknown fields are ignored. - Skill names must be lowercase alphanumeric with single hyphen separators and must match the directory name. ## Commands - Commands can be configured in `opencode.json` or as Markdown files in `~/.config/opencode/commands/` or `.opencode/commands/`. -- Markdown command frontmatter can include fields such as `description`, `agent`, and `model`; the body becomes the prompt template. +- Markdown command frontmatter can include fields such as `description`, `agent`, `model`, and `subtask`; the body becomes the prompt template. +- If a command targets an agent whose mode is `subagent`, OpenCode invokes it as a subagent by default. `subtask: true` can force subagent invocation. ## Plugins and events @@ -79,8 +84,9 @@ https://opencode.ai/config.json - The current CE writer shape is still appropriate in April 2026: - `~/.config/opencode/opencode.json` - `~/.config/opencode/agents/*.md` - - `~/.config/opencode/commands/*.md` + - `~/.config/opencode/commands/*.md` only when a source plugin ships commands - `~/.config/opencode/plugins/*.ts` - `~/.config/opencode/skills/*/SKILL.md` - OpenCode's plugin system is useful for JS/TS hooks and custom tools, but current docs do not describe a native marketplace command that consumes CE's `.claude-plugin/marketplace.json` and installs the full skills/agents/commands payload. - Keep the custom Bun writer until OpenCode documents a native distribution path for packaged skills and agents. +- The `compound-engineering` plugin currently emits skills and subagent Markdown files for OpenCode. It should not emit deprecated `tools` config; permission config is enough for non-default permission modes. diff --git a/src/converters/claude-to-opencode.ts b/src/converters/claude-to-opencode.ts index 5539ed80..990d4a3d 100644 --- a/src/converters/claude-to-opencode.ts +++ b/src/converters/claude-to-opencode.ts @@ -362,11 +362,6 @@ function applyPermissions( } const permission: Record> = {} - const tools: Record = {} - - for (const tool of sourceTools) { - tools[tool] = mode === "broad" ? true : enabled.has(tool) - } if (mode === "broad") { for (const tool of sourceTools) { @@ -415,7 +410,6 @@ function applyPermissions( } config.permission = permission - config.tools = tools } function normalizeTool(raw: string): string | null { diff --git a/src/types/opencode.ts b/src/types/opencode.ts index a4e2fb3a..36b9ae26 100644 --- a/src/types/opencode.ts +++ b/src/types/opencode.ts @@ -4,19 +4,37 @@ export type OpenCodeConfig = { $schema?: string model?: string default_agent?: string + /** @deprecated OpenCode v1.1.1+ uses permission as the canonical control surface. */ tools?: Record permission?: Record> agent?: Record mcp?: Record + skills?: OpenCodeSkillsConfig } export type OpenCodeAgentConfig = { description?: string - mode?: "primary" | "subagent" + mode?: "primary" | "subagent" | "all" model?: string + variant?: string temperature?: number + top_p?: number + prompt?: string + disable?: boolean + hidden?: boolean + color?: string + steps?: number + /** @deprecated Use steps instead. */ + maxSteps?: number + options?: Record + /** @deprecated OpenCode v1.1.1+ uses permission as the canonical control surface. */ tools?: Record - permission?: Record + permission?: Record> +} + +export type OpenCodeSkillsConfig = { + paths?: string[] + urls?: string[] } export type OpenCodeMcpServer = { diff --git a/tests/converter.test.ts b/tests/converter.test.ts index 5928794d..aae36324 100644 --- a/tests/converter.test.ts +++ b/tests/converter.test.ts @@ -15,6 +15,24 @@ const compoundEngineeringRoot = path.join( ) describe("convertClaudeToOpenCode", () => { + test("current compound-engineering output is skills and subagents, not commands", async () => { + const plugin = await loadClaudePlugin(compoundEngineeringRoot) + const bundle = convertClaudeToOpenCode(plugin, { + agentMode: "subagent", + inferTemperature: true, + permissions: "none", + }) + + expect(bundle.agents.length).toBeGreaterThan(0) + expect(bundle.skillDirs.length).toBeGreaterThan(0) + expect(bundle.commandFiles).toHaveLength(0) + expect(bundle.plugins).toHaveLength(0) + expect(bundle.config.tools).toBeUndefined() + + const parsedAgents = bundle.agents.map((agent) => parseFrontmatter(agent.content)) + expect(parsedAgents.every((agent) => agent.data.mode === "subagent")).toBe(true) + }) + test("from-command mode: map allowedTools to global permission block", async () => { const plugin = await loadClaudePlugin(fixtureRoot) const bundle = convertClaudeToOpenCode(plugin, { @@ -24,6 +42,7 @@ describe("convertClaudeToOpenCode", () => { }) expect(bundle.config.command).toBeUndefined() + expect(bundle.config.tools).toBeUndefined() expect(bundle.commandFiles.find((f) => f.name === "workflows:review")).toBeDefined() expect(bundle.commandFiles.find((f) => f.name === "plan_review")).toBeDefined() @@ -275,6 +294,7 @@ describe("convertClaudeToOpenCode", () => { inferTemperature: false, permissions: "broad", }) + expect(broadBundle.config.tools).toBeUndefined() expect(broadBundle.config.permission).toEqual({ read: "allow", write: "allow",