fix: skip non-artifact UAT dispatch in auto-mode (#1277)
* fix(gsd): skip non-artifact UAT dispatch in auto-mode Non-artifact-driven UATs (human-experience, live-runtime, mixed) were dispatched only to write a "surfaced-for-human-review" verdict, which then blocked the verdict gate and killed auto-mode progression. Auto now only dispatches artifact-driven UATs it can actually execute. - checkNeedsRunUat returns null for non-artifact-driven UAT types - Remove pauseAfterDispatch flag (always artifact-driven now) - Strip human-review template path from run-uat prompt - Remove dead pause-after-UAT logic from auto.ts - Add test for non-artifact UAT skip + stale replay guard Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update buildRunUatPrompt call in direct dispatch after signature change Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a488de99bb
commit
84b556908c
6 changed files with 67 additions and 75 deletions
|
|
@ -182,15 +182,10 @@ export async function dispatchDirectPhase(
|
|||
ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
|
||||
return;
|
||||
}
|
||||
const uatContent = await loadFile(uatFile);
|
||||
if (!uatContent) {
|
||||
ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
|
||||
return;
|
||||
}
|
||||
const uatPath = relSliceFile(base, mid, sid, "UAT");
|
||||
unitType = "run-uat";
|
||||
unitId = `${mid}/${sid}`;
|
||||
prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
|
||||
prompt = await buildRunUatPrompt(mid, sid, uatPath, base);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@
|
|||
|
||||
import type { GSDState } from "./types.js";
|
||||
import type { GSDPreferences } from "./preferences.js";
|
||||
import type { UatType } from "./files.js";
|
||||
import { loadFile, extractUatType, loadActiveOverrides, parseRoadmap } from "./files.js";
|
||||
import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
|
||||
import {
|
||||
resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile,
|
||||
relSliceFile, buildMilestoneFileName,
|
||||
|
|
@ -39,7 +38,7 @@ import {
|
|||
// ─── Types ────────────────────────────────────────────────────────────────
|
||||
|
||||
export type DispatchAction =
|
||||
| { action: "dispatch"; unitType: string; unitId: string; prompt: string; pauseAfterDispatch?: boolean }
|
||||
| { action: "dispatch"; unitType: string; unitId: string; prompt: string }
|
||||
| { action: "stop"; reason: string; level: "info" | "warning" | "error" }
|
||||
| { action: "skip" };
|
||||
|
||||
|
|
@ -138,17 +137,14 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|||
match: async ({ state, mid, basePath, prefs }) => {
|
||||
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
||||
if (!needsRunUat) return null;
|
||||
const { sliceId, uatType } = needsRunUat;
|
||||
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
||||
const uatContent = await loadFile(uatFile);
|
||||
const { sliceId } = needsRunUat;
|
||||
return {
|
||||
action: "dispatch",
|
||||
unitType: "run-uat",
|
||||
unitId: `${mid}/${sliceId}`,
|
||||
prompt: await buildRunUatPrompt(
|
||||
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
||||
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), basePath,
|
||||
),
|
||||
pauseAfterDispatch: uatType !== "artifact-driven",
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -540,8 +540,11 @@ export async function checkNeedsRunUat(
|
|||
if (resultContent) return null;
|
||||
}
|
||||
|
||||
// Classify UAT type; unknown type → treat as human-experience (human review)
|
||||
// Classify UAT type; skip non-artifact-driven types — auto-mode can only
|
||||
// execute mechanical checks. Non-artifact UATs are tracked in the dashboard
|
||||
// but don't block auto-mode progression.
|
||||
const uatType = extractUatType(uatContent) ?? "human-experience";
|
||||
if (uatType !== "artifact-driven") return null;
|
||||
|
||||
return { sliceId: sid, uatType };
|
||||
}
|
||||
|
|
@ -1111,7 +1114,7 @@ export async function buildReplanSlicePrompt(
|
|||
}
|
||||
|
||||
export async function buildRunUatPrompt(
|
||||
mid: string, sliceId: string, uatPath: string, uatContent: string, base: string,
|
||||
mid: string, sliceId: string, uatPath: string, base: string,
|
||||
): Promise<string> {
|
||||
const inlined: string[] = [];
|
||||
inlined.push(await inlineFile(resolveSliceFile(base, mid, sliceId, "UAT"), uatPath, `${sliceId} UAT`));
|
||||
|
|
@ -1129,7 +1132,6 @@ export async function buildRunUatPrompt(
|
|||
const inlinedContext = `## 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-RESULT"));
|
||||
const uatType = extractUatType(uatContent) ?? "human-experience";
|
||||
|
||||
return loadPrompt("run-uat", {
|
||||
workingDirectory: base,
|
||||
|
|
@ -1137,7 +1139,6 @@ export async function buildRunUatPrompt(
|
|||
sliceId,
|
||||
uatPath,
|
||||
uatResultPath,
|
||||
uatType,
|
||||
inlinedContext,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1457,7 +1457,6 @@ async function dispatchNextUnit(
|
|||
unitType = dispatchResult.unitType;
|
||||
unitId = dispatchResult.unitId;
|
||||
prompt = dispatchResult.prompt;
|
||||
let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
||||
|
||||
// ── Pre-dispatch hooks ──
|
||||
const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
||||
|
|
@ -1712,13 +1711,6 @@ async function dispatchNextUnit(
|
|||
{ triggerTurn: true },
|
||||
);
|
||||
|
||||
if (pauseAfterUatDispatch) {
|
||||
ctx.ui.notify(
|
||||
"UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
|
||||
"info",
|
||||
);
|
||||
await pauseAuto(ctx, pi);
|
||||
}
|
||||
} finally {
|
||||
s.dispatching = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,8 @@ If a `GSD Skill Preferences` block is present in system context, use it to decid
|
|||
## UAT Instructions
|
||||
|
||||
**UAT file:** `{{uatPath}}`
|
||||
**UAT type:** `{{uatType}}`
|
||||
**Result file to write:** `{{uatResultPath}}`
|
||||
|
||||
### If UAT type is `artifact-driven`
|
||||
|
||||
You are the test runner. Execute every check defined in `{{uatPath}}` directly:
|
||||
|
||||
- Run shell commands with `bash`
|
||||
|
|
@ -46,7 +43,7 @@ Write `{{uatResultPath}}` with:
|
|||
```markdown
|
||||
---
|
||||
sliceId: {{sliceId}}
|
||||
uatType: {{uatType}}
|
||||
uatType: artifact-driven
|
||||
verdict: PASS | FAIL | PARTIAL
|
||||
date: <ISO 8601 timestamp>
|
||||
---
|
||||
|
|
@ -68,44 +65,6 @@ date: <ISO 8601 timestamp>
|
|||
<any additional context, errors encountered, or follow-up items>
|
||||
```
|
||||
|
||||
### If UAT type is NOT `artifact-driven` (type is `{{uatType}}`)
|
||||
|
||||
This UAT type requires human execution or live-runtime observation that you cannot perform mechanically. Your role is to surface it clearly for review.
|
||||
|
||||
Write `{{uatResultPath}}` with:
|
||||
|
||||
```markdown
|
||||
---
|
||||
sliceId: {{sliceId}}
|
||||
uatType: {{uatType}}
|
||||
verdict: surfaced-for-human-review
|
||||
date: <ISO 8601 timestamp>
|
||||
---
|
||||
|
||||
# UAT Result — {{sliceId}}
|
||||
|
||||
## UAT Type
|
||||
|
||||
`{{uatType}}` — requires human execution or live-runtime verification.
|
||||
|
||||
## Status
|
||||
|
||||
Surfaced for human review. Auto-mode will pause after this unit so the UAT can be performed manually.
|
||||
|
||||
## UAT File
|
||||
|
||||
See `{{uatPath}}` for the full UAT specification and acceptance criteria.
|
||||
|
||||
## Instructions for Human Reviewer
|
||||
|
||||
Review `{{uatPath}}`, perform the described UAT steps, then update this file with:
|
||||
- The actual verdict (PASS / FAIL / PARTIAL)
|
||||
- Results for each check
|
||||
- Date completed
|
||||
|
||||
Once updated, run `/gsd auto` to resume auto-mode.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**You MUST write `{{uatResultPath}}` before finishing.**
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
// (a)–(j) extractUatType classification (17 assertions from T01)
|
||||
// (k) run-uat prompt template loading and content integrity (8 assertions)
|
||||
// (l) dispatch precondition assertions via resolveSliceFile (4 assertions)
|
||||
// (m) stale replay guard: existing UAT-RESULT never re-dispatches (2 assertions)
|
||||
// (m) non-artifact UAT skip: human-experience UATs are not dispatched (1 assertion)
|
||||
// (n) stale replay guard: existing UAT-RESULT never re-dispatches (1 assertion)
|
||||
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
|
|
@ -254,8 +255,8 @@ async function main(): Promise<void> {
|
|||
'prompt contains artifact-driven execution language (artifact/execute/run)',
|
||||
);
|
||||
assertTrue(
|
||||
/surfaced for human review/i.test(promptResult ?? ''),
|
||||
'prompt contains "surfaced for human review" text for non-artifact-driven path',
|
||||
!/surfaced for human review/i.test(promptResult ?? ''),
|
||||
'prompt does not contain "surfaced for human review" (non-artifact UATs are skipped, not dispatched)',
|
||||
);
|
||||
|
||||
// ─── (l) dispatch precondition assertions via resolveSliceFile ────────────
|
||||
|
|
@ -310,8 +311,56 @@ async function main(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── (m) stale replay guard: existing UAT-RESULT never re-dispatches ─────
|
||||
console.log('\n── (m) stale replay guard');
|
||||
// ─── (m) non-artifact UATs are skipped (not dispatched) ─────────────────
|
||||
console.log('\n── (m) non-artifact UAT skip');
|
||||
|
||||
{
|
||||
const base = createFixtureBase();
|
||||
try {
|
||||
const roadmapDir = join(base, '.gsd', 'milestones', 'M001');
|
||||
mkdirSync(roadmapDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(roadmapDir, 'M001-ROADMAP.md'),
|
||||
[
|
||||
'# M001: Test roadmap',
|
||||
'',
|
||||
'## Slices',
|
||||
'',
|
||||
'- [x] **S01: First slice** `risk:low` `depends:[]`',
|
||||
'- [ ] **S02: Next slice** `risk:low` `depends:[S01]`',
|
||||
'',
|
||||
'## Boundary Map',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
// human-experience UAT — should not dispatch
|
||||
writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('human-experience'));
|
||||
|
||||
const state = {
|
||||
activeMilestone: { id: 'M001', title: 'Test roadmap' },
|
||||
activeSlice: { id: 'S02', title: 'Next slice' },
|
||||
activeTask: null,
|
||||
phase: 'planning',
|
||||
recentDecisions: [],
|
||||
blockers: [],
|
||||
nextAction: 'Plan S02',
|
||||
registry: [],
|
||||
} as const;
|
||||
|
||||
const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
|
||||
assertEq(
|
||||
result,
|
||||
null,
|
||||
'human-experience UAT is skipped — auto-mode only dispatches artifact-driven UATs',
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── (n) existing UAT-RESULT never re-dispatches ──────────────────────
|
||||
console.log('\n── (n) stale replay guard');
|
||||
|
||||
{
|
||||
const base = createFixtureBase();
|
||||
|
|
@ -334,7 +383,7 @@ async function main(): Promise<void> {
|
|||
);
|
||||
|
||||
writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('artifact-driven'));
|
||||
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: surfaced-for-human-review\n---\n');
|
||||
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: FAIL\n---\n');
|
||||
|
||||
const state = {
|
||||
activeMilestone: { id: 'M001', title: 'Test roadmap' },
|
||||
|
|
@ -351,7 +400,7 @@ async function main(): Promise<void> {
|
|||
assertEq(
|
||||
result,
|
||||
null,
|
||||
'existing UAT-RESULT with non-PASS verdict does not re-dispatch run-uat; verdict gate owns blocking',
|
||||
'existing UAT-RESULT with FAIL verdict does not re-dispatch; verdict gate owns blocking',
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue