From d07e78855cb1c339543b656bbf86881309f00d51 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 19:50:06 +0100 Subject: [PATCH] fix(workspace): align clawd + bootstrap --- CHANGELOG.md | 2 ++ docs/agent.md | 10 ++++++- docs/clawd.md | 12 +++++++- docs/configuration.md | 12 ++++++++ docs/faq.md | 8 +++--- src/agents/workspace.test.ts | 13 +++++++++ src/agents/workspace.ts | 19 ++++++++++++- src/commands/doctor.test.ts | 12 ++++---- src/commands/doctor.ts | 37 ++++++++++++++----------- src/commands/onboard-helpers.ts | 3 +- src/commands/onboard-non-interactive.ts | 4 ++- src/commands/setup.ts | 2 +- src/wizard/onboarding.ts | 4 ++- 13 files changed, 105 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ebd1acc..b5e60ca1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ - Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins. - Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding). - Doctor: suggest adding the workspace memory system when missing (opt-out via `--no-workspace-suggestions`). +- Doctor: normalize default workspace path to `~/clawd` (avoid `~/clawdbot`). +- Workspace: only create `BOOTSTRAP.md` for brand-new workspaces (don’t recreate after deletion). - Build: fix duplicate protocol export, align Codex OAuth options, and add proper-lockfile typings. - Build: install Bun in the Dockerfile so `pnpm build` can run Bun scripts. Thanks @loukotal for PR #284. - Typing indicators: stop typing once the reply dispatcher drains to prevent stuck typing across Discord/Telegram/WhatsApp. diff --git a/docs/agent.md b/docs/agent.md index e4d870112..8a27f9507 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -9,7 +9,7 @@ CLAWDBOT runs a single embedded agent runtime derived from **p-mono** (internal ## Workspace (required) -You must set an agent home directory via `agent.workspace`. CLAWDBOT uses this as the agent’s **only** working directory (`cwd`) for tools and context. +CLAWDBOT uses a single agent workspace directory (`agent.workspace`) as the agent’s **only** working directory (`cwd`) for tools and context. Recommended: use `clawdbot setup` to create `~/.clawdbot/clawdbot.json` if missing and initialize the workspace files. @@ -31,6 +31,14 @@ On the first turn of a new session, CLAWDBOT injects the contents of these files If a file is missing, CLAWDBOT injects a single “missing file” marker line (and `clawdbot setup` will create a safe default template). +`BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). If you delete it after completing the ritual, it should not be recreated on later restarts. + +To disable bootstrap file creation entirely (for pre-seeded workspaces), set: + +```json5 +{ agent: { skipBootstrap: true } } +``` + ## Built-in tools (internal) p’s embedded core tools (read/bash/edit/write and related internals) are defined in code and always available. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used. diff --git a/docs/clawd.md b/docs/clawd.md index 5e9518754..871aba585 100644 --- a/docs/clawd.md +++ b/docs/clawd.md @@ -85,7 +85,7 @@ Now message the assistant number from your allowlisted phone. 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`) automatically on setup/first agent run. +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. @@ -103,6 +103,16 @@ Optional: choose a different workspace with `agent.workspace` (supports `~`). } ``` +If you already ship your own workspace files from a repo, you can disable bootstrap file creation entirely: + +```json5 +{ + agent: { + skipBootstrap: true + } +} +``` + ## The config that turns it into “an assistant” CLAWDBOT defaults to a good assistant setup, but you’ll usually want to tune: diff --git a/docs/configuration.md b/docs/configuration.md index aabf72baf..240a15c12 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -616,6 +616,18 @@ Default: `~/clawd`. If `agent.sandbox` is enabled, non-main sessions can override this with their own per-session workspaces under `agent.sandbox.workspaceRoot`. +### `agent.skipBootstrap` + +Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`). + +Use this for pre-seeded deployments where your workspace files come from a repo. + +```json5 +{ + agent: { skipBootstrap: true } +} +``` + ### `agent.userTimezone` Sets the user’s timezone for **system prompt context** (not for timestamps in diff --git a/docs/faq.md b/docs/faq.md index f12502c0d..9dcd422af 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -545,10 +545,10 @@ pkill -f "clawdbot" # Remove data trash ~/.clawdbot -# Remove repo and re-clone -trash ~/clawdbot -git clone https://github.com/clawdbot/clawdbot.git -cd clawdbot && pnpm install && pnpm build +# Remove repo and re-clone (adjust path if you cloned elsewhere) +trash ~/Projects/clawdbot +git clone https://github.com/clawdbot/clawdbot.git ~/Projects/clawdbot +cd ~/Projects/clawdbot && pnpm install && pnpm build pnpm clawdbot onboard ``` diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.test.ts index e03a559f9..0f2afafa0 100644 --- a/src/agents/workspace.test.ts +++ b/src/agents/workspace.test.ts @@ -36,4 +36,17 @@ describe("ensureAgentWorkspace", () => { await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true }); expect(await fs.readFile(agentsPath, "utf-8")).toBe("custom"); }); + + it("does not recreate BOOTSTRAP.md once workspace exists", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); + const agentsPath = path.join(dir, "AGENTS.md"); + const bootstrapPath = path.join(dir, "BOOTSTRAP.md"); + + await fs.writeFile(agentsPath, "custom", "utf-8"); + await fs.rm(bootstrapPath, { force: true }); + + await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true }); + + await expect(fs.stat(bootstrapPath)).rejects.toBeDefined(); + }); }); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index ca9ecfe72..8351f870b 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -222,6 +222,21 @@ export async function ensureAgentWorkspace(params?: { const userPath = path.join(dir, DEFAULT_USER_FILENAME); const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME); + const isBrandNewWorkspace = await (async () => { + const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath]; + const existing = await Promise.all( + paths.map(async (p) => { + try { + await fs.access(p); + return true; + } catch { + return false; + } + }), + ); + return existing.every((v) => !v); + })(); + const agentsTemplate = await loadTemplate( DEFAULT_AGENTS_FILENAME, DEFAULT_AGENTS_TEMPLATE, @@ -252,7 +267,9 @@ export async function ensureAgentWorkspace(params?: { await writeFileIfMissing(toolsPath, toolsTemplate); await writeFileIfMissing(identityPath, identityTemplate); await writeFileIfMissing(userPath, userTemplate); - await writeFileIfMissing(bootstrapPath, bootstrapTemplate); + if (isBrandNewWorkspace) { + await writeFileIfMissing(bootstrapPath, bootstrapTemplate); + } return { dir, diff --git a/src/commands/doctor.test.ts b/src/commands/doctor.test.ts index d0b587653..6be0753a2 100644 --- a/src/commands/doctor.test.ts +++ b/src/commands/doctor.test.ts @@ -268,9 +268,9 @@ describe("doctor", () => { parsed: { gateway: { mode: "local", bind: "loopback" }, agent: { - workspace: "/Users/steipete/clawdbot", + workspace: "/Users/steipete/clawd", sandbox: { - workspaceRoot: "/Users/steipete/clawdbot/sandboxes", + workspaceRoot: "/Users/steipete/clawd/sandboxes", docker: { image: "clawdbot-sandbox", containerPrefix: "clawdbot-sbx", @@ -282,9 +282,9 @@ describe("doctor", () => { config: { gateway: { mode: "local", bind: "loopback" }, agent: { - workspace: "/Users/steipete/clawdbot", + workspace: "/Users/steipete/clawd", sandbox: { - workspaceRoot: "/Users/steipete/clawdbot/sandboxes", + workspaceRoot: "/Users/steipete/clawd/sandboxes", docker: { image: "clawdbot-sandbox", containerPrefix: "clawdbot-sbx", @@ -365,8 +365,8 @@ describe("doctor", () => { const sandbox = agent.sandbox as Record; const docker = sandbox.docker as Record; - expect(agent.workspace).toBe("/Users/steipete/clawdbot"); - expect(sandbox.workspaceRoot).toBe("/Users/steipete/clawdbot/sandboxes"); + expect(agent.workspace).toBe("/Users/steipete/clawd"); + expect(sandbox.workspaceRoot).toBe("/Users/steipete/clawd/sandboxes"); expect(docker.image).toBe("clawdbot-sandbox"); expect(docker.containerPrefix).toBe("clawdbot-sbx"); }); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 0bb984cd9..e0e4b8bcc 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -247,15 +247,28 @@ async function noteSecurityWarnings(cfg: ClawdbotConfig) { } } -function replacePathSegment( +function normalizeDefaultWorkspacePath( value: string | undefined, - from: string, - to: string, ): string | undefined { if (!value) return value; - const pattern = new RegExp(`(^|[\\/])${from}([\\/]|$)`, "g"); - if (!pattern.test(value)) return value; - return value.replace(pattern, `$1${to}$2`); + + const resolved = resolveUserPath(value); + const home = os.homedir(); + + const next = [ + ["clawdis", "clawd"], + ["clawdbot", "clawd"], + ].reduce((acc, [from, to]) => { + const fromPrefix = path.join(home, from); + if (acc === fromPrefix) return path.join(home, to); + const withSep = `${fromPrefix}${path.sep}`; + if (acc.startsWith(withSep)) { + return path.join(home, to).concat(acc.slice(fromPrefix.length)); + } + return acc; + }, resolved); + + return next === resolved ? value : next; } function replaceLegacyName(value: string | undefined): string | undefined { @@ -556,11 +569,7 @@ function normalizeLegacyConfigValues(cfg: ClawdbotConfig): { let next: ClawdbotConfig = cfg; const workspace = cfg.agent?.workspace; - const updatedWorkspace = replacePathSegment( - replacePathSegment(workspace, "clawdis", "clawdbot"), - "clawd", - "clawdbot", - ); + const updatedWorkspace = normalizeDefaultWorkspacePath(workspace); if (updatedWorkspace && updatedWorkspace !== workspace) { next = { ...next, @@ -573,11 +582,7 @@ function normalizeLegacyConfigValues(cfg: ClawdbotConfig): { } const workspaceRoot = cfg.agent?.sandbox?.workspaceRoot; - const updatedWorkspaceRoot = replacePathSegment( - replacePathSegment(workspaceRoot, "clawdis", "clawdbot"), - "clawd", - "clawdbot", - ); + const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(workspaceRoot); if (updatedWorkspaceRoot && updatedWorkspaceRoot !== workspaceRoot) { next = { ...next, diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 0e81bf768..43e91e33d 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -223,10 +223,11 @@ export async function openUrl(url: string): Promise { export async function ensureWorkspaceAndSessions( workspaceDir: string, runtime: RuntimeEnv, + options?: { skipBootstrap?: boolean }, ) { const ws = await ensureAgentWorkspace({ dir: workspaceDir, - ensureBootstrapFiles: true, + ensureBootstrapFiles: !options?.skipBootstrap, }); runtime.log(`Workspace OK: ${ws.dir}`); const sessionsDir = resolveSessionTranscriptsDir(); diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index 17d845cd7..7b8127ddc 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -219,7 +219,9 @@ export async function runNonInteractiveOnboarding( nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); await writeConfigFile(nextConfig); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); - await ensureWorkspaceAndSessions(workspaceDir, runtime); + await ensureWorkspaceAndSessions(workspaceDir, runtime, { + skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap), + }); if (opts.installDaemon) { const service = resolveGatewayService(); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 3bc176df9..0dc1d9048 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -74,7 +74,7 @@ export async function setupCommand( const ws = await ensureAgentWorkspace({ dir: workspace, - ensureBootstrapFiles: true, + ensureBootstrapFiles: !next.agent?.skipBootstrap, }); runtime.log(`Workspace OK: ${ws.dir}`); diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index 539e4497e..7536ab44b 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -604,7 +604,9 @@ export async function runOnboardingWizard( await writeConfigFile(nextConfig); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); - await ensureWorkspaceAndSessions(workspaceDir, runtime); + await ensureWorkspaceAndSessions(workspaceDir, runtime, { + skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap), + }); nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter); nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });