From 8251a1e6a73da44e3134865e63004563af31704f Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:08:58 -0500 Subject: [PATCH] fix(integration): preserve ai_skills on `use` for skills-mode Copilot (#3550) `specify integration use copilot` against a Copilot install configured with `--integration-options "--skills"` dropped `"ai_skills": true` from init-options.json and regenerated extension commands in the legacy `.agent.md`/`.prompt.md` layout, contradicting `integration.json`'s stored `parsed_options.skills: true`. `_update_init_options_for_integration` only inspected `SkillsIntegration` / the instance `_skills_mode` flag. On the `use` path no `setup()` runs, so the freshly-resolved Copilot instance has `_skills_mode == False` and the stored skills intent in `parsed_options` was ignored. Thread the resolved `parsed_options` through and treat `parsed_options["skills"]` as skills mode. Adds a regression test that resets the registry singleton's `_skills_mode` to simulate a fresh process (in-process singleton reuse otherwise masks the bug). Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 06fb6ae9-f444-4dfd-ab3f-d0669c5d0604 --- src/specify_cli/integrations/_helpers.py | 17 ++++++++- .../test_integration_subcommand.py | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/integrations/_helpers.py b/src/specify_cli/integrations/_helpers.py index d1bf051f77..b206e8d1d4 100644 --- a/src/specify_cli/integrations/_helpers.py +++ b/src/specify_cli/integrations/_helpers.py @@ -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. @@ -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) @@ -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: diff --git a/tests/integrations/test_integration_subcommand.py b/tests/integrations/test_integration_subcommand.py index 34114a564e..e4c21172f9 100644 --- a/tests/integrations/test_integration_subcommand.py +++ b/tests/integrations/test_integration_subcommand.py @@ -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()