Skip to content

Commit 3677e01

Browse files
jasonwang82Jason Wang
andauthored
feat: add CodeBuddy Code support to configuration and documentation (#217)
Co-authored-by: Jason Wang <[email protected]>
1 parent 3ddf258 commit 3677e01

File tree

8 files changed

+213
-4
lines changed

8 files changed

+213
-4
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
9191
| Tool | Commands |
9292
|------|----------|
9393
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
94+
| **CodeBuddy** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) |
9495
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
9596
| **Cline** | Rules in `.clinerules/` directory (`.clinerules/openspec-*.md`) |
9697
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
@@ -102,6 +103,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
102103
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
103104
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
104105

106+
105107
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`.
106108

107109
#### AGENTS.md Compatible
@@ -140,15 +142,15 @@ openspec init
140142
```
141143

142144
**What happens during initialization:**
143-
- 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
145+
- 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
144146
- OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
145147
- A new `openspec/` directory structure is created in your project
146148

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

153155
### Create Your First Change
154156

@@ -215,7 +217,7 @@ Or run the command yourself in terminal:
215217
$ openspec archive add-profile-filters --yes # Archive the completed change without prompts
216218
```
217219

218-
**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".
220+
**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".
219221

220222
## Command Reference
221223

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

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

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: 'CodeBuddy', value: 'codebuddy', available: true, successLabel: 'CodeBuddy' },
2324
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
2425
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
2526
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import path from 'path';
2+
import { ToolConfigurator } from './base.js';
3+
import { FileSystemUtils } from '../../utils/file-system.js';
4+
import { TemplateManager } from '../templates/index.js';
5+
import { OPENSPEC_MARKERS } from '../config.js';
6+
7+
export class CodeBuddyConfigurator implements ToolConfigurator {
8+
name = 'CodeBuddy';
9+
configFileName = 'CodeBuddy.md';
10+
isAvailable = true;
11+
12+
async configure(projectPath: string, openspecDir: string): Promise<void> {
13+
const filePath = path.join(projectPath, this.configFileName);
14+
const content = TemplateManager.getClaudeTemplate();
15+
16+
await FileSystemUtils.updateFileWithMarkers(
17+
filePath,
18+
content,
19+
OPENSPEC_MARKERS.start,
20+
OPENSPEC_MARKERS.end
21+
);
22+
}
23+
}
24+

src/core/configurators/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ToolConfigurator } from './base.js';
22
import { ClaudeConfigurator } from './claude.js';
33
import { ClineConfigurator } from './cline.js';
4+
import { CodeBuddyConfigurator } from './codebuddy.js';
45
import { AgentsStandardConfigurator } from './agents.js';
56

67
export class ToolRegistry {
@@ -9,10 +10,12 @@ export class ToolRegistry {
910
static {
1011
const claudeConfigurator = new ClaudeConfigurator();
1112
const clineConfigurator = new ClineConfigurator();
13+
const codeBuddyConfigurator = new CodeBuddyConfigurator();
1214
const agentsConfigurator = new AgentsStandardConfigurator();
1315
// Register with the ID that matches the checkbox value
1416
this.tools.set('claude', claudeConfigurator);
1517
this.tools.set('cline', clineConfigurator);
18+
this.tools.set('codebuddy', codeBuddyConfigurator);
1619
this.tools.set('agents', agentsConfigurator);
1720
}
1821

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { SlashCommandConfigurator } from './base.js';
2+
import { SlashCommandId } from '../../templates/index.js';
3+
4+
const FILE_PATHS: Record<SlashCommandId, string> = {
5+
proposal: '.codebuddy/commands/openspec/proposal.md',
6+
apply: '.codebuddy/commands/openspec/apply.md',
7+
archive: '.codebuddy/commands/openspec/archive.md'
8+
};
9+
10+
const FRONTMATTER: Record<SlashCommandId, string> = {
11+
proposal: `---
12+
name: OpenSpec: Proposal
13+
description: Scaffold a new OpenSpec change and validate strictly.
14+
category: OpenSpec
15+
tags: [openspec, change]
16+
---`,
17+
apply: `---
18+
name: OpenSpec: Apply
19+
description: Implement an approved OpenSpec change and keep tasks in sync.
20+
category: OpenSpec
21+
tags: [openspec, apply]
22+
---`,
23+
archive: `---
24+
name: OpenSpec: Archive
25+
description: Archive a deployed OpenSpec change and update specs.
26+
category: OpenSpec
27+
tags: [openspec, archive]
28+
---`
29+
};
30+
31+
export class CodeBuddySlashCommandConfigurator extends SlashCommandConfigurator {
32+
readonly toolId = 'codebuddy';
33+
readonly isAvailable = true;
34+
35+
protected getRelativePath(id: SlashCommandId): string {
36+
return FILE_PATHS[id];
37+
}
38+
39+
protected getFrontmatter(id: SlashCommandId): string {
40+
return FRONTMATTER[id];
41+
}
42+
}
43+

src/core/configurators/slash/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SlashCommandConfigurator } from './base.js';
22
import { ClaudeSlashCommandConfigurator } from './claude.js';
3+
import { CodeBuddySlashCommandConfigurator } from './codebuddy.js';
34
import { CursorSlashCommandConfigurator } from './cursor.js';
45
import { WindsurfSlashCommandConfigurator } from './windsurf.js';
56
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
@@ -17,6 +18,7 @@ export class SlashCommandRegistry {
1718

1819
static {
1920
const claude = new ClaudeSlashCommandConfigurator();
21+
const codeBuddy = new CodeBuddySlashCommandConfigurator();
2022
const cursor = new CursorSlashCommandConfigurator();
2123
const windsurf = new WindsurfSlashCommandConfigurator();
2224
const kilocode = new KiloCodeSlashCommandConfigurator();
@@ -30,6 +32,7 @@ export class SlashCommandRegistry {
3032
const crush = new CrushSlashCommandConfigurator();
3133

3234
this.configurators.set(claude.toolId, claude);
35+
this.configurators.set(codeBuddy.toolId, codeBuddy);
3336
this.configurators.set(cursor.toolId, cursor);
3437
this.configurators.set(windsurf.toolId, windsurf);
3538
this.configurators.set(kilocode.toolId, kilocode);

test/core/init.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,61 @@ describe('InitCommand', () => {
810810
expect(auggieChoice.configured).toBe(true);
811811
});
812812

813+
it('should create CodeBuddy slash command files with templates', async () => {
814+
queueSelections('codebuddy', DONE);
815+
816+
await initCommand.execute(testDir);
817+
818+
const codeBuddyProposal = path.join(
819+
testDir,
820+
'.codebuddy/commands/openspec/proposal.md'
821+
);
822+
const codeBuddyApply = path.join(
823+
testDir,
824+
'.codebuddy/commands/openspec/apply.md'
825+
);
826+
const codeBuddyArchive = path.join(
827+
testDir,
828+
'.codebuddy/commands/openspec/archive.md'
829+
);
830+
831+
expect(await fileExists(codeBuddyProposal)).toBe(true);
832+
expect(await fileExists(codeBuddyApply)).toBe(true);
833+
expect(await fileExists(codeBuddyArchive)).toBe(true);
834+
835+
const proposalContent = await fs.readFile(codeBuddyProposal, 'utf-8');
836+
expect(proposalContent).toContain('---');
837+
expect(proposalContent).toContain('name: OpenSpec: Proposal');
838+
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
839+
expect(proposalContent).toContain('category: OpenSpec');
840+
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
841+
expect(proposalContent).toContain('**Guardrails**');
842+
843+
const applyContent = await fs.readFile(codeBuddyApply, 'utf-8');
844+
expect(applyContent).toContain('---');
845+
expect(applyContent).toContain('name: OpenSpec: Apply');
846+
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
847+
expect(applyContent).toContain('Work through tasks sequentially');
848+
849+
const archiveContent = await fs.readFile(codeBuddyArchive, 'utf-8');
850+
expect(archiveContent).toContain('---');
851+
expect(archiveContent).toContain('name: OpenSpec: Archive');
852+
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
853+
expect(archiveContent).toContain('openspec archive <id> --yes');
854+
});
855+
856+
it('should mark CodeBuddy as already configured during extend mode', async () => {
857+
queueSelections('codebuddy', DONE, 'codebuddy', DONE);
858+
await initCommand.execute(testDir);
859+
await initCommand.execute(testDir);
860+
861+
const secondRunArgs = mockPrompt.mock.calls[1][0];
862+
const codeBuddyChoice = secondRunArgs.choices.find(
863+
(choice: any) => choice.value === 'codebuddy'
864+
);
865+
expect(codeBuddyChoice.configured).toBe(true);
866+
});
867+
813868
it('should create Crush slash command files with templates', async () => {
814869
queueSelections('crush', DONE);
815870

test/core/update.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,84 @@ Old body
663663
await expect(FileSystemUtils.fileExists(auggieArchive)).resolves.toBe(false);
664664
});
665665

666+
it('should refresh existing CodeBuddy slash command files', async () => {
667+
const codeBuddyPath = path.join(
668+
testDir,
669+
'.codebuddy/commands/openspec/proposal.md'
670+
);
671+
await fs.mkdir(path.dirname(codeBuddyPath), { recursive: true });
672+
const initialContent = `---
673+
name: OpenSpec: Proposal
674+
description: Old description
675+
category: OpenSpec
676+
tags: [openspec, change]
677+
---
678+
<!-- OPENSPEC:START -->
679+
Old slash content
680+
<!-- OPENSPEC:END -->`;
681+
await fs.writeFile(codeBuddyPath, initialContent);
682+
683+
const consoleSpy = vi.spyOn(console, 'log');
684+
685+
await updateCommand.execute(testDir);
686+
687+
const updated = await fs.readFile(codeBuddyPath, 'utf-8');
688+
expect(updated).toContain('name: OpenSpec: Proposal');
689+
expect(updated).toContain('**Guardrails**');
690+
expect(updated).toContain(
691+
'Validate with `openspec validate <id> --strict`'
692+
);
693+
expect(updated).not.toContain('Old slash content');
694+
695+
const [logMessage] = consoleSpy.mock.calls[0];
696+
expect(logMessage).toContain(
697+
'Updated OpenSpec instructions (openspec/AGENTS.md'
698+
);
699+
expect(logMessage).toContain('AGENTS.md (created)');
700+
expect(logMessage).toContain(
701+
'Updated slash commands: .codebuddy/commands/openspec/proposal.md'
702+
);
703+
704+
consoleSpy.mockRestore();
705+
});
706+
707+
it('should not create missing CodeBuddy slash command files on update', async () => {
708+
const codeBuddyApply = path.join(
709+
testDir,
710+
'.codebuddy/commands/openspec/apply.md'
711+
);
712+
713+
// Only create apply; leave proposal and archive missing
714+
await fs.mkdir(path.dirname(codeBuddyApply), { recursive: true });
715+
await fs.writeFile(
716+
codeBuddyApply,
717+
`---
718+
name: OpenSpec: Apply
719+
description: Old description
720+
category: OpenSpec
721+
tags: [openspec, apply]
722+
---
723+
<!-- OPENSPEC:START -->
724+
Old body
725+
<!-- OPENSPEC:END -->`
726+
);
727+
728+
await updateCommand.execute(testDir);
729+
730+
const codeBuddyProposal = path.join(
731+
testDir,
732+
'.codebuddy/commands/openspec/proposal.md'
733+
);
734+
const codeBuddyArchive = path.join(
735+
testDir,
736+
'.codebuddy/commands/openspec/archive.md'
737+
);
738+
739+
// Confirm they weren't created by update
740+
await expect(FileSystemUtils.fileExists(codeBuddyProposal)).resolves.toBe(false);
741+
await expect(FileSystemUtils.fileExists(codeBuddyArchive)).resolves.toBe(false);
742+
});
743+
666744
it('should refresh existing Crush slash command files', async () => {
667745
const crushPath = path.join(
668746
testDir,

0 commit comments

Comments
 (0)