From f0e727d3699493e9c015dff9228e16d6c77af3ef Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:13:36 +0100 Subject: [PATCH] fix: update DB task status in writeBlockerPlaceholder for execute-task (#2657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit writeBlockerPlaceholder writes a placeholder SUMMARY file when idle recovery exhausts all retries, but never updated the DB task status. verifyExpectedArtifact checks the DB as the authoritative source for execute-task units — with status still "pending", verification failed, deriveState re-derived the same task, and the dispatch loop repeated indefinitely (observed as 8-9 "Advancing pipeline" messages). After writing the file, call updateTaskStatus to mark the task as "complete" in the DB. This lets verifyExpectedArtifact pass and breaks the infinite re-dispatch loop. Closes #2531 --- src/resources/extensions/gsd/auto-recovery.ts | 16 ++++- .../gsd/tests/idle-recovery.test.ts | 63 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/auto-recovery.ts b/src/resources/extensions/gsd/auto-recovery.ts index b92f5dcf5..42c7cac97 100644 --- a/src/resources/extensions/gsd/auto-recovery.ts +++ b/src/resources/extensions/gsd/auto-recovery.ts @@ -12,7 +12,7 @@ import { parseUnitId } from "./unit-id.js"; import { atomicWriteSync } from "./atomic-write.js"; import { clearParseCache } from "./files.js"; import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js"; -import { isDbAvailable, getTask, getSlice, getSliceTasks } from "./gsd-db.js"; +import { isDbAvailable, getTask, getSlice, getSliceTasks, updateTaskStatus } from "./gsd-db.js"; import { isValidationTerminal } from "./state.js"; import { nativeConflictFiles, @@ -425,6 +425,20 @@ export function writeBlockerPlaceholder( `Review and replace this file before relying on downstream artifacts.`, ].join("\n"); writeFileSync(absPath, content, "utf-8"); + + // Mark the task as complete in the DB so verifyExpectedArtifact passes. + // Without this, the DB status stays "pending" and the dispatch loop + // re-derives the same task indefinitely (#2531). + if (unitType === "execute-task" && isDbAvailable()) { + const parts = unitId.split("/"); + const mid = parts[0]; + const sid = parts[1]; + const tid = parts[2]; + if (mid && sid && tid) { + try { updateTaskStatus(mid, sid, tid, "complete", new Date().toISOString()); } catch { /* non-fatal */ } + } + } + return diagnoseExpectedArtifact(unitType, unitId, base); } diff --git a/src/resources/extensions/gsd/tests/idle-recovery.test.ts b/src/resources/extensions/gsd/tests/idle-recovery.test.ts index 664d1480a..f8940dc61 100644 --- a/src/resources/extensions/gsd/tests/idle-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/idle-recovery.test.ts @@ -294,3 +294,66 @@ test('verifyExpectedArtifact: hook types always return true', () => { } }); + +test('writeBlockerPlaceholder: updates DB task status for execute-task (#2531)', async () => { + const base = createFixtureBase(); + try { + const { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, getTask, isDbAvailable } = + await import("../gsd-db.ts"); + + const dbPath = join(base, ".gsd", "gsd.db"); + // Create the tasks directory (required for artifact path resolution) + mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true }); + + openDatabase(dbPath); + try { + insertMilestone({ id: "M001", title: "Test", status: "active" }); + insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "active" }); + insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Task", status: "pending" }); + + // Before fix: writeBlockerPlaceholder wrote the file but left DB as "pending" + writeBlockerPlaceholder("execute-task", "M001/S01/T01", base, "idle recovery exhausted"); + + const task = getTask("M001", "S01", "T01"); + assert.equal(task?.status, "complete", + "writeBlockerPlaceholder must update DB task status to 'complete' so verifyExpectedArtifact passes"); + + // Verify the full chain works: verifyExpectedArtifact should return true + const verified = verifyExpectedArtifact("execute-task", "M001/S01/T01", base); + assert.equal(verified, true, + "verifyExpectedArtifact should pass after writeBlockerPlaceholder updates DB status"); + } finally { + if (isDbAvailable()) closeDatabase(); + } + } finally { + cleanup(base); + } +}); + +test('writeBlockerPlaceholder: does NOT update DB for non-execute-task types', async () => { + const base = createFixtureBase(); + try { + const { openDatabase, closeDatabase, insertMilestone, insertSlice, getSlice, isDbAvailable } = + await import("../gsd-db.ts"); + + const dbPath = join(base, ".gsd", "gsd.db"); + mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true }); + + openDatabase(dbPath); + try { + insertMilestone({ id: "M001", title: "Test", status: "active" }); + insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "active" }); + + // research-slice is NOT execute-task — DB should NOT be updated + writeBlockerPlaceholder("research-slice", "M001/S01", base, "idle recovery exhausted"); + + const slice = getSlice("M001", "S01"); + assert.equal(slice?.status, "active", + "writeBlockerPlaceholder should not change DB status for non-execute-task types"); + } finally { + if (isDbAvailable()) closeDatabase(); + } + } finally { + cleanup(base); + } +});