gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

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 (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -1,691 +0,0 @@
1
- /**
2
- * auto-dispatch-loop.test.ts — End-to-end regression tests for the
3
- * auto-mode dispatch loop: deriveState() → resolveDispatch()
4
- *
5
- * Exercises the full state-machine chain WITHOUT an LLM. Each test
6
- * creates a .gsd/ filesystem fixture, derives state, runs the dispatch
7
- * table, and verifies the correct unit type/id is produced.
8
- *
9
- * Regression coverage for:
10
- * #1270 Replaying completed run-uat units
11
- * #1277 Non-artifact UATs dispatched, blocking progression
12
- * #1241 Slice progression gated on file existence, not verdict content
13
- * #909 Missing task plan files → infinite plan-slice loop
14
- * #807 Prose slice headers not parsed → "No slice eligible" block
15
- * #1248 Prose header regex only matched H2 with colon separator
16
- * #1289 Crash recovery false-positive on own PID
17
- * #1217 (orphaned processes — tested via post-unit, not dispatch)
18
- *
19
- * Pattern: create fixture → deriveState → resolveDispatch → assert
20
- */
21
-
22
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
23
- import { join } from 'node:path';
24
- import { tmpdir } from 'node:os';
25
-
26
- import { deriveState, invalidateStateCache } from '../state.ts';
27
- import { resolveDispatch, type DispatchContext } from '../auto-dispatch.ts';
28
- import { parseRoadmapSlices } from '../roadmap-slices.ts';
29
- import { checkNeedsRunUat } from '../auto-prompts.ts';
30
- import { checkIdempotency, type IdempotencyContext } from '../auto-idempotency.ts';
31
- import { invalidateAllCaches } from '../cache.ts';
32
- import { AutoSession } from '../auto/session.ts';
33
- import { createTestContext } from './test-helpers.ts';
34
-
35
- const { assertEq, assertTrue, assertMatch, report } = createTestContext();
36
-
37
- // ═══════════════════════════════════════════════════════════════════════════
38
- // Fixture Helpers
39
- // ═══════════════════════════════════════════════════════════════════════════
40
-
41
- function createBase(): string {
42
- const base = mkdtempSync(join(tmpdir(), 'gsd-dispatch-loop-'));
43
- mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
44
- return base;
45
- }
46
-
47
- function cleanup(base: string): void {
48
- rmSync(base, { recursive: true, force: true });
49
- }
50
-
51
- function writeMilestoneFile(base: string, mid: string, suffix: string, content: string): void {
52
- const dir = join(base, '.gsd', 'milestones', mid);
53
- mkdirSync(dir, { recursive: true });
54
- writeFileSync(join(dir, `${mid}-${suffix}.md`), content);
55
- }
56
-
57
- function writeSliceFile(base: string, mid: string, sid: string, suffix: string, content: string): void {
58
- const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
59
- mkdirSync(dir, { recursive: true });
60
- writeFileSync(join(dir, `${sid}-${suffix}.md`), content);
61
- }
62
-
63
- function writeTaskFile(base: string, mid: string, sid: string, tid: string, suffix: string, content: string): void {
64
- const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid, 'tasks');
65
- mkdirSync(dir, { recursive: true });
66
- writeFileSync(join(dir, `${tid}-${suffix}.md`), content);
67
- }
68
-
69
- /** Standard machine-readable roadmap with checkbox slices */
70
- function standardRoadmap(mid: string, title: string, slices: Array<{ id: string; title: string; done: boolean; risk?: string; depends?: string[] }>): string {
71
- const lines = [
72
- `# ${mid}: ${title}`,
73
- '',
74
- '## Slices',
75
- '',
76
- ];
77
- for (const s of slices) {
78
- const check = s.done ? 'x' : ' ';
79
- const risk = s.risk ?? 'low';
80
- const deps = s.depends ?? [];
81
- lines.push(`- [${check}] **${s.id}: ${s.title}** \`risk:${risk}\` \`depends:[${deps.join(',')}]\``);
82
- }
83
- lines.push('', '## Boundary Map', '');
84
- return lines.join('\n');
85
- }
86
-
87
- /** Standard slice plan with tasks */
88
- function standardPlan(sid: string, title: string, tasks: Array<{ id: string; title: string; done: boolean; est?: string }>): string {
89
- const lines = [
90
- `# ${sid}: ${title}`,
91
- '',
92
- '## Tasks',
93
- '',
94
- ];
95
- for (const t of tasks) {
96
- const check = t.done ? 'x' : ' ';
97
- const est = t.est ?? '1h';
98
- lines.push(`- [${check}] **${t.id}: ${t.title}** \`est:${est}\``);
99
- }
100
- return lines.join('\n');
101
- }
102
-
103
- function freshState(): void {
104
- invalidateAllCaches();
105
- invalidateStateCache();
106
- }
107
-
108
- async function dispatchFor(base: string): Promise<ReturnType<typeof resolveDispatch>> {
109
- freshState();
110
- const state = await deriveState(base);
111
- const mid = state.activeMilestone?.id;
112
- if (!mid) return { action: 'stop', reason: 'No active milestone', level: 'info' };
113
- const midTitle = state.activeMilestone?.title ?? mid;
114
- const ctx: DispatchContext = { basePath: base, mid, midTitle, state, prefs: undefined };
115
- return resolveDispatch(ctx);
116
- }
117
-
118
- // ═══════════════════════════════════════════════════════════════════════════
119
- // Tests
120
- // ═══════════════════════════════════════════════════════════════════════════
121
-
122
- async function main(): Promise<void> {
123
-
124
- // ─── 1. Basic state derivation: pre-planning → plan-milestone ─────────
125
- console.log('\n=== 1. pre-planning with context → plan-milestone (or research) ===');
126
- {
127
- const base = createBase();
128
- try {
129
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001: Test Project\n\nBuild a thing.\n');
130
- const result = await dispatchFor(base);
131
- assertTrue(
132
- result.action === 'dispatch',
133
- 'pre-planning with context dispatches a unit',
134
- );
135
- if (result.action === 'dispatch') {
136
- assertTrue(
137
- result.unitType === 'research-milestone' || result.unitType === 'plan-milestone',
138
- `dispatches research-milestone or plan-milestone, got ${result.unitType}`,
139
- );
140
- assertEq(result.unitId, 'M001', 'unit ID is M001');
141
- }
142
- } finally {
143
- cleanup(base);
144
- }
145
- }
146
-
147
- // ─── 2. Planning → plan-slice ─────────────────────────────────────────
148
- console.log('\n=== 2. has roadmap, no slice plan → plan-slice ===');
149
- {
150
- const base = createBase();
151
- try {
152
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001: Test\n\nDesc.\n');
153
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
154
- { id: 'S01', title: 'First Slice', done: false },
155
- { id: 'S02', title: 'Second Slice', done: false, depends: ['S01'] },
156
- ]));
157
- const result = await dispatchFor(base);
158
- assertTrue(result.action === 'dispatch', 'planning phase dispatches');
159
- if (result.action === 'dispatch') {
160
- assertTrue(
161
- result.unitType === 'plan-slice' || result.unitType === 'research-slice',
162
- `dispatches plan-slice or research-slice, got ${result.unitType}`,
163
- );
164
- assertMatch(result.unitId, /M001\/S01/, 'targets S01');
165
- }
166
- } finally {
167
- cleanup(base);
168
- }
169
- }
170
-
171
- // ─── 3. Executing → execute-task ──────────────────────────────────────
172
- console.log('\n=== 3. has plan with incomplete task → execute-task ===');
173
- {
174
- const base = createBase();
175
- try {
176
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
177
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
178
- { id: 'S01', title: 'First Slice', done: false },
179
- ]));
180
- writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'First Slice', [
181
- { id: 'T01', title: 'First Task', done: false },
182
- { id: 'T02', title: 'Second Task', done: false },
183
- ]));
184
- writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01: First Task\n\nDo the thing.\n');
185
- writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02: Second Task\n\nDo more.\n');
186
-
187
- const result = await dispatchFor(base);
188
- assertTrue(result.action === 'dispatch', 'executing phase dispatches');
189
- if (result.action === 'dispatch') {
190
- assertEq(result.unitType, 'execute-task', 'dispatches execute-task');
191
- assertEq(result.unitId, 'M001/S01/T01', 'targets T01');
192
- }
193
- } finally {
194
- cleanup(base);
195
- }
196
- }
197
-
198
- // ─── 4. All tasks done → complete-slice (summarizing) ─────────────────
199
- console.log('\n=== 4. all tasks done → summarizing → complete-slice ===');
200
- {
201
- const base = createBase();
202
- try {
203
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
204
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
205
- { id: 'S01', title: 'First Slice', done: false },
206
- ]));
207
- writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'First Slice', [
208
- { id: 'T01', title: 'First Task', done: true },
209
- { id: 'T02', title: 'Second Task', done: true },
210
- ]));
211
- writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDone.');
212
- writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02\nDone.');
213
-
214
- const result = await dispatchFor(base);
215
- assertTrue(result.action === 'dispatch', 'summarizing phase dispatches');
216
- if (result.action === 'dispatch') {
217
- assertEq(result.unitType, 'complete-slice', 'dispatches complete-slice');
218
- assertEq(result.unitId, 'M001/S01', 'targets S01');
219
- }
220
- } finally {
221
- cleanup(base);
222
- }
223
- }
224
-
225
- // ─── 5. Regression #909: Missing task plan files → plan-slice ─────────
226
- console.log('\n=== 5. #909: tasks in plan but empty tasks/ dir → plan-slice (not stuck loop) ===');
227
- {
228
- const base = createBase();
229
- try {
230
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
231
- // Add milestone research so research-slice doesn't fire first
232
- writeMilestoneFile(base, 'M001', 'RESEARCH', '# Research\n\nDone.\n');
233
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
234
- { id: 'S01', title: 'First Slice', done: false },
235
- ]));
236
- // Also write slice research so research-slice is skipped
237
- writeSliceFile(base, 'M001', 'S01', 'RESEARCH', '# Slice Research\n\nDone.\n');
238
- // Plan references tasks but tasks/ dir has no files
239
- writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'First Slice', [
240
- { id: 'T01', title: 'First Task', done: false },
241
- ]));
242
- // Create empty tasks directory (no task plan files)
243
- mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks'), { recursive: true });
244
-
245
- freshState();
246
- const state = await deriveState(base);
247
- // Should fall back to planning phase since tasks dir is empty
248
- assertEq(state.phase, 'planning', '#909: empty tasks dir → planning phase (not executing)');
249
-
250
- const result = await dispatchFor(base);
251
- assertTrue(result.action === 'dispatch', '#909: dispatches');
252
- if (result.action === 'dispatch') {
253
- assertEq(result.unitType, 'plan-slice', '#909: dispatches plan-slice to regenerate task plans');
254
- }
255
- } finally {
256
- cleanup(base);
257
- }
258
- }
259
-
260
- // ─── 6. Regression #1277: Non-artifact UAT not dispatched ─────────────
261
- console.log('\n=== 6. #1277: human-experience UAT → null (skip, not dispatch) ===');
262
- {
263
- const base = createBase();
264
- try {
265
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
266
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
267
- { id: 'S01', title: 'Done Slice', done: true },
268
- { id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
269
- ]));
270
- writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: human-experience\n');
271
-
272
- const state = {
273
- activeMilestone: { id: 'M001', title: 'Test' },
274
- activeSlice: { id: 'S02', title: 'Next Slice' },
275
- activeTask: null,
276
- phase: 'planning',
277
- recentDecisions: [],
278
- blockers: [],
279
- nextAction: 'Plan S02',
280
- registry: [],
281
- };
282
-
283
- freshState();
284
- const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
285
- assertEq(result, null, '#1277: human-experience UAT returns null (not dispatched)');
286
- } finally {
287
- cleanup(base);
288
- }
289
- }
290
-
291
- // ─── 7. Regression #1277: artifact-driven UAT without result → dispatch ──
292
- console.log('\n=== 7. artifact-driven UAT without result → dispatch ===');
293
- {
294
- const base = createBase();
295
- try {
296
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
297
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
298
- { id: 'S01', title: 'Done Slice', done: true },
299
- { id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
300
- ]));
301
- writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n');
302
- // No UAT-RESULT file
303
-
304
- const state = {
305
- activeMilestone: { id: 'M001', title: 'Test' },
306
- activeSlice: { id: 'S02', title: 'Next Slice' },
307
- activeTask: null,
308
- phase: 'planning',
309
- recentDecisions: [],
310
- blockers: [],
311
- nextAction: 'Plan S02',
312
- registry: [],
313
- };
314
-
315
- freshState();
316
- const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
317
- assertTrue(result !== null, 'artifact-driven UAT without result → dispatch (not null)');
318
- if (result) {
319
- assertEq(result.sliceId, 'S01', 'targets S01');
320
- }
321
- } finally {
322
- cleanup(base);
323
- }
324
- }
325
-
326
- // ─── 8. Regression #1270: Existing UAT-RESULT never re-dispatches ─────
327
- console.log('\n=== 8. #1270: UAT-RESULT exists → no re-dispatch (any verdict) ===');
328
- {
329
- const base = createBase();
330
- try {
331
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
332
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
333
- { id: 'S01', title: 'Done Slice', done: true },
334
- { id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
335
- ]));
336
- writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n');
337
- writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: FAIL\n---\nFailed.\n');
338
-
339
- const state = {
340
- activeMilestone: { id: 'M001', title: 'Test' },
341
- activeSlice: { id: 'S02', title: 'Next Slice' },
342
- activeTask: null,
343
- phase: 'planning',
344
- recentDecisions: [],
345
- blockers: [],
346
- nextAction: 'Plan S02',
347
- registry: [],
348
- };
349
-
350
- freshState();
351
- const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
352
- assertEq(result, null, '#1270: existing UAT-RESULT with FAIL → null (no re-dispatch)');
353
- } finally {
354
- cleanup(base);
355
- }
356
- }
357
-
358
- // ─── 9. Regression #1241: UAT verdict gate blocks non-PASS ────────────
359
- console.log('\n=== 9. #1241: UAT verdict gate blocks progression on non-PASS verdict ===');
360
- {
361
- const base = createBase();
362
- try {
363
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
364
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
365
- { id: 'S01', title: 'Done Slice', done: true },
366
- { id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
367
- ]));
368
- writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Done Slice', [
369
- { id: 'T01', title: 'Task', done: true },
370
- ]));
371
- writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n');
372
- writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: FAIL\n---\nFailed some check.\n');
373
-
374
- freshState();
375
- const state = await deriveState(base);
376
- const ctx: DispatchContext = {
377
- basePath: base,
378
- mid: 'M001',
379
- midTitle: 'Test',
380
- state,
381
- prefs: { uat_dispatch: true } as any,
382
- };
383
- const result = await resolveDispatch(ctx);
384
- // The uat-verdict-gate rule should stop progression
385
- assertEq(result.action, 'stop', '#1241: non-PASS verdict → stop (blocks progression)');
386
- } finally {
387
- cleanup(base);
388
- }
389
- }
390
-
391
- // ─── 10. #1241: UAT verdict PASS allows progression ───────────────────
392
- console.log('\n=== 10. UAT verdict PASS → allows progression ===');
393
- {
394
- const base = createBase();
395
- try {
396
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
397
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
398
- { id: 'S01', title: 'Done Slice', done: true },
399
- { id: 'S02', title: 'Next Slice', done: false, depends: ['S01'] },
400
- ]));
401
- writeSliceFile(base, 'M001', 'S01', 'UAT', '# UAT\n\n## UAT Type\n\n- UAT mode: artifact-driven\n');
402
- writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '---\nverdict: PASS\n---\nAll good.\n');
403
-
404
- freshState();
405
- const state = await deriveState(base);
406
- const ctx: DispatchContext = {
407
- basePath: base,
408
- mid: 'M001',
409
- midTitle: 'Test',
410
- state,
411
- prefs: { uat_dispatch: true } as any,
412
- };
413
- const result = await resolveDispatch(ctx);
414
- // PASS verdict should NOT block — dispatch should continue to plan-slice for S02
415
- assertTrue(result.action !== 'stop' || !('reason' in result && result.reason.includes('verdict')), 'PASS verdict does not block progression');
416
- } finally {
417
- cleanup(base);
418
- }
419
- }
420
-
421
- // ─── 11. Complete state derivation: all slices done → completing ───────
422
- console.log('\n=== 11. all slices done, no validation → validating-milestone ===');
423
- {
424
- const base = createBase();
425
- try {
426
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
427
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
428
- { id: 'S01', title: 'First Slice', done: true },
429
- ]));
430
-
431
- freshState();
432
- const state = await deriveState(base);
433
- assertEq(state.phase, 'validating-milestone', 'all slices done → validating-milestone');
434
- } finally {
435
- cleanup(base);
436
- }
437
- }
438
-
439
- // ─── 12. Complete milestone → complete phase ──────────────────────────
440
- console.log('\n=== 12. validated + summarized milestone → complete ===');
441
- {
442
- const base = createBase();
443
- try {
444
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
445
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
446
- { id: 'S01', title: 'First Slice', done: true },
447
- ]));
448
- writeMilestoneFile(base, 'M001', 'VALIDATION', '---\nverdict: pass\nremediation_round: 0\n---\n# Validation\nAll good.\n');
449
- writeMilestoneFile(base, 'M001', 'SUMMARY', '---\nstatus: complete\n---\n# Summary\nDone.\n');
450
-
451
- freshState();
452
- const state = await deriveState(base);
453
- assertEq(state.phase, 'complete', 'validated+summarized → complete');
454
- } finally {
455
- cleanup(base);
456
- }
457
- }
458
-
459
- // ─── 13. Multi-milestone: M001 complete, M002 active ─────────────────
460
- console.log('\n=== 13. multi-milestone: M001 complete, M002 becomes active ===');
461
- {
462
- const base = createBase();
463
- try {
464
- // M001 — complete
465
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDone.\n');
466
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'First', [
467
- { id: 'S01', title: 'Slice', done: true },
468
- ]));
469
- writeMilestoneFile(base, 'M001', 'VALIDATION', '---\nverdict: pass\nremediation_round: 0\n---\n');
470
- writeMilestoneFile(base, 'M001', 'SUMMARY', '---\nstatus: complete\n---\n# Summary\n');
471
-
472
- // M002 — active
473
- writeMilestoneFile(base, 'M002', 'CONTEXT', '# M002\n\nNext.\n');
474
-
475
- freshState();
476
- const state = await deriveState(base);
477
- assertEq(state.activeMilestone?.id, 'M002', 'M002 is the active milestone');
478
- assertEq(state.phase, 'pre-planning', 'M002 is in pre-planning');
479
- assertEq(state.registry.length, 2, 'registry has 2 milestones');
480
- assertEq(state.registry[0].status, 'complete', 'M001 is complete');
481
- assertEq(state.registry[1].status, 'active', 'M002 is active');
482
- } finally {
483
- cleanup(base);
484
- }
485
- }
486
-
487
- // ─── 14. Dependency blocking: S02 depends on S01 ─────────────────────
488
- console.log('\n=== 14. slice dependency: S02 blocked until S01 done ===');
489
- {
490
- const base = createBase();
491
- try {
492
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
493
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
494
- { id: 'S01', title: 'First', done: false },
495
- { id: 'S02', title: 'Second', done: false, depends: ['S01'] },
496
- ]));
497
-
498
- freshState();
499
- const state = await deriveState(base);
500
- // Active slice should be S01, not S02
501
- assertEq(state.activeSlice?.id, 'S01', 'S01 is the active slice (S02 is dep-blocked)');
502
- } finally {
503
- cleanup(base);
504
- }
505
- }
506
-
507
- // ─── 15. Blocker detection: task with blocker_discovered → replan ─────
508
- console.log('\n=== 15. blocker_discovered in task summary → replanning-slice ===');
509
- {
510
- const base = createBase();
511
- try {
512
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
513
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
514
- { id: 'S01', title: 'Slice', done: false },
515
- ]));
516
- writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Slice', [
517
- { id: 'T01', title: 'Task One', done: true },
518
- { id: 'T02', title: 'Task Two', done: false },
519
- ]));
520
- writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDo thing.');
521
- writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02\nDo other thing.');
522
- writeTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY', '---\nblocker_discovered: true\n---\n# T01 Summary\nFound a blocker.');
523
-
524
- freshState();
525
- const state = await deriveState(base);
526
- assertEq(state.phase, 'replanning-slice', 'blocker_discovered → replanning-slice');
527
- } finally {
528
- cleanup(base);
529
- }
530
- }
531
-
532
- // ─── 16. Blocker + REPLAN exists → loop protection, resume executing ──
533
- console.log('\n=== 16. blocker_discovered + REPLAN exists → loop protection (executing) ===');
534
- {
535
- const base = createBase();
536
- try {
537
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
538
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
539
- { id: 'S01', title: 'Slice', done: false },
540
- ]));
541
- writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Slice', [
542
- { id: 'T01', title: 'Task One', done: true },
543
- { id: 'T02', title: 'Task Two', done: false },
544
- ]));
545
- writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDo thing.');
546
- writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02\nDo other thing.');
547
- writeTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY', '---\nblocker_discovered: true\n---\n# T01\nBlocker.');
548
- // REPLAN.md exists → loop protection
549
- writeSliceFile(base, 'M001', 'S01', 'REPLAN', '# Replan\nAlready replanned.\n');
550
-
551
- freshState();
552
- const state = await deriveState(base);
553
- assertEq(state.phase, 'executing', 'blocker + REPLAN exists → executing (loop protection)');
554
- } finally {
555
- cleanup(base);
556
- }
557
- }
558
-
559
- // ─── 17. Needs-discussion phase ───────────────────────────────────────
560
- console.log('\n=== 17. CONTEXT-DRAFT without CONTEXT → needs-discussion ===');
561
- {
562
- const base = createBase();
563
- try {
564
- const mDir = join(base, '.gsd', 'milestones', 'M001');
565
- mkdirSync(mDir, { recursive: true });
566
- writeFileSync(join(mDir, 'M001-CONTEXT-DRAFT.md'), '# Draft\n\nSome rough ideas.\n');
567
-
568
- freshState();
569
- const state = await deriveState(base);
570
- assertEq(state.phase, 'needs-discussion', 'CONTEXT-DRAFT without CONTEXT → needs-discussion');
571
- } finally {
572
- cleanup(base);
573
- }
574
- }
575
-
576
- // ─── 18. Idempotency: completed key → skip ───────────────────────────
577
- console.log('\n=== 18. idempotency: completed key → skip ===');
578
- {
579
- const base = createBase();
580
- try {
581
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n');
582
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
583
- { id: 'S01', title: 'Slice', done: false },
584
- ]));
585
- // Task must be marked [x] in the plan for verifyExpectedArtifact to return true
586
- writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Slice', [
587
- { id: 'T01', title: 'Task', done: true },
588
- { id: 'T02', title: 'Next Task', done: false },
589
- ]));
590
- writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDo thing.');
591
- writeTaskFile(base, 'M001', 'S01', 'T02', 'PLAN', '# T02\nNext.');
592
- // Write SUMMARY as the expected artifact for execute-task
593
- writeTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY', '---\nstatus: done\n---\n# T01 Summary\nDone.');
594
-
595
- // Force cache clearance so verifyExpectedArtifact finds the file
596
- freshState();
597
-
598
- const session = new AutoSession();
599
- session.basePath = base;
600
- session.completedKeySet.add('execute-task/M001/S01/T01');
601
-
602
- const notifications: string[] = [];
603
- const result = checkIdempotency({
604
- s: session,
605
- unitType: 'execute-task',
606
- unitId: 'M001/S01/T01',
607
- basePath: base,
608
- notify: (msg) => notifications.push(msg),
609
- });
610
-
611
- assertEq(result.action, 'skip', 'completed key → skip');
612
- assertTrue('reason' in result && result.reason === 'completed', 'reason is completed');
613
- } finally {
614
- cleanup(base);
615
- }
616
- }
617
-
618
- // ─── 19. Idempotency: stale key (artifact missing) → rerun ───────────
619
- console.log('\n=== 19. idempotency: stale key (no artifact) → rerun ===');
620
- {
621
- const base = createBase();
622
- try {
623
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n');
624
- writeMilestoneFile(base, 'M001', 'ROADMAP', standardRoadmap('M001', 'Test', [
625
- { id: 'S01', title: 'Slice', done: false },
626
- ]));
627
- writeSliceFile(base, 'M001', 'S01', 'PLAN', standardPlan('S01', 'Slice', [
628
- { id: 'T01', title: 'Task', done: false },
629
- ]));
630
- writeTaskFile(base, 'M001', 'S01', 'T01', 'PLAN', '# T01\nDo thing.');
631
- // NO summary file — artifact missing
632
-
633
- const session = new AutoSession();
634
- session.basePath = base;
635
- session.completedKeySet.add('execute-task/M001/S01/T01');
636
-
637
- const notifications: string[] = [];
638
- const result = checkIdempotency({
639
- s: session,
640
- unitType: 'execute-task',
641
- unitId: 'M001/S01/T01',
642
- basePath: base,
643
- notify: (msg) => notifications.push(msg),
644
- });
645
-
646
- assertEq(result.action, 'rerun', 'stale key (no artifact) → rerun');
647
- assertTrue(!session.completedKeySet.has('execute-task/M001/S01/T01'), 'stale key removed from set');
648
- } finally {
649
- cleanup(base);
650
- }
651
- }
652
-
653
- // ─── 20. Idempotency: consecutive skip loop → evict ──────────────────
654
- console.log('\n=== 20. idempotency: consecutive skip loop → evict ===');
655
- {
656
- const base = createBase();
657
- try {
658
- writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n');
659
- writeTaskFile(base, 'M001', 'S01', 'T01', 'SUMMARY', '---\nstatus: done\n---\n# Done');
660
-
661
- const session = new AutoSession();
662
- session.basePath = base;
663
- session.completedKeySet.add('execute-task/M001/S01/T01');
664
- // Pre-fill skip count to just below threshold
665
- session.unitConsecutiveSkips.set('execute-task/M001/S01/T01', 3);
666
-
667
- const notifications: string[] = [];
668
- const result = checkIdempotency({
669
- s: session,
670
- unitType: 'execute-task',
671
- unitId: 'M001/S01/T01',
672
- basePath: base,
673
- notify: (msg) => notifications.push(msg),
674
- });
675
-
676
- assertEq(result.action, 'skip', 'exceeds consecutive skip threshold → skip with eviction');
677
- assertTrue('reason' in result && result.reason === 'evicted', 'reason is evicted');
678
- assertTrue(!session.completedKeySet.has('execute-task/M001/S01/T01'), 'key evicted from completed set');
679
- assertTrue(session.recentlyEvictedKeys.has('execute-task/M001/S01/T01'), 'key tracked in evicted set');
680
- } finally {
681
- cleanup(base);
682
- }
683
- }
684
-
685
- report();
686
- }
687
-
688
- main().catch((error) => {
689
- console.error(error);
690
- process.exit(1);
691
- });