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 @@ -124,7 +124,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 @@ -145,9 +150,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
16 changes: 13 additions & 3 deletions 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';
import { SKILL_NAMES } from './shared/tool-detection.js';
import { resolveToolSkillsDir, toolSupportsSkills } from './shared/skill-paths.js';

Expand All @@ -16,11 +17,11 @@ import { resolveToolSkillsDir, toolSupportsSkills } from './shared/skill-paths.j
* the tools that are present.
*
* For tools with `detectionPaths`, checks those specific paths (files or
* directories). Otherwise checks for the tool's `skillsDir` directory at
* the project root. Only tools with a `skillsDir` property are considered.
* directories). Otherwise checks the project's `skillsDir`, or managed skill
* files in the user's home directory for a global skill target.
*/
export function getAvailableTools(projectPath: string): AIToolOption[] {
return AI_TOOLS.filter((tool) => {
const available = AI_TOOLS.filter((tool) => {
if (!toolSupportsSkills(tool)) return false;

if (tool.globalSkillsDir) {
Expand Down Expand Up @@ -51,4 +52,13 @@ export function getAvailableTools(projectPath: string): AIToolOption[] {
return false;
}
});
const activeProjectTools = new Set(
reconcileSharedSkillTargets(
projectPath,
available.filter((tool) => tool.skillsDir)
).map((tool) => tool.value)
);
return available.filter(
(tool) => tool.globalSkillsDir || activeProjectTools.has(tool.value)
);
}
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
globalSkillsDir?: string; // e.g., '.minimax' - /skills suffix, resolved from the user's home directory
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 @@ -28,7 +44,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 @@ -54,6 +54,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 @@ -614,7 +615,18 @@ export class InitCommand {
): ValidatedInitTool[] {
const validatedTools: ValidatedInitTool[] = [];

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 @@ -759,9 +771,13 @@ export class InitCommand {
FileSystemUtils.assertPathWithin(tool.skillsRoot, skillFile);
await FileSystemUtils.writeFile(skillFile, skillContent);
}
writeSharedSkillTarget(projectPath, tool.value);
}
if (shouldRemoveSkillsForTool(tool.value, delivery) && !tool.isGlobalSkillTarget) {
removedSkillCount += await this.removeSkillDirs(tool.skillsRoot, tool.skillsPath);
// 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 @@ -802,6 +818,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