From 012c9b2bdfa1574134b54bf4adcb919a5d395a86 Mon Sep 17 00:00:00 2001 From: cuddly-life <103327635+cuddly-life@users.noreply.github.com> Date: Sun, 26 Jul 2026 20:28:57 -0700 Subject: [PATCH 1/2] fix: trim Codex skill catalog --- scripts/gen-skill-docs.ts | 8 ++- scripts/resolvers/codex-helpers.ts | 2 +- test/catalog-mode-full.test.ts | 2 +- test/fixtures/golden/codex-ship-SKILL.md | 16 +++-- test/gen-skill-docs.test.ts | 76 ++++++++++++------------ 5 files changed, 54 insertions(+), 50 deletions(-) diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 71aa1a34ca..0062d28b90 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -495,7 +495,7 @@ function generateOpenAIYaml(displayName: string, shortDescription: string): stri return `interface: display_name: ${JSON.stringify(displayName)} short_description: ${JSON.stringify(shortDescription)} - default_prompt: ${JSON.stringify(`Use ${displayName} for this task.`)} + default_prompt: ${JSON.stringify(`Use $${displayName} for this task.`)} policy: allow_implicit_invocation: true `; @@ -853,9 +853,11 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: content = header + content; } - // Catalog trim (Claude only — external hosts have their own frontmatter shapes) + // Catalog trim for hosts whose skill catalogs consume SKILL.md descriptions. + // Codex uses the same description field after its host-specific transform, so + // keep routing/voice prose in the body instead of spending catalog budget on it. let catalogParts: CatalogParts | null = null; - if (host === 'claude' && CATALOG_MODE === 'trim') { + if ((host === 'claude' || host === 'codex') && CATALOG_MODE === 'trim') { const trimmed = applyCatalogTrim(content, skillName); if (trimmed) { content = trimmed.content; diff --git a/scripts/resolvers/codex-helpers.ts b/scripts/resolvers/codex-helpers.ts index 0471689012..a725652596 100644 --- a/scripts/resolvers/codex-helpers.ts +++ b/scripts/resolvers/codex-helpers.ts @@ -55,7 +55,7 @@ export function generateOpenAIYaml(displayName: string, shortDescription: string return `interface: display_name: ${JSON.stringify(displayName)} short_description: ${JSON.stringify(shortDescription)} - default_prompt: ${JSON.stringify(`Use ${displayName} for this task.`)} + default_prompt: ${JSON.stringify(`Use $${displayName} for this task.`)} policy: allow_implicit_invocation: true `; diff --git a/test/catalog-mode-full.test.ts b/test/catalog-mode-full.test.ts index c964f35abc..d21b551143 100644 --- a/test/catalog-mode-full.test.ts +++ b/test/catalog-mode-full.test.ts @@ -4,7 +4,7 @@ * The catalog trim is the default. The opt-out (`--catalog-mode=full`) * preserves v1.44 multi-line frontmatter descriptions for users / hosts * that depend on the legacy fat catalog. Without this test, someone could - * break the conditional `if (host === 'claude' && CATALOG_MODE === 'trim')` + * break the conditional that gates supported hosts on `CATALOG_MODE === 'trim'` * and silently turn the opt-out path into a no-op — users with the flag * still get trim'd output, the v1.44 behavior is gone. * diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index d99630c4b3..267288f36b 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -1,15 +1,19 @@ --- name: ship -description: | - Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, - update CHANGELOG, commit, push, create PR. Use when asked to "ship", "deploy", - "push to main", "create a PR", "merge and push", or "get it deployed". - Proactively invoke this skill (do NOT push/PR directly) when the user says code - is ready, asks about deploying, wants to push code up, or asks to create a PR. (gstack) +description: "Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR. (gstack)" + --- + +## When to invoke this skill + +Use when asked to "ship", "deploy", +"push to main", "create a PR", "merge and push", or "get it deployed". +Proactively invoke this skill (do NOT push/PR directly) when the user says code +is ready, asks about deploying, wants to push code up, or asks to create a PR. + ## Preamble (run first) ```bash diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 2fb783ffd0..e0445c4fd4 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -7,6 +7,8 @@ import * as os from 'os'; const ROOT = path.resolve(import.meta.dir, '..'); const MAX_SKILL_DESCRIPTION_LENGTH = 1024; +const MAX_CODEX_CATALOG_DESCRIPTION_LENGTH = 240; +const MAX_CODEX_CATALOG_TOTAL_DESCRIPTION_LENGTH = 6000; // Carved-skill aware (v2 plan T9): ship is now a skeleton SKILL.md + sections/*.md. // Read the union so assertions about content that MOVED into a section still pass. @@ -30,32 +32,8 @@ function extractDescription(content: string): string { const fmEnd = content.indexOf('\n---', 4); expect(fmEnd).toBeGreaterThan(0); const frontmatter = content.slice(4, fmEnd); - const lines = frontmatter.split('\n'); - let description = ''; - 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; - } - } - } - - if (descLines.length > 0) { - description = descLines.join('\n').trim(); - } - return description; + const parsed = Bun.YAML.parse(frontmatter) as { description?: unknown }; + return typeof parsed.description === 'string' ? parsed.description.trim() : ''; } function extractMarkdownSection(content: string, heading: string): string { @@ -222,7 +200,7 @@ describe('gen-skill-docs', () => { expect(fs.existsSync(path.join(ROOT, 'claude', 'SKILL.md'))).toBe(false); }); - test(`every Codex SKILL.md description stays within ${MAX_SKILL_DESCRIPTION_LENGTH} chars`, () => { + test(`every Codex SKILL.md description stays within ${MAX_CODEX_CATALOG_DESCRIPTION_LENGTH} chars`, () => { const agentsDir = path.join(ROOT, '.agents', 'skills'); if (!fs.existsSync(agentsDir)) return; // skip if not generated for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) { @@ -231,12 +209,27 @@ describe('gen-skill-docs', () => { if (!fs.existsSync(skillMd)) continue; const content = fs.readFileSync(skillMd, 'utf-8'); const description = extractDescription(content); - expect(description.length).toBeLessThanOrEqual(MAX_SKILL_DESCRIPTION_LENGTH); + expect(description.length).toBeLessThanOrEqual(MAX_CODEX_CATALOG_DESCRIPTION_LENGTH); } }); - test('every Codex SKILL.md description stays under 900-char warning threshold', () => { - const WARN_THRESHOLD = 900; + test(`Codex catalog descriptions stay within ${MAX_CODEX_CATALOG_TOTAL_DESCRIPTION_LENGTH} chars total`, () => { + const agentsDir = path.join(ROOT, '.agents', 'skills'); + if (!fs.existsSync(agentsDir)) return; + const lengths: number[] = []; + for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const skillMd = path.join(agentsDir, entry.name, 'SKILL.md'); + if (!fs.existsSync(skillMd)) continue; + lengths.push(extractDescription(fs.readFileSync(skillMd, 'utf-8')).length); + } + + expect(lengths.length).toBeGreaterThan(0); + expect(lengths.reduce((sum, length) => sum + length, 0)) + .toBeLessThanOrEqual(MAX_CODEX_CATALOG_TOTAL_DESCRIPTION_LENGTH); + }); + + test('every Codex SKILL.md keeps routing guidance out of catalog descriptions', () => { const agentsDir = path.join(ROOT, '.agents', 'skills'); if (!fs.existsSync(agentsDir)) return; const violations: string[] = []; @@ -246,8 +239,8 @@ describe('gen-skill-docs', () => { if (!fs.existsSync(skillMd)) continue; const content = fs.readFileSync(skillMd, 'utf-8'); const description = extractDescription(content); - if (description.length > WARN_THRESHOLD) { - violations.push(`${entry.name}: ${description.length} chars (limit ${MAX_SKILL_DESCRIPTION_LENGTH}, ${MAX_SKILL_DESCRIPTION_LENGTH - description.length} remaining)`); + if (/\b(?:Use when|Proactively suggest when|Voice triggers):/i.test(description)) { + violations.push(entry.name); } } expect(violations).toEqual([]); @@ -1733,9 +1726,11 @@ describe('Codex generation (--host codex)', () => { const fmEnd = content.indexOf('\n---', 4); expect(fmEnd).toBeGreaterThan(0); const frontmatter = content.slice(4, fmEnd); + const parsed = Bun.YAML.parse(frontmatter) as Record; // Must have name and description expect(frontmatter).toContain('name:'); expect(frontmatter).toContain('description:'); + expect(Object.keys(parsed).sort()).toEqual(['description', 'name']); // Must NOT have allowed-tools, version, or hooks expect(frontmatter).not.toContain('allowed-tools:'); expect(frontmatter).not.toContain('version:'); @@ -1743,13 +1738,14 @@ describe('Codex generation (--host codex)', () => { } }); - test('all Codex skills have agents/openai.yaml metadata', () => { + test('all generated Codex skills keep implicit routing and valid explicit prompts', () => { for (const skill of CODEX_SKILLS) { const metadata = path.join(AGENTS_DIR, skill.codexName, 'agents', 'openai.yaml'); expect(fs.existsSync(metadata)).toBe(true); const content = fs.readFileSync(metadata, 'utf-8'); expect(content).toContain(`display_name: "${skill.codexName}"`); expect(content).toContain('short_description:'); + expect(content).toContain(`default_prompt: "Use $${skill.codexName} for this task."`); expect(content).toContain('allow_implicit_invocation: true'); } }); @@ -1829,16 +1825,18 @@ describe('Codex generation (--host codex)', () => { expect(codexResult.stdout.toString()).toBe(agentsResult.stdout.toString()); }); - test('multiline descriptions preserved in Codex output', () => { - // office-hours has a multiline description — verify it survives the frontmatter transform + test('Codex catalog trim moves routing prose into the skill body', () => { + // office-hours has a long routed description in its template. 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); - // Description should span multiple lines (block scalar) - const descLines = frontmatter.split('\n').filter(l => l.startsWith(' ')); - expect(descLines.length).toBeGreaterThan(1); - // Verify key phrases survived + const description = extractDescription(content); + + expect(description.length).toBeLessThanOrEqual(MAX_CODEX_CATALOG_DESCRIPTION_LENGTH); expect(frontmatter).toContain('YC Office Hours'); + expect(frontmatter).not.toMatch(/\bUse when\b/i); + expect(content).toContain('## When to invoke this skill'); + expect(content).toMatch(/\bUse when\b/i); }); test('hook skills have safety prose and no hooks: in frontmatter', () => { From 07b872100c9e99ab85409bdbdaa63295f08b808d Mon Sep 17 00:00:00 2001 From: cuddly-life <103327635+cuddly-life@users.noreply.github.com> Date: Sun, 26 Jul 2026 20:36:02 -0700 Subject: [PATCH 2/2] chore: bump version and changelog (v1.60.2.0) --- CHANGELOG.md | 17 +++++++++++++++++ VERSION | 2 +- package.json | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351e7cabde..27219279ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.60.2.0] - 2026-07-26 + +## **Codex can load the full gstack catalog without truncating skill descriptions.** + +Codex installs now receive the same compact catalog descriptions as Claude while +keeping natural-language routing and proactive guidance inside each skill body. +The generated 54-skill catalog uses 3,854 description characters, and explicit +invocations now use Codex's `$skill-name` syntax. + +### Fixed + +- Apply catalog trimming to Codex generation without hiding skills from implicit + routing. +- Generate valid `$gstack-*` default prompts in OpenAI metadata. +- Enforce per-skill and aggregate Codex description budgets and verify that + routing guidance remains available after a skill loads. + ## [1.60.1.0] - 2026-07-09 ## **The /autoplan dual-voice eval is back on the board, catching real regressions.** diff --git a/VERSION b/VERSION index c4190e0048..762f175545 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.60.1.0 +1.60.2.0 diff --git a/package.json b/package.json index 846438a603..78b98a86f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.60.1.0", + "version": "1.60.2.0", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module",