From 09acec6dce92ca1a904e42edb9598b94e49c545e Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 19:32:17 -0700 Subject: [PATCH] fix(gsd): normalize list inputs in complete-task + fix roadmap dep parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: 1. gsd_complete_task now normalizes keyFiles/keyDecisions from strings (newline-delimited bullet lists) into arrays at the tool boundary, preventing type mismatch rejections on first call (#3361) 2. Legacy roadmap table parser now detects the dependency column from the header and only parses deps from that column or cells with explicit depends/deps keywords — prevents false deps from slice titles that mention other S-IDs (#3383, #3336) Closes #3361 Closes #3383 Closes #3336 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../extensions/gsd/roadmap-slices.ts | 26 +++++++++++++++---- .../extensions/gsd/tools/complete-task.ts | 16 ++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/resources/extensions/gsd/roadmap-slices.ts b/src/resources/extensions/gsd/roadmap-slices.ts index 781105ff8..33ec34b83 100644 --- a/src/resources/extensions/gsd/roadmap-slices.ts +++ b/src/resources/extensions/gsd/roadmap-slices.ts @@ -66,6 +66,17 @@ function parseTableSlices(section: string): RoadmapSliceEntry[] { const lines = section.split("\n"); const slices: RoadmapSliceEntry[] = []; + // Detect dependency column index from the header row (#3383, #3336). + // Only parse deps from this column (or cells with explicit "depends"/"deps" keywords). + let depColumnIndex = -1; + for (const line of lines) { + if (!line.includes("|")) continue; + if (/S\d+/.test(line)) break; // reached data rows + const headerCells = line.split("|").map(c => c.trim()).filter(Boolean); + depColumnIndex = headerCells.findIndex(c => /^(depends|deps|depend)/i.test(c)); + if (depColumnIndex >= 0) break; + } + for (const line of lines) { // Skip non-table lines, separator lines (|---|---|), and header rows if (!line.includes("|")) continue; @@ -95,12 +106,17 @@ function parseTableSlices(section: string): RoadmapSliceEntry[] { if (/\bmedium\b/.test(cellLower) || /\bmed\b/.test(cellLower)) { risk = "medium"; break; } } - // Extract dependencies from cells containing S-prefixed IDs (excluding the slice's own ID) + // Extract dependencies only from the dependency column or cells with + // explicit "depends"/"deps" keywords — never from title cells (#3383). let depends: string[] = []; - for (const cell of cells) { - if (/depends|deps/i.test(cell) || (cell.match(/S\d+/g)?.length ?? 0) > 0) { - const depIds = (cell.match(/S\d+/g) ?? []).filter(d => d !== id); - if (depIds.length > 0 || /none|—|-/i.test(cell)) { + if (depColumnIndex >= 0 && cells[depColumnIndex]) { + const depCell = cells[depColumnIndex]!; + const depIds = (depCell.match(/S\d+/g) ?? []).filter(d => d !== id); + depends = expandDependencies(depIds); + } else { + for (const cell of cells) { + if (/depends|deps/i.test(cell)) { + const depIds = (cell.match(/S\d+/g) ?? []).filter(d => d !== id); depends = expandDependencies(depIds); break; } diff --git a/src/resources/extensions/gsd/tools/complete-task.ts b/src/resources/extensions/gsd/tools/complete-task.ts index a862ae92b..e3c422aa9 100644 --- a/src/resources/extensions/gsd/tools/complete-task.ts +++ b/src/resources/extensions/gsd/tools/complete-task.ts @@ -44,6 +44,18 @@ export interface CompleteTaskResult { import type { TaskRow } from "../gsd-db.js"; +/** + * Normalize a list parameter that may arrive as a string (newline-delimited + * bullet list from the LLM) into a string array (#3361). + */ +function normalizeListParam(value: unknown): string[] { + if (Array.isArray(value)) return value.map(String); + if (typeof value === "string" && value.trim()) { + return value.split(/\n/).map(s => s.replace(/^[\s\-*•]+/, "").trim()).filter(Boolean); + } + return []; +} + /** * Build a TaskRow-shaped object from CompleteTaskParams so the unified * renderSummaryContent() can be used at completion time (#2720). @@ -63,8 +75,8 @@ function paramsToTaskRow(params: CompleteTaskParams, completedAt: string): TaskR blocker_discovered: params.blockerDiscovered ?? false, deviations: params.deviations ?? "", known_issues: params.knownIssues ?? "", - key_files: params.keyFiles ?? [], - key_decisions: params.keyDecisions ?? [], + key_files: normalizeListParam(params.keyFiles), + key_decisions: normalizeListParam(params.keyDecisions), full_summary_md: "", description: "", estimate: "",