From 9d791b3bf1950684b28734cf4e499034b2b22b28 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 15 Jul 2026 21:52:06 -0700 Subject: [PATCH] FN-8009: preserve approved plans through prompt hygiene Keep manually approved plans idempotent when deterministic prompt hygiene is applied. - Document normalized fingerprint comparison at the approval gate - Cover approval reuse after Original Description and Frontend UX injection Files changed: packages/engine/src/__tests__/triage.test.ts | 37 ++++++++++++++++++++++++++++ packages/engine/src/triage.ts | 8 ++++++ 2 files changed, 45 insertions(+) Fusion-Task-Id: FN-8009 Fusion-Task-Lineage: 8474bc13-61e0-421a-8e99-080f99382285 Co-authored-by: Fusion (runfusion.ai) --- packages/engine/src/__tests__/triage.test.ts | 37 ++++++++++++++++++++ packages/engine/src/triage.ts | 8 +++++ 2 files changed, 45 insertions(+) diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 48ac9a3f62..6aa5c78b41 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -3206,6 +3206,43 @@ describe("requirePlanApproval setting", () => { ); }); + /* + * FNXC:PlanApproval 2026-07-15-21:10: + * FN-8009 requires the as-approved comparison to cover both deterministic hygiene + * mutations. A legacy operator-approved plan may lack both its original request and + * the frontend checklist; these injections must not turn an unchanged plan into a + * new manual approval request. + */ + it("auto-approves a pre-hygiene plan after Original Description and Frontend UX injection", async () => { + const frontendPlan = "# Task: FN-FRONTEND-HYGIENE - Frontend plan\n\n## Mission\n\nUpdate the dashboard.\n\n## File Scope\n\n- `packages/dashboard/app/TaskCard.tsx`\n"; + const task = createTriageTask({ + id: "FN-FRONTEND-HYGIENE", + description: "The original dashboard request is absent from this planner output.", + status: "planning", + approvedPlanFingerprint: computePlanApprovalFingerprint(frontendPlan), + } as Partial); + const store = createMockStore({ + getTask: vi.fn().mockResolvedValue(task), + } as Partial); + await mkdir(join(rootDir, ".fusion", "tasks", task.id), { recursive: true }); + await writeFile(join(rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), frontendPlan); + const processor = new TriageProcessor(store, rootDir); + + await (processor as unknown as { + finalizeApprovedTask(task: Task, writtenInput: string, settings: Settings): Promise; + }).finalizeApprovedTask( + task, + frontendPlan, + { requirePlanApproval: true, planApprovalMode: "require-all" } as Settings, + ); + + expect(store.moveTask).toHaveBeenCalledWith(task.id, "todo"); + expect(store.updateTask).not.toHaveBeenCalledWith(task.id, expect.objectContaining({ status: "awaiting-approval" })); + const persisted = readFileSync(join(rootDir, ".fusion", "tasks", task.id, "PROMPT.md"), "utf8"); + expect(persisted).toContain("## Original Description"); + expect(persisted).toContain("## Frontend UX Criteria"); + }); + it("still re-asks approval for a CHANGED plan when the prior approval predates prompt hygiene", async () => { // The safety edge: legacy tolerance must not approve a plan the operator never saw. const legacyFingerprint = computePlanApprovalFingerprint(planText); diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index e0ccc365f2..129f46eff3 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -2951,6 +2951,14 @@ export class TriageProcessor { * already made their independent decisions, so it never weakens either of those gates * or auto-approve-all (which never reaches this branch at all). */ + /* + * FNXC:PlanApproval 2026-07-15-21:05: + * FN-7569 / FN-8009 — compare the normalized as-approved fingerprint after deterministic + * Original Description or Frontend UX hygiene. approve-plan fingerprints the on-disk + * PROMPT.md, while recovery can receive pre-injection text; the shared hasher removes only + * those generated sections so an unchanged plan does not re-park. A genuinely changed plan + * still produces a different fingerprint and requires approval. + */ const priorFingerprint = latestTransitionTask?.approvedPlanFingerprint ?? task.approvedPlanFingerprint; // FNXC:PlanApproval 2026-07-15-20:45: The shared hasher strips deterministic // Original Description / Frontend UX hygiene, so approve-plan's on-disk fingerprint and