feat(FN-4508): complete Step 7 — docs and compatibility updates

Fusion-Task-Id: FN-4508
Fusion-Task-Lineage: 948a9cae-3975-4f15-bd47-2f88b379171d
This commit is contained in:
Fusion
2026-05-14 16:28:25 -07:00
committed by gsxdsm
parent 88ce836f35
commit 9fe81424bd
3 changed files with 14 additions and 43 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Branch-conflict detection now clears stale cached task metadata (`worktree`, `branch`, `baseCommitSha`) when live branch/worktree mappings are missing, and classifies branch tips already reachable from `main` as `tip-already-merged` instead of reporting main's forward progress as stranded commits. This fixes FN-4471-class false-positive `branch-conflict-unrecoverable` parking.

View File

@@ -196,7 +196,7 @@ Port 4040 is the production dashboard port. A user's live dashboard session is t
## Architecture ## Architecture
- Merge deadlock self-healing now layers `recoverAlreadyMergedReviewTasks()`, `clearStaleBlockedBy()`, and `reclaimSelfOwnedBranchConflicts()` in `packages/engine/src/self-healing.ts`, plus the paused-aware in-review scope filter in `packages/engine/src/scheduler.ts` (`inReviewWithWorktree` excludes `paused` tasks). `reclaimSelfOwnedBranchConflicts()` now also recovers paused `branch-conflict-unrecoverable` review rows when ownership is self-proven, auto-reclaims `fusion/<task-id>` branches that are still live-mapped but have zero unique commits vs main by force-removing the stale worktree and deleting the branch, and clears `task.worktree`/`task.branch` so retries recreate a fresh checkout. `inspectBranchConflict()` backs this with a patch-id fallback for degraded/empty `git cherry` output before declaring zero-unique-commit subsumption; orphan `fusion/*` branches are still resolved by prune-or-rescue logic (subsumed branches pruned; unique-commit branches rescued into triage tasks instead of force delete). Completion fan-out now runs synchronously on `in-review → done` via `SelfHealingManager.reconcileCompletedTask()`, so downstream stale `blockedBy` links and residual `fusion/<task-id>` branch/worktree artifacts are reconciled immediately instead of waiting for periodic sweeps. - Merge deadlock self-healing now layers `recoverAlreadyMergedReviewTasks()`, `clearStaleBlockedBy()`, and `reclaimSelfOwnedBranchConflicts()` in `packages/engine/src/self-healing.ts`, plus the paused-aware in-review scope filter in `packages/engine/src/scheduler.ts` (`inReviewWithWorktree` excludes `paused` tasks). `reclaimSelfOwnedBranchConflicts()` now also recovers paused `branch-conflict-unrecoverable` review rows when ownership is self-proven, auto-reclaims `fusion/<task-id>` branches that are still live-mapped but have zero unique commits vs main by force-removing the stale worktree and deleting the branch, and clears `task.worktree`/`task.branch` so retries recreate a fresh checkout. `inspectBranchConflict()` backs this with a patch-id fallback for degraded/empty `git cherry` output before declaring zero-unique-commit subsumption, treats ghost live-worktree admin entries (mapping path missing on disk) as `stale-resolved`, and classifies tips already reachable from the integration target as `tip-already-merged` so stale cached `baseCommitSha` values are invalidated instead of enumerating main's forward progress as stranded commits. Orphan `fusion/*` branches are still resolved by prune-or-rescue logic (subsumed branches pruned; unique-commit branches rescued into triage tasks instead of force delete). Completion fan-out now runs synchronously on `in-review → done` via `SelfHealingManager.reconcileCompletedTask()`, so downstream stale `blockedBy` links and residual `fusion/<task-id>` branch/worktree artifacts are reconciled immediately instead of waiting for periodic sweeps.
- Restart recovery is coordinated through `RestartRecoveryCoordinator` (`packages/engine/src/restart-recovery-coordinator.ts`), which classifies interrupted `in-progress` runs at runtime startup: no-progress `fn_task_done` failures are safely requeued to `todo`, then remaining orphaned work is resumed via the executor. - Restart recovery is coordinated through `RestartRecoveryCoordinator` (`packages/engine/src/restart-recovery-coordinator.ts`), which classifies interrupted `in-progress` runs at runtime startup: no-progress `fn_task_done` failures are safely requeued to `todo`, then remaining orphaned work is resumed via the executor.
## Engine Process Rules ## Engine Process Rules

View File

@@ -15,7 +15,6 @@ async function run(command: string, cwd: string): Promise<string> {
describe("inspectBranchConflict zero-unique behavior", () => { describe("inspectBranchConflict zero-unique behavior", () => {
const dirs: string[] = []; const dirs: string[] = [];
afterEach(async () => { afterEach(async () => {
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
}); });
@@ -31,7 +30,7 @@ describe("inspectBranchConflict zero-unique behavior", () => {
return repoDir; return repoDir;
} }
it("returns fully-subsumed when branch tip is ancestor of main", async () => { it("returns tip-already-merged when branch tip is ancestor of main", async () => {
const repoDir = await setupRepo(); const repoDir = await setupRepo();
await run("git checkout -b fusion/fn-9001", repoDir); await run("git checkout -b fusion/fn-9001", repoDir);
await run("git checkout main", repoDir); await run("git checkout main", repoDir);
@@ -40,26 +39,17 @@ describe("inspectBranchConflict zero-unique behavior", () => {
const stalePath = path.join(repoDir, "wt-stale-9001"); const stalePath = path.join(repoDir, "wt-stale-9001");
await mkdir(stalePath, { recursive: true }); await mkdir(stalePath, { recursive: true });
const result = await inspectBranchConflict({ const result = await inspectBranchConflict({ repoDir, branchName: "fusion/fn-9001", conflictingWorktreePath: stalePath, requestingTaskId: "FN-9001", ownerTaskId: "FN-9001", startPoint: "main" });
repoDir, expect(result.kind).toBe("tip-already-merged");
branchName: "fusion/fn-9001",
conflictingWorktreePath: stalePath,
requestingTaskId: "FN-9001",
ownerTaskId: "FN-9001",
startPoint: "main",
});
expect(result.kind).toBe("fully-subsumed");
}); });
it("returns fully-subsumed when branch patch already exists upstream", async () => { it("returns tip-already-merged when branch patch already exists upstream", async () => {
const repoDir = await setupRepo(); const repoDir = await setupRepo();
await run("git checkout -b fusion/fn-9001", repoDir); await run("git checkout -b fusion/fn-9001", repoDir);
await appendFile(path.join(repoDir, "note.txt"), "change\n", "utf-8"); await appendFile(path.join(repoDir, "note.txt"), "change\n", "utf-8");
await run("git add note.txt", repoDir); await run("git add note.txt", repoDir);
await run("git commit -m 'feat(FN-9001): change' -m 'Fusion-Task-Id: FN-9001'", repoDir); await run("git commit -m 'feat(FN-9001): change' -m 'Fusion-Task-Id: FN-9001'", repoDir);
const branchCommit = await run("git rev-parse HEAD", repoDir); const branchCommit = await run("git rev-parse HEAD", repoDir);
await run("git checkout main", repoDir); await run("git checkout main", repoDir);
await run(`git cherry-pick ${branchCommit}`, repoDir); await run(`git cherry-pick ${branchCommit}`, repoDir);
@@ -68,16 +58,8 @@ describe("inspectBranchConflict zero-unique behavior", () => {
const stalePath = path.join(repoDir, "wt-stale-9001-upstream"); const stalePath = path.join(repoDir, "wt-stale-9001-upstream");
await mkdir(stalePath, { recursive: true }); await mkdir(stalePath, { recursive: true });
const result = await inspectBranchConflict({ const result = await inspectBranchConflict({ repoDir, branchName: "fusion/fn-9001", conflictingWorktreePath: stalePath, requestingTaskId: "FN-9001", ownerTaskId: "FN-9001", startPoint: "main" });
repoDir, expect(result.kind).toBe("tip-already-merged");
branchName: "fusion/fn-9001",
conflictingWorktreePath: stalePath,
requestingTaskId: "FN-9001",
ownerTaskId: "FN-9001",
startPoint: "main",
});
expect(result.kind).toBe("fully-subsumed");
}); });
it("returns reclaimable when branch still has unique commit", async () => { it("returns reclaimable when branch still has unique commit", async () => {
@@ -93,15 +75,7 @@ describe("inspectBranchConflict zero-unique behavior", () => {
const stalePath = path.join(repoDir, "wt-stale-9001-unique"); const stalePath = path.join(repoDir, "wt-stale-9001-unique");
await mkdir(stalePath, { recursive: true }); await mkdir(stalePath, { recursive: true });
const result = await inspectBranchConflict({ const result = await inspectBranchConflict({ repoDir, branchName: "fusion/fn-9001", conflictingWorktreePath: stalePath, requestingTaskId: "FN-9001", ownerTaskId: "FN-9001", startPoint: "main" });
repoDir,
branchName: "fusion/fn-9001",
conflictingWorktreePath: stalePath,
requestingTaskId: "FN-9001",
ownerTaskId: "FN-9001",
startPoint: "main",
});
expect(result.kind).toBe("reclaimable"); expect(result.kind).toBe("reclaimable");
}); });
@@ -118,15 +92,7 @@ describe("inspectBranchConflict zero-unique behavior", () => {
const stalePath = path.join(repoDir, "wt-stale-other"); const stalePath = path.join(repoDir, "wt-stale-other");
await mkdir(stalePath, { recursive: true }); await mkdir(stalePath, { recursive: true });
const result = await inspectBranchConflict({ const result = await inspectBranchConflict({ repoDir, branchName: "topic/other", conflictingWorktreePath: stalePath, requestingTaskId: "FN-9001", ownerTaskId: "FN-9001", startPoint: "main" });
repoDir,
branchName: "topic/other",
conflictingWorktreePath: stalePath,
requestingTaskId: "FN-9001",
ownerTaskId: "FN-9001",
startPoint: "main",
});
expect(result.kind).toBe("live-foreign"); expect(result.kind).toBe("live-foreign");
}); });
}); });