Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/add-trae-command-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fission-ai/openspec": minor
---

### New Features

- **TRAE command adapter** — Added command adapter for Trae IDE, enabling generation of `.trae/commands/opsx-<id>.md` files for custom slash commands
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,6 @@ opencode.json

# Bob
.bob/

# Trae
.trae/
11 changes: 8 additions & 3 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,24 +323,28 @@ AI: Implementing add-dark-mode...
Revise a change's existing planning artifacts and keep them coherent with one another. Planning artifacts only - it never edits code.

**Syntax:**
```

```text
/opsx:update [change-name]
```

**Arguments:**

| Argument | Required | Description |
|----------|----------|-------------|
| `change-name` | No | Which change to update (inferred from context if not provided) |

**What it does:**

- Reads the change's artifacts via `openspec status --change <name> --json`
- Applies your requested revision, or reviews the artifacts for contradictions if you didn't name one
- Reconciles the other existing artifacts in any direction (a design edit may ripple back to the proposal)
- Confirms every edit with you before writing, one artifact at a time
- Ends by recommending the next step: `/opsx:continue` (artifacts missing), `/opsx:apply` (carry a revised plan into code), or `/opsx:archive` (all done)

**Example:**
```

```text
You: /opsx:update add-dark-mode - we're storing the theme in a cookie now, not localStorage

AI: Reading add-dark-mode artifacts...
Expand All @@ -356,6 +360,7 @@ AI: Reading add-dark-mode artifacts...
```

**Tips:**

- It won't create missing artifacts - that's `/opsx:continue`
- If the change was already implemented, follow up with `/opsx:apply` so the code matches the revised plan
- If your revision changes the *intent* of the change, start fresh with a new change instead (see [When to Update vs. Start Fresh](opsx.md#when-to-update-vs-start-fresh))
Expand Down Expand Up @@ -666,7 +671,7 @@ Different AI tools use slightly different command syntax. Use the format that ma
| Windsurf | `/opsx-propose`, `/opsx-apply` |
| Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
| 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) |
| Trae | `/opsx-propose`, `/opsx-apply` |

The intent is the same across tools, but how commands are surfaced can differ by integration.

Expand Down
2 changes: 1 addition & 1 deletion docs/how-commands-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The intent is identical everywhere. The punctuation differs. Use the form that m
| Windsurf | `/opsx-propose`, `/opsx-apply` |
| GitHub Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
| Kimi CLI | skill-style, e.g. `/skill:openspec-propose` |
| Trae | skill-style, e.g. `/openspec-propose` |
| Trae | `/opsx-propose`, `/opsx-apply` |

Most tools use either the colon form (`/opsx:propose`) or the dash form (`/opsx-propose`). A few tools surface OpenSpec as named skills instead of slash commands; for those you invoke the skill by name. The full per-tool list, including exactly which files get written where, lives in [Supported Tools](supported-tools.md).

Expand Down
2 changes: 1 addition & 1 deletion docs/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch
| Qoder (`qoder`) | `.qoder/skills/openspec-*/SKILL.md` | `.qoder/commands/opsx/<id>.md` |
| Qwen Code (`qwen`) | `.qwen/skills/openspec-*/SKILL.md` | `.qwen/commands/opsx-<id>.toml` |
| RooCode (`roocode`) | `.roo/skills/openspec-*/SKILL.md` | `.roo/commands/opsx-<id>.md` |
| Trae (`trae`) | `.trae/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| Trae (`trae`) | `.trae/skills/openspec-*/SKILL.md` | `.trae/commands/opsx-<id>.md` |
| Windsurf (`windsurf`) | `.windsurf/skills/openspec-*/SKILL.md` | `.windsurf/workflows/opsx-<id>.md` |

\* Codex commands are installed in the global Codex home (`$CODEX_HOME/prompts/` if set, otherwise `~/.codex/prompts/`), not your project directory.
Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (Kimi CLI, 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

OpenSpec currently assumes command delivery maps directly to command adapters. That assumption does not hold for all tools.

Trae is a concrete example: it invokes OpenSpec workflows via skill entries (for example `/openspec-new-change`) rather than adapter-generated command files. In this model, skills are the command surface.
Some tools expose OpenSpec workflows via skill entries rather than adapter-generated command files. Kimi CLI is a concrete example: it invokes skills with forms such as `/skill:openspec-new-change`. In this model, skills are the command surface.

Today, this creates a behavior gap:

- `delivery=commands` can remove skills
- tools without adapters skip command generation
- result: selected tools like Trae can end up with no invocable workflow artifacts
- result: selected tools like Kimi CLI, ForgeCode, or Mistral Vibe can end up with no invocable workflow artifacts

This is more than a prompt UX issue because non-interactive and CI flows bypass interactive guidance. We need a capability-aware model in core generation logic.

Expand All @@ -25,9 +25,13 @@ Add an optional field in tool metadata to describe how a tool exposes commands:
Field should be optional. Default behavior is inferred from adapter registry presence: tools with a registered adapter resolve to `adapter`; tools with no adapter registration and no explicit annotation resolve to `none`.
Capability values use kebab-case string tokens for consistency with serialized metadata conventions.

Initial explicit override:
Initial explicit overrides:

- Trae -> `skills-invocable`
- ForgeCode -> `skills-invocable`
- Kimi CLI -> `skills-invocable`
- Mistral Vibe -> `skills-invocable`

Trae no longer belongs in this override set once its `.trae/commands/opsx-<id>.md` adapter is available; it should resolve to `adapter` like other file-backed command integrations.

### 2. Make delivery behavior capability-aware

Expand Down Expand Up @@ -62,12 +66,12 @@ Update summaries to show effective delivery outcomes per tool (for example, when

### 4. Update docs and tests

- document capability model and Trae behavior under delivery modes
- document capability model and skills-invocable behavior under delivery modes
- ensure CLI docs and supported-tools docs reflect effective behavior
- add test coverage for:
- `init --tools trae` with `delivery=commands`
- `update` with Trae configured under `delivery=commands`
- mixed selections (`claude + trae`) across all delivery modes
- `init --tools kimi` with `delivery=commands`
- `update` with Kimi CLI configured under `delivery=commands`
- mixed selections (`claude + kimi`) across all delivery modes
- explicit error path for tools with no command surface under `delivery=commands`

### 5. Coordinate with install-scope behavior
Expand All @@ -94,7 +98,7 @@ Implementation tests should cover mixed-tool matrices to ensure deterministic be

## Impact

- `src/core/config.ts` - add optional command-surface metadata and Trae override
- `src/core/config.ts` - add optional command-surface metadata and skills-invocable tool overrides
- `src/core/command-generation/registry.ts` (or shared helper) - capability inference from adapter presence
- `src/core/init.ts` - capability-aware generation/removal planning + compatibility validation + summary messaging
- `src/core/update.ts` - capability-aware sync/removal planning + compatibility validation + summary messaging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

- [ ] 1.1 Extend tool metadata in `src/core/config.ts` with an optional command-surface capability field
- [ ] 1.2 Define supported capability values: `adapter`, `skills-invocable`, `none`
- [ ] 1.3 Mark Trae as `skills-invocable`
- [ ] 1.3 Mark known skills-invocable tools such as ForgeCode, Kimi CLI, and Mistral Vibe as `skills-invocable`
- [ ] 1.4 Add a shared capability resolver (explicit metadata override first, inferred fallback from adapter presence second)
- [ ] 1.5 Add focused unit tests for capability resolution (explicit override, inferred adapter, inferred none)

Expand All @@ -20,7 +20,7 @@
- [ ] 2.3 In `delivery=commands`, fail fast before writes when any selected tool resolves to `none`
- [ ] 2.4 Update init output to clearly report effective behavior for `skills-invocable` tools (skills used as command surface)
- [ ] 2.5 Ensure init no longer reports "no adapter" for tools intentionally using `skills-invocable`
- [ ] 2.6 Add/adjust init tests for `delivery=commands` + `trae` (skills retained/generated, no adapter error), mixed tools (`claude,trae`) with per-tool expected outputs, and deterministic failure path for unsupported command surface (`none`)
- [ ] 2.6 Add/adjust init tests for `delivery=commands` + `kimi` (skills retained/generated, no adapter error), mixed tools (`claude,kimi`) with per-tool expected outputs, and deterministic failure path for unsupported command surface (`none`)

## 3. Update: Capability-Aware Sync and Drift Detection

Expand All @@ -30,7 +30,7 @@
- [ ] 3.4 Update profile/delivery drift detection to avoid perpetual drift for `skills-invocable` tools under commands delivery
- [ ] 3.5 Ensure configured-tool detection still includes `skills-invocable` tools under commands delivery when managed skills exist
- [ ] 3.6 Update summary output so skills-invocable behavior is reported as expected behavior (not implicit skip/error)
- [ ] 3.7 Add/adjust update tests for `delivery=commands` + configured Trae (skills retained/generated), idempotent second update (no false drift loop), mixed configured tools (`claude` + `trae`), and deterministic preflight failure for unsupported command surface (`none`)
- [ ] 3.7 Add/adjust update tests for `delivery=commands` + configured Kimi CLI (skills retained/generated), idempotent second update (no false drift loop), mixed configured tools (`claude` + `kimi`), and deterministic preflight failure for unsupported command surface (`none`)

## 4. UX and Error Messaging

Expand All @@ -40,7 +40,7 @@

## 5. Documentation Updates

- [ ] 5.1 Update `docs/supported-tools.md` to document command-surface semantics for Trae and clarify delivery interactions
- [ ] 5.1 Update `docs/supported-tools.md` to document command-surface semantics for skills-invocable tools and clarify delivery interactions
- [ ] 5.2 Update `docs/cli.md` delivery guidance to explain capability-aware behavior for `delivery=commands`
- [ ] 5.3 Add a short troubleshooting note for "commands-only + unsupported tool" failures

Expand All @@ -49,5 +49,5 @@
- [ ] 6.1 Run targeted tests: `test/core/init.test.ts` and `test/core/update.test.ts`
- [ ] 6.2 Run any new capability/unit test files added in this change
- [ ] 6.3 Run full test suite (`pnpm test`) and resolve regressions
- [ ] 6.4 Manual smoke check: `openspec init --tools trae` with `delivery=commands`
- [ ] 6.5 Manual smoke check: mixed tools (`claude,trae`) with `delivery=commands`
- [ ] 6.4 Manual smoke check: `openspec init --tools kimi` with `delivery=commands`
- [ ] 6.5 Manual smoke check: mixed tools (`claude,kimi`) with `delivery=commands`
7 changes: 6 additions & 1 deletion openspec/specs/command-generation/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ The system SHALL define a `ToolCommandAdapter` interface for per-tool formatting
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
- **AND** file path SHALL follow pattern `.windsurf/workflows/opsx-<id>.md`

#### Scenario: Trae adapter formatting

- **WHEN** formatting a command for Trae
- **THEN** the adapter SHALL output YAML frontmatter with `name` and `description` fields
- **AND** file path SHALL follow pattern `.trae/commands/opsx-<id>.md`

### Requirement: Command generator function

The system SHALL provide a `generateCommand` function that combines content with adapter.
Expand Down Expand Up @@ -94,4 +100,3 @@ The body content of commands SHALL be shared across all tools.
- **WHEN** generating the 'explore' command for Claude and Cursor
- **THEN** both SHALL use the same `body` content
- **AND** only the frontmatter and file path SHALL differ

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 @@ -29,4 +29,5 @@ 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';
53 changes: 53 additions & 0 deletions src/core/command-generation/adapters/trae.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Trae Command Adapter
*
* Formats commands for Trae IDE following its command specification.
*/

import path from 'path';
import type { CommandContent, ToolCommandAdapter } from '../types.js';

/**
* Escapes a string value for safe YAML output.
* Quotes the string if it contains special YAML characters.
*/
function escapeYamlValue(value: string): string {
if (value === '') {
return '""';
}
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
// Use double quotes and escape internal double quotes, backslashes, and newlines
const escaped = value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
return `"${escaped}"`;
}
return value;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Trae adapter for command generation.
* File path: .trae/commands/opsx-<id>.md
* Frontmatter: name, description
*/
export const traeAdapter: ToolCommandAdapter = {
toolId: 'trae',

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

formatFile(content: CommandContent): string {
return `---
name: ${escapeYamlValue(content.name)}
description: ${escapeYamlValue(content.description)}
---

${content.body}
`;
},
};
2 changes: 2 additions & 0 deletions src/core/command-generation/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { qoderAdapter } from './adapters/qoder.js';
import { lingmaAdapter } from './adapters/lingma.js';
import { qwenAdapter } from './adapters/qwen.js';
import { roocodeAdapter } from './adapters/roocode.js';
import { traeAdapter } from './adapters/trae.js';
import { windsurfAdapter } from './adapters/windsurf.js';

/**
Expand Down Expand Up @@ -66,6 +67,7 @@ export class CommandAdapterRegistry {
CommandAdapterRegistry.register(lingmaAdapter);
CommandAdapterRegistry.register(qwenAdapter);
CommandAdapterRegistry.register(roocodeAdapter);
CommandAdapterRegistry.register(traeAdapter);
CommandAdapterRegistry.register(windsurfAdapter);
}

Expand Down
74 changes: 73 additions & 1 deletion test/core/command-generation/adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { piAdapter } from '../../../src/core/command-generation/adapters/pi.js';
import { qoderAdapter } from '../../../src/core/command-generation/adapters/qoder.js';
import { qwenAdapter } from '../../../src/core/command-generation/adapters/qwen.js';
import { roocodeAdapter } from '../../../src/core/command-generation/adapters/roocode.js';
import { traeAdapter } from '../../../src/core/command-generation/adapters/trae.js';
import { windsurfAdapter } from '../../../src/core/command-generation/adapters/windsurf.js';
import type { CommandContent } from '../../../src/core/command-generation/types.js';

Expand Down Expand Up @@ -674,6 +675,77 @@ describe('command-generation/adapters', () => {
});
});

describe('traeAdapter', () => {
it('should have correct toolId', () => {
expect(traeAdapter.toolId).toBe('trae');
});

it('should generate correct file path', () => {
const filePath = traeAdapter.getFilePath('explore');
expect(filePath).toBe(path.join('.trae', 'commands', 'opsx-explore.md'));
});

it('should generate correct file paths for different commands', () => {
expect(traeAdapter.getFilePath('new')).toBe(path.join('.trae', 'commands', 'opsx-new.md'));
expect(traeAdapter.getFilePath('bulk-archive')).toBe(path.join('.trae', 'commands', 'opsx-bulk-archive.md'));
});

it('should format file with name and description frontmatter', () => {
const output = traeAdapter.formatFile(sampleContent);

expect(output).toContain('---\n');
expect(output).toContain('name: OpenSpec Explore');
expect(output).toContain('description: Enter explore mode for thinking');
expect(output).toContain('---\n\n');
expect(output).toContain('This is the command body.\n\nWith multiple lines.');
});

it('should escape YAML special characters in name', () => {
const contentWithSpecialChars: CommandContent = {
...sampleContent,
name: 'Test: Command',
};
const output = traeAdapter.formatFile(contentWithSpecialChars);
expect(output).toContain('name: "Test: Command"');
});

it('should escape YAML special characters in description', () => {
const contentWithSpecialChars: CommandContent = {
...sampleContent,
description: 'Fix: regression in "auth" feature',
};
const output = traeAdapter.formatFile(contentWithSpecialChars);
expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"');
});

it('should escape newlines in description', () => {
const contentWithNewline: CommandContent = {
...sampleContent,
description: 'Line 1\nLine 2',
};
const output = traeAdapter.formatFile(contentWithNewline);
expect(output).toContain('description: "Line 1\\nLine 2"');
});

it('should handle empty description', () => {
const contentEmptyDesc: CommandContent = {
...sampleContent,
description: '',
};
const output = traeAdapter.formatFile(contentEmptyDesc);
expect(output).toContain('description: ""');
});

it('should escape carriage returns in description', () => {
const contentWithCR: CommandContent = {
...sampleContent,
description: 'Line 1\r\nLine 2',
};
const output = traeAdapter.formatFile(contentWithCR);
expect(output).toContain('description: "Line 1\\r\\nLine 2"');
});
});

describe('cross-platform path handling', () => {
it('Claude adapter uses path.join for paths', () => {
// path.join handles platform-specific separators
Expand All @@ -699,7 +771,7 @@ describe('command-generation/adapters', () => {
codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter,
crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter,
iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter,
qwenAdapter, roocodeAdapter
qwenAdapter, roocodeAdapter, traeAdapter
];
for (const adapter of adapters) {
const filePath = adapter.getFilePath('test');
Expand Down
Loading
Loading