Merge pull request #301257 from microsoft/connor4312/301252

chat: support root-level SKILL.md as fallback for plugins
This commit is contained in:
Connor Peet
2026-03-12 15:16:17 -07:00
committed by GitHub
parent cbc25fffb2
commit e2e1d877d6
2 changed files with 49 additions and 2 deletions

View File

@@ -510,7 +510,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements
};
const commands = observeComponent('commands', d => this._readMarkdownComponents(d));
const skills = observeComponent('skills', d => this._readSkills(d));
const skills = observeComponent('skills', d => this._readSkills(uri, d));
const agents = observeComponent('agents', d => this._readMarkdownComponents(d));
const instructions = observeComponent('rules', d => this._readRules(d));
const hooks = observeComponent(
@@ -699,7 +699,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements
}
}
private async _readSkills(dirs: readonly URI[]): Promise<readonly IAgentPluginSkill[]> {
private async _readSkills(pluginRoot: URI, dirs: readonly URI[]): Promise<readonly IAgentPluginSkill[]> {
const seen = new Set<string>();
const skills: IAgentPluginSkill[] = [];
@@ -738,6 +738,14 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements
}
}
// Fallback: support single-skill plugins with SKILL.md at the plugin root
if (skills.length === 0) {
const rootSkillMd = URI.joinPath(pluginRoot, 'SKILL.md');
if (await this._pathExists(rootSkillMd)) {
addSkill(basename(pluginRoot), rootSkillMd);
}
}
skills.sort((a, b) => a.name.localeCompare(b.name));
return skills;
}

View File

@@ -242,6 +242,45 @@ suite('AgentPlugin format detection', () => {
assert.deepStrictEqual(skillNames, ['deploy', 'lint']);
}));
test('reads root-level SKILL.md as a fallback skill', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
const uri = pluginUri('/plugins/root-skill');
await writeFile('/plugins/root-skill/.plugin/plugin.json', JSON.stringify({ name: 'root-skill' }));
await writeFile('/plugins/root-skill/SKILL.md', '# Visual Explainer');
const discovery = createDiscovery();
discovery.start(mockEnablementModel);
await discovery.setSourcesAndRefresh([uri]);
const plugins = discovery.plugins.get();
assert.strictEqual(plugins.length, 1);
await waitForState(plugins[0].skills, s => s.length > 0);
assert.deepStrictEqual(
plugins[0].skills.get().map(s => s.name),
['root-skill'],
);
}));
test('root-level SKILL.md is ignored when skills/ has content', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
const uri = pluginUri('/plugins/root-skill-ignored');
await writeFile('/plugins/root-skill-ignored/.plugin/plugin.json', JSON.stringify({ name: 'root-skill-ignored' }));
await writeFile('/plugins/root-skill-ignored/SKILL.md', '# Root skill');
await writeFile('/plugins/root-skill-ignored/skills/real/SKILL.md', '# Real skill');
const discovery = createDiscovery();
discovery.start(mockEnablementModel);
await discovery.setSourcesAndRefresh([uri]);
const plugins = discovery.plugins.get();
assert.strictEqual(plugins.length, 1);
await waitForState(plugins[0].skills, s => s.length > 0);
assert.deepStrictEqual(
plugins[0].skills.get().map(s => s.name),
['real'],
);
}));
test('reads agents from agents/ directory', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
const uri = pluginUri('/plugins/agents-plugin');
await writeFile('/plugins/agents-plugin/.plugin/plugin.json', JSON.stringify({ name: 'agents-plugin' }));