Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 81 additions & 4 deletions src/specify_cli/integrations/agy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,91 @@ def build_exec_args(
*,
model: str | None = None,
output_json: bool = True,
project_root: Path | None = None,
) -> list[str] | None:
# agy does not support --model or JSON output; both params are ignored
args = [self._resolve_executable(), "--print", prompt]
# Honor SPECKIT_INTEGRATION_AGY_EXTRA_ARGS (operator-supplied flags),
# appended after the positional prompt like the devin integration.
# agy does not support JSON output; output_json is ignored.
args = [self._resolve_executable()]
# Pass --model before --print so agy can parse it as a flag.
# agy >=1.20 supports: agy --model <name> --print <prompt>
if model:
args.extend(["--model", model])
# Inject --add-dir so agy discovers the project workspace when invoked
# from an arbitrary working directory (e.g. the workflow engine's cwd).
# Without this agy falls back to its own scratch directory and cannot
# locate .agents/skills/, reporting "no active workspace".
if project_root is not None:
args.extend(["--add-dir", str(project_root)])
# Honor SPECKIT_INTEGRATION_AGY_EXTRA_ARGS (operator-supplied flags).
# These MUST be inserted before --print because agy treats every token
# that follows --print as part of the prompt, not as CLI flags.
self._apply_extra_args_env_var(args)
args.extend(["--print", prompt])
return args

def dispatch_command(
self,
command_name: str,
args: str = "",
*,
project_root: Path | None = None,
model: str | None = None,
timeout: int = 600,
stream: bool = True,
) -> dict[str, Any]:
"""Dispatch a Spec Kit command through agy.

Overrides the base implementation solely to thread *project_root*
into :meth:`build_exec_args` as ``--add-dir``. Without this,
``agy`` is launched without a workspace hint and cannot locate
``.agents/skills/``, falling back to its own scratch directory
and reporting *"no active workspace"*.

All other behaviour is identical to
:meth:`~specify_cli.integrations.base.IntegrationBase.dispatch_command`.
"""
import shutil
import subprocess

prompt = self.build_command_invocation(command_name, args)
exec_args = self.build_exec_args(
prompt,
model=model,
output_json=not stream,
project_root=project_root,
)

if exec_args is None: # pragma: no cover — build_exec_args always returns a list
raise NotImplementedError(
f"Integration {self.key!r} does not support CLI dispatch. "
f"Override build_exec_args() to enable it."
)

resolved = shutil.which(exec_args[0])
if resolved:
exec_args = [resolved, *exec_args[1:]]

cwd = str(project_root) if project_root else None

if stream:
try:
result = subprocess.run(exec_args, text=True, cwd=cwd)
except KeyboardInterrupt:
return {"exit_code": 130, "stdout": "", "stderr": "Interrupted by user"}
return {"exit_code": result.returncode, "stdout": "", "stderr": ""}

result = subprocess.run(
exec_args,
capture_output=True,
text=True,
cwd=cwd,
timeout=timeout,
)
return {
"exit_code": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
}

def setup(
self,
project_root: Path,
Expand Down
74 changes: 63 additions & 11 deletions tests/integrations/test_integration_agy.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,19 @@ def test_build_exec_args_returns_print_command(self):
result = i.build_exec_args("describe my feature")
assert result == ["agy", "--print", "describe my feature"]

def test_build_exec_args_ignores_model(self):
"""agy does not support --model; model param must be ignored."""
def test_build_exec_args_honors_model(self):
"""agy >=1.20 supports --model; it must be prepended before --print."""
from specify_cli.integrations import get_integration
i = get_integration("agy")
result = i.build_exec_args("my prompt", model="gemini-pro")
assert result == ["agy", "--print", "my prompt"]
assert result == ["agy", "--model", "gemini-pro", "--print", "my prompt"]

def test_build_exec_args_no_model_flag_when_model_is_none(self):
"""When model is None, no --model flag should appear in the args."""
from specify_cli.integrations import get_integration
i = get_integration("agy")
result = i.build_exec_args("my prompt", model=None)
assert "--model" not in result

def test_build_exec_args_ignores_output_json(self):
"""agy does not support JSON output; output_json param must be ignored."""
Expand All @@ -81,19 +88,63 @@ def test_build_exec_args_ignores_output_json(self):
result = i.build_exec_args("my prompt", output_json=False)
assert result == ["agy", "--print", "my prompt"]

def test_build_exec_args_honors_extra_args(self, monkeypatch):
"""SPECKIT_INTEGRATION_AGY_EXTRA_ARGS must be appended after the prompt.
def test_build_exec_args_extra_args_before_print(self, monkeypatch):
"""SPECKIT_INTEGRATION_AGY_EXTRA_ARGS must be inserted BEFORE --print.

agy treats every token after --print as part of the prompt string,
not as CLI flags. Appending flags after --print (the previous
behaviour) caused them to be silently absorbed into the prompt.

agy previously skipped _apply_extra_args_env_var entirely, so the
documented per-integration extra-args hook was silently ignored
(same class as the merged cursor-agent fix #3265).
See issue #4480.
"""
from specify_cli.integrations import get_integration
monkeypatch.setenv("SPECKIT_INTEGRATION_AGY_EXTRA_ARGS", "--verbose")
i = get_integration("agy")
assert i.build_exec_args("my prompt") == [
"agy", "--print", "my prompt", "--verbose",
]
result = i.build_exec_args("my prompt")
# --verbose must appear before --print
assert result.index("--verbose") < result.index("--print")
assert result == ["agy", "--verbose", "--print", "my prompt"]

def test_build_exec_args_add_dir_for_workspace(self, tmp_path):
"""--add-dir <project_root> must be injected before --print when project_root is given.

Without --add-dir, agy cannot locate .agents/skills/ and reports
'no active workspace', ignoring installed Spec Kit skills entirely.

See issue #4480.
"""
from specify_cli.integrations import get_integration
i = get_integration("agy")
result = i.build_exec_args("my prompt", project_root=tmp_path)
assert "--add-dir" in result
add_dir_idx = result.index("--add-dir")
print_idx = result.index("--print")
assert add_dir_idx < print_idx, "--add-dir must come before --print"
assert result[add_dir_idx + 1] == str(tmp_path)

def test_build_exec_args_no_add_dir_when_project_root_is_none(self):
"""When project_root is None, --add-dir must not appear."""
from specify_cli.integrations import get_integration
i = get_integration("agy")
result = i.build_exec_args("my prompt", project_root=None)
assert "--add-dir" not in result

def test_build_exec_args_combined_flag_order(self, monkeypatch, tmp_path):
"""When model, project_root, and EXTRA_ARGS are all set, order must be:
agy --model <m> --add-dir <d> <extra-args> --print <prompt>.
"""
from specify_cli.integrations import get_integration
monkeypatch.setenv("SPECKIT_INTEGRATION_AGY_EXTRA_ARGS", "--dangerously-skip-permissions")
i = get_integration("agy")
result = i.build_exec_args("hello", model="claude-3", project_root=tmp_path)
assert result[0] == "agy"
assert "--model" in result
assert "--add-dir" in result
assert "--dangerously-skip-permissions" in result
print_idx = result.index("--print")
for flag in ("--model", "--add-dir", "--dangerously-skip-permissions"):
assert result.index(flag) < print_idx, f"{flag} must appear before --print"
assert result[-1] == "hello"

def test_build_exec_args_honors_executable_override(self, monkeypatch):
from specify_cli.integrations import get_integration
Expand All @@ -102,6 +153,7 @@ def test_build_exec_args_honors_executable_override(self, monkeypatch):
assert i.build_exec_args("my prompt")[0] == "/custom/agy"



class TestAgyHookCommandNote:
"""Verify dot-to-hyphen normalization note is injected into hook sections."""

Expand Down