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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-15 21:52:06 -07:00
parent a048a619fc
commit 9d791b3bf1
2 changed files with 45 additions and 0 deletions

View File

@@ -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<Task>);
const store = createMockStore({
getTask: vi.fn().mockResolvedValue(task),
} as Partial<TaskStore>);
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<void>;
}).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);

View File

@@ -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