mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
feat: add ZCode (Z.AI) integration (#3063)
* 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-<name>/SKILL.md, invoked in chat as $speckit-<name>. - 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 <noreply@anthropic.com> * fix: render $speckit-* invocations for ZCode skills ZCode is documented as a skills agent invoked with $speckit-<command>, but the central invocation rendering only special-cased codex, so specify init Next Steps and extension hooks rendered the dotted /speckit.<command> 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -403,7 +403,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 <project_name> --integration copilot --ignore-agent-tools
|
||||
|
||||
@@ -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-<command>` |
|
||||
| [Zed](https://zed.dev/) | `zed` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `/speckit-<command>` |
|
||||
| Generic | `generic` | Bring your own agent — use `--integration generic --integration-options="--commands-dir <path>"` for AI coding agents not listed above |
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ and ``specify init``'s next-steps output stay consistent.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Agents that render $speckit-<name> (chat invocation) when in skills mode.
|
||||
DOLLAR_SKILLS_AGENTS: frozenset[str] = frozenset({"codex", "zcode"})
|
||||
|
||||
# Agents that always render /speckit-<name>, regardless of ai_skills.
|
||||
ALWAYS_SLASH_AGENTS: frozenset[str] = frozenset({"devin", "trae", "zed"})
|
||||
|
||||
@@ -26,6 +29,17 @@ CONDITIONAL_SLASH_AGENTS: frozenset[str] = frozenset(
|
||||
)
|
||||
|
||||
|
||||
def is_dollar_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) -> bool:
|
||||
"""Return ``True`` if *selected_ai* uses ``$speckit-<name>`` invocations.
|
||||
|
||||
Agents in `DOLLAR_SKILLS_AGENTS` (e.g. ``codex``, ``zcode``) render
|
||||
``$speckit-<name>`` 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-<name>`` invocations.
|
||||
|
||||
|
||||
@@ -693,6 +693,7 @@ def register(app: typer.Typer) -> None:
|
||||
) 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 register(app: typer.Typer) -> None:
|
||||
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 register(app: typer.Typer) -> None:
|
||||
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 register(app: typer.Typer) -> None:
|
||||
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 register(app: typer.Typer) -> None:
|
||||
_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}"
|
||||
|
||||
@@ -27,7 +27,7 @@ from packaging import version as pkg_version
|
||||
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, relative_extension_path_violation
|
||||
from .catalogs import CatalogEntry as BaseCatalogEntry
|
||||
from .catalogs import CatalogStackBase
|
||||
@@ -2886,12 +2886,12 @@ class HookExecutor:
|
||||
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}"
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
|
||||
43
src/specify_cli/integrations/zcode/__init__.py
Normal file
43
src/specify_cli/integrations/zcode/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""ZCode integration — skills-based agent (Z.AI).
|
||||
|
||||
ZCode uses the ``.zcode/skills/speckit-<name>/SKILL.md`` layout, matching
|
||||
the Claude Code skill format. Skills are invoked in chat with
|
||||
``$speckit-<name>``. 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)",
|
||||
),
|
||||
]
|
||||
38
tests/integrations/test_integration_zcode.py
Normal file
38
tests/integrations/test_integration_zcode.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""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"
|
||||
|
||||
|
||||
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
|
||||
@@ -6052,6 +6052,24 @@ class TestHookInvocationRendering:
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user