From 3728f3890587eaa356c37940d4c64751860d3982 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 14 Jul 2026 12:57:33 -0700 Subject: [PATCH 1/3] fix(skill-check): honor primary host exclusions --- scripts/skill-check.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/skill-check.ts b/scripts/skill-check.ts index 9182737ee1..4f511400da 100644 --- a/scripts/skill-check.ts +++ b/scripts/skill-check.ts @@ -10,6 +10,7 @@ import { validateSkill } from '../test/helpers/skill-parser'; import { discoverTemplates, discoverSkillFiles } from './discover-skills'; +import { claude as PRIMARY_HOST } from '../hosts/index'; import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; @@ -64,15 +65,21 @@ for (const file of SKILL_FILES) { console.log('\n Templates:'); const TEMPLATES = discoverTemplates(ROOT); +const PRIMARY_SKIP_SKILLS = new Set(PRIMARY_HOST.generation.skipSkills ?? []); for (const { tmpl, output } of TEMPLATES) { const tmplPath = path.join(ROOT, tmpl); const outPath = path.join(ROOT, output); + const skillDir = output.split('/')[0]; if (!fs.existsSync(tmplPath)) { console.log(` \u26a0\ufe0f ${output.padEnd(30)} — no template`); continue; } if (!fs.existsSync(outPath)) { + if (PRIMARY_SKIP_SKILLS.has(skillDir)) { + console.log(` - ${output.padEnd(30)} — skipped per ${PRIMARY_HOST.name} host config`); + continue; + } hasErrors = true; console.log(` \u274c ${output.padEnd(30)} — generated file missing! Run: bun run gen:skill-docs`); continue; From ae814ebf86b3bd3a307799ea879301a234d4bf08 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 14 Jul 2026 12:59:41 -0700 Subject: [PATCH 2/3] fix(codex): resolve qa-only from generated skill root --- hosts/codex.ts | 1 + test/fixtures/golden/codex-ship-SKILL.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hosts/codex.ts b/hosts/codex.ts index 7dc80ea877..1b99e8c911 100644 --- a/hosts/codex.ts +++ b/hosts/codex.ts @@ -29,6 +29,7 @@ const codex: HostConfig = { { from: '.claude/skills/gstack', to: '.agents/skills/gstack' }, { from: '.claude/skills/review', to: '.agents/skills/gstack/review' }, { from: '.claude/skills', to: '.agents/skills' }, + { from: '${CLAUDE_SKILL_DIR}/../qa-only/SKILL.md', to: '$GSTACK_ROOT/../gstack-qa-only/SKILL.md' }, ], suppressedResolvers: [ diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index d99630c4b3..91ac1fadb7 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -1826,7 +1826,7 @@ curl -s -o /dev/null -w '%{http_code}' http://localhost:4000 2>/dev/null || echo Read the `/qa-only` skill from disk: ```bash -cat ${CLAUDE_SKILL_DIR}/../qa-only/SKILL.md +cat $GSTACK_ROOT/../gstack-qa-only/SKILL.md ``` **If unreadable:** Skip with "Could not load /qa-only — skipping plan verification." From bb68b1c2763db674542eb89b96312592d56444a4 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 14 Jul 2026 13:08:43 -0700 Subject: [PATCH 3/3] fix(hermes): namespace generated skill identities --- hosts/hermes.ts | 1 + scripts/gen-skill-docs.ts | 11 ++++++----- scripts/host-config.ts | 2 ++ test/gen-skill-docs.test.ts | 9 +++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/hosts/hermes.ts b/hosts/hermes.ts index 43598989df..7687eda053 100644 --- a/hosts/hermes.ts +++ b/hosts/hermes.ts @@ -15,6 +15,7 @@ const hermes: HostConfig = { mode: 'allowlist', keepFields: ['name', 'description'], descriptionLimit: null, + nameTransform: 'external-skill-name', }, generation: { diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 71aa1a34ca..e8e1057d0d 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -507,7 +507,7 @@ policy: * Codex: keeps name + description only, enforces 1024-char limit. * Factory: keeps name + description + user-invocable, conditionally adds disable-model-invocation. */ -function transformFrontmatter(content: string, host: Host): string { +function transformFrontmatter(content: string, host: Host, externalName?: string): string { const hostConfig = getHostConfig(host); const fm = hostConfig.frontmatter; @@ -531,6 +531,7 @@ function transformFrontmatter(content: string, host: Host): string { const frontmatter = content.slice(fmStart + 4, fmEnd); const body = content.slice(fmEnd + 4); const { name, description } = extractNameAndDescription(content); + const emittedName = fm.nameTransform === 'external-skill-name' && externalName ? externalName : name; // Description limit enforcement if (fm.descriptionLimit) { @@ -538,11 +539,11 @@ function transformFrontmatter(content: string, host: Host): string { if (description.length > fm.descriptionLimit) { if (behavior === 'error') { throw new Error( - `${hostConfig.displayName} description for "${name}" is ${description.length} chars (max ${fm.descriptionLimit}). ` + + `${hostConfig.displayName} description for "${emittedName}" is ${description.length} chars (max ${fm.descriptionLimit}). ` + `Compress the description in the .tmpl file.` ); } else if (behavior === 'warn') { - console.warn(`WARNING: ${hostConfig.displayName} description for "${name}" exceeds ${fm.descriptionLimit} chars`); + console.warn(`WARNING: ${hostConfig.displayName} description for "${emittedName}" exceeds ${fm.descriptionLimit} chars`); } // 'truncate' — silently proceed } @@ -550,7 +551,7 @@ function transformFrontmatter(content: string, host: Host): string { // Build frontmatter with allowed fields const indentedDesc = description.split('\n').map(l => ` ${l}`).join('\n'); - let newFm = `---\nname: ${name}\ndescription: |\n${indentedDesc}\n`; + let newFm = `---\nname: ${emittedName}\ndescription: |\n${indentedDesc}\n`; // Add extra fields (host-wide) if (fm.extraFields) { @@ -772,7 +773,7 @@ function processExternalHost( const safetyProse = extractHookSafetyProse(tmplContent); // Transform frontmatter (host-aware) - let result = transformFrontmatter(content, host); + let result = transformFrontmatter(content, host, name); // Insert safety advisory at the top of the body (after frontmatter) if (safetyProse) { diff --git a/scripts/host-config.ts b/scripts/host-config.ts index 4421c4a799..0d45e43e82 100644 --- a/scripts/host-config.ts +++ b/scripts/host-config.ts @@ -46,6 +46,8 @@ export interface HostConfig { descriptionLimit?: number | null; /** What to do when description exceeds limit. Default: 'error'. */ descriptionLimitBehavior?: 'error' | 'truncate' | 'warn'; + /** Use the generated external directory name as frontmatter name. */ + nameTransform?: 'external-skill-name'; /** Additional frontmatter fields to inject (host-wide). */ extraFields?: Record; /** Rename fields from template (e.g., { 'voice-triggers': 'triggers' }). */ diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 2fb783ffd0..7645f69d3a 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -2195,6 +2195,15 @@ describe('Parameterized host smoke tests', () => { } }); + if (hostConfig.name === 'hermes') { + test('frontmatter names match namespaced Hermes directories', () => { + for (const skill of ['gstack', 'gstack-review', 'gstack-qa', 'gstack-ship']) { + const content = fs.readFileSync(path.join(hostDir, skill, 'SKILL.md'), 'utf-8'); + expect(content).toMatch(new RegExp(`^name: ${skill}$`, 'm')); + } + }); + } + test('generates Claude outside-voice skill for external hosts', () => { const skillMd = path.join(hostDir, 'gstack-claude', 'SKILL.md'); expect(fs.existsSync(skillMd)).toBe(true);