From 1592fecc335f1b33d96e74ff6ea5d9b8489e2e0f Mon Sep 17 00:00:00 2001 From: Yuming Chen Date: Fri, 19 Jun 2026 02:43:34 +0800 Subject: [PATCH 1/2] feat: add ZCode (Z.AI) integration Add a skills-based integration for ZCode, Z.AI's Claude-Code-style agent. ZCode uses the same SKILL.md layout as Claude Code, so spec-kit installs workflows into .zcode/skills/speckit-/SKILL.md, invoked in chat as $speckit-. - ZcodeIntegration(SkillsIntegration) with .zcode/ folder and --skills option - Register in INTEGRATION_REGISTRY - Catalog entry (tags: cli, skills, z-ai) - Tests via SkillsIntegrationTests mixin - Document in integrations reference and README Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- docs/reference/integrations.md | 1 + integrations/catalog.json | 9 ++++ src/specify_cli/integrations/__init__.py | 2 + .../integrations/zcode/__init__.py | 43 +++++++++++++++++++ tests/integrations/test_integration_zcode.py | 11 +++++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/specify_cli/integrations/zcode/__init__.py create mode 100644 tests/integrations/test_integration_zcode.py diff --git a/README.md b/README.md index afca9b15a5..3eb82395c1 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ specify init . --force --integration copilot specify init --here --force --integration copilot ``` -The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi, Forge, Goose, or Mistral Vibe installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command: +The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi, Forge, Goose, Mistral Vibe, or ZCode installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command: ```bash specify init --integration copilot --ignore-agent-tools diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index a790389774..1fe4a53640 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -38,6 +38,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` | | +| [ZCode](https://zcode.z.ai/) | `zcode` | Skills-based integration; installs skills into `.zcode/skills/` and invokes them as `$speckit-` | | [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 | diff --git a/integrations/catalog.json b/integrations/catalog.json index 33c6ddd931..f89af37d5c 100644 --- a/integrations/catalog.json +++ b/integrations/catalog.json @@ -299,6 +299,15 @@ "author": "spec-kit-core", "repository": "https://github.com/github/spec-kit", "tags": ["cli", "skills"] + }, + "zcode": { + "id": "zcode", + "name": "ZCode", + "version": "1.0.0", + "description": "Z.AI ZCode CLI skills-based integration", + "author": "spec-kit-core", + "repository": "https://github.com/github/spec-kit", + "tags": ["cli", "skills", "z-ai"] } } } diff --git a/src/specify_cli/integrations/__init__.py b/src/specify_cli/integrations/__init__.py index 07d3cc1a6d..a81d705543 100644 --- a/src/specify_cli/integrations/__init__.py +++ b/src/specify_cli/integrations/__init__.py @@ -80,6 +80,7 @@ def _register_builtins() -> None: from .trae import TraeIntegration from .vibe import VibeIntegration from .windsurf import WindsurfIntegration + from .zcode import ZcodeIntegration from .zed import ZedIntegration # -- Registration (alphabetical) -------------------------------------- @@ -116,6 +117,7 @@ def _register_builtins() -> None: _register(TraeIntegration()) _register(VibeIntegration()) _register(WindsurfIntegration()) + _register(ZcodeIntegration()) _register(ZedIntegration()) diff --git a/src/specify_cli/integrations/zcode/__init__.py b/src/specify_cli/integrations/zcode/__init__.py new file mode 100644 index 0000000000..ea47f31555 --- /dev/null +++ b/src/specify_cli/integrations/zcode/__init__.py @@ -0,0 +1,43 @@ +"""ZCode integration — skills-based agent (Z.AI). + +ZCode uses the ``.zcode/skills/speckit-/SKILL.md`` layout, matching +the Claude Code skill format. Skills are invoked in chat with +``$speckit-``. Z.AI recommends skills (over simple ``/`` commands) +for template- and script-driven workflows such as spec-kit. +""" + +from __future__ import annotations + +from ..base import IntegrationOption, SkillsIntegration + + +class ZcodeIntegration(SkillsIntegration): + """Integration for ZCode CLI (Z.AI).""" + + key = "zcode" + config = { + "name": "ZCode", + "folder": ".zcode/", + "commands_subdir": "skills", + "install_url": "https://zcode.z.ai/", + "requires_cli": True, + } + registrar_config = { + "dir": ".zcode/skills", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": "/SKILL.md", + } + context_file = "ZCODE.md" + multi_install_safe = True + + @classmethod + def options(cls) -> list[IntegrationOption]: + return [ + IntegrationOption( + "--skills", + is_flag=True, + default=True, + help="Install as agent skills (default for ZCode)", + ), + ] diff --git a/tests/integrations/test_integration_zcode.py b/tests/integrations/test_integration_zcode.py new file mode 100644 index 0000000000..4c54ed8ff5 --- /dev/null +++ b/tests/integrations/test_integration_zcode.py @@ -0,0 +1,11 @@ +"""Tests for ZcodeIntegration — skills-based integration (Z.AI).""" + +from .test_integration_base_skills import SkillsIntegrationTests + + +class TestZcodeIntegration(SkillsIntegrationTests): + KEY = "zcode" + FOLDER = ".zcode/" + COMMANDS_SUBDIR = "skills" + REGISTRAR_DIR = ".zcode/skills" + CONTEXT_FILE = "ZCODE.md" From efc3cfdc5355610211dcc31fa625730ad7a6b2f3 Mon Sep 17 00:00:00 2001 From: Yuming Chen Date: Mon, 22 Jun 2026 01:33:52 +0800 Subject: [PATCH 2/2] fix: render $speckit-* invocations for ZCode skills ZCode is documented as a skills agent invoked with $speckit-, but the central invocation rendering only special-cased codex, so specify init Next Steps and extension hooks rendered the dotted /speckit. form instead. Centralize the $speckit-* decision in a DOLLAR_SKILLS_AGENTS set with an is_dollar_skills_agent() helper, and route both init Next Steps and HookExecutor._render_hook_invocation through it. Add ZCode invocation regression tests mirroring the existing Codex/Kimi coverage. Co-Authored-By: Claude Opus 4.8 --- src/specify_cli/_invocation_style.py | 14 ++++++++++ src/specify_cli/commands/init.py | 14 ++++++++-- src/specify_cli/extensions.py | 6 ++--- tests/integrations/test_integration_zcode.py | 27 ++++++++++++++++++++ tests/test_extensions.py | 18 +++++++++++++ 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/specify_cli/_invocation_style.py b/src/specify_cli/_invocation_style.py index a61f699a53..627967cfbd 100644 --- a/src/specify_cli/_invocation_style.py +++ b/src/specify_cli/_invocation_style.py @@ -8,6 +8,9 @@ from __future__ import annotations +# Agents that render $speckit- (chat invocation) when in skills mode. +DOLLAR_SKILLS_AGENTS: frozenset[str] = frozenset({"codex", "zcode"}) + # Agents that always render /speckit-, regardless of ai_skills. ALWAYS_SLASH_AGENTS: frozenset[str] = frozenset({"devin", "trae", "zed"}) @@ -26,6 +29,17 @@ ) +def is_dollar_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) -> bool: + """Return ``True`` if *selected_ai* uses ``$speckit-`` invocations. + + Agents in `DOLLAR_SKILLS_AGENTS` (e.g. ``codex``, ``zcode``) render + ``$speckit-`` chat invocations when installed in skills mode. + """ + if not isinstance(selected_ai, str): + return False + return selected_ai in DOLLAR_SKILLS_AGENTS and ai_skills_enabled + + def is_slash_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) -> bool: """Return ``True`` if *selected_ai* uses ``/speckit-`` invocations. diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index 997b9ee679..fc82334da2 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -693,6 +693,7 @@ def init( ) or getattr(resolved_integration, "_skills_mode", False) codex_skill_mode = selected_ai == "codex" and _is_skills_integration + zcode_skill_mode = selected_ai == "zcode" and _is_skills_integration claude_skill_mode = selected_ai == "claude" and _is_skills_integration kimi_skill_mode = selected_ai == "kimi" agy_skill_mode = selected_ai == "agy" and _is_skills_integration @@ -706,6 +707,7 @@ def init( cline_skill_mode = selected_ai == "cline" native_skill_mode = ( codex_skill_mode + or zcode_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode @@ -721,6 +723,11 @@ def init( f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]" ) step_num += 1 + if zcode_skill_mode: + steps_lines.append( + f"{step_num}. Start ZCode in this project directory; spec-kit skills were installed to [cyan].zcode/skills[/cyan]" + ) + step_num += 1 if claude_skill_mode: steps_lines.append( f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]" @@ -743,7 +750,10 @@ def init( step_num += 1 usage_label = "skills" if native_skill_mode else "slash commands" - from .._invocation_style import is_slash_skills_agent as _is_slash_skills_agent + from .._invocation_style import ( + is_dollar_skills_agent as _is_dollar_skills_agent, + is_slash_skills_agent as _is_slash_skills_agent, + ) # `_is_skills_integration` means the integration is installed in # skills mode, which is the semantic equivalent of `ai_skills_enabled` @@ -751,7 +761,7 @@ def init( _ai_skills_enabled = _is_skills_integration def _display_cmd(name: str) -> str: - if codex_skill_mode: + if _is_dollar_skills_agent(selected_ai, _ai_skills_enabled): return f"$speckit-{name}" if kimi_skill_mode: return f"/skill:speckit-{name}" diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index ecc26fa877..56daf20721 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -27,7 +27,7 @@ from packaging.specifiers import InvalidSpecifier, SpecifierSet from ._init_options import is_ai_skills_enabled -from ._invocation_style import is_slash_skills_agent +from ._invocation_style import is_dollar_skills_agent, is_slash_skills_agent from ._utils import dump_frontmatter from .catalogs import CatalogEntry as BaseCatalogEntry from .catalogs import CatalogStackBase @@ -2848,12 +2848,12 @@ def _render_hook_invocation(self, command: Any) -> str: selected_ai = init_options.get("ai") ai_skills_enabled = is_ai_skills_enabled(init_options) - codex_skill_mode = selected_ai == "codex" and ai_skills_enabled + dollar_skill_mode = is_dollar_skills_agent(selected_ai, ai_skills_enabled) kimi_skill_mode = selected_ai == "kimi" cline_mode = selected_ai == "cline" skill_name = self._skill_name_from_command(command_id) - if codex_skill_mode and skill_name: + if dollar_skill_mode and skill_name: return f"${skill_name}" if kimi_skill_mode and skill_name: return f"/skill:{skill_name}" diff --git a/tests/integrations/test_integration_zcode.py b/tests/integrations/test_integration_zcode.py index 4c54ed8ff5..3eb82ed4f2 100644 --- a/tests/integrations/test_integration_zcode.py +++ b/tests/integrations/test_integration_zcode.py @@ -9,3 +9,30 @@ class TestZcodeIntegration(SkillsIntegrationTests): COMMANDS_SUBDIR = "skills" REGISTRAR_DIR = ".zcode/skills" CONTEXT_FILE = "ZCODE.md" + + +class TestZcodeInvocation: + """ZCode renders $speckit-* chat invocations (like Codex).""" + + def test_next_steps_show_dollar_skill_invocation(self, tmp_path): + """ZCode next-steps guidance should display $speckit-* usage.""" + import os + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / "zcode-next-steps" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + runner = CliRunner() + result = runner.invoke(app, [ + "init", "--here", "--integration", "zcode", + "--ignore-agent-tools", "--script", "sh", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + + assert result.exit_code == 0 + assert "$speckit-constitution" in result.output + assert "/speckit.constitution" not in result.output diff --git a/tests/test_extensions.py b/tests/test_extensions.py index e063571b14..daa2acd921 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -6018,6 +6018,24 @@ def test_codex_hooks_render_dollar_skill_invocation(self, project_dir): assert execution["command"] == "speckit.tasks" assert execution["invocation"] == "$speckit-tasks" + def test_zcode_hooks_render_dollar_skill_invocation(self, project_dir): + """ZCode projects with skills mode should render $speckit-* invocations.""" + init_options = project_dir / ".specify" / "init-options.json" + init_options.parent.mkdir(parents=True, exist_ok=True) + init_options.write_text(json.dumps({"ai": "zcode", "ai_skills": True})) + + hook_executor = HookExecutor(project_dir) + execution = hook_executor.execute_hook( + { + "extension": "test-ext", + "command": "speckit.tasks", + "optional": False, + } + ) + + assert execution["command"] == "speckit.tasks" + assert execution["invocation"] == "$speckit-tasks" + def test_non_boolean_ai_skills_keeps_default_hook_invocation(self, project_dir): """Corrupted truthy ai_skills values should not enable skill invocation.""" init_options = project_dir / ".specify" / "init-options.json"