mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-17 23:30:09 +02:00
fix(gen-skill-docs): quote frontmatter descriptions with interior colons (#1778)
Generated SKILL.md frontmatter emitted the catalog-trimmed description: as a plain YAML scalar. A description with an interior ": " (e.g. "Ship workflow: detect...") parses as a nested mapping under strict YAML loaders, so Codex/OpenAI skill loading rejected those skills. applyCatalogTrim now routes the value through toYamlInlineScalar, which quotes (via JSON.stringify) only when a plain scalar would be invalid — interior ": ", inline " #", leading indicator char, or surrounding whitespace. Strings that are already valid plain scalars pass through unchanged to keep regen diffs small. The frontmatter test now parses every generated block (Claude + Codex hosts) with Bun.YAML.parse instead of string-checking that name:/description: substrings exist, so the regression can't reappear. Runs under `bun test` (already in CI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -356,6 +356,28 @@ export function buildWhenToInvokeSection(parts: CatalogParts): string {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a string as a YAML inline scalar value (the text after `key: `),
|
||||
* quoting only when a plain scalar would be invalid or ambiguous.
|
||||
*
|
||||
* The bug this guards (#1778): a description like "Ship workflow: detect..."
|
||||
* emitted as a plain scalar has an interior ": " that a strict YAML parser
|
||||
* (Codex/OpenAI skill loading) reads as a nested mapping and rejects with
|
||||
* "mapping values are not allowed in this context". When quoting is needed we
|
||||
* fall back to JSON.stringify, which produces a double-quoted scalar that YAML
|
||||
* accepts verbatim (YAML is a superset of JSON for flow scalars). Strings that
|
||||
* are already valid plain scalars pass through unchanged to keep regen diffs small.
|
||||
*/
|
||||
export function toYamlInlineScalar(s: string): string {
|
||||
const needsQuote =
|
||||
s.length === 0 ||
|
||||
s !== s.trim() || // leading/trailing whitespace
|
||||
/:(\s|$)/.test(s) || // "foo: bar" / trailing colon → mapping ambiguity
|
||||
/\s#/.test(s) || // " #" → inline comment
|
||||
/^[\s>|&*!%@`"'#,\[\]{}?-]/.test(s); // leading YAML indicator char
|
||||
return needsQuote ? JSON.stringify(s) : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply catalog trim to a SKILL.md body:
|
||||
* - shorten frontmatter `description:` to lead + (gstack)
|
||||
@@ -397,8 +419,12 @@ export function applyCatalogTrim(content: string, skillName: string): { content:
|
||||
|
||||
// Replace description in frontmatter — keep trailing newline so the next
|
||||
// YAML field doesn't collide on the same line as the description value.
|
||||
// Quote the value when it would be an invalid YAML plain scalar (the common
|
||||
// case: an interior ": " like "Ship workflow: detect..." which a strict YAML
|
||||
// parser reads as a nested mapping and rejects — #1778). toYamlInlineScalar
|
||||
// only quotes when needed, so descriptions without special chars stay plain.
|
||||
const newDesc = buildTrimmedDescription(parts);
|
||||
const newFrontmatter = frontmatter.replace(descMatch[0], `description: ${newDesc}\n`);
|
||||
const newFrontmatter = frontmatter.replace(descMatch[0], `description: ${toYamlInlineScalar(newDesc)}\n`);
|
||||
let newContent = '---\n' + newFrontmatter + content.slice(fmEnd);
|
||||
|
||||
// Insert body section after frontmatter (after the closing ---\n and any
|
||||
|
||||
Reference in New Issue
Block a user