Skip to content

Commit a858c1d

Browse files
authored
Install Claude Code as native skills and align preset/integration flows (#2051)
* Use Claude skills for generated commands * Fix Claude integration and preset skill flows * Group Claude tests in integration suite * Align Claude skill frontmatter across generators * Fix native skill preset cleanup * Keep legacy AI skills test on legacy path * Move Claude here-mode test to CLI suite
1 parent d9ce7c1 commit a858c1d

File tree

11 files changed

+633
-79
lines changed

11 files changed

+633
-79
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ Community projects that extend, visualize, or build on Spec Kit:
281281
| [Kiro CLI](https://kiro.dev/docs/cli/) || Use `--ai kiro-cli` (alias: `--ai kiro`) |
282282
| [Amp](https://ampcode.com/) || |
283283
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) || |
284-
| [Claude Code](https://www.anthropic.com/claude-code) || |
284+
| [Claude Code](https://www.anthropic.com/claude-code) || Installs skills in `.claude/skills`; invoke spec-kit as `/speckit-constitution`, `/speckit-plan`, etc. |
285285
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) || |
286286
| [Codex CLI](https://github.com/openai/codex) || Requires `--ai-skills`. Codex recommends [skills](https://developers.openai.com/codex/skills) and treats [custom prompts](https://developers.openai.com/codex/custom-prompts) as deprecated. Spec-kit installs Codex skills into `.agents/skills` and invokes them as `$speckit-<command>`. |
287287
| [Cursor](https://cursor.sh/) || |
@@ -401,8 +401,8 @@ specify init my-project --ai claude --debug
401401
# Use GitHub token for API requests (helpful for corporate environments)
402402
specify init my-project --ai claude --github-token ghp_your_token_here
403403

404-
# Install agent skills with the project
405-
specify init my-project --ai claude --ai-skills
404+
# Claude Code installs skills with the project by default
405+
specify init my-project --ai claude
406406

407407
# Initialize in current directory with agent skills
408408
specify init --here --ai gemini --ai-skills
@@ -416,7 +416,11 @@ specify check
416416

417417
### Available Slash Commands
418418

419-
After running `specify init`, your AI coding agent will have access to these slash commands for structured development.
419+
After running `specify init`, your AI coding agent will have access to these structured development commands.
420+
421+
Most agents expose the traditional dotted slash commands shown below, like `/speckit.plan`.
422+
423+
Claude Code installs spec-kit as skills and invokes them as `/speckit-constitution`, `/speckit-specify`, `/speckit-plan`, `/speckit-tasks`, and `/speckit-implement`.
420424

421425
For Codex CLI, `--ai-skills` installs spec-kit as agent skills instead of slash-command prompt files. In Codex skills mode, invoke spec-kit as `$speckit-constitution`, `$speckit-specify`, `$speckit-plan`, `$speckit-tasks`, and `$speckit-implement`.
422426

src/specify_cli/__init__.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,8 @@ def install_ai_skills(
16401640
``True`` if at least one skill was installed or all skills were
16411641
already present (idempotent re-run), ``False`` otherwise.
16421642
"""
1643+
from .agents import CommandRegistrar
1644+
16431645
# Locate command templates in the agent's extracted commands directory.
16441646
# download_and_extract_template() already placed the .md files here.
16451647
agent_config = AGENT_CONFIG.get(selected_ai, {})
@@ -1741,15 +1743,12 @@ def install_ai_skills(
17411743
if source_name.endswith(".agent.md"):
17421744
source_name = source_name[:-len(".agent.md")] + ".md"
17431745

1744-
frontmatter_data = {
1745-
"name": skill_name,
1746-
"description": enhanced_desc,
1747-
"compatibility": "Requires spec-kit project structure with .specify/ directory",
1748-
"metadata": {
1749-
"author": "github-spec-kit",
1750-
"source": f"templates/commands/{source_name}",
1751-
},
1752-
}
1746+
frontmatter_data = CommandRegistrar.build_skill_frontmatter(
1747+
selected_ai,
1748+
skill_name,
1749+
enhanced_desc,
1750+
f"templates/commands/{source_name}",
1751+
)
17531752
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
17541753
skill_content = (
17551754
f"---\n"
@@ -1859,6 +1858,23 @@ def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:
18591858

18601859

18611860
AGENT_SKILLS_MIGRATIONS = {
1861+
"claude": {
1862+
"error": (
1863+
"Claude Code now installs spec-kit as agent skills; "
1864+
"legacy .claude/commands projects are kept for backwards compatibility."
1865+
),
1866+
"usage": "specify init <project> --ai claude",
1867+
"interactive_note": (
1868+
"'claude' was selected interactively; enabling [cyan]--ai-skills[/cyan] "
1869+
"automatically so spec-kit is installed to [cyan].claude/skills[/cyan]."
1870+
),
1871+
"explicit_note": (
1872+
"'claude' now installs spec-kit as agent skills; enabling "
1873+
"[cyan]--ai-skills[/cyan] automatically so commands are written to "
1874+
"[cyan].claude/skills[/cyan]."
1875+
),
1876+
"auto_enable_explicit": True,
1877+
},
18621878
"agy": {
18631879
"error": "Explicit command support was deprecated in Antigravity version 1.20.5.",
18641880
"usage": "specify init <project> --ai agy --ai-skills",
@@ -1943,7 +1959,7 @@ def init(
19431959
specify init --here --ai vibe # Initialize with Mistral Vibe support
19441960
specify init --here
19451961
specify init --here --force # Skip confirmation when current directory not empty
1946-
specify init my-project --ai claude --ai-skills # Install agent skills
1962+
specify init my-project --ai claude # Claude installs skills by default
19471963
specify init --here --ai gemini --ai-skills
19481964
specify init my-project --ai generic --ai-commands-dir .myagent/commands/ # Unsupported agent
19491965
specify init my-project --offline # Use bundled assets (no network access)
@@ -1977,6 +1993,7 @@ def init(
19771993

19781994
# Auto-promote: --ai <key> → integration path with a nudge (if registered)
19791995
use_integration = False
1996+
resolved_integration = None
19801997
if integration:
19811998
from .integrations import INTEGRATION_REGISTRY, get_integration
19821999
resolved_integration = get_integration(integration)
@@ -2098,11 +2115,13 @@ def init(
20982115
# If selected interactively (no --ai provided), automatically enable
20992116
# ai_skills so the agent remains usable without requiring an extra flag.
21002117
# Preserve fail-fast behavior only for explicit '--ai <agent>' without skills.
2101-
if ai_assistant:
2118+
migration = AGENT_SKILLS_MIGRATIONS[selected_ai]
2119+
if ai_assistant and not migration.get("auto_enable_explicit", False):
21022120
_handle_agent_skills_migration(console, selected_ai)
21032121
else:
21042122
ai_skills = True
2105-
console.print(f"\n[yellow]Note:[/yellow] {AGENT_SKILLS_MIGRATIONS[selected_ai]['interactive_note']}")
2123+
note_key = "explicit_note" if ai_assistant else "interactive_note"
2124+
console.print(f"\n[yellow]Note:[/yellow] {migration[note_key]}")
21062125

21072126
# Validate --ai-commands-dir usage.
21082127
# Skip validation when --integration-options is provided — the integration
@@ -2540,27 +2559,33 @@ def init(
25402559
step_num = 2
25412560

25422561
# Determine skill display mode for the next-steps panel.
2543-
# Skills integrations (codex, kimi, agy) should show skill invocation syntax
2544-
# regardless of whether --ai-skills was explicitly passed.
2562+
# Skills integrations (codex, claude, kimi, agy) should show skill
2563+
# invocation syntax regardless of whether --ai-skills was explicitly passed.
25452564
_is_skills_integration = False
25462565
if use_integration:
25472566
from .integrations.base import SkillsIntegration as _SkillsInt
25482567
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)
25492568

25502569
codex_skill_mode = selected_ai == "codex" and (ai_skills or _is_skills_integration)
2570+
claude_skill_mode = selected_ai == "claude" and (ai_skills or _is_skills_integration)
25512571
kimi_skill_mode = selected_ai == "kimi"
25522572
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
2553-
native_skill_mode = codex_skill_mode or kimi_skill_mode or agy_skill_mode
2573+
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode
25542574

25552575
if codex_skill_mode and not ai_skills:
25562576
# Integration path installed skills; show the helpful notice
25572577
steps_lines.append(f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]")
25582578
step_num += 1
2579+
if claude_skill_mode and not ai_skills:
2580+
steps_lines.append(f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]")
2581+
step_num += 1
25592582
usage_label = "skills" if native_skill_mode else "slash commands"
25602583

25612584
def _display_cmd(name: str) -> str:
25622585
if codex_skill_mode or agy_skill_mode:
25632586
return f"$speckit-{name}"
2587+
if claude_skill_mode:
2588+
return f"/speckit-{name}"
25642589
if kimi_skill_mode:
25652590
return f"/skill:speckit-{name}"
25662591
return f"/speckit.{name}"

src/specify_cli/agents.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,16 +370,35 @@ def render_skill_command(
370370
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
371371

372372
description = frontmatter.get("description", f"Spec-kit workflow command: {skill_name}")
373+
skill_frontmatter = self.build_skill_frontmatter(
374+
agent_name,
375+
skill_name,
376+
description,
377+
f"{source_id}:{source_file}",
378+
)
379+
return self.render_frontmatter(skill_frontmatter) + "\n" + body
380+
381+
@staticmethod
382+
def build_skill_frontmatter(
383+
agent_name: str,
384+
skill_name: str,
385+
description: str,
386+
source: str,
387+
) -> dict:
388+
"""Build consistent SKILL.md frontmatter across all skill generators."""
373389
skill_frontmatter = {
374390
"name": skill_name,
375391
"description": description,
376392
"compatibility": "Requires spec-kit project structure with .specify/ directory",
377393
"metadata": {
378394
"author": "github-spec-kit",
379-
"source": f"{source_id}:{source_file}",
395+
"source": source,
380396
},
381397
}
382-
return self.render_frontmatter(skill_frontmatter) + "\n" + body
398+
if agent_name == "claude":
399+
# Claude skills should only run when explicitly invoked.
400+
skill_frontmatter["disable-model-invocation"] = True
401+
return skill_frontmatter
383402

384403
@staticmethod
385404
def resolve_skill_placeholders(agent_name: str, frontmatter: dict, body: str, project_root: Path) -> str:

src/specify_cli/extensions.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -801,15 +801,12 @@ def _register_extension_skills(
801801
original_desc = frontmatter.get("description", "")
802802
description = original_desc or f"Extension command: {cmd_name}"
803803

804-
frontmatter_data = {
805-
"name": skill_name,
806-
"description": description,
807-
"compatibility": "Requires spec-kit project structure with .specify/ directory",
808-
"metadata": {
809-
"author": "github-spec-kit",
810-
"source": f"extension:{manifest.id}",
811-
},
812-
}
804+
frontmatter_data = registrar.build_skill_frontmatter(
805+
selected_ai,
806+
skill_name,
807+
description,
808+
f"extension:{manifest.id}",
809+
)
813810
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
814811

815812
# Derive a human-friendly title from the command name
@@ -2138,11 +2135,14 @@ def _render_hook_invocation(self, command: Any) -> str:
21382135
init_options = self._load_init_options()
21392136
selected_ai = init_options.get("ai")
21402137
codex_skill_mode = selected_ai == "codex" and bool(init_options.get("ai_skills"))
2138+
claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills"))
21412139
kimi_skill_mode = selected_ai == "kimi"
21422140

21432141
skill_name = self._skill_name_from_command(command_id)
21442142
if codex_skill_mode and skill_name:
21452143
return f"${skill_name}"
2144+
if claude_skill_mode and skill_name:
2145+
return f"/{skill_name}"
21462146
if kimi_skill_mode and skill_name:
21472147
return f"/skill:{skill_name}"
21482148

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,106 @@
11
"""Claude Code integration."""
22

3-
from ..base import MarkdownIntegration
3+
from __future__ import annotations
44

5+
from pathlib import Path
6+
from typing import Any
7+
8+
import yaml
9+
10+
from ...agents import CommandRegistrar
11+
from ..base import SkillsIntegration
12+
from ..manifest import IntegrationManifest
13+
14+
15+
class ClaudeIntegration(SkillsIntegration):
16+
"""Integration for Claude Code skills."""
517

6-
class ClaudeIntegration(MarkdownIntegration):
718
key = "claude"
819
config = {
920
"name": "Claude Code",
1021
"folder": ".claude/",
11-
"commands_subdir": "commands",
22+
"commands_subdir": "skills",
1223
"install_url": "https://docs.anthropic.com/en/docs/claude-code/setup",
1324
"requires_cli": True,
1425
}
1526
registrar_config = {
16-
"dir": ".claude/commands",
27+
"dir": ".claude/skills",
1728
"format": "markdown",
1829
"args": "$ARGUMENTS",
19-
"extension": ".md",
30+
"extension": "/SKILL.md",
2031
}
2132
context_file = "CLAUDE.md"
33+
34+
def command_filename(self, template_name: str) -> str:
35+
"""Claude skills live at .claude/skills/<name>/SKILL.md."""
36+
skill_name = f"speckit-{template_name.replace('.', '-')}"
37+
return f"{skill_name}/SKILL.md"
38+
39+
def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: str) -> str:
40+
"""Render a processed command template as a Claude skill."""
41+
skill_name = f"speckit-{template_name.replace('.', '-')}"
42+
description = frontmatter.get(
43+
"description",
44+
f"Spec-kit workflow command: {template_name}",
45+
)
46+
skill_frontmatter = CommandRegistrar.build_skill_frontmatter(
47+
self.key,
48+
skill_name,
49+
description,
50+
f"templates/commands/{template_name}.md",
51+
)
52+
frontmatter_text = yaml.safe_dump(skill_frontmatter, sort_keys=False).strip()
53+
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"
54+
55+
def setup(
56+
self,
57+
project_root: Path,
58+
manifest: IntegrationManifest,
59+
parsed_options: dict[str, Any] | None = None,
60+
**opts: Any,
61+
) -> list[Path]:
62+
"""Install Claude skills into .claude/skills."""
63+
templates = self.list_command_templates()
64+
if not templates:
65+
return []
66+
67+
project_root_resolved = project_root.resolve()
68+
if manifest.project_root != project_root_resolved:
69+
raise ValueError(
70+
f"manifest.project_root ({manifest.project_root}) does not match "
71+
f"project_root ({project_root_resolved})"
72+
)
73+
74+
dest = self.skills_dest(project_root).resolve()
75+
try:
76+
dest.relative_to(project_root_resolved)
77+
except ValueError as exc:
78+
raise ValueError(
79+
f"Integration destination {dest} escapes "
80+
f"project root {project_root_resolved}"
81+
) from exc
82+
dest.mkdir(parents=True, exist_ok=True)
83+
84+
script_type = opts.get("script_type", "sh")
85+
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS")
86+
registrar = CommandRegistrar()
87+
created: list[Path] = []
88+
89+
for src_file in templates:
90+
raw = src_file.read_text(encoding="utf-8")
91+
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
92+
frontmatter, body = registrar.parse_frontmatter(processed)
93+
if not isinstance(frontmatter, dict):
94+
frontmatter = {}
95+
96+
rendered = self._render_skill(src_file.stem, frontmatter, body)
97+
dst_file = self.write_file_and_record(
98+
rendered,
99+
dest / self.command_filename(src_file.stem),
100+
project_root,
101+
manifest,
102+
)
103+
created.append(dst_file)
104+
105+
created.extend(self.install_scripts(project_root, manifest))
106+
return created

0 commit comments

Comments
 (0)