diff --git a/packages/engine/src/merger-ai.ts b/packages/engine/src/merger-ai.ts index 306a426d87..fa33c041d3 100644 --- a/packages/engine/src/merger-ai.ts +++ b/packages/engine/src/merger-ai.ts @@ -1222,10 +1222,28 @@ export async function landWorkspaceTask( // it so a retry never re-advances the ref. This makes a re-run after a partial // land idempotent for the already-landed repos. if (await isRepoLanded(repoRootDir, integrationBranch, entry.landedSha, taskId, entry.branch)) { - await log(`AI merge (workspace): sub-repo ${repoRel} already landed (${short(entry.landedSha!)} ⊑ ${integrationBranch}) — skipping`); + /* + FNXC:Workspace 2026-07-07-08:35 (Phase C A1 recovery — recover landedSha for finalize proof): + isRepoLanded's A1 trailer-fallback can prove a sub-repo is landed even when its landedSha + was never persisted (the persist-after-advance window in persistRepoLandedSha threw). That + left the in-memory result with landedSha: undefined, so finalizeWorkspaceTask's + `status === "landed" && landedSha` filter dropped the recovered repo, `anyLanded` stayed + false, and the proven repo's retry STRANDED the task in-review with missing-merge-confirmation + (finalizeTask's hasDurableMergeProof needs mergeConfirmed). Recover the CURRENT integration + tip as landedSha — the trailer-fallback already proved the task branch is an ancestor of this + tip — so the finalize builds durable mergeConfirmed proof and the A1 retry completes to done. + */ + let recoveredLandedSha = entry.landedSha; + if (!recoveredLandedSha) { + recoveredLandedSha = await git( + ["rev-parse", "--verify", `refs/heads/${integrationBranch}`], + repoRootDir, + ).catch(() => undefined); + } + await log(`AI merge (workspace): sub-repo ${repoRel} already landed (${short(recoveredLandedSha ?? "?")} ⊑ ${integrationBranch}) — skipping`); repos.push({ repo: repoRel, repoRootDir, integrationBranch, branch: entry.branch, - status: "landed", landedSha: entry.landedSha, alreadyLanded: true, + status: "landed", landedSha: recoveredLandedSha, alreadyLanded: true, }); continue; } diff --git a/packages/engine/src/worktree-acquisition.ts b/packages/engine/src/worktree-acquisition.ts index e0bda12776..dd4ed98775 100644 --- a/packages/engine/src/worktree-acquisition.ts +++ b/packages/engine/src/worktree-acquisition.ts @@ -862,7 +862,15 @@ export async function acquireWorkspaceRepoWorktree( task: { ...task, worktree: undefined, branch: undefined }, rootDir: repoAbsPath, store, - settings, + // FNXC:Workspace 2026-07-07-08:40 (FN-7360 regression — strip shared branch overrides for per-repo start-point): + // FN-7360 pinned fresh task worktree creation to `resolveIntegrationBranch(rootDir, settings)` + // when no executionStartBranch is present, so new branches never inherit an ambient root HEAD. + // For a workspace sub-repo, `settings` carries the SHARED project integrationBranch/baseBranch; + // honoring it resolves a branch absent from this sub-repo and fails `git worktree add` with + // "invalid reference". Strip both overrides here so freshStartPoint falls through to this + // sub-repo's own origin/HEAD — matching the per-repo base-SHA capture below, which already + // resolves against stripped settings (F4/KTD3). + settings: { ...settings, integrationBranch: undefined, baseBranch: undefined }, logger, secretsStore, audit,