style: format agent workspace and prompts

This commit is contained in:
Peter Steinberger
2026-01-22 08:05:47 +00:00
parent 87baca82db
commit 1a8b106f34
8 changed files with 85 additions and 1 deletions

View File

@@ -140,6 +140,9 @@ workspace lives).
### 1) Initialize the repo
If git is installed, brand-new workspaces are initialized automatically. If this
workspace is not already a repo, run:
```bash
cd ~/clawd
git init

View File

@@ -95,7 +95,7 @@ Clawd reads operating instructions and “memory” from its workspace directory
By default, Clawdbot uses `~/clawd` as the agent workspace, and will create it (plus starter `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`) automatically on setup/first agent run. `BOOTSTRAP.md` is only created when the workspace is brand new (it should not come back after you delete it).
Tip: treat this folder like Clawds “memory” and make it a git repo (ideally private) so your `AGENTS.md` + memory files are backed up.
Tip: treat this folder like Clawds “memory” and make it a git repo (ideally private) so your `AGENTS.md` + memory files are backed up. If git is installed, brand-new workspaces are auto-initialized.
```bash
clawdbot setup

View File

@@ -46,6 +46,7 @@ import {
loadWorkspaceSkillEntries,
resolveSkillsPromptForRun,
} from "../../skills.js";
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
import { buildSystemPromptReport } from "../../system-prompt-report.js";
import { resolveDefaultModelForAgent } from "../../model-selection.js";
@@ -184,6 +185,11 @@ export async function runEmbeddedAttempt(
sessionId: params.sessionId,
warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }),
});
const workspaceNotes = hookAdjustedBootstrapFiles.some(
(file) => file.name === DEFAULT_BOOTSTRAP_FILENAME && !file.missing,
)
? ["Reminder: commit your changes in this workspace after edits."]
: undefined;
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
@@ -314,6 +320,7 @@ export async function runEmbeddedAttempt(
: undefined,
skillsPrompt,
docsPath: docsPath ?? undefined,
workspaceNotes,
reactionGuidance,
promptMode,
runtimeInfo,

View File

@@ -20,6 +20,7 @@ export function buildEmbeddedSystemPrompt(params: {
level: "minimal" | "extensive";
channel: string;
};
workspaceNotes?: string[];
/** Controls which hardcoded sections to include. Defaults to "full". */
promptMode?: PromptMode;
runtimeInfo: {
@@ -54,6 +55,7 @@ export function buildEmbeddedSystemPrompt(params: {
heartbeatPrompt: params.heartbeatPrompt,
skillsPrompt: params.skillsPrompt,
docsPath: params.docsPath,
workspaceNotes: params.workspaceNotes,
reactionGuidance: params.reactionGuidance,
promptMode: params.promptMode,
runtimeInfo: params.runtimeInfo,

View File

@@ -115,6 +115,15 @@ describe("buildAgentSystemPrompt", () => {
);
});
it("includes workspace notes when provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",
workspaceNotes: ["Reminder: commit your changes in this workspace after edits."],
});
expect(prompt).toContain("Reminder: commit your changes in this workspace after edits.");
});
it("includes user time when provided (12-hour)", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",

View File

@@ -148,6 +148,7 @@ export function buildAgentSystemPrompt(params: {
skillsPrompt?: string;
heartbeatPrompt?: string;
docsPath?: string;
workspaceNotes?: string[];
/** Controls which hardcoded sections to include. Defaults to "full". */
promptMode?: PromptMode;
runtimeInfo?: {
@@ -327,6 +328,7 @@ export function buildAgentSystemPrompt(params: {
isMinimal,
readToolName,
});
const workspaceNotes = (params.workspaceNotes ?? []).map((note) => note.trim()).filter(Boolean);
// For "none" mode, return just the basic identity line
if (promptMode === "none") {
@@ -403,6 +405,7 @@ export function buildAgentSystemPrompt(params: {
"## Workspace",
`Your working directory is: ${params.workspaceDir}`,
"Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.",
...workspaceNotes,
"",
...docsSection,
params.sandboxInfo?.enabled ? "## Sandbox" : "",

View File

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { runCommandWithTimeout } from "../process/exec.js";
import type { WorkspaceBootstrapFile } from "./workspace.js";
import {
DEFAULT_AGENTS_FILENAME,
@@ -40,6 +41,34 @@ describe("ensureAgentWorkspace", () => {
await expect(fs.stat(bootstrap)).resolves.toBeDefined();
});
it("initializes a git repo for brand-new workspaces when git is available", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-"));
const nested = path.join(dir, "nested");
const gitAvailable = await runCommandWithTimeout(["git", "--version"], { timeoutMs: 2_000 })
.then((res) => res.code === 0)
.catch(() => false);
if (!gitAvailable) return;
await ensureAgentWorkspace({
dir: nested,
ensureBootstrapFiles: true,
});
await expect(fs.stat(path.join(nested, ".git"))).resolves.toBeDefined();
});
it("does not initialize git when workspace already exists", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-"));
await fs.writeFile(path.join(dir, "AGENTS.md"), "custom", "utf-8");
await ensureAgentWorkspace({
dir,
ensureBootstrapFiles: true,
});
await expect(fs.stat(path.join(dir, ".git"))).rejects.toBeDefined();
});
it("does not overwrite existing AGENTS.md", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-"));
const agentsPath = path.join(dir, "AGENTS.md");

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { isSubagentSessionKey } from "../routing/session-key.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { resolveUserPath } from "../utils.js";
export function resolveDefaultAgentWorkspaceDir(
@@ -81,6 +82,35 @@ async function writeFileIfMissing(filePath: string, content: string) {
}
}
async function hasGitRepo(dir: string): Promise<boolean> {
try {
await fs.stat(path.join(dir, ".git"));
return true;
} catch {
return false;
}
}
async function isGitAvailable(): Promise<boolean> {
try {
const result = await runCommandWithTimeout(["git", "--version"], { timeoutMs: 2_000 });
return result.code === 0;
} catch {
return false;
}
}
async function ensureGitRepo(dir: string, isBrandNewWorkspace: boolean) {
if (!isBrandNewWorkspace) return;
if (await hasGitRepo(dir)) return;
if (!(await isGitAvailable())) return;
try {
await runCommandWithTimeout(["git", "init"], { cwd: dir, timeoutMs: 10_000 });
} catch {
// Ignore git init failures; workspace creation should still succeed.
}
}
export async function ensureAgentWorkspace(params?: {
dir?: string;
ensureBootstrapFiles?: boolean;
@@ -140,6 +170,7 @@ export async function ensureAgentWorkspace(params?: {
if (isBrandNewWorkspace) {
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
}
await ensureGitRepo(dir, isBrandNewWorkspace);
return {
dir,