From ceb934299bf08561eb43b6702dc4edb9cdb419df Mon Sep 17 00:00:00 2001 From: Robby Date: Sun, 15 Feb 2026 00:41:35 +0100 Subject: [PATCH] fix(workspace): create BOOTSTRAP.md regardless of workspace state (#16457) (#16504) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: a57718c09e9b601087edcb3ee15dd7ac6b96fee2 Co-authored-by: robbyczgw-cla <239660374+robbyczgw-cla@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 1 + src/agents/workspace.e2e.test.ts | 39 ++++++++++++++++++++++++++++++++ src/agents/workspace.ts | 23 +++++++++++-------- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3cc5612..f6f6ec35d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - Agents/Process/Bootstrap: preserve unbounded `process log` offset-only pagination (default tail applies only when both `offset` and `limit` are omitted) and enforce strict `bootstrapTotalMaxChars` budgeting across injected bootstrap content (including markers), skipping additional injection when remaining budget is too small. (#16539) Thanks @CharlieGreenman. - Sessions/Agents: harden transcript path resolution for mismatched agent context by preserving explicit store roots and adding safe absolute-path fallback to the correct agent sessions directory. (#16288) Thanks @robbyczgw-cla. - Agents: keep unresolved mutating tool failures visible until the same action retry succeeds, scope mutation-error surfacing to mutating calls (including `session_status` model changes), and dedupe duplicate failure warnings in outbound replies. (#16131) Thanks @Swader. +- Agents/Workspace: create `BOOTSTRAP.md` when core workspace files are seeded in partially initialized workspaces, while keeping BOOTSTRAP one-shot after onboarding deletion. (#16457) Thanks @robbyczgw-cla. - Agents: classify external timeout aborts during compaction the same as internal timeouts, preventing unnecessary auth-profile rotation and preserving compaction-timeout snapshot fallback behavior. (#9855) Thanks @mverrilli. - Ollama/Agents: avoid forcing `` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg. - Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238. diff --git a/src/agents/workspace.e2e.test.ts b/src/agents/workspace.e2e.test.ts index d4f842e6e..7bc5782b3 100644 --- a/src/agents/workspace.e2e.test.ts +++ b/src/agents/workspace.e2e.test.ts @@ -1,9 +1,13 @@ +import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js"; import { + DEFAULT_AGENTS_FILENAME, + DEFAULT_BOOTSTRAP_FILENAME, DEFAULT_MEMORY_ALT_FILENAME, DEFAULT_MEMORY_FILENAME, + ensureAgentWorkspace, loadWorkspaceBootstrapFiles, resolveDefaultAgentWorkspaceDir, } from "./workspace.js"; @@ -19,6 +23,41 @@ describe("resolveDefaultAgentWorkspaceDir", () => { }); }); +describe("ensureAgentWorkspace", () => { + it("creates BOOTSTRAP.md for a brand new workspace", async () => { + const tempDir = await makeTempWorkspace("openclaw-workspace-"); + + await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); + + await expect( + fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)), + ).resolves.toBeUndefined(); + }); + + it("creates BOOTSTRAP.md even when workspace already has other bootstrap files", async () => { + const tempDir = await makeTempWorkspace("openclaw-workspace-"); + await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_AGENTS_FILENAME, content: "existing" }); + + await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); + + await expect( + fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)), + ).resolves.toBeUndefined(); + }); + + it("does not recreate BOOTSTRAP.md after onboarding deletion", async () => { + const tempDir = await makeTempWorkspace("openclaw-workspace-"); + await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); + await fs.unlink(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)); + + await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); + + await expect(fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME))).rejects.toMatchObject({ + code: "ENOENT", + }); + }); +}); + describe("loadWorkspaceBootstrapFiles", () => { it("includes MEMORY.md when present", async () => { const tempDir = await makeTempWorkspace("openclaw-workspace-"); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index c13fe29f7..c3e644487 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -106,17 +106,19 @@ const VALID_BOOTSTRAP_NAMES: ReadonlySet = new Set([ DEFAULT_MEMORY_ALT_FILENAME, ]); -async function writeFileIfMissing(filePath: string, content: string) { +async function writeFileIfMissing(filePath: string, content: string): Promise { try { await fs.writeFile(filePath, content, { encoding: "utf-8", flag: "wx", }); + return true; } catch (err) { const anyErr = err as { code?: string }; if (anyErr.code !== "EEXIST") { throw err; } + return false; } } @@ -213,15 +215,16 @@ export async function ensureAgentWorkspace(params?: { const identityTemplate = await loadTemplate(DEFAULT_IDENTITY_FILENAME); const userTemplate = await loadTemplate(DEFAULT_USER_FILENAME); const heartbeatTemplate = await loadTemplate(DEFAULT_HEARTBEAT_FILENAME); - const bootstrapTemplate = await loadTemplate(DEFAULT_BOOTSTRAP_FILENAME); - - await writeFileIfMissing(agentsPath, agentsTemplate); - await writeFileIfMissing(soulPath, soulTemplate); - await writeFileIfMissing(toolsPath, toolsTemplate); - await writeFileIfMissing(identityPath, identityTemplate); - await writeFileIfMissing(userPath, userTemplate); - await writeFileIfMissing(heartbeatPath, heartbeatTemplate); - if (isBrandNewWorkspace) { + const wroteAgents = await writeFileIfMissing(agentsPath, agentsTemplate); + const wroteSoul = await writeFileIfMissing(soulPath, soulTemplate); + const wroteTools = await writeFileIfMissing(toolsPath, toolsTemplate); + const wroteIdentity = await writeFileIfMissing(identityPath, identityTemplate); + const wroteUser = await writeFileIfMissing(userPath, userTemplate); + const wroteHeartbeat = await writeFileIfMissing(heartbeatPath, heartbeatTemplate); + const wroteAnyCoreBootstrapFile = + wroteAgents || wroteSoul || wroteTools || wroteIdentity || wroteUser || wroteHeartbeat; + if (isBrandNewWorkspace || wroteAnyCoreBootstrapFile) { + const bootstrapTemplate = await loadTemplate(DEFAULT_BOOTSTRAP_FILENAME); await writeFileIfMissing(bootstrapPath, bootstrapTemplate); } await ensureGitRepo(dir, isBrandNewWorkspace);