From 5e0a8ea2f39ffac157599daf17027d8fba632ebd Mon Sep 17 00:00:00 2001 From: jheyworth <8269695+jheyworth@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:55:26 +0100 Subject: [PATCH] fix(installer): filter Copilot Custom Agents picker to persona agents only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Earlier commit naively wrote a `.github/agents/.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 #2267 Co-Authored-By: Claude Opus 4.7 (1M context) --- test/test-installation-components.js | 56 +++++++++++++++++++------ tools/installer/ide/_config-driven.js | 29 +++++++++++++ tools/installer/ide/platform-codes.yaml | 8 ++++ 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 0e5f12ad4..8da88958c 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -566,10 +566,30 @@ async function runTests() { typeof copilotInstaller?.commands_body_template === 'string' && copilotInstaller.commands_body_template.includes('{canonicalId}'), 'GitHub Copilot defines a commands_body_template with {canonicalId} placeholder', ); + assert( + copilotInstaller?.commands_filter === 'agents-only', + 'GitHub Copilot filters Custom Agents picker to persona agents only (agents-only)', + ); const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-')); const installedBmadDir17 = await createTestBmadFixture(); + // Extend the fixture: add a persona-style agent skill (`-agent-` in id) + // and the `bmad-tea` non-conventional agent so we can verify that the + // agents-only filter routes them to .github/agents/ while leaving the + // workflow-style `bmad-master` out. + const fixtureCsvPath17 = path.join(installedBmadDir17, '_config', 'skill-manifest.csv'); + await fs.writeFile( + fixtureCsvPath17, + [ + 'canonicalId,name,description,module,path', + '"bmad-master","bmad-master","Workflow-style fixture (no -agent- in id, should NOT appear in Copilot agents picker)","core","_bmad/core/bmad-master/SKILL.md"', + '"bmad-agent-fixture","bmad-agent-fixture","Persona agent fixture (-agent- in id, SHOULD appear in Copilot agents picker)","core","_bmad/core/bmad-agent-fixture/SKILL.md"', + '"bmad-tea","bmad-tea","Non-conventional persona agent fixture (Murat-style, SHOULD appear despite no -agent- segment)","core","_bmad/core/bmad-tea/SKILL.md"', + '', + ].join('\n'), + ); + const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md'); await fs.ensureDir(path.dirname(copilotInstructionsPath17)); await fs.writeFile( @@ -605,31 +625,43 @@ async function runTests() { 'GitHub Copilot setup preserves user content in copilot-instructions.md', ); - // Custom Agents picker integration: a per-skill .agent.md file should be - // generated under .github/agents/ so the skill appears in Copilot's - // Custom Agents picker. Body uses the LOAD-{project-root}/... pattern - // (Copilot's body has no @skills/ resolver, so the agent file - // instructs the model to load the SKILL.md directly). - const agentFile17 = path.join(tempProjectDir17, '.github', 'agents', 'bmad-master.agent.md'); - assert(await fs.pathExists(agentFile17), 'GitHub Copilot install writes per-skill .agent.md pointer file'); - const agentContent17 = await fs.readFile(agentFile17, 'utf8'); + // Custom Agents picker integration: persona agents (and bmad-tea) get + // .agent.md files in .github/agents/. Workflows do NOT — the + // agents-only filter keeps the picker uncluttered. + const agentsDir17 = path.join(tempProjectDir17, '.github', 'agents'); + const agentFileForPersona17 = path.join(agentsDir17, 'bmad-agent-fixture.agent.md'); + const agentFileForTea17 = path.join(agentsDir17, 'bmad-tea.agent.md'); + const agentFileForWorkflow17 = path.join(agentsDir17, 'bmad-master.agent.md'); + + assert(await fs.pathExists(agentFileForPersona17), 'Persona agent (-agent- in id) gets a .agent.md file in .github/agents/'); + assert(await fs.pathExists(agentFileForTea17), 'bmad-tea persona (non-conventional id) is allowlisted into .github/agents/'); assert( - agentContent17.includes('description:'), + !(await fs.pathExists(agentFileForWorkflow17)), + 'Workflow skill (no -agent- in id, not in allowlist) is FILTERED OUT of .github/agents/', + ); + + // Body content of the persona agent file: frontmatter description + + // LOAD pattern referencing the skill's SKILL.md path under target_dir. + const personaAgentContent17 = await fs.readFile(agentFileForPersona17, 'utf8'); + assert( + personaAgentContent17.includes('description:'), 'Copilot agent pointer carries a description in YAML frontmatter (drives the agents picker label)', ); assert( - agentContent17.includes('{project-root}/.agents/skills/bmad-master/SKILL.md'), + personaAgentContent17.includes('{project-root}/.agents/skills/bmad-agent-fixture/SKILL.md'), 'Copilot agent pointer body resolves to the skill via LOAD {project-root}///SKILL.md', ); // Idempotency: re-running setup must not duplicate or rewrite the agent - // pointer when the source manifest is unchanged. + // pointer when the source manifest is unchanged, AND must not start + // emitting workflow-skill agent files. const result17b = await ideManager17.setup('github-copilot', tempProjectDir17, installedBmadDir17, { silent: true, selectedModules: ['bmm'], }); assert(result17b.success === true, 'Second GitHub Copilot install succeeds (idempotent)'); - assert(await fs.pathExists(agentFile17), 'Copilot agent pointer survives a second install pass'); + assert(await fs.pathExists(agentFileForPersona17), 'Persona agent pointer survives a second install pass'); + assert(!(await fs.pathExists(agentFileForWorkflow17)), 'Workflow skill remains filtered out of agents picker on second install'); await fs.remove(tempProjectDir17); await fs.remove(path.dirname(installedBmadDir17)); diff --git a/tools/installer/ide/_config-driven.js b/tools/installer/ide/_config-driven.js index 1644aecb3..b725aaa77 100644 --- a/tools/installer/ide/_config-driven.js +++ b/tools/installer/ide/_config-driven.js @@ -57,6 +57,24 @@ function isSafeCanonicalId(value) { // OpenCode's native `@skills/` skill-reference syntax. const DEFAULT_COMMANDS_BODY_TEMPLATE = '@skills/{canonicalId}'; +// Persona-agent id outside the `-agent-` naming convention. +// TEA's Murat is the only persona whose canonical id is the bare module code +// rather than `-agent-`. Listed here so platforms that filter +// for "agents only" (e.g. GitHub Copilot's Custom Agents picker) include it. +const NON_CONVENTIONAL_AGENT_IDS = new Set(['bmad-tea']); + +// Is this skill a persona agent (vs. a workflow/tool/standalone skill)? +// Used by platforms that surface only persona agents (e.g. Copilot's Custom +// Agents picker). Rule: canonical id contains `-agent-` OR is in the +// known non-conventional allowlist. Tested against the full installed +// manifest — catches all 20 description-confirmed personas across BMM, +// CIS, GDS, WDS, TEA without false positives. +function isAgentSkill(canonicalId) { + if (typeof canonicalId !== 'string') return false; + if (NON_CONVENTIONAL_AGENT_IDS.has(canonicalId)) return true; + return canonicalId.includes('-agent-'); +} + // Resolve placeholders in a body template. Supported placeholders: // {canonicalId} — the skill's canonical id // {target_dir} — the platform's skill install directory (e.g. .agents/skills) @@ -265,6 +283,7 @@ class ConfigDrivenIdeSetup { skippedExisting: 0, skippedCollision: 0, skippedInvalidId: 0, + skippedFiltered: 0, writeFailures: 0, fallbackDescription: 0, }; @@ -279,6 +298,7 @@ class ConfigDrivenIdeSetup { const extension = config.commands_extension || '.md'; const template = config.commands_body_template || DEFAULT_COMMANDS_BODY_TEMPLATE; const targetDir = config.target_dir; + const filter = config.commands_filter || null; const csvContent = await fs.readFile(csvPath, 'utf8'); const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true }); @@ -295,6 +315,15 @@ class ConfigDrivenIdeSetup { continue; } + // Optional per-platform filter: surfaces that should only show + // persona agents (e.g. Copilot's Custom Agents picker) skip + // workflow/tool skills here so the picker isn't cluttered with + // 90+ unrelated entries. + if (filter === 'agents-only' && !isAgentSkill(canonicalId)) { + result.skippedFiltered++; + continue; + } + // Reserved-name guard is OpenCode-specific. Other adapters that opt // into commands_target_dir later should declare their own reserved // set rather than inheriting OpenCode's. diff --git a/tools/installer/ide/platform-codes.yaml b/tools/installer/ide/platform-codes.yaml index c80996107..0e84c4f02 100644 --- a/tools/installer/ide/platform-codes.yaml +++ b/tools/installer/ide/platform-codes.yaml @@ -135,6 +135,14 @@ platforms: commands_target_dir: .github/agents commands_extension: .agent.md commands_body_template: "LOAD the FULL {project-root}/{target_dir}/{canonicalId}/SKILL.md, READ its entire contents and follow its directions exactly!" + # The Custom Agents picker should only show persona agents (not + # workflows/tools). BMAD's persona agents are conventionally named + # with an `-agent-` segment in their canonical id (e.g. + # `bmad-agent-pm`, `gds-agent-game-dev`, `wds-agent-freya-ux`, + # `bmad-cis-agent-storyteller`). The one exception is `bmad-tea` — + # TEA's Murat persona uses the module code as its id rather than the + # `-agent-` convention. This filter keeps the picker uncluttered. + commands_filter: agents-only goose: name: "Block Goose"