feat: migrate Codex/agy init to native skills workflow (#1906)

* feat: migrate codex and agy to native skills flow

* fix: harden codex skill frontmatter and script fallback

* fix: clarify skills separator default expansion

* fix: rewrite agent_scripts paths for codex skills

* fix: align kimi guidance and platform-aware codex fallback
This commit is contained in:
Hamilton Snow
2026-03-19 22:00:41 +08:00
committed by GitHub
parent a4b60aca7f
commit c8af730b14
9 changed files with 767 additions and 121 deletions

View File

@@ -31,7 +31,6 @@ import sys
import zipfile
import tempfile
import shutil
import shlex
import json
import json5
import stat
@@ -172,8 +171,8 @@ AGENT_CONFIG = {
},
"codex": {
"name": "Codex CLI",
"folder": ".codex/",
"commands_subdir": "prompts", # Special: uses prompts/ not commands/
"folder": ".agents/",
"commands_subdir": "skills", # Codex now uses project skills directly
"install_url": "https://github.com/openai/codex",
"requires_cli": True,
},
@@ -1211,6 +1210,9 @@ AGENT_SKILLS_DIR_OVERRIDES = {
# Default skills directory for agents not in AGENT_CONFIG
DEFAULT_SKILLS_DIR = ".agents/skills"
# Agents whose downloaded template already contains skills in the final layout.
NATIVE_SKILLS_AGENTS = {"codex", "kimi"}
# Enhanced descriptions for each spec-kit command skill
SKILL_DESCRIPTIONS = {
"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.",
@@ -1339,8 +1341,6 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker
command_name = command_name[len("speckit."):]
if command_name.endswith(".agent"):
command_name = command_name[:-len(".agent")]
# Kimi CLI discovers skills by directory name and invokes them as
# /skill:<name> — use dot separator to match packaging convention.
if selected_ai == "kimi":
skill_name = f"speckit.{command_name}"
else:
@@ -1416,20 +1416,45 @@ def install_ai_skills(project_path: Path, selected_ai: str, tracker: StepTracker
return installed_count > 0 or skipped_count > 0
def _handle_agy_deprecation(console: Console) -> None:
"""
Print the deprecation error for the Antigravity (agy) agent and exit.
def _has_bundled_skills(project_path: Path, selected_ai: str) -> bool:
"""Return True when a native-skills agent has spec-kit bundled skills."""
skills_dir = _get_skills_dir(project_path, selected_ai)
if not skills_dir.is_dir():
return False
Context for future AI reviewers (e.g., GitHub Copilot):
- Prior to Antigravity v1.20.5, users could rely on explicit agent command definitions generated by this tool.
- v1.20.5 removed this "command support" in favor of "Agent Skills".
- `specify_cli` handles this by throwing a fail-fast deprecation error if `--ai agy` is
invoked without the accompanying `--ai-skills` flag. Users are instructed to add
the skills flag to generate agent skills templates instead.
"""
console.print("\n[red]Error:[/red] Explicit command support was deprecated in Antigravity version 1.20.5.")
pattern = "speckit.*/SKILL.md" if selected_ai == "kimi" else "speckit-*/SKILL.md"
return any(skills_dir.glob(pattern))
AGENT_SKILLS_MIGRATIONS = {
"agy": {
"error": "Explicit command support was deprecated in Antigravity version 1.20.5.",
"usage": "specify init <project> --ai agy --ai-skills",
"interactive_note": (
"'agy' was selected interactively; enabling [cyan]--ai-skills[/cyan] "
"automatically for compatibility (explicit .agent/commands usage is deprecated)."
),
},
"codex": {
"error": (
"Custom prompt-based spec-kit initialization is deprecated for Codex CLI; "
"use agent skills instead."
),
"usage": "specify init <project> --ai codex --ai-skills",
"interactive_note": (
"'codex' was selected interactively; enabling [cyan]--ai-skills[/cyan] "
"automatically for compatibility (.agents/skills is the recommended Codex layout)."
),
},
}
def _handle_agent_skills_migration(console: Console, agent_key: str) -> None:
"""Print a fail-fast migration error for agents that now require skills."""
migration = AGENT_SKILLS_MIGRATIONS[agent_key]
console.print(f"\n[red]Error:[/red] {migration['error']}")
console.print("Please use [cyan]--ai-skills[/cyan] when initializing to install templates as agent skills instead.")
console.print("[yellow]Usage:[/yellow] specify init <project> --ai agy --ai-skills")
console.print(f"[yellow]Usage:[/yellow] {migration['usage']}")
raise typer.Exit(1)
@app.command()
@@ -1467,7 +1492,7 @@ def init(
specify init . --ai claude # Initialize in current directory
specify init . # Initialize in current directory (interactive AI selection)
specify init --here --ai claude # Alternative syntax for current directory
specify init --here --ai codex
specify init --here --ai codex --ai-skills
specify init --here --ai codebuddy
specify init --here --ai vibe # Initialize with Mistral Vibe support
specify init --here
@@ -1557,24 +1582,16 @@ def init(
"copilot"
)
# [DEPRECATION NOTICE: Antigravity (agy)]
# As of Antigravity v1.20.5, traditional CLI "command" support was fully removed
# in favor of "Agent Skills" (SKILL.md files under <agent_folder>/skills/<skill_name>/).
# Because 'specify_cli' historically populated .agent/commands/, we now must explicitly
# enforce the `--ai-skills` flag for `agy` to ensure valid template generation.
if selected_ai == "agy" and not ai_skills:
# If agy was selected interactively (no --ai provided), automatically enable
# Agents that have moved from explicit commands/prompts to agent skills.
if selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
# If selected interactively (no --ai provided), automatically enable
# ai_skills so the agent remains usable without requiring an extra flag.
# Preserve deprecation behavior only for explicit '--ai agy' without skills.
# Preserve fail-fast behavior only for explicit '--ai <agent>' without skills.
if ai_assistant:
_handle_agy_deprecation(console)
_handle_agent_skills_migration(console, selected_ai)
else:
ai_skills = True
console.print(
"\n[yellow]Note:[/yellow] 'agy' was selected interactively; "
"enabling [cyan]--ai-skills[/cyan] automatically for compatibility "
"(explicit .agent/commands usage is deprecated)."
)
console.print(f"\n[yellow]Note:[/yellow] {AGENT_SKILLS_MIGRATIONS[selected_ai]['interactive_note']}")
# Validate --ai-commands-dir usage
if selected_ai == "generic":
@@ -1698,28 +1715,41 @@ def init(
ensure_constitution_from_template(project_path, tracker=tracker)
if ai_skills:
skills_ok = install_ai_skills(project_path, selected_ai, tracker=tracker)
if selected_ai in NATIVE_SKILLS_AGENTS:
skills_dir = _get_skills_dir(project_path, selected_ai)
if not _has_bundled_skills(project_path, selected_ai):
raise RuntimeError(
f"Expected bundled agent skills in {skills_dir.relative_to(project_path)}, "
"but none were found. Re-run with an up-to-date template."
)
if tracker:
tracker.start("ai-skills")
tracker.complete("ai-skills", f"bundled skills → {skills_dir.relative_to(project_path)}")
else:
console.print(f"[green]✓[/green] Using bundled agent skills in {skills_dir.relative_to(project_path)}/")
else:
skills_ok = install_ai_skills(project_path, selected_ai, tracker=tracker)
# When --ai-skills is used on a NEW project and skills were
# successfully installed, remove the command files that the
# template archive just created. Skills replace commands, so
# keeping both would be confusing. For --here on an existing
# repo we leave pre-existing commands untouched to avoid a
# breaking change. We only delete AFTER skills succeed so the
# project always has at least one of {commands, skills}.
if skills_ok and not here:
agent_cfg = AGENT_CONFIG.get(selected_ai, {})
agent_folder = agent_cfg.get("folder", "")
commands_subdir = agent_cfg.get("commands_subdir", "commands")
if agent_folder:
cmds_dir = project_path / agent_folder.rstrip("/") / commands_subdir
if cmds_dir.exists():
try:
shutil.rmtree(cmds_dir)
except OSError:
# Best-effort cleanup: skills are already installed,
# so leaving stale commands is non-fatal.
console.print("[yellow]Warning: could not remove extracted commands directory[/yellow]")
# When --ai-skills is used on a NEW project and skills were
# successfully installed, remove the command files that the
# template archive just created. Skills replace commands, so
# keeping both would be confusing. For --here on an existing
# repo we leave pre-existing commands untouched to avoid a
# breaking change. We only delete AFTER skills succeed so the
# project always has at least one of {commands, skills}.
if skills_ok and not here:
agent_cfg = AGENT_CONFIG.get(selected_ai, {})
agent_folder = agent_cfg.get("folder", "")
commands_subdir = agent_cfg.get("commands_subdir", "commands")
if agent_folder:
cmds_dir = project_path / agent_folder.rstrip("/") / commands_subdir
if cmds_dir.exists():
try:
shutil.rmtree(cmds_dir)
except OSError:
# Best-effort cleanup: skills are already installed,
# so leaving stale commands is non-fatal.
console.print("[yellow]Warning: could not remove extracted commands directory[/yellow]")
if not no_git:
tracker.start("git")
@@ -1843,38 +1873,48 @@ def init(
steps_lines.append("1. You're already in the project directory!")
step_num = 2
# Add Codex-specific setup step if needed
if selected_ai == "codex":
codex_path = project_path / ".codex"
quoted_path = shlex.quote(str(codex_path))
if os.name == "nt": # Windows
cmd = f"setx CODEX_HOME {quoted_path}"
else: # Unix-like systems
cmd = f"export CODEX_HOME={quoted_path}"
steps_lines.append(f"{step_num}. Set [cyan]CODEX_HOME[/cyan] environment variable before running Codex: [cyan]{cmd}[/cyan]")
if selected_ai == "codex" and ai_skills:
steps_lines.append(f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]")
step_num += 1
steps_lines.append(f"{step_num}. Start using slash commands with your AI agent:")
codex_skill_mode = selected_ai == "codex" and ai_skills
kimi_skill_mode = selected_ai == "kimi"
native_skill_mode = codex_skill_mode or kimi_skill_mode
usage_label = "skills" if native_skill_mode else "slash commands"
steps_lines.append(" 2.1 [cyan]/speckit.constitution[/] - Establish project principles")
steps_lines.append(" 2.2 [cyan]/speckit.specify[/] - Create baseline specification")
steps_lines.append(" 2.3 [cyan]/speckit.plan[/] - Create implementation plan")
steps_lines.append(" 2.4 [cyan]/speckit.tasks[/] - Generate actionable tasks")
steps_lines.append(" 2.5 [cyan]/speckit.implement[/] - Execute implementation")
def _display_cmd(name: str) -> str:
if codex_skill_mode:
return f"$speckit-{name}"
if kimi_skill_mode:
return f"/skill:speckit.{name}"
return f"/speckit.{name}"
steps_lines.append(f"{step_num}. Start using {usage_label} with your AI agent:")
steps_lines.append(f" {step_num}.1 [cyan]{_display_cmd('constitution')}[/] - Establish project principles")
steps_lines.append(f" {step_num}.2 [cyan]{_display_cmd('specify')}[/] - Create baseline specification")
steps_lines.append(f" {step_num}.3 [cyan]{_display_cmd('plan')}[/] - Create implementation plan")
steps_lines.append(f" {step_num}.4 [cyan]{_display_cmd('tasks')}[/] - Generate actionable tasks")
steps_lines.append(f" {step_num}.5 [cyan]{_display_cmd('implement')}[/] - Execute implementation")
steps_panel = Panel("\n".join(steps_lines), title="Next Steps", border_style="cyan", padding=(1,2))
console.print()
console.print(steps_panel)
enhancement_intro = (
"Optional skills that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]"
if native_skill_mode
else "Optional commands that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]"
)
enhancement_lines = [
"Optional commands that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]",
enhancement_intro,
"",
"○ [cyan]/speckit.clarify[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]/speckit.plan[/] if used)",
"○ [cyan]/speckit.analyze[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]/speckit.tasks[/], before [cyan]/speckit.implement[/])",
"○ [cyan]/speckit.checklist[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]/speckit.plan[/])"
f"○ [cyan]{_display_cmd('clarify')}[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]{_display_cmd('plan')}[/] if used)",
f"○ [cyan]{_display_cmd('analyze')}[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]{_display_cmd('tasks')}[/], before [cyan]{_display_cmd('implement')}[/])",
f"○ [cyan]{_display_cmd('checklist')}[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]{_display_cmd('plan')}[/])"
]
enhancements_panel = Panel("\n".join(enhancement_lines), title="Enhancement Commands", border_style="cyan", padding=(1,2))
enhancements_title = "Enhancement Skills" if native_skill_mode else "Enhancement Commands"
enhancements_panel = Panel("\n".join(enhancement_lines), title=enhancements_title, border_style="cyan", padding=(1,2))
console.print()
console.print(enhancements_panel)