fix(integrations): dispatch goose commands via goose run (#2416) - #3781
Conversation
`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) <[email protected]>
f8558f8 to
4d7fe7f
Compare
There was a problem hiding this comment.
Pull request overview
Adds Goose CLI dispatch through goose run, mapping Spec Kit commands to installed recipes.
Changes:
- Builds Goose CLI arguments for recipes and free-form prompts.
- Supports models, JSON output, extra arguments, and executable overrides.
- Adds dispatch and recipe-path tests.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/integrations/goose/__init__.py |
Implements Goose command dispatch. |
tests/integrations/test_integration_goose.py |
Tests recipe and prompt argument construction. |
tests/integrations/test_extra_args.py |
Tests environment-based Goose overrides. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Medium
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
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) <[email protected]>
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) <[email protected]>
|
Status summary for the record, since I can't re-request review without push access. All Copilot feedback on this PR is addressed and both threads are resolved:
Copilot re-reviewed after that push (2026-07-30T18:36Z) and reported 0 new comments across all 3 changed files. Happy to keep iterating if you'd like the extra-args validation added at the shared helper instead. |
|
Thank you! |
* main: (27 commits) Add SpecAssay bundle to community catalog (github#4125) chore: release 0.16.4, begin 0.16.5.dev0 development (github#4124) Add SpecAssay preset to community catalog (github#4123) Update Intake Authoring Governance preset to v0.3.1 (github#4121) Update Superspec extension to v1.0.2 (github#4120) fix(taskstoissues): widen task-ID regex to match IDs longer than 3 digits (github#4101) Add Architecture Governance extension to community catalog (github#4122) fix(workflows): validate non-string step types (github#4111) Harden community submission workflow output allowlists (github#4103) chore(deps): bump github/codeql-action (init + analyze) from 4.37.5 to 4.37.6 (github#4114) Add SpecAssay Check extension to community catalog (github#4113) fix(integrations): dispatch goose commands via `goose run` (github#2416) (github#3781) fix(powershell): stop Out-Null swallowing the AVAILABLE_DOCS status lines (github#3891) fix: remove TOCTOU race in RunState.load (github#3839) fix: decode the zipped manifest as UTF-8 before parsing (github#3958) Update Agent Parity Governance preset to v0.4.2 (github#4110) fix: log progress tracker refresh errors instead of silently swallowing (github#3975) [extension] Add SpecJudge extension to community catalog (github#4079) fix(bundler): read the authoritative `default_integration` field, not only its legacy aliases (github#3880) fix(auth): treat exact host patterns literally (github#4108) ...
Addresses the Goose item in #2416. (Not
Fixes, since that issue also tracks a broader flag audit — see Scope below.)Problem
YamlIntegrationnever overrodebuild_exec_args(), soGooseIntegrationinherited theIntegrationBaseno-op that returnsNone. Callers readNoneas "this CLI is unavailable", so a workflow command/prompt step targeting Goose reportedCLI not found or not installed— even withgoosepresent onPATH.Reproduced with the CLI on
PATH(shutil.whichstubbed to a real path,subprocess.runstubbed), using the samecommand: speckit.specifyform the shippedworkflows/speckit/workflow.ymluses:The message is simply false — goose is installed.
CommandStepswallows theNotImplementedErrorfromdispatch_commandand reports the not-installed result;PromptStepsilently no-ops on a falsyexec_args.Fix
Implement
build_exec_args()for Goose. Per the goose CLI docs there is no-pflag; the non-interactive entry point isgoose run:-t, --text <TEXT>--recipe <RECIPE_FILE_NAME>--params <KEY=VALUE>--model/--output-format <FORMAT>text|json|stream-jsonSpec Kit already installs its commands as Goose recipes under
.goose/recipes/, each declaring an optionalargsstring parameter (enforced today bytest_setup_declares_args_parameter_for_args_prompt), so a/speckit.<name> <rest>invocation maps exactly onto--recipe <path> --params args=<rest>. This mirrorsOpencodeIntegration, which maps the same leading slash-command onto opencode's native--command.Anti-drift: the recipe path is derived from the same two sources
setup()uses —config["folder"]+config["commands_subdir"](ascommands_dest()does) andcommand_filename()— rather thanregistrar_config["extension"], so the dispatch target cannot diverge from the file that was actually installed. A test runssetup()and asserts the resolved--recipepath is a real file on disk.Note on #2424
#2424 proposes the opposite —
exec_mode = "none"for goose, on the grounds that it is a "recipe-based workflow". Respectfully, recipe-based execution is precisely whatgoose runis built for:--recipeand--params KEY=VALUEare first-class flags, and Spec Kit already generates recipes declaring the matchingargsparameter, so the mapping is exact rather than approximate. Happy to defer if you prefer that direction — this PR is deliberately narrow so either can land independently.Verification
tests/integrations/test_integration_goose.py: 35 passed (28 pre-existing untouched).tests/test_agent_config_consistency.py: 27 passed —requires_cliis deliberately not touched.uvx [email protected] checkclean.-t/--text,--recipe,--params,--model,--output-format; confirmed no-p).Scope
Only the Goose dispatch gap. The issue's headline
opencodeitem was already fixed by c079b2c, and the broader "audit CLI flags for 11+ integrations" / declarative-exec_*refactor is intentionally left out — that is a larger change needing a maintainer design call.AI-assisted: authored with Claude Code. I reproduced the false "CLI not found" result on
main, verified the goose flags against the vendor docs, and added the anti-drift test tying dispatch to whatsetup()writes.