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

@@ -83,6 +83,26 @@ function deriveStatusFromCheckbox(
return 'not_started';
}
function listPhasePlanAndSummaryCounts(phasePath: string): { plans: string[]; summaries: string[] } {
const phaseFiles = readdirSync(phasePath);
const rootPlans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
const rootSummaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
const plansDir = join(phasePath, 'plans');
let nestedPlans: string[] = [];
let nestedSummaries: string[] = [];
if (existsSync(plansDir)) {
const files = readdirSync(plansDir);
nestedPlans = files.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
nestedSummaries = files.filter(f => /^SUMMARY-\d+.*\.md$/i.test(f));
}
return {
plans: rootPlans.concat(nestedPlans),
summaries: rootSummaries.concat(nestedSummaries),
};
}
// ─── initNewProject ───────────────────────────────────────────────────────
/**
@@ -258,8 +278,7 @@ export const initProgress: QueryHandler = async (_args, projectDir, workstream)
const phasePath = join(paths.phases, dir);
const phaseFiles = 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, summaries } = listPhasePlanAndSummaryCounts(phasePath);
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
let status =
@@ -431,8 +450,9 @@ export const initManager: QueryHandler = async (_args, projectDir, workstream) =
if (dirMatch) {
const fullDir = join(paths.phases, dirMatch);
const phaseFiles = 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;
const counts = listPhasePlanAndSummaryCounts(fullDir);
planCount = counts.plans.length;
summaryCount = counts.summaries.length;
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');

View File

@@ -404,6 +404,28 @@ function searchPhaseInContent(content: string, escapedPhase: string, phaseNum: s
};
}
async function countPhasePlansAndSummaries(phaseDir: string): Promise<{ planCount: number; summaryCount: number; hasContext: boolean; hasResearch: boolean; }> {
const phaseFiles = await readdir(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: string[] = [];
let nestedSummaries: string[] = [];
const plansDir = join(phaseDir, 'plans');
if (existsSync(plansDir)) {
const files = await readdir(plansDir);
nestedPlans = files.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
nestedSummaries = files.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'),
};
}
// ─── Exported handlers ────────────────────────────────────────────────────
/**
@@ -509,11 +531,11 @@ export const roadmapAnalyze: QueryHandler = async (_args, projectDir, workstream
const dirMatch = dirs.find(d => phaseTokenMatches(d, normalized));
if (dirMatch) {
const phaseFiles = await readdir(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 = await countPhasePlansAndSummaries(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';