Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| ------------------------------------------------------------------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [Alquimia AI](https://docs.alquimia.ai) | `alquimia` | Skills-based integration; installs skills into `.alquimia/skills` and invokes them as `/speckit-<command>` |
| [Amp](https://ampcode.com/) | `amp` | |
| [Antigravity (agy)](https://antigravity.google/) | `agy` | Skills-based integration; skills are installed automatically |
| [Antigravity (agy)](https://antigravity.google/) | `agy` | Skills-based integration; installs skills into `.agents/skills/` and invokes them as `/speckit-<command>`. In headless non-interactive runs, automatic tool approval via `--dangerously-skip-permissions` can be enabled by setting `SPECKIT_AGY_ALLOW_ALL_TOOLS=1` or `SPECKIT_INTEGRATION_AGY_ALLOW_ALL_TOOLS=1`. |
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | `auggie` | |
| [Claude Code](https://www.anthropic.com/claude-code) | `claude` | Skills-based integration; installs skills in `.claude/skills` |
| [Cline](https://github.com/cline/cline) | `cline` | IDE-based agent |
Expand Down
4 changes: 2 additions & 2 deletions integrations/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,10 @@
"id": "agy",
"name": "Antigravity",
"version": "1.0.0",
"description": "Antigravity IDE skills-based integration",
"description": "Antigravity CLI and IDE skills-based integration",
"author": "spec-kit-core",
"repository": "https://github.com/github/spec-kit",
"tags": ["ide", "skills"]
"tags": ["cli", "ide", "skills"]
},
"generic": {
"id": "generic",
Expand Down
129 changes: 44 additions & 85 deletions src/specify_cli/integrations/agy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,87 +1,61 @@
"""Antigravity (agy) integration — skills-based agent.

Antigravity uses ``.agents/skills/speckit-<name>/SKILL.md`` layout (enforced since v1.20.5).
Antigravity uses ``.agents/skills/speckit-<name>/SKILL.md`` layout
(supported in Antigravity CLI v1.0.0+ and Antigravity IDE v2.0.0+).
"""

from __future__ import annotations

import re
import os
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, ClassVar

from ..base import SkillsIntegration

if TYPE_CHECKING:
from ..manifest import IntegrationManifest

# Note injected into hook sections so agy maps dot-notation command
# names (from extensions.yml) to the hyphenated skill names it uses.
# Without this, agy emits ``/speckit.git.commit`` (which does not
# resolve) instead of ``/speckit-git-commit``.
_HOOK_COMMAND_NOTE = (
"- When constructing slash commands from hook command names, "
"replace dots (`.`) with hyphens (`-`). "
"For example, `speckit.git.commit` → `/speckit-git-commit`.\n"
)

def _allow_all_tools() -> bool:
"""Return True if agy should run with auto-approved permissions in headless mode.

Disabled by default for security. Set SPECKIT_AGY_ALLOW_ALL_TOOLS=1 (or
SPECKIT_INTEGRATION_AGY_ALLOW_ALL_TOOLS=1) to enable.
"""
for key in (
"SPECKIT_INTEGRATION_AGY_ALLOW_ALL_TOOLS",
"SPECKIT_AGY_ALLOW_ALL_TOOLS",
):
val = os.environ.get(key)
if val is not None and val.strip():
return val.strip().lower() in ("1", "true", "yes", "on")
return False


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

key = "agy"
config = {
Inherits hook command normalization and post-processing from
SkillsIntegration, ensuring slash commands constructed from dotted hook
names are automatically converted to hyphenated skill invocations.
"""

key: ClassVar[str] = "agy"
config: ClassVar[dict[str, Any]] = {
"name": "Antigravity",
"folder": ".agents/",
"commands_subdir": "skills",
"install_url": "https://antigravity.google/",
"requires_cli": True,
}
registrar_config = {
registrar_config: ClassVar[dict[str, Any]] = {
"dir": ".agents/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}

@staticmethod
def _inject_hook_command_note(content: str) -> str:
"""Insert a dot-to-hyphen note before each hook output instruction.

Targets the line ``- For each executable hook, output the following``
and inserts the note on the line before it, matching its indentation.
Skips if the note is already present.
"""
if "replace dots" in content:
return content

def repl(m: re.Match[str]) -> str:
indent = m.group(1)
instruction = m.group(2)
# ``eol`` is empty when the regex matched via ``$`` because the
# instruction was the final line of a file with no trailing
# newline. Default to ``\n`` so the note never collapses onto
# the same line as the instruction.
eol = m.group(3) or "\n"
return (
indent
+ _HOOK_COMMAND_NOTE.rstrip("\n")
+ eol
+ indent
+ instruction
+ eol
)

return re.sub(
r"(?m)^(\s*)(- For each executable hook, output the following[^\r\n]*)(\r\n|\n|$)",
repl,
content,
)

def post_process_skill_content(self, content: str) -> str:
"""Inject the dot-to-hyphen hook command note."""
return self._inject_hook_command_note(content)

def build_exec_args(
self,
prompt: str,
Expand All @@ -93,21 +67,20 @@ def build_exec_args(
project_root: Path | None = None,
) -> list[str] | None:
self.validate_runtime_config(integration_args, integration_options)
# agy does not support JSON output; output_json is ignored.
args = [self._resolve_executable()]
# Pass --model before --print so agy can parse it as a flag.
# agy >=1.20 supports: agy --model <name> --print <prompt>
if _allow_all_tools():
args.append("--dangerously-skip-permissions")
if model:
args.extend(["--model", model])
# Inject --add-dir so agy discovers the project workspace when invoked
# from an arbitrary working directory (e.g. the workflow engine's cwd).
# Without this agy falls back to its own scratch directory and cannot
# locate .agents/skills/, reporting "no active workspace".
if project_root is not None:
args.extend(["--add-dir", str(project_root.resolve())])
if output_json:
args.extend(["--output-format", "json"])
if project_root is not None and str(project_root).strip():
# agy requires an active workspace directory to discover skills under
# .agents/skills/ when invoked from workflow directories (see issue #4480, PR #4481).
args.extend(["--add-dir", str(Path(project_root).resolve())])
# Honor SPECKIT_INTEGRATION_AGY_EXTRA_ARGS (operator-supplied flags).
# These MUST be inserted before --print because agy treats every token
# that follows --print as part of the prompt, not as CLI flags.
# Positioned before --print because agy consumes all trailing arguments
# as prompt text (see #4480).
self._apply_extra_args_env_var(args)
args.extend(["--print", prompt])
return args
Expand All @@ -122,26 +95,12 @@ def setup(
import click

click.secho(
"Warning: The .agents/ layout requires Antigravity v1.20.5 or newer. "
"Please ensure your agy installation is up to date.",
"Warning: The .agents/ layout requires Antigravity CLI v1.0.0 or newer "
"(or Antigravity IDE v2.0.0 or newer). "
"Please ensure your installation is up to date.",
fg="yellow",
err=True,
)
created = super().setup(project_root, manifest, parsed_options=parsed_options, **opts)

skills_dir = self.skills_dest(project_root).resolve()
for path in created:
try:
path.resolve().relative_to(skills_dir)
except ValueError:
continue
if path.name != "SKILL.md":
continue

content = path.read_bytes().decode("utf-8")
updated = self.post_process_skill_content(content)
if updated != content:
path.write_bytes(updated.encode("utf-8"))
self.record_file_in_manifest(path, project_root, manifest)

return created
return super().setup(
project_root, manifest, parsed_options=parsed_options, **opts
)
Loading