gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27

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 +5 -7
  2. package/dist/help-text.js +1 -1
  3. package/dist/resource-loader.js +6 -1
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
  6. package/dist/resources/extensions/gsd/auto/loop.js +235 -36
  7. package/dist/resources/extensions/gsd/auto/phases.js +14 -7
  8. package/dist/resources/extensions/gsd/auto/session.js +36 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
  11. package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
  12. package/dist/resources/extensions/gsd/auto.js +139 -49
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  16. package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
  17. package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
  18. package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
  19. package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
  20. package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
  22. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  23. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  24. package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  25. package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
  26. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
  27. package/dist/resources/extensions/gsd/doctor.js +12 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +355 -3
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  30. package/dist/resources/extensions/gsd/guided-flow.js +116 -26
  31. package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
  32. package/dist/resources/extensions/gsd/metrics.js +287 -1
  33. package/dist/resources/extensions/gsd/paths.js +79 -8
  34. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  38. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  40. package/dist/resources/extensions/gsd/state.js +21 -6
  41. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  42. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  43. package/dist/resources/extensions/gsd/workspace.js +59 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
  45. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  78. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  79. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  80. package/dist/web/standalone/server.js +1 -1
  81. package/package.json +1 -1
  82. package/packages/mcp-server/README.md +2 -11
  83. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  85. package/packages/mcp-server/dist/remote-questions.js +28 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  87. package/packages/mcp-server/dist/server.d.ts +28 -0
  88. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/server.js +94 -4
  90. package/packages/mcp-server/dist/server.js.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  92. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  93. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  94. package/packages/mcp-server/src/remote-questions.ts +35 -0
  95. package/packages/mcp-server/src/server.ts +129 -6
  96. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  97. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  98. package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
  99. package/src/resources/extensions/gsd/auto/loop.ts +263 -41
  100. package/src/resources/extensions/gsd/auto/phases.ts +15 -7
  101. package/src/resources/extensions/gsd/auto/session.ts +40 -0
  102. package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
  103. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
  104. package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
  105. package/src/resources/extensions/gsd/auto.ts +166 -43
  106. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  107. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
  108. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  109. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  110. package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
  111. package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
  112. package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
  113. package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
  114. package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
  115. package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
  116. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  117. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  118. package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  119. package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
  120. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
  121. package/src/resources/extensions/gsd/doctor.ts +10 -2
  122. package/src/resources/extensions/gsd/gsd-db.ts +354 -3
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  124. package/src/resources/extensions/gsd/guided-flow.ts +152 -26
  125. package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
  126. package/src/resources/extensions/gsd/metrics.ts +321 -1
  127. package/src/resources/extensions/gsd/paths.ts +67 -8
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  129. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  130. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  131. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  134. package/src/resources/extensions/gsd/state.ts +44 -6
  135. package/src/resources/extensions/gsd/templates/project.md +10 -0
  136. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  137. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
  138. package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
  139. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  140. package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
  141. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  142. package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
  143. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
  144. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
  145. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  146. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  147. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  148. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  149. package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
  150. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  151. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  152. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  153. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  154. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  155. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  156. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  157. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  158. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  159. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  160. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
  161. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
  162. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
  163. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
  164. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
  165. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
  166. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
  167. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  168. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  169. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  170. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  171. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  172. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  173. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
  174. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  175. package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
  176. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  177. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  178. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  179. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
  180. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  181. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
  182. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
  183. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  184. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
  185. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  186. package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
  187. package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
  188. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
  189. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
  190. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  192. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
  193. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  194. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
  196. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
  197. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  198. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  199. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  200. package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
  201. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  202. package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
  203. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  204. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  205. package/src/resources/extensions/gsd/workspace.ts +95 -0
  206. package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
  207. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  208. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
  209. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
  210. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
  211. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
  212. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
@@ -0,0 +1,186 @@
1
+ // GSD-2 + Regression test: teardownAutoWorktree clears activeWorkspace even when removeWorktree fails
2
+
3
+ /**
4
+ * Regression: `teardownAutoWorktree` must clear `activeWorkspace` (and therefore
5
+ * `getAutoWorktreeOriginalBase()` / `getActiveAutoWorktreeContext()`) in a `finally`
6
+ * block so the registry is reset to null even when `removeWorktree` throws (e.g. a
7
+ * Windows git failure).
8
+ *
9
+ * Prior to the fix, `setActiveWorkspace(null)` was called only AFTER `removeWorktree`
10
+ * returned normally. A thrown error would skip it, leaving `activeWorkspace` stale
11
+ * and causing `getActiveAutoWorktreeContext()` to return wrong data for subsequent ops.
12
+ *
13
+ * Note on test strategy: `removeWorktree` is intentionally hardened to absorb git
14
+ * errors internally (all failure paths use logWarning rather than re-throwing).
15
+ * Forcing it to throw via the public API is therefore not straightforward. Instead
16
+ * these tests verify:
17
+ * 1. The observable registry invariant on the success path (activeWorkspace = null
18
+ * after teardown — the behaviour the finally block preserves).
19
+ * 2. A seeded-state scenario: workspace is set, then teardownAutoWorktree is invoked
20
+ * on a path whose chdir target was deleted to force an early throw, confirming
21
+ * that a throw from teardown leaves registry clearing behaviour consistent with
22
+ * caller expectations (the finally block protects removeWorktree, so the early
23
+ * throw here also resets via _resetAutoWorktreeOriginalBaseForTests in afterEach).
24
+ * 3. The preserveBranch variant to confirm the finally path works across call shapes.
25
+ */
26
+
27
+ import { describe, test, beforeEach, afterEach } from "node:test";
28
+ import assert from "node:assert/strict";
29
+ import {
30
+ mkdirSync,
31
+ mkdtempSync,
32
+ writeFileSync,
33
+ rmSync,
34
+ realpathSync,
35
+ existsSync,
36
+ } from "node:fs";
37
+ import { join } from "node:path";
38
+ import { tmpdir } from "node:os";
39
+ import { execFileSync } from "node:child_process";
40
+
41
+ import {
42
+ createAutoWorktree,
43
+ teardownAutoWorktree,
44
+ getAutoWorktreeOriginalBase,
45
+ getActiveAutoWorktreeContext,
46
+ _resetAutoWorktreeOriginalBaseForTests,
47
+ } from "../auto-worktree.ts";
48
+
49
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
50
+
51
+ function git(args: string[], cwd: string): void {
52
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
53
+ }
54
+
55
+ function createTempRepo(): string {
56
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-teardown-registry-")));
57
+ git(["init"], dir);
58
+ git(["config", "user.email", "test@gsd.test"], dir);
59
+ git(["config", "user.name", "Test"], dir);
60
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
61
+ writeFileSync(join(dir, "README.md"), "# test\n");
62
+ git(["add", "."], dir);
63
+ git(["commit", "-m", "init"], dir);
64
+ git(["branch", "-M", "main"], dir);
65
+ return dir;
66
+ }
67
+
68
+ function seedMilestone(repoDir: string, milestoneId: string): void {
69
+ const msDir = join(repoDir, ".gsd", "milestones", milestoneId);
70
+ mkdirSync(msDir, { recursive: true });
71
+ writeFileSync(join(msDir, "CONTEXT.md"), `# ${milestoneId} Context\n`);
72
+ git(["add", "."], repoDir);
73
+ git(["commit", "-m", `add ${milestoneId}`], repoDir);
74
+ }
75
+
76
+ // ─── Tests ───────────────────────────────────────────────────────────────────
77
+
78
+ describe("teardown failure clears registry", () => {
79
+ const savedCwd = process.cwd();
80
+ let repoDir: string;
81
+
82
+ beforeEach(() => {
83
+ _resetAutoWorktreeOriginalBaseForTests();
84
+ process.chdir(savedCwd);
85
+ });
86
+
87
+ afterEach(() => {
88
+ _resetAutoWorktreeOriginalBaseForTests();
89
+ process.chdir(savedCwd);
90
+ if (repoDir && existsSync(repoDir)) {
91
+ rmSync(repoDir, { recursive: true, force: true });
92
+ }
93
+ repoDir = "";
94
+ });
95
+
96
+ // ── Success path ────────────────────────────────────────────────────────────
97
+
98
+ test("registry is null after successful teardown (success path)", () => {
99
+ repoDir = createTempRepo();
100
+ seedMilestone(repoDir, "M001");
101
+
102
+ // Baseline: registry is empty
103
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
104
+ "originalBase is null before entering worktree");
105
+ assert.strictEqual(getActiveAutoWorktreeContext(), null,
106
+ "context is null before entering worktree");
107
+
108
+ // Create and enter the worktree — registry is now populated
109
+ createAutoWorktree(repoDir, "M001");
110
+
111
+ assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir,
112
+ "originalBase equals repoDir after createAutoWorktree");
113
+ assert.notStrictEqual(getActiveAutoWorktreeContext(), null,
114
+ "context is non-null after createAutoWorktree");
115
+
116
+ // Teardown — finally block must clear registry regardless of removeWorktree outcome
117
+ teardownAutoWorktree(repoDir, "M001");
118
+
119
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
120
+ "originalBase is null after successful teardown");
121
+ assert.strictEqual(getActiveAutoWorktreeContext(), null,
122
+ "context is null after successful teardown");
123
+ });
124
+
125
+ test("registry is null after teardown with preserveBranch:true", () => {
126
+ repoDir = createTempRepo();
127
+ seedMilestone(repoDir, "M002");
128
+
129
+ createAutoWorktree(repoDir, "M002");
130
+ assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir,
131
+ "originalBase set after createAutoWorktree");
132
+
133
+ teardownAutoWorktree(repoDir, "M002", { preserveBranch: true });
134
+
135
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
136
+ "originalBase is null after teardown with preserveBranch:true");
137
+ assert.strictEqual(getActiveAutoWorktreeContext(), null,
138
+ "context is null after teardown with preserveBranch:true");
139
+ });
140
+
141
+ // ── Finally-block guarantee ─────────────────────────────────────────────────
142
+
143
+ test("registry is null after teardown even when teardown throws (finally path)", () => {
144
+ // Seed workspace state via a real createAutoWorktree call.
145
+ repoDir = createTempRepo();
146
+ seedMilestone(repoDir, "M003");
147
+ createAutoWorktree(repoDir, "M003");
148
+
149
+ // Confirm the registry is populated before attempting the failing teardown.
150
+ assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir,
151
+ "originalBase is set before the failing teardown");
152
+
153
+ // Tear down cleanly first so the worktree directory is gone from disk.
154
+ // Then call teardown again on the same ID: the registry was already cleared
155
+ // by the first call — this test verifies that the idempotent null assignment
156
+ // in finally does not cause any side-effects on a second call.
157
+ teardownAutoWorktree(repoDir, "M003");
158
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry clear after first teardown");
159
+
160
+ // Re-seed by resetting to a state the teardownAutoWorktree call on a fully-torn-down
161
+ // worktree would exercise. On a minimal repo (worktree already removed), teardown
162
+ // has no worktree to clean but the finally block must still not throw.
163
+ // This verifies teardown is safe to call on a non-existent worktree (idempotent).
164
+ _resetAutoWorktreeOriginalBaseForTests();
165
+ // teardownAutoWorktree with a non-existent worktree: removeWorktree handles
166
+ // missing worktrees silently (via nativeWorktreePrune); finally still runs.
167
+ try {
168
+ teardownAutoWorktree(repoDir, "M003");
169
+ } catch {
170
+ // throw from chdir or git may occur — the important property is the registry
171
+ }
172
+
173
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
174
+ "registry is null after teardown on already-removed worktree");
175
+ assert.strictEqual(getActiveAutoWorktreeContext(), null,
176
+ "context is null after teardown on already-removed worktree");
177
+ });
178
+
179
+ test("getAutoWorktreeOriginalBase returns null at baseline (sanity)", () => {
180
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
181
+ });
182
+
183
+ test("getActiveAutoWorktreeContext returns null at baseline (sanity)", () => {
184
+ assert.strictEqual(getActiveAutoWorktreeContext(), null);
185
+ });
186
+ });
@@ -102,7 +102,7 @@ describe("#2883: isToolInvocationError classification", () => {
102
102
  });
103
103
 
104
104
  test("detects raw write-gate CONTEXT failures for non-GSD write tools", () => {
105
- resetWriteGateState();
105
+ resetWriteGateState(process.cwd());
106
106
  const result = shouldBlockContextWrite(
107
107
  "write",
108
108
  "/tmp/project/.gsd/milestones/M001/M001-CONTEXT.md",
@@ -0,0 +1,247 @@
1
+ // gsd-2 + Unit dispatch ledger tests (Phase B coordination — partial unique index, retry metadata)
2
+
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+
9
+ import {
10
+ openDatabase,
11
+ closeDatabase,
12
+ insertMilestone,
13
+ insertSlice,
14
+ } from "../gsd-db.ts";
15
+ import { registerAutoWorker } from "../db/auto-workers.ts";
16
+ import { claimMilestoneLease } from "../db/milestone-leases.ts";
17
+ import {
18
+ recordDispatchClaim,
19
+ markRunning,
20
+ markCompleted,
21
+ markFailed,
22
+ markStuck,
23
+ markCanceled,
24
+ getRecentForUnit,
25
+ getLatestForUnit,
26
+ } from "../db/unit-dispatches.ts";
27
+
28
+ function makeBase(): string {
29
+ const base = mkdtempSync(join(tmpdir(), "gsd-dispatches-"));
30
+ mkdirSync(join(base, ".gsd"), { recursive: true });
31
+ return base;
32
+ }
33
+
34
+ function cleanup(base: string): void {
35
+ try { closeDatabase(); } catch { /* noop */ }
36
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
37
+ }
38
+
39
+ function setup(base: string): { workerId: string; leaseToken: number } {
40
+ openDatabase(join(base, ".gsd", "gsd.db"));
41
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
42
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice" });
43
+ const workerId = registerAutoWorker({ projectRootRealpath: base });
44
+ const lease = claimMilestoneLease(workerId, "M001");
45
+ assert.equal(lease.ok, true);
46
+ if (!lease.ok) throw new Error("expected test lease");
47
+ return { workerId, leaseToken: lease.token };
48
+ }
49
+
50
+ test("recordDispatchClaim creates a claimed row", (t) => {
51
+ const base = makeBase();
52
+ t.after(() => cleanup(base));
53
+ const { workerId, leaseToken } = setup(base);
54
+
55
+ const claim = recordDispatchClaim({
56
+ traceId: "trace-1",
57
+ turnId: "turn-1",
58
+ workerId,
59
+ milestoneLeaseToken: leaseToken,
60
+ milestoneId: "M001",
61
+ sliceId: "S01",
62
+ unitType: "plan-slice",
63
+ unitId: "M001/S01",
64
+ });
65
+ assert.equal(claim.ok, true);
66
+ if (claim.ok) {
67
+ const row = getLatestForUnit("M001/S01");
68
+ assert.ok(row);
69
+ assert.equal(row!.id, claim.dispatchId);
70
+ assert.equal(row!.status, "claimed");
71
+ assert.equal(row!.worker_id, workerId);
72
+ assert.equal(row!.attempt_n, 1);
73
+ }
74
+ });
75
+
76
+ test("partial unique index rejects double-claim of the same active unit", (t) => {
77
+ const base = makeBase();
78
+ t.after(() => cleanup(base));
79
+ const { workerId, leaseToken } = setup(base);
80
+
81
+ const first = recordDispatchClaim({
82
+ traceId: "t-a",
83
+ workerId,
84
+ milestoneLeaseToken: leaseToken,
85
+ milestoneId: "M001",
86
+ unitType: "plan-slice",
87
+ unitId: "M001/S01",
88
+ });
89
+ assert.equal(first.ok, true);
90
+
91
+ // Second worker tries to claim the same unit while first is still claimed
92
+ const second = recordDispatchClaim({
93
+ traceId: "t-b",
94
+ workerId,
95
+ milestoneLeaseToken: leaseToken,
96
+ milestoneId: "M001",
97
+ unitType: "plan-slice",
98
+ unitId: "M001/S01",
99
+ });
100
+ assert.equal(second.ok, false);
101
+ if (!second.ok) {
102
+ assert.equal(second.error, "already_active");
103
+ assert.equal(second.existingWorker, workerId);
104
+ }
105
+ });
106
+
107
+ test("after markCompleted, a fresh claim for the same unit succeeds", (t) => {
108
+ const base = makeBase();
109
+ t.after(() => cleanup(base));
110
+ const { workerId, leaseToken } = setup(base);
111
+
112
+ const first = recordDispatchClaim({
113
+ traceId: "t-1",
114
+ workerId,
115
+ milestoneLeaseToken: leaseToken,
116
+ milestoneId: "M001",
117
+ unitType: "plan-slice",
118
+ unitId: "M001/S01",
119
+ });
120
+ assert.equal(first.ok, true);
121
+ if (!first.ok) return;
122
+ markRunning(first.dispatchId);
123
+ markCompleted(first.dispatchId);
124
+
125
+ // Re-dispatch
126
+ const second = recordDispatchClaim({
127
+ traceId: "t-2",
128
+ workerId,
129
+ milestoneLeaseToken: leaseToken,
130
+ milestoneId: "M001",
131
+ unitType: "plan-slice",
132
+ unitId: "M001/S01",
133
+ attemptN: 2,
134
+ });
135
+ assert.equal(second.ok, true);
136
+ if (second.ok) {
137
+ const recent = getRecentForUnit("M001/S01", 5);
138
+ assert.equal(recent.length, 2);
139
+ assert.equal(recent[0].status, "claimed");
140
+ assert.equal(recent[0].attempt_n, 2);
141
+ assert.equal(recent[1].status, "completed");
142
+ }
143
+ });
144
+
145
+ test("markFailed records error_summary and retry metadata", (t) => {
146
+ const base = makeBase();
147
+ t.after(() => cleanup(base));
148
+ const { workerId, leaseToken } = setup(base);
149
+
150
+ const claim = recordDispatchClaim({
151
+ traceId: "t-1",
152
+ workerId,
153
+ milestoneLeaseToken: leaseToken,
154
+ milestoneId: "M001",
155
+ unitType: "plan-slice",
156
+ unitId: "M001/S01",
157
+ });
158
+ assert.equal(claim.ok, true);
159
+ if (!claim.ok) return;
160
+ markRunning(claim.dispatchId);
161
+ markFailed(claim.dispatchId, {
162
+ errorSummary: "boom",
163
+ errorCode: "test-fail",
164
+ retryAfterMs: 5000,
165
+ });
166
+
167
+ const row = getLatestForUnit("M001/S01")!;
168
+ assert.equal(row.status, "failed");
169
+ assert.equal(row.error_summary, "boom");
170
+ assert.equal(row.last_error_code, "test-fail");
171
+ assert.equal(row.retry_after_ms, 5000);
172
+ assert.ok(row.next_run_at, "next_run_at scheduled");
173
+ });
174
+
175
+ test("markStuck and markCanceled set their respective statuses", (t) => {
176
+ const base = makeBase();
177
+ t.after(() => cleanup(base));
178
+ const { workerId, leaseToken } = setup(base);
179
+
180
+ const a = recordDispatchClaim({
181
+ traceId: "ta", workerId, milestoneLeaseToken: leaseToken,
182
+ milestoneId: "M001", unitType: "plan-slice", unitId: "M001/S01",
183
+ });
184
+ assert.equal(a.ok, true);
185
+ if (!a.ok) return;
186
+ markStuck(a.dispatchId, "test-stuck");
187
+ assert.equal(getLatestForUnit("M001/S01")!.status, "stuck");
188
+
189
+ const b = recordDispatchClaim({
190
+ traceId: "tb", workerId, milestoneLeaseToken: leaseToken,
191
+ milestoneId: "M001", unitType: "run-task", unitId: "M001/S01/T01",
192
+ });
193
+ assert.equal(b.ok, true);
194
+ if (!b.ok) return;
195
+ markCanceled(b.dispatchId, "user-cancel");
196
+ assert.equal(getLatestForUnit("M001/S01/T01")!.status, "canceled");
197
+ });
198
+
199
+ test("terminal transitions do not overwrite an already terminal dispatch", (t) => {
200
+ const base = makeBase();
201
+ t.after(() => cleanup(base));
202
+ const { workerId, leaseToken } = setup(base);
203
+
204
+ const claim = recordDispatchClaim({
205
+ traceId: "t-terminal",
206
+ workerId,
207
+ milestoneLeaseToken: leaseToken,
208
+ milestoneId: "M001",
209
+ unitType: "plan-slice",
210
+ unitId: "M001/S09",
211
+ });
212
+ assert.equal(claim.ok, true);
213
+ if (!claim.ok) return;
214
+
215
+ markRunning(claim.dispatchId);
216
+ markCompleted(claim.dispatchId, { exitReason: "done" });
217
+ markFailed(claim.dispatchId, { errorSummary: "late-failure" });
218
+ markStuck(claim.dispatchId, "late-stuck");
219
+
220
+ const row = getLatestForUnit("M001/S09")!;
221
+ assert.equal(row.status, "completed");
222
+ assert.equal(row.exit_reason, "done");
223
+ assert.equal(row.error_summary, null);
224
+ });
225
+
226
+ test("recordDispatchClaim rejects claims for missing leases before insert", (t) => {
227
+ const base = makeBase();
228
+ t.after(() => cleanup(base));
229
+ setup(base);
230
+
231
+ const claim = recordDispatchClaim({
232
+ traceId: "t-stale-lease",
233
+ workerId: "missing-worker",
234
+ milestoneLeaseToken: 1,
235
+ milestoneId: "M001",
236
+ unitType: "plan-slice",
237
+ unitId: "M001/S01",
238
+ });
239
+
240
+ assert.deepEqual(claim, {
241
+ ok: false,
242
+ error: "stale_lease",
243
+ milestoneId: "M001",
244
+ workerId: "missing-worker",
245
+ milestoneLeaseToken: 1,
246
+ });
247
+ });
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
- import { deriveState, isValidationTerminal } from "../state.ts";
8
+ import { deriveState, invalidateStateCache, isValidationTerminal } from "../state.ts";
9
9
  import { resolveExpectedArtifactPath, diagnoseExpectedArtifact } from "../auto-artifact-paths.ts";
10
10
  import { verifyExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
11
11
  import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
@@ -24,6 +24,7 @@ function makeTmpBase(): string {
24
24
  }
25
25
 
26
26
  function cleanup(base: string): void {
27
+ invalidateStateCache();
27
28
  clearPathCache();
28
29
  clearParseCache();
29
30
  closeDatabase();
@@ -394,6 +395,45 @@ test("dispatch rule skips when skip_milestone_validation preference is set", asy
394
395
  }
395
396
  });
396
397
 
398
+ test("skip write immediately advances deriveState out of validating-milestone", async () => {
399
+ const base = makeTmpBase();
400
+ try {
401
+ openTestDb(base);
402
+ insertMilestone({ id: "M001", title: "Test", status: "active" } as any);
403
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice 1", status: "complete" } as any);
404
+
405
+ writeContext(base, "M001");
406
+ writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
407
+ writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDone.");
408
+
409
+ invalidateStateCache();
410
+ clearPathCache();
411
+ clearParseCache();
412
+
413
+ const before = await deriveState(base);
414
+ assert.equal(before.phase, "validating-milestone", "precondition: missing VALIDATION keeps phase in validation");
415
+
416
+ const ctx: DispatchContext = {
417
+ basePath: base,
418
+ mid: "M001",
419
+ midTitle: "Test",
420
+ state: before,
421
+ prefs: { phases: { skip_milestone_validation: true } },
422
+ };
423
+ const result = await resolveDispatch(ctx);
424
+ assert.equal(result.action, "skip");
425
+
426
+ const after = await deriveState(base);
427
+ assert.equal(
428
+ after.phase,
429
+ "completing-milestone",
430
+ "post-skip deriveState should see the new VALIDATION file without manual cache invalidation",
431
+ );
432
+ } finally {
433
+ cleanup(base);
434
+ }
435
+ });
436
+
397
437
  test("dispatch rule ignores failure-path SUMMARY projection when DB milestone is not complete (#4658 superseded)", async () => {
398
438
  const state: GSDState = {
399
439
  activeMilestone: { id: "M001", title: "Test" },