From b1223034c3acbf2893593dfe8ae5fc5bcb352e09 Mon Sep 17 00:00:00 2001 From: ARRRRNY Date: Sun, 31 May 2026 11:38:27 +0300 Subject: [PATCH 1/5] feat: add Zed integration --- docs/index.md | 2 +- docs/reference/integrations.md | 1 + src/specify_cli/commands/init.py | 8 ++- src/specify_cli/extensions.py | 3 ++ src/specify_cli/integrations/__init__.py | 2 + src/specify_cli/integrations/zed/__init__.py | 41 +++++++++++++++ .../test_integration_subcommand.py | 1 + tests/integrations/test_integration_zed.py | 51 +++++++++++++++++++ tests/integrations/test_registry.py | 2 +- 9 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 src/specify_cli/integrations/zed/__init__.py create mode 100644 tests/integrations/test_integration_zed.py diff --git a/docs/index.md b/docs/index.md index c8b02d98cb..486193e823 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ Define what to build before building it. Rich templates, quality checklists, and ### Use any coding agent -30 integrations — Copilot, Gemini, Codex, Windsurf, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in. +31 integrations — Copilot, Gemini, Codex, Windsurf, Zed, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in. Run `specify init` with your agent of choice and Spec Kit sets up the right command files, context rules, and directory structures automatically. If your agent isn't listed, the `generic` integration is an escape hatch for any tool. diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index ec6c894652..ab34bf5846 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -35,6 +35,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify | [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | `tabnine` | | | [Trae](https://www.trae.ai/) | `trae` | Skills-based integration; skills are installed automatically | | [Windsurf](https://windsurf.com/) | `windsurf` | | +| [Zed](https://zed.dev/) | `zed` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `/speckit-` | | Generic | `generic` | Bring your own agent — use `--integration generic --integration-options="--commands-dir "` for AI coding agents not listed above | ## List Available Integrations diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index e5dc47e98c..e034affddf 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -726,7 +726,8 @@ def init( cursor_agent_skill_mode = selected_ai == "cursor-agent" and (ai_skills or _is_skills_integration) copilot_skill_mode = selected_ai == "copilot" and _is_skills_integration devin_skill_mode = selected_ai == "devin" - native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode + zed_skill_mode = selected_ai == "zed" and _is_skills_integration + native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode or zed_skill_mode if codex_skill_mode and not ai_skills: steps_lines.append(f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]") @@ -740,6 +741,9 @@ def init( if devin_skill_mode: steps_lines.append(f"{step_num}. Start Devin in this project directory; spec-kit skills were installed to [cyan].devin/skills[/cyan]") step_num += 1 + if zed_skill_mode: + steps_lines.append(f"{step_num}. Start Zed in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]") + step_num += 1 usage_label = "skills" if native_skill_mode else "slash commands" def _display_cmd(name: str) -> str: @@ -749,7 +753,7 @@ def _display_cmd(name: str) -> str: return f"/speckit-{name}" if kimi_skill_mode: return f"/skill:speckit-{name}" - if cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode: + if cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode or zed_skill_mode: return f"/speckit-{name}" return f"/speckit.{name}" diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 5a595fbffa..a11eedcbdb 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -2413,6 +2413,7 @@ def _render_hook_invocation(self, command: Any) -> str: claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills")) kimi_skill_mode = selected_ai == "kimi" cursor_skill_mode = selected_ai == "cursor-agent" and bool(init_options.get("ai_skills")) + zed_skill_mode = selected_ai == "zed" and bool(init_options.get("ai_skills")) skill_name = self._skill_name_from_command(command_id) if codex_skill_mode and skill_name: @@ -2423,6 +2424,8 @@ def _render_hook_invocation(self, command: Any) -> str: return f"/skill:{skill_name}" if cursor_skill_mode and skill_name: return f"/{skill_name}" + if zed_skill_mode and skill_name: + return f"/{skill_name}" return f"/{command_id}" diff --git a/src/specify_cli/integrations/__init__.py b/src/specify_cli/integrations/__init__.py index ad1440d074..c3d7499ff3 100644 --- a/src/specify_cli/integrations/__init__.py +++ b/src/specify_cli/integrations/__init__.py @@ -78,6 +78,7 @@ def _register_builtins() -> None: from .trae import TraeIntegration from .vibe import VibeIntegration from .windsurf import WindsurfIntegration + from .zed import ZedIntegration # -- Registration (alphabetical) -------------------------------------- _register(AgyIntegration()) @@ -111,6 +112,7 @@ def _register_builtins() -> None: _register(TraeIntegration()) _register(VibeIntegration()) _register(WindsurfIntegration()) + _register(ZedIntegration()) _register_builtins() diff --git a/src/specify_cli/integrations/zed/__init__.py b/src/specify_cli/integrations/zed/__init__.py new file mode 100644 index 0000000000..004cfc57f6 --- /dev/null +++ b/src/specify_cli/integrations/zed/__init__.py @@ -0,0 +1,41 @@ +"""Zed editor integration. — skills-based agent. + +Zed uses the ``.agents/skills/speckit-/SKILL.md`` layout so Spec Kit +commands are exposed as project-local skills that can be invoked from Zed's +slash-command menu. +""" + +from __future__ import annotations + +from ..base import IntegrationOption, SkillsIntegration + + +class ZedIntegration(SkillsIntegration): + """Integration for Zed editor skills.""" + + key = "zed" + config = { + "name": "Zed", + "folder": ".agents/", + "commands_subdir": "skills", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".agents/skills", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": "/SKILL.md", + } + context_file = "AGENTS.md" + + @classmethod + def options(cls) -> list[IntegrationOption]: + return [ + IntegrationOption( + "--skills", + is_flag=True, + default=True, + help="Install as agent skills (default for Zed)", + ), + ] diff --git a/tests/integrations/test_integration_subcommand.py b/tests/integrations/test_integration_subcommand.py index f40adb7ae9..42a088f541 100644 --- a/tests/integrations/test_integration_subcommand.py +++ b/tests/integrations/test_integration_subcommand.py @@ -93,6 +93,7 @@ def test_list_shows_available_integrations(self, tmp_path): # Should show multiple integrations assert "claude" in result.output assert "gemini" in result.output + assert "zed" in result.output def test_list_shows_multi_install_safe_status(self, tmp_path): project = _init_project(tmp_path, "claude") diff --git a/tests/integrations/test_integration_zed.py b/tests/integrations/test_integration_zed.py new file mode 100644 index 0000000000..f3256c7fc7 --- /dev/null +++ b/tests/integrations/test_integration_zed.py @@ -0,0 +1,51 @@ +"""Tests for ZedIntegration.""" + +import json + +from specify_cli.integrations import get_integration + +from .test_integration_base_skills import SkillsIntegrationTests + + +class TestZedIntegration(SkillsIntegrationTests): + KEY = "zed" + FOLDER = ".agents/" + COMMANDS_SUBDIR = "skills" + REGISTRAR_DIR = ".agents/skills" + CONTEXT_FILE = "AGENTS.md" + + def test_requires_cli_is_false(self): + """Zed is IDE-based; requires_cli must remain False.""" + i = get_integration(self.KEY) + assert i is not None + assert i.config is not None + assert i.config["requires_cli"] is False + + +class TestZedHookInvocations: + """Zed hook messages should reference slash-invokable skills.""" + + def test_hooks_render_skill_invocation(self, tmp_path): + from specify_cli.extensions import HookExecutor + + project = tmp_path / "zed-hooks" + project.mkdir() + init_options = project / ".specify" / "init-options.json" + init_options.parent.mkdir(parents=True, exist_ok=True) + init_options.write_text(json.dumps({"ai": "zed", "ai_skills": True})) + + hook_executor = HookExecutor(project) + message = hook_executor.format_hook_message( + "before_plan", + [ + { + "extension": "test-ext", + "command": "speckit.plan", + "optional": False, + } + ], + ) + + assert "Executing: `/speckit-plan`" in message + assert "EXECUTE_COMMAND: speckit.plan" in message + assert "EXECUTE_COMMAND_INVOCATION: /speckit-plan" in message diff --git a/tests/integrations/test_registry.py b/tests/integrations/test_registry.py index 1b36501056..b6f7439254 100644 --- a/tests/integrations/test_registry.py +++ b/tests/integrations/test_registry.py @@ -27,7 +27,7 @@ # Stage 4 — TOML integrations "gemini", "tabnine", # Stage 5 — skills, generic & option-driven integrations - "codex", "kimi", "agy", "generic", + "codex", "kimi", "agy", "zed", "generic", ] From d6df1e8320a310b8d1b59c70dced0607c10249a6 Mon Sep 17 00:00:00 2001 From: ARRRRNY Date: Wed, 3 Jun 2026 02:07:25 +0300 Subject: [PATCH 2/5] fix: update integrations stats grid to 31 for consistency --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 486193e823..7f838902c3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ **Define what to build before building it — with any AI coding agent.** -Spec Kit is a toolkit for [Spec-Driven Development](concepts/sdd.md) (SDD), a methodology that puts specifications at the center of AI-assisted software development. Instead of jumping straight to code, you describe *what* to build, refine it through structured phases, and let your AI coding agent implement it. +Spec Kit is a toolkit for [Spec-Driven Development](concepts/sdd.md) (SDD), a methodology that puts specifications at the center of AI-assisted software development. Instead of jumping straight to code, you describe _what_ to build, refine it through structured phases, and let your AI coding agent implement it. Install Spec Kit  Quick Start @@ -90,7 +90,7 @@ Community extensions like CI Guard and Architecture Guard add compliance gates a Contributors
- 30 + 31 Integrations
From 2e99b1958efaa42043f3d292aed6ac309c2619ba Mon Sep 17 00:00:00 2001 From: ARRRRNY Date: Wed, 3 Jun 2026 02:20:03 +0300 Subject: [PATCH 3/5] fix: address Copilot review feedback - Remove non-actionable --skills flag from ZedIntegration (Zed is always skills-based, like Agy) - Align zed_skill_mode predicate with ai_skills for consistency across init output and hook rendering - Consolidate claude/cursor/zed slash-skill return blocks in _render_hook_invocation to reduce duplication - Override test_options_include_skills_flag for Zed (no --skills flag) --- src/specify_cli/commands/init.py | 2 +- src/specify_cli/extensions.py | 10 ++++------ src/specify_cli/integrations/zed/__init__.py | 11 ++--------- tests/integrations/test_integration_zed.py | 6 ++++++ 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index 29dfefeaef..4cb745be5f 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -728,7 +728,7 @@ def init( cursor_agent_skill_mode = selected_ai == "cursor-agent" and (ai_skills or _is_skills_integration) copilot_skill_mode = selected_ai == "copilot" and _is_skills_integration devin_skill_mode = selected_ai == "devin" - zed_skill_mode = selected_ai == "zed" and _is_skills_integration + zed_skill_mode = selected_ai == "zed" and (ai_skills or _is_skills_integration) cline_skill_mode = selected_ai == "cline" native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode or zed_skill_mode diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 997cd41d3f..8c1fa9158c 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -2492,19 +2492,17 @@ def _render_hook_invocation(self, command: Any) -> str: skill_name = self._skill_name_from_command(command_id) if codex_skill_mode and skill_name: return f"${skill_name}" - if claude_skill_mode and skill_name: - return f"/{skill_name}" if kimi_skill_mode and skill_name: return f"/skill:{skill_name}" - if cursor_skill_mode and skill_name: - return f"/{skill_name}" - if zed_skill_mode and skill_name: - return f"/{skill_name}" if cline_mode: from .integrations.cline import format_cline_command_name return f"/{format_cline_command_name(command_id)}" + # Slash-skill integrations (Claude, Cursor, Zed…) + if skill_name and (claude_skill_mode or cursor_skill_mode or zed_skill_mode): + return f"/{skill_name}" + return f"/{command_id}" def get_project_config(self) -> Dict[str, Any]: diff --git a/src/specify_cli/integrations/zed/__init__.py b/src/specify_cli/integrations/zed/__init__.py index 575c97c43a..8ee2f4609a 100644 --- a/src/specify_cli/integrations/zed/__init__.py +++ b/src/specify_cli/integrations/zed/__init__.py @@ -7,7 +7,7 @@ from __future__ import annotations -from ..base import IntegrationOption, SkillsIntegration +from ..base import SkillsIntegration class ZedIntegration(SkillsIntegration): @@ -31,11 +31,4 @@ class ZedIntegration(SkillsIntegration): @classmethod def options(cls) -> list[IntegrationOption]: - return [ - IntegrationOption( - "--skills", - is_flag=True, - default=True, - help="Install as agent skills (default for Zed)", - ), - ] + return [] diff --git a/tests/integrations/test_integration_zed.py b/tests/integrations/test_integration_zed.py index f3256c7fc7..18d59ea125 100644 --- a/tests/integrations/test_integration_zed.py +++ b/tests/integrations/test_integration_zed.py @@ -14,6 +14,12 @@ class TestZedIntegration(SkillsIntegrationTests): REGISTRAR_DIR = ".agents/skills" CONTEXT_FILE = "AGENTS.md" + def test_options_include_skills_flag(self): + """Zed is always skills-based; no --skills option needed.""" + i = get_integration(self.KEY) + skills_opts = [o for o in i.options() if o.name == "--skills"] + assert len(skills_opts) == 0 + def test_requires_cli_is_false(self): """Zed is IDE-based; requires_cli must remain False.""" i = get_integration(self.KEY) From e4f3b4c5732e498ca4ccbd308714cd64e2078fa8 Mon Sep 17 00:00:00 2001 From: Ahmet TOK <48218623+arrrrny@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:28:26 +0300 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/specify_cli/integrations/zed/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/specify_cli/integrations/zed/__init__.py b/src/specify_cli/integrations/zed/__init__.py index 8ee2f4609a..882d83cc59 100644 --- a/src/specify_cli/integrations/zed/__init__.py +++ b/src/specify_cli/integrations/zed/__init__.py @@ -7,7 +7,7 @@ from __future__ import annotations -from ..base import SkillsIntegration +from ..base import IntegrationOption, SkillsIntegration class ZedIntegration(SkillsIntegration): From 2b9b482a2c32b41cfe7994961569c5d0326a212b Mon Sep 17 00:00:00 2001 From: ARRRRNY Date: Wed, 3 Jun 2026 18:32:22 +0300 Subject: [PATCH 5/5] fix: address Copilot review round 2 - Make zed_skill_mode unconditional in hook rendering (Zed is always skills-based, no --skills option) - Add test_init_persists_ai_skills_for_zed that exercises the actual CLI init path and verifies HookExecutor renders /speckit-plan without manual init-options manipulation --- src/specify_cli/extensions.py | 2 +- tests/integrations/test_integration_zed.py | 63 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 8c1fa9158c..82ac06f568 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -2486,7 +2486,7 @@ def _render_hook_invocation(self, command: Any) -> str: claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills")) kimi_skill_mode = selected_ai == "kimi" cursor_skill_mode = selected_ai == "cursor-agent" and bool(init_options.get("ai_skills")) - zed_skill_mode = selected_ai == "zed" and bool(init_options.get("ai_skills")) + zed_skill_mode = selected_ai == "zed" cline_mode = selected_ai == "cline" skill_name = self._skill_name_from_command(command_id) diff --git a/tests/integrations/test_integration_zed.py b/tests/integrations/test_integration_zed.py index 18d59ea125..ebe9a50d5a 100644 --- a/tests/integrations/test_integration_zed.py +++ b/tests/integrations/test_integration_zed.py @@ -55,3 +55,66 @@ def test_hooks_render_skill_invocation(self, tmp_path): assert "Executing: `/speckit-plan`" in message assert "EXECUTE_COMMAND: speckit.plan" in message assert "EXECUTE_COMMAND_INVOCATION: /speckit-plan" in message + + def test_init_persists_ai_skills_for_zed(self, tmp_path): + """specify init --integration zed must persist ai_skills: true, + so HookExecutor renders slash-skill invocations without manual + init-options manipulation.""" + from typer.testing import CliRunner + + from specify_cli import app + from specify_cli.extensions import HookExecutor + + project = tmp_path / "zed-init-test" + project.mkdir() + old_cwd = None + try: + import os + + old_cwd = os.getcwd() + os.chdir(project) + runner = CliRunner() + result = runner.invoke( + app, + [ + "init", + "--here", + "--integration", + "zed", + "--script", + "sh", + "--no-git", + "--ignore-agent-tools", + ], + catch_exceptions=False, + ) + finally: + if old_cwd is not None: + os.chdir(old_cwd) + + assert result.exit_code == 0, f"init failed: {result.output}" + + opts_path = project / ".specify" / "init-options.json" + assert opts_path.exists() + opts = json.loads(opts_path.read_text(encoding="utf-8")) + assert opts.get("ai") == "zed" + assert opts.get("ai_skills") is True, ( + f"init must persist ai_skills=true for Zed, got: {opts.get('ai_skills')}" + ) + + hook_executor = HookExecutor(project) + message = hook_executor.format_hook_message( + "before_plan", + [ + { + "extension": "test-ext", + "command": "speckit.plan", + "optional": False, + } + ], + ) + assert "Executing: `/speckit-plan`" in message, ( + "Hook rendering must produce /speckit-plan for Zed without hint injection\n" + f"Got message: {message}" + ) + assert "EXECUTE_COMMAND_INVOCATION: /speckit-plan" in message