From ba979dbd6f1ab470c88a4ef8bf80cd08c330cf00 Mon Sep 17 00:00:00 2001 From: Stefan Andrei <89592870+sneakygriff@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:46:29 +0300 Subject: [PATCH] fix(gen-skill-docs): quote YAML inline scalars containing '...' (Bun strict parser breaks on bare ellipsis) A bare ... inside a plain YAML scalar is a document-end marker that strict YAML parsers (Bun.YAML among them) reject mid-scalar. catalog-trim truncation appends '...' to any description whose lead exceeds 200 chars, so any truncated description would generate a SKILL.md with unparseable frontmatter. Add the ellipsis test to toYamlInlineScalar's needsQuote so such scalars are emitted double-quoted, plus unit coverage for the quoting rules. Co-Authored-By: Claude Fable 5 --- scripts/gen-skill-docs.ts | 1 + test/catalog-trim.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index b348c5162..fbff381b7 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -413,6 +413,7 @@ export function toYamlInlineScalar(s: string): string { s !== s.trim() || // leading/trailing whitespace /:(\s|$)/.test(s) || // "foo: bar" / trailing colon → mapping ambiguity /\s#/.test(s) || // " #" → inline comment + /\.\.\./.test(s) || // "..." → document-end marker; strict parsers reject mid-scalar (catalog-trim truncation appends it) /^[\s>|&*!%@`"'#,\[\]{}?-]/.test(s); // leading YAML indicator char return needsQuote ? JSON.stringify(s) : s; } diff --git a/test/catalog-trim.test.ts b/test/catalog-trim.test.ts index 6ff8cb4dd..79380c341 100644 --- a/test/catalog-trim.test.ts +++ b/test/catalog-trim.test.ts @@ -23,8 +23,37 @@ import { buildTrimmedDescription, buildWhenToInvokeSection, applyCatalogTrim, + toYamlInlineScalar, } from '../scripts/gen-skill-docs'; +describe('toYamlInlineScalar', () => { + const parses = (out: string) => Bun.YAML.parse(`d: ${out}`); + + test("scalar containing '...' (YAML document-end marker) is quoted and round-trips", () => { + const out = toYamlInlineScalar('Truncated lead ends with... more (gstack)'); + expect(out.startsWith('"')).toBe(true); + expect((parses(out) as { d: string }).d).toContain('...'); + }); + + test("interior ': ' is quoted (nested-mapping ambiguity, #1778)", () => { + const out = toYamlInlineScalar('Ship workflow: detect and merge'); + expect(out.startsWith('"')).toBe(true); + expect((parses(out) as { d: string }).d).toBe('Ship workflow: detect and merge'); + }); + + test('plain safe scalar passes through unquoted', () => { + expect(toYamlInlineScalar('Simple description here')).toBe('Simple description here'); + }); + + test('leading YAML indicator char is quoted', () => { + expect(toYamlInlineScalar('- leading dash').startsWith('"')).toBe(true); + }); + + test('trailing whitespace is quoted', () => { + expect(toYamlInlineScalar('has trailing space ').startsWith('"')).toBe(true); + }); +}); + describe('splitCatalogDescription', () => { test('extracts lead sentence + routing prose from simple multi-line description', () => { const desc =