gsd-pi 2.44.0-dev.62b5d6c → 2.44.0-dev.848dd4c

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 (190) hide show
  1. package/README.md +30 -12
  2. package/dist/resources/extensions/gsd/auto-start.js +10 -0
  3. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +5 -0
  4. package/dist/web/standalone/.next/BUILD_ID +1 -1
  5. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  6. package/dist/web/standalone/.next/build-manifest.json +2 -2
  7. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  8. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  9. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  10. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/index.html +1 -1
  25. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  32. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  33. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  34. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  35. package/package.json +1 -1
  36. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +6 -8
  37. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +24 -26
  39. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +29 -48
  41. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
  42. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +34 -44
  43. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/session-manager.test.js +30 -34
  45. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +10 -12
  47. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  48. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +43 -47
  49. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
  50. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
  51. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
  52. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +31 -43
  53. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +40 -45
  54. package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
  55. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
  56. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
  57. package/src/resources/extensions/gsd/auto-start.ts +14 -0
  58. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +8 -0
  59. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
  60. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +14 -16
  61. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +43 -57
  62. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +11 -13
  63. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +465 -523
  64. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +73 -75
  65. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +34 -56
  66. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +533 -656
  67. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +165 -143
  68. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +29 -52
  69. package/src/resources/extensions/gsd/tests/captures.test.ts +148 -176
  70. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +32 -33
  71. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +141 -143
  72. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
  73. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
  74. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +38 -59
  75. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +228 -263
  76. package/src/resources/extensions/gsd/tests/complete-task.test.ts +250 -302
  77. package/src/resources/extensions/gsd/tests/context-store.test.ts +354 -367
  78. package/src/resources/extensions/gsd/tests/continue-here.test.ts +68 -72
  79. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +92 -106
  80. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +27 -35
  81. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +220 -237
  82. package/src/resources/extensions/gsd/tests/db-writer.test.ts +390 -420
  83. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +76 -92
  84. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +68 -83
  85. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +152 -183
  86. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +78 -101
  87. package/src/resources/extensions/gsd/tests/derive-state.test.ts +192 -227
  88. package/src/resources/extensions/gsd/tests/detection.test.ts +232 -278
  89. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +30 -34
  90. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +164 -180
  91. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +43 -49
  92. package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +28 -32
  93. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +27 -29
  94. package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +34 -38
  95. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +54 -75
  96. package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +21 -32
  97. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +72 -97
  98. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +38 -44
  99. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +104 -145
  100. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +84 -106
  101. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +54 -60
  102. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +72 -93
  103. package/src/resources/extensions/gsd/tests/doctor.test.ts +104 -134
  104. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +123 -131
  105. package/src/resources/extensions/gsd/tests/exit-command.test.ts +20 -24
  106. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +48 -57
  107. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +5 -7
  108. package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +30 -42
  109. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +198 -206
  110. package/src/resources/extensions/gsd/tests/git-locale.test.ts +13 -27
  111. package/src/resources/extensions/gsd/tests/git-service.test.ts +285 -388
  112. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +31 -39
  113. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +63 -69
  114. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +255 -264
  115. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +108 -119
  116. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +81 -103
  117. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +229 -262
  118. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
  119. package/src/resources/extensions/gsd/tests/health-widget.test.ts +29 -37
  120. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +81 -102
  121. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +16 -18
  122. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +41 -46
  123. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +42 -53
  124. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +75 -91
  125. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
  126. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +150 -194
  127. package/src/resources/extensions/gsd/tests/md-importer.test.ts +101 -125
  128. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +45 -54
  129. package/src/resources/extensions/gsd/tests/memory-store.test.ts +80 -93
  130. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +57 -66
  131. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +83 -93
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +161 -170
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +125 -141
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +107 -131
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +87 -96
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +125 -164
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +81 -94
  138. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +35 -36
  139. package/src/resources/extensions/gsd/tests/overrides.test.ts +99 -106
  140. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +40 -47
  141. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +25 -28
  142. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +66 -83
  143. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +54 -77
  144. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +68 -115
  145. package/src/resources/extensions/gsd/tests/parsers.test.ts +546 -611
  146. package/src/resources/extensions/gsd/tests/paths.test.ts +72 -87
  147. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +77 -117
  148. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
  149. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +93 -119
  150. package/src/resources/extensions/gsd/tests/queue-order.test.ts +70 -82
  151. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +42 -55
  152. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +100 -0
  153. package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +45 -73
  154. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +28 -38
  155. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +73 -80
  156. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +71 -74
  157. package/src/resources/extensions/gsd/tests/requirements.test.ts +70 -75
  158. package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +44 -66
  159. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +114 -181
  160. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +63 -65
  161. package/src/resources/extensions/gsd/tests/run-uat.test.ts +66 -128
  162. package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +18 -25
  163. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +37 -44
  164. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +19 -26
  165. package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +63 -0
  166. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +6 -8
  167. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +22 -28
  168. package/src/resources/extensions/gsd/tests/token-savings.test.ts +54 -56
  169. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +23 -25
  170. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +9 -11
  171. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +66 -82
  172. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +46 -47
  173. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -22
  174. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +84 -86
  175. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +41 -43
  176. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +94 -96
  177. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +11 -13
  178. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +27 -29
  179. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +50 -52
  180. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +10 -13
  181. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +14 -18
  182. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +38 -39
  183. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -21
  184. package/src/resources/extensions/gsd/tests/worktree-health.test.ts +25 -30
  185. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +30 -37
  186. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +15 -22
  187. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +59 -66
  188. package/src/resources/extensions/gsd/tests/worktree.test.ts +44 -50
  189. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → -zps1Q9mQmioAKLcQiCr8}/_buildManifest.js +0 -0
  190. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → -zps1Q9mQmioAKLcQiCr8}/_ssgManifest.js +0 -0
@@ -73,7 +73,7 @@ describe("DevWorkflowEngine", () => {
73
73
  assert.equal(engine.engineId, "dev");
74
74
  });
75
75
 
76
- test("deriveState returns EngineState with expected fields", async () => {
76
+ test("deriveState returns EngineState with expected fields", async (t) => {
77
77
  const { DevWorkflowEngine } = await import("../dev-workflow-engine.ts");
78
78
  const engine = new DevWorkflowEngine();
79
79
 
@@ -81,31 +81,29 @@ describe("DevWorkflowEngine", () => {
81
81
  const tempDir = mkdtempSync(join(tmpdir(), "gsd-engine-test-"));
82
82
  mkdirSync(join(tempDir, ".gsd", "milestones"), { recursive: true });
83
83
 
84
- try {
85
- const state = await engine.deriveState(tempDir);
86
-
87
- assert.equal(typeof state.phase, "string", "phase should be a string");
88
- assert.ok(
89
- "currentMilestoneId" in state,
90
- "state should have currentMilestoneId",
91
- );
92
- assert.ok(
93
- "activeSliceId" in state,
94
- "state should have activeSliceId",
95
- );
96
- assert.ok(
97
- "activeTaskId" in state,
98
- "state should have activeTaskId",
99
- );
100
- assert.equal(
101
- typeof state.isComplete,
102
- "boolean",
103
- "isComplete should be boolean",
104
- );
105
- assert.ok("raw" in state, "state should have raw field");
106
- } finally {
107
- rmSync(tempDir, { recursive: true, force: true });
108
- }
84
+ t.after(() => rmSync(tempDir, { recursive: true, force: true }));
85
+
86
+ const state = await engine.deriveState(tempDir);
87
+
88
+ assert.equal(typeof state.phase, "string", "phase should be a string");
89
+ assert.ok(
90
+ "currentMilestoneId" in state,
91
+ "state should have currentMilestoneId",
92
+ );
93
+ assert.ok(
94
+ "activeSliceId" in state,
95
+ "state should have activeSliceId",
96
+ );
97
+ assert.ok(
98
+ "activeTaskId" in state,
99
+ "state should have activeTaskId",
100
+ );
101
+ assert.equal(
102
+ typeof state.isComplete,
103
+ "boolean",
104
+ "isComplete should be boolean",
105
+ );
106
+ assert.ok("raw" in state, "state should have raw field");
109
107
  });
110
108
 
111
109
  test("reconcile returns continue for non-complete state", async () => {
@@ -280,16 +278,14 @@ describe("Kill switch (GSD_ENGINE_BYPASS)", () => {
280
278
  }
281
279
  });
282
280
 
283
- test("GSD_ENGINE_BYPASS=1 does not affect resolveEngine (bypass checked in autoLoop)", async () => {
281
+ test("GSD_ENGINE_BYPASS=1 does not affect resolveEngine (bypass checked in autoLoop)", async (t) => {
284
282
  const { resolveEngine } = await import("../engine-resolver.ts");
285
283
  process.env.GSD_ENGINE_BYPASS = "1";
286
- try {
287
- // resolveEngine should still resolve normally — bypass is checked in autoLoop
288
- const { engine } = resolveEngine({ activeEngineId: null });
289
- assert.ok(engine, "should return an engine even with bypass set");
290
- } finally {
291
- delete process.env.GSD_ENGINE_BYPASS;
292
- }
284
+ t.after(() => delete process.env.GSD_ENGINE_BYPASS);
285
+
286
+ // resolveEngine should still resolve normally bypass is checked in autoLoop
287
+ const { engine } = resolveEngine({ activeEngineId: null });
288
+ assert.ok(engine, "should return an engine even with bypass set");
293
289
  });
294
290
  });
295
291
 
@@ -20,215 +20,199 @@ function teardownRepo(repo: string): void {
20
20
  rmSync(repo, { recursive: true, force: true });
21
21
  }
22
22
 
23
- test("dispatch guard blocks when prior milestone has incomplete slices", () => {
23
+ test("dispatch guard blocks when prior milestone has incomplete slices", (t) => {
24
24
  const repo = setupRepo();
25
- try {
26
- mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
27
- mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
28
-
29
- // Seed DB: M002 with S01 complete, S02 pending
30
- insertMilestone({ id: "M002", title: "Previous" });
31
- insertSlice({ id: "S01", milestoneId: "M002", title: "Done", status: "complete", depends: [], sequence: 1 });
32
- insertSlice({ id: "S02", milestoneId: "M002", title: "Pending", status: "pending", depends: ["S01"], sequence: 2 });
33
-
34
- // M003 with two pending slices
35
- insertMilestone({ id: "M003", title: "Current" });
36
- insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "pending", depends: [], sequence: 1 });
37
- insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
38
-
39
- // Need ROADMAP files for milestone discovery (findMilestoneIds reads disk)
40
- writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
41
- writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
42
-
43
- assert.equal(
44
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M003/S01"),
45
- "Cannot dispatch plan-slice M003/S01: earlier slice M002/S02 is not complete.",
46
- );
47
- } finally {
48
- teardownRepo(repo);
49
- }
25
+ t.after(() => teardownRepo(repo));
26
+
27
+ mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
28
+ mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
29
+
30
+ // Seed DB: M002 with S01 complete, S02 pending
31
+ insertMilestone({ id: "M002", title: "Previous" });
32
+ insertSlice({ id: "S01", milestoneId: "M002", title: "Done", status: "complete", depends: [], sequence: 1 });
33
+ insertSlice({ id: "S02", milestoneId: "M002", title: "Pending", status: "pending", depends: ["S01"], sequence: 2 });
34
+
35
+ // M003 with two pending slices
36
+ insertMilestone({ id: "M003", title: "Current" });
37
+ insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "pending", depends: [], sequence: 1 });
38
+ insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
39
+
40
+ // Need ROADMAP files for milestone discovery (findMilestoneIds reads disk)
41
+ writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
42
+ writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
43
+
44
+ assert.equal(
45
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M003/S01"),
46
+ "Cannot dispatch plan-slice M003/S01: earlier slice M002/S02 is not complete.",
47
+ );
50
48
  });
51
49
 
52
- test("dispatch guard blocks later slice in same milestone when earlier incomplete", () => {
50
+ test("dispatch guard blocks later slice in same milestone when earlier incomplete", (t) => {
53
51
  const repo = setupRepo();
54
- try {
55
- mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
56
- mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
57
-
58
- insertMilestone({ id: "M002", title: "Previous" });
59
- insertSlice({ id: "S01", milestoneId: "M002", title: "Done", status: "complete", depends: [], sequence: 1 });
60
- insertSlice({ id: "S02", milestoneId: "M002", title: "Done", status: "complete", depends: ["S01"], sequence: 2 });
61
-
62
- insertMilestone({ id: "M003", title: "Current" });
63
- insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "pending", depends: [], sequence: 1 });
64
- insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
65
-
66
- writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
67
- writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
68
-
69
- assert.equal(
70
- getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
71
- "Cannot dispatch execute-task M003/S02/T01: dependency slice M003/S01 is not complete.",
72
- );
73
- } finally {
74
- teardownRepo(repo);
75
- }
52
+ t.after(() => teardownRepo(repo));
53
+
54
+ mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
55
+ mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
56
+
57
+ insertMilestone({ id: "M002", title: "Previous" });
58
+ insertSlice({ id: "S01", milestoneId: "M002", title: "Done", status: "complete", depends: [], sequence: 1 });
59
+ insertSlice({ id: "S02", milestoneId: "M002", title: "Done", status: "complete", depends: ["S01"], sequence: 2 });
60
+
61
+ insertMilestone({ id: "M003", title: "Current" });
62
+ insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "pending", depends: [], sequence: 1 });
63
+ insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
64
+
65
+ writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
66
+ writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
67
+
68
+ assert.equal(
69
+ getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
70
+ "Cannot dispatch execute-task M003/S02/T01: dependency slice M003/S01 is not complete.",
71
+ );
76
72
  });
77
73
 
78
- test("dispatch guard allows dispatch when all earlier slices complete", () => {
74
+ test("dispatch guard allows dispatch when all earlier slices complete", (t) => {
79
75
  const repo = setupRepo();
80
- try {
81
- mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
76
+ t.after(() => teardownRepo(repo));
77
+
78
+ mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
82
79
 
83
- insertMilestone({ id: "M003", title: "Current" });
84
- insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "complete", depends: [], sequence: 1 });
85
- insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
80
+ insertMilestone({ id: "M003", title: "Current" });
81
+ insertSlice({ id: "S01", milestoneId: "M003", title: "First", status: "complete", depends: [], sequence: 1 });
82
+ insertSlice({ id: "S02", milestoneId: "M003", title: "Second", status: "pending", depends: ["S01"], sequence: 2 });
86
83
 
87
- writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
84
+ writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), "# M003\n");
88
85
 
89
- assert.equal(getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"), null);
90
- assert.equal(getPriorSliceCompletionBlocker(repo, "main", "plan-milestone", "M003"), null);
91
- } finally {
92
- teardownRepo(repo);
93
- }
86
+ assert.equal(getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"), null);
87
+ assert.equal(getPriorSliceCompletionBlocker(repo, "main", "plan-milestone", "M003"), null);
94
88
  });
95
89
 
96
- test("dispatch guard unblocks slice when positionally-earlier slice depends on it (#1638)", () => {
90
+ test("dispatch guard unblocks slice when positionally-earlier slice depends on it (#1638)", (t) => {
97
91
  // S05 depends on S06, but S05 appears first positionally.
98
92
  // Old behavior: S06 blocked because S05 (positionally earlier) is incomplete.
99
93
  // Fixed behavior: S06 has no unmet dependencies, so it can dispatch.
100
94
  const repo = setupRepo();
101
- try {
102
- mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
103
-
104
- insertMilestone({ id: "M001", title: "Test" });
105
- insertSlice({ id: "S01", milestoneId: "M001", title: "Setup", status: "complete", depends: [], sequence: 1 });
106
- insertSlice({ id: "S02", milestoneId: "M001", title: "Core", status: "complete", depends: ["S01"], sequence: 2 });
107
- insertSlice({ id: "S03", milestoneId: "M001", title: "API", status: "complete", depends: ["S02"], sequence: 3 });
108
- insertSlice({ id: "S04", milestoneId: "M001", title: "Auth", status: "complete", depends: ["S03"], sequence: 4 });
109
- insertSlice({ id: "S05", milestoneId: "M001", title: "Integration", status: "pending", depends: ["S04", "S06"], sequence: 5 });
110
- insertSlice({ id: "S06", milestoneId: "M001", title: "Data Layer", status: "pending", depends: ["S04"], sequence: 6 });
111
-
112
- writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
113
-
114
- // S06 depends only on S04 (complete) — should be unblocked
115
- assert.equal(
116
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S06"),
117
- null,
118
- );
119
-
120
- // S05 depends on S04 (complete) and S06 (incomplete) — should be blocked
121
- assert.equal(
122
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S05"),
123
- "Cannot dispatch plan-slice M001/S05: dependency slice M001/S06 is not complete.",
124
- );
125
- } finally {
126
- teardownRepo(repo);
127
- }
95
+ t.after(() => teardownRepo(repo));
96
+
97
+ mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
98
+
99
+ insertMilestone({ id: "M001", title: "Test" });
100
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Setup", status: "complete", depends: [], sequence: 1 });
101
+ insertSlice({ id: "S02", milestoneId: "M001", title: "Core", status: "complete", depends: ["S01"], sequence: 2 });
102
+ insertSlice({ id: "S03", milestoneId: "M001", title: "API", status: "complete", depends: ["S02"], sequence: 3 });
103
+ insertSlice({ id: "S04", milestoneId: "M001", title: "Auth", status: "complete", depends: ["S03"], sequence: 4 });
104
+ insertSlice({ id: "S05", milestoneId: "M001", title: "Integration", status: "pending", depends: ["S04", "S06"], sequence: 5 });
105
+ insertSlice({ id: "S06", milestoneId: "M001", title: "Data Layer", status: "pending", depends: ["S04"], sequence: 6 });
106
+
107
+ writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
108
+
109
+ // S06 depends only on S04 (complete) — should be unblocked
110
+ assert.equal(
111
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S06"),
112
+ null,
113
+ );
114
+
115
+ // S05 depends on S04 (complete) and S06 (incomplete) — should be blocked
116
+ assert.equal(
117
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S05"),
118
+ "Cannot dispatch plan-slice M001/S05: dependency slice M001/S06 is not complete.",
119
+ );
128
120
  });
129
121
 
130
- test("dispatch guard falls back to positional ordering when no dependencies declared", () => {
122
+ test("dispatch guard falls back to positional ordering when no dependencies declared", (t) => {
131
123
  const repo = setupRepo();
132
- try {
133
- mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
134
-
135
- insertMilestone({ id: "M001", title: "Test" });
136
- insertSlice({ id: "S01", milestoneId: "M001", title: "First", status: "complete", depends: [], sequence: 1 });
137
- insertSlice({ id: "S02", milestoneId: "M001", title: "Second", status: "pending", depends: [], sequence: 2 });
138
- insertSlice({ id: "S03", milestoneId: "M001", title: "Third", status: "pending", depends: [], sequence: 3 });
139
-
140
- writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
141
-
142
- // S03 has no dependencies — positional fallback blocks on S02
143
- assert.equal(
144
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S03"),
145
- "Cannot dispatch plan-slice M001/S03: earlier slice M001/S02 is not complete.",
146
- );
147
-
148
- // S02 has no dependencies — positional fallback: S01 is done, so unblocked
149
- assert.equal(
150
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S02"),
151
- null,
152
- );
153
- } finally {
154
- teardownRepo(repo);
155
- }
124
+ t.after(() => teardownRepo(repo));
125
+
126
+ mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
127
+
128
+ insertMilestone({ id: "M001", title: "Test" });
129
+ insertSlice({ id: "S01", milestoneId: "M001", title: "First", status: "complete", depends: [], sequence: 1 });
130
+ insertSlice({ id: "S02", milestoneId: "M001", title: "Second", status: "pending", depends: [], sequence: 2 });
131
+ insertSlice({ id: "S03", milestoneId: "M001", title: "Third", status: "pending", depends: [], sequence: 3 });
132
+
133
+ writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
134
+
135
+ // S03 has no dependencies — positional fallback blocks on S02
136
+ assert.equal(
137
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S03"),
138
+ "Cannot dispatch plan-slice M001/S03: earlier slice M001/S02 is not complete.",
139
+ );
140
+
141
+ // S02 has no dependencies — positional fallback: S01 is done, so unblocked
142
+ assert.equal(
143
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S02"),
144
+ null,
145
+ );
156
146
  });
157
147
 
158
- test("dispatch guard allows slice with all declared dependencies complete", () => {
148
+ test("dispatch guard allows slice with all declared dependencies complete", (t) => {
159
149
  const repo = setupRepo();
160
- try {
161
- mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
162
-
163
- insertMilestone({ id: "M001", title: "Test" });
164
- insertSlice({ id: "S01", milestoneId: "M001", title: "Setup", status: "complete", depends: [], sequence: 1 });
165
- insertSlice({ id: "S02", milestoneId: "M001", title: "Core", status: "complete", depends: ["S01"], sequence: 2 });
166
- insertSlice({ id: "S03", milestoneId: "M001", title: "Feature A", status: "pending", depends: ["S01", "S02"], sequence: 3 });
167
- insertSlice({ id: "S04", milestoneId: "M001", title: "Feature B", status: "pending", depends: ["S01"], sequence: 4 });
168
-
169
- writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
170
-
171
- // S03 depends on S01 (done) and S02 (done) — unblocked
172
- assert.equal(
173
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S03"),
174
- null,
175
- );
176
-
177
- // S04 depends only on S01 (done) — unblocked even though S03 is incomplete
178
- assert.equal(
179
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S04"),
180
- null,
181
- );
182
- } finally {
183
- teardownRepo(repo);
184
- }
150
+ t.after(() => teardownRepo(repo));
151
+
152
+ mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
153
+
154
+ insertMilestone({ id: "M001", title: "Test" });
155
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Setup", status: "complete", depends: [], sequence: 1 });
156
+ insertSlice({ id: "S02", milestoneId: "M001", title: "Core", status: "complete", depends: ["S01"], sequence: 2 });
157
+ insertSlice({ id: "S03", milestoneId: "M001", title: "Feature A", status: "pending", depends: ["S01", "S02"], sequence: 3 });
158
+ insertSlice({ id: "S04", milestoneId: "M001", title: "Feature B", status: "pending", depends: ["S01"], sequence: 4 });
159
+
160
+ writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
161
+
162
+ // S03 depends on S01 (done) and S02 (done) — unblocked
163
+ assert.equal(
164
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S03"),
165
+ null,
166
+ );
167
+
168
+ // S04 depends only on S01 (done) — unblocked even though S03 is incomplete
169
+ assert.equal(
170
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S04"),
171
+ null,
172
+ );
185
173
  });
186
174
 
187
- test("dispatch guard skips completed milestone with SUMMARY even if it has unchecked remediation slices (#1716)", () => {
175
+ test("dispatch guard skips completed milestone with SUMMARY even if it has unchecked remediation slices (#1716)", (t) => {
188
176
  const repo = setupRepo();
189
- try {
190
- mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
191
- mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
192
-
193
- // M001 is complete (has SUMMARY) but has unchecked remediation slices in DB
194
- insertMilestone({ id: "M001", title: "Previous" });
195
- insertSlice({ id: "S01", milestoneId: "M001", title: "Core", status: "complete", depends: [], sequence: 1 });
196
- insertSlice({ id: "S02", milestoneId: "M001", title: "Tests", status: "complete", depends: ["S01"], sequence: 2 });
197
- insertSlice({ id: "S03-R", milestoneId: "M001", title: "Remediation", status: "pending", depends: ["S02"], sequence: 3 });
198
- insertSlice({ id: "S04-R", milestoneId: "M001", title: "Remediation 2", status: "pending", depends: ["S02"], sequence: 4 });
199
-
200
- insertMilestone({ id: "M002", title: "Current" });
201
- insertSlice({ id: "S01", milestoneId: "M002", title: "Start", status: "pending", depends: [], sequence: 1 });
202
-
203
- // M001 SUMMARY on disk triggers skip
204
- writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
205
- writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-SUMMARY.md"),
206
- "---\nstatus: complete\n---\n# M001 Summary\nDone.\n");
207
- writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
208
-
209
- // M001 has SUMMARY — should be skipped, not block M002/S01
210
- assert.equal(
211
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M002/S01"),
212
- null,
213
- );
214
- } finally {
215
- teardownRepo(repo);
216
- }
177
+ t.after(() => teardownRepo(repo));
178
+
179
+ mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
180
+ mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
181
+
182
+ // M001 is complete (has SUMMARY) but has unchecked remediation slices in DB
183
+ insertMilestone({ id: "M001", title: "Previous" });
184
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Core", status: "complete", depends: [], sequence: 1 });
185
+ insertSlice({ id: "S02", milestoneId: "M001", title: "Tests", status: "complete", depends: ["S01"], sequence: 2 });
186
+ insertSlice({ id: "S03-R", milestoneId: "M001", title: "Remediation", status: "pending", depends: ["S02"], sequence: 3 });
187
+ insertSlice({ id: "S04-R", milestoneId: "M001", title: "Remediation 2", status: "pending", depends: ["S02"], sequence: 4 });
188
+
189
+ insertMilestone({ id: "M002", title: "Current" });
190
+ insertSlice({ id: "S01", milestoneId: "M002", title: "Start", status: "pending", depends: [], sequence: 1 });
191
+
192
+ // M001 SUMMARY on disk triggers skip
193
+ writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
194
+ writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-SUMMARY.md"),
195
+ "---\nstatus: complete\n---\n# M001 Summary\nDone.\n");
196
+ writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
197
+
198
+ // M001 has SUMMARY — should be skipped, not block M002/S01
199
+ assert.equal(
200
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M002/S01"),
201
+ null,
202
+ );
217
203
  });
218
204
 
219
- test("dispatch guard works without git repo", () => {
205
+ test("dispatch guard works without git repo", (t) => {
220
206
  const repo = setupRepo();
221
- try {
222
- mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
207
+ t.after(() => teardownRepo(repo));
208
+
209
+ mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
223
210
 
224
- insertMilestone({ id: "M001", title: "Test" });
225
- insertSlice({ id: "S01", milestoneId: "M001", title: "Done", status: "complete", depends: [], sequence: 1 });
226
- insertSlice({ id: "S02", milestoneId: "M001", title: "Pending", status: "pending", depends: ["S01"], sequence: 2 });
211
+ insertMilestone({ id: "M001", title: "Test" });
212
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Done", status: "complete", depends: [], sequence: 1 });
213
+ insertSlice({ id: "S02", milestoneId: "M001", title: "Pending", status: "pending", depends: ["S01"], sequence: 2 });
227
214
 
228
- writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
215
+ writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
229
216
 
230
- assert.equal(getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S02"), null);
231
- } finally {
232
- teardownRepo(repo);
233
- }
217
+ assert.equal(getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M001/S02"), null);
234
218
  });
@@ -71,62 +71,56 @@ function scaffoldTaskPlan(basePath: string, mid: string, sid: string, tid: strin
71
71
 
72
72
  // ─── Tests ─────────────────────────────────────────────────────────────────
73
73
 
74
- test("dispatch: missing task plan triggers plan-slice (not stop) — issue #909", async () => {
74
+ test("dispatch: missing task plan triggers plan-slice (not stop) — issue #909", async (t) => {
75
75
  const tmp = mkdtempSync(join(tmpdir(), "gsd-909-"));
76
- try {
77
- // Slice plan exists with tasks, but tasks/ directory is empty
78
- scaffoldSlicePlan(tmp, "M002", "S03");
79
-
80
- const ctx = makeContext(tmp);
81
- const result = await resolveDispatch(ctx);
82
-
83
- assert.equal(result.action, "dispatch", "should dispatch, not stop");
84
- assert.ok(result.action === "dispatch" && result.unitType === "plan-slice",
85
- `unitType should be plan-slice, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
86
- assert.ok(result.action === "dispatch" && result.unitId === "M002/S03",
87
- `unitId should be M002/S03, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
88
- } finally {
89
- rmSync(tmp, { recursive: true, force: true });
90
- }
76
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
77
+
78
+ // Slice plan exists with tasks, but tasks/ directory is empty
79
+ scaffoldSlicePlan(tmp, "M002", "S03");
80
+
81
+ const ctx = makeContext(tmp);
82
+ const result = await resolveDispatch(ctx);
83
+
84
+ assert.equal(result.action, "dispatch", "should dispatch, not stop");
85
+ assert.ok(result.action === "dispatch" && result.unitType === "plan-slice",
86
+ `unitType should be plan-slice, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
87
+ assert.ok(result.action === "dispatch" && result.unitId === "M002/S03",
88
+ `unitId should be M002/S03, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
91
89
  });
92
90
 
93
- test("dispatch: present task plan proceeds to execute-task normally", async () => {
91
+ test("dispatch: present task plan proceeds to execute-task normally", async (t) => {
94
92
  const tmp = mkdtempSync(join(tmpdir(), "gsd-909-ok-"));
95
- try {
96
- scaffoldSlicePlan(tmp, "M002", "S03");
97
- scaffoldTaskPlan(tmp, "M002", "S03", "T01");
98
-
99
- const ctx = makeContext(tmp);
100
- const result = await resolveDispatch(ctx);
101
-
102
- assert.equal(result.action, "dispatch");
103
- assert.ok(result.action === "dispatch" && result.unitType === "execute-task",
104
- `unitType should be execute-task, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
105
- assert.ok(result.action === "dispatch" && result.unitId === "M002/S03/T01",
106
- `unitId should be M002/S03/T01, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
107
- } finally {
108
- rmSync(tmp, { recursive: true, force: true });
109
- }
93
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
94
+
95
+ scaffoldSlicePlan(tmp, "M002", "S03");
96
+ scaffoldTaskPlan(tmp, "M002", "S03", "T01");
97
+
98
+ const ctx = makeContext(tmp);
99
+ const result = await resolveDispatch(ctx);
100
+
101
+ assert.equal(result.action, "dispatch");
102
+ assert.ok(result.action === "dispatch" && result.unitType === "execute-task",
103
+ `unitType should be execute-task, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
104
+ assert.ok(result.action === "dispatch" && result.unitId === "M002/S03/T01",
105
+ `unitId should be M002/S03/T01, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
110
106
  });
111
107
 
112
- test("dispatch: plan-slice recovery loop — second call after plan-slice still recovers cleanly", async () => {
108
+ test("dispatch: plan-slice recovery loop — second call after plan-slice still recovers cleanly", async (t) => {
113
109
  // Simulate: plan-slice ran but T01-PLAN.md is still missing (e.g. agent crashed mid-write).
114
110
  // Dispatch should still re-dispatch plan-slice, not hard-stop.
115
111
  const tmp = mkdtempSync(join(tmpdir(), "gsd-909-loop-"));
116
- try {
117
- scaffoldSlicePlan(tmp, "M002", "S03");
118
-
119
- const ctx = makeContext(tmp);
120
- const r1 = await resolveDispatch(ctx);
121
- assert.equal(r1.action, "dispatch");
122
- assert.ok(r1.action === "dispatch" && r1.unitType === "plan-slice");
123
-
124
- // Still no task plan written — dispatch again
125
- const r2 = await resolveDispatch(ctx);
126
- assert.equal(r2.action, "dispatch");
127
- assert.ok(r2.action === "dispatch" && r2.unitType === "plan-slice",
128
- "should keep dispatching plan-slice until task plans appear");
129
- } finally {
130
- rmSync(tmp, { recursive: true, force: true });
131
- }
112
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
113
+
114
+ scaffoldSlicePlan(tmp, "M002", "S03");
115
+
116
+ const ctx = makeContext(tmp);
117
+ const r1 = await resolveDispatch(ctx);
118
+ assert.equal(r1.action, "dispatch");
119
+ assert.ok(r1.action === "dispatch" && r1.unitType === "plan-slice");
120
+
121
+ // Still no task plan written — dispatch again
122
+ const r2 = await resolveDispatch(ctx);
123
+ assert.equal(r2.action, "dispatch");
124
+ assert.ok(r2.action === "dispatch" && r2.unitType === "plan-slice",
125
+ "should keep dispatching plan-slice until task plans appear");
132
126
  });