gsd-pi 2.23.0 → 2.25.0

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 (212) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +12 -3
  3. package/dist/headless.d.ts +4 -0
  4. package/dist/headless.js +118 -10
  5. package/dist/help-text.js +22 -7
  6. package/dist/models-resolver.d.ts +0 -11
  7. package/dist/models-resolver.js +0 -15
  8. package/dist/resource-loader.d.ts +0 -1
  9. package/dist/resource-loader.js +64 -18
  10. package/dist/resources/GSD-WORKFLOW.md +12 -9
  11. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  12. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  13. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  14. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  15. package/dist/resources/extensions/gsd/auto-prompts.ts +87 -0
  16. package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
  17. package/dist/resources/extensions/gsd/auto-worktree.ts +134 -4
  18. package/dist/resources/extensions/gsd/auto.ts +307 -77
  19. package/dist/resources/extensions/gsd/cache.ts +3 -1
  20. package/dist/resources/extensions/gsd/commands.ts +176 -10
  21. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  22. package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
  23. package/dist/resources/extensions/gsd/doctor.ts +58 -11
  24. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  25. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  26. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  27. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  28. package/dist/resources/extensions/gsd/guided-flow.ts +109 -12
  29. package/dist/resources/extensions/gsd/index.ts +48 -2
  30. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  31. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  32. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  33. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  34. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  35. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  36. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  37. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  39. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
  40. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  43. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  45. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  47. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
  48. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  49. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  50. package/dist/resources/extensions/gsd/state.ts +72 -30
  51. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  52. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  53. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  54. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
  55. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  56. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  57. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  58. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  59. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  60. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  62. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  63. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  64. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  65. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  66. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  67. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  68. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  69. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  70. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  71. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  72. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  73. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  74. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  75. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  76. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  77. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  78. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  79. package/dist/resources/extensions/gsd/types.ts +15 -1
  80. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  81. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  82. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  83. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  84. package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
  85. package/dist/resources/extensions/subagent/index.ts +5 -0
  86. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  87. package/dist/update-check.d.ts +9 -0
  88. package/dist/update-check.js +97 -0
  89. package/package.json +6 -1
  90. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  91. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  92. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  93. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/anthropic.js +55 -7
  95. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  96. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  97. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  98. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  99. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  100. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  101. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  102. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  103. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  106. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  110. package/packages/pi-ai/dist/types.d.ts +23 -1
  111. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  112. package/packages/pi-ai/dist/types.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +59 -9
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  117. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  118. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  119. package/packages/pi-ai/src/types.ts +19 -1
  120. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  123. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  128. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  131. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  132. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  133. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
  135. package/scripts/postinstall.js +7 -109
  136. package/src/resources/GSD-WORKFLOW.md +12 -9
  137. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  138. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  139. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  140. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  141. package/src/resources/extensions/gsd/auto-prompts.ts +87 -0
  142. package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
  143. package/src/resources/extensions/gsd/auto-worktree.ts +134 -4
  144. package/src/resources/extensions/gsd/auto.ts +307 -77
  145. package/src/resources/extensions/gsd/cache.ts +3 -1
  146. package/src/resources/extensions/gsd/commands.ts +176 -10
  147. package/src/resources/extensions/gsd/complexity.ts +1 -0
  148. package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
  149. package/src/resources/extensions/gsd/doctor.ts +58 -11
  150. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  151. package/src/resources/extensions/gsd/git-service.ts +74 -14
  152. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  153. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  154. package/src/resources/extensions/gsd/guided-flow.ts +109 -12
  155. package/src/resources/extensions/gsd/index.ts +48 -2
  156. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  157. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  158. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  159. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  160. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  161. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  162. package/src/resources/extensions/gsd/preferences.ts +65 -1
  163. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  165. package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
  166. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  167. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  168. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  169. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  170. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  171. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  172. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
  174. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  175. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  176. package/src/resources/extensions/gsd/state.ts +72 -30
  177. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  178. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  179. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  180. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
  181. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  182. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  183. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  184. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  185. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  186. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  187. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  188. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  189. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  190. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  191. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  192. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  193. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  194. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  195. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  196. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  197. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  198. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  199. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  200. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  201. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  202. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  203. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  204. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  205. package/src/resources/extensions/gsd/types.ts +15 -1
  206. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  207. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  208. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  209. package/src/resources/extensions/gsd/worktree.ts +9 -2
  210. package/src/resources/extensions/search-the-web/native-search.ts +15 -5
  211. package/src/resources/extensions/subagent/index.ts +5 -0
  212. package/src/resources/extensions/subagent/worker-registry.ts +99 -0
@@ -10,12 +10,15 @@ import {
10
10
  verifyExpectedArtifact,
11
11
  diagnoseExpectedArtifact,
12
12
  buildLoopRemediationSteps,
13
+ selfHealRuntimeRecords,
13
14
  completedKeysPath,
14
15
  persistCompletedKey,
15
16
  removePersistedKey,
16
17
  loadPersistedKeys,
17
18
  } from "../auto-recovery.ts";
18
19
  import { parseRoadmap, clearParseCache } from "../files.ts";
20
+ import { invalidateAllCaches } from "../cache.ts";
21
+ import { deriveState, invalidateStateCache } from "../state.ts";
19
22
 
20
23
  function makeTmpBase(): string {
21
24
  const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
@@ -273,6 +276,68 @@ test("removePersistedKey is safe when file doesn't exist", () => {
273
276
  }
274
277
  });
275
278
 
279
+ // ─── Dual-load across worktree boundary (#769) ───────────────────────────
280
+
281
+ test("loadPersistedKeys unions keys from project root and worktree", () => {
282
+ // Simulate two separate .gsd directories (project root + worktree)
283
+ // each with a different set of completed keys. Loading from both
284
+ // into the same Set should produce the union.
285
+ const projectRoot = makeTmpBase();
286
+ const worktree = makeTmpBase();
287
+ try {
288
+ // Persist different keys in each location
289
+ persistCompletedKey(projectRoot, "execute-task/M001/S01/T01");
290
+ persistCompletedKey(projectRoot, "plan-slice/M001/S02");
291
+
292
+ persistCompletedKey(worktree, "execute-task/M001/S01/T02");
293
+ persistCompletedKey(worktree, "plan-slice/M001/S02"); // overlap
294
+
295
+ // Load from both into the same set (mimicking startup dual-load)
296
+ const keys = new Set<string>();
297
+ loadPersistedKeys(projectRoot, keys);
298
+ loadPersistedKeys(worktree, keys);
299
+
300
+ assert.ok(keys.has("execute-task/M001/S01/T01"), "key from project root");
301
+ assert.ok(keys.has("plan-slice/M001/S02"), "shared key");
302
+ assert.ok(keys.has("execute-task/M001/S01/T02"), "key from worktree");
303
+ assert.equal(keys.size, 3, "union should deduplicate overlapping keys");
304
+ } finally {
305
+ cleanup(projectRoot);
306
+ cleanup(worktree);
307
+ }
308
+ });
309
+
310
+ test("completed-units.json set-union merge produces correct result", () => {
311
+ // Verify that a manual set-union merge (as done in syncStateToProjectRoot)
312
+ // correctly merges two JSON arrays of keys.
313
+ const projectRoot = makeTmpBase();
314
+ const worktree = makeTmpBase();
315
+ try {
316
+ // Write keys to both locations
317
+ const prKeysFile = join(projectRoot, ".gsd", "completed-units.json");
318
+ const wtKeysFile = join(worktree, ".gsd", "completed-units.json");
319
+
320
+ writeFileSync(prKeysFile, JSON.stringify(["a", "b"]));
321
+ writeFileSync(wtKeysFile, JSON.stringify(["b", "c", "d"]));
322
+
323
+ // Perform the same merge logic used in syncStateToProjectRoot
324
+ const srcKeys: string[] = JSON.parse(readFileSync(wtKeysFile, "utf8"));
325
+ let dstKeys: string[] = [];
326
+ if (existsSync(prKeysFile)) {
327
+ dstKeys = JSON.parse(readFileSync(prKeysFile, "utf8"));
328
+ }
329
+ const merged = [...new Set([...dstKeys, ...srcKeys])];
330
+ writeFileSync(prKeysFile, JSON.stringify(merged, null, 2));
331
+
332
+ // Verify the merged result
333
+ const result: string[] = JSON.parse(readFileSync(prKeysFile, "utf8"));
334
+ assert.deepStrictEqual(result.sort(), ["a", "b", "c", "d"]);
335
+ } finally {
336
+ cleanup(projectRoot);
337
+ cleanup(worktree);
338
+ }
339
+ });
340
+
276
341
  // ─── verifyExpectedArtifact: parse cache collision regression ─────────────
277
342
 
278
343
  test("verifyExpectedArtifact detects roadmap [x] change despite parse cache", () => {
@@ -343,7 +408,8 @@ test("verifyExpectedArtifact accepts plan-slice with actual tasks", () => {
343
408
  const base = makeTmpBase();
344
409
  try {
345
410
  const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
346
- mkdirSync(sliceDir, { recursive: true });
411
+ const tasksDir = join(sliceDir, "tasks");
412
+ mkdirSync(tasksDir, { recursive: true });
347
413
  writeFileSync(join(sliceDir, "S01-PLAN.md"), [
348
414
  "# S01: Test Slice",
349
415
  "",
@@ -352,6 +418,8 @@ test("verifyExpectedArtifact accepts plan-slice with actual tasks", () => {
352
418
  "- [ ] **T01: Implement feature** `est:2h`",
353
419
  "- [ ] **T02: Write tests** `est:1h`",
354
420
  ].join("\n"));
421
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
422
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
355
423
  assert.strictEqual(
356
424
  verifyExpectedArtifact("plan-slice", "M001/S01", base),
357
425
  true,
@@ -366,7 +434,8 @@ test("verifyExpectedArtifact accepts plan-slice with completed tasks", () => {
366
434
  const base = makeTmpBase();
367
435
  try {
368
436
  const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
369
- mkdirSync(sliceDir, { recursive: true });
437
+ const tasksDir = join(sliceDir, "tasks");
438
+ mkdirSync(tasksDir, { recursive: true });
370
439
  writeFileSync(join(sliceDir, "S01-PLAN.md"), [
371
440
  "# S01: Test Slice",
372
441
  "",
@@ -375,6 +444,8 @@ test("verifyExpectedArtifact accepts plan-slice with completed tasks", () => {
375
444
  "- [x] **T01: Implement feature** `est:2h`",
376
445
  "- [ ] **T02: Write tests** `est:1h`",
377
446
  ].join("\n"));
447
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
448
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
378
449
  assert.strictEqual(
379
450
  verifyExpectedArtifact("plan-slice", "M001/S01", base),
380
451
  true,
@@ -384,3 +455,186 @@ test("verifyExpectedArtifact accepts plan-slice with completed tasks", () => {
384
455
  cleanup(base);
385
456
  }
386
457
  });
458
+
459
+ // ─── verifyExpectedArtifact: plan-slice task plan check (#739) ────────────
460
+
461
+ test("verifyExpectedArtifact plan-slice passes when all task plan files exist", () => {
462
+ const base = makeTmpBase();
463
+ try {
464
+ const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
465
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
466
+ const planContent = [
467
+ "# S01: Test Slice",
468
+ "",
469
+ "## Tasks",
470
+ "",
471
+ "- [ ] **T01: First task** `est:1h`",
472
+ "- [ ] **T02: Second task** `est:2h`",
473
+ ].join("\n");
474
+ writeFileSync(planPath, planContent);
475
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n\nDo the thing.");
476
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan\n\nDo the other thing.");
477
+
478
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
479
+ assert.equal(result, true, "should pass when all task plan files exist");
480
+ } finally {
481
+ cleanup(base);
482
+ }
483
+ });
484
+
485
+ test("verifyExpectedArtifact plan-slice fails when a task plan file is missing (#739)", () => {
486
+ const base = makeTmpBase();
487
+ try {
488
+ const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
489
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
490
+ const planContent = [
491
+ "# S01: Test Slice",
492
+ "",
493
+ "## Tasks",
494
+ "",
495
+ "- [ ] **T01: First task** `est:1h`",
496
+ "- [ ] **T02: Second task** `est:2h`",
497
+ ].join("\n");
498
+ writeFileSync(planPath, planContent);
499
+ // Only write T01-PLAN.md — T02 is missing
500
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n\nDo the thing.");
501
+
502
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
503
+ assert.equal(result, false, "should fail when T02-PLAN.md is missing");
504
+ } finally {
505
+ cleanup(base);
506
+ }
507
+ });
508
+
509
+ test("verifyExpectedArtifact plan-slice fails for plan with no tasks (#699)", () => {
510
+ const base = makeTmpBase();
511
+ try {
512
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
513
+ const planContent = [
514
+ "# S01: Test Slice",
515
+ "",
516
+ "## Goal",
517
+ "",
518
+ "Just some documentation updates, no tasks.",
519
+ ].join("\n");
520
+ writeFileSync(planPath, planContent);
521
+
522
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
523
+ assert.equal(result, false, "should fail when plan has no task entries (empty scaffold, #699)");
524
+ } finally {
525
+ cleanup(base);
526
+ }
527
+ });
528
+
529
+ // ─── selfHealRuntimeRecords — worktree base path (#769) ──────────────────
530
+
531
+ test("selfHealRuntimeRecords clears stale record when artifact exists at worktree base (#769)", async () => {
532
+ // Simulate worktree layout: the runtime record AND the artifact both live
533
+ // under the worktree's .gsd/, not the main project root.
534
+ const worktreeBase = makeTmpBase();
535
+ const mainBase = makeTmpBase();
536
+ try {
537
+ const { writeUnitRuntimeRecord, readUnitRuntimeRecord } = await import("../unit-runtime.ts");
538
+
539
+ // Write a stale runtime record in the worktree .gsd/runtime/units/
540
+ writeUnitRuntimeRecord(worktreeBase, "run-uat", "M001/S01", Date.now() - 7200_000, {
541
+ phase: "dispatched",
542
+ });
543
+
544
+ // Write the UAT result artifact in the worktree .gsd/milestones/
545
+ const uatPath = join(worktreeBase, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT-RESULT.md");
546
+ writeFileSync(uatPath, "---\nresult: pass\n---\n# UAT Result\nAll tests passed.\n");
547
+
548
+ // Verify the runtime record exists before heal
549
+ const before = readUnitRuntimeRecord(worktreeBase, "run-uat", "M001/S01");
550
+ assert.ok(before, "runtime record should exist before heal");
551
+
552
+ // Mock ExtensionContext with minimal notify
553
+ const notifications: string[] = [];
554
+ const mockCtx = {
555
+ ui: { notify: (msg: string) => { notifications.push(msg); } },
556
+ } as any;
557
+
558
+ // Call selfHeal with worktreeBase — this is the fix: using the worktree path
559
+ // so both the runtime record and artifact are found
560
+ const completedKeys = new Set<string>();
561
+ await selfHealRuntimeRecords(worktreeBase, mockCtx, completedKeys);
562
+
563
+ // The stale record should be cleared
564
+ const after = readUnitRuntimeRecord(worktreeBase, "run-uat", "M001/S01");
565
+ assert.equal(after, null, "runtime record should be cleared after heal");
566
+
567
+ // The completion key should be persisted
568
+ assert.ok(completedKeys.has("run-uat/M001/S01"), "completion key should be added");
569
+ assert.ok(notifications.some(n => n.includes("Self-heal")), "should emit self-heal notification");
570
+
571
+ // Now verify that calling with mainBase does NOT find/clear anything (the old bug)
572
+ // Write a stale record at mainBase but NO artifact there
573
+ writeUnitRuntimeRecord(mainBase, "run-uat", "M001/S01", Date.now() - 7200_000, {
574
+ phase: "dispatched",
575
+ });
576
+ const mainKeys = new Set<string>();
577
+ await selfHealRuntimeRecords(mainBase, mockCtx, mainKeys);
578
+
579
+ // The record at mainBase should be cleared by the stale timeout (>1h),
580
+ // but the completion key should NOT be set (artifact doesn't exist at mainBase)
581
+ const afterMain = readUnitRuntimeRecord(mainBase, "run-uat", "M001/S01");
582
+ assert.equal(afterMain, null, "stale record at main base should be cleared by timeout");
583
+ assert.ok(!mainKeys.has("run-uat/M001/S01"), "completion key should NOT be set when artifact is missing");
584
+ } finally {
585
+ cleanup(worktreeBase);
586
+ cleanup(mainBase);
587
+ }
588
+ });
589
+
590
+ // ─── #793: invalidateAllCaches unblocks skip-loop ─────────────────────────
591
+ // When the skip-loop breaker fires, it must call invalidateAllCaches() (not
592
+ // just invalidateStateCache()) to clear path/parse caches that deriveState
593
+ // depends on. Without this, even after cache invalidation, deriveState reads
594
+ // stale directory listings and returns the same unit, looping forever.
595
+ test("#793: invalidateAllCaches clears all caches so deriveState sees fresh disk state", async () => {
596
+ const base = makeTmpBase();
597
+ try {
598
+ const mid = "M001";
599
+ const sid = "S01";
600
+ const planDir = join(base, ".gsd", "milestones", mid, "slices", sid);
601
+ const tasksDir = join(planDir, "tasks");
602
+ mkdirSync(tasksDir, { recursive: true });
603
+ mkdirSync(join(base, ".gsd", "milestones", mid), { recursive: true });
604
+
605
+ writeFileSync(
606
+ join(base, ".gsd", "milestones", mid, `${mid}-ROADMAP.md`),
607
+ `# M001: Test Milestone\n\n**Vision:** test.\n\n## Slices\n\n- [ ] **${sid}: Slice One** \`risk:low\` \`depends:[]\`\n > After this: done.\n`,
608
+ );
609
+ 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`;
610
+ writeFileSync(join(planDir, `${sid}-PLAN.md`), planUnchecked);
611
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01: Task One\n\n**Goal:** t\n\n## Steps\n- step\n\n## Verification\n- v\n");
612
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02: Task Two\n\n**Goal:** t\n\n## Steps\n- step\n\n## Verification\n- v\n");
613
+
614
+ // Warm all caches
615
+ const state1 = await deriveState(base);
616
+ assert.equal(state1.activeTask?.id, "T01", "initial: T01 is active");
617
+
618
+ // Simulate task completion on disk (what the LLM does)
619
+ 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`;
620
+ writeFileSync(join(planDir, `${sid}-PLAN.md`), planChecked);
621
+ writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "---\nid: T01\n---\n# Summary\n");
622
+
623
+ // invalidateStateCache alone: _stateCache cleared but path/parse caches warm
624
+ invalidateStateCache();
625
+
626
+ // invalidateAllCaches: all caches cleared — deriveState must re-read disk
627
+ invalidateAllCaches();
628
+ const state2 = await deriveState(base);
629
+
630
+ // After full invalidation, T01 should be complete and T02 should be next
631
+ assert.notEqual(state2.activeTask?.id, "T01", "#793: T01 not re-dispatched after full invalidation");
632
+
633
+ // Verify the caches are truly cleared by calling clearParseCache and clearPathCache
634
+ // do not throw (they should be no-ops after invalidateAllCaches already cleared them)
635
+ clearParseCache(); // no-op, but should not throw
636
+ assert.ok(true, "clearParseCache after invalidateAllCaches is safe");
637
+ } finally {
638
+ cleanup(base);
639
+ }
640
+ });
@@ -290,6 +290,40 @@ async function main(): Promise<void> {
290
290
  assertTrue(existsSync(join(repo, "feature.ts")), "feature.ts merged to main");
291
291
  }
292
292
 
293
+ // ─── Test 6: Skip checkout when main already current (#757) ───────
294
+ console.log("\n=== skip checkout when main already current (#757) ===");
295
+ {
296
+ const repo = freshRepo();
297
+ const wtPath = createAutoWorktree(repo, "M060");
298
+
299
+ addSliceToMilestone(repo, wtPath, "M060", "S01", "Skip checkout test", [
300
+ { file: "skip-checkout.ts", content: "export const skip = true;\n", message: "add skip-checkout" },
301
+ ]);
302
+
303
+ const roadmap = makeRoadmap("M060", "Skip checkout verification", [
304
+ { id: "S01", title: "Skip checkout test" },
305
+ ]);
306
+
307
+ // Verify main is already checked out at repo root (worktree default)
308
+ const branchAtRoot = run("git rev-parse --abbrev-ref HEAD", repo);
309
+ assertEq(branchAtRoot, "main", "main is already checked out at project root");
310
+
311
+ // mergeMilestoneToMain should succeed without attempting to checkout main
312
+ // (which would fail with "already used by worktree" error)
313
+ let threw = false;
314
+ try {
315
+ const result = mergeMilestoneToMain(repo, "M060", roadmap);
316
+ assertTrue(result.commitMessage.includes("feat(M060)"), "merge commit created");
317
+ } catch (err) {
318
+ threw = true;
319
+ console.error("Unexpected error:", err);
320
+ }
321
+ assertTrue(!threw, "does not fail when main is already checked out at project root");
322
+
323
+ // Verify the merge actually happened
324
+ assertTrue(existsSync(join(repo, "skip-checkout.ts")), "skip-checkout.ts merged to main");
325
+ }
326
+
293
327
  } finally {
294
328
  process.chdir(savedCwd);
295
329
  for (const d of tempDirs) {
@@ -153,6 +153,64 @@ async function main(): Promise<void> {
153
153
  // After teardown, originalBase should be null
154
154
  assertEq(getAutoWorktreeOriginalBase(), null, "no split-brain: originalBase cleared");
155
155
 
156
+ // ─── #778: reconcile plan checkboxes on re-attach ─────────────────
157
+ console.log("\n=== #778: reconcile plan checkboxes on re-attach ===");
158
+ {
159
+ // Simulate: T01 [x] was committed to milestone branch, T02 [x] was
160
+ // written to project root by syncStateToProjectRoot() but the
161
+ // auto-commit crashed before it fired. On restart the worktree is
162
+ // re-created from the milestone branch HEAD (T02 still [ ]).
163
+ // reconcilePlanCheckboxes should forward-apply T02 [x] from the root.
164
+
165
+ const planRelPath = join(".gsd", "milestones", "M004", "slices", "S01", "S01-PLAN.md");
166
+ const planDir = join(tempDir, ".gsd", "milestones", "M004", "slices", "S01");
167
+ const { mkdirSync: mkdir, writeFileSync: write, readFileSync: read } = await import("node:fs");
168
+
169
+ // Plan on integration branch (project root): T01 [x], T02 [x]
170
+ mkdir(planDir, { recursive: true });
171
+ write(
172
+ join(tempDir, planRelPath),
173
+ "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
174
+ );
175
+
176
+ // Write integration-branch plan to git so milestone branch starts from it
177
+ run(`git add .`, tempDir);
178
+ run(`git commit -m "add plan with T01 and T02 checked" --allow-empty`, tempDir);
179
+
180
+ // Create milestone branch with only T01 [x] (simulating crash before T02 commit)
181
+ const milestoneBranch = "milestone/M004";
182
+ run(`git checkout -b ${milestoneBranch}`, tempDir);
183
+ mkdir(planDir, { recursive: true });
184
+ write(
185
+ join(tempDir, planRelPath),
186
+ "# S01 Plan\n- [x] **T01:** task one\n- [ ] **T02:** task two\n- [ ] **T03:** task three\n",
187
+ );
188
+ run(`git add .`, tempDir);
189
+ run(`git commit -m "milestone: only T01 checked"`, tempDir);
190
+ run(`git checkout main`, tempDir);
191
+
192
+ // Restore project root plan (T01+T02 [x]) — simulates syncStateToProjectRoot
193
+ write(
194
+ join(tempDir, planRelPath),
195
+ "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
196
+ );
197
+
198
+ // Create worktree re-attached to existing milestone branch (T02 still [ ] in branch)
199
+ const wtPath = createAutoWorktree(tempDir, "M004");
200
+
201
+ try {
202
+ const wtPlanPath = join(wtPath, planRelPath);
203
+ assertTrue(existsSync(wtPlanPath), "plan file exists in worktree after re-attach");
204
+
205
+ const wtPlan = read(wtPlanPath, "utf-8");
206
+ assertTrue(wtPlan.includes("- [x] **T02:"), "T02 should be [x] after reconciliation (was [ ] on branch)");
207
+ assertTrue(wtPlan.includes("- [x] **T01:"), "T01 stays [x]");
208
+ assertTrue(wtPlan.includes("- [ ] **T03:"), "T03 stays [ ] (not in root either)");
209
+ } finally {
210
+ teardownAutoWorktree(tempDir, "M004");
211
+ }
212
+ }
213
+
156
214
  } finally {
157
215
  // Always restore cwd and clean up
158
216
  process.chdir(savedCwd);
@@ -45,6 +45,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
45
45
  writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
46
46
  }
47
47
 
48
+ function writeMilestoneValidation(base: string, mid: string, verdict: string = "pass"): void {
49
+ const dir = join(base, ".gsd", "milestones", mid);
50
+ mkdirSync(dir, { recursive: true });
51
+ writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: ${verdict}\nremediation_round: 0\n---\n\n# Validation\nValidated.`);
52
+ }
53
+
48
54
  function cleanup(base: string): void {
49
55
  rmSync(base, { recursive: true, force: true });
50
56
  }
@@ -176,7 +182,8 @@ async function main(): Promise<void> {
176
182
  const roadmap = parseRoadmap(roadmapContent!);
177
183
  assertTrue(isMilestoneComplete(roadmap), "isMilestoneComplete returns true when all slices are [x]");
178
184
 
179
- // Verify deriveState returns completing-milestone phase
185
+ // Verify deriveState returns completing-milestone phase (with validation already done)
186
+ writeMilestoneValidation(base, "M001");
180
187
  const state = await deriveState(base);
181
188
  assertEq(state.phase, "completing-milestone", "deriveState returns completing-milestone when all slices done, no summary");
182
189
  assertEq(state.activeMilestone?.id, "M001", "active milestone is M001");
@@ -248,31 +248,24 @@ async function main(): Promise<void> {
248
248
  }
249
249
  }
250
250
 
251
- // ─── Test 5: Requirements counting from DB content ────────────────────
252
- console.log('\n=== derive-state-db: requirements from DB content ===');
251
+ // ─── Test 5: Requirements counting from disk (DB no longer used for content)
252
+ console.log('\n=== derive-state-db: requirements from disk content ===');
253
253
  {
254
254
  const base = createFixtureBase();
255
255
  try {
256
256
  // Write minimal milestone dir (needed for milestone discovery)
257
257
  mkdirSync(join(base, '.gsd', 'milestones', 'M001'), { recursive: true });
258
- // Do NOT write REQUIREMENTS.md to disk only in DB
259
-
260
- openDatabase(':memory:');
261
- insertArtifactRow('REQUIREMENTS.md', REQUIREMENTS_CONTENT, {
262
- artifact_type: 'requirements',
263
- });
258
+ // Write REQUIREMENTS.md to disk (DB content is no longer used by deriveState)
259
+ writeFile(base, 'REQUIREMENTS.md', REQUIREMENTS_CONTENT);
264
260
 
265
261
  invalidateStateCache();
266
262
  const state = await deriveState(base);
267
263
 
268
- // Requirements should come from DB
269
- assertEq(state.requirements?.active, 2, 'req-from-db: requirements.active = 2');
270
- assertEq(state.requirements?.validated, 1, 'req-from-db: requirements.validated = 1');
271
- assertEq(state.requirements?.total, 3, 'req-from-db: requirements.total = 3');
272
-
273
- closeDatabase();
264
+ // Requirements should come from disk
265
+ assertEq(state.requirements?.active, 2, 'req-from-disk: requirements.active = 2');
266
+ assertEq(state.requirements?.validated, 1, 'req-from-disk: requirements.validated = 1');
267
+ assertEq(state.requirements?.total, 3, 'req-from-disk: requirements.total = 3');
274
268
  } finally {
275
- closeDatabase();
276
269
  cleanup(base);
277
270
  }
278
271
  }
@@ -310,6 +303,7 @@ async function main(): Promise<void> {
310
303
  mkdirSync(join(base, '.gsd', 'milestones', 'M001'), { recursive: true });
311
304
  mkdirSync(join(base, '.gsd', 'milestones', 'M002'), { recursive: true });
312
305
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', completedRoadmap);
306
+ writeFile(base, 'milestones/M001/M001-VALIDATION.md', `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.`);
313
307
  writeFile(base, 'milestones/M001/M001-SUMMARY.md', summaryContent);
314
308
  writeFile(base, 'milestones/M002/M002-ROADMAP.md', activeRoadmap);
315
309
 
@@ -26,6 +26,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
26
26
  writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
27
27
  }
28
28
 
29
+ function writeMilestoneValidation(base: string, mid: string): void {
30
+ const dir = join(base, '.gsd', 'milestones', mid);
31
+ mkdirSync(dir, { recursive: true });
32
+ writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.`);
33
+ }
34
+
29
35
  /**
30
36
  * Creates M00x-CONTEXT.md with a valid YAML frontmatter block.
31
37
  * frontmatter is the raw YAML lines between the --- delimiters.
@@ -120,6 +126,7 @@ async function main(): Promise<void> {
120
126
  - [x] **S01: Done** \`risk:low\` \`depends:[]\`
121
127
  > After this: Done.
122
128
  `);
129
+ writeMilestoneValidation(base, 'M001');
123
130
  writeMilestoneSummary(base, 'M001', '# M001 Summary\n\nFirst milestone is complete.');
124
131
 
125
132
  // M002: depends on M001, now unblocked
@@ -252,6 +259,7 @@ async function main(): Promise<void> {
252
259
  - [x] **S01: Done** \`risk:low\` \`depends:[]\`
253
260
  > After this: Done.
254
261
  `);
262
+ writeMilestoneValidation(base, 'M002');
255
263
  writeMilestoneSummary(base, 'M002', '# M002 Summary\n\nSecond milestone is complete.');
256
264
 
257
265
  const state = await deriveState(base);
@@ -321,6 +329,7 @@ async function main(): Promise<void> {
321
329
  - [x] **S01: Done** \`risk:low\` \`depends:[]\`
322
330
  > After this: Done.
323
331
  `);
332
+ writeMilestoneValidation(base, 'M004-0zjrg0');
324
333
  writeMilestoneSummary(base, 'M004-0zjrg0', '# M004-0zjrg0 Summary\n\nComplete.');
325
334
 
326
335
  // M005-b0m2hl: depends on M004-0zjrg0 (lowercase hex suffix)
@@ -54,6 +54,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
54
54
  writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
55
55
  }
56
56
 
57
+ function writeMilestoneValidation(base: string, mid: string): void {
58
+ const dir = join(base, '.gsd', 'milestones', mid);
59
+ mkdirSync(dir, { recursive: true });
60
+ writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.`);
61
+ }
62
+
57
63
  function cleanup(base: string): void {
58
64
  rmSync(base, { recursive: true, force: true });
59
65
  }
@@ -143,6 +149,7 @@ async function main(): Promise<void> {
143
149
  - [x] **S01: Done** \`risk:low\` \`depends:[]\`
144
150
  > After this: Done.
145
151
  `);
152
+ writeMilestoneValidation(base, 'M001');
146
153
  writeMilestoneSummary(base, 'M001', '# M001 Summary\n\nFirst milestone complete.');
147
154
 
148
155
  // M002: only CONTEXT-DRAFT.md
@@ -178,6 +185,7 @@ async function main(): Promise<void> {
178
185
  - [x] **S01: Done** \`risk:low\` \`depends:[]\`
179
186
  > After this: Done.
180
187
  `);
188
+ writeMilestoneValidation(base, 'M001');
181
189
  writeMilestoneSummary(base, 'M001', '# M001 Summary\n\nComplete.');
182
190
 
183
191
  // M002: draft only — should become active with needs-discussion
@@ -38,6 +38,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
38
38
  writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
39
39
  }
40
40
 
41
+ function writeMilestoneValidation(base: string, mid: string, verdict: string = 'pass'): void {
42
+ const dir = join(base, '.gsd', 'milestones', mid);
43
+ mkdirSync(dir, { recursive: true });
44
+ writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: ${verdict}\nremediation_round: 0\n---\n\n# Validation\nValidated.`);
45
+ }
46
+
41
47
  function writeRequirements(base: string, content: string): void {
42
48
  writeFileSync(join(base, '.gsd', 'REQUIREMENTS.md'), content);
43
49
  }
@@ -285,6 +291,7 @@ Continue from step 2.
285
291
  > After this: Done.
286
292
  `);
287
293
 
294
+ writeMilestoneValidation(base, 'M001');
288
295
  writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nMilestone complete.`);
289
296
 
290
297
  const state = await deriveState(base);
@@ -381,6 +388,7 @@ Continue from step 2.
381
388
  > After this: Done.
382
389
  `);
383
390
 
391
+ writeMilestoneValidation(base, 'M001');
384
392
  writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nFirst milestone complete.`);
385
393
 
386
394
  // M002: active (has incomplete slices)
@@ -486,6 +494,8 @@ Continue from step 2.
486
494
  > After this: S02 complete.
487
495
  `);
488
496
 
497
+ writeMilestoneValidation(base, 'M001');
498
+
489
499
  const state = await deriveState(base);
490
500
 
491
501
  assertEq(state.phase, 'completing-milestone', 'completing-ms: phase is completing-milestone');
@@ -521,6 +531,7 @@ Continue from step 2.
521
531
  > After this: Done.
522
532
  `);
523
533
 
534
+ writeMilestoneValidation(base, 'M001');
524
535
  writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nMilestone is complete.`);
525
536
 
526
537
  const state = await deriveState(base);
@@ -550,6 +561,7 @@ Continue from step 2.
550
561
  - [x] **S01: Done** \`risk:low\` \`depends:[]\`
551
562
  > After this: Done.
552
563
  `);
564
+ writeMilestoneValidation(base, 'M001');
553
565
  writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nFirst milestone complete.`);
554
566
 
555
567
  // M002: all slices done, no summary → completing-milestone
@@ -566,6 +578,8 @@ Continue from step 2.
566
578
  > After this: Done.
567
579
  `);
568
580
 
581
+ writeMilestoneValidation(base, 'M002');
582
+
569
583
  // M003: has incomplete slices → pending (M002 is active)
570
584
  writeRoadmap(base, 'M003', `# M003: Third Milestone
571
585