fix: preserve reclaimed worktree branch provenance (#3507)

## Summary

- persist engine branch-write provenance when reclaiming an existing
task worktree
- cover branch-conflict reclaim with a regression assertion for the
branch, worktree, and provenance tuple

## Test plan

- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/executor-worktree.test.ts --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/engine typecheck`
- `pnpm check:changesets -- --strict`
- `pnpm check:fnxc-future-dates`
- `pnpm build`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
  * Improved recovery when reclaiming existing task worktrees.
* Preserved task branch details and worktree paths during
branch-conflict recovery.
* Recorded whether branch updates originated from the system or an
operator for more reliable task state tracking.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
This commit is contained in:
Phil Larson
2026-08-23 16:53:38 -07:00
committed by GitHub
parent c9f3f11a72
commit 00b7078f79
3 changed files with 65 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Preserve task branches when Fusion reclaims an existing task worktree.
category: fix
dev: Supplies engine branch-write provenance during branch-conflict reclamation.

View File

@@ -1175,7 +1175,58 @@ describe("TaskExecutor worktree recovery", () => {
expect(result).toBe("reclaimed");
expect(normalize).toHaveBeenCalledWith(conflictPath, targetPath, "FN-8400", expect.objectContaining({ worktreesDir: ".worktrees" }));
expect(store.updateTask).toHaveBeenCalledWith("FN-8400", expect.objectContaining({ worktree: targetPath }));
expect(store.updateTask).toHaveBeenCalledWith(
"FN-8400",
expect.objectContaining({
worktree: targetPath,
branch: "fusion/fn-8400",
branchWriteOrigin: "engine",
}),
);
});
it("preserves operator provenance when reclaiming an operator-owned Fusion-namespaced branch", async () => {
const store = createMockStore();
store.getSettings.mockResolvedValue({ worktreesDir: ".worktrees" } as any);
const executor = createWorktreeExecutor(store, "/tmp/test");
const conflictPath = "/tmp/legacy-worktrees/recover-fn-8401";
const targetPath = "/tmp/test/.worktrees/recover-fn-8401";
const branch = "fusion/fn-8401";
vi.spyOn(branchConflictModule, "inspectBranchConflict").mockResolvedValueOnce({
kind: "reclaimable",
livePath: conflictPath,
tipSha: "70b47804bc6f27659638e17ac7cf279ed343ff6f",
taskAttributedCommitCount: 1,
strandedCommits: [{ sha: "70b47804bc6f27659638e17ac7cf279ed343ff6f", subject: "fix(FN-8401): preserve implementation" }],
} as any);
vi.spyOn(executor as any, "normalizeReclaimableWorktreePath").mockResolvedValue(targetPath);
const result = await (executor as any).handleBranchConflict(
{
...makeTask("FN-8401"),
branch,
worktree: conflictPath,
branchContext: { branchOverride: { by: "operator", at: "2026-08-22T22:00:00.000Z", branch } },
},
new BranchConflictError({
branchName: branch,
conflictingWorktreePath: conflictPath,
existingTipSha: "70b47804bc6f27659638e17ac7cf279ed343ff6f",
strandedCommits: [],
startPoint: "main",
recommendedAction: "reclaim",
}),
);
expect(result).toBe("reclaimed");
expect(store.updateTask).toHaveBeenCalledWith(
"FN-8401",
expect.objectContaining({
worktree: targetPath,
branch,
branchWriteOrigin: "operator",
}),
);
});
it("uses the task-pinned target when normalizing a branch-conflict reclaim", async () => {

View File

@@ -5,7 +5,7 @@
*/
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { isFusionDeletableBranch, type Settings, type Task, type TaskStore } from "@fusion/core";
import { classifyTaskBranchOrigin, isFusionDeletableBranch, type Settings, type Task, type TaskStore } from "@fusion/core";
import {
assertCleanBranchAtBase,
BranchConflictError,
@@ -55,7 +55,11 @@ export async function reclaimExistingWorktree(
): Promise<void> {
const targetPath = preservedWorktreeTargetPathForTask(task.id, livePath, settings, deps.rootDir);
const normalizedPath = await deps.normalizeReclaimableWorktreePath(livePath, targetPath, task.id, settings);
await deps.store.updateTask(task.id, { worktree: normalizedPath, branch });
await deps.store.updateTask(task.id, {
worktree: normalizedPath,
branch,
branchWriteOrigin: classifyTaskBranchOrigin(task, branch) === "operator-supplied" ? "operator" : "engine",
});
const latestTask = await deps.store.getTask(task.id);
const baseRef = await resolveDiffBaseRef(normalizedPath, latestTask.baseCommitSha);
if (baseRef) {