feat: add --prune to remove orphaned skills on update - #77
Merged
Conversation
When an upstream tap renames or deprecates a skill, the old installed copy was left stale — update only printed an advisory. Add an opt-in --prune flag to both 'skillshub update' and 'skillshub tap update' that uninstalls installed skills no longer present upstream (files + db entry + empty tap dir). Extract remove_installed_skill_files as an in-memory helper shared by uninstall_skill and the prune paths, avoiding a nested init/save cycle that would otherwise let the caller's own save_db resurrect the removed entry. Default behavior is unchanged; without --prune the advisory now also hints at the flag. Symlink cleanup for pruned skills is tracked in #76. Docs updated (README, docs/cli-reference, CLAUDE); adds two prune tests.
fenfenai
requested changes
Jul 29, 2026
fenfenai
left a comment
Collaborator
There was a problem hiding this comment.
Review Summary
Reviewed 8 files (+340/-20) across 6 review aspects (bug detection, error handling, type design, test coverage, comment quality, guidelines compliance). Core mechanics are sound — the helper extraction correctly avoids the db-resurrection trap, and iterating over cloned key Vecs while mutating db.installed is safe. Two functional defects in the prune logic and one workflow violation need attention before merge.
Critical Issues (confidence 90-100)
None.
Important Issues (confidence 75-89)
- src/registry/tap.rs:296-310 — the
tap update --prunere-run path silently no-ops (confidence: 85).removed_installedis a diff against the pre-update cache, which the same run overwrites; the advisory's own suggestion ("re-run with --prune") then finds an empty diff. - src/registry/skill.rs:579-611 —
update --prunedecides against a cache-only registry (confidence: 80). Stale cache → misses real removals (falls through to a confusing "Skill path not found in local clone" copy error) or can prune a skill that currently exists upstream. - plans/prune-orphaned-skills.md — plan shipped as-is instead of rewritten into
docs/(confidence: 88). Explicit AGENTS.md workflow rule; the merged plan also still presents already-resolved decisions as open questions. - Test gap: prune flag wiring untested (confidence: 78). Only the extracted helper is tested; the prune branches in
update_tap/update_skill(tap, registry, and gist paths) — where findings 1 and 2 live — have no coverage, though the plan promised exactly those tests. - src/cli.rs:41 —
update --prunehelp text omits the gist case (confidence: 76). The code prunes gist-sourced skills too; the doc says only "in their tap".
Positive Observations
- The
remove_installed_skill_filesextraction is the right fix for the db-resurrection hazard, and its doc comment explains the caller-owns-persistence contract precisely. - Both new tests are high quality:
TestHomeGuard+#[serial]+TempDir, deterministic, offline, behavior-focused. - Default behavior is unchanged;
--pruneis strictly opt-in, matching the plan's non-destructive-by-default design. - README, CLAUDE.md, and cli-reference docs all updated per repo rules.
Verdict: Request changes — findings 1 and 2 undermine the feature's core promise in exactly the scenarios it targets.
- tap update --prune: derive orphans from current registry membership so a re-run after a non-prune update still prunes (was a silent no-op because the diff baseline is overwritten in the same run) - update --prune: decide prune against the freshly pulled clone, not the cache-only registry, avoiding wrong prunes and cryptic copy errors when the cached path no longer exists upstream - widen --prune help text to cover gist-sourced skills - add flow-level tests for orphan detection (incl. re-run regression) and the prune-vs-report decision - migrate plan to docs/prune-orphaned-skills.md per repo workflow Addresses review on #77
fenfenai
approved these changes
Jul 29, 2026
fenfenai
left a comment
Collaborator
There was a problem hiding this comment.
Re-Review: All previous findings addressed
Resolved (5 threads)
- src/registry/tap.rs — baseline-diff no-op ✓ Fixed. Prune set now comes from
orphaned_installed_skills(membership againstcurrent_skillsfrom the fresh registry), independent of the diff baseline; the "re-run with --prune" hint now works after a non-prune update. Covered bytest_orphaned_installed_skills_detected_after_cache_refresh. - src/registry/skill.rs — cache-only prune decision ✓ Fixed. Clone-backed taps decide membership against the freshly-pulled clone via
discover_skills_from_localafterpull_or_reclone(never the stale cache); default/gist taps decide from their authoritative caches; the cache is refreshed after each scan. Both failure directions (missed removal → cryptic copy error; pruning a skill that exists upstream) are eliminated. - plans/prune-orphaned-skills.md ✓ Fixed. Plan removed from
plans/; rewritten asdocs/prune-orphaned-skills.mdper the repo workflow. - Test coverage ✓ Addressed. Four orphan-membership tests plus
test_prune_or_report_missing_{prunes_when_flag_set,keeps_when_flag_unset}cover the previously flagged defect scenarios. - src/cli.rs help text ✓ Fixed. Now reads "no longer exist upstream (tap or gist)".
Verification
- Checked out PR head (4a46a5c) in a worktree and ran the full suite: 290 passed, 0 failed (202 bin + 88 integration).
- Read the reworked
update_tap/update_skill/update_single_tap/orphaned_installed_skills/prune_or_report_missingpaths; no new findings at reportable confidence. One sub-threshold nit (not blocking): afterupdate --pruneprunes a clone-backed skill, the tap's cached registry is only refreshed on the skill-found path, solistcan show a stale available entry until the nexttap update.
Approving.
New `--prune` CLI flag on `update` and `tap update`. Document the feature and the two prune correctness fixes in CHANGELOG.
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.
Problem
When an upstream tap renames or deprecates a skill, the local installed copy is left stale.
skillshub tap updateonly printed an advisory ("run `skillshub uninstall` manually"), andskillshub updateskipped the skill with "(not in registry)" — neither removed anything. A rename is the worst case: the old skill stays installed + linked while its replacement is never installed, so the user silently keeps a deprecated skill.Change
Add an opt-in
--pruneflag to both update commands:skillshub tap update --prune— after refreshing the tap, uninstall installed skills no longer present upstream (removed_installed).skillshub update --prune— uninstall installed skills that are gone upstream (missing from the registry, or from a gist source).Pruning removes the install dir, the
db.installedentry, and the now-empty tap dir. Default behavior is unchanged; without--prunethe advisory now also hints at the flag.Key implementation detail
uninstall_skillopens its own DB and saves it. The update flows hold their own in-memoryDatabaseand save once at the end, so callinguninstall_skillfrom inside them would let that finalsave_dbresurrect the removed entry. To avoid this, the file/entry removal is extracted intoremove_installed_skill_files(db, install_dir, skill_id)— an in-memory helper that mutates the caller'sdbwithout init/save. Bothuninstall_skilland the prune paths use it, so persistence happens exactly once per command.Scope
uninstallsemantics (files + db entry). Dangling agent symlinks left by bothuninstalland--pruneare tracked as a follow-up in Prune/uninstall leave dangling agent symlinks behind #76.SKILL.md), so prune treats a rename as delete-old + add-new rather than following it.Tests & verification
src/registry/tap.rs: prune removes only the targeted orphan (siblings intact), and prune cleans up an emptied tap dir.cargo clippy --all-targetsclean,--pruneverified in both--helpoutputs.Docs
README.md,docs/cli-reference.md,CLAUDE.mdupdated with the new flag.🤖 Generated with Claude Code