gsd-pi 2.67.0-dev.fe39184 → 2.68.0-dev.4cf2433

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 (105) hide show
  1. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  2. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  3. package/dist/resources/extensions/gsd/auto-start.js +5 -31
  4. package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
  5. package/dist/resources/extensions/gsd/auto.js +94 -59
  6. package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
  7. package/dist/resources/extensions/gsd/doctor.js +8 -4
  8. package/dist/resources/extensions/gsd/gsd-db.js +11 -0
  9. package/dist/resources/extensions/gsd/guided-flow.js +40 -31
  10. package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
  11. package/dist/resources/extensions/gsd/state.js +7 -2
  12. package/dist/web/standalone/.next/BUILD_ID +1 -1
  13. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  14. package/dist/web/standalone/.next/build-manifest.json +3 -3
  15. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  16. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  17. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.html +1 -1
  36. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  37. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  38. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  39. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  44. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  47. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  48. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  49. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
  50. package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
  51. package/dist/web/standalone/.next/static/chunks/{webpack-42a66876b763aa26.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
  52. package/package.json +1 -1
  53. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
  54. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
  55. package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
  56. package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
  57. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
  58. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
  59. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
  60. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  62. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  63. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  64. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
  66. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  70. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -0
  72. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  73. package/packages/pi-coding-agent/package.json +1 -1
  74. package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
  75. package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
  76. package/packages/pi-coding-agent/src/core/index.ts +2 -0
  77. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
  78. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +17 -0
  79. package/pkg/package.json +1 -1
  80. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  81. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  82. package/src/resources/extensions/gsd/auto-start.ts +8 -54
  83. package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
  84. package/src/resources/extensions/gsd/auto.ts +104 -63
  85. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
  86. package/src/resources/extensions/gsd/doctor.ts +9 -5
  87. package/src/resources/extensions/gsd/gsd-db.ts +12 -0
  88. package/src/resources/extensions/gsd/guided-flow.ts +42 -36
  89. package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
  90. package/src/resources/extensions/gsd/state.ts +7 -1
  91. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
  92. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
  93. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
  94. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
  95. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
  96. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
  97. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
  98. package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
  99. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
  100. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
  101. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
  102. package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
  103. package/dist/web/standalone/.next/static/chunks/6502.5dcdcf1e1432e20d.js +0 -9
  104. /package/dist/web/standalone/.next/static/{gbSATDX4Jt2ufxzUr5nYm → gd7sngpqfUCltp8w_pCwF}/_buildManifest.js +0 -0
  105. /package/dist/web/standalone/.next/static/{gbSATDX4Jt2ufxzUr5nYm → gd7sngpqfUCltp8w_pCwF}/_ssgManifest.js +0 -0
@@ -1,14 +1,30 @@
1
1
  import test, { afterEach } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
6
7
 
7
- import { verifyExpectedArtifact } from "../auto-recovery.ts";
8
+ import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
8
9
  import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow } from "../gsd-db.ts";
10
+ import { clearParseCache } from "../files.ts";
11
+ import { parseRoadmap } from "../parsers-legacy.ts";
12
+ import { invalidateAllCaches } from "../cache.ts";
13
+ import { deriveState, invalidateStateCache } from "../state.ts";
9
14
 
10
15
  const tmpDirs: string[] = [];
11
16
 
17
+ function makeTmpBase(): string {
18
+ const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
19
+ // Create .gsd/milestones/M001/slices/S01/tasks/ structure
20
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
21
+ return base;
22
+ }
23
+
24
+ function cleanup(base: string): void {
25
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
26
+ }
27
+
12
28
  function makeTmpProject(): string {
13
29
  const dir = mkdtempSync(join(tmpdir(), "auto-recovery-"));
14
30
  mkdirSync(join(dir, ".gsd"), { recursive: true });
@@ -39,6 +55,656 @@ afterEach(() => {
39
55
  tmpDirs.length = 0;
40
56
  });
41
57
 
58
+ test("resolveExpectedArtifactPath returns correct path for execute-task", () => {
59
+ const base = makeTmpBase();
60
+ try {
61
+ const result = resolveExpectedArtifactPath("execute-task", "M001/S01/T01", base);
62
+ assert.ok(result);
63
+ assert.ok(result!.includes("tasks"));
64
+ assert.ok(result!.includes("SUMMARY"));
65
+ } finally {
66
+ cleanup(base);
67
+ }
68
+ });
69
+
70
+ test("resolveExpectedArtifactPath returns correct path for complete-slice", () => {
71
+ const base = makeTmpBase();
72
+ try {
73
+ const result = resolveExpectedArtifactPath("complete-slice", "M001/S01", base);
74
+ assert.ok(result);
75
+ assert.ok(result!.includes("SUMMARY"));
76
+ } finally {
77
+ cleanup(base);
78
+ }
79
+ });
80
+
81
+ test("resolveExpectedArtifactPath returns correct path for plan-slice", () => {
82
+ const base = makeTmpBase();
83
+ try {
84
+ const result = resolveExpectedArtifactPath("plan-slice", "M001/S01", base);
85
+ assert.ok(result);
86
+ assert.ok(result!.includes("PLAN"));
87
+ } finally {
88
+ cleanup(base);
89
+ }
90
+ });
91
+
92
+ test("resolveExpectedArtifactPath returns null for unknown type", () => {
93
+ const base = makeTmpBase();
94
+ try {
95
+ const result = resolveExpectedArtifactPath("unknown-type", "M001", base);
96
+ assert.equal(result, null);
97
+ } finally {
98
+ cleanup(base);
99
+ }
100
+ });
101
+
102
+ test("resolveExpectedArtifactPath returns correct path for all milestone-level types", () => {
103
+ const base = makeTmpBase();
104
+ try {
105
+ const planResult = resolveExpectedArtifactPath("plan-milestone", "M001", base);
106
+ assert.ok(planResult);
107
+ assert.ok(planResult!.includes("ROADMAP"));
108
+
109
+ const completeResult = resolveExpectedArtifactPath("complete-milestone", "M001", base);
110
+ assert.ok(completeResult);
111
+ assert.ok(completeResult!.includes("SUMMARY"));
112
+ } finally {
113
+ cleanup(base);
114
+ }
115
+ });
116
+
117
+ test("resolveExpectedArtifactPath returns correct path for all slice-level types", () => {
118
+ const base = makeTmpBase();
119
+ try {
120
+ const researchResult = resolveExpectedArtifactPath("research-slice", "M001/S01", base);
121
+ assert.ok(researchResult);
122
+ assert.ok(researchResult!.includes("RESEARCH"));
123
+
124
+ const assessResult = resolveExpectedArtifactPath("reassess-roadmap", "M001/S01", base);
125
+ assert.ok(assessResult);
126
+ assert.ok(assessResult!.includes("ASSESSMENT"));
127
+
128
+ const uatResult = resolveExpectedArtifactPath("run-uat", "M001/S01", base);
129
+ assert.ok(uatResult);
130
+ assert.ok(uatResult!.includes("ASSESSMENT"));
131
+ } finally {
132
+ cleanup(base);
133
+ }
134
+ });
135
+
136
+ // ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
137
+
138
+ test("diagnoseExpectedArtifact returns description for known types", () => {
139
+ const base = makeTmpBase();
140
+ try {
141
+ const research = diagnoseExpectedArtifact("research-milestone", "M001", base);
142
+ assert.ok(research);
143
+ assert.ok(research!.includes("research"));
144
+
145
+ const plan = diagnoseExpectedArtifact("plan-slice", "M001/S01", base);
146
+ assert.ok(plan);
147
+ assert.ok(plan!.includes("plan"));
148
+
149
+ const task = diagnoseExpectedArtifact("execute-task", "M001/S01/T01", base);
150
+ assert.ok(task);
151
+ assert.ok(task!.includes("T01"));
152
+ } finally {
153
+ cleanup(base);
154
+ }
155
+ });
156
+
157
+ test("diagnoseExpectedArtifact returns null for unknown type", () => {
158
+ const base = makeTmpBase();
159
+ try {
160
+ assert.equal(diagnoseExpectedArtifact("unknown", "M001", base), null);
161
+ } finally {
162
+ cleanup(base);
163
+ }
164
+ });
165
+
166
+ // ─── buildLoopRemediationSteps ────────────────────────────────────────────
167
+
168
+ test("buildLoopRemediationSteps returns steps for execute-task", () => {
169
+ const base = makeTmpBase();
170
+ try {
171
+ const steps = buildLoopRemediationSteps("execute-task", "M001/S01/T01", base);
172
+ assert.ok(steps);
173
+ assert.ok(steps!.includes("T01"));
174
+ assert.ok(steps!.includes("gsd undo-task"));
175
+ } finally {
176
+ cleanup(base);
177
+ }
178
+ });
179
+
180
+ test("buildLoopRemediationSteps returns steps for plan-slice", () => {
181
+ const base = makeTmpBase();
182
+ try {
183
+ const steps = buildLoopRemediationSteps("plan-slice", "M001/S01", base);
184
+ assert.ok(steps);
185
+ assert.ok(steps!.includes("PLAN"));
186
+ assert.ok(steps!.includes("gsd recover"));
187
+ } finally {
188
+ cleanup(base);
189
+ }
190
+ });
191
+
192
+ test("buildLoopRemediationSteps returns steps for complete-slice", () => {
193
+ const base = makeTmpBase();
194
+ try {
195
+ const steps = buildLoopRemediationSteps("complete-slice", "M001/S01", base);
196
+ assert.ok(steps);
197
+ assert.ok(steps!.includes("S01"));
198
+ assert.ok(steps!.includes("gsd reset-slice"));
199
+ } finally {
200
+ cleanup(base);
201
+ }
202
+ });
203
+
204
+ test("buildLoopRemediationSteps returns null for unknown type", () => {
205
+ const base = makeTmpBase();
206
+ try {
207
+ assert.equal(buildLoopRemediationSteps("unknown", "M001", base), null);
208
+ } finally {
209
+ cleanup(base);
210
+ }
211
+ });
212
+
213
+ // ─── verifyExpectedArtifact: parse cache collision regression ─────────────
214
+
215
+ test("verifyExpectedArtifact detects roadmap [x] change despite parse cache", () => {
216
+ // Regression test: cacheKey collision when [ ] → [x] doesn't change
217
+ // file length or first/last 100 chars. Without the fix, parseRoadmap
218
+ // returns stale cached data with done=false even though the file has [x].
219
+ const base = makeTmpBase();
220
+ try {
221
+ // Build a roadmap long enough that the [x] change is outside the first/last 100 chars
222
+ const padding = "A".repeat(200);
223
+ const roadmapBefore = [
224
+ `# M001: Test Milestone ${padding}`,
225
+ "",
226
+ "## Slices",
227
+ "",
228
+ "- [ ] **S01: First slice** `risk:low`",
229
+ "",
230
+ `## Footer ${padding}`,
231
+ ].join("\n");
232
+ const roadmapAfter = roadmapBefore.replace("- [ ] **S01:", "- [x] **S01:");
233
+
234
+ // Verify lengths are identical (the key collision condition)
235
+ assert.equal(roadmapBefore.length, roadmapAfter.length);
236
+
237
+ // Populate parse cache with the pre-edit roadmap
238
+ const before = parseRoadmap(roadmapBefore);
239
+ const sliceBefore = before.slices.find(s => s.id === "S01");
240
+ assert.ok(sliceBefore);
241
+ assert.equal(sliceBefore!.done, false);
242
+
243
+ // Now write the post-edit roadmap to disk and create required artifacts
244
+ const roadmapPath = join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
245
+ writeFileSync(roadmapPath, roadmapAfter);
246
+ const summaryPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md");
247
+ writeFileSync(summaryPath, "# Summary\nDone.");
248
+ const uatPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT.md");
249
+ writeFileSync(uatPath, "# UAT\nPassed.");
250
+
251
+ // verifyExpectedArtifact should see the [x] despite the parse cache
252
+ // having the [ ] version. The fix clears the parse cache inside verify.
253
+ const verified = verifyExpectedArtifact("complete-slice", "M001/S01", base);
254
+ assert.equal(verified, true, "verifyExpectedArtifact should return true when roadmap has [x]");
255
+ } finally {
256
+ clearParseCache();
257
+ cleanup(base);
258
+ }
259
+ });
260
+
261
+ // ─── verifyExpectedArtifact: plan-slice empty scaffold regression (#699) ──
262
+
263
+ test("verifyExpectedArtifact rejects plan-slice with empty scaffold", () => {
264
+ const base = makeTmpBase();
265
+ try {
266
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
267
+ mkdirSync(sliceDir, { recursive: true });
268
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01: Test Slice\n\n## Tasks\n\n");
269
+ assert.strictEqual(
270
+ verifyExpectedArtifact("plan-slice", "M001/S01", base),
271
+ false,
272
+ "Empty scaffold should not be treated as completed artifact",
273
+ );
274
+ } finally {
275
+ cleanup(base);
276
+ }
277
+ });
278
+
279
+ test("verifyExpectedArtifact accepts plan-slice with actual tasks", () => {
280
+ const base = makeTmpBase();
281
+ try {
282
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
283
+ const tasksDir = join(sliceDir, "tasks");
284
+ mkdirSync(tasksDir, { recursive: true });
285
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), [
286
+ "# S01: Test Slice",
287
+ "",
288
+ "## Tasks",
289
+ "",
290
+ "- [ ] **T01: Implement feature** `est:2h`",
291
+ "- [ ] **T02: Write tests** `est:1h`",
292
+ ].join("\n"));
293
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
294
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
295
+ assert.strictEqual(
296
+ verifyExpectedArtifact("plan-slice", "M001/S01", base),
297
+ true,
298
+ "Plan with task entries should be treated as completed artifact",
299
+ );
300
+ } finally {
301
+ cleanup(base);
302
+ }
303
+ });
304
+
305
+ test("verifyExpectedArtifact accepts plan-slice with completed tasks", () => {
306
+ const base = makeTmpBase();
307
+ try {
308
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
309
+ const tasksDir = join(sliceDir, "tasks");
310
+ mkdirSync(tasksDir, { recursive: true });
311
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), [
312
+ "# S01: Test Slice",
313
+ "",
314
+ "## Tasks",
315
+ "",
316
+ "- [x] **T01: Implement feature** `est:2h`",
317
+ "- [ ] **T02: Write tests** `est:1h`",
318
+ ].join("\n"));
319
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
320
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
321
+ assert.strictEqual(
322
+ verifyExpectedArtifact("plan-slice", "M001/S01", base),
323
+ true,
324
+ "Plan with completed task entries should be treated as completed artifact",
325
+ );
326
+ } finally {
327
+ cleanup(base);
328
+ }
329
+ });
330
+
331
+ test("verifyExpectedArtifact treats complete-slice as satisfied when summary, UAT, and roadmap checkbox exist", () => {
332
+ const base = makeTmpBase();
333
+ try {
334
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
335
+ const sliceDir = join(milestoneDir, "slices", "S01");
336
+ mkdirSync(sliceDir, { recursive: true });
337
+ writeFileSync(join(milestoneDir, "M001-ROADMAP.md"), [
338
+ "# M001: Test Milestone",
339
+ "",
340
+ "## Slices",
341
+ "",
342
+ "- [x] **S01: First slice** `risk:low`",
343
+ "",
344
+ "## Boundary Map",
345
+ "",
346
+ "- S01 → terminal",
347
+ " - Produces: done",
348
+ " - Consumes: nothing",
349
+ ].join("\n"));
350
+ writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n");
351
+ writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n");
352
+
353
+ assert.equal(
354
+ verifyExpectedArtifact("complete-slice", "M001/S01", base),
355
+ true,
356
+ "complete-slice should verify when expected artifact and state mutation are already satisfied",
357
+ );
358
+ } finally {
359
+ cleanup(base);
360
+ }
361
+ });
362
+
363
+ test("verifyExpectedArtifact rejects complete-slice when roadmap checkbox is still unchecked", () => {
364
+ const base = makeTmpBase();
365
+ try {
366
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
367
+ const sliceDir = join(milestoneDir, "slices", "S01");
368
+ mkdirSync(sliceDir, { recursive: true });
369
+ writeFileSync(join(milestoneDir, "M001-ROADMAP.md"), [
370
+ "# M001: Test Milestone",
371
+ "",
372
+ "## Slices",
373
+ "",
374
+ "- [ ] **S01: First slice** `risk:low`",
375
+ "",
376
+ "## Boundary Map",
377
+ "",
378
+ "- S01 → terminal",
379
+ " - Produces: done",
380
+ " - Consumes: nothing",
381
+ ].join("\n"));
382
+ writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n");
383
+ writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n");
384
+
385
+ assert.equal(
386
+ verifyExpectedArtifact("complete-slice", "M001/S01", base),
387
+ false,
388
+ "complete-slice should remain unsatisfied when roadmap state still requires the unit to run",
389
+ );
390
+ } finally {
391
+ cleanup(base);
392
+ }
393
+ });
394
+
395
+
396
+ // ─── verifyExpectedArtifact: plan-slice task plan check (#739) ────────────
397
+
398
+ test("verifyExpectedArtifact plan-slice passes when all task plan files exist", () => {
399
+ const base = makeTmpBase();
400
+ try {
401
+ const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
402
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
403
+ const planContent = [
404
+ "# S01: Test Slice",
405
+ "",
406
+ "## Tasks",
407
+ "",
408
+ "- [ ] **T01: First task** `est:1h`",
409
+ "- [ ] **T02: Second task** `est:2h`",
410
+ ].join("\n");
411
+ writeFileSync(planPath, planContent);
412
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n\nDo the thing.");
413
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan\n\nDo the other thing.");
414
+
415
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
416
+ assert.equal(result, true, "should pass when all task plan files exist");
417
+ } finally {
418
+ cleanup(base);
419
+ }
420
+ });
421
+
422
+ test("verifyExpectedArtifact plan-slice fails when a task plan file is missing (#739)", () => {
423
+ const base = makeTmpBase();
424
+ try {
425
+ const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
426
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
427
+ const planContent = [
428
+ "# S01: Test Slice",
429
+ "",
430
+ "## Tasks",
431
+ "",
432
+ "- [ ] **T01: First task** `est:1h`",
433
+ "- [ ] **T02: Second task** `est:2h`",
434
+ ].join("\n");
435
+ writeFileSync(planPath, planContent);
436
+ // Only write T01-PLAN.md — T02 is missing
437
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n\nDo the thing.");
438
+
439
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
440
+ assert.equal(result, false, "should fail when T02-PLAN.md is missing");
441
+ } finally {
442
+ cleanup(base);
443
+ }
444
+ });
445
+
446
+ test("verifyExpectedArtifact plan-slice fails for plan with no tasks (#699)", () => {
447
+ const base = makeTmpBase();
448
+ try {
449
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
450
+ const planContent = [
451
+ "# S01: Test Slice",
452
+ "",
453
+ "## Goal",
454
+ "",
455
+ "Just some documentation updates, no tasks.",
456
+ ].join("\n");
457
+ writeFileSync(planPath, planContent);
458
+
459
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
460
+ assert.equal(result, false, "should fail when plan has no task entries (empty scaffold, #699)");
461
+ } finally {
462
+ cleanup(base);
463
+ }
464
+ });
465
+
466
+ // ─── verifyExpectedArtifact: heading-style plan tasks (#1691) ─────────────
467
+
468
+ test("verifyExpectedArtifact accepts plan-slice with heading-style tasks (### T01 --)", () => {
469
+ const base = makeTmpBase();
470
+ try {
471
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
472
+ const tasksDir = join(sliceDir, "tasks");
473
+ mkdirSync(tasksDir, { recursive: true });
474
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), [
475
+ "# S01: Test Slice",
476
+ "",
477
+ "## Tasks",
478
+ "",
479
+ "### T01 -- Implement feature",
480
+ "",
481
+ "Feature description.",
482
+ "",
483
+ "### T02 -- Write tests",
484
+ "",
485
+ "Test description.",
486
+ ].join("\n"));
487
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
488
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
489
+ assert.strictEqual(
490
+ verifyExpectedArtifact("plan-slice", "M001/S01", base),
491
+ true,
492
+ "Heading-style plan with task entries should be treated as completed artifact",
493
+ );
494
+ } finally {
495
+ cleanup(base);
496
+ }
497
+ });
498
+
499
+ test("verifyExpectedArtifact accepts plan-slice with colon-style heading tasks (### T01:)", () => {
500
+ const base = makeTmpBase();
501
+ try {
502
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
503
+ const tasksDir = join(sliceDir, "tasks");
504
+ mkdirSync(tasksDir, { recursive: true });
505
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), [
506
+ "# S01: Test Slice",
507
+ "",
508
+ "## Tasks",
509
+ "",
510
+ "### T01: Implement feature",
511
+ "",
512
+ "Feature description.",
513
+ ].join("\n"));
514
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
515
+ assert.strictEqual(
516
+ verifyExpectedArtifact("plan-slice", "M001/S01", base),
517
+ true,
518
+ "Colon heading-style plan should be treated as completed artifact",
519
+ );
520
+ } finally {
521
+ cleanup(base);
522
+ }
523
+ });
524
+
525
+ test("verifyExpectedArtifact execute-task requires checked checkbox or DB status for heading-style plan entry (#1691, #3607)", () => {
526
+ const base = makeTmpBase();
527
+ try {
528
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
529
+ const tasksDir = join(sliceDir, "tasks");
530
+ mkdirSync(tasksDir, { recursive: true });
531
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), [
532
+ "# S01: Test Slice",
533
+ "",
534
+ "## Tasks",
535
+ "",
536
+ "### T01 -- Implement feature",
537
+ "",
538
+ "Feature description.",
539
+ ].join("\n"));
540
+ writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "# T01 Summary\n\nDone.");
541
+ // Without DB or checked checkbox, heading-style plans cannot verify
542
+ // execute-task completion (summary file alone is insufficient, #3607)
543
+ assert.strictEqual(
544
+ verifyExpectedArtifact("execute-task", "M001/S01/T01", base),
545
+ false,
546
+ "execute-task requires DB status or checked checkbox, not just heading + summary (#3607)",
547
+ );
548
+ } finally {
549
+ cleanup(base);
550
+ }
551
+ });
552
+
553
+ // ─── #793: invalidateAllCaches unblocks skip-loop ─────────────────────────
554
+ // When the skip-loop breaker fires, it must call invalidateAllCaches() (not
555
+ // just invalidateStateCache()) to clear path/parse caches that deriveState
556
+ // depends on. Without this, even after cache invalidation, deriveState reads
557
+ // stale directory listings and returns the same unit, looping forever.
558
+ test("#793: invalidateAllCaches clears all caches so deriveState sees fresh disk state", async () => {
559
+ const base = makeTmpBase();
560
+ try {
561
+ const mid = "M001";
562
+ const sid = "S01";
563
+ const planDir = join(base, ".gsd", "milestones", mid, "slices", sid);
564
+ const tasksDir = join(planDir, "tasks");
565
+ mkdirSync(tasksDir, { recursive: true });
566
+ mkdirSync(join(base, ".gsd", "milestones", mid), { recursive: true });
567
+
568
+ writeFileSync(
569
+ join(base, ".gsd", "milestones", mid, `${mid}-ROADMAP.md`),
570
+ `# M001: Test Milestone\n\n**Vision:** test.\n\n## Slices\n\n- [ ] **${sid}: Slice One** \`risk:low\` \`depends:[]\`\n > After this: done.\n`,
571
+ );
572
+ const planUnchecked = `# ${sid}: Slice One\n\n**Goal:** test.\n\n## Tasks\n\n- [ ] **T01: Task One** \`est:10m\`\n- [ ] **T02: Task Two** \`est:10m\`\n`;
573
+ writeFileSync(join(planDir, `${sid}-PLAN.md`), planUnchecked);
574
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01: Task One\n\n**Goal:** t\n\n## Steps\n- step\n\n## Verification\n- v\n");
575
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02: Task Two\n\n**Goal:** t\n\n## Steps\n- step\n\n## Verification\n- v\n");
576
+
577
+ // Warm all caches
578
+ const state1 = await deriveState(base);
579
+ assert.equal(state1.activeTask?.id, "T01", "initial: T01 is active");
580
+
581
+ // Simulate task completion on disk (what the LLM does)
582
+ const planChecked = `# ${sid}: Slice One\n\n**Goal:** test.\n\n## Tasks\n\n- [x] **T01: Task One** \`est:10m\`\n- [ ] **T02: Task Two** \`est:10m\`\n`;
583
+ writeFileSync(join(planDir, `${sid}-PLAN.md`), planChecked);
584
+ writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "---\nid: T01\n---\n# Summary\n");
585
+
586
+ // invalidateStateCache alone: _stateCache cleared but path/parse caches warm
587
+ invalidateStateCache();
588
+
589
+ // invalidateAllCaches: all caches cleared — deriveState must re-read disk
590
+ invalidateAllCaches();
591
+ const state2 = await deriveState(base);
592
+
593
+ // After full invalidation, T01 should be complete and T02 should be next
594
+ assert.notEqual(state2.activeTask?.id, "T01", "#793: T01 not re-dispatched after full invalidation");
595
+
596
+ // Verify the caches are truly cleared by calling clearParseCache and clearPathCache
597
+ // do not throw (they should be no-ops after invalidateAllCaches already cleared them)
598
+ clearParseCache(); // no-op, but should not throw
599
+ assert.ok(true, "clearParseCache after invalidateAllCaches is safe");
600
+ } finally {
601
+ cleanup(base);
602
+ }
603
+ });
604
+
605
+ // ─── hasImplementationArtifacts (#1703) ───────────────────────────────────
606
+
607
+ import { execFileSync } from "node:child_process";
608
+
609
+ function makeGitBase(): string {
610
+ const base = join(tmpdir(), `gsd-test-git-${randomUUID()}`);
611
+ mkdirSync(base, { recursive: true });
612
+ execFileSync("git", ["init", "--initial-branch=main"], { cwd: base, stdio: "ignore" });
613
+ execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: base, stdio: "ignore" });
614
+ execFileSync("git", ["config", "user.name", "Test"], { cwd: base, stdio: "ignore" });
615
+ // Create initial commit so HEAD exists
616
+ writeFileSync(join(base, ".gitkeep"), "");
617
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
618
+ execFileSync("git", ["commit", "-m", "initial"], { cwd: base, stdio: "ignore" });
619
+ return base;
620
+ }
621
+
622
+ test("hasImplementationArtifacts returns false when only .gsd/ files committed (#1703)", () => {
623
+ const base = makeGitBase();
624
+ try {
625
+ // Create a feature branch and commit only .gsd/ files
626
+ execFileSync("git", ["checkout", "-b", "feat/test-milestone"], { cwd: base, stdio: "ignore" });
627
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
628
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# Roadmap");
629
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Summary");
630
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
631
+ execFileSync("git", ["commit", "-m", "chore: add plan files"], { cwd: base, stdio: "ignore" });
632
+
633
+ const result = hasImplementationArtifacts(base);
634
+ assert.equal(result, "absent", "should return absent when only .gsd/ files were committed");
635
+ } finally {
636
+ cleanup(base);
637
+ }
638
+ });
639
+
640
+ test("hasImplementationArtifacts returns true when implementation files committed (#1703)", () => {
641
+ const base = makeGitBase();
642
+ try {
643
+ // Create a feature branch with both .gsd/ and implementation files
644
+ execFileSync("git", ["checkout", "-b", "feat/test-impl"], { cwd: base, stdio: "ignore" });
645
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
646
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# Roadmap");
647
+ mkdirSync(join(base, "src"), { recursive: true });
648
+ writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}");
649
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
650
+ execFileSync("git", ["commit", "-m", "feat: add feature"], { cwd: base, stdio: "ignore" });
651
+
652
+ const result = hasImplementationArtifacts(base);
653
+ assert.equal(result, "present", "should return present when implementation files are present");
654
+ } finally {
655
+ cleanup(base);
656
+ }
657
+ });
658
+
659
+ test("hasImplementationArtifacts returns true on non-git directory (fail-open)", () => {
660
+ const base = join(tmpdir(), `gsd-test-nogit-${randomUUID()}`);
661
+ mkdirSync(base, { recursive: true });
662
+ try {
663
+ const result = hasImplementationArtifacts(base);
664
+ assert.equal(result, "unknown", "should return unknown (fail-open) in non-git directory");
665
+ } finally {
666
+ cleanup(base);
667
+ }
668
+ });
669
+
670
+ // ─── verifyExpectedArtifact: complete-milestone requires impl artifacts (#1703) ──
671
+
672
+ test("verifyExpectedArtifact complete-milestone fails with only .gsd/ files (#1703)", () => {
673
+ const base = makeGitBase();
674
+ try {
675
+ // Create feature branch with only .gsd/ files
676
+ execFileSync("git", ["checkout", "-b", "feat/ms-only-gsd"], { cwd: base, stdio: "ignore" });
677
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
678
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Milestone Summary\nDone.");
679
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
680
+ execFileSync("git", ["commit", "-m", "chore: milestone plan files"], { cwd: base, stdio: "ignore" });
681
+
682
+ const result = verifyExpectedArtifact("complete-milestone", "M001", base);
683
+ assert.equal(result, false, "complete-milestone should fail verification when only .gsd/ files present");
684
+ } finally {
685
+ cleanup(base);
686
+ }
687
+ });
688
+
689
+ test("verifyExpectedArtifact complete-milestone passes with impl files (#1703)", () => {
690
+ const base = makeGitBase();
691
+ try {
692
+ // Create feature branch with implementation files AND milestone summary
693
+ execFileSync("git", ["checkout", "-b", "feat/ms-with-impl"], { cwd: base, stdio: "ignore" });
694
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
695
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Milestone Summary\nDone.");
696
+ mkdirSync(join(base, "src"), { recursive: true });
697
+ writeFileSync(join(base, "src", "app.ts"), "console.log('hello');");
698
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
699
+ execFileSync("git", ["commit", "-m", "feat: implementation"], { cwd: base, stdio: "ignore" });
700
+
701
+ const result = verifyExpectedArtifact("complete-milestone", "M001", base);
702
+ assert.equal(result, true, "complete-milestone should pass verification with implementation files");
703
+ } finally {
704
+ cleanup(base);
705
+ }
706
+ });
707
+
42
708
  test("verifyExpectedArtifact checks pending gate-evaluate artifacts without ESM require failures", () => {
43
709
  const base = makeTmpProject();
44
710