Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale ac659f7993 feat: add CodeRabbit AI assistant support
Add CodeRabbit configuration to enable AI-powered code reviews with customized review settings, path-specific instructions, and chat configuration.
2025-10-21 13:18:31 +11:00
scala63andningchen ece61a6d68 feat: add Cline support (#213)
Co-authored-by: ningchen <n.ning.c.chen@oracle.com>
2025-10-21 12:08:21 +11:00
Tabish Bidiwale ecddffc22e fix: improve delta spec validation with case-insensitive headers and empty section detection (#191)
This commit enhances the validation logic for delta specs:
- Delta section headers are now parsed case-insensitively (e.g., "Added Requirements" and "ADDED Requirements" both work)
- Empty delta sections now produce clear error messages guiding users to add requirement entries
- Specs with no delta headers at all now receive specific error messages
- Added test coverage for case-insensitive delta header parsing
2025-10-19 23:56:53 +11:00
14 changed files with 297 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
# Minimal configuration for getting started
language: "en-US"
reviews:
profile: "chill"
high_level_summary: true
auto_review:
enabled: true
drafts: false
base_branches:
- ".*"
+1
View File
@@ -92,6 +92,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|------|----------|
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
| **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/`) |
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
@@ -0,0 +1,15 @@
## Why
Add support for Cline (VS Code extension) in OpenSpec to enable developers to use Cline's AI-powered coding capabilities for spec-driven development workflows.
## What Changes
- Add Cline slash command configurator for proposal, apply, and archive operations
- Add Cline root CLINE.md configurator for project-level instructions
- Add Cline template exports
- Update tool and slash command registries to include Cline
- Add comprehensive test coverage
- **BREAKING**: None - this is additive functionality
## Impact
- Affected specs: cli-init (new tool option)
- Affected code: src/core/configurators/slash/cline.ts, src/core/configurators/cline.ts, registry files
- New files: .clinerules/openspec-*.md, CLINE.md
@@ -0,0 +1,27 @@
## ADDED Requirements
### Requirement: Cline Tool Support
The system SHALL provide Cline (VS Code extension) as a supported tool option during OpenSpec initialization.
#### Scenario: Initialize project with Cline support
- **WHEN** user runs `openspec init --tools cline`
- **THEN** Cline-specific rule files are configured in `.clinerules/`
- **AND** CLINE.md root file includes OpenSpec workflow instructions
- **AND** Cline is registered as available configurator
#### Scenario: Cline proposal rule generation
- **WHEN** Cline rules are configured
- **THEN** `.clinerules/openspec-proposal.md` contains proposal workflow with guardrails
- **AND** Includes Cline-specific Markdown heading frontmatter
- **AND** Follows established slash command template pattern
#### Scenario: Cline apply and archive rules
- **WHEN** Cline rules are configured
- **THEN** `.clinerules/openspec-apply.md` contains implementation workflow
- **AND** `.clinerules/openspec-archive.md` contains archiving workflow
- **AND** Both commands include appropriate headers and references
#### Scenario: Cline root instructions
- **WHEN** Cline is selected during initialization
- **THEN** CLINE.md is created at project root
- **AND** Contains OpenSpec markers for managed content
- **AND** References `@/openspec/AGENTS.md` for workflow instructions
@@ -0,0 +1,19 @@
## 1. Implementation
- [x] 1.1 Create ClineSlashCommandConfigurator class in src/core/configurators/slash/cline.ts
- [x] 1.2 Create ClineConfigurator class in src/core/configurators/cline.ts
- [x] 1.3 Create cline-template.ts for template exports
- [x] 1.4 Define file paths for Cline rules (.clinerules/)
- [x] 1.5 Create Cline-specific frontmatter (Markdown heading format)
- [x] 1.6 Register Cline in slash/registry.ts
- [x] 1.7 Register Cline in configurators/registry.ts
- [x] 1.8 Add Cline to AI_TOOLS in config.ts
- [x] 1.9 Add getClineTemplate() to templates/index.ts
- [x] 1.10 Update README with Cline documentation
## 2. Testing
- [x] 2.1 Add init tests for CLINE.md creation and updates
- [x] 2.2 Add init tests for .clinerules/ file creation
- [x] 2.3 Add update tests for CLINE.md updates
- [x] 2.4 Add update tests for .clinerules/ file refreshes
- [x] 2.5 Test integration with openspec init --tools cline
- [x] 2.6 Verify all 225 tests pass
+1
View File
@@ -19,6 +19,7 @@ export interface AIToolOption {
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: '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' },
+23
View File
@@ -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 ClineConfigurator implements ToolConfigurator {
name = 'Cline';
configFileName = 'CLINE.md';
isAvailable = true;
async configure(projectPath: string, openspecDir: string): Promise<void> {
const filePath = path.join(projectPath, this.configFileName);
const content = TemplateManager.getClineTemplate();
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
}
}
+3
View File
@@ -1,5 +1,6 @@
import { ToolConfigurator } from './base.js';
import { ClaudeConfigurator } from './claude.js';
import { ClineConfigurator } from './cline.js';
import { AgentsStandardConfigurator } from './agents.js';
export class ToolRegistry {
@@ -7,9 +8,11 @@ export class ToolRegistry {
static {
const claudeConfigurator = new ClaudeConfigurator();
const clineConfigurator = new ClineConfigurator();
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('agents', agentsConfigurator);
}
+27
View File
@@ -0,0 +1,27 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.clinerules/openspec-proposal.md',
apply: '.clinerules/openspec-apply.md',
archive: '.clinerules/openspec-archive.md'
};
export class ClineSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'cline';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return 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}`;
}
}
+3
View File
@@ -9,6 +9,7 @@ import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
import { FactorySlashCommandConfigurator } from './factory.js';
import { AuggieSlashCommandConfigurator } from './auggie.js';
import { ClineSlashCommandConfigurator } from './cline.js';
import { CrushSlashCommandConfigurator } from './crush.js';
export class SlashCommandRegistry {
@@ -25,6 +26,7 @@ export class SlashCommandRegistry {
const amazonQ = new AmazonQSlashCommandConfigurator();
const factory = new FactorySlashCommandConfigurator();
const auggie = new AuggieSlashCommandConfigurator();
const cline = new ClineSlashCommandConfigurator();
const crush = new CrushSlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
@@ -37,6 +39,7 @@ export class SlashCommandRegistry {
this.configurators.set(amazonQ.toolId, amazonQ);
this.configurators.set(factory.toolId, factory);
this.configurators.set(auggie.toolId, auggie);
this.configurators.set(cline.toolId, cline);
this.configurators.set(crush.toolId, crush);
}
+1
View File
@@ -0,0 +1 @@
export { agentsRootStubTemplate as clineTemplate } from './agents-root-stub.js';
+5
View File
@@ -1,6 +1,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 { agentsRootStubTemplate } from './agents-root-stub.js';
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
@@ -27,6 +28,10 @@ export class TemplateManager {
return claudeTemplate;
}
static getClineTemplate(): string {
return clineTemplate;
}
static getAgentsStandardTemplate(): string {
return agentsRootStubTemplate;
}
+72
View File
@@ -136,6 +136,39 @@ describe('InitCommand', () => {
expect(updatedContent).toContain('Custom instructions here');
});
it('should create CLINE.md when Cline is selected', async () => {
queueSelections('cline', DONE);
await initCommand.execute(testDir);
const clinePath = path.join(testDir, 'CLINE.md');
expect(await fileExists(clinePath)).toBe(true);
const content = await fs.readFile(clinePath, '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 CLINE.md with markers', async () => {
queueSelections('cline', DONE);
const clinePath = path.join(testDir, 'CLINE.md');
const existingContent =
'# My Cline Rules\nCustom Cline instructions here';
await fs.writeFile(clinePath, existingContent);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(clinePath, '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 Cline instructions here');
});
it('should create Windsurf workflows when Windsurf is selected', async () => {
queueSelections('windsurf', DONE);
@@ -316,6 +349,45 @@ describe('InitCommand', () => {
expect(archiveContent).toContain('openspec list --specs');
});
it('should create Cline rule files with templates', async () => {
queueSelections('cline', DONE);
await initCommand.execute(testDir);
const clineProposal = path.join(
testDir,
'.clinerules/openspec-proposal.md'
);
const clineApply = path.join(
testDir,
'.clinerules/openspec-apply.md'
);
const clineArchive = path.join(
testDir,
'.clinerules/openspec-archive.md'
);
expect(await fileExists(clineProposal)).toBe(true);
expect(await fileExists(clineApply)).toBe(true);
expect(await fileExists(clineArchive)).toBe(true);
const proposalContent = await fs.readFile(clineProposal, 'utf-8');
expect(proposalContent).toContain('# OpenSpec: Proposal');
expect(proposalContent).toContain('Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
const applyContent = await fs.readFile(clineApply, 'utf-8');
expect(applyContent).toContain('# OpenSpec: Apply');
expect(applyContent).toContain('Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(clineArchive, 'utf-8');
expect(archiveContent).toContain('# OpenSpec: Archive');
expect(archiveContent).toContain('Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('openspec archive <id>');
});
it('should create Factory slash command files with templates', async () => {
queueSelections('factory', DONE);
+89
View File
@@ -126,6 +126,95 @@ Old slash content
expect(fileExists).toBe(false);
});
it('should update only existing CLINE.md file', async () => {
// Create CLINE.md file with initial content
const clinePath = path.join(testDir, 'CLINE.md');
const initialContent = `# Cline Rules
Some existing Cline rules here.
<!-- OPENSPEC:START -->
Old OpenSpec content
<!-- OPENSPEC:END -->
More rules after.`;
await fs.writeFile(clinePath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
// Execute update command
await updateCommand.execute(testDir);
// Check that CLINE.md was updated
const updatedContent = await fs.readFile(clinePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('Some existing Cline rules here');
expect(updatedContent).toContain('More rules after');
// Check console output
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 AI tool files: CLINE.md');
consoleSpy.mockRestore();
});
it('should not create CLINE.md if it does not exist', async () => {
// Ensure CLINE.md does not exist
const clinePath = path.join(testDir, 'CLINE.md');
// Execute update command
await updateCommand.execute(testDir);
// Check that CLINE.md was not created
const fileExists = await FileSystemUtils.fileExists(clinePath);
expect(fileExists).toBe(false);
});
it('should refresh existing Cline rule files', async () => {
const proposalPath = path.join(
testDir,
'.clinerules/openspec-proposal.md'
);
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
const initialContent = `# OpenSpec: Proposal
Scaffold a new OpenSpec change and validate strictly.
<!-- OPENSPEC:START -->
Old slash content
<!-- OPENSPEC:END -->`;
await fs.writeFile(proposalPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(proposalPath, 'utf-8');
expect(updated).toContain('# 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: .clinerules/openspec-proposal.md'
);
consoleSpy.mockRestore();
});
it('should refresh existing Cursor slash command files', async () => {
const cursorPath = path.join(testDir, '.cursor/commands/openspec-apply.md');
await fs.mkdir(path.dirname(cursorPath), { recursive: true });