feat: allow overriding default init integration via SPECKIT_INTEGRATION_DEFAULT (#3952)

* feat: allow overriding default init integration via SPECKIT_INTEGRATION_DEFAULT

Resolve the non-interactive/init default integration from the
SPECKIT_INTEGRATION_DEFAULT environment variable, fitting the existing
SPECKIT_INTEGRATION_* namespace. Falls back to the hardcoded "copilot"
default when unset, and warns to stderr (rather than silently falling
back) when the value is not a registered integration key.

Wires the resolver into specify init (interactive prompt default and
non-interactive fallback), the init workflow step, and the bundle init
default. Adds unit and CLI tests and documents the variable.

Closes #3939

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0

* test: cover env-var default wiring for picker, workflow step, and bundle

Address PR review: add regression tests so each SPECKIT_INTEGRATION_DEFAULT
wiring site cannot silently revert to the hardcoded constant.

- init.py: interactive picker receives the resolved key as default_key.
- workflow init step: no step/workflow default + env var drives output
  integration and argv.
- bundle _resolve_init_integration: env-var default applies when unspecified,
  while explicit override and manifest-declared integration still win.

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0
This commit is contained in:
Manfred Riem
2026-08-03 12:20:49 -05:00
committed by GitHub
co-authored by Copilot App
parent 4751777a38
commit e9ffc9d8e7
10 changed files with 192 additions and 10 deletions
+2
View File
@@ -77,7 +77,9 @@ from ._version import (
from ._agent_config import (
AGENT_CONFIG as AGENT_CONFIG,
DEFAULT_INIT_INTEGRATION as DEFAULT_INIT_INTEGRATION,
DEFAULT_INIT_INTEGRATION_ENV_VAR as DEFAULT_INIT_INTEGRATION_ENV_VAR,
SCRIPT_TYPE_CHOICES as SCRIPT_TYPE_CHOICES,
resolve_default_init_integration as resolve_default_init_integration,
)
from ._init_options import (
INIT_OPTIONS_FILE as INIT_OPTIONS_FILE,
+32
View File
@@ -1,6 +1,8 @@
"""Agent configuration constants derived from the integration registry."""
from __future__ import annotations
import os
import sys
from typing import Any
@@ -17,6 +19,36 @@ AGENT_CONFIG: dict[str, dict[str, Any]] = _build_agent_config()
DEFAULT_INIT_INTEGRATION = "copilot"
#: Environment variable used to override the fallback integration that
#: ``specify init`` selects in non-interactive sessions. Follows the existing
#: ``SPECKIT_INTEGRATION_*`` namespace (see ``SPECKIT_INTEGRATION_<KEY>_EXECUTABLE``
#: and ``SPECKIT_INTEGRATION_CATALOG_URL``).
DEFAULT_INIT_INTEGRATION_ENV_VAR = "SPECKIT_INTEGRATION_DEFAULT"
def resolve_default_init_integration() -> str:
"""Return the default init integration, honoring an env-var override.
Reads :data:`DEFAULT_INIT_INTEGRATION_ENV_VAR`
(``SPECKIT_INTEGRATION_DEFAULT``). When it names a registered integration
key, that key is returned; otherwise the hardcoded
:data:`DEFAULT_INIT_INTEGRATION` (``"copilot"``) is used. An invalid value
emits a warning to stderr rather than silently falling back, so operators
can tell a typo from an intentional default.
"""
override = (os.environ.get(DEFAULT_INIT_INTEGRATION_ENV_VAR) or "").strip()
if not override:
return DEFAULT_INIT_INTEGRATION
if override in AGENT_CONFIG:
return override
print(
f"Warning: {DEFAULT_INIT_INTEGRATION_ENV_VAR}='{override}' is not a "
f"recognized integration; falling back to '{DEFAULT_INIT_INTEGRATION}'. "
f"Choose from: {', '.join(sorted(AGENT_CONFIG.keys()))}.",
file=sys.stderr,
)
return DEFAULT_INIT_INTEGRATION
SCRIPT_TYPE_CHOICES: dict[str, str] = {
"sh": "POSIX Shell (bash/zsh)",
"ps": "PowerShell",
+2 -2
View File
@@ -131,13 +131,13 @@ def _run_init(integration: str, *, script_type: str, offline: bool = False) -> N
def _resolve_init_integration(override: str | None, manifest) -> str:
"""Precedence (FR-013): explicit override → bundle-declared → default."""
from ..._agent_config import DEFAULT_INIT_INTEGRATION
from ..._agent_config import resolve_default_init_integration
if override:
return override
if manifest is not None and manifest.integration is not None:
return manifest.integration.id
return DEFAULT_INIT_INTEGRATION
return resolve_default_init_integration()
# ===== Consume =====
+5 -4
View File
@@ -14,8 +14,8 @@ from rich.panel import Panel
from .._agent_config import (
AGENT_CONFIG,
DEFAULT_INIT_INTEGRATION,
SCRIPT_TYPE_CHOICES,
resolve_default_init_integration,
)
from .._assets import (
_locate_bundled_preset,
@@ -466,17 +466,18 @@ def register(app: typer.Typer) -> None:
raise typer.Exit(1)
selected_ai = integration
elif not _stdin_is_interactive():
default_integration = resolve_default_init_integration()
console.print(
f"[dim]Non-interactive session detected: defaulting to '{DEFAULT_INIT_INTEGRATION}'. "
f"[dim]Non-interactive session detected: defaulting to '{default_integration}'. "
"Use --integration to choose a different agent.[/dim]"
)
selected_ai = DEFAULT_INIT_INTEGRATION
selected_ai = default_integration
else:
ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()}
selected_ai = select_with_arrows(
ai_choices,
"Choose your coding agent integration:",
DEFAULT_INIT_INTEGRATION,
resolve_default_init_integration(),
)
if not integration:
@@ -11,7 +11,10 @@ from __future__ import annotations
import os
from typing import Any
from specify_cli._agent_config import DEFAULT_INIT_INTEGRATION, SCRIPT_TYPE_CHOICES
from specify_cli._agent_config import (
SCRIPT_TYPE_CHOICES,
resolve_default_init_integration,
)
from specify_cli.workflows.base import StepBase, StepContext, StepResult, StepStatus
from specify_cli.workflows.expressions import evaluate_expression
@@ -54,7 +57,8 @@ class InitStep(StepBase):
Initialize in the target directory instead of creating a new one.
``integration``
Integration key (e.g. ``copilot``). Defaults to the workflow's
default integration, then to ``DEFAULT_INIT_INTEGRATION``.
default integration, then to the resolved default init integration
(``SPECKIT_INTEGRATION_DEFAULT`` env var, else ``copilot``).
``integration_options``
Extra options for the integration (e.g. ``"--skills"`` or
``"--commands-dir .myagent/cmds"``).
@@ -81,7 +85,7 @@ class InitStep(StepBase):
# Apply the same default that specify init uses in non-interactive mode
# so that output.integration reflects the actual integration used.
if not integration:
integration = DEFAULT_INIT_INTEGRATION
integration = resolve_default_init_integration()
integration_options = self._resolve(
config.get("integration_options"), context