diff --git a/src/resources/extensions/gsd/auto-dispatch.ts b/src/resources/extensions/gsd/auto-dispatch.ts index 8862880968..b529ba8f8b 100644 --- a/src/resources/extensions/gsd/auto-dispatch.ts +++ b/src/resources/extensions/gsd/auto-dispatch.ts @@ -84,6 +84,7 @@ import { annotateBackgroundable } from "./delegation-policy.js"; import { invalidateAllCaches } from "./cache.js"; import { insertMilestoneValidationGates } from "./milestone-validation-gates.js"; import { nativeHasChanges } from "./native-git-bridge.js"; +import { parseUnitId } from "./unit-id.js"; // ─── Types ──────────────────────────────────────────────────────────────── @@ -1192,11 +1193,36 @@ export const DISPATCH_RULES: DispatchRule[] = [ }, { name: "executing → execute-task", - match: async ({ state, mid, basePath, sessionContextWindow, modelRegistry, sessionProvider }) => { - if (state.phase !== "executing" || !state.activeTask) return null; + match: async ({ state, mid, basePath, session, sessionContextWindow, modelRegistry, sessionProvider }) => { + if (state.phase !== "executing") return null; if (!state.activeSlice) return missingSliceStop(mid, state.phase); const sid = state.activeSlice!.id; const sTitle = state.activeSlice!.title; + const retryUnitId = session?.pendingVerificationRetry?.unitId; + if (retryUnitId) { + const { milestone: retryMid, slice: retrySid, task: retryTid } = parseUnitId(retryUnitId); + if (retryMid === mid && retrySid === sid && retryTid) { + const retryTitle = state.activeTask?.id === retryTid + ? state.activeTask.title + : retryTid; + return { + action: "dispatch", + unitType: "execute-task", + unitId: retryUnitId, + prompt: await buildExecuteTaskPrompt( + mid, + sid, + sTitle, + retryTid, + retryTitle, + basePath, + { sessionContextWindow, modelRegistry, sessionProvider }, + ), + }; + } + } + + if (!state.activeTask) return null; const tid = state.activeTask.id; const tTitle = state.activeTask.title; diff --git a/src/resources/extensions/gsd/auto/phases.ts b/src/resources/extensions/gsd/auto/phases.ts index 7ac5f619a0..a895c7a31e 100644 --- a/src/resources/extensions/gsd/auto/phases.ts +++ b/src/resources/extensions/gsd/auto/phases.ts @@ -1867,7 +1867,7 @@ export async function runUnitPhase( } } - if (s.pendingVerificationRetry) { + if (s.pendingVerificationRetry && s.pendingVerificationRetry.unitId === unitId) { const retryCtx = s.pendingVerificationRetry; s.pendingVerificationRetry = null; const capped = diff --git a/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts b/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts index 3790c4d164..4c362d0d00 100644 --- a/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +++ b/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts @@ -249,6 +249,51 @@ test("dispatch-rule-coverage: executing with task plan present → execute-task" ); }); +test("dispatch-rule-coverage: executing honors pending verification retry unit", async (t) => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-exec-retry-")); + t.after(() => rmSync(tmp, { recursive: true, force: true })); + + writeMilestoneFile(tmp, "M001", "CONTEXT", "# Context\n"); + writeSliceFile(tmp, "M001", "S01", "PLAN", "# Plan\n"); + writeTaskPlan(tmp, "M001", "S01", "T01"); + writeTaskPlan(tmp, "M001", "S01", "T02"); + + const state = makeState({ + phase: "executing", + activeSlice: { id: "S01", title: "First Slice" }, + activeTask: { id: "T02", title: "Second Task" }, + }); + const ctx: DispatchContext = { + basePath: tmp, + mid: "M001", + midTitle: "Test Milestone", + state, + prefs: { reactive_execution: { enabled: false } } as DispatchContext["prefs"], + session: { + pendingVerificationRetry: { + unitId: "M001/S01/T01", + attempt: 2, + failureContext: "verification failed", + }, + } as DispatchContext["session"], + }; + const match = await findFirstMatch(ctx); + assertMatch( + match, + { + ruleName: "executing → execute-task", + action: "dispatch", + unitType: "execute-task", + }, + "executing pending verification retry", + ); + assert.equal( + match?.result.action === "dispatch" ? match.result.unitId : null, + "M001/S01/T01", + "executing pending verification retry: should redispatch the retry unit", + ); +}); + test("dispatch-rule-coverage: summarizing → complete-slice", async (t) => { const tmp = mkdtempSync(join(tmpdir(), "gsd-disp-cov-sum-")); t.after(() => rmSync(tmp, { recursive: true, force: true }));