fix: update DB task status in writeBlockerPlaceholder for execute-task (#2657)

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
This commit is contained in:
mastertyko 2026-03-26 15:13:36 +01:00 committed by GitHub
parent c2aaf6ace8
commit f0e727d369
2 changed files with 78 additions and 1 deletions

View file

@ -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);
}

View file

@ -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);
}
});