Skip to content

Commit c58d3c8

Browse files
fsilvaortizclaude
andcommitted
fix(opencode): use plural commands/ directory to match OpenCode convention
The OpenCode adapter was using `.opencode/command/` (singular) but OpenCode's official documentation specifies `.opencode/commands/` (plural). This aligns with every other adapter in the codebase. Legacy cleanup updated to detect old singular-path artifacts. Fixes #748. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d7d1860 commit c58d3c8

10 files changed

Lines changed: 160 additions & 5 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@fission-ai/openspec": patch
3+
---
4+
5+
fix: OpenCode adapter now uses `.opencode/commands/` (plural) to match OpenCode's official directory convention. Fixes #748.

docs/supported-tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `sync`, `b
3838
| iFlow (`iflow`) | `.iflow/skills/openspec-*/SKILL.md` | `.iflow/commands/opsx-<id>.md` |
3939
| Kilo Code (`kilocode`) | `.kilocode/skills/openspec-*/SKILL.md` | `.kilocode/workflows/opsx-<id>.md` |
4040
| Kiro (`kiro`) | `.kiro/skills/openspec-*/SKILL.md` | `.kiro/prompts/opsx-<id>.prompt.md` |
41-
| OpenCode (`opencode`) | `.opencode/skills/openspec-*/SKILL.md` | `.opencode/command/opsx-<id>.md` |
41+
| OpenCode (`opencode`) | `.opencode/skills/openspec-*/SKILL.md` | `.opencode/commands/opsx-<id>.md` |
4242
| Pi (`pi`) | `.pi/skills/openspec-*/SKILL.md` | `.pi/prompts/opsx-<id>.md` |
4343
| Qoder (`qoder`) | `.qoder/skills/openspec-*/SKILL.md` | `.qoder/commands/opsx/<id>.md` |
4444
| Qwen Code (`qwen`) | `.qwen/skills/openspec-*/SKILL.md` | `.qwen/commands/opsx-<id>.toml` |
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-02-25
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
## Context
2+
3+
The OpenCode adapter in `src/core/command-generation/adapters/opencode.ts` currently generates command files at `.opencode/command/opsx-<id>.md` (singular `command`). OpenCode's official documentation uses `.opencode/commands/` (plural), and every other adapter in the codebase follows the plural convention for commands directories. The legacy cleanup module in `src/core/legacy-cleanup.ts` also references the singular form for detecting old artifacts.
4+
5+
## Goals / Non-Goals
6+
7+
**Goals:**
8+
- Align the OpenCode adapter path with OpenCode's official `.opencode/commands/` convention
9+
- Add the old singular path `.opencode/command/` to legacy cleanup so existing installations are properly cleaned
10+
- Update documentation to reflect the corrected path
11+
- Update test assertions to match the new path
12+
13+
**Non-Goals:**
14+
- Changing the OpenCode skill path (`.opencode/skills/`) — already correct
15+
- Modifying any other adapter's directory structure
16+
- Adding migration prompts or interactive upgrade flows
17+
18+
## Decisions
19+
20+
### 1. Direct path rename in adapter
21+
22+
**Decision:** Change `path.join('.opencode', 'command', ...)` to `path.join('.opencode', 'commands', ...)` in the adapter's `getFilePath` method.
23+
24+
**Rationale:** This is a single-line change that aligns with the established pattern across all other adapters. No abstraction or indirection needed.
25+
26+
**Alternatives considered:**
27+
- Add a configuration option for the directory name — rejected as over-engineering for a bug fix
28+
- Keep singular and add plural as alias — rejected as it creates ambiguity about which is canonical
29+
30+
### 2. Legacy cleanup via existing constant map
31+
32+
**Decision:** Update the `LEGACY_SLASH_COMMAND_PATHS` entry for `'opencode'` from `'.opencode/command/openspec-*.md'` to `'.opencode/command/opsx-*.md'` (the old singular path becomes the legacy pattern) and ensure the new path is handled by the current command generation pipeline.
33+
34+
**Rationale:** The existing legacy cleanup infrastructure uses `LEGACY_SLASH_COMMAND_PATHS` as an explicit lookup. The old singular-path pattern already matches the legacy format (`openspec-*` prefix from the old SlashCommandRegistry era). The current command generation uses the `opsx-*` prefix, so we also need to add a legacy pattern for `opsx-*` files in the old singular directory.
35+
36+
**Alternatives considered:**
37+
- Add a separate migration script — rejected; the existing legacy cleanup mechanism handles this scenario
38+
39+
### 3. Documentation update
40+
41+
**Decision:** Update the `docs/supported-tools.md` table entry for OpenCode from `.opencode/command/opsx-<id>.md` to `.opencode/commands/opsx-<id>.md`.
42+
43+
**Rationale:** Documentation must match the actual generated paths.
44+
45+
## Risks / Trade-offs
46+
47+
- **[Existing installations have files at old path]** → Mitigated by legacy cleanup detecting `.opencode/command/` artifacts. On next `openspec init`, old files are cleaned up and new files written to `.opencode/commands/`.
48+
- **[Users referencing old path in custom scripts]** → Low risk. The old path was incorrect per OpenCode's specification, so custom references were already misaligned.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## Why
2+
3+
The OpenCode adapter uses `.opencode/command/` (singular) for its commands directory, but OpenCode's official documentation specifies `.opencode/commands/` (plural). Every other adapter in the codebase also uses plural directory names (`.claude/commands/`, `.cursor/commands/`, `.factory/commands/`, etc.). This inconsistency was introduced in Oct 2025 without documented rationale. Fixes [#748](https://github.com/Fission-AI/OpenSpec/issues/748).
4+
5+
## What Changes
6+
7+
- OpenCode adapter path changes from `.opencode/command/` to `.opencode/commands/`
8+
- Legacy cleanup adds `.opencode/command/` (old singular path) for backward compatibility
9+
- Documentation updated to reflect the new plural path
10+
11+
## Capabilities
12+
13+
### New Capabilities
14+
15+
_None._
16+
17+
### Modified Capabilities
18+
19+
- `command-generation`: OpenCode adapter path changes from singular `command/` to plural `commands/` to match OpenCode's official directory convention
20+
21+
## Impact
22+
23+
- `src/core/command-generation/adapters/opencode.ts` — adapter path
24+
- `src/core/legacy-cleanup.ts` — legacy cleanup pattern + add old singular path
25+
- `docs/supported-tools.md` — documentation table
26+
- `test/core/command-generation/adapters.test.ts` — test assertion
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: ToolCommandAdapter interface
4+
5+
The system SHALL define a `ToolCommandAdapter` interface for per-tool formatting.
6+
7+
#### Scenario: Adapter interface structure
8+
9+
- **WHEN** implementing a tool adapter
10+
- **THEN** `ToolCommandAdapter` SHALL require:
11+
- `toolId`: string identifier matching `AIToolOption.value`
12+
- `getFilePath(commandId: string)`: returns file path for command (relative from project root, or absolute for global-scoped tools like Codex)
13+
- `formatFile(content: CommandContent)`: returns complete file content with frontmatter
14+
15+
#### Scenario: Claude adapter formatting
16+
17+
- **WHEN** formatting a command for Claude Code
18+
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
19+
- **AND** file path SHALL follow pattern `.claude/commands/opsx/<id>.md`
20+
21+
#### Scenario: Cursor adapter formatting
22+
23+
- **WHEN** formatting a command for Cursor
24+
- **THEN** the adapter SHALL output YAML frontmatter with `name` as `/opsx-<id>`, `id`, `category`, `description` fields
25+
- **AND** file path SHALL follow pattern `.cursor/commands/opsx-<id>.md`
26+
27+
#### Scenario: Windsurf adapter formatting
28+
29+
- **WHEN** formatting a command for Windsurf
30+
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
31+
- **AND** file path SHALL follow pattern `.windsurf/workflows/opsx-<id>.md`
32+
33+
#### Scenario: OpenCode adapter formatting
34+
35+
- **WHEN** formatting a command for OpenCode
36+
- **THEN** the adapter SHALL output YAML frontmatter with `description` field
37+
- **AND** file path SHALL follow pattern `.opencode/commands/opsx-<id>.md` using `path.join('.opencode', 'commands', ...)` for cross-platform compatibility
38+
- **AND** the adapter SHALL transform colon-based command references (`/opsx:name`) to hyphen-based (`/opsx-name`) in the body
39+
40+
## ADDED Requirements
41+
42+
### Requirement: Legacy cleanup for renamed OpenCode command directory
43+
44+
The legacy cleanup module SHALL detect and remove old OpenCode command files from the previous singular `.opencode/command/` directory path.
45+
46+
#### Scenario: Detect old singular-path OpenCode command files
47+
48+
- **WHEN** running legacy artifact detection on a project with files matching `.opencode/command/openspec-*.md` or `.opencode/command/opsx-*.md`
49+
- **THEN** the system SHALL include those files in the legacy slash command files list via `LEGACY_SLASH_COMMAND_PATHS`
50+
51+
#### Scenario: Clean up old OpenCode command files on init
52+
53+
- **WHEN** a user runs `openspec init` in a project with old `.opencode/command/` artifacts
54+
- **THEN** the system SHALL remove the old files
55+
- **AND** generate new command files at `.opencode/commands/`
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## 1. Adapter Fix
2+
3+
- [x] 1.1 Update `src/core/command-generation/adapters/opencode.ts`: change `path.join('.opencode', 'command', ...)` to `path.join('.opencode', 'commands', ...)` and update the JSDoc comment
4+
5+
## 2. Legacy Cleanup
6+
7+
- [x] 2.1 Update `src/core/legacy-cleanup.ts`: change the `'opencode'` entry in `LEGACY_SLASH_COMMAND_PATHS` from `'.opencode/command/openspec-*.md'` to `'.opencode/command/opsx-*.md'` to detect old singular-path artifacts with the current `opsx-*` prefix
8+
9+
## 3. Documentation
10+
11+
- [x] 3.1 Update `docs/supported-tools.md`: change OpenCode command path from `.opencode/command/opsx-<id>.md` to `.opencode/commands/opsx-<id>.md`
12+
13+
## 4. Tests
14+
15+
- [x] 4.1 Update `test/core/command-generation/adapters.test.ts`: change the OpenCode file path assertion from `path.join('.opencode', 'command', 'opsx-explore.md')` to `path.join('.opencode', 'commands', 'opsx-explore.md')`
16+
17+
## 5. Changeset
18+
19+
- [x] 5.1 Create a changeset file (`.changeset/fix-opencode-commands-directory.md`) with a patch bump describing the path fix

src/core/command-generation/adapters/opencode.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import { transformToHyphenCommands } from '../../../utils/command-references.js'
1010

1111
/**
1212
* OpenCode adapter for command generation.
13-
* File path: .opencode/command/opsx-<id>.md
13+
* File path: .opencode/commands/opsx-<id>.md
1414
* Frontmatter: description
1515
*/
1616
export const opencodeAdapter: ToolCommandAdapter = {
1717
toolId: 'opencode',
1818

1919
getFilePath(commandId: string): string {
20-
return path.join('.opencode', 'command', `opsx-${commandId}.md`);
20+
return path.join('.opencode', 'commands', `opsx-${commandId}.md`);
2121
},
2222

2323
formatFile(content: CommandContent): string {

src/core/legacy-cleanup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashCommandPatter
4949
'roocode': { type: 'files', pattern: '.roo/commands/openspec-*.md' },
5050
'auggie': { type: 'files', pattern: '.augment/commands/openspec-*.md' },
5151
'factory': { type: 'files', pattern: '.factory/commands/openspec-*.md' },
52-
'opencode': { type: 'files', pattern: '.opencode/command/openspec-*.md' },
52+
'opencode': { type: 'files', pattern: '.opencode/command/opsx-*.md' },
5353
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
5454
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
5555
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },

test/core/command-generation/adapters.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ describe('command-generation/adapters', () => {
444444

445445
it('should generate correct file path', () => {
446446
const filePath = opencodeAdapter.getFilePath('explore');
447-
expect(filePath).toBe(path.join('.opencode', 'command', 'opsx-explore.md'));
447+
expect(filePath).toBe(path.join('.opencode', 'commands', 'opsx-explore.md'));
448448
});
449449

450450
it('should format file with description frontmatter', () => {

0 commit comments

Comments
 (0)