




















@@ -81,6 +81,21 @@ function registerDuplicateBootstrapFileHook() {
8181});
8282}
838384+function registerBootstrapFileHook(relativePath = "BOOTSTRAP.md") {
85+registerInternalHook("agent:bootstrap", (event) => {
86+const context = event.context as AgentBootstrapHookContext;
87+context.bootstrapFiles = [
88+ ...context.bootstrapFiles,
89+{
90+name: "BOOTSTRAP.md",
91+path: path.join(context.workspaceDir, relativePath),
92+content: "stale ritual",
93+missing: false,
94+},
95+];
96+});
97+}
98+8499async function createHeartbeatAgentsWorkspace() {
85100const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
86101await fs.writeFile(path.join(workspaceDir, "HEARTBEAT.md"), "check inbox", "utf8");
@@ -149,6 +164,124 @@ describe("resolveBootstrapFilesForRun", () => {
149164expect(agentsContextFiles).toHaveLength(1);
150165expect(agentsContextFiles[0]?.content).toBe("workspace rules");
151166});
167+168+it("ignores stale workspace BOOTSTRAP.md once setup is completed", async () => {
169+const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
170+await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
171+await fs.writeFile(
172+path.join(workspaceDir, ".openclaw", "workspace-state.json"),
173+`${JSON.stringify({
174+ version: 1,
175+ bootstrapSeededAt: "2026-05-16T00:00:00.000Z",
176+ setupCompletedAt: "2026-05-16T00:00:01.000Z",
177+ })}\n`,
178+"utf8",
179+);
180+await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
181+await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "stale ritual", "utf8");
182+183+const files = await resolveBootstrapFilesForRun({ workspaceDir });
184+185+expect(files.map((file) => file.name)).toContain("AGENTS.md");
186+expect(files.map((file) => file.name)).not.toContain("BOOTSTRAP.md");
187+});
188+189+it("keeps BOOTSTRAP.md when setup state cannot be read", async () => {
190+const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
191+await fs.mkdir(path.join(workspaceDir, ".openclaw", "workspace-state.json"), {
192+recursive: true,
193+});
194+await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
195+await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "ritual", "utf8");
196+197+const files = await resolveBootstrapFilesForRun({ workspaceDir });
198+199+expect(files.map((file) => file.name)).toContain("BOOTSTRAP.md");
200+});
201+202+it("does not let hooks re-add stale root BOOTSTRAP.md after setup is completed", async () => {
203+registerBootstrapFileHook();
204+const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
205+await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
206+await fs.writeFile(
207+path.join(workspaceDir, ".openclaw", "workspace-state.json"),
208+`${JSON.stringify({
209+ version: 1,
210+ bootstrapSeededAt: "2026-05-16T00:00:00.000Z",
211+ setupCompletedAt: "2026-05-16T00:00:01.000Z",
212+ })}\n`,
213+"utf8",
214+);
215+await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
216+await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "stale ritual", "utf8");
217+218+const files = await resolveBootstrapFilesForRun({ workspaceDir });
219+220+expect(files.map((file) => file.name)).not.toContain("BOOTSTRAP.md");
221+});
222+223+it("ignores stale root BOOTSTRAP.md for home-relative workspace paths", async () => {
224+registerBootstrapFileHook();
225+const parentDir = await makeTempWorkspace("openclaw-bootstrap-home-");
226+const workspaceDir = path.join(parentDir, "workspace");
227+await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
228+await fs.writeFile(
229+path.join(workspaceDir, ".openclaw", "workspace-state.json"),
230+`${JSON.stringify({
231+ version: 1,
232+ bootstrapSeededAt: "2026-05-16T00:00:00.000Z",
233+ setupCompletedAt: "2026-05-16T00:00:01.000Z",
234+ })}\n`,
235+"utf8",
236+);
237+await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
238+await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "stale ritual", "utf8");
239+240+const previousOpenClawHome = process.env.OPENCLAW_HOME;
241+process.env.OPENCLAW_HOME = parentDir;
242+try {
243+const files = await resolveBootstrapFilesForRun({ workspaceDir: "~/workspace" });
244+245+expect(files.map((file) => file.name)).toContain("AGENTS.md");
246+expect(files.map((file) => file.name)).not.toContain("BOOTSTRAP.md");
247+} finally {
248+if (previousOpenClawHome === undefined) {
249+delete process.env.OPENCLAW_HOME;
250+} else {
251+process.env.OPENCLAW_HOME = previousOpenClawHome;
252+}
253+}
254+});
255+256+it("keeps hook-added nested BOOTSTRAP.md after setup is completed", async () => {
257+registerBootstrapFileHook(path.join("packages", "core", "BOOTSTRAP.md"));
258+const workspaceDir = await makeTempWorkspace("openclaw-bootstrap-");
259+await fs.mkdir(path.join(workspaceDir, ".openclaw"), { recursive: true });
260+await fs.mkdir(path.join(workspaceDir, "packages", "core"), { recursive: true });
261+await fs.writeFile(
262+path.join(workspaceDir, ".openclaw", "workspace-state.json"),
263+`${JSON.stringify({
264+ version: 1,
265+ bootstrapSeededAt: "2026-05-16T00:00:00.000Z",
266+ setupCompletedAt: "2026-05-16T00:00:01.000Z",
267+ })}\n`,
268+"utf8",
269+);
270+await fs.writeFile(path.join(workspaceDir, "AGENTS.md"), "rules", "utf8");
271+await fs.writeFile(path.join(workspaceDir, "BOOTSTRAP.md"), "stale ritual", "utf8");
272+await fs.writeFile(
273+path.join(workspaceDir, "packages", "core", "BOOTSTRAP.md"),
274+"package ritual",
275+"utf8",
276+);
277+278+const files = await resolveBootstrapFilesForRun({ workspaceDir });
279+280+expect(files.map((file) => path.relative(workspaceDir, file.path))).toContain(
281+path.join("packages", "core", "BOOTSTRAP.md"),
282+);
283+expect(files.map((file) => file.path)).not.toContain(path.join(workspaceDir, "BOOTSTRAP.md"));
284+});
152285});
153286154287describe("resolveBootstrapContextForRun", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。