* 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>
5.0 KiB
name, description, argument-hint, allowed-tools, requires
| name | description | argument-hint | allowed-tools | requires | |||||
|---|---|---|---|---|---|---|---|---|---|
| gsd:surface | Toggle which skills are surfaced — apply a profile, list, or disable a cluster without reinstall | [list|status|profile <name>|disable <cluster>|enable <cluster>|reset] |
|
|
Sub-commands: list · status · profile · disable · enable · reset
Sub-command routing
Parse the first token of $ARGUMENTS:
| Token | Action |
|---|---|
list |
Show enabled + disabled clusters and skills |
status |
Alias for list plus token cost summary |
profile <name> |
Write baseProfile and re-stage |
profile <n1>,<n2> |
Composed profiles (comma-separated, no spaces) |
disable <cluster> |
Add cluster to disabledClusters, re-stage |
enable <cluster> |
Remove cluster from disabledClusters, re-stage |
reset |
Delete .gsd-surface.json, return to install-time profile |
| (none) | Treat as list |
list / status
Call listSurface(runtimeConfigDir, manifest, CLUSTERS) from
get-shit-done/bin/lib/surface.cjs. Display:
Enabled (N skills, ~T tokens):
core_loop: new-project discuss-phase plan-phase execute-phase help update
audit_review: …
…
Disabled:
utility: health stats settings …
Token cost: ~T (budget cap ~500 tokens for 200k context @ 1%)
For status also append:
Base profile: standard (from .gsd-surface.json)
Install profile: standard (from .gsd-profile)
profile <name>
- Read current surface:
readSurface(runtimeConfigDir)→ if null, seed fromreadActiveProfile(runtimeConfigDir). - Set
surfaceState.baseProfile = name. writeSurface(runtimeConfigDir, surfaceState).- Resolve and re-apply:
const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope); applySurface(runtimeConfigDir, layout, manifest, CLUSTERS); - Confirm: "Surface updated to profile
<name>. N skills enabled."
disable <cluster>
Valid cluster names: core_loop, audit_review, milestone, research_ideate,
workspace_state, docs, ui, ai_eval, ns_meta, utility.
- Validate cluster name against
Object.keys(CLUSTERS). - Read or initialize surface state.
- Add cluster to
surfaceState.disabledClusters(deduplicate). writeSurface→ resolve layout →applySurface:const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope); applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);- Confirm: "Disabled cluster
<cluster>. N skills removed from surface."
enable <cluster>
- Read surface state; if null, nothing to enable — print "No surface delta active."
- Remove cluster from
surfaceState.disabledClusters. writeSurface→ resolve layout →applySurface:const layout = resolveRuntimeArtifactLayout(runtime, runtimeConfigDir, scope); applySurface(runtimeConfigDir, layout, manifest, CLUSTERS);- Confirm: "Enabled cluster
<cluster>. N skills added back to surface."
reset
- Check if
.gsd-surface.jsonexists. - Delete it.
- Re-apply using only
readActiveProfile(runtimeConfigDir)(install-time profile). - Confirm: "Surface reset to install-time profile
<name>."
runtimeConfigDir resolution
The runtimeConfigDir for applySurface is the base Claude config directory
(~/.claude), NOT the skills sub-directory (~/.claude/skills).
This matches installRuntimeArtifacts and uninstallRuntimeArtifacts, which also
receive ~/.claude as configDir. The skill dirs themselves live at
~/.claude/skills/gsd-*/ because the claude global layout has destSubpath = 'skills' — they are derived from configDir, not the root for it.
# Claude Code — global install
RUNTIME_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
SCOPE="global"
# Artifact destinations are derived from runtime layout
# via resolveRuntimeArtifactLayout(runtime, RUNTIME_CONFIG_DIR, SCOPE)
# then applySurface(RUNTIME_CONFIG_DIR, layout, manifest, CLUSTERS)
Surface state is stored at ${RUNTIME_CONFIG_DIR}/.gsd-surface.json
(i.e. ~/.claude/.gsd-surface.json).
All paths can be overridden by reading the CLAUDE_CONFIG_DIR env var if set.
Error handling
- Unknown cluster name → list valid cluster names, exit without writing.
- Unknown profile name → list known profiles (
core,standard,full), exit. - Missing
surface.cjs→ prompt: "Runnpm i -g get-shit-doneto reinstall GSD."
<execution_context>
Surface state file: ~/.claude/.gsd-surface.json
Install profile marker: ~/.claude/.gsd-profile
Skill dirs: ~/.claude/skills/gsd-*/
Engine module: ~/.claude/get-shit-done/bin/lib/surface.cjs
Cluster definitions: ~/.claude/get-shit-done/bin/lib/clusters.cjs
</execution_context>