[bug-fix] Fix upgrade-overwrites-copilot-skills: pass force=True to extension skill re-registration after upgrade (#3853)

* Fix upgrade-overwrites-copilot-skills: pass force=True to extension skill re-registration after upgrade

Apply the remediation from the bug assessment on issue #3849.

_register_extension_skills() had a skip guard that refused to overwrite
existing SKILL.md files (protecting user customizations). In the upgrade
path, setup() regenerates all core-template SKILL.md files first, then
calls register_enabled_extensions_for_agent(). The guard then sees those
freshly-written core files as 'existing' and skips every extension, leaving
only core template content on disk.

Fix: add force: bool = False to _register_extension_skills() and thread it
through register_enabled_extensions_for_agent() and
_register_extensions_for_agent(). In integration_upgrade(), pass force=True
so extension content layers on top of the just-regenerated core files.

The force flag is off-by-default so plain extension add still protects
user-modified skill files.

Refs #3849

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Potential fix for pull request finding 'Unused local variable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* test: add end-to-end regression guard for upgrade-overwrites-copilot-skills (#3849)

The existing regression tests in TestRegisterExtensionSkillsForceFlag exercise
the new force parameter at the helper level, so without the fix they fail only
with a TypeError (unknown kwarg) rather than on the user-facing behaviour.

Add a command-level test that runs 'specify integration upgrade copilot --skills
--force' end-to-end and asserts the installed git extension's SKILL.md is
restored (with its extension content, not a bare core-template stub) when the
skill directory already exists — the exact skill_dir_preexists path the bug
depends on. The test fails on pre-fix source (the skill is never recreated) and
passes with the fix, so it is a genuine behavioural regression guard rather than
an API-surface check.

Refs #3849

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
github-actions[bot]
2026-07-29 10:03:14 -05:00
committed by GitHub
parent 89126f3a33
commit 8394c8d536
5 changed files with 213 additions and 6 deletions

View File

@@ -1324,6 +1324,7 @@ class ExtensionManager:
manifest: ExtensionManifest,
extension_dir: Path,
link_outputs: bool = False,
force: bool = False,
) -> List[str]:
"""Generate SKILL.md files for extension commands as agent skills.
@@ -1337,6 +1338,11 @@ class ExtensionManager:
extension_dir: Installed extension directory.
link_outputs: If True, create dev-mode symlinks for rendered
skill files when supported by the OS.
force: If True, overwrite existing SKILL.md files even when they
are not dev-mode symlinks. Use in the upgrade path, where
``setup()`` has just freshly regenerated core-template skill
files and the skip guard would otherwise prevent extension
content from being layered on top.
Returns:
List of skill names that were created (for registry storage).
@@ -1424,13 +1430,16 @@ class ExtensionManager:
)
# Do not overwrite user-customized skills, but allow dev-mode
# symlinks that point back to this extension's generated cache
# to be refreshed on a subsequent dev install.
if not is_expected_dev_symlink:
# to be refreshed on a subsequent dev install. In the upgrade
# path (force=True) the file was just written by setup(), so
# overwriting it with the composed extension content is correct.
if not is_expected_dev_symlink and not force:
continue
elif skill_dir_preexists:
elif skill_dir_preexists and not force:
# Never add files to a pre-existing user directory. Without a
# verifiable SKILL.md ownership marker, rollback/removal cannot
# distinguish our output from unrelated user artifacts.
# Skipped when force=True (upgrade path).
continue
# Create skill directory; track whether we created it so we can clean
@@ -2669,7 +2678,7 @@ class ExtensionManager:
if updates:
self.registry.update(ext_id, updates)
def register_enabled_extensions_for_agent(self, agent_name: str) -> None:
def register_enabled_extensions_for_agent(self, agent_name: str, *, force: bool = False) -> None:
"""Register installed, enabled extensions for ``agent_name``.
Command-file registration is scoped to the explicit ``agent_name``
@@ -2787,7 +2796,7 @@ class ExtensionManager:
if agent_name == active_agent:
try:
registered_skills = self._register_extension_skills(
manifest, ext_dir
manifest, ext_dir, force=force
)
except Exception as skills_err:
# Skills are a companion artifact. If command registration

View File

@@ -395,6 +395,7 @@ def _register_extensions_for_agent(
agent_key: str,
*,
continuing: str,
force: bool = False,
) -> None:
"""Register all enabled extensions' commands/skills for ``agent_key``.
@@ -408,6 +409,11 @@ def _register_extensions_for_agent(
before registering), so extension *skill* rendering — which is scoped to
the active ``ai`` / ``ai_skills`` init-options — matches ``agent_key``.
When ``force=True``, existing skill files are overwritten even when they
are not dev-mode symlinks. Pass ``force=True`` in the upgrade path so that
extension content is layered on top of the core-template files that
``setup()`` just regenerated (fixes the skip-guard bug for skills mode).
Best-effort: never aborts the surrounding integration operation. Callers
invoke it *after* the use/upgrade/switch transaction has committed so a
failure here cannot trigger a rollback.
@@ -415,7 +421,7 @@ def _register_extensions_for_agent(
_best_effort_extension_op(
project_root,
agent_key,
lambda mgr, key: mgr.register_enabled_extensions_for_agent(key),
lambda mgr, key: mgr.register_enabled_extensions_for_agent(key, force=force),
phase="register extension artifacts for",
continuing=continuing,
)

View File

@@ -886,6 +886,7 @@ def integration_upgrade(
_register_extensions_for_agent(
project_root,
key,
force=True,
continuing="The integration was upgraded, but installed extensions may need re-registration.",
)
_register_presets_for_agent(