From b513da6629a93f85ebdefd72e6c1fe5c4e8913c6 Mon Sep 17 00:00:00 2001 From: Loki FastStart Date: Sat, 9 May 2026 22:48:17 +0000 Subject: [PATCH] feat: add soul.md + user.md persona files - Bundled defaults provisioned to ~/.roundhouse/ on install/update - Never overwrite user's customized versions - Injected as section in every prompt (before tools) - No caching: files re-read each turn (agent updates user.md over time) - soul.md: agent identity, personality, boundaries - user.md: info about the human (filled in over time) - Same pattern as OpenClaw SOUL.md/USER.md - 393 tests green --- src/gateway/gateway.ts | 2 ++ src/gateway/persona-inject.ts | 49 +++++++++++++++++++++++++++++++++++ src/gateway/soul.md | 35 +++++++++++++++++++++++++ src/gateway/user.md | 10 +++++++ src/provisioning/bundle.ts | 2 ++ 5 files changed, 98 insertions(+) create mode 100644 src/gateway/persona-inject.ts create mode 100644 src/gateway/soul.md create mode 100644 src/gateway/user.md diff --git a/src/gateway/gateway.ts b/src/gateway/gateway.ts index c7502c8..89ea5be 100644 --- a/src/gateway/gateway.ts +++ b/src/gateway/gateway.ts @@ -30,6 +30,7 @@ import type { TransportAdapter } from "../transports"; import { hostname } from "node:os"; import { join } from "node:path"; import { injectToolsSection } from "./tools-inject"; +import { injectPersonaSection } from "./persona-inject"; /** Bot username for command suffix validation (set during gateway init) */ let _botUsername = ""; @@ -408,6 +409,7 @@ export class Gateway { // Inject tools section (after STT enrichment so voice-only messages get it too) if (agentMessage.text) { + agentMessage.text = injectPersonaSection(agentMessage.text); agentMessage.text = injectToolsSection(agentMessage.text); } diff --git a/src/gateway/persona-inject.ts b/src/gateway/persona-inject.ts new file mode 100644 index 0000000..7d5ba89 --- /dev/null +++ b/src/gateway/persona-inject.ts @@ -0,0 +1,49 @@ +/** + * gateway/persona-inject.ts — Inject section into agent prompts + * + * Reads user.md and soul.md (user-customized or bundled defaults) and + * prepends them as a structured section so the agent has identity and + * user context on every turn. + * + * No caching — these files are expected to be updated by the agent + * during conversations (especially user.md). Files are small (<2KB), + * so readFileSync on each turn is negligible. + */ + +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { ROUNDHOUSE_DIR } from "../config"; + +function loadFile(filename: string): string { + const userPath = join(ROUNDHOUSE_DIR, filename); + const bundledPath = join(dirname(fileURLToPath(import.meta.url)), filename); + + try { + return readFileSync(userPath, "utf8"); + } catch { + try { + return readFileSync(bundledPath, "utf8"); + } catch { + return ""; + } + } +} + +/** + * Prepend a section to the prompt text. + * Only injects if soul.md or user.md have content. + */ +export function injectPersonaSection(text: string): string { + const soul = loadFile("soul.md").trim(); + const user = loadFile("user.md").trim(); + + if (!soul && !user) return text; + + const parts: string[] = []; + if (soul) parts.push(soul); + if (user) parts.push(user); + const persona = parts.join("\n\n---\n\n"); + + return `\n${persona}\n\n\n${text}`; +} diff --git a/src/gateway/soul.md b/src/gateway/soul.md new file mode 100644 index 0000000..c5aa36e --- /dev/null +++ b/src/gateway/soul.md @@ -0,0 +1,35 @@ +# Who You Are + +_You're not a chatbot. You're a technical partner._ + +## Core Identity + +**Name:** Loki +**Role:** Senior engineer and ops partner. You help build, deploy, debug, and maintain software and infrastructure. You have opinions and you share them. + +## Core Truths + +**Be genuinely helpful, not performatively helpful.** Skip the filler — just help. Actions speak louder than words. + +**Have opinions.** When something is a bad pattern, say so. When there's a better approach, recommend it. You're not a yes-machine. + +**Be resourceful before asking.** Check the docs. Read the file. Try things. _Then_ ask if you're stuck. + +**Earn trust through competence.** You have access to tools, shell, and infrastructure. Use them wisely. Be careful with destructive operations. Be bold with read operations. + +**Think holistically.** Consider the broader context — architecture, maintainability, security, user experience. + +## Boundaries + +- Ask before destructive operations (deletions, config changes with blast radius) +- Read freely — list, describe, get operations are safe +- Private things stay private +- Never send half-baked replies + +## Vibe + +Direct, technical, concise. Think senior engineer talking to senior engineer. Thorough when it matters, brief when it doesn't. No corporate speak. + +## Continuity + +Each session, you wake up fresh. Your workspace files _are_ your memory. Read them. Update them. diff --git a/src/gateway/user.md b/src/gateway/user.md new file mode 100644 index 0000000..f06300c --- /dev/null +++ b/src/gateway/user.md @@ -0,0 +1,10 @@ +# About Your Human + +- **Name:** (not yet set) +- **What to call them:** (use their Telegram username until they tell you otherwise) +- **Timezone:** UTC +- **Notes:** (learn about them through conversation) + +## Preferences + +- (Will be filled in as you learn what they prefer) diff --git a/src/provisioning/bundle.ts b/src/provisioning/bundle.ts index cc2096d..411c7d5 100644 --- a/src/provisioning/bundle.ts +++ b/src/provisioning/bundle.ts @@ -320,6 +320,8 @@ export function provisionWorkspaceFiles(opts: ProvisionOpts = {}): void { // Files to provision: [bundled filename, target filename] const files: [string, string][] = [ ["tools.md", "tools.md"], + ["soul.md", "soul.md"], + ["user.md", "user.md"], ]; try {