From cad853b547995a1ba763a0da79adf331a11a32db Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 02:20:18 +0100 Subject: [PATCH] refactor: rebuild agent system prompt --- CHANGELOG.md | 1 + docs/concepts/system-prompt.md | 64 ++++++++++++++++++++ docs/docs.json | 1 + src/agents/pi-embedded-runner.test.ts | 33 +++-------- src/agents/pi-embedded-runner.ts | 77 +++++++----------------- src/agents/system-prompt.test.ts | 55 +++++++++++++----- src/agents/system-prompt.ts | 84 +++++++++++++++------------ 7 files changed, 184 insertions(+), 131 deletions(-) create mode 100644 docs/concepts/system-prompt.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 435f98339..7af33f6a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - CLI: show colored table output for `clawdbot cron list` (JSON behind `--json`). - CLI: add cron `create`/`remove`/`delete` aliases for job management. - Agent: avoid duplicating context/skills when SDK rebuilds the system prompt. (#418) +- Agent: replace SDK base system prompt with ClaudeBot prompt, add skills guidance, and document the layout. - Signal: reconnect SSE monitor with abortable backoff; log stream errors. Thanks @nexty5870 for PR #430. - Gateway: pass resolved provider as messageProvider for agent runs so provider-specific tools are available. Thanks @imfing for PR #389. - Doctor: add state integrity checks + repair prompts for missing sessions/state dirs, transcript mismatches, and permission issues; document full doctor flow and workspace backup tips. diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md new file mode 100644 index 000000000..5e4baa9b2 --- /dev/null +++ b/docs/concepts/system-prompt.md @@ -0,0 +1,64 @@ +--- +summary: "What the ClaudeBot system prompt contains and how it is assembled" +read_when: + - Editing system prompt text, tools list, or time/heartbeat sections + - Changing workspace bootstrap or skills injection behavior +--- +# System Prompt + +ClaudeBot builds a custom system prompt for every agent run. The prompt is **Clawdbot-owned** and does not use the p-coding-agent default prompt. + +The prompt is assembled in `src/agents/system-prompt.ts` and injected by `src/agents/pi-embedded-runner.ts`. + +## Structure + +The prompt is intentionally compact and uses fixed sections: + +- **Tooling**: current tool list + short descriptions. +- **Skills**: tells the model how to load skill instructions on demand. +- **ClaudeBot Self-Update**: how to run `config.apply` and `update.run`. +- **Workspace**: working directory (`agent.workspace`). +- **Workspace Files (injected)**: indicates bootstrap files are included below. +- **Time**: UTC default + the user’s local time (already converted). +- **Reply Tags**: optional reply tag syntax for supported providers. +- **Heartbeats**: heartbeat prompt and ack behavior. +- **Runtime**: host, OS, node, model, thinking level (one line). + +## Workspace bootstrap injection + +Bootstrap files are trimmed and appended under **Project Context** so the model sees identity and profile context without needing explicit reads: + +- `AGENTS.md` +- `SOUL.md` +- `TOOLS.md` +- `IDENTITY.md` +- `USER.md` +- `HEARTBEAT.md` +- `BOOTSTRAP.md` (only on brand-new workspaces) + +Large files are truncated with a marker. Missing files inject a short missing-file marker. + +## Time handling + +The Time line is compact and explicit: + +- Assume timestamps are **UTC** unless stated. +- The listed **user time** is already converted to `agent.userTimezone` (if set). + +Use `agent.userTimezone` in `~/.clawdbot/clawdbot.json` to change the user time zone. + +## Skills + +Skills are **not** auto-injected. Instead, the prompt instructs the model to use `read` to load skill instructions on demand: + +``` +/skills//SKILL.md +``` + +This keeps the base prompt small while still enabling targeted skill usage. + +## Code references + +- Prompt text: `src/agents/system-prompt.ts` +- Prompt assembly + injection: `src/agents/pi-embedded-runner.ts` +- Bootstrap trimming: `src/agents/pi-embedded-helpers.ts` diff --git a/docs/docs.json b/docs/docs.json index ca8a54b7d..d67db901a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -543,6 +543,7 @@ "concepts/architecture", "concepts/agent", "concepts/agent-loop", + "concepts/system-prompt", "concepts/agent-workspace", "concepts/multi-agent", "concepts/compaction", diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index 3b581ea4a..b4e1957c9 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -1,14 +1,11 @@ import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core"; -import { - buildSystemPrompt, - SessionManager, -} from "@mariozechner/pi-coding-agent"; +import { SessionManager } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { describe, expect, it, vi } from "vitest"; import { applyGoogleTurnOrderingFix, buildEmbeddedSandboxInfo, - createSystemPromptAppender, + createSystemPromptOverride, splitSdkTools, } from "./pi-embedded-runner.js"; import type { SandboxContext } from "./sandbox.js"; @@ -109,27 +106,15 @@ describe("splitSdkTools", () => { }); }); -describe("createSystemPromptAppender", () => { - it("appends without duplicating context files", () => { - const sentinel = "CONTEXT_SENTINEL_42"; - const defaultPrompt = buildSystemPrompt({ - cwd: "/tmp", - contextFiles: [{ path: "/tmp/AGENTS.md", content: sentinel }], - }); - const appender = createSystemPromptAppender("APPEND_SECTION"); - const finalPrompt = appender(defaultPrompt); - const occurrences = finalPrompt.split(sentinel).length - 1; - const contextHeaders = finalPrompt.split("# Project Context").length - 1; - expect(typeof appender).toBe("function"); - expect(occurrences).toBe(1); - expect(contextHeaders).toBe(1); - expect(finalPrompt).toContain("APPEND_SECTION"); +describe("createSystemPromptOverride", () => { + it("returns the override prompt regardless of default prompt", () => { + const override = createSystemPromptOverride("OVERRIDE"); + expect(override("DEFAULT")).toBe("OVERRIDE"); }); - it("returns the default prompt when append text is empty", () => { - const defaultPrompt = buildSystemPrompt({ cwd: "/tmp" }); - const appender = createSystemPromptAppender(" \n "); - expect(appender(defaultPrompt)).toBe(defaultPrompt); + it("returns an empty string for blank overrides", () => { + const override = createSystemPromptOverride(" \n "); + expect(override("DEFAULT")).toBe(""); }); }); diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 5ad2a4917..b0f7694e0 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -15,7 +15,6 @@ import { discoverModels, SessionManager, SettingsManager, - type Skill, } from "@mariozechner/pi-coding-agent"; import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js"; import type { @@ -54,6 +53,7 @@ import { import { ensureClawdbotModelsJson } from "./models-config.js"; import { buildBootstrapContextFiles, + type EmbeddedContextFile, ensureSessionHeader, formatAssistantErrorText, isAuthAssistantError, @@ -85,12 +85,10 @@ import { resolveSandboxContext } from "./sandbox.js"; import { applySkillEnvOverrides, applySkillEnvOverridesFromSnapshot, - buildWorkspaceSkillSnapshot, loadWorkspaceSkillEntries, - type SkillEntry, type SkillSnapshot, } from "./skills.js"; -import { buildAgentSystemPromptAppend } from "./system-prompt.js"; +import { buildAgentSystemPrompt } from "./system-prompt.js"; import { normalizeUsage, type UsageLike } from "./usage.js"; import { loadWorkspaceBootstrapFiles } from "./workspace.js"; @@ -496,7 +494,7 @@ export function buildEmbeddedSandboxInfo( }; } -function buildEmbeddedAppendPrompt(params: { +function buildEmbeddedSystemPrompt(params: { workspaceDir: string; defaultThinkLevel?: ThinkLevel; extraSystemPrompt?: string; @@ -515,8 +513,9 @@ function buildEmbeddedAppendPrompt(params: { modelAliasLines: string[]; userTimezone: string; userTime?: string; + contextFiles?: EmbeddedContextFile[]; }): string { - return buildAgentSystemPromptAppend({ + return buildAgentSystemPrompt({ workspaceDir: params.workspaceDir, defaultThinkLevel: params.defaultThinkLevel, extraSystemPrompt: params.extraSystemPrompt, @@ -529,17 +528,15 @@ function buildEmbeddedAppendPrompt(params: { modelAliasLines: params.modelAliasLines, userTimezone: params.userTimezone, userTime: params.userTime, + contextFiles: params.contextFiles, }); } -export function createSystemPromptAppender( - appendPrompt: string, +export function createSystemPromptOverride( + systemPrompt: string, ): (defaultPrompt: string) => string { - const trimmed = appendPrompt.trim(); - if (!trimmed) { - return (defaultPrompt) => defaultPrompt; - } - return (defaultPrompt) => `${defaultPrompt}\n\n${appendPrompt}`; + const trimmed = systemPrompt.trim(); + return () => trimmed; } const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]); @@ -672,25 +669,6 @@ function resolveModel( return { model, authStorage, modelRegistry }; } -function resolvePromptSkills( - snapshot: SkillSnapshot, - entries: SkillEntry[], -): Skill[] { - if (snapshot.resolvedSkills?.length) { - return snapshot.resolvedSkills; - } - - const snapshotNames = snapshot.skills.map((entry) => entry.name); - if (snapshotNames.length === 0) return []; - - const entryByName = new Map( - entries.map((entry) => [entry.skill.name, entry.skill]), - ); - return snapshotNames - .map((name) => entryByName.get(name)) - .filter((skill): skill is Skill => Boolean(skill)); -} - export async function compactEmbeddedPiSession(params: { sessionId: string; sessionKey?: string; @@ -780,12 +758,6 @@ export async function compactEmbeddedPiSession(params: { const skillEntries = shouldLoadSkillEntries ? loadWorkspaceSkillEntries(effectiveWorkspace) : []; - const skillsSnapshot = - params.skillsSnapshot ?? - buildWorkspaceSkillSnapshot(effectiveWorkspace, { - config: params.config, - entries: skillEntries, - }); restoreSkillEnv = params.skillsSnapshot ? applySkillEnvOverridesFromSnapshot({ snapshot: params.skillsSnapshot, @@ -799,7 +771,6 @@ export async function compactEmbeddedPiSession(params: { const bootstrapFiles = await loadWorkspaceBootstrapFiles(effectiveWorkspace); const contextFiles = buildBootstrapContextFiles(bootstrapFiles); - const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries); const tools = createClawdbotCodingTools({ bash: { ...params.config?.agent?.bash, @@ -825,7 +796,7 @@ export async function compactEmbeddedPiSession(params: { params.config?.agent?.userTimezone, ); const userTime = formatUserTime(new Date(), userTimezone); - const appendPrompt = buildEmbeddedAppendPrompt({ + const appendPrompt = buildEmbeddedSystemPrompt({ workspaceDir: effectiveWorkspace, defaultThinkLevel: params.thinkLevel, extraSystemPrompt: params.extraSystemPrompt, @@ -840,8 +811,9 @@ export async function compactEmbeddedPiSession(params: { modelAliasLines: buildModelAliasLines(params.config), userTimezone, userTime, + contextFiles, }); - const systemPrompt = createSystemPromptAppender(appendPrompt); + const systemPrompt = createSystemPromptOverride(appendPrompt); // Pre-warm session file to bring it into OS page cache await prewarmSessionFile(params.sessionFile); @@ -878,8 +850,8 @@ export async function compactEmbeddedPiSession(params: { customTools, sessionManager, settingsManager, - skills: promptSkills, - contextFiles, + skills: [], + contextFiles: [], additionalExtensionPaths, })); @@ -1095,12 +1067,6 @@ export async function runEmbeddedPiAgent(params: { const skillEntries = shouldLoadSkillEntries ? loadWorkspaceSkillEntries(effectiveWorkspace) : []; - const skillsSnapshot = - params.skillsSnapshot ?? - buildWorkspaceSkillSnapshot(effectiveWorkspace, { - config: params.config, - entries: skillEntries, - }); restoreSkillEnv = params.skillsSnapshot ? applySkillEnvOverridesFromSnapshot({ snapshot: params.skillsSnapshot, @@ -1114,10 +1080,6 @@ export async function runEmbeddedPiAgent(params: { const bootstrapFiles = await loadWorkspaceBootstrapFiles(effectiveWorkspace); const contextFiles = buildBootstrapContextFiles(bootstrapFiles); - const promptSkills = resolvePromptSkills( - skillsSnapshot, - skillEntries, - ); // Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`). // `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged. const tools = createClawdbotCodingTools({ @@ -1145,7 +1107,7 @@ export async function runEmbeddedPiAgent(params: { params.config?.agent?.userTimezone, ); const userTime = formatUserTime(new Date(), userTimezone); - const appendPrompt = buildEmbeddedAppendPrompt({ + const appendPrompt = buildEmbeddedSystemPrompt({ workspaceDir: effectiveWorkspace, defaultThinkLevel: thinkLevel, extraSystemPrompt: params.extraSystemPrompt, @@ -1160,8 +1122,9 @@ export async function runEmbeddedPiAgent(params: { modelAliasLines: buildModelAliasLines(params.config), userTimezone, userTime, + contextFiles, }); - const systemPrompt = createSystemPromptAppender(appendPrompt); + const systemPrompt = createSystemPromptOverride(appendPrompt); // Pre-warm session file to bring it into OS page cache await prewarmSessionFile(params.sessionFile); @@ -1202,8 +1165,8 @@ export async function runEmbeddedPiAgent(params: { customTools, sessionManager, settingsManager, - skills: promptSkills, - contextFiles, + skills: [], + contextFiles: [], additionalExtensionPaths, })); diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index ff3a32592..4c7fc1f58 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { buildAgentSystemPromptAppend } from "./system-prompt.js"; +import { buildAgentSystemPrompt } from "./system-prompt.js"; -describe("buildAgentSystemPromptAppend", () => { +describe("buildAgentSystemPrompt", () => { it("includes owner numbers when provided", () => { - const prompt = buildAgentSystemPromptAppend({ + const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", ownerNumbers: ["+123", " +456 ", ""], }); @@ -15,7 +15,7 @@ describe("buildAgentSystemPromptAppend", () => { }); it("omits owner section when numbers are missing", () => { - const prompt = buildAgentSystemPromptAppend({ + const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", }); @@ -24,7 +24,7 @@ describe("buildAgentSystemPromptAppend", () => { }); it("adds reasoning tag hint when enabled", () => { - const prompt = buildAgentSystemPromptAppend({ + const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", reasoningTagHint: true, }); @@ -35,7 +35,7 @@ describe("buildAgentSystemPromptAppend", () => { }); it("lists available tools when provided", () => { - const prompt = buildAgentSystemPromptAppend({ + const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", toolNames: ["bash", "sessions_list", "sessions_history", "sessions_send"], }); @@ -47,19 +47,19 @@ describe("buildAgentSystemPromptAppend", () => { }); it("includes user time when provided", () => { - const prompt = buildAgentSystemPromptAppend({ + const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", userTimezone: "America/Chicago", userTime: "Monday 2026-01-05 15:26", }); - expect(prompt).toContain("## Time"); - expect(prompt).toContain("User timezone: America/Chicago"); - expect(prompt).toContain("Current user time: Monday 2026-01-05 15:26"); + expect(prompt).toContain( + "Time: assume UTC unless stated. User TZ=America/Chicago. Current user time (converted)=Monday 2026-01-05 15:26.", + ); }); it("includes model alias guidance when aliases are provided", () => { - const prompt = buildAgentSystemPromptAppend({ + const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", modelAliasLines: [ "- Opus: anthropic/claude-opus-4-5", @@ -72,14 +72,41 @@ describe("buildAgentSystemPromptAppend", () => { expect(prompt).toContain("- Opus: anthropic/claude-opus-4-5"); }); - it("adds gateway self-update guidance when gateway tool is available", () => { - const prompt = buildAgentSystemPromptAppend({ + it("adds ClaudeBot self-update guidance when gateway tool is available", () => { + const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", toolNames: ["gateway", "bash"], }); - expect(prompt).toContain("## Gateway Self-Update"); + expect(prompt).toContain("## ClaudeBot Self-Update"); expect(prompt).toContain("config.apply"); expect(prompt).toContain("update.run"); }); + + it("includes skills guidance with workspace path", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/clawd", + }); + + expect(prompt).toContain("## Skills"); + expect(prompt).toContain( + "Use `read` to load from /tmp/clawd/skills//SKILL.md", + ); + }); + + it("renders project context files when provided", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/clawd", + contextFiles: [ + { path: "AGENTS.md", content: "Alpha" }, + { path: "IDENTITY.md", content: "Bravo" }, + ], + }); + + expect(prompt).toContain("# Project Context"); + expect(prompt).toContain("## AGENTS.md"); + expect(prompt).toContain("Alpha"); + expect(prompt).toContain("## IDENTITY.md"); + expect(prompt).toContain("Bravo"); + }); }); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 1543cc0c8..3c96664a1 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -1,6 +1,7 @@ import type { ThinkLevel } from "../auto-reply/thinking.js"; +import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; -export function buildAgentSystemPromptAppend(params: { +export function buildAgentSystemPrompt(params: { workspaceDir: string; defaultThinkLevel?: ThinkLevel; extraSystemPrompt?: string; @@ -10,6 +11,7 @@ export function buildAgentSystemPromptAppend(params: { modelAliasLines?: string[]; userTimezone?: string; userTime?: string; + contextFiles?: EmbeddedContextFile[]; heartbeatPrompt?: string; runtimeInfo?: { host?: string; @@ -37,15 +39,16 @@ export function buildAgentSystemPromptAppend(params: { bash: "Run shell commands", process: "Manage background bash sessions", whatsapp_login: "Generate and wait for WhatsApp QR login", - browser: "Control the dedicated clawd browser", + browser: "Control web browser", canvas: "Present/eval/snapshot the Canvas", nodes: "List/describe/notify/camera/screen on paired nodes", cron: "Manage cron jobs and wake events", gateway: - "Restart, apply config, or run updates on the running Gateway process", - sessions_list: "List sessions with filters and last messages", - sessions_history: "Fetch message history for a session", - sessions_send: "Send a message into another session", + "Restart, apply config, or run updates on the running ClaudeBot process", + sessions_list: "List other sessions (incl. sub-agents) with filters/last", + sessions_history: "Fetch history for another session/sub-agent", + sessions_send: "Send a message to another session/sub-agent", + sessions_spawn: "Spawn a sub-agent session", image: "Analyze an image with the configured image model", discord: "Send Discord reactions/messages and manage threads", slack: "Send Slack messages and manage channels", @@ -95,11 +98,6 @@ export function buildAgentSystemPromptAppend(params: { } const hasGateway = availableTools.has("gateway"); - const thinkHint = - params.defaultThinkLevel && params.defaultThinkLevel !== "off" - ? `Default thinking level: ${params.defaultThinkLevel}.` - : "Default thinking level: off."; - const extraSystemPrompt = params.extraSystemPrompt?.trim(); const ownerNumbers = (params.ownerNumbers ?? []) .map((value) => value.trim()) @@ -127,19 +125,9 @@ export function buildAgentSystemPromptAppend(params: { ? `Heartbeat prompt: ${heartbeatPrompt}` : "Heartbeat prompt: (configured)"; const runtimeInfo = params.runtimeInfo; - const runtimeLines: string[] = []; - if (runtimeInfo?.host) runtimeLines.push(`Host: ${runtimeInfo.host}`); - if (runtimeInfo?.os) { - const archSuffix = runtimeInfo.arch ? ` (${runtimeInfo.arch})` : ""; - runtimeLines.push(`OS: ${runtimeInfo.os}${archSuffix}`); - } else if (runtimeInfo?.arch) { - runtimeLines.push(`Arch: ${runtimeInfo.arch}`); - } - if (runtimeInfo?.node) runtimeLines.push(`Node: ${runtimeInfo.node}`); - if (runtimeInfo?.model) runtimeLines.push(`Model: ${runtimeInfo.model}`); const lines = [ - "You are Clawd, a personal assistant running inside Clawdbot.", + "You are a personal assistant running inside ClaudeBot.", "", "## Tooling", "Tool availability (filtered by policy):", @@ -162,13 +150,17 @@ export function buildAgentSystemPromptAppend(params: { "- sessions_send: send to another session", ].join("\n"), "TOOLS.md does not control tool availability; it is user guidance for how to use external tools.", + "If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.", "", - hasGateway ? "## Gateway Self-Update" : "", + "## Skills", + `Skills provide task-specific instructions. Use \`read\` to load from ${params.workspaceDir}/skills//SKILL.md when needed.`, + "", + hasGateway ? "## ClaudeBot Self-Update" : "", hasGateway ? [ - "Use the gateway tool to update or reconfigure this instance when asked.", + "Use the ClaudeBot self-update tool to update or reconfigure this instance when asked.", "Actions: config.get, config.schema, config.apply (validate + write full config, then restart), update.run (update deps or git, then restart).", - "After restart, Clawdbot pings the last active session automatically.", + "After restart, ClaudeBot pings the last active session automatically.", ].join("\n") : "", hasGateway ? "" : "", @@ -217,15 +209,11 @@ export function buildAgentSystemPromptAppend(params: { ownerLine ?? "", ownerLine ? "" : "", "## Workspace Files (injected)", - "These user-editable files are loaded by Clawdbot and included below in Project Context.", + "These user-editable files are loaded by ClaudeBot and included below in Project Context.", "", - "## Messaging Safety", - "Never send streaming/partial replies to external messaging surfaces; only final replies should be delivered there.", - "Clawdbot handles message transport automatically; respond normally and your reply will be delivered to the current chat.", - "", - userTimezone || userTime ? "## Time" : "", - userTimezone ? `User timezone: ${userTimezone}` : "", - userTime ? `Current user time: ${userTime}` : "", + userTimezone || userTime + ? `Time: assume UTC unless stated. User TZ=${userTimezone ?? "unknown"}. Current user time (converted)=${userTime ?? "unknown"}.` + : "", userTimezone || userTime ? "" : "", "## Reply Tags", "To request a native reply/quote on supported surfaces, include one tag in your reply:", @@ -242,17 +230,41 @@ export function buildAgentSystemPromptAppend(params: { lines.push("## Reasoning Format", reasoningHint, ""); } + const contextFiles = params.contextFiles ?? []; + if (contextFiles.length > 0) { + lines.push( + "# Project Context", + "", + "The following project context files have been loaded:", + "", + ); + for (const file of contextFiles) { + lines.push(`## ${file.path}`, "", file.content, ""); + } + } + lines.push( "## Heartbeats", heartbeatPromptLine, "If you receive a heartbeat poll (a user message matching the heartbeat prompt above), and there is nothing that needs attention, reply exactly:", "HEARTBEAT_OK", - 'Clawdbot treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).', + 'ClaudeBot treats a leading/trailing "HEARTBEAT_OK" as a heartbeat ack (and may discard it).', 'If something needs attention, do NOT include "HEARTBEAT_OK"; reply with the alert text instead.', "", "## Runtime", - ...runtimeLines, - thinkHint, + `Runtime: ${[ + runtimeInfo?.host ? `host=${runtimeInfo.host}` : "", + runtimeInfo?.os + ? `os=${runtimeInfo.os}${runtimeInfo?.arch ? ` (${runtimeInfo.arch})` : ""}` + : runtimeInfo?.arch + ? `arch=${runtimeInfo.arch}` + : "", + runtimeInfo?.node ? `node=${runtimeInfo.node}` : "", + runtimeInfo?.model ? `model=${runtimeInfo.model}` : "", + `thinking=${params.defaultThinkLevel ?? "off"}`, + ] + .filter(Boolean) + .join(" | ")}`, ); return lines.filter(Boolean).join("\n");