fix(relink): skill_prefix patches the gbrain render too — the file the host actually serves (#2738)

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 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-31 21:21:38 +00:00
co-authored by Claude Fable 5
parent f7e378cebe
commit e2d6cb570f
3 changed files with 46 additions and 2 deletions
+38
View File
@@ -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']);