Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| Tool | Commands |
|------|----------|
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
| **CodeBuddy** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) |
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Cline** | Rules in `.clinerules/` directory (`.clinerules/openspec-*.md`) |
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
Expand All @@ -102,6 +103,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |


Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.

#### AGENTS.md Compatible
Expand Down Expand Up @@ -140,15 +142,15 @@ openspec init
```

**What happens during initialization:**
- You'll be prompted to pick any natively supported AI tools (Claude Code, Cursor, OpenCode, etc.); other assistants always rely on the shared `AGENTS.md` stub
- You'll be prompted to pick any natively supported AI tools (Claude Code, CodeBuddy, Cursor, OpenCode, etc.); other assistants always rely on the shared `AGENTS.md` stub
- OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
- A new `openspec/` directory structure is created in your project

**After setup:**
- Primary AI tools can trigger `/openspec` workflows without additional configuration
- Run `openspec list` to verify the setup and view any active changes
- If your coding assistant doesn't surface the new slash commands right away, restart it. Slash commands are loaded at startup,
so a fresh launch ensures they appear.
so a fresh launch ensures they appear

### Create Your First Change

Expand Down Expand Up @@ -215,7 +217,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, Cursor, Codex) 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) 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 Expand Up @@ -327,7 +329,7 @@ Without specs, AI coding assistants generate code from vague prompts, often miss
1. **Initialize OpenSpec** – Run `openspec init` in your repo.
2. **Start with new features** – Ask your AI to capture upcoming work as change proposals.
3. **Grow incrementally** – Each change archives into living specs that document your system.
4. **Stay flexible** – Different teammates can use Claude Code, Cursor, or any AGENTS.md-compatible tool while sharing the same specs.
4. **Stay flexible** – Different teammates can use Claude Code, CodeBuddy, Cursor, or any AGENTS.md-compatible tool while sharing the same specs.

Run `openspec update` whenever someone switches tools so your agents pick up the latest instructions and slash-command bindings.

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: 'CodeBuddy', value: 'codebuddy', available: true, successLabel: 'CodeBuddy' },
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
Expand Down
24 changes: 24 additions & 0 deletions src/core/configurators/codebuddy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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 CodeBuddyConfigurator implements ToolConfigurator {
name = 'CodeBuddy';
configFileName = 'CodeBuddy.md';
isAvailable = true;

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

await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
}
}

3 changes: 3 additions & 0 deletions src/core/configurators/registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ToolConfigurator } from './base.js';
import { ClaudeConfigurator } from './claude.js';
import { ClineConfigurator } from './cline.js';
import { CodeBuddyConfigurator } from './codebuddy.js';
import { AgentsStandardConfigurator } from './agents.js';

export class ToolRegistry {
Expand All @@ -9,10 +10,12 @@ export class ToolRegistry {
static {
const claudeConfigurator = new ClaudeConfigurator();
const clineConfigurator = new ClineConfigurator();
const codeBuddyConfigurator = new CodeBuddyConfigurator();
const agentsConfigurator = new AgentsStandardConfigurator();
// Register with the ID that matches the checkbox value
this.tools.set('claude', claudeConfigurator);
this.tools.set('cline', clineConfigurator);
this.tools.set('codebuddy', codeBuddyConfigurator);
this.tools.set('agents', agentsConfigurator);
}

Expand Down
43 changes: 43 additions & 0 deletions src/core/configurators/slash/codebuddy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';

const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.codebuddy/commands/openspec/proposal.md',
apply: '.codebuddy/commands/openspec/apply.md',
archive: '.codebuddy/commands/openspec/archive.md'
};

const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
name: OpenSpec: Proposal
description: Scaffold a new OpenSpec change and validate strictly.
category: OpenSpec
tags: [openspec, change]
---`,
apply: `---
name: OpenSpec: Apply
description: Implement an approved OpenSpec change and keep tasks in sync.
category: OpenSpec
tags: [openspec, apply]
---`,
archive: `---
name: OpenSpec: Archive
description: Archive a deployed OpenSpec change and update specs.
category: OpenSpec
tags: [openspec, archive]
---`
};

export class CodeBuddySlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'codebuddy';
readonly isAvailable = true;

protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}

protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}

3 changes: 3 additions & 0 deletions src/core/configurators/slash/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SlashCommandConfigurator } from './base.js';
import { ClaudeSlashCommandConfigurator } from './claude.js';
import { CodeBuddySlashCommandConfigurator } from './codebuddy.js';
import { CursorSlashCommandConfigurator } from './cursor.js';
import { WindsurfSlashCommandConfigurator } from './windsurf.js';
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
Expand All @@ -17,6 +18,7 @@ export class SlashCommandRegistry {

static {
const claude = new ClaudeSlashCommandConfigurator();
const codeBuddy = new CodeBuddySlashCommandConfigurator();
const cursor = new CursorSlashCommandConfigurator();
const windsurf = new WindsurfSlashCommandConfigurator();
const kilocode = new KiloCodeSlashCommandConfigurator();
Expand All @@ -30,6 +32,7 @@ export class SlashCommandRegistry {
const crush = new CrushSlashCommandConfigurator();

this.configurators.set(claude.toolId, claude);
this.configurators.set(codeBuddy.toolId, codeBuddy);
this.configurators.set(cursor.toolId, cursor);
this.configurators.set(windsurf.toolId, windsurf);
this.configurators.set(kilocode.toolId, kilocode);
Expand Down
55 changes: 55 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,61 @@ describe('InitCommand', () => {
expect(auggieChoice.configured).toBe(true);
});

it('should create CodeBuddy slash command files with templates', async () => {
queueSelections('codebuddy', DONE);

await initCommand.execute(testDir);

const codeBuddyProposal = path.join(
testDir,
'.codebuddy/commands/openspec/proposal.md'
);
const codeBuddyApply = path.join(
testDir,
'.codebuddy/commands/openspec/apply.md'
);
const codeBuddyArchive = path.join(
testDir,
'.codebuddy/commands/openspec/archive.md'
);

expect(await fileExists(codeBuddyProposal)).toBe(true);
expect(await fileExists(codeBuddyApply)).toBe(true);
expect(await fileExists(codeBuddyArchive)).toBe(true);

const proposalContent = await fs.readFile(codeBuddyProposal, 'utf-8');
expect(proposalContent).toContain('---');
expect(proposalContent).toContain('name: OpenSpec: Proposal');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('category: OpenSpec');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');

const applyContent = await fs.readFile(codeBuddyApply, 'utf-8');
expect(applyContent).toContain('---');
expect(applyContent).toContain('name: OpenSpec: Apply');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('Work through tasks sequentially');

const archiveContent = await fs.readFile(codeBuddyArchive, 'utf-8');
expect(archiveContent).toContain('---');
expect(archiveContent).toContain('name: OpenSpec: Archive');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('openspec archive <id> --yes');
});

it('should mark CodeBuddy as already configured during extend mode', async () => {
queueSelections('codebuddy', DONE, 'codebuddy', DONE);
await initCommand.execute(testDir);
await initCommand.execute(testDir);

const secondRunArgs = mockPrompt.mock.calls[1][0];
const codeBuddyChoice = secondRunArgs.choices.find(
(choice: any) => choice.value === 'codebuddy'
);
expect(codeBuddyChoice.configured).toBe(true);
});

it('should create Crush slash command files with templates', async () => {
queueSelections('crush', DONE);

Expand Down
78 changes: 78 additions & 0 deletions test/core/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,84 @@ Old body
await expect(FileSystemUtils.fileExists(auggieArchive)).resolves.toBe(false);
});

it('should refresh existing CodeBuddy slash command files', async () => {
const codeBuddyPath = path.join(
testDir,
'.codebuddy/commands/openspec/proposal.md'
);
await fs.mkdir(path.dirname(codeBuddyPath), { recursive: true });
const initialContent = `---
name: OpenSpec: Proposal
description: Old description
category: OpenSpec
tags: [openspec, change]
---
<!-- OPENSPEC:START -->
Old slash content
<!-- OPENSPEC:END -->`;
await fs.writeFile(codeBuddyPath, initialContent);

const consoleSpy = vi.spyOn(console, 'log');

await updateCommand.execute(testDir);

const updated = await fs.readFile(codeBuddyPath, 'utf-8');
expect(updated).toContain('name: OpenSpec: Proposal');
expect(updated).toContain('**Guardrails**');
expect(updated).toContain(
'Validate with `openspec validate <id> --strict`'
);
expect(updated).not.toContain('Old slash content');

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: .codebuddy/commands/openspec/proposal.md'
);

consoleSpy.mockRestore();
});

it('should not create missing CodeBuddy slash command files on update', async () => {
const codeBuddyApply = path.join(
testDir,
'.codebuddy/commands/openspec/apply.md'
);

// Only create apply; leave proposal and archive missing
await fs.mkdir(path.dirname(codeBuddyApply), { recursive: true });
await fs.writeFile(
codeBuddyApply,
`---
name: OpenSpec: Apply
description: Old description
category: OpenSpec
tags: [openspec, apply]
---
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`
);

await updateCommand.execute(testDir);

const codeBuddyProposal = path.join(
testDir,
'.codebuddy/commands/openspec/proposal.md'
);
const codeBuddyArchive = path.join(
testDir,
'.codebuddy/commands/openspec/archive.md'
);

// Confirm they weren't created by update
await expect(FileSystemUtils.fileExists(codeBuddyProposal)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(codeBuddyArchive)).resolves.toBe(false);
});

it('should refresh existing Crush slash command files', async () => {
const crushPath = path.join(
testDir,
Expand Down
Loading