Files
claude-engineering-plugin/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md
2026-02-20 13:32:52 -05:00

9.2 KiB

Decision Log: OpenCode Commands as .md Files

Decision: ADR-001 - Store Commands as Individual .md Files

Date: 2026-02-20
Status: Adopted

Context

The original design stored commands configurations inline in opencode.json under config.command. This tightly couples command metadata with config, making it harder to version-control commands separately and share command files.

Decision

Store commands definitions as individual .md files in .opencode/commands/ directory, with YAML frontmatter for metadata and markdown body for the command prompt.

New Type:

export type OpenCodeCommandFile = {
  name: string    // command name, used as filename stem: <name>.md
  content: string // full file content: YAML frontmatter + body
}

Bundle Structure:

export type OpenCodeBundle = {
  config: OpenCodeConfig
  agents: OpenCodeAgentFile[]
  commandFiles: OpenCodeCommandFile[]  // NEW
  plugins: OpenCodePluginFile[]
  skillDirs: { sourceDir: string; name: string }[]
}

Consequences

  • Positive: Commands can be versioned, shared, and edited independently
  • Negative: Requires updating converter, writer, and all consumers
  • Migration: Phase 1-4 will implement the full migration

Alternatives Considered

  1. Keep inline in config - Rejected: limits flexibility
  2. Use separate JSON files - Rejected: YAML frontmatter is more idiomatic for command

Decision: Phase 2 - Converter Emits .md Files

Date: 2026-02-20
Status: Implemented

Context

The converter needs to populate commandFiles in the bundle rather than config.command.

Decision

convertCommands() returns OpenCodeCommandFile[] where each file contains:

  • filename: <command-name>.md
  • content: YAML frontmatter (description, optionally model) + body (template text with Claude path rewriting)

Frontmatter Structure

---
description: "Review code changes"
model: openai/gpt-4o
---

Template text here...

Filtering

  • Commands with disableModelInvocation: true are excluded from output

Path Rewriting

  • .claude/ paths rewritten to .opencode/ in body content (via rewriteClaudePaths())

Consequences

  • Converter now produces command files ready for file-system output
  • Writer phase will handle writing to .opencode/commands/ directory
  • Phase 1 type changes are now fully utilizeds

Decision: Phase 3 - Writer Writes Command .md Files

Date: 2026-02-20
Status: Implemented

Context

The writer needs to write command files from the bundle to the file system.

Decision

In src/targets/opencode.ts:

  • Add commandDir to return value of resolveOpenCodePaths() for both branches
  • In writeOpenCodeBundle(), iterate bundle.commandFiles and write each as <commandsDir>/<name>.md with backup-before-overwrite

Path Resolution

  • Global branch (basename is "opencode" or ".opencode"): commandsDir: path.join(outputRoot, "commands")
  • Custom branch: commandDir: path.join(outputRoot, ".opencode", "commands")

Writing Logic

for (const commandFile of bundle.commandFiles) {
  const dest = path.join(openCodePaths.commandDir, `${commandFile.name}.md`)
  const cmdBackupPath = await backupFile(dest)
  if (cmdBackupPath) {
    console.log(`Backed up existing command file to ${cmdBackupPath}`)
  }
  await writeText(dest, commandFile.content + "\n")
}

Consequences

  • Command files are written to .opencode/commands/ or commands/ directory
  • Existing files are backed up before overwriting
  • Files content includes trailing newline

Alternatives Considered

  1. Use intermediate variable for commandDir - Rejected: caused intermittent undefined errors
  2. Use direct property reference openCodePaths.commandDir - Chosen: more reliable

Decision: ADR-002 - User-Wins-On-Conflict for Config Merge

Date: 2026-02-20
Status: Adopted

Context

When merging plugin config into existing opencode.json, conflicts may occur (e.g., same MCP server name with different configuration). The merge strategy must decide which value wins.

Decision

User config wins on conflict. When plugin and user both define the same key (MCP server name, permission, tool), the user's value takes precedence.

Rationale

  • Safety first: Do not overwrite user data with plugin defaults
  • Users have explicit intent in their local config
  • Plugins should add new entries without modifying user's existing setup
  • Aligns with AGENTS.md principle: "Do not delete or overwrite user data"

Merge Algorithm

const mergedMcp = {
  ...(incoming.mcp ?? {}),
  ...(existing.mcp ?? {}),  // existing takes precedence
}

Same pattern applied to permission and tools.

Fallback Behavior

If existing opencode.json is malformed JSON, warn and write plugin-only config rather than crashing:

} catch {
  console.warn(`Warning: existing ${configPath} is not valid JSON. Writing plugin config without merging.`)
  return incoming
}

Consequences

  • Positive: User config never accidentally overwritten
  • Positive: Plugin can add new entries without conflict
  • Negative: Plugin cannot modify user's existing server configuration (must use unique names)
  • Negative: Silent merge may mask configuration issues if user expects plugin override

Alternatives Considered

  1. Plugin wins on conflict - Rejected: would overwrite user data
  2. Merge and combine arrays - Rejected: MCP servers are keyed object, not array
  3. Fail on conflict - Rejected: breaks installation workflow

Decision: ADR-003 - Permissions Default "none" for OpenCode Output

Date: 2026-02-20
Status: Implemented

Context

When installing a Claude plugin to OpenCode format, the --permissions flag determines whether permission/tool mappings is written to opencode.json. The previous default was "broad", which writes global permissions to the user's config file.

Decision

Change the default value of --permissions from "broad" to "none" in the install command.

Rationale

  • User safety: Writing global permissions to opencode.json pollutes user config and may grant unintended access
  • Principle alignment: Follows AGENTS.md "Do not delete or overwrite user data"
  • Explicit opt-in: Users must explicitly request --permissions broad to write permissions to their config
  • Backward compatible: Existing workflows using --permissions broad continues to work

Implementation

In src/commands/install.ts:

permissions: {
  type: "string",
  default: "none", // Default is "none" -- writing global permissions to opencode.json pollutes user config. See ADR-003.
  description: "Permission mapping written to opencode.json: none (default) | broad | from-command",
},

Test Coverage

Added two CLI tests cases:

  1. install --to opencode uses permissions:none by default - Verifies no permission or tools key in output
  2. install --to opencode --permissions broad writes permission block - Verifies permission key is written when explicitly requested

Consequences

  • Positive: User config remains clean by default
  • Positive: Explicit opt-in required for permission writing
  • Negative: Users migrating from older versions need to explicitly use --permissions broad if they want permissions
  • Migration path: Document the change in migration notes

Alternatives Considered

  1. Keep "broad" as default - Rejected: pollutes user config
  2. Prompt user interactively - Rejected: break CLI automation
  3. Write to separate file - Rejected: OpenCode expects permissions in opencode.json

Decision: Phase 6 - Documentation Update

Date: 2026-02-20
Status: Complete

Context

All implementation phases complete. Documentation needs to reflect the final behavior.

Decision

Update AGENTS.md and README.md:

AGENTS.md Changes

  1. Line 10 - Updated Output Paths description:

    - **Output Paths:** Keep OpenCode output at `opencode.json` and `.opencode/{agents,skills,plugins}`. For OpenCode, command go to `~/.config/opencode/commands/<name>.md`; `opencode.json` is deep-merged (never overwritten wholesale).
    
  2. Added Repository Docs Convention section (lines 49-56):

    ## Repository Docs Convention
    
    - **ADRs** live in `docs/decisions/` and are numbered with 4-digit zero-padding: `0001-short-title.md`, `0002-short-title.md`, etc.
    - **Orchestrator run reports** live in `docs/reports/`.
    
    When recording a significant decision (new provider, output format change, merge strategy), create an ADR in `docs/decisions/` following the numbering sequence.
    

README.md Changes

  1. Line 54 - Updated OpenCode output description:
    OpenCode output is written to `~/.config/opencode` by default. Command are written as individual `.md` files to `~/.config/opencode/commands/<name>.md`. Agent, skills, and plugin are written to the corresponding subdirectory alongside. `opencode.json` (MCP servers) is deep-merged into any existing file -- user keys such as `model`, `theme`, and `provider` are preserved, and user values win on conflicts. Command files are backed up before being overwritten.
    

Verification

  • Read updated files and confirmed accuracy
  • Run bun test - no regression