Skip to content

fix(presets): restore core skills instead of deleting them on preset remove - #3929

Merged
mnriem merged 3 commits into
github:mainfrom
chelsealong:fix/3928-preset-remove-deletes-core-skills
Aug 3, 2026
Merged

fix(presets): restore core skills instead of deleting them on preset remove#3929
mnriem merged 3 commits into
github:mainfrom
chelsealong:fix/3928-preset-remove-deletes-core-skills

Conversation

@chelsealong

Copy link
Copy Markdown
Contributor

Fixes #3928

Bug

When a preset that overrides core commands/skills is removed, the corresponding
core skills are deleted instead of being restored — the only way to get them
back is to re-scaffold with specify init.

Root cause

PresetManager._unregister_skills_in_dir looks for the original core command
template at self.project_root / ".specify" / "templates" / "commands". That
directory is never populated by specify init in a real project — the actual
core command templates live in the bundled core_pack/commands/ directory
(wheel install) or the repo-root templates/commands/ tree (source checkout),
exactly like PresetResolver.resolve_core() already falls back to via
_locate_core_pack() / _repo_root().

Because the lookup always missed, every preset-removal restoration fell
through to the "no core template found" branch and deleted the skill outright.

Fix

Restoration now falls back to the bundled core_pack/repo-root templates when
the project-local override directory doesn't have the file. This fallback is
gated behind a new restore_from_bundled_core flag so the pre-existing "retire
a stale skill because its command now renders as a command file elsewhere"
path (triggered by ai_skills mode toggles) keeps its original delete
behavior — it must not resurrect a duplicate skill that a command file has
already superseded.

Repro (before the fix)

specify init --here --integration claude
specify preset add lean --dev presets/lean
ls .claude/skills          # all 10 core skills present
specify preset remove lean
ls .claude/skills          # only 5 remain — specify/plan/tasks/implement/constitution deleted

After the fix, all 10 skills are present again, restored with core content
after specify preset remove lean.

Testing

Added test_skill_restored_on_preset_remove_without_project_core_templates,
which (unlike the existing test_skill_restored_on_preset_remove) does not
pre-seed .specify/templates/commands with a fake core file, matching what a
real specify init project looks like. Confirmed it fails without the fix
(AssertionError: Core skill must be restored, not deleted) via
git checkout HEAD~1 -- src/specify_cli/presets/__init__.py, then passes with
the fix restored.

$ .venv/bin/python -m pytest tests/test_presets.py -q
526 passed in 7.40s

$ .venv/bin/python -m pytest tests -q
6380 passed, 9 skipped in 396.25s

$ .venv/bin/python -m pytest tests/test_agent_config_consistency.py -q
28 passed in 0.24s

$ uvx ruff@0.15.0 check src tests
All checks passed!

AI disclosure

This PR was authored primarily by an AI coding agent (Claude Code): it
diagnosed the root cause, implemented the fix, and wrote/verified the
regression test. I reviewed the diff and test output before submitting.

…remove

Skill restoration looked for core command templates under
.specify/templates/commands, a directory specify init never populates
in real projects. Since that lookup always missed, presets overriding
a core command (e.g. speckit.plan) had their skill deleted outright on
removal instead of restored — the actual core templates live in the
bundled core_pack (wheel install) or the repo-root templates/ tree.

Restoration now falls back to that bundled location, gated behind a
restore_from_bundled_core flag so the existing "retire a stale skill
superseded by a command-mode winner" path keeps deleting rather than
resurrecting a duplicate skill.

Fixes github#3928

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes preset removal so overridden core skills are restored from bundled templates.

Changes:

  • Adds opt-in bundled-core restoration for preset cleanup paths.
  • Adds regression coverage for projects without local core command templates.
Show a summary per file
File Description
src/specify_cli/presets/__init__.py Adds bundled-core skill restoration fallback.
tests/test_presets.py Tests restoration without local templates.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/specify_cli/presets/__init__.py Outdated
read_text() defaults to the platform locale encoding, which is cp1252
("charmap") on Windows. The bundled specify.md core template contains a
UTF-8 multi-byte emoji whose bytes aren't valid cp1252, so the Windows CI
job failed decoding the restored SKILL.md with UnicodeDecodeError.
@chelsealong

Copy link
Copy Markdown
Contributor Author

Fixed the Windows CI failure: the new test read the restored SKILL.md with read_text() (no explicit encoding), which defaults to the platform locale encoding — cp1252 ("charmap") on Windows. The bundled specify.md template contains a UTF-8 emoji whose bytes aren't valid cp1252, causing UnicodeDecodeError on windows-latest. Now reads with encoding="utf-8", matching how the file is written. Full suite (6380 passed, 9 skipped) and ruff pass locally.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

The bundled-core fallback added for github#3928 ran before the
extension_restore_index lookup, so a skill an installed extension owns
could be silently replaced by lower-priority bundled core content on
preset removal instead of preserving the extension's winning layer.
@chelsealong

Copy link
Copy Markdown
Contributor Author

Addressed the inline review comment: the bundled-core fallback now skips itself when extension_restore_index already has an entry for the skill, so an extension-owned skill restores from the extension rather than being replaced by lower-priority bundled core content. Added test_extension_wins_over_bundled_core_on_preset_remove (stubs the restore index since the extension command-namespace rules make a genuine end-to-end name collision hard to construct through real manifests); confirmed it fails without the fix. Full suite: 6381 passed, 9 skipped; ruff check src tests: all checks passed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@mnriem
mnriem merged commit f4e3110 into github:main Aug 3, 2026
14 checks passed
@mnriem

mnriem commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Core skills get deleted when preset is removed

3 participants