Skip to content

Commit 9402ebd

Browse files
mnriemdhilipkumars
andauthored
Feat/ai skills (#1632)
* implement ai-skills command line switch * fix: address review comments, remove breaking change for existing projects, add tests * fix: review comments * fix: review comments * fix: review comments * fix: review comments * fix: review comments, add test cases for all the agents * fix: review comments * fix: review comments * chore: trigger CI * chore: trigger CodeQL * ci: add CodeQL workflow for code scanning * ci: add actions language to CodeQL workflow, disable default setup --------- Co-authored-by: dhilipkumars <s.dhilipkumar@gmail.com>
1 parent d410d18 commit 9402ebd

File tree

6 files changed

+920
-1
lines changed

6 files changed

+920
-1
lines changed

.github/workflows/codeql.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: "CodeQL"
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
analyze:
11+
name: Analyze
12+
runs-on: ubuntu-latest
13+
permissions:
14+
security-events: write
15+
contents: read
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
language: [ 'actions', 'python' ]
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
24+
- name: Initialize CodeQL
25+
uses: github/codeql-action/init@v3
26+
with:
27+
languages: ${{ matrix.language }}
28+
29+
- name: Perform CodeQL Analysis
30+
uses: github/codeql-action/analyze@v3
31+
with:
32+
category: "/language:${{ matrix.language }}"

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,27 @@
22

33
<!-- markdownlint-disable MD024 -->
44

5+
56
All notable changes to the Specify CLI and templates are documented here.
67

78
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
89
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
910

11+
## [0.1.1] - 2026-02-13
12+
13+
### Added
14+
15+
- **Agent Skills Installation**: New `--ai-skills` CLI option to install Prompt.MD templates as agent skills following [agentskills.io specification](https://agentskills.io/specification)
16+
- Skills are installed to agent-specific directories (e.g., `.claude/skills/`, `.gemini/skills/`, `.github/skills/`)
17+
- Codex uses `.agents/skills/` following Codex agent directory conventions
18+
- Default fallback directory is `.agents/skills/` for agents without a specific mapping
19+
- Requires `--ai` flag to be specified
20+
- Converts all 9 spec-kit command templates (specify, plan, tasks, implement, analyze, clarify, constitution, checklist, taskstoissues) to properly formatted SKILL.md files
21+
- **New projects**: command files are not installed when `--ai-skills` is used (skills replace commands)
22+
- **Existing repos** (`--here`): pre-existing command files are preserved — no breaking changes
23+
- `pyyaml` dependency (already present) used for YAML frontmatter parsing
24+
- **Unit tests** for `install_ai_skills`, `_get_skills_dir`, and `--ai-skills` CLI validation (51 test cases covering all 18 supported agents)
25+
1026
## [0.1.0] - 2026-01-28
1127

1228
### Added

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ The `specify` command supports the following options:
188188
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
189189
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
190190
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
191+
| `--ai-skills` | Flag | Install Prompt.MD templates as agent skills in agent-specific `skills/` directory (requires `--ai`) |
191192

192193
### Examples
193194

@@ -238,6 +239,12 @@ specify init my-project --ai claude --debug
238239
# Use GitHub token for API requests (helpful for corporate environments)
239240
specify init my-project --ai claude --github-token ghp_your_token_here
240241

242+
# Install agent skills with the project
243+
specify init my-project --ai claude --ai-skills
244+
245+
# Initialize in current directory with agent skills
246+
specify init --here --ai gemini --ai-skills
247+
241248
# Check system requirements
242249
specify check
243250
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "specify-cli"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
55
requires-python = ">=3.11"
66
dependencies = [

src/specify_cli/__init__.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import shutil
3333
import shlex
3434
import json
35+
import yaml
3536
from pathlib import Path
3637
from typing import Optional, Tuple
3738

@@ -983,6 +984,203 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker |
983984
else:
984985
console.print(f"[yellow]Warning: Could not initialize constitution: {e}[/yellow]")
985986

987+
# Agent-specific skill directory overrides for agents whose skills directory
988+
# doesn't follow the standard <agent_folder>/skills/ pattern
989+
AGENT_SKILLS_DIR_OVERRIDES = {
990+
"codex": ".agents/skills", # Codex agent layout override
991+
}
992+
993+
# Default skills directory for agents not in AGENT_CONFIG
994+
DEFAULT_SKILLS_DIR = ".agents/skills"
995+
996+
# Enhanced descriptions for each spec-kit command skill
997+
SKILL_DESCRIPTIONS = {
998+
"specify": "Create or update feature specifications from natural language descriptions. Use when starting new features or refining requirements. Generates spec.md with user stories, functional requirements, and acceptance criteria following spec-driven development methodology.",
999+
"plan": "Generate technical implementation plans from feature specifications. Use after creating a spec to define architecture, tech stack, and implementation phases. Creates plan.md with detailed technical design.",
1000+
"tasks": "Break down implementation plans into actionable task lists. Use after planning to create a structured task breakdown. Generates tasks.md with ordered, dependency-aware tasks.",
1001+
"implement": "Execute all tasks from the task breakdown to build the feature. Use after task generation to systematically implement the planned solution following TDD approach where applicable.",
1002+
"analyze": "Perform cross-artifact consistency analysis across spec.md, plan.md, and tasks.md. Use after task generation to identify gaps, duplications, and inconsistencies before implementation.",
1003+
"clarify": "Structured clarification workflow for underspecified requirements. Use before planning to resolve ambiguities through coverage-based questioning. Records answers in spec clarifications section.",
1004+
"constitution": "Create or update project governing principles and development guidelines. Use at project start to establish code quality, testing standards, and architectural constraints that guide all development.",
1005+
"checklist": "Generate custom quality checklists for validating requirements completeness and clarity. Use to create unit tests for English that ensure spec quality before implementation.",
1006+
"taskstoissues": "Convert tasks from tasks.md into GitHub issues. Use after task breakdown to track work items in GitHub project management.",
1007+
}
1008+
1009+
1010+
def _get_skills_dir(project_path: Path, selected_ai: str) -> Path:
1011+
"""Resolve the agent-specific skills directory for the given AI assistant.
1012+
1013+
Uses ``AGENT_SKILLS_DIR_OVERRIDES`` first, then falls back to
1014+
``AGENT_CONFIG[agent]["folder"] + "skills"``, and finally to
1015+
``DEFAULT_SKILLS_DIR``.
1016+
"""
1017+
if selected_ai in AGENT_SKILLS_DIR_OVERRIDES:
1018+
return project_path / AGENT_SKILLS_DIR_OVERRIDES[selected_ai]
1019+
1020+
agent_config = AGENT_CONFIG.get(selected_ai, {})
1021+
agent_folder = agent_config.get("folder", "")
1022+
if agent_folder:
1023+
return project_path / agent_folder.rstrip("/") / "skills"
1024+
1025+
return project_path / DEFAULT_SKILLS_DIR
1026+
1027+
1028+
def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker | None = None) -> bool:
1029+
"""Install Prompt.MD files from templates/commands/ as agent skills.
1030+
1031+
Skills are written to the agent-specific skills directory following the
1032+
`agentskills.io <https://agentskills.io/specification>`_ specification.
1033+
Installation is additive — existing files are never removed and prompt
1034+
command files in the agent's commands directory are left untouched.
1035+
1036+
Args:
1037+
project_path: Target project directory.
1038+
selected_ai: AI assistant key from ``AGENT_CONFIG``.
1039+
tracker: Optional progress tracker.
1040+
1041+
Returns:
1042+
``True`` if at least one skill was installed or all skills were
1043+
already present (idempotent re-run), ``False`` otherwise.
1044+
"""
1045+
# Locate command templates in the agent's extracted commands directory.
1046+
# download_and_extract_template() already placed the .md files here.
1047+
agent_config = AGENT_CONFIG.get(selected_ai, {})
1048+
agent_folder = agent_config.get("folder", "")
1049+
if agent_folder:
1050+
templates_dir = project_path / agent_folder.rstrip("/") / "commands"
1051+
else:
1052+
templates_dir = project_path / "commands"
1053+
1054+
if not templates_dir.exists() or not any(templates_dir.glob("*.md")):
1055+
# Fallback: try the repo-relative path (for running from source checkout)
1056+
# This also covers agents whose extracted commands are in a different
1057+
# format (e.g. gemini uses .toml, not .md).
1058+
script_dir = Path(__file__).parent.parent.parent # up from src/specify_cli/
1059+
fallback_dir = script_dir / "templates" / "commands"
1060+
if fallback_dir.exists() and any(fallback_dir.glob("*.md")):
1061+
templates_dir = fallback_dir
1062+
1063+
if not templates_dir.exists() or not any(templates_dir.glob("*.md")):
1064+
if tracker:
1065+
tracker.error("ai-skills", "command templates not found")
1066+
else:
1067+
console.print("[yellow]Warning: command templates not found, skipping skills installation[/yellow]")
1068+
return False
1069+
1070+
command_files = sorted(templates_dir.glob("*.md"))
1071+
if not command_files:
1072+
if tracker:
1073+
tracker.skip("ai-skills", "no command templates found")
1074+
else:
1075+
console.print("[yellow]No command templates found to install[/yellow]")
1076+
return False
1077+
1078+
# Resolve the correct skills directory for this agent
1079+
skills_dir = _get_skills_dir(project_path, selected_ai)
1080+
skills_dir.mkdir(parents=True, exist_ok=True)
1081+
1082+
if tracker:
1083+
tracker.start("ai-skills")
1084+
1085+
installed_count = 0
1086+
skipped_count = 0
1087+
for command_file in command_files:
1088+
try:
1089+
content = command_file.read_text(encoding="utf-8")
1090+
1091+
# Parse YAML frontmatter
1092+
if content.startswith("---"):
1093+
parts = content.split("---", 2)
1094+
if len(parts) >= 3:
1095+
frontmatter = yaml.safe_load(parts[1])
1096+
if not isinstance(frontmatter, dict):
1097+
frontmatter = {}
1098+
body = parts[2].strip()
1099+
else:
1100+
# File starts with --- but has no closing ---
1101+
console.print(f"[yellow]Warning: {command_file.name} has malformed frontmatter (no closing ---), treating as plain content[/yellow]")
1102+
frontmatter = {}
1103+
body = content
1104+
else:
1105+
frontmatter = {}
1106+
body = content
1107+
1108+
command_name = command_file.stem
1109+
# Normalize: extracted commands may be named "speckit.<cmd>.md";
1110+
# strip the "speckit." prefix so skill names stay clean and
1111+
# SKILL_DESCRIPTIONS lookups work.
1112+
if command_name.startswith("speckit."):
1113+
command_name = command_name[len("speckit."):]
1114+
skill_name = f"speckit-{command_name}"
1115+
1116+
# Create skill directory (additive — never removes existing content)
1117+
skill_dir = skills_dir / skill_name
1118+
skill_dir.mkdir(parents=True, exist_ok=True)
1119+
1120+
# Select the best description available
1121+
original_desc = frontmatter.get("description", "")
1122+
enhanced_desc = SKILL_DESCRIPTIONS.get(command_name, original_desc or f"Spec-kit workflow command: {command_name}")
1123+
1124+
# Build SKILL.md following agentskills.io spec
1125+
# Use yaml.safe_dump to safely serialise the frontmatter and
1126+
# avoid YAML injection from descriptions containing colons,
1127+
# quotes, or newlines.
1128+
# Normalize source filename for metadata — strip speckit. prefix
1129+
# so it matches the canonical templates/commands/<cmd>.md path.
1130+
source_name = command_file.name
1131+
if source_name.startswith("speckit."):
1132+
source_name = source_name[len("speckit."):]
1133+
1134+
frontmatter_data = {
1135+
"name": skill_name,
1136+
"description": enhanced_desc,
1137+
"compatibility": "Requires spec-kit project structure with .specify/ directory",
1138+
"metadata": {
1139+
"author": "github-spec-kit",
1140+
"source": f"templates/commands/{source_name}",
1141+
},
1142+
}
1143+
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
1144+
skill_content = (
1145+
f"---\n"
1146+
f"{frontmatter_text}\n"
1147+
f"---\n\n"
1148+
f"# Speckit {command_name.title()} Skill\n\n"
1149+
f"{body}\n"
1150+
)
1151+
1152+
skill_file = skill_dir / "SKILL.md"
1153+
if skill_file.exists():
1154+
# Do not overwrite user-customized skills on re-runs
1155+
skipped_count += 1
1156+
continue
1157+
skill_file.write_text(skill_content, encoding="utf-8")
1158+
installed_count += 1
1159+
1160+
except Exception as e:
1161+
console.print(f"[yellow]Warning: Failed to install skill {command_file.stem}: {e}[/yellow]")
1162+
continue
1163+
1164+
if tracker:
1165+
if installed_count > 0 and skipped_count > 0:
1166+
tracker.complete("ai-skills", f"{installed_count} new + {skipped_count} existing skills in {skills_dir.relative_to(project_path)}")
1167+
elif installed_count > 0:
1168+
tracker.complete("ai-skills", f"{installed_count} skills → {skills_dir.relative_to(project_path)}")
1169+
elif skipped_count > 0:
1170+
tracker.complete("ai-skills", f"{skipped_count} skills already present")
1171+
else:
1172+
tracker.error("ai-skills", "no skills installed")
1173+
else:
1174+
if installed_count > 0:
1175+
console.print(f"[green]✓[/green] Installed {installed_count} agent skills to {skills_dir.relative_to(project_path)}/")
1176+
elif skipped_count > 0:
1177+
console.print(f"[green]✓[/green] {skipped_count} agent skills already present in {skills_dir.relative_to(project_path)}/")
1178+
else:
1179+
console.print("[yellow]No skills were installed[/yellow]")
1180+
1181+
return installed_count > 0 or skipped_count > 0
1182+
1183+
9861184
@app.command()
9871185
def init(
9881186
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
@@ -995,6 +1193,7 @@ def init(
9951193
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
9961194
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
9971195
github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
1196+
ai_skills: bool = typer.Option(False, "--ai-skills", help="Install Prompt.MD templates as agent skills (requires --ai)"),
9981197
):
9991198
"""
10001199
Initialize a new Specify project from the latest template.
@@ -1019,6 +1218,8 @@ def init(
10191218
specify init --here --ai codebuddy
10201219
specify init --here
10211220
specify init --here --force # Skip confirmation when current directory not empty
1221+
specify init my-project --ai claude --ai-skills # Install agent skills
1222+
specify init --here --ai gemini --ai-skills
10221223
"""
10231224

10241225
show_banner()
@@ -1035,6 +1236,11 @@ def init(
10351236
console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag")
10361237
raise typer.Exit(1)
10371238

1239+
if ai_skills and not ai_assistant:
1240+
console.print("[red]Error:[/red] --ai-skills requires --ai to be specified")
1241+
console.print("[yellow]Usage:[/yellow] specify init <project> --ai <agent> --ai-skills")
1242+
raise typer.Exit(1)
1243+
10381244
if here:
10391245
project_name = Path.cwd().name
10401246
project_path = Path.cwd()
@@ -1150,6 +1356,11 @@ def init(
11501356
("extracted-summary", "Extraction summary"),
11511357
("chmod", "Ensure scripts executable"),
11521358
("constitution", "Constitution setup"),
1359+
]:
1360+
tracker.add(key, label)
1361+
if ai_skills:
1362+
tracker.add("ai-skills", "Install agent skills")
1363+
for key, label in [
11531364
("cleanup", "Cleanup"),
11541365
("git", "Initialize git repository"),
11551366
("final", "Finalize")
@@ -1172,6 +1383,29 @@ def init(
11721383

11731384
ensure_constitution_from_template(project_path, tracker=tracker)
11741385

1386+
if ai_skills:
1387+
skills_ok = install_ai_skills(project_path, selected_ai, tracker=tracker)
1388+
1389+
# When --ai-skills is used on a NEW project and skills were
1390+
# successfully installed, remove the command files that the
1391+
# template archive just created. Skills replace commands, so
1392+
# keeping both would be confusing. For --here on an existing
1393+
# repo we leave pre-existing commands untouched to avoid a
1394+
# breaking change. We only delete AFTER skills succeed so the
1395+
# project always has at least one of {commands, skills}.
1396+
if skills_ok and not here:
1397+
agent_cfg = AGENT_CONFIG.get(selected_ai, {})
1398+
agent_folder = agent_cfg.get("folder", "")
1399+
if agent_folder:
1400+
cmds_dir = project_path / agent_folder.rstrip("/") / "commands"
1401+
if cmds_dir.exists():
1402+
try:
1403+
shutil.rmtree(cmds_dir)
1404+
except OSError:
1405+
# Best-effort cleanup: skills are already installed,
1406+
# so leaving stale commands is non-fatal.
1407+
console.print("[yellow]Warning: could not remove extracted commands directory[/yellow]")
1408+
11751409
if not no_git:
11761410
tracker.start("git")
11771411
if is_git_repo(project_path):

0 commit comments

Comments
 (0)