Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>` |
Comment thread
mnriem marked this conversation as resolved.
| [Zed](https://zed.dev/) | `zed` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `/speckit-<command>` |
Comment thread
mnriem marked this conversation as resolved.
| Generic | `generic` | Bring your own agent — use `--integration generic --integration-options="--commands-dir <path>"` for AI coding agents not listed above |

Expand Down
9 changes: 9 additions & 0 deletions integrations/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
}
14 changes: 14 additions & 0 deletions src/specify_cli/_invocation_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

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

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

Expand Down
14 changes: 12 additions & 2 deletions src/specify_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]"
Expand All @@ -743,15 +750,18 @@ 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`
# used by `is_slash_skills_agent()`.
_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}"
Expand Down
6 changes: 3 additions & 3 deletions src/specify_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, relative_extension_path_violation
from .catalogs import CatalogEntry as BaseCatalogEntry
from .catalogs import CatalogStackBase
Expand Down Expand Up @@ -2886,12 +2886,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}"
Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) --------------------------------------
Expand Down Expand Up @@ -116,6 +117,7 @@ def _register_builtins() -> None:
_register(TraeIntegration())
_register(VibeIntegration())
_register(WindsurfIntegration())
_register(ZcodeIntegration())
_register(ZedIntegration())


Expand Down
43 changes: 43 additions & 0 deletions src/specify_cli/integrations/zcode/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
mnriem marked this conversation as resolved.
"""

from __future__ import annotations

from ..base import IntegrationOption, SkillsIntegration


class ZcodeIntegration(SkillsIntegration):
Comment thread
mnriem marked this conversation as resolved.
"""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)",
),
]
Comment thread
mnriem marked this conversation as resolved.
38 changes: 38 additions & 0 deletions tests/integrations/test_integration_zcode.py
Original file line number Diff line number Diff line change
@@ -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"
Comment thread
mnriem marked this conversation as resolved.


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)
Comment thread
mnriem marked this conversation as resolved.

assert result.exit_code == 0
assert "$speckit-constitution" in result.output
assert "/speckit.constitution" not in result.output
18 changes: 18 additions & 0 deletions tests/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6052,6 +6052,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"
Expand Down