fix(engine): auto-reattach HEAD at handoff when branch ref is authoritative

The merge handoff refused with head-branch-mismatch whenever the reused
worktree's HEAD wasn't on fusion/<id> (detached, recycled to main, or on
a sibling branch), even when the branch ref itself still held a clean,
task-attributed lineage. That wedged FN-5339-class tasks in review for
no good reason.

Add isBranchAuthoritativeForTask in branch-conflicts.ts (branch ref
exists, tip carries Fusion-Task-Id trailer, base..branch is foreign-
contamination-free) and use it in acquireReuseHandoff: when HEAD drifts
but the branch ref is authoritative, run a plain `git checkout <branch>`
inside the already-asserted-clean worktree, re-read HEAD, and emit a
branch:auto-reattach-authoritative audit. Refusal still fires unchanged
when the branch ref is missing, missing the trailer, or contaminated,
so FN-5363 strict-lease and foreign-commit guards remain authoritative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-21 19:03:35 -07:00
parent 025683ca60
commit d947197c2a
4 changed files with 184 additions and 5 deletions

View File

@@ -410,6 +410,75 @@ describe("acquireReuseHandoff", () => {
);
});
it("re-attaches a detached HEAD when the branch ref carries the task's trailer", async () => {
vi.spyOn(branchAutocorrect, "attemptBranchAutocorrect").mockResolvedValue({ status: "failed", reason: "case-not-applicable" });
const store = createStore();
const auditEmit = vi.fn();
let headReads = 0;
mockedExecSync.mockImplementation((cmd: any) => {
const command = String(cmd);
if (command === "git rev-parse --abbrev-ref HEAD") {
headReads += 1;
// First read sees the detached state, post-checkout read sees the branch.
return Buffer.from(headReads === 1 ? "HEAD\n" : "fusion/fn-5279\n");
}
if (command === "git diff -z --name-only") return Buffer.from("");
if (command === "git diff -z --cached --name-only") return Buffer.from("");
if (command === "git status -z --porcelain") return Buffer.from("");
if (command === "git diff HEAD") return Buffer.from("");
if (command.startsWith("git rev-parse --verify")) return Buffer.from("abc123\n");
if (command.startsWith("git log -1 --pretty=%B")) {
return Buffer.from("feat(FN-5279): step 3\n\nFusion-Task-Id: FN-5279\n");
}
if (command === "git checkout fusion/fn-5279") return Buffer.from("");
return Buffer.from("");
});
const handoff = await acquireReuseHandoff({
task: await store.getTask("FN-5279"),
store,
projectRoot: "/tmp/project-root",
settings: {} as any,
worktreePath: "/tmp/task-worktree",
auditEmit,
});
expect(handoff.ok).toBe(true);
expect(auditEmit).toHaveBeenCalledWith(expect.objectContaining({
type: "branch:auto-reattach-authoritative",
metadata: expect.objectContaining({ taskId: "FN-5279", expectedBranch: "fusion/fn-5279" }),
}));
});
it("still refuses when HEAD is wrong AND the branch ref is not authoritative", async () => {
vi.spyOn(branchAutocorrect, "attemptBranchAutocorrect").mockResolvedValue({ status: "failed", reason: "case-not-applicable" });
mockedExecSync.mockImplementation((cmd: any) => {
const command = String(cmd);
if (command === "git rev-parse --abbrev-ref HEAD") return Buffer.from("feature/elsewhere\n");
if (command === "git diff -z --name-only") return Buffer.from("");
if (command === "git diff -z --cached --name-only") return Buffer.from("");
if (command === "git status -z --porcelain") return Buffer.from("");
if (command === "git diff HEAD") return Buffer.from("");
if (command.startsWith("git rev-parse --verify")) return Buffer.from("abc123\n");
// Trailer absent => not authoritative.
if (command.startsWith("git log -1 --pretty=%B")) return Buffer.from("feat(FN-9999): unrelated\n");
return Buffer.from("");
});
const refusal = await expectRefusal(
acquireReuseHandoff({
task: await createStore().getTask("FN-5279"),
store: createStore(),
projectRoot: "/tmp/project-root",
settings: {} as any,
worktreePath: "/tmp/task-worktree",
}),
"head-branch-mismatch",
"unexpected-branch",
);
expect(refusal.payload).toMatchObject({ authorityProbe: "tip-missing-task-trailer" });
});
it("reconciles stale same-task activeSessionRegistry entries before proceeding", async () => {
const store = createStore();
activeSessionRegistry.registerPath("/tmp/task-worktree", {

View File

@@ -275,6 +275,62 @@ async function summarizeTaskAttributedCommits(repoDir: string, range: string, ta
return { ownCount, foreignCount };
}
/**
* True iff `branch`'s tip commit carries a `Fusion-Task-Id: <taskId>` trailer.
* Used as the cheap "is this branch ref authoritative for this task" probe
* at merge handoff so that HEAD drift (detached, wrong branch) can recover
* via a safe re-attach instead of refusing the handoff outright.
*/
export async function branchTipCarriesTaskIdTrailer(
repoDir: string,
branch: string,
taskId: string,
): Promise<boolean> {
try {
const body = await runGit(repoDir, `git log -1 --pretty=%B ${quoteShellArg(branch)}`);
const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`(?:^|\\n)${FUSION_TASK_ID_TRAILER_KEY}: ${escaped}\\s*(?:\\n|$)`);
return pattern.test(body);
} catch {
return false;
}
}
/**
* Whole-branch authority check: the branch ref exists, its tip carries the
* task's Fusion-Task-Id trailer, and (when a base is supplied) the range
* `base..branch` has no foreign FN-attributed commits.
*
* Returns `{ ok: true }` when safe to treat the branch ref as authoritative
* for `taskId`. On failure, returns `{ ok: false, reason }` so callers can
* log/audit why the gentle recovery was refused.
*/
export async function isBranchAuthoritativeForTask(
repoDir: string,
branch: string,
taskId: string,
baseSha?: string,
): Promise<{ ok: true } | { ok: false; reason: string }> {
try {
await revParse(repoDir, `refs/heads/${branch}`);
} catch {
return { ok: false, reason: "branch-ref-missing" };
}
const tipCarriesTrailer = await branchTipCarriesTaskIdTrailer(repoDir, branch, taskId);
if (!tipCarriesTrailer) {
return { ok: false, reason: "tip-missing-task-trailer" };
}
if (baseSha) {
try {
await assertCleanBranchAtBase(repoDir, branch, baseSha, taskId);
} catch (err) {
const reason = err instanceof BranchCrossContaminationError ? "foreign-contamination" : "clean-branch-check-failed";
return { ok: false, reason };
}
}
return { ok: true };
}
export async function assertCleanBranchAtBase(
repoDir: string,
branchName: string,

View File

@@ -17,6 +17,7 @@ import {
reconcileSelfOwnedActiveSessionForRemoval,
} from "./active-session-registry.js";
import { attemptBranchAutocorrect } from "./branch-autocorrect.js";
import { isBranchAuthoritativeForTask } from "./branch-conflicts.js";
import { MeshLeaseManager } from "./mesh-lease-manager.js";
import {
canonicalizePath,
@@ -352,12 +353,56 @@ export async function acquireReuseHandoff(input: ReuseHandoffInput): Promise<Han
}
}
if (observedBranch !== expectedBranch) {
throw new MergeHandoffRefusedError("head-branch-mismatch", "unexpected-branch", {
taskId: input.task.id,
worktreePath,
observedBranch,
// The worktree's HEAD points elsewhere (detached or different branch) but
// the expected branch ref may still hold this task's authoritative work.
// If the branch tip carries the task's Fusion-Task-Id trailer and the
// range against base is contamination-free, re-attach via plain checkout
// (worktree was already asserted clean above, so this is safe and
// non-destructive — unlike `checkout -B` which would clobber the ref).
const authority = await isBranchAuthoritativeForTask(
input.projectRoot,
expectedBranch,
});
input.task.id,
);
if (authority.ok) {
const reattach = await execAsync(`git checkout ${expectedBranch}`, {
cwd: worktreePath,
encoding: "utf-8",
timeout: 30_000,
maxBuffer: 1024 * 1024,
}).then(
() => ({ ok: true as const }),
(err: unknown) => ({ ok: false as const, reason: err instanceof Error ? err.message : String(err) }),
);
if (reattach.ok) {
const { stdout: reattachedHead } = await execAsync("git rev-parse --abbrev-ref HEAD", {
cwd: worktreePath,
encoding: "utf-8",
timeout: 10_000,
maxBuffer: 1024 * 1024,
});
observedBranch = reattachedHead.trim();
await input.auditEmit?.({
type: "branch:auto-reattach-authoritative",
target: worktreePath,
metadata: {
taskId: input.task.id,
previousHead: observedBranch === expectedBranch ? undefined : observedBranch,
expectedBranch,
worktreePath,
},
});
}
}
if (observedBranch !== expectedBranch) {
throw new MergeHandoffRefusedError("head-branch-mismatch", "unexpected-branch", {
taskId: input.task.id,
worktreePath,
observedBranch,
expectedBranch,
authorityProbe: authority.ok ? "ok" : authority.reason,
});
}
}
const activeRecord = activeSessionRegistry.lookupByPath(worktreePath);