gsd-pi 2.31.2-dev.64d8832 → 2.31.2-dev.c8d7e03
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/cli.js +5 -5
- package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +20 -26
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/dist/resources/extensions/gsd/auto-post-unit.ts +27 -32
- package/dist/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/dist/resources/extensions/gsd/auto-start.ts +4 -4
- package/dist/resources/extensions/gsd/auto.ts +54 -33
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/dist/resources/extensions/gsd/git-service.ts +9 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
- package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/dist/resources/extensions/gsd/quick.ts +3 -5
- package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/dist/worktree-cli.d.ts +42 -6
- package/dist/worktree-cli.js +88 -48
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-constants.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +20 -26
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -32
- package/src/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/src/resources/extensions/gsd/auto-start.ts +4 -4
- package/src/resources/extensions/gsd/auto.ts +54 -33
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/src/resources/extensions/gsd/git-service.ts +9 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/src/resources/extensions/gsd/quick.ts +3 -5
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto-reentrancy-guard.test.ts — Tests for the unconditional reentrancy guard.
|
|
3
|
+
*
|
|
4
|
+
* Regression for #1272: auto-mode stuck-loop where gap watchdog or
|
|
5
|
+
* pendingAgentEndRetry could enter dispatchNextUnit concurrently during
|
|
6
|
+
* recursive skip chains because the reentrancy guard was bypassed when
|
|
7
|
+
* skipDepth > 0.
|
|
8
|
+
*
|
|
9
|
+
* The fix makes the guard unconditional (`if (s.dispatching)` without
|
|
10
|
+
* `&& s.skipDepth === 0`), and defers recursive re-dispatch via
|
|
11
|
+
* setImmediate/setTimeout so s.dispatching is released first.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
_getDispatching,
|
|
16
|
+
_setDispatching,
|
|
17
|
+
_getSkipDepth,
|
|
18
|
+
_setSkipDepth,
|
|
19
|
+
} from "../auto.ts";
|
|
20
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
21
|
+
|
|
22
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
23
|
+
|
|
24
|
+
async function main(): Promise<void> {
|
|
25
|
+
// ─── Test-only accessors work ───────────────────────────────────────────
|
|
26
|
+
console.log("\n=== reentrancy guard: test accessors round-trip ===");
|
|
27
|
+
{
|
|
28
|
+
_setDispatching(false);
|
|
29
|
+
assertEq(_getDispatching(), false, "dispatching starts false");
|
|
30
|
+
|
|
31
|
+
_setDispatching(true);
|
|
32
|
+
assertEq(_getDispatching(), true, "dispatching set to true");
|
|
33
|
+
|
|
34
|
+
_setDispatching(false);
|
|
35
|
+
assertEq(_getDispatching(), false, "dispatching reset to false");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── skipDepth accessors ────────────────────────────────────────────────
|
|
39
|
+
console.log("\n=== reentrancy guard: skipDepth accessors round-trip ===");
|
|
40
|
+
{
|
|
41
|
+
_setSkipDepth(0);
|
|
42
|
+
assertEq(_getSkipDepth(), 0, "skipDepth starts at 0");
|
|
43
|
+
|
|
44
|
+
_setSkipDepth(3);
|
|
45
|
+
assertEq(_getSkipDepth(), 3, "skipDepth set to 3");
|
|
46
|
+
|
|
47
|
+
_setSkipDepth(0);
|
|
48
|
+
assertEq(_getSkipDepth(), 0, "skipDepth reset to 0");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Guard blocks even when skipDepth > 0 (#1272 regression) ───────────
|
|
52
|
+
console.log("\n=== reentrancy guard: blocks when dispatching=true regardless of skipDepth ===");
|
|
53
|
+
{
|
|
54
|
+
// Simulate the scenario from #1272: dispatching=true + skipDepth>0
|
|
55
|
+
// The old guard (`if (s.dispatching && s.skipDepth === 0)`) would allow
|
|
56
|
+
// concurrent entry when skipDepth > 0. The fix makes the check
|
|
57
|
+
// unconditional on skipDepth.
|
|
58
|
+
_setDispatching(true);
|
|
59
|
+
_setSkipDepth(2);
|
|
60
|
+
|
|
61
|
+
// Verify dispatching is true — guard should block regardless of skipDepth
|
|
62
|
+
assertTrue(
|
|
63
|
+
_getDispatching() === true,
|
|
64
|
+
"dispatching flag is true during skip chain"
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// The actual reentrancy guard in dispatchNextUnit checks:
|
|
68
|
+
// if (s.dispatching) { return; }
|
|
69
|
+
// We verify the state that would trigger the guard:
|
|
70
|
+
const wouldBlock = _getDispatching(); // unconditional check
|
|
71
|
+
const wouldBlockOld = _getDispatching() && _getSkipDepth() === 0; // old check
|
|
72
|
+
|
|
73
|
+
assertTrue(wouldBlock === true, "new guard blocks when dispatching=true, skipDepth=2");
|
|
74
|
+
assertTrue(wouldBlockOld === false, "old guard WOULD NOT block when dispatching=true, skipDepth=2 (the bug)");
|
|
75
|
+
|
|
76
|
+
// Clean up
|
|
77
|
+
_setDispatching(false);
|
|
78
|
+
_setSkipDepth(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Guard allows entry when dispatching=false ──────────────────────────
|
|
82
|
+
console.log("\n=== reentrancy guard: allows entry when dispatching=false ===");
|
|
83
|
+
{
|
|
84
|
+
_setDispatching(false);
|
|
85
|
+
_setSkipDepth(0);
|
|
86
|
+
assertTrue(!_getDispatching(), "guard allows entry when dispatching=false, skipDepth=0");
|
|
87
|
+
|
|
88
|
+
_setDispatching(false);
|
|
89
|
+
_setSkipDepth(3);
|
|
90
|
+
assertTrue(!_getDispatching(), "guard allows entry when dispatching=false, skipDepth=3");
|
|
91
|
+
|
|
92
|
+
_setSkipDepth(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── skipDepth does not affect guard decision (the fix) ─────────────────
|
|
96
|
+
console.log("\n=== reentrancy guard: skipDepth is irrelevant to guard decision ===");
|
|
97
|
+
{
|
|
98
|
+
for (const depth of [0, 1, 2, 5]) {
|
|
99
|
+
_setDispatching(true);
|
|
100
|
+
_setSkipDepth(depth);
|
|
101
|
+
assertTrue(
|
|
102
|
+
_getDispatching() === true,
|
|
103
|
+
`guard blocks at skipDepth=${depth} when dispatching=true`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const depth of [0, 1, 2, 5]) {
|
|
108
|
+
_setDispatching(false);
|
|
109
|
+
_setSkipDepth(depth);
|
|
110
|
+
assertTrue(
|
|
111
|
+
_getDispatching() === false,
|
|
112
|
+
`guard allows at skipDepth=${depth} when dispatching=false`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Clean up
|
|
117
|
+
_setDispatching(false);
|
|
118
|
+
_setSkipDepth(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
report();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main().catch((err) => {
|
|
125
|
+
console.error(err);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
|
@@ -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)
|
|
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
|
-
|
|
258
|
-
'prompt
|
|
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)
|
|
314
|
-
console.log('\n── (m)
|
|
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:
|
|
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
|
|
403
|
+
'existing UAT-RESULT with FAIL verdict does not re-dispatch; verdict gate owns blocking',
|
|
355
404
|
);
|
|
356
405
|
} finally {
|
|
357
406
|
cleanup(base);
|