fix(skills): warn on unterminated frontmatter (#110479)

This commit is contained in:
Peter Steinberger
2026-07-18 07:52:06 +01:00
committed by GitHub
parent b116ae5a43
commit ba5752dd63
3 changed files with 42 additions and 4 deletions
@@ -201,6 +201,16 @@ metadata:
expect(parseFrontmatterBlock(content)).toStrictEqual({});
});
it("reports an unterminated frontmatter block", () => {
const result = parseFrontmatterBlockResult(`---
name: broken
description: Missing the closing delimiter
`);
expect(result.frontmatter).toEqual({});
expect(result.issues).toEqual([expect.objectContaining({ code: "UNTERMINATED_FRONTMATTER" })]);
});
it("ignores non-delimiter opening prefixes", () => {
for (const prefix of ["---not", "----", "--- name"]) {
const content = `${prefix}
+18 -3
View File
@@ -200,11 +200,12 @@ function normalizeFrontmatterContent(content: string): string {
}
const FRONTMATTER_CLOSING_DELIMITER = /(?:^|\n)---[^\S\n]*(?:\n|(?![\s\S]))/;
const FRONTMATTER_OPENING_DELIMITER = /^---[^\S\n]*\n/;
function extractFrontmatterBlockFromNormalized(
normalized: string,
): ExtractedFrontmatterBlock | undefined {
const opening = /^---[^\S\n]*\n/.exec(normalized);
const opening = FRONTMATTER_OPENING_DELIMITER.exec(normalized);
if (!opening) {
return undefined;
}
@@ -239,6 +240,20 @@ export function parseFrontmatterBlock(content: string): ParsedFrontmatter {
/** Parses frontmatter once while retaining recoverable YAML parser issues for owning loaders. */
export function parseFrontmatterBlockResult(content: string): ParsedFrontmatterBlockResult {
const block = extractFrontmatterBlock(content)?.block;
return block ? parseYamlFrontmatter(block) : { frontmatter: {}, issues: [] };
const normalized = normalizeFrontmatterContent(content);
const block = extractFrontmatterBlockFromNormalized(normalized)?.block;
if (block !== undefined) {
return block ? parseYamlFrontmatter(block) : { frontmatter: {}, issues: [] };
}
return FRONTMATTER_OPENING_DELIMITER.test(normalized)
? {
frontmatter: {},
issues: [
{
code: "UNTERMINATED_FRONTMATTER",
message: "missing closing --- delimiter",
},
],
}
: { frontmatter: {}, issues: [] };
}
+14 -1
View File
@@ -423,11 +423,14 @@ metadata:
expect(entry?.metadata?.requires?.env).toEqual(["EXAMPLE_VAR"]);
});
it("warns with the malformed file and keeps loading sibling workspace skills", async () => {
it("warns for malformed files and keeps loading sibling workspace skills", async () => {
const workspaceDir = await createTempWorkspaceDir();
const brokenDir = path.join(workspaceDir, "skills", "broken");
const brokenFile = path.join(brokenDir, "SKILL.md");
const unterminatedDir = path.join(workspaceDir, "skills", "unterminated");
const unterminatedFile = path.join(unterminatedDir, "SKILL.md");
await fs.mkdir(brokenDir, { recursive: true });
await fs.mkdir(unterminatedDir, { recursive: true });
await fs.writeFile(
brokenFile,
`---
@@ -437,6 +440,11 @@ description: Broken skill
`,
"utf8",
);
await fs.writeFile(
unterminatedFile,
"---\nname: unterminated\ndescription: Missing closing delimiter\n",
"utf8",
);
await writeSkill({
dir: path.join(workspaceDir, "skills", "valid"),
name: "valid",
@@ -451,6 +459,11 @@ description: Broken skill
expect(entries.map((entry) => entry.skill.name)).not.toContain("broken");
expect(warningText).toContain(brokenFile);
expect(warningText).toContain("invalid frontmatter: BAD_INDENT");
expect(entries.map((entry) => entry.skill.name)).not.toContain("unterminated");
expect(warningText).toContain(unterminatedFile);
expect(warningText).toContain(
"invalid frontmatter: UNTERMINATED_FRONTMATTER: missing closing --- delimiter",
);
});
it("applies agent skill filters and replacement semantics", async () => {