fix(installer): filter Copilot Custom Agents picker to persona agents 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 #2267 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56a3f267f0
commit
5e0a8ea2f3
|
|
@ -566,10 +566,30 @@ async function runTests() {
|
||||||
typeof copilotInstaller?.commands_body_template === 'string' && copilotInstaller.commands_body_template.includes('{canonicalId}'),
|
typeof copilotInstaller?.commands_body_template === 'string' && copilotInstaller.commands_body_template.includes('{canonicalId}'),
|
||||||
'GitHub Copilot defines a commands_body_template with {canonicalId} placeholder',
|
'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 tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-'));
|
||||||
const installedBmadDir17 = await createTestBmadFixture();
|
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');
|
const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
|
||||||
await fs.ensureDir(path.dirname(copilotInstructionsPath17));
|
await fs.ensureDir(path.dirname(copilotInstructionsPath17));
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
|
|
@ -605,31 +625,43 @@ async function runTests() {
|
||||||
'GitHub Copilot setup preserves user content in copilot-instructions.md',
|
'GitHub Copilot setup preserves user content in copilot-instructions.md',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Custom Agents picker integration: a per-skill .agent.md file should be
|
// Custom Agents picker integration: persona agents (and bmad-tea) get
|
||||||
// generated under .github/agents/ so the skill appears in Copilot's
|
// .agent.md files in .github/agents/. Workflows do NOT — the
|
||||||
// Custom Agents picker. Body uses the LOAD-{project-root}/... pattern
|
// agents-only filter keeps the picker uncluttered.
|
||||||
// (Copilot's body has no @skills/<id> resolver, so the agent file
|
const agentsDir17 = path.join(tempProjectDir17, '.github', 'agents');
|
||||||
// instructs the model to load the SKILL.md directly).
|
const agentFileForPersona17 = path.join(agentsDir17, 'bmad-agent-fixture.agent.md');
|
||||||
const agentFile17 = path.join(tempProjectDir17, '.github', 'agents', 'bmad-master.agent.md');
|
const agentFileForTea17 = path.join(agentsDir17, 'bmad-tea.agent.md');
|
||||||
assert(await fs.pathExists(agentFile17), 'GitHub Copilot install writes per-skill .agent.md pointer file');
|
const agentFileForWorkflow17 = path.join(agentsDir17, 'bmad-master.agent.md');
|
||||||
const agentContent17 = await fs.readFile(agentFile17, 'utf8');
|
|
||||||
|
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(
|
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)',
|
'Copilot agent pointer carries a description in YAML frontmatter (drives the agents picker label)',
|
||||||
);
|
);
|
||||||
assert(
|
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}/<target_dir>/<id>/SKILL.md',
|
'Copilot agent pointer body resolves to the skill via LOAD {project-root}/<target_dir>/<id>/SKILL.md',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Idempotency: re-running setup must not duplicate or rewrite the agent
|
// 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, {
|
const result17b = await ideManager17.setup('github-copilot', tempProjectDir17, installedBmadDir17, {
|
||||||
silent: true,
|
silent: true,
|
||||||
selectedModules: ['bmm'],
|
selectedModules: ['bmm'],
|
||||||
});
|
});
|
||||||
assert(result17b.success === true, 'Second GitHub Copilot install succeeds (idempotent)');
|
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(tempProjectDir17);
|
||||||
await fs.remove(path.dirname(installedBmadDir17));
|
await fs.remove(path.dirname(installedBmadDir17));
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,24 @@ function isSafeCanonicalId(value) {
|
||||||
// OpenCode's native `@skills/<id>` skill-reference syntax.
|
// OpenCode's native `@skills/<id>` skill-reference syntax.
|
||||||
const DEFAULT_COMMANDS_BODY_TEMPLATE = '@skills/{canonicalId}';
|
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 `<module>-agent-<role>`. 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:
|
// Resolve placeholders in a body template. Supported placeholders:
|
||||||
// {canonicalId} — the skill's canonical id
|
// {canonicalId} — the skill's canonical id
|
||||||
// {target_dir} — the platform's skill install directory (e.g. .agents/skills)
|
// {target_dir} — the platform's skill install directory (e.g. .agents/skills)
|
||||||
|
|
@ -265,6 +283,7 @@ class ConfigDrivenIdeSetup {
|
||||||
skippedExisting: 0,
|
skippedExisting: 0,
|
||||||
skippedCollision: 0,
|
skippedCollision: 0,
|
||||||
skippedInvalidId: 0,
|
skippedInvalidId: 0,
|
||||||
|
skippedFiltered: 0,
|
||||||
writeFailures: 0,
|
writeFailures: 0,
|
||||||
fallbackDescription: 0,
|
fallbackDescription: 0,
|
||||||
};
|
};
|
||||||
|
|
@ -279,6 +298,7 @@ class ConfigDrivenIdeSetup {
|
||||||
const extension = config.commands_extension || '.md';
|
const extension = config.commands_extension || '.md';
|
||||||
const template = config.commands_body_template || DEFAULT_COMMANDS_BODY_TEMPLATE;
|
const template = config.commands_body_template || DEFAULT_COMMANDS_BODY_TEMPLATE;
|
||||||
const targetDir = config.target_dir;
|
const targetDir = config.target_dir;
|
||||||
|
const filter = config.commands_filter || null;
|
||||||
|
|
||||||
const csvContent = await fs.readFile(csvPath, 'utf8');
|
const csvContent = await fs.readFile(csvPath, 'utf8');
|
||||||
const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
|
const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
|
||||||
|
|
@ -295,6 +315,15 @@ class ConfigDrivenIdeSetup {
|
||||||
continue;
|
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
|
// Reserved-name guard is OpenCode-specific. Other adapters that opt
|
||||||
// into commands_target_dir later should declare their own reserved
|
// into commands_target_dir later should declare their own reserved
|
||||||
// set rather than inheriting OpenCode's.
|
// set rather than inheriting OpenCode's.
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,14 @@ platforms:
|
||||||
commands_target_dir: .github/agents
|
commands_target_dir: .github/agents
|
||||||
commands_extension: .agent.md
|
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!"
|
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:
|
goose:
|
||||||
name: "Block Goose"
|
name: "Block Goose"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue