fix: merge worktree back to main when stopAuto is called after milestone completion (#2317) (#2430)

stopAuto Step 4 previously always called exitMilestone(preserveBranch: true),
which preserved the worktree branch but never merged it back. When auto-mode
stopped after complete-milestone, the code stayed stranded on the worktree branch.

Now checks if the milestone has a SUMMARY file (completion signal) and calls
mergeAndExit instead, so completed milestone code reaches main.

Fixes #2317

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-25 01:21:00 -04:00 committed by GitHub
parent f21ad837ac
commit 58631bba2b
2 changed files with 104 additions and 3 deletions

View file

@ -610,14 +610,48 @@ export async function stopAuto(
}
// ── Step 4: Auto-worktree exit ──
// When the milestone is complete (has a SUMMARY), merge the worktree branch
// back to main so code isn't stranded on the worktree branch (#2317).
// For incomplete milestones, preserve the branch for later resumption.
try {
if (s.currentMilestoneId) {
const notifyCtx = ctx
? { notify: ctx.ui.notify.bind(ctx.ui) }
: { notify: () => {} };
buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
preserveBranch: true,
});
const resolver = buildResolver();
// Check if the milestone is complete — SUMMARY file is the authoritative signal.
let milestoneComplete = false;
try {
const summaryPath = resolveMilestoneFile(
s.originalBasePath || s.basePath,
s.currentMilestoneId,
"SUMMARY",
);
if (!summaryPath) {
// Also check in the worktree path (SUMMARY may not be synced yet)
const wtSummaryPath = resolveMilestoneFile(
s.basePath,
s.currentMilestoneId,
"SUMMARY",
);
milestoneComplete = wtSummaryPath !== null;
} else {
milestoneComplete = true;
}
} catch {
// Non-fatal — fall through to preserveBranch path
}
if (milestoneComplete) {
// Milestone is complete — merge worktree branch back to main
resolver.mergeAndExit(s.currentMilestoneId, notifyCtx);
} else {
// Milestone still in progress — preserve branch for later resumption
resolver.exitMilestone(s.currentMilestoneId, notifyCtx, {
preserveBranch: true,
});
}
}
} catch (e) {
debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });

View file

@ -0,0 +1,67 @@
/**
* stop-auto-merge-back.test.ts Regression test for #2317.
*
* When auto-mode stops after a milestone is complete, stopAuto should trigger
* merge-back (mergeAndExit) instead of just exiting the worktree with
* preserveBranch: true. Otherwise milestone code stays stranded on the
* worktree branch and never reaches main.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
// ─── Source analysis: stopAuto calls mergeAndExit for complete milestones ────
const autoSrcPath = join(import.meta.dirname, "..", "auto.ts");
const autoSrc = readFileSync(autoSrcPath, "utf-8");
test("#2317: stopAuto should check milestone completion status before choosing exit strategy", () => {
// stopAuto Step 4 should NOT unconditionally call exitMilestone(preserveBranch: true).
// It should check if the milestone is complete and call mergeAndExit instead.
// Find the Step 4 section
const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
assert.ok(step4Idx !== -1, "Step 4 comment exists in stopAuto");
// Extract a reasonable window around Step 4 (up to Step 5)
const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
const step4Block = autoSrc.slice(step4Idx, step5Idx);
// The fix: Step 4 should call mergeAndExit when milestone is complete
assert.ok(
step4Block.includes("mergeAndExit"),
"Step 4 should call mergeAndExit for completed milestones",
);
});
test("#2317: stopAuto should detect milestone completion via SUMMARY file or DB", () => {
const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
const step4Block = autoSrc.slice(step4Idx, step5Idx);
// Should check completion status — either via SUMMARY file, DB getMilestone, or phase
const checksCompletion =
step4Block.includes("SUMMARY") ||
step4Block.includes("getMilestone") ||
step4Block.includes("complete") ||
step4Block.includes("isMilestoneComplete");
assert.ok(
checksCompletion,
"Step 4 should check if milestone is complete before deciding exit strategy",
);
});
test("#2317: stopAuto still preserves branch for incomplete milestones", () => {
const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
const step4Block = autoSrc.slice(step4Idx, step5Idx);
// preserveBranch should still be used as fallback for non-complete milestones
assert.ok(
step4Block.includes("preserveBranch"),
"Step 4 should still preserve branch for incomplete milestones (fallback path)",
);
});