mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
fix(tools): correct the Grok vendor-compat claim and close test gaps
Hardening pass on #1851. The docs and the adapter comment claimed Grok's flat command scan skips every vendor-compat directory, so a command could not register twice. That holds for `.claude/`, which nests commands under `opsx/`, and for `.agents/`, which no adapter writes commands into — but Cursor writes flat `opsx-<id>.md`, which the scan does find, and each of those roots can also hold an identical `openspec-*` skill. The copies differ only in the frontmatter each tool needs, so the outcome was never wrong; the stated reason was. Both places now describe the real shape. Also: - Adds the Grok row to `docs-lab/reference/supported-tools.md`, the source the published docs site renders. Without it the live page would not list the tool. - Orders the `--tools` id list in docs/supported-tools.md to match AI_TOOLS, so it is now identical to the copy in docs/cli.md. - Warns against naming your own Grok command `opsx-<workflow>.md`: Grok's flat scan means OpenSpec shares that directory with the user, and it owns those names. - Drops the upstream source citations from the user-facing footnote, which is the wrong altitude for it; they remain in the adapter header. - Asserts `available` on the Grok tool entry. Mutation testing found that flipping it to false removed Grok from the workset tool picker with a fully green suite, because the only test reading the field derives its expectation from AI_TOOLS itself. - Guards the command-stem loop against an empty command set, and adds grok to the flat-invocation tool list in command-references tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
347c9ee178
commit
1fe56159c4
@@ -2,4 +2,4 @@
|
||||
"@fission-ai/openspec": minor
|
||||
---
|
||||
|
||||
Add Grok Build, xAI's `grok` CLI, as a supported tool. `openspec init --tools grok` installs skills to `.grok/skills/openspec-*/SKILL.md` and commands to `.grok/commands/opsx-<id>.md`, invoked as `/opsx-propose`. Grok's command scan is flat, so commands are written directly in `.grok/commands/` rather than nested under `opsx/`.
|
||||
Add Grok Build, xAI's `grok` CLI, as a supported tool. `openspec init --tools grok` installs skills to `.grok/skills/openspec-*/SKILL.md` and commands to `.grok/commands/opsx-<id>.md`, invoked as `/opsx-propose`.
|
||||
|
||||
@@ -32,6 +32,7 @@ The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
|
||||
| ForgeCode | `forgecode` | `.forge/skills/` | `/openspec-apply-change` | none | none |
|
||||
| Gemini CLI | `gemini` | `.gemini/skills/` | `/openspec-apply-change` | `.gemini/commands/opsx/` | `/opsx:apply` |
|
||||
| GitHub Copilot | `github-copilot` | `.github/skills/` | `/openspec-apply-change` | `.github/prompts/` | `/opsx-apply` |
|
||||
| Grok Build | `grok` | `.grok/skills/` | `/openspec-apply-change` | `.grok/commands/` | `/opsx-apply` |
|
||||
| Hermes Agent | `hermes` | `.hermes/skills/` | `/openspec-apply-change` | none | none |
|
||||
| iFlow | `iflow` | `.iflow/skills/` | `/openspec-apply-change` | `.iflow/commands/` | `/opsx-apply` |
|
||||
| Junie | `junie` | `.junie/skills/` | `/openspec-apply-change` | `.junie/commands/` | `/opsx-apply` |
|
||||
|
||||
@@ -112,11 +112,11 @@ 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-*`.
|
||||
|
||||
\*\*\*\*\* Grok Build is xAI's `grok` CLI. It has no separate command subsystem: the loader that discovers `.grok/skills/<name>/SKILL.md` also scans `.grok/commands/` and registers each Markdown file there as a slash command. That scan is flat — a nested `commands/opsx/<id>.md` is skipped, not namespaced — so OpenSpec writes `.grok/commands/opsx-<id>.md` and the filename is the command: `/opsx-propose`. Under skills-only delivery no command files are written and the generated skills are invoked by name instead, as `/openspec-propose`.
|
||||
\*\*\*\*\* Grok Build is xAI's `grok` CLI. It has no separate command subsystem: the loader that discovers `.grok/skills/<name>/SKILL.md` also scans `.grok/commands/` and registers each Markdown file there as a slash command. That scan is flat — a nested `commands/opsx/<id>.md` is skipped rather than namespaced — so OpenSpec writes `.grok/commands/opsx-<id>.md`, and the filename is the command: `/opsx-propose`. Because that directory is shared with your own command files, avoid naming one of yours `opsx-<workflow>.md`: OpenSpec owns those names and will overwrite or remove them. Under skills-only delivery no command files are written, and the skills are invoked by name instead, as `/openspec-propose`.
|
||||
|
||||
The same loader also scans `commands/` under `.agents/`, `.claude/`, and `.cursor/` for vendor compatibility. Those directories hold `opsx/<id>.*` files, which the flat scan skips, so configuring Grok alongside Claude Code registers each command once rather than twice.
|
||||
Grok also scans `.agents/`, `.claude/`, and `.cursor/` for skills and commands, so a project configured for Grok alongside one of those tools can offer Grok the same workflow from two directories. The copies differ only in the frontmatter each tool needs, so either one runs the same workflow.
|
||||
|
||||
[Skills](https://docs.x.ai/build/features/skills-plugins-marketplaces) are documented upstream. The `commands/` directory is not yet in the published docs, so it is cited here from the shipping CLI's source: `skill_config_dirs()` returns `[".grok", ".agents", ".claude", ".cursor"]` (`crates/codegen/xai-grok-tools/src/types/compat.rs`), each of which is passed to `find_command_paths`, which reads `commands/` via `scan_md_files` — documented there as "Scan a directory for `.md` files (flat, no recursion)" (`crates/codegen/xai-grok-tools/src/implementations/skills/discovery.rs`).
|
||||
[Skills](https://docs.x.ai/build/features/skills-plugins-marketplaces) are documented upstream. The `commands/` directory is read by the shipping CLI but is not yet in the published docs, so a pinned test guards its shape.
|
||||
|
||||
SourceCraft Code Assistant support targets its VS Code extension. Its [custom commands](https://sourcecraft.dev/portal/docs/en/code-assistant/operations/agent/slash-commands) and [skills](https://sourcecraft.dev/portal/docs/ru/code-assistant/operations/agent/skills) are available only in VS Code. This integration does not configure SourceCraft web or JetBrains.
|
||||
|
||||
@@ -226,7 +226,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`, `grok`, `hermes`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `lingma`, `minimax-code`, `vibe`, `oh-my-pi`, `opencode`, `pi`, `qoder`, `qwen`, `rovodev`, `roocode`, `codeassistant`, `trae`, `zed`, `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`, `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`, `codeassistant`, `qoder`, `qwen`, `rovodev`, `roocode`, `trae`, `zed`, `zcode`, `agents`
|
||||
|
||||
## Workflow-Dependent Installation
|
||||
|
||||
|
||||
@@ -20,10 +20,17 @@
|
||||
* the command, which keeps the generated name identical to the one
|
||||
* advertised in skills, docs, and the getting-started hint.
|
||||
*
|
||||
* Grok also reads `.agents/`, `.claude/`, and `.cursor/` command directories
|
||||
* for vendor compatibility. Those receive `opsx/<id>.md` from their own
|
||||
* adapters, which Grok's flat scan skips, so a project configured for both
|
||||
* Grok and Claude Code registers each command exactly once.
|
||||
* Grok also scans `.agents/`, `.claude/`, and `.cursor/` for skills and
|
||||
* commands. Nothing writes `.agents/commands/`, and Claude Code nests its
|
||||
* commands under `opsx/`, which the flat scan skips — but Cursor writes them
|
||||
* flat as `opsx-<id>.md`, and every one of those roots can hold an identical
|
||||
* `skills/openspec-<name>/SKILL.md`. So a project configured for Grok alongside
|
||||
* Cursor or Claude Code can present Grok with the same name from two roots.
|
||||
* The copies differ only in the frontmatter each tool needs, so whichever one
|
||||
* Grok resolves to runs the same workflow. OpenSpec writes `.grok/`
|
||||
* unconditionally rather than trying to predict that resolution: the user may
|
||||
* drop the other tool at any time, and a missing `.grok/` tree would then
|
||||
* leave Grok with nothing.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
@@ -543,6 +543,10 @@ describe('available-tools', () => {
|
||||
const grokTool = tools.find((t) => t.value === 'grok');
|
||||
expect(grokTool?.name).toBe('Grok Build');
|
||||
expect(grokTool?.skillsDir).toBe('.grok');
|
||||
// `available` gates the workset tool picker. The cli-e2e `--tools` list
|
||||
// derives its expectation from AI_TOOLS itself, so it cancels out when
|
||||
// this flag flips and nothing else would notice Grok disappearing.
|
||||
expect(grokTool?.available).toBe(true);
|
||||
// Grok is a CLI: it picks up new command and skill files without an
|
||||
// editor restart, so no restart hint should be offered (#1067).
|
||||
expect(grokTool?.requiresIdeRestart).toBeUndefined();
|
||||
|
||||
@@ -1180,9 +1180,12 @@ describe('command-generation/adapters', () => {
|
||||
// The file stem is what Grok registers, so it has to survive Grok's own
|
||||
// name rules, which its loader applies to commands and skills alike:
|
||||
// lowercase [a-z0-9-], no leading or trailing hyphen, no `--`, and at most
|
||||
// 64 characters. A stem that fails them is dropped rather than corrected.
|
||||
// 64 characters. Grok normalizes a stem that breaks the character rules,
|
||||
// so the risk is a silently renamed command rather than a rejected one.
|
||||
it('names every command with a stem Grok accepts verbatim', () => {
|
||||
for (const { id } of getCommandContents()) {
|
||||
const contents = getCommandContents();
|
||||
expect(contents.length).toBeGreaterThan(0);
|
||||
for (const { id } of contents) {
|
||||
const stem = path.basename(grokAdapter.getFilePath(id), '.md');
|
||||
expect(stem).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
||||
expect(stem.length).toBeLessThanOrEqual(64);
|
||||
|
||||
@@ -272,7 +272,7 @@ describe('getTransformerForTool', () => {
|
||||
it('selects hyphen commands for every flat-invocation tool when commands are generated', () => {
|
||||
// These tools invoke commands by filename (/opsx-<id>), so skills must
|
||||
// reference the hyphen form their command files actually answer to.
|
||||
for (const toolId of ['bob', 'cursor', 'github-copilot', 'oh-my-pi', 'opencode', 'pi', 'qwen'] as const) {
|
||||
for (const toolId of ['bob', 'cursor', 'github-copilot', 'grok', 'oh-my-pi', 'opencode', 'pi', 'qwen'] as const) {
|
||||
for (const delivery of ['both', 'commands'] as const) {
|
||||
const transformer = getTransformerForTool(toolId, delivery, 'adapter-backed', FLAT_SLASH);
|
||||
expect(transformer?.('/opsx:apply'), `${toolId} ${delivery}`).toBe('/opsx-apply');
|
||||
|
||||
Reference in New Issue
Block a user