Skip to content

Commit f50839a

Browse files
doquanghuyclaude
andauthored
feat(integrations): support SPECIFY_<KEY>_EXTRA_ARGS env var for agent subprocess flags (#2596)
* feat(integrations): support SPECIFY_<KEY>_EXTRA_ARGS env var for agent subprocess flags Read a per-integration env var (SPECIFY_<KEY>_EXTRA_ARGS) inside `SkillsIntegration.build_exec_args`, `MarkdownIntegration.build_exec_args`, and `TomlIntegration.build_exec_args` and append the parsed flags to the spawned agent's argv, gated per integration key. Operators can now opt into extra CLI flags (e.g. `SPECIFY_CLAUDE_EXTRA_ARGS=--dangerously-skip-permissions`) without modifying any SKILL or workflow YAML. Useful in CI / non-interactive contexts where the spawned `<agent> -p ...` would otherwise hang on an internal permission or input prompt invisible to the parent `specify workflow run` process. Key normalization: `kiro-cli` → `SPECIFY_KIRO_CLI_EXTRA_ARGS` (hyphen replaced with underscore, then uppercased). Default (env var unset or whitespace-only) is byte-identical to previous behaviour. Extra args are inserted between `-p prompt` and the model / output-format flags so they cannot clobber canonical Spec Kit args. Implementation: a single helper `IntegrationBase._apply_extra_args_env_var` encapsulates the env-var read + shlex parsing; each of the three concrete `build_exec_args` implementations calls it. Closes #2595 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(integrations): wire SPECIFY_<KEY>_EXTRA_ARGS into Codex/Devin/Opencode/Copilot Four integrations override `build_exec_args` and were silently ignoring the env-var hook introduced in the previous commit: - CodexIntegration (`codex exec ...`) - DevinIntegration (`devin -p ...`) - OpencodeIntegration (`opencode run ...`) - CopilotIntegration (`copilot -p ...`) Each now calls `self._apply_extra_args_env_var(args)` between the base argv and the canonical Spec Kit flags (matching the placement in `MarkdownIntegration`, `TomlIntegration`, and `SkillsIntegration`), so operator-injected flags cannot clobber `--model` / `--output-format` / `--json`. Adds 4 parameterized override-integration tests locking the wiring per agent. Also cleans up an inline `__import__("os").environ` in the fixture to a top-of-file `import os`. Drive-by typing fix: guard `self.registrar_config.get(...)` in `CopilotIntegration.add_commands` against the `None` case, matching the pattern already used in `base.py` for the same access. Addresses Copilot review on #2596. * fix(integrations): apply Opencode extra-args before prompt-derived --command When the Opencode prompt starts with `/`, `build_exec_args` injects `--command <X>` derived from the prompt. The previous placement of `self._apply_extra_args_env_var(args)` appended operator-injected args AFTER that `--command`, so a user setting `SPECIFY_OPENCODE_EXTRA_ARGS="--command override"` could redirect the command under typical last-wins repeated-flag CLI semantics. Move the hook to immediately after `args = [self.key, "run"]`, before the prompt-parsing block. The operator's `--command override` (if any) now precedes the Spec Kit-derived `--command speckit`, so the canonical choice wins. Adds `test_opencode_extra_args_cannot_clobber_prompt_derived_command` locking the ordering. Also corrects the module docstring to describe the hook as living in `IntegrationBase` (not `SkillsIntegration`) and to acknowledge that this file covers Codex/Devin/Opencode/Copilot in addition to SkillsIntegration stubs. Addresses Copilot review on #2596. * fix(integrations): honour SPECIFY_COPILOT_EXTRA_ARGS in dispatch_command `CopilotIntegration` is the only integration that overrides `dispatch_command` — it builds `cli_args` inline rather than going through `build_exec_args`. The previous commit wired `_apply_extra_args_env_var` into `build_exec_args` but workflow execution calls `dispatch_command`, so `SPECIFY_COPILOT_EXTRA_ARGS` was silently ignored at runtime. Add the hook in `dispatch_command` immediately after `cli_args = ["copilot", "-p", prompt]`, mirroring the placement in `build_exec_args` (between `-p prompt` and the canonical `--agent` / `--yolo` / `--model` / `--output-format` flags). `IntegrationBase.dispatch_command` already delegates to `build_exec_args`, so Codex, Devin, and Opencode continue to honour their respective env vars through inheritance — no further changes needed for them. Adds two end-to-end tests that monkeypatch `subprocess.run` and assert the env-var args reach the executed argv: - `test_copilot_dispatch_command_includes_extra_args` locks the bypass fix on the overridden path. - `test_codex_dispatch_command_includes_extra_args` locks the inherited-base-dispatch path for the other three integrations. Addresses Copilot review on #2596. * refactor(integrations): rename env var to SPECIFY_INTEGRATION_<KEY>_EXTRA_ARGS Per maintainer request: scope the operator-injected env var to the integration subsystem by prepending `INTEGRATION_` to the key segment, so it does not collide with other Spec Kit env-var namespaces. Renames everywhere it appears: - Helper `IntegrationBase._apply_extra_args_env_var` env_name format and docstring (`base.py`). - Inline comment in `CopilotIntegration.dispatch_command`. - All `monkeypatch.setenv(...)` calls, docstrings, and the autouse fixture's scope filter in `tests/integrations/test_extra_args.py`. No behaviour change beyond the variable name. Default (env var unset) still byte-identical to before this PR. Addresses review on #2596. * fix(integrations): raise actionable error on malformed EXTRA_ARGS quoting Wrap `shlex.split` in `_apply_extra_args_env_var` so an unmatched quote in `SPECIFY_INTEGRATION_<KEY>_EXTRA_ARGS` surfaces a clear `ValueError` naming the offending env var and showing the invalid value, instead of crashing workflow dispatch with a bare shlex traceback. Adds a new test locking the actionable error path. Addresses Copilot review feedback on #2596. * test(integrations): use `_copilot_executable()` in Copilot extra-args test `test_copilot_integration_honours_extra_args` hardcoded `"copilot"` in the expected argv, but `CopilotIntegration.build_exec_args` calls `_copilot_executable()` which returns `"copilot.cmd"` on Windows (`os.name == "nt"`). The test passed on Linux/macOS and failed on all three Windows-latest matrix entries. Resolve by importing `_copilot_executable` alongside `CopilotIntegration` and using it as the first expected argv token. The companion `test_copilot_dispatch_command_includes_extra_args` already uses `index()` lookups rather than full-argv equality so it was unaffected. * fix(integrations): couple Codex executable to self.key + cover base classes Two Copilot findings on the latest pass: 1. `CodexIntegration.build_exec_args` hardcoded the executable name as the literal `"codex"` while the env-var lookup derives from `self.key`. The two should stay coupled (matching Devin/Opencode, which both use `self.key` already). Replace the literal with `self.key` so the argv and env-var scoping cannot drift. 2. `tests/integrations/test_extra_args.py` covered the `SkillsIntegration` mechanism (via stubs near the top) and the four `build_exec_args` overrides (Codex/Devin/Opencode/Copilot) end-to-end, but did not exercise the `MarkdownIntegration` or `TomlIntegration` base implementations directly. Add bare `_MarkdownAgentStub` and `_TomlAgentStub` test stubs and a test each — the most common integration pattern (Amp, Auggie, Generic, Gemini, Tabnine, …) inherits without overriding, so the base wiring is now locked. Full suite: 3011 passed (was 3009), 40 skipped, no regressions. Addresses Copilot review feedback on #2596. * fix(integrations): rename env var to SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS Renames the env-var hook prefix from `SPECIFY_INTEGRATION_*` to `SPECKIT_INTEGRATION_*` to match the established codebase convention for integration-subsystem env vars (`SPECKIT_INTEGRATION_CATALOG_URL` in `integrations/catalog.py`, `SPECKIT_COPILOT_ALLOW_ALL_TOOLS` in `integrations/copilot/__init__.py`). The `SPECIFY_*` prefix is reserved for user-facing feature-resolution variables (`SPECIFY_FEATURE`, `SPECIFY_FEATURE_DIRECTORY`); reusing it for integration-subsystem scoping would introduce a second integration namespace under a different prefix, confusing operators who already set `SPECKIT_INTEGRATION_CATALOG_URL`. Also reverts the unrelated defensive `arg_placeholder` / `registrar_config is None` guard in `CopilotIntegration.setup_skills_mode` — it was a drive-by pyright cleanup mixed into this PR. Every concrete integration sets `registrar_config` so the guard never fires in practice; the typing issue belongs in a focused follow-up rather than this env-var-hook PR. Updates everywhere the old prefix appeared: - `IntegrationBase._apply_extra_args_env_var` helper + docstring - `CopilotIntegration.dispatch_command` inline comment - All `monkeypatch.setenv(...)` calls in `tests/integrations/test_extra_args.py` - The autouse fixture scope filter - Test module docstring Full suite: 3011 passed, 40 skipped, no regressions. Addresses Copilot review feedback on #2596. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ae96f97 commit f50839a

6 files changed

Lines changed: 540 additions & 1 deletion

File tree

src/specify_cli/integrations/base.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
from __future__ import annotations
1515

16+
import os
1617
import re
18+
import shlex
1719
import shutil
1820
from abc import ABC
1921
from dataclasses import dataclass
@@ -144,6 +146,43 @@ def build_exec_args(
144146
"""
145147
return None
146148

149+
def _apply_extra_args_env_var(self, args: list[str]) -> None:
150+
"""Append `SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS` env-var value to *args*.
151+
152+
Operators can inject extra CLI flags into the spawned agent
153+
subprocess by setting an env var named for the integration key,
154+
e.g. `SPECKIT_INTEGRATION_CLAUDE_EXTRA_ARGS="--dangerously-skip-permissions"`.
155+
The `INTEGRATION` segment scopes the variable to this subsystem
156+
so it does not collide with other Spec Kit env-var namespaces.
157+
Hyphens in the integration key are replaced with underscores
158+
and the key is uppercased
159+
(e.g. `kiro-cli` → `SPECKIT_INTEGRATION_KIRO_CLI_EXTRA_ARGS`).
160+
161+
Useful in CI / non-interactive contexts where the spawned agent
162+
needs flags that change its prompt-handling behaviour.
163+
Default behaviour (env var unset or whitespace-only) is a no-op
164+
— *args* is unchanged. Multi-token values are parsed via
165+
`shlex.split`.
166+
167+
See issue #2595.
168+
"""
169+
env_name = (
170+
f"SPECKIT_INTEGRATION_{self.key.upper().replace('-', '_')}_EXTRA_ARGS"
171+
)
172+
extra = os.environ.get(env_name, "").strip()
173+
if not extra:
174+
return
175+
try:
176+
tokens = shlex.split(extra)
177+
except ValueError as exc:
178+
raise ValueError(
179+
f"{env_name} is not parseable as a POSIX-quoted command line "
180+
f"(value: {extra!r}). shlex reported: {exc}. "
181+
f"Use single or double quotes to group multi-word values, e.g. "
182+
f'{env_name}=\'--flag "value with spaces"\'.'
183+
) from exc
184+
args.extend(tokens)
185+
147186
def build_command_invocation(self, command_name: str, args: str = "") -> str:
148187
"""Build the native slash-command invocation for a Spec Kit command.
149188
@@ -857,6 +896,7 @@ def build_exec_args(
857896
if not self.config or not self.config.get("requires_cli"):
858897
return None
859898
args = [self.key, "-p", prompt]
899+
self._apply_extra_args_env_var(args)
860900
if model:
861901
args.extend(["--model", model])
862902
if output_json:
@@ -944,6 +984,7 @@ def build_exec_args(
944984
if not self.config or not self.config.get("requires_cli"):
945985
return None
946986
args = [self.key, "-p", prompt]
987+
self._apply_extra_args_env_var(args)
947988
if model:
948989
args.extend(["-m", model])
949990
if output_json:
@@ -1362,6 +1403,7 @@ def build_exec_args(
13621403
if not self.config or not self.config.get("requires_cli"):
13631404
return None
13641405
args = [self.key, "-p", prompt]
1406+
self._apply_extra_args_env_var(args)
13651407
if model:
13661408
args.extend(["--model", model])
13671409
if output_json:

src/specify_cli/integrations/codex/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ def build_exec_args(
3737
output_json: bool = True,
3838
) -> list[str] | None:
3939
# Codex uses ``codex exec "prompt"`` for non-interactive mode.
40-
args: list[str] = ["codex", "exec", prompt]
40+
# Use ``self.key`` so the executable name stays coupled to the
41+
# env-var lookup (which also derives from ``self.key``), matching
42+
# the pattern in Devin/Opencode and avoiding drift if the key
43+
# ever changes.
44+
args: list[str] = [self.key, "exec", prompt]
45+
self._apply_extra_args_env_var(args)
4146
if model:
4247
args.extend(["--model", model])
4348
if output_json:

src/specify_cli/integrations/copilot/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def build_exec_args(
149149
# (default: enabled). The deprecated SPECKIT_ALLOW_ALL_TOOLS
150150
# is also honoured as a fallback.
151151
args = [_copilot_executable(), "-p", prompt]
152+
self._apply_extra_args_env_var(args)
152153
if _allow_all():
153154
args.append("--yolo")
154155
if model:
@@ -217,6 +218,11 @@ def dispatch_command(
217218
prompt = args or ""
218219

219220
cli_args = [_copilot_executable(), "-p", prompt]
221+
# Honour SPECKIT_INTEGRATION_COPILOT_EXTRA_ARGS for real workflow
222+
# runs. `dispatch_command` builds cli_args inline rather than
223+
# going through `build_exec_args`, so the hook must be invoked
224+
# here too — otherwise the env var is silently ignored.
225+
self._apply_extra_args_env_var(cli_args)
220226
if not skills_mode:
221227
cli_args.extend(["--agent", agent_name])
222228
if _allow_all():

src/specify_cli/integrations/devin/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def build_exec_args(
4949
kept on the integration for tool detection.
5050
"""
5151
args = [self.key, "-p", prompt]
52+
self._apply_extra_args_env_var(args)
5253
if model:
5354
args.extend(["--model", model])
5455
return args

src/specify_cli/integrations/opencode/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def build_exec_args(
2929
output_json: bool = True,
3030
) -> list[str] | None:
3131
args = [self.key, "run"]
32+
# Apply operator-injected extra args before the prompt-derived
33+
# --command and the canonical --format/-m flags so Spec Kit's
34+
# later appends remain authoritative under repeated-flag CLI
35+
# semantics.
36+
self._apply_extra_args_env_var(args)
3237

3338
message = prompt
3439
if prompt.startswith("/"):

0 commit comments

Comments
 (0)