From 20b9eacc1492a89764c634a556201d81d7f81160 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 19 Apr 2026 08:09:15 -0700 Subject: [PATCH] feat(FN-2133): standardize project memory path guidance to .fusion/memory/ - Update executor, reviewer, core prompt templates, and path-boundary messaging to reference the .fusion/memory/ directory instead of a single MEMORY.md file - Broaden worktree boundary checks in pi path validation to allow .fusion/memory/ directory access from task worktrees - Align memory backend metadata and dashboard backend labels to display file backend storage as .fusion/memory/ - Refresh core/engine tests to assert the new directory-based memory wording and boundary behavior --- packages/core/src/agent-prompts.test.ts | 4 +-- packages/core/src/agent-prompts.ts | 4 +-- packages/core/src/memory-backend.test.ts | 2 +- packages/core/src/memory-backend.ts | 2 +- packages/core/src/types.ts | 6 ++-- .../dashboard/app/components/MemoryView.tsx | 2 +- packages/engine/src/executor.test.ts | 32 +++++++++---------- packages/engine/src/executor.ts | 4 +-- .../engine/src/pi-create-kb-agent.test.ts | 4 +-- packages/engine/src/pi.ts | 12 ++++--- packages/engine/src/reviewer.test.ts | 4 +-- packages/engine/src/reviewer.ts | 4 +-- .../engine/src/step-session-executor.test.ts | 4 +-- packages/engine/src/triage.test.ts | 16 +++++----- 14 files changed, 52 insertions(+), 48 deletions(-) diff --git a/packages/core/src/agent-prompts.test.ts b/packages/core/src/agent-prompts.test.ts index 7acb609c1..d8f9b2fc8 100644 --- a/packages/core/src/agent-prompts.test.ts +++ b/packages/core/src/agent-prompts.test.ts @@ -154,7 +154,7 @@ describe("resolveAgentPrompt", () => { it("built-in executor prompt mentions memory exception", () => { const result = resolveAgentPrompt("executor"); - expect(result).toContain(".fusion/memory/MEMORY.md"); + expect(result).toContain(".fusion/memory/"); }); it("built-in executor prompt mentions attachments exception", () => { @@ -183,7 +183,7 @@ describe("resolveAgentPrompt", () => { }; const result = resolveAgentPrompt("executor", config); - expect(result).toContain(".fusion/memory/MEMORY.md"); + expect(result).toContain(".fusion/memory/"); }); it("senior-engineer prompt mentions attachments exception", () => { diff --git a/packages/core/src/agent-prompts.ts b/packages/core/src/agent-prompts.ts index ce432fa73..99f06804e 100644 --- a/packages/core/src/agent-prompts.ts +++ b/packages/core/src/agent-prompts.ts @@ -108,7 +108,7 @@ model, read-only access) to independently assess your work. You are running in an **isolated git worktree**. This means: - **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree — the worktree is your isolated execution environment. -- **Exception — Project memory:** You MAY read and write to .fusion/memory/MEMORY.md at the project root to save durable project learnings (architecture patterns, conventions, pitfalls). +- **Exception — Project memory:** You MAY read and write to files under .fusion/memory/ at the project root to save durable project learnings (architecture patterns, conventions, pitfalls). - **Exception — Task attachments:** You MAY read files under .fusion/tasks/{taskId}/attachments/ at the project root for context screenshots and documents attached to this task. - **Shell commands** run inside the worktree by default. Avoid using cd to navigate outside the worktree. @@ -519,7 +519,7 @@ model, read-only access) to independently assess your work. You are running in an **isolated git worktree**. This means: - **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree — the worktree is your isolated execution environment. -- **Exception — Project memory:** You MAY read and write to .fusion/memory/MEMORY.md at the project root to save durable project learnings (architecture patterns, conventions, pitfalls). +- **Exception — Project memory:** You MAY read and write to files under .fusion/memory/ at the project root to save durable project learnings (architecture patterns, conventions, pitfalls). - **Exception — Task attachments:** You MAY read files under .fusion/tasks/{taskId}/attachments/ at the project root for context screenshots and documents attached to this task. - **Shell commands** run inside the worktree by default. Avoid using cd to navigate outside the worktree. diff --git a/packages/core/src/memory-backend.test.ts b/packages/core/src/memory-backend.test.ts index 3db288522..72dc65b2d 100644 --- a/packages/core/src/memory-backend.test.ts +++ b/packages/core/src/memory-backend.test.ts @@ -85,7 +85,7 @@ describe("memory-backend", () => { it("should have human-readable name", () => { const backend = new FileMemoryBackend(); - expect(backend.name).toBe("File (.fusion/memory/MEMORY.md)"); + expect(backend.name).toBe("File (.fusion/memory/)"); }); }); diff --git a/packages/core/src/memory-backend.ts b/packages/core/src/memory-backend.ts index 1bc5e45e8..bc9215017 100644 --- a/packages/core/src/memory-backend.ts +++ b/packages/core/src/memory-backend.ts @@ -215,7 +215,7 @@ const backendRegistry = new Map(); */ export class FileMemoryBackend implements MemoryBackend { readonly type = "file"; - readonly name = "File (.fusion/memory/MEMORY.md)"; + readonly name = "File (.fusion/memory/)"; readonly capabilities: MemoryBackendCapabilities = { readable: true, writable: true, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3e0bf3d07..789aaa9aa 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1276,15 +1276,15 @@ export interface ProjectSettings { * Extraction only runs if BOTH this time has elapsed AND memory has grown * by more than MIN_INSIGHT_GROWTH_CHARS characters. Default: 86400000 (24h). */ insightExtractionMinIntervalMs?: number; - /** When enabled, agents will consult and update .fusion/memory/MEMORY.md with durable + /** When enabled, agents will consult and update files under .fusion/memory/ with durable * project learnings. When disabled, agents will not include memory instructions - * in their prompts and will not read or write to .fusion/memory/MEMORY.md. + * in their prompts and will not read or write to .fusion/memory/ files. * Default: true (enabled for backward compatibility). */ memoryEnabled?: boolean; /** Memory backend type for pluggable memory storage. * Available built-in backends: * - "qmd": QMD (Quantized Memory Distillation) backend using the qmd CLI tool (default) - * - "file": File-based backend storing memory in `.fusion/memory/MEMORY.md` + * - "file": File-based backend storing memory in `.fusion/memory/` * - "readonly": Read-only backend that returns empty memory (for external management) * - Any registered custom backend type * Default: "qmd" */ diff --git a/packages/dashboard/app/components/MemoryView.tsx b/packages/dashboard/app/components/MemoryView.tsx index 2dc20a2ce..b520f306a 100644 --- a/packages/dashboard/app/components/MemoryView.tsx +++ b/packages/dashboard/app/components/MemoryView.tsx @@ -93,7 +93,7 @@ function countTotalInsights(categories: ParsedInsightCategory[]): number { function getBackendDisplayName(backend: string): string { switch (backend) { case "file": - return "File (.fusion/memory/MEMORY.md)"; + return "File (.fusion/memory/)"; case "readonly": return "Read-Only"; case "qmd": diff --git a/packages/engine/src/executor.test.ts b/packages/engine/src/executor.test.ts index b6a8b0b7d..6e576322b 100644 --- a/packages/engine/src/executor.test.ts +++ b/packages/engine/src/executor.test.ts @@ -2116,7 +2116,7 @@ describe("buildExecutionPrompt", () => { "# test", "## Context to Read First", "- `/home/user/project/web/app/page.tsx`", - "- `/home/user/project/.fusion/memory/MEMORY.md`", + "- `/home/user/project/.fusion/memory/`", "## Steps", "### Step 0: Preflight", "- [ ] inspect `/home/user/project/web/app/layout.tsx`", @@ -2132,8 +2132,8 @@ describe("buildExecutionPrompt", () => { expect(result).toContain("/home/user/project/.worktrees/happy-robin/web/app/page.tsx"); expect(result).toContain("/home/user/project/.worktrees/happy-robin/web/app/layout.tsx"); - expect(result).toContain("/home/user/project/.fusion/memory/MEMORY.md"); - expect(result).not.toContain("/home/user/project/.worktrees/happy-robin/.fusion/memory/MEMORY.md"); + expect(result).toContain("/home/user/project/.fusion/memory/"); + expect(result).not.toContain("/home/user/project/.worktrees/happy-robin/.fusion/memory/"); }); it("omits attachment section when no attachments", () => { @@ -2419,7 +2419,7 @@ describe("buildExecutionPrompt", () => { } as any); expect(result).toContain("Execute this task."); expect(result).toContain("## Project Memory"); - expect(result).toContain(".fusion/memory/MEMORY.md"); + expect(result).toContain(".fusion/memory/"); }); it("excludes memory instructions when memoryEnabled: false", () => { @@ -2436,7 +2436,7 @@ describe("buildExecutionPrompt", () => { const result = buildExecutionPrompt(task, "/project", {} as any); expect(result).toContain("Execute this task."); expect(result).toContain("## Project Memory"); - expect(result).toContain(".fusion/memory/MEMORY.md"); + expect(result).toContain(".fusion/memory/"); }); it("includes selective memory write instruction for durable learnings at end of execution", () => { @@ -2458,22 +2458,22 @@ describe("buildExecutionPrompt", () => { const result = buildExecutionPrompt(task, "/project", { memoryEnabled: true, } as any); - expect(result).toContain("`.fusion/memory/MEMORY.md`"); + expect(result).toContain("`.fusion/memory/`"); }); }); describe("memoryBackendType setting", () => { - it("includes .fusion/memory/MEMORY.md for file backend", () => { + it("includes .fusion/memory/ for file backend", () => { const task = createMockTaskDetail(); const result = buildExecutionPrompt(task, "/project", { memoryEnabled: true, memoryBackendType: "file", } as any); expect(result).toContain("## Project Memory"); - // Check that the Project Memory section contains .fusion/memory/MEMORY.md + // Check that the Project Memory section contains .fusion/memory/ const memorySectionMatch = result.match(/## Project Memory\n([\s\S]*?)(?=\n## [^#]|$)/); expect(memorySectionMatch).toBeTruthy(); - expect(memorySectionMatch![1]).toContain(".fusion/memory/MEMORY.md"); + expect(memorySectionMatch![1]).toContain(".fusion/memory/"); }); it("includes read-only wording for readonly backend without write directives in memory section", () => { @@ -2490,10 +2490,10 @@ describe("buildExecutionPrompt", () => { // Should NOT contain write/update directives in the memory section expect(memorySection).not.toMatch(/write.*memory|update.*memory/i); // Should NOT contain the specific file path in the memory section - expect(memorySection).not.toContain(".fusion/memory/MEMORY.md"); + expect(memorySection).not.toContain(".fusion/memory/"); }); - it("does not include .fusion/memory/MEMORY.md in Project Memory section for qmd backend", () => { + it("does not include .fusion/memory/ in Project Memory section for qmd backend", () => { const task = createMockTaskDetail(); const result = buildExecutionPrompt(task, "/project", { memoryEnabled: true, @@ -2504,8 +2504,8 @@ describe("buildExecutionPrompt", () => { const memorySectionMatch = result.match(/## Project Memory\n([\s\S]*?)(?=\n## [^#]|$)/); expect(memorySectionMatch).toBeTruthy(); const memorySection = memorySectionMatch![1]; - // QMD should NOT unconditionally reference .fusion/memory/MEMORY.md in the memory section - expect(memorySection).not.toContain(".fusion/memory/MEMORY.md"); + // QMD should NOT unconditionally reference .fusion/memory/ in the memory section + expect(memorySection).not.toContain(".fusion/memory/"); expect(memorySection).toContain("memory_search"); expect(memorySection).toContain("memory_get"); }); @@ -2521,8 +2521,8 @@ describe("buildExecutionPrompt", () => { const memorySectionMatch = result.match(/## Project Memory\n([\s\S]*?)(?=\n## [^#]|$)/); expect(memorySectionMatch).toBeTruthy(); const memorySection = memorySectionMatch![1]; - // QMD should NOT contain .fusion/memory/MEMORY.md - expect(memorySection).not.toContain(".fusion/memory/MEMORY.md"); + // QMD should NOT contain .fusion/memory/ + expect(memorySection).not.toContain(".fusion/memory/"); expect(memorySection).toContain("memory_search"); // Contains "end of execution" write guidance expect(memorySection).toMatch(/end of execution/i); @@ -10885,7 +10885,7 @@ describe("buildExecutionPrompt", () => { const prompt = buildExecutionPrompt(task, "/project"); - expect(prompt).toContain(".fusion/memory/MEMORY.md"); + expect(prompt).toContain(".fusion/memory/"); expect(prompt).toContain("memory"); expect(prompt).toContain("durable"); }); diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index a3f4245ce..306282413 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -300,7 +300,7 @@ If the task's PROMPT.md includes a "Documentation Requirements" section listing You are running in an **isolated git worktree**. This means: - **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree — the worktree is your isolated execution environment. -- **Exception — Project memory:** You MAY read and write to .fusion/memory/MEMORY.md at the project root to save durable project learnings (architecture patterns, conventions, pitfalls). +- **Exception — Project memory:** You MAY read and write to files under .fusion/memory/ at the project root to save durable project learnings (architecture patterns, conventions, pitfalls). - **Exception — Task attachments:** You MAY read files under .fusion/tasks/{taskId}/attachments/ at the project root for context screenshots and documents attached to this task. - **Shell commands** run inside the worktree by default. Avoid using cd to navigate outside the worktree. @@ -4509,7 +4509,7 @@ ${reviewLevel >= 3 ? `After tests, also call review_step with type="code" for te You are running in an **isolated git worktree**. This means: - **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree. -- **Exception — Project memory:** You MAY read and write to \`.fusion/memory/MEMORY.md\` at the project root to save durable project learnings. +- **Exception — Project memory:** You MAY read and write to files under \`.fusion/memory/\` at the project root to save durable project learnings. - **Exception — Task attachments:** You MAY read files under \`.fusion/tasks/{taskId}/attachments/\` at the project root for context. - **Shell commands** run inside the worktree by default. Avoid using \`cd\` to navigate outside the worktree. diff --git a/packages/engine/src/pi-create-kb-agent.test.ts b/packages/engine/src/pi-create-kb-agent.test.ts index 14bab6df1..76939007a 100644 --- a/packages/engine/src/pi-create-kb-agent.test.ts +++ b/packages/engine/src/pi-create-kb-agent.test.ts @@ -155,7 +155,7 @@ describe("worktree path boundary helpers", () => { expect(mockReadTool.execute).not.toHaveBeenCalled(); }); - it("allows project root .fusion/memory/MEMORY.md from worktree session", async () => { + it("allows project root .fusion/memory/ files from worktree session", async () => { const mockReadTool = { name: "read", label: "Read", @@ -172,7 +172,7 @@ describe("worktree path boundary helpers", () => { "/project", ); - // Reading project root .fusion/memory/MEMORY.md should be allowed + // Reading project root .fusion/memory/ files should be allowed const result = await (wrapped[0] as any).execute("call-1", { path: "/project/.fusion/memory/MEMORY.md" }); expect(mockReadTool.execute).toHaveBeenCalled(); expect(result).toEqual({ ok: true, content: [{ type: "text", text: "memory content" }] }); diff --git a/packages/engine/src/pi.ts b/packages/engine/src/pi.ts index 3ce77d30b..d23413b82 100644 --- a/packages/engine/src/pi.ts +++ b/packages/engine/src/pi.ts @@ -561,7 +561,7 @@ async function assertValidWorktreeSession(cwd: string, projectRoot: string): Pro * Check if a path is allowed to be accessed from a worktree session. * Rules: * - Paths inside the worktree are always allowed - * - Project root .fusion/memory/* is allowed (for durable project learnings) + * - Project root .fusion/memory/ files are allowed (for durable project learnings) * - Task attachments under .fusion/tasks/N/attachments/ are allowed (for reading context files) * - All other paths outside the worktree are rejected * @@ -582,9 +582,13 @@ function isWorktreeAllowedPath(worktreePath: string, projectRoot: string, reques return true; // Path is inside the worktree } - // Exception: project root `.fusion/memory/*` for durable project learnings + // Exception: project root `.fusion/memory/` files for durable project learnings const relToProjectRoot = relative(projectRootResolved, requestedResolved).replace(/\\/g, "/"); - if (/^\.fusion\/memory\/[^/]+\.md$/.test(relToProjectRoot)) { + if ( + relToProjectRoot === ".fusion/memory" || + relToProjectRoot === ".fusion/memory/" || + relToProjectRoot.startsWith(".fusion/memory/") + ) { return true; } @@ -642,7 +646,7 @@ export function wrapToolsWithBoundary( ok: false, error: `Path "${relToProject}" is outside the worktree boundary. ` + `Coding agents can only modify files inside the current worktree. ` + - `Exception: .fusion/memory/* (project root) and .fusion/tasks/*/attachments/* are permitted for reading.`, + `Exception: .fusion/memory/ (project root) and .fusion/tasks/*/attachments/* are permitted for reading.`, }; } diff --git a/packages/engine/src/reviewer.test.ts b/packages/engine/src/reviewer.test.ts index b23d33552..008ca8e13 100644 --- a/packages/engine/src/reviewer.test.ts +++ b/packages/engine/src/reviewer.test.ts @@ -483,7 +483,7 @@ describe("REVIEWER_SYSTEM_PROMPT", () => { expect(REVIEWER_SYSTEM_PROMPT).toContain("Worktree Boundary Review"); expect(REVIEWER_SYSTEM_PROMPT).toContain("assigned task worktree"); expect(REVIEWER_SYSTEM_PROMPT).toContain("blocking REVISE"); - expect(REVIEWER_SYSTEM_PROMPT).toContain(".fusion/memory/MEMORY.md"); + expect(REVIEWER_SYSTEM_PROMPT).toContain(".fusion/memory/"); }); }); @@ -633,7 +633,7 @@ describe("reviewStep — user comments in spec review", () => { expect(capturedPrompt).toContain("## Worktree Boundary"); expect(capturedPrompt).toContain("Assigned task worktree: `/tmp/project/.worktrees/happy-robin`"); expect(capturedPrompt).toContain("primary project checkout"); - expect(capturedPrompt).toContain(".fusion/memory/MEMORY.md"); + expect(capturedPrompt).toContain(".fusion/memory/"); }); }); diff --git a/packages/engine/src/reviewer.ts b/packages/engine/src/reviewer.ts index 967e7edfc..658f7b503 100644 --- a/packages/engine/src/reviewer.ts +++ b/packages/engine/src/reviewer.ts @@ -163,7 +163,7 @@ For code reviews, verify that implementation changes are in the assigned task worktree. The review request includes the current worktree path. Inspect git state and recent commits from that worktree, and treat changes outside it as a blocking REVISE unless they are expected project-root state such as -\`.fusion/memory/MEMORY.md\`, task attachments, or other explicitly documented +\`.fusion/memory/\` files, task attachments, or other explicitly documented Fusion metadata. If you see edits or commits in the primary project checkout instead of the task worktree, call that out directly and ask the worker to move the changes into the assigned worktree. @@ -478,7 +478,7 @@ function buildReviewRequest( "", "## Worktree Boundary", `Assigned task worktree: \`${cwd}\``, - "Verify that implementation changes are in this worktree. If you find changes or commits in the primary project checkout or any other path, issue REVISE unless the outside path is an expected project-root exception such as .fusion/memory/MEMORY.md, task attachments, or explicitly documented Fusion metadata.", + "Verify that implementation changes are in this worktree. If you find changes or commits in the primary project checkout or any other path, issue REVISE unless the outside path is an expected project-root exception such as .fusion/memory/ files, task attachments, or explicitly documented Fusion metadata.", "", ); if (baseline) { diff --git a/packages/engine/src/step-session-executor.test.ts b/packages/engine/src/step-session-executor.test.ts index 3541a0a75..dccbab559 100644 --- a/packages/engine/src/step-session-executor.test.ts +++ b/packages/engine/src/step-session-executor.test.ts @@ -471,8 +471,8 @@ Do important work. ); expect(result).toContain("/repo/project/.worktrees/happy-robin/packages/engine/src/new-module.ts"); - expect(result).toContain("/repo/project/.fusion/memory/MEMORY.md"); - expect(result).not.toContain("/repo/project/.worktrees/happy-robin/.fusion/memory/MEMORY.md"); + expect(result).toContain("/repo/project/.fusion/memory/"); + expect(result).not.toContain("/repo/project/.worktrees/happy-robin/.fusion/memory/"); }); it("handles step 0 (preflight) correctly", () => { diff --git a/packages/engine/src/triage.test.ts b/packages/engine/src/triage.test.ts index 00cbe3396..f91514e0a 100644 --- a/packages/engine/src/triage.test.ts +++ b/packages/engine/src/triage.test.ts @@ -321,7 +321,7 @@ describe("buildSpecificationPrompt", () => { }); describe("memoryBackendType setting", () => { - it("includes .fusion/memory/MEMORY.md for file backend", () => { + it("includes .fusion/memory/ for file backend", () => { const settings: Settings = { maxConcurrent: 2, maxWorktrees: 4, @@ -337,7 +337,7 @@ describe("buildSpecificationPrompt", () => { settings, ); expect(prompt).toContain("## Project Memory"); - expect(prompt).toContain(".fusion/memory/MEMORY.md"); + expect(prompt).toContain(".fusion/memory/"); }); it("includes read-only wording for readonly backend without write directives", () => { @@ -359,10 +359,10 @@ describe("buildSpecificationPrompt", () => { // Should NOT contain write/update directives expect(prompt).not.toMatch(/write.*memory|update.*memory/i); // Should NOT contain the specific file path - expect(prompt).not.toContain(".fusion/memory/MEMORY.md"); + expect(prompt).not.toContain(".fusion/memory/"); }); - it("does not include .fusion/memory/MEMORY.md for qmd backend", () => { + it("does not include .fusion/memory/ for qmd backend", () => { const settings: Settings = { maxConcurrent: 2, maxWorktrees: 4, @@ -378,8 +378,8 @@ describe("buildSpecificationPrompt", () => { settings, ); expect(prompt).toContain("## Project Memory"); - // QMD should NOT unconditionally reference .fusion/memory/MEMORY.md - expect(prompt).not.toContain(".fusion/memory/MEMORY.md"); + // QMD should NOT unconditionally reference .fusion/memory/ + expect(prompt).not.toContain(".fusion/memory/"); expect(prompt).toContain("memory_search"); expect(prompt).toContain("memory_get"); }); @@ -400,8 +400,8 @@ describe("buildSpecificationPrompt", () => { settings, ); expect(prompt).toContain("## Project Memory"); - // QMD should NOT contain .fusion/memory/MEMORY.md - expect(prompt).not.toContain(".fusion/memory/MEMORY.md"); + // QMD should NOT contain .fusion/memory/ + expect(prompt).not.toContain(".fusion/memory/"); expect(prompt).toContain("memory_search"); }); });