mirror of
https://github.com/github/spec-kit.git
synced 2026-07-04 04:45:43 +08:00
* Initial plan * feat: support SPECKIT_INTEGRATION_<KEY>_EXECUTABLE env var override Adds `IntegrationBase._resolve_executable()` which reads `SPECKIT_INTEGRATION_<KEY>_EXECUTABLE` (hyphens→underscores, uppercased) and falls back to `self.key` when unset or whitespace-only. All concrete `build_exec_args()` implementations now call `self._resolve_executable()` instead of hard-coding `self.key` or `"agy"` as the first argv token: - MarkdownIntegration, TomlIntegration, SkillsIntegration (base classes) - CodexIntegration, DevinIntegration, OpencodeIntegration, HermesIntegration, AgyIntegration - CopilotIntegration (overrides `_resolve_executable()` to fall back to the platform-specific `_copilot_executable()` default; also updates `dispatch_command()` to use `_resolve_executable()`) Tests added to tests/integrations/test_extra_args.py covering: - default (unset) falls back to key - env var replaces first argv token - whitespace-only env var is a no-op - key hyphen→underscore normalisation - cross-integration scoping (wrong key ignored) - all override integrations (Codex, Devin, Opencode, Copilot) - Copilot dispatch_command path - EXECUTABLE and EXTRA_ARGS can be set simultaneously See issue #2596." * fix: complete docstring sentence in _resolve_executable * test: generalize extra-args test comments for override coverage * Fix stale Codex executable comment --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
"""Antigravity (agy) integration — skills-based agent.
|
|
|
|
Antigravity uses ``.agents/skills/speckit-<name>/SKILL.md`` layout (enforced since v1.20.5).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
class AgyIntegration(SkillsIntegration):
|
|
"""Integration for Antigravity IDE."""
|
|
|
|
key = "agy"
|
|
config = {
|
|
"name": "Antigravity",
|
|
"folder": ".agents/",
|
|
"commands_subdir": "skills",
|
|
"install_url": "https://antigravity.google/",
|
|
"requires_cli": True,
|
|
}
|
|
registrar_config = {
|
|
"dir": ".agents/skills",
|
|
"format": "markdown",
|
|
"args": "$ARGUMENTS",
|
|
"extension": "/SKILL.md",
|
|
}
|
|
context_file = "AGENTS.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,
|
|
*,
|
|
model: str | None = None,
|
|
output_json: bool = True,
|
|
) -> list[str] | None:
|
|
# agy does not support --model or JSON output; both params are ignored
|
|
return [self._resolve_executable(), "--print", prompt]
|
|
|
|
def setup(
|
|
self,
|
|
project_root: Path,
|
|
manifest: IntegrationManifest,
|
|
parsed_options: dict[str, Any] | None = None,
|
|
**opts: Any,
|
|
) -> list[Path]:
|
|
import click
|
|
|
|
click.secho(
|
|
"Warning: The .agents/ layout requires Antigravity v1.20.5 or newer. "
|
|
"Please ensure your agy 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
|