Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

### Minor Changes

- Add Continue slash command support so `openspec init` can generate `.continue/prompts/openspec-*.prompt` files with MARKDOWN frontmatter and `$ARGUMENTS` placeholder, and refresh them on `openspec update`.

- Add Antigravity slash command support so `openspec init` can generate `.agent/workflows/openspec-*.md` files with description-only frontmatter and `openspec update` refreshes existing workflows alongside Windsurf.

## 0.15.0
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
| **Continue** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.continue/prompts/`) |
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
Expand Down
7 changes: 7 additions & 0 deletions openspec/specs/cli-init/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
- **THEN** create or update `CODEBUDDY.md` in the project root directory (not inside openspec/)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`


#### Scenario: Configuring Cline

- **WHEN** Cline is selected
Expand Down Expand Up @@ -204,6 +205,12 @@ The init command SHALL generate slash command files for supported editors using
- **AND** populate each file from shared templates so command text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage

#### Scenario: Generating slash commands for Continue
- **WHEN** the user selects Continue during initialization
- **THEN** create `.continue/prompts/openspec-proposal.prompt`, `.continue/prompts/openspec-apply.prompt`, and `.continue/prompts/openspec-archive.prompt`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage

#### Scenario: Generating slash commands for Factory Droid
- **WHEN** the user selects Factory Droid during initialization
- **THEN** create `.factory/commands/openspec-proposal.md`, `.factory/commands/openspec-apply.md`, and `.factory/commands/openspec-archive.md`
Expand Down
5 changes: 5 additions & 0 deletions openspec/specs/cli-update/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** ensure templates include instructions for the relevant workflow stage

#### Scenario: Updating slash commands for Continue
- **WHEN** `.continue/prompts/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage

#### Scenario: Updating slash commands for Crush
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
- **THEN** refresh each file using shared templates
Expand Down
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)' },
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
Expand Down
42 changes: 42 additions & 0 deletions src/core/configurators/slash/continue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId, TemplateManager } from '../../templates/index.js';

const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.continue/prompts/openspec-proposal.prompt',
apply: '.continue/prompts/openspec-apply.prompt',
archive: '.continue/prompts/openspec-archive.prompt'
};

/*
* Continue .prompt format:
* name: commandName
* description: description
* ---
* Body...
*
* Note: We use 'openspec-proposal' as the name so the command becomes /openspec-proposal
*/
const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `name: openspec-proposal
description: Scaffold a new OpenSpec change and validate strictly.
---`,
apply: `name: openspec-apply
description: Implement an approved OpenSpec change and keep tasks in sync.
---`,
archive: `name: openspec-archive
description: Archive a deployed OpenSpec change and update specs.
---`
};

export class ContinueSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'continue';
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
Expand Up @@ -19,6 +19,7 @@ import { QwenSlashCommandConfigurator } from './qwen.js';
import { RooCodeSlashCommandConfigurator } from './roocode.js';
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
import { IflowSlashCommandConfigurator } from './iflow.js';
import { ContinueSlashCommandConfigurator } from './continue.js';

export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
Expand All @@ -44,6 +45,7 @@ export class SlashCommandRegistry {
const roocode = new RooCodeSlashCommandConfigurator();
const antigravity = new AntigravitySlashCommandConfigurator();
const iflow = new IflowSlashCommandConfigurator();
const continueTool = new ContinueSlashCommandConfigurator();

this.configurators.set(claude.toolId, claude);
this.configurators.set(codeBuddy.toolId, codeBuddy);
Expand All @@ -65,6 +67,7 @@ export class SlashCommandRegistry {
this.configurators.set(roocode.toolId, roocode);
this.configurators.set(antigravity.toolId, antigravity);
this.configurators.set(iflow.toolId, iflow);
this.configurators.set(continueTool.toolId, continueTool);
}

static register(configurator: SlashCommandConfigurator): void {
Expand Down
53 changes: 53 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,59 @@ describe('InitCommand', () => {
expect(codeBuddyChoice.configured).toBe(true);
});

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

await initCommand.execute(testDir);

const continueProposal = path.join(
testDir,
'.continue/prompts/openspec-proposal.prompt'
);
const continueApply = path.join(
testDir,
'.continue/prompts/openspec-apply.prompt'
);
const continueArchive = path.join(
testDir,
'.continue/prompts/openspec-archive.prompt'
);

expect(await fileExists(continueProposal)).toBe(true);
expect(await fileExists(continueApply)).toBe(true);
expect(await fileExists(continueArchive)).toBe(true);

const proposalContent = await fs.readFile(continueProposal, 'utf-8');
expect(proposalContent).toContain('name: openspec-proposal');
expect(proposalContent).toContain('---');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');

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

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

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

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


it('should create CODEBUDDY.md when CodeBuddy is selected', async () => {
queueSelections('codebuddy', DONE);

Expand Down
71 changes: 70 additions & 1 deletion test/core/update.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { UpdateCommand } from '../../src/core/update.js';
import { UpdateCommand } from '../../src/core/update.js';
import { FileSystemUtils } from '../../src/utils/file-system.js';
import { ToolRegistry } from '../../src/core/configurators/registry.js';
import path from 'path';
Expand Down Expand Up @@ -368,6 +368,75 @@ Old body
consoleSpy.mockRestore();
});

it('should refresh existing Continue prompt files', async () => {
const continuePath = path.join(
testDir,
'.continue/prompts/openspec-apply.prompt'
);
await fs.mkdir(path.dirname(continuePath), { recursive: true });
const initialContent = `name: openspec-apply
description: Old description
---
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`;
await fs.writeFile(continuePath, initialContent);

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

await updateCommand.execute(testDir);

const updated = await fs.readFile(continuePath, 'utf-8');
expect(updated).toContain('name: openspec-apply');
expect(updated).toContain('Work through tasks sequentially');
expect(updated).not.toContain('Old body');

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: .continue/prompts/openspec-apply.prompt'
);

consoleSpy.mockRestore();
});

it('should not create missing Continue prompt files on update', async () => {
const continueApply = path.join(
testDir,
'.continue/prompts/openspec-apply.prompt'
);

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

await updateCommand.execute(testDir);

const continueProposal = path.join(
testDir,
'.continue/prompts/openspec-proposal.prompt'
);
const continueArchive = path.join(
testDir,
'.continue/prompts/openspec-archive.prompt'
);

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

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