From 36930694e4952cdbd3a74e4d424271d9f59f8abf Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:29:03 +0100 Subject: [PATCH] fix(gsd): use project root for prior-slice dispatch guard (#2863) Resolve the prior-slice completion guard against originalBasePath when auto-mode is running in a worktree. This keeps completed upstream milestones from blocking new dispatches because their SUMMARY state lives at the project root, not the stale worktree snapshot. Closes #2838 Co-authored-by: Paperclip --- src/resources/extensions/gsd/auto/phases.ts | 16 +++++- .../gsd/tests/journal-integration.test.ts | 55 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/auto/phases.ts b/src/resources/extensions/gsd/auto/phases.ts index 966247a5e..6269bfc0d 100644 --- a/src/resources/extensions/gsd/auto/phases.ts +++ b/src/resources/extensions/gsd/auto/phases.ts @@ -45,6 +45,17 @@ export function _resolveReportBasePath(s: Pick, +): string { + return s.originalBasePath || s.basePath; +} + /** * Generate and write an HTML milestone report snapshot. * Extracted from the milestone-transition block in autoLoop. @@ -667,9 +678,10 @@ export async function runDispatch( prompt = preDispatchResult.prompt; } + const guardBasePath = _resolveDispatchGuardBasePath(s); const priorSliceBlocker = deps.getPriorSliceCompletionBlocker( - s.basePath, - deps.getMainBranch(s.basePath), + guardBasePath, + deps.getMainBranch(guardBasePath), unitType, unitId, ); diff --git a/src/resources/extensions/gsd/tests/journal-integration.test.ts b/src/resources/extensions/gsd/tests/journal-integration.test.ts index 49f64d7a3..8447019ce 100644 --- a/src/resources/extensions/gsd/tests/journal-integration.test.ts +++ b/src/resources/extensions/gsd/tests/journal-integration.test.ts @@ -260,6 +260,61 @@ test("runDispatch emits dispatch-stop when dispatch returns stop action", async assert.equal(stopEvents[0].flowId, ic.flowId); }); +test("runDispatch checks prior-slice completion against the project root in worktree mode", async () => { + const capture = createEventCapture(); + const guardCalls: Array<{ fn: string; args: unknown[] }> = []; + const deps = makeMockDeps(capture, { + getMainBranch: (basePath: string) => { + guardCalls.push({ fn: "getMainBranch", args: [basePath] }); + return "main"; + }, + getPriorSliceCompletionBlocker: ( + basePath: string, + mainBranch: string, + unitType: string, + unitId: string, + ) => { + guardCalls.push({ + fn: "getPriorSliceCompletionBlocker", + args: [basePath, mainBranch, unitType, unitId], + }); + return null; + }, + }); + const ic = makeIC(deps, { + s: { + ...makeSession(), + basePath: "/tmp/project/.gsd/worktrees/M029-xoklo9", + originalBasePath: "/tmp/project", + } as any, + }); + const preData: PreDispatchData = { + state: { + phase: "executing", + activeMilestone: { id: "M029-xoklo9", title: "Test", status: "active" }, + activeSlice: { id: "S01", title: "Slice 1" }, + registry: [{ id: "M029-xoklo9", status: "active" }], + blockers: [], + } as any, + mid: "M029-xoklo9", + midTitle: "Test Milestone", + }; + + const result = await runDispatch(ic, preData, { + recentUnits: [], + stuckRecoveryAttempts: 0, + }); + + assert.equal(result.action, "next"); + assert.deepEqual(guardCalls, [ + { fn: "getMainBranch", args: ["/tmp/project"] }, + { + fn: "getPriorSliceCompletionBlocker", + args: ["/tmp/project", "main", "execute-task", "M001/S01/T01"], + }, + ]); +}); + test("runUnitPhase emits unit-start and unit-end with causedBy reference", async () => { const capture = createEventCapture();