Skip to content

Commit 2fb18c7

Browse files
mnriemCopilot
andauthored
fix(integration): preserve ai_skills on use for skills-mode Copilot (#3550) (#3551)
`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) Copilot-Session: 06fb6ae9-f444-4dfd-ab3f-d0669c5d0604 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 5409670 commit 2fb18c7

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

src/specify_cli/integrations/_helpers.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ def _update_init_options_for_integration(
260260
project_root: Path,
261261
integration: Any,
262262
script_type: str | None = None,
263+
parsed_options: dict[str, Any] | None = None,
263264
) -> None:
264265
"""Update init-options.json to reflect *integration* as the active one.
265266
@@ -278,7 +279,17 @@ def _update_init_options_for_integration(
278279
opts["speckit_version"] = _get_speckit_version()
279280
if script_type:
280281
opts["script"] = script_type
281-
if isinstance(integration, SkillsIntegration) or getattr(integration, "_skills_mode", False):
282+
# Skills mode is either intrinsic (SkillsIntegration), set on the instance
283+
# during setup() (_skills_mode), or requested via parsed options (e.g.
284+
# Copilot's --skills, persisted as parsed_options["skills"]). The latter is
285+
# the only signal available on the `use` path, where no setup() runs and a
286+
# fresh integration instance has _skills_mode == False (issue #3550).
287+
skills_mode = (
288+
isinstance(integration, SkillsIntegration)
289+
or getattr(integration, "_skills_mode", False)
290+
or bool((parsed_options or {}).get("skills"))
291+
)
292+
if skills_mode:
282293
opts["ai_skills"] = True
283294
else:
284295
opts.pop("ai_skills", None)
@@ -334,7 +345,9 @@ def _set_default_integration(
334345
) from exc
335346

336347
_write_integration_json(project_root, key, installed_keys, settings)
337-
_update_init_options_for_integration(project_root, integration, script_type=resolved_script)
348+
_update_init_options_for_integration(
349+
project_root, integration, script_type=resolved_script, parsed_options=parsed_options
350+
)
338351

339352

340353
def _set_default_integration_or_exit(*args: Any, **kwargs: Any) -> None:

tests/integrations/test_integration_subcommand.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,43 @@ def test_use_installed_integration_sets_default(self, tmp_path):
15661566
assert opts["integration"] == "codex"
15671567
assert opts["ai"] == "codex"
15681568

1569+
def test_use_preserves_copilot_skills_mode(self, tmp_path):
1570+
"""`use` on a skills-mode Copilot keeps ``ai_skills`` (issue #3550).
1571+
1572+
Re-selecting the same skills-mode Copilot must not drop ``ai_skills``
1573+
from init-options.json nor regenerate extension commands in the legacy
1574+
``.agent.md``/``.prompt.md`` layout.
1575+
"""
1576+
project = _init_project(tmp_path, "copilot", integration_options="--skills")
1577+
1578+
opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
1579+
assert opts.get("ai_skills") is True, "precondition: init recorded skills mode"
1580+
1581+
result = _run_in_project(project, ["extension", "add", "git"])
1582+
assert result.exit_code == 0, f"extension add failed: {result.output}"
1583+
1584+
# Simulate a fresh process: `use` in real life runs in its own process
1585+
# where the registry's Copilot instance has _skills_mode == False (it is
1586+
# only set during setup()). In-process test invocations otherwise reuse
1587+
# the singleton left in skills mode by init, masking the bug (#3550).
1588+
from specify_cli.integrations import get_integration
1589+
1590+
get_integration("copilot")._skills_mode = False
1591+
1592+
result = _run_in_project(project, ["integration", "use", "copilot"])
1593+
assert result.exit_code == 0, result.output
1594+
1595+
opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
1596+
assert opts.get("ai_skills") is True, "ai_skills must survive `use copilot`"
1597+
1598+
# No legacy command-layout files should be regenerated for the
1599+
# skills-mode agent.
1600+
assert not (project / ".github" / "agents" / "speckit.git.feature.agent.md").exists()
1601+
assert not (project / ".github" / "prompts" / "speckit.git.feature.prompt.md").exists()
1602+
assert (
1603+
project / ".github" / "skills" / "speckit-git-feature" / "SKILL.md"
1604+
).exists()
1605+
15691606
def test_use_requires_installed_integration(self, tmp_path):
15701607
project = _init_project(tmp_path, "claude")
15711608
old_cwd = os.getcwd()

0 commit comments

Comments
 (0)