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:
meymchen
2026-06-23 01:14:18 +08:00
committed by GitHub
parent bbdf1b8f40
commit 6a3ee9b64e
10 changed files with 141 additions and 6 deletions

View File

@@ -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.

View File

@@ -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}"

View File

@@ -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}"

View File

@@ -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())

View 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)",
),
]