gsd-pi 2.31.1-dev.d338017 → 2.31.1-dev.ffe48ad
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.
- package/dist/resources/extensions/gsd/auto-dispatch.ts +19 -19
- package/dist/resources/extensions/gsd/auto-prompts.ts +12 -5
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +0 -50
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +19 -19
- package/src/resources/extensions/gsd/auto-prompts.ts +12 -5
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +0 -50
|
@@ -104,6 +104,25 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
104
104
|
};
|
|
105
105
|
},
|
|
106
106
|
},
|
|
107
|
+
{
|
|
108
|
+
name: "run-uat (post-completion)",
|
|
109
|
+
match: async ({ state, mid, basePath, prefs }) => {
|
|
110
|
+
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
|
111
|
+
if (!needsRunUat) return null;
|
|
112
|
+
const { sliceId, uatType } = needsRunUat;
|
|
113
|
+
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
|
114
|
+
const uatContent = await loadFile(uatFile);
|
|
115
|
+
return {
|
|
116
|
+
action: "dispatch",
|
|
117
|
+
unitType: "run-uat",
|
|
118
|
+
unitId: `${mid}/${sliceId}`,
|
|
119
|
+
prompt: await buildRunUatPrompt(
|
|
120
|
+
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
|
121
|
+
),
|
|
122
|
+
pauseAfterDispatch: uatType !== "artifact-driven",
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
},
|
|
107
126
|
{
|
|
108
127
|
name: "uat-verdict-gate (non-PASS blocks progression)",
|
|
109
128
|
match: async ({ mid, basePath, prefs }) => {
|
|
@@ -133,25 +152,6 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
133
152
|
return null;
|
|
134
153
|
},
|
|
135
154
|
},
|
|
136
|
-
{
|
|
137
|
-
name: "run-uat (post-completion)",
|
|
138
|
-
match: async ({ state, mid, basePath, prefs }) => {
|
|
139
|
-
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
|
140
|
-
if (!needsRunUat) return null;
|
|
141
|
-
const { sliceId, uatType } = needsRunUat;
|
|
142
|
-
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
|
143
|
-
const uatContent = await loadFile(uatFile);
|
|
144
|
-
return {
|
|
145
|
-
action: "dispatch",
|
|
146
|
-
unitType: "run-uat",
|
|
147
|
-
unitId: `${mid}/${sliceId}`,
|
|
148
|
-
prompt: await buildRunUatPrompt(
|
|
149
|
-
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
|
150
|
-
),
|
|
151
|
-
pauseAfterDispatch: uatType !== "artifact-driven",
|
|
152
|
-
};
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
155
|
{
|
|
156
156
|
name: "reassess-roadmap (post-completion)",
|
|
157
157
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
@@ -530,14 +530,21 @@ export async function checkNeedsRunUat(
|
|
|
530
530
|
const uatContent = await loadFile(uatFile);
|
|
531
531
|
if (!uatContent) return null;
|
|
532
532
|
|
|
533
|
-
// If
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
// with a human-action message instead of replaying the same run-uat unit.
|
|
533
|
+
// If UAT result already exists with a PASS verdict, skip (idempotent).
|
|
534
|
+
// Non-PASS verdicts (FAIL, surfaced-for-human-review) should block slice
|
|
535
|
+
// progression — return the slice for re-evaluation (#1231).
|
|
537
536
|
const uatResultFile = resolveSliceFile(base, mid, sid, "UAT-RESULT");
|
|
538
537
|
if (uatResultFile) {
|
|
539
538
|
const resultContent = await loadFile(uatResultFile);
|
|
540
|
-
if (resultContent)
|
|
539
|
+
if (resultContent) {
|
|
540
|
+
const verdictMatch = resultContent.match(/verdict:\s*([\w-]+)/i);
|
|
541
|
+
const verdict = verdictMatch?.[1]?.toLowerCase();
|
|
542
|
+
if (verdict === "pass" || verdict === "passed") return null; // PASS — skip
|
|
543
|
+
// Non-PASS verdict exists — don't re-run UAT, but don't advance either.
|
|
544
|
+
// Return null here since the UAT already ran; the dispatch table's
|
|
545
|
+
// complete-slice rule should check the verdict before advancing.
|
|
546
|
+
// For now, returning the slice signals it still needs attention.
|
|
547
|
+
}
|
|
541
548
|
}
|
|
542
549
|
|
|
543
550
|
// Classify UAT type; unknown type → treat as human-experience (human review)
|
|
@@ -6,7 +6,6 @@
|
|
|
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)
|
|
10
9
|
|
|
11
10
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
11
|
import { join, dirname } from 'node:path';
|
|
@@ -15,7 +14,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
14
|
|
|
16
15
|
import { extractUatType } from '../files.ts';
|
|
17
16
|
import { resolveSliceFile } from '../paths.ts';
|
|
18
|
-
import { checkNeedsRunUat } from '../auto-prompts.ts';
|
|
19
17
|
import { createTestContext } from './test-helpers.ts';
|
|
20
18
|
|
|
21
19
|
// ─── Worktree-aware prompt loader ──────────────────────────────────────────
|
|
@@ -310,54 +308,6 @@ async function main(): Promise<void> {
|
|
|
310
308
|
}
|
|
311
309
|
}
|
|
312
310
|
|
|
313
|
-
// ─── (m) stale replay guard: existing UAT-RESULT never re-dispatches ─────
|
|
314
|
-
console.log('\n── (m) stale replay guard');
|
|
315
|
-
|
|
316
|
-
{
|
|
317
|
-
const base = createFixtureBase();
|
|
318
|
-
try {
|
|
319
|
-
const roadmapDir = join(base, '.gsd', 'milestones', 'M001');
|
|
320
|
-
mkdirSync(roadmapDir, { recursive: true });
|
|
321
|
-
writeFileSync(
|
|
322
|
-
join(roadmapDir, 'M001-ROADMAP.md'),
|
|
323
|
-
[
|
|
324
|
-
'# M001: Test roadmap',
|
|
325
|
-
'',
|
|
326
|
-
'## Slices',
|
|
327
|
-
'',
|
|
328
|
-
'- [x] **S01: First slice** `risk:low` `depends:[]`',
|
|
329
|
-
'- [ ] **S02: Next slice** `risk:low` `depends:[S01]`',
|
|
330
|
-
'',
|
|
331
|
-
'## Boundary Map',
|
|
332
|
-
'',
|
|
333
|
-
].join('\n'),
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('artifact-driven'));
|
|
337
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: surfaced-for-human-review\n---\n');
|
|
338
|
-
|
|
339
|
-
const state = {
|
|
340
|
-
activeMilestone: { id: 'M001', title: 'Test roadmap' },
|
|
341
|
-
activeSlice: { id: 'S02', title: 'Next slice' },
|
|
342
|
-
activeTask: null,
|
|
343
|
-
phase: 'planning',
|
|
344
|
-
recentDecisions: [],
|
|
345
|
-
blockers: [],
|
|
346
|
-
nextAction: 'Plan S02',
|
|
347
|
-
registry: [],
|
|
348
|
-
} as const;
|
|
349
|
-
|
|
350
|
-
const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
|
|
351
|
-
assertEq(
|
|
352
|
-
result,
|
|
353
|
-
null,
|
|
354
|
-
'existing UAT-RESULT with non-PASS verdict does not re-dispatch run-uat; verdict gate owns blocking',
|
|
355
|
-
);
|
|
356
|
-
} finally {
|
|
357
|
-
cleanup(base);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
311
|
report();
|
|
362
312
|
}
|
|
363
313
|
|
package/package.json
CHANGED
|
@@ -104,6 +104,25 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
104
104
|
};
|
|
105
105
|
},
|
|
106
106
|
},
|
|
107
|
+
{
|
|
108
|
+
name: "run-uat (post-completion)",
|
|
109
|
+
match: async ({ state, mid, basePath, prefs }) => {
|
|
110
|
+
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
|
111
|
+
if (!needsRunUat) return null;
|
|
112
|
+
const { sliceId, uatType } = needsRunUat;
|
|
113
|
+
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
|
114
|
+
const uatContent = await loadFile(uatFile);
|
|
115
|
+
return {
|
|
116
|
+
action: "dispatch",
|
|
117
|
+
unitType: "run-uat",
|
|
118
|
+
unitId: `${mid}/${sliceId}`,
|
|
119
|
+
prompt: await buildRunUatPrompt(
|
|
120
|
+
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
|
121
|
+
),
|
|
122
|
+
pauseAfterDispatch: uatType !== "artifact-driven",
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
},
|
|
107
126
|
{
|
|
108
127
|
name: "uat-verdict-gate (non-PASS blocks progression)",
|
|
109
128
|
match: async ({ mid, basePath, prefs }) => {
|
|
@@ -133,25 +152,6 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
133
152
|
return null;
|
|
134
153
|
},
|
|
135
154
|
},
|
|
136
|
-
{
|
|
137
|
-
name: "run-uat (post-completion)",
|
|
138
|
-
match: async ({ state, mid, basePath, prefs }) => {
|
|
139
|
-
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
|
140
|
-
if (!needsRunUat) return null;
|
|
141
|
-
const { sliceId, uatType } = needsRunUat;
|
|
142
|
-
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
|
143
|
-
const uatContent = await loadFile(uatFile);
|
|
144
|
-
return {
|
|
145
|
-
action: "dispatch",
|
|
146
|
-
unitType: "run-uat",
|
|
147
|
-
unitId: `${mid}/${sliceId}`,
|
|
148
|
-
prompt: await buildRunUatPrompt(
|
|
149
|
-
mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
|
|
150
|
-
),
|
|
151
|
-
pauseAfterDispatch: uatType !== "artifact-driven",
|
|
152
|
-
};
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
155
|
{
|
|
156
156
|
name: "reassess-roadmap (post-completion)",
|
|
157
157
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
@@ -530,14 +530,21 @@ export async function checkNeedsRunUat(
|
|
|
530
530
|
const uatContent = await loadFile(uatFile);
|
|
531
531
|
if (!uatContent) return null;
|
|
532
532
|
|
|
533
|
-
// If
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
// with a human-action message instead of replaying the same run-uat unit.
|
|
533
|
+
// If UAT result already exists with a PASS verdict, skip (idempotent).
|
|
534
|
+
// Non-PASS verdicts (FAIL, surfaced-for-human-review) should block slice
|
|
535
|
+
// progression — return the slice for re-evaluation (#1231).
|
|
537
536
|
const uatResultFile = resolveSliceFile(base, mid, sid, "UAT-RESULT");
|
|
538
537
|
if (uatResultFile) {
|
|
539
538
|
const resultContent = await loadFile(uatResultFile);
|
|
540
|
-
if (resultContent)
|
|
539
|
+
if (resultContent) {
|
|
540
|
+
const verdictMatch = resultContent.match(/verdict:\s*([\w-]+)/i);
|
|
541
|
+
const verdict = verdictMatch?.[1]?.toLowerCase();
|
|
542
|
+
if (verdict === "pass" || verdict === "passed") return null; // PASS — skip
|
|
543
|
+
// Non-PASS verdict exists — don't re-run UAT, but don't advance either.
|
|
544
|
+
// Return null here since the UAT already ran; the dispatch table's
|
|
545
|
+
// complete-slice rule should check the verdict before advancing.
|
|
546
|
+
// For now, returning the slice signals it still needs attention.
|
|
547
|
+
}
|
|
541
548
|
}
|
|
542
549
|
|
|
543
550
|
// Classify UAT type; unknown type → treat as human-experience (human review)
|
|
@@ -6,7 +6,6 @@
|
|
|
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)
|
|
10
9
|
|
|
11
10
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
11
|
import { join, dirname } from 'node:path';
|
|
@@ -15,7 +14,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
14
|
|
|
16
15
|
import { extractUatType } from '../files.ts';
|
|
17
16
|
import { resolveSliceFile } from '../paths.ts';
|
|
18
|
-
import { checkNeedsRunUat } from '../auto-prompts.ts';
|
|
19
17
|
import { createTestContext } from './test-helpers.ts';
|
|
20
18
|
|
|
21
19
|
// ─── Worktree-aware prompt loader ──────────────────────────────────────────
|
|
@@ -310,54 +308,6 @@ async function main(): Promise<void> {
|
|
|
310
308
|
}
|
|
311
309
|
}
|
|
312
310
|
|
|
313
|
-
// ─── (m) stale replay guard: existing UAT-RESULT never re-dispatches ─────
|
|
314
|
-
console.log('\n── (m) stale replay guard');
|
|
315
|
-
|
|
316
|
-
{
|
|
317
|
-
const base = createFixtureBase();
|
|
318
|
-
try {
|
|
319
|
-
const roadmapDir = join(base, '.gsd', 'milestones', 'M001');
|
|
320
|
-
mkdirSync(roadmapDir, { recursive: true });
|
|
321
|
-
writeFileSync(
|
|
322
|
-
join(roadmapDir, 'M001-ROADMAP.md'),
|
|
323
|
-
[
|
|
324
|
-
'# M001: Test roadmap',
|
|
325
|
-
'',
|
|
326
|
-
'## Slices',
|
|
327
|
-
'',
|
|
328
|
-
'- [x] **S01: First slice** `risk:low` `depends:[]`',
|
|
329
|
-
'- [ ] **S02: Next slice** `risk:low` `depends:[S01]`',
|
|
330
|
-
'',
|
|
331
|
-
'## Boundary Map',
|
|
332
|
-
'',
|
|
333
|
-
].join('\n'),
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('artifact-driven'));
|
|
337
|
-
writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: surfaced-for-human-review\n---\n');
|
|
338
|
-
|
|
339
|
-
const state = {
|
|
340
|
-
activeMilestone: { id: 'M001', title: 'Test roadmap' },
|
|
341
|
-
activeSlice: { id: 'S02', title: 'Next slice' },
|
|
342
|
-
activeTask: null,
|
|
343
|
-
phase: 'planning',
|
|
344
|
-
recentDecisions: [],
|
|
345
|
-
blockers: [],
|
|
346
|
-
nextAction: 'Plan S02',
|
|
347
|
-
registry: [],
|
|
348
|
-
} as const;
|
|
349
|
-
|
|
350
|
-
const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
|
|
351
|
-
assertEq(
|
|
352
|
-
result,
|
|
353
|
-
null,
|
|
354
|
-
'existing UAT-RESULT with non-PASS verdict does not re-dispatch run-uat; verdict gate owns blocking',
|
|
355
|
-
);
|
|
356
|
-
} finally {
|
|
357
|
-
cleanup(base);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
311
|
report();
|
|
362
312
|
}
|
|
363
313
|
|