fix(3663): Hermes empty-prefix uses manifest membership for stale-skill prune (Codex P1-2)

Replace the blunt kindPrefix !== '' guard in _syncGsdDir with a
manifest-membership discriminator. For non-empty prefix runtimes, behavior
is unchanged (prefix match). For Hermes (empty prefix), a directory is
GSD-owned iff its stem appears in the canonical manifest; only those dirs
are removal candidates when absent from the staged set. Dirs not in the
manifest (user-owned) are preserved unconditionally. When no manifest is
provided (legacy callers), the removal pass is skipped (conservative fallback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-17 01:01:10 -04:00
parent 043cfd97b3
commit 06cf085bfb

View File

@@ -227,6 +227,53 @@ describe('applySurface', () => {
assert.ok(files.includes('help.md'), 'help.md should be present after applySurface on missing dest');
});
test('Hermes profile shrink: stale GSD skill dirs are removed; user skills preserved', (t) => {
const { _syncGsdDir } = require('../get-shit-done/bin/lib/surface.cjs');
const base = createTempDir('gsd-surface-hermes-shrink-');
t.after(() => cleanup(base));
const stagedDir = path.join(base, 'staged');
const destDir = path.join(base, 'dest');
fs.mkdirSync(destDir, { recursive: true });
// Staged: only gsd-executor (profile shrunk — gsd-planner no longer in profile)
fs.mkdirSync(path.join(stagedDir, 'gsd-executor'), { recursive: true });
fs.writeFileSync(path.join(stagedDir, 'gsd-executor', 'SKILL.md'), '# executor\n', 'utf8');
// Dest already has: gsd-executor (keep), gsd-planner (stale GSD), user-skill (user-owned)
fs.mkdirSync(path.join(destDir, 'gsd-executor'), { recursive: true });
fs.writeFileSync(path.join(destDir, 'gsd-executor', 'SKILL.md'), '# executor\n', 'utf8');
fs.mkdirSync(path.join(destDir, 'gsd-planner'), { recursive: true });
fs.writeFileSync(path.join(destDir, 'gsd-planner', 'SKILL.md'), '# planner\n', 'utf8');
fs.mkdirSync(path.join(destDir, 'user-skill'), { recursive: true });
fs.writeFileSync(path.join(destDir, 'user-skill', 'SKILL.md'), '# user\n', 'utf8');
// Manifest contains gsd-executor and gsd-planner as canonical GSD skills.
// user-skill is NOT in manifest (user-owned).
const manifest = new Map([
['gsd-executor', []],
['gsd-planner', []],
]);
// Hermes kind: empty prefix, destSubpath = skills/gsd
const hermesKind = { kind: 'skills', destSubpath: 'skills/gsd', prefix: '', stage: () => stagedDir };
_syncGsdDir(stagedDir, destDir, hermesKind, manifest);
assert.ok(
fs.existsSync(path.join(destDir, 'gsd-executor', 'SKILL.md')),
'gsd-executor should be kept (in staged set)'
);
assert.ok(
!fs.existsSync(path.join(destDir, 'gsd-planner')),
'gsd-planner should be removed (in manifest but not in staged set — stale GSD skill)'
);
assert.ok(
fs.existsSync(path.join(destDir, 'user-skill', 'SKILL.md')),
'user-skill should be preserved (not in manifest — user-owned)'
);
});
test('_syncGsdDir skills kind (hermes): preserves non-GSD user dir under skills/gsd/ when kindPrefix is empty', (t) => {
const { _syncGsdDir } = require('../get-shit-done/bin/lib/surface.cjs');