Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/gateway/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand Down Expand Up @@ -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);
}

Expand Down
49 changes: 49 additions & 0 deletions src/gateway/persona-inject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* gateway/persona-inject.ts — Inject <persona> 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 <persona> 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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope learned user persona per conversation identity

This always loads a single global user.md file and injects it into every turn, which means notes learned from one person are automatically shown to all chats/users handled by this gateway. In environments with multiple allowedUsers or shared bots, that causes cross-user context bleed and privacy leakage; persona state should be keyed per user/chat (or disabled when multiple users are configured).

Useful? React with 👍 / 👎.


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 `<persona>\n${persona}\n</persona>\n\n${text}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape persona content before closing the persona block

injectPersonaSection inserts raw soul.md/user.md text inside <persona>...</persona> without escaping, so any literal </persona> in those files will terminate the wrapper early and leak subsequent content into the main prompt. Because this feature is intended to evolve user.md from conversation data, a crafted note can create persistent prompt-injection behavior across turns; this should be sanitized the same way tools-inject.ts sanitizes <tools> tags.

Useful? React with 👍 / 👎.

}
35 changes: 35 additions & 0 deletions src/gateway/soul.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions src/gateway/user.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions src/provisioning/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading