fix(gsd): normalize list inputs in complete-task + fix roadmap dep parsing

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) <noreply@anthropic.com>
This commit is contained in:
Tibsfox 2026-04-06 19:32:17 -07:00
parent b4c6229360
commit 09acec6dce
2 changed files with 35 additions and 7 deletions

View file

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

View file

@ -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: "",