From e2d6cb570f1236901cba87124e8a45f2f1470754 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 31 Aug 2026 21:21:38 +0000 Subject: [PATCH] =?UTF-8?q?fix(relink):=20skill=5Fprefix=20patches=20the?= =?UTF-8?q?=20gbrain=20render=20too=20=E2=80=94=20the=20file=20the=20host?= =?UTF-8?q?=20actually=20serves=20(#2738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gstack-relink linked SKILL.md from RENDER_DIR when a gbrain render was active but ran gstack-patch-names only on INSTALL_DIR, so the served frontmatter kept the unprefixed name and skill_prefix=true silently no-oped for every brain-aware skill. The render tree (user-owned, untracked) is now patched too; gstack-patch-names is idempotent so repeat relinks never double-prefix. The gen-skill-docs note that pointed users at relink now describes what relink actually covers. Receipt: the new test fails on a v1.77.0.0 scratch worktree (served render keeps 'name: qa'). Fixes #2738 Co-Authored-By: Claude Fable 5 --- bin/gstack-relink | 8 +++++++- scripts/gen-skill-docs.ts | 2 +- test/relink.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/bin/gstack-relink b/bin/gstack-relink index e87bc3fac..7a3b4b0e6 100755 --- a/bin/gstack-relink +++ b/bin/gstack-relink @@ -110,8 +110,14 @@ for skill_dir in "$INSTALL_DIR"/*/; do SKILL_COUNT=$((SKILL_COUNT + 1)) done -# Patch SKILL.md name: fields to match prefix setting +# Patch SKILL.md name: fields to match prefix setting. When a gbrain render +# is active the loop above links SKILL.md from RENDER_DIR — the file the host +# actually serves — so patch THAT tree too or skill_prefix=true is a no-op +# for every brain-aware skill (#2738). gstack-patch-names takes an arbitrary +# root, skips already-prefixed names (idempotent), and the render dir is +# user-owned and untracked, so patching it never dirties a checkout. "$INSTALL_DIR/bin/gstack-patch-names" "$INSTALL_DIR" "$PREFIX" +[ -d "$RENDER_DIR" ] && "$INSTALL_DIR/bin/gstack-patch-names" "$RENDER_DIR" "$PREFIX" if [ "$PREFIX" = "true" ]; then echo "Relinked $SKILL_COUNT skills as gstack-*" diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index bbf5c80ee..c015f3be2 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -1146,7 +1146,7 @@ if (!DRY_RUN) { if (fs.existsSync(configPath)) { const config = fs.readFileSync(configPath, 'utf-8'); if (/^skill_prefix:\s*true/m.test(config)) { - console.log('\nNote: skill_prefix is true. Run gstack-relink to re-apply name: patches.'); + console.log('\nNote: skill_prefix is true. Run gstack-relink to re-apply name: patches (it patches both the install and any active gbrain render).'); } } } catch { /* non-fatal */ } diff --git a/test/relink.test.ts b/test/relink.test.ts index 767db6006..8f18d7281 100644 --- a/test/relink.test.ts +++ b/test/relink.test.ts @@ -298,6 +298,44 @@ describe('gstack-relink (#578)', () => { ); }); + // #2738: with skill_prefix=true AND an active gbrain render, the symlink + // target is the RENDER copy — so the render's `name:` must get the gstack- + // prefix too, or the served frontmatter stays unprefixed and skill_prefix + // silently no-ops for every brain-aware skill. + test('skill_prefix=true patches the rendered SKILL.md name too (#2738)', () => { + setupMockInstall(['qa']); + const renderDir = path.join(tmpDir, 'render', 'claude', 'qa'); + fs.mkdirSync(renderDir, { recursive: true }); + fs.writeFileSync( + path.join(renderDir, 'SKILL.md'), + '---\nname: qa\ndescription: test\n---\nrendered brain-aware qa', + ); + + run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix true`, { + GSTACK_INSTALL_DIR: installDir, + GSTACK_SKILLS_DIR: skillsDir, + GSTACK_HOME: tmpDir, + }); + run(`${path.join(installDir, 'bin', 'gstack-relink')}`, { + GSTACK_INSTALL_DIR: installDir, + GSTACK_SKILLS_DIR: skillsDir, + GSTACK_HOME: tmpDir, + }); + + const served = path.join(skillsDir, 'gstack-qa', 'SKILL.md'); + expect(fs.readlinkSync(served)).toBe(path.join(renderDir, 'SKILL.md')); + // The SERVED file (the render) carries the prefixed name. + expect(fs.readFileSync(served, 'utf-8')).toContain('name: gstack-qa'); + // Idempotent: a second relink must not double-prefix. + run(`${path.join(installDir, 'bin', 'gstack-relink')}`, { + GSTACK_INSTALL_DIR: installDir, + GSTACK_SKILLS_DIR: skillsDir, + GSTACK_HOME: tmpDir, + }); + expect(fs.readFileSync(served, 'utf-8')).toContain('name: gstack-qa'); + expect(fs.readFileSync(served, 'utf-8')).not.toContain('gstack-gstack-'); + }); + // FIRST INSTALL: --no-prefix must create ONLY flat names, zero gstack-* pollution test('first install --no-prefix: only flat names exist, zero gstack-* entries', () => { setupMockInstall(['qa', 'ship', 'review', 'plan-ceo-review', 'gstack-upgrade']);