From ac8e30cce6ea97e62abab14ca4083d25a3528e0d Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 21:51:27 +0000 Subject: [PATCH] C4: rewrite section refs on hosts whose root stays a literal ~/ Kiro, OpenCode, Cursor and Copilot render ~/.claude/skills/gstack as their own literal root, not $GSTACK_ROOT, so plan-eng-review's self-check pointer escaped the rewrite (found by the setup lane's installed-pointer check). --- scripts/resolvers/sections.ts | 7 ++++--- test/external-host-carve.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/resolvers/sections.ts b/scripts/resolvers/sections.ts index da6e51374..df3eb7077 100644 --- a/scripts/resolvers/sections.ts +++ b/scripts/resolvers/sections.ts @@ -38,13 +38,14 @@ export function usesLazySections(host: Host, skill: string): boolean { /** * External hosts: a template's literal `~/.claude/skills/gstack//sections/` - * reference arrives here as `$GSTACK_ROOT//sections/`, which external - * runtime roots do not have. For a carved skill, point it at the installed copy; + * reference arrives here as `$GSTACK_ROOT/...` or `~//...`, and external + * runtime roots have no section trees. For a carved skill, point it at the installed copy; * for this skill's own section inlined by {{SECTION}}, point at the inlined text. */ export function rewriteCarvedSectionRefs(content: string, ctx: TemplateContext): string { if (ctx.host === 'claude') return content; - return content.replace(/`\$GSTACK_ROOT\/([a-z0-9-]+)\/sections\/([A-Za-z0-9._-]+)`/g, (match, skill: string, file: string) => { + // The root arrives as $GSTACK_ROOT or as the host's literal ~/ (path rewrites). + return content.replace(/`(?:\$GSTACK_ROOT|~\/[\w./-]*?skills\/gstack)\/([a-z0-9-]+)\/sections\/([A-Za-z0-9._-]+)`/g, (match, skill: string, file: string) => { if (!fs.existsSync(path.join(ROOT, skill, 'sections', `${file}.tmpl`))) return match; if (usesLazySections(ctx.host, skill)) return `\`sections/${file}\` relative to the installed \`gstack-${skill}\` SKILL.md directory`; const entry = loadManifest(skill).sections.find(e => e.file === file); diff --git a/test/external-host-carve.test.ts b/test/external-host-carve.test.ts index b9c36d9a4..dedf269e1 100644 --- a/test/external-host-carve.test.ts +++ b/test/external-host-carve.test.ts @@ -83,11 +83,11 @@ describe('C4: ship and plan-ceo-review are carved on every external host', () => }); } - test('no external render points at $GSTACK_ROOT//sections/ (runtime roots have no section trees)', () => { + test('no external render points at //sections/ (runtime roots have no section trees)', () => { const offenders: string[] = []; for (const config of EXTERNAL) { for (const file of skillFiles(path.join(renders.all, config.hostSubdir))) { - const hits = fs.readFileSync(file, 'utf8').match(/\$GSTACK_ROOT\/[a-z0-9-]+\/sections\/[\w.-]+/g) ?? []; + const hits = fs.readFileSync(file, 'utf8').match(/(?:\$GSTACK_ROOT|~\/[\w./-]*skills\/gstack)\/[a-z0-9-]+\/sections\/[\w.-]+/g) ?? []; offenders.push(...hits.map(hit => `${path.relative(renders.all, file)}: ${hit}`)); } }