From 55087dc31b6085c4cc020320a5effaad8d51eb28 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 28 Mar 2026 23:26:59 -0700 Subject: [PATCH] feat: use frontmatter name: for skill symlinks and Codex paths Patch all 3 name-derivation paths to read name: from SKILL.md frontmatter instead of relying solely on directory basenames. This enables directory names that differ from invocation names (e.g., run-tests/ directory with name: test). - setup: link_claude_skill_dirs reads name: via grep, falls back to basename - gen-skill-docs.ts: codexSkillName uses frontmatter name for Codex output paths - gen-skill-docs.ts: moved frontmatter extraction before Codex path logic Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/gen-skill-docs.ts | 24 ++++++++++++++---------- setup | 9 ++++++--- test/gen-skill-docs.test.ts | 5 +++-- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 09a8c2a9..68bd8de2 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -76,11 +76,13 @@ const OPENAI_LITMUS_CHECKS = [ // ─── Codex Helpers ─────────────────────────────────────────── -function codexSkillName(skillDir: string): string { - if (skillDir === '.' || skillDir === '') return 'gstack'; +function codexSkillName(skillDir: string, frontmatterName?: string): string { + // Use frontmatter name: if it differs from directory name (e.g., run-tests/ with name: test) + const baseName = frontmatterName && frontmatterName !== skillDir ? frontmatterName : skillDir; + if (baseName === '.' || baseName === '') return 'gstack'; // Don't double-prefix: gstack-upgrade → gstack-upgrade (not gstack-gstack-upgrade) - if (skillDir.startsWith('gstack-')) return skillDir; - return `gstack-${skillDir}`; + if (baseName.startsWith('gstack-')) return baseName; + return `gstack-${baseName}`; } function extractNameAndDescription(content: string): { name: string; description: string } { @@ -217,12 +219,18 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: // Determine skill directory relative to ROOT const skillDir = path.relative(ROOT, path.dirname(tmplPath)); + // Extract skill name from frontmatter early — needed for both TemplateContext and Codex output paths. + // When frontmatter name: differs from directory name (e.g., run-tests/ with name: test), + // the frontmatter name is used for Codex skill naming and setup script symlinks. + const { name: extractedName, description: extractedDescription } = extractNameAndDescription(tmplContent); + const skillName = extractedName || path.basename(path.dirname(tmplPath)); + let outputDir: string | null = null; // For codex host, route output to .agents/skills/{codexSkillName}/SKILL.md let symlinkLoop = false; if (host === 'codex') { - const codexName = codexSkillName(skillDir === '.' ? '' : skillDir); + const codexName = codexSkillName(skillDir === '.' ? '' : skillDir, extractedName || undefined); outputDir = path.join(ROOT, '.agents', 'skills', codexName); fs.mkdirSync(outputDir, { recursive: true }); outputPath = path.join(outputDir, 'SKILL.md'); @@ -243,10 +251,6 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: } } - // Extract skill name from frontmatter for TemplateContext - const { name: extractedName, description: extractedDescription } = extractNameAndDescription(tmplContent); - const skillName = extractedName || path.basename(path.dirname(tmplPath)); - // Extract benefits-from list from frontmatter (inline YAML: benefits-from: [a, b]) const benefitsMatch = tmplContent.match(/^benefits-from:\s*\[([^\]]*)\]/m); const benefitsFrom = benefitsMatch @@ -296,7 +300,7 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: content = content.replace(/\.claude\/skills/g, '.agents/skills'); if (outputDir && !symlinkLoop) { - const codexName = codexSkillName(skillDir === '.' ? '' : skillDir); + const codexName = codexSkillName(skillDir === '.' ? '' : skillDir, extractedName || undefined); const agentsDir = path.join(outputDir, 'agents'); fs.mkdirSync(agentsDir, { recursive: true }); const displayName = codexName; diff --git a/setup b/setup index e66a6df0..14e48edc 100755 --- a/setup +++ b/setup @@ -250,9 +250,12 @@ link_claude_skill_dirs() { local linked=() for skill_dir in "$gstack_dir"/*/; do if [ -f "$skill_dir/SKILL.md" ]; then - skill_name="$(basename "$skill_dir")" + dir_name="$(basename "$skill_dir")" # Skip node_modules - [ "$skill_name" = "node_modules" ] && continue + [ "$dir_name" = "node_modules" ] && continue + # Use frontmatter name: if present (e.g., run-tests/ with name: test → symlink as "test") + skill_name=$(grep -m1 '^name:' "$skill_dir/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]') + [ -z "$skill_name" ] && skill_name="$dir_name" # Apply gstack- prefix unless --no-prefix or already prefixed if [ "$SKILL_PREFIX" -eq 1 ]; then case "$skill_name" in @@ -265,7 +268,7 @@ link_claude_skill_dirs() { target="$skills_dir/$link_name" # Create or update symlink; skip if a real file/directory exists if [ -L "$target" ] || [ ! -e "$target" ]; then - ln -snf "gstack/$skill_name" "$target" + ln -snf "gstack/$dir_name" "$target" linked+=("$link_name") fi fi diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 3bbc1869..3b86453b 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1639,11 +1639,12 @@ describe('setup script validation', () => { }); test('link_claude_skill_dirs creates relative symlinks', () => { - // Claude links should be relative: ln -snf "gstack/skill_name" + // Claude links should be relative: ln -snf "gstack/$dir_name" + // Uses dir_name (not skill_name) because symlink target must point to the physical directory const fnStart = setupContent.indexOf('link_claude_skill_dirs()'); const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart)); const fnBody = setupContent.slice(fnStart, fnEnd); - expect(fnBody).toContain('ln -snf "gstack/$skill_name"'); + expect(fnBody).toContain('ln -snf "gstack/$dir_name"'); }); test('setup supports --host auto|claude|codex|kiro', () => {