Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e0d01c1
proposal: add devin desktop support
mehdishahdoost Jun 4, 2026
a0a3fb6
feat(adapters): add devin desktop command adapter
mehdishahdoost Jun 4, 2026
514699e
fix(config): add devin desktop to AI_TOOLS
mehdishahdoost Jun 4, 2026
fd974e1
fix(devin-adapter): escape implicit YAML scalars in frontmatter
mehdishahdoost Jun 4, 2026
9a8befa
test(devin-adapter): add comprehensive tests for Devin Desktop adapter
mehdishahdoost Jun 6, 2026
cbb8301
Add cross-platform testcase.
mehdishahdoost Jun 6, 2026
c83bdd4
Merge branch 'main' into feat/add-devin-desktop-support
mehdishahdoost Jul 2, 2026
48e6b28
Merge branch 'main' into feat/add-devin-desktop-support
mehdishahdoost Jul 9, 2026
254fec2
Merge origin/main into feat/add-devin-desktop-support (resolve confli…
mehdishahdoost Jul 25, 2026
3563114
Merge branch 'main' into feat/add-devin-desktop-support
clay-good Jul 28, 2026
e310b41
fix(devin): refresh deltas against canonical specs and point skills a…
clay-good Jul 28, 2026
872e34c
fix(devin): correct commands-only hint, fill doc gaps, cover both sur…
clay-good Jul 28, 2026
44258ce
fix(devin): name the right command in the profile migration notice
clay-good Jul 28, 2026
306e8e6
Merge branch 'main' into feat/add-devin-desktop-support
clay-good Jul 28, 2026
ad43431
Merge remote-tracking branch 'origin/main' into pr-1167-local
clay-good Jul 28, 2026
343ae0a
fix(devin): keep the cli-update delta in step with the canonical spec
clay-good Jul 28, 2026
9005be2
feat(adapters): follow the Windsurf rename to Devin Desktop, with mig…
clay-good Jul 28, 2026
8012bc3
fix(devin): stop the migration from deleting anything it does not own
clay-good Jul 28, 2026
d94af8b
docs(devin): add a changeset for the Windsurf rename and migration
clay-good Jul 28, 2026
2a9c0e9
fix(devin): move only SKILL.md, never the skill directory around it
clay-good Jul 28, 2026
90eaee1
fix(devin): treat an edited skill the way an edited command is alread…
clay-good Jul 28, 2026
37bff11
fix(devin): report divergent legacy files even when nothing is movable
clay-good Jul 28, 2026
c164701
test(devin): stop matching the unrelated profile-migration line
clay-good Jul 28, 2026
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 @@ -33,6 +33,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch
| CodeArts (`codeartsagent`) | `.codeartsdoer/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/<id>.md` |
| Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | Not generated (skills-only; use `.codex/skills/openspec-*`) |
| Devin Desktop (`devin`) | `.devin/skills/openspec-*/SKILL.md` | `.devin/workflows/opsx-<id>.md` |
Comment thread
mehdishahdoost marked this conversation as resolved.
Outdated
| 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 @@ -81,7 +82,7 @@ openspec init --tools none
openspec init --profile core
```

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

## 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.
Outdated

### 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;
}

/**
* 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}
`;
},
};
4 changes: 3 additions & 1 deletion src/core/command-generation/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@ export { continueAdapter } from './continue.js';
export { costrictAdapter } from './costrict.js';
export { crushAdapter } from './crush.js';
export { cursorAdapter } from './cursor.js';
export { devinAdapter } from './devin.js';
export { factoryAdapter } from './factory.js';
export { geminiAdapter } from './gemini.js';
export { githubCopilotAdapter } from './github-copilot.js';
export { iflowAdapter } from './iflow.js';
export { junieAdapter } from './junie.js';
export { kilocodeAdapter } from './kilocode.js';
export { kiroAdapter } from './kiro.js';
export { lingmaAdapter } from './lingma.js';
export { ohMyPiAdapter } from './oh-my-pi.js';
export { opencodeAdapter } from './opencode.js';
export { piAdapter } from './pi.js';
export { qoderAdapter } from './qoder.js';
export { lingmaAdapter } from './lingma.js';
export { qwenAdapter } from './qwen.js';
export { roocodeAdapter } from './roocode.js';
export { traeAdapter } from './trae.js';
export { windsurfAdapter } from './windsurf.js';
export { zcodeAdapter } from './zcode.js';
2 changes: 2 additions & 0 deletions src/core/command-generation/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { auggieAdapter } from './adapters/auggie.js';
import { bobAdapter } from './adapters/bob.js';
import { claudeAdapter } from './adapters/claude.js';
import { clineAdapter } from './adapters/cline.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 @@ -49,6 +50,7 @@ export class CommandAdapterRegistry {
CommandAdapterRegistry.register(bobAdapter);
CommandAdapterRegistry.register(claudeAdapter);
CommandAdapterRegistry.register(clineAdapter);
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 @@ -28,6 +28,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
{ name: 'CodeArts', value: 'codeartsagent', available: true, successLabel: 'CodeArts', skillsDir: '.codeartsdoer' },
{ 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 @@ -44,6 +44,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