gsd-pi 2.31.2-dev.64d8832 → 2.31.2-dev.91f95cf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.ts +20 -26
  3. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  4. package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
  5. package/dist/resources/extensions/gsd/auto-post-unit.ts +27 -32
  6. package/dist/resources/extensions/gsd/auto-prompts.ts +38 -34
  7. package/dist/resources/extensions/gsd/auto-start.ts +4 -4
  8. package/dist/resources/extensions/gsd/auto.ts +11 -22
  9. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
  10. package/dist/resources/extensions/gsd/git-service.ts +9 -0
  11. package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  12. package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
  13. package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
  14. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
  15. package/dist/resources/extensions/gsd/quick.ts +3 -5
  16. package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  17. package/package.json +1 -1
  18. package/src/resources/extensions/gsd/auto-constants.ts +6 -0
  19. package/src/resources/extensions/gsd/auto-dashboard.ts +20 -26
  20. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  21. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
  22. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -32
  23. package/src/resources/extensions/gsd/auto-prompts.ts +38 -34
  24. package/src/resources/extensions/gsd/auto-start.ts +4 -4
  25. package/src/resources/extensions/gsd/auto.ts +11 -22
  26. package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
  27. package/src/resources/extensions/gsd/git-service.ts +9 -0
  28. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  29. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  30. package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
  31. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
  32. package/src/resources/extensions/gsd/quick.ts +3 -5
  33. package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
@@ -118,7 +118,7 @@ import {
118
118
  parseSliceBranch,
119
119
  setActiveMilestoneId,
120
120
  } from "./worktree.js";
121
- import { GitServiceImpl, type TaskCommitContext } from "./git-service.js";
121
+ import { createGitService, type TaskCommitContext } from "./git-service.js";
122
122
  import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
123
123
  import { formatGitError } from "./git-self-heal.js";
124
124
  import {
@@ -204,8 +204,7 @@ import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerifi
204
204
  // ─────────────────────────────────────────────────────────────────────────────
205
205
  const s = new AutoSession();
206
206
 
207
- /** Throttle STATE.md rebuilds at most once per 30 seconds */
208
- const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
207
+ import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
209
208
 
210
209
  export function shouldUseWorktreeIsolation(): boolean {
211
210
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
@@ -462,7 +461,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
462
461
  try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: e instanceof Error ? e.message : String(e) }); }
463
462
  teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true });
464
463
  s.basePath = s.originalBasePath;
465
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
464
+ s.gitService = createGitService(s.basePath);
466
465
  ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
467
466
  } catch (err) {
468
467
  ctx?.ui.notify(
@@ -626,12 +625,12 @@ export async function startAuto(
626
625
  if (existingWtPath) {
627
626
  const wtPath = enterAutoWorktree(s.originalBasePath, s.currentMilestoneId);
628
627
  s.basePath = wtPath;
629
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
628
+ s.gitService = createGitService(s.basePath);
630
629
  ctx.ui.notify(`Re-entered auto-worktree at ${wtPath}`, "info");
631
630
  } else {
632
631
  const wtPath = createAutoWorktree(s.originalBasePath, s.currentMilestoneId);
633
632
  s.basePath = wtPath;
634
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
633
+ s.gitService = createGitService(s.basePath);
635
634
  ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
636
635
  }
637
636
  } catch (err) {
@@ -1124,7 +1123,7 @@ async function dispatchNextUnit(
1124
1123
  }
1125
1124
 
1126
1125
  s.basePath = s.originalBasePath;
1127
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1126
+ s.gitService = createGitService(s.basePath);
1128
1127
  invalidateAllCaches();
1129
1128
 
1130
1129
  state = await deriveState(s.basePath);
@@ -1136,7 +1135,7 @@ async function dispatchNextUnit(
1136
1135
  try {
1137
1136
  const wtPath = createAutoWorktree(s.basePath, mid);
1138
1137
  s.basePath = wtPath;
1139
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1138
+ s.gitService = createGitService(s.basePath);
1140
1139
  ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
1141
1140
  } catch (err) {
1142
1141
  ctx.ui.notify(
@@ -1176,7 +1175,7 @@ async function dispatchNextUnit(
1176
1175
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1177
1176
  const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
1178
1177
  s.basePath = s.originalBasePath;
1179
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1178
+ s.gitService = createGitService(s.basePath);
1180
1179
  ctx.ui.notify(
1181
1180
  `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1182
1181
  "info",
@@ -1201,7 +1200,7 @@ async function dispatchNextUnit(
1201
1200
  if (roadmapPath) {
1202
1201
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1203
1202
  const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
1204
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1203
+ s.gitService = createGitService(s.basePath);
1205
1204
  ctx.ui.notify(
1206
1205
  `Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1207
1206
  "info",
@@ -1279,7 +1278,7 @@ async function dispatchNextUnit(
1279
1278
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1280
1279
  const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
1281
1280
  s.basePath = s.originalBasePath;
1282
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1281
+ s.gitService = createGitService(s.basePath);
1283
1282
  ctx.ui.notify(
1284
1283
  `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1285
1284
  "info",
@@ -1303,7 +1302,7 @@ async function dispatchNextUnit(
1303
1302
  if (roadmapPath) {
1304
1303
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1305
1304
  const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
1306
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1305
+ s.gitService = createGitService(s.basePath);
1307
1306
  ctx.ui.notify(
1308
1307
  `Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1309
1308
  "info",
@@ -1457,7 +1456,6 @@ async function dispatchNextUnit(
1457
1456
  unitType = dispatchResult.unitType;
1458
1457
  unitId = dispatchResult.unitId;
1459
1458
  prompt = dispatchResult.prompt;
1460
- let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
1461
1459
 
1462
1460
  // ── Pre-dispatch hooks ──
1463
1461
  const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
@@ -1712,13 +1710,6 @@ async function dispatchNextUnit(
1712
1710
  { triggerTurn: true },
1713
1711
  );
1714
1712
 
1715
- if (pauseAfterUatDispatch) {
1716
- ctx.ui.notify(
1717
- "UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
1718
- "info",
1719
- );
1720
- await pauseAuto(ctx, pi);
1721
- }
1722
1713
  } finally {
1723
1714
  s.dispatching = false;
1724
1715
  }
@@ -1874,8 +1865,6 @@ export async function dispatchHookUnit(
1874
1865
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1875
1866
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
1876
1867
 
1877
- console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
1878
- console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
1879
1868
  pi.sendMessage(
1880
1869
  { customType: "gsd-auto", content: hookPrompt, display: true },
1881
1870
  { triggerTurn: true },
@@ -19,8 +19,7 @@ import {
19
19
  } from "./workflow-templates.js";
20
20
  import { loadPrompt } from "./prompt-loader.js";
21
21
  import { gsdRoot } from "./paths.js";
22
- import { GitServiceImpl, runGit } from "./git-service.js";
23
- import { loadEffectiveGSDPreferences } from "./preferences.js";
22
+ import { createGitService, runGit } from "./git-service.js";
24
23
  import { isAutoActive, isAutoPaused } from "./auto.js";
25
24
 
26
25
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -423,9 +422,8 @@ export async function handleStart(
423
422
 
424
423
  // ─── Create git branch (unless isolation: none) ─────────────────────────
425
424
 
426
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
427
- const git = new GitServiceImpl(basePath, gitPrefs);
428
- const skipBranch = gitPrefs.isolation === "none";
425
+ const git = createGitService(basePath);
426
+ const skipBranch = git.prefs.isolation === "none";
429
427
  const slug = slugify(description || templateId);
430
428
  const branchName = `gsd/${templateId}/${slug}`;
431
429
  let branchCreated = false;
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { gsdRoot } from "./paths.js";
15
15
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
16
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
16
17
 
17
18
  import {
18
19
  detectWorktreeName,
@@ -541,6 +542,14 @@ export class GitServiceImpl {
541
542
 
542
543
  }
543
544
 
545
+ // ─── Factory ───────────────────────────────────────────────────────────────
546
+
547
+ /** Create a GitServiceImpl with the current effective git preferences. */
548
+ export function createGitService(basePath: string): GitServiceImpl {
549
+ const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
550
+ return new GitServiceImpl(basePath, gitPrefs);
551
+ }
552
+
544
553
  // ─── Commit Type Inference ─────────────────────────────────────────────────
545
554
 
546
555
  /**
@@ -23,13 +23,6 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
23
23
  import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.js";
24
24
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
25
25
 
26
- // ─── Commit Instruction Helper (local copy — avoids circular dep) ───────────
27
-
28
- /** Build commit instruction for queue prompts. .gsd/ is managed externally and always gitignored. */
29
- function buildDocsCommitInstruction(_message: string): string {
30
- return "Do not commit planning artifacts — .gsd/ is managed externally.";
31
- }
32
-
33
26
  // ─── Queue Entry Point ──────────────────────────────────────────────────────
34
27
 
35
28
  /**
@@ -207,7 +200,7 @@ export async function showQueueAdd(
207
200
  preamble,
208
201
  existingMilestonesContext: existingContext,
209
202
  inlinedTemplates: queueInlinedTemplates,
210
- commitInstruction: buildDocsCommitInstruction("docs: queue <milestone list>"),
203
+ commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
211
204
  });
212
205
 
213
206
  pi.sendMessage(
@@ -86,6 +86,14 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
86
86
  "context_selection",
87
87
  ]);
88
88
 
89
+ /** Canonical list of all dispatch unit types. */
90
+ export const KNOWN_UNIT_TYPES = [
91
+ "research-milestone", "plan-milestone", "research-slice", "plan-slice",
92
+ "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
93
+ "run-uat", "complete-milestone",
94
+ ] as const;
95
+ export type UnitType = (typeof KNOWN_UNIT_TYPES)[number];
96
+
89
97
  export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
90
98
 
91
99
  export interface GSDSkillRule {
@@ -14,6 +14,7 @@ import { normalizeStringArray } from "../shared/mod.js";
14
14
 
15
15
  import {
16
16
  KNOWN_PREFERENCE_KEYS,
17
+ KNOWN_UNIT_TYPES,
17
18
  SKILL_ACTIONS,
18
19
  type WorkflowMode,
19
20
  type GSDPreferences,
@@ -239,11 +240,7 @@ export function validatePreferences(preferences: GSDPreferences): {
239
240
  if (preferences.post_unit_hooks && Array.isArray(preferences.post_unit_hooks)) {
240
241
  const validHooks: PostUnitHookConfig[] = [];
241
242
  const seenNames = new Set<string>();
242
- const knownUnitTypes = new Set([
243
- "research-milestone", "plan-milestone", "research-slice", "plan-slice",
244
- "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
245
- "run-uat", "complete-milestone",
246
- ]);
243
+ const knownUnitTypes = new Set<string>(KNOWN_UNIT_TYPES);
247
244
  for (const hook of preferences.post_unit_hooks) {
248
245
  if (!hook || typeof hook !== "object") {
249
246
  errors.push("post_unit_hooks entry must be an object");
@@ -305,11 +302,7 @@ export function validatePreferences(preferences: GSDPreferences): {
305
302
  if (preferences.pre_dispatch_hooks && Array.isArray(preferences.pre_dispatch_hooks)) {
306
303
  const validPreHooks: PreDispatchHookConfig[] = [];
307
304
  const seenPreNames = new Set<string>();
308
- const knownUnitTypes = new Set([
309
- "research-milestone", "plan-milestone", "research-slice", "plan-slice",
310
- "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
311
- "run-uat", "complete-milestone",
312
- ]);
305
+ const knownUnitTypes = new Set<string>(KNOWN_UNIT_TYPES);
313
306
  const validActions = new Set(["modify", "skip", "replace"]);
314
307
  for (const hook of preferences.pre_dispatch_hooks) {
315
308
  if (!hook || typeof hook !== "object") {
@@ -17,11 +17,8 @@ If a `GSD Skill Preferences` block is present in system context, use it to decid
17
17
  ## UAT Instructions
18
18
 
19
19
  **UAT file:** `{{uatPath}}`
20
- **UAT type:** `{{uatType}}`
21
20
  **Result file to write:** `{{uatResultPath}}`
22
21
 
23
- ### If UAT type is `artifact-driven`
24
-
25
22
  You are the test runner. Execute every check defined in `{{uatPath}}` directly:
26
23
 
27
24
  - Run shell commands with `bash`
@@ -46,7 +43,7 @@ Write `{{uatResultPath}}` with:
46
43
  ```markdown
47
44
  ---
48
45
  sliceId: {{sliceId}}
49
- uatType: {{uatType}}
46
+ uatType: artifact-driven
50
47
  verdict: PASS | FAIL | PARTIAL
51
48
  date: <ISO 8601 timestamp>
52
49
  ---
@@ -68,44 +65,6 @@ date: <ISO 8601 timestamp>
68
65
  <any additional context, errors encountered, or follow-up items>
69
66
  ```
70
67
 
71
- ### If UAT type is NOT `artifact-driven` (type is `{{uatType}}`)
72
-
73
- This UAT type requires human execution or live-runtime observation that you cannot perform mechanically. Your role is to surface it clearly for review.
74
-
75
- Write `{{uatResultPath}}` with:
76
-
77
- ```markdown
78
- ---
79
- sliceId: {{sliceId}}
80
- uatType: {{uatType}}
81
- verdict: surfaced-for-human-review
82
- date: <ISO 8601 timestamp>
83
- ---
84
-
85
- # UAT Result — {{sliceId}}
86
-
87
- ## UAT Type
88
-
89
- `{{uatType}}` — requires human execution or live-runtime verification.
90
-
91
- ## Status
92
-
93
- Surfaced for human review. Auto-mode will pause after this unit so the UAT can be performed manually.
94
-
95
- ## UAT File
96
-
97
- See `{{uatPath}}` for the full UAT specification and acceptance criteria.
98
-
99
- ## Instructions for Human Reviewer
100
-
101
- Review `{{uatPath}}`, perform the described UAT steps, then update this file with:
102
- - The actual verdict (PASS / FAIL / PARTIAL)
103
- - Results for each check
104
- - Date completed
105
-
106
- Once updated, run `/gsd auto` to resume auto-mode.
107
- ```
108
-
109
68
  ---
110
69
 
111
70
  **You MUST write `{{uatResultPath}}` before finishing.**
@@ -14,8 +14,7 @@ import { existsSync, mkdirSync, readdirSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { loadPrompt } from "./prompt-loader.js";
16
16
  import { gsdRoot } from "./paths.js";
17
- import { GitServiceImpl, runGit } from "./git-service.js";
18
- import { loadEffectiveGSDPreferences } from "./preferences.js";
17
+ import { createGitService, runGit } from "./git-service.js";
19
18
 
20
19
  // ─── Quick Task Helpers ───────────────────────────────────────────────────────
21
20
 
@@ -103,10 +102,9 @@ export async function handleQuick(
103
102
  const date = new Date().toISOString().split("T")[0];
104
103
 
105
104
  // Create git branch for the quick task (unless isolation: none)
106
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
107
- const git = new GitServiceImpl(basePath, gitPrefs);
105
+ const git = createGitService(basePath);
108
106
  const branchName = `gsd/quick/${taskNum}-${slug}`;
109
- const skipBranch = gitPrefs.isolation === "none";
107
+ const skipBranch = git.prefs.isolation === "none";
110
108
 
111
109
  let branchCreated = false;
112
110
  if (!skipBranch) {
@@ -6,7 +6,8 @@
6
6
  // (a)–(j) extractUatType classification (17 assertions from T01)
7
7
  // (k) run-uat prompt template loading and content integrity (8 assertions)
8
8
  // (l) dispatch precondition assertions via resolveSliceFile (4 assertions)
9
- // (m) stale replay guard: existing UAT-RESULT never re-dispatches (2 assertions)
9
+ // (m) non-artifact UAT skip: human-experience UATs are not dispatched (1 assertion)
10
+ // (n) stale replay guard: existing UAT-RESULT never re-dispatches (1 assertion)
10
11
 
11
12
  import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
13
  import { join, dirname } from 'node:path';
@@ -254,8 +255,8 @@ async function main(): Promise<void> {
254
255
  'prompt contains artifact-driven execution language (artifact/execute/run)',
255
256
  );
256
257
  assertTrue(
257
- /surfaced for human review/i.test(promptResult ?? ''),
258
- 'prompt contains "surfaced for human review" text for non-artifact-driven path',
258
+ !/surfaced for human review/i.test(promptResult ?? ''),
259
+ 'prompt does not contain "surfaced for human review" (non-artifact UATs are skipped, not dispatched)',
259
260
  );
260
261
 
261
262
  // ─── (l) dispatch precondition assertions via resolveSliceFile ────────────
@@ -310,8 +311,56 @@ async function main(): Promise<void> {
310
311
  }
311
312
  }
312
313
 
313
- // ─── (m) stale replay guard: existing UAT-RESULT never re-dispatches ─────
314
- console.log('\n── (m) stale replay guard');
314
+ // ─── (m) non-artifact UATs are skipped (not dispatched) ─────────────────
315
+ console.log('\n── (m) non-artifact UAT skip');
316
+
317
+ {
318
+ const base = createFixtureBase();
319
+ try {
320
+ const roadmapDir = join(base, '.gsd', 'milestones', 'M001');
321
+ mkdirSync(roadmapDir, { recursive: true });
322
+ writeFileSync(
323
+ join(roadmapDir, 'M001-ROADMAP.md'),
324
+ [
325
+ '# M001: Test roadmap',
326
+ '',
327
+ '## Slices',
328
+ '',
329
+ '- [x] **S01: First slice** `risk:low` `depends:[]`',
330
+ '- [ ] **S02: Next slice** `risk:low` `depends:[S01]`',
331
+ '',
332
+ '## Boundary Map',
333
+ '',
334
+ ].join('\n'),
335
+ );
336
+
337
+ // human-experience UAT — should not dispatch
338
+ writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('human-experience'));
339
+
340
+ const state = {
341
+ activeMilestone: { id: 'M001', title: 'Test roadmap' },
342
+ activeSlice: { id: 'S02', title: 'Next slice' },
343
+ activeTask: null,
344
+ phase: 'planning',
345
+ recentDecisions: [],
346
+ blockers: [],
347
+ nextAction: 'Plan S02',
348
+ registry: [],
349
+ } as const;
350
+
351
+ const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
352
+ assertEq(
353
+ result,
354
+ null,
355
+ 'human-experience UAT is skipped — auto-mode only dispatches artifact-driven UATs',
356
+ );
357
+ } finally {
358
+ cleanup(base);
359
+ }
360
+ }
361
+
362
+ // ─── (n) existing UAT-RESULT never re-dispatches ──────────────────────
363
+ console.log('\n── (n) stale replay guard');
315
364
 
316
365
  {
317
366
  const base = createFixtureBase();
@@ -334,7 +383,7 @@ async function main(): Promise<void> {
334
383
  );
335
384
 
336
385
  writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('artifact-driven'));
337
- writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: surfaced-for-human-review\n---\n');
386
+ writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: FAIL\n---\n');
338
387
 
339
388
  const state = {
340
389
  activeMilestone: { id: 'M001', title: 'Test roadmap' },
@@ -351,7 +400,7 @@ async function main(): Promise<void> {
351
400
  assertEq(
352
401
  result,
353
402
  null,
354
- 'existing UAT-RESULT with non-PASS verdict does not re-dispatch run-uat; verdict gate owns blocking',
403
+ 'existing UAT-RESULT with FAIL verdict does not re-dispatch; verdict gate owns blocking',
355
404
  );
356
405
  } finally {
357
406
  cleanup(base);