Skip to content

fix(installer): OpenCode integration: replace name frontmatter with mode: all and update directory names#1764

Merged
bmadcode merged 4 commits intobmad-code-org:mainfrom
dracic:fix/opencode
Feb 25, 2026
Merged

fix(installer): OpenCode integration: replace name frontmatter with mode: all and update directory names#1764
bmadcode merged 4 commits intobmad-code-org:mainfrom
dracic:fix/opencode

Conversation

@dracic
Copy link
Copy Markdown
Contributor

@dracic dracic commented Feb 25, 2026

Summary

Closes #1762
Partial revert of #1644 (see notes below)


What

  1. Remove name: '{{name}}' from opencode-task.md, opencode-tool.md, opencode-workflow.md, opencode-workflow-yaml.md
  2. Fix deprecated singular directory names in platform-codes.yaml: .opencode/agent.opencode/agents, .opencode/command.opencode/commands
  3. Add legacy_targets migration in platform-codes.yaml + cleanup() in _config-driven.js so existing users with old singular dirs get them cleaned up on reinstall

On name: in frontmatter

name: is not a valid OpenCode frontmatter field. Per the OpenCode commands documentation:

The markdown file name becomes the command name. For example, test.md lets you run /test.

The supported options are description, agent, subtask, and model. There is no name field. The same applies to agents — the agent name is derived from the filename, not from a name: frontmatter key.

The presence of name: '{{name}}' (where {{name}} resolves to e.g. pm, analyst) was overriding the bmad--prefixed filename-based name with a bare, non-prefixed name — causing the name collisions reported in #1762.

This PR removes name: from all five OpenCode templates.


Migration note

Users who installed BMAD with a previous version of the OpenCode installer will have bmad-* files in .opencode/agent/ and .opencode/command/ (singular). The old cleanup() logic only cleaned directories listed in the current config, so those stale files would have persisted alongside the new plural-directory files — causing duplicates in OpenCode's command discovery.

This PR adds legacy_targets to the OpenCode platform config and updates cleanup() in _config-driven.js to clean the old singular directories on the next reinstall.


Testing

  • Confirmed all 5 opencode-*.md templates have no name: field after this change
  • Confirmed opencode-agent.md frontmatter is exactly mode: all + description:
  • Confirmed platform-codes.yaml targets are .opencode/agents and .opencode/commands
  • Manual verification against OpenCode agents docs and commands docs

…er, fix directory names

- Replace name: '{{name}}' with mode: all in opencode-agent.md
  mode: all enables both Tab-key agent switching in the TUI and @subagent
  invocation via the Task tool (mode: primary blocked subagent use)
- Remove name: '{{name}}' from opencode-task/tool/workflow/workflow-yaml templates
  OpenCode derives command name from filename, not from a name frontmatter field;
  the bare {{name}} value was overriding the bmad- prefixed filename causing
  name collisions with built-in OpenCode commands (fixes bmad-code-org#1762)
- Fix deprecated singular directory names in platform-codes.yaml:
  .opencode/agent -> .opencode/agents, .opencode/command -> .opencode/commands
- Add legacy_targets migration: cleanup() now removes stale bmad-* files from
  old singular directories on reinstall so existing users don't get duplicates
- Fix removeEmptyParents to continue walking up to parent when starting dir is
  already absent instead of breaking early

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@augmentcode
Copy link
Copy Markdown

augmentcode bot commented Feb 25, 2026

🤖 Augment PR Summary

Summary: Fixes OpenCode installer templates to avoid command/agent name collisions by removing invalid name: frontmatter and setting OpenCode agents to mode: all.
Changes: Updates OpenCode install targets to plural directory names and adds a cleanup migration for legacy singular directories on reinstall.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

The PR updates the OpenCode installer to use pluralized target directories (.opencode/agents and .opencode/commands instead of deprecated singular forms), adds legacy target cleanup support during installation, and removes the name frontmatter field from templates while adding mode: all to one agent template.

Changes

Cohort / File(s) Summary
Config-Driven Script
tools/cli/installers/lib/ide/_config-driven.js
Enhanced cleanup logic to migrate legacy target directories by iterating over installerConfig.legacy_targets and improved removeEmptyParents to gracefully skip missing intermediary paths during recursive directory deletion.
OpenCode Platform Configuration
tools/cli/installers/lib/ide/platform-codes.yaml
Updated installer targets to pluralized directories (.opencode/agents, .opencode/commands) and added legacy_targets list pointing to deprecated singular directory names for backward compatibility migration.
Template Frontmatter Updates
tools/cli/installers/lib/ide/templates/combined/opencode-*.md
Removed name: '{{name}}' frontmatter field from task, tool, and workflow templates; replaced with mode: all in agent template and removed entirely from workflow-yaml template.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • bmadcode
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: removing the invalid name frontmatter, adding mode: all to the agent template, and updating directory names to plural forms.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the rationale, specific changes, migration strategy, and testing performed.
Linked Issues check ✅ Passed All requirements from #1762 are addressed: deprecated singular directories replaced with plurals [#1762], name frontmatter removed to enable filename-derived bmad- prefixed names [#1762], and legacy directory cleanup implemented [#1762].
Out of Scope Changes check ✅ Passed All changes are directly scoped to requirements in #1762. No unrelated modifications detected in templates, configuration, or cleanup logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
tools/cli/installers/lib/ide/_config-driven.js (3)

337-362: ⚠️ Potential issue | 🔴 Critical

getDefaultTemplate fallback re-introduces the name: frontmatter bug this PR claims to fix.

The hardcoded fallback at line 339 still contains name: '{{name}}', and line 341 uses disable-model-invocation: true — neither of which matches the updated opencode-agent.md template (mode: all + description). If loadTemplate exhausts all candidates and falls through to this code path (e.g., missing template file in an unexpected install), the deployed agent file will have the invalid name: frontmatter and be missing mode: all, silently reintroducing issue #1762.

The non-agent fallback (lines 353–362) similarly still emits name: '{{name}}', which is equally irrelevant for OpenCode commands.

🐛 Proposed fix for agent fallback
   getDefaultTemplate(artifactType) {
     if (artifactType === 'agent') {
       return `---
-name: '{{name}}'
-description: '{{description}}'
-disable-model-invocation: true
+mode: all
+description: '{{description}}'
 ---

 You must fully embody this agent's persona and follow all activation instructions exactly as specified.

 <agent-activation CRITICAL="TRUE">
 1. LOAD the FULL agent file from {project-root}/{{bmadFolderName}}/{{path}}
 2. READ its entire contents - this contains the complete agent persona, menu, and instructions
 3. FOLLOW every step in the <activation> section precisely
 </agent-activation>
 `;
     }
     return `---
-name: '{{name}}'
-description: '{{description}}'
+description: '{{description}}'
 ---

-# {{name}}
-
 LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
 `;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/ide/_config-driven.js` around lines 337 - 362, The
agent fallback in getDefaultTemplate still emits invalid frontmatter (e.g.,
"name: '{{name}}'" and "disable-model-invocation: true"); update the agent
branch (when artifactType === 'agent') to match opencode-agent.md by removing
the hardcoded name, adding "mode: all" and keeping "description" (and any other
fields from opencode-agent.md), and remove "disable-model-invocation"; likewise
update the non-agent fallback to stop emitting "name: '{{name}}'" and instead
emit the correct open-code frontmatter (include "mode: all" and "description" as
appropriate) so both fallbacks match the new template format used by
loadTemplate and avoid reintroducing the original bug.

489-540: ⚠️ Potential issue | 🟠 Major

cleanupTarget only removes files starting with bmad — case-sensitive, no glob; non-bmad files in legacy dirs silently block directory removal.

Line 515 uses entry.startsWith('bmad'). This is case-sensitive, so a file named Bmad-foo.md (unlikely but possible from a Windows install) is skipped. More practically: if a user placed any non-bmad file in .opencode/agent, the directory is not empty after cleanup (remaining.length > 0), so the directory is never removed by cleanupTarget. removeEmptyParents likewise breaks at that level (line 560). The legacy directory persists, and OpenCode may still scan it, potentially producing duplicate command names — the precise problem this PR aims to solve.

This is arguably by design (preserve user content), but there is no warning to the user that the legacy directory was not fully migrated due to non-BMAD files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/ide/_config-driven.js` around lines 489 - 540, The
cleanupTarget function currently only deletes entries where
entry.startsWith('bmad') (case-sensitive) and silently leaves directories
containing non-BMAD files, preventing the directory removal and downstream
removeEmptyParents logic; change the matching to a case-insensitive check (e.g.
test entry against /^bmad/i or use entry.toLowerCase().startsWith('bmad')) so
names like "Bmad-foo.md" are removed, and after the removal pass if the
directory is not empty and removedCount > 0 (use the existing remaining
variable) emit a non-silent warning/log (unless options.silent) that the legacy
directory could not be fully cleaned due to non-BMAD files so the caller/user is
informed and removeEmptyParents behavior is predictable.

548-567: ⚠️ Potential issue | 🟡 Minor

JSDoc terminology is misleading — "recursively" implies downward traversal, not upward ancestor walking.

The method removes empty directories while walking up the path chain toward projectDir, not recursively into subdirectories. The doc should say "walks up" or "traverses ancestor directories."

Additionally, line 561 uses fs.rmdir(fullPath) while the rest of the file uses fs.remove() for removals (lines 518, 535). For consistency and clarity, use fs.remove() here as well — it's safe for empty directories and aligns with the fs-extra patterns used throughout the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/ide/_config-driven.js` around lines 548 - 567,
Update the removeEmptyParents function documentation and implementation: change
any JSDoc text that says "recursively" to clarify it "walks up" or "traverses
ancestor directories toward projectDir" to reflect upward traversal, and replace
the fs.rmdir(fullPath) call inside removeEmptyParents with fs.remove(fullPath)
to match the rest of the file's fs-extra usage (refer to function
removeEmptyParents and the fs.rmdir call).
🧹 Nitpick comments (2)
tools/cli/installers/lib/ide/platform-codes.yaml (1)

134-143: legacy_targets is undocumented in the schema comment block.

The schema comment at lines 188–203 documents every installer field (target_dir, template_type, header_template, body_template, targets, artifact_types, skip_existing) but omits legacy_targets. Any developer adding a new platform with a migration need will have no schema guidance.

📄 Proposed schema addition
 #   targets: array (optional)             # For multi-target installations
 #     - target_dir: string
 #       template_type: string
 #       artifact_types: [agents, workflows, tasks, tools]
+#   legacy_targets: array (optional)      # Old target dirs to clean up on reinstall (migration)
 #   artifact_types: array (optional)      # Filter which artifacts to install (default: all)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/ide/platform-codes.yaml` around lines 134 - 143, Add
documentation for the legacy_targets field to the schema comment block alongside
the existing fields (target_dir, template_type, header_template, body_template,
targets, artifact_types, skip_existing); describe that legacy_targets is an
optional list of string paths representing deprecated installer target locations
used for migration/compatibility, specify expected value type (array of
strings), intended usage (lookup/remapping during migration), and include a
short example (e.g., [" .opencode/agent", ".opencode/command"]) and note it is
ignored for new installs. Update the comment block where installer fields are
documented so developers see legacy_targets next to targets and artifact_types.
tools/cli/installers/lib/ide/_config-driven.js (1)

456-462: No user-visible signal for the legacy migration step.

cleanupTarget at line 527 logs individual file removals (Cleaned N BMAD files from ${targetDir}), but there is no higher-level message indicating a migration is in progress. Users reinstalling OpenCode will see files deleted from .opencode/agent and .opencode/command with no contextual explanation that this is an intentional one-time migration, which may cause alarm.

🔍 Suggested addition
     if (this.installerConfig?.legacy_targets) {
+      if (!options.silent) await prompts.log.message('  Migrating legacy OpenCode directories...');
       for (const legacyDir of this.installerConfig.legacy_targets) {
         await this.cleanupTarget(projectDir, legacyDir, options);
         await this.removeEmptyParents(projectDir, legacyDir);
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/ide/_config-driven.js` around lines 456 - 462, The
legacy migration loop using installerConfig.legacy_targets calls cleanupTarget
and removeEmptyParents but emits no high-level user-visible message; add a clear
informational log before processing (e.g., via this.logger.info or
processLogger) that states a one-time legacy-directory migration is starting and
lists the legacy targets (reference installerConfig.legacy_targets, projectDir,
cleanupTarget, removeEmptyParents) so users know deletions from directories like
.opencode/agent and .opencode/command are intentional; keep the message concise
and only emitted when installerConfig.legacy_targets is non-empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/cli/installers/lib/ide/_config-driven.js`:
- Around line 457-461: The legacy_targets cleanup runs unconditionally and can
execute when the current installer config is invalid; change the cleanup flow so
legacy migration only runs if a valid current installer config exists (e.g.,
require this.installerConfig.targets or this.installerConfig.target_dir or an
isValidInstallerConfig() check) before iterating
this.installerConfig.legacy_targets; move the for-loop that calls cleanupTarget
and removeEmptyParents behind that guard (or add an early return when the
installer config is invalid) so legacy cleanup cannot run when installation
would later fail.
- Around line 555-558: The loop advances current with current =
path.dirname(current) when fullPath is missing but doesn't update last, which is
confusing and can cause an extra iteration; update last = current immediately
when you advance current (and add a brief comment explaining the current/last
invariant), then proceed. To reduce the TOCTOU window between
fs.readdir(fullPath) and fs.rmdir(fullPath), don't bail out of the entire upward
walk on any rmdir error: after readdir returns empty, attempt fs.rmdir(fullPath)
and on error inspect the code—if it's ENOENT or ENOTEMPTY (or another transient
concurrency-related error), treat it as “skip this level and continue upward”
rather than breaking the loop; only break/throw for unexpected/fatal errors. Use
the existing symbols fullPath, current, last, fs.pathExists, fs.readdir,
fs.rmdir to locate and modify the logic.

---

Outside diff comments:
In `@tools/cli/installers/lib/ide/_config-driven.js`:
- Around line 337-362: The agent fallback in getDefaultTemplate still emits
invalid frontmatter (e.g., "name: '{{name}}'" and "disable-model-invocation:
true"); update the agent branch (when artifactType === 'agent') to match
opencode-agent.md by removing the hardcoded name, adding "mode: all" and keeping
"description" (and any other fields from opencode-agent.md), and remove
"disable-model-invocation"; likewise update the non-agent fallback to stop
emitting "name: '{{name}}'" and instead emit the correct open-code frontmatter
(include "mode: all" and "description" as appropriate) so both fallbacks match
the new template format used by loadTemplate and avoid reintroducing the
original bug.
- Around line 489-540: The cleanupTarget function currently only deletes entries
where entry.startsWith('bmad') (case-sensitive) and silently leaves directories
containing non-BMAD files, preventing the directory removal and downstream
removeEmptyParents logic; change the matching to a case-insensitive check (e.g.
test entry against /^bmad/i or use entry.toLowerCase().startsWith('bmad')) so
names like "Bmad-foo.md" are removed, and after the removal pass if the
directory is not empty and removedCount > 0 (use the existing remaining
variable) emit a non-silent warning/log (unless options.silent) that the legacy
directory could not be fully cleaned due to non-BMAD files so the caller/user is
informed and removeEmptyParents behavior is predictable.
- Around line 548-567: Update the removeEmptyParents function documentation and
implementation: change any JSDoc text that says "recursively" to clarify it
"walks up" or "traverses ancestor directories toward projectDir" to reflect
upward traversal, and replace the fs.rmdir(fullPath) call inside
removeEmptyParents with fs.remove(fullPath) to match the rest of the file's
fs-extra usage (refer to function removeEmptyParents and the fs.rmdir call).

---

Nitpick comments:
In `@tools/cli/installers/lib/ide/_config-driven.js`:
- Around line 456-462: The legacy migration loop using
installerConfig.legacy_targets calls cleanupTarget and removeEmptyParents but
emits no high-level user-visible message; add a clear informational log before
processing (e.g., via this.logger.info or processLogger) that states a one-time
legacy-directory migration is starting and lists the legacy targets (reference
installerConfig.legacy_targets, projectDir, cleanupTarget, removeEmptyParents)
so users know deletions from directories like .opencode/agent and
.opencode/command are intentional; keep the message concise and only emitted
when installerConfig.legacy_targets is non-empty.

In `@tools/cli/installers/lib/ide/platform-codes.yaml`:
- Around line 134-143: Add documentation for the legacy_targets field to the
schema comment block alongside the existing fields (target_dir, template_type,
header_template, body_template, targets, artifact_types, skip_existing);
describe that legacy_targets is an optional list of string paths representing
deprecated installer target locations used for migration/compatibility, specify
expected value type (array of strings), intended usage (lookup/remapping during
migration), and include a short example (e.g., [" .opencode/agent",
".opencode/command"]) and note it is ignored for new installs. Update the
comment block where installer fields are documented so developers see
legacy_targets next to targets and artifact_types.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97a533e and 0b219d0.

📒 Files selected for processing (7)
  • tools/cli/installers/lib/ide/_config-driven.js
  • tools/cli/installers/lib/ide/platform-codes.yaml
  • tools/cli/installers/lib/ide/templates/combined/opencode-agent.md
  • tools/cli/installers/lib/ide/templates/combined/opencode-task.md
  • tools/cli/installers/lib/ide/templates/combined/opencode-tool.md
  • tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md
  • tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md
💤 Files with no reviewable changes (4)
  • tools/cli/installers/lib/ide/templates/combined/opencode-workflow-yaml.md
  • tools/cli/installers/lib/ide/templates/combined/opencode-task.md
  • tools/cli/installers/lib/ide/templates/combined/opencode-workflow.md
  • tools/cli/installers/lib/ide/templates/combined/opencode-tool.md

dracic and others added 3 commits February 25, 2026 10:47
- Add project boundary guard to removeEmptyParents() using path.resolve
  and startsWith check to prevent traversal outside projectDir (Augment)
- Fix JSDoc: "Recursively remove" -> "Walk up ancestor directories"
- Add user-visible migration log message when processing legacy_targets
- Document legacy_targets field in Installer Config Schema comment block
  in platform-codes.yaml (CodeRabbit + Augment)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rity

- Distinguish recoverable errors (ENOTEMPTY, ENOENT) from fatal errors in
  removeEmptyParents() catch block — skip level and continue upward on
  TOCTOU races or concurrent removal, break only on fatal errors (EACCES)
- Add comment clarifying loop invariant for missing-path continue branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

[BUG] Opencode Directories & Naming Conventions

2 participants