From a7453719f58be2b7865394e46a265ec7bc695ac6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 09:49:50 -0400 Subject: [PATCH] fix: add fallback parser for prose-style roadmaps without ## Slices section (#807) (#835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the LLM writes freeform prose roadmaps with `## Slice S01: Title` headers instead of the machine-readable `## Slices` checklist, parseRoadmapSlices() returned zero slices, causing deriveState() to permanently block with 'No slice eligible'. Add a fallback parser that detects prose-style `## Slice SXX:` headers (and variants like `## S01:`, `## S01 —`) and extracts slice IDs, titles, and dependencies from the prose. Also parses `Depends on:` text patterns. All fallback slices default to risk:medium and done:false. --- .../extensions/gsd/roadmap-slices.ts | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/roadmap-slices.ts b/src/resources/extensions/gsd/roadmap-slices.ts index 9eed68e27..6d57a2d14 100644 --- a/src/resources/extensions/gsd/roadmap-slices.ts +++ b/src/resources/extensions/gsd/roadmap-slices.ts @@ -53,7 +53,12 @@ function extractSlicesSection(content: string): string { export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] { const slicesSection = extractSlicesSection(content); const slices: RoadmapSliceEntry[] = []; - if (!slicesSection) return slices; + if (!slicesSection) { + // Fallback: detect prose-style slice headers (## Slice S01: Title) + // when the LLM writes freeform prose instead of the ## Slices checklist. + // This prevents a permanent "No slice eligible" block (#807). + return parseProseSliceHeaders(content); + } const checkboxItems = slicesSection.split("\n"); let currentSlice: RoadmapSliceEntry | null = null; @@ -88,3 +93,42 @@ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] { if (currentSlice) slices.push(currentSlice); return slices; } + +/** + * Fallback parser for prose-style roadmaps where the LLM wrote + * `## Slice S01: Title` headers instead of the machine-readable + * `## Slices` checklist. Extracts slice IDs and titles so auto-mode + * can at least identify slices and plan them. + * + * Also handles `## S01: Title` and `## S01 — Title` variants. + */ +function parseProseSliceHeaders(content: string): RoadmapSliceEntry[] { + const slices: RoadmapSliceEntry[] = []; + const headerPattern = /^##\s+(?:Slice\s+)?(S\d+)[:\s—–-]+\s*(.+)/gm; + let match: RegExpExecArray | null; + + while ((match = headerPattern.exec(content)) !== null) { + const id = match[1]!; + const title = match[2]!.trim(); + + // Try to extract depends from prose: "Depends on: S01" or "**Depends on:** S01, S02" + const afterHeader = content.slice(match.index + match[0].length); + const nextHeader = afterHeader.search(/^##\s/m); + const section = nextHeader !== -1 ? afterHeader.slice(0, nextHeader) : afterHeader.slice(0, 500); + + const depsMatch = section.match(/\*{0,2}Depends\s+on:?\*{0,2}\s*(.+)/i); + let depends: string[] = []; + if (depsMatch) { + const rawDeps = depsMatch[1]!.replace(/none/i, "").trim(); + if (rawDeps) { + depends = expandDependencies( + rawDeps.split(/[,;]/).map(s => s.trim().replace(/[^A-Za-z0-9]/g, "")).filter(Boolean) + ); + } + } + + slices.push({ id, title, risk: "medium" as RiskLevel, depends, done: false, demo: "" }); + } + + return slices; +}