Skip to content

Commit c4b0826

Browse files
authored
feat(roocode): add RooCode integration (configurator, slash commands, templates) (#288)
* feat(roocode): Added RooCode tool support and related configurations Added RooCode tool integration, including: - Added RooCode configurator class - Registered RooCode to the tool registry - Implemented RooCode template files - Added RooCode slash command support - Updated README documentation - Added related test cases * Removed RooCode related configurations from the project. This includes deleting the RooCode configurator, its template, and associated tests.
1 parent 537e607 commit c4b0826

File tree

7 files changed

+162
-2
lines changed

7 files changed

+162
-2
lines changed

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

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)