Merge pull request #3115 from gsd-build/fix/3053-sdk-ignores-multi-plan-phase-layout-plan

fix: count nested plans/ layout in phase status indexing
This commit is contained in:
Tom Boucher
2026-05-04 23:35:26 -04:00
committed by GitHub
7 changed files with 159 additions and 22 deletions

View File

@@ -14,6 +14,30 @@ const { maskIfSecret } = require('./secrets.cjs');
// same in markdown but differ textually.
const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
function listPhaseSummaryFiles(phaseDir) {
const phaseFiles = fs.readdirSync(phaseDir);
const rootSummaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
const plansDir = path.join(phaseDir, 'plans');
let nestedSummaries = [];
if (fs.existsSync(plansDir)) {
const files = fs.readdirSync(plansDir);
nestedSummaries = files.filter(f => /^SUMMARY-\d+.*\.md$/i.test(f));
}
return rootSummaries.concat(nestedSummaries);
}
function listPhasePlanFiles(phaseDir) {
const phaseFiles = fs.readdirSync(phaseDir);
const rootPlans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
const plansDir = path.join(phaseDir, 'plans');
let nestedPlans = [];
if (fs.existsSync(plansDir)) {
const files = fs.readdirSync(plansDir);
nestedPlans = files.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
}
return rootPlans.concat(nestedPlans);
}
function getLatestCompletedMilestone(cwd) {
const milestonesPath = path.join(planningRoot(cwd), 'MILESTONES.md');
if (!fs.existsSync(milestonesPath)) return null;
@@ -901,8 +925,7 @@ function cmdInitMilestoneOp(cwd, raw) {
const dirName = diskPhaseDirs.get(canonicalizePhase(num));
if (!dirName) continue;
try {
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirName));
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
const hasSummary = listPhaseSummaryFiles(path.join(phasesDir, dirName)).length > 0;
if (hasSummary) completedPhases++;
} catch { /* intentionally empty */ }
}
@@ -914,8 +937,7 @@ function cmdInitMilestoneOp(cwd, raw) {
phaseCount = dirs.length;
for (const dir of dirs) {
try {
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
const hasSummary = listPhaseSummaryFiles(path.join(phasesDir, dir)).length > 0;
if (hasSummary) completedPhases++;
} catch { /* intentionally empty */ }
}
@@ -1072,8 +1094,8 @@ function cmdInitManager(cwd, raw) {
if (dirMatch) {
const fullDir = path.join(phasesDir, dirMatch);
const phaseFiles = fs.readdirSync(fullDir);
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
planCount = listPhasePlanFiles(fullDir).length;
summaryCount = listPhaseSummaryFiles(fullDir).length;
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
@@ -1353,8 +1375,8 @@ function cmdInitProgress(cwd, raw) {
const phasePath = path.join(phasesDir, dir);
const phaseFiles = fs.readdirSync(phasePath);
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
const plans = listPhasePlanFiles(phasePath);
const summaries = listPhaseSummaryFiles(phasePath);
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' :

View File

@@ -36,6 +36,28 @@ function coerceTruthToString(t) {
return '';
}
function countPhasePlansAndSummaries(phaseDir) {
const phaseFiles = fs.readdirSync(phaseDir);
const rootPlans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
const rootSummaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
let nestedPlans = [];
let nestedSummaries = [];
const plansDir = path.join(phaseDir, 'plans');
if (fs.existsSync(plansDir)) {
const planFiles = fs.readdirSync(plansDir);
nestedPlans = planFiles.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
nestedSummaries = planFiles.filter(f => /^SUMMARY-\d+.*\.md$/i.test(f));
}
return {
planCount: rootPlans.length + nestedPlans.length,
summaryCount: rootSummaries.length + nestedSummaries.length,
hasContext: phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'),
hasResearch: phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
};
}
/**
* Search for a phase header (and its section) within the given content string.
* Returns a result object if found (either a full match or a malformed_roadmap
@@ -197,11 +219,11 @@ function cmdRoadmapAnalyze(cwd, raw) {
const dirMatch = _phaseDirNames.find(d => phaseTokenMatches(d, normalized));
if (dirMatch) {
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
const counts = countPhasePlansAndSummaries(path.join(phasesDir, dirMatch));
planCount = counts.planCount;
summaryCount = counts.summaryCount;
hasContext = counts.hasContext;
hasResearch = counts.hasResearch;
if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
else if (summaryCount > 0) diskStatus = 'partial';