Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/opencode/src/config/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ export function shell(template: 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---/)
const match = content.match(/^---(\r?\n)([\s\S]*?)(\r?\n)?^---[ \t]*(\r?\n|$)/m)
if (!match) return content

const frontmatter = match[1]
const lines = frontmatter.split(/\r?\n/)
const lines = match[2].split(/\r?\n/)
const result: string[] = []

for (const line of lines) {
Expand Down Expand Up @@ -63,15 +62,18 @@ export function fallbackSanitization(content: string): string {
result.push(line)
}

const processed = result.join("\n")
return content.replace(frontmatter, () => processed)
const processed = result.join(match[1])
return content.replace(match[0], () => `---${match[1]}${processed}${match[3] ?? ""}---${match[4]}`)
}

export async function parse(filePath: string) {
const template = await Filesystem.readText(filePath)

try {
const md = matter(template)
const sanitized = fallbackSanitization(template)
// Retry sanitized frontmatter when gray-matter silently treated it as content.
if (Object.keys(md.data).length === 0 && sanitized !== template) return matter(sanitized)
return md
} catch {
try {
Expand Down
16 changes: 16 additions & 0 deletions packages/opencode/test/config/fixtures/colon-in-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
description: Reviews changes: returns a verdict.
mode: subagent
hidden: true
model: anthropic/claude-sonnet-4-5
temperature: 0.2
steps: 7
permission:
read: allow
bash:
npm test: allow
rm -rf *: deny
---
You are the code review agent.

Review the full diff and return a concise verdict.
68 changes: 68 additions & 0 deletions packages/opencode/test/config/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect, test, describe } from "bun:test"
import path from "path"
import { mkdir } from "fs/promises"
import { ConfigAgent } from "@/config/agent"
import { ConfigMarkdown } from "@/config/markdown"
import { tmpdir } from "../fixture/fixture"

describe("ConfigMarkdown: normal template", () => {
const template = `This is a @valid/path/to/a/file and it should also match at
Expand Down Expand Up @@ -226,3 +230,67 @@ describe("ConfigMarkdown: frontmatter has weird model id", async () => {
expect(result.content.trim()).toBe("Strictly follow da rules")
})
})

describe("ConfigMarkdown: frontmatter with colon in value followed by other fields", async () => {
const result = await ConfigMarkdown.parse(import.meta.dir + "/fixtures/colon-in-value.md")

test("should preserve full description including colons", () => {
expect(result.data.description).toBe("Reviews changes: returns a verdict.")
})

test("should preserve subsequent mode field", () => {
expect(result.data.mode).toBe("subagent")
})

test("should preserve hidden field", () => {
expect(result.data.hidden).toBe(true)
})

test("should preserve model field", () => {
expect(result.data.model).toBe("anthropic/claude-sonnet-4-5")
})

test("should preserve temperature field", () => {
expect(result.data.temperature).toBe(0.2)
})

test("should preserve steps field", () => {
expect(result.data.steps).toBe(7)
})

test("should preserve nested permission fields", () => {
expect(result.data.permission.read).toBe("allow")
expect(result.data.permission.bash["npm test"]).toBe("allow")
expect(result.data.permission.bash["rm -rf *"]).toBe("deny")
})

test("should preserve body content", () => {
expect(result.content.trim()).toStartWith("You are the code review agent.")
})

test("should load colon in value config through ConfigAgent", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await mkdir(path.join(dir, "agent"))
await Bun.write(
path.join(dir, "agent", "code-reviewer.md"),
await Bun.file(import.meta.dir + "/fixtures/colon-in-value.md").text(),
)
},
})

const agent = (await ConfigAgent.load(tmp.path))["code-reviewer"]
if (!agent) throw new Error("Expected code-reviewer agent to load")
expect(agent.description).toBe("Reviews changes: returns a verdict.")
expect(agent.mode).toBe("subagent")
expect(agent.hidden).toBe(true)
expect(agent.prompt).toStartWith("You are the code review agent.")
expect(agent.prompt).not.toContain("---")
if (!agent.permission) throw new Error("Expected code-reviewer agent permissions to load")
expect(agent.permission.read).toBe("allow")
expect(agent.permission.bash).toEqual({
"npm test": "allow",
"rm -rf *": "deny",
})
})
})
Loading