From 3e69d3990d4d31e6c10c2635bbe35bc10c16f175 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 11 Jun 2026 15:54:49 -0700 Subject: [PATCH] FN-6244: protect active AI merge worktrees Ensure AI merge cleanup only reaps stale, inactive temp worktrees. - Register live AI merge clean-room paths in the active session registry while merges run. - Add a minimum temp-worktree reap age and conservative lookup-error handling to sweep and prune paths. - Expand cleanup tests and release the repaired AI merge cleanup test from quarantine. Files changed: docs/architecture.md | 2 +- .../engine/src/__tests__/merger-ai-cleanup.test.ts | 38 ++++++++++++++-- .../__tests__/self-healing-tempdir-sweep.test.ts | 52 +++++++++++++++++++--- packages/engine/src/active-session-registry.ts | 2 +- packages/engine/src/merger-ai.ts | 29 +++++++++++- packages/engine/src/self-healing.ts | 28 +++++++++--- packages/engine/vitest.config.ts | 1 - scripts/lib/test-quarantine.json | 5 --- 8 files changed, 132 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-6244 Fusion-Task-Lineage: 5c8ced12-d38f-4e45-9ea0-119b3236a2cb --- docs/architecture.md | 2 +- .../src/__tests__/merger-ai-cleanup.test.ts | 38 ++++++++++++-- .../self-healing-tempdir-sweep.test.ts | 52 ++++++++++++++++--- .../engine/src/active-session-registry.ts | 2 +- packages/engine/src/merger-ai.ts | 29 ++++++++++- packages/engine/src/self-healing.ts | 28 +++++++--- packages/engine/vitest.config.ts | 1 - scripts/lib/test-quarantine.json | 5 -- 8 files changed, 132 insertions(+), 25 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 6319f2e9d8..f839a8d79a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -670,7 +670,7 @@ Runtime action-gate flow (v1): - `TransientErrorDetector` (`transient-error-detector.ts`) — retriable error classification - `SelfHealingManager` (`self-healing.ts`) — auto-unpause/maintenance recovery actions - Batch 1 maintenance now includes one `fts-maintenance` step for both search indexes. The live `tasks_fts` branch still runs `merge` every tick, `optimize` every 4th tick, and `rebuild` above `32 MiB` or `1 MiB × live task count`. The archive `archived_tasks_fts` branch is lighter because archive writes are mostly append-only: `merge` every 8th tick, `optimize` every 24th tick, and `rebuild` above `64 MiB` or `512 KiB × archived row count`. Each branch is independently guarded by `fts5Available` and emits `task:fts-maintenance` run-audit telemetry with distinct `target` values (`tasks_fts` vs `archived_tasks_fts`). - - Batch 1 also sweeps stale AI merge clean-room worktrees under `tmpdir()` whose names start with `fusion-ai-merge-`. The default age gate is 2 hours, but task-aware cleanup uses a 10-minute grace period for `done`/`archived` tasks and treats missing/deleted task rows as immediately stale. The sweep canonicalizes paths before checking `activeSessionRegistry`, attempts `git worktree remove --force ` before filesystem removal, and emits `worktree:tempdir-sweep` run-audit telemetry for removal attempts and failures. It is intentionally native even when `worktrunk.enabled` because these temp-dir worktrees are outside the worktrunk-managed project layout. Fresh directories, active-session paths, and individual removal failures are skipped/logged without aborting the maintenance cycle. + - Batch 1 also sweeps stale AI merge clean-room worktrees under `tmpdir()` whose names start with `fusion-ai-merge-`. `runAiMerge` registers each live clean-room worktree in `activeSessionRegistry` with kind `ai-merge` for the duration of the merge, and both the periodic sweep and pre-merge prune defer when the canonical or raw path is active. The default age gate is 2 hours; task-aware cleanup uses a 10-minute grace period for `done`/`archived` tasks and for genuinely missing/deleted task rows, and every removal path is clamped by the same 10-minute minimum-age floor so a freshly created worktree is never reaped. Transient `getTask` lookup failures (for example SQLite busy/parse errors) are not treated as deletion evidence; they log a warning, emit `lookup-error` only if eventually removed, and retain the conservative 2-hour gate. The sweep canonicalizes paths before checking `activeSessionRegistry`, attempts `git worktree remove --force ` before filesystem removal, and emits `worktree:tempdir-sweep` run-audit telemetry for removal attempts and failures. It is intentionally native even when `worktrunk.enabled` because these temp-dir worktrees are outside the worktrunk-managed project layout. Fresh directories, active-session paths, and individual removal failures are skipped/logged without aborting the maintenance cycle. - `recoverGhostReviewTasks()` is a fallback only for idle, non-terminal `in-review` states. Terminal/actionable states (notably `status: "failed"`) are preserved and **not** auto-kicked back to `todo`. - Mission validation has a dedicated stale-run reaper: startup recovery and Batch 2 maintenance call `reapStaleMissionValidatorRuns()` when wired by the runtime, using `VALIDATOR_RUN_STALE_MAX_AGE_MS` (currently 6 hours). The sweep terminates ownerless `mission_validator_runs.status='running'` rows as `error`, writes the reap reason into `summary`, leaves `lastValidatorRunId` pointing at the now-terminal run, and emits run-audit telemetry with `mutationType: "mission:validator-run-reaped"` plus `runId`/`featureId`/`missionId`/`triggerType`/`elapsedMs` metadata. Active mission features move to `loopState="needs_fix"` + `lastValidatorStatus="error"` unless their parent mission is already `complete`/`archived`. diff --git a/packages/engine/src/__tests__/merger-ai-cleanup.test.ts b/packages/engine/src/__tests__/merger-ai-cleanup.test.ts index 26e9647507..1f8fa07dfe 100644 --- a/packages/engine/src/__tests__/merger-ai-cleanup.test.ts +++ b/packages/engine/src/__tests__/merger-ai-cleanup.test.ts @@ -1,11 +1,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, utimesSync, writeFileSync } from "node:fs"; import { rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { execSync } from "node:child_process"; import { cleanupAiMergeWorktree, pruneExistingAiMergeWorktrees, runAiMerge } from "../merger-ai.js"; import { activeSessionRegistry } from "../active-session-registry.js"; +import { MIN_TEMP_WORKTREE_REAP_AGE_MS } from "../self-healing.js"; import type { RunAuditor } from "../run-audit.js"; const fsState = vi.hoisted(() => ({ failReaddirPath: "" })); @@ -117,6 +118,11 @@ function tempAiMergeDir(name: string): string { return dir; } +function makeAge(path: string, ageMs: number): void { + const old = new Date(Date.now() - ageMs); + utimesSync(path, old, old); +} + function realMergeAgent(taskId = "FN-1") { return vi.fn(async (cwd: string) => { execSync(`git merge --squash fusion/${taskId.toLowerCase()}`, { cwd, stdio: "pipe" }); @@ -177,6 +183,7 @@ describe("AI merge temp worktree cleanup", () => { it("pruneExistingAiMergeWorktrees removes stale same-task directories", async () => { const stale = tempAiMergeDir("fusion-ai-merge-fn-777-stale"); + makeAge(stale, MIN_TEMP_WORKTREE_REAP_AGE_MS + 1_000); const { audit, events } = makeAudit(); const logs: string[] = []; @@ -188,6 +195,18 @@ describe("AI merge temp worktree cleanup", () => { ])); }); + it("pruneExistingAiMergeWorktrees skips too-new same-task directories", async () => { + const fresh = tempAiMergeDir("fusion-ai-merge-fn-777-fresh"); + const { audit, events } = makeAudit(); + const logs: string[] = []; + + await expect(pruneExistingAiMergeWorktrees("FN-777", process.cwd(), audit, vi.fn(async (message: string) => { logs.push(message); }))).resolves.toBe(0); + + expect(existsSync(fresh)).toBe(true); + expect(events).toEqual([]); + expect(logs.join("\n")).toContain("skipping too-new worktree"); + }); + it("pruneExistingAiMergeWorktrees skips directories for other tasks", async () => { const other = tempAiMergeDir("fusion-ai-merge-fn-778-stale"); const { audit, events } = makeAudit(); @@ -201,7 +220,7 @@ describe("AI merge temp worktree cleanup", () => { it("pruneExistingAiMergeWorktrees skips active-session paths", async () => { const stale = tempAiMergeDir("fusion-ai-merge-fn-777-active"); const canonical = realpathSync(stale); - activeSessionRegistry.registerPath(canonical, { taskId: "FN-777", kind: "executor", ownerKey: "FN-777" }); + activeSessionRegistry.registerPath(canonical, { taskId: "FN-777", kind: "ai-merge", ownerKey: "ai-merge:FN-777" }); const { audit, events } = makeAudit(); await expect(pruneExistingAiMergeWorktrees("FN-777", process.cwd(), audit, vi.fn(async () => undefined))).resolves.toBe(0); @@ -209,19 +228,29 @@ describe("AI merge temp worktree cleanup", () => { expect(events).toEqual([]); activeSessionRegistry.unregisterPath(canonical); + makeAge(stale, MIN_TEMP_WORKTREE_REAP_AGE_MS + 1_000); await expect(pruneExistingAiMergeWorktrees("FN-777", process.cwd(), audit, vi.fn(async () => undefined))).resolves.toBe(1); expect(existsSync(stale)).toBe(false); }); - it("runAiMerge emits success cleanup audit events", async () => { + it("runAiMerge registers the clean-room worktree while merging and unregisters after", async () => { const { dir } = initRepoWithBranch(); const { store, audits } = makeStore(); + let observedMergeRoot = ""; + const mergeAgent = vi.fn(async (cwd: string) => { + observedMergeRoot = cwd; + expect(activeSessionRegistry.isPathActive(realpathSync(cwd))).toBe(true); + expect(activeSessionRegistry.isPathActive(cwd)).toBe(true); + await realMergeAgent()(cwd); + }); await runAiMerge(store, dir, "FN-1", { manual: true }, { - mergeAgent: realMergeAgent(), + mergeAgent, reviewAgent: vi.fn(async () => "REVIEW_VERDICT: approve"), }); + expect(observedMergeRoot).toContain("fusion-ai-merge-fn-1-"); + expect(activeSessionRegistry.pathsForTask("FN-1")).toEqual([]); const cleanupEvents = audits.filter((event) => event.mutationType === "merge:ai-worktree-cleanup"); expect(cleanupEvents).toEqual(expect.arrayContaining([ expect.objectContaining({ metadata: expect.objectContaining({ phase: "git-remove", success: true }) }), @@ -233,6 +262,7 @@ describe("AI merge temp worktree cleanup", () => { const taskId = "FN-777"; const { dir } = initRepoWithBranch(taskId); const orphan = tempAiMergeDir("fusion-ai-merge-fn-777-orphan"); + makeAge(orphan, MIN_TEMP_WORKTREE_REAP_AGE_MS + 1_000); const { store, audits } = makeStore(taskId); await runAiMerge(store, dir, taskId, { manual: true }, { diff --git a/packages/engine/src/__tests__/self-healing-tempdir-sweep.test.ts b/packages/engine/src/__tests__/self-healing-tempdir-sweep.test.ts index 94ae335786..a03e674737 100644 --- a/packages/engine/src/__tests__/self-healing-tempdir-sweep.test.ts +++ b/packages/engine/src/__tests__/self-healing-tempdir-sweep.test.ts @@ -45,7 +45,7 @@ vi.mock("node:child_process", async () => { }); import { activeSessionRegistry } from "../active-session-registry.js"; -import { SelfHealingManager } from "../self-healing.js"; +import { DONE_TASK_TEMP_WORKTREE_GRACE_MS, MIN_TEMP_WORKTREE_REAP_AGE_MS, SelfHealingManager, STALE_TEMP_MERGE_WORKTREE_MS } from "../self-healing.js"; const RM = { recursive: true, force: true, maxRetries: 5, retryDelay: 50 } as const; let sandboxRoot = ""; @@ -115,6 +115,10 @@ function missingTask(): () => Promise { return async () => { throw new Error("Task FN-999 not found"); }; } +function transientErrorTask(): () => Promise { + return async () => { throw new Error("SQLITE_BUSY: database is locked"); }; +} + async function sweep(manager: SelfHealingManager): Promise { return await (manager as any).cleanupStaleTempMergeWorktrees(); } @@ -150,7 +154,7 @@ describe("SelfHealingManager temp-dir AI merge worktree sweep", () => { const stale = tempMergeDir(); makeStale(stale); const canonical = realpathSync(stale); - activeSessionRegistry.registerPath(canonical, { taskId: "FN-1", kind: "executor", ownerKey: "FN-1" }); + activeSessionRegistry.registerPath(canonical, { taskId: "FN-1", kind: "ai-merge", ownerKey: "ai-merge:FN-1" }); const { manager, audits } = makeManager(); await expect(sweep(manager)).resolves.toBe(0); @@ -231,18 +235,54 @@ describe("SelfHealingManager temp-dir AI merge worktree sweep", () => { ])); }); - it("removes worktree for deleted task immediately", async () => { - const fresh = tempMergeDir("fusion-ai-merge-fn-999-deletedtask"); + it("keeps fresh worktree for deleted task until minimum age floor", async () => { + const fresh = tempMergeDir("fusion-ai-merge-fn-999-deletedtaskfresh"); + makeAge(fresh, MIN_TEMP_WORKTREE_REAP_AGE_MS - 1_000); + const { manager, audits } = makeManager({}, missingTask()); + + await expect(sweep(manager)).resolves.toBe(0); + + expect(existsSync(fresh)).toBe(true); + expect(sweepAudits(audits)).toEqual([]); + }); + + it("removes worktree for deleted task after minimum age floor", async () => { + const stale = tempMergeDir("fusion-ai-merge-fn-999-deletedtaskstale"); + makeAge(stale, MIN_TEMP_WORKTREE_REAP_AGE_MS + 1_000); const { manager, audits } = makeManager({}, missingTask()); await expect(sweep(manager)).resolves.toBe(1); - expect(existsSync(fresh)).toBe(false); + expect(existsSync(stale)).toBe(false); expect(sweepAudits(audits)).toEqual(expect.arrayContaining([ expect.objectContaining({ metadata: expect.objectContaining({ success: true, reason: "deleted-task" }) }), ])); }); + it("keeps fresh worktree on transient task lookup error", async () => { + const fresh = tempMergeDir("fusion-ai-merge-fn-999-lookuperrorfresh"); + makeAge(fresh, MIN_TEMP_WORKTREE_REAP_AGE_MS - 1_000); + const { manager, audits } = makeManager({}, transientErrorTask()); + + await expect(sweep(manager)).resolves.toBe(0); + + expect(existsSync(fresh)).toBe(true); + expect(sweepAudits(audits)).toEqual([]); + }); + + it("removes worktree on transient task lookup error only after full stale gate", async () => { + const stale = tempMergeDir("fusion-ai-merge-fn-999-lookuperrorstale"); + makeAge(stale, STALE_TEMP_MERGE_WORKTREE_MS + 1_000); + const { manager, audits } = makeManager({}, transientErrorTask()); + + await expect(sweep(manager)).resolves.toBe(1); + + expect(existsSync(stale)).toBe(false); + expect(sweepAudits(audits)).toEqual(expect.arrayContaining([ + expect.objectContaining({ metadata: expect.objectContaining({ success: true, reason: "lookup-error" }) }), + ])); + }); + it("keeps worktree for in-progress task within 2h gate", async () => { const fresh = tempMergeDir("fusion-ai-merge-fn-999-inprogressfresh"); const { manager } = makeManager({}, taskWithColumn("in-progress")); @@ -267,7 +307,7 @@ describe("SelfHealingManager temp-dir AI merge worktree sweep", () => { it("keeps fresh worktree for done task within grace period", async () => { const fresh = tempMergeDir("fusion-ai-merge-fn-999-donefresh"); - makeAge(fresh, 5 * 60 * 1000); + makeAge(fresh, DONE_TASK_TEMP_WORKTREE_GRACE_MS - 1_000); const { manager } = makeManager({}, taskWithColumn("done")); await expect(sweep(manager)).resolves.toBe(0); diff --git a/packages/engine/src/active-session-registry.ts b/packages/engine/src/active-session-registry.ts index 045afe3e8c..ec28db0158 100644 --- a/packages/engine/src/active-session-registry.ts +++ b/packages/engine/src/active-session-registry.ts @@ -1,4 +1,4 @@ -export type ActiveSessionKind = "executor" | "step-session" | "workflow-step" | "step-session-parallel"; +export type ActiveSessionKind = "executor" | "step-session" | "workflow-step" | "step-session-parallel" | "ai-merge"; export interface ActiveSessionRegistration { taskId: string; diff --git a/packages/engine/src/merger-ai.ts b/packages/engine/src/merger-ai.ts index e06102db25..55e9fc4c2b 100644 --- a/packages/engine/src/merger-ai.ts +++ b/packages/engine/src/merger-ai.ts @@ -32,7 +32,7 @@ */ import { execFile } from "node:child_process"; import { promisify } from "node:util"; -import { readdirSync, realpathSync, rmSync } from "node:fs"; +import { readdirSync, realpathSync, rmSync, statSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -64,6 +64,7 @@ import { createRunAuditor, generateSyntheticRunId, type RunAuditor } from "./run import { createLogger } from "./logger.js"; import { captureSingleCommitLandedMetadata, type MergerOptions } from "./merger.js"; import { activeSessionRegistry } from "./active-session-registry.js"; +import { MIN_TEMP_WORKTREE_REAP_AGE_MS } from "./self-healing.js"; const execFileAsync = promisify(execFile); const aiMergeLog = createLogger("merger-ai"); @@ -136,6 +137,18 @@ export async function pruneExistingAiMergeWorktrees( continue; } + try { + const stat = statSync(canonicalPath); + const ageMs = Date.now() - stat.mtimeMs; + if (ageMs < MIN_TEMP_WORKTREE_REAP_AGE_MS) { + await log(`AI merge pre-merge prune: skipping too-new worktree ${canonicalPath} (age ${Math.max(0, Math.round(ageMs))}ms)`); + continue; + } + } catch (err: unknown) { + await log(`AI merge pre-merge prune: failed to stat ${canonicalPath}: ${getErrorMessage(err)} — skipping candidate`); + continue; + } + try { await execFileAsync("git", ["worktree", "remove", "--force", canonicalPath], { cwd: projectRootDir, @@ -910,9 +923,20 @@ export async function runAiMerge( // 1. Clean-room worktree at the integration tip. const mergeRoot = await mkdtemp(join(tmpdir(), `fusion-ai-merge-${taskId.toLowerCase()}-`)); let worktreeAdded = false; + const registeredMergePaths = new Set(); try { await git(["worktree", "add", "--detach", mergeRoot, tipSha], projectRootDir); worktreeAdded = true; + let canonicalMergeRoot = mergeRoot; + try { + canonicalMergeRoot = realpathSync(mergeRoot); + } catch { + canonicalMergeRoot = mergeRoot; + } + for (const pathToRegister of new Set([canonicalMergeRoot, mergeRoot])) { + activeSessionRegistry.registerPath(pathToRegister, { taskId, kind: "ai-merge", ownerKey: `ai-merge:${taskId}` }); + registeredMergePaths.add(pathToRegister); + } await audit.git({ type: "merge:ai-clean-room", target: integrationBranch, metadata: { taskId, tipSha, mergeRoot } }); await log(`AI merge: merging ${branch} into ${integrationBranch} (clean room at ${short(tipSha)})${advanceRetries ? ` — retry ${advanceRetries} after concurrent advance` : ""}`); @@ -948,6 +972,9 @@ export async function runAiMerge( await log(`AI merge: advanced ${integrationBranch} → ${short(squashSha)} (local checkout: ${landed.localSync})`); return await finalizeMerged(store, projectRootDir, taskId, task, branch, integrationBranch, squashSha, audit, log, { empty: false }); } finally { + for (const registeredPath of registeredMergePaths) { + activeSessionRegistry.unregisterPath(registeredPath); + } await cleanupAiMergeWorktree({ taskId, mergeRoot, projectRootDir, worktreeAdded, audit, log }); } } diff --git a/packages/engine/src/self-healing.ts b/packages/engine/src/self-healing.ts index dcbd736c99..0ad184647d 100644 --- a/packages/engine/src/self-healing.ts +++ b/packages/engine/src/self-healing.ts @@ -75,8 +75,9 @@ const BOARD_STALL_NOTIFICATION_COOLDOWN_MS = 60 * 60_000; const DB_CORRUPTION_NOTIFICATION_COOLDOWN_MS = 60 * 60 * 1000; const FTS_MAINTENANCE_MERGE_CADENCE_TICKS = 1; const FTS_MAINTENANCE_OPTIMIZE_CADENCE_TICKS = 4; -const STALE_TEMP_MERGE_WORKTREE_MS = 2 * 60 * 60 * 1000; -const DONE_TASK_TEMP_WORKTREE_GRACE_MS = 10 * 60 * 1000; +export const STALE_TEMP_MERGE_WORKTREE_MS = 2 * 60 * 60 * 1000; +export const DONE_TASK_TEMP_WORKTREE_GRACE_MS = 10 * 60 * 1000; +export const MIN_TEMP_WORKTREE_REAP_AGE_MS = DONE_TASK_TEMP_WORKTREE_GRACE_MS; // Live pathology peaked around 775 KB/task (~96 MB for ~120 tasks), while a // rebuilt healthy index was ~0.1 MB. Keep the steady-state budget generous but // bounded so sustained text churn heals before segment growth becomes material. @@ -100,6 +101,14 @@ function extractTaskIdFromTempMergeDir(dirname: string): string | null { return match?.[1]?.toUpperCase() ?? null; } +function getErrorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + +function isTaskNotFoundError(err: unknown): boolean { + return /\btask\s+fn-\d+\s+not found\b/i.test(getErrorMessage(err)); +} + type BranchGroupLandingRecorder = { recordBranchGroupMemberLanded?: (groupId: string, payload: { taskId: string; @@ -8919,12 +8928,19 @@ export class SelfHealingManager { ageGateMs = DONE_TASK_TEMP_WORKTREE_GRACE_MS; cleanupReason = "done-task-stale"; } - } catch { - ageGateMs = 0; - cleanupReason = "deleted-task"; + } catch (err: unknown) { + if (isTaskNotFoundError(err)) { + ageGateMs = MIN_TEMP_WORKTREE_REAP_AGE_MS; + cleanupReason = "deleted-task"; + } else { + const errorMessage = getErrorMessage(err); + cleanupReason = "lookup-error"; + log.warn(`[self-healing] temp-dir sweep: task lookup failed for ${taskId}: ${errorMessage}; using conservative age gate`); + } } } - if (ageGateMs > 0 && ageMs < ageGateMs) continue; + ageGateMs = Math.max(ageGateMs, MIN_TEMP_WORKTREE_REAP_AGE_MS); + if (ageMs < ageGateMs) continue; try { canonicalPath = realpathSync(path); } catch { diff --git a/packages/engine/vitest.config.ts b/packages/engine/vitest.config.ts index 4cd26d5fcd..ca494cc864 100644 --- a/packages/engine/vitest.config.ts +++ b/packages/engine/vitest.config.ts @@ -107,7 +107,6 @@ export default defineConfig({ "dist/**", "src/__tests__/merger-file-scope-invariant.test.ts", "src/__tests__/project-engine-manager.test.ts", - "src/__tests__/merger-ai-cleanup.test.ts", "src/__tests__/self-healing-already-merged.real-git.test.ts", ], }, diff --git a/scripts/lib/test-quarantine.json b/scripts/lib/test-quarantine.json index c8f1c06d6d..5df7917058 100644 --- a/scripts/lib/test-quarantine.json +++ b/scripts/lib/test-quarantine.json @@ -11,11 +11,6 @@ "reason": "Flake: vi.mock('node:child_process') occasionally doesn't take under workspace-concurrent runs, letting real git binary leak and report staged files unrelated to test scope (trips FileScopeViolationError). Same logic covered by real-git fixture tests in reliability-interactions/workflow-and-file-scope. FN-6206.", "quarantinedAt": "2026-06-10" }, - { - "file": "packages/engine/src/__tests__/merger-ai-cleanup.test.ts", - "reason": "Flake observed during FN-6206 verification: `pruneExistingAiMergeWorktrees skips active-session paths` failed in full `pnpm --filter @fusion/engine test` runs while the file passed standalone, indicating suite-order/concurrency sensitivity. Follow-up FN-6207.", - "quarantinedAt": "2026-06-10" - }, { "file": "packages/engine/src/__tests__/self-healing-already-merged.real-git.test.ts", "reason": "Flake observed during FN-6226 verification: full `pnpm --filter @fusion/engine test` expected two run-audit events but saw four after unrelated real-git/self-healing cleanup activity. The failure is outside fast-mode workflow changes and indicates suite-order/temp-state sensitivity.",