Skip to content
2 changes: 1 addition & 1 deletion docs/how-commands-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ works too, for the tools that surface slash commands at all.
When you run `openspec init` (or `openspec update`), OpenSpec writes small files into your project so your AI tool can find the workflow. Depending on your tool and settings, these are **skills**, **commands**, or both.

- **Skills** live in places like `.claude/skills/openspec-*/SKILL.md`. They're the emerging cross-tool standard: a folder of instructions your assistant auto-detects.
- **Commands** live in places like `.cursor/commands/opsx-<id>.md` or `.claude/commands/opsx/<id>.md` — the layout is the tool's, and it decides how you type the command. They're the older per-tool slash command files. Codex does not get generated command files; use `.codex/skills/openspec-*`.
- **Commands** live in places like `.cursor/commands/opsx-<id>.md` or `.claude/commands/opsx/<id>.md` — the layout is the tool's, and it decides how you type the command. They're the older per-tool slash command files. Codex does not get generated command files; use `.agents/skills/openspec-*`.

You don't have to care which one your tool uses. You just type the slash command and it works. But knowing these files exist helps when something goes wrong: if your commands vanish, it usually means these files are missing or stale, and `openspec update` regenerates them.

Expand Down
6 changes: 3 additions & 3 deletions docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Only OpenSpec-managed files that are being replaced:
- Cline: `.clinerules/workflows/openspec-*.md`
- Roo: `.roo/commands/openspec-*.md`
- GitHub Copilot: `.github/prompts/openspec-*.prompt.md` (IDE extensions only; not supported in Copilot CLI)
- Codex: OpenSpec now uses `.codex/skills/openspec-*`; legacy cleanup only targets OpenSpec's allowlisted prompt filenames in `$CODEX_HOME/prompts` or `~/.codex/prompts`, and only removes them after replacement skills exist.
- Codex: OpenSpec now uses the canonical `.agents/skills/openspec-*` path. OpenSpec-managed `SKILL.md` files under the former `.codex/skills` path are reconciled only after replacements exist; custom files and divergent copies stay in place. If an unmarked `.agents` tree already contains OpenSpec skills, OpenSpec preserves its existing Codex (`$openspec-*`) or generic (`/openspec-*`) rendering instead of guessing from the legacy directory. Select `codex` explicitly with `openspec init` to switch ownership. Legacy prompt cleanup still targets only OpenSpec's allowlisted filenames in `$CODEX_HOME/prompts` or `~/.codex/prompts`.
- And others (Augment, Continue, Amazon Q, etc.)

The migration detects whichever tools you have configured and cleans up their legacy files.
Expand Down Expand Up @@ -157,7 +157,7 @@ openspec init --force --tools claude

The `--force` flag skips prompts and auto-accepts cleanup.

This includes cleanup of OpenSpec-managed Codex prompt files in the global Codex prompt directory. Cleanup only targets OpenSpec's allowlisted legacy Codex prompt filenames, removes them only after replacement `.codex/skills/openspec-*` skills exist, and preserves all other files.
This includes cleanup of OpenSpec-managed Codex prompt files in the global Codex prompt directory. Cleanup only targets OpenSpec's allowlisted legacy Codex prompt filenames, removes them only after replacement `.agents/skills/openspec-*` skills exist, and preserves all other files.

---

Expand Down Expand Up @@ -411,7 +411,7 @@ OPSX uses the emerging **skills** standard:

Skills are recognized across multiple AI coding tools and provide richer metadata.

Codex is skills-only in OPSX. OpenSpec no longer generates Codex custom prompt files; use the generated `.codex/skills/openspec-*` directories instead.
Codex is skills-only in OPSX. OpenSpec no longer generates Codex custom prompt files; use the generated `.agents/skills/openspec-*` directories instead.

---

Expand Down
24 changes: 18 additions & 6 deletions docs/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ For each selected tool, OpenSpec can install:
1. **Skills** (if delivery includes skills): `.../skills/openspec-*/SKILL.md`
2. **Commands** (if delivery includes commands): tool-specific `opsx-*` command files

Codex is skills-only: OpenSpec installs `.codex/skills/openspec-*/SKILL.md` for Codex even when delivery is set to `commands`, and it does not generate Codex custom prompt files.
Codex is skills-only: OpenSpec installs `.agents/skills/openspec-*/SKILL.md` for Codex even when delivery is set to `commands`, and it does not generate Codex custom prompt files. Existing OpenSpec-managed skills under the legacy `.codex/skills` path are reconciled after their replacements are written; custom and divergent files are preserved.

By default, OpenSpec uses the `core` profile, which includes:
- `propose`
Expand Down Expand Up @@ -72,7 +72,7 @@ to read the hint.
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.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/<id>.md` |
| Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | Not generated (skills-only; use `.codex/skills/openspec-*`) |
| Codex (`codex`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (skills-only; use `$openspec-*`) |
| Devin Desktop, formerly Windsurf (`devin`) | `.devin/skills/openspec-*/SKILL.md` | `.devin/workflows/opsx-<id>.md`\*\*\*\* |
| 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` |
Expand Down Expand Up @@ -117,7 +117,12 @@ shared root many agent tools read, instead of a tool-specific directory.
| Several agents on one repo, all reading `.agents/skills` | `agents` — one skill tree instead of one per tool |
| Your tool isn't listed yet but reads `.agents/skills` | `agents` |

Selecting it alongside a tool-specific ID is fine; each writes to its own root.
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
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.
OpenSpec also offers it automatically once a project has a `.agents/skills/`
directory — a bare `.agents/` is not enough, since tools use that root for rules
and subagent definitions too. Note `.agents` is not `.agent`: the singular
Expand All @@ -138,9 +143,16 @@ Two things to know:

Because `.agents/skills/` is shared, it is worth knowing what OpenSpec claims there:
it writes, refreshes, and removes only the `openspec-*` skill directories for your
selected workflows. Anything else in that directory is left alone. Treat the
`openspec-*` names as OpenSpec's — edits inside them are replaced on the next
`openspec update`, the same as for every other tool.
selected workflows, plus an `.openspec-target` marker that records whether Codex
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.

For pre-marker projects, OpenSpec infers ownership from managed skill references:
`$openspec-*` means Codex and `/openspec-*` means the vendor-neutral target. A
generic canonical tree alongside legacy `.codex/skills` is treated as an older
dual-target install and consolidated into the compatible shared tree.

## Non-Interactive Setup

Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ You're in CI or a non-interactive shell, and OpenSpec found old files to clean u
openspec init --force
```

For Codex, OpenSpec may detect old managed prompt files in `$CODEX_HOME/prompts` or `~/.codex/prompts`. That cleanup is limited to OpenSpec's allowlisted legacy Codex prompt filenames, and non-interactive `openspec init` removes only the files whose replacement `.codex/skills/openspec-*` skills exist. Non-interactive `openspec update` leaves all legacy cleanup untouched unless you pass `--force`.
For Codex, OpenSpec may detect old managed prompt files in `$CODEX_HOME/prompts` or `~/.codex/prompts`. That cleanup is limited to OpenSpec's allowlisted legacy Codex prompt filenames, and non-interactive `openspec init` removes only the files whose replacement `.agents/skills/openspec-*` skills exist. Non-interactive `openspec update` leaves all legacy cleanup untouched unless you pass `--force`.

### Commands didn't appear after migrating

Expand Down
4 changes: 3 additions & 1 deletion src/core/available-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import path from 'path';
import * as fs from 'fs';
import { AI_TOOLS, type AIToolOption } from './config.js';
import { reconcileSharedSkillTargets } from './shared-skill-target.js';

/**
* Scans the project path for AI tool configuration directories and returns
Expand All @@ -18,7 +19,7 @@ import { AI_TOOLS, type AIToolOption } from './config.js';
* the project root. Only tools with a `skillsDir` property are considered.
*/
export function getAvailableTools(projectPath: string): AIToolOption[] {
return AI_TOOLS.filter((tool) => {
const available = AI_TOOLS.filter((tool) => {
if (!tool.skillsDir) return false;

if (tool.detectionPaths && tool.detectionPaths.length > 0) {
Expand All @@ -40,4 +41,5 @@ export function getAvailableTools(projectPath: string): AIToolOption[] {
return false;
}
});
return reconcileSharedSkillTargets(projectPath, available);
}
18 changes: 17 additions & 1 deletion src/core/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
export const OPENSPEC_DIR_NAME = 'openspec';

export const OPENSPEC_SKILL_NAMES = [
'openspec-explore',
'openspec-new-change',
'openspec-continue-change',
'openspec-apply-change',
'openspec-update-change',
'openspec-ff-change',
'openspec-sync-specs',
'openspec-archive-change',
'openspec-bulk-archive-change',
'openspec-verify-change',
'openspec-onboard',
'openspec-propose',
] as const;

export const OPENSPEC_MARKERS = {
start: '<!-- OPENSPEC:START -->',
end: '<!-- OPENSPEC:END -->'
Expand All @@ -15,6 +30,7 @@ export interface AIToolOption {
available: boolean;
successLabel?: string;
skillsDir?: string; // e.g., '.claude' - /skills suffix per Agent Skills spec
legacySkillsDirs?: string[]; // Former roots read for detection and migrated after replacement
detectionPaths?: string[]; // Override skillsDir for auto-detection; any path existing triggers detection
setupNote?: string; // Manual setup required before the tool picks up generated files; shown after init/update
}
Expand All @@ -27,7 +43,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ 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: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.agents', legacySkillsDirs: ['.codex'], detectionPaths: ['.agents/skills', '.codex/skills'] },
{ name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'] },
{ name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
Expand Down
32 changes: 31 additions & 1 deletion src/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
import { getGlobalConfig, type Delivery, type Profile } from './global-config.js';
import { getProfileWorkflows, CORE_WORKFLOWS, ALL_WORKFLOWS } from './profiles.js';
import { getAvailableTools } from './available-tools.js';
import { writeSharedSkillTarget } from './shared-skill-target.js';
import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
import {
resolveCommandSurfaceCapability,
Expand Down Expand Up @@ -599,7 +600,18 @@ export class InitCommand {
): Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }> {
const validatedTools: Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }> = [];

for (const toolId of toolIds) {
const reconciledToolIds = toolIds.includes('codex') && toolIds.includes('agents')
? toolIds.filter((toolId) => toolId !== 'agents')
: toolIds;
if (reconciledToolIds.length !== toolIds.length) {
console.log(
chalk.dim(
'Codex and agents share .agents/skills; writing one tree with Codex and generic skill references.'
)
);
}

for (const toolId of reconciledToolIds) {
const tool = AI_TOOLS.find((t) => t.value === toolId);
if (!tool) {
const validToolIds = getToolsWithSkillsDir();
Expand Down Expand Up @@ -742,10 +754,14 @@ export class InitCommand {
FileSystemUtils.assertProjectArtifactPath(projectPath, skillFile);
await FileSystemUtils.writeFile(skillFile, skillContent);
}
writeSharedSkillTarget(projectPath, tool.value);
}
if (shouldRemoveSkillsForTool(tool.value, delivery)) {
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
removedSkillCount += await this.removeSkillDirs(projectPath, skillsDir);
// Retain an explicit selection even when this delivery mode produces
// no skills, so a divergent legacy sibling cannot reclaim ownership.
writeSharedSkillTarget(projectPath, tool.value);
}

// Generate commands if delivery includes commands
Expand Down Expand Up @@ -783,6 +799,20 @@ export class InitCommand {
}
}

for (const tool of [...createdTools, ...refreshedTools]) {
for (const migration of migrateLegacyToolDirs(
projectPath,
[tool.value],
'after-generation'
)) {
if (hasMovableContent(migration)) {
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
}
const kept = keptInPlaceNotice(migration);
if (kept) console.log(chalk.dim(kept));
}
}

return {
createdTools,
refreshedTools,
Expand Down
Loading
Loading