From 68cabd06a686d4fbe042cb5e8220e882ac0334ff Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 21:04:49 +0000 Subject: [PATCH] C6: gstack-relink discovers skills structurally, so /browse survives a skill_prefix toggle relink skipped a hardcoded list of support-directory names that included browse, a real skill, so toggling skill_prefix removed /browse. A top-level directory is now a skill exactly when it carries a SKILL.md. Co-authored-by: Jonathan Zhang --- bin/gstack-relink | 6 +++--- test/relink.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/bin/gstack-relink b/bin/gstack-relink index 9dfb06a4b..79a2e8ba9 100755 --- a/bin/gstack-relink +++ b/bin/gstack-relink @@ -330,7 +330,7 @@ _link_root_skill_alias() { _link_root_skill_alias -# Discover skills (directories with SKILL.md, excluding meta dirs) +# Discover skills: every top-level directory with a SKILL.md SKILL_COUNT=0 for skill_dir in "$INSTALL_DIR"/*/; do [ -d "$skill_dir" ] || continue @@ -340,8 +340,8 @@ for skill_dir in "$INSTALL_DIR"/*/; do # rewritten-copy alias for those. [ -L "${skill_dir%/}" ] && continue skill=$(basename "$skill_dir") - # Skip non-skill directories - case "$skill" in bin|browse|design|docs|extension|lib|node_modules|scripts|test|.git|.github) continue ;; esac + # A directory is a skill iff it carries a SKILL.md (#2263): a name list + # dropped the real browse skill. Support dirs have no SKILL.md. [ -f "$skill_dir/SKILL.md" ] || continue case "$DISABLED" in *" ${skill#gstack-} "*) diff --git a/test/relink.test.ts b/test/relink.test.ts index 45b9e6233..87dfa51d9 100644 --- a/test/relink.test.ts +++ b/test/relink.test.ts @@ -526,6 +526,32 @@ describe('gstack-relink (#578)', () => { expect(fs.existsSync(path.join(skillsDir, 'gstack-qa'))).toBe(true); expect(fs.existsSync(path.join(skillsDir, 'gstack-ship'))).toBe(true); }); + + // C6 (#2263): discovery is structural (has a SKILL.md), never name-based. A + // hardcoded meta-dir list dropped the real `browse` skill, so /browse + // disappeared after toggling skill_prefix. + test('materializes every dir carrying a SKILL.md, even names once meta-excluded (browse)', () => { + const formerlyExcluded = ['bin', 'browse', 'design', 'docs', 'extension', 'lib', 'node_modules', 'scripts', 'test']; + setupMockInstall(['qa', ...formerlyExcluded]); + const env = { GSTACK_INSTALL_DIR: installDir, GSTACK_SKILLS_DIR: skillsDir }; + run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix false`, env); + run(`${path.join(installDir, 'bin', 'gstack-relink')}`, env); + const missing = ['qa', ...formerlyExcluded].filter(s => !fs.existsSync(path.join(skillsDir, s, 'SKILL.md'))); + expect(missing).toEqual([]); + }); + + test('skips top-level dirs that have no SKILL.md', () => { + setupMockInstall(['qa']); + for (const meta of ['node_modules', 'lib', 'scripts']) { + fs.mkdirSync(path.join(installDir, meta), { recursive: true }); + fs.writeFileSync(path.join(installDir, meta, 'placeholder.txt'), 'not a skill'); + } + const env = { GSTACK_INSTALL_DIR: installDir, GSTACK_SKILLS_DIR: skillsDir }; + run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix false`, env); + run(`${path.join(installDir, 'bin', 'gstack-relink')}`, env); + expect(fs.existsSync(path.join(skillsDir, 'qa', 'SKILL.md'))).toBe(true); + expect(['bin', 'node_modules', 'lib', 'scripts'].filter(m => fs.existsSync(path.join(skillsDir, m)))).toEqual([]); + }); }); describe('upgrade migrations', () => {