fix: normalize quoted user-invocable frontmatter (#87)

* fix: normalize quoted user-invokable frontmatter

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: preserve quoted non-boolean frontmatter values

Only normalize quoted booleans for the user-invocable frontmatter flag so other quoted fields like argument-hint and description continue to round-trip as plain strings.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Ryanba
2026-04-28 18:19:50 -07:00
committed by GitHub
co-authored by Sisyphus
parent 04709eadf0
commit 18fa503d44
2 changed files with 43 additions and 7 deletions
+11 -5
View File
@@ -101,13 +101,19 @@ export function parseFrontmatter(content) {
if (colonIndex > 0) {
const key = trimmed.slice(0, colonIndex).trim();
const value = trimmed.slice(colonIndex + 1).trim();
const isQuoted = /^(".*"|'.*')$/.test(value);
const unquotedValue = isQuoted ? value.slice(1, -1) : value;
const shouldCoerceBoolean =
key === 'user-invocable' || key === 'user-invokable' || !isQuoted;
if (value) {
// Strip YAML quotes
const unquoted = (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))
? value.slice(1, -1)
: value;
frontmatter[key] = unquoted === 'true' ? true : unquoted === 'false' ? false : unquoted;
frontmatter[key] = shouldCoerceBoolean
? unquotedValue === 'true'
? true
: unquotedValue === 'false'
? false
: unquotedValue
: unquotedValue;
currentKey = key;
currentArray = null;
} else {
+32 -2
View File
@@ -89,7 +89,7 @@ Body.`;
expect(result.frontmatter['user-invocable']).toBe(true);
});
test('should parse user-invocable as string true (code behavior)', () => {
test('should parse quoted user-invocable boolean as true', () => {
const content = `---
name: test-skill
user-invocable: 'true'
@@ -98,10 +98,21 @@ user-invocable: 'true'
Body.`;
const result = parseFrontmatter(content);
// parseFrontmatter strips YAML quotes, so 'true' becomes boolean true
expect(result.frontmatter['user-invocable']).toBe(true);
});
test('should keep quoted non-user-invocable booleans as plain strings', () => {
const content = `---
name: test-skill
description: 'true'
---
Body.`;
const result = parseFrontmatter(content);
expect(result.frontmatter.description).toBe('true');
});
test('should parse allowed-tools field', () => {
const content = `---
name: test-skill
@@ -393,6 +404,25 @@ description: Run technical quality checks
user-invocable: true
---
Audit the code.`;
const skillDir = path.join(testRootDir, 'source/skills/audit');
ensureDir(skillDir);
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent);
const { skills } = readSourceFiles(testRootDir);
expect(skills).toHaveLength(1);
expect(skills[0].userInvocable).toBe(true);
});
test('should read skill with quoted user-invocable flag', () => {
const skillContent = `---
name: audit
description: Run technical quality checks
user-invocable: 'true'
---
Audit the code.`;
const skillDir = path.join(testRootDir, 'source/skills/audit');