fix(gsd): reconcile plan-file tasks into DB when planner skips persistence (#3600)
When the planning agent writes S##-PLAN.md with task entries but never calls the gsd_plan_slice persistence tool, the DB has zero task rows even though the plan file on disk contains valid tasks. This causes deriveState to return phase='planning' forever — the auto-mode dispatcher re-dispatches plan-slice in an infinite loop. Add a reconciliation step in deriveStateFromDb: when the DB returns zero tasks but the plan file exists and contains parsed tasks, import them into the DB so the state machine can advance past planning into execution. This mirrors the existing #2514 reconciliation pattern for stale task status. Fixes #3600 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b4c6229360
commit
b1d9798e30
1 changed files with 39 additions and 0 deletions
|
|
@ -55,6 +55,7 @@ import {
|
|||
getSlice,
|
||||
insertMilestone,
|
||||
insertSlice,
|
||||
insertTask,
|
||||
updateTaskStatus,
|
||||
getPendingSliceGateCount,
|
||||
type MilestoneRow,
|
||||
|
|
@ -738,6 +739,44 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
|
|||
// ── Get tasks from DB ────────────────────────────────────────────────
|
||||
let tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
|
||||
|
||||
// ── Reconcile missing tasks: plan file has tasks but DB is empty (#3600) ──
|
||||
// When the planning agent writes S##-PLAN.md with task entries but never
|
||||
// calls the gsd_plan_slice persistence tool, the DB has zero task rows
|
||||
// even though the plan file contains valid tasks. Without this reconciliation,
|
||||
// deriveState returns phase='planning' forever — the dispatcher re-dispatches
|
||||
// plan-slice in an infinite loop.
|
||||
if (tasks.length === 0 && planFile) {
|
||||
try {
|
||||
const planContent = await loadFile(planFile);
|
||||
if (planContent) {
|
||||
const diskPlan = parsePlan(planContent);
|
||||
if (diskPlan.tasks.length > 0) {
|
||||
for (let i = 0; i < diskPlan.tasks.length; i++) {
|
||||
const t = diskPlan.tasks[i];
|
||||
try {
|
||||
insertTask({
|
||||
id: t.id,
|
||||
sliceId: activeSlice.id,
|
||||
milestoneId: activeMilestone.id,
|
||||
title: t.title,
|
||||
status: t.done ? 'complete' : 'pending',
|
||||
sequence: i + 1,
|
||||
});
|
||||
} catch (insertErr) {
|
||||
// Task may already exist from a partial previous import — skip
|
||||
logWarning("reconcile", `failed to insert task ${t.id} from plan file: ${insertErr instanceof Error ? insertErr.message : String(insertErr)}`);
|
||||
}
|
||||
}
|
||||
tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
|
||||
logWarning("reconcile", `imported ${tasks.length} tasks from plan file for ${activeMilestone.id}/${activeSlice.id} — DB was empty (#3600)`, { mid: activeMilestone.id, sid: activeSlice.id });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Non-fatal — fall through to the existing "empty plan" logic
|
||||
logError("reconcile", `plan-file task import failed for ${activeMilestone.id}/${activeSlice.id}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reconcile stale task status (#2514) ──────────────────────────────
|
||||
// When a session disconnects after the agent writes SUMMARY + VERIFY
|
||||
// artifacts but before postUnitPostVerification updates the DB, tasks
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue