feat(tools): add GSD skills support (#1082)

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Storm-Chaser <Storm-Chaser@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
姜陈骏
2026-09-29 20:24:04 +00:00
committed by GitHub
co-authored by Clay Good Storm-Chaser Claude Opus 5.5
parent f197804a38
commit a7f08b8a46
8 changed files with 55 additions and 2 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@fission-ai/openspec": minor
---
### New Features
- **GSD support**: Install OpenSpec workflows as project skills with `openspec init --tools gsd`.
+10
View File
@@ -36,6 +36,7 @@ The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
| Gemini CLI | `gemini` | `.gemini/skills/` | `/openspec-apply-change` | `.gemini/commands/opsx/` | `/opsx:apply` |
| GigaCode | `gigacode` | `.gigacode/skills/` | `/openspec-apply-change` | `.gigacode/commands/` | `/opsx-apply` |
| GitHub Copilot | `github-copilot` | `.github/skills/` | `/openspec-apply-change` | `.github/prompts/` | `/opsx-apply` |
| GSD | `gsd` | `.agents/skills/` | ask for `openspec-apply-change` | none | none |
| Hermes Agent | `hermes` | `.hermes/skills/` | `/openspec-apply-change` | none | none |
| iFlow | `iflow` | `.iflow/skills/` | `/openspec-apply-change` | `.iflow/commands/` | `/opsx-apply` |
| Junie | `junie` | `.junie/skills/` | `/openspec-apply-change` | `.junie/commands/` | `/opsx-apply` |
@@ -139,6 +140,15 @@ Skills stay in `.cline/skills/`.
`/openspec-<skill>`. If a skill disappears while its file still exists, run
`/skills reload`, then `/skills info openspec-propose` to confirm discovery.
### GSD
- **Project skills**: GSD reads OpenSpec workflows from
[`.agents/skills/`](https://github.com/open-gsd/gsd-pi/blob/main/docs/user-docs/skills.md).
- **Invocation**: ask GSD to use the `openspec-<workflow>` skill. GSD can also select
a matching skill through its skill discovery setting.
- **No subagent files**: [`.gsd/agents/`](https://github.com/open-gsd/gsd-pi/blob/main/docs/user-docs/subagents.md)
contains GSD subagent definitions. OpenSpec does not write workflow skills there.
### Hermes Agent
Hermes loads skills only from `~/.hermes/skills/` by default. Add the project's
+1
View File
@@ -67,6 +67,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'], requiresIdeRestart: true },
{ name: 'GigaCode', value: 'gigacode', available: true, successLabel: 'GigaCode', skillsDir: '.gigacode' },
{ name: 'GSD', value: 'gsd', available: true, successLabel: 'GSD', skillsDir: '.agents', detectionPaths: ['.gsd'] },
{ name: 'Hermes Agent', value: 'hermes', available: true, successLabel: 'Hermes Agent', skillsDir: '.hermes', detectionPaths: ['.hermes', 'HERMES.md', '.hermes.md'], setupNote: "Hermes only loads skills from ~/.hermes/skills by default. Add this project's .hermes/skills directory to skills.external_dirs in ~/.hermes/config.yaml so Hermes picks up the generated OpenSpec skills." },
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
{ name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie', requiresIdeRestart: true },
+1 -1
View File
@@ -85,7 +85,7 @@ const SKILL_INVOCATION_PREFIX: Record<string, string> = {
* Skill references for these tools are spelled as prose ("the openspec-propose
* skill") so skills-only delivery does not advertise unregistered commands.
*/
const NATURAL_LANGUAGE_SKILL_TOOLS = new Set<string>(['rovodev', 'codeassistant']);
const NATURAL_LANGUAGE_SKILL_TOOLS = new Set<string>(['gsd', 'rovodev', 'codeassistant']);
/**
* Whether a tool references skills by natural language rather than a slash
+8
View File
@@ -182,6 +182,14 @@ describe('available-tools', () => {
expect(getAvailableTools(testDir).map((tool) => tool.value)).toEqual(['zed']);
});
it('should detect GSD from its project directory', async () => {
await fs.mkdir(path.join(testDir, '.gsd'), { recursive: true });
const tools = getAvailableTools(testDir);
expect(tools.map((tool) => tool.value)).toEqual(['gsd']);
expect(tools[0].skillsDir).toBe('.agents');
});
it('should not detect the shared agents target from a bare .agents directory', async () => {
// Frameworks use `.agents/` for more than skills (rules, subagent definitions).
// The bare root therefore says nothing about whether this project keeps agent
+26
View File
@@ -760,6 +760,32 @@ describe('InitCommand', () => {
).toBe(true);
});
it('should support GSD through project skills without writing subagent definitions', async () => {
saveGlobalConfig({
featureFlags: {},
profile: 'core',
delivery: 'both',
});
const initCommand = new InitCommand({ tools: 'gsd', force: true });
await initCommand.execute(testDir);
const skillFile = path.join(testDir, '.agents', 'skills', 'openspec-explore', 'SKILL.md');
expect(await fileExists(skillFile)).toBe(true);
expect(await directoryExists(path.join(testDir, '.gsd', 'agents'))).toBe(false);
const skillContent = await fs.readFile(skillFile, 'utf-8');
expect(skillContent).toContain('the openspec-propose skill');
expect(skillContent).not.toContain('/opsx:propose');
const logCalls = vi.mocked(console.log).mock.calls.flat().map(String);
expect(
logCalls.some(
(entry) => entry.includes('Commands skipped for: gsd') && entry.includes('(no adapter)'),
),
).toBe(true);
});
it('should install MiniMax Code skills only in the user-home target', async () => {
saveGlobalConfig({
featureFlags: {},
+1
View File
@@ -53,6 +53,7 @@ describe('tool-detection', () => {
expect(tools).toContain('codeartsagent');
expect(tools).toContain('cursor');
expect(tools).toContain('devin');
expect(tools).toContain('gsd');
expect(tools).toContain('dsh');
// `--tools all` resolves to exactly this list, so `agents` being here is what
// puts the shared target in an `--tools all` run.
+1 -1
View File
@@ -238,7 +238,7 @@ describe('getSkillReferenceTransformer', () => {
expect(transformer('/opsx:unknown-command')).toBe('/opsx:unknown-command');
});
it.each(['rovodev', 'codeassistant'])('uses natural-language skill references for %s', (toolId) => {
it.each(['gsd', 'rovodev', 'codeassistant'])('uses natural-language skill references for %s', (toolId) => {
const transformer = getSkillReferenceTransformer(toolId);
expect(transformer('/opsx:propose')).toBe('the openspec-propose skill');
expect(transformer('Run `/opsx:apply` then /opsx:archive')).toBe(