Fix: quote YAML scalars that contain colon-space in frontmatter

generateYamlFrontmatter only re-quoted values starting with `[` or `{`,
but parseFrontmatter strips surrounding quotes on input. Descriptions
containing `: ` (e.g. "Also handles: critique...") round-tripped into
unquoted plain scalars that YAML parsers reject. Added a yamlNeedsQuoting
check covering colon-space, space-hash, YAML indicator chars, reserved
keywords, and number-like strings, plus regression tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-10 19:09:55 -07:00
co-authored by Claude Opus 4.6
parent 00d485659a
commit f957fcad20
2 changed files with 75 additions and 5 deletions
+39 -5
View File
@@ -456,6 +456,41 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki
return result;
}
/**
* Decide whether a YAML scalar string value must be quoted to survive parsing.
*
* Plain (unquoted) YAML scalars cannot contain `: ` or ` #`, cannot start with
* a YAML indicator character, cannot look like a boolean/null/number, and
* cannot carry leading/trailing whitespace. parseFrontmatter strips surrounding
* quotes on input, so we must re-detect the need to quote on output — otherwise
* descriptions like "Handles: critique/review..." round-trip into invalid YAML.
*/
function yamlNeedsQuoting(value) {
if (typeof value !== 'string') return false;
if (value === '') return true;
// Leading or trailing whitespace
if (/^\s|\s$/.test(value)) return true;
// Starts with a YAML flow/indicator character
if (/^[\[\]{},&*!|>'"%@`#]/.test(value)) return true;
// Starts with `?`, `:`, or `-` followed by space or end of string
if (/^[?:-](\s|$)/.test(value)) return true;
// Contains `: ` (ends plain scalar) or ` #` (starts comment), or ends with `:`
if (/: |\s#|:$/.test(value)) return true;
// Reserved keywords that YAML 1.1 parsers coerce to boolean/null
if (/^(true|false|null|yes|no|on|off|~)$/i.test(value)) return true;
// Looks like a number
if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value)) return true;
return false;
}
function formatYamlScalar(value) {
if (typeof value !== 'string') return String(value);
if (yamlNeedsQuoting(value)) {
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return value;
}
/**
* Generate YAML frontmatter string
*/
@@ -467,18 +502,17 @@ export function generateYamlFrontmatter(data) {
lines.push(`${key}:`);
for (const item of value) {
if (typeof item === 'object') {
lines.push(` - name: ${item.name}`);
if (item.description) lines.push(` description: ${item.description}`);
lines.push(` - name: ${formatYamlScalar(item.name)}`);
if (item.description) lines.push(` description: ${formatYamlScalar(item.description)}`);
if (item.required !== undefined) lines.push(` required: ${item.required}`);
} else {
lines.push(` - ${item}`);
lines.push(` - ${formatYamlScalar(item)}`);
}
}
} else if (typeof value === 'boolean') {
lines.push(`${key}: ${value}`);
} else {
const needsQuoting = typeof value === 'string' && /^[\[{]/.test(value);
lines.push(`${key}: ${needsQuoting ? `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : value}`);
lines.push(`${key}: ${formatYamlScalar(value)}`);
}
}