feat(FN-679): regenerate PROMPT.md on task title/description updates

- Implement automatic PROMPT.md regeneration in updateTask() when title or description changes
- Add comprehensive tests for PROMPT.md sync behavior in store.test.ts
- Create changeset for the PROMPT.md sync fix
- Remove multi-project migration changeset (superseded)
- Update dashboard components and styles for setup wizard and provider icons
This commit is contained in:
gsxdsm
2026-04-01 08:18:00 -07:00
parent ad2e3353fd
commit e9808ada71
4 changed files with 305 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@gsxdsm/fusion": patch
---
Fix PROMPT.md not syncing when task title or description is edited in the dashboard
Previously, when users edited a task's title or description via `updateTask()`, the PROMPT.md file was not being regenerated, causing a mismatch between what users saw in the dashboard and what the AI executor saw. Now PROMPT.md is automatically regenerated whenever title or description changes, preserving all existing sections (Dependencies, Steps, File Scope, Acceptance Criteria, Notifications) while updating the heading and Mission section.

View File

@@ -101,7 +101,8 @@ CREATE TABLE IF NOT EXISTS tasks (
mergeDetails TEXT,
breakIntoSubtasks INTEGER DEFAULT 0,
enabledWorkflowSteps TEXT DEFAULT '[]',
modifiedFiles TEXT DEFAULT '[]'
modifiedFiles TEXT DEFAULT '[]',
sliceId TEXT
);
-- Config table (single row with project settings)

View File

@@ -1099,6 +1099,226 @@ describe("TaskStore", () => {
});
});
describe("updateTask — PROMPT.md regeneration", () => {
it("regenerates PROMPT.md when title is updated", async () => {
const task = await store.createTask({ description: "Test task", column: "todo" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Verify initial PROMPT.md
const initialPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(initialPrompt).toContain(`# ${task.id}`);
expect(initialPrompt).toContain("Test task");
// Update title
await store.updateTask(task.id, { title: "New Title" });
// Verify PROMPT.md was regenerated with new title
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toContain(`# ${task.id}: New Title`);
expect(updatedPrompt).toContain("Test task"); // Description preserved
});
it("regenerates PROMPT.md when description is updated", async () => {
const task = await store.createTask({ description: "Old description", column: "todo" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Verify initial PROMPT.md
const initialPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(initialPrompt).toContain("Old description");
// Update description
await store.updateTask(task.id, { description: "New description" });
// Verify PROMPT.md was regenerated with new description
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toContain("New description");
});
it("preserves existing steps when regenerating PROMPT.md", async () => {
const task = await store.createTask({ description: "Task with steps", column: "todo" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Write custom steps to PROMPT.md
const customPrompt = `# ${task.id}: Task with steps
**Created:** ${task.createdAt.split("T")[0]}
**Size:** M
## Mission
Task with steps
## Steps
### Step 1: Custom Step
- [ ] Custom action 1
- [ ] Custom action 2
### Step 2: Another Custom Step
- [ ] Another action
`;
await writeFile(join(dir, "PROMPT.md"), customPrompt);
// Update title
await store.updateTask(task.id, { title: "Updated Title" });
// Verify custom steps are preserved
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toContain(`# ${task.id}: Updated Title`);
expect(updatedPrompt).toContain("### Step 1: Custom Step");
expect(updatedPrompt).toContain("- [ ] Custom action 1");
expect(updatedPrompt).toContain("### Step 2: Another Custom Step");
expect(updatedPrompt).toContain("- [ ] Another action");
});
it("preserves file scope when regenerating PROMPT.md", async () => {
const task = await store.createTask({ description: "Task with file scope", column: "todo" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Write PROMPT.md with custom file scope
const customPrompt = `# ${task.id}: Task with file scope
**Created:** ${task.createdAt.split("T")[0]}
**Size:** M
## Mission
Task with file scope
## File Scope
- \`src/store.ts\`
- \`src/db.ts\`
`;
await writeFile(join(dir, "PROMPT.md"), customPrompt);
// Update description
await store.updateTask(task.id, { description: "Updated description" });
// Verify file scope is preserved
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toContain("Updated description");
expect(updatedPrompt).toContain("## File Scope");
expect(updatedPrompt).toContain("`src/store.ts`");
expect(updatedPrompt).toContain("`src/db.ts`");
});
it("preserves dependencies section when regenerating PROMPT.md", async () => {
const task = await store.createTask({ description: "Task with deps", column: "todo", dependencies: ["KB-001"] });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Verify initial PROMPT.md has dependencies
const initialPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(initialPrompt).toContain("## Dependencies");
expect(initialPrompt).toContain("- **Task:** KB-001");
// Update title
await store.updateTask(task.id, { title: "Updated Title" });
// Verify dependencies section is preserved
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toContain("## Dependencies");
expect(updatedPrompt).toContain("- **Task:** KB-001");
});
it("preserves acceptance criteria section when regenerating PROMPT.md", async () => {
const task = await store.createTask({ description: "Task with acceptance criteria", column: "todo" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Write PROMPT.md with acceptance criteria
const customPrompt = `# ${task.id}: Task with acceptance criteria
**Created:** ${task.createdAt.split("T")[0]}
**Size:** M
## Mission
Task with acceptance criteria
## Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Criterion 3
`;
await writeFile(join(dir, "PROMPT.md"), customPrompt);
// Update description
await store.updateTask(task.id, { description: "Updated description" });
// Verify acceptance criteria is preserved
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toContain("Updated description");
expect(updatedPrompt).toContain("## Acceptance Criteria");
expect(updatedPrompt).toContain("- [ ] Criterion 1");
expect(updatedPrompt).toContain("- [ ] Criterion 2");
expect(updatedPrompt).toContain("- [ ] Criterion 3");
});
it("updates simple PROMPT.md for triage tasks", async () => {
const task = await store.createTask({ description: "Triage task", column: "triage" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Verify initial simple format
const initialPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(initialPrompt).toBe(`# ${task.id}\n\nTriage task\n`);
// Update title
await store.updateTask(task.id, { title: "Updated Title" });
// Verify simple format is maintained but updated
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toBe(`# ${task.id}: Updated Title\n\nTriage task\n`);
});
it("updates description in simple PROMPT.md for triage tasks", async () => {
const task = await store.createTask({ title: "My Task", description: "Original desc", column: "triage" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Verify initial simple format
const initialPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(initialPrompt).toBe(`# ${task.id}: My Task\n\nOriginal desc\n`);
// Update description
await store.updateTask(task.id, { description: "Updated desc" });
// Verify simple format is maintained but updated
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toBe(`# ${task.id}: My Task\n\nUpdated desc\n`);
});
it("does not regenerate PROMPT.md when explicit prompt is provided", async () => {
const task = await store.createTask({ description: "Test task", column: "todo" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Update with explicit prompt
const customPrompt = "# Custom\n\nCustom prompt content";
await store.updateTask(task.id, { title: "Updated Title", prompt: customPrompt });
// Verify the explicit prompt was used, not regenerated
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toBe(customPrompt);
});
it("does not regenerate PROMPT.md when neither title nor description changes", async () => {
const task = await store.createTask({ description: "Test task", column: "todo" });
const dir = join(rootDir, ".fusion", "tasks", task.id);
// Write custom PROMPT.md
const customPrompt = `# ${task.id}\n\n**Created:** 2024-01-01\n**Size:** L\n\n## Mission\n\nTest task\n\n## Custom Section\n\nCustom content\n`;
await writeFile(join(dir, "PROMPT.md"), customPrompt);
// Update worktree only
await store.updateTask(task.id, { worktree: "/tmp/worktree" });
// Verify PROMPT.md was not changed
const updatedPrompt = await readFile(join(dir, "PROMPT.md"), "utf-8");
expect(updatedPrompt).toBe(customPrompt);
});
});
describe("agent log persistence", () => {
it("appendAgentLog creates agent.log and getAgentLogs reads it back", async () => {
const task = await createTestTask();

View File

@@ -992,6 +992,26 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
await writeFile(join(dir, "PROMPT.md"), updates.prompt);
}
// Regenerate PROMPT.md when title or description changes (but not when explicit prompt update)
if (updates.prompt === undefined && (updates.title !== undefined || updates.description !== undefined)) {
const promptPath = join(dir, "PROMPT.md");
if (existsSync(promptPath)) {
const existingPrompt = await readFile(promptPath, "utf-8");
let newPrompt: string;
if (task.column === "triage") {
// Simple format for triage tasks: # heading\n\ndescription
const heading = task.title ? `${task.id}: ${task.title}` : task.id;
newPrompt = `# ${heading}\n\n${task.description}\n`;
} else {
// Structured format for other columns - preserve sections
newPrompt = this.regeneratePrompt(task, existingPrompt);
}
await writeFile(promptPath, newPrompt);
}
}
if (movedToTriage) {
this.emit("task:moved", { task, from: "todo" as Column, to: "triage" as Column });
}
@@ -2598,6 +2618,62 @@ ${deps}
${notificationsSection}`;
}
/**
* Regenerate PROMPT.md when task title or description changes.
* Preserves existing sections (Dependencies, Steps, File Scope, etc.) from the original prompt,
* while updating the heading and Mission section with new values.
*/
private regeneratePrompt(task: Task, existingPrompt: string): string {
// Generate the new heading
const heading = task.title ? `${task.id}: ${task.title}` : task.id;
// Helper to extract a section by heading name
const extractSection = (sectionName: string): string | null => {
const regex = new RegExp(`^##\\s+${sectionName}\\s*$`, "m");
const match = existingPrompt.match(regex);
if (!match) return null;
const startIdx = match.index! + match[0].length;
const rest = existingPrompt.slice(startIdx);
// Find next ## heading (any level) or end of string
const nextHeading = rest.search(/\n##\\s/);
const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
return section.trim();
};
// Extract preserved sections
const depsSection = extractSection("Dependencies");
const stepsSection = extractSection("Steps");
const fileScopeSection = extractSection("File Scope");
const acceptanceSection = extractSection("Acceptance Criteria");
const notificationsSection = extractSection("Notifications");
// Reconstruct PROMPT.md with preserved sections
let result = `# ${heading}\n\n**Created:** ${task.createdAt.split("T")[0]}\n**Size:** ${task.size || "M"}\n\n## Mission\n\n${task.description}\n`;
if (depsSection !== null) {
result += `\n## Dependencies\n\n${depsSection}\n`;
}
if (stepsSection !== null) {
result += `\n## Steps\n\n${stepsSection}\n`;
}
if (fileScopeSection !== null) {
result += `\n## File Scope\n\n${fileScopeSection}\n`;
}
if (acceptanceSection !== null) {
result += `\n## Acceptance Criteria\n\n${acceptanceSection}\n`;
}
if (notificationsSection !== null) {
result += `\n## Notifications\n\n${notificationsSection}\n`;
}
return result;
}
/**
* Synchronous version of getSettings for internal use.
* Returns project-level settings merged with defaults.