feat(FN-3056): merge fusion/fn-3056
This merge ships several feature and infrastructure improvements across the codebase. Task title validation is strengthened in triage with stricter rejection of malformed titles and preference for prompt-declared titles (FN-3056), while task creation now preserves priority settings (FN-3210). The Mi Fusion-Task-Id: FN-3056
This commit is contained in:
@@ -1335,6 +1335,84 @@ describe("approved triage recovery", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("updates malformed metadata title from prompt heading when task ID matches", async () => {
|
||||
await writeFile(
|
||||
join(rootDir, ".fusion", "tasks", "FN-001", "PROMPT.md"),
|
||||
"# Task: FN-001 - Experimental AI Agent Onboarding Flow\n\n**Size:** M\n\n## Review Level: 2\n\nRecovered specification",
|
||||
);
|
||||
|
||||
const store = createMockStore({
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
requirePlanApproval: false,
|
||||
} as Settings),
|
||||
});
|
||||
|
||||
const processor = new TriageProcessor(store, rootDir);
|
||||
const recovered = await processor.recoverApprovedTask({
|
||||
id: "FN-001",
|
||||
description: "Recovered triage task",
|
||||
column: "triage",
|
||||
status: "planning",
|
||||
title: "Created task **FN-999** in triage",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [{ timestamp: "2026-01-01T00:00:00.000Z", action: "Spec review: APPROVE" }],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:02:00.000Z",
|
||||
});
|
||||
|
||||
expect(recovered).toBe(true);
|
||||
expect(store.updateTask).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
expect.objectContaining({ title: "Experimental AI Agent Onboarding Flow" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not overwrite title when heading task ID does not match", async () => {
|
||||
await writeFile(
|
||||
join(rootDir, ".fusion", "tasks", "FN-001", "PROMPT.md"),
|
||||
"# Task: FN-999 - Wrong Task\n\n**Size:** M\n\n## Review Level: 2\n\nRecovered specification",
|
||||
);
|
||||
|
||||
const store = createMockStore({
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
requirePlanApproval: false,
|
||||
} as Settings),
|
||||
});
|
||||
|
||||
const processor = new TriageProcessor(store, rootDir);
|
||||
const recovered = await processor.recoverApprovedTask({
|
||||
id: "FN-001",
|
||||
description: "Recovered triage task",
|
||||
column: "triage",
|
||||
status: "planning",
|
||||
title: "Existing title",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [{ timestamp: "2026-01-01T00:00:00.000Z", action: "Spec review: APPROVE" }],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:02:00.000Z",
|
||||
});
|
||||
|
||||
expect(recovered).toBe(true);
|
||||
expect(store.updateTask).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
expect.not.objectContaining({ title: expect.any(String) }),
|
||||
);
|
||||
});
|
||||
|
||||
it("clears status and error before moving approved tasks to todo", async () => {
|
||||
const store = createMockStore({
|
||||
getSettings: vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -1967,6 +1967,11 @@ export class TriageProcessor {
|
||||
taskUpdates.reviewLevel = parseInt(reviewMatch[1], 10);
|
||||
}
|
||||
|
||||
const promptDeclaredTitle = extractPromptDeclaredTitle(written, task.id);
|
||||
if (promptDeclaredTitle) {
|
||||
taskUpdates.title = promptDeclaredTitle;
|
||||
}
|
||||
|
||||
await this.store.updateTask(task.id, taskUpdates);
|
||||
|
||||
if (settings.requirePlanApproval) {
|
||||
@@ -1996,6 +2001,23 @@ export class TriageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
function extractPromptDeclaredTitle(prompt: string, taskId: string): string | null {
|
||||
const headingMatch = prompt.match(/^#\s+Task:\s+([A-Z]+-\d+)\s+-\s+(.+)$/m);
|
||||
if (!headingMatch) return null;
|
||||
const [, headingTaskId, rawTitle] = headingMatch;
|
||||
if (headingTaskId !== taskId) return null;
|
||||
|
||||
const title = rawTitle.trim().replace(/[\s.!?,;:]+$/g, "");
|
||||
if (!title) return null;
|
||||
|
||||
// Conservative guard: do not overwrite metadata with confirmation prose.
|
||||
if (/^created\s+(?:task\s+)?(?:fn-\d+\b|\*\*\s*fn-\d+\s*\*\*)/i.test(title)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
function hasLatestSpecReviewApproval(task: Task): boolean {
|
||||
for (let i = task.log.length - 1; i >= 0; i--) {
|
||||
const action = task.log[i]?.action ?? "";
|
||||
|
||||
Reference in New Issue
Block a user