From 48e8a056476e76164d9354e53794303ed8646a52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:20:10 +0000 Subject: [PATCH 1/9] Initial plan From 93a4ea374aeb618ec1f08c7c5fbdd95f4e50df0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:38:26 +0000 Subject: [PATCH 2/9] feat!: remove legacy --ai, --ai-commands-dir, and --ai-skills flags at 0.10.0 --- CHANGELOG.md | 16 +-- src/specify_cli/__init__.py | 2 - src/specify_cli/commands/init.py | 133 +++--------------- tests/integrations/test_cli.py | 96 ++++--------- tests/integrations/test_integration_agy.py | 14 +- .../test_integration_base_markdown.py | 10 +- .../test_integration_base_skills.py | 10 +- .../test_integration_base_toml.py | 10 +- .../test_integration_base_yaml.py | 10 +- tests/integrations/test_integration_claude.py | 8 +- tests/integrations/test_integration_codex.py | 12 +- .../test_integration_cursor_agent.py | 14 +- tests/integrations/test_integration_devin.py | 12 +- .../integrations/test_integration_generic.py | 15 +- tests/integrations/test_integration_hermes.py | 13 +- tests/integrations/test_integration_kimi.py | 2 +- .../integrations/test_integration_kiro_cli.py | 12 +- .../integrations/test_integration_rovodev.py | 8 +- tests/test_agent_config_consistency.py | 37 +++-- tests/test_branch_numbering.py | 8 +- tests/test_commands_package.py | 4 - 21 files changed, 147 insertions(+), 299 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5731432cb4..7456c3c3ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,13 @@ -## [0.9.5] - 2026-06-05 +## [0.10.0] - Unreleased -### Changed +### Breaking Changes -- feat(extensions): add bundled bug triage workflow extension (#2871) -- fix: resolve GitHub release asset API URL for private repo preset and workflow downloads (#2855) -- chore(deps): bump github/gh-aw-actions from 0.77.0 to 0.78.1 (#2860) -- chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#2859) -- chore(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 (#2858) -- chore(deps): bump github/codeql-action from 4.36.0 to 4.36.2 (#2857) -- fix(workflows): render gate show_file contents in the interactive prompt (#2810) -- feat: add support for rovodev (#2539) -- chore: release 0.9.4, begin 0.9.5.dev0 development (#2853) +- **Removed legacy `--ai`, `--ai-commands-dir`, and `--ai-skills` flags from `specify init`.** + Use `--integration `, `--integration-options="--commands-dir "`, and + `--integration-options="--skills"` respectively. ## [0.9.4] - 2026-06-04 diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 99b312621f..23d31cb0cf 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -82,8 +82,6 @@ ) from ._agent_config import ( AGENT_CONFIG as AGENT_CONFIG, - AI_ASSISTANT_ALIASES as AI_ASSISTANT_ALIASES, - AI_ASSISTANT_HELP as AI_ASSISTANT_HELP, DEFAULT_INIT_INTEGRATION as DEFAULT_INIT_INTEGRATION, SCRIPT_TYPE_CHOICES as SCRIPT_TYPE_CHOICES, ) diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index 83578feaee..f13e4cfc7e 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -2,7 +2,6 @@ from __future__ import annotations import os -import shlex import shutil import sys from pathlib import Path @@ -14,8 +13,6 @@ from .._agent_config import ( AGENT_CONFIG, - AI_ASSISTANT_ALIASES, - AI_ASSISTANT_HELP, DEFAULT_INIT_INTEGRATION, SCRIPT_TYPE_CHOICES, ) @@ -28,31 +25,6 @@ from .._console import StepTracker, console, select_with_arrows, show_banner from .._utils import check_tool, init_git_repo, is_git_repo -def _build_integration_equivalent( - integration_key: str, - ai_commands_dir: str | None = None, -) -> str: - parts = [f"--integration {integration_key}"] - if integration_key == "generic" and ai_commands_dir: - parts.append( - f'--integration-options="--commands-dir {shlex.quote(ai_commands_dir)}"' - ) - return " ".join(parts) - - -def _build_ai_deprecation_warning( - integration_key: str, - ai_commands_dir: str | None = None, -) -> str: - replacement = _build_integration_equivalent( - integration_key, - ai_commands_dir=ai_commands_dir, - ) - return ( - "[bold]--ai[/bold] is deprecated and will no longer be available in version 0.10.0 or later.\n\n" - f"Use [bold]{replacement}[/bold] instead." - ) - def _stdin_is_interactive() -> bool: return sys.stdin.isatty() @@ -97,8 +69,6 @@ def register(app: typer.Typer) -> None: @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)"), - ai_assistant: str = typer.Option(None, "--ai", help=AI_ASSISTANT_HELP), - ai_commands_dir: str = typer.Option(None, "--ai-commands-dir", help="Directory for agent command files (required with --ai generic, e.g. .myagent/commands/)"), script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"), ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for coding agent tools like Claude Code"), no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), @@ -107,11 +77,10 @@ def init( skip_tls: bool = typer.Option(False, "--skip-tls", help="Deprecated (no-op). Previously: skip SSL/TLS verification.", hidden=True), debug: bool = typer.Option(False, "--debug", help="Deprecated. Previously: show verbose diagnostic output; currently only prints additional diagnostic details on failure.", hidden=True), github_token: str = typer.Option(None, "--github-token", help="Deprecated (no-op). Previously: GitHub token for API requests.", hidden=True), - ai_skills: bool = typer.Option(False, "--ai-skills", help="Install Prompt.MD templates as agent skills (requires --ai)"), offline: bool = typer.Option(False, "--offline", help="Deprecated (no-op). All scaffolding now uses bundled assets.", hidden=True), 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, …, 1000, … — expands past 999 automatically) 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: str = typer.Option(None, "--integration", help="AI coding agent integration to use (e.g. --integration copilot). See 'specify check' for available integrations."), integration_options: str = typer.Option(None, "--integration-options", help='Options for the integration (e.g. --integration-options="--commands-dir .myagent/cmds")'), ): """ @@ -163,27 +132,6 @@ def init( from ..integration_runtime import with_integration_setting as _with_integration_setting show_banner() - ai_deprecation_warning: str | None = None - - if ai_assistant and ai_assistant.startswith("--"): - console.print(f"[red]Error:[/red] Invalid value for --ai: '{ai_assistant}'") - console.print("[yellow]Hint:[/yellow] Did you forget to provide a value for --ai?") - console.print("[yellow]Example:[/yellow] specify init --integration claude --here") - console.print(f"[yellow]Available agents:[/yellow] {', '.join(AGENT_CONFIG.keys())}") - raise typer.Exit(1) - - if ai_commands_dir and ai_commands_dir.startswith("--"): - console.print(f"[red]Error:[/red] Invalid value for --ai-commands-dir: '{ai_commands_dir}'") - console.print("[yellow]Hint:[/yellow] Did you forget to provide a value for --ai-commands-dir?") - console.print("[yellow]Example:[/yellow] specify init --integration generic --integration-options=\"--commands-dir .myagent/commands/\"") - raise typer.Exit(1) - - if ai_assistant: - ai_assistant = AI_ASSISTANT_ALIASES.get(ai_assistant, ai_assistant) - - if integration and ai_assistant: - console.print("[red]Error:[/red] --integration and --ai are mutually exclusive") - raise typer.Exit(1) from ..integrations import INTEGRATION_REGISTRY, get_integration if integration: @@ -193,35 +141,6 @@ def init( available = ", ".join(sorted(INTEGRATION_REGISTRY)) console.print(f"[yellow]Available integrations:[/yellow] {available}") raise typer.Exit(1) - ai_assistant = integration - elif ai_assistant: - resolved_integration = get_integration(ai_assistant) - if not resolved_integration: - console.print(f"[red]Error:[/red] Unknown agent '{ai_assistant}'. Choose from: {', '.join(sorted(INTEGRATION_REGISTRY))}") - raise typer.Exit(1) - ai_deprecation_warning = _build_ai_deprecation_warning( - resolved_integration.key, - ai_commands_dir=ai_commands_dir, - ) - - if ai_assistant or 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; " - "skills are the default for this integration.[/dim]" - ) - else: - console.print( - "[dim]Note: --ai-skills has no effect with " - 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 "[/bold] instead.[/dim]' - ) if no_git: console.print( @@ -242,11 +161,6 @@ def init( console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag") raise typer.Exit(1) - if ai_skills and not ai_assistant: - console.print("[red]Error:[/red] --ai-skills requires --ai to be specified") - console.print("[yellow]Usage:[/yellow] specify init --ai --ai-skills") - raise typer.Exit(1) - BRANCH_NUMBERING_CHOICES = {"sequential", "timestamp"} if branch_numbering and branch_numbering not in BRANCH_NUMBERING_CHOICES: console.print(f"[red]Error:[/red] Invalid --branch-numbering value '{branch_numbering}'. Choose from: {', '.join(sorted(BRANCH_NUMBERING_CHOICES))}") @@ -295,11 +209,11 @@ 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())}") + if integration: + if integration not in AGENT_CONFIG: + console.print(f"[red]Error:[/red] Invalid integration '{integration}'. Choose from: {', '.join(AGENT_CONFIG.keys())}") raise typer.Exit(1) - selected_ai = ai_assistant + selected_ai = integration elif not _stdin_is_interactive(): console.print( f"[dim]Non-interactive session detected: defaulting to '{DEFAULT_INIT_INTEGRATION}'. " @@ -314,17 +228,16 @@ def init( DEFAULT_INIT_INTEGRATION, ) - if not ai_assistant: + if not integration: resolved_integration = get_integration(selected_ai) if not resolved_integration: console.print(f"[red]Error:[/red] Unknown agent '{selected_ai}'") raise typer.Exit(1) 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 or --integration generic") - console.print('[dim]Example: specify init my-project --integration generic --integration-options="--commands-dir .myagent/commands/"[/dim]') - raise typer.Exit(1) + console.print("[red]Error:[/red] --integration generic requires --integration-options with --commands-dir") + console.print('[dim]Example: specify init my-project --integration generic --integration-options="--commands-dir .myagent/commands/"[/dim]') + raise typer.Exit(1) current_dir = Path.cwd() @@ -414,10 +327,6 @@ def init( ) 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 if integration_options: extra = _parse_integration_options(resolved_integration, integration_options) if extra: @@ -675,7 +584,7 @@ def init( agent_config = AGENT_CONFIG.get(selected_ai) if agent_config: - agent_folder = ai_commands_dir if selected_ai == "generic" else agent_config["folder"] + agent_folder = agent_config["folder"] if agent_folder: security_notice = Panel( f"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.\n" @@ -687,16 +596,6 @@ def init( console.print() console.print(security_notice) - if ai_deprecation_warning: - deprecation_notice = Panel( - ai_deprecation_warning, - title="[bold red]Deprecation Warning[/bold red]", - border_style="red", - padding=(1, 2), - ) - console.print() - console.print(deprecation_notice) - if git_default_notice: default_change_notice = Panel( "The git extension is currently enabled by default during [bold]specify init[/bold].\n" @@ -720,24 +619,24 @@ def init( from ..integrations.base import SkillsIntegration as _SkillsInt _is_skills_integration = isinstance(resolved_integration, _SkillsInt) or getattr(resolved_integration, "_skills_mode", False) - codex_skill_mode = selected_ai == "codex" and (ai_skills or _is_skills_integration) - claude_skill_mode = selected_ai == "claude" and (ai_skills or _is_skills_integration) + codex_skill_mode = selected_ai == "codex" and _is_skills_integration + claude_skill_mode = selected_ai == "claude" and _is_skills_integration kimi_skill_mode = selected_ai == "kimi" agy_skill_mode = selected_ai == "agy" and _is_skills_integration trae_skill_mode = selected_ai == "trae" - cursor_agent_skill_mode = selected_ai == "cursor-agent" and (ai_skills or _is_skills_integration) + cursor_agent_skill_mode = selected_ai == "cursor-agent" and _is_skills_integration copilot_skill_mode = selected_ai == "copilot" and _is_skills_integration devin_skill_mode = selected_ai == "devin" cline_skill_mode = selected_ai == "cline" native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode - if codex_skill_mode and not ai_skills: + if codex_skill_mode: 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 - if claude_skill_mode and not ai_skills: + if claude_skill_mode: steps_lines.append(f"{step_num}. Start Claude in this project directory; spec-kit skills were installed to [cyan].claude/skills[/cyan]") step_num += 1 - if cursor_agent_skill_mode and not ai_skills: + if cursor_agent_skill_mode: steps_lines.append(f"{step_num}. Start Cursor Agent in this project directory; spec-kit skills were installed to [cyan].cursor/skills[/cyan]") step_num += 1 if devin_skill_mode: diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index 6198f294a4..a13a8095dd 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -43,16 +43,6 @@ def test_cli_phase_label_includes_target(self): class TestInitIntegrationFlag: - def test_integration_and_ai_mutually_exclusive(self, tmp_path): - from typer.testing import CliRunner - from specify_cli import app - runner = CliRunner() - result = runner.invoke(app, [ - "init", str(tmp_path / "test-project"), "--ai", "claude", "--integration", "copilot", - ]) - assert result.exit_code != 0 - assert "mutually exclusive" in result.output - def test_unknown_integration_rejected(self, tmp_path): from typer.testing import CliRunner from specify_cli import app @@ -131,7 +121,7 @@ def fail_select(*_args, **_kwargs): data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8")) assert data["integration"] == specify_cli.DEFAULT_INIT_INTEGRATION - def test_ai_copilot_auto_promotes(self, tmp_path): + def test_integration_copilot_auto_promotes(self, tmp_path): from typer.testing import CliRunner from specify_cli import app project = tmp_path / "promote-test" @@ -141,65 +131,41 @@ def test_ai_copilot_auto_promotes(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--ai", "copilot", "--script", "sh", "--no-git", + "init", "--here", "--integration", "copilot", "--script", "sh", "--no-git", ], catch_exceptions=False) finally: os.chdir(old_cwd) assert result.exit_code == 0 assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists() - def test_ai_emits_deprecation_warning_with_integration_replacement(self, tmp_path): + def test_legacy_ai_flag_rejected(self, tmp_path): from typer.testing import CliRunner from specify_cli import app + runner = CliRunner() + result = runner.invoke(app, [ + "init", str(tmp_path / "test-project"), "--ai", "copilot", + ]) + assert result.exit_code != 0 - project = tmp_path / "warn-ai" - project.mkdir() - old_cwd = os.getcwd() - try: - os.chdir(project) - runner = CliRunner() - result = runner.invoke(app, [ - "init", "--here", "--ai", "copilot", "--script", "sh", "--no-git", - ], catch_exceptions=False) - finally: - os.chdir(old_cwd) - - normalized_output = _normalize_cli_output(result.output) - assert result.exit_code == 0, result.output - assert "Deprecation Warning" in normalized_output - assert "--ai" in normalized_output - assert "deprecated" in normalized_output - assert "no longer be available" in normalized_output - assert "0.10.0" in normalized_output - assert "--integration copilot" in normalized_output - assert normalized_output.index("Deprecation Warning") < normalized_output.index("Next Steps") - assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists() - - def test_ai_generic_warning_suggests_integration_options_equivalent(self, tmp_path): + def test_legacy_ai_commands_dir_flag_rejected(self, tmp_path): from typer.testing import CliRunner from specify_cli import app + runner = CliRunner() + result = runner.invoke(app, [ + "init", str(tmp_path / "test-project"), "--integration", "generic", + "--ai-commands-dir", ".myagent/commands", + ]) + assert result.exit_code != 0 - project = tmp_path / "warn-generic" - project.mkdir() - old_cwd = os.getcwd() - try: - os.chdir(project) - runner = CliRunner() - result = runner.invoke(app, [ - "init", "--here", "--ai", "generic", "--ai-commands-dir", ".myagent/commands", - "--script", "sh", "--no-git", - ], catch_exceptions=False) - finally: - os.chdir(old_cwd) - - normalized_output = _normalize_cli_output(result.output) - assert result.exit_code == 0, result.output - assert "Deprecation Warning" in normalized_output - assert "--integration generic" in normalized_output - assert "--integration-options" in normalized_output - assert ".myagent/commands" in normalized_output - assert normalized_output.index("Deprecation Warning") < normalized_output.index("Next Steps") - assert (project / ".myagent" / "commands" / "speckit.plan.md").exists() + def test_legacy_ai_skills_flag_rejected(self, tmp_path): + from typer.testing import CliRunner + from specify_cli import app + runner = CliRunner() + result = runner.invoke(app, [ + "init", str(tmp_path / "test-project"), "--integration", "claude", + "--ai-skills", + ]) + assert result.exit_code != 0 def test_init_optional_preset_failure_reports_target_and_continues( self, tmp_path, monkeypatch @@ -237,7 +203,7 @@ def fail_install(self, path, version): assert "Continuing without the optional preset" in normalized assert "Project ready" in normalized - def test_ai_claude_here_preserves_preexisting_commands(self, tmp_path): + def test_integration_claude_here_preserves_preexisting_commands(self, tmp_path): from typer.testing import CliRunner from specify_cli import app @@ -255,7 +221,7 @@ def test_ai_claude_here_preserves_preexisting_commands(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--force", "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git", "--ignore-agent-tools", + "init", "--here", "--force", "--integration", "claude", "--script", "sh", "--no-git", "--ignore-agent-tools", ], catch_exceptions=False) finally: os.chdir(old_cwd) @@ -800,7 +766,7 @@ def test_git_extension_auto_installed(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--ai", "claude", "--script", "sh", + "init", "--here", "--integration", "claude", "--script", "sh", "--ignore-agent-tools", ], catch_exceptions=False) finally: @@ -838,7 +804,7 @@ def test_no_git_skips_extension(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--ai", "claude", "--script", "sh", + "init", "--here", "--integration", "claude", "--script", "sh", "--no-git", "--ignore-agent-tools", ], catch_exceptions=False) finally: @@ -862,7 +828,7 @@ def test_no_git_emits_deprecation_warning(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--ai", "claude", "--script", "sh", + "init", "--here", "--integration", "claude", "--script", "sh", "--no-git", "--ignore-agent-tools", ], catch_exceptions=False) finally: @@ -889,7 +855,7 @@ def test_default_git_auto_enable_emits_notice(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--ai", "claude", "--script", "sh", + "init", "--here", "--integration", "claude", "--script", "sh", "--ignore-agent-tools", ], catch_exceptions=False) finally: @@ -915,7 +881,7 @@ def test_git_extension_commands_registered(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--ai", "claude", "--script", "sh", + "init", "--here", "--integration", "claude", "--script", "sh", "--ignore-agent-tools", ], catch_exceptions=False) finally: diff --git a/tests/integrations/test_integration_agy.py b/tests/integrations/test_integration_agy.py index 29ddd51321..19fb81665f 100644 --- a/tests/integrations/test_integration_agy.py +++ b/tests/integrations/test_integration_agy.py @@ -29,19 +29,19 @@ def test_install_url_is_set(self): assert i.config["install_url"] == "https://antigravity.google/" -class TestAgyAutoPromote: - """--ai agy auto-promotes to integration path.""" +class TestAgyIntegration: + """--integration agy creates expected files.""" - def test_ai_agy_without_ai_skills_auto_promotes(self, tmp_path): - """--ai agy should work the same as --integration agy.""" + def test_integration_agy_creates_skills(self, tmp_path): + """--integration agy should create skills directory.""" from typer.testing import CliRunner from specify_cli import app runner = CliRunner() target = tmp_path / "test-proj" - result = runner.invoke(app, ["init", str(target), "--ai", "agy", "--no-git", "--script", "sh", "--ignore-agent-tools"]) + result = runner.invoke(app, ["init", str(target), "--integration", "agy", "--no-git", "--script", "sh", "--ignore-agent-tools"]) - assert result.exit_code == 0, f"init --ai agy failed: {result.output}" + assert result.exit_code == 0, f"init --integration agy failed: {result.output}" assert (target / ".agents" / "skills" / "speckit-plan" / "SKILL.md").exists() def test_agy_setup_warning(self, tmp_path): @@ -52,7 +52,7 @@ def test_agy_setup_warning(self, tmp_path): # Click >= 8.2 separates stdout and stderr natively runner = CliRunner() target = tmp_path / "test-proj2" - result = runner.invoke(app, ["init", str(target), "--ai", "agy", "--no-git", "--script", "sh", "--ignore-agent-tools"]) + result = runner.invoke(app, ["init", str(target), "--integration", "agy", "--no-git", "--script", "sh", "--ignore-agent-tools"]) assert result.exit_code == 0 assert "Warning: The .agents/ layout requires Antigravity v1.20.5 or newer" in result.stderr diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index 515fea816c..5d1222c965 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -179,9 +179,9 @@ def test_teardown_removes_context_section(self, tmp_path): assert "" not in remaining assert "# My Rules" in remaining - # -- CLI auto-promote ------------------------------------------------- + # -- CLI integration flag ------------------------------------------------- - def test_ai_flag_auto_promotes(self, tmp_path): + def test_integration_flag_auto_promotes(self, tmp_path): from typer.testing import CliRunner from specify_cli import app @@ -192,15 +192,15 @@ def test_ai_flag_auto_promotes(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--ai", self.KEY, "--script", "sh", "--no-git", + "init", "--here", "--integration", self.KEY, "--script", "sh", "--no-git", "--ignore-agent-tools", ], catch_exceptions=False) finally: os.chdir(old_cwd) - assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}" + assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}" i = get_integration(self.KEY) cmd_dir = i.commands_dest(project) - assert cmd_dir.is_dir(), f"--ai {self.KEY} did not create commands directory" + assert cmd_dir.is_dir(), f"--integration {self.KEY} did not create commands directory" def test_integration_flag_creates_files(self, tmp_path): from typer.testing import CliRunner diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 35852217c0..ca30831985 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -312,9 +312,9 @@ def test_teardown_removes_context_section(self, tmp_path): assert "" not in remaining assert "# My Rules" in remaining - # -- CLI auto-promote ------------------------------------------------- + # -- CLI integration flag ------------------------------------------------- - def test_ai_flag_auto_promotes(self, tmp_path): + def test_integration_flag_auto_promotes(self, tmp_path): from typer.testing import CliRunner from specify_cli import app @@ -325,15 +325,15 @@ def test_ai_flag_auto_promotes(self, tmp_path): os.chdir(project) runner = CliRunner() result = runner.invoke(app, [ - "init", "--here", "--ai", self.KEY, "--script", "sh", "--no-git", + "init", "--here", "--integration", self.KEY, "--script", "sh", "--no-git", "--ignore-agent-tools", ], catch_exceptions=False) finally: os.chdir(old_cwd) - assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}" + assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}" i = get_integration(self.KEY) skills_dir = i.skills_dest(project) - assert skills_dir.is_dir(), f"--ai {self.KEY} did not create skills directory" + assert skills_dir.is_dir(), f"--integration {self.KEY} did not create skills directory" def test_integration_flag_creates_files(self, tmp_path): from typer.testing import CliRunner diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 1187da09d6..eac51bd206 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -388,9 +388,9 @@ def test_teardown_removes_context_section(self, tmp_path): assert "" not in remaining assert "# My Rules" in remaining - # -- CLI auto-promote ------------------------------------------------- + # -- CLI integration flag ------------------------------------------------- - def test_ai_flag_auto_promotes(self, tmp_path): + def test_integration_flag_auto_promotes(self, tmp_path): from typer.testing import CliRunner from specify_cli import app @@ -405,7 +405,7 @@ def test_ai_flag_auto_promotes(self, tmp_path): [ "init", "--here", - "--ai", + "--integration", self.KEY, "--script", "sh", @@ -416,10 +416,10 @@ def test_ai_flag_auto_promotes(self, tmp_path): ) finally: os.chdir(old_cwd) - assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}" + assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}" i = get_integration(self.KEY) cmd_dir = i.commands_dest(project) - assert cmd_dir.is_dir(), f"--ai {self.KEY} did not create commands directory" + assert cmd_dir.is_dir(), f"--integration {self.KEY} did not create commands directory" def test_integration_flag_creates_files(self, tmp_path): from typer.testing import CliRunner diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index 5bc8764f63..a1a02b7811 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -267,9 +267,9 @@ def test_teardown_removes_context_section(self, tmp_path): assert "" not in remaining assert "# My Rules" in remaining - # -- CLI auto-promote ------------------------------------------------- + # -- CLI integration flag ------------------------------------------------- - def test_ai_flag_auto_promotes(self, tmp_path): + def test_integration_flag_auto_promotes(self, tmp_path): from typer.testing import CliRunner from specify_cli import app @@ -284,7 +284,7 @@ def test_ai_flag_auto_promotes(self, tmp_path): [ "init", "--here", - "--ai", + "--integration", self.KEY, "--script", "sh", @@ -295,10 +295,10 @@ def test_ai_flag_auto_promotes(self, tmp_path): ) finally: os.chdir(old_cwd) - assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}" + assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}" i = get_integration(self.KEY) cmd_dir = i.commands_dest(project) - assert cmd_dir.is_dir(), f"--ai {self.KEY} did not create commands directory" + assert cmd_dir.is_dir(), f"--integration {self.KEY} did not create commands directory" def test_integration_flag_creates_files(self, tmp_path): from typer.testing import CliRunner diff --git a/tests/integrations/test_integration_claude.py b/tests/integrations/test_integration_claude.py index 48c76bdd36..e1bda1fb68 100644 --- a/tests/integrations/test_integration_claude.py +++ b/tests/integrations/test_integration_claude.py @@ -118,7 +118,7 @@ def test_remove_context_section_strips_bom(self, tmp_path): assert b" -## [0.10.0] - Unreleased +## [0.9.5] - 2026-06-05 -### Breaking Changes +### Changed -- **Removed legacy `--ai`, `--ai-commands-dir`, and `--ai-skills` flags from `specify init`.** - Use `--integration `, `--integration-options="--commands-dir "`, and - `--integration-options="--skills"` respectively. +- feat(extensions): add bundled bug triage workflow extension (#2871) +- fix: resolve GitHub release asset API URL for private repo preset and workflow downloads (#2855) +- chore(deps): bump github/gh-aw-actions from 0.77.0 to 0.78.1 (#2860) +- chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#2859) +- chore(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 (#2858) +- chore(deps): bump github/codeql-action from 4.36.0 to 4.36.2 (#2857) +- fix(workflows): render gate show_file contents in the interactive prompt (#2810) +- feat: add support for rovodev (#2539) +- chore: release 0.9.4, begin 0.9.5.dev0 development (#2853) ## [0.9.4] - 2026-06-04 From 25f1069ca4a96bcecd7ebfa39163609f6aa7574e Mon Sep 17 00:00:00 2001 From: Manfred Riem Date: Fri, 5 Jun 2026 13:41:07 -0500 Subject: [PATCH 8/9] fix: make generic catalog description self-explanatory Include the required --commands-dir sub-option in the description so readers don't need to look up integration docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- integrations/catalog.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/catalog.json b/integrations/catalog.json index 6fa0d05c34..33c6ddd931 100644 --- a/integrations/catalog.json +++ b/integrations/catalog.json @@ -277,7 +277,7 @@ "id": "generic", "name": "Generic (bring your own agent)", "version": "1.0.0", - "description": "Generic integration for any agent via --integration-options", + "description": "Generic integration for any agent via --integration-options=\"--commands-dir \"", "author": "spec-kit-core", "repository": "https://github.com/github/spec-kit", "tags": ["generic"] From e08a6e506ec712c56f3c8f7e7503717ae5003d29 Mon Sep 17 00:00:00 2001 From: Manfred Riem Date: Fri, 5 Jun 2026 14:49:19 -0500 Subject: [PATCH 9/9] fix(tests): rename duplicate test classes to avoid shadowing The rename from Test*AutoPromote to Test*Integration collided with the existing Test*Integration(SkillsIntegrationTests) base classes, causing the shared test suites to be silently overwritten. Rename the CLI init flow classes to Test*InitFlow instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/integrations/test_integration_agy.py | 2 +- tests/integrations/test_integration_codex.py | 2 +- tests/integrations/test_integration_cursor_agent.py | 2 +- tests/integrations/test_integration_devin.py | 2 +- tests/integrations/test_integration_hermes.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integrations/test_integration_agy.py b/tests/integrations/test_integration_agy.py index 19fb81665f..fd7b82e120 100644 --- a/tests/integrations/test_integration_agy.py +++ b/tests/integrations/test_integration_agy.py @@ -29,7 +29,7 @@ def test_install_url_is_set(self): assert i.config["install_url"] == "https://antigravity.google/" -class TestAgyIntegration: +class TestAgyInitFlow: """--integration agy creates expected files.""" def test_integration_agy_creates_skills(self, tmp_path): diff --git a/tests/integrations/test_integration_codex.py b/tests/integrations/test_integration_codex.py index 4a46b15463..8ce0d82d64 100644 --- a/tests/integrations/test_integration_codex.py +++ b/tests/integrations/test_integration_codex.py @@ -14,7 +14,7 @@ class TestCodexIntegration(SkillsIntegrationTests): CONTEXT_FILE = "AGENTS.md" -class TestCodexIntegration: +class TestCodexInitFlow: """--integration codex creates expected files.""" def test_integration_codex_creates_skills(self, tmp_path): diff --git a/tests/integrations/test_integration_cursor_agent.py b/tests/integrations/test_integration_cursor_agent.py index ed65ffb26a..f03238a4ea 100644 --- a/tests/integrations/test_integration_cursor_agent.py +++ b/tests/integrations/test_integration_cursor_agent.py @@ -92,7 +92,7 @@ def test_remove_deletes_mdc_with_only_frontmatter(self, tmp_path): assert not ctx_path.exists() -class TestCursorAgentIntegration: +class TestCursorAgentInitFlow: """--integration cursor-agent creates expected files.""" def test_integration_cursor_agent_creates_skills(self, tmp_path): diff --git a/tests/integrations/test_integration_devin.py b/tests/integrations/test_integration_devin.py index bb6a1912c0..642a55ce62 100644 --- a/tests/integrations/test_integration_devin.py +++ b/tests/integrations/test_integration_devin.py @@ -56,7 +56,7 @@ def test_model_flag_passed_through(self): assert args == ["devin", "-p", "hi", "--model", "claude-sonnet-4"] -class TestDevinIntegration: +class TestDevinInitFlow: """--integration devin creates expected files.""" def test_integration_devin_creates_skills(self, tmp_path): diff --git a/tests/integrations/test_integration_hermes.py b/tests/integrations/test_integration_hermes.py index 5ecbc16d8f..d19a2c32ab 100644 --- a/tests/integrations/test_integration_hermes.py +++ b/tests/integrations/test_integration_hermes.py @@ -326,7 +326,7 @@ def test_install_uninstall_cleanup(self, tmp_path, monkeypatch): ) -class TestHermesIntegration: +class TestHermesInitFlow: """--integration hermes creates expected files.""" def test_integration_hermes_creates_global_skills(self, tmp_path, monkeypatch):