From 4ff17c58a8964c2060eab70d43c55728b8685631 Mon Sep 17 00:00:00 2001 From: Suyeol Jeon Date: Thu, 15 Jan 2026 23:57:26 +0900 Subject: [PATCH 1/4] feat(agents): add file:// URI support in prompt_append configuration --- src/agents/utils.test.ts | 57 +++++++++++++++++++++++++++++++++++++++- src/agents/utils.ts | 26 ++++++++++++++---- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 336ed628a3..53691e4570 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -1,7 +1,23 @@ -import { describe, test, expect } from "bun:test" +import { describe, test, expect, beforeAll, afterAll } from "bun:test" +import { mkdirSync, writeFileSync, rmSync } from "fs" +import { join } from "path" +import { tmpdir } from "os" import { createBuiltinAgents } from "./utils" import type { AgentConfig } from "@opencode-ai/sdk" +const TEST_DIR = join(tmpdir(), "utils-test-prompts") +const TEST_PROMPT_FILE = join(TEST_DIR, "custom-prompt.md") +const TEST_PROMPT_CONTENT = "Custom prompt from file\nWith multiple lines" + +beforeAll(() => { + mkdirSync(TEST_DIR, { recursive: true }) + writeFileSync(TEST_PROMPT_FILE, TEST_PROMPT_CONTENT) +}) + +afterAll(() => { + rmSync(TEST_DIR, { recursive: true, force: true }) +}) + describe("createBuiltinAgents with model overrides", () => { test("Sisyphus with default model has thinking config", () => { // #given - no overrides @@ -72,6 +88,45 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.oracle.textVerbosity).toBeUndefined() }) + test("resolves file:// URI in prompt_append with absolute path", () => { + // #given + const overrides = { + oracle: { prompt_append: `file://${TEST_PROMPT_FILE}` }, + } + + // #when + const agents = createBuiltinAgents([], overrides) + + // #then + expect(agents.oracle.prompt).toContain(TEST_PROMPT_CONTENT) + }) + + test("resolves file:// URI in prompt_append with relative path", () => { + // #given + const overrides = { + oracle: { prompt_append: "file://custom-prompt.md" }, + } + + // #when + const agents = createBuiltinAgents([], overrides, TEST_DIR) + + // #then + expect(agents.oracle.prompt).toContain(TEST_PROMPT_CONTENT) + }) + + test("handles non-existent file:// URI in prompt_append with warning", () => { + // #given + const overrides = { + oracle: { prompt_append: "file://non-existent-prompt.md" }, + } + + // #when + const agents = createBuiltinAgents([], overrides, TEST_DIR) + + // #then + expect(agents.oracle.prompt).toContain("[WARNING: Could not resolve file URI: file://non-existent-prompt.md]") + }) + test("non-model overrides are still applied after factory rebuild", () => { // #given const overrides = { diff --git a/src/agents/utils.ts b/src/agents/utils.ts index d831caa871..70e1254609 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -13,6 +13,9 @@ import { createOrchestratorSisyphusAgent, orchestratorSisyphusAgent } from "./or import { createMomusAgent } from "./momus" import type { AvailableAgent } from "./sisyphus-prompt-builder" import { deepMerge } from "../shared" +import { existsSync, readFileSync } from "fs" +import { resolve, isAbsolute } from "path" +import { homedir } from "os" import { DEFAULT_CATEGORIES } from "../tools/sisyphus-task/constants" import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content" @@ -113,13 +116,26 @@ export function createEnvContext(): string { function mergeAgentConfig( base: AgentConfig, - override: AgentOverrideConfig + override: AgentOverrideConfig, + configDir?: string ): AgentConfig { const { prompt_append, ...rest } = override const merged = deepMerge(base, rest as Partial) if (prompt_append && merged.prompt) { - merged.prompt = merged.prompt + "\n" + prompt_append + let resolved = prompt_append + if (prompt_append.startsWith("file://")) { + const path = prompt_append.slice(7) + const abs = path.startsWith("~/") + ? resolve(homedir(), path.slice(2)) + : isAbsolute(path) + ? path + : resolve(configDir ?? process.cwd(), path) + resolved = existsSync(abs) + ? readFileSync(abs, "utf-8") + : `[WARNING: Could not resolve file URI: ${prompt_append}]` + } + merged.prompt = merged.prompt + "\n" + resolved } return merged @@ -157,7 +173,7 @@ export function createBuiltinAgents( } if (override) { - config = mergeAgentConfig(config, override) + config = mergeAgentConfig(config, override, directory) } result[name] = config @@ -184,7 +200,7 @@ export function createBuiltinAgents( } if (sisyphusOverride) { - sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride) + sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride, directory) } result["Sisyphus"] = sisyphusConfig @@ -199,7 +215,7 @@ export function createBuiltinAgents( }) if (orchestratorOverride) { - orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride) + orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride, directory) } result["orchestrator-sisyphus"] = orchestratorConfig From 1e95477f4f2a6cf33095dfb0642f07db135c861b Mon Sep 17 00:00:00 2001 From: Suyeol Jeon Date: Thu, 15 Jan 2026 23:57:30 +0900 Subject: [PATCH 2/4] docs: document file:// URI support in agent prompt_append --- README.md | 20 ++++++++++++++++++++ src/config/schema.ts | 1 + 2 files changed, 21 insertions(+) diff --git a/README.md b/README.md index a23e3fdc0e..023cc55235 100644 --- a/README.md +++ b/README.md @@ -886,6 +886,26 @@ Use `prompt_append` to add extra instructions without replacing the default syst } ``` +You can also reference external files using `file://` URIs: + +```json +{ + "agents": { + "oracle": { + "prompt_append": "file://~/.config/opencode/my-oracle-instructions.md" + }, + "Sisyphus": { + "prompt_append": "file://./project-context.md" + } + } +} +``` + +Supported URI formats: +- `file:///absolute/path` - Absolute path +- `file://relative/path` - Relative to project root +- `file://~/path` - Relative to home directory + You can also override settings for `Sisyphus` (the main orchestrator) and `build` (the default agent) using the same options. #### Permission Options diff --git a/src/config/schema.ts b/src/config/schema.ts index d5fad7e014..685bc703b7 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -106,6 +106,7 @@ export const AgentOverrideConfigSchema = z.object({ temperature: z.number().min(0).max(2).optional(), top_p: z.number().min(0).max(1).optional(), prompt: z.string().optional(), + /** Text to append to agent prompt. Supports file:// URIs (file:///abs, file://rel, file://~/home) */ prompt_append: z.string().optional(), tools: z.record(z.string(), z.boolean()).optional(), disable: z.boolean().optional(), From 8391fc92279fe76e7ba6a5d4030a0aa8f01de0d6 Mon Sep 17 00:00:00 2001 From: Suyeol Jeon Date: Fri, 16 Jan 2026 00:50:43 +0900 Subject: [PATCH 3/4] fix(agents): decode percent-encoded file paths in file:// URIs Properly handle encoded characters (e.g., %20 for spaces) when resolving file:// URIs in prompt_append configuration. Adds decodeURIComponent() to unwrap percent-encoded paths before filesystem access. Includes test case verifying percent-encoded paths with spaces can be loaded. --- src/agents/utils.test.ts | 16 ++++++++++++++++ src/agents/utils.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 53691e4570..8dd7a6e3ee 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -8,10 +8,13 @@ import type { AgentConfig } from "@opencode-ai/sdk" const TEST_DIR = join(tmpdir(), "utils-test-prompts") const TEST_PROMPT_FILE = join(TEST_DIR, "custom-prompt.md") const TEST_PROMPT_CONTENT = "Custom prompt from file\nWith multiple lines" +const TEST_ENCODED_FILE = join(TEST_DIR, "my prompt file.md") +const TEST_ENCODED_CONTENT = "Prompt from percent-encoded path" beforeAll(() => { mkdirSync(TEST_DIR, { recursive: true }) writeFileSync(TEST_PROMPT_FILE, TEST_PROMPT_CONTENT) + writeFileSync(TEST_ENCODED_FILE, TEST_ENCODED_CONTENT) }) afterAll(() => { @@ -127,6 +130,19 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.oracle.prompt).toContain("[WARNING: Could not resolve file URI: file://non-existent-prompt.md]") }) + test("resolves file:// URI with percent-encoded path", () => { + // #given - filename has space: "my prompt file.md" → "my%20prompt%20file.md" + const overrides = { + oracle: { prompt_append: "file://my%20prompt%20file.md" }, + } + + // #when + const agents = createBuiltinAgents([], overrides, TEST_DIR) + + // #then + expect(agents.oracle.prompt).toContain(TEST_ENCODED_CONTENT) + }) + test("non-model overrides are still applied after factory rebuild", () => { // #given const overrides = { diff --git a/src/agents/utils.ts b/src/agents/utils.ts index 70e1254609..d6d23c6328 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -125,7 +125,7 @@ function mergeAgentConfig( if (prompt_append && merged.prompt) { let resolved = prompt_append if (prompt_append.startsWith("file://")) { - const path = prompt_append.slice(7) + const path = decodeURIComponent(prompt_append.slice(7)) const abs = path.startsWith("~/") ? resolve(homedir(), path.slice(2)) : isAbsolute(path) From ea65b2853c3cde8ac27b505d42bec82929dd9158 Mon Sep 17 00:00:00 2001 From: Suyeol Jeon Date: Fri, 16 Jan 2026 01:05:24 +0900 Subject: [PATCH 4/4] fix(agents): handle malformed percent-encoding in file:// URIs Add try-catch wrapper around decodeURIComponent() to gracefully handle invalid percent-encoding sequences (e.g., file://my%prompt.md). Shows warning message instead of crashing with URIError. Add test case verifying warning output for malformed URIs. --- src/agents/utils.test.ts | 13 +++++++++++++ src/agents/utils.ts | 27 ++++++++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/agents/utils.test.ts b/src/agents/utils.test.ts index 8dd7a6e3ee..c2d3bb9bc4 100644 --- a/src/agents/utils.test.ts +++ b/src/agents/utils.test.ts @@ -143,6 +143,19 @@ describe("createBuiltinAgents with model overrides", () => { expect(agents.oracle.prompt).toContain(TEST_ENCODED_CONTENT) }) + test("handles malformed percent-encoding in file URI with warning", () => { + // #given - malformed percent-encoding (incomplete escape sequence) + const overrides = { + oracle: { prompt_append: "file://my%prompt.md" }, + } + + // #when + const agents = createBuiltinAgents([], overrides, TEST_DIR) + + // #then + expect(agents.oracle.prompt).toContain("[WARNING: Malformed file URI (invalid percent-encoding): file://my%prompt.md]") + }) + test("non-model overrides are still applied after factory rebuild", () => { // #given const overrides = { diff --git a/src/agents/utils.ts b/src/agents/utils.ts index d6d23c6328..2a8cf3247d 100644 --- a/src/agents/utils.ts +++ b/src/agents/utils.ts @@ -125,15 +125,24 @@ function mergeAgentConfig( if (prompt_append && merged.prompt) { let resolved = prompt_append if (prompt_append.startsWith("file://")) { - const path = decodeURIComponent(prompt_append.slice(7)) - const abs = path.startsWith("~/") - ? resolve(homedir(), path.slice(2)) - : isAbsolute(path) - ? path - : resolve(configDir ?? process.cwd(), path) - resolved = existsSync(abs) - ? readFileSync(abs, "utf-8") - : `[WARNING: Could not resolve file URI: ${prompt_append}]` + let path: string + try { + path = decodeURIComponent(prompt_append.slice(7)) + } catch { + resolved = `[WARNING: Malformed file URI (invalid percent-encoding): ${prompt_append}]` + path = "" + } + + if (path) { + const abs = path.startsWith("~/") + ? resolve(homedir(), path.slice(2)) + : isAbsolute(path) + ? path + : resolve(configDir ?? process.cwd(), path) + resolved = existsSync(abs) + ? readFileSync(abs, "utf-8") + : `[WARNING: Could not resolve file URI: ${prompt_append}]` + } } merged.prompt = merged.prompt + "\n" + resolved }