fix(#2674): align initProgress with initManager ROADMAP [x] precedence (#2681)

initProgress computed phase status purely from disk (PLAN/SUMMARY counts),
consulting the ROADMAP `- [x] Phase N` checkbox only for phases with no
directory. initManager, by contrast, applied an explicit override: a
ROADMAP `[x]` forces status to `complete` regardless of disk state.

Result: a phase with a stub directory (no SUMMARY.md) and a ticked
ROADMAP checkbox reported `complete` from /gsd-manager and `pending`
from /gsd-progress — same data, different answer.

Apply ROADMAP-[x]-wins as the unified policy inside initProgress, mirroring
initManager's override. A user who typed `- [x] Phase 3` has made an
explicit assertion; a leftover stub dir is the weaker signal.

Adds sdk/src/query/init-progress-precedence.test.ts covering six cases
(stub dir + [x], full dir + [x], full dir + [ ], stub dir + [ ],
ROADMAP-only + [x], and completed_count parity). Pre-fix: cases 1 and 6
failed. Post-fix: all six pass. No existing tests were modified.

Closes #2674
This commit is contained in:
Tom Boucher
2026-04-24 20:20:11 -04:00
committed by GitHub
parent 303fd26b45
commit b1278f6fc3
2 changed files with 189 additions and 1 deletions

View File

@@ -262,11 +262,22 @@ export const initProgress: QueryHandler = async (_args, projectDir, _workstream)
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
const status =
let status =
summaries.length >= plans.length && plans.length > 0 ? 'complete' :
plans.length > 0 ? 'in_progress' :
hasResearch ? 'researched' : 'pending';
// #2674: align with initManager — a ROADMAP `- [x] Phase N` checkbox
// wins over disk state. A stub phase dir with no SUMMARY is leftover
// scaffolding; the user's explicit [x] is the authoritative signal.
const strippedNum = phaseNumber.replace(/^0+/, '') || '0';
const roadmapComplete =
checkboxStates.get(phaseNumber) === true ||
checkboxStates.get(strippedNum) === true;
if (roadmapComplete && status !== 'complete') {
status = 'complete';
}
const phaseInfo: Record<string, unknown> = {
number: phaseNumber,
name: phaseName,