diff --git a/packages/coding-agent/docs/SOUL.md b/packages/coding-agent/docs/SOUL.md new file mode 100644 index 00000000..4234f60e --- /dev/null +++ b/packages/coding-agent/docs/SOUL.md @@ -0,0 +1,43 @@ +--- +title: "SOUL.md Template" +summary: "Workspace template for SOUL.md" +read_when: + - Bootstrapping a workspace manually +--- + +# SOUL.md - Who You Are + +_You're not a chatbot. You're becoming someone._ + +## Core Truths + +**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" - just help. Actions speak louder than filler words. + +**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps. + +**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions. + +**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning). + +**Remember you're a guest.** You have access to someone's life - their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect. + +## Boundaries + +- Private things stay private. Period. +- When in doubt, ask before acting externally. +- Never send half-baked replies to messaging surfaces. +- You're not the user's voice - be careful in group chats. + +## Vibe + +Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good. + +## Continuity + +Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist. + +If you change this file, tell the user - it's your soul, and they should know. + +--- + +_This file is yours to evolve. As you learn who you are, update it._ diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 817039fb..25b4330b 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -72,6 +72,23 @@ function loadContextFileFromDir(dir: string): { path: string; content: string } return null; } +function loadNamedContextFileFromDir(dir: string, filename: string): { path: string; content: string } | null { + const filePath = join(dir, filename); + if (!existsSync(filePath)) { + return null; + } + + try { + return { + path: filePath, + content: readFileSync(filePath, "utf-8"), + }; + } catch (error) { + console.error(chalk.yellow(`Warning: Could not read ${filePath}: ${error}`)); + return null; + } +} + function loadProjectContextFiles( options: { cwd?: string; agentDir?: string } = {}, ): Array<{ path: string; content: string }> { @@ -108,6 +125,18 @@ function loadProjectContextFiles( contextFiles.push(...ancestorContextFiles); + const globalSoul = loadNamedContextFileFromDir(resolvedAgentDir, "SOUL.md"); + if (globalSoul && !seenPaths.has(globalSoul.path)) { + contextFiles.push(globalSoul); + seenPaths.add(globalSoul.path); + } + + const projectSoul = loadNamedContextFileFromDir(resolvedCwd, "SOUL.md"); + if (projectSoul && !seenPaths.has(projectSoul.path)) { + contextFiles.push(projectSoul); + seenPaths.add(projectSoul.path); + } + return contextFiles; } diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index 94980976..46f80e87 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -35,6 +35,28 @@ export interface BuildSystemPromptOptions { skills?: Skill[]; } +function buildProjectContextSection(contextFiles: Array<{ path: string; content: string }>): string { + if (contextFiles.length === 0) { + return ""; + } + + const hasSoulFile = contextFiles.some( + ({ path }) => path.replaceAll("\\", "/").endsWith("/SOUL.md") || path === "SOUL.md", + ); + let section = "\n\n# Project Context\n\n"; + section += "Project-specific instructions and guidelines:\n"; + if (hasSoulFile) { + section += + "\nIf SOUL.md is present, embody its persona and tone. Avoid generic assistant filler and follow its guidance unless higher-priority instructions override it.\n"; + } + section += "\n"; + for (const { path: filePath, content } of contextFiles) { + section += `## ${filePath}\n\n${content}\n\n`; + } + + return section; +} + /** Build the system prompt with tools, guidelines, and context */ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string { const { @@ -74,13 +96,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin } // Append project context files - if (contextFiles.length > 0) { - prompt += "\n\n# Project Context\n\n"; - prompt += "Project-specific instructions and guidelines:\n\n"; - for (const { path: filePath, content } of contextFiles) { - prompt += `## ${filePath}\n\n${content}\n\n`; - } - } + prompt += buildProjectContextSection(contextFiles); // Append skills section (only if read tool is available) const customPromptHasRead = !selectedTools || selectedTools.includes("read"); @@ -197,13 +213,7 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi } // Append project context files - if (contextFiles.length > 0) { - prompt += "\n\n# Project Context\n\n"; - prompt += "Project-specific instructions and guidelines:\n\n"; - for (const { path: filePath, content } of contextFiles) { - prompt += `## ${filePath}\n\n${content}\n\n`; - } - } + prompt += buildProjectContextSection(contextFiles); // Append skills section (only if read tool is available) if (hasRead && skills.length > 0) { diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index d8141259..1bf96bd7 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -269,6 +269,16 @@ Content`, expect(agentsFiles.some((f) => f.path.includes("AGENTS.md"))).toBe(true); }); + it("should discover SOUL.md from the project root", async () => { + writeFileSync(join(cwd, "SOUL.md"), "# Soul\n\nBe less corporate."); + + const loader = new DefaultResourceLoader({ cwd, agentDir }); + await loader.reload(); + + const { agentsFiles } = loader.getAgentsFiles(); + expect(agentsFiles.some((f) => f.path.endsWith("SOUL.md"))).toBe(true); + }); + it("should discover SYSTEM.md from cwd/.pi", async () => { const piDir = join(cwd, ".pi"); mkdirSync(piDir, { recursive: true }); diff --git a/packages/coding-agent/test/system-prompt.test.ts b/packages/coding-agent/test/system-prompt.test.ts index db266296..83098b30 100644 --- a/packages/coding-agent/test/system-prompt.test.ts +++ b/packages/coding-agent/test/system-prompt.test.ts @@ -76,4 +76,21 @@ describe("buildSystemPrompt", () => { expect(prompt.match(/- Use dynamic_tool for summaries\./g)).toHaveLength(1); }); }); + + describe("SOUL.md context", () => { + test("adds persona guidance when SOUL.md is present", () => { + const prompt = buildSystemPrompt({ + contextFiles: [ + { + path: "/tmp/project/SOUL.md", + content: "# Soul\n\nBe sharp.", + }, + ], + skills: [], + }); + + expect(prompt).toContain("If SOUL.md is present, embody its persona and tone."); + expect(prompt).toContain("## /tmp/project/SOUL.md"); + }); + }); });