Skip to content

Commit d7a81fa

Browse files
committed
fix: clean up extension skills across all agents on remove (#2948)
Addresses Copilot review feedback: remove() called _unregister_extension_skills() without a directory, so its fast path removed skill files only from the resolved active-agent directory whenever one resolved, and never scanned the directories of other agents whose skills were rendered via the new multi-agent registration paths added earlier in this PR — leaving those orphaned on the filesystem after removal. _unregister_extension_skills() now always scans every known agent skills directory (previously this broader scan only ran as a fallback when no active-agent directory could be resolved at all), in addition to any explicitly-passed or resolved directory. Each candidate directory's matching skill subdirectories are still verified against the SKILL.md metadata.source field before removal, so this doesn't touch the existing safety guarantees or the registered_skills schema (intentionally kept as a flat, deduped list rather than a per-agent dict, to avoid a breaking registry-schema change for this fix). Tests: - Added test_remove_cleans_up_skills_for_non_active_agent, covering removal after skills were rendered for both an active and a non-active agent. - pytest tests/ -k extension: 620/620 pass (619 previous + 1 new). - git diff --check: clean. Note: a related Copilot suggestion to also have plain 'integration install' immediately register extension commands/skills for the newly-installed agent was evaluated but not applied here, since it directly conflicts with the maintainer-requested, explicitly-tested deferred-registration design from #2886 (see tests/integrations/test_integration_subcommand.py::TestIntegrationInstall::test_install_defers_extension_commands_until_use). Flagging this for maintainer input rather than silently reversing that prior decision.
1 parent acab576 commit d7a81fa

2 files changed

Lines changed: 55 additions & 11 deletions

File tree

src/specify_cli/extensions/__init__.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,19 +1299,19 @@ def _unregister_extension_skills(
12991299
Called during extension removal to clean up skill files that
13001300
were created by ``_register_extension_skills()``.
13011301
1302-
If *skills_dir* is not provided and ``_get_skills_dir()`` returns
1303-
``None`` (e.g. the user removed init-options.json or toggled
1304-
ai_skills after installation), we fall back to scanning all known
1305-
agent skills directories so that orphaned skill directories are
1306-
still cleaned up. In that case each candidate directory is
1307-
verified against the SKILL.md ``metadata.source`` field before
1308-
removal to avoid accidentally deleting user-created skills with
1309-
the same name.
1302+
Skills may have been rendered for more than one agent (#2948),
1303+
so this always scans every known agent skills directory (plus
1304+
*skills_dir* or the resolved active-agent directory, if any)
1305+
rather than only a single resolved directory, to avoid orphaning
1306+
skills registered for a non-active agent. Each candidate
1307+
directory is verified against the SKILL.md ``metadata.source``
1308+
field before removal to avoid accidentally deleting user-created
1309+
skills with the same name.
13101310
13111311
Args:
13121312
skill_names: List of skill names to remove.
13131313
extension_id: Extension ID used to verify ownership during
1314-
fallback candidate scanning.
1314+
candidate scanning.
13151315
skills_dir: Optional explicit skills directory to use instead
13161316
of resolving via ``_get_skills_dir()``. Useful when the
13171317
caller needs to target a specific agent's skills directory
@@ -1365,8 +1365,13 @@ def _unregister_extension_skills(
13651365
except (OSError, UnicodeDecodeError, Exception):
13661366
continue
13671367
shutil.rmtree(skill_subdir)
1368-
else:
1369-
# Fallback: scan all possible agent skills directories
1368+
# Additionally always scan every other known agent skills
1369+
# directory: skills may have been rendered for more than one
1370+
# agent (#2948), so limiting cleanup to only the directory above
1371+
# would orphan skills registered for a non-active agent.
1372+
# Directories already handled above are silently skipped since
1373+
# their matching skill subdirectories no longer exist.
1374+
if True:
13701375
from .. import AGENT_CONFIG, DEFAULT_SKILLS_DIR
13711376

13721377
candidate_dirs: set[Path] = set()

tests/test_extension_skills.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,3 +2034,42 @@ def test_extension_add_renders_skills_for_all_installed_skills_mode_agents(
20342034
assert (
20352035
copilot_skills_dir / "speckit-test-ext-hello" / "SKILL.md"
20362036
).exists()
2037+
2038+
def test_remove_cleans_up_skills_for_non_active_agent(
2039+
self, project_dir, extension_dir
2040+
):
2041+
"""remove() must clean up skills rendered for a non-active agent too (#2948)."""
2042+
_create_init_options(project_dir, ai="claude", ai_skills=True)
2043+
claude_skills_dir = _create_skills_dir(project_dir, ai="claude")
2044+
_create_integration_json(
2045+
project_dir,
2046+
default_agent="claude",
2047+
installed=["claude", "copilot"],
2048+
skills_by_agent={"copilot": True},
2049+
)
2050+
copilot_skills_dir = _create_skills_dir(project_dir, ai="copilot")
2051+
2052+
manager = ExtensionManager(project_dir)
2053+
manifest = manager.install_from_directory(
2054+
extension_dir, "0.1.0", register_commands=False
2055+
)
2056+
2057+
# Precondition: both agents got the skill rendered.
2058+
assert (
2059+
claude_skills_dir / "speckit-test-ext-hello" / "SKILL.md"
2060+
).exists()
2061+
assert (
2062+
copilot_skills_dir / "speckit-test-ext-hello" / "SKILL.md"
2063+
).exists()
2064+
2065+
result = manager.remove(manifest.id, keep_config=False)
2066+
assert result is True
2067+
2068+
# Removal must clean up both agents' skill directories, not just
2069+
# the active one.
2070+
assert not (
2071+
claude_skills_dir / "speckit-test-ext-hello"
2072+
).exists()
2073+
assert not (
2074+
copilot_skills_dir / "speckit-test-ext-hello"
2075+
).exists()

0 commit comments

Comments
 (0)