diff --git a/src/specify_cli/integrations/cursor_agent/__init__.py b/src/specify_cli/integrations/cursor_agent/__init__.py index 70af454ce..7196c7fd0 100644 --- a/src/specify_cli/integrations/cursor_agent/__init__.py +++ b/src/specify_cli/integrations/cursor_agent/__init__.py @@ -2,6 +2,10 @@ Cursor Agent uses the ``.cursor/skills/speckit-/SKILL.md`` layout. Commands are deprecated; ``--skills`` defaults to ``True``. + +CLI dispatch via ``cursor-agent -p --trust `` is supported so +``specify workflow run`` can drive cursor-agent headlessly, in addition +to the existing in-IDE skill flow. """ from __future__ import annotations @@ -15,8 +19,8 @@ class CursorAgentIntegration(SkillsIntegration): "name": "Cursor", "folder": ".cursor/", "commands_subdir": "skills", - "install_url": None, - "requires_cli": False, + "install_url": "https://docs.cursor.com/en/cli/overview", + "requires_cli": True, } registrar_config = { "dir": ".cursor/skills", @@ -28,6 +32,29 @@ class CursorAgentIntegration(SkillsIntegration): context_file = ".cursor/rules/specify-rules.mdc" multi_install_safe = True + def build_exec_args( + self, + prompt: str, + *, + model: str | None = None, + output_json: bool = True, + ) -> list[str] | None: + """Build CLI arguments for non-interactive ``cursor-agent`` execution. + + Uses ``-p`` (print/headless mode, which gives access to all + tools including write and shell) plus ``--trust`` (bypass the + Workspace Trust prompt — mandatory for headless execution; the + CLI exits non-zero without it). + """ + if not self.config or not self.config.get("requires_cli"): + return None + args = [self.key, "-p", "--trust", prompt] + if model: + args.extend(["--model", model]) + if output_json: + args.extend(["--output-format", "json"]) + return args + @classmethod def options(cls) -> list[IntegrationOption]: return [ diff --git a/tests/integrations/test_integration_cursor_agent.py b/tests/integrations/test_integration_cursor_agent.py index 352a0475b..5c8e518c4 100644 --- a/tests/integrations/test_integration_cursor_agent.py +++ b/tests/integrations/test_integration_cursor_agent.py @@ -106,3 +106,55 @@ class TestCursorAgentAutoPromote: assert result.exit_code == 0, f"init --ai cursor-agent failed: {result.output}" assert (target / ".cursor" / "skills" / "speckit-plan" / "SKILL.md").exists() + +class TestCursorAgentCliDispatch: + """Verify the CLI dispatch path for cursor-agent (issue #2629). + + The ``cursor-agent`` CLI supports headless execution via ``-p`` (with + full tool access including write/shell) and requires ``--trust`` to + bypass the Workspace Trust prompt. These tests pin the exact argv + shape that the workflow runner will use. + """ + + def test_requires_cli_is_true(self): + i = get_integration("cursor-agent") + assert i.config.get("requires_cli") is True + + def test_install_url_is_set(self): + i = get_integration("cursor-agent") + url = i.config.get("install_url") + assert url is not None + assert "cursor.com" in url + + def test_build_exec_args_default_includes_trust_and_json(self): + i = get_integration("cursor-agent") + args = i.build_exec_args("/speckit-specify some-feature") + assert args == [ + "cursor-agent", "-p", "--trust", + "/speckit-specify some-feature", + "--output-format", "json", + ] + + def test_build_exec_args_text_output_omits_format(self): + i = get_integration("cursor-agent") + args = i.build_exec_args("/speckit-plan", output_json=False) + assert args == [ + "cursor-agent", "-p", "--trust", "/speckit-plan", + ] + + def test_build_exec_args_with_model(self): + i = get_integration("cursor-agent") + args = i.build_exec_args( + "/speckit-specify", model="sonnet-4-thinking", output_json=False + ) + assert args == [ + "cursor-agent", "-p", "--trust", "/speckit-specify", + "--model", "sonnet-4-thinking", + ] + + def test_build_command_invocation_uses_hyphenated_skill_name(self): + """SkillsIntegration: /speckit-plan (not /speckit.plan).""" + i = get_integration("cursor-agent") + assert i.build_command_invocation("speckit.plan", "feature-x") == "/speckit-plan feature-x" + assert i.build_command_invocation("plan") == "/speckit-plan" +