mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
* fix(integrations): dispatch goose commands via `goose run` (#2416) `YamlIntegration` never overrode `build_exec_args()`, so `GooseIntegration` inherited the `IntegrationBase` no-op that returns `None`. Callers read `None` as "this CLI is unavailable", so every workflow command/prompt step targeting Goose reported `CLI not found or not installed` even with `goose` on PATH. Reproduced with the agent CLI present on PATH (shutil.which stubbed to a real path, subprocess.run stubbed): amp -> completed argv=['amp', '-p', '/speckit.specify'] opencode -> completed argv=['opencode', 'run', '--command', 'speckit.specify'] goose -> FAILED "integration 'goose' CLI not found or not installed" Implement `build_exec_args()` for Goose. Per the goose CLI docs there is no `-p` flag; the non-interactive entry point is `goose run`, which takes `-t/--text` for free-form text, `--recipe` for a stored recipe, `--params KEY=VALUE` for recipe parameters, plus `--model` and `--output-format`. Spec Kit installs its commands as Goose *recipes* under `.goose/recipes/`, each declaring an optional `args` parameter (already enforced by test_setup_declares_args_parameter_for_args_prompt), so a `/speckit.<name> <rest>` invocation maps exactly onto `--recipe <path> --params args=<rest>`. This mirrors `OpencodeIntegration`, which maps the same leading slash-command onto opencode's `--command`. The recipe path is derived from the same two sources `setup()` uses -- `config["folder"]` + `config["commands_subdir"]` and `command_filename()` -- so the dispatch target cannot drift from the installed file; a test asserts the resolved `--recipe` path exists after `setup()`. Dotted extension commands (`speckit.git.commit`) round-trip. Extra args are applied before the canonical flags so Spec Kit's selection stays authoritative, matching opencode. No behaviour change for other integrations, and `requires_cli` is untouched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(goose): only map the speckit. namespace onto --recipe build_exec_args() treated every prompt starting with "/" as a Spec Kit recipe. Because command_filename() unconditionally re-adds the "speckit." prefix, a free-form slash prompt was silently promoted into a recipe run against a file that was never installed: /help -> --recipe .goose/recipes/speckit.help.yaml /plan the sprint -> --recipe .goose/recipes/speckit.plan.yaml /speckit. -> --recipe .goose/recipes/speckit..yaml PromptStep passes arbitrary prompt: strings to build_exec_args, and both /help and /plan are Goose's own session commands, so this is reachable. Unlike opencode's --command or hermes' -s, which hand a bare name to the agent's own resolver, --recipe is a path Spec Kit synthesizes -- so only the namespace it can actually spell may take that branch. Gate the branch on "/speckit." and fall through to -t otherwise. A bare "/speckit." leaves no stem and also falls through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(goose): stop asserting an argv that goose would reject test_goose_extra_args_cannot_clobber_prompt_derived_recipe asserted that a duplicated --recipe is merely reordered, on a "last value wins" premise. That premise is wrong for goose: `goose run` is clap-derive based and --recipe/--model/--output-format are single-value args without args_override_self, so a duplicate makes goose exit with "cannot be used multiple times" whichever side comes first. The test passed in pytest while pinning a command line that cannot run. Replace it with an ordering-parity test that asserts only what Spec Kit actually controls: extra args precede the canonical flags (matching opencode/codex/cursor-agent), and Spec Kit never emits a duplicate single-value flag itself. Verified non-vacuous -- it fails if the extra-args hook is moved after the canonical flags. The ordering comment claimed precedence it cannot deliver; corrected to state positional parity only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2b36f0ce94
commit
b485cd8c1f
@@ -1,5 +1,7 @@
|
||||
"""Goose integration — open source AI agent (Agentic AI Foundation)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..base import YamlIntegration
|
||||
|
||||
|
||||
@@ -18,3 +20,84 @@ class GooseIntegration(YamlIntegration):
|
||||
"args": "{{args}}",
|
||||
"extension": ".yaml",
|
||||
}
|
||||
|
||||
def build_exec_args(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
model: str | None = None,
|
||||
output_json: bool = True,
|
||||
) -> list[str] | None:
|
||||
"""Build CLI arguments for non-interactive ``goose`` execution.
|
||||
|
||||
``YamlIntegration`` never overrode ``build_exec_args()``, so Goose
|
||||
inherited the ``IntegrationBase`` no-op returning ``None``. Callers read
|
||||
``None`` as "this CLI is unavailable", so a workflow command/prompt step
|
||||
targeting Goose reported ``CLI not found or not installed`` even with
|
||||
``goose`` on ``PATH`` (the Goose item in issue #2416).
|
||||
|
||||
``goose`` has no ``-p`` flag; its non-interactive entry point is
|
||||
``goose run``, which takes ``-t/--text`` for free-form text,
|
||||
``--recipe`` for a stored recipe, ``--params KEY=VALUE`` for recipe
|
||||
parameters, plus ``--model`` and ``--output-format``.
|
||||
|
||||
Spec Kit installs its commands as Goose *recipes* under
|
||||
``.goose/recipes/``, each declaring an optional ``args`` string
|
||||
parameter, so a ``/speckit.<name> <rest>`` invocation maps onto
|
||||
``--recipe <path> --params args=<rest>``. Only that namespace is
|
||||
mapped: ``--recipe`` is a *path* Spec Kit synthesizes, unlike
|
||||
opencode's ``--command`` or hermes' ``-s``, which hand a bare name to
|
||||
the agent's own resolver. Any other prompt -- including Goose's own
|
||||
session commands such as ``/help`` or ``/plan`` -- goes to ``-t``.
|
||||
"""
|
||||
args = [self._resolve_executable(), "run"]
|
||||
# Extra args are applied first, matching the opencode / codex /
|
||||
# cursor-agent ordering. Positional parity only, NOT precedence:
|
||||
# ``goose run`` is clap-derive based, and --recipe / --model /
|
||||
# --output-format are single-value args with no ``args_override_self``,
|
||||
# so re-passing any of them through
|
||||
# SPECKIT_INTEGRATION_GOOSE_EXTRA_ARGS makes goose exit with
|
||||
# "cannot be used multiple times" whichever side comes first. The same
|
||||
# is true of goose's boolean flags. Only its ``Vec``-typed args (which
|
||||
# clap infers as ArgAction::Append) may legitimately repeat.
|
||||
self._apply_extra_args_env_var(args)
|
||||
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
if output_json:
|
||||
args.extend(["--output-format", "json"])
|
||||
|
||||
# Only the ``speckit.`` namespace maps to a recipe: this branch
|
||||
# synthesizes a *path*, and ``command_filename()`` can only ever spell
|
||||
# ``speckit.<name>.yaml``. ``PromptStep`` passes arbitrary ``prompt:``
|
||||
# strings here, so other slash text -- including Goose's own session
|
||||
# commands ``/help`` and ``/plan`` -- must reach ``-t`` unchanged.
|
||||
if prompt.startswith("/speckit."):
|
||||
command, _, remainder = prompt[1:].partition(" ")
|
||||
# ``command_filename`` re-adds the ``speckit.`` prefix and the
|
||||
# ``.yaml`` extension, so strip it here; a dotted extension command
|
||||
# (``speckit.git.commit``) round-trips too. A bare ``/speckit.``
|
||||
# leaves no stem and falls through to ``-t``.
|
||||
stem = command[len("speckit."):]
|
||||
if stem:
|
||||
# Derive the recipe path from the same two sources ``setup()``
|
||||
# uses -- ``config["folder"]`` + ``config["commands_subdir"]``
|
||||
# (exactly what ``commands_dest()`` does) and
|
||||
# ``command_filename()`` -- so the dispatch target cannot drift
|
||||
# from the file that was actually installed.
|
||||
folder = (self.config.get("folder") or "").strip("/")
|
||||
subdir = (self.config.get("commands_subdir") or "").strip("/")
|
||||
# Relative, forward-slash path: dispatch runs with
|
||||
# ``cwd=project_root``, and goose accepts a POSIX separator on
|
||||
# every platform (``commands_dest()`` yields backslashes on
|
||||
# win32).
|
||||
parts = [
|
||||
p for p in (folder, subdir, self.command_filename(stem)) if p
|
||||
]
|
||||
args.extend(["--recipe", "/".join(parts)])
|
||||
if remainder.strip():
|
||||
args.extend(["--params", f"args={remainder}"])
|
||||
return args
|
||||
|
||||
args.extend(["-t", prompt])
|
||||
return args
|
||||
|
||||
Reference in New Issue
Block a user