Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
97 changes: 81 additions & 16 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,7 @@ def init(
preset: str = typer.Option(None, "--preset", help="Install a preset during initialization (by preset ID)"),
branch_numbering: str = typer.Option(None, "--branch-numbering", help="Branch numbering strategy: 'sequential' (001, 002, ...) or 'timestamp' (YYYYMMDD-HHMMSS)"),
integration: str = typer.Option(None, "--integration", help="Use the new integration system (e.g. --integration copilot). Mutually exclusive with --ai."),
integration_options: str = typer.Option(None, "--integration-options", help='Options for the integration (e.g. --integration-options="--commands-dir .myagent/cmds")'),
):
"""
Initialize a new Specify project.
Expand Down Expand Up @@ -1997,6 +1998,26 @@ def init(
f"--ai {ai_assistant}. The --ai flag will be deprecated in a future release.[/dim]"
)

# Deprecation warnings for --ai-skills and --ai-commands-dir when using integration path
if use_integration:
if ai_skills:
from .integrations.base import SkillsIntegration as _SkillsCheck
if isinstance(resolved_integration, _SkillsCheck):
console.print(
"[dim]Note: --ai-skills is not needed with --integration; "
"skills are the default for this integration.[/dim]"
)
else:
console.print(
"[dim]Note: --ai-skills has no effect with --integration "
f"{resolved_integration.key}; this integration uses commands, not skills.[/dim]"
)
if ai_commands_dir and resolved_integration.key != "generic":
console.print(
"[dim]Note: --ai-commands-dir is deprecated; "
'use [bold]--integration generic --integration-options="--commands-dir <dir>"[/bold] instead.[/dim]'
)

if project_name == ".":
here = True
project_name = None # Clear project_name to use existing validation logic
Expand Down Expand Up @@ -2062,8 +2083,18 @@ def init(
"copilot"
)

# Auto-promote interactively selected agents to the integration path
# when a matching integration is registered (same behavior as --ai).
if not use_integration:
from .integrations import get_integration as _get_int
_resolved = _get_int(selected_ai)
if _resolved:
use_integration = True
resolved_integration = _resolved

# Agents that have moved from explicit commands/prompts to agent skills.
if selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
# Skip this check when using the integration path — skills are the default.
if not use_integration and 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 fail-fast behavior only for explicit '--ai <agent>' without skills.
Expand All @@ -2073,14 +2104,20 @@ def init(
ai_skills = True
console.print(f"\n[yellow]Note:[/yellow] {AGENT_SKILLS_MIGRATIONS[selected_ai]['interactive_note']}")

# Validate --ai-commands-dir usage
if selected_ai == "generic":
# Validate --ai-commands-dir usage.
# Skip validation when --integration-options is provided — the integration
# will validate its own options in setup().
if selected_ai == "generic" and not integration_options:
if not ai_commands_dir:
console.print("[red]Error:[/red] --ai-commands-dir is required when using --ai generic")
console.print("[dim]Example: specify init my-project --ai generic --ai-commands-dir .myagent/commands/[/dim]")
console.print("[red]Error:[/red] --ai-commands-dir is required when using --ai generic or --integration generic")
console.print("[dim]Example: specify init my-project --integration generic --integration-options=\"--commands-dir .myagent/commands/\"[/dim]")
raise typer.Exit(1)
elif ai_commands_dir:
console.print(f"[red]Error:[/red] --ai-commands-dir can only be used with --ai generic (not '{selected_ai}')")
elif ai_commands_dir and not use_integration:
console.print(
f"[red]Error:[/red] --ai-commands-dir can only be used with the "
f"'generic' integration via --ai generic or --integration generic "
f"(not '{selected_ai}')"
)
raise typer.Exit(1)

current_dir = Path.cwd()
Expand Down Expand Up @@ -2210,9 +2247,21 @@ def init(
manifest = IntegrationManifest(
resolved_integration.key, project_path, version=get_speckit_version()
)

# Forward all legacy CLI flags to the integration as parsed_options.
# Integrations receive every option and decide what to use;
# irrelevant keys are simply ignored by the integration's setup().
integration_parsed_options: dict[str, Any] = {}
if ai_commands_dir:
integration_parsed_options["commands_dir"] = ai_commands_dir
if ai_skills:
integration_parsed_options["skills"] = True

resolved_integration.setup(
project_path, manifest,
parsed_options=integration_parsed_options or None,
script_type=selected_script,
raw_options=integration_options,
)
manifest.save()

Expand Down Expand Up @@ -2268,7 +2317,7 @@ def init(
shutil.rmtree(project_path)
raise typer.Exit(1)
# For generic agent, rename placeholder directory to user-specified path
if selected_ai == "generic" and ai_commands_dir:
if not use_integration and selected_ai == "generic" and ai_commands_dir:
placeholder_dir = project_path / ".speckit" / "commands"
target_dir = project_path / ai_commands_dir
if placeholder_dir.is_dir():
Expand All @@ -2284,18 +2333,19 @@ def init(
ensure_constitution_from_template(project_path, tracker=tracker)

# Determine skills directory and migrate any legacy Kimi dotted skills.
# (Legacy path only — integration path handles skills in setup().)
migrated_legacy_kimi_skills = 0
removed_legacy_kimi_skills = 0
skills_dir: Optional[Path] = None
if selected_ai in NATIVE_SKILLS_AGENTS:
if not use_integration and selected_ai in NATIVE_SKILLS_AGENTS:
skills_dir = _get_skills_dir(project_path, selected_ai)
if selected_ai == "kimi" and skills_dir.is_dir():
(
migrated_legacy_kimi_skills,
removed_legacy_kimi_skills,
) = _migrate_legacy_kimi_dotted_skills(skills_dir)

if ai_skills:
if not use_integration and ai_skills:
if selected_ai in NATIVE_SKILLS_AGENTS:
bundled_found = _has_bundled_skills(project_path, selected_ai)
if bundled_found:
Expand Down Expand Up @@ -2383,6 +2433,11 @@ def init(
}
if use_integration:
init_opts["integration"] = resolved_integration.key
# Ensure ai_skills is set for SkillsIntegration so downstream
# tools (extensions, presets) emit SKILL.md overrides correctly.
from .integrations.base import SkillsIntegration as _SkillsPersist
if isinstance(resolved_integration, _SkillsPersist):
init_opts["ai_skills"] = True
save_init_options(project_path, init_opts)

# Install preset if specified
Expand Down Expand Up @@ -2484,17 +2539,27 @@ def init(
steps_lines.append("1. You're already in the project directory!")
step_num = 2

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
# Determine skill display mode for the next-steps panel.
# Skills integrations (codex, kimi, agy) should show skill invocation syntax
# regardless of whether --ai-skills was explicitly passed.
_is_skills_integration = False
if use_integration:
from .integrations.base import SkillsIntegration as _SkillsInt
_is_skills_integration = isinstance(resolved_integration, _SkillsInt)

codex_skill_mode = selected_ai == "codex" and ai_skills
codex_skill_mode = selected_ai == "codex" and (ai_skills or _is_skills_integration)
kimi_skill_mode = selected_ai == "kimi"
native_skill_mode = codex_skill_mode or kimi_skill_mode
agy_skill_mode = selected_ai == "agy" and _is_skills_integration
native_skill_mode = codex_skill_mode or kimi_skill_mode or agy_skill_mode

if codex_skill_mode and not ai_skills:
# Integration path installed skills; show the helpful notice
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
usage_label = "skills" if native_skill_mode else "slash commands"

def _display_cmd(name: str) -> str:
if codex_skill_mode:
if codex_skill_mode or agy_skill_mode:
return f"$speckit-{name}"
if kimi_skill_mode:
return f"/skill:speckit-{name}"
Expand Down
6 changes: 6 additions & 0 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ class CommandRegistrar:
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"agy": {
"dir": ".agent/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/specify_cli/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,21 @@ def _register_builtins() -> None:
users install and invoke.
"""
# -- Imports (alphabetical) -------------------------------------------
from .agy import AgyIntegration
from .amp import AmpIntegration
from .auggie import AuggieIntegration
from .bob import BobIntegration
from .claude import ClaudeIntegration
from .codex import CodexIntegration
from .codebuddy import CodebuddyIntegration
from .copilot import CopilotIntegration
from .cursor_agent import CursorAgentIntegration
from .gemini import GeminiIntegration
from .generic import GenericIntegration
from .iflow import IflowIntegration
from .junie import JunieIntegration
from .kilocode import KilocodeIntegration
from .kimi import KimiIntegration
from .kiro_cli import KiroCliIntegration
from .opencode import OpencodeIntegration
from .pi import PiIntegration
Expand All @@ -70,17 +74,21 @@ def _register_builtins() -> None:
from .windsurf import WindsurfIntegration

# -- Registration (alphabetical) --------------------------------------
_register(AgyIntegration())
_register(AmpIntegration())
_register(AuggieIntegration())
_register(BobIntegration())
_register(ClaudeIntegration())
_register(CodexIntegration())
_register(CodebuddyIntegration())
_register(CopilotIntegration())
_register(CursorAgentIntegration())
_register(GeminiIntegration())
_register(GenericIntegration())
_register(IflowIntegration())
_register(JunieIntegration())
_register(KilocodeIntegration())
_register(KimiIntegration())
_register(KiroCliIntegration())
_register(OpencodeIntegration())
_register(PiIntegration())
Expand Down
41 changes: 41 additions & 0 deletions src/specify_cli/integrations/agy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Antigravity (agy) integration — skills-based agent.

Antigravity uses ``.agent/skills/speckit-<name>/SKILL.md`` layout.
Explicit command support was deprecated in version 1.20.5;
``--skills`` defaults to ``True``.
"""

from __future__ import annotations

from ..base import IntegrationOption, SkillsIntegration


class AgyIntegration(SkillsIntegration):
"""Integration for Antigravity IDE."""

key = "agy"
config = {
"name": "Antigravity",
"folder": ".agent/",
"commands_subdir": "skills",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".agent/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = "AGENTS.md"

@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills (default for Antigravity since v1.20.5)",
),
]
17 changes: 17 additions & 0 deletions src/specify_cli/integrations/agy/scripts/update-context.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# update-context.ps1 — Antigravity (agy) integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.

$ErrorActionPreference = 'Stop'

$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}

& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType agy
24 changes: 24 additions & 0 deletions src/specify_cli/integrations/agy/scripts/update-context.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# update-context.sh — Antigravity (agy) integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.

set -euo pipefail

_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi

exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" agy
Loading
Loading