diff --git a/src/resources/extensions/gsd/workflow-reconcile.ts b/src/resources/extensions/gsd/workflow-reconcile.ts index 580473ad7..8236ffbe9 100644 --- a/src/resources/extensions/gsd/workflow-reconcile.ts +++ b/src/resources/extensions/gsd/workflow-reconcile.ts @@ -9,6 +9,7 @@ import { updateSliceStatus, updateMilestoneStatus, getSliceTasks, + getMilestoneSlices, insertVerificationEvidence, upsertDecision, openDatabase, @@ -77,6 +78,11 @@ function replayEvents(events: WorkflowEvent[]): void { const p = event.params; // Normalize cmd format: completion tools write hyphens ("complete-task"), // legacy logs use underscores ("complete_task"). Accept both formats. + // Type guard: malformed event lines with non-string cmd are skipped. + if (typeof event.cmd !== "string") { + logWarning("reconcile", `Event with non-string cmd skipped: ${JSON.stringify(event.cmd)}`); + continue; + } const cmd = event.cmd.replace(/-/g, "_"); switch (cmd) { case "complete_task": { @@ -125,9 +131,16 @@ function replayEvents(events: WorkflowEvent[]): void { } case "complete_milestone": { const milestoneId = p["milestoneId"] as string; - // Milestone completion via worktree replay — update status to complete - if (milestoneId) { + if (!milestoneId) break; + // Invariant check: only mark complete if all slices are closed. + // Without this guard, a reordered/partial event stream could close + // a milestone while work is still incomplete. + const mSlices = getMilestoneSlices(milestoneId); + const allClosed = mSlices.length === 0 || mSlices.every(s => isClosedStatus(s.status)); + if (allClosed) { updateMilestoneStatus(milestoneId, "complete", event.ts); + } else { + logWarning("reconcile", `Skipping complete_milestone replay for ${milestoneId}: not all slices are closed`); } break; } @@ -170,6 +183,7 @@ export function extractEntityKey( ): { type: string; id: string } | null { const p = event.params; // Normalize cmd format: accept both hyphens and underscores + if (typeof event.cmd !== "string") return null; const cmd = event.cmd.replace(/-/g, "_"); switch (cmd) {