From 43de82912ef72820242b6340be5984c24cfa1cf6 Mon Sep 17 00:00:00 2001 From: ahwlsqja Date: Thu, 26 Mar 2026 14:14:34 +0900 Subject: [PATCH] fix: reconcile stale task status in filesystem-based state derivation (#2514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_deriveStateImpl` (used when no gsd.db exists) lacked the SUMMARY-based reconciliation added to `deriveStateFromDb` in 0e7a01f4. Heading-style tasks (`### T01:`) are always parsed as `done=false` by `parsePlan` because the heading syntax has no checkbox. When the agent writes a SUMMARY file but the plan heading has no checkbox, the task appears incomplete forever, causing infinite re-dispatch. Now checks each non-done task for a SUMMARY file on disk after `parsePlan()`, mirroring the DB reconciliation logic. Root cause: `parsePlan()` recognizes two task formats: 1. `- [x] **T01: Title**` → done from checkbox state 2. `### T01: Title` → always done=false (no checkbox to read) The DB path (deriveStateFromDb) was already fixed in 0e7a01f4 to reconcile via SUMMARY files. This commit applies the same fix to the filesystem path used by projects without gsd.db. --- src/resources/extensions/gsd/state.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/resources/extensions/gsd/state.ts b/src/resources/extensions/gsd/state.ts index 4a7180c29..9af4d785b 100644 --- a/src/resources/extensions/gsd/state.ts +++ b/src/resources/extensions/gsd/state.ts @@ -1248,6 +1248,24 @@ export async function _deriveStateImpl(basePath: string): Promise { } const slicePlan = parsePlan(slicePlanContent); + + // ── Reconcile stale task status for filesystem-based projects (#2514) ── + // Heading-style tasks (### T01:) are always parsed as done=false by + // parsePlan because the heading syntax has no checkbox. When the agent + // writes a SUMMARY file but the plan's heading isn't converted to a + // checkbox, the task appears incomplete forever — causing infinite + // re-dispatch. Reconcile by checking SUMMARY files on disk. + for (const t of slicePlan.tasks) { + if (t.done) continue; + const summaryPath = resolveTaskFile(basePath, activeMilestone.id, activeSlice.id, t.id, "SUMMARY"); + if (summaryPath && existsSync(summaryPath)) { + t.done = true; + process.stderr.write( + `gsd-reconcile: task ${activeMilestone.id}/${activeSlice.id}/${t.id} has SUMMARY on disk but plan shows incomplete — marking done (#2514)\n`, + ); + } + } + const taskProgress = { done: slicePlan.tasks.filter(t => t.done).length, total: slicePlan.tasks.length,