diff --git a/.changeset/add-dsh-support.md b/.changeset/add-dsh-support.md new file mode 100644 index 0000000000..35ccd909cd --- /dev/null +++ b/.changeset/add-dsh-support.md @@ -0,0 +1,5 @@ +--- +"@fission-ai/openspec": minor +--- + +- **DeepSeek Harness** — `openspec init --tools dsh` (command-line id `dsh`) installs the OpenSpec workflow skills into `.dsh/skills/` for DeepSeek Harness. It is skills-only (no command adapter or command files): dsh discovers the generated `SKILL.md` files as its highest-priority project root and surfaces them through its skill catalog, `skill` tool, and `/openspec-*` user invocations. diff --git a/docs/cli.md b/docs/cli.md index d17c6d662f..0a91dccb9a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -109,9 +109,9 @@ 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`, `dsh`(shorthand for DeepSeek Harness), `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` -> This list mirrors `AI_TOOLS` in `src/core/config.ts`. See [Supported Tools](supported-tools.md) for each tool's skill and command paths. +> This list mirrors `AI_TOOLS` in `src/core/config.ts`. See [Supported Tools](supported-tools.md) for each tool's skill and command paths. **Examples:** diff --git a/docs/commands.md b/docs/commands.md index 473df68228..83dd3fe444 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, MiniMax Code, Mistral Vibe, shared `.agents` | +| none — skills only | `/openspec-propose`, `/openspec-apply-change` | CodeArts, DeepSeek Harness, ForgeCode, Hermes, MiniMax Code, 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 328eca6090..44282f47b9 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, shared `.agents` | +| none — skills only | `/openspec-propose` | CodeArts, DeepSeek Harness, 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, 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, DeepSeek Harness, 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 1ca52938b1..ef8e3473d7 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, MiniMax Code, Mistral Vibe, shared `.agents` | +| none — skills only | `/openspec-` | CodeArts, DeepSeek Harness, ForgeCode, Hermes, MiniMax Code, 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)) | @@ -75,6 +75,7 @@ to read the hint. | CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/.md` | | 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-.md`\*\*\*\* | +| DeepSeek Harness (`dsh`) | `.dsh/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | | 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-.prompt` | | CoStrict (`costrict`) | `.cospec/skills/openspec-*/SKILL.md` | `.cospec/openspec/commands/opsx-.md` | @@ -115,6 +116,15 @@ repo-local `.minimax` or `.mavis` directories. Commands-only delivery leaves existing global MiniMax Code skills untouched so one project's delivery setting cannot remove skills used by another project. +DeepSeek Harness is a project-local skills-only integration. Its command-line +tool id is `dsh` — for example, `openspec init --tools dsh`. OpenSpec +writes `.dsh/skills/openspec-*/SKILL.md`, which dsh scans as its highest-priority +project root. dsh also reads the shared `.agents/skills` root at a lower +priority, so a project that configures both keeps the dedicated `.dsh` tree +without the two writers clobbering each other. dsh surfaces skills to the model +through its skill catalog and `skill` tool, and supports user invocations such +as `/openspec-propose`. + ### GitHub Copilot cloud coding agent GitHub's [Copilot coding agent](https://docs.github.com/en/copilot/using-github-copilot/coding-agent) runs on GitHub in a GitHub Actions environment — separate from Copilot in your editor. OpenSpec can set it up to use the OpenSpec CLI by generating two files: @@ -206,7 +216,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`, `dsh`, `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` ## Workflow-Dependent Installation diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1097aaadc0..5c2afb05f3 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, 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, DeepSeek Harness, 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/archive/2026-08-15-add-dsh-support/.openspec.yaml b/openspec/changes/archive/2026-08-15-add-dsh-support/.openspec.yaml new file mode 100644 index 0000000000..0c73c8f54e --- /dev/null +++ b/openspec/changes/archive/2026-08-15-add-dsh-support/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-15 diff --git a/openspec/changes/archive/2026-08-15-add-dsh-support/design.md b/openspec/changes/archive/2026-08-15-add-dsh-support/design.md new file mode 100644 index 0000000000..2efa698e7c --- /dev/null +++ b/openspec/changes/archive/2026-08-15-add-dsh-support/design.md @@ -0,0 +1,87 @@ +## Context + +See proposal.md - Why. + +OpenSpec already routes every skill-capable tool through one pipeline: `AI_TOOLS` metadata in `src/core/config.ts` drives tool detection (`available-tools.ts`), selection and validation (`init.ts`), skill path resolution (`shared/skill-paths.ts`), generation, version drift, and update. Tools that expose no custom command files simply have no `ToolCommandAdapter`, which `command-surface.ts` classifies as capability `none`. + +DeepSeek Harness parses skills from fixed local roots (see `.ref/dsh-skills-解析逻辑.md`): `/.dsh/skills` (rank 100), `/.agents/skills` (rank 200), and user-level `~/.dsh/skills` (rank 400). It discovers only one level (`//SKILL.md` or `/.md`), requires `name` (kebab-case) and non-empty `description` frontmatter, tolerates extra fields, and exposes skills to the model through `` plus a `skill` tool; users can also trigger them with the `/name` gesture. OpenSpec's generated `SKILL.md` files already satisfy every dsh constraint, so no template or frontmatter changes are needed. + +## Goals / Non-Goals + +**Goals:** + +- Add one `dsh` entry to `AI_TOOLS` that opts into the existing project-local skills pipeline. +- Make first-time setup, auto-detection, refresh, and profile/delivery drift work through existing generic code. +- Lock the dsh path and invocation behavior with focused tests. + +**Non-Goals:** + +- A dsh command adapter or any `.dsh/commands/` output — dsh has no file-based command surface. +- A global `~/.dsh/skills` install target — dsh has a higher-priority project root and OpenSpec manages per-project artifacts. +- Reclassifying dsh as `skills-invocable` in `command-surface.ts`; that belongs to the in-flight `add-tool-command-surface-capabilities` work. Until then dsh shares the current adapterless behavior of Rovo Dev CLI and Kimi Code. +- Changing generated skill templates or frontmatter. + +## Decisions + +### 1. Represent dsh as an adapterless, project-local tool entry + +Add to `src/core/config.ts`: + +```ts +{ + name: 'DeepSeek Harness', + value: 'dsh', + available: true, + successLabel: 'DeepSeek Harness', + skillsDir: '.dsh', + detectionPaths: ['.dsh/skills', '.dsh'], +}, +``` + +`resolveToolSkillsDir()` then resolves to `/.dsh/skills`, which is dsh's rank-100 project root. Nothing else in init/update/selection needs a code change because those paths derive from `AI_TOOLS`. + +Alternative considered: write to `~/.dsh/skills` via `globalSkillsDir`. Rejected because the project root outranks the user root, keeps artifacts repo-local and reviewable, and matches OpenSpec's project-scoped update/removal semantics (MiniMax Code's global-only design exists to work around a tool that only reads the user root, which is not dsh's case). + +### 2. Detect dsh from `.dsh/skills` and `.dsh` + +`detectionPaths: ['.dsh/skills', '.dsh']` mirrors Rovo Dev CLI's `['.rovodev/skills', '.rovodev']`. `.dsh/skills` is the actual dsh skill root; `.dsh` recognizes an existing dsh project config root even before any skill exists. + +Alternative considered: `.dsh/skills` only. Rejected as needlessly strict — `.dsh` is tool-specific (unlike the generic `.agents`), so a bare root is a meaningful signal. + +### 3. No command adapter; inherit capability `none` + +`resolveCommandSurfaceCapability('dsh')` returns `none` because no adapter is registered. Consequences, all existing generic behavior: + +- `delivery=both` / `skills`: skills generated; init reports `Commands skipped for: dsh (no adapter)`. +- `delivery=commands`: no dsh artifacts and the existing zero-artifact correction is printed. + +Alternative considered: special-case dsh as `skills-invocable` like Codex so commands-only delivery keeps skills. Semantically dsh's skill tool + `/name` gesture are invocable, but the current shipped model only special-cases Codex; widening it here would duplicate the open `add-tool-command-surface-capabilities` change and expand this change's test matrix. Deferred deliberately. + +### 4. Use the default `/openspec-*` skill reference spelling + +dsh's user-facing `/name` gesture makes `/openspec-propose` a real, typeable invocation, so the default transformer (`getSkillReferenceTransformer` fallback) is correct. The model side can call the `skill` tool by name regardless. + +Alternative considered: add `dsh` to `NATURAL_LANGUAGE_SKILL_TOOLS` (like Rovo). Rejected because Rovo has no slash-like gesture at all, while dsh documents `/name`. + +### 5. No shared-root ownership work + +`.dsh/skills` is used by no other `AI_TOOLS` entry, so `shared-skill-target.ts` marker/reconciliation logic does not apply. If the same repo also generates the `.agents` target, dsh will prefer its rank-100 `.dsh/skills` tree and there is no single-writer conflict to resolve. + +### 6. No frontmatter or template changes + +OpenSpec writes `---` first line, kebab-case `name`, non-empty `description`, one-level `/SKILL.md`, and only kebab-case extra fields. This satisfies dsh's fail-closed validation rules from `.ref` §3. Tests assert the generated file shape so a future template change cannot silently break dsh discovery. + +## Risks / Trade-offs + +- [Commands-only delivery leaves dsh with zero artifacts] → Mitigation: init/update already print the existing `delivery` correction for capability-`none` tools; docs list dsh as skills-only, and the deferred capability work is the real fix. +- [`.dsh` detection can fire on a stale empty directory after commands-only removal] → Mitigation: interactive init shows detected-but-unconfigured tools as unselected in extend mode; behavior matches Rovo and is a cosmetic pre-selection, never a forced write. +- [dsh fail-closed parsing could silently drop skills] → Mitigation: generated files already comply; the init regression test checks frontmatter shape, and manual smoke testing against a real dsh session is in tasks. +- [Same-name skills under `.dsh/skills` and `.agents/skills`] → Mitigation: dsh's rank ordering (100 < 200) deterministically prefers `.dsh/skills`; this is upstream behavior, documented in supported-tools. + +## Migration Plan + +Additive metadata change: no data migration and no rollback beyond reverting the entry. Projects using the shared `.agents` target today keep working; selecting `dsh` on a later `openspec init` writes the dedicated higher-priority root without touching `.agents`. + +## Open Questions + +_None._ diff --git a/openspec/changes/archive/2026-08-15-add-dsh-support/proposal.md b/openspec/changes/archive/2026-08-15-add-dsh-support/proposal.md new file mode 100644 index 0000000000..14e00a210f --- /dev/null +++ b/openspec/changes/archive/2026-08-15-add-dsh-support/proposal.md @@ -0,0 +1,31 @@ +## Why + +DeepSeek Harness discovers skills from fixed local roots, with `/.dsh/skills` as its highest-priority project root. OpenSpec supports many assistants but has no dedicated target for it today, so dsh users can only use the vendor-neutral shared `.agents` target or hand-place skills — losing the dedicated `.dsh` integration. + +## What Changes + +- Add DeepSeek Harness as a supported tool with id `dsh`, `skillsDir: '.dsh'`, and detection paths `.dsh/skills` and `.dsh`. +- Generate the OpenSpec workflow skills into `.dsh/skills/openspec-*/SKILL.md` for dsh via `openspec init --tools dsh` and `openspec update`. +- Keep dsh skills-only: no command adapter and no `.dsh/commands/` files, because dsh has no file-based custom command surface. +- Spell dsh skill references as `/openspec-*` (dsh supports the user `/name` gesture), matching the existing skills-only tool pattern. +- Document dsh in the supported tools and command syntax docs. +- Add regression tests for detection, path resolution, init, update, and invocation spelling. + +## Capabilities + +### New Capabilities + +_None._ + +### Modified Capabilities + +- `ai-tool-paths`: define the `.dsh` skills root and detection paths for DeepSeek Harness. + +## Impact + +- `src/core/config.ts` — add the `dsh` entry to `AI_TOOLS` +- `docs/supported-tools.md` — tool row, invocation table, and `--tools` id list +- `docs/cli.md` — supported `--tools` id list +- `docs/commands.md`, `docs/how-commands-work.md`, `docs/troubleshooting.md` — skills-only invocation tables and notes +- `test/core/available-tools.test.ts`, `test/core/shared/skill-paths.test.ts`, `test/core/shared/tool-detection.test.ts`, `test/core/init.test.ts`, `test/core/update.test.ts`, `test/utils/command-references.test.ts`, `test/core/command-generation/registry.test.ts` — targeted dsh coverage +- `.changeset/add-dsh-support.md` — release note diff --git a/openspec/changes/archive/2026-08-15-add-dsh-support/specs/ai-tool-paths/spec.md b/openspec/changes/archive/2026-08-15-add-dsh-support/specs/ai-tool-paths/spec.md new file mode 100644 index 0000000000..39e35b20b5 --- /dev/null +++ b/openspec/changes/archive/2026-08-15-add-dsh-support/specs/ai-tool-paths/spec.md @@ -0,0 +1,47 @@ +# ai-tool-paths Delta Specification + +## MODIFIED Requirements + +### Requirement: Path configuration for supported tools + +The `AI_TOOLS` array SHALL include `skillsDir` for tools that support the Agent Skills specification. + +#### Scenario: Claude Code paths defined + +- **WHEN** looking up the `claude` tool +- **THEN** `skillsDir` SHALL be `.claude` + +#### Scenario: Cursor paths defined + +- **WHEN** looking up the `cursor` tool +- **THEN** `skillsDir` SHALL be `.cursor` + +#### Scenario: Windsurf paths defined + +- **WHEN** looking up the `windsurf` tool +- **THEN** `skillsDir` SHALL be `.windsurf` + +#### Scenario: Kimi Code paths defined + +- **WHEN** looking up the `kimi` tool +- **THEN** `skillsDir` SHALL be `.kimi-code` +- **AND** OpenSpec-managed skills remaining under the legacy `.kimi/skills` directory SHALL be migrated to `.kimi-code/skills` during init and update, preserving user files + +#### Scenario: Hermes Agent paths defined + +- **WHEN** looking up the `hermes` tool +- **THEN** `skillsDir` SHALL be `.hermes` +- **AND** `setupNote` SHALL explain that project `.hermes/skills` must be added to `skills.external_dirs` in `~/.hermes/config.yaml` +- **AND** `openspec init` and `openspec update` SHALL display the note whenever `hermes` is configured + +#### Scenario: DeepSeek Harness paths defined + +- **WHEN** looking up the `dsh` tool +- **THEN** `skillsDir` SHALL be `.dsh` +- **AND** `detectionPaths` SHALL include `.dsh/skills` and `.dsh` +- **AND** OpenSpec SHALL write dsh skills under `/.dsh/skills/` using platform-native path joining + +#### Scenario: Tools without skillsDir + +- **WHEN** a tool has no `skillsDir` defined +- **THEN** skill generation SHALL error with message indicating the tool is not supported diff --git a/openspec/changes/archive/2026-08-15-add-dsh-support/tasks.md b/openspec/changes/archive/2026-08-15-add-dsh-support/tasks.md new file mode 100644 index 0000000000..8daf666801 --- /dev/null +++ b/openspec/changes/archive/2026-08-15-add-dsh-support/tasks.md @@ -0,0 +1,30 @@ +## 1. Tool Metadata + +- [x] 1.1 Add the `DeepSeek Harness` entry to `AI_TOOLS` in `src/core/config.ts` with `value: 'dsh'`, `skillsDir: '.dsh'`, and `detectionPaths: ['.dsh/skills', '.dsh']` +- [x] 1.2 Verify no other production code changes are required: init selection, `--tools` help, command surface capability, update drift, and shared-root handling must all derive from the new metadata + +## 2. Detection and Path Tests + +- [x] 2.1 Add `test/core/available-tools.test.ts` cases: detect `dsh` from `.dsh/skills` and from a bare `.dsh` directory; do not detect when neither exists +- [x] 2.2 Add a `test/core/shared/skill-paths.test.ts` case resolving `dsh` to `path.join(root, '.dsh', 'skills')` +- [x] 2.3 Add `test/core/shared/tool-detection.test.ts` cases: `getToolsWithSkillsDir()` includes `dsh`; skill status and configured-tool detection work for `.dsh/skills/openspec-*/SKILL.md` + +## 3. Generation and Update Tests + +- [x] 3.1 Add an `InitCommand` regression in `test/core/init.test.ts`: `--tools dsh` writes `.dsh/skills/openspec-explore/SKILL.md`, creates no `.dsh/commands`, logs the no-adapter skip, uses `/openspec-*` references in skill bodies and the getting-started hint, and the generated frontmatter satisfies dsh parsing (leading `---`, kebab-case name, non-empty description) +- [x] 3.2 Add an `UpdateCommand` regression in `test/core/update.test.ts`: refresh a stale dsh skill and verify a second update is idempotent +- [x] 3.3 Add `test/utils/command-references.test.ts` coverage that dsh uses the default `/openspec-*` form, and `test/core/command-generation/registry.test.ts` coverage that dsh has no command adapter + +## 4. Documentation + +- [x] 4.1 Update `docs/supported-tools.md`: add the dsh tool row, add dsh to the skills-only invocation row and the `--tools` id list, and explain that dsh reads `.dsh/skills` at higher priority than `.agents/skills` +- [x] 4.2 Update the supported `--tools` id list in `docs/cli.md` +- [x] 4.3 Update the skills-only syntax tables in `docs/commands.md` and `docs/how-commands-work.md`, and the skills-only tool list in `docs/troubleshooting.md` + +## 5. Release and Validation + +- [x] 5.1 Add `.changeset/add-dsh-support.md` with a minor bump describing `openspec init --tools dsh` +- [x] 5.2 Run `pnpm run lint`, `pnpm run build`, and the targeted vitest files for detection, paths, init, update, and command references +- [x] 5.3 Run the full test suite (`pnpm test`) and confirm cross-platform path assertions pass on Windows (no hardcoded separators in new tests) +- [x] 5.4 Run `openspec validate` for this change and fix any spec or change validation issues +- [x] 5.5 Manual smoke test in a temporary git project: `openspec init --tools dsh`, confirm `.dsh/skills/openspec-*/SKILL.md` files, start a dsh session and confirm the skills appear in the catalog and load via the skill tool or `/openspec-propose` diff --git a/openspec/specs/ai-tool-paths/spec.md b/openspec/specs/ai-tool-paths/spec.md index 4394812570..e1e0a5e40d 100644 --- a/openspec/specs/ai-tool-paths/spec.md +++ b/openspec/specs/ai-tool-paths/spec.md @@ -50,6 +50,13 @@ The `AI_TOOLS` array SHALL include `skillsDir` for tools that support the Agent - **AND** `setupNote` SHALL explain that project `.hermes/skills` must be added to `skills.external_dirs` in `~/.hermes/config.yaml` - **AND** `openspec init` and `openspec update` SHALL display the note whenever `hermes` is configured +#### Scenario: DeepSeek Harness paths defined + +- **WHEN** looking up the `dsh` tool +- **THEN** `skillsDir` SHALL be `.dsh` +- **AND** `detectionPaths` SHALL include `.dsh/skills` and `.dsh` +- **AND** OpenSpec SHALL write dsh skills under `/.dsh/skills/` using platform-native path joining + #### Scenario: Tools without skillsDir - **WHEN** a tool has no `skillsDir` defined diff --git a/src/core/config.ts b/src/core/config.ts index 78dfac3074..d0a8e7dd80 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -47,6 +47,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Command Code', value: 'command-code', available: true, successLabel: 'Command Code', skillsDir: '.commandcode' }, { name: 'CodeArts', value: 'codeartsagent', available: true, successLabel: 'CodeArts', skillsDir: '.codeartsdoer' }, { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.agents', legacySkillsDirs: ['.codex'], detectionPaths: ['.agents/skills', '.codex/skills'] }, + { name: 'DeepSeek Harness', value: 'dsh', available: true, successLabel: 'DeepSeek Harness', skillsDir: '.dsh', detectionPaths: ['.dsh/skills', '.dsh'] }, { name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'], requiresIdeRestart: true }, { name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' }, { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' }, diff --git a/test/core/available-tools.test.ts b/test/core/available-tools.test.ts index 3678954a32..6478fa8fed 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -488,5 +488,32 @@ describe('available-tools', () => { expect(ohMyPiTool?.name).toBe('Oh My Pi'); expect(ohMyPiTool?.skillsDir).toBe('.omp'); }); + + it('should detect DeepSeek Harness when .dsh/skills exists', async () => { + // dsh discovers skills from /.dsh/skills, its rank-100 project root. + await fs.mkdir(path.join(testDir, '.dsh', 'skills'), { recursive: true }); + + const tools = getAvailableTools(testDir); + const dsh = tools.find((t) => t.value === 'dsh'); + expect(dsh).toMatchObject({ + name: 'DeepSeek Harness', + skillsDir: '.dsh', + detectionPaths: ['.dsh/skills', '.dsh'], + }); + }); + + it('should detect DeepSeek Harness from a bare .dsh project directory', async () => { + // .dsh is DeepSeek Harness's project config root, so its presence is a + // meaningful signal even before any skill directory exists. + await fs.mkdir(path.join(testDir, '.dsh'), { recursive: true }); + + const tools = getAvailableTools(testDir); + expect(tools.map((t) => t.value)).toContain('dsh'); + }); + + it('should not detect DeepSeek Harness when no .dsh signal exists', () => { + const tools = getAvailableTools(testDir); + expect(tools.map((t) => t.value)).not.toContain('dsh'); + }); }); }); diff --git a/test/core/command-generation/registry.test.ts b/test/core/command-generation/registry.test.ts index b5d150a23a..a1e6f9ec24 100644 --- a/test/core/command-generation/registry.test.ts +++ b/test/core/command-generation/registry.test.ts @@ -47,6 +47,7 @@ describe('command-generation/registry', () => { it('should return undefined for skills-only tools without adapters', () => { expect(CommandAdapterRegistry.get('codeartsagent')).toBeUndefined(); + expect(CommandAdapterRegistry.get('dsh')).toBeUndefined(); expect(CommandAdapterRegistry.get('hermes')).toBeUndefined(); expect(CommandAdapterRegistry.get('kimi')).toBeUndefined(); }); diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 42a60a79f6..0cb320a78f 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -620,6 +620,59 @@ describe('InitCommand', () => { expect(hintLine).toContain('the openspec-propose skill'); }); + it('should support DeepSeek Harness as an adapterless skills-only tool', async () => { + saveGlobalConfig({ + featureFlags: {}, + profile: 'core', + delivery: 'both', + }); + + const initCommand = new InitCommand({ tools: 'dsh', force: true }); + await initCommand.execute(testDir); + + const skillFile = path.join(testDir, '.dsh', 'skills', 'openspec-explore', 'SKILL.md'); + expect(await fileExists(skillFile)).toBe(true); + + const commandsDir = path.join(testDir, '.dsh', 'commands'); + expect(await directoryExists(commandsDir)).toBe(false); + + // dsh is fail-closed about frontmatter shape: the first line must be + // exactly `---`, name must be kebab-case, and description non-empty. + const exploreBody = await fs.readFile(skillFile, 'utf-8'); + expect(exploreBody.startsWith('---\n')).toBe(true); + expect(exploreBody).toMatch(/name: openspec-explore/); + expect(exploreBody).toMatch(/description: \S/); + + // dsh's user-facing `/name` gesture answers to `/openspec-*`, so no + // generated skill may reference `/opsx:` commands that dsh never loads. + const skillsRoot = path.join(testDir, '.dsh', 'skills'); + const skillDirs = await fs.readdir(skillsRoot); + expect(skillDirs.length).toBeGreaterThan(0); + for (const dir of skillDirs) { + const body = await fs.readFile(path.join(skillsRoot, dir, 'SKILL.md'), 'utf-8'); + expect(body, `${dir}/SKILL.md should not reference /opsx commands`).not.toMatch(/\/opsx[:-]/); + } + const applyBody = await fs.readFile( + path.join(skillsRoot, 'openspec-apply-change', 'SKILL.md'), + 'utf-8', + ); + expect(applyBody).toMatch(/\/openspec-archive-change/); + + const dshLogCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String); + expect(dshLogCalls.some((entry) => entry.includes('Created: DeepSeek Harness'))).toBe(true); + expect( + dshLogCalls.some( + (entry) => entry.includes('Commands skipped for: dsh') && entry.includes('(no adapter)'), + ), + ).toBe(true); + // The getting-started hint must use the skill name dsh actually loads + // when the user types it (`/openspec-propose`). + const hintLine = dshLogCalls.find((entry) => entry.includes('Start your first change')); + expect(hintLine).toBeDefined(); + expect(hintLine).toContain('/openspec-propose'); + expect(hintLine).not.toContain('/opsx:'); + }); + it('should support Hermes Agent as an adapterless skills-only tool with a setup note', async () => { saveGlobalConfig({ featureFlags: {}, diff --git a/test/core/shared/skill-paths.test.ts b/test/core/shared/skill-paths.test.ts index a2a1157a3a..e5496531af 100644 --- a/test/core/shared/skill-paths.test.ts +++ b/test/core/shared/skill-paths.test.ts @@ -34,4 +34,14 @@ describe('skill-paths', () => { path.join('/home/alex', '.minimax', 'skills') ); }); + + it('resolves DeepSeek Harness skills under the project .dsh root', () => { + const dsh = AI_TOOLS.find((tool) => tool.value === 'dsh'); + expect(dsh && toolSupportsSkills(dsh)).toBe(true); + if (!dsh || !toolSupportsSkills(dsh)) return; + + expect(resolveToolSkillsDir('/repo/app', dsh)).toBe( + path.join('/repo/app', '.dsh', 'skills') + ); + }); }); diff --git a/test/core/shared/tool-detection.test.ts b/test/core/shared/tool-detection.test.ts index 5905c87b73..63fe6362da 100644 --- a/test/core/shared/tool-detection.test.ts +++ b/test/core/shared/tool-detection.test.ts @@ -53,6 +53,7 @@ describe('tool-detection', () => { expect(tools).toContain('codeartsagent'); expect(tools).toContain('cursor'); expect(tools).toContain('devin'); + expect(tools).toContain('dsh'); // `--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'); @@ -140,6 +141,19 @@ describe('tool-detection', () => { expect(getToolSkillStatus(testDir, 'minimax-code').configured).toBe(false); }); + + it('should detect DeepSeek Harness skills under project .dsh/skills', async () => { + const skillDir = path.join(testDir, '.dsh', 'skills', 'openspec-explore'); + await fs.mkdir(skillDir, { recursive: true }); + await fs.writeFile(path.join(skillDir, 'SKILL.md'), 'test content'); + + expect(getToolSkillStatus(testDir, 'dsh')).toMatchObject({ + configured: true, + fullyConfigured: false, + skillCount: 1, + }); + expect(getConfiguredTools(testDir)).toEqual(['dsh']); + }); }); describe('getToolStates', () => { diff --git a/test/core/update.test.ts b/test/core/update.test.ts index b541bdadc1..8a13fc1ed1 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -169,6 +169,40 @@ Old instructions content consoleSpy.mockRestore(); }); + it('should refresh configured DeepSeek Harness skills and stay idempotent', async () => { + const skillsDir = path.join(testDir, '.dsh', 'skills'); + const skillFile = path.join(skillsDir, 'openspec-explore', 'SKILL.md'); + await fs.mkdir(path.dirname(skillFile), { recursive: true }); + + const oldSkillContent = `--- +name: openspec-explore (old) +description: Old description +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "0.9" +--- + +Old instructions content +`; + await fs.writeFile(skillFile, oldSkillContent); + + await updateCommand.execute(testDir); + + const refreshed = await fs.readFile(skillFile, 'utf-8'); + expect(refreshed).toContain('name: openspec-explore'); + expect(refreshed).not.toContain('Old instructions content'); + expect(refreshed).toContain('license: MIT'); + + const consoleSpy = vi.spyOn(console, 'log'); + await updateCommand.execute(testDir); + expect(consoleSpy.mock.calls.flat().map(String).join('\n')).toContain('up to date'); + consoleSpy.mockRestore(); + + expect(await fs.readFile(skillFile, 'utf-8')).toBe(refreshed); + }); + it('should update MiniMax Code skills without touching unrelated global skills', async () => { const skillsDir = path.join(testDir, 'home', '.minimax', 'skills'); const exploreSkill = path.join(skillsDir, 'openspec-explore', 'SKILL.md'); diff --git a/test/utils/command-references.test.ts b/test/utils/command-references.test.ts index d5886f2dfe..3e5d575da9 100644 --- a/test/utils/command-references.test.ts +++ b/test/utils/command-references.test.ts @@ -217,6 +217,8 @@ describe('getSkillReferenceTransformer', () => { it('uses the default / form for tools without a custom prefix', () => { expect(getSkillReferenceTransformer('vibe')).toBe(transformToSkillReferences); expect(getSkillReferenceTransformer('hermes')('/opsx:apply')).toBe('/openspec-apply-change'); + // dsh supports the user-facing / gesture, so it keeps the default. + expect(getSkillReferenceTransformer('dsh')('/opsx:apply')).toBe('/openspec-apply-change'); }); it('uses /skill: for Kimi Code, per its documented invocation syntax', () => {