diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 9877187c2..4522f0f37 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -576,7 +576,7 @@ async function runTests() { // Extend the fixture to exercise the agents-only filter, which detects // persona agents by the `[agent]` section in each skill's source - // customize.toml. Three skill types covered: + // customize.toml. Five skill types covered: // // 1. Persona agent — has customize.toml with [agent] → INCLUDED // 2. Persona with non-conventional id — also has [agent] → INCLUDED @@ -585,6 +585,9 @@ async function runTests() { // persona — has customize.toml with [workflow] → EXCLUDED // (mirrors `bmad-agent-builder` in the real manifest) // 4. Workflow skill — no customize.toml at all → EXCLUDED + // 5. `bmad-help` — structural exception via ALWAYS_AGENT_IDS; + // has no customize.toml of its own but surfaces in the + // agents picker because it's the meta-help skill → INCLUDED const fixtureCsvPath17 = path.join(installedBmadDir17, '_config', 'skill-manifest.csv'); await fs.writeFile( fixtureCsvPath17, @@ -594,6 +597,7 @@ async function runTests() { '"bmad-agent-fixture","bmad-agent-fixture","Persona agent — customize.toml has [agent], SHOULD appear","core","_bmad/core/bmad-agent-fixture/SKILL.md"', '"bmad-tea","bmad-tea","Non-conventional id but [agent] in customize.toml — SHOULD appear","core","_bmad/core/bmad-tea/SKILL.md"', '"bmad-agent-builder","bmad-agent-builder","Skill-builder workflow — id contains -agent- but customize.toml has [workflow] — should NOT appear","core","_bmad/core/bmad-agent-builder/SKILL.md"', + '"bmad-help","bmad-help","Meta-help skill — no customize.toml but ALWAYS_AGENT_IDS exception; SHOULD appear in agents picker","core","_bmad/core/bmad-help/SKILL.md"', '', ].join('\n'), ); @@ -603,7 +607,7 @@ async function runTests() { // SKILL.md files were already populated by createTestBmadFixture (they // share the bmad-master target_dir layout); only the customize.toml // and the new agent fixtures need to be created here. - for (const id of ['bmad-agent-fixture', 'bmad-tea', 'bmad-agent-builder']) { + for (const id of ['bmad-agent-fixture', 'bmad-tea', 'bmad-agent-builder', 'bmad-help']) { const dir17 = path.join(installedBmadDir17, 'core', id); await fs.ensureDir(dir17); await fs.writeFile( @@ -611,6 +615,9 @@ async function runTests() { ['---', `name: ${id}`, `description: fixture for ${id}`, '---', '', `Body of ${id}.`].join('\n'), ); } + // Note: bmad-help intentionally has NO customize.toml — it's the + // structural exception for which the ALWAYS_AGENT_IDS allowlist + // exists. // [agent] customize.toml for the two persona fixtures. await fs.writeFile( path.join(installedBmadDir17, 'core', 'bmad-agent-fixture', 'customize.toml'), @@ -672,6 +679,7 @@ async function runTests() { const agentFileForTea17 = path.join(agentsDir17, 'bmad-tea.agent.md'); const agentFileForWorkflow17 = path.join(agentsDir17, 'bmad-master.agent.md'); const agentFileForMetaSkill17 = path.join(agentsDir17, 'bmad-agent-builder.agent.md'); + const agentFileForBmadHelp17 = path.join(agentsDir17, 'bmad-help.agent.md'); assert( await fs.pathExists(agentFileForPersona17), @@ -679,6 +687,10 @@ async function runTests() { ); assert(await fs.pathExists(agentFileForTea17), 'Non-conventional id with [agent] in customize.toml is included (no allowlist needed)'); assert(!(await fs.pathExists(agentFileForWorkflow17)), 'Workflow skill (no customize.toml) is FILTERED OUT of .github/agents/'); + assert( + await fs.pathExists(agentFileForBmadHelp17), + 'bmad-help is INCLUDED in agents picker via ALWAYS_AGENT_IDS exception (structural meta-skill, no customize.toml)', + ); assert( !(await fs.pathExists(agentFileForMetaSkill17)), 'Meta-skill with -agent- in id but [workflow] in customize.toml is FILTERED OUT (signal is behavior, not naming)', diff --git a/tools/installer/ide/_config-driven.js b/tools/installer/ide/_config-driven.js index bf6fffbc5..77be9b6c5 100644 --- a/tools/installer/ide/_config-driven.js +++ b/tools/installer/ide/_config-driven.js @@ -57,6 +57,15 @@ function isSafeCanonicalId(value) { // OpenCode's native `@skills/` skill-reference syntax. const DEFAULT_COMMANDS_BODY_TEMPLATE = '@skills/{canonicalId}'; +// `bmad-help` is the structural meta-skill across BMAD: the orientation +// helper that points users at every other skill. It is invoked +// persona-style ("ask the helper") even though it has no [agent] +// customize.toml of its own (it isn't a configurable persona). Surfacing +// it in agents-picker contexts mirrors how users actually reach for it, +// and the inclusion is unique and stable — there is no second meta-help +// skill to encourage growth of this exception. +const ALWAYS_AGENT_IDS = new Set(['bmad-help']); + // 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). Signal: the skill's source `customize.toml` has an @@ -68,13 +77,15 @@ const DEFAULT_COMMANDS_BODY_TEMPLATE = '@skills/{canonicalId}'; // GDS, WDS, TEA, and correctly excludes meta-skills like // `bmad-agent-builder` (a skill-builder workflow whose canonical id // contains `-agent-` but which has no [agent] section because it isn't a -// persona itself). +// persona itself). Plus the explicit `ALWAYS_AGENT_IDS` set for the one +// structural exception (`bmad-help`). // // Reading the source toml — at install time the source skill directory // (resolved from manifest record.path) still exists; cleanup runs later // in the install flow. async function isAgentSkill(record, bmadDir) { if (!record?.path || !bmadDir) return false; + if (record.canonicalId && ALWAYS_AGENT_IDS.has(record.canonicalId)) return true; const bmadFolderName = path.basename(bmadDir); const bmadPrefix = bmadFolderName + '/'; const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;