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
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ openspec init [path] [options]

The welcome animation is also skipped when the `OPENSPEC_NO_ANIMATION` environment variable is set (any value, including empty), when `NO_COLOR` is set to a non-empty value, or when the OS reduced-motion preference is enabled (macOS Reduce Motion, GNOME animations disabled).

**Supported tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `command-code`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`, `agents`
**Supported tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `command-code`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zed`, `zcode`, `agents`

> This list mirrors `AI_TOOLS` in `src/core/config.ts`. See [Supported Tools](supported-tools.md) for each tool's skill and command paths.

Expand Down
2 changes: 1 addition & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ Different AI tools use slightly different command syntax. Use the format that ma
|--------------------------|----------------|---------------|
| `.../commands/opsx/<id>.*` | `/opsx:propose`, `/opsx:apply` | Claude Code, Gemini CLI, Crush |
| `.../opsx-<id>.*` | `/opsx-propose`, `/opsx-apply` | Cursor, Devin Desktop, Copilot (IDE), Trae, Oh My Pi |
| none — skills only | `/openspec-propose`, `/openspec-apply-change` | CodeArts, ForgeCode, Hermes, MiniMax Code, Mistral Vibe, shared `.agents` |
| none — skills only | `/openspec-propose`, `/openspec-apply-change` | CodeArts, ForgeCode, Hermes, MiniMax Code, Mistral Vibe, Zed Agent, shared `.agents` |
| none — Kimi Code | `/skill:openspec-propose` | Kimi Code |
| none — Codex CLI | `$openspec-propose` | Codex |

Expand Down
4 changes: 2 additions & 2 deletions docs/how-commands-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ The intent is identical everywhere. The spelling follows the file your tool load
| `.../commands/opsx/<id>.*` | `/opsx:propose` | Claude Code, Gemini CLI, Crush |
| `.../opsx-<id>.*` | `/opsx-propose` | Cursor, GitHub Copilot (IDE), Devin Desktop, Trae, Oh My Pi |
| `.amazonq/prompts/opsx-<id>.md` | `@opsx-propose` | Amazon Q Developer |
| none — skills only | `/openspec-propose` | CodeArts, ForgeCode, Hermes, Mistral Vibe, shared `.agents` |
| none — skills only | `/openspec-propose` | CodeArts, ForgeCode, Hermes, Mistral Vibe, Zed Agent, shared `.agents` |
| none — Kimi Code | `/skill:openspec-propose` | Kimi Code |
| none — Codex CLI | `$openspec-propose` | Codex |

Expand Down Expand Up @@ -114,7 +114,7 @@ See [Supported Tools](supported-tools.md) for the exact paths per tool, and [Mig

Quick checks, fastest first:

1. **Type a slash in your AI chat.** Start typing `/opsx` and watch for autocomplete suggestions. If they appear, you're set. On a skills-only tool (Codex, Kimi Code, CodeArts, ForgeCode, Hermes, Mistral Vibe, or the shared `.agents` target) `/opsx` never completes even on a healthy install — try the skill name from the table above instead.
1. **Type a slash in your AI chat.** Start typing `/opsx` and watch for autocomplete suggestions. If they appear, you're set. On a skills-only tool (Codex, Kimi Code, CodeArts, ForgeCode, Hermes, Mistral Vibe, Zed Agent, or the shared `.agents` target) `/opsx` never completes even on a healthy install — try the skill name from the table above instead.
2. **Look for the files.** For Claude Code, check that `.claude/skills/` contains `openspec-*` folders. Other tools use their own directories ([Supported Tools](supported-tools.md) lists them).
3. **Re-run setup.** From your project root, run `openspec update`. This regenerates the skill and command files for whatever tools you configured.
4. **Restart your assistant.** Many tools scan for skills and commands at startup, so a fresh window can be the missing step.
Expand Down
22 changes: 15 additions & 7 deletions docs/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ way it loads the file OpenSpec wrote. Find your tool's command path in the
| `.../opsx-<id>.*` — the filename is the command | `/opsx-<id>` | Every other tool with generated command files, except Amazon Q and Devin |
| `.devin/workflows/opsx-<id>.md` — read by only one of Devin's two agents | `/opsx-<id>` on Devin Desktop, `/openspec-<skill>` on Devin Local | Devin Desktop\*\*\*\* |
| `.amazonq/prompts/opsx-<id>.md` — a prompt, not a command | `@opsx-<id>` | Amazon Q Developer |
| none — skills only | `/openspec-<skill>` | CodeArts, ForgeCode, Hermes, MiniMax Code, Mistral Vibe, shared `.agents` |
| none — skills only | `/openspec-<skill>` | CodeArts, ForgeCode, Hermes, MiniMax Code, Mistral Vibe, Zed Agent, shared `.agents` |
| none — Kimi Code | `/skill:openspec-<skill>` | Kimi Code |
| none — Codex CLI | `$openspec-<skill>` | Codex ([`/openspec-<skill>` is not recognized](https://github.com/openai/codex/issues/11817)) |

Expand Down Expand Up @@ -100,6 +100,7 @@ to read the hint.
| [Rovo Dev CLI](https://support.atlassian.com/rovo/docs/use-rovo-dev-cli/) (`rovodev`) | `.rovodev/skills/openspec-*/SKILL.md` | Not generated. Rovo has no slash-command surface — it matches skills automatically or by prompt (e.g. "use the openspec-propose skill"); `/skills` only manages them. Generated content references skills by name, never as `/openspec-*` commands. |
| [Zoo Code](https://github.com/Zoo-Code-Org/Zoo-Code) (`roocode`) | `.roo/skills/openspec-*/SKILL.md` | `.roo/commands/opsx-<id>.md` |
| Trae (`trae`) | `.trae/skills/openspec-*/SKILL.md` | `.trae/commands/opsx-<id>.md` |
| [Zed Agent](https://zed.dev/docs/ai/skills) (`zed`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (skills-only; use `/openspec-*` or `@openspec-*`) |
| ZCode (`zcode`) | `.zcode/skills/openspec-*/SKILL.md` | `.zcode/commands/opsx/<id>.md` |
| Shared `.agents` skills (`agents`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |

Expand Down Expand Up @@ -145,8 +146,8 @@ shared root many agent tools read, instead of a tool-specific directory.
| Your tool isn't listed yet but reads `.agents/skills` | `agents` |

Selecting it alongside a tool-specific ID is fine; each normally writes to its
own root. Codex is the exception because it uses the same canonical `.agents`
root. If both `codex` and `agents` are selected, OpenSpec keeps one
own root. Codex and Zed Agent are the exceptions because they use the same canonical
`.agents` root. If Codex is selected with Zed or `agents`, OpenSpec keeps one
Codex-led tree. Its handoffs name both `$openspec-*` for Codex and
`/openspec-*` for other agents, so `--tools all` and existing multi-agent
setups keep working without two writers overwriting the same files.
Expand All @@ -168,10 +169,17 @@ Two things to know:
If your root `AGENTS.md` still carries OpenSpec marker blocks from an older
version, `openspec update` strips them — see the [Migration Guide](migration-guide.md).

Because `.agents/skills/` is shared, it is worth knowing what OpenSpec claims there:
Zed support here is for the built-in Zed Agent. Zed External Agents and Terminal
Threads use their own integrations. Agent Skills require
[Zed v1.4.2](https://github.com/zed-industries/zed/releases/tag/v1.4.2) or newer.
Project-local skills are unavailable in an untrusted worktree until you
[grant trust](https://zed.dev/docs/worktree-trust).

Because `.agents/skills/` is shared by Codex, Zed Agent, and the vendor-neutral target,
it is worth knowing what OpenSpec claims there:
it writes, refreshes, and removes only the `openspec-*` skill directories for your
selected workflows, plus an `.openspec-target` marker that records whether Codex
or the vendor-neutral target rendered that shared tree. Anything else in that
selected workflows, plus an `.openspec-target` marker that records whether Codex,
Zed Agent, or the vendor-neutral target rendered that shared tree. Anything else in that
directory is left alone. Treat the `openspec-*` names and marker as OpenSpec's —
edits inside them are replaced on the next `openspec update`, the same as for
every other tool.
Expand Down Expand Up @@ -206,7 +214,7 @@ openspec init --tools none
openspec init --profile core
```

**Available tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `command-code`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`, `agents`
**Available tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `command-code`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zed`, `zcode`, `agents`

## Workflow-Dependent Installation

Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,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.** Codex, CodeArts, ForgeCode, Hermes, Kimi Code, Mistral Vibe and the shared `.agents` target don't get generated `opsx-*` command files; they use skill-based invocations instead, so `/opsx` will never autocomplete for them. Type `$openspec-propose` in Codex, `/skill:openspec-propose` in Kimi Code, and `/openspec-propose` in the rest. The shared `.agents` target is vendor-neutral, so `/openspec-propose` is the common form rather than a guaranteed one — if your assistant does not answer to it, check its own docs for how it invokes a skill. Amazon Q does get command files, but loads them into its prompt library rather than its slash menu — type `@opsx-propose` there, not `/opsx`. Every tool's form is listed in [How To Invoke](supported-tools.md#how-to-invoke).
6. **Confirm your tool supports command files.** Codex, CodeArts, ForgeCode, Hermes, Kimi Code, Mistral Vibe, Zed Agent, and the shared `.agents` target don't get generated `opsx-*` command files; they use skill-based invocations instead, so `/opsx` will never autocomplete for them. Type `$openspec-propose` in Codex, `/skill:openspec-propose` in Kimi Code, and `/openspec-propose` in the rest. The shared `.agents` target is vendor-neutral, so `/openspec-propose` is the common form rather than a guaranteed one — if your assistant does not answer to it, check its own docs for how it invokes a skill. Amazon Q does get command files, but loads them into its prompt library rather than its slash menu — type `@opsx-propose` there, not `/opsx`. Every tool's form is listed in [How To Invoke](supported-tools.md#how-to-invoke).

## Working with changes

Expand Down
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },
{ name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo', requiresIdeRestart: true },
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae', requiresIdeRestart: true },
{ name: 'Zed Agent', value: 'zed', available: true, successLabel: 'Zed Agent', skillsDir: '.agents', detectionPaths: ['.zed', '.agents/skills'] },
{ name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' },
// Vendor-neutral target for assistants that read the shared `.agents` root.
// Detection keys off `.agents/skills` rather than the bare root: frameworks use
Expand Down
29 changes: 25 additions & 4 deletions src/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,13 +751,34 @@ export class InitCommand {
): ValidatedInitTool[] {
const validatedTools: ValidatedInitTool[] = [];

const reconciledToolIds = toolIds.includes('codex') && toolIds.includes('agents')
? toolIds.filter((toolId) => toolId !== 'agents')
const sharedAgentsTargets = ['codex', 'zed', 'agents'];
const selectedSharedTargets = sharedAgentsTargets.filter((toolId) => toolIds.includes(toolId));
// A Codex-rendered tree already serves Zed. Keep it when Zed is added later
// so Codex users do not lose the `$openspec-*` references they require.
const preserveConfiguredCodex = selectedSharedTargets.includes('zed') &&
toolStates.get('codex')?.configured;
const sharedTargetCandidates = preserveConfiguredCodex
? [...new Set([...selectedSharedTargets, 'codex'])]
: selectedSharedTargets;
const sharedTargetOwner = sharedTargetCandidates.includes('codex')
? 'codex'
: selectedSharedTargets.includes('zed')
? 'zed'
: selectedSharedTargets[0];
const firstSharedIndex = toolIds.findIndex((id) => sharedAgentsTargets.includes(id));
const reconciledToolIds = sharedTargetCandidates.length > 1
? toolIds.flatMap((toolId, index) => {
if (!sharedAgentsTargets.includes(toolId)) return [toolId];
return index === firstSharedIndex && sharedTargetOwner ? [sharedTargetOwner] : [];
})
: toolIds;
if (reconciledToolIds.length !== toolIds.length) {
if (
reconciledToolIds.length !== toolIds.length ||
reconciledToolIds.some((toolId, index) => toolId !== toolIds[index])
) {
console.log(
chalk.dim(
'Codex and agents share .agents/skills; writing one tree with Codex and generic skill references.'
`Codex, Zed, and agents share .agents/skills; writing one tree for ${sharedTargetOwner}.`
)
);
}
Expand Down
29 changes: 29 additions & 0 deletions test/cli-e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,35 @@ describe('openspec CLI e2e basics', () => {
expect(await fileExists(skillPath)).toBe(true);
});

it('initializes with --tools zed option', async () => {
const projectDir = await prepareFixture('tmp-init');
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
await fs.mkdir(emptyProjectDir, { recursive: true });

const result = await runCLI(['init', '--tools', 'zed'], { cwd: emptyProjectDir });
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('OpenSpec Setup Complete');
expect(result.stdout).toContain('Zed Agent');
expect(result.stdout).not.toContain('Restart your IDE');

const skillPath = path.join(emptyProjectDir, '.agents', 'skills', 'openspec-explore', 'SKILL.md');
expect(await fileExists(skillPath)).toBe(true);
expect(await fs.readFile(
path.join(emptyProjectDir, '.agents', 'skills', '.openspec-target'),
'utf-8'
)).toBe('zed\n');

const updateResult = await runCLI(['update'], { cwd: emptyProjectDir });
expect(updateResult.exitCode).toBe(0);
expect(await fs.readFile(
path.join(emptyProjectDir, '.agents', 'skills', '.openspec-target'),
'utf-8'
)).toBe('zed\n');
const updatedSkill = await fs.readFile(skillPath, 'utf-8');
expect(updatedSkill).toContain('/openspec-explore');
expect(updatedSkill).not.toContain('$openspec-explore');
});

it('initializes with --tools none option', async () => {
const projectDir = await prepareFixture('tmp-init');
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
Expand Down
15 changes: 15 additions & 0 deletions test/core/available-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ describe('available-tools', () => {
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('agents');
expect(toolValues).not.toContain('codex');
expect(toolValues).not.toContain('zed');
});

it('should detect Zed Agent from its project configuration directory', async () => {
await fs.mkdir(path.join(testDir, '.zed'), { recursive: true });

expect(getAvailableTools(testDir).map((tool) => tool.value)).toEqual(['zed']);
});

it('should not detect the shared agents target from a bare .agents directory', async () => {
Expand Down Expand Up @@ -157,6 +164,14 @@ describe('available-tools', () => {
const tools = getAvailableTools(testDir);
expect(tools.map((tool) => tool.value)).toContain('codex');
expect(tools.map((tool) => tool.value)).not.toContain('agents');
expect(tools.map((tool) => tool.value)).not.toContain('zed');
});

it('should use the shared-root marker to detect a configured Zed Agent target', async () => {
await fs.mkdir(path.join(testDir, '.agents', 'skills'), { recursive: true });
await fs.writeFile(path.join(testDir, '.agents', 'skills', '.openspec-target'), 'zed\n');

expect(getAvailableTools(testDir).map((tool) => tool.value)).toEqual(['zed']);
});

it('should preserve a global tool while reconciling a shared project root', async () => {
Expand Down
Loading
Loading