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:
5
.changeset/fn-4508-ghost-branch-detector.md
Normal file
5
.changeset/fn-4508-ghost-branch-detector.md
Normal 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.
|
||||
@@ -196,7 +196,7 @@ Port 4040 is the production dashboard port. A user's live dashboard session is t
|
||||
|
||||
## 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.
|
||||
|
||||
## Engine Process Rules
|
||||
|
||||
@@ -15,7 +15,6 @@ async function run(command: string, cwd: string): Promise<string> {
|
||||
|
||||
describe("inspectBranchConflict zero-unique behavior", () => {
|
||||
const dirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
await run("git checkout -b fusion/fn-9001", repoDir);
|
||||
await run("git checkout main", repoDir);
|
||||
@@ -40,26 +39,17 @@ describe("inspectBranchConflict zero-unique behavior", () => {
|
||||
const stalePath = path.join(repoDir, "wt-stale-9001");
|
||||
await mkdir(stalePath, { recursive: true });
|
||||
|
||||
const result = await inspectBranchConflict({
|
||||
repoDir,
|
||||
branchName: "fusion/fn-9001",
|
||||
conflictingWorktreePath: stalePath,
|
||||
requestingTaskId: "FN-9001",
|
||||
ownerTaskId: "FN-9001",
|
||||
startPoint: "main",
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("fully-subsumed");
|
||||
const result = await inspectBranchConflict({ repoDir, branchName: "fusion/fn-9001", conflictingWorktreePath: stalePath, requestingTaskId: "FN-9001", ownerTaskId: "FN-9001", startPoint: "main" });
|
||||
expect(result.kind).toBe("tip-already-merged");
|
||||
});
|
||||
|
||||
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();
|
||||
await run("git checkout -b fusion/fn-9001", repoDir);
|
||||
await appendFile(path.join(repoDir, "note.txt"), "change\n", "utf-8");
|
||||
await run("git add note.txt", 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);
|
||||
|
||||
await run("git checkout main", 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");
|
||||
await mkdir(stalePath, { recursive: true });
|
||||
|
||||
const result = await inspectBranchConflict({
|
||||
repoDir,
|
||||
branchName: "fusion/fn-9001",
|
||||
conflictingWorktreePath: stalePath,
|
||||
requestingTaskId: "FN-9001",
|
||||
ownerTaskId: "FN-9001",
|
||||
startPoint: "main",
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("fully-subsumed");
|
||||
const result = await inspectBranchConflict({ repoDir, branchName: "fusion/fn-9001", conflictingWorktreePath: stalePath, requestingTaskId: "FN-9001", ownerTaskId: "FN-9001", startPoint: "main" });
|
||||
expect(result.kind).toBe("tip-already-merged");
|
||||
});
|
||||
|
||||
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");
|
||||
await mkdir(stalePath, { recursive: true });
|
||||
|
||||
const result = await inspectBranchConflict({
|
||||
repoDir,
|
||||
branchName: "fusion/fn-9001",
|
||||
conflictingWorktreePath: stalePath,
|
||||
requestingTaskId: "FN-9001",
|
||||
ownerTaskId: "FN-9001",
|
||||
startPoint: "main",
|
||||
});
|
||||
|
||||
const result = await inspectBranchConflict({ repoDir, branchName: "fusion/fn-9001", conflictingWorktreePath: stalePath, requestingTaskId: "FN-9001", ownerTaskId: "FN-9001", startPoint: "main" });
|
||||
expect(result.kind).toBe("reclaimable");
|
||||
});
|
||||
|
||||
@@ -118,15 +92,7 @@ describe("inspectBranchConflict zero-unique behavior", () => {
|
||||
const stalePath = path.join(repoDir, "wt-stale-other");
|
||||
await mkdir(stalePath, { recursive: true });
|
||||
|
||||
const result = await inspectBranchConflict({
|
||||
repoDir,
|
||||
branchName: "topic/other",
|
||||
conflictingWorktreePath: stalePath,
|
||||
requestingTaskId: "FN-9001",
|
||||
ownerTaskId: "FN-9001",
|
||||
startPoint: "main",
|
||||
});
|
||||
|
||||
const result = await inspectBranchConflict({ repoDir, branchName: "topic/other", conflictingWorktreePath: stalePath, requestingTaskId: "FN-9001", ownerTaskId: "FN-9001", startPoint: "main" });
|
||||
expect(result.kind).toBe("live-foreign");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user