From 9175eb0aa37b474873c3f0bfe76b7b536095e096 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 10:03:06 -0400 Subject: [PATCH] fix: treat needs-remediation as terminal validation verdict to prevent hard loop (#832) (#848) --- src/resources/extensions/gsd/state.ts | 6 +++++- .../extensions/gsd/tests/validate-milestone.test.ts | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/state.ts b/src/resources/extensions/gsd/state.ts index 780e870c6..ef76fb648 100644 --- a/src/resources/extensions/gsd/state.ts +++ b/src/resources/extensions/gsd/state.ts @@ -62,7 +62,11 @@ export function isValidationTerminal(validationContent: string): boolean { if (!match) return false; const verdict = match[1].match(/verdict:\s*(\S+)/); if (!verdict) return false; - return verdict[1] === 'pass' || verdict[1] === 'needs-attention'; + // 'pass' and 'needs-attention' are always terminal. + // 'needs-remediation' is treated as terminal to prevent infinite loops + // when no remediation slices exist in the roadmap (#832). The validation + // report is preserved on disk for manual review. + return verdict[1] === 'pass' || verdict[1] === 'needs-attention' || verdict[1] === 'needs-remediation'; } // ─── State Derivation ────────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/tests/validate-milestone.test.ts b/src/resources/extensions/gsd/tests/validate-milestone.test.ts index d0e0f4c2d..7bdf687a3 100644 --- a/src/resources/extensions/gsd/tests/validate-milestone.test.ts +++ b/src/resources/extensions/gsd/tests/validate-milestone.test.ts @@ -97,9 +97,11 @@ test("isValidationTerminal returns true for verdict: needs-attention", () => { assert.equal(isValidationTerminal(content), true); }); -test("isValidationTerminal returns false for verdict: needs-remediation", () => { +test("isValidationTerminal returns true for verdict: needs-remediation (#832)", () => { + // needs-remediation is treated as terminal to prevent infinite loops + // when no remediation slices exist in the roadmap. const content = "---\nverdict: needs-remediation\nremediation_round: 0\n---\n\n# Validation"; - assert.equal(isValidationTerminal(content), false); + assert.equal(isValidationTerminal(content), true); }); test("isValidationTerminal returns false for missing frontmatter", () => { @@ -145,14 +147,16 @@ test("deriveState returns completing-milestone when VALIDATION exists with termi } }); -test("deriveState returns validating-milestone when VALIDATION exists with needs-remediation verdict", async () => { +test("deriveState treats needs-remediation as terminal — does not re-enter validating-milestone (#832)", async () => { const base = makeTmpBase(); try { writeRoadmap(base, "M001", ALL_DONE_ROADMAP); writeValidation(base, "M001", "---\nverdict: needs-remediation\nremediation_round: 0\n---\n\n# Validation\nNeeds fixes."); const state = await deriveState(base); - assert.equal(state.phase, "validating-milestone"); + // needs-remediation is now terminal — milestone needs a SUMMARY to be fully complete + // Without SUMMARY, it enters completing-milestone (not validating-milestone) + assert.notEqual(state.phase, "validating-milestone"); assert.equal(state.activeMilestone?.id, "M001"); } finally { cleanup(base);