Skip to content

fix(installer): generate slash-command and Agent pointer files (OpenCode + GitHub Copilot)#2324

Merged
bmadcode merged 10 commits intobmad-code-org:mainfrom
jheyworth:feat/opencode-command-pointers
Apr 30, 2026
Merged

fix(installer): generate slash-command and Agent pointer files (OpenCode + GitHub Copilot)#2324
bmadcode merged 10 commits intobmad-code-org:mainfrom
jheyworth:feat/opencode-command-pointers

Conversation

@jheyworth
Copy link
Copy Markdown
Contributor

@jheyworth jheyworth commented Apr 26, 2026

The Problem

Two pain points from real BMAD users that this PR addresses together because they're the same architectural shape:

OpenCode users:
Can't invoke installed skills directly. Today, every skill use requires:

  1. Type /skills to open the OpenCode skills menu.
  2. Navigate the menu to find the desired skill.
  3. Select it.

GitHub Copilot users:
Report installed BMAD 'Agents' ( and NOT Workflows or misc tools ) Skills no longer appear in the Github Copilot Custom 'Agents' picker since the consolidation to skills

Both are flagged in the follow-up to #2267.

The Proposed Solution

One generic per-platform pointer-file mechanism in the installer, with all platform-specific shape pushed into platform-codes.yaml as data — no per-platform branching in the JS.

  • What gets created (OpenCode): .opencode/commands/<canonicalId>.md with @skills/<canonicalId> body, populating OpenCode's slash-command palette.
  • What gets created (GitHub Copilot): .github/agents/<canonicalId>.agent.md for persona agents only (canonical id contains -agent-, plus bmad-tea), with a LOAD {project-root}/.agents/skills/<id>/SKILL.md body. Workflows and tools are filtered out so the Custom Agents picker stays uncluttered.
  • Same code path for both: installCommandPointers / cleanupCommandPointers read three new yaml fields and produce the right file shape for each platform.

Scope

This PR only modifies the BMAD-METHOD installer. Two platforms opt in via three new fields in platform-codes.yaml:

  • commands_target_dir — where pointer files live.
  • commands_extension — file extension (default .md; Copilot uses .agent.md per the VS Code Custom Agents docs).
  • commands_body_template — pointer body, supports {canonicalId} and {target_dir} placeholders.
  • commands_filter — optional, set to agents-only to restrict emission to persona agents (BMAD's -agent- naming convention plus the bmad-tea allowlist). Used by github-copilot so the Custom Agents picker isn't flooded with 90+ workflow entries.

Other adapters in the installer are unaffected — the new code path is gated behind commands_target_dir being set. Future platforms with the same gap can opt in by adding the same three fields to their entry; no JS changes required.

External / expansion modules (WDS, GDS, CIS, TEA)

External-module skills automatically receive pointers too for both OpenCode and Copilot — they flow through the same unified _bmad/_config/skill-manifest.csv that this PR's installCommandPointers reads. No parallel code changes are required in the external-module repos:

  • bmad-code-org/bmad-method-wds-expansion (WDS)
  • bmad-code-org/bmad-module-game-dev-studio (GDS)
  • bmad-code-org/bmad-module-creative-intelligence-suite (CIS)
  • bmad-code-org/bmad-method-test-architecture-enterprise (TEA)

These repos are content-only (no installer code; the per-module installer pattern was deliberately removed in favor of declarative config — see official-modules.js:553). After this PR lands, a user who runs bmad install and selects any of these modules gets /<canonicalId> slash commands (OpenCode, all skills) and Custom Agents picker entries (Copilot, persona agents only) generated automatically.

Files

  • tools/installer/ide/platform-codes.yaml — adds commands_target_dir for opencode and adds commands_target_dir + commands_extension + commands_body_template for github-copilot (4 lines total)
  • tools/installer/ide/_config-driven.js — generic installCommandPointers() / cleanupCommandPointers() driven by per-platform yaml data, plus reserved-name collision guard, basename validation, YAML-safe description quoting, hand-edit preservation, and per-file try/catch
  • test/test-installation-components.js — extends Suite 8 (OpenCode) and Suite 17 (GitHub Copilot) with assertions for pointer creation, body content, idempotency, and per-platform shape

Behavior

  • Write: for each row in _bmad/_config/skill-manifest.csv, writes <commands_target_dir>/<canonicalId><extension> with the description in YAML frontmatter and the platform's body template (placeholder-expanded).
  • Skip-if-exists by default — hand-edited pointers are preserved. Generator-shaped pointer files get refreshed when the manifest description changes; user-modified files (body diverges from the platform's template) are left alone.
  • Collision guard — skips canonicalIds matching reserved OpenCode commands (review, commit, init, help, skills, fast, etc.). Gated on this.name === 'opencode' so other platforms don't inherit OpenCode-specific reservations.
  • Cleanup — uninstall and skill-removal flows delete matching pointer files and remove the empty commands directory if all pointers are removed. Install-flow cleanup spares pointers for skills still in the manifest so hand edits survive a routine reinstall.
  • Multi-IDE installs — works correctly when OpenCode and Copilot are both selected, or when either is a peer in a .agents/skills shared-target batch.

Tests

Local npm test green: lint, format, markdown-lint, 318 installer-component tests (extended Suite 8 + Suite 17 with new assertions for both platforms; was 290 pre-PR), 83 channel-resolution tests.

Why this scope (not broader)

The Related — other IDEs / LLMs section that earlier versions of this PR included has been removed. Of the 8 platforms originally listed, only OpenCode and GitHub Copilot are hands-on verified and have explicit user demand. The rest were desktop research. If/when other platforms need the same fix, they can opt in with a small yaml-only change — no installer code edits required.

Coordination

Happy to close this if a parallel skills/installer rework is in flight that would conflict — let me know.

Refs: #2267

@jheyworth jheyworth marked this pull request as ready for review April 26, 2026 19:48
@github-actions
Copy link
Copy Markdown
Contributor

@coderabbitai review

@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented Apr 26, 2026

🤖 Augment PR Summary

Summary: Adds OpenCode direct / invocation support by generating per-skill command-pointer files during BMAD skill installation.

Changes:

  • Adds commands_target_dir: .opencode/commands to the OpenCode platform config.
  • Extends the config-driven IDE installer to write .opencode/commands/<canonicalId>.md files with YAML frontmatter (description) and a body pointing to @skills/<canonicalId>.
  • Skips generation when canonicalId collides with reserved OpenCode built-in commands, and preserves existing pointer files unless overridden.
  • Adds symmetric cleanup to delete pointer files for removed skills and remove the commands directory when it becomes empty.
  • Extends the OpenCode installer test suite to assert pointer creation/content and idempotent re-install behavior.

Technical Notes: Descriptions are normalized to single-line YAML-safe values for frontmatter emission.

🤖 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. 4 suggestions posted.

Fix All in Augment

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

Comment thread tools/installer/ide/_config-driven.js
Comment thread tools/installer/ide/_config-driven.js Outdated
Comment thread tools/installer/ide/_config-driven.js Outdated
Comment thread tools/installer/ide/_config-driven.js Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

This PR introduces IDE slash-command support by adding command-pointer file generation during installation. The installer now parses the skill manifest, generates per-skill markdown files with YAML frontmatter routed to skill references, manages command artifacts through a dedicated directory, and handles cleanup during uninstallation with test coverage for idempotency.

Changes

Cohort / File(s) Summary
Test Coverage
test/test-installation-components.js
New test validating slash-command wiring: asserts commands_target_dir configuration, verifies command pointer file generation with YAML frontmatter, and confirms idempotency across multiple install runs.
Core Implementation
tools/installer/ide/_config-driven.js
Adds command-pointer generation logic: parses skill manifest CSV, generates per-skill markdown files with YAML frontmatter and skill references, skips reserved canonical IDs, avoids overwriting existing files unless forced, and mirrors cleanup during uninstallation.
Platform Configuration
tools/installer/ide/platform-codes.yaml
Adds commands_target_dir: .opencode/commands configuration field to the OpenCode platform installer specification.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • bmadcode
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title accurately describes the main feature: generating slash-command and Agent pointer files for OpenCode and GitHub Copilot installers. It directly reflects the substantial changes across platform config, installer logic, and tests.
Description check ✅ Passed The PR description comprehensively explains the problem (OpenCode users can't invoke skills directly), the proposed solution (generic per-platform pointer-file mechanism), scope (BMAD-METHOD installer only), implementation details, test coverage, and coordination notes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

🧹 Nitpick comments (4)
test/test-installation-components.js (1)

308-324: Consider extending coverage to cleanup symmetry and hand-edit preservation.

The new assertions verify creation and basic idempotency, but two behaviors documented in the PR objectives aren't exercised:

  • Hand-edit preservation: The installer skips existing pointer files unless forceCommands is set. The current "second install pass" test only re-runs setup with the same generated content, so it can't distinguish "skip-existing" from "rewrite-with-identical-bytes". Pre-write a sentinel byte sequence into commandFile after the first install and assert it survives the second run.
  • Cleanup symmetry: cleanupCommandPointers removes pointer files for canonicalIds in removalSet and deletes the directory when empty. There's no test asserting that a follow-up uninstall (or a removals.txt-driven removal) actually deletes .opencode/commands/bmad-master.md and the directory.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test-installation-components.js` around lines 308 - 324, Add tests to
verify hand-edit preservation and cleanup symmetry by (1) after the first
ideManager.setup('opencode', ...) write a sentinel byte sequence into the
commandFile ('.opencode/commands/bmad-master.md'), run ideManager.setup again
with the same args (without forceCommands) and assert the file contents still
contain the sentinel to prove skip-existing behavior, and (2) exercise
cleanupCommandPointers (or call ideManager.setup/remove flow that triggers
removalSet) for the bmad canonicalId and assert that
'.opencode/commands/bmad-master.md' is removed and the '.opencode/commands'
directory is deleted when empty; reference ideManager.setup, forceCommands, and
cleanupCommandPointers when locating code to change.
tools/installer/ide/_config-driven.js (3)

458-501: Cleanup logic is sound; one minor defensive check is unnecessary.

The symmetric pointer-cleanup path (skip when empty removalSet, only remove .md entries whose stem is in the set, drop the directory if empty) is correct. The early bail at Line 469 correctly preserves user-authored commands during partial uninstalls because removalSet is built from skill-manifest.csv + removals.txt, and the directory teardown at Line 494–497 only fires when nothing else lives there.

Minor: typeof entry !== 'string' at Line 482 is dead defensive code — fs.readdir (without withFileTypes) always yields strings. Safe to drop.

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

In `@tools/installer/ide/_config-driven.js` around lines 458 - 501, In
cleanupCommandPointers, remove the unnecessary defensive check for typeof entry
!== 'string' since fs.readdir (called without withFileTypes) always returns
strings; locate the loop that iterates over entries (for (const entry of
entries)) and delete the condition that guards on typeof entry, keeping only the
check that entry.endsWith('.md') and the subsequent canonicalId/removalSet logic
so behavior and error handling remain unchanged.

9-31: Reserved-command list is OpenCode-specific but applied to any platform that defines commands_target_dir.

installCommandPointers consults RESERVED_OPENCODE_COMMANDS unconditionally for every platform that has commands_target_dir configured. Today that's only OpenCode, so this is harmless. The moment a second IDE opts into command pointers, it will silently inherit OpenCode's reserved-name set — which may neither include its own reserved names nor be appropriate for it.

Two cheap mitigations:

  • Move the reserved set into platform-codes.yaml (e.g. installer.reserved_commands: [...]) and let the installer read it per platform; or
  • Gate the lookup with if (this.name === 'opencode') and rename the constant accordingly so the coupling is explicit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/installer/ide/_config-driven.js` around lines 9 - 31, The
reserved-command list RESERVED_OPENCODE_COMMANDS is being applied globally when
installCommandPointers checks commands_target_dir, so move the platform-specific
reservation so other platforms don't inherit OpenCode names: either (A) load a
per-platform list from configuration (e.g., installer.reserved_commands in
platform-codes.yaml) and use that list inside installCommandPointers for the
current platform, or (B) restrict the existing lookup to OpenCode only by gating
it with a check like this.name === 'opencode' and rename the constant (e.g.,
RESERVED_OPENCODE_COMMANDS) to make the coupling explicit; update
installCommandPointers to retrieve the appropriate reserved set from config or
from the platform check before skipping skill name collisions.

168-170: results.commands is populated but never surfaced.

printSummary (Line 306) only logs skillDirectories || skills, so the new results.commands object — which carries useful counters (created, skippedExisting, skippedCollision, fallbackDescription) — is collected and discarded. For OpenCode installs, users won't know how many slash-command pointers were generated or whether some were skipped due to collisions/existing files.

Consider adding a one-liner to printSummary when results.commands?.created > 0, e.g. ${created} commands → ${commands_target_dir} (and a hint when skippedCollision > 0).

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

In `@tools/installer/ide/_config-driven.js` around lines 168 - 170, printSummary
currently only logs skillDirectories/skills and discards results.commands
returned from installCommandPointers, so users never see created/skipped counts
for slash-command pointers; update printSummary to check results.commands (from
installCommandPointers) and, when results.commands?.created > 0, append a short
line like "<created> commands → <commands_target_dir>" and when
results.commands?.skippedCollision > 0 include a hint about collisions;
reference the results.commands object fields (created, skippedExisting,
skippedCollision, fallbackDescription), and use config.commands_target_dir to
display the target directory.
🤖 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/installer/ide/_config-driven.js`:
- Around line 195-236: installCommandPointers currently lets a single
fs.writeFile error abort the whole install; wrap the per-record write (the await
fs.writeFile(commandFile, body, 'utf8') inside installCommandPointers) in a
try/catch, add a counter on the result object (e.g. result.pointerWriteFailures
or result.writeFailures) and increment it on failure, and in the catch log a
warning with the canonicalId, commandFile and error (console.warn or the module
logger) instead of rethrowing; also ensure callers (installToTarget) can surface
this summary if needed and apply the same per-file try/catch pattern to
installVerbatimSkills for symmetry.
- Around line 216-232: Compute the pointer file content (body) up front using
description and yamlSafeSingleLine, then if commandFile exists and
!options.forceCommands read its content; if the existing content exactly equals
body, treat as unchanged (increment result.skippedExisting and continue), but if
it matches the generator pattern (e.g., has the frontmatter and the trailing
line `@skills/${canonicalId}\n`) then overwrite with the new body to propagate
description changes (increment result.created or result.fallbackDescription as
appropriate); otherwise (user-modified file differs and does not match the
generator pattern) leave it alone and increment result.skippedExisting. Use the
variables canonicalId, commandFile, options.forceCommands, description, body,
yamlSafeSingleLine, result.skippedExisting, result.created, and
result.fallbackDescription to locate and implement this logic.

---

Nitpick comments:
In `@test/test-installation-components.js`:
- Around line 308-324: Add tests to verify hand-edit preservation and cleanup
symmetry by (1) after the first ideManager.setup('opencode', ...) write a
sentinel byte sequence into the commandFile
('.opencode/commands/bmad-master.md'), run ideManager.setup again with the same
args (without forceCommands) and assert the file contents still contain the
sentinel to prove skip-existing behavior, and (2) exercise
cleanupCommandPointers (or call ideManager.setup/remove flow that triggers
removalSet) for the bmad canonicalId and assert that
'.opencode/commands/bmad-master.md' is removed and the '.opencode/commands'
directory is deleted when empty; reference ideManager.setup, forceCommands, and
cleanupCommandPointers when locating code to change.

In `@tools/installer/ide/_config-driven.js`:
- Around line 458-501: In cleanupCommandPointers, remove the unnecessary
defensive check for typeof entry !== 'string' since fs.readdir (called without
withFileTypes) always returns strings; locate the loop that iterates over
entries (for (const entry of entries)) and delete the condition that guards on
typeof entry, keeping only the check that entry.endsWith('.md') and the
subsequent canonicalId/removalSet logic so behavior and error handling remain
unchanged.
- Around line 9-31: The reserved-command list RESERVED_OPENCODE_COMMANDS is
being applied globally when installCommandPointers checks commands_target_dir,
so move the platform-specific reservation so other platforms don't inherit
OpenCode names: either (A) load a per-platform list from configuration (e.g.,
installer.reserved_commands in platform-codes.yaml) and use that list inside
installCommandPointers for the current platform, or (B) restrict the existing
lookup to OpenCode only by gating it with a check like this.name === 'opencode'
and rename the constant (e.g., RESERVED_OPENCODE_COMMANDS) to make the coupling
explicit; update installCommandPointers to retrieve the appropriate reserved set
from config or from the platform check before skipping skill name collisions.
- Around line 168-170: printSummary currently only logs skillDirectories/skills
and discards results.commands returned from installCommandPointers, so users
never see created/skipped counts for slash-command pointers; update printSummary
to check results.commands (from installCommandPointers) and, when
results.commands?.created > 0, append a short line like "<created> commands →
<commands_target_dir>" and when results.commands?.skippedCollision > 0 include a
hint about collisions; reference the results.commands object fields (created,
skippedExisting, skippedCollision, fallbackDescription), and use
config.commands_target_dir to display the target directory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4d1dd33b-7b8f-4e48-b127-d142300900f2

📥 Commits

Reviewing files that changed from the base of the PR and between be85e5b and 6065a92.

📒 Files selected for processing (3)
  • test/test-installation-components.js
  • tools/installer/ide/_config-driven.js
  • tools/installer/ide/platform-codes.yaml

Comment thread tools/installer/ide/_config-driven.js
Comment thread tools/installer/ide/_config-driven.js Outdated
@jheyworth jheyworth changed the title feat(installer): generate OpenCode /<skill> slash commands fix(installer): generate OpenCode /<skill> slash commands Apr 26, 2026
@jheyworth jheyworth force-pushed the feat/opencode-command-pointers branch from 6065a92 to 9d9d576 Compare April 26, 2026 20:20
jheyworth added a commit to jheyworth/BMAD-METHOD that referenced this pull request Apr 26, 2026
Six fixes from CodeRabbit + Augment review on the OpenCode command
pointer generation:

- skipTarget no longer suppresses installCommandPointers in multi-IDE
  shared-target_dir batches. Pointers live in a per-IDE directory and
  are not deduped across peers, so OpenCode must still generate them
  even when a peer (e.g. openhands) won the .agents/skills write race.
- skipTarget no longer suppresses cleanupCommandPointers either, so
  partial uninstalls leave no stale pointers when a peer remains.
- canonicalId is validated as a safe basename before being interpolated
  into a file path (defense in depth against a malformed manifest entry
  writing outside commands_target_dir).
- yamlSafeSingleLine now quotes descriptions starting with `[` or `{`
  so YAML doesn't parse them as a sequence/map.
- Per-record fs.writeFile failures are caught and counted (writeFailures)
  rather than aborting the whole IDE install — pointer files are a
  non-essential adjunct to the skill copy.
- Generator-shaped pointer files are refreshed when the manifest
  description changes; hand-modified files (body diverges from the
  generator pattern) are still preserved unless forceCommands is set.

Tests: extends Suite 8 with description-update propagation; adds new
Suite 40c covering OpenCode + openhands batches in both orderings plus
partial-IDE uninstall pointer cleanup. 308 tests pass (was 296).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jheyworth added a commit to jheyworth/BMAD-METHOD that referenced this pull request Apr 26, 2026
Four nitpicks from CodeRabbit's original review that were missed in the
first triage pass:

- Hand-edited pointers now survive the production install flow.
  cleanupCommandPointers spares pointers for canonicalIds that are still
  in the new manifest when called from the install/update flow (signal:
  options.previousSkillIds is set). Uninstall and partial-IDE removal
  flows still wipe pointers as before. The previous behavior wiped every
  pointer in removalSet before installCommandPointers could run, so its
  skip-if-exists guard never fired and hand edits were lost on every
  reinstall — contradicting the docstring's preservation claim.
- RESERVED_OPENCODE_COMMANDS is now gated on this.name === 'opencode'
  so future adapters opting into commands_target_dir don't silently
  inherit OpenCode's reserved-name set.
- printSummary now surfaces results.commands so users see how many
  pointers were created/refreshed/skipped per install, plus a warning
  for any per-file write failures.
- Dropped a dead `typeof entry !== 'string'` check; fs.readdir without
  withFileTypes always yields strings.

Tests: extends Suite 8 with a hand-edit-preservation regression that
calls setup with previousSkillIds (the production shape) and asserts a
sentinel byte sequence in the pointer body survives. 310 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jheyworth added a commit to jheyworth/BMAD-METHOD that referenced this pull request Apr 26, 2026
Six fixes from CodeRabbit + Augment review on the OpenCode command
pointer generation:

- skipTarget no longer suppresses installCommandPointers in multi-IDE
  shared-target_dir batches. Pointers live in a per-IDE directory and
  are not deduped across peers, so OpenCode must still generate them
  even when a peer (e.g. openhands) won the .agents/skills write race.
- skipTarget no longer suppresses cleanupCommandPointers either, so
  partial uninstalls leave no stale pointers when a peer remains.
- canonicalId is validated as a safe basename before being interpolated
  into a file path (defense in depth against a malformed manifest entry
  writing outside commands_target_dir).
- yamlSafeSingleLine now quotes descriptions starting with `[` or `{`
  so YAML doesn't parse them as a sequence/map.
- Per-record fs.writeFile failures are caught and counted (writeFailures)
  rather than aborting the whole IDE install — pointer files are a
  non-essential adjunct to the skill copy.
- Generator-shaped pointer files are refreshed when the manifest
  description changes; hand-modified files (body diverges from the
  generator pattern) are still preserved unless forceCommands is set.

Tests: extends Suite 8 with description-update propagation; adds new
Suite 40c covering OpenCode + openhands batches in both orderings plus
partial-IDE uninstall pointer cleanup. 308 tests pass (was 296).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jheyworth added a commit to jheyworth/BMAD-METHOD that referenced this pull request Apr 26, 2026
Four nitpicks from CodeRabbit's original review that were missed in the
first triage pass:

- Hand-edited pointers now survive the production install flow.
  cleanupCommandPointers spares pointers for canonicalIds that are still
  in the new manifest when called from the install/update flow (signal:
  options.previousSkillIds is set). Uninstall and partial-IDE removal
  flows still wipe pointers as before. The previous behavior wiped every
  pointer in removalSet before installCommandPointers could run, so its
  skip-if-exists guard never fired and hand edits were lost on every
  reinstall — contradicting the docstring's preservation claim.
- RESERVED_OPENCODE_COMMANDS is now gated on this.name === 'opencode'
  so future adapters opting into commands_target_dir don't silently
  inherit OpenCode's reserved-name set.
- printSummary now surfaces results.commands so users see how many
  pointers were created/refreshed/skipped per install, plus a warning
  for any per-file write failures.
- Dropped a dead `typeof entry !== 'string'` check; fs.readdir without
  withFileTypes always yields strings.

Tests: extends Suite 8 with a hand-edit-preservation regression that
calls setup with previousSkillIds (the production shape) and asserts a
sentinel byte sequence in the pointer body survives. 310 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jheyworth jheyworth force-pushed the feat/opencode-command-pointers branch from 8210a82 to ccc0d4f Compare April 26, 2026 21:05
@jheyworth jheyworth closed this Apr 26, 2026
@jheyworth jheyworth deleted the feat/opencode-command-pointers branch April 26, 2026 21:58
@jheyworth jheyworth restored the feat/opencode-command-pointers branch April 26, 2026 21:59
@jheyworth jheyworth reopened this Apr 26, 2026
@jheyworth
Copy link
Copy Markdown
Contributor Author

jheyworth commented Apr 26, 2026

Following up on the 4 CodeRabbit nitpicks from the original review.

All addressed in d3bf855 and ccc0d4f:

  • Hand-edit preservation test: Suite 8 now writes a sentinel into the pointer file and asserts it survives a reinstall called with previousSkillIds (the production install shape).
  • Cleanup symmetry test: Suite 40c asserts the .opencode/commands/.md pointer is removed on partial-IDE uninstall even when a peer keeps the shared .agents/skills directory.
  • Dead typeof entry !== 'string' check in cleanupCommandPointers: dropped.
  • RESERVED_OPENCODE_COMMANDS coupling: now gated on this.name === 'opencode' so future adapters opting into commands_target_dir don't inherit OpenCode's reserved set.
  • printSummary now surfaces results.commands (created/refreshed/skipped collisions, with a warning line for writeFailures).

Test count: 296 → 310.

jheyworth added a commit to jheyworth/BMAD-METHOD that referenced this pull request Apr 26, 2026
Six fixes from CodeRabbit + Augment review on the OpenCode command
pointer generation:

- skipTarget no longer suppresses installCommandPointers in multi-IDE
  shared-target_dir batches. Pointers live in a per-IDE directory and
  are not deduped across peers, so OpenCode must still generate them
  even when a peer (e.g. openhands) won the .agents/skills write race.
- skipTarget no longer suppresses cleanupCommandPointers either, so
  partial uninstalls leave no stale pointers when a peer remains.
- canonicalId is validated as a safe basename before being interpolated
  into a file path (defense in depth against a malformed manifest entry
  writing outside commands_target_dir).
- yamlSafeSingleLine now quotes descriptions starting with `[` or `{`
  so YAML doesn't parse them as a sequence/map.
- Per-record fs.writeFile failures are caught and counted (writeFailures)
  rather than aborting the whole IDE install — pointer files are a
  non-essential adjunct to the skill copy.
- Generator-shaped pointer files are refreshed when the manifest
  description changes; hand-modified files (body diverges from the
  generator pattern) are still preserved unless forceCommands is set.

Tests: extends Suite 8 with description-update propagation; adds new
Suite 40c covering OpenCode + openhands batches in both orderings plus
partial-IDE uninstall pointer cleanup. 308 tests pass (was 296).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jheyworth added a commit to jheyworth/BMAD-METHOD that referenced this pull request Apr 26, 2026
Four nitpicks from CodeRabbit's original review that were missed in the
first triage pass:

- Hand-edited pointers now survive the production install flow.
  cleanupCommandPointers spares pointers for canonicalIds that are still
  in the new manifest when called from the install/update flow (signal:
  options.previousSkillIds is set). Uninstall and partial-IDE removal
  flows still wipe pointers as before. The previous behavior wiped every
  pointer in removalSet before installCommandPointers could run, so its
  skip-if-exists guard never fired and hand edits were lost on every
  reinstall — contradicting the docstring's preservation claim.
- RESERVED_OPENCODE_COMMANDS is now gated on this.name === 'opencode'
  so future adapters opting into commands_target_dir don't silently
  inherit OpenCode's reserved-name set.
- printSummary now surfaces results.commands so users see how many
  pointers were created/refreshed/skipped per install, plus a warning
  for any per-file write failures.
- Dropped a dead `typeof entry !== 'string'` check; fs.readdir without
  withFileTypes always yields strings.

Tests: extends Suite 8 with a hand-edit-preservation regression that
calls setup with previousSkillIds (the production shape) and asserts a
sentinel byte sequence in the pointer body survives. 310 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jheyworth jheyworth force-pushed the feat/opencode-command-pointers branch from ccc0d4f to 6bc635f Compare April 26, 2026 22:43
jheyworth and others added 3 commits April 27, 2026 07:10
Adds .opencode/commands/<canonicalId>.md pointer files for each installed
skill so users can invoke skills directly (e.g. /bmad-quick-dev) instead
of going through the /skills menu.

- platform-codes.yaml: add commands_target_dir field for opencode
- _config-driven.js: installCommandPointers() with skip-if-exists default,
  reserved-name collision guard, YAML-safe description quoting
- _config-driven.js: cleanupCommandPointers() for symmetric uninstall
- test-installation-components.js: extend OpenCode suite with assertions
  covering pointer creation, content, and idempotency

OpenCode-only and opt-in via the new yaml field; other adapters unchanged.

Refs bmad-code-org#2267

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six fixes from CodeRabbit + Augment review on the OpenCode command
pointer generation:

- skipTarget no longer suppresses installCommandPointers in multi-IDE
  shared-target_dir batches. Pointers live in a per-IDE directory and
  are not deduped across peers, so OpenCode must still generate them
  even when a peer (e.g. openhands) won the .agents/skills write race.
- skipTarget no longer suppresses cleanupCommandPointers either, so
  partial uninstalls leave no stale pointers when a peer remains.
- canonicalId is validated as a safe basename before being interpolated
  into a file path (defense in depth against a malformed manifest entry
  writing outside commands_target_dir).
- yamlSafeSingleLine now quotes descriptions starting with `[` or `{`
  so YAML doesn't parse them as a sequence/map.
- Per-record fs.writeFile failures are caught and counted (writeFailures)
  rather than aborting the whole IDE install — pointer files are a
  non-essential adjunct to the skill copy.
- Generator-shaped pointer files are refreshed when the manifest
  description changes; hand-modified files (body diverges from the
  generator pattern) are still preserved unless forceCommands is set.

Tests: extends Suite 8 with description-update propagation; adds new
Suite 40c covering OpenCode + openhands batches in both orderings plus
partial-IDE uninstall pointer cleanup. 308 tests pass (was 296).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four nitpicks from CodeRabbit's original review that were missed in the
first triage pass:

- Hand-edited pointers now survive the production install flow.
  cleanupCommandPointers spares pointers for canonicalIds that are still
  in the new manifest when called from the install/update flow (signal:
  options.previousSkillIds is set). Uninstall and partial-IDE removal
  flows still wipe pointers as before. The previous behavior wiped every
  pointer in removalSet before installCommandPointers could run, so its
  skip-if-exists guard never fired and hand edits were lost on every
  reinstall — contradicting the docstring's preservation claim.
- RESERVED_OPENCODE_COMMANDS is now gated on this.name === 'opencode'
  so future adapters opting into commands_target_dir don't silently
  inherit OpenCode's reserved-name set.
- printSummary now surfaces results.commands so users see how many
  pointers were created/refreshed/skipped per install, plus a warning
  for any per-file write failures.
- Dropped a dead `typeof entry !== 'string'` check; fs.readdir without
  withFileTypes always yields strings.

Tests: extends Suite 8 with a hand-edit-preservation regression that
calls setup with previousSkillIds (the production shape) and asserts a
sentinel byte sequence in the pointer body survives. 310 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jheyworth jheyworth force-pushed the feat/opencode-command-pointers branch from 6bc635f to 46a3d85 Compare April 27, 2026 06:10
…gents

Re-scopes bmad-code-org#2324 to cover the second user-facing pain: GitHub Copilot's
Custom Agents picker, where installed BMAD skills currently don't show up
even though slash commands work natively.

Generalizes the per-platform pointer-file mechanism so the same
installCommandPointers / cleanupCommandPointers code path serves both
OpenCode (slash commands palette) and Copilot (Custom Agents picker), with
all platform-specific shape pushed into platform-codes.yaml as data:

- commands_target_dir       — where pointer files live (existing)
- commands_extension        — file extension (default '.md'; Copilot uses
                              '.agent.md' per VS Code Custom Agents docs)
- commands_body_template    — pointer body, supports {canonicalId} and
                              {target_dir} placeholders. Default matches
                              OpenCode's `@skills/<id>` resolver. Copilot
                              has no such resolver, so its template uses
                              the {project-root}/<target_dir>/<id>/SKILL.md
                              LOAD pattern (consistent with PR bmad-code-org#1769).

OpenCode behavior is unchanged. Copilot users now get a per-skill
.github/agents/<canonicalId>.agent.md file that surfaces the skill in the
Custom Agents picker — addressing the "agents being gone" complaint
flagged by enterprise users.

Tests: extends Suite 17 with assertions for Copilot agent pointer
creation, body content (LOAD pattern with {project-root}-rooted path),
and idempotency. 318 tests pass (was 310).

Refs bmad-code-org#2267

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jheyworth jheyworth changed the title fix(installer): generate OpenCode /<skill> slash commands fix(installer): generate slash-command and Custom-Agent pointer files (OpenCode + GitHub Copilot) Apr 28, 2026
… only

Earlier commit naively wrote a `.github/agents/<id>.agent.md` for every
installed skill, which would clutter the Custom Agents picker with 90+
workflow/tool entries that don't belong there.

Adds an `agents-only` filter that gates the per-skill emission on whether
the canonical id signals a persona agent:

- Primary rule: id contains `-agent-` (e.g. `bmad-agent-pm`,
  `gds-agent-game-dev`, `wds-agent-freya-ux`,
  `bmad-cis-agent-storyteller`).
- Allowlist: `bmad-tea` — TEA's Murat persona uses the bare module code
  rather than the `-agent-` convention. Listed explicitly so the rule
  still surfaces it.

Verified against the full installed manifest (114 skills): catches all
20 description-confirmed personas across BMM, CIS, GDS, WDS, TEA;
excludes all 94 workflows/tools.

Wired through a new yaml field on github-copilot:

  commands_filter: agents-only

OpenCode is unaffected — it has no `commands_filter` set, so the loop
behaves as before (every skill becomes a slash command).

Tests: extends Suite 17 with a multi-skill manifest fixture covering
persona/agent + bmad-tea + workflow cases; asserts persona agents and
bmad-tea get .agent.md files while workflows do not. 322 tests pass.

Refs bmad-code-org#2267

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jheyworth jheyworth changed the title fix(installer): generate slash-command and Custom-Agent pointer files (OpenCode + GitHub Copilot) fix(installer): generate slash-command and Agent pointer files (OpenCode + GitHub Copilot) Apr 28, 2026
@bmadcode
Copy link
Copy Markdown
Collaborator

First — thanks for taking this on. Restoring the BMAD personas in Copilot's Custom Agents picker is a big win for the enterprise users who've been asking for it, and the fact that you generalized the mechanism to cover OpenCode in the same code path is exactly the right shape. Appreciate the work.

The data-driven commands_* yaml shape, hand-edit preservation, and cleanup symmetry all read well. One concern on the agents-only filter:

The -agent- naming convention isn't a load-bearing contract anywhere else in the codebase, so we can't rely on it long-term. A future persona that doesn't follow the convention silently won't show up in the Copilot picker, and the bmad-tea allowlist already shows the convention starting to break. Adding exceptions every time someone names a persona differently is a maintenance trap.

Suggested change: derive "is this a persona agent?" from the presence of customize.toml with an [agent] section in the source skill directory, rather than from the canonical id.

Verified across all five modules — every persona has it, no workflow/tool has it:

  • bmad-bmm: 6 (analyst, architect, dev, pm, tech-writer, ux-designer)
  • bmad-cis: 6 (storyteller, design-thinking-coach, brainstorming-coach, creative-problem-solver, innovation-strategist, presentation-master)
  • bmad-gds: 5 (game-architect, game-designer, game-dev, game-solo-dev, tech-writer)
  • bmad-wds: 2 (freya-ux, saga-analyst)
  • bmad-tea: 1 (bmad-tea, falls in naturally, no allowlist needed)

Total = 20, matches your "20 description-confirmed personas" exactly.

Why this is better than a name regex or a new type field:

  • Zero schema change. The signal already exists in every agent skill on disk today.
  • Source of truth is the behavior (skill has agent customization) rather than a label that has to be maintained in sync.
  • Self-correcting: anyone adding a new persona will add customize.toml with [agent] because that's how personas are configured. They can't forget to tag it.
  • NON_CONVENTIONAL_AGENT_IDS (and the bmad-tea exception) goes away entirely.

Implementation sketch. The manifest already records path (SKILL.md location), so the toml path is derivable:

```js
function isAgentSkill(record, projectDir) {
if (!record?.path) return false;
const tomlPath = path.join(projectDir, path.dirname(record.path), 'customize.toml');
if (!fs.pathExistsSync(tomlPath)) return false;
const content = fs.readFileSync(tomlPath, 'utf8');
return /^[agent]/m.test(content);
}
```

One file read per skill at install time, no manifest column, no frontmatter change. The commands_filter: agents-only yaml stays as-is. Only the implementation behind it changes.

Everything else in the PR can land as written. Happy to merge once this swap is in.

jheyworth and others added 3 commits April 29, 2026 08:00
Per maintainer review on PR bmad-code-org#2324: the `-agent-` naming convention isn't
a load-bearing contract anywhere else in the codebase, and the bmad-tea
allowlist already shows it starting to break. A future persona that
doesn't follow the convention would silently disappear from the Copilot
Custom Agents picker.

Replaces the name-based filter with a behavior-based signal: read each
skill's source `customize.toml` and check for an `[agent]` section. This
is the actual configuration source of truth — every BMAD persona is
configured under `[agent]`, every workflow under `[workflow]`, every
standalone skill has no customize.toml.

Verified on disk against the full installed manifest (114 skills):

- 20 personas detected — exactly the description-confirmed count across
  BMM, CIS, GDS, WDS, TEA. bmad-tea is caught natively (no allowlist).
- 94 workflows/tools correctly excluded.
- `bmad-agent-builder` (meta-skill that builds agent skills) is now
  CORRECTLY excluded — its canonical id contains `-agent-` but its
  customize.toml has [workflow], not [agent], because it isn't a
  persona itself. The previous naming-based filter was including it in
  the agents picker, which would have been a silent UX bug.

`NON_CONVENTIONAL_AGENT_IDS` constant is removed entirely — the toml
signal subsumes it.

Tests: extends Suite 17 with a 4-skill fixture that covers persona +
non-conventional persona + workflow + meta-skill cases. 388 tests pass.

Refs bmad-code-org#2267

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a single, deliberate exception to the toml-based agents-only filter:
`bmad-help` is the structural meta-skill across BMAD — the orientation
helper that points users at every other skill. Users invoke it
persona-style ("ask the helper") even though it has no `[agent]`
customize.toml of its own (it isn't a configurable persona).

Implemented as a one-element ALWAYS_AGENT_IDS set rather than a hardcode
in the function body so the exception is named, documented, and
discoverable. The skill is structurally unique — there is no second
meta-help skill — so this is not the start of a growing allowlist; it's
a one-off for the one orientation surface BMAD ships.

Verified on disk: agents picker now shows 21 entries (20 personas via
[agent] in customize.toml + bmad-help). bmad-agent-builder stays
correctly excluded (its customize.toml has [workflow], not [agent]).

Tests: extends Suite 17 with a `bmad-help` fixture (no customize.toml,
must still appear in agents picker). 389 tests pass.

Refs bmad-code-org#2267

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jheyworth
Copy link
Copy Markdown
Contributor Author

jheyworth commented Apr 29, 2026

@bmadcode

Agreed — naming convention was a trap. Swapped to the customize.toml signal in 75e1fa23.

Bonus: the new signal correctly excludes bmad-agent-builder (id contains -agent- but its toml has [workflow], not [agent] — it's a skill-builder workflow, not a persona). The old filter was wrongly including it. Real persona count is 20, matches yours exactly:

Module Personas
BMM 6 (analyst, architect, dev, pm, tech-writer, ux-designer)
CIS 6
GDS 5
WDS 2
TEA 1 (bmad-tea — caught natively, no allowlist)
Total 20

Disk-verified against the full installed manifest (114 skills): 20 personas detected, 94 skipped. Implementation matches your sketch except using bmadDir (not projectDir) so test fixtures with cross-temp-dir bmad still resolve correctly.


One addition worth flagging

(commit 9ed88e42): ALWAYS_AGENT_IDS = new Set(['bmad-help'])

So bmad-help appears in Copilot's picker despite having no customize.toml.

Reasoning: bmad-help is the discovery surface — what new users reach for when they don't know what to invoke yet. Hiding it from the picker punishes onboarding the most. Structurally unique (no second meta-help skill), so the allowlist can't grow the way a naming convention could.

Same shape you flagged for bmad-tea. Three options if you'd prefer differently:

  • A. Ship as-is (one named exception, documented).
  • B. Add customize.toml with [agent] to src/core-skills/bmad-help/ — same outcome via the toml filter, no installer-side allowlist.
  • C. Drop bmad-help from the picker.

Defaulting to A; happy to swap to B in this PR if you'd prefer.


389 tests pass. NON_CONVENTIONAL_AGENT_IDS is gone. Ready for re-review.

@bmadcode bmadcode merged commit 65b810a into bmad-code-org:main Apr 30, 2026
5 checks passed
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.

2 participants