From e2da0c54585d1c4c70185c65b30f14335de9d87b Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 19 Jun 2026 11:50:45 +0000 Subject: [PATCH] fix(prompting): rendered system prompt template for system md Route resolved SYSTEM.md content through the custom prompt template path instead of replacing the fully rendered default prompt block. Fixes #3014 --- packages/coding-agent/CHANGELOG.md | 4 +++ packages/coding-agent/src/main.ts | 22 +++++++++++----- packages/coding-agent/src/sdk.ts | 15 +++++++++-- .../test/main-interactive-input.test.ts | 15 ++++++++++- .../test/system-prompt-dedup.test.ts | 26 +++++++++++++++++-- 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2b50ee6d1..17f0b0317 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `SYSTEM.md` prompt customization going through the raw system prompt override path, which dropped sections rendered by `custom-system-prompt.md` such as skills and rules ([#3014](https://github.com/can1357/oh-my-pi/issues/3014)). + ## [16.0.10] - 2026-06-18 ### Added diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index a737bf412..0b82d888c 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -748,6 +748,20 @@ function discoverAppendSystemPromptFile(): string | undefined { return undefined; } +/** Apply resolved CLI/discovered prompt files without bypassing system prompt templates. */ +export function applyResolvedSystemPromptInputs( + options: CreateAgentSessionOptions, + resolvedSystemPrompt: string | undefined, + resolvedAppendPrompt: string | undefined, +): void { + if (resolvedSystemPrompt) { + options.customSystemPrompt = resolvedSystemPrompt; + } + if (resolvedAppendPrompt) { + options.appendSystemPrompt = resolvedAppendPrompt; + } +} + async function buildSessionOptions( parsed: Args, scopedModels: ScopedModel[], @@ -872,13 +886,7 @@ async function buildSessionOptions( // (handled by caller before createAgentSession) // System prompt - if (resolvedSystemPrompt && resolvedAppendPrompt) { - options.systemPrompt = defaultPrompt => [resolvedSystemPrompt, resolvedAppendPrompt, ...defaultPrompt.slice(1)]; - } else if (resolvedSystemPrompt) { - options.systemPrompt = defaultPrompt => [resolvedSystemPrompt, ...defaultPrompt.slice(1)]; - } else if (resolvedAppendPrompt) { - options.systemPrompt = defaultPrompt => [...defaultPrompt, resolvedAppendPrompt]; - } + applyResolvedSystemPromptInputs(options, resolvedSystemPrompt, resolvedAppendPrompt); // Tools if (parsed.noTools) { diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index afcaa5dda..b6a5db736 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -405,8 +405,12 @@ export interface CreateAgentSessionOptions { /** Models available for cycling (Ctrl+P in interactive mode) */ scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>; - /** System prompt blocks. Array replaces default, function receives default blocks and returns final blocks. */ + /** Provider-facing system prompt override. Replaces the fully rendered default blocks. */ systemPrompt?: string | string[] | ((defaultPrompt: string[]) => string | string[]); + /** Custom base prompt rendered through the bundled custom system prompt template. */ + customSystemPrompt?: string; + /** Text appended through the bundled system prompt templates. */ + appendSystemPrompt?: string; /** Optional provider-facing session identifier for prompt caches and sticky auth selection. * Keeps persisted session files isolated while reusing provider-side caches. */ providerSessionId?: string; @@ -837,6 +841,7 @@ export interface BuildSystemPromptOptions { skills?: Skill[]; contextFiles?: Array<{ path: string; content: string }>; cwd?: string; + customPrompt?: string; appendPrompt?: string; repeatToolDescriptions?: boolean; } @@ -850,6 +855,7 @@ export interface BuildSystemPromptOptions { export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise { return await buildSystemPromptInternal({ cwd: options.cwd, + customPrompt: options.customPrompt, skills: options.skills, contextFiles: options.contextFiles, appendSystemPrompt: options.appendPrompt, @@ -2201,15 +2207,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // Owned/in-band tool dialect (non-native) repeats the catalog as `# Tool:` // sections; native tool calling lets the compact name list suffice. const nativeTools = resolveDialect(settings.get("tools.format"), agent?.state.model ?? model) === undefined; + if (options.appendSystemPrompt) { + appendPrompt = appendPrompt + ? `${appendPrompt}\n\n${options.appendSystemPrompt}` + : options.appendSystemPrompt; + } const defaultPrompt = await buildSystemPromptInternal({ cwd, + customPrompt: options.customSystemPrompt, skills, contextFiles, tools: promptTools, toolNames, rules: rulebookRules, alwaysApplyRules, - skillsSettings: settings.getGroup("skills"), appendSystemPrompt: appendPrompt, repeatToolDescriptions, nativeTools, diff --git a/packages/coding-agent/test/main-interactive-input.test.ts b/packages/coding-agent/test/main-interactive-input.test.ts index 7d16608f1..ba7e6adcc 100644 --- a/packages/coding-agent/test/main-interactive-input.test.ts +++ b/packages/coding-agent/test/main-interactive-input.test.ts @@ -2,8 +2,9 @@ import { afterEach, describe, expect, it, vi } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; -import { submitInteractiveInput } from "@oh-my-pi/pi-coding-agent/main"; +import { applyResolvedSystemPromptInputs, submitInteractiveInput } from "@oh-my-pi/pi-coding-agent/main"; import type { SubmittedUserInput } from "@oh-my-pi/pi-coding-agent/modes/types"; +import type { CreateAgentSessionOptions } from "@oh-my-pi/pi-coding-agent/sdk"; import { discoverTitleSystemPromptFile } from "@oh-my-pi/pi-coding-agent/system-prompt"; const cleanupDirs: string[] = []; @@ -35,6 +36,18 @@ describe("discoverTitleSystemPromptFile", () => { }); }); +describe("applyResolvedSystemPromptInputs", () => { + it("routes SYSTEM.md content through template-aware session options", () => { + const options: CreateAgentSessionOptions = {}; + + applyResolvedSystemPromptInputs(options, "project system prompt", "append prompt"); + + expect(options.customSystemPrompt).toBe("project system prompt"); + expect(options.appendSystemPrompt).toBe("append prompt"); + expect(options.systemPrompt).toBeUndefined(); + }); +}); + describe("submitInteractiveInput", () => { it("routes already-started synthetic continue submissions to a hidden developer prompt", async () => { const mode = { diff --git a/packages/coding-agent/test/system-prompt-dedup.test.ts b/packages/coding-agent/test/system-prompt-dedup.test.ts index f351d6421..1f3d2c384 100644 --- a/packages/coding-agent/test/system-prompt-dedup.test.ts +++ b/packages/coding-agent/test/system-prompt-dedup.test.ts @@ -6,6 +6,7 @@ import { buildSystemPrompt, loadProjectContextFiles, loadSystemPromptFiles, + type SystemPromptToolMetadata, } from "@oh-my-pi/pi-coding-agent/system-prompt"; import { cleanupTempHome } from "./helpers/temp-home-cleanup"; @@ -13,6 +14,17 @@ function escapeRegExp(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +const READ_TOOL = new Map([ + [ + "read", + { + label: "Read", + description: "Reads files from disk.", + parameters: { type: "object", properties: { path: { type: "string" } } }, + }, + ], +]); + describe("SYSTEM.md prompt assembly", () => { let tempDir = ""; let tempHomeDir = ""; @@ -38,9 +50,18 @@ describe("SYSTEM.md prompt assembly", () => { cwd: projectDir, customPrompt: systemPrompt, contextFiles: [], - skills: [], + skills: [ + { + name: "focused-work", + description: "Focused work instructions", + filePath: "skills/focused-work/SKILL.md", + baseDir: "skills/focused-work", + source: "test", + }, + ], rules: [], - toolNames: [], + toolNames: ["read"], + tools: READ_TOOL, workspaceTree: { rootPath: projectDir, rendered: "", @@ -53,6 +74,7 @@ describe("SYSTEM.md prompt assembly", () => { const promptText = renderedPrompt.join("\n\n"); const matches = promptText.match(new RegExp(escapeRegExp(systemPrompt), "g")) ?? []; expect(matches).toHaveLength(1); + expect(promptText).toContain(''); }); it("prefers project SYSTEM.md over user SYSTEM.md", async () => {