diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index cea0148bb95..edb3d5be983 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -13,6 +13,7 @@ import * as ConfigMarkdown from "./markdown" import { ConfigModelID } from "./model-id" import { ConfigParse } from "./parse" import { ConfigPermission } from "./permission" +import { ConfigVariable } from "./variable" // kilocode_change start import { KilocodeConfig } from "@/kilocode/config/config" import type { Warning } from "./config" @@ -162,10 +163,18 @@ export async function load(dir: string, warnings?: Warning[]) { // kilocode_change end const name = configEntryNameFromPath(item, patterns) + const prompt = await ConfigVariable.substitute({ + text: md.content.trim(), + type: "virtual", + dir: path.dirname(item), + source: item, + missing: "empty", + }) + const config = { name, ...md.data, - prompt: md.content.trim(), + prompt, } // kilocode_change start - use Effect schema (propertyOrder: original) + non-fatal handleInvalid try { diff --git a/packages/opencode/test/config/agent-file-syntax.test.ts b/packages/opencode/test/config/agent-file-syntax.test.ts new file mode 100644 index 00000000000..cf0420f4ffc --- /dev/null +++ b/packages/opencode/test/config/agent-file-syntax.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "bun:test" +import path from "node:path" +import fs from "node:fs/promises" +import { tmpdir } from "../fixture/fixture" +import { Instance } from "../../src/project/instance" +import { Agent } from "../../src/agent/agent" + +describe("agent {file:...} syntax in markdown body", () => { + test("loads file content when {file:...} is used in agent body", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + await Bun.write(path.join(dir, "prompts", "guide.md"), "Guidelines content") + + const agentsDir = path.join(dir, ".kilo", "agents") + await fs.mkdir(agentsDir, { recursive: true }) + await Bun.write( + path.join(agentsDir, "test.md"), + ["---", "description: Test Agent", "---", "Base prompt", "", "{file:../../prompts/guide.md}", ""].join("\n"), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const agent = await Agent.get("test") + + expect(agent).toBeDefined() + expect(agent?.prompt).toContain("Base prompt") + expect(agent?.prompt).toContain("Guidelines content") + }, + }) + }) + + test("replaces missing file reference with empty string", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const agentsDir = path.join(dir, ".kilo", "agents") + await fs.mkdir(agentsDir, { recursive: true }) + await Bun.write( + path.join(agentsDir, "test.md"), + ["---", "description: Test Agent", "---", "Base prompt", "", "{file:missing.txt}", ""].join("\n"), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const agent = await Agent.get("test") + + expect(agent).toBeDefined() + expect(agent?.prompt).toContain("Base prompt") + expect(agent?.prompt).not.toContain("{file:") + }, + }) + }) + + test("resolves multiple file references in same agent", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + await Bun.write(path.join(dir, "a.md"), "Content A") + await Bun.write(path.join(dir, "b.md"), "Content B") + + const agentsDir = path.join(dir, ".kilo", "agents") + await fs.mkdir(agentsDir, { recursive: true }) + await Bun.write( + path.join(agentsDir, "test.md"), + ["---", "description: Test", "---", "Base", "", "{file:../../a.md}", "{file:../../b.md}", ""].join("\n"), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const agent = await Agent.get("test") + + expect(agent).toBeDefined() + expect(agent?.prompt).toContain("Content A") + expect(agent?.prompt).toContain("Content B") + }, + }) + }) +})