Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/specify_cli/integrations/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def _update_init_options_for_integration(
project_root: Path,
integration: Any,
script_type: str | None = None,
parsed_options: dict[str, Any] | None = None,
) -> None:
"""Update init-options.json to reflect *integration* as the active one.

Expand All @@ -270,7 +271,17 @@ def _update_init_options_for_integration(
opts["speckit_version"] = _get_speckit_version()
if script_type:
opts["script"] = script_type
if isinstance(integration, SkillsIntegration) or getattr(integration, "_skills_mode", False):
# Skills mode is either intrinsic (SkillsIntegration), set on the instance
# during setup() (_skills_mode), or requested via parsed options (e.g.
# Copilot's --skills, persisted as parsed_options["skills"]). The latter is
# the only signal available on the `use` path, where no setup() runs and a
# fresh integration instance has _skills_mode == False (issue #3550).
skills_mode = (
isinstance(integration, SkillsIntegration)
or getattr(integration, "_skills_mode", False)
or bool((parsed_options or {}).get("skills"))
)
if skills_mode:
opts["ai_skills"] = True
else:
opts.pop("ai_skills", None)
Expand Down Expand Up @@ -326,7 +337,9 @@ def _set_default_integration(
) from exc

_write_integration_json(project_root, key, installed_keys, settings)
_update_init_options_for_integration(project_root, integration, script_type=resolved_script)
_update_init_options_for_integration(
project_root, integration, script_type=resolved_script, parsed_options=parsed_options
)


def _set_default_integration_or_exit(*args: Any, **kwargs: Any) -> None:
Expand Down
37 changes: 37 additions & 0 deletions tests/integrations/test_integration_subcommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -1566,6 +1566,43 @@ def test_use_installed_integration_sets_default(self, tmp_path):
assert opts["integration"] == "codex"
assert opts["ai"] == "codex"

def test_use_preserves_copilot_skills_mode(self, tmp_path):
"""`use` on a skills-mode Copilot keeps ``ai_skills`` (issue #3550).

Re-selecting the same skills-mode Copilot must not drop ``ai_skills``
from init-options.json nor regenerate extension commands in the legacy
``.agent.md``/``.prompt.md`` layout.
"""
project = _init_project(tmp_path, "copilot", integration_options="--skills")

opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
assert opts.get("ai_skills") is True, "precondition: init recorded skills mode"

result = _run_in_project(project, ["extension", "add", "git"])
assert result.exit_code == 0, f"extension add failed: {result.output}"

# Simulate a fresh process: `use` in real life runs in its own process
# where the registry's Copilot instance has _skills_mode == False (it is
# only set during setup()). In-process test invocations otherwise reuse
# the singleton left in skills mode by init, masking the bug (#3550).
from specify_cli.integrations import get_integration

get_integration("copilot")._skills_mode = False

result = _run_in_project(project, ["integration", "use", "copilot"])
assert result.exit_code == 0, result.output

opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
assert opts.get("ai_skills") is True, "ai_skills must survive `use copilot`"

# No legacy command-layout files should be regenerated for the
# skills-mode agent.
assert not (project / ".github" / "agents" / "speckit.git.feature.agent.md").exists()
assert not (project / ".github" / "prompts" / "speckit.git.feature.prompt.md").exists()
assert (
project / ".github" / "skills" / "speckit-git-feature" / "SKILL.md"
).exists()

def test_use_requires_installed_integration(self, tmp_path):
project = _init_project(tmp_path, "claude")
old_cwd = os.getcwd()
Expand Down