diff --git a/sdk/src/query/phase-lifecycle.ts b/sdk/src/query/phase-lifecycle.ts index 5211c46a6..48620dbd1 100644 --- a/sdk/src/query/phase-lifecycle.ts +++ b/sdk/src/query/phase-lifecycle.ts @@ -85,6 +85,12 @@ function generateSlugInternal(text: string): string { * If no `` blocks exist, replaces in the entire content. * Otherwise, only replaces in content after the last `` close tag. * + * Edge case: when the active milestone is itself wrapped in a `
` block + * (e.g. collapsed before it is fully shipped), the last `
` belongs to + * the active milestone and the `after` slice is empty. In that case the function + * falls back to searching the full content with all complete `
` blocks + * stripped, so archived milestones are never touched. + * * @param content - Full ROADMAP.md content * @param pattern - Regex or string pattern to match * @param replacement - Replacement string @@ -102,7 +108,40 @@ export function replaceInCurrentMilestone( const offset = lastDetailsClose + '
'.length; const before = content.slice(0, offset); const after = content.slice(offset); - return before + after.replace(pattern, replacement); + + // Fast path: the current milestone is not inside a
block — the + // pattern lives in the plain text after the last
. + if (after.trim().length > 0) { + return before + after.replace(pattern, replacement); + } + + // Slow path: the active milestone is inside the last
block. + // Strip every complete
…
block except the last one, then + // apply the replacement inside that last block while leaving the stripped + // (archived) blocks untouched. + // + // Strategy: + // 1. Collect all complete
…
spans. + // 2. Replace only inside the LAST span; leave earlier spans unchanged. + const detailsBlockRe = /
[\s\S]*?<\/details>/gi; + const spans: { start: number; end: number; text: string }[] = []; + let m: RegExpExecArray | null; + while ((m = detailsBlockRe.exec(content)) !== null) { + spans.push({ start: m.index, end: m.index + m[0].length, text: m[0] }); + } + + if (spans.length === 0) { + // No complete blocks found — fall back to full-content replace. + return content.replace(pattern, replacement); + } + + const lastSpan = spans[spans.length - 1]; + const updatedLastBlock = lastSpan.text.replace(pattern, replacement); + return ( + content.slice(0, lastSpan.start) + + updatedLastBlock + + content.slice(lastSpan.end) + ); } // ─── readModifyWriteRoadmapMd ───────────────────────────────────────────