diff --git a/.github/workflows/scripts/create-release-packages.ps1 b/.github/workflows/scripts/create-release-packages.ps1 index 8c7d4078..fc307084 100644 --- a/.github/workflows/scripts/create-release-packages.ps1 +++ b/.github/workflows/scripts/create-release-packages.ps1 @@ -442,7 +442,7 @@ function Build-Variant { if (Test-Path $tabnineTemplate) { Copy-Item $tabnineTemplate (Join-Path $baseDir 'TABNINE.md') } } 'agy' { - $cmdDir = Join-Path $baseDir ".agent/workflows" + $cmdDir = Join-Path $baseDir ".agent/commands" Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script } 'vibe' { diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 8be5a054..ada3a289 100755 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -280,8 +280,8 @@ build_variant() { mkdir -p "$base_dir/.kiro/prompts" generate_commands kiro-cli md "\$ARGUMENTS" "$base_dir/.kiro/prompts" "$script" ;; agy) - mkdir -p "$base_dir/.agent/workflows" - generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/workflows" "$script" ;; + mkdir -p "$base_dir/.agent/commands" + generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/commands" "$script" ;; bob) mkdir -p "$base_dir/.bob/commands" generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;; diff --git a/AGENTS.md b/AGENTS.md index aa373022..561bf257 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,7 +88,7 @@ This eliminates the need for special-case mappings throughout the codebase. - `folder`: Directory where agent-specific files are stored (relative to project root) - `commands_subdir`: Subdirectory name within the agent folder where command/prompt files are stored (default: `"commands"`) - Most agents use `"commands"` (e.g., `.claude/commands/`) - - Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode, agy), `"prompts"` (codex, kiro-cli), `"command"` (opencode - singular) + - Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode), `"prompts"` (codex, kiro-cli), `"command"` (opencode - singular) - This field enables `--ai-skills` to locate command templates correctly for skill generation - `install_url`: Installation documentation URL (set to `None` for IDE-based agents) - `requires_cli`: Whether the agent requires a CLI tool check during initialization diff --git a/README.md b/README.md index 7bda2b9c..006d5522 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ See Spec-Driven Development in action across different scenarios with these comm | [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ | | | [Kimi Code](https://code.kimi.com/) | ✅ | | | [Windsurf](https://windsurf.com/) | ✅ | | -| [Antigravity (agy)](https://antigravity.google/) | ✅ | | +| [Antigravity (agy)](https://antigravity.google/) | ✅ | Requires `--ai-skills` | | Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir ` for unsupported agents | ## 🔧 Specify CLI Reference @@ -248,7 +248,7 @@ specify init my-project --ai vibe specify init my-project --ai bob # Initialize with Antigravity support -specify init my-project --ai agy +specify init my-project --ai agy --ai-skills # Initialize with an unsupported agent (generic / bring your own agent) specify init my-project --ai generic --ai-commands-dir .myagent/commands/ diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 55e97ea9..e7781b2b 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -247,7 +247,7 @@ AGENT_CONFIG = { "agy": { "name": "Antigravity", "folder": ".agent/", - "commands_subdir": "workflows", # Special: uses workflows/ not commands/ + "commands_subdir": "commands", "install_url": None, # IDE-based "requires_cli": False, }, @@ -1270,6 +1270,22 @@ 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. + + 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.") + console.print("Please use [cyan]--ai-skills[/cyan] when initializing to install templates as agent skills instead.") + console.print("[yellow]Usage:[/yellow] specify init --ai agy --ai-skills") + raise typer.Exit(1) + @app.command() def init( project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"), @@ -1379,6 +1395,49 @@ def init( console.print(error_panel) raise typer.Exit(1) + if ai_assistant: + if ai_assistant not in AGENT_CONFIG: + console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AGENT_CONFIG.keys())}") + raise typer.Exit(1) + selected_ai = ai_assistant + else: + # Create options dict for selection (agent_key: display_name) + ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()} + selected_ai = select_with_arrows( + ai_choices, + "Choose your AI assistant:", + "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 /skills//). + # 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 + # ai_skills so the agent remains usable without requiring an extra flag. + # Preserve deprecation behavior only for explicit '--ai agy' without skills. + if ai_assistant: + _handle_agy_deprecation(console) + 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)." + ) + + # Validate --ai-commands-dir usage + if selected_ai == "generic": + 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]") + 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}')") + raise typer.Exit(1) + current_dir = Path.cwd() setup_lines = [ @@ -1399,30 +1458,6 @@ def init( if not should_init_git: console.print("[yellow]Git not found - will skip repository initialization[/yellow]") - if ai_assistant: - if ai_assistant not in AGENT_CONFIG: - console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AGENT_CONFIG.keys())}") - raise typer.Exit(1) - selected_ai = ai_assistant - else: - # Create options dict for selection (agent_key: display_name) - ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()} - selected_ai = select_with_arrows( - ai_choices, - "Choose your AI assistant:", - "copilot" - ) - - # Validate --ai-commands-dir usage - if selected_ai == "generic": - 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]") - 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}')") - raise typer.Exit(1) - if not ignore_agent_tools: agent_config = AGENT_CONFIG.get(selected_ai) if agent_config and agent_config["requires_cli"]: diff --git a/tests/test_agent_config_consistency.py b/tests/test_agent_config_consistency.py index d77c6f10..6831fad3 100644 --- a/tests/test_agent_config_consistency.py +++ b/tests/test_agent_config_consistency.py @@ -62,7 +62,14 @@ class TestAgentConfigConsistency: ps_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.ps1").read_text(encoding="utf-8") assert re.search(r"'shai'\s*\{.*?\.shai/commands", ps_text, re.S) is not None - assert re.search(r"'agy'\s*\{.*?\.agent/workflows", ps_text, re.S) is not None + assert re.search(r"'agy'\s*\{.*?\.agent/commands", ps_text, re.S) is not None + + def test_release_sh_switch_has_shai_and_agy_generation(self): + """Bash release builder must generate files for shai and agy agents.""" + sh_text = (REPO_ROOT / ".github" / "workflows" / "scripts" / "create-release-packages.sh").read_text(encoding="utf-8") + + assert re.search(r"shai\)\s*\n.*?\.shai/commands", sh_text, re.S) is not None + assert re.search(r"agy\)\s*\n.*?\.agent/commands", sh_text, re.S) is not None def test_init_ai_help_includes_roo_and_kiro_alias(self): """CLI help text for --ai should stay in sync with agent config and alias guidance.""" diff --git a/tests/test_ai_skills.py b/tests/test_ai_skills.py index e9bd71d0..3c50cd50 100644 --- a/tests/test_ai_skills.py +++ b/tests/test_ai_skills.py @@ -661,6 +661,59 @@ class TestCliValidation: assert "Usage:" in result.output assert "--ai" in result.output + def test_agy_without_ai_skills_fails(self): + """--ai agy without --ai-skills should fail with exit code 1.""" + from typer.testing import CliRunner + + runner = CliRunner() + result = runner.invoke(app, ["init", "test-proj", "--ai", "agy"]) + + assert result.exit_code == 1 + assert "Explicit command support was deprecated in Antigravity version 1.20.5." in result.output + assert "--ai-skills" in result.output + + def test_interactive_agy_without_ai_skills_prompts_skills(self, monkeypatch): + """Interactive selector returning agy without --ai-skills should automatically enable --ai-skills.""" + from typer.testing import CliRunner + + # Mock select_with_arrows to simulate the user picking 'agy' for AI, + # and return a deterministic default for any other prompts to avoid + # calling the real interactive implementation. + def _fake_select_with_arrows(*args, **kwargs): + options = kwargs.get("options") + if options is None and len(args) >= 1: + options = args[0] + + # If the options include 'agy', simulate selecting it. + if isinstance(options, dict) and "agy" in options: + return "agy" + if isinstance(options, (list, tuple)) and "agy" in options: + return "agy" + + # For any other prompt, return a deterministic, non-interactive default: + # pick the first option if available. + if isinstance(options, dict) and options: + return next(iter(options.keys())) + if isinstance(options, (list, tuple)) and options: + return options[0] + + # If no options are provided, fall back to None (should not occur in normal use). + return None + + monkeypatch.setattr("specify_cli.select_with_arrows", _fake_select_with_arrows) + + # Mock download_and_extract_template to prevent real HTTP downloads during testing + monkeypatch.setattr("specify_cli.download_and_extract_template", lambda *args, **kwargs: None) + # We need to bypass the `git init` step, wait, it has `--no-git` by default in tests maybe? + runner = CliRunner() + # Create temp dir to avoid directory already exists errors or whatever + with runner.isolated_filesystem(): + result = runner.invoke(app, ["init", "test-proj", "--no-git"]) + + # Interactive selection should NOT raise the deprecation error! + assert result.exit_code == 0 + assert "Explicit command support was deprecated" not in result.output + def test_ai_skills_flag_appears_in_help(self): """--ai-skills should appear in init --help output.""" from typer.testing import CliRunner