feat(FN-1022): add project memory bootstrap with agent integration

- Add project-memory module in @fusion/core with read/write/resolve helpers and upsert-on-store hook
- Bootstrap project memory during task creation (store) with structured task context
- Inject resolved project memory into executor and triage agent system prompts
- Add comprehensive tests for project-memory module, store integration, and agent prompt changes
- Document project memory architecture and usage in README
This commit is contained in:
gsxdsm
2026-04-07 00:41:19 -07:00
parent 4097d9d250
commit c852620090
10 changed files with 566 additions and 16 deletions

View File

@@ -2163,27 +2163,48 @@ describe("buildExecutionPrompt", () => {
});
describe("memoryEnabled setting", () => {
it("accepts memoryEnabled: true without error", () => {
it("includes memory instructions when memoryEnabled: true", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {
memoryEnabled: true,
} as any);
// Memory instructions are a placeholder until FN-810; just verify no crash
expect(result).toContain("Execute this task.");
expect(result).toContain("## Project Memory");
expect(result).toContain(".fusion/memory.md");
});
it("accepts memoryEnabled: false without error", () => {
it("excludes memory instructions when memoryEnabled: false", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {
memoryEnabled: false,
} as any);
expect(result).toContain("Execute this task.");
expect(result).not.toContain("## Project Memory");
});
it("accepts undefined memoryEnabled (default enabled) without error", () => {
it("includes memory instructions when memoryEnabled is undefined (default enabled)", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {} as any);
expect(result).toContain("Execute this task.");
expect(result).toContain("## Project Memory");
expect(result).toContain(".fusion/memory.md");
});
it("includes append instruction for updating memory at end of execution", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {
memoryEnabled: true,
} as any);
expect(result).toContain("append");
expect(result).toMatch(/end of execution|before calling.*task_done/i);
});
it("uses project-root memory path not worktree-local path", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {
memoryEnabled: true,
} as any);
expect(result).toContain("`.fusion/memory.md`");
});
});
});

View File

@@ -3,6 +3,7 @@ import { join } from "node:path";
import { existsSync } from "node:fs";
import type { TaskStore, Task, TaskDetail, StepStatus, Settings, WorkflowStep, MissionStore, Slice, AgentState, AgentCapability } from "@fusion/core";
import type { AgentStore } from "@fusion/core";
import { buildExecutionMemoryInstructions } from "@fusion/core";
import { findWorktreeUser } from "./merger.js";
import { generateWorktreeName, slugify } from "./worktree-names.js";
import { Type, type Static } from "@mariozechner/pi-ai";
@@ -3024,13 +3025,10 @@ git log --oneline
// Build project memory section from settings
// When enabled, agents consult and update .fusion/memory.md for durable project learnings.
// Actual memory instructions will be injected by FN-810; this placeholder establishes
// the conditional integration point.
const memoryEnabled = settings?.memoryEnabled !== false;
let memorySection = "";
if (memoryEnabled && rootDir) {
// TODO(FN-810): Call buildMemoryInstructions(rootDir) to populate memory context
memorySection = "";
memorySection = "\n" + buildExecutionMemoryInstructions(rootDir);
}
// Build steering comments section (last 10 comments only to avoid context bloat)

View File

@@ -217,7 +217,7 @@ describe("buildSpecificationPrompt", () => {
});
describe("memoryEnabled setting", () => {
it("accepts memoryEnabled: true without error", () => {
it("includes memory instructions when memoryEnabled: true", () => {
const settings: Settings = {
maxConcurrent: 2,
maxWorktrees: 4,
@@ -231,11 +231,12 @@ describe("buildSpecificationPrompt", () => {
".fusion/tasks/KB-001/PROMPT.md",
settings,
);
// Memory instructions are a placeholder until FN-810; just verify no crash
expect(prompt).toContain("Specify this task");
expect(prompt).toContain("## Project Memory");
expect(prompt).toContain(".fusion/memory.md");
});
it("accepts memoryEnabled: false without error", () => {
it("excludes memory instructions when memoryEnabled: false", () => {
const settings: Settings = {
maxConcurrent: 2,
maxWorktrees: 4,
@@ -250,15 +251,18 @@ describe("buildSpecificationPrompt", () => {
settings,
);
expect(prompt).toContain("Specify this task");
expect(prompt).not.toContain("## Project Memory");
});
it("accepts undefined memoryEnabled (default enabled) without error", () => {
it("includes memory instructions when memoryEnabled is undefined (default enabled)", () => {
const prompt = buildSpecificationPrompt(
baseTask,
".fusion/tasks/KB-001/PROMPT.md",
undefined,
);
expect(prompt).toContain("Specify this task");
expect(prompt).toContain("## Project Memory");
expect(prompt).toContain(".fusion/memory.md");
});
});

View File

@@ -5,6 +5,7 @@ import type {
TaskAttachment,
Settings,
} from "@fusion/core";
import { buildTriageMemoryInstructions } from "@fusion/core";
import type { ImageContent } from "@mariozechner/pi-ai";
import { Type, type Static } from "@mariozechner/pi-ai";
import type {
@@ -1252,13 +1253,10 @@ export function buildSpecificationPrompt(
// Build project memory section from settings.
// When enabled, agents consult .fusion/memory.md for durable project learnings.
// Actual memory instructions will be injected by FN-810; this placeholder
// establishes the conditional integration point.
const memoryEnabled = settings?.memoryEnabled !== false;
let memorySection = "";
if (memoryEnabled) {
// TODO(FN-810): Call buildMemoryInstructions(rootDir) to populate memory context
memorySection = "";
memorySection = "\n\n" + buildTriageMemoryInstructions("");
}
let attachmentsSection = "";