[bug-fix] Fix upgrade-overwrites-copilot-skills: pass force=True to extension skill re-registration after upgrade - #3853
Merged
mnriem merged 3 commits intoJul 29, 2026
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds forced extension-skill re-registration after integration upgrades to restore extension content.
Changes:
- Threads a
forceflag through extension registration. - Bypasses existing-skill guards during upgrades.
- Adds force-flag regression tests.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/extensions/__init__.py |
Adds forced skill overwrites. |
src/specify_cli/integrations/_helpers.py |
Forwards the force flag. |
src/specify_cli/integrations/_migrate_commands.py |
Enables forced upgrade re-registration. |
tests/test_extension_skills.py |
Adds regression tests. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Medium
mnriem
force-pushed
the
fix/3849-upgrade-overwrites-copilot-skills-629c8acbe03af673
branch
from
July 29, 2026 14:04
3662776 to
d650c3a
Compare
mnriem
marked this pull request as ready for review
July 29, 2026 14:19
Contributor
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (2)
src/specify_cli/integrations/_migrate_commands.py:889
- Pass the CLI's
forcevalue through instead of enabling this unconditionally.integration upgradealso runs without--force, and extension-only skills are tracked in the extension registry rather than the integration manifest, soold_manifest.check_modified()cannot detect a user-edited extension skill. As written, an ordinary upgrade now overwrites that customization even though the user did not opt into forced replacement; forwardingforcestill fixes the reportedupgrade --forcepath.
force=True,
src/specify_cli/extensions/init.py:1438
- Do not let
forcebypass the guard for a symlinked skill directory. The active skills root is validated, but this per-skill child is not; if.github/skills/<skill-name>is a symlink to an outside directory,skill_file.exists()follows it and the forced write below overwrites a file outside the project. Reject a symlinkedskill_subdirbefore examining or writingSKILL.md.
if not is_expected_dev_symlink and not force:
continue
elif skill_dir_preexists and not force:
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Medium
…kill 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>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…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)
mnriem
force-pushed
the
fix/3849-upgrade-overwrites-copilot-skills-629c8acbe03af673
branch
from
July 29, 2026 14:28
d650c3a to
7e2ad89
Compare
Contributor
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (3)
src/specify_cli/extensions/init.py:1436
forcebypasses the ownership guard for every extension skill, althoughsetup()regenerates only core-template skills. Extension-only skill files are tracked in the extension registry rather than the integration manifest, so an activeintegration upgradecan now overwrite a user-edited extensionSKILL.mdeven without CLI--force;old_manifest.check_modified()cannot detect it. Scope the bypass to the exact paths recorded by the new integration manifest (or pass the regenerated skill-name set) and keep this guard for all other files.
if not is_expected_dev_symlink and not force:
src/specify_cli/extensions/init.py:1438
skill_dir_preexistsis also true for a directory symlink. Withforce=True, this now falls through tomkdir()and the laterSKILL.mdwrite, following an extension-only skill directory symlink outside the project. Reject a symlinked skill directory unconditionally;forceshould only relax the policy for an existing regular directory.
elif skill_dir_preexists and not force:
tests/integrations/test_integration_subcommand.py:3875
- This does not reproduce the reported upgrade flow:
speckit-git-featureis extension-only (not a core template), and deleting its file manually meanssetup()never replaces it with core content. The test therefore covers only the pre-existing-directory guard and can pass while the actual “setup rewrites an overridden core skill” scenario remains broken. Add an end-to-end case that leaves the installed artifact intact, runs upgrade, and verifies the reported extension composition survives.
# Simulate the exact pre-condition the bug depends on: the skill file is
# gone but its directory survives (as it does once setup() regenerates the
# core-template layout during upgrade), triggering the skill_dir_preexists
# skip guard on re-registration.
skill_file.unlink()
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Medium
mnriem
deleted the
fix/3849-upgrade-overwrites-copilot-skills-629c8acbe03af673
branch
July 29, 2026 15:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug fix — upgrade-overwrites-copilot-skills
Proposed fix for issue #3849, applying the remediation from the bug assessment.
Verdict: Valid · Severity: high
Summary
_register_extension_skills()had a skip guard that refused to overwrite existingSKILL.mdfiles (to protect user customizations). In the upgrade path,setup()regenerates all core-templateSKILL.mdfiles first, then callsregister_enabled_extensions_for_agent(). The skip guard then sees those freshly-written core files as "existing" and skips every extension, leaving only core template content on disk. This fix introduces aforce=Truepath that bypasses the guard in the upgrade context.Changes
src/specify_cli/extensions/__init__.pyforce: bool = Falseto_register_extension_skills(); bypass skip guards whenforce=True; threadforcethroughregister_enabled_extensions_for_agent()src/specify_cli/integrations/_helpers.pyforce: bool = Falseto_register_extensions_for_agent(); forward it tomgr.register_enabled_extensions_for_agent()lambdasrc/specify_cli/integrations/_migrate_commands.pyforce=Truewhen calling_register_extensions_for_agent()inintegration_upgrade()tests/test_extension_skills.pyTestRegisterExtensionSkillsForceFlagTests Added or Updated
TestRegisterExtensionSkillsForceFlag::test_force_false_skips_existing_skill— confirms the default (non-upgrade) behavior is unchanged: existing files are still protectedTestRegisterExtensionSkillsForceFlag::test_force_true_overwrites_existing_skill— core regression test:force=Trueoverwrites a core-template stub with extension contentTestRegisterExtensionSkillsForceFlag::test_register_enabled_extensions_for_agent_force_flag_threads_through— end-to-end:register_enabled_extensions_for_agent(force=True)produces the correct composed SKILL.mdTestRegisterExtensionSkillsForceFlag::test_force_true_with_preexisting_dir_but_no_skill_file—force=Truealso bypasses the second skip guard (pre-existing dir, no file)Local Verification
uv/ venv present). Changes verified by code inspection against the assessment root cause and confirmed guard logic is correct.Deviations from Assessment
None. The preferred remediation (add
forceflag, thread through call chain, passforce=Truein upgrade path) was implemented as described. Theskill_dir_preexistsguard was also conditioned onnot forceas a natural follow-on (the assessment focused on theskill_file.exists()guard but the dir-preexists guard would have the same effect when a dir exists without a SKILL.md).Risks & Review Notes
forceisFalseby default — no behavior change forextension add,use, orswitchpathsintegration_upgrade()passesforce=True, scoped to the active integration only (guarded byif key == installed_key)switchpath continues to call_register_extensions_for_agent()withoutforce, which is correct: switch starts fresh and should not overwrite user-customized skillsRefs #3849 · cc
@dtimonintegration upgradeoverwrites extension-and preset- provided Copilot skills #3849