Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ Community projects that extend, visualize, or build on Spec Kit:
| [Kiro CLI](https://kiro.dev/docs/cli/) | ✅ | Use `--ai kiro-cli` (alias: `--ai kiro`) |
| [Amp](https://ampcode.com/) | ✅ | |
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | |
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | |
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | Installs skills in `.claude/skills`; invoke spec-kit as `/speckit-constitution`, `/speckit-plan`, etc. |
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
| [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>`. |
| [Cursor](https://cursor.sh/) | ✅ | |
Expand Down Expand Up @@ -382,8 +382,8 @@ specify init my-project --ai claude --debug
# Use GitHub token for API requests (helpful for corporate environments)
specify init my-project --ai claude --github-token ghp_your_token_here

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

# Initialize in current directory with agent skills
specify init --here --ai gemini --ai-skills
Expand All @@ -397,7 +397,11 @@ specify check

### Available Slash Commands

After running `specify init`, your AI coding agent will have access to these slash commands for structured development.
After running `specify init`, your AI coding agent will have access to these structured development commands.

Most agents expose the traditional dotted slash commands shown below, like `/speckit.plan`.

Claude Code installs spec-kit as skills and invokes them as `/speckit-constitution`, `/speckit-specify`, `/speckit-plan`, `/speckit-tasks`, and `/speckit-implement`.

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

Expand Down
38 changes: 34 additions & 4 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,23 @@ def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:


AGENT_SKILLS_MIGRATIONS = {
"claude": {
"error": (
"Claude Code now installs spec-kit as agent skills; "
"legacy .claude/commands projects are kept for backwards compatibility."
),
"usage": "specify init <project> --ai claude",
"interactive_note": (
"'claude' was selected interactively; enabling [cyan]--ai-skills[/cyan] "
"automatically so spec-kit is installed to [cyan].claude/skills[/cyan]."
),
"explicit_note": (
"'claude' now installs spec-kit as agent skills; enabling "
"[cyan]--ai-skills[/cyan] automatically so commands are written to "
"[cyan].claude/skills[/cyan]."
),
"auto_enable_explicit": True,
},
"agy": {
"error": "Explicit command support was deprecated in Antigravity version 1.20.5.",
"usage": "specify init <project> --ai agy --ai-skills",
Expand Down Expand Up @@ -1942,7 +1959,7 @@ def init(
specify init --here --ai vibe # Initialize with Mistral Vibe support
specify init --here
specify init --here --force # Skip confirmation when current directory not empty
specify init my-project --ai claude --ai-skills # Install agent skills
specify init my-project --ai claude # Claude installs skills by default
specify init --here --ai gemini --ai-skills
specify init my-project --ai generic --ai-commands-dir .myagent/commands/ # Unsupported agent
specify init my-project --offline # Use bundled assets (no network access)
Expand Down Expand Up @@ -1976,6 +1993,7 @@ def init(

# Auto-promote: --ai <key> → integration path with a nudge (if registered)
use_integration = False
resolved_integration = None
if integration:
from .integrations import INTEGRATION_REGISTRY, get_integration
resolved_integration = get_integration(integration)
Expand Down Expand Up @@ -2062,16 +2080,25 @@ def init(
"copilot"
)

if not use_integration:
from .integrations import get_integration

resolved_integration = get_integration(selected_ai)
if resolved_integration:
use_integration = True

# Agents that have moved from explicit commands/prompts to agent skills.
if selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
migration = AGENT_SKILLS_MIGRATIONS[selected_ai]
# If selected interactively (no --ai provided), automatically enable
# ai_skills so the agent remains usable without requiring an extra flag.
# Preserve fail-fast behavior only for explicit '--ai <agent>' without skills.
if ai_assistant:
if ai_assistant and not migration.get("auto_enable_explicit", False):
_handle_agent_skills_migration(console, selected_ai)
else:
ai_skills = True
console.print(f"\n[yellow]Note:[/yellow] {AGENT_SKILLS_MIGRATIONS[selected_ai]['interactive_note']}")
note_key = "explicit_note" if ai_assistant else "interactive_note"
console.print(f"\n[yellow]Note:[/yellow] {migration[note_key]}")

# Validate --ai-commands-dir usage
if selected_ai == "generic":
Expand Down Expand Up @@ -2489,13 +2516,16 @@ def init(
step_num += 1

codex_skill_mode = selected_ai == "codex" and ai_skills
claude_skill_mode = selected_ai == "claude" and ai_skills
kimi_skill_mode = selected_ai == "kimi"
native_skill_mode = codex_skill_mode or kimi_skill_mode
native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode
usage_label = "skills" if native_skill_mode else "slash commands"

def _display_cmd(name: str) -> str:
if codex_skill_mode:
return f"$speckit-{name}"
if claude_skill_mode:
return f"/speckit-{name}"
if kimi_skill_mode:
return f"/skill:speckit-{name}"
return f"/speckit.{name}"
Expand Down
3 changes: 3 additions & 0 deletions src/specify_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2138,11 +2138,14 @@ def _render_hook_invocation(self, command: Any) -> str:
init_options = self._load_init_options()
selected_ai = init_options.get("ai")
codex_skill_mode = selected_ai == "codex" and bool(init_options.get("ai_skills"))
claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills"))
kimi_skill_mode = selected_ai == "kimi"

skill_name = self._skill_name_from_command(command_id)
if codex_skill_mode and skill_name:
return f"${skill_name}"
if claude_skill_mode and skill_name:
return f"/{skill_name}"
if kimi_skill_mode and skill_name:
return f"/skill:{skill_name}"

Expand Down
100 changes: 95 additions & 5 deletions src/specify_cli/integrations/claude/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,111 @@
"""Claude Code integration."""

from ..base import MarkdownIntegration
from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml

from ...agents import CommandRegistrar
from ..base import IntegrationBase
from ..manifest import IntegrationManifest


class ClaudeIntegration(IntegrationBase):
"""Integration for Claude Code skills."""

class ClaudeIntegration(MarkdownIntegration):
key = "claude"
config = {
"name": "Claude Code",
"folder": ".claude/",
"commands_subdir": "commands",
"commands_subdir": "skills",
"install_url": "https://docs.anthropic.com/en/docs/claude-code/setup",
"requires_cli": True,
}
registrar_config = {
"dir": ".claude/commands",
"dir": ".claude/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
"extension": "/SKILL.md",
}
context_file = "CLAUDE.md"

def command_filename(self, template_name: str) -> str:
"""Claude skills live at .claude/skills/<name>/SKILL.md."""
skill_name = f"speckit-{template_name.replace('.', '-')}"
return f"{skill_name}/SKILL.md"

def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: str) -> str:
"""Render a processed command template as a Claude skill."""
skill_name = f"speckit-{template_name.replace('.', '-')}"
description = frontmatter.get(
"description",
f"Spec-kit workflow command: {template_name}",
)
skill_frontmatter = {
"name": skill_name,
"description": description,
# Spec-kit workflows should only run when explicitly invoked.
"disable-model-invocation": True,
"compatibility": "Requires spec-kit project structure with .specify/ directory",
"metadata": {
"author": "github-spec-kit",
"source": f"templates/commands/{template_name}.md",
},
}
Comment thread
afurm marked this conversation as resolved.
Outdated
frontmatter_text = yaml.safe_dump(skill_frontmatter, sort_keys=False).strip()
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"

def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Install Claude skills into .claude/skills."""
templates = self.list_command_templates()
if not templates:
return []

project_root_resolved = project_root.resolve()
if manifest.project_root != project_root_resolved:
raise ValueError(
f"manifest.project_root ({manifest.project_root}) does not match "
f"project_root ({project_root_resolved})"
)

dest = self.commands_dest(project_root).resolve()
Comment thread
afurm marked this conversation as resolved.
Outdated
try:
dest.relative_to(project_root_resolved)
except ValueError as exc:
raise ValueError(
f"Integration destination {dest} escapes "
f"project root {project_root_resolved}"
) from exc
dest.mkdir(parents=True, exist_ok=True)

script_type = opts.get("script_type", "sh")
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS")
registrar = CommandRegistrar()
created: list[Path] = []

for src_file in templates:
raw = src_file.read_text(encoding="utf-8")
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
frontmatter, body = registrar.parse_frontmatter(processed)
if not isinstance(frontmatter, dict):
frontmatter = {}

rendered = self._render_skill(src_file.stem, frontmatter, body)
dst_file = self.write_file_and_record(
rendered,
dest / self.command_filename(src_file.stem),
project_root,
manifest,
)
created.append(dst_file)

created.extend(self.install_scripts(project_root, manifest))
return created
13 changes: 12 additions & 1 deletion src/specify_cli/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,10 @@ def _register_skills(
selected_ai = init_opts.get("ai")
if not isinstance(selected_ai, str):
return []
ai_skills_enabled = bool(init_opts.get("ai_skills"))
registrar = CommandRegistrar()
agent_config = registrar.AGENT_CONFIGS.get(selected_ai, {})
create_missing_skills = ai_skills_enabled and agent_config.get("extension") != "/SKILL.md"
Comment thread
afurm marked this conversation as resolved.

written: List[str] = []

Expand All @@ -741,6 +744,10 @@ def _register_skills(
target_skill_names.append(skill_name)
if legacy_skill_name != skill_name and (skills_dir / legacy_skill_name).is_dir():
target_skill_names.append(legacy_skill_name)
if not target_skill_names and create_missing_skills:
missing_skill_dir = skills_dir / skill_name
if not missing_skill_dir.exists():
target_skill_names.append(skill_name)
if not target_skill_names:
continue

Expand All @@ -760,6 +767,10 @@ def _register_skills(
)

for target_skill_name in target_skill_names:
skill_subdir = skills_dir / target_skill_name
if skill_subdir.exists() and not skill_subdir.is_dir():
continue
skill_subdir.mkdir(parents=True, exist_ok=True)
frontmatter_data = {
Comment thread
afurm marked this conversation as resolved.
Outdated
"name": target_skill_name,
"description": enhanced_desc,
Expand All @@ -778,7 +789,7 @@ def _register_skills(
f"{body}\n"
)

skill_file = skills_dir / target_skill_name / "SKILL.md"
skill_file = skill_subdir / "SKILL.md"
skill_file.write_text(skill_content, encoding="utf-8")
written.append(target_skill_name)

Expand Down
Loading