diff --git a/docs/concepts/agent-workspace.md b/docs/concepts/agent-workspace.md index 33b0e174a..4033fd267 100644 --- a/docs/concepts/agent-workspace.md +++ b/docs/concepts/agent-workspace.md @@ -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 diff --git a/docs/start/clawd.md b/docs/start/clawd.md index 0ea039cc2..e33870168 100644 --- a/docs/start/clawd.md +++ b/docs/start/clawd.md @@ -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 Clawd’s “memory” and make it a git repo (ideally private) so your `AGENTS.md` + memory files are backed up. +Tip: treat this folder like Clawd’s “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 diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index f16a71759..c770b241a 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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, diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/pi-embedded-runner/system-prompt.ts index 196458df9..cde0f0a15 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/pi-embedded-runner/system-prompt.ts @@ -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, diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index b5fe28556..e37a17008 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -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", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 6a20391c0..9716fed0d 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -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" : "", diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.test.ts index e14022fde..8c4f5a0de 100644 --- a/src/agents/workspace.test.ts +++ b/src/agents/workspace.test.ts @@ -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"); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index cf1de7daf..6732069a9 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -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 { + try { + await fs.stat(path.join(dir, ".git")); + return true; + } catch { + return false; + } +} + +async function isGitAvailable(): Promise { + 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,