Skip to content
Open
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
3 changes: 2 additions & 1 deletion docs/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.md` |
| CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/<id>.md` |
| Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | `$CODEX_HOME/prompts/opsx-<id>.md`\* |
| Devin Desktop (`devin`) | `.devin/skills/openspec-*/SKILL.md` | `.devin/workflows/opsx-<id>.md` |
Comment thread
mehdishahdoost marked this conversation as resolved.
| ForgeCode (`forgecode`) | `.forge/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| Continue (`continue`) | `.continue/skills/openspec-*/SKILL.md` | `.continue/prompts/opsx-<id>.prompt` |
| CoStrict (`costrict`) | `.cospec/skills/openspec-*/SKILL.md` | `.cospec/openspec/commands/opsx-<id>.md` |
Expand Down Expand Up @@ -75,7 +76,7 @@ openspec init --tools none
openspec init --profile core
```

**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `vibe`, `windsurf`
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `vibe`, `windsurf`

## Workflow-Dependent Installation

Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-devin-desktop-support/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-04
33 changes: 33 additions & 0 deletions openspec/changes/add-devin-desktop-support/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Why

- Windsurf has been rebranded to **Devin Desktop**, the new flagship AI coding assistant from Cognition. Users who previously used Windsurf are now transitioning to Devin Desktop.
- Devin Desktop uses the same workflow system as Windsurf (Cascade workflows stored in `.devin/workflows/`), making it a natural migration path for existing OpenSpec users.
- OpenSpec currently supports Windsurf but not Devin Desktop. Adding Devin Desktop support ensures users can continue using OpenSpec with their new tool without manual migration.
- The adapter pattern is already established and proven with Windsurf; extending it to Devin Desktop is straightforward and maintains consistency across the tool ecosystem.

## What Changes

- Add **Devin Desktop** (`devin`) to the CLI tool picker (`openspec init`) so users can select it during setup.
- Create a new **Devin adapter** (`src/core/command-generation/adapters/devin.ts`) that generates commands in `.devin/workflows/opsx-<id>.md` with the same frontmatter structure as Windsurf.
- Register the Devin adapter in the command adapter registry (`src/core/command-generation/registry.ts`) and export it from the adapters index.
- Update `docs/supported-tools.md` to include Devin Desktop in the tool reference table.
- Ensure `openspec update` refreshes existing Devin workflows in-place, mirroring current behavior for other editors.
- Extend unit tests for init/update to cover Devin Desktop generation and updates.
- Update CLI prompts and documentation to advertise Devin Desktop support.

## Impact

- **Specs:** `cli-init`, `cli-update`, `command-generation`
- **Code:**
- `src/core/command-generation/adapters/devin.ts` (new adapter)
- `src/core/command-generation/registry.ts` (register adapter)
- `src/core/command-generation/adapters/index.ts` (export adapter)
- CLI tool selection logic
- **Docs:** `docs/supported-tools.md`
- **Tests:** init/update integration coverage for Devin Desktop workflows

## Notes

- This is a **migration enabler** for existing Windsurf users transitioning to Devin Desktop.
- Windsurf support can remain in place for backward compatibility with users still on Windsurf.
- The implementation closely mirrors the existing Windsurf adapter, reducing complexity and risk.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## MODIFIED Requirements

### Requirement: AI Tool Configuration
The command SHALL configure AI coding assistants with OpenSpec instructions using a marker system.

#### Scenario: Prompting for AI tool selection
- **WHEN** run interactively
- **THEN** prompt the user with "Which AI tools do you use?" using a multi-select menu
- **AND** list every available tool with a checkbox:
- Claude Code (creates or refreshes CLAUDE.md and slash commands)
- Cursor (creates or refreshes `.cursor/commands/*` slash commands)
- OpenCode (creates or refreshes `.opencode/command/openspec-*.md` slash commands)
- Devin Desktop (creates or refreshes `.devin/workflows/opsx-*.md` workflows)
- Windsurf (creates or refreshes `.windsurf/workflows/opsx-*.md` workflows)
- AGENTS.md standard (creates or refreshes AGENTS.md with OpenSpec markers)
- **AND** show "(already configured)" beside tools whose managed files exist so users understand selections will refresh content
- **AND** treat disabled tools as "coming soon" and keep them unselectable
- **AND** allow confirming with Enter after selecting one or more tools

### Requirement: Slash Command Configuration
The init command SHALL generate slash command files for supported editors using shared templates.

#### Scenario: Generating workflows for Devin Desktop
- **WHEN** the user selects Devin Desktop during initialization
- **THEN** create `.devin/workflows/opsx-propose.md`, `.devin/workflows/opsx-apply.md`, and `.devin/workflows/opsx-archive.md`
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
- **AND** use the same frontmatter structure as Windsurf (name, description, category, tags)

#### Scenario: Generating workflows for Windsurf
- **WHEN** the user selects Windsurf during initialization
- **THEN** create `.windsurf/workflows/opsx-propose.md`, `.windsurf/workflows/opsx-apply.md`, and `.windsurf/workflows/opsx-archive.md`
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## MODIFIED Requirements

### Requirement: Slash Command Updates
The update command SHALL refresh existing slash command files for configured tools without creating new ones.

#### Scenario: Updating workflows for Devin Desktop
- **WHEN** `.devin/workflows/` contains `opsx-propose.md`, `opsx-apply.md`, and `opsx-archive.md`
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
- **AND** ensure templates include instructions for the relevant workflow stage
- **AND** preserve the frontmatter structure (name, description, category, tags)

#### Scenario: Updating workflows for Windsurf
- **WHEN** `.windsurf/workflows/` contains `opsx-propose.md`, `opsx-apply.md`, and `opsx-archive.md`
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
- **AND** ensure templates include instructions for the relevant workflow stage

#### Scenario: Missing workflow file
- **WHEN** a tool lacks a workflow file
- **THEN** do not create a new file during update
92 changes: 92 additions & 0 deletions openspec/changes/add-devin-desktop-support/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Implementation Tasks

## 1. Create Devin Desktop Adapter

### 1.1 Create adapter file
- Create `src/core/command-generation/adapters/devin.ts`
- Base implementation on the existing Windsurf adapter (`src/core/command-generation/adapters/windsurf.ts`)
- Use `.devin/workflows/` as the target directory
- Use `opsx-<id>.md` as the filename pattern
- Include frontmatter with: name, description, category, tags

### 1.2 Implement adapter interface
- Export `devinAdapter` object implementing `ToolCommandAdapter`
- Set `toolId` to `'devin'`
- Implement `getFilePath()` to return `.devin/workflows/opsx-<id>.md`
- Implement `formatFile()` to generate YAML frontmatter + body content

## 2. Register Adapter

### 2.1 Update registry
- Edit `src/core/command-generation/registry.ts`
- Import the new `devinAdapter`
- Register it in the static initializer: `CommandAdapterRegistry.register(devinAdapter)`

### 2.2 Export adapter
- Edit `src/core/command-generation/adapters/index.ts`
- Add export: `export { devinAdapter } from './devin.js'`
- Update main index if needed: `src/core/command-generation/index.ts`

## 3. Update CLI Tool Selection

### 3.1 Add Devin Desktop to tool picker
- Locate CLI initialization code that prompts for tool selection
- Add "Devin Desktop" option to the multi-select menu
- Ensure it appears alongside Windsurf and other tools
- Map selection to `devin` tool ID

## 4. Update Documentation

### 4.1 Update supported tools reference
- Edit `docs/supported-tools.md`
- Add Devin Desktop row to the tool directory reference table
- Include:
- Tool name and ID: `Devin Desktop (devin)`
- Skills path: `.devin/skills/openspec-*/SKILL.md`
- Command path: `.devin/workflows/opsx-<id>.md`
- Add `devin` to the available tool IDs list in the "Non-Interactive Setup" section
Comment thread
mehdishahdoost marked this conversation as resolved.

### 4.2 Update README if needed
- Check if README mentions tool count or lists specific tools
- Update any references to reflect Devin Desktop support

## 5. Add Tests

### 5.1 Test adapter functionality
- Create or update tests for the Devin adapter
- Test `getFilePath()` returns correct path
- Test `formatFile()` generates valid YAML frontmatter
- Test cross-platform path handling (Windows, macOS, Linux)

### 5.2 Test CLI integration
- Test `openspec init --tools devin` generates `.devin/workflows/` files
- Test `openspec update` refreshes existing Devin workflows
- Test that Devin Desktop appears in interactive tool selection
- Verify files are created with correct structure and content

### 5.3 Test backward compatibility
- Ensure Windsurf adapter still works
- Verify both Devin and Windsurf can be selected together
- Test that existing Windsurf installations are not affected

## 6. Verify and Polish

### 6.1 Manual testing
- Run `openspec init` and select Devin Desktop
- Verify `.devin/workflows/` directory is created
- Check that workflow files have correct frontmatter and content
- Run `openspec update` and verify files are refreshed
- Test on Windows, macOS, and Linux if possible

### 6.2 Code review checklist
- Adapter follows existing patterns (Windsurf, Cursor, Claude)
- No hardcoded paths (use `path.join()`)
- YAML escaping handles special characters
- Error handling is consistent with other adapters
- Comments are clear and helpful

### 6.3 Documentation review
- Supported tools table is accurate and complete
- Tool IDs are consistent across docs
- Examples show Devin Desktop usage
- Links and references are correct
69 changes: 69 additions & 0 deletions src/core/command-generation/adapters/devin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Devin Desktop Command Adapter
*
* Formats commands for Devin Desktop following its frontmatter specification.
* Devin Desktop uses the same Cascade workflow system as Windsurf.
*/

import path from 'path';
import { transformToHyphenCommands } from '../../../utils/command-references.js';
import type { CommandContent, ToolCommandAdapter } from '../types.js';

/**
* Escapes a string value for safe YAML output.
* Quotes the string if it contains special YAML characters or would be
* interpreted as an implicit YAML scalar (boolean, null, number, etc).
*/
function escapeYamlValue(value: string): string {
// Check if value needs quoting due to special YAML characters or whitespace
const hasSpecialChars = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);

// Check if value would be interpreted as an implicit YAML scalar
// Matches: booleans (true/false/yes/no/on/off), null variants, numbers, hex/octal
const isImplicitScalar = /^(true|false|yes|no|on|off|null|~|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|0x[0-9a-fA-F]+|0o[0-7]+|-|\.?)$/.test(value);

if (hasSpecialChars || isImplicitScalar) {
// Use double quotes and escape internal double quotes and backslashes
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `"${escaped}"`;
}
return value;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Formats a tags array as a YAML array with proper escaping.
*/
function formatTagsArray(tags: string[]): string {
const escapedTags = tags.map((tag) => escapeYamlValue(tag));
return `[${escapedTags.join(', ')}]`;
}

/**
* Devin Desktop adapter for command generation.
* File path: .devin/workflows/opsx-<id>.md
* Frontmatter: name, description, category, tags
*
* Devin Desktop uses slash-hyphen syntax (/opsx-apply) instead of colon syntax (/opsx:apply).
*/
export const devinAdapter: ToolCommandAdapter = {
toolId: 'devin',

getFilePath(commandId: string): string {
return path.join('.devin', 'workflows', `opsx-${commandId}.md`);
},

formatFile(content: CommandContent): string {
// Transform command references from colon to hyphen syntax
const transformedBody = transformToHyphenCommands(content.body);

return `---
name: ${escapeYamlValue(content.name)}
description: ${escapeYamlValue(content.description)}
category: ${escapeYamlValue(content.category)}
tags: ${formatTagsArray(content.tags)}
---

${transformedBody}
`;
},
};
1 change: 1 addition & 0 deletions src/core/command-generation/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { bobAdapter } from './bob.js';
export { claudeAdapter } from './claude.js';
export { clineAdapter } from './cline.js';
export { codexAdapter } from './codex.js';
export { devinAdapter } from './devin.js';
export { codebuddyAdapter } from './codebuddy.js';
export { continueAdapter } from './continue.js';
export { costrictAdapter } from './costrict.js';
Expand Down
2 changes: 2 additions & 0 deletions src/core/command-generation/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { bobAdapter } from './adapters/bob.js';
import { claudeAdapter } from './adapters/claude.js';
import { clineAdapter } from './adapters/cline.js';
import { codexAdapter } from './adapters/codex.js';
import { devinAdapter } from './adapters/devin.js';
import { codebuddyAdapter } from './adapters/codebuddy.js';
import { continueAdapter } from './adapters/continue.js';
import { costrictAdapter } from './adapters/costrict.js';
Expand Down Expand Up @@ -48,6 +49,7 @@ export class CommandAdapterRegistry {
CommandAdapterRegistry.register(claudeAdapter);
CommandAdapterRegistry.register(clineAdapter);
CommandAdapterRegistry.register(codexAdapter);
CommandAdapterRegistry.register(devinAdapter);
CommandAdapterRegistry.register(codebuddyAdapter);
CommandAdapterRegistry.register(continueAdapter);
CommandAdapterRegistry.register(costrictAdapter);
Expand Down
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' },
{ name: 'Devin Desktop', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin' },
{ name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue' },
Expand Down
13 changes: 13 additions & 0 deletions test/core/available-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ describe('available-tools', () => {
expect(tools).toHaveLength(3);
});

it('should detect Devin Desktop when .devin directory exists', async () => {
await fs.mkdir(path.join(testDir, '.devin'), { recursive: true });

const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('devin');

const devinTool = tools.find((t) => t.value === 'devin');
expect(devinTool).toBeDefined();
expect(devinTool?.name).toBe('Devin Desktop');
expect(devinTool?.skillsDir).toBe('.devin');
});

it('should ignore files that are not directories', async () => {
// Create a file named .claude instead of a directory
await fs.writeFile(path.join(testDir, '.claude'), 'not a directory');
Expand Down
Loading