From 312cf1721f86e4be5c1a42847d5cf9d9e568a54b Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:10:15 -0500 Subject: [PATCH 1/4] fix(cli): align integration guidance with available output Show integration selection and switching guidance when an integration has no command templates, and report CLI requirements without inferring IDE-only usage. Preserve ordinary command and skill next steps. Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/reference/integrations.md | 6 +- src/specify_cli/command_check.py | 4 +- src/specify_cli/command_init.py | 18 ++++++ src/specify_cli/integrations/command_list.py | 2 +- .../integrations/test_command_list.py | 14 +++++ .../integrations/test_installed_adapters.py | 3 +- tests/specify_cli/test_command_check.py | 34 +++++++++++ tests/specify_cli/test_command_init.py | 61 +++++++++++++++++++ 8 files changed, 136 insertions(+), 6 deletions(-) diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 2480695630..d6f7ec4a33 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -80,7 +80,7 @@ specify integration list | `--catalog` | Also browse the catalog, including external integrations not installed in this project. | Shows built-in and trusted installed external integrations, which one is -currently installed, and whether each requires a CLI tool or is IDE-based. +currently installed, and whether each requires a CLI tool. When multiple integrations are installed, the list marks the default integration separately from the other installed integrations. The list also shows whether each integration is declared multi-install safe. @@ -565,7 +565,9 @@ Run `specify integration list` to see all available integrations with their keys ### Do I need the AI coding agent installed to use an integration? -CLI-based integrations (like Claude Code, Gemini CLI) require the tool to be installed. IDE-based integrations (like Cursor) work through the IDE itself. Some agents like GitHub Copilot support both IDE and CLI usage. `specify integration list` shows which type each integration is. +CLI-based integrations (like Claude Code, Gemini CLI) require the tool to be installed. IDE-based integrations (like Cursor) work through the IDE itself. Some agents like GitHub Copilot support both IDE and CLI usage. The `CLI Required` column in `specify integration list` reports only whether an executable is required, not whether the integration is IDE-only. + +If an integration generates no command templates, `specify init` keeps the project-directory guidance and shows how to choose an integration with `specify integration list` and add it with `specify integration switch `, rather than recommending unavailable agent commands. ### When should I use `upgrade` vs `switch`? diff --git a/src/specify_cli/command_check.py b/src/specify_cli/command_check.py index d595709bb3..9162bbb6ee 100644 --- a/src/specify_cli/command_check.py +++ b/src/specify_cli/command_check.py @@ -30,7 +30,7 @@ def _check_external_tools( args = integration.build_exec_args("test", project_root=project_root) tools = [args[0] if args else integration._resolve_executable()] if not tools: - tracker.skip(key, "IDE-based, no CLI check") + tracker.skip(key, "no CLI required") return False tools = list(dict.fromkeys(tools)) missing = [tool for tool in tools if shutil.which(tool) is None] @@ -90,7 +90,7 @@ def check() -> None: elif requires_cli: agent_results[agent_key] = check_tool(agent_key, tracker=tracker) else: - tracker.skip(agent_key, "IDE-based, no CLI check") + tracker.skip(agent_key, "no CLI required") agent_results[agent_key] = False tracker.add("code", "Visual Studio Code") diff --git a/src/specify_cli/command_init.py b/src/specify_cli/command_init.py index ea21fb1a54..a169ab9761 100644 --- a/src/specify_cli/command_init.py +++ b/src/specify_cli/command_init.py @@ -1057,6 +1057,24 @@ def init( steps_lines.append("1. You're already in the project directory!") step_num = 2 + if not resolved_integration.list_command_templates(): + steps_lines.append( + f"{step_num}. Choose an integration: [cyan]specify integration list[/cyan]" + ) + steps_lines.append( + f"{step_num + 1}. Add it to this project: [cyan]specify integration switch [/cyan]" + ) + console.print() + console.print( + Panel( + "\n".join(steps_lines), + title="Next Steps", + border_style="cyan", + padding=(1, 2), + ) + ) + return + _is_skills_integration = resolved_integration.is_skills_mode( integration_parsed_options or None, project_root=project_path ) diff --git a/src/specify_cli/integrations/command_list.py b/src/specify_cli/integrations/command_list.py index 540de82d35..30f9774401 100644 --- a/src/specify_cli/integrations/command_list.py +++ b/src/specify_cli/integrations/command_list.py @@ -102,7 +102,7 @@ def integration_list( status = "[green]installed[/green]" else: status = "" - cli_req = "yes" if requires_cli else "no (IDE)" + cli_req = "yes" if requires_cli else "no" safe = "yes" if getattr(integration, "multi_install_safe", False) else "no" table.add_row(key, escape(str(name)), status, cli_req, safe) diff --git a/tests/specify_cli/integrations/test_command_list.py b/tests/specify_cli/integrations/test_command_list.py index 5d11931a3d..ad169feef9 100644 --- a/tests/specify_cli/integrations/test_command_list.py +++ b/tests/specify_cli/integrations/test_command_list.py @@ -24,6 +24,20 @@ ) class TestIntegrationList: + def test_cli_requirement_does_not_imply_ide_classification(self, tmp_path, monkeypatch): + from types import SimpleNamespace + from specify_cli.integrations import INTEGRATION_REGISTRY + + project = _init_project(tmp_path, "copilot") + monkeypatch.setitem(INTEGRATION_REGISTRY, "artifact-free", SimpleNamespace( + config={"name": "Artifact-free", "requires_cli": False}, + )) + result = _run_in_project(project, ["integration", "list"]) + assert result.exit_code == 0, result.output + assert _integration_list_row_cells(result.output, "artifact-free")[-2] == "no" + assert _integration_list_row_cells(result.output, "claude")[-2] == "yes" + assert "(IDE)" not in result.output + def test_list_requires_speckit_project(self, tmp_path): old_cwd = os.getcwd() try: diff --git a/tests/specify_cli/integrations/test_installed_adapters.py b/tests/specify_cli/integrations/test_installed_adapters.py index 6f8fbadd92..0c1bb0a60a 100644 --- a/tests/specify_cli/integrations/test_installed_adapters.py +++ b/tests/specify_cli/integrations/test_installed_adapters.py @@ -3803,7 +3803,8 @@ def which(name, *args, **kwargs): assert KEY not in probes assert "optional-helper" not in probes assert "Sample Agent" in result.output - assert "IDE-based, no CLI check" in result.output + assert "no CLI required" in result.output + assert "IDE-based" not in result.output @pytest.mark.parametrize("error_type", ["ValueError", "OSError", "NotImplementedError"]) diff --git a/tests/specify_cli/test_command_check.py b/tests/specify_cli/test_command_check.py index 2a49b64ccd..af4853919d 100644 --- a/tests/specify_cli/test_command_check.py +++ b/tests/specify_cli/test_command_check.py @@ -2,6 +2,7 @@ from unittest.mock import patch +import pytest from typer.testing import CliRunner from specify_cli import app @@ -11,6 +12,39 @@ runner = CliRunner() +@pytest.mark.parametrize("external", [False, True]) +def test_no_cli_requirement_is_not_classified_as_ide(tmp_path, monkeypatch, external): + from specify_cli.command_check import _check_external_tools + from specify_cli._agent_config import AGENT_CONFIG + from specify_cli._console import StepTracker + + if external: + tracker = StepTracker("Tools") + tracker.add("generic", "Generic") + with patch("specify_cli.command_check.shutil.which") as which: + assert not _check_external_tools( + tmp_path, "generic", {"requires": {}}, False, tracker, + ) + which.assert_not_called() + from rich.console import Console + console = Console(width=120) + with console.capture() as capture: + console.print(tracker.render()) + output = capture.get() + else: + monkeypatch.chdir(tmp_path) + monkeypatch.setitem(AGENT_CONFIG, "artifact-free", { + "name": "Artifact-free", "requires_cli": False, + }) + with patch("specify_cli.check_tool", return_value=True) as check_tool: + result = runner.invoke(app, ["check"]) + assert result.exit_code == 0, result.output + assert "artifact-free" not in [call.args[0] for call in check_tool.call_args_list] + output = strip_ansi(result.output) + assert "no CLI required" in output + assert "IDE-based" not in output + + class TestCheckTip: """`specify check` should point users to the existing version check.""" diff --git a/tests/specify_cli/test_command_init.py b/tests/specify_cli/test_command_init.py index 33ad48aa0a..f5128dd328 100644 --- a/tests/specify_cli/test_command_init.py +++ b/tests/specify_cli/test_command_init.py @@ -209,3 +209,64 @@ def test_shell_quote_arg_is_host_appropriate(): assert quoted == '"my project"' else: assert quoted == "'my project'" + + +@pytest.mark.parametrize("here", [False, True]) +def test_output_free_init_recommends_installing_an_integration(tmp_path, monkeypatch, here): + from specify_cli.integrations.generic import GenericIntegration + + monkeypatch.setattr(GenericIntegration, "list_command_templates", lambda self: []) + monkeypatch.chdir(tmp_path) + result = CliRunner().invoke( + app, + [ + "init", *(["--here"] if here else ["proj"]), + "--integration", "generic", + "--integration-options", "--commands-dir .agent/commands", + "--ignore-agent-tools", + ], + ) + assert result.exit_code == 0, result.output + output = _strip(result.output) + assert "Next Steps" in output + if here: + assert "You're already in the project directory!" in output + else: + assert "cd proj" in output + assert "specify integration switch " in output + assert "specify integration list" in output + assert "Start using" not in output + assert "speckit." not in output + assert "Enhancement Commands" not in output + assert "Enhancement Skills" not in output + project = tmp_path if here else tmp_path / "proj" + assert not (project / ".agent" / "commands").exists() + + +def test_rendering_init_preserves_command_guidance(tmp_path): + result = _init(tmp_path, "proj") + assert result.exit_code == 0, result.output + output = _strip(result.output) + assert "Start using slash commands" in output + for command in ("constitution", "specify", "plan", "tasks", "implement", "converge", + "clarify", "analyze", "checklist"): + assert f"/speckit.{command}" in output + assert (tmp_path / "proj" / ".agent" / "commands" / f"speckit.{command}.md").is_file() + assert "Enhancement Commands" in output + assert "specify integration switch " not in output + + +def test_rendering_init_preserves_skill_guidance(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + result = CliRunner().invoke( + app, ["init", "proj", "--integration", "copilot", "--ignore-agent-tools"], + ) + assert result.exit_code == 0, result.output + output = _strip(result.output) + assert "Start using skills" in output + assert "Enhancement Skills" in output + assert "specify integration switch " not in output + for command in ("constitution", "specify", "plan", "tasks", "implement", "converge", + "clarify", "analyze", "checklist"): + assert f"/speckit-{command}" in output + assert (tmp_path / "proj" / ".github" / "skills" / f"speckit-{command}" / "SKILL.md").is_file() From 7497edf8b47c7cca23ea0c63d99661fd20a272f5 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:08:18 -0500 Subject: [PATCH 2/4] docs(integrations): distinguish agent CLI and descriptor tools Clarify that integration list reports config.requires_cli while external adapter descriptor tools are validated separately during installation and specify check. Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/integrations/cli.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/integrations/cli.md b/docs/integrations/cli.md index 7a5eeb63f7..e240627e59 100644 --- a/docs/integrations/cli.md +++ b/docs/integrations/cli.md @@ -45,8 +45,11 @@ specify integration list Shows built-in and trusted installed external integrations, their setup requirements, and their multi-install safety declarations. Installed integrations and the default are marked separately. -The `CLI Required` column reports whether an executable is required, not -whether the integration is IDE-only. +The `CLI Required` column reports the agent CLI requirement declared by +`config.requires_cli`, not whether the integration is IDE-only. External adapters +can separately require executables through their descriptor's `requires.tools`; +installation and `specify check` validate those tool requirements even when +`CLI Required` is `no`. ## Search Available Integrations From 694d4610143c47bd14757e1506784855b7592121 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:50:27 -0500 Subject: [PATCH 3/4] test(check): exercise registered artifact-free integration Use the registered none integration for the no-probe assertion and require its own no-CLI output, so registry refresh cannot make the negative case vacuous. Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/specify_cli/test_command_check.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/specify_cli/test_command_check.py b/tests/specify_cli/test_command_check.py index af4853919d..7026db27f9 100644 --- a/tests/specify_cli/test_command_check.py +++ b/tests/specify_cli/test_command_check.py @@ -15,7 +15,6 @@ @pytest.mark.parametrize("external", [False, True]) def test_no_cli_requirement_is_not_classified_as_ide(tmp_path, monkeypatch, external): from specify_cli.command_check import _check_external_tools - from specify_cli._agent_config import AGENT_CONFIG from specify_cli._console import StepTracker if external: @@ -33,14 +32,12 @@ def test_no_cli_requirement_is_not_classified_as_ide(tmp_path, monkeypatch, exte output = capture.get() else: monkeypatch.chdir(tmp_path) - monkeypatch.setitem(AGENT_CONFIG, "artifact-free", { - "name": "Artifact-free", "requires_cli": False, - }) with patch("specify_cli.check_tool", return_value=True) as check_tool: result = runner.invoke(app, ["check"]) assert result.exit_code == 0, result.output - assert "artifact-free" not in [call.args[0] for call in check_tool.call_args_list] + assert "none" not in [call.args[0] for call in check_tool.call_args_list] output = strip_ansi(result.output) + assert "None (shared scaffolding only) (no CLI required)" in output assert "no CLI required" in output assert "IDE-based" not in output From 5215b8c373271094bccad13d7ad8571e3fcc501d Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:12:43 -0500 Subject: [PATCH 4/4] fix(cli): describe integration switch guidance precisely Use switch terminology rather than add for empty-template next steps, describe the template inventory directly, and assert the revised wording for both initialization locations. Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/integrations/cli.md | 4 ++-- src/specify_cli/command_init.py | 2 +- tests/specify_cli/test_command_init.py | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/integrations/cli.md b/docs/integrations/cli.md index e240627e59..c320a0f5e6 100644 --- a/docs/integrations/cli.md +++ b/docs/integrations/cli.md @@ -535,9 +535,9 @@ work through the IDE itself and may not require a separate CLI for setup. Workflow dispatch can have additional CLI requirements; see your integration's guide. `specify integration list` shows each integration's setup requirements. -If an integration generates no command templates, `specify init` keeps the +If an integration has no command templates, `specify init` keeps the project-directory guidance and shows how to choose an integration with -`specify integration list` and add it with `specify integration switch `, +`specify integration list` and switch to it with `specify integration switch `, rather than recommending unavailable agent commands. ### When should I use `upgrade` vs `switch`? diff --git a/src/specify_cli/command_init.py b/src/specify_cli/command_init.py index a169ab9761..7d1536d4ba 100644 --- a/src/specify_cli/command_init.py +++ b/src/specify_cli/command_init.py @@ -1062,7 +1062,7 @@ def init( f"{step_num}. Choose an integration: [cyan]specify integration list[/cyan]" ) steps_lines.append( - f"{step_num + 1}. Add it to this project: [cyan]specify integration switch [/cyan]" + f"{step_num + 1}. Switch this project to it: [cyan]specify integration switch [/cyan]" ) console.print() console.print( diff --git a/tests/specify_cli/test_command_init.py b/tests/specify_cli/test_command_init.py index f5128dd328..e353445870 100644 --- a/tests/specify_cli/test_command_init.py +++ b/tests/specify_cli/test_command_init.py @@ -212,7 +212,7 @@ def test_shell_quote_arg_is_host_appropriate(): @pytest.mark.parametrize("here", [False, True]) -def test_output_free_init_recommends_installing_an_integration(tmp_path, monkeypatch, here): +def test_output_free_init_recommends_switching_integrations(tmp_path, monkeypatch, here): from specify_cli.integrations.generic import GenericIntegration monkeypatch.setattr(GenericIntegration, "list_command_templates", lambda self: []) @@ -234,6 +234,8 @@ def test_output_free_init_recommends_installing_an_integration(tmp_path, monkeyp else: assert "cd proj" in output assert "specify integration switch " in output + assert "Switch this project to it:" in output + assert "Add it to this project:" not in output assert "specify integration list" in output assert "Start using" not in output assert "speckit." not in output