From f4280495039ab62d08609232b441a9b560f75340 Mon Sep 17 00:00:00 2001 From: hmohammadi Date: Mon, 14 Sep 2026 13:07:08 +0100 Subject: [PATCH] fix(integrations): dispatch Amp via execute mode Fixes #4580. `AmpIntegration` never overrode `build_exec_args()`, so it inherited `MarkdownIntegration`'s generic `-p --model --output-format json`. None of those flags exist in the Amp CLI, so every workflow `command:`/`prompt:` step targeting Amp aborted at argument parsing with `error: unknown option '-p'` before the agent ever ran. Amp's non-interactive entry point is `-x/--execute`, and its structured output flag is `--stream-json` (valid only alongside `--execute`), so this dispatches through those. `model` is deliberately dropped rather than remapped: Amp exposes no model-selection flag. `-m/--mode` takes an agent mode (low/medium/high/ ultra or a plugin mode), not a model identifier, so forwarding the caller's model onto it would silently select the wrong thing. Extra args from `SPECKIT_INTEGRATION_AMP_EXTRA_ARGS` are applied before `--execute` because the flag takes the prompt as an optional inline value; appending between the two would consume the prompt as the operator's flag value. Same fix shape as the one-off overrides for opencode (#2409) and goose (#3781). Part of the audit in #2416. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01X8QcZkxX87sSjsfUzBRm9h --- src/specify_cli/integrations/amp/__init__.py | 32 +++++++++ tests/integrations/test_integration_amp.py | 74 ++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/specify_cli/integrations/amp/__init__.py b/src/specify_cli/integrations/amp/__init__.py index 5d9d14250d..2f92f22c01 100644 --- a/src/specify_cli/integrations/amp/__init__.py +++ b/src/specify_cli/integrations/amp/__init__.py @@ -1,5 +1,8 @@ """Amp CLI integration.""" +from collections.abc import Mapping, Sequence +from typing import Any + from ..base import MarkdownIntegration @@ -18,3 +21,32 @@ class AmpIntegration(MarkdownIntegration): "args": "$ARGUMENTS", "extension": ".md", } + + def build_exec_args( + self, + prompt: str, + *, + model: str | None = None, + output_json: bool = True, + integration_args: Sequence[str] | None = None, + integration_options: Mapping[str, Any] | None = None, + ) -> list[str] | None: + self.validate_runtime_config(integration_args, integration_options) + args = [self._resolve_executable()] + # Operator-injected extra args go before --execute: the flag takes the + # prompt as an optional inline value, so anything appended between the + # two would be consumed as the message instead. + self._apply_extra_args_env_var(args) + + args.extend(["--execute", prompt]) + + if output_json: + # Amp's structured output is --stream-json (Claude Code-compatible + # stream JSON), valid only alongside --execute. + args.append("--stream-json") + + # `model` is deliberately dropped: Amp has no model-selection flag. + # `-m/--mode` takes an agent mode (low/medium/high/ultra or a plugin + # mode), not a model identifier, so forwarding the caller's model onto + # it would silently select the wrong thing. + return args diff --git a/tests/integrations/test_integration_amp.py b/tests/integrations/test_integration_amp.py index f0689c21f5..587bf61add 100644 --- a/tests/integrations/test_integration_amp.py +++ b/tests/integrations/test_integration_amp.py @@ -1,5 +1,7 @@ """Tests for AmpIntegration.""" +from specify_cli.integrations import get_integration + from .test_integration_base_markdown import MarkdownIntegrationTests @@ -8,3 +10,75 @@ class TestAmpIntegration(MarkdownIntegrationTests): FOLDER = ".agents/" COMMANDS_SUBDIR = "commands" REGISTRAR_DIR = ".agents/commands" + + def test_build_exec_args_uses_execute_mode(self): + """Amp dispatches through execute mode, not the inherited `-p`. + + The Amp CLI has no `-p`/`--prompt` flag; passing one aborts with + `error: unknown option '-p'` before the agent runs (#4580). + """ + integration = get_integration(self.KEY) + + args = integration.build_exec_args( + "/speckit.specify build a login page", + output_json=False, + ) + + assert args == [ + "amp", + "--execute", + "/speckit.specify build a login page", + ] + assert "-p" not in args + + def test_build_exec_args_requests_stream_json(self): + """`--stream-json` is Amp's structured-output flag, used with --execute.""" + integration = get_integration(self.KEY) + + args = integration.build_exec_args("/speckit.plan add OAuth", output_json=True) + + assert args == [ + "amp", + "--execute", + "/speckit.plan add OAuth", + "--stream-json", + ] + assert "--output-format" not in args + + def test_build_exec_args_omits_model_flag(self): + """Amp exposes no model-selection flag, so `model` is not forwarded. + + `-m/--mode` takes an agent mode (low/medium/high/ultra), not a model + identifier, so remapping the caller's model onto it would be wrong. + """ + integration = get_integration(self.KEY) + + args = integration.build_exec_args( + "explain this repository", + model="gpt-5", + output_json=False, + ) + + assert args == ["amp", "--execute", "explain this repository"] + assert "--model" not in args + assert "-m" not in args + assert "gpt-5" not in args + + def test_build_exec_args_applies_extra_args_before_execute(self, monkeypatch): + """Operator-injected flags precede `--execute` so they stay global. + + `--execute [message]` takes the prompt as an optional inline value, so + injecting between the flag and the prompt would consume the prompt. + """ + monkeypatch.setenv("SPECKIT_INTEGRATION_AMP_EXTRA_ARGS", "--no-notifications") + integration = get_integration(self.KEY) + + args = integration.build_exec_args("check the build", output_json=True) + + assert args == [ + "amp", + "--no-notifications", + "--execute", + "check the build", + "--stream-json", + ]