mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-12 05:00:36 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17d7e59343 | ||
|
|
4758c5c68d | ||
|
|
c4b0826da7 |
@@ -1,5 +1,16 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 4758c5c: Add support for new AI tools with native slash command integration
|
||||
|
||||
- **Gemini CLI**: Add native TOML-based slash command support for Gemini CLI with `.gemini/commands/openspec/` integration
|
||||
- **RooCode**: Add RooCode integration with configurator, slash commands, and templates
|
||||
- **Cline**: Fix Cline to use workflows instead of rules for slash commands (`.clinerules/workflows/` paths)
|
||||
- **Documentation**: Update documentation to reflect new integrations and workflow changes
|
||||
|
||||
## 0.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -96,6 +96,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
|
||||
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
|
||||
| **RooCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.roo/commands/`) |
|
||||
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
|
||||
| **Gemini CLI** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.gemini/commands/openspec/`) |
|
||||
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
@@ -233,7 +234,7 @@ Or run the command yourself in terminal:
|
||||
$ openspec archive add-profile-filters --yes # Archive the completed change without prompts
|
||||
```
|
||||
|
||||
**Note:** Tools with native slash commands (Claude Code, CodeBuddy, Cursor, Codex, Qoder) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change".
|
||||
**Note:** Tools with native slash commands (Claude Code, CodeBuddy, Cursor, Codex, Qoder, RooCode) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change".
|
||||
|
||||
## Command Reference
|
||||
|
||||
|
||||
@@ -238,6 +238,14 @@ The init command SHALL generate slash command files for supported editors using
|
||||
- **AND** wrap the OpenSpec managed markers (`<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->`) inside the `prompt` value so `openspec update` can safely refresh the body between markers without touching the TOML framing
|
||||
- **AND** ensure the slash-command copy matches the existing proposal/apply/archive templates used by other tools
|
||||
|
||||
#### Scenario: Generating slash commands for RooCode
|
||||
- **WHEN** the user selects RooCode during initialization
|
||||
- **THEN** create `.roo/commands/openspec-proposal.md`, `.roo/commands/openspec-apply.md`, and `.roo/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include simple Markdown headings (e.g., `# OpenSpec: Proposal`) without YAML frontmatter
|
||||
- **AND** wrap the generated content in OpenSpec managed markers where applicable so `openspec update` can safely refresh the commands
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
### Requirement: Non-Interactive Mode
|
||||
The command SHALL support non-interactive operation through command-line options for automation and CI/CD use cases.
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
@@ -20,6 +20,7 @@ export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie' },
|
||||
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
|
||||
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
|
||||
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode' },
|
||||
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
|
||||
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
|
||||
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ClineSlashCommandConfigurator } from './cline.js';
|
||||
import { CrushSlashCommandConfigurator } from './crush.js';
|
||||
import { CostrictSlashCommandConfigurator } from './costrict.js';
|
||||
import { QwenSlashCommandConfigurator } from './qwen.js';
|
||||
import { RooCodeSlashCommandConfigurator } from './roocode.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
|
||||
@@ -38,6 +39,7 @@ export class SlashCommandRegistry {
|
||||
const crush = new CrushSlashCommandConfigurator();
|
||||
const costrict = new CostrictSlashCommandConfigurator();
|
||||
const qwen = new QwenSlashCommandConfigurator();
|
||||
const roocode = new RooCodeSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(codeBuddy.toolId, codeBuddy);
|
||||
@@ -56,6 +58,7 @@ export class SlashCommandRegistry {
|
||||
this.configurators.set(crush.toolId, crush);
|
||||
this.configurators.set(costrict.toolId, costrict);
|
||||
this.configurators.set(qwen.toolId, qwen);
|
||||
this.configurators.set(roocode.toolId, roocode);
|
||||
}
|
||||
|
||||
static register(configurator: SlashCommandConfigurator): void {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const NEW_FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.roo/commands/openspec-proposal.md',
|
||||
apply: '.roo/commands/openspec-apply.md',
|
||||
archive: '.roo/commands/openspec-archive.md'
|
||||
};
|
||||
|
||||
export class RooCodeSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'roocode';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return NEW_FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string | undefined {
|
||||
const descriptions: Record<SlashCommandId, string> = {
|
||||
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
|
||||
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
|
||||
archive: 'Archive a deployed OpenSpec change and update specs.'
|
||||
};
|
||||
const description = descriptions[id];
|
||||
return `# OpenSpec: ${id.charAt(0).toUpperCase() + id.slice(1)}\n\n${description}`;
|
||||
}
|
||||
}
|
||||
+47
-1
@@ -1194,6 +1194,53 @@ describe('InitCommand', () => {
|
||||
expect(costrictChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should create RooCode slash command files with templates', async () => {
|
||||
queueSelections('roocode', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const rooProposal = path.join(
|
||||
testDir,
|
||||
'.roo/commands/openspec-proposal.md'
|
||||
);
|
||||
const rooApply = path.join(
|
||||
testDir,
|
||||
'.roo/commands/openspec-apply.md'
|
||||
);
|
||||
const rooArchive = path.join(
|
||||
testDir,
|
||||
'.roo/commands/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(rooProposal)).toBe(true);
|
||||
expect(await fileExists(rooApply)).toBe(true);
|
||||
expect(await fileExists(rooArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(rooProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('# OpenSpec: Proposal');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(rooApply, 'utf-8');
|
||||
expect(applyContent).toContain('# OpenSpec: Apply');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(rooArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('# OpenSpec: Archive');
|
||||
expect(archiveContent).toContain('openspec archive <id> --yes');
|
||||
});
|
||||
|
||||
it('should mark RooCode as already configured during extend mode', async () => {
|
||||
queueSelections('roocode', DONE, 'roocode', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const rooChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'roocode'
|
||||
);
|
||||
expect(rooChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should create Qoder slash command files with templates', async () => {
|
||||
queueSelections('qoder', DONE);
|
||||
|
||||
@@ -1278,7 +1325,6 @@ describe('InitCommand', () => {
|
||||
expect(content).toContain('openspec update');
|
||||
expect(content).toContain('<!-- OPENSPEC:END -->');
|
||||
});
|
||||
|
||||
it('should update existing COSTRICT.md with markers', async () => {
|
||||
queueSelections('costrict', DONE);
|
||||
|
||||
|
||||
@@ -1071,6 +1071,79 @@ Old slash content
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing RooCode slash command files', async () => {
|
||||
const rooPath = path.join(
|
||||
testDir,
|
||||
'.roo/commands/openspec-proposal.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(rooPath), { recursive: true });
|
||||
const initialContent = `# OpenSpec: Proposal
|
||||
|
||||
Old description
|
||||
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(rooPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(rooPath, 'utf-8');
|
||||
// For RooCode, the header is Markdown, preserve it and update only managed block
|
||||
expect(updated).toContain('# OpenSpec: Proposal');
|
||||
expect(updated).toContain('**Guardrails**');
|
||||
expect(updated).toContain(
|
||||
'Validate with `openspec validate <id> --strict`'
|
||||
);
|
||||
expect(updated).not.toContain('Old body');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
||||
);
|
||||
expect(logMessage).toContain('AGENTS.md (created)');
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .roo/commands/openspec-proposal.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create missing RooCode slash command files on update', async () => {
|
||||
const rooApply = path.join(
|
||||
testDir,
|
||||
'.roo/commands/openspec-apply.md'
|
||||
);
|
||||
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(rooApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
rooApply,
|
||||
`# OpenSpec: Apply
|
||||
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const rooProposal = path.join(
|
||||
testDir,
|
||||
'.roo/commands/openspec-proposal.md'
|
||||
);
|
||||
const rooArchive = path.join(
|
||||
testDir,
|
||||
'.roo/commands/openspec-archive.md'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(rooProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(rooArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should not create missing CoStrict slash command files on update', async () => {
|
||||
const costrictApply = path.join(
|
||||
testDir,
|
||||
@@ -1181,6 +1254,7 @@ More instructions after.`;
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
|
||||
it('should not create COSTRICT.md if it does not exist', async () => {
|
||||
// Ensure COSTRICT.md does not exist
|
||||
const costrictPath = path.join(testDir, 'COSTRICT.md');
|
||||
|
||||
Reference in New Issue
Block a user