Skip to content

feat: OpenCode runtime support - translation layer and plugin - #46

Merged
ldangelo merged 18 commits into
mainfrom
feature/opencode-support
Mar 14, 2026
Merged

feat: OpenCode runtime support - translation layer and plugin#46
ldangelo merged 18 commits into
mainfrom
feature/opencode-support

Conversation

@ldangelo

Copy link
Copy Markdown
Contributor

Summary

Adds OpenCode runtime support to the Ensemble plugin ecosystem via a translation layer that converts Ensemble YAML/JSON/Markdown artifacts to OpenCode-compatible formats.

What's Included

New Package: packages/opencode/ (ensemble-opencode)

  • Plugin entry point with ensemble-info tool for capability discovery
  • npm publishing config with optional @opencode-ai/plugin peer dependency
  • Local install support via file:/// path

Translation Layer: scripts/generate-opencode/

Component Purpose Tests
SkillCopier Copies SKILL.md files with frontmatter injection 24
CommandTranslator Converts YAML commands to OpenCode Markdown + JSON config 62 + 6 snapshots
ManifestGenerator Generates unified opencode.json from plugin manifests 35
Generator CLI npm run generate:opencode with --dry-run, --verbose, --validate, --force 31
Plugin Entry OpenCode plugin with ensemble-info tool 34
Package Structure Scaffolding validation 15
Performance Generation timing benchmarks 2

CI/CD

  • .github/workflows/opencode-generate.yml - Validation, regression, staleness checks
  • Updated validate.yml with OpenCode plugin.json validation

Documentation

  • Updated CLAUDE.md with OpenCode references
  • Updated marketplace.json with ensemble-opencode entry
  • TRD progress tracking (58/84 tasks complete, 21 deferred to Sprint 2)

Test Results

Test Suites: 7 passed, 7 total
Tests:       203 passed, 203 total
Snapshots:   6 passed, 6 total

Deferred (Sprint 2 - Agent Mesh & Hook Bridge)

The following are intentionally deferred as the agent mesh delegation system is not yet needed:

  • Agent Translator (28 YAML agents → OpenCode JSON/Markdown)
  • Hook Bridge Plugin (PreToolUse/PostToolUse → OpenCode typed hooks)
  • Related distribution wiring and E2E tests

TRD Reference

  • PRD: docs/PRD/opencode-support.md
  • TRD: docs/TRD/opencode-support.md

Test plan

  • 203 unit tests passing
  • 6 snapshot tests passing
  • Performance benchmarks (< 10s full, < 2s incremental)
  • npm run validate passes
  • Manual: npm run generate:opencode --dry-run produces expected output

🤖 Generated with Claude Code

ldangelo and others added 6 commits February 26, 2026 11:19
Cherry-picked from docs/opencode-support-trd and docs/opencode-support-prd branches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OC-S1-PKG-001 through OC-S1-PKG-006: Create packages/opencode/ with
package.json, plugin.json, tsconfig.json; add generator script stubs
in scripts/generate-opencode/; add generate:opencode npm script.
15 structure tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OC-S1-SK-001 through SK-006, TEST-001: SkillCopier discovers SKILL.md
and REFERENCE.md files, injects frontmatter, copies to OpenCode paths.
24 tests passing.

OC-S1-CMD-001 through CMD-010, TEST-002, TEST-003: CommandTranslator
parses YAML commands and generates OpenCode Markdown format with
argument mapping, workflow sections, and JSON config entries.
62 tests + 6 snapshots passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OC-S3-MF-001, MF-003 through MF-008, TEST-009: ManifestGenerator reads
plugin manifests and generates unified opencode.json with command,
skills, plugin, instructions, and permission sections. 35 tests.

OC-S3-CLI-001 through CLI-009, TEST-010, TEST-011: Generator CLI with
--dry-run, --verbose, --validate, --output-dir, --force flags. Full
pipeline orchestration with incremental hash-based caching, progress
reporting, and error collection. 31 tests.

167 total tests passing across 5 suites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OC-S4-CI-001 through CI-003: GitHub Actions workflow for OpenCode
generation validation, regression checks, and staleness detection.
OC-S4-CI-005: OpenCode plugin.json validation in validate.yml.
OC-S4-PKG-007: Already covered by existing glob validation.
OC-S4-PKG-008: ensemble-opencode added to marketplace.json.
OC-S4-DOC-004: CLAUDE.md updated with OpenCode references.
OC-S4-TEST-019: Performance tests for generation timing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OC-S3-DIST-001/002/004-007, TEST-012: Plugin entry point with
ensemble-info tool, npm publishing config, build step, local file
install support, and version sync. 34 tests.

203 total tests passing across 7 suites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

test review

@claude

claude Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

OpenCode Research Document

Date: 2026-02-26
Source: https://github.com/anomalyco/opencode (111k+ stars, MIT License)
Version: 1.2.15 (current @opencode-ai/plugin package)
Homepage: https://opencode.ai
Language: TypeScript (Bun runtime) -- was originally Go, rewritten in TypeScript
Organization: anomalyco (formerly opencode-ai, originally sst)

Note: Context7 MCP was not available in this environment. All data was gathered
directly from the GitHub API and raw source files on the dev branch.


1. What OpenCode Is

OpenCode is an open-source AI coding agent built for the terminal. It provides a TUI
(Terminal User Interface), desktop app, and web interface for interacting with LLMs to
assist with coding tasks. It was created by the SST team (now Anomaly Co), the same
people behind terminal.shop and the SST framework.

Architecture

OpenCode uses a client/server architecture:

  • Server: A Hono-based HTTP server that manages sessions, LLM communication, tool
    execution, and plugin lifecycle. Runs locally.
  • Clients: The TUI (built with Solid.js + OpenTUI), a desktop app (Electron-based),
    and a web interface are all frontends that connect to the server.
  • Database: SQLite (via Drizzle ORM) for persistent session and conversation storage.
  • Runtime: Bun (not Node.js). The project uses Bun-specific APIs extensively
    (Bun.file, Bun.$, bun-pty).

Internal Module Structure (packages/opencode/src/)

Module Purpose
agent/ Agent definitions, prompt templates, agent configuration
cli/ CLI entry point using yargs
config/ Configuration loading, merging, validation (JSONC support)
command/ Custom command system (slash commands)
lsp/ Language Server Protocol client integration
mcp/ Model Context Protocol client for external tools
permission/ Permission system with granular allow/ask/deny rules
plugin/ Plugin loader and hook dispatch
provider/ LLM provider integrations (via Vercel AI SDK)
server/ Hono HTTP server, SSE event streaming
session/ Conversation session management
skill/ Skill discovery and loading
tool/ Built-in tool definitions (bash, edit, glob, grep, etc.)
snapshot/ File snapshot/undo system
worktree/ Git worktree detection

Key Technical Choices

  • LLM Integration: Uses the Vercel AI SDK (ai package) with provider-specific
    adapters (@ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/google, etc.)
  • TUI Framework: OpenTUI + Solid.js (reactive terminal UI)
  • Config Format: JSONC (JSON with comments) via jsonc-parser
  • Package Manager: Bun workspaces (monorepo with packages/ directory)

Monorepo Packages

Package Description
packages/opencode Core CLI and server
packages/plugin Plugin SDK (@opencode-ai/plugin)
packages/sdk Client SDK (@opencode-ai/sdk)
packages/app Desktop application
packages/console Web console
packages/docs Documentation site
packages/extensions IDE extensions (Zed)
packages/web Marketing website
packages/enterprise Enterprise features
packages/ui Shared UI components

2. Plugin/Extension System

OpenCode has a TypeScript-based plugin system that allows extending the agent with
custom tools, hooks, authentication providers, and event handlers.

Installing Plugins

Plugins are specified in the configuration file as npm package references:

{
  "plugin": [
    "my-opencode-plugin@1.0.0",
    "file:///path/to/local/plugin"
  ]
}

Plugins are installed automatically via Bun.install() at startup. Built-in plugins
(like opencode-anthropic-auth) are loaded by default and can be disabled with the
OPENCODE_DISABLE_DEFAULT_PLUGINS flag.

Plugins can also be placed in .opencode/plugins/ directories (project or global level).

Plugin API (@opencode-ai/plugin)

A plugin is an async function that receives a PluginInput context and returns a
Hooks object:

import type { Plugin } from "@opencode-ai/plugin"
import { tool } from "@opencode-ai/plugin"

export const MyPlugin: Plugin = async (ctx) => {
  // ctx.client    - OpenCode SDK client
  // ctx.project   - Current project info
  // ctx.directory  - Current working directory
  // ctx.worktree   - Git worktree root
  // ctx.serverUrl  - Local server URL
  // ctx.$          - Bun shell instance

  return {
    // Custom tools available to the AI
    tool: {
      mytool: tool({
        description: "This is a custom tool",
        args: {
          foo: tool.schema.string().describe("foo"),
        },
        async execute(args, context) {
          // context.sessionID, context.agent, context.directory, etc.
          return `Hello ${args.foo}!`
        },
      }),
    },

    // Lifecycle hooks (see full list below)
    "tool.execute.before": async (input, output) => { /* ... */ },
    "tool.execute.after": async (input, output) => { /* ... */ },
  }
}

Available Plugin Hooks

Hook Description
event Called on any bus event
config Called with loaded configuration
tool Register custom tools (object map of tool definitions)
auth Authentication provider (OAuth or API key flows)
chat.message Called when a new message is received
chat.params Modify LLM parameters (temperature, topP, etc.)
chat.headers Modify HTTP headers sent to LLM providers
permission.ask Intercept permission prompts
command.execute.before Before a slash command executes
tool.execute.before Before a tool executes (modify args)
tool.execute.after After a tool executes (modify output)
tool.definition Modify tool descriptions/parameters sent to LLM
shell.env Inject environment variables into shell commands
experimental.chat.messages.transform Transform message history
experimental.chat.system.transform Transform system prompt
experimental.session.compacting Customize session compaction
experimental.text.complete Modify completed text

Custom Commands

Commands are Markdown files stored in specific directories:

  • User commands (user: prefix): ~/.config/opencode/commands/*.md
  • Project commands (project: prefix): <PROJECT>/.opencode/commands/*.md
  • Subdirectory organization: commands/git/commit.md becomes user:git:commit

Commands support named arguments with $PLACEHOLDER syntax:

# Fix Issue $ISSUE_NUMBER

RUN gh issue view $ISSUE_NUMBER --json title,body,comments
READ README.md

Commands can also be defined in configuration:

{
  "command": {
    "my-command": {
      "description": "My custom command",
      "template": "Do something with $1",
      "agent": "build",
      "subtask": false
    }
  }
}

Skills (Agent Knowledge)

Skills are Markdown files with frontmatter that provide domain knowledge to agents.
They follow the SKILL.md convention:

---
name: my-skill
description: Knowledge about X
---

# Skill Content

Detailed instructions and knowledge the agent can reference...

Skill Discovery Locations (in order of precedence):

  1. .claude/skills/**/SKILL.md (Claude Code compatibility)
  2. .agents/skills/**/SKILL.md (generic agent directory)
  3. .opencode/skill/**/SKILL.md or .opencode/skills/**/SKILL.md
  4. Additional paths from config.skills.paths
  5. Remote URLs from config.skills.urls (fetches index.json + files)
  6. Global: ~/.claude/skills/ and ~/.agents/skills/

Skills are automatically exposed as invocable commands (if no command with the same
name exists).

Agents (Custom Agent Definitions)

OpenCode ships with built-in agents and allows custom agent configuration:

Built-in Agents:

Agent Mode Description
build primary Default full-access agent for development
plan primary Read-only agent for analysis and code exploration
general subagent General-purpose for complex searches and multistep tasks
explore subagent Fast agent specialized for codebase exploration
compaction primary (hidden) Session compaction/summarization
title primary (hidden) Session title generation
summary primary (hidden) Session summary generation

Custom Agent Configuration (in opencode.json):

{
  "agent": {
    "build": {
      "model": "anthropic/claude-sonnet-4-20250514",
      "temperature": 0.7,
      "steps": 50,
      "prompt": "You are a specialized backend developer..."
    },
    "my-custom-agent": {
      "name": "my-custom-agent",
      "description": "Specialized agent for database work",
      "mode": "primary",
      "model": { "providerID": "anthropic", "modelID": "claude-sonnet-4-20250514" },
      "permission": {
        "bash": "ask",
        "edit": "allow"
      }
    }
  }
}

Custom agents can also be defined as Markdown files in .opencode/agents/ directories.


3. Configuration Format and File Structure

Configuration File Locations

OpenCode uses JSONC format and searches these locations (low to high precedence):

  1. Remote .well-known/opencode (organization defaults)
  2. Global config: ~/.config/opencode/opencode.json or opencode.jsonc
  3. Custom config: $OPENCODE_CONFIG environment variable
  4. Project config: ./opencode.json or ./opencode.jsonc
  5. .opencode/ directories: .opencode/opencode.json
  6. Inline config: $OPENCODE_CONFIG_CONTENT
  7. Enterprise managed: /Library/Application Support/opencode/ (highest priority)

Full Configuration Schema

{
  "$schema": "https://opencode.ai/config.json",
  "logLevel": "info",
  "model": "anthropic/claude-sonnet-4-20250514",
  "small_model": "anthropic/claude-3-5-haiku-20241022",
  "default_agent": "build",
  "username": "custom-name",
  "snapshot": true,
  "share": "manual",
  "autoupdate": true,

  "server": { },

  "agent": {
    "build": {
      "model": { "providerID": "anthropic", "modelID": "claude-sonnet-4-20250514" },
      "temperature": 0.7,
      "topP": 0.9,
      "steps": 50,
      "prompt": "Custom system prompt addition"
    },
    "plan": { },
    "general": { },
    "explore": { }
  },

  "command": {
    "deploy": {
      "description": "Deploy the application",
      "template": "Deploy to $1 environment",
      "agent": "build",
      "subtask": false
    }
  },

  "skills": {
    "paths": ["./custom-skills", "~/shared-skills"],
    "urls": ["https://skills.example.com/"]
  },

  "plugin": [
    "my-plugin@1.0.0",
    "file:///local/plugin"
  ],

  "instructions": [
    "path/to/instructions.md",
    "another/instructions.md"
  ],

  "permission": {
    "bash": "ask",
    "edit": "allow",
    "read": {
      "*": "allow",
      "*.env": "ask"
    }
  },

  "disabled_providers": ["groq"],
  "enabled_providers": ["anthropic", "openai"],

  "mcpServers": {
    "example-stdio": {
      "type": "stdio",
      "command": "path/to/mcp-server",
      "args": ["--flag"],
      "env": { "KEY": "value" }
    },
    "example-sse": {
      "type": "sse",
      "url": "https://example.com/mcp",
      "headers": { "Authorization": "Bearer token" }
    }
  },

  "lsp": {
    "typescript": {
      "disabled": false,
      "command": "typescript-language-server",
      "args": ["--stdio"]
    },
    "go": {
      "disabled": false,
      "command": "gopls"
    }
  },

  "shell": {
    "path": "/bin/zsh",
    "args": ["-l"]
  },

  "watcher": {
    "ignore": ["node_modules", ".git"]
  },

  "autoCompact": true
}

Project Directory Structure

project/
|-- opencode.json              # Project-level config
|-- .opencode/
|   |-- opencode.json          # Additional config (merged)
|   |-- commands/
|   |   |-- deploy.md          # project:deploy command
|   |   |-- git/
|   |   |   +-- commit.md      # project:git:commit command
|   |-- agents/
|   |   +-- db-expert.md       # Custom agent definition
|   |-- plugins/
|   |   +-- my-plugin/         # Local plugin directory
|   |-- skill/
|   |   +-- react/
|   |       +-- SKILL.md       # React skill
|   +-- plans/                 # Plan mode output
+-- AGENTS.md                  # Project context file (like CLAUDE.md)

Global Configuration

~/.config/opencode/
|-- opencode.json              # Global config
|-- commands/
|   +-- prime-context.md       # user:prime-context command
+-- skills/
    +-- general/
        +-- SKILL.md

4. Comparison with Claude Code Extensibility

Feature OpenCode Claude Code (Ensemble)
Open Source Yes (MIT) Proprietary CLI, open source plugin ecosystem
Provider Lock-in None - supports 15+ providers Anthropic only
Plugin Format TypeScript/npm packages JSON manifests + YAML agents + Markdown commands
Plugin SDK @opencode-ai/plugin (typed, async) No formal SDK; file-based conventions
Custom Tools TypeScript functions via plugin API Not directly supported (MCP servers instead)
Hook System 15+ typed hook points (before/after) PreToolUse / PostToolUse hooks (shell commands)
Agent Definition JSON config or Markdown files YAML files with frontmatter
Skill System SKILL.md with frontmatter + remote URLs SKILL.md and REFERENCE.md files
Custom Commands Markdown files with $ARG placeholders YAML/Markdown command definitions
MCP Support Yes (stdio + SSE) Yes (via MCP servers in config)
LSP Integration Built-in (diagnostics exposed to AI) Not built-in
Permission System Granular per-tool with glob patterns Allowlist-based (.claude/settings.json)
Configuration JSONC with 7-level precedence JSON settings + YAML manifests
Agent Delegation @general inline + agent tool Task tool with subagent_type
Session Management SQLite-backed, auto-compact Conversation-based
Desktop App Yes (beta) No
Web Interface Yes (console) No
TUI Framework Solid.js + OpenTUI (custom) Ink (React-based)
Runtime Bun Node.js
Architecture Client/server (separable) Monolithic CLI
Skill Discovery Cross-compatible (.claude/, .agents/) Own directories only

Key Differences in Extensibility

  1. Plugin Power: OpenCode plugins are full TypeScript programs that can intercept
    and modify virtually every aspect of the agent lifecycle. Ensemble plugins are
    declarative (YAML/JSON manifests) with shell-based hooks.

  2. Tool Creation: OpenCode allows defining custom tools directly in plugins with
    Zod schemas. Claude Code relies on MCP servers for custom tool creation.

  3. Agent Customization: OpenCode allows deep agent customization (model, temperature,
    permissions, prompts) via config. Ensemble defines agents as YAML files with
    mission/behavior documentation.

  4. Hook Granularity: OpenCode has 15+ specific hook points (chat.params, shell.env,
    permission.ask, etc.). Ensemble has 2 hook points (PreToolUse, PostToolUse).

  5. Cross-Compatibility: OpenCode explicitly supports .claude/skills/ directories,
    making it compatible with Claude Code skill definitions.


5. SDK and API for Building Plugins/Extensions

Plugin SDK (@opencode-ai/plugin)

Published as @opencode-ai/plugin on npm. Provides:

  • Plugin type: The main plugin function signature
  • tool() helper: Creates typed tool definitions with Zod schemas
  • PluginInput: Context object with client, project info, shell access
  • Hooks interface: All available hook points with typed signatures
  • ToolContext: Runtime context for tool execution (sessionID, abort signal, etc.)
  • AuthHook: OAuth and API key authentication provider interface

Client SDK (@opencode-ai/sdk)

Published as @opencode-ai/sdk. Provides a typed client for the OpenCode server API:

import { createOpencodeClient } from "@opencode-ai/sdk"

const client = createOpencodeClient({
  baseUrl: "http://localhost:4096",
  directory: "/path/to/project",
})

The SDK is auto-generated from an OpenAPI spec (packages/sdk/openapi.json).

Server API

OpenCode runs a local HTTP server (default port 4096) with:

  • REST API endpoints for sessions, messages, tools
  • SSE (Server-Sent Events) for real-time streaming
  • OpenAPI specification available at https://opencode.ai/openapi.json

Building a Plugin (Step-by-Step)

  1. Create a new npm package:
mkdir my-opencode-plugin && cd my-opencode-plugin
bun init
bun add @opencode-ai/plugin
  1. Define the plugin (src/index.ts):
import type { Plugin } from "@opencode-ai/plugin"
import { tool } from "@opencode-ai/plugin"

export const MyPlugin: Plugin = async (ctx) => {
  return {
    tool: {
      "my-tool": tool({
        description: "Does something useful",
        args: {
          input: tool.schema.string().describe("Input text"),
        },
        async execute(args, context) {
          // context.sessionID - current session
          // context.directory - project directory
          // context.abort - AbortSignal for cancellation
          // context.metadata() - set tool call metadata
          // context.ask() - request permission
          return `Processed: ${args.input}`
        },
      }),
    },
    "tool.execute.before": async (input, output) => {
      // Intercept any tool call before execution
      console.log(`Tool ${input.tool} called`)
    },
    "shell.env": async (input, output) => {
      // Inject environment variables into shell commands
      output.env["MY_CUSTOM_VAR"] = "value"
    },
  }
}
  1. Register in project config (opencode.json):
{
  "plugin": ["file:///path/to/my-opencode-plugin"]
}

Or publish to npm and reference by package name:

{
  "plugin": ["my-opencode-plugin@1.0.0"]
}

Tool Definition API

import { tool } from "@opencode-ai/plugin"

const myTool = tool({
  description: "Human-readable description for the LLM",
  args: {
    // Uses Zod schemas
    filePath: tool.schema.string().describe("Path to the file"),
    lines: tool.schema.number().optional().describe("Number of lines"),
    options: tool.schema.object({
      recursive: tool.schema.boolean(),
    }).optional(),
  },
  async execute(args, context) {
    // args is fully typed from the schema
    // Must return a string (the tool output shown to the LLM)
    return "result"
  },
})

Authentication Plugin API

Plugins can provide authentication for custom LLM providers:

export const MyAuthPlugin: Plugin = async (ctx) => {
  return {
    auth: {
      provider: "my-provider",
      methods: [
        {
          type: "api",
          label: "API Key",
          prompts: [
            {
              type: "text",
              key: "apiKey",
              message: "Enter your API key",
              placeholder: "sk-...",
            },
          ],
          async authorize(inputs) {
            return {
              type: "success",
              key: inputs.apiKey,
              provider: "my-provider",
            }
          },
        },
        {
          type: "oauth",
          label: "Login with MyProvider",
          async authorize() {
            return {
              url: "https://my-provider.com/oauth",
              instructions: "Complete login in your browser",
              method: "auto",
              async callback() {
                // Exchange code for tokens
                return {
                  type: "success",
                  refresh: "refresh-token",
                  access: "access-token",
                  expires: Date.now() + 3600000,
                }
              },
            }
          },
        },
      ],
    },
  }
}

6. Relevance to Ensemble

Opportunities

  1. Cross-Compatibility: OpenCode already scans .claude/skills/ directories.
    Ensemble skills defined as SKILL.md files would be automatically discovered
    by OpenCode users.

  2. Plugin Inspiration: OpenCode's typed hook system with 15+ hook points is
    more granular than Ensemble's PreToolUse/PostToolUse. Consider expanding
    Ensemble's hook points.

  3. Client/Server Pattern: OpenCode's separable client/server architecture
    enables remote driving (mobile app, web). This could inspire Ensemble features.

  4. Skill URLs: OpenCode supports remote skill repositories via URL + index.json.
    Ensemble could adopt a similar pattern for distributing skills.

  5. Permission Granularity: OpenCode's glob-pattern permissions per tool
    (e.g., read: { "*.env": "ask" }) are more granular than Ensemble's allowlist.

Competitive Positioning

  • OpenCode is provider-agnostic; Ensemble is Claude-native (strength for Anthropic users)
  • OpenCode has richer programmatic extensibility; Ensemble has richer declarative agent mesh
  • OpenCode's 28-agent mesh equivalent would require custom agent configs in JSON
  • Ensemble's multi-tier plugin architecture (core/workflow/framework/testing) has no
    direct parallel in OpenCode

Sources

  • GitHub Repository: https://github.com/anomalyco/opencode (dev branch, accessed 2026-02-26)
  • Plugin SDK source: packages/plugin/src/index.ts, tool.ts, example.ts
  • Core config: packages/opencode/src/config/config.ts
  • Agent system: packages/opencode/src/agent/agent.ts
  • Skill system: packages/opencode/src/skill/skill.ts, discovery.ts
  • Command system: packages/opencode/src/command/index.ts
  • Plugin loader: packages/opencode/src/plugin/index.ts
  • Documentation site: https://opencode.ai/docs
  • npm package: opencode-ai (CLI), @opencode-ai/plugin (SDK), @opencode-ai/sdk (client)

@claude

claude Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Review: See detailed analysis in subsequent comment

ldangelo and others added 2 commits February 26, 2026 13:33
OC-S2-HK-001 through HK-007, TEST-007, TEST-008, DIST-003:
HookBridgeGenerator discovers hooks.json files and maps
PreToolUse/PostToolUse to OpenCode's tool.execute.before/after.
Supports matcher patterns (exact, wildcard, regex), env var
bridging, and blocking behavior. Plugin entry point updated
with hook wiring. 62 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Review: feat: OpenCode runtime support - translation layer and plugin

Great work on Sprint 1! The 203-test suite and clean pipeline architecture give confidence. Here is my detailed feedback.

Architecture and Design

Dual JS/TS file strategy is confusing

The repo ships paired .js (runtime) and .ts (stub/type) files for the same modules without a build pipeline that compiles TS to JS:

  • scripts/generate-opencode/index.ts re-exports from index.js for backward compat only
  • scripts/generate-opencode/src/command-translator.ts is a thin stub
  • scripts/generate-opencode/src/agent-translator.ts and hook-bridge.ts are empty TODO classes
  • scripts/generate-opencode/src/skill-copier.ts re-exports the JS implementation

Recommendation: Either commit to TypeScript (add a proper build step) or remove the .ts stubs and add them in Sprint 2. They currently add confusion without value.

Custom YAML parser duplicates an existing dependency

packages/opencode/lib/skill-copier.js implements parseFrontmatter/parseSimpleYaml/stringifyFrontmatter, yet packages/opencode/package.json already lists gray-matter@^4.0.3 as a dependency. This reinvents the wheel while paying the dependency cost. The custom parser has correctness gaps described below. Recommend using gray-matter directly.

Potential Bugs

Bug 1: parseFrontmatter fails on CRLF line endings

In packages/opencode/lib/skill-copier.js, the startsWith check against the LF-terminated frontmatter delimiter fails on Windows CRLF files. Also, const trimmed = input; -- the variable is named trimmed but no trimming is done. A leading BOM will cause silent parse failure. gray-matter handles all these cases.

Bug 2: stringifyFrontmatter produces invalid YAML for embedded double-quotes

The function only wraps values containing colon, hash, braces in double-quotes. A description with embedded double-quotes will produce malformed YAML. gray-matter/js-yaml handle escaping correctly.

Bug 3: usesArguments uses JSON serialization for text detection

JSON.stringify serializes the entire parsed command object on every call and may escape content differently than raw YAML text. A targeted check of mission.summary, step descriptions, and action arrays is more reliable.

Bug 4: AGENTS.md unconditionally added to instructions

generateInstructionsSection in manifest-generator.js always appends AGENTS.md to the instructions array even when that file does not exist. This file is not created until a later pipeline stage (not Sprint 1). If OpenCode validates instruction paths eagerly, this will fail. Recommend checking file existence first.

Bug 5: Incomplete ARGUMENT_PLACEHOLDERS map

The PRD mentions 15+ commands but only 8 are mapped in command-translator.js. Unmapped commands silently fall through to a generic dollar-SLUG_INPUT fallback. For example, ensemble:playwright-test would produce dollar-PLAYWRIGHT_TEST_INPUT. The test suite does not exercise this fallback against the full real command set.

CI/CD Issues

Staleness check assumes dist/opencode/ is committed

In .github/workflows/opencode-generate.yml the staleness check uses git diff after regenerating output. If dist/opencode/ is in .gitignore, git will never report differences and the check is a no-op. Please confirm whether generated output is committed or gitignored and update accordingly. Also git checkout -- . resets the entire worktree -- scoping it to dist/ would be safer.

Performance

The hash-based incremental caching is well-designed. One note: the skill cache uses a single aggregated hash (_skillHash), so any single skill change causes a full re-copy of all skills. Acceptable for 10 skills but worth revisiting at scale.

Minor Items

Hardcoded model version strings will go stale: manifest-generator.js hardcodes anthropic/claude-sonnet-4-20250514 as the default model, and command-translator.js has the same in MODEL_MAP. Centralizing in a MODEL_DEFAULTS config file would make updates easier.

Unused gray-matter dependency: packages/opencode/package.json lists both gray-matter and js-yaml but the JS implementation never calls gray-matter (the custom parser replaced it). Remove it or use it.

marketplace.json category: The new ensemble-opencode entry uses category: core. Consider runtime or integration to distinguish it from core infrastructure packages.

Unchecked PR checklist: The npm run validate passes item is unchecked. Please confirm before merge since validate.yml now includes OpenCode plugin.json validation.

What Is Working Well

  • Clean pipeline architecture with well-separated concerns (SkillCopier, CommandTranslator, ManifestGenerator)
  • TDD approach with 203 tests and 6 snapshot tests provides strong confidence
  • Hash-based incremental generation is production-quality
  • Task ID traceability (OC-S1-CMD-001 etc.) ties nicely to the TRD
  • Conservative permission defaults (bash: ask) are the right call for a first release
  • --dry-run and --validate flags make the tool safe to use in CI
  • Clear sprint boundary: deferred items are explicitly marked as empty stubs

Summary

The core translation logic is solid. Main issues to address:

  1. Bug (fix in this PR): Replace the custom frontmatter parser with gray-matter to fix CRLF, BOM, and quote-escaping
  2. Bug (fix in this PR): Fix or guard the unconditional AGENTS.md instruction entry
  3. Clarity (follow-up OK): Resolve the JS/TS dual-file strategy
  4. CI (follow-up OK): Verify and fix the staleness check against .gitignore reality
  5. Completeness (follow-up OK): Complete or document the ARGUMENT_PLACEHOLDERS gaps

Reviewed with Claude Code

ldangelo and others added 2 commits February 26, 2026 13:37
Implement OC-S2-AGT-001 through OC-S2-AGT-014 and OC-S2-TEST-006:
- AgentTranslator class: YAML agents → OpenCode JSON config + Markdown
- Tool permission mapping (Read/Write/Edit → file, Bash → shell, etc.)
- Mode classification (plan, bypassPermissions, default)
- Routing prompt generation with @agent-name references
- Category metadata tags for agent organization
- 5 representative agent fixtures (orchestrator, developer, quality,
  workflow, utility)
- 81 unit tests + 10 snapshot tests

Replaces TypeScript stub with full JS implementation and .d.ts declarations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Integrate AgentTranslator into generate-opencode CLI (step 3)
- Agent config block now flows into opencode.json manifest (MF-002)
- Update package-structure test to accept .js/.d.ts module variants
- Update generator-cli test: AgentTranslator is no longer deferred
- Mark all OC-S2-AGT-* and OC-S2-TEST-004/005/006 tasks as done in TRD
- Mark OC-S3-MF-002 as done (agent block generation)

All 346 tests pass across 9 test suites (16 snapshots).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Review: feat: OpenCode runtime support - translation layer and plugin

This is an impressive, well-scoped PR with solid test coverage (203 tests, 6 snapshots). The architecture — a build-time translation layer producing OpenCode-compatible artifacts from Ensemble's existing YAML/JSON/Markdown sources — is sound. The deferred scope for Sprint 2 (agent mesh, hook bridge) is appropriate. Below are my findings, organized by severity.


🔴 Issues (Should Fix Before Merge)

1. ReDoS / Regex injection in matchToolName (packages/opencode/src/hooks/bridge.js)

The matcher string read from hooks.json is passed directly into new RegExp(...) without sanitisation:

const regex = new RegExp('^(?:' + matcher + ')$');

A maliciously crafted hooks.json or a typo could produce a catastrophic backtracking pattern or silently match everything. Since hooks.json files come from third-party packages installed into the monorepo, this is a realistic attack surface.

Recommendation: Either use a safe regex escape library, or restrict matchers to an allow-list of patterns (exact name, *, or a pre-validated regex field).


2. execSync blocks the Node.js event loop (packages/opencode/src/hooks/bridge.js)

defaultExecutor wraps execSync in a function declared async. The function returns a Promise, but the underlying call is still synchronous — it blocks the entire event loop for up to 30 seconds per hook. OpenCode is likely async/await throughout, and this will freeze its runtime.

// Current — blocks event loop despite async declaration
async function defaultExecutor(command, env) {
  const stdout = execSync(command, { timeout: 30000, ... });
  ...
}

Replace execSync with child_process.execFile wrapped in a Promise (or use util.promisify).


3. Incorrect types field in packages/opencode/package.json

"types": "src/index.ts"

The types field must point to a .d.ts declaration file, not a .ts source file. TypeScript consumers will see compilation errors. It should be "types": "src/index.d.ts" (generated by tsc), or omitted until a build step produces the declaration file.


4. Unused gray-matter dependency in packages/opencode/package.json

gray-matter is listed as a runtime dependency, but the implementation uses a hand-rolled parseFrontmatter/parseSimpleYaml. This adds ~200 KB of dead weight to the installed package and will appear as an unused dependency in audits. Remove it, or replace the custom parser with gray-matter to simplify the code.


🟡 Warnings (Strong Suggestions)

5. Committed compiled JS alongside TypeScript source

Both .ts source files and their compiled .js counterparts are committed (e.g., scripts/generate-opencode/src/command-translator.ts + .js, packages/opencode/src/index.ts + .js). This creates a drift risk — the .ts and .js can get out of sync.

Options:

  • Add *.js (in the relevant dirs) to .gitignore and compile in CI/as a pre-publish step.
  • Or remove the .ts files and work in plain JS (simpler, no build step needed for a generator script).

6. Hardcoded capability counts in ENSEMBLE_META (packages/opencode/src/index.js)

const ENSEMBLE_META = { agents: 28, commands: 15, skills: 10, ... };

These will silently go stale as the ecosystem grows. The ManifestGenerator already counts packages — pipe that data into the plugin's metadata at build time, or compute it dynamically by scanning packages/*/ at startup.

7. Code duplication: parseFrontmatter / parseSimpleYaml / stringifyFrontmatter

These helpers are duplicated verbatim in:

  • packages/opencode/lib/skill-copier.js
  • scripts/generate-opencode/src/skill-copier.ts (TypeScript version)

Extract to a shared lib/frontmatter.js utility and require it from both places.

8. Silent error swallowing in loadHookBridge (packages/opencode/src/index.js)

} catch (_err) {
  // Skip files that cannot be parsed
}

And the outer:

} catch (_err) {
  // If discovery fails, return empty hooks
}

Both swallow errors completely. Hook registration failures should at minimum log a warning so operators know hooks are not running. In a plugin context, silent failures are hard to debug.

9. CI staleness check logic is fragile (opencode-generate.yml)

The staleness check resets all changes with git checkout -- . then generates fresh output and does git diff --exit-code. This fails if dist/opencode/ is gitignored (common for generated artifacts). If the intent is to commit dist/opencode/ to the repo, that should be documented explicitly. If not, the check is meaningless. Clarify the contract in the workflow comments.

10. Incremental 2-second performance test is environment-sensitive

expect(result.elapsed).toBeLessThan(2000);

This assertion is fragile on CI runners under load. The hash check is lightweight, but spawning a child process, loading commander, scanning filesystem, and writing cache files easily exceeds 2s on slow GitHub-hosted runners. Consider relaxing to 5s or skipping the timing assertion in --ci mode.


🟢 Observations / Minor Points

11. marketplace.json plugin name inconsistency

The npm package is @fortium/ensemble-opencode but marketplace.json and plugin.json use ensemble-opencode (no scope). The other packages use the scoped name in npm but unscoped in plugin.json — so this matches existing convention, but worth confirming it won't break the validate script's name-prefix check.

12. generate:opencode not hooked into npm run generate

The CLAUDE.md lists npm run generate as the way to "Regenerate markdown from YAML", but the new OpenCode generator requires a separate npm run generate:opencode invocation. Consider adding it to the main generate pipeline (or at least the validate step) so it doesn't fall out of sync.

13. --validate flag in generate:opencode has no OpenCode schema to validate against

Looking at the implementation, --validate checks that opencode.json is valid JSON and has expected keys, but there's no official schema file for opencode.json. Document the assumption, or link to the upstream schema if one exists.


Positives Worth Calling Out

  • Excellent task-ID traceability throughout the code (OC-S1-CMD-001, etc.) — makes auditing against the TRD straightforward.
  • The --dry-run flag and hash-based incremental cache are well-implemented.
  • The testable executor injection in createHookBridge is great design — it makes the bridge fully unit-testable without spawning real processes.
  • 203 tests with good coverage across all new modules is a high bar for a new package.
  • Deferring agent mesh and hook bridge to Sprint 2 is the right call — the Sprint 1 scope is coherent and shippable.

Overall this is close to merge-ready. The execSync-blocking-event-loop bug (#2) and the ReDoS risk (#1) are the ones I'd prioritize before shipping.

- Add ensemble-opencode to Architecture listing under Runtime Translation
- Add OpenCode Support section with generator CLI usage, installation
  instructions, and output directory structure
@claude

claude Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

PR Review: feat: OpenCode runtime support - translation layer and plugin

Great scope and well-structured PR! The translation layer concept is sound, the TRD coverage is thorough, and 203 passing tests with snapshots is a solid baseline. Below are the items that should be addressed before merging, roughly prioritized.


🔴 High Priority

1. Dual .js/.ts files create ambiguity about the canonical source

Every translator has both a .js and a .ts file committed:

  • scripts/generate-opencode/index.js + index.ts
  • scripts/generate-opencode/src/command-translator.js + command-translator.ts
  • scripts/generate-opencode/src/manifest-generator.js + manifest-generator.ts
  • etc.

The index.ts comment says "The actual implementation lives in index.js" and the .ts is "for backward compatibility if anyone invokes via ts-node". This is backwards from the standard pattern (TypeScript is source, .js is compiled output). The result is two diverging implementations in the same repo with no build step enforcing consistency.

Recommendation: Pick one canonical format. If Node CJS without transpilation is the goal (reasonable for a generator script), remove the .ts files and keep just .js. If TypeScript with a build step is desired, set up tsc properly, add the output directory to .gitignore, and remove the hand-written .js files.


2. dist/opencode/ staleness check forces committing generated artifacts

The CI workflow (opencode-generate.yml) regenerates dist/opencode/ and then checks git diff --exit-code to detect staleness. This means developers must commit the generated output on every PR that touches sources. dist/ is not in .gitignore.

This is a significant maintenance overhead and is inconsistent with how the rest of the project treats generated files (the generate script output is not committed). It also means every diff to source YAML will have a corresponding dist/opencode/ diff bloating PRs.

Recommendation: Either add dist/opencode/ to .gitignore and remove the staleness check, or document explicitly that committing generated artifacts is the chosen strategy (and make --force the default in CI so it always regenerates cleanly).


3. Unsafe RegExp construction from matcher in bridge.js

// packages/opencode/src/hooks/bridge.js
const regex = new RegExp('^(?:' + matcher + ')$');

The matcher string from hooks.json is embedded directly in RegExp() without escaping. While hooks.json is project-owned, any specially crafted pattern can cause Catastrophic Backtracking (ReDoS). E.g. a matcher like (a+)+b or (x|x|x|x|x|x|x|x|x|x|x|x|x|x|x)y will hang Node.js under test input.

Recommendation: Use a safe regex escape function or validate matcher values against a whitelist of allowed patterns (exact strings, *, and simple |-delimited alternatives).


4. execSync(command, ...) with no command path validation

// packages/opencode/src/hooks/bridge.js
const stdout = execSync(command, { env: mergedEnv, timeout: 30000, ... });

The command string comes from hooks.json files discovered at runtime. While ${CLAUDE_PLUGIN_ROOT} is substituted, there is no validation that the resulting command path is within the expected plugin directory. A malicious or accidentally misconfigured hooks.json could execute arbitrary system commands.

This is roughly equivalent risk to Ensemble's existing hooks.json system—but since this bridge runs in the OpenCode context (potentially with different permissions), it deserves explicit validation and a security notice in the README.

Recommendation: Validate that the resolved command path starts with pluginRoot before executing. Document the trust model (hooks.json files are project-controlled, not user-controlled).


🟡 Medium Priority

5. README/CLAUDE.md document deferred features as already implemented

The README update says:

Agents: Converts 28 agent YAML definitions to OpenCode JSON config + Markdown agent files
Hooks: Bridges Ensemble PreToolUse/PostToolUse hooks to OpenCode's typed hook API

But the PR summary clearly states both Agent Translation and Hook Bridge are deferred to Sprint 2. The documentation describes planned end-state, not current state, which will confuse users who install the package expecting full agent/hook support.

Recommendation: Update README to accurately reflect what's implemented now vs. what's planned for Sprint 2. Use "coming in v6.x" or similar language.


6. Version disparity: ensemble-opencode is 5.3.0, ecosystem is 5.1.0

All other packages are at 5.0.0 or 5.1.0. The new ensemble-opencode package starts at 5.3.0. The CLAUDE.md update also describes it as v5.3.0 while calling the ecosystem v5.1.0.

Recommendation: Either align on 5.1.0 (or 5.2.0) for initial release, or add a note explaining the versioning rationale.


7. plugin.json is missing standard fields

// packages/opencode/.claude-plugin/plugin.json
{
  "name": "ensemble-opencode",
  "version": "5.3.0",
  ...
  // No "commands", "skills", or "agents" fields
}

Every other plugin.json in the ecosystem includes paths for commands, skills, and/or agents. The opencode plugin omits them, which means it won't register with Claude Code's plugin loader even if installed there.

Recommendation: Add "commands": "./commands" or "skills": "./skills" if applicable, or add a comment explaining why this package intentionally has no Claude Code commands/skills.


8. package.json types field points to a .ts source file

"main": "src/index.js",
"types": "src/index.ts"

The types field conventionally points to a .d.ts declaration file, not a raw .ts source file. TypeScript consumers importing this package will get surprising behavior.

Recommendation: Generate src/index.d.ts via tsc --declaration and point types there, or remove types entirely if the package isn't meant to be consumed as a TypeScript library.


9. npm ci --legacy-peer-deps in CI

- name: Install dependencies
  run: npm ci --legacy-peer-deps

--legacy-peer-deps bypasses peer dependency validation and can silently mask conflicts. If this flag is needed, the root cause (likely the optional @opencode-ai/plugin peer dependency) should be addressed in package.json rather than bypassed in CI.

Recommendation: Move @opencode-ai/plugin to optionalDependencies with a proper version range, which eliminates the need for this flag.


🟢 Positive Notes

  • Comprehensive test suite: 203 tests with snapshots and performance benchmarks is excellent for a new subsystem of this complexity.
  • TDD discipline: Tests are written with clear task ID traceability (OC-S1-CMD-001, etc.) that maps directly to TRD requirements.
  • Incremental generation: The SHA-256 hash cache for incremental builds is a thoughtful performance optimization.
  • Conservative permissions: DEFAULT_PERMISSIONS defaults bash to ask rather than allow—the right choice.
  • Staleness/regression split in CI: Separating the "unintended modification" check from the staleness check is good CI design.
  • Deferred items are documented: The PR clearly calls out what's Sprint 2 scope, which is much better than silently omitting it.

Summary

Priority Issue Suggested Action
🔴 Dual .js/.ts source files Pick one canonical format
🔴 dist/opencode/ staleness commits Add to .gitignore or document strategy
🔴 Unsafe RegExp from matcher Escape/validate matcher values
🔴 execSync without path validation Validate command path within pluginRoot
🟡 README documents deferred features Update to reflect current implementation state
🟡 Version 5.3.0 vs ecosystem 5.1.0 Align versioning
🟡 plugin.json missing standard fields Add or comment on omission
🟡 types → raw .ts file Use .d.ts or remove
🟡 --legacy-peer-deps in CI Address root dependency conflict

Happy to discuss any of these—the overall direction is excellent and most of these are straightforward to fix.

🤖 Reviewed with Claude Code

ldangelo and others added 7 commits March 10, 2026 22:10
Resolve 4 of 5 open questions in TRD v1.0.0:
- br --json support confirmed, updated all br list calls to use --json
- br create --json for bead ID capture
- br comment add replaces removed comment functionality
- NTM naming uses TRD_SLUG-track-N pattern

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…wheel instructions

Implement TRD for beads br/bv migration with 35 tasks across 4 phases:
- Phase 1: Foundation (br/bv CLI setup, wheel-based installation)
- Phase 2: Core Migration (command migration from bd to br/bv)
- Phase 3: Integration (workflow integration and validation)
- Phase 4: Cleanup (bd removal and documentation updates)

Includes YAML command definition, generated markdown, and PRD.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 35 TRD tasks marked complete — tracked via br beads with
full dependency graph (51 edges, 42 nodes). Implementation
completed in commit e30f297.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… PRD

- Add mandatory @code-reviewer step before task closure (max 2 review rounds)
- Add --description to br create for epic/story/task beads with full TRD context
- Add closing comments with structured work summaries from agents
- Add @tech-lead-orchestrator to routing table for architecture/design tasks
- Expand keyword routing for @documentation-specialist
- Add PRD for team-based execution model (role-based multi-agent collaboration)
- Bump implement-trd-beads to v2.1.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ldangelo
ldangelo merged commit cd50186 into main Mar 14, 2026
1 of 6 checks passed
@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

PR #46 Code Review: feat: OpenCode runtime support

Summary

This is a substantial PR (+15,873/-1,394 lines) adding OpenCode runtime support to the Ensemble ecosystem. The overall approach is well-architected and the test suite is thorough for individual components (346 tests, 16 snapshots across 9 suites). However, there are several concrete issues worth addressing before merge.


🔴 Critical Issues

1. Staleness check in CI will always pass vacuously (.github/workflows/opencode-generate.yml, lines 50–65)

dist/ is listed in .gitignore. The staleness check runs npm run generate:opencode -- --force --output-dir dist/opencode then git diff --exit-code --quiet. Because dist/opencode/ is gitignored, git will never report it as changed — the check always reports "Generated output is up to date" regardless of actual state. OC-S4-CI-003 is completely non-functional.

Either commit dist/opencode/ (remove from .gitignore) or use a hash-based comparison instead of git diff.

2. ManifestGenerator class is not used in the actual pipeline (scripts/generate-opencode/index.js, lines 355–392)

Step 5 inlines a simplified manifest rather than calling ManifestGenerator. The standalone class has 35 unit tests, but those tests validate code the generator CLI never instantiates. The produced opencode.json:

  • Uses hard-coded $schema: https://opencode.ai/schema/opencode.json instead of the correct https://opencode.ai/config.json
  • Sets plugin: [] (empty), omitting the ensemble-opencode plugin reference
  • Sets instructions: [] (empty), omitting CLAUDE.md and AGENTS.md references

Tests validate the ManifestGenerator in isolation but do not catch that the pipeline bypasses it entirely.

3. HookBridgeGenerator is implemented but the pipeline skips it (scripts/generate-opencode/index.js, lines 345–353)

Step 4 is marked "deferred - Sprint 2" and skipped, yet the PR includes a full HookBridgeGenerator implementation with tests. If the code is done, wire it in; if genuinely deferred, it should not be in this PR. This creates confusion about what is production-ready.


🟡 Bugs

4. Unused variable skillCacheKey (scripts/generate-opencode/index.js, line 221)

const skillCacheKey = 'skills'; is assigned but never referenced — the composite hash is stored under _skillHash instead. Dead code.

5. Model ID inconsistency between translators

  • command-translator.js (lines 29–34): sonnet'anthropic/claude-sonnet-4-20250514'
  • agent-translator.js (lines 31–37): sonnet'claude-sonnet-4-6'
  • manifest-generator.js (line 240): hard-codes 'anthropic/claude-sonnet-4-20250514'

These should be identical. This produces inconsistent OpenCode configs depending on whether the model hint originates from a command or agent YAML.

6. Hook timeout field is silently dropped at runtime

hook-bridge.js (line 151) collects hookDef.timeout from hooks.json, but packages/opencode/src/hooks/bridge.js ignores it entirely, using a hard-coded 30s timeout via execSync. Silent contract violation.


🟡 Design / Architecture Concerns

7. Runtime artifacts committed to .beads/

.beads/dolt-monitor.pid.lock and .beads/interactions.jsonl (both empty) appear to be machine-specific runtime state that shouldn't be in source control. The .beads/.gitignore already lists *.lock — these may have been committed before the gitignore was applied.

8. Dual TypeScript stub + compiled JS pattern creates maintenance risk

Multiple files exist in both .ts stub form and hand-written .js form:

  • command-translator.ts (23-line stub) + command-translator.js (438 lines)
  • hook-bridge.ts (55-line stub) + hook-bridge.js (247 lines)
  • manifest-generator.ts (25-line stub) + manifest-generator.js (322 lines)
  • packages/opencode/src/index.ts (192 lines) + src/index.js (135 lines)

package.json sets "main": "src/index.js", so TypeScript source is never executed. Commit to pure JS (remove .ts stubs) or commit to TypeScript (generate .js via tsc, gitignore compiled output).

9. package.json types field is incorrect

packages/opencode/package.json: "types": "src/index.ts". The types field should point to a .d.ts declaration file, not a raw .ts source file. Breaks in strict publishing scenarios.

10. plugin.json is missing commands and skills fields

Per CLAUDE.md's Plugin Manifest spec and the schema used elsewhere, packages/opencode/.claude-plugin/plugin.json should include commands and skills fields.


🟡 Code Quality

11. toTitleCase is duplicated with different behavior

command-translator.js has a toTitleCase that handles acronyms (with an ACRONYMS set); agent-translator.js has a simpler version that does not. Agent names like api-documentation-specialist will render as "Api Documentation Specialist" instead of "API Documentation Specialist". These should be a shared utility.

12. SkillCopier hand-rolls a YAML parser instead of using js-yaml

packages/opencode/lib/skill-copier.js (lines 24–90) implements parseFrontmatter and parseSimpleYaml manually. js-yaml is already in package.json dependencies. A hand-rolled parser introduces correctness risk for multi-line strings, quoted values with colons, and nested structures.

13. Silent error swallowing in the plugin entry point (packages/opencode/src/index.js, lines 87–92)

} catch (_err) {
  // Skip files that cannot be parsed
}

If hook discovery fails for any reason (e.g., incorrect monorepo root path), the plugin loads with zero hooks and no diagnostic output. At minimum, add a console.warn with the error message.


🟡 Test Coverage Gaps

14. No integration test verifying ManifestGenerator is called by the CLI

Generator-CLI tests validate the CLI as a black box but don't assert on opencode.json contents. No test catches that plugin: [] and instructions: [] are always empty (issue #2 above).

15. Performance tests have no quality assertions

tests/performance.test.js tests timing benchmarks only. Tests asserting on output correctness ("28 agents appear in opencode.json", "all command YAMLs produce corresponding .md files") would prevent silent regressions more effectively than timing-only checks.


Nits

  • .beads/metadata.json is missing a trailing newline.
  • CLAUDE.md architecture diagram still shows "25 packages" while Key Paths section was updated to 26.
  • PR description lists "203 passed" but final test count is 346.

Verdict

The translation layer concept is well-architected and the per-component test suite is solid. Before merging, the following should be addressed:

Priority Issue
Must fix Staleness CI check is non-functional (#1)
Must fix Wire ManifestGenerator into the pipeline or remove the unused class (#2)
Must fix Resolve HookBridgeGenerator ambiguity — wire in or remove from this PR (#3)
Should fix Model ID inconsistency between translators (#5)
Should fix Remove committed runtime artifacts from .beads/ (#7)
Should fix Hand-rolled YAML parser in SkillCopier (#12)

Review generated by Claude Code (claude-sonnet-4-6)

ldangelo added a commit that referenced this pull request Jun 18, 2026
feat: OpenCode runtime support - translation layer and plugin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant