diff --git a/.changeset/add-agents-tool.md b/.changeset/add-agents-tool.md new file mode 100644 index 0000000000..1f6d3fc1ed --- /dev/null +++ b/.changeset/add-agents-tool.md @@ -0,0 +1,5 @@ +--- +"@fission-ai/openspec": minor +--- + +Add the vendor-neutral `agents` target: `openspec init --tools agents` installs the workflow skills to `.agents/skills/openspec-*/SKILL.md`, the shared location AGENTS.md-compatible assistants read. It is skills-only, so no slash commands are generated. Because `agents` is now a real target, `--tools all` includes it and creates `.agents/skills/` where it previously did not. diff --git a/docs/cli.md b/docs/cli.md index 04cb514d2d..8ea9cb2e4b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -107,7 +107,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`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode` +**Supported tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `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. @@ -144,6 +144,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 skills for AGENTS.md-compatible tools (if agents selected) ... (other tool configs) ``` diff --git a/docs/commands.md b/docs/commands.md index c6fa8841a9..4c15d4e9eb 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -673,7 +673,7 @@ Different AI tools use slightly different command syntax. Use the format that ma |--------------------------|----------------|---------------| | `.../commands/opsx/.*` | `/opsx:propose`, `/opsx:apply` | Claude Code, Gemini CLI, Crush | | `.../opsx-.*` | `/opsx-propose`, `/opsx-apply` | Cursor, Devin Desktop, Copilot (IDE), Trae, Oh My Pi | -| none — skills only | `/openspec-propose`, `/openspec-apply-change` | CodeArts, ForgeCode, Hermes, Mistral Vibe | +| none — skills only | `/openspec-propose`, `/openspec-apply-change` | CodeArts, ForgeCode, Hermes, Mistral Vibe, shared `.agents` | | none — Kimi Code | `/skill:openspec-propose` | Kimi Code | | none — Codex CLI | `$openspec-propose` | Codex | diff --git a/docs/how-commands-work.md b/docs/how-commands-work.md index 887dfacb5e..22e09dd77a 100644 --- a/docs/how-commands-work.md +++ b/docs/how-commands-work.md @@ -78,7 +78,7 @@ The intent is identical everywhere. The spelling follows the file your tool load | `.../commands/opsx/.*` | `/opsx:propose` | Claude Code, Gemini CLI, Crush | | `.../opsx-.*` | `/opsx-propose` | Cursor, GitHub Copilot (IDE), Devin Desktop, Trae, Oh My Pi | | `.amazonq/prompts/opsx-.md` | `@opsx-propose` | Amazon Q Developer | -| none — skills only | `/openspec-propose` | CodeArts, ForgeCode, Hermes, Mistral Vibe | +| none — skills only | `/openspec-propose` | CodeArts, ForgeCode, Hermes, Mistral Vibe, shared `.agents` | | none — Kimi Code | `/skill:openspec-propose` | Kimi Code | | none — Codex CLI | `$openspec-propose` | Codex | @@ -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) `/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, 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. diff --git a/docs/supported-tools.md b/docs/supported-tools.md index 71d0f1d947..eedac149b3 100644 --- a/docs/supported-tools.md +++ b/docs/supported-tools.md @@ -33,7 +33,7 @@ way it loads the file OpenSpec wrote. Find your tool's command path in the | `.../opsx-.*` — the filename is the command | `/opsx-` | Every other tool with generated command files, except Amazon Q and Devin | | `.devin/workflows/opsx-.md` — read by only one of Devin's two agents | `/opsx-` on Devin Desktop, `/openspec-` on Devin Local | Devin Desktop\*\*\*\* | | `.amazonq/prompts/opsx-.md` — a prompt, not a command | `@opsx-` | Amazon Q Developer | -| none — skills only | `/openspec-` | CodeArts, ForgeCode, Hermes, Mistral Vibe | +| none — skills only | `/openspec-` | CodeArts, ForgeCode, Hermes, Mistral Vibe, shared `.agents` | | none — Kimi Code | `/skill:openspec-` | Kimi Code | | none — Codex CLI | `$openspec-` | Codex ([`/openspec-` is not recognized](https://github.com/openai/codex/issues/11817)) | @@ -98,6 +98,7 @@ to read the hint. | [Zoo Code](https://github.com/Zoo-Code-Org/Zoo-Code) (`roocode`) | `.roo/skills/openspec-*/SKILL.md` | `.roo/commands/opsx-.md` | | Trae (`trae`) | `.trae/skills/openspec-*/SKILL.md` | `.trae/commands/opsx-.md` | | ZCode (`zcode`) | `.zcode/skills/openspec-*/SKILL.md` | `.zcode/commands/opsx/.md` | +| Shared `.agents` skills (`agents`) | `.agents/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | \*\* GitHub Copilot prompt files are recognized as custom slash commands in IDE extensions (VS Code, JetBrains, Visual Studio). Copilot CLI does not currently consume `.github/prompts/*.prompt.md` directly. @@ -105,6 +106,42 @@ to read the hint. \*\*\*\* Windsurf was [rebranded to Devin Desktop](https://docs.devin.ai/desktop/devin-desktop-faq) on June 2, 2026, and its config directory moved: `.devin/` is the preferred read + write location, `.windsurf/` a legacy read-only fallback. OpenSpec follows the rename — the tool id is `devin`, and `--tools windsurf` still resolves to it so existing setup scripts keep working. A project still holding OpenSpec files in `.windsurf/` is offered the move on the next `openspec update`; declining leaves them in place, and files you wrote yourself are never touched. Workflows are invoked by filename, so `.devin/workflows/opsx-apply.md` is `/opsx-apply`. The [Devin Local agent does not support workflows](https://docs.devin.ai/desktop/devin-local) — only skills, and it does not read `.windsurf/` at all — so whenever OpenSpec writes Devin skills it keeps their bodies, and the getting-started hint, on `/openspec-*` skill invocations, which work on both agents. Under commands-only delivery no skills are written and both fall back to `/opsx-*`. +### When to pick the shared `.agents` target + +`agents` is the vendor-neutral option: it writes skills to `.agents/skills/`, the +shared root many agent tools read, instead of a tool-specific directory. + +| Situation | Pick | +|-----------|------| +| Your tool has its own row above | Its own ID — you get that tool's integration, including slash commands where it supports them | +| 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. +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 +directory belongs to Antigravity. + +Two things to know: + +- **Skills only.** No command adapter exists, so no `opsx-*` command files are + written; with a commands-inclusive delivery mode `openspec init` lists `agents` + among the tools it reports under `Commands skipped for: … (no adapter)`. + Invoke the workflows by skill name — + most assistants that read `.agents/skills` spell that `/openspec-propose`, the form + OpenSpec's setup hint prints. The target is vendor-neutral, so check your + assistant's own docs if it uses another form. +- **No `AGENTS.md` is created or edited.** The target is the `.agents/` directory. + 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: +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. + ## Non-Interactive Setup For CI/CD or scripted setup, use `--tools` (and optionally `--profile`): @@ -123,7 +160,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`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode` +**Available tool IDs (`--tools`)** — `windsurf` is also accepted, as an alias for `devin`: `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `zcode`, `agents` ## Workflow-Dependent Installation diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 72f47a5e59..db4c4c740d 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -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 and Mistral Vibe 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. 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 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 diff --git a/openspec/changes/add-init-agents-target/.openspec.yaml b/openspec/changes/add-init-agents-target/.openspec.yaml new file mode 100644 index 0000000000..f205fc727f --- /dev/null +++ b/openspec/changes/add-init-agents-target/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-29 diff --git a/openspec/changes/add-init-agents-target/proposal.md b/openspec/changes/add-init-agents-target/proposal.md new file mode 100644 index 0000000000..44d6fb57e6 --- /dev/null +++ b/openspec/changes/add-init-agents-target/proposal.md @@ -0,0 +1,33 @@ +## Why + +`.agents/skills` has become the shared, vendor-neutral location modern agent tools read. OpenSpec already carried an `agents` entry in `AI_TOOLS`, but with `available: false` and no `skillsDir` it was unreachable — every real gate keys off `skillsDir`. Teams running several agents on one repo, or a tool with no first-class integration yet, had to generate for some other tool and move the files by hand (#1480), or pick a vendor target they do not use (#1104, #653). + +## What Changes + +- Enable `agents` in `AI_TOOLS` with `skillsDir: '.agents'`, making it selectable interactively and via `--tools agents`. +- Scope detection to `detectionPaths: ['.agents/skills']` so a bare `.agents/` written by another framework does not select — or silently install into — the target. +- Rename the entry to `Shared .agents skills`. The old label said "AGENTS.md", but OpenSpec writes no `AGENTS.md` — it strips its markers out of one. +- Document the target, including when to prefer it over a tool-specific integration. + +## Capabilities + +### New Capabilities + +_None._ + +### Modified Capabilities + +- `ai-tool-paths`: define the `.agents` skills root and its scoped detection path +- `cli-init`: record that the shared target installs skills and skips command generation + +## Impact + +- `src/core/config.ts` - enable the `agents` entry, scope detection, correct the label +- `.changeset/add-agents-tool.md` - minor release note, including the `--tools all` behavior change +- `docs/supported-tools.md`, `docs/cli.md`, `docs/commands.md`, `docs/how-commands-work.md`, `docs/troubleshooting.md` - list `agents` among skills-only tools and explain when to choose it +- `test/core/*`, `test/commands/*`, `test/cli-e2e/*` - cover init, update, detection, and the deprecated alias + +## Non-Goals + +- No command adapter for `agents`. There is no cross-vendor slash-command format, so commands stay skills-only (the Kimi/Hermes pattern). +- No `.pi`, `.codex`, or `.agent` migration into `.agents`. Moving vendor tools to the shared root is separate work (#830, #1157). diff --git a/openspec/changes/add-init-agents-target/specs/ai-tool-paths/spec.md b/openspec/changes/add-init-agents-target/specs/ai-tool-paths/spec.md new file mode 100644 index 0000000000..53c86cbc11 --- /dev/null +++ b/openspec/changes/add-init-agents-target/specs/ai-tool-paths/spec.md @@ -0,0 +1,23 @@ +# ai-tool-paths Delta Specification + +## ADDED Requirements + +### Requirement: Shared .agents skills target + +OpenSpec SHALL provide a vendor-neutral `agents` tool target rooted at the shared `.agents` directory, for assistants that read skills from the shared location rather than a vendor-specific one. + +#### Scenario: Shared agents target paths defined + +- **WHEN** looking up the `agents` tool +- **THEN** `skillsDir` SHALL be `.agents` + +#### Scenario: Detection keys off the shared skills subtree + +- **WHEN** a project contains a `.agents/skills` path +- **THEN** OpenSpec SHALL detect `agents` as an available target + +#### Scenario: A bare shared root does not select the target + +- **GIVEN** a project contains `.agents` but no `.agents/skills` path +- **WHEN** OpenSpec detects available tools +- **THEN** `agents` SHALL NOT be reported as available diff --git a/openspec/changes/add-init-agents-target/specs/cli-init/spec.md b/openspec/changes/add-init-agents-target/specs/cli-init/spec.md new file mode 100644 index 0000000000..d053495c9b --- /dev/null +++ b/openspec/changes/add-init-agents-target/specs/cli-init/spec.md @@ -0,0 +1,20 @@ +# cli-init Delta Specification + +## ADDED Requirements + +### Requirement: Shared .agents target initialization + +`openspec init` SHALL accept the shared `agents` target wherever tool IDs are selected, and SHALL treat it as a skills-only tool. + +#### Scenario: Non-interactive selection of the shared target + +- **WHEN** the user runs `openspec init --tools agents` +- **THEN** OpenSpec SHALL generate skills for the `agents` target +- **AND** initialization SHALL NOT fail because `agents` has no registered command adapter + +#### Scenario: Shared agents target skips command-file generation + +- **GIVEN** the configured delivery includes command generation +- **WHEN** the user selects the shared `agents` target during initialization +- **THEN** command-file generation SHALL be skipped because no `agents` adapter is registered +- **AND** `agents` SHALL be listed among the tools reported as having commands skipped diff --git a/openspec/changes/add-init-agents-target/tasks.md b/openspec/changes/add-init-agents-target/tasks.md new file mode 100644 index 0000000000..ad27c74363 --- /dev/null +++ b/openspec/changes/add-init-agents-target/tasks.md @@ -0,0 +1,21 @@ +## 1. Tests + +- [x] 1.1 Cover `agents` init, update, detection, and the deprecated `experimental --tool` alias +- [x] 1.2 Assert a bare `.agents/` directory does not select the target + +## 2. Registry + +- [x] 2.1 Enable `agents` in `src/core/config.ts` with `skillsDir: '.agents'` +- [x] 2.2 Scope detection with `detectionPaths: ['.agents/skills']` +- [x] 2.3 Rename the entry to `Shared .agents skills` so it names the directory instead of a file OpenSpec never writes + +## 3. Docs + +- [x] 3.1 Add `agents` to the tool ID lists in `docs/cli.md` and `docs/supported-tools.md` +- [x] 3.2 Add the Tool Directory row and the skills-only invocation rows across `docs/supported-tools.md`, `docs/commands.md`, `docs/how-commands-work.md`, and `docs/troubleshooting.md` +- [x] 3.3 Document when to choose the shared target over a tool-specific integration + +## 4. Verification + +- [x] 4.1 Run `pnpm run build` and the full Vitest suite +- [x] 4.2 Validate with `openspec validate --strict`, and confirm `openspec archive` applies cleanly against a scratch copy of `openspec/` diff --git a/src/core/config.ts b/src/core/config.ts index fd18c3f82e..8473ee5546 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -54,7 +54,13 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo' }, { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' }, { name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' }, - { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' } + // Vendor-neutral target for assistants that read the shared `.agents` root. + // Detection keys off `.agents/skills` rather than the bare root: frameworks use + // `.agents/` for more than skills, so the root alone says nothing about skills. + // A project that does keep skills there is a project this target fits, the same + // way `.claude/` selects Claude Code — the signal is the user's setup, not + // OpenSpec's own files. + { name: 'Shared .agents skills', value: 'agents', available: true, successLabel: 'shared .agents skills', skillsDir: '.agents', detectionPaths: ['.agents/skills'] } ]; /** diff --git a/test/cli-e2e/basic.test.ts b/test/cli-e2e/basic.test.ts index 22657513d8..3fceea6a40 100644 --- a/test/cli-e2e/basic.test.ts +++ b/test/cli-e2e/basic.test.ts @@ -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'); diff --git a/test/commands/artifact-workflow.test.ts b/test/commands/artifact-workflow.test.ts index 3927718137..6abfb4b8e1 100644 --- a/test/commands/artifact-workflow.test.ts +++ b/test/commands/artifact-workflow.test.ts @@ -1128,14 +1128,15 @@ operations: 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 () => { diff --git a/test/core/available-tools.test.ts b/test/core/available-tools.test.ts index ae20603613..e071148dd6 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -84,14 +84,30 @@ 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); + expect(tools.map((t) => t.value)).toContain('claude'); + // The filter's contract: nothing without a skillsDir can ever be returned. + expect(tools.filter((t) => !t.skillsDir)).toEqual([]); + }); + + it('should detect the shared agents target from .agents/skills', async () => { + await fs.mkdir(path.join(testDir, '.agents', 'skills'), { recursive: true }); + const tools = getAvailableTools(testDir); const toolValues = tools.map((t) => t.value); - expect(toolValues).toContain('claude'); - expect(toolValues).not.toContain('agents'); + expect(toolValues).toContain('agents'); + }); + + it('should not detect the shared agents target from a bare .agents directory', async () => { + // Frameworks use `.agents/` for more than skills (rules, subagent definitions). + // The bare root therefore says nothing about whether this project keeps agent + // skills in the shared location, so it must not select the target. + await fs.mkdir(path.join(testDir, '.agents', 'some-other-framework'), { recursive: true }); + + const tools = getAvailableTools(testDir); + expect(tools.map((t) => t.value)).not.toContain('agents'); }); it('should return full AIToolOption objects', async () => { diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 1ba3c1144a..995b7fce02 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -199,10 +199,35 @@ describe('InitCommand', () => { expect(cmdContent).toContain('category:'); expect(cmdContent).toContain('tags:'); - // .agents is a detection-only root and must never be created during generation + // ZCode writes only to its own root; selecting it must never create another + // tool's root, including the shared .agents target. expect(await directoryExists(path.join(testDir, '.agents'))).toBe(false); }); + it('should support the shared agents target as an adapterless skills-only tool', async () => { + saveGlobalConfig({ + featureFlags: {}, + profile: 'core', + delivery: 'both', + }); + + 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); + + const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String); + expect( + logCalls.some( + (entry) => entry.includes('Commands skipped for: agents') && entry.includes('(no adapter)'), + ), + ).toBe(true); + }); + it('should support Kimi Code as an adapterless skills-only tool', async () => { saveGlobalConfig({ featureFlags: {}, diff --git a/test/core/shared/tool-detection.test.ts b/test/core/shared/tool-detection.test.ts index 5b86abf0dd..c4f955f5c8 100644 --- a/test/core/shared/tool-detection.test.ts +++ b/test/core/shared/tool-detection.test.ts @@ -51,6 +51,9 @@ describe('tool-detection', () => { expect(tools).toContain('codeartsagent'); expect(tools).toContain('cursor'); expect(tools).toContain('devin'); + // `--tools all` resolves to exactly this list, so `agents` being here is what + // puts the shared target in an `--tools all` run. + expect(tools).toContain('agents'); expect(tools.length).toBeGreaterThan(0); }); }); diff --git a/test/core/update.test.ts b/test/core/update.test.ts index 52a669c094..40cf804906 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -293,6 +293,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', () => {