mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
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:
co-authored by
Clay Good
Storm-Chaser
Claude Opus 5.5
parent
f197804a38
commit
a7f08b8a46
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@fission-ai/openspec": minor
|
||||
---
|
||||
|
||||
### New Features
|
||||
|
||||
- **GSD support**: Install OpenSpec workflows as project skills with `openspec init --tools gsd`.
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user