Merge pull request #3692 from Tibsfox/fix/complete-task-normalize-list-inputs

fix(gsd): normalize list inputs in complete-task + fix roadmap dep parsing
This commit is contained in:
Jeremy McSpadden 2026-04-07 07:02:02 -05:00 committed by GitHub
commit 1146352202
3 changed files with 89 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

@ -0,0 +1,54 @@
/**
* Regression test for #3692 normalizeListParam in complete-task
*
* Agents sometimes pass keyFiles/keyDecisions as comma-separated strings
* instead of arrays. normalizeListParam coerces both forms to string[].
*
* Also verifies roadmap-slices.ts detects dependency column from header.
*/
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const completeTaskSrc = readFileSync(
join(__dirname, '..', 'tools', 'complete-task.ts'),
'utf-8',
);
const roadmapSlicesSrc = readFileSync(
join(__dirname, '..', 'roadmap-slices.ts'),
'utf-8',
);
describe('complete-task normalizeListParam (#3692)', () => {
test('normalizeListParam function is defined', () => {
assert.match(completeTaskSrc, /function normalizeListParam\(/,
'normalizeListParam function should be defined in complete-task.ts');
});
test('normalizeListParam is applied to keyFiles', () => {
assert.match(completeTaskSrc, /normalizeListParam\(params\.keyFiles\)/,
'normalizeListParam should be applied to keyFiles');
});
test('normalizeListParam is applied to keyDecisions', () => {
assert.match(completeTaskSrc, /normalizeListParam\(params\.keyDecisions\)/,
'normalizeListParam should be applied to keyDecisions');
});
});
describe('roadmap-slices depColumnIndex detection (#3692)', () => {
test('depColumnIndex is detected from header row', () => {
assert.match(roadmapSlicesSrc, /depColumnIndex/,
'depColumnIndex variable should exist in roadmap-slices.ts');
assert.match(roadmapSlicesSrc, /headerCells/,
'headerCells should be parsed from the header row');
assert.match(roadmapSlicesSrc, /depends|deps|depend/i,
'header detection should match depends/deps/depend');
});
});

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