Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions openspec/specs/cli-init/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ This project uses OpenSpec to manage AI assistant workflows.
<!-- OPENSPEC:END -->
```

#### Scenario: Configuring RooCode

- **WHEN** RooCode is selected
- **THEN** create or update `ROOCODE.md` in the project root directory (not inside `openspec/`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jax-max I believe roocode supports the AGENTS.md can we get away with not having to create another rules file for roo if not needed?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TabishB AFAIK claude also supports AGENTS.md, maybe we can get rid of CLAUDE.md as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads-up. Roocode doesn’t have a native mechanism to "default scan ROOCODE.md and auto-apply its rules". I’ve removed the ROOCODE.md-related mechanism and only retained the slash command functionality – please help review, much appreciated!
@TabishB

Copy link
Contributor

@TabishB TabishB Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ravshansbox There's currently only 2 coding agent that don't support AGENTS.md. Claude and Cline. Cline will support it soon, but I don't think Claude will haha. it's been requested for a while: anthropics/claude-code#6235

- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`

### Requirement: Interactive Mode
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
#### Scenario: Displaying interactive menu
Expand Down Expand Up @@ -238,6 +244,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.

Expand Down
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
3 changes: 3 additions & 0 deletions src/core/configurators/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CostrictConfigurator } from './costrict.js';
import { QoderConfigurator } from './qoder.js';
import { AgentsStandardConfigurator } from './agents.js';
import { QwenConfigurator } from './qwen.js';
import { RooCodeConfigurator } from './roocode.js';

export class ToolRegistry {
private static tools: Map<string, ToolConfigurator> = new Map();
Expand All @@ -18,6 +19,7 @@ export class ToolRegistry {
const qoderConfigurator = new QoderConfigurator();
const agentsConfigurator = new AgentsStandardConfigurator();
const qwenConfigurator = new QwenConfigurator();
const roocodeConfigurator = new RooCodeConfigurator();
// Register with the ID that matches the checkbox value
this.tools.set('claude', claudeConfigurator);
this.tools.set('cline', clineConfigurator);
Expand All @@ -26,6 +28,7 @@ export class ToolRegistry {
this.tools.set('qoder', qoderConfigurator);
this.tools.set('agents', agentsConfigurator);
this.tools.set('qwen', qwenConfigurator);
this.tools.set('roocode', roocodeConfigurator);
}

static register(tool: ToolConfigurator): void {
Expand Down
23 changes: 23 additions & 0 deletions src/core/configurators/roocode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import path from 'path';
import { ToolConfigurator } from './base.js';
import { FileSystemUtils } from '../../utils/file-system.js';
import { TemplateManager } from '../templates/index.js';
import { OPENSPEC_MARKERS } from '../config.js';

export class RooCodeConfigurator implements ToolConfigurator {
name = 'RooCode';
configFileName = 'ROOCODE.md';
isAvailable = true;

async configure(projectPath: string, _openspecDir: string): Promise<void> {
const filePath = path.join(projectPath, this.configFileName);
const content = TemplateManager.getRooCodeTemplate();

await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
}
}
3 changes: 3 additions & 0 deletions src/core/configurators/slash/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions src/core/configurators/slash/roocode.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
}
5 changes: 5 additions & 0 deletions src/core/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { agentsTemplate } from './agents-template.js';
import { projectTemplate, ProjectContext } from './project-template.js';
import { claudeTemplate } from './claude-template.js';
import { clineTemplate } from './cline-template.js';
import { roocodeTemplate } from './roocode-template.js';
import { costrictTemplate } from './costrict-template.js';
import { agentsRootStubTemplate } from './agents-root-stub.js';
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
Expand Down Expand Up @@ -33,6 +34,10 @@ export class TemplateManager {
return clineTemplate;
}

static getRooCodeTemplate(): string {
return roocodeTemplate;
}

static getCostrictTemplate(): string {
return costrictTemplate;
}
Expand Down
1 change: 1 addition & 0 deletions src/core/templates/roocode-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { agentsRootStubTemplate as roocodeTemplate } from './agents-root-stub.js';
79 changes: 79 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -1279,6 +1326,38 @@ describe('InitCommand', () => {
expect(content).toContain('<!-- OPENSPEC:END -->');
});

it('should create ROOCODE.md when RooCode is selected', async () => {
queueSelections('roocode', DONE);

await initCommand.execute(testDir);

const roocodePath = path.join(testDir, 'ROOCODE.md');
expect(await fileExists(roocodePath)).toBe(true);

const content = await fs.readFile(roocodePath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain("@/openspec/AGENTS.md");
expect(content).toContain('openspec update');
expect(content).toContain('<!-- OPENSPEC:END -->');
});

it('should update existing ROOCODE.md with markers', async () => {
queueSelections('roocode', DONE);

const roocodePath = path.join(testDir, 'ROOCODE.md');
const existingContent =
'# My RooCode Instructions\nCustom instructions here';
await fs.writeFile(roocodePath, existingContent);

await initCommand.execute(testDir);

const updatedContent = await fs.readFile(roocodePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('Custom instructions here');
});
it('should update existing COSTRICT.md with markers', async () => {
queueSelections('costrict', DONE);

Expand Down
Loading