feat: inject SOUL.md/IDENTITY.md persona and detect workspace context files

- input-filter: scan workspace for SOUL.md, IDENTITY.md, MEMORY.md etc. on each turn
- bootstrap: when these files exist, instruct Claude to Read them at session start
- This gives the contractor agent its OpenClaw persona (SOUL.md embody, IDENTITY.md fill)
- Memory note added to bootstrap when workspace has memory files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
h z
2026-04-11 15:41:25 +01:00
parent 76a7931f97
commit fb1ec1b5c4
3 changed files with 61 additions and 3 deletions

View File

@@ -4,6 +4,8 @@ export type BootstrapInput = {
workspace: string; workspace: string;
/** Skills XML block extracted from the OpenClaw system prompt, if any */ /** Skills XML block extracted from the OpenClaw system prompt, if any */
skillsBlock?: string; skillsBlock?: string;
/** Subset of OpenClaw context files present in the workspace (for persona/identity) */
workspaceContextFiles?: string[];
}; };
/** /**
@@ -12,6 +14,10 @@ export type BootstrapInput = {
* Must NOT be re-injected on every turn. * Must NOT be re-injected on every turn.
*/ */
export function buildBootstrap(input: BootstrapInput): string { export function buildBootstrap(input: BootstrapInput): string {
const contextFiles = input.workspaceContextFiles ?? [];
const hasSoul = contextFiles.includes("SOUL.md");
const hasIdentity = contextFiles.includes("IDENTITY.md");
const lines = [ const lines = [
`You are operating as a contractor agent inside OpenClaw.`, `You are operating as a contractor agent inside OpenClaw.`,
``, ``,
@@ -32,6 +38,38 @@ export function buildBootstrap(input: BootstrapInput): string {
`- If a task is unclear, ask one focused clarifying question.`, `- If a task is unclear, ask one focused clarifying question.`,
]; ];
// Inject persona/identity context from OpenClaw workspace files.
// These files define who this agent is and how it should behave.
if (hasSoul || hasIdentity) {
lines.push(``, `## Persona & Identity`);
if (hasSoul) {
lines.push(
`Read ${input.workspace}/SOUL.md now (using the Read tool) and embody the persona and tone it describes.`,
`SOUL.md defines your values, communication style, and boundaries — treat it as authoritative.`,
);
}
if (hasIdentity) {
lines.push(
`Read ${input.workspace}/IDENTITY.md now (using the Read tool) to understand your name, creature type, and personal vibe.`,
`If IDENTITY.md is empty/template-only, you may fill it in as you develop your identity.`,
);
}
}
// Memory system note: OpenClaw uses workspace/memory/*.md + MEMORY.md.
// Claude Code also maintains per-project auto-memory at ~/.claude/projects/<path>/memory/.
// Both are active; Claude Code's auto-memory is loaded automatically each session.
// OpenClaw memory tools (memory_search, memory_get) are available as MCP tools
// but require bridge-side implementations to execute (not yet wired).
if (contextFiles.includes("MEMORY.md") || contextFiles.some(f => f.startsWith("memory/"))) {
lines.push(
``,
`## Memory`,
`OpenClaw memory files exist in ${input.workspace}/memory/ and ${input.workspace}/MEMORY.md.`,
`Use the Read tool to access these directly. For writing new memories, write to ${input.workspace}/memory/<topic>.md.`,
);
}
if (input.skillsBlock) { if (input.skillsBlock) {
lines.push( lines.push(
``, ``,

View File

@@ -1,3 +1,5 @@
import fs from "node:fs";
import path from "node:path";
import type { BridgeInboundRequest, OpenAIMessage } from "../types/model.js"; import type { BridgeInboundRequest, OpenAIMessage } from "../types/model.js";
function messageText(m: OpenAIMessage): string { function messageText(m: OpenAIMessage): string {
@@ -33,6 +35,8 @@ export type RequestContext = {
workspace: string; workspace: string;
/** Raw <available_skills>...</available_skills> XML block from the system prompt */ /** Raw <available_skills>...</available_skills> XML block from the system prompt */
skillsBlock: string; skillsBlock: string;
/** OpenClaw context files present in the workspace (SOUL.md, IDENTITY.md, etc.) */
workspaceContextFiles: string[];
}; };
/** /**
@@ -67,9 +71,25 @@ export function extractRequestContext(req: BridgeInboundRequest): RequestContext
? skillsMatch[0].replace(/~\//g, `${home}/`) ? skillsMatch[0].replace(/~\//g, `${home}/`)
: ""; : "";
// Detect which OpenClaw context files are present in the workspace.
// These tell us what persona/memory files to surface to Claude.
const workspace = repoMatch?.[1] ?? "";
const CONTEXT_FILES = ["SOUL.md", "IDENTITY.md", "MEMORY.md", "AGENTS.md", "USER.md"];
const workspaceContextFiles: string[] = [];
if (workspace) {
for (const f of CONTEXT_FILES) {
if (fs.existsSync(path.join(workspace, f))) workspaceContextFiles.push(f);
}
// Also check for memory/ directory
if (fs.existsSync(path.join(workspace, "memory"))) {
workspaceContextFiles.push("memory/");
}
}
return { return {
agentId: agentMatch?.[1] ?? "", agentId: agentMatch?.[1] ?? "",
workspace: repoMatch?.[1] ?? "", workspace,
skillsBlock, skillsBlock,
workspaceContextFiles,
}; };
} }

View File

@@ -104,7 +104,7 @@ export function createBridgeServer(config: BridgeServerConfig): http.Server {
// Extract agent ID and workspace from the system prompt's Runtime line. // Extract agent ID and workspace from the system prompt's Runtime line.
// OpenClaw does NOT send agent/session info as HTTP headers — it's in the system prompt. // OpenClaw does NOT send agent/session info as HTTP headers — it's in the system prompt.
const { agentId: parsedAgentId, workspace: parsedWorkspace, skillsBlock } = extractRequestContext(body); const { agentId: parsedAgentId, workspace: parsedWorkspace, skillsBlock, workspaceContextFiles } = extractRequestContext(body);
const latestMessage = extractLatestUserMessage(body); const latestMessage = extractLatestUserMessage(body);
if (!latestMessage) { if (!latestMessage) {
@@ -139,7 +139,7 @@ export function createBridgeServer(config: BridgeServerConfig): http.Server {
// Build prompt: bootstrap on first turn, bare message on subsequent turns // Build prompt: bootstrap on first turn, bare message on subsequent turns
const isFirstTurn = !claudeSessionId; const isFirstTurn = !claudeSessionId;
const prompt = isFirstTurn const prompt = isFirstTurn
? `${buildBootstrap({ agentId, openclawSessionKey: sessionKey, workspace, skillsBlock: skillsBlock || undefined })}\n\n---\n\n${latestMessage}` ? `${buildBootstrap({ agentId, openclawSessionKey: sessionKey, workspace, skillsBlock: skillsBlock || undefined, workspaceContextFiles })}\n\n---\n\n${latestMessage}`
: latestMessage; : latestMessage;
// Start SSE response // Start SSE response