From f1079d8506398a844197f1bf67dfd260602e8217 Mon Sep 17 00:00:00 2001 From: Anthony Mendez <738092+0x6d6e647a@users.noreply.github.com> Date: Sat, 11 Jul 2026 01:52:12 -0700 Subject: [PATCH 1/2] feat: add Grok Build skills-only support --- docs/cli.md | 2 +- docs/commands.md | 2 +- docs/how-commands-work.md | 2 +- docs/supported-tools.md | 5 +- docs/troubleshooting.md | 2 +- .../.openspec.yaml | 2 + .../design.md | 109 ++++++++++++++++++ .../proposal.md | 40 +++++++ .../specs/ai-tool-paths/spec.md | 37 ++++++ .../specs/cli-init/spec.md | 43 +++++++ .../tasks.md | 24 ++++ openspec/specs/ai-tool-paths/spec.md | 5 + openspec/specs/cli-init/spec.md | 6 + src/core/config.ts | 1 + test/core/init.test.ts | 24 ++++ 15 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/.openspec.yaml create mode 100644 openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/design.md create mode 100644 openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/proposal.md create mode 100644 openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/specs/ai-tool-paths/spec.md create mode 100644 openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/specs/cli-init/spec.md create mode 100644 openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/tasks.md diff --git a/docs/cli.md b/docs/cli.md index c76ffb9add..f4fa72456d 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -109,7 +109,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`, `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`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `grok`, `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. diff --git a/docs/commands.md b/docs/commands.md index 473df68228..35b3cdf09d 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, ForgeCode, Grok Build, 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..da39692d22 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, ForgeCode, Grok Build, Hermes, Mistral Vibe, shared `.agents` | | none — Kimi Code | `/skill:openspec-propose` | Kimi Code | | none — Codex CLI | `$openspec-propose` | Codex | diff --git a/docs/supported-tools.md b/docs/supported-tools.md index 756a80e878..29e84a8a52 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, ForgeCode, Grok Build, 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)) | @@ -82,6 +82,7 @@ to read the hint. | Factory Droid (`factory`) | `.factory/skills/openspec-*/SKILL.md` | `.factory/commands/opsx-.md` | | Gemini CLI (`gemini`) | `.gemini/skills/openspec-*/SKILL.md` | `.gemini/commands/opsx/.toml` | | GitHub Copilot (`github-copilot`) | `.github/skills/openspec-*/SKILL.md` | `.github/prompts/opsx-.prompt.md`\*\* | +| Grok Build (`grok`) | `.grok/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | | Hermes Agent (`hermes`) | `.hermes/skills/openspec-*/SKILL.md`\*\*\* | Not generated (no command adapter; use skill-based `/openspec-*` invocations) | | iFlow (`iflow`) | `.iflow/skills/openspec-*/SKILL.md` | `.iflow/commands/opsx-.md` | | Junie (`junie`) | `.junie/skills/openspec-*/SKILL.md` | `.junie/commands/opsx-.md` | @@ -198,7 +199,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`, `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`, `codeartsagent`, `codex`, `devin`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `grok`, `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 b6e65eec82..839baa7a62 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, ForgeCode, Grok Build, 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 (including Grok Build). 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-07-11-add-grok-build-skills-only-support/.openspec.yaml b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/.openspec.yaml new file mode 100644 index 0000000000..68b717479d --- /dev/null +++ b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-11 diff --git a/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/design.md b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/design.md new file mode 100644 index 0000000000..54aed8cd8f --- /dev/null +++ b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/design.md @@ -0,0 +1,109 @@ +## Context + +Grok Build (CLI binary `grok`) is not a Claude/Codex-style command-file adapter target. Its documented extension model is skill-centric: + +- project skills from `./.grok/skills/` (walked up to the repo root) +- user skills from `~/.grok/skills/` +- plugin skills and optional `[skills] paths` in config +- user-invocable skills appear as slash commands: `/` +- core TUI commands (`/plan`, `/model`, `/skills`, …) are built-in, not project-generated files +- no documented project-local `.grok/commands/` layout for custom OpenSpec command generation + +Grok also has Claude/Cursor compatibility scanners that can free-ride existing `.claude/skills` or `.cursor/skills`. That is a personal workaround, not the product integration: OpenSpec should own a native `.grok` skills install so pure-Grok users and multi-tool projects get first-class init/update behavior. + +OpenSpec already represents this shape: + +- `AI_TOOLS` can advertise a `skillsDir` +- `init`/`update` install skills for any selected tool with `skillsDir` +- when command generation is attempted for a tool without an adapter, OpenSpec records `commandsSkipped` + +## Goals / Non-Goals + +**Goals:** + +- Add Grok Build using the same narrow skills-only pattern as Kimi CLI / ForgeCode / Mistral Vibe +- Keep the implementation small: metadata, docs, focused regression test, changeset +- Align specs with the existing adapterless code path + +**Non-Goals:** + +- designing a Grok-specific command adapter without a documented project command-file surface +- relying on Claude/Cursor free-ride as the supported integration +- changing tool capability modeling or `delivery=commands` behavior for all adapterless tools (tracked in `add-tool-command-surface-capabilities`) + +## Decisions + +### 1. Represent Grok Build as an adapterless tool with `.grok` + +Add a new `AI_TOOLS` entry: + +```ts +{ name: 'Grok Build', value: 'grok', available: true, successLabel: 'Grok Build', skillsDir: '.grok' } +``` + +Rationale for IDs: + +- `value: 'grok'` matches the CLI binary and the project directory `.grok` (same pattern as `claude` → `.claude`, `kimi` → `.kimi`) +- display name `Grok Build` matches xAI product naming +- alternatives considered: `grok-build` (product-accurate but inconsistent with other short tool IDs) + +### 2. Do not add a Grok command adapter + +No `src/core/command-generation/adapters/grok.ts`, and no registry change. + +Rationale: + +- skills are the documented custom extension surface and already become slash commands +- inventing `.grok/commands/...` would create OpenSpec behavior that cannot be justified against xAI docs +- existing adapterless path already skips command generation with an informational message + +### 3. Document Grok by its real invocation surface + +Grok docs in OpenSpec must use skill-name slash form: + +- supported-tools: no generated command files; use skill-based `/openspec-*` invocations +- commands / how-commands-work: examples such as `/openspec-propose`, `/openspec-apply-change` + +Do not claim generated `opsx-*` files or Claude-style `/opsx:propose` as Grok's primary surface. + +### 4. Treat Claude free-ride as out-of-scope workaround, not design + +Grok can discover Claude skills when compat scanners are enabled. Native `.grok` support remains required because: + +- pure Grok users may never select Claude +- free-ride couples Grok to Claude layout and can be disabled via Grok config/env +- OpenSpec update tracks configured tools by skillsDir presence; free-ride never registers Grok + +If both Claude and Grok are configured, duplicate skill discovery is acceptable; `.grok` remains the canonical OpenSpec target for Grok Build. + +### 5. Keep behavior aligned with current adapterless tools + +- skills are created whenever delivery includes skills +- command generation is skipped when no adapter exists +- init output reports `Commands skipped for: grok (no adapter)` +- update refreshes Grok when `.grok/skills/openspec-*` exists + +## Test Strategy + +Add one focused regression test in `test/core/init.test.ts`: + +- configure `delivery=both` +- run init with `--tools grok` +- verify skills under `.grok/skills/...` (use `path.join` for expectations) +- verify no `.grok/commands` directory is created +- verify init log includes skipped command generation for `grok` with `(no adapter)` (use relaxed `.some()` matching, as in the Kimi follow-up commit) + +That is enough because: + +- adapterless update behavior already has generic coverage +- CLI tool-id rendering is derived from `AI_TOOLS` +- no command adapter or path-formatting logic is introduced + +## Risks / Trade-offs + +| Risk | Mitigation | +|------|------------| +| Users confuse Claude free-ride with native support | Document native `.grok` path; optional brief note that Claude compat is separate | +| `delivery=commands` still not capability-aware for skills-only tools | Accept same limitation as Kimi/ForgeCode/Vibe; capability work is separate | +| Duplicate skills when both Claude and Grok selected | Acceptable; document that Grok may see both trees | +| xAI later documents project command files | Skills-only remains correct today; adapter can be added later without breaking skills | diff --git a/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/proposal.md b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/proposal.md new file mode 100644 index 0000000000..e2abb22e65 --- /dev/null +++ b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/proposal.md @@ -0,0 +1,40 @@ +## Why + +xAI Grok Build is a coding agent with a documented project skills root at `.grok/skills/`, and user-invocable skills surface as slash commands (`/`). OpenSpec does not yet list Grok Build as a supported tool, so users must free-ride on Claude/Cursor compat scanners or configure extra skill paths manually. + +OpenSpec already supports adapterless skills-only tools (Kimi CLI, ForgeCode, Mistral Vibe). Grok Build should follow that pattern: install skills under `.grok/skills/` without inventing a command adapter for a project command-file surface that xAI docs do not define. + +## What Changes + +- Add Grok Build as a supported tool in `AI_TOOLS` with `value: 'grok'` and `skillsDir: '.grok'` +- Document Grok Build as a skills-only integration (no generated `opsx-*` command files; invoke via `/openspec-*` skill names) +- Align specs so `ai-tool-paths` and `cli-init` cover the Grok Build path and adapterless init behavior + +## Capabilities + +### New Capabilities + +_None._ + +### Modified Capabilities + +- `ai-tool-paths`: define the `.grok` skills root for Grok Build +- `cli-init`: treat Grok Build as a supported adapterless selection that still generates skills and skips command-file generation + +## Impact + +- `src/core/config.ts` - add Grok Build tool metadata +- `docs/supported-tools.md` - add Grok Build row and tool id +- `docs/commands.md` - document `/openspec-*` skill invocations for Grok Build +- `docs/how-commands-work.md` - include Grok Build in slash-syntax table +- `docs/cli.md` - include `grok` in the supported `--tools` list +- `docs/troubleshooting.md` - list Grok Build among skills-only tools +- `test/core/init.test.ts` - cover Grok Build as an adapterless tool during init +- `.changeset/` - minor release note for the new tool + +## Non-Goals + +- Adding `src/core/command-generation/adapters/grok.ts` +- Defining a `.grok/commands/...` output path +- Relying on Claude/Cursor free-ride as the product integration +- Changing the broader delivery model for adapterless tools under `delivery=commands` (tracked separately in `add-tool-command-surface-capabilities`) diff --git a/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/specs/ai-tool-paths/spec.md b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/specs/ai-tool-paths/spec.md new file mode 100644 index 0000000000..903b2b15c6 --- /dev/null +++ b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/specs/ai-tool-paths/spec.md @@ -0,0 +1,37 @@ +# 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 CLI paths defined + +- **WHEN** looking up the `kimi` tool +- **THEN** `skillsDir` SHALL be `.kimi` + +#### Scenario: Grok Build paths defined + +- **WHEN** looking up the `grok` tool +- **THEN** `skillsDir` SHALL be `.grok` + +#### 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-07-11-add-grok-build-skills-only-support/specs/cli-init/spec.md b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/specs/cli-init/spec.md new file mode 100644 index 0000000000..6177784f9d --- /dev/null +++ b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/specs/cli-init/spec.md @@ -0,0 +1,43 @@ +# cli-init Delta Specification + +## MODIFIED Requirements + +### Requirement: Slash Command Generation + +The command SHALL generate opsx slash commands only for selected tools that have a registered command adapter, while keeping adapterless tools valid for skill generation. + +#### Scenario: Generating slash commands for a tool with a registered adapter + +- **WHEN** a tool with a registered command adapter is selected during initialization +- **THEN** create 9 slash command files using the tool's command adapter: + - `/opsx:explore` + - `/opsx:new` + - `/opsx:continue` + - `/opsx:apply` + - `/opsx:ff` + - `/opsx:verify` + - `/opsx:sync` + - `/opsx:archive` + - `/opsx:bulk-archive` +- **AND** use tool-specific path conventions (e.g., `.claude/commands/opsx/` for Claude) +- **AND** include tool-specific frontmatter format + +#### Scenario: Selected tool has no command adapter + +- **GIVEN** a selected tool has `skillsDir` configured but no registered command adapter +- **WHEN** initialization includes command generation +- **THEN** skill generation for that tool SHALL still remain valid +- **AND** command-file generation SHALL be skipped for that tool +- **AND** the command output SHALL include `Commands skipped for: (no adapter)` + +#### Scenario: Kimi CLI skips command-file generation + +- **WHEN** the user selects Kimi CLI during initialization +- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi'` +- **AND** command-file generation SHALL be skipped because no Kimi adapter is registered + +#### Scenario: Grok Build skips command-file generation + +- **WHEN** the user selects Grok Build during initialization +- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.grok'` +- **AND** command-file generation SHALL be skipped because no Grok adapter is registered diff --git a/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/tasks.md b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/tasks.md new file mode 100644 index 0000000000..332ddb5b6c --- /dev/null +++ b/openspec/changes/archive/2026-07-11-add-grok-build-skills-only-support/tasks.md @@ -0,0 +1,24 @@ +## 1. Tool Metadata + +- [x] 1.1 Add `Grok Build` to `src/core/config.ts` with `value: 'grok'`, `successLabel: 'Grok Build'`, and `skillsDir: '.grok'` (alphabetically near related tools) + +## 2. Documentation + +- [x] 2.1 Update `docs/supported-tools.md` with a Grok Build row (`skillsDir` `.grok`, no command adapter; skill-based `/openspec-*` invocations) and add `grok` to the `--tools` list +- [x] 2.2 Update `docs/commands.md` to document Grok Build skill invocations such as `/openspec-propose`, `/openspec-apply-change` +- [x] 2.3 Update `docs/how-commands-work.md` slash-syntax table to include Grok Build (`/openspec-*` skill form) +- [x] 2.4 Update `docs/cli.md` so the supported `--tools` list includes `grok` +- [x] 2.5 Update `docs/troubleshooting.md` skills-only tool list to include Grok Build + +## 3. Tests + +- [x] 3.1 Add a targeted init regression test for `--tools grok` with `delivery=both`: skills under `.grok/skills/...`, no `.grok/commands`, and commands-skipped log for `grok` `(no adapter)` using relaxed log matching and `path.join` expectations + +## 4. Release Notes + +- [x] 4.1 Add a changeset noting Grok Build as a supported skills-only tool via `.grok/skills/` + +## 5. Validation + +- [x] 5.1 Validate the change artifacts with `openspec validate add-grok-build-skills-only-support --strict` (or project-equivalent) +- [x] 5.2 Run targeted tests (`test/core/init.test.ts` Grok case) and fix any regressions diff --git a/openspec/specs/ai-tool-paths/spec.md b/openspec/specs/ai-tool-paths/spec.md index 4394812570..934726a877 100644 --- a/openspec/specs/ai-tool-paths/spec.md +++ b/openspec/specs/ai-tool-paths/spec.md @@ -50,6 +50,11 @@ 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: Grok Build paths defined + +- **WHEN** looking up the `grok` tool +- **THEN** `skillsDir` SHALL be `.grok` + #### Scenario: Tools without skillsDir - **WHEN** a tool has no `skillsDir` defined diff --git a/openspec/specs/cli-init/spec.md b/openspec/specs/cli-init/spec.md index d35b2ad390..8b1b788ff5 100644 --- a/openspec/specs/cli-init/spec.md +++ b/openspec/specs/cli-init/spec.md @@ -231,6 +231,12 @@ The command SHALL generate opsx slash commands only for selected tools that have - **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi-code'` - **AND** command-file generation SHALL be skipped because no Kimi adapter is registered +#### Scenario: Grok Build skips command-file generation + +- **WHEN** the user selects Grok Build during initialization +- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.grok'` +- **AND** command-file generation SHALL be skipped because no Grok adapter is registered + ### Requirement: Config File Generation The command SHALL create an OpenSpec config file with schema settings. diff --git a/src/core/config.ts b/src/core/config.ts index 4e027d28fb..9cfe469b37 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -55,6 +55,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' }, { name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' }, { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] }, + { name: 'Grok Build', value: 'grok', available: true, successLabel: 'Grok Build', skillsDir: '.grok' }, { name: 'Hermes Agent', value: 'hermes', available: true, successLabel: 'Hermes Agent', skillsDir: '.hermes', detectionPaths: ['.hermes', 'HERMES.md', '.hermes.md'], setupNote: "Hermes only loads skills from ~/.hermes/skills by default. Add this project's .hermes/skills directory to skills.external_dirs in ~/.hermes/config.yaml so Hermes picks up the generated OpenSpec skills." }, { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' }, { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' }, diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 55e611f762..59f2bbe3c2 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -606,6 +606,30 @@ describe('InitCommand', () => { ).toBe(true); }); + it('should support Grok Build as an adapterless skills-only tool', async () => { + saveGlobalConfig({ + featureFlags: {}, + profile: 'core', + delivery: 'both', + }); + + const initCommand = new InitCommand({ tools: 'grok', force: true }); + await initCommand.execute(testDir); + + const skillFile = path.join(testDir, '.grok', 'skills', 'openspec-explore', 'SKILL.md'); + expect(await fileExists(skillFile)).toBe(true); + + const commandsDir = path.join(testDir, '.grok', '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: grok') && entry.includes('(no adapter)'), + ), + ).toBe(true); + }); + it('should migrate OpenSpec skills from legacy .kimi to .kimi-code during init', async () => { const legacySkillDir = path.join(testDir, '.kimi', 'skills', 'openspec-explore'); await fs.mkdir(legacySkillDir, { recursive: true }); From 38b44b749b021ae492db2d1ccd37604efeb88bdd Mon Sep 17 00:00:00 2001 From: Clay Good Date: Thu, 27 Aug 2026 16:19:05 -0500 Subject: [PATCH 2/2] test(grok): cover skills-only installation and update lifecycle --- .changeset/grok-build-skills.md | 5 ++ test/core/available-tools.test.ts | 19 +++++++ test/core/init.test.ts | 46 +++++++++++----- test/core/update.test.ts | 90 +++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 .changeset/grok-build-skills.md diff --git a/.changeset/grok-build-skills.md b/.changeset/grok-build-skills.md new file mode 100644 index 0000000000..7adb38095f --- /dev/null +++ b/.changeset/grok-build-skills.md @@ -0,0 +1,5 @@ +--- +'@fission-ai/openspec': minor +--- + +Add Grok Build as a skills-only tool. Run `openspec init --tools grok` to install skills in `.grok/skills`, then invoke them with `/openspec-propose` and other skill names. Existing Grok installations are refreshed by `openspec update`. diff --git a/test/core/available-tools.test.ts b/test/core/available-tools.test.ts index dc0aff52dc..ccf66bc8e6 100644 --- a/test/core/available-tools.test.ts +++ b/test/core/available-tools.test.ts @@ -62,6 +62,25 @@ describe('available-tools', () => { expect(getAvailableTools(testDir).map((tool) => tool.value)).not.toContain('minimax-code'); }); + it('should detect Grok Build from a project .grok directory', async () => { + await fs.mkdir(path.join(testDir, '.grok')); + + expect(getAvailableTools(testDir)).toEqual([ + expect.objectContaining({ + name: 'Grok Build', + value: 'grok', + skillsDir: '.grok', + available: true, + }), + ]); + }); + + it('should not detect Grok Build from a .grok file', async () => { + await fs.writeFile(path.join(testDir, '.grok'), 'not a directory'); + + expect(getAvailableTools(testDir)).toEqual([]); + }); + it('should detect multiple tool directories', async () => { await fs.mkdir(path.join(testDir, '.claude'), { recursive: true }); await fs.mkdir(path.join(testDir, '.cursor'), { recursive: true }); diff --git a/test/core/init.test.ts b/test/core/init.test.ts index ac6fa45eb6..2fdb05cb30 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -814,28 +814,46 @@ describe('InitCommand', () => { ).toBe(true); }); - it('should support Grok Build as an adapterless skills-only tool', async () => { + it.each(['both', 'skills'] as const)('should install invocable Grok Build skills with delivery=%s', async (delivery) => { saveGlobalConfig({ featureFlags: {}, profile: 'core', - delivery: 'both', + delivery, }); const initCommand = new InitCommand({ tools: 'grok', force: true }); await initCommand.execute(testDir); - const skillFile = path.join(testDir, '.grok', 'skills', 'openspec-explore', 'SKILL.md'); - expect(await fileExists(skillFile)).toBe(true); + const skillsDir = path.join(testDir, '.grok', 'skills'); + const skillNames = [ + 'openspec-apply-change', + 'openspec-archive-change', + 'openspec-explore', + 'openspec-propose', + 'openspec-sync-specs', + 'openspec-update-change', + ]; + expect((await fs.readdir(skillsDir)).sort()).toEqual(skillNames); + for (const skillName of skillNames) { + const content = await fs.readFile(path.join(skillsDir, skillName, 'SKILL.md'), 'utf-8'); + expect(content).toContain(`name: ${skillName}`); + expect(content).not.toMatch(/\/opsx[:-]/); + } + const applyContent = await fs.readFile(path.join(skillsDir, 'openspec-apply-change', 'SKILL.md'), 'utf-8'); + expect(applyContent).toContain('/openspec-archive-change'); const commandsDir = path.join(testDir, '.grok', '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( + if (delivery === 'both') { + expect(logCalls.some( (entry) => entry.includes('Commands skipped for: grok') && entry.includes('(no adapter)'), - ), - ).toBe(true); + )).toBe(true); + } + const startHint = logCalls.find((entry) => entry.includes('Start your first change')); + expect(startHint).toContain('/openspec-propose'); + expect(startHint).not.toContain('/opsx:'); }); it('should migrate OpenSpec skills from legacy .kimi to .kimi-code during init', async () => { @@ -2017,19 +2035,21 @@ describe('InitCommand - profile and detection features', () => { expect(startHint).not.toContain('/opsx:propose'); }); - it('should print a configuration correction, not a dead hint, when delivery=commands generates nothing (adapterless tool)', async () => { + it.each([ + ['kimi', '.kimi-code'], + ['grok', '.grok'], + ])('should print a configuration correction, not a dead hint, when delivery=commands generates nothing for %s', async (toolId, skillsDir) => { saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'commands', }); - const initCommand = new InitCommand({ tools: 'kimi', force: true }); + const initCommand = new InitCommand({ tools: toolId, force: true }); await initCommand.execute(testDir); - // Kimi has no command adapter and delivery excludes skills: nothing is generated - expect(await fileExists(path.join(testDir, '.kimi-code', 'skills', 'openspec-explore', 'SKILL.md'))).toBe(false); - expect(await fileExists(path.join(testDir, '.kimi-code', 'commands'))).toBe(false); + // No command adapter and delivery excludes skills: nothing is generated + expect(await directoryExists(path.join(testDir, skillsDir))).toBe(false); const logCalls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls.flat().map(String); // No invocation hint may be shown — neither /opsx:* nor a skill reference exists diff --git a/test/core/update.test.ts b/test/core/update.test.ts index a8e2634a11..f4e769129a 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -3443,6 +3443,96 @@ More user content after markers. } ); + it.each(['both', 'skills'] as const)( + 'should refresh Grok Build skills and preserve user files when delivery=%s', + async (delivery) => { + setMockConfig({ featureFlags: {}, profile: 'core', delivery }); + + const grokDir = path.join(testDir, '.grok'); + const skillsDir = path.join(grokDir, 'skills'); + const exploreSkill = path.join(skillsDir, 'openspec-explore', 'SKILL.md'); + const userNotes = path.join(skillsDir, 'openspec-explore', 'notes.md'); + const customSkill = path.join(skillsDir, 'my-custom-skill', 'SKILL.md'); + const userConfig = path.join(grokDir, 'settings.json'); + await fs.mkdir(path.dirname(exploreSkill), { recursive: true }); + await fs.writeFile(exploreSkill, 'old instructions: /opsx:explore'); + await fs.writeFile(userNotes, 'my exploration notes'); + await fs.mkdir(path.dirname(customSkill), { recursive: true }); + await fs.writeFile(customSkill, 'my custom skill'); + await fs.writeFile(userConfig, '{"custom":true}\n'); + + await updateCommand.execute(testDir); + + for (const skillName of ['openspec-explore', 'openspec-update-change']) { + const content = await fs.readFile(path.join(skillsDir, skillName, 'SKILL.md'), 'utf-8'); + expect(content).toContain(`name: ${skillName}`); + expect(content).toContain('/openspec-'); + expect(content).not.toMatch(/\/opsx[:-]/); + expect(content).not.toContain('old instructions'); + } + expect(await fs.readFile(userNotes, 'utf-8')).toBe('my exploration notes'); + expect(await fs.readFile(customSkill, 'utf-8')).toBe('my custom skill'); + expect(await fs.readFile(userConfig, 'utf-8')).toBe('{"custom":true}\n'); + expect((await fs.readdir(grokDir)).sort()).toEqual(['settings.json', 'skills']); + } + ); + + it('should remove only managed Grok Build skills when switching to commands-only delivery', async () => { + await new InitCommand({ tools: 'grok', force: true }).execute(testDir); + const grokDir = path.join(testDir, '.grok'); + const skillsDir = path.join(grokDir, 'skills'); + const customSkill = path.join(skillsDir, 'openspec-custom', 'SKILL.md'); + await fs.mkdir(path.dirname(customSkill)); + await fs.writeFile(customSkill, 'my custom skill'); + await fs.writeFile(path.join(grokDir, 'notes.md'), 'my Grok notes'); + setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'commands' }); + const consoleSpy = vi.spyOn(console, 'log'); + + await updateCommand.execute(testDir); + + expect(await fs.readdir(skillsDir)).toEqual(['openspec-custom']); + expect(await fs.readFile(customSkill, 'utf-8')).toBe('my custom skill'); + expect(await fs.readFile(path.join(grokDir, 'notes.md'), 'utf-8')).toBe('my Grok notes'); + expect((await fs.readdir(grokDir)).sort()).toEqual(['notes.md', 'skills']); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining( + "No skills or commands remain for Grok Build: delivery is set to 'commands' but it supports only skills." + )); + }); + + it('should sync Grok Build from a custom profile to core without removing unrelated files', async () => { + setMockConfig({ + featureFlags: {}, + profile: 'custom', + workflows: ['explore', 'new'], + delivery: 'both', + }); + await new InitCommand({ tools: 'grok', force: true }).execute(testDir); + + const grokDir = path.join(testDir, '.grok'); + const skillsDir = path.join(grokDir, 'skills'); + expect((await fs.readdir(skillsDir)).sort()).toEqual(['openspec-explore', 'openspec-new-change']); + const customSkill = path.join(skillsDir, 'openspec-custom', 'SKILL.md'); + await fs.mkdir(path.dirname(customSkill)); + await fs.writeFile(customSkill, 'my custom skill'); + await fs.writeFile(path.join(grokDir, 'notes.md'), 'my Grok notes'); + + setMockConfig({ featureFlags: {}, profile: 'core', delivery: 'both' }); + await updateCommand.execute(testDir); + + expect((await fs.readdir(skillsDir)).sort()).toEqual([ + 'openspec-apply-change', + 'openspec-archive-change', + 'openspec-custom', + 'openspec-explore', + 'openspec-propose', + 'openspec-sync-specs', + 'openspec-update-change', + ]); + expect(await fs.readFile(customSkill, 'utf-8')).toBe('my custom skill'); + expect(await fs.readFile(path.join(grokDir, 'notes.md'), 'utf-8')).toBe('my Grok notes'); + expect((await fs.readdir(grokDir)).sort()).toEqual(['notes.md', 'skills']); + }); + it('should report Codex command generation as skipped because it uses skills', async () => { setMockConfig({ featureFlags: {},