Skip to content

Commit 5bf5cb0

Browse files
hmohammadiclaude
andcommitted
fix(integrations): dispatch Forge via prompt flag only
Fixes #4666. `ForgeIntegration` never overrode `build_exec_args()`, so it inherited `MarkdownIntegration`'s generic `-p <prompt> --model <model> --output-format json`. Forge accepts `-p/--prompt`, but `--model` and `--output-format` do not exist in its CLI, so every workflow `command:`/`prompt:` step targeting Forge aborted at argument parsing with `error: unexpected argument '--model' found` (exit 2) before the agent ever ran. Unlike the Amp case, both inherited extras are invalid, so both dispatch paths broke: `stream=False` appends `--output-format json`, while `stream=True` still appends `--model` whenever a model is configured. The only surviving combination was `stream=True` with no model, which reduces to plain `forge -p <prompt>`. `model` is deliberately dropped rather than remapped: Forge exposes no model-selection flag. Model choice is a persisted setting (`forge config set model`), and `--agent` takes an agent ID, not a model identifier, so forwarding the caller's model onto it would silently select the wrong thing. `output_json` is dropped for the same reason: Forge's machine-readable `--porcelain` exists only on certain subcommands, not on the top-level prompt invocation. Extra args from `SPECKIT_INTEGRATION_FORGE_EXTRA_ARGS` are applied before `-p`, matching the opencode / goose / codex ordering; Forge parses its global flags ahead of the prompt flag. Same fix shape as the one-off overrides for Amp (#4581), opencode (#2409) and goose (#3781). Part of the audit in #2416. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent b9e7389 commit 5bf5cb0

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

‎src/specify_cli/integrations/forge/__init__.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from __future__ import annotations
1111

12+
from collections.abc import Mapping, Sequence
1213
from pathlib import Path
1314
from typing import Any
1415

@@ -103,6 +104,36 @@ def build_command_invocation(self, command_name: str, args: str = "") -> str:
103104
invocation = f"{invocation} {args}"
104105
return invocation
105106

107+
def build_exec_args(
108+
self,
109+
prompt: str,
110+
*,
111+
model: str | None = None,
112+
output_json: bool = True,
113+
integration_args: Sequence[str] | None = None,
114+
integration_options: Mapping[str, Any] | None = None,
115+
project_root: Path | None = None,
116+
) -> list[str] | None:
117+
self.validate_runtime_config(integration_args, integration_options)
118+
args = [self._resolve_executable()]
119+
# Operator-injected extra args go before -p: Forge parses its global
120+
# flags ahead of the prompt flag, matching the opencode/goose/codex
121+
# ordering.
122+
self._apply_extra_args_env_var(args)
123+
124+
args.extend(["-p", prompt])
125+
126+
# `model` is deliberately dropped: Forge has no model-selection flag.
127+
# Model choice is a persisted setting (`forge config set model`), and
128+
# `--agent` takes an agent ID rather than a model identifier, so
129+
# forwarding the caller's model onto it would silently select the
130+
# wrong thing.
131+
#
132+
# `output_json` is likewise dropped: Forge has no `--output-format`.
133+
# Its machine-readable `--porcelain` exists only on certain
134+
# subcommands, not on the top-level prompt invocation.
135+
return args
136+
106137
def setup(
107138
self,
108139
project_root: Path,

‎tests/integrations/test_integration_forge.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,74 @@ def test_name_field_uses_hyphenated_format(self, tmp_path):
281281
f"{cmd_file.name} name field should start with 'speckit-': {name_value}"
282282
)
283283

284+
def test_build_exec_args_uses_prompt_flag(self):
285+
"""Forge dispatches through ``-p``, with no extra flags appended.
286+
287+
Forge accepts ``-p/--prompt``, but the inherited ``--model`` and
288+
``--output-format`` do not exist in its CLI; sending them aborts the
289+
run at argument parsing with ``unexpected argument`` (#4666).
290+
"""
291+
integration = get_integration("forge")
292+
293+
args = integration.build_exec_args(
294+
"/speckit-specify build a login page",
295+
model="gpt-4o",
296+
output_json=True,
297+
)
298+
299+
assert args == [
300+
"forge",
301+
"-p",
302+
"/speckit-specify build a login page",
303+
]
304+
305+
def test_build_exec_args_omits_output_format(self):
306+
"""Forge has no ``--output-format``; requesting JSON must not add one."""
307+
integration = get_integration("forge")
308+
309+
args = integration.build_exec_args("/speckit-plan add OAuth", output_json=True)
310+
311+
assert args == ["forge", "-p", "/speckit-plan add OAuth"]
312+
assert "--output-format" not in args
313+
assert "json" not in args
314+
315+
def test_build_exec_args_omits_model_flag(self):
316+
"""Forge exposes no model-selection flag, so ``model`` is not forwarded.
317+
318+
Model choice is a persisted setting (``forge config set model``).
319+
``--agent`` takes an agent ID, not a model identifier, so remapping the
320+
caller's model onto it would select the wrong thing.
321+
"""
322+
integration = get_integration("forge")
323+
324+
args = integration.build_exec_args(
325+
"explain this repository",
326+
model="gpt-4o",
327+
output_json=False,
328+
)
329+
330+
assert args == ["forge", "-p", "explain this repository"]
331+
assert "--model" not in args
332+
assert "gpt-4o" not in args
333+
334+
def test_build_exec_args_applies_extra_args_before_prompt(self, monkeypatch):
335+
"""Operator-injected flags precede ``-p`` so they stay global.
336+
337+
Forge parses its global flags ahead of ``-p`` (``forge --verbose -p x``
338+
is accepted), matching the opencode / goose / codex ordering.
339+
"""
340+
monkeypatch.setenv("SPECKIT_INTEGRATION_FORGE_EXTRA_ARGS", "--verbose")
341+
integration = get_integration("forge")
342+
343+
args = integration.build_exec_args("check the build", output_json=True)
344+
345+
assert args == [
346+
"forge",
347+
"--verbose",
348+
"-p",
349+
"check the build",
350+
]
351+
284352

285353
class TestForgeCommandRegistrar:
286354
"""Test CommandRegistrar's Forge-specific name formatting."""

0 commit comments

Comments
 (0)