diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index d1eeeac38..4cd17746c 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -14,7 +14,9 @@ export namespace ConfigMarkdown { return Array.from(template.matchAll(SHELL_REGEX)) } - export function preprocessFrontmatter(content: string): string { + // other coding agents like claude code allow invalid yaml in their + // frontmatter, we need to fallback to a more permissive parser for those cases + export function fallbackSanitization(content: string): string { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) if (!match) return content @@ -53,7 +55,7 @@ export namespace ConfigMarkdown { // if value contains a colon, convert to block scalar if (value.includes(":")) { - result.push(`${key}: |`) + result.push(`${key}: |-`) result.push(` ${value}`) continue } @@ -66,20 +68,23 @@ export namespace ConfigMarkdown { } export async function parse(filePath: string) { - const raw = await Bun.file(filePath).text() - const template = preprocessFrontmatter(raw) + const template = await Bun.file(filePath).text() try { const md = matter(template) return md - } catch (err) { - throw new FrontmatterError( - { - path: filePath, - message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`, - }, - { cause: err }, - ) + } catch { + try { + return matter(fallbackSanitization(template)) + } catch (err) { + throw new FrontmatterError( + { + path: filePath, + message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`, + }, + { cause: err }, + ) + } } } diff --git a/packages/opencode/test/config/fixtures/markdown-header.md b/packages/opencode/test/config/fixtures/markdown-header.md new file mode 100644 index 000000000..d5af1f1c2 --- /dev/null +++ b/packages/opencode/test/config/fixtures/markdown-header.md @@ -0,0 +1,11 @@ +# Response Formatting Requirements + +Always structure your responses using clear markdown formatting: + +- By default don't put information into tables for questions (but do put information into tables when creating or updating files) +- Use headings (##, ###) to organise sections, always +- Use bullet points or numbered lists for multiple items +- Use code blocks with language tags for any code +- Use **bold** for key terms and emphasis +- Use tables when comparing options or listing structured data +- Break long responses into logical sections with headings diff --git a/packages/opencode/test/config/fixtures/weird-model-id.md b/packages/opencode/test/config/fixtures/weird-model-id.md new file mode 100644 index 000000000..bb02b0650 --- /dev/null +++ b/packages/opencode/test/config/fixtures/weird-model-id.md @@ -0,0 +1,13 @@ +--- +description: General coding and planning agent +mode: subagent +model: synthetic/hf:zai-org/GLM-4.7 +tools: + write: true + read: true + edit: true +stuff: > + This is some stuff +--- + +Strictly follow da rules diff --git a/packages/opencode/test/config/markdown.test.ts b/packages/opencode/test/config/markdown.test.ts index b4263ee6b..c6133317e 100644 --- a/packages/opencode/test/config/markdown.test.ts +++ b/packages/opencode/test/config/markdown.test.ts @@ -104,7 +104,7 @@ describe("ConfigMarkdown: frontmatter parsing", async () => { }) test("should extract occupation field with colon in value", () => { - expect(parsed.data.occupation).toBe("This man has the following occupation: Software Engineer\n") + expect(parsed.data.occupation).toBe("This man has the following occupation: Software Engineer") }) test("should extract title field with single quotes", () => { @@ -128,15 +128,15 @@ describe("ConfigMarkdown: frontmatter parsing", async () => { }) test("should extract URL with port", () => { - expect(parsed.data.url).toBe("https://example.com:8080/path?query=value\n") + expect(parsed.data.url).toBe("https://example.com:8080/path?query=value") }) test("should extract time with colons", () => { - expect(parsed.data.time).toBe("The time is 12:30:00 PM\n") + expect(parsed.data.time).toBe("The time is 12:30:00 PM") }) test("should extract value with multiple colons", () => { - expect(parsed.data.nested).toBe("First: Second: Third: Fourth\n") + expect(parsed.data.nested).toBe("First: Second: Third: Fourth") }) test("should preserve already double-quoted values with colons", () => { @@ -148,7 +148,7 @@ describe("ConfigMarkdown: frontmatter parsing", async () => { }) test("should extract value with quotes and colons mixed", () => { - expect(parsed.data.mixed).toBe('He said "hello: world" and then left\n') + expect(parsed.data.mixed).toBe('He said "hello: world" and then left') }) test("should handle empty values", () => { @@ -190,3 +190,39 @@ describe("ConfigMarkdown: frontmatter parsing w/ no frontmatter", async () => { expect(result.content.trim()).toBe("Content") }) }) + +describe("ConfigMarkdown: frontmatter parsing w/ Markdown header", async () => { + const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/markdown-header.md") + + test("should parse and match", () => { + expect(result).toBeDefined() + expect(result.data).toEqual({}) + expect(result.content.trim()).toBe(`# Response Formatting Requirements + +Always structure your responses using clear markdown formatting: + +- By default don't put information into tables for questions (but do put information into tables when creating or updating files) +- Use headings (##, ###) to organise sections, always +- Use bullet points or numbered lists for multiple items +- Use code blocks with language tags for any code +- Use **bold** for key terms and emphasis +- Use tables when comparing options or listing structured data +- Break long responses into logical sections with headings`) + }) +}) + +describe("ConfigMarkdown: frontmatter has weird model id", async () => { + const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/weird-model-id.md") + + test("should parse and match", () => { + expect(result).toBeDefined() + expect(result.data["description"]).toEqual("General coding and planning agent") + expect(result.data["mode"]).toEqual("subagent") + expect(result.data["model"]).toEqual("synthetic/hf:zai-org/GLM-4.7") + expect(result.data["tools"]["write"]).toBeTrue() + expect(result.data["tools"]["read"]).toBeTrue() + expect(result.data["stuff"]).toBe("This is some stuff\n") + + expect(result.content.trim()).toBe("Strictly follow da rules") + }) +})