fix: deduplicate custom SYSTEM.md in context assembly

fixes #473
This commit is contained in:
can1357
2026-03-18 22:24:25 +01:00
parent 964ee1bba8
commit 38eeeeda1b
2 changed files with 89 additions and 11 deletions
+9 -11
View File
@@ -294,8 +294,8 @@ export async function loadProjectContextFiles(
}
/**
* Load system prompt customization files (SYSTEM.md).
* Returns combined content from all discovered SYSTEM.md files.
* Load the effective system prompt customization from SYSTEM.md.
* Project-level SYSTEM.md overrides user-level SYSTEM.md.
*/
export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
const resolvedCwd = options.cwd ?? getProjectDir();
@@ -304,16 +304,13 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
if (result.items.length === 0) return null;
// Combine all SYSTEM.md contents (user-level first, then project-level)
const userLevel = result.items.filter(item => item.level === "user");
const projectLevel = result.items.filter(item => item.level === "project");
const parts: string[] = [];
for (const item of [...userLevel, ...projectLevel]) {
parts.push(item.content);
const projectLevel = result.items.find(item => item.level === "project");
if (projectLevel) {
return projectLevel.content;
}
return parts.join("\n\n");
const userLevel = result.items.find(item => item.level === "user");
return userLevel?.content ?? null;
}
export interface SystemPromptToolMetadata {
@@ -510,7 +507,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
const data = {
systemPromptCustomization: systemPromptCustomization ?? "",
// Explicit custom prompts replace discovered SYSTEM.md content rather than layering it twice.
systemPromptCustomization: resolvedCustomPrompt ? "" : (systemPromptCustomization ?? ""),
customPrompt: resolvedCustomPrompt,
appendPrompt: resolvedAppendPrompt ?? "",
tools: toolNames,
@@ -0,0 +1,80 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { loadSystemPromptFiles } from "@oh-my-pi/pi-coding-agent/system-prompt";
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
describe("SYSTEM.md prompt assembly", () => {
let tempDir = "";
let tempHomeDir = "";
let originalHome: string | undefined;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-system-prompt-"));
tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-system-home-"));
originalHome = process.env.HOME;
process.env.HOME = tempHomeDir;
});
afterEach(() => {
if (tempDir) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
if (tempHomeDir) {
fs.rmSync(tempHomeDir, { recursive: true, force: true });
}
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
});
it("renders SYSTEM.md exactly once when it is used as the custom base prompt", async () => {
const projectDir = path.join(tempDir, "project");
const systemDir = path.join(projectDir, ".omp");
const systemPrompt = "You are the project SYSTEM prompt.";
fs.mkdirSync(systemDir, { recursive: true });
fs.writeFileSync(path.join(systemDir, "SYSTEM.md"), systemPrompt);
const { session } = await createAgentSession({
cwd: projectDir,
agentDir: projectDir,
sessionManager: SessionManager.inMemory(),
settings: Settings.isolated(),
systemPrompt,
disableExtensionDiscovery: true,
skills: [],
contextFiles: [],
promptTemplates: [],
slashCommands: [],
enableMCP: false,
enableLsp: false,
});
try {
const formatted = session.formatSessionAsText();
const matches = formatted.match(new RegExp(escapeRegExp(systemPrompt), "g")) ?? [];
expect(matches).toHaveLength(1);
} finally {
await session.dispose();
}
});
it("prefers project SYSTEM.md over user SYSTEM.md", async () => {
const projectDir = path.join(tempDir, "project");
fs.mkdirSync(path.join(projectDir, ".omp"), { recursive: true });
fs.mkdirSync(path.join(tempHomeDir, ".omp", "agent"), { recursive: true });
fs.writeFileSync(path.join(tempHomeDir, ".omp", "agent", "SYSTEM.md"), "User SYSTEM prompt");
fs.writeFileSync(path.join(projectDir, ".omp", "SYSTEM.md"), "Project SYSTEM prompt");
await expect(loadSystemPromptFiles({ cwd: projectDir })).resolves.toBe("Project SYSTEM prompt");
});
});