# 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:** ```typescript export type OpenCodeCommandFile = { name: string // command name, used as filename stem: .md content: string // full file content: YAML frontmatter + body } ``` **Bundle Structure:** ```typescript 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**: `.md` - **content**: YAML frontmatter (`description`, optionally `model`) + body (template text with Claude path rewriting) ### Frontmatter Structure ```yaml --- 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 `/.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 ```typescript 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 ```typescript 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: ```typescript } 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`: ```typescript 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/.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/.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