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

`--profile custom` uses whatever workflows are currently selected in global config (`openspec config profile`).

**Supported tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
**Supported tool IDs (`--tools`):** `amazon-q`, `antigravity`, `agents`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`

> 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 Expand Up @@ -141,6 +141,7 @@ openspec/
.claude/skills/ # Claude Code skills (if claude selected)
.cursor/skills/ # Cursor skills (if cursor selected)
.cursor/commands/ # Cursor OPSX commands (if delivery includes commands)
.agents/skills/ # Shared AGENTS-compatible skills (if agents selected)
... (other tool configs)
```

Expand Down
3 changes: 2 additions & 1 deletion docs/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-arch
|-----------|---------------------|----------------------|
| Amazon Q Developer (`amazon-q`) | `.amazonq/skills/openspec-*/SKILL.md` | `.amazonq/prompts/opsx-<id>.md` |
| Antigravity (`antigravity`) | `.agent/skills/openspec-*/SKILL.md` | `.agent/workflows/opsx-<id>.md` |
| AGENTS.md-compatible tools (`agents`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based instructions from your AGENTS-compatible assistant) |
| Auggie (`auggie`) | `.augment/skills/openspec-*/SKILL.md` | `.augment/commands/opsx-<id>.md` |
| IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-<id>.md` |
| Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/<id>.md` |
Expand Down Expand Up @@ -75,7 +76,7 @@ openspec init --tools none
openspec init --profile core
```

**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `vibe`, `windsurf`
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `agents`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `vibe`, `windsurf`

## Workflow-Dependent Installation

Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-init-agents-target/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-29
16 changes: 16 additions & 0 deletions openspec/changes/add-init-agents-target/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Why

OpenSpec already hints at a vendor-neutral `agents` target, but `openspec init` does not let users choose it. Users who want shared AGENTS-compatible skills currently have to patch OpenSpec or hand-copy skills.

## What Changes

- Make `agents` a first-class `openspec init` target.
- Install skills to `.agents/skills/openspec-*/SKILL.md`.
- Keep this PR scoped to init/update/detection/docs for the shared target only.
- Do not migrate `.pi` or `.codex` content in this change.

## Non-Goals

- No `.pi` -> `.agents` migration logic.
- No `.codex` -> `.agents` migration logic.
- No new command adapter for `agents`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Purpose

OpenSpec SHALL support a vendor-neutral shared skills directory for AGENTS-compatible assistants.

## MODIFIED Requirements

### Requirement: AGENTS-compatible installs use the shared `.agents` root
OpenSpec SHALL treat `agents` as a first-class skills target rooted at `.agents`.

#### Scenario: Shared agents install path
- **WHEN** OpenSpec generates skills for `agents`
- **THEN** each generated skill SHALL be written under `.agents/skills/openspec-*/SKILL.md`

#### Scenario: Detect an existing shared agents install
- **WHEN** `.agents/skills/openspec-explore/SKILL.md` exists in the project
- **THEN** OpenSpec detects `agents` as a configured skills target

### Requirement: Legacy tool migrations remain unchanged
OpenSpec SHALL leave `.pi` and `.codex` migration behavior unchanged in this change.

#### Scenario: Existing legacy tools
- **WHEN** a project already uses `.pi` or `.codex`
- **THEN** enabling `agents` does not add new migration or cleanup behavior for those tool roots
21 changes: 21 additions & 0 deletions openspec/changes/add-init-agents-target/specs/cli-init/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Purpose

The init command SHALL allow users to choose a shared `agents` skills target in the same way they choose tool-specific targets such as `claude`, `cursor`, `pi`, or `codex`.

## MODIFIED Requirements

### Requirement: Init accepts the shared agents target
`openspec init` SHALL accept `agents` anywhere tool IDs are selected or passed through `--tools`.

#### Scenario: Non-interactive agents setup
- **WHEN** the user runs `openspec init --tools agents`
- **THEN** OpenSpec creates skills under `.agents/skills/openspec-*/SKILL.md`
- **AND** OpenSpec does not fail just because `agents` has no command adapter

#### Scenario: Interactive agents setup
- **WHEN** the user selects `agents` from the init tool picker
- **THEN** OpenSpec configures `.agents/skills` using the same workflow/profile rules as other skill targets

#### Scenario: Deprecated init alias accepts agents
- **WHEN** the user runs the deprecated `experimental --tool agents` path
- **THEN** OpenSpec treats it the same as the equivalent init tool selection
11 changes: 11 additions & 0 deletions openspec/changes/add-init-agents-target/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 1. Tests
- [ ] 1.1 Add failing tests for `agents` init, detection, update, and deprecated alias coverage

## 2. Registry
- [ ] 2.1 Enable `agents` in `src/core/config.ts` with `skillsDir: '.agents'`

## 3. Docs
- [ ] 3.1 Update `docs/cli.md` and `docs/supported-tools.md`

## 4. Verification
- [ ] 4.1 Run focused Vitest coverage and `pnpm run build`
2 changes: 1 addition & 1 deletion src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' },
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: true, successLabel: 'your AGENTS.md-compatible assistant', skillsDir: '.agents' }
];
13 changes: 13 additions & 0 deletions test/cli-e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,19 @@ describe('openspec CLI e2e basics', () => {
expect(await fileExists(cursorSkillPath)).toBe(false); // Not selected
});

it('initializes with --tools agents 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', 'agents'], { cwd: emptyProjectDir });
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('OpenSpec Setup Complete');

const skillPath = path.join(emptyProjectDir, '.agents', 'skills', 'openspec-explore', 'SKILL.md');
expect(await fileExists(skillPath)).toBe(true);
});

it('initializes with --tools none option', async () => {
const projectDir = await prepareFixture('tmp-init');
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
Expand Down
20 changes: 15 additions & 5 deletions test/commands/artifact-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@ import { FileSystemUtils } from '../../src/utils/file-system.js';
describe('artifact-workflow CLI commands', () => {
let tempDir: string;
let changesDir: string;
let configTempDir: string;
let originalEnv: NodeJS.ProcessEnv;

const canonical = (targetPath: string): string => FileSystemUtils.canonicalizeExistingPath(targetPath);

beforeEach(async () => {
originalEnv = { ...process.env };
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-artifact-workflow-'));
configTempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-artifact-config-'));
process.env.XDG_CONFIG_HOME = configTempDir;
changesDir = path.join(tempDir, 'openspec', 'changes');
await fs.mkdir(changesDir, { recursive: true });
});

afterEach(async () => {
process.env = originalEnv;
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
if (configTempDir) {
await fs.rm(configTempDir, { recursive: true, force: true });
}
});

/**
Expand Down Expand Up @@ -719,14 +728,15 @@ artifacts:
expect(output).toContain('Invalid tool(s): unknown-tool');
});

it('errors for tool without skillsDir', async () => {
// Using 'agents' which doesn't have skillsDir configured
it('creates skills for the shared agents target', async () => {
const result = await runCLI(['experimental', '--tool', 'agents'], {
cwd: tempDir,
});
expect(result.exitCode).toBe(1);
const output = getOutput(result);
expect(output).toContain('Invalid tool(s): agents');
expect(result.exitCode).toBe(0);

const skillFile = path.join(tempDir, '.agents', 'skills', 'openspec-explore', 'SKILL.md');
const stat = await fs.stat(skillFile);
expect(stat.isFile()).toBe(true);
});

it('creates skills for Claude tool', async () => {
Expand Down
11 changes: 8 additions & 3 deletions test/core/available-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,19 @@ describe('available-tools', () => {
});

it('should only return tools that have a skillsDir property', async () => {
// .agents value has no skillsDir in AI_TOOLS config
// Create directories for both a valid and the agents case
await fs.mkdir(path.join(testDir, '.claude'), { recursive: true });

const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('claude');
expect(toolValues).not.toContain('agents');
});

it('should detect the shared agents directory', async () => {
await fs.mkdir(path.join(testDir, '.agents'), { recursive: true });

const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('agents');
});

it('should return full AIToolOption objects', async () => {
Expand Down
12 changes: 12 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ describe('InitCommand', () => {
expect(await fileExists(skillFile)).toBe(true);
});

it('should create skills in shared .agents skills directory', async () => {
const initCommand = new InitCommand({ tools: 'agents', force: true });

await initCommand.execute(testDir);

const skillFile = path.join(testDir, '.agents', 'skills', 'openspec-explore', 'SKILL.md');
expect(await fileExists(skillFile)).toBe(true);

const commandsDir = path.join(testDir, '.agents', 'commands');
expect(await directoryExists(commandsDir)).toBe(false);
});

it('should support Kimi CLI as an adapterless skills-only tool', async () => {
saveGlobalConfig({
featureFlags: {},
Expand Down
1 change: 1 addition & 0 deletions test/core/shared/tool-detection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('tool-detection', () => {
expect(tools).toContain('claude');
expect(tools).toContain('cursor');
expect(tools).toContain('windsurf');
expect(tools).toContain('agents');
expect(tools.length).toBeGreaterThan(0);
});
});
Expand Down
15 changes: 15 additions & 0 deletions test/core/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,21 @@ Old instructions content
expect(exists).toBe(false);
}
});

it('should update skill files for configured shared agents target', async () => {
const skillsDir = path.join(testDir, '.agents', 'skills');
const exploreSkillDir = path.join(skillsDir, 'openspec-explore');
await fs.mkdir(exploreSkillDir, { recursive: true });
await fs.writeFile(path.join(exploreSkillDir, 'SKILL.md'), 'old content');

await updateCommand.execute(testDir);

const updatedSkill = await fs.readFile(
path.join(exploreSkillDir, 'SKILL.md'),
'utf-8'
);
expect(updatedSkill).toContain('name: openspec-explore');
});
});

describe('command updates', () => {
Expand Down