From 16416022474a64aacd06fee11c558bd1f7eb4ccf Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Wed, 29 Apr 2026 18:17:08 +0300 Subject: [PATCH 1/4] fix: default non-interactive init integration --- src/specify_cli/__init__.py | 2 ++ tests/integrations/test_cli.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index f5e117beef..948749b4a1 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1164,6 +1164,8 @@ def init( 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 + elif not sys.stdin.isatty(): + selected_ai = "copilot" else: # Create options dict for selection (agent_key: display_name) ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()} diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index 60e51a5fb9..9faa0b29c2 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -73,6 +73,28 @@ def test_integration_copilot_creates_files(self, tmp_path): shared_manifest = project / ".specify" / "integrations" / "speckit.manifest.json" assert shared_manifest.exists() + def test_noninteractive_init_defaults_to_copilot(self, tmp_path, monkeypatch): + from typer.testing import CliRunner + from specify_cli import app + import specify_cli + + def fail_select(*_args, **_kwargs): + raise AssertionError("non-interactive init should not open the integration picker") + + monkeypatch.setattr(specify_cli, "select_with_arrows", fail_select) + + runner = CliRunner() + project = tmp_path / "noninteractive" + result = runner.invoke(app, [ + "init", str(project), "--script", "sh", "--no-git", "--ignore-agent-tools", + ], catch_exceptions=False) + + assert result.exit_code == 0, result.output + assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists() + + data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8")) + assert data["integration"] == "copilot" + def test_ai_copilot_auto_promotes(self, tmp_path): from typer.testing import CliRunner from specify_cli import app From 10ebaf0da9075168830153b53b8d64f2ade20afd Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Wed, 29 Apr 2026 18:22:00 +0300 Subject: [PATCH 2/4] chore: clarify non-interactive init default integration --- src/specify_cli/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 948749b4a1..25ec76508d 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1165,6 +1165,7 @@ def init( raise typer.Exit(1) selected_ai = ai_assistant elif not sys.stdin.isatty(): + console.print("[dim]Non-interactive session detected: defaulting to 'copilot'. Use --integration to choose a different agent.[/dim]") selected_ai = "copilot" else: # Create options dict for selection (agent_key: display_name) From a8313d31dbc3466f9202bbf51a6128fa9421bfdb Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Wed, 6 May 2026 09:44:59 +0300 Subject: [PATCH 3/4] Address non-interactive init review feedback --- README.md | 2 +- docs/installation.md | 2 ++ docs/reference/core.md | 2 ++ src/specify_cli/__init__.py | 13 +++++++++---- tests/integrations/test_cli.py | 3 ++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6450e35601..c47e95f49b 100644 --- a/README.md +++ b/README.md @@ -478,7 +478,7 @@ specify init --here --force ![Specify CLI bootstrapping a new project in the terminal](./media/specify_cli.gif) -You will be prompted to select the coding agent integration you are using. You can also proactively specify it directly in the terminal: +In an interactive terminal, you will be prompted to select the coding agent integration you are using. In non-interactive sessions, such as CI or piped runs, `specify init` defaults to GitHub Copilot unless you pass `--integration`. You can also proactively specify the integration directly in the terminal: ```bash specify init --integration copilot diff --git a/docs/installation.md b/docs/installation.md index 7f6aa089b7..e53282c0b9 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -41,6 +41,8 @@ uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here ### Specify Integration +Interactive terminals prompt you to choose a coding agent integration during initialization. Non-interactive sessions, such as CI or piped runs, default to GitHub Copilot unless you pass `--integration`. + You can proactively specify your coding agent integration during initialization: ```bash diff --git a/docs/reference/core.md b/docs/reference/core.md index fdab05a02b..2f38202b2b 100644 --- a/docs/reference/core.md +++ b/docs/reference/core.md @@ -24,6 +24,8 @@ Creates a new Spec Kit project with the necessary directory structure, templates Use `` to create a new directory, or `--here` (or `.`) to initialize in the current directory. If the directory already has files, use `--force` to merge without confirmation. +When `--integration` is omitted, interactive terminals prompt you to choose an integration. Non-interactive sessions, such as CI or piped runs, default to GitHub Copilot; pass `--integration ` to choose a different integration explicitly. + ### Examples ```bash diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 25ec76508d..49deda6a65 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -69,6 +69,7 @@ def _build_agent_config() -> dict[str, dict[str, Any]]: return config AGENT_CONFIG = _build_agent_config() +DEFAULT_INIT_INTEGRATION = "copilot" AI_ASSISTANT_ALIASES = { "kiro": "kiro-cli", @@ -997,7 +998,8 @@ def init( This command will: 1. Check that required tools are installed (git is optional) - 2. Let you choose your coding agent integration + 2. Let you choose your coding agent integration, or default to Copilot + in non-interactive sessions 3. Download template from GitHub (or use bundled assets with --offline) 4. Initialize a fresh git repository (if not --no-git and no existing repo) 5. Optionally set up coding agent integration commands @@ -1165,15 +1167,18 @@ def init( raise typer.Exit(1) selected_ai = ai_assistant elif not sys.stdin.isatty(): - console.print("[dim]Non-interactive session detected: defaulting to 'copilot'. Use --integration to choose a different agent.[/dim]") - selected_ai = "copilot" + console.print( + f"[dim]Non-interactive session detected: defaulting to '{DEFAULT_INIT_INTEGRATION}'. " + "Use --integration to choose a different agent.[/dim]" + ) + selected_ai = DEFAULT_INIT_INTEGRATION 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 coding agent integration:", - "copilot" + DEFAULT_INIT_INTEGRATION, ) # Auto-promote interactively selected agents to the integration path diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index 9faa0b29c2..de65b8f84c 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -90,10 +90,11 @@ def fail_select(*_args, **_kwargs): ], catch_exceptions=False) assert result.exit_code == 0, result.output + assert f"defaulting to '{specify_cli.DEFAULT_INIT_INTEGRATION}'" in result.output assert (project / ".github" / "agents" / "speckit.plan.agent.md").exists() data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8")) - assert data["integration"] == "copilot" + assert data["integration"] == specify_cli.DEFAULT_INIT_INTEGRATION def test_ai_copilot_auto_promotes(self, tmp_path): from typer.testing import CliRunner From 62438d1e5106d5b3f44ad37dd83b9115c6fc3aa7 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Wed, 6 May 2026 20:13:32 +0300 Subject: [PATCH 4/4] Fix interactive init test after fallback --- src/specify_cli/__init__.py | 7 +++++-- tests/integrations/test_integration_claude.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 49deda6a65..dc5475a34b 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -132,6 +132,9 @@ def _build_ai_deprecation_warning( f"Use [bold]{replacement}[/bold] instead." ) +def _stdin_is_interactive() -> bool: + return sys.stdin.isatty() + SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude" @@ -1166,7 +1169,7 @@ def init( 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 - elif not sys.stdin.isatty(): + elif not _stdin_is_interactive(): console.print( f"[dim]Non-interactive session detected: defaulting to '{DEFAULT_INIT_INTEGRATION}'. " "Use --integration to choose a different agent.[/dim]" @@ -1243,7 +1246,7 @@ def init( else: default_script = "ps" if os.name == "nt" else "sh" - if sys.stdin.isatty(): + if _stdin_is_interactive(): selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script) else: selected_script = default_script diff --git a/tests/integrations/test_integration_claude.py b/tests/integrations/test_integration_claude.py index b3236a66b7..142db0dd92 100644 --- a/tests/integrations/test_integration_claude.py +++ b/tests/integrations/test_integration_claude.py @@ -196,7 +196,10 @@ def test_interactive_claude_selection_uses_integration_path(self, tmp_path): try: os.chdir(project) runner = CliRunner() - with patch("specify_cli.select_with_arrows", return_value="claude"): + with ( + patch("specify_cli._stdin_is_interactive", return_value=True), + patch("specify_cli.select_with_arrows", return_value="claude"), + ): result = runner.invoke( app, [