@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user