From dfb18c6e6280576d42ce9415735a2138e02132fd Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 30 Mar 2026 16:45:57 -0400 Subject: [PATCH] fix: align run-uat artifact path to ASSESSMENT, preventing false stuck retries (#3053) The run-uat prompt instructs the agent to save results via gsd_summary_save with artifact_type: "ASSESSMENT", which writes S##-ASSESSMENT.md. But resolveExpectedArtifactPath and diagnoseExpectedArtifact expected S##-UAT.md, causing artifact verification to fail and auto-mode to retry indefinitely. Align all three contract points (prompt uatResultPath, artifact resolution, and diagnostic message) to use ASSESSMENT as the canonical artifact type. Closes #2873 Co-authored-by: Claude Opus 4.6 --- .../extensions/gsd/auto-artifact-paths.ts | 4 +- src/resources/extensions/gsd/auto-prompts.ts | 2 +- .../tests/integration/auto-recovery.test.ts | 46 ++++++++++++++++++- .../gsd/tests/integration/run-uat.test.ts | 2 +- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/auto-artifact-paths.ts b/src/resources/extensions/gsd/auto-artifact-paths.ts index df8b52ad2..6e54f5b07 100644 --- a/src/resources/extensions/gsd/auto-artifact-paths.ts +++ b/src/resources/extensions/gsd/auto-artifact-paths.ts @@ -56,7 +56,7 @@ export function resolveExpectedArtifactPath( } case "run-uat": { const dir = resolveSlicePath(base, mid, sid!); - return dir ? join(dir, buildSliceFileName(sid!, "UAT")) : null; + return dir ? join(dir, buildSliceFileName(sid!, "ASSESSMENT")) : null; } case "execute-task": { const dir = resolveSlicePath(base, mid, sid!); @@ -124,7 +124,7 @@ export function diagnoseExpectedArtifact( case "reassess-roadmap": return `${relSliceFile(base, mid, sid!, "ASSESSMENT")} (roadmap reassessment)`; case "run-uat": - return `${relSliceFile(base, mid, sid!, "UAT")} (UAT result)`; + return `${relSliceFile(base, mid, sid!, "ASSESSMENT")} (UAT assessment result)`; case "validate-milestone": return `${relMilestoneFile(base, mid, "VALIDATION")} (milestone validation report)`; case "complete-milestone": diff --git a/src/resources/extensions/gsd/auto-prompts.ts b/src/resources/extensions/gsd/auto-prompts.ts index 1ea0e3366..5b6e9de5b 100644 --- a/src/resources/extensions/gsd/auto-prompts.ts +++ b/src/resources/extensions/gsd/auto-prompts.ts @@ -1568,7 +1568,7 @@ export async function buildRunUatPrompt( const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`); - const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT")); + const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "ASSESSMENT")); const uatType = getUatType(uatContent); return loadPrompt("run-uat", { diff --git a/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts b/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts index 8aef15b20..77588ecc8 100644 --- a/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts @@ -111,7 +111,51 @@ test("resolveExpectedArtifactPath returns correct path for all slice-level types const uatResult = resolveExpectedArtifactPath("run-uat", "M001/S01", base); assert.ok(uatResult); - assert.ok(uatResult!.includes("UAT")); + assert.ok(uatResult!.includes("ASSESSMENT")); +}); + +// ─── run-uat artifact path contract (#2873) ────────────────────────────── + +test("resolveExpectedArtifactPath for run-uat returns ASSESSMENT path, not UAT (#2873)", (t) => { + // The run-uat prompt instructs the agent to call gsd_summary_save with + // artifact_type: "ASSESSMENT", which writes S##-ASSESSMENT.md. The artifact + // verification path must match — otherwise verification fails and auto-mode + // retries the unit in an infinite loop. + const base = makeTmpBase(); + t.after(() => cleanup(base)); + + const result = resolveExpectedArtifactPath("run-uat", "M001/S01", base); + assert.ok(result, "run-uat should resolve to a non-null artifact path"); + assert.ok( + result!.endsWith("S01-ASSESSMENT.md"), + `run-uat artifact path should end with S01-ASSESSMENT.md, got: ${result}`, + ); +}); + +test("diagnoseExpectedArtifact for run-uat references ASSESSMENT (#2873)", (t) => { + const base = makeTmpBase(); + t.after(() => cleanup(base)); + + const diag = diagnoseExpectedArtifact("run-uat", "M001/S01", base); + assert.ok(diag, "run-uat should have a diagnostic message"); + assert.ok( + diag!.includes("ASSESSMENT"), + `run-uat diagnostic should reference ASSESSMENT, got: ${diag}`, + ); +}); + +test("verifyExpectedArtifact passes for run-uat when ASSESSMENT file exists (#2873)", (t) => { + // Regression test: run-uat writes S##-ASSESSMENT.md via gsd_summary_save, + // but verification looked for S##-UAT.md, causing false stuck retries. + const base = makeTmpBase(); + t.after(() => cleanup(base)); + + // Write the ASSESSMENT file (what gsd_summary_save actually produces) + const assessPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-ASSESSMENT.md"); + writeFileSync(assessPath, "---\nverdict: PASS\n---\n# UAT Assessment\n"); + + const verified = verifyExpectedArtifact("run-uat", "M001/S01", base); + assert.ok(verified, "verifyExpectedArtifact should pass when ASSESSMENT file exists"); }); // ─── diagnoseExpectedArtifact ───────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/tests/integration/run-uat.test.ts b/src/resources/extensions/gsd/tests/integration/run-uat.test.ts index cf9d44f74..b4427751b 100644 --- a/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +++ b/src/resources/extensions/gsd/tests/integration/run-uat.test.ts @@ -171,7 +171,7 @@ test('(k) run-uat prompt template', () => { const milestoneId = 'M001'; const sliceId = 'S01'; const uatPath = '.gsd/milestones/M001/slices/S01/S01-UAT.md'; - const uatResultPath = '.gsd/milestones/M001/slices/S01/S01-UAT.md'; + const uatResultPath = '.gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md'; const uatType = 'live-runtime'; const inlinedContext = ''; let promptResult: string | undefined;