fix(engine): prevent phantom merges when verification fix runs without a commit

When the AI merge agent reported a build failure via fn_report_build_failure,
executeMergeAttempt immediately ran `git reset --merge` and threw. The catch
handler then spawned an in-merge fix agent on a clean main and called
amendMergeCommitWithFixes, which blindly amended HEAD — the *previous* task's
merge commit — silently dropping the current task's branch and inheriting
the prior task's stats. The dashboard then reported the new task as merged
with completely unrelated files.

- Drop the immediate reset at the build-failure throw site so the squash
  state survives for the in-merge fix path.
- Capture preAttemptHeadSha at each mergeAttempt and refuse to amend when
  HEAD never moved past it; instead, create a fresh commit from the squash
  + fix changes. If neither HEAD moved nor anything is staged, abort the
  merge instead of fabricating success.
- Move the cleanup reset into the mergeAttempt catch handler (with a
  labeled resetMergeWithWarn helper) so it still fires when the fix path
  is exhausted or disabled.
- Replace the AI-authored commit body with a deterministic body built from
  the branch's actual step-commit subjects after every successful AI merge.
  Stops the recurring problem of merge messages describing files that are
  not in the diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-28 21:04:01 -07:00
parent 0684a18a6f
commit 98c5736d22
2 changed files with 212 additions and 50 deletions

View File

@@ -2964,7 +2964,11 @@ describe("aiMergeTask — reset cleanup failure diagnostics", () => {
.map(([message]) => String(message))
.filter((message) => message.includes("git reset --merge cleanup failed"));
expect(cleanupWarnMessages.some((message) => message.includes("build-verification reset"))).toBe(true);
// Reset cleanup now runs from mergeAttempt's catch handler (after the
// squash state is preserved across the build-failure throw site for the
// in-merge fix path). With both verificationFixRetries=0 and
// buildRetryCount=0 the rollback fires from the "no retries left" branch.
expect(cleanupWarnMessages.some((message) => message.includes("build-verification rollback"))).toBe(true);
expect(cleanupWarnMessages.some((message) => message.includes(resetFailureMessage))).toBe(true);
warnSpy.mockRestore();