fix(phase-lifecycle): detect phases in bullet/bold ROADMAP formats

phaseAdd's phase-number regex only matched heading format (## Phase N:),
missing bullet checklist (- [x] Phase N:) and bold (**Phase N:**) entries.
When zero regex matches, newPhaseId defaulted to 1.

Fix: broaden regex to match all three formats, and add filesystem fallback
scanning .planning/phases/ when ROADMAP scan finds nothing.

Closes #2726
This commit is contained in:
Tom Boucher
2026-04-26 10:00:39 -04:00
parent f3685d9173
commit 54cbc2ad96
2 changed files with 93 additions and 1 deletions

View File

@@ -191,7 +191,8 @@ export const phaseAdd: QueryHandler = async (args, projectDir, workstream) => {
} else {
// Sequential mode: find highest integer phase number (in current milestone only)
// Skip 999.x backlog phases — they live outside the active sequence
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
// Matches heading (## Phase N:), bullet checklist (- [x] Phase N:), and bold (**Phase N:**)
const phasePattern = /(?:^|\n)\s*(?:[-*]\s*(?:\[[x ]\]\s*)?|#{2,4}\s*|\*{1,2}\s*)Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
let maxPhase = 0;
let m: RegExpExecArray | null;
while ((m = phasePattern.exec(content)) !== null) {
@@ -200,6 +201,26 @@ export const phaseAdd: QueryHandler = async (args, projectDir, workstream) => {
if (num > maxPhase) maxPhase = num;
}
// Belt-and-suspenders: if ROADMAP scan found nothing, fall back to scanning
// .planning/phases/ directory names as the canonical source of truth
if (maxPhase === 0) {
const phasesDir = planningPaths(projectDir, workstream).phases;
try {
const entries = await readdir(phasesDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const dirMatch = /^(\d+)[A-Z]?(?:\.\d+)*-/.exec(entry.name);
if (dirMatch) {
const num = parseInt(dirMatch[1], 10);
if (num >= 999) continue;
if (num > maxPhase) maxPhase = num;
}
}
} catch {
// phases dir may not exist yet — leave maxPhase as 0
}
}
newPhaseId = maxPhase + 1;
const paddedNum = String(newPhaseId).padStart(2, '0');
dirName = `${prefix}${paddedNum}-${slug}`;