fix(#2028): phase.complete milestone-end out-of-order + workstream root-fallback guard
Two code-confirmed defects in `gsd-tools phase complete` (re-verified against next; the three severe corruption paths the issue filed are superseded by the ADR-1769 Transition Module migration + #2012, so this is the confirmed remainder). 1. Milestone-end mislabel (isLastPhase). The milestone-end determination only cleared isLastPhase when a HIGHER-numbered phase existed, so completing the numerically-highest phase out of order (e.g. Phase 10 before Phase 9) stamped STATE.md `Status: Milestone complete` while a lower phase was still outstanding. Added a lower-phase check: after the existing higher-phase scans, if any earlier phase in the current milestone has an unchecked roadmap checkbox (`[ ]`), isLastPhase becomes false AND next_phase/next_phase_name point at the LOWEST outstanding lower phase — so STATE.md advances to the real gap instead of parking on the just-completed phase. A completed phase always has `[x]` (phase.complete sets it), so all-lower-complete still reports milestone-end; heading-only roadmaps (no checkboxes) retain prior behavior. The checkbox regex mirrors the sibling phasePattern's anchoring (whitespace/bold + required `:`) so unrelated checklist lines mentioning "Phase N" don't match. 2. Workstream root-fallback (no guard). cmdPhaseComplete resolves every path via planningDir(cwd); with a `workstreams/` dir present but no active workstream and no --ws, that returns root `.planning`, so phase.complete wrote STATE.md/ ROADMAP.md (and the mislabel) into the shared root other workstreams read. Added the same #1912 fail-safe guard init.progress got: refuse (asking for `--ws`/active workstream) instead of silently writing root. Resolution itself was already wired globally (resolveActiveWorkstream: --ws > GSD_WORKSTREAM > pointer, set in bin/gsd-tools.cjs), so only the refusal guard was missing. The workstream-mode detection (`listAvailableWorkstreams`) is extracted into planning-workspace.cts as the single source of truth and consumed by BOTH init.progress and phase.complete, so the two fail-safe paths cannot drift. Tests (tests/phase.test.cjs, new #2028 describe): out-of-order completion becomes `Ready to plan` with is_last_phase=false, next_phase pointing at the outstanding phase and Current Phase advancing to it (not the completed phase); all-lower- complete still reports milestone-end; workstream-mode-no-active refuses with an `--ws` hint; `--ws` completes in the workstream leaving root untouched; flat mode unaffected. Fail-first verified locally via direct gsd-tools invocation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,22 @@ function planningRoot(cwd: string): string {
|
||||
return path.join(cwd, '.planning');
|
||||
}
|
||||
|
||||
// Sorted list of workstream directory names under `<root>/.planning/workstreams`,
|
||||
// or `[]` when the project is flat (no workstreams dir). Single source of truth
|
||||
// for the "workstream mode" detection shared by the #1912/#2028 fail-safe guards
|
||||
// (init.progress, phase.complete) so the two paths cannot drift.
|
||||
function listAvailableWorkstreams(cwd: string): string[] {
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(path.join(planningRoot(cwd), 'workstreams'), { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((e) => e.name)
|
||||
.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
interface PlanningPaths {
|
||||
planning: string;
|
||||
state: string;
|
||||
@@ -389,6 +405,7 @@ export = {
|
||||
createMemoryPointerAdapter,
|
||||
planningDir,
|
||||
planningRoot,
|
||||
listAvailableWorkstreams,
|
||||
planningPaths,
|
||||
withPlanningLock,
|
||||
getActiveWorkstream,
|
||||
|
||||
Reference in New Issue
Block a user