gsd-pi 2.32.0-dev.f3d5d53 → 2.33.0-dev.69bff0f

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 (66) hide show
  1. package/README.md +13 -18
  2. package/dist/resources/extensions/gsd/auto-dashboard.ts +3 -1
  3. package/dist/resources/extensions/gsd/auto-dispatch.ts +40 -12
  4. package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
  5. package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
  6. package/dist/resources/extensions/gsd/auto-post-unit.ts +5 -5
  7. package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
  8. package/dist/resources/extensions/gsd/auto-start.ts +2 -1
  9. package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  10. package/dist/resources/extensions/gsd/auto-supervisor.ts +10 -5
  11. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  12. package/dist/resources/extensions/gsd/auto-verification.ts +4 -5
  13. package/dist/resources/extensions/gsd/auto-worktree.ts +135 -1
  14. package/dist/resources/extensions/gsd/auto.ts +89 -164
  15. package/dist/resources/extensions/gsd/commands.ts +14 -2
  16. package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
  17. package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
  18. package/dist/resources/extensions/gsd/metrics.ts +3 -3
  19. package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  20. package/dist/resources/extensions/gsd/session-lock.ts +80 -16
  21. package/dist/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
  22. package/dist/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +691 -0
  23. package/dist/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +317 -0
  24. package/dist/resources/extensions/gsd/tests/loop-regression.test.ts +877 -0
  25. package/dist/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +358 -0
  26. package/dist/resources/extensions/gsd/tests/session-lock-regression.test.ts +216 -0
  27. package/dist/resources/extensions/gsd/tests/session-lock.test.ts +119 -0
  28. package/dist/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +206 -0
  29. package/dist/resources/extensions/gsd/undo.ts +5 -7
  30. package/dist/resources/extensions/gsd/unit-id.ts +14 -0
  31. package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
  32. package/package.json +3 -2
  33. package/packages/pi-coding-agent/package.json +1 -1
  34. package/pkg/package.json +1 -1
  35. package/src/resources/extensions/gsd/auto-dashboard.ts +3 -1
  36. package/src/resources/extensions/gsd/auto-dispatch.ts +40 -12
  37. package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
  38. package/src/resources/extensions/gsd/auto-observability.ts +2 -4
  39. package/src/resources/extensions/gsd/auto-post-unit.ts +5 -5
  40. package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
  41. package/src/resources/extensions/gsd/auto-start.ts +2 -1
  42. package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  43. package/src/resources/extensions/gsd/auto-supervisor.ts +10 -5
  44. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  45. package/src/resources/extensions/gsd/auto-verification.ts +4 -5
  46. package/src/resources/extensions/gsd/auto-worktree.ts +135 -1
  47. package/src/resources/extensions/gsd/auto.ts +89 -164
  48. package/src/resources/extensions/gsd/commands.ts +14 -2
  49. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
  50. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
  51. package/src/resources/extensions/gsd/metrics.ts +3 -3
  52. package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  53. package/src/resources/extensions/gsd/session-lock.ts +80 -16
  54. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +14 -11
  55. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +691 -0
  56. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +317 -0
  57. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +877 -0
  58. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +358 -0
  59. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +216 -0
  60. package/src/resources/extensions/gsd/tests/session-lock.test.ts +119 -0
  61. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +206 -0
  62. package/src/resources/extensions/gsd/undo.ts +5 -7
  63. package/src/resources/extensions/gsd/unit-id.ts +14 -0
  64. package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
  65. package/dist/resources/extensions/mcporter/extension-manifest.json +0 -12
  66. package/src/resources/extensions/mcporter/extension-manifest.json +0 -12
@@ -0,0 +1,317 @@
1
+ /**
2
+ * cache-staleness-regression.test.ts — Regression tests for stale cache bugs.
3
+ *
4
+ * The GSD parser caches are critical for performance but have caused multiple
5
+ * production bugs when not invalidated at the right time.
6
+ *
7
+ * Regression coverage for:
8
+ * #1249 Stale caches in discuss loop → slice appears "not discussed"
9
+ * #1240 Stale caches after milestone creation → "No roadmap yet"
10
+ * #1236 Same root cause as #1240
11
+ *
12
+ * Pattern: derive state → write file → invalidate cache → derive again → verify update
13
+ */
14
+
15
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { tmpdir } from 'node:os';
18
+
19
+ import { deriveState, invalidateStateCache } from '../state.ts';
20
+ import { invalidateAllCaches } from '../cache.ts';
21
+ import { createTestContext } from './test-helpers.ts';
22
+
23
+ const { assertEq, assertTrue, report } = createTestContext();
24
+
25
+ function createBase(): string {
26
+ const base = mkdtempSync(join(tmpdir(), 'gsd-cache-stale-'));
27
+ mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
28
+ return base;
29
+ }
30
+
31
+ function cleanup(base: string): void {
32
+ rmSync(base, { recursive: true, force: true });
33
+ }
34
+
35
+ function writeMilestoneFile(base: string, mid: string, suffix: string, content: string): void {
36
+ const dir = join(base, '.gsd', 'milestones', mid);
37
+ mkdirSync(dir, { recursive: true });
38
+ writeFileSync(join(dir, `${mid}-${suffix}.md`), content);
39
+ }
40
+
41
+ function writeSliceFile(base: string, mid: string, sid: string, suffix: string, content: string): void {
42
+ const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
43
+ mkdirSync(dir, { recursive: true });
44
+ writeFileSync(join(dir, `${sid}-${suffix}.md`), content);
45
+ }
46
+
47
+ async function main(): Promise<void> {
48
+
49
+ // ─── 1. Regression #1240: New roadmap detected after cache invalidation ─
50
+ console.log('\n=== 1. #1240: roadmap written after first derive → detected after invalidation ===');
51
+ {
52
+ const base = createBase();
53
+ try {
54
+ // Step 1: Create milestone with just context (no roadmap)
55
+ writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001: Test\n\nBuild a thing.\n');
56
+
57
+ invalidateAllCaches();
58
+ invalidateStateCache();
59
+ const state1 = await deriveState(base);
60
+ assertEq(state1.phase, 'pre-planning', 'initial: pre-planning (no roadmap)');
61
+
62
+ // Step 2: Write roadmap (simulating what the LLM does during planning)
63
+ const roadmap = [
64
+ '# M001: Test',
65
+ '',
66
+ '## Slices',
67
+ '',
68
+ '- [ ] **S01: First Slice** `risk:low` `depends:[]`',
69
+ '',
70
+ '## Boundary Map',
71
+ '',
72
+ ].join('\n');
73
+ writeMilestoneFile(base, 'M001', 'ROADMAP', roadmap);
74
+
75
+ // Step 3: WITHOUT invalidation, the old state might be cached
76
+ // The state cache has a 100ms TTL, so wait just past it
77
+ await new Promise(r => setTimeout(r, 150));
78
+
79
+ // Step 4: Invalidate and re-derive — should see the new roadmap
80
+ invalidateAllCaches();
81
+ invalidateStateCache();
82
+ const state2 = await deriveState(base);
83
+ assertEq(state2.phase, 'planning', '#1240: after roadmap write + invalidation → planning phase');
84
+ assertEq(state2.activeSlice?.id, 'S01', '#1240: S01 is now the active slice');
85
+ } finally {
86
+ cleanup(base);
87
+ }
88
+ }
89
+
90
+ // ─── 2. Regression #1249: Slice context detected after cache invalidation ─
91
+ console.log('\n=== 2. #1249: slice context written mid-loop → detected after invalidation ===');
92
+ {
93
+ const base = createBase();
94
+ try {
95
+ // Create a milestone in needs-discussion phase (CONTEXT-DRAFT, no CONTEXT)
96
+ const mDir = join(base, '.gsd', 'milestones', 'M001');
97
+ mkdirSync(mDir, { recursive: true });
98
+ writeFileSync(join(mDir, 'M001-CONTEXT-DRAFT.md'), '# Draft\n\nSome ideas.\n');
99
+
100
+ invalidateAllCaches();
101
+ invalidateStateCache();
102
+ const state1 = await deriveState(base);
103
+ assertEq(state1.phase, 'needs-discussion', 'initial: needs-discussion');
104
+
105
+ // Simulate: discussion completes, CONTEXT.md is written
106
+ writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001: Test\n\nFull context after discussion.\n');
107
+
108
+ // Wait past TTL
109
+ await new Promise(r => setTimeout(r, 150));
110
+
111
+ // Without invalidation, we'd still see 'needs-discussion'
112
+ invalidateAllCaches();
113
+ invalidateStateCache();
114
+ const state2 = await deriveState(base);
115
+ // Should now be pre-planning (has context, but no roadmap yet)
116
+ // Actually needs-discussion won't trigger because now CONTEXT exists
117
+ // The state should advance past needs-discussion
118
+ assertTrue(
119
+ state2.phase !== 'needs-discussion',
120
+ '#1249: after context write + invalidation → not stuck in needs-discussion',
121
+ );
122
+ } finally {
123
+ cleanup(base);
124
+ }
125
+ }
126
+
127
+ // ─── 3. State cache TTL expires naturally ─────────────────────────────
128
+ console.log('\n=== 3. state cache TTL: fresh reads after 100ms ===');
129
+ {
130
+ const base = createBase();
131
+ try {
132
+ writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
133
+
134
+ invalidateAllCaches();
135
+ invalidateStateCache();
136
+ const state1 = await deriveState(base);
137
+ assertEq(state1.phase, 'pre-planning', 'initial: pre-planning');
138
+
139
+ // Write roadmap immediately
140
+ writeMilestoneFile(base, 'M001', 'ROADMAP', [
141
+ '# M001: Test',
142
+ '',
143
+ '## Slices',
144
+ '',
145
+ '- [ ] **S01: Slice** `risk:low` `depends:[]`',
146
+ '',
147
+ ].join('\n'));
148
+
149
+ // Immediately after writing (within 100ms TTL), the cache might be stale
150
+ const state2 = await deriveState(base);
151
+ // This MAY still show pre-planning if within TTL — that's expected behavior
152
+
153
+ // Wait past TTL
154
+ await new Promise(r => setTimeout(r, 150));
155
+
156
+ // ALSO invalidate parse cache (not just state cache)
157
+ invalidateAllCaches();
158
+ invalidateStateCache();
159
+ const state3 = await deriveState(base);
160
+ assertEq(state3.phase, 'planning', 'after TTL expiry + invalidation → planning');
161
+ } finally {
162
+ cleanup(base);
163
+ }
164
+ }
165
+
166
+ // ─── 4. Task completion detection after file write ────────────────────
167
+ console.log('\n=== 4. task marked done in plan → state advances ===');
168
+ {
169
+ const base = createBase();
170
+ try {
171
+ writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
172
+ writeMilestoneFile(base, 'M001', 'ROADMAP', [
173
+ '# M001: Test',
174
+ '',
175
+ '## Slices',
176
+ '',
177
+ '- [ ] **S01: Slice** `risk:low` `depends:[]`',
178
+ '',
179
+ ].join('\n'));
180
+ writeSliceFile(base, 'M001', 'S01', 'PLAN', [
181
+ '# S01: Slice',
182
+ '',
183
+ '## Tasks',
184
+ '',
185
+ '- [ ] **T01: First Task** `est:1h`',
186
+ '- [ ] **T02: Second Task** `est:1h`',
187
+ ].join('\n'));
188
+ // Write task plan files
189
+ const tasksDir = join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks');
190
+ mkdirSync(tasksDir, { recursive: true });
191
+ writeFileSync(join(tasksDir, 'T01-PLAN.md'), '# T01\nDo thing.');
192
+ writeFileSync(join(tasksDir, 'T02-PLAN.md'), '# T02\nDo other thing.');
193
+
194
+ invalidateAllCaches();
195
+ invalidateStateCache();
196
+ const state1 = await deriveState(base);
197
+ assertEq(state1.activeTask?.id, 'T01', 'initial: T01 is active task');
198
+
199
+ // Mark T01 as done by rewriting the plan
200
+ writeSliceFile(base, 'M001', 'S01', 'PLAN', [
201
+ '# S01: Slice',
202
+ '',
203
+ '## Tasks',
204
+ '',
205
+ '- [x] **T01: First Task** `est:1h`',
206
+ '- [ ] **T02: Second Task** `est:1h`',
207
+ ].join('\n'));
208
+
209
+ await new Promise(r => setTimeout(r, 150));
210
+ invalidateAllCaches();
211
+ invalidateStateCache();
212
+ const state2 = await deriveState(base);
213
+ assertEq(state2.activeTask?.id, 'T02', 'after T01 done → T02 is active task');
214
+ } finally {
215
+ cleanup(base);
216
+ }
217
+ }
218
+
219
+ // ─── 5. Slice completion detection ────────────────────────────────────
220
+ console.log('\n=== 5. all tasks done → summarizing phase ===');
221
+ {
222
+ const base = createBase();
223
+ try {
224
+ writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
225
+ writeMilestoneFile(base, 'M001', 'ROADMAP', [
226
+ '# M001: Test',
227
+ '',
228
+ '## Slices',
229
+ '',
230
+ '- [ ] **S01: First** `risk:low` `depends:[]`',
231
+ '- [ ] **S02: Second** `risk:low` `depends:[S01]`',
232
+ '',
233
+ ].join('\n'));
234
+ writeSliceFile(base, 'M001', 'S01', 'PLAN', [
235
+ '# S01',
236
+ '',
237
+ '## Tasks',
238
+ '',
239
+ '- [ ] **T01: Task** `est:1h`',
240
+ ].join('\n'));
241
+ const tasksDir = join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks');
242
+ mkdirSync(tasksDir, { recursive: true });
243
+ writeFileSync(join(tasksDir, 'T01-PLAN.md'), '# T01\nDo it.');
244
+
245
+ invalidateAllCaches();
246
+ invalidateStateCache();
247
+ const state1 = await deriveState(base);
248
+ assertEq(state1.phase, 'executing', 'initial: executing');
249
+
250
+ // Mark task done
251
+ writeSliceFile(base, 'M001', 'S01', 'PLAN', [
252
+ '# S01',
253
+ '',
254
+ '## Tasks',
255
+ '',
256
+ '- [x] **T01: Task** `est:1h`',
257
+ ].join('\n'));
258
+
259
+ await new Promise(r => setTimeout(r, 150));
260
+ invalidateAllCaches();
261
+ invalidateStateCache();
262
+ const state2 = await deriveState(base);
263
+ assertEq(state2.phase, 'summarizing', 'after all tasks done → summarizing');
264
+ } finally {
265
+ cleanup(base);
266
+ }
267
+ }
268
+
269
+ // ─── 6. Roadmap slice marked done → advance to next slice ─────────────
270
+ console.log('\n=== 6. roadmap slice marked [x] → next slice active ===');
271
+ {
272
+ const base = createBase();
273
+ try {
274
+ writeMilestoneFile(base, 'M001', 'CONTEXT', '# M001\n\nDesc.\n');
275
+ writeMilestoneFile(base, 'M001', 'ROADMAP', [
276
+ '# M001: Test',
277
+ '',
278
+ '## Slices',
279
+ '',
280
+ '- [ ] **S01: First** `risk:low` `depends:[]`',
281
+ '- [ ] **S02: Second** `risk:low` `depends:[S01]`',
282
+ '',
283
+ ].join('\n'));
284
+
285
+ invalidateAllCaches();
286
+ invalidateStateCache();
287
+ const state1 = await deriveState(base);
288
+ assertEq(state1.activeSlice?.id, 'S01', 'initial: S01 active');
289
+
290
+ // Mark S01 as done in roadmap
291
+ writeMilestoneFile(base, 'M001', 'ROADMAP', [
292
+ '# M001: Test',
293
+ '',
294
+ '## Slices',
295
+ '',
296
+ '- [x] **S01: First** `risk:low` `depends:[]`',
297
+ '- [ ] **S02: Second** `risk:low` `depends:[S01]`',
298
+ '',
299
+ ].join('\n'));
300
+
301
+ await new Promise(r => setTimeout(r, 150));
302
+ invalidateAllCaches();
303
+ invalidateStateCache();
304
+ const state2 = await deriveState(base);
305
+ assertEq(state2.activeSlice?.id, 'S02', 'after S01 done → S02 active');
306
+ } finally {
307
+ cleanup(base);
308
+ }
309
+ }
310
+
311
+ report();
312
+ }
313
+
314
+ main().catch((error) => {
315
+ console.error(error);
316
+ process.exit(1);
317
+ });