From 365fce6cb1e133d15eb3d4c15231d87802789984 Mon Sep 17 00:00:00 2001 From: Santhanakrishnan Date: Fri, 20 Mar 2026 21:48:17 +0530 Subject: [PATCH] fix codex skill loading and setup layout --- .agents/skills/gstack/SKILL.md | 41 ++---------------- SKILL.md | 41 ++---------------- SKILL.md.tmpl | 41 ++---------------- scripts/gen-skill-docs.ts | 72 ++++++++++++++++++++++---------- setup | 8 ++-- test/gen-skill-docs.test.ts | 76 ++++++++++++++++++++++++++++++---- 6 files changed, 134 insertions(+), 145 deletions(-) diff --git a/.agents/skills/gstack/SKILL.md b/.agents/skills/gstack/SKILL.md index 3b4f93b5..5134982c 100644 --- a/.agents/skills/gstack/SKILL.md +++ b/.agents/skills/gstack/SKILL.md @@ -1,43 +1,10 @@ --- name: gstack description: | - Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with - elements, verify page state, diff before/after actions, take annotated screenshots, check - responsive layouts, test forms and uploads, handle dialogs, and assert element states. - ~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a - user flow, or file a bug with evidence. - - gstack also includes development workflow skills. When you notice the user is at - these stages, suggest the appropriate skill: - - Brainstorming a new idea → suggest /office-hours - - Reviewing a plan (strategy) → suggest /plan-ceo-review - - Reviewing a plan (architecture) → suggest /plan-eng-review - - Reviewing a plan (design) → suggest /plan-design-review - - Creating a design system → suggest /design-consultation - - Debugging errors → suggest /investigate - - Testing the app → suggest /qa - - Code review before merge → suggest /review - - Visual design audit → suggest /design-review - - Ready to deploy / create PR → suggest /ship - - Post-ship doc updates → suggest /document-release - - Weekly retrospective → suggest /retro - - Wanting a second opinion or adversarial code review → suggest /codex - - Working with production or live systems → suggest /careful - - Want to scope edits to one module/directory → suggest /freeze - - Maximum safety mode (destructive warnings + edit restrictions) → suggest /guard - - Removing edit restrictions → suggest /unfreeze - - Upgrading gstack to latest version → suggest /gstack-upgrade - - If the user pushes back on skill suggestions ("stop suggesting things", - "I don't need suggestions", "too aggressive"): - 1. Stop suggesting for the rest of this session - 2. Run: gstack-config set proactive false - 3. Say: "Got it — I'll stop suggesting skills. Just tell me to be proactive - again if you change your mind." - - If the user says "be proactive again" or "turn on suggestions": - 1. Run: gstack-config set proactive true - 2. Say: "Proactive suggestions are back on." + Fast headless browser plus workflow router for the gstack skill bundle. Use it for QA + testing, site dogfooding, screenshots, responsive checks, and routing into focused skills + like /office-hours, /investigate, /review, /qa, /ship, and /design-review when the task + clearly matches. --- diff --git a/SKILL.md b/SKILL.md index fe66b618..595b9d67 100644 --- a/SKILL.md +++ b/SKILL.md @@ -2,43 +2,10 @@ name: gstack version: 1.1.0 description: | - Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with - elements, verify page state, diff before/after actions, take annotated screenshots, check - responsive layouts, test forms and uploads, handle dialogs, and assert element states. - ~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a - user flow, or file a bug with evidence. - - gstack also includes development workflow skills. When you notice the user is at - these stages, suggest the appropriate skill: - - Brainstorming a new idea → suggest /office-hours - - Reviewing a plan (strategy) → suggest /plan-ceo-review - - Reviewing a plan (architecture) → suggest /plan-eng-review - - Reviewing a plan (design) → suggest /plan-design-review - - Creating a design system → suggest /design-consultation - - Debugging errors → suggest /investigate - - Testing the app → suggest /qa - - Code review before merge → suggest /review - - Visual design audit → suggest /design-review - - Ready to deploy / create PR → suggest /ship - - Post-ship doc updates → suggest /document-release - - Weekly retrospective → suggest /retro - - Wanting a second opinion or adversarial code review → suggest /codex - - Working with production or live systems → suggest /careful - - Want to scope edits to one module/directory → suggest /freeze - - Maximum safety mode (destructive warnings + edit restrictions) → suggest /guard - - Removing edit restrictions → suggest /unfreeze - - Upgrading gstack to latest version → suggest /gstack-upgrade - - If the user pushes back on skill suggestions ("stop suggesting things", - "I don't need suggestions", "too aggressive"): - 1. Stop suggesting for the rest of this session - 2. Run: gstack-config set proactive false - 3. Say: "Got it — I'll stop suggesting skills. Just tell me to be proactive - again if you change your mind." - - If the user says "be proactive again" or "turn on suggestions": - 1. Run: gstack-config set proactive true - 2. Say: "Proactive suggestions are back on." + Fast headless browser plus workflow router for the gstack skill bundle. Use it for QA + testing, site dogfooding, screenshots, responsive checks, and routing into focused skills + like /office-hours, /investigate, /review, /qa, /ship, and /design-review when the task + clearly matches. allowed-tools: - Bash - Read diff --git a/SKILL.md.tmpl b/SKILL.md.tmpl index 0c985965..dc83f0f3 100644 --- a/SKILL.md.tmpl +++ b/SKILL.md.tmpl @@ -2,43 +2,10 @@ name: gstack version: 1.1.0 description: | - Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with - elements, verify page state, diff before/after actions, take annotated screenshots, check - responsive layouts, test forms and uploads, handle dialogs, and assert element states. - ~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a - user flow, or file a bug with evidence. - - gstack also includes development workflow skills. When you notice the user is at - these stages, suggest the appropriate skill: - - Brainstorming a new idea → suggest /office-hours - - Reviewing a plan (strategy) → suggest /plan-ceo-review - - Reviewing a plan (architecture) → suggest /plan-eng-review - - Reviewing a plan (design) → suggest /plan-design-review - - Creating a design system → suggest /design-consultation - - Debugging errors → suggest /investigate - - Testing the app → suggest /qa - - Code review before merge → suggest /review - - Visual design audit → suggest /design-review - - Ready to deploy / create PR → suggest /ship - - Post-ship doc updates → suggest /document-release - - Weekly retrospective → suggest /retro - - Wanting a second opinion or adversarial code review → suggest /codex - - Working with production or live systems → suggest /careful - - Want to scope edits to one module/directory → suggest /freeze - - Maximum safety mode (destructive warnings + edit restrictions) → suggest /guard - - Removing edit restrictions → suggest /unfreeze - - Upgrading gstack to latest version → suggest /gstack-upgrade - - If the user pushes back on skill suggestions ("stop suggesting things", - "I don't need suggestions", "too aggressive"): - 1. Stop suggesting for the rest of this session - 2. Run: gstack-config set proactive false - 3. Say: "Got it — I'll stop suggesting skills. Just tell me to be proactive - again if you change your mind." - - If the user says "be proactive again" or "turn on suggestions": - 1. Run: gstack-config set proactive true - 2. Say: "Proactive suggestions are back on." + Fast headless browser plus workflow router for the gstack skill bundle. Use it for QA + testing, site dogfooding, screenshots, responsive checks, and routing into focused skills + like /office-hours, /investigate, /review, /qa, /ship, and /design-review when the task + clearly matches. allowed-tools: - Bash - Read diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 53e8834f..a900624f 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -16,6 +16,7 @@ import * as path from 'path'; const ROOT = path.resolve(import.meta.dir, '..'); const DRY_RUN = process.argv.includes('--dry-run'); +const MAX_SKILL_DESCRIPTION_CHARS = 1024; // ─── Template Context ─────────────────────────────────────── @@ -1437,57 +1438,79 @@ function codexSkillName(skillDir: string): string { return `gstack-${skillDir}`; } -/** - * Transform frontmatter for Codex: keep only name + description. - * Strips allowed-tools, hooks, version, and all other fields. - * Handles multiline block scalar descriptions (YAML | syntax). - */ -function transformFrontmatter(content: string, host: Host): string { - if (host === 'claude') return content; - - // Find frontmatter boundaries +function splitFrontmatter(content: string): { frontmatter: string; body: string } | null { const fmStart = content.indexOf('---\n'); - if (fmStart !== 0) return content; // frontmatter must be at the start + if (fmStart !== 0) return null; const fmEnd = content.indexOf('\n---', fmStart + 4); - if (fmEnd === -1) return content; - - const frontmatter = content.slice(fmStart + 4, fmEnd); - const body = content.slice(fmEnd + 4); // includes the leading \n after --- - - // Parse name - const nameMatch = frontmatter.match(/^name:\s*(.+)$/m); - const name = nameMatch ? nameMatch[1].trim() : ''; + if (fmEnd === -1) return null; + return { + frontmatter: content.slice(fmStart + 4, fmEnd), + body: content.slice(fmEnd + 4), + }; +} - // Parse description — handle both simple and block scalar (|) formats +function extractDescriptionFromFrontmatter(frontmatter: string): string { let description = ''; const lines = frontmatter.split('\n'); let inDescription = false; const descLines: string[] = []; + for (const line of lines) { if (line.match(/^description:\s*\|?\s*$/)) { - // Block scalar start: "description: |" or "description:" inDescription = true; continue; } if (line.match(/^description:\s*\S/)) { - // Simple inline: "description: some text" description = line.replace(/^description:\s*/, '').trim(); break; } if (inDescription) { - // Block scalar continuation — indented lines (2 spaces) or blank lines if (line === '' || line.match(/^\s/)) { descLines.push(line.replace(/^ /, '')); } else { - // End of block scalar — hit a non-indented, non-blank line break; } } } + if (descLines.length > 0) { description = descLines.join('\n').trim(); } + return description; +} + +function assertDescriptionLength(content: string, relOutput: string): void { + const parts = splitFrontmatter(content); + if (!parts) return; + + const description = extractDescriptionFromFrontmatter(parts.frontmatter); + if (description.length > MAX_SKILL_DESCRIPTION_CHARS) { + throw new Error( + `${relOutput}: description is ${description.length} chars; loaders reject descriptions over ${MAX_SKILL_DESCRIPTION_CHARS} chars. Shorten the frontmatter description.` + ); + } +} + +/** + * Transform frontmatter for Codex: keep only name + description. + * Strips allowed-tools, hooks, version, and all other fields. + * Handles multiline block scalar descriptions (YAML | syntax). + */ +function transformFrontmatter(content: string, host: Host): string { + if (host === 'claude') return content; + + const parts = splitFrontmatter(content); + if (!parts) return content; + const { frontmatter, body } = parts; + + // Parse name + const nameMatch = frontmatter.match(/^name:\s*(.+)$/m); + const name = nameMatch ? nameMatch[1].trim() : ''; + + // Parse description — handle both simple and block scalar (|) formats + const description = extractDescriptionFromFrontmatter(frontmatter); + // Re-emit Codex frontmatter (name + description only) const indentedDesc = description.split('\n').map(l => ` ${l}`).join('\n'); const codexFm = `---\nname: ${name}\ndescription: |\n${indentedDesc}\n---`; @@ -1601,6 +1624,9 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: content = header + content; } + const relOutput = path.relative(ROOT, outputPath); + assertDescriptionLength(content, relOutput); + return { outputPath, content }; } diff --git a/setup b/setup index cf3e5050..275639b8 100755 --- a/setup +++ b/setup @@ -137,6 +137,8 @@ link_codex_skill_dirs() { for skill_dir in "$agents_dir"/gstack*/; do if [ -f "$skill_dir/SKILL.md" ]; then skill_name="$(basename "$skill_dir")" + # ~/.codex/skills/gstack must stay the full repo (for bin/, browse/) — root SKILL.md covers the umbrella skill. + [ "$skill_name" = "gstack" ] && continue target="$skills_dir/$skill_name" # Create or update symlink if [ -L "$target" ] || [ ! -e "$target" ]; then @@ -191,12 +193,10 @@ if [ "$INSTALL_CODEX" -eq 1 ]; then CODEX_GSTACK="$CODEX_SKILLS/gstack" mkdir -p "$CODEX_SKILLS" - # Symlink gstack source for runtime assets (bin/, browse/dist/) - if [ -L "$CODEX_GSTACK" ] || [ ! -e "$CODEX_GSTACK" ]; then - ln -snf "$GSTACK_DIR" "$CODEX_GSTACK" - fi # Install generated Codex-format skills (not Claude source dirs) link_codex_skill_dirs "$GSTACK_DIR" "$CODEX_SKILLS" + # Must run after skill links: ~/.codex/skills/gstack is the full repo (bin/, browse/dist/, root SKILL.md). + ln -snf "$GSTACK_DIR" "$CODEX_GSTACK" echo "gstack ready (codex)." echo " browse: $BROWSE_BIN" diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 68d84465..d44ccff2 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; const ROOT = path.resolve(import.meta.dir, '..'); +const MAX_SKILL_DESCRIPTION_CHARS = 1024; // Dynamic template discovery — matches the generator's findTemplates() behavior. // New skills automatically get test coverage without updating a static list. @@ -22,6 +23,37 @@ const ALL_SKILLS = (() => { return skills; })(); +function extractFrontmatter(content: string): string { + const fmEnd = content.indexOf('\n---', 4); + expect(fmEnd).toBeGreaterThan(0); + return content.slice(4, fmEnd); +} + +function extractDescription(frontmatter: string): string { + const lines = frontmatter.split('\n'); + let inDescription = false; + const descLines: string[] = []; + + for (const line of lines) { + if (line.match(/^description:\s*\|?\s*$/)) { + inDescription = true; + continue; + } + if (line.match(/^description:\s*\S/)) { + return line.replace(/^description:\s*/, '').trim(); + } + if (inDescription) { + if (line === '' || line.match(/^\s/)) { + descLines.push(line.replace(/^ /, '')); + } else { + break; + } + } + } + + return descLines.join('\n').trim(); +} + describe('gen-skill-docs', () => { test('generated SKILL.md contains all command categories', () => { const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8'); @@ -98,6 +130,14 @@ describe('gen-skill-docs', () => { } }); + test('every generated Claude description stays within loader limits', () => { + for (const skill of ALL_SKILLS) { + const content = fs.readFileSync(path.join(ROOT, skill.dir, 'SKILL.md'), 'utf-8'); + const description = extractDescription(extractFrontmatter(content)); + expect(description.length).toBeLessThanOrEqual(MAX_SKILL_DESCRIPTION_CHARS); + } + }); + test('generated files are fresh (match --dry-run)', () => { const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--dry-run'], { cwd: ROOT, @@ -552,9 +592,7 @@ describe('Codex generation (--host codex)', () => { for (const skill of CODEX_SKILLS) { const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8'); expect(content.startsWith('---\n')).toBe(true); - const fmEnd = content.indexOf('\n---', 4); - expect(fmEnd).toBeGreaterThan(0); - const frontmatter = content.slice(4, fmEnd); + const frontmatter = extractFrontmatter(content); // Must have name and description expect(frontmatter).toContain('name:'); expect(frontmatter).toContain('description:'); @@ -619,8 +657,7 @@ describe('Codex generation (--host codex)', () => { test('multiline descriptions preserved in Codex output', () => { // office-hours has a multiline description — verify it survives the frontmatter transform const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-office-hours', 'SKILL.md'), 'utf-8'); - const fmEnd = content.indexOf('\n---', 4); - const frontmatter = content.slice(4, fmEnd); + const frontmatter = extractFrontmatter(content); // Description should span multiple lines (block scalar) const descLines = frontmatter.split('\n').filter(l => l.startsWith(' ')); expect(descLines.length).toBeGreaterThan(1); @@ -635,12 +672,19 @@ describe('Codex generation (--host codex)', () => { // Must have safety advisory prose expect(content).toContain('Safety Advisory'); // Must NOT have hooks: in frontmatter - const fmEnd = content.indexOf('\n---', 4); - const frontmatter = content.slice(4, fmEnd); + const frontmatter = extractFrontmatter(content); expect(frontmatter).not.toContain('hooks:'); } }); + test('every generated Codex description stays within loader limits', () => { + for (const skill of CODEX_SKILLS) { + const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8'); + const description = extractDescription(extractFrontmatter(content)); + expect(description.length).toBeLessThanOrEqual(MAX_SKILL_DESCRIPTION_CHARS); + } + }); + test('all Codex SKILL.md files have auto-generated header', () => { for (const skill of CODEX_SKILLS) { const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8'); @@ -782,6 +826,17 @@ describe('setup script validation', () => { expect(codexSection).not.toContain('link_claude_skill_dirs'); }); + test('Codex install relinks ~/.codex/skills/gstack after generated skill links', () => { + const codexSection = setupContent.slice( + setupContent.indexOf('# 5. Install for Codex'), + setupContent.indexOf('# 6. Create') + ); + const linkFnIndex = codexSection.indexOf('link_codex_skill_dirs'); + const repoLinkIndex = codexSection.indexOf('ln -snf "$GSTACK_DIR" "$CODEX_GSTACK"'); + expect(linkFnIndex).toBeGreaterThanOrEqual(0); + expect(repoLinkIndex).toBeGreaterThan(linkFnIndex); + }); + test('link_codex_skill_dirs reads from .agents/skills/', () => { // The Codex link function must reference .agents/skills for generated Codex skills const fnStart = setupContent.indexOf('link_codex_skill_dirs()'); @@ -791,6 +846,13 @@ describe('setup script validation', () => { expect(fnBody).toContain('gstack*'); }); + test('link_codex_skill_dirs skips the umbrella gstack symlink', () => { + const fnStart = setupContent.indexOf('link_codex_skill_dirs()'); + const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart)); + const fnBody = setupContent.slice(fnStart, fnEnd); + expect(fnBody).toContain('[ "$skill_name" = "gstack" ] && continue'); + }); + test('link_claude_skill_dirs creates relative symlinks', () => { // Claude links should be relative: ln -snf "gstack/skill_name" const fnStart = setupContent.indexOf('link_claude_skill_dirs()');