A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.
fix(3659): applySurface prunes skill dirs on cluster disable (#3766)
* test(3659): add regression tests for applySurface skill-dir pruning on cluster disable
Tests that applySurface with claude global scope correctly prunes
~/.claude/skills/gsd-STEM/ dirs for disabled clusters, preserves
gsd-STEM dirs in enabled clusters, leaves non-gsd user dirs untouched,
and is idempotent across two consecutive calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3659): applySurface now prunes ~/.claude/skills/gsd-STEM/ on cluster disable
Root cause: surface.md directed the AI to use RUNTIME_CONFIG_DIR=~/.claude/skills
(the skills sub-directory) instead of the base Claude config dir (~/.claude).
When runtimeConfigDir=~/.claude/skills and scope=global, the layout computes
dest=~/.claude/skills/skills — the wrong target — so pruning never reached
the actual gsd-STEM dirs in ~/.claude/skills/.
Fix:
- surface.md: correct RUNTIME_CONFIG_DIR to use the base config dir (~/.claude),
add explicit SCOPE=global, and update all path references in execution_context.
Surface state file moves from ~/.claude/skills/.gsd-surface.json to
~/.claude/.gsd-surface.json, matching install/uninstall conventions.
- surface.cjs: extract pruneSkillDirs() as a shared helper (single point of truth
for gsd-STEM dir removal). _syncGsdDir now delegates to it instead of having
the ownership/prune logic inline. Export pruneSkillDirs for callers that need
stand-alone pruning without a full applySurface pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(3659): update changeset to reference PR #3766
* fix(3659): manifest-membership gate on pruneSkillDirs prevents user gsd-* dir data loss
Finding 1 (CRITICAL): the prefixed branch previously deleted any on-disk dir that
matched the 'gsd-' prefix and was not in retainedNames. A user-created gsd-mything/
would be silently destroyed. Fix: deletion now requires BOTH prefix match AND
manifest membership (stem present in manifest). Dirs that match the prefix but are
not manifest-known are preserved with a process.stderr warning so the user knows the
dir was kept.
Finding 2 (type guard): the Hermes (empty-prefix) branch passed manifest directly to
new Set([...manifest.keys()]) without verifying it is actually a Map. A truthy
non-Map would throw. Fix: safeManifest = (manifest instanceof Map) ? manifest : null,
used in both branches. Non-Map manifest triggers the same conservative
no-deletions path already used when manifest is absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3659): counter-test for all-clusters-disabled + user gsd-* dir preservation
Finding 3: add test (e) that disables every cluster (Object.keys(CLUSTERS)) and
asserts three things:
1. All GSD-owned skill dirs (gsd-explore/, gsd-help/) are removed.
2. Non-gsd user dir (my-custom-skill/) is preserved.
3. User-created gsd-mything/ (prefix match, not in manifest) is preserved —
this is the critical regression guard for the Finding 1 data-loss fix.
Also update the existing _syncGsdDir skills-kind test in surface-apply.test.cjs to
pass a manifest that declares old-skill as GSD-owned. Without a manifest the new
conservative path correctly preserves all unknown gsd-* dirs, which broke the
pre-existing no-manifest assertion; supplying the manifest restores the expected
pruning behavior and documents the required calling contract.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3659): address pr-review-toolkit + codex review findings
- Collapse redundant if/else in _syncGsdDir
- Collapse duplicate canonicalStems branches in pruneSkillDirs
- Update stale module-header comment (config-dir root)
- Clarify dead isGsdOwned guard comment
- Log rmSync failures to stderr
- Add pruneSkillDirs to module-header Exports JSDoc
- Remove unused imports in bug-3659 test file
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> T
Tom Boucher committed
99b52a302af2f9e67495be659dc1e14bc1bdcc20
Parent: 172e692
Committed by GitHub <noreply@github.com>
on 5/21/2026, 3:12:44 AM