Skip to content

Commit 7043cf4

Browse files
authored
Merge branch 'main' into feature/oh-my-zsh-completions
2 parents 816d6e4 + 17d7e59 commit 7043cf4

File tree

9 files changed

+174
-3
lines changed

9 files changed

+174
-3
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# @fission-ai/openspec
22

3+
## 0.15.0
4+
5+
### Minor Changes
6+
7+
- 4758c5c: Add support for new AI tools with native slash command integration
8+
9+
- **Gemini CLI**: Add native TOML-based slash command support for Gemini CLI with `.gemini/commands/openspec/` integration
10+
- **RooCode**: Add RooCode integration with configurator, slash commands, and templates
11+
- **Cline**: Fix Cline to use workflows instead of rules for slash commands (`.clinerules/workflows/` paths)
12+
- **Documentation**: Update documentation to reflect new integrations and workflow changes
13+
314
## 0.14.0
415

516
### Minor Changes

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
9696
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
9797
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
9898
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
99+
| **RooCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.roo/commands/`) |
99100
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
100101
| **Gemini CLI** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.gemini/commands/openspec/`) |
101102
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
@@ -233,7 +234,7 @@ Or run the command yourself in terminal:
233234
$ openspec archive add-profile-filters --yes # Archive the completed change without prompts
234235
```
235236

236-
**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".
237+
**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".
237238

238239
## Command Reference
239240

openspec/specs/cli-init/spec.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,14 @@ The init command SHALL generate slash command files for supported editors using
238238
- **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
239239
- **AND** ensure the slash-command copy matches the existing proposal/apply/archive templates used by other tools
240240

241+
#### Scenario: Generating slash commands for RooCode
242+
- **WHEN** the user selects RooCode during initialization
243+
- **THEN** create `.roo/commands/openspec-proposal.md`, `.roo/commands/openspec-apply.md`, and `.roo/commands/openspec-archive.md`
244+
- **AND** populate each file from shared templates so command text matches other tools
245+
- **AND** include simple Markdown headings (e.g., `# OpenSpec: Proposal`) without YAML frontmatter
246+
- **AND** wrap the generated content in OpenSpec managed markers where applicable so `openspec update` can safely refresh the commands
247+
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
248+
241249
### Requirement: Non-Interactive Mode
242250
The command SHALL support non-interactive operation through command-line options for automation and CI/CD use cases.
243251

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fission-ai/openspec",
3-
"version": "0.14.0",
3+
"version": "0.15.0",
44
"description": "AI-native system for spec-driven development",
55
"keywords": [
66
"openspec",

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const AI_TOOLS: AIToolOption[] = [
2020
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie' },
2121
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
2222
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
23+
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode' },
2324
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
2425
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
2526
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },

src/core/configurators/slash/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ClineSlashCommandConfigurator } from './cline.js';
1616
import { CrushSlashCommandConfigurator } from './crush.js';
1717
import { CostrictSlashCommandConfigurator } from './costrict.js';
1818
import { QwenSlashCommandConfigurator } from './qwen.js';
19+
import { RooCodeSlashCommandConfigurator } from './roocode.js';
1920

2021
export class SlashCommandRegistry {
2122
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -38,6 +39,7 @@ export class SlashCommandRegistry {
3839
const crush = new CrushSlashCommandConfigurator();
3940
const costrict = new CostrictSlashCommandConfigurator();
4041
const qwen = new QwenSlashCommandConfigurator();
42+
const roocode = new RooCodeSlashCommandConfigurator();
4143

4244
this.configurators.set(claude.toolId, claude);
4345
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -56,6 +58,7 @@ export class SlashCommandRegistry {
5658
this.configurators.set(crush.toolId, crush);
5759
this.configurators.set(costrict.toolId, costrict);
5860
this.configurators.set(qwen.toolId, qwen);
61+
this.configurators.set(roocode.toolId, roocode);
5962
}
6063

6164
static register(configurator: SlashCommandConfigurator): void {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { SlashCommandConfigurator } from './base.js';
2+
import { SlashCommandId } from '../../templates/index.js';
3+
4+
const NEW_FILE_PATHS: Record<SlashCommandId, string> = {
5+
proposal: '.roo/commands/openspec-proposal.md',
6+
apply: '.roo/commands/openspec-apply.md',
7+
archive: '.roo/commands/openspec-archive.md'
8+
};
9+
10+
export class RooCodeSlashCommandConfigurator extends SlashCommandConfigurator {
11+
readonly toolId = 'roocode';
12+
readonly isAvailable = true;
13+
14+
protected getRelativePath(id: SlashCommandId): string {
15+
return NEW_FILE_PATHS[id];
16+
}
17+
18+
protected getFrontmatter(id: SlashCommandId): string | undefined {
19+
const descriptions: Record<SlashCommandId, string> = {
20+
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
21+
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
22+
archive: 'Archive a deployed OpenSpec change and update specs.'
23+
};
24+
const description = descriptions[id];
25+
return `# OpenSpec: ${id.charAt(0).toUpperCase() + id.slice(1)}\n\n${description}`;
26+
}
27+
}

test/core/init.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,53 @@ describe('InitCommand', () => {
11941194
expect(costrictChoice.configured).toBe(true);
11951195
});
11961196

1197+
it('should create RooCode slash command files with templates', async () => {
1198+
queueSelections('roocode', DONE);
1199+
1200+
await initCommand.execute(testDir);
1201+
1202+
const rooProposal = path.join(
1203+
testDir,
1204+
'.roo/commands/openspec-proposal.md'
1205+
);
1206+
const rooApply = path.join(
1207+
testDir,
1208+
'.roo/commands/openspec-apply.md'
1209+
);
1210+
const rooArchive = path.join(
1211+
testDir,
1212+
'.roo/commands/openspec-archive.md'
1213+
);
1214+
1215+
expect(await fileExists(rooProposal)).toBe(true);
1216+
expect(await fileExists(rooApply)).toBe(true);
1217+
expect(await fileExists(rooArchive)).toBe(true);
1218+
1219+
const proposalContent = await fs.readFile(rooProposal, 'utf-8');
1220+
expect(proposalContent).toContain('# OpenSpec: Proposal');
1221+
expect(proposalContent).toContain('**Guardrails**');
1222+
1223+
const applyContent = await fs.readFile(rooApply, 'utf-8');
1224+
expect(applyContent).toContain('# OpenSpec: Apply');
1225+
expect(applyContent).toContain('Work through tasks sequentially');
1226+
1227+
const archiveContent = await fs.readFile(rooArchive, 'utf-8');
1228+
expect(archiveContent).toContain('# OpenSpec: Archive');
1229+
expect(archiveContent).toContain('openspec archive <id> --yes');
1230+
});
1231+
1232+
it('should mark RooCode as already configured during extend mode', async () => {
1233+
queueSelections('roocode', DONE, 'roocode', DONE);
1234+
await initCommand.execute(testDir);
1235+
await initCommand.execute(testDir);
1236+
1237+
const secondRunArgs = mockPrompt.mock.calls[1][0];
1238+
const rooChoice = secondRunArgs.choices.find(
1239+
(choice: any) => choice.value === 'roocode'
1240+
);
1241+
expect(rooChoice.configured).toBe(true);
1242+
});
1243+
11971244
it('should create Qoder slash command files with templates', async () => {
11981245
queueSelections('qoder', DONE);
11991246

@@ -1278,7 +1325,6 @@ describe('InitCommand', () => {
12781325
expect(content).toContain('openspec update');
12791326
expect(content).toContain('<!-- OPENSPEC:END -->');
12801327
});
1281-
12821328
it('should update existing COSTRICT.md with markers', async () => {
12831329
queueSelections('costrict', DONE);
12841330

test/core/update.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,79 @@ Old slash content
10711071
consoleSpy.mockRestore();
10721072
});
10731073

1074+
it('should refresh existing RooCode slash command files', async () => {
1075+
const rooPath = path.join(
1076+
testDir,
1077+
'.roo/commands/openspec-proposal.md'
1078+
);
1079+
await fs.mkdir(path.dirname(rooPath), { recursive: true });
1080+
const initialContent = `# OpenSpec: Proposal
1081+
1082+
Old description
1083+
1084+
<!-- OPENSPEC:START -->
1085+
Old body
1086+
<!-- OPENSPEC:END -->`;
1087+
await fs.writeFile(rooPath, initialContent);
1088+
1089+
const consoleSpy = vi.spyOn(console, 'log');
1090+
1091+
await updateCommand.execute(testDir);
1092+
1093+
const updated = await fs.readFile(rooPath, 'utf-8');
1094+
// For RooCode, the header is Markdown, preserve it and update only managed block
1095+
expect(updated).toContain('# OpenSpec: Proposal');
1096+
expect(updated).toContain('**Guardrails**');
1097+
expect(updated).toContain(
1098+
'Validate with `openspec validate <id> --strict`'
1099+
);
1100+
expect(updated).not.toContain('Old body');
1101+
1102+
const [logMessage] = consoleSpy.mock.calls[0];
1103+
expect(logMessage).toContain(
1104+
'Updated OpenSpec instructions (openspec/AGENTS.md'
1105+
);
1106+
expect(logMessage).toContain('AGENTS.md (created)');
1107+
expect(logMessage).toContain(
1108+
'Updated slash commands: .roo/commands/openspec-proposal.md'
1109+
);
1110+
1111+
consoleSpy.mockRestore();
1112+
});
1113+
1114+
it('should not create missing RooCode slash command files on update', async () => {
1115+
const rooApply = path.join(
1116+
testDir,
1117+
'.roo/commands/openspec-apply.md'
1118+
);
1119+
1120+
// Only create apply; leave proposal and archive missing
1121+
await fs.mkdir(path.dirname(rooApply), { recursive: true });
1122+
await fs.writeFile(
1123+
rooApply,
1124+
`# OpenSpec: Apply
1125+
1126+
<!-- OPENSPEC:START -->
1127+
Old body
1128+
<!-- OPENSPEC:END -->`
1129+
);
1130+
1131+
await updateCommand.execute(testDir);
1132+
1133+
const rooProposal = path.join(
1134+
testDir,
1135+
'.roo/commands/openspec-proposal.md'
1136+
);
1137+
const rooArchive = path.join(
1138+
testDir,
1139+
'.roo/commands/openspec-archive.md'
1140+
);
1141+
1142+
// Confirm they weren't created by update
1143+
await expect(FileSystemUtils.fileExists(rooProposal)).resolves.toBe(false);
1144+
await expect(FileSystemUtils.fileExists(rooArchive)).resolves.toBe(false);
1145+
});
1146+
10741147
it('should not create missing CoStrict slash command files on update', async () => {
10751148
const costrictApply = path.join(
10761149
testDir,
@@ -1181,6 +1254,7 @@ More instructions after.`;
11811254
consoleSpy.mockRestore();
11821255
});
11831256

1257+
11841258
it('should not create COSTRICT.md if it does not exist', async () => {
11851259
// Ensure COSTRICT.md does not exist
11861260
const costrictPath = path.join(testDir, 'COSTRICT.md');

0 commit comments

Comments
 (0)