From a7f08b8a462db5eeddae4e0b03f427987e3806a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=9C=E9=99=88=E9=AA=8F?= <1966722295@qq.com> Date: Tue, 29 Sep 2026 20:24:04 +0000 Subject: [PATCH] feat(tools): add GSD skills support (#1082) Co-authored-by: Clay Good Co-authored-by: Storm-Chaser Co-authored-by: Claude Opus 5.5 --- .changeset/brave-dingos-plan.md | 7 +++++++ docs-lab/reference/supported-tools.md | 10 ++++++++++ src/core/config.ts | 1 + src/utils/command-references.ts | 2 +- test/core/available-tools.test.ts | 8 ++++++++ test/core/init.test.ts | 26 +++++++++++++++++++++++++ test/core/shared/tool-detection.test.ts | 1 + test/utils/command-references.test.ts | 2 +- 8 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 .changeset/brave-dingos-plan.md diff --git a/.changeset/brave-dingos-plan.md b/.changeset/brave-dingos-plan.md new file mode 100644 index 00000000..d6569fdd --- /dev/null +++ b/.changeset/brave-dingos-plan.md @@ -0,0 +1,7 @@ +--- +"@fission-ai/openspec": minor +--- + +### New Features + +- **GSD support**: Install OpenSpec workflows as project skills with `openspec init --tools gsd`. diff --git a/docs-lab/reference/supported-tools.md b/docs-lab/reference/supported-tools.md index 8810eeb5..483b20de 100644 --- a/docs-lab/reference/supported-tools.md +++ b/docs-lab/reference/supported-tools.md @@ -36,6 +36,7 @@ The id goes to `openspec init --tools ` 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-`. 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-` 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 diff --git a/src/core/config.ts b/src/core/config.ts index 14afdc89..f3157c29 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -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 }, diff --git a/src/utils/command-references.ts b/src/utils/command-references.ts index 115cdce5..f161678b 100644 --- a/src/utils/command-references.ts +++ b/src/utils/command-references.ts @@ -85,7 +85,7 @@ const SKILL_INVOCATION_PREFIX: Record = { * 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(['rovodev', 'codeassistant']); +const NATURAL_LANGUAGE_SKILL_TOOLS = new Set(['gsd', 'rovodev', 'codeassistant']); /** * Whether a tool references skills by natural language rather than a slash diff --git a/test/core/available-tools.test.ts b/test/core/available-tools.test.ts index 3c1e777e..60f91bf9 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -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 diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 9586d6dd..bffe6a1d 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -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: {}, diff --git a/test/core/shared/tool-detection.test.ts b/test/core/shared/tool-detection.test.ts index 63fe6362..94434445 100644 --- a/test/core/shared/tool-detection.test.ts +++ b/test/core/shared/tool-detection.test.ts @@ -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. diff --git a/test/utils/command-references.test.ts b/test/utils/command-references.test.ts index 469700eb..1c1ac35c 100644 --- a/test/utils/command-references.test.ts +++ b/test/utils/command-references.test.ts @@ -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(