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 ### 1) Initialize the repo
If git is installed, brand-new workspaces are initialized automatically. If this
workspace is not already a repo, run:
```bash ```bash
cd ~/clawd cd ~/clawd
git init 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). 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 ```bash
clawdbot setup clawdbot setup

View File

@@ -46,6 +46,7 @@ import {
loadWorkspaceSkillEntries, loadWorkspaceSkillEntries,
resolveSkillsPromptForRun, resolveSkillsPromptForRun,
} from "../../skills.js"; } from "../../skills.js";
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
import { buildSystemPromptReport } from "../../system-prompt-report.js"; import { buildSystemPromptReport } from "../../system-prompt-report.js";
import { resolveDefaultModelForAgent } from "../../model-selection.js"; import { resolveDefaultModelForAgent } from "../../model-selection.js";
@@ -184,6 +185,11 @@ export async function runEmbeddedAttempt(
sessionId: params.sessionId, sessionId: params.sessionId,
warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }), 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(); const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
@@ -314,6 +320,7 @@ export async function runEmbeddedAttempt(
: undefined, : undefined,
skillsPrompt, skillsPrompt,
docsPath: docsPath ?? undefined, docsPath: docsPath ?? undefined,
workspaceNotes,
reactionGuidance, reactionGuidance,
promptMode, promptMode,
runtimeInfo, runtimeInfo,

View File

@@ -20,6 +20,7 @@ export function buildEmbeddedSystemPrompt(params: {
level: "minimal" | "extensive"; level: "minimal" | "extensive";
channel: string; channel: string;
}; };
workspaceNotes?: string[];
/** Controls which hardcoded sections to include. Defaults to "full". */ /** Controls which hardcoded sections to include. Defaults to "full". */
promptMode?: PromptMode; promptMode?: PromptMode;
runtimeInfo: { runtimeInfo: {
@@ -54,6 +55,7 @@ export function buildEmbeddedSystemPrompt(params: {
heartbeatPrompt: params.heartbeatPrompt, heartbeatPrompt: params.heartbeatPrompt,
skillsPrompt: params.skillsPrompt, skillsPrompt: params.skillsPrompt,
docsPath: params.docsPath, docsPath: params.docsPath,
workspaceNotes: params.workspaceNotes,
reactionGuidance: params.reactionGuidance, reactionGuidance: params.reactionGuidance,
promptMode: params.promptMode, promptMode: params.promptMode,
runtimeInfo: params.runtimeInfo, 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)", () => { it("includes user time when provided (12-hour)", () => {
const prompt = buildAgentSystemPrompt({ const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd", workspaceDir: "/tmp/clawd",

View File

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

View File

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { runCommandWithTimeout } from "../process/exec.js";
import type { WorkspaceBootstrapFile } from "./workspace.js"; import type { WorkspaceBootstrapFile } from "./workspace.js";
import { import {
DEFAULT_AGENTS_FILENAME, DEFAULT_AGENTS_FILENAME,
@@ -40,6 +41,34 @@ describe("ensureAgentWorkspace", () => {
await expect(fs.stat(bootstrap)).resolves.toBeDefined(); 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 () => { it("does not overwrite existing AGENTS.md", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-"));
const agentsPath = path.join(dir, "AGENTS.md"); const agentsPath = path.join(dir, "AGENTS.md");

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { isSubagentSessionKey } from "../routing/session-key.js"; import { isSubagentSessionKey } from "../routing/session-key.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { resolveUserPath } from "../utils.js"; import { resolveUserPath } from "../utils.js";
export function resolveDefaultAgentWorkspaceDir( 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?: { export async function ensureAgentWorkspace(params?: {
dir?: string; dir?: string;
ensureBootstrapFiles?: boolean; ensureBootstrapFiles?: boolean;
@@ -140,6 +170,7 @@ export async function ensureAgentWorkspace(params?: {
if (isBrandNewWorkspace) { if (isBrandNewWorkspace) {
await writeFileIfMissing(bootstrapPath, bootstrapTemplate); await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
} }
await ensureGitRepo(dir, isBrandNewWorkspace);
return { return {
dir, dir,