diff --git a/docs/cli.md b/docs/cli.md index 8f9c03bae..8321dab75 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -104,7 +104,7 @@ openspec init [path] [options] `--profile custom` uses whatever workflows are currently selected in global config (`openspec config profile`). -**Supported 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`, `vibe`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` +**Supported tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codeartsagent`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf` > This list mirrors `AI_TOOLS` in `src/core/config.ts`. See [Supported Tools](supported-tools.md) for each tool's skill and command paths. diff --git a/docs/commands.md b/docs/commands.md index 5d52c056c..2d1802cc4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -620,6 +620,7 @@ Different AI tools use slightly different command syntax. Use the format that ma | Cursor | `/opsx-propose`, `/opsx-apply` | | Windsurf | `/opsx-propose`, `/opsx-apply` | | Copilot (IDE) | `/opsx-propose`, `/opsx-apply` | +| CodeArts | Skill-based invocations such as `/openspec-propose`, `/openspec-apply-change` (no generated `opsx-*` command files) | | Kimi CLI | Skill-based invocations such as `/skill:openspec-propose`, `/skill:openspec-apply-change` (no generated `opsx-*` command files) | | Trae | Skill-based invocations such as `/openspec-propose`, `/openspec-apply-change` (no generated `opsx-*` command files) | diff --git a/docs/how-commands-work.md b/docs/how-commands-work.md index e60c9a761..5533a5799 100644 --- a/docs/how-commands-work.md +++ b/docs/how-commands-work.md @@ -79,6 +79,7 @@ The intent is identical everywhere. The punctuation differs. Use the form that m | Cursor | `/opsx-propose`, `/opsx-apply` | | Windsurf | `/opsx-propose`, `/opsx-apply` | | GitHub Copilot (IDE) | `/opsx-propose`, `/opsx-apply` | +| CodeArts | skill-style, e.g. `/openspec-propose` | | Kimi CLI | skill-style, e.g. `/skill:openspec-propose` | | Trae | skill-style, e.g. `/openspec-propose` | diff --git a/docs/supported-tools.md b/docs/supported-tools.md index b2ee30fb4..920517eca 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -28,6 +28,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch | IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-.md` | | Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/.md` | | Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-.md` | +| 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/.md` | | Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | `$CODEX_HOME/prompts/opsx-.md`\* | | ForgeCode (`forgecode`) | `.forge/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | @@ -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`, `codeartsagent`, `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` ## Workflow-Dependent Installation diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 07b5bb725..bc64ca64f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -55,7 +55,7 @@ If `/opsx:propose` (or your tool's equivalent) doesn't appear or doesn't do anyt 5. **Check you initialized this project.** Skills are written per project. If you cloned a repo or switched folders, run `openspec init` (or `openspec update`) there. -6. **Confirm your tool supports command files.** A few tools (Kimi CLI, Trae, ForgeCode, Mistral Vibe) don't get generated `opsx-*` command files; they use skill-based invocations instead. The forms differ per tool: see [Supported Tools](supported-tools.md) and [How Commands Work](how-commands-work.md#slash-command-syntax-by-tool). +6. **Confirm your tool supports command files.** A few tools (CodeArts, Kimi CLI, Trae, ForgeCode, Mistral Vibe) don't get generated `opsx-*` command files; they use skill-based invocations instead. The forms differ per tool: see [Supported Tools](supported-tools.md) and [How Commands Work](how-commands-work.md#slash-command-syntax-by-tool). ## Working with changes diff --git a/src/core/config.ts b/src/core/config.ts index 3be428b26..17d406812 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -25,6 +25,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' }, { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' }, { 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: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' }, { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' }, diff --git a/test/core/available-tools.test.ts b/test/core/available-tools.test.ts index 50d758070..3a43a7960 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -163,5 +163,24 @@ describe('available-tools', () => { expect(vibeTool?.name).toBe('Mistral Vibe'); expect(vibeTool?.skillsDir).toBe('.vibe'); }); + + it('should detect CodeArts when .codeartsdoer directory exists', async () => { + await fs.mkdir(path.join(testDir, '.codeartsdoer'), { recursive: true }); + + const tools = getAvailableTools(testDir); + const codeArtsTool = tools.find((t) => t.value === 'codeartsagent'); + expect(codeArtsTool).toMatchObject({ + name: 'CodeArts', + value: 'codeartsagent', + available: true, + skillsDir: '.codeartsdoer', + }); + }); + + it('should not detect CodeArts when .codeartsdoer directory does not exist', () => { + const tools = getAvailableTools(testDir); + const toolValues = tools.map((t) => t.value); + expect(toolValues).not.toContain('codeartsagent'); + }); }); }); diff --git a/test/core/command-generation/registry.test.ts b/test/core/command-generation/registry.test.ts index 14165ff51..814a8ab2b 100644 --- a/test/core/command-generation/registry.test.ts +++ b/test/core/command-generation/registry.test.ts @@ -32,6 +32,11 @@ describe('command-generation/registry', () => { expect(adapter).toBeUndefined(); }); + it('should return undefined for CodeArts without a command adapter', () => { + const adapter = CommandAdapterRegistry.get('codeartsagent'); + expect(adapter).toBeUndefined(); + }); + it('should return undefined for empty string', () => { const adapter = CommandAdapterRegistry.get(''); expect(adapter).toBeUndefined(); @@ -67,6 +72,10 @@ describe('command-generation/registry', () => { expect(CommandAdapterRegistry.has('unknown')).toBe(false); expect(CommandAdapterRegistry.has('')).toBe(false); }); + + it('should return false for CodeArts without a command adapter', () => { + expect(CommandAdapterRegistry.has('codeartsagent')).toBe(false); + }); }); describe('adapter functionality', () => { diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 6a436eaed..929edf727 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -192,6 +192,31 @@ describe('InitCommand', () => { ).toBe(true); }); + it('should support CodeArts as an adapterless skills-only tool', async () => { + saveGlobalConfig({ + featureFlags: {}, + profile: 'core', + delivery: 'both', + }); + + const initCommand = new InitCommand({ tools: 'codeartsagent', force: true }); + await initCommand.execute(testDir); + + const skillFile = path.join(testDir, '.codeartsdoer', 'skills', 'openspec-explore', 'SKILL.md'); + expect(await fileExists(skillFile)).toBe(true); + + const commandsDir = path.join(testDir, '.codeartsdoer', 'commands'); + expect(await directoryExists(commandsDir)).toBe(false); + + const codeArtsLogCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String); + expect(codeArtsLogCalls.some((entry) => entry.includes('Created: CodeArts'))).toBe(true); + expect( + codeArtsLogCalls.some( + (entry) => entry.includes('Commands skipped for: codeartsagent') && entry.includes('(no adapter)'), + ), + ).toBe(true); + }); + it('should create skills for multiple tools at once', async () => { const initCommand = new InitCommand({ tools: 'claude,cursor', force: true }); @@ -211,10 +236,12 @@ describe('InitCommand', () => { // Check a few representative tools const claudeSkill = path.join(testDir, '.claude', 'skills', 'openspec-explore', 'SKILL.md'); + const codeArtsSkill = path.join(testDir, '.codeartsdoer', 'skills', 'openspec-explore', 'SKILL.md'); const cursorSkill = path.join(testDir, '.cursor', 'skills', 'openspec-explore', 'SKILL.md'); const windsurfSkill = path.join(testDir, '.windsurf', 'skills', 'openspec-explore', 'SKILL.md'); expect(await fileExists(claudeSkill)).toBe(true); + expect(await fileExists(codeArtsSkill)).toBe(true); expect(await fileExists(cursorSkill)).toBe(true); expect(await fileExists(windsurfSkill)).toBe(true); }); diff --git a/test/core/shared/tool-detection.test.ts b/test/core/shared/tool-detection.test.ts index 5a66ff3cd..d903e6fe7 100644 --- a/test/core/shared/tool-detection.test.ts +++ b/test/core/shared/tool-detection.test.ts @@ -47,6 +47,7 @@ describe('tool-detection', () => { it('should return tools that have skillsDir configured', () => { const tools = getToolsWithSkillsDir(); expect(tools).toContain('claude'); + expect(tools).toContain('codeartsagent'); expect(tools).toContain('cursor'); expect(tools).toContain('windsurf'); expect(tools.length).toBeGreaterThan(0);