Skip to content

Commit e9ede6d

Browse files
committed
Stage 5: Skills, Generic & Option-Driven Integrations (#1924)
Add SkillsIntegration base class and migrate codex, kimi, agy, and generic to the integration system. Integrations: - SkillsIntegration(IntegrationBase) in base.py — creates speckit-<name>/SKILL.md layout matching release ZIP output byte-for-byte - CodexIntegration — .agents/skills/, --skills default=True - KimiIntegration — .kimi/skills/, --skills + --migrate-legacy options, dotted→hyphenated skill directory migration - AgyIntegration — .agent/skills/, skills-only (commands deprecated v1.20.5) - GenericIntegration — user-specified --commands-dir, MarkdownIntegration - All four have update-context.sh/.ps1 scripts - All four registered in INTEGRATION_REGISTRY CLI changes: - --ai <agent> auto-promotes to integration path for all registered agents - Interactive agent selection also auto-promotes (bug fix) - --ai-skills and --ai-commands-dir show deprecation notices on integration path - Next-steps display shows correct skill invocation syntax for skills integrations - agy added to CommandRegistrar.AGENT_CONFIGS Tests: - test_integration_base_skills.py — reusable mixin with setup, frontmatter, directory structure, scripts, CLI auto-promote, and complete file inventory (sh+ps) tests - Per-agent test files: test_integration_{codex,kimi,agy,generic}.py - Kimi legacy migration tests, generic --commands-dir validation - Registry updated with Stage 5 keys - Removed 9 dead-mock tests, moved 4 integration tests to proper locations - Fixed all bare project-name tests to use tmp_path - Fixed 6 pre-existing ANSI escape code test failures in test_extensions.py and test_presets.py 1524 tests pass, 0 failures.
1 parent 682ffbf commit e9ede6d

27 files changed

Lines changed: 1705 additions & 410 deletions

src/specify_cli/__init__.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,19 @@ def init(
19971997
f"--ai {ai_assistant}. The --ai flag will be deprecated in a future release.[/dim]"
19981998
)
19991999

2000+
# Deprecation warnings for --ai-skills and --ai-commands-dir when using integration path
2001+
if use_integration:
2002+
if ai_skills:
2003+
console.print(
2004+
"[dim]Note: --ai-skills is not needed with --integration; "
2005+
"skills are the default for this integration.[/dim]"
2006+
)
2007+
if ai_commands_dir:
2008+
console.print(
2009+
"[dim]Note: --ai-commands-dir is deprecated with --integration; "
2010+
"use --integration generic with parsed options instead.[/dim]"
2011+
)
2012+
20002013
if project_name == ".":
20012014
here = True
20022015
project_name = None # Clear project_name to use existing validation logic
@@ -2062,8 +2075,18 @@ def init(
20622075
"copilot"
20632076
)
20642077

2078+
# Auto-promote interactively selected agents to the integration path
2079+
# when a matching integration is registered (same behavior as --ai).
2080+
if not use_integration:
2081+
from .integrations import get_integration as _get_int
2082+
_resolved = _get_int(selected_ai)
2083+
if _resolved:
2084+
use_integration = True
2085+
resolved_integration = _resolved
2086+
20652087
# Agents that have moved from explicit commands/prompts to agent skills.
2066-
if selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
2088+
# Skip this check when using the integration path — skills are the default.
2089+
if not use_integration and selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
20672090
# If selected interactively (no --ai provided), automatically enable
20682091
# ai_skills so the agent remains usable without requiring an extra flag.
20692092
# Preserve fail-fast behavior only for explicit '--ai <agent>' without skills.
@@ -2073,15 +2096,16 @@ def init(
20732096
ai_skills = True
20742097
console.print(f"\n[yellow]Note:[/yellow] {AGENT_SKILLS_MIGRATIONS[selected_ai]['interactive_note']}")
20752098

2076-
# Validate --ai-commands-dir usage
2077-
if selected_ai == "generic":
2078-
if not ai_commands_dir:
2079-
console.print("[red]Error:[/red] --ai-commands-dir is required when using --ai generic")
2080-
console.print("[dim]Example: specify init my-project --ai generic --ai-commands-dir .myagent/commands/[/dim]")
2099+
# Validate --ai-commands-dir usage (legacy path only)
2100+
if not use_integration:
2101+
if selected_ai == "generic":
2102+
if not ai_commands_dir:
2103+
console.print("[red]Error:[/red] --ai-commands-dir is required when using --ai generic")
2104+
console.print("[dim]Example: specify init my-project --ai generic --ai-commands-dir .myagent/commands/[/dim]")
2105+
raise typer.Exit(1)
2106+
elif ai_commands_dir:
2107+
console.print(f"[red]Error:[/red] --ai-commands-dir can only be used with --ai generic (not '{selected_ai}')")
20812108
raise typer.Exit(1)
2082-
elif ai_commands_dir:
2083-
console.print(f"[red]Error:[/red] --ai-commands-dir can only be used with --ai generic (not '{selected_ai}')")
2084-
raise typer.Exit(1)
20852109

20862110
current_dir = Path.cwd()
20872111

@@ -2210,8 +2234,17 @@ def init(
22102234
manifest = IntegrationManifest(
22112235
resolved_integration.key, project_path, version=get_speckit_version()
22122236
)
2237+
2238+
# Build parsed_options from legacy CLI flags for Stage 5 integrations
2239+
integration_parsed_options: dict[str, Any] = {}
2240+
if ai_commands_dir:
2241+
integration_parsed_options["commands_dir"] = ai_commands_dir
2242+
if ai_skills:
2243+
integration_parsed_options["skills"] = True
2244+
22132245
resolved_integration.setup(
22142246
project_path, manifest,
2247+
parsed_options=integration_parsed_options or None,
22152248
script_type=selected_script,
22162249
)
22172250
manifest.save()
@@ -2268,7 +2301,7 @@ def init(
22682301
shutil.rmtree(project_path)
22692302
raise typer.Exit(1)
22702303
# For generic agent, rename placeholder directory to user-specified path
2271-
if selected_ai == "generic" and ai_commands_dir:
2304+
if not use_integration and selected_ai == "generic" and ai_commands_dir:
22722305
placeholder_dir = project_path / ".speckit" / "commands"
22732306
target_dir = project_path / ai_commands_dir
22742307
if placeholder_dir.is_dir():
@@ -2284,18 +2317,19 @@ def init(
22842317
ensure_constitution_from_template(project_path, tracker=tracker)
22852318

22862319
# Determine skills directory and migrate any legacy Kimi dotted skills.
2320+
# (Legacy path only — integration path handles skills in setup().)
22872321
migrated_legacy_kimi_skills = 0
22882322
removed_legacy_kimi_skills = 0
22892323
skills_dir: Optional[Path] = None
2290-
if selected_ai in NATIVE_SKILLS_AGENTS:
2324+
if not use_integration and selected_ai in NATIVE_SKILLS_AGENTS:
22912325
skills_dir = _get_skills_dir(project_path, selected_ai)
22922326
if selected_ai == "kimi" and skills_dir.is_dir():
22932327
(
22942328
migrated_legacy_kimi_skills,
22952329
removed_legacy_kimi_skills,
22962330
) = _migrate_legacy_kimi_dotted_skills(skills_dir)
22972331

2298-
if ai_skills:
2332+
if not use_integration and ai_skills:
22992333
if selected_ai in NATIVE_SKILLS_AGENTS:
23002334
bundled_found = _has_bundled_skills(project_path, selected_ai)
23012335
if bundled_found:
@@ -2484,17 +2518,27 @@ def init(
24842518
steps_lines.append("1. You're already in the project directory!")
24852519
step_num = 2
24862520

2487-
if selected_ai == "codex" and ai_skills:
2488-
steps_lines.append(f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]")
2489-
step_num += 1
2521+
# Determine skill display mode for the next-steps panel.
2522+
# Skills integrations (codex, kimi, agy) should show skill invocation syntax
2523+
# regardless of whether --ai-skills was explicitly passed.
2524+
_is_skills_integration = False
2525+
if use_integration:
2526+
from .integrations.base import SkillsIntegration as _SkillsInt
2527+
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)
24902528

2491-
codex_skill_mode = selected_ai == "codex" and ai_skills
2529+
codex_skill_mode = selected_ai == "codex" and (ai_skills or _is_skills_integration)
24922530
kimi_skill_mode = selected_ai == "kimi"
2493-
native_skill_mode = codex_skill_mode or kimi_skill_mode
2531+
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
2532+
native_skill_mode = codex_skill_mode or kimi_skill_mode or agy_skill_mode
2533+
2534+
if codex_skill_mode and not ai_skills:
2535+
# Integration path installed skills; show the helpful notice
2536+
steps_lines.append(f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]")
2537+
step_num += 1
24942538
usage_label = "skills" if native_skill_mode else "slash commands"
24952539

24962540
def _display_cmd(name: str) -> str:
2497-
if codex_skill_mode:
2541+
if codex_skill_mode or agy_skill_mode:
24982542
return f"$speckit-{name}"
24992543
if kimi_skill_mode:
25002544
return f"/skill:speckit-{name}"

src/specify_cli/agents.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ class CommandRegistrar:
168168
"format": "markdown",
169169
"args": "$ARGUMENTS",
170170
"extension": ".md"
171+
},
172+
"agy": {
173+
"dir": ".agent/skills",
174+
"format": "markdown",
175+
"args": "$ARGUMENTS",
176+
"extension": "/SKILL.md",
171177
}
172178
}
173179

src/specify_cli/integrations/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,21 @@ def _register_builtins() -> None:
4646
users install and invoke.
4747
"""
4848
# -- Imports (alphabetical) -------------------------------------------
49+
from .agy import AgyIntegration
4950
from .amp import AmpIntegration
5051
from .auggie import AuggieIntegration
5152
from .bob import BobIntegration
5253
from .claude import ClaudeIntegration
54+
from .codex import CodexIntegration
5355
from .codebuddy import CodebuddyIntegration
5456
from .copilot import CopilotIntegration
5557
from .cursor_agent import CursorAgentIntegration
5658
from .gemini import GeminiIntegration
59+
from .generic import GenericIntegration
5760
from .iflow import IflowIntegration
5861
from .junie import JunieIntegration
5962
from .kilocode import KilocodeIntegration
63+
from .kimi import KimiIntegration
6064
from .kiro_cli import KiroCliIntegration
6165
from .opencode import OpencodeIntegration
6266
from .pi import PiIntegration
@@ -70,17 +74,21 @@ def _register_builtins() -> None:
7074
from .windsurf import WindsurfIntegration
7175

7276
# -- Registration (alphabetical) --------------------------------------
77+
_register(AgyIntegration())
7378
_register(AmpIntegration())
7479
_register(AuggieIntegration())
7580
_register(BobIntegration())
7681
_register(ClaudeIntegration())
82+
_register(CodexIntegration())
7783
_register(CodebuddyIntegration())
7884
_register(CopilotIntegration())
7985
_register(CursorAgentIntegration())
8086
_register(GeminiIntegration())
87+
_register(GenericIntegration())
8188
_register(IflowIntegration())
8289
_register(JunieIntegration())
8390
_register(KilocodeIntegration())
91+
_register(KimiIntegration())
8492
_register(KiroCliIntegration())
8593
_register(OpencodeIntegration())
8694
_register(PiIntegration())
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Antigravity (agy) integration — skills-based agent.
2+
3+
Antigravity uses ``.agent/skills/speckit-<name>/SKILL.md`` layout.
4+
Explicit command support was deprecated in version 1.20.5;
5+
``--skills`` defaults to ``True``.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from ..base import IntegrationOption, SkillsIntegration
11+
12+
13+
class AgyIntegration(SkillsIntegration):
14+
"""Integration for Antigravity IDE."""
15+
16+
key = "agy"
17+
config = {
18+
"name": "Antigravity",
19+
"folder": ".agent/",
20+
"commands_subdir": "skills",
21+
"install_url": None,
22+
"requires_cli": False,
23+
}
24+
registrar_config = {
25+
"dir": ".agent/skills",
26+
"format": "markdown",
27+
"args": "$ARGUMENTS",
28+
"extension": "/SKILL.md",
29+
}
30+
context_file = "AGENTS.md"
31+
32+
@classmethod
33+
def options(cls) -> list[IntegrationOption]:
34+
return [
35+
IntegrationOption(
36+
"--skills",
37+
is_flag=True,
38+
default=True,
39+
help="Install as agent skills (default for Antigravity since v1.20.5)",
40+
),
41+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# update-context.ps1 — Antigravity (agy) integration: create/update AGENTS.md
2+
#
3+
# Thin wrapper that delegates to the shared update-agent-context script.
4+
5+
$ErrorActionPreference = 'Stop'
6+
7+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
8+
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
9+
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
10+
$repoRoot = $scriptDir
11+
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
12+
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
13+
$repoRoot = Split-Path -Parent $repoRoot
14+
}
15+
}
16+
17+
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType agy
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env bash
2+
# update-context.sh — Antigravity (agy) integration: create/update AGENTS.md
3+
#
4+
# Thin wrapper that delegates to the shared update-agent-context script.
5+
6+
set -euo pipefail
7+
8+
_script_dir="$(cd "$(dirname "$0")" && pwd)"
9+
_root="$_script_dir"
10+
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
11+
if [ -z "${REPO_ROOT:-}" ]; then
12+
if [ -d "$_root/.specify" ]; then
13+
REPO_ROOT="$_root"
14+
else
15+
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
16+
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
17+
REPO_ROOT="$git_root"
18+
else
19+
REPO_ROOT="$_root"
20+
fi
21+
fi
22+
fi
23+
24+
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" agy

0 commit comments

Comments
 (0)