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,226 @@
1
+ // GSD-2 + gsd-db workspace-scoped connection cache tests
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ realpathSync,
9
+ rmSync,
10
+ } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+
14
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
15
+ import {
16
+ openDatabaseByWorkspace,
17
+ openDatabaseByScope,
18
+ closeDatabaseByWorkspace,
19
+ closeAllDatabases,
20
+ _getDbCache,
21
+ _getAdapter,
22
+ } from "../gsd-db.ts";
23
+
24
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Create a minimal project directory with the artifacts that make
28
+ * createWorkspace() resolve it as a proper project root (not a bare temp dir).
29
+ * Returns the realpath-normalised absolute path.
30
+ */
31
+ function makeProjectDir(): string {
32
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-db-ws-scope-")));
33
+ // hasGsdBootstrapArtifacts checks for .gsd/milestones or .gsd/PREFERENCES.md
34
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
35
+ return dir;
36
+ }
37
+
38
+ /**
39
+ * Create a worktree path inside a project's .gsd/worktrees/<MID>/ layout.
40
+ * createWorkspace() will detect the /.gsd/worktrees/ segment and resolve the
41
+ * project root back to `projectDir`.
42
+ */
43
+ function makeWorktreeDir(projectDir: string, mid: string): string {
44
+ const worktreeDir = join(projectDir, ".gsd", "worktrees", mid);
45
+ mkdirSync(worktreeDir, { recursive: true });
46
+ return worktreeDir;
47
+ }
48
+
49
+ // ─── Suite: same realpath → same identityKey → same DB instance ──────────────
50
+
51
+ describe("openDatabaseByWorkspace: same project reuses connection", () => {
52
+ let projectDir: string;
53
+
54
+ beforeEach(() => {
55
+ projectDir = makeProjectDir();
56
+ });
57
+
58
+ afterEach(() => {
59
+ closeAllDatabases();
60
+ rmSync(projectDir, { recursive: true, force: true });
61
+ });
62
+
63
+ test("two createWorkspace calls with the same path share identityKey", () => {
64
+ const ws1 = createWorkspace(projectDir);
65
+ const ws2 = createWorkspace(projectDir);
66
+ assert.equal(ws1.identityKey, ws2.identityKey);
67
+ });
68
+
69
+ test("openDatabaseByWorkspace returns the same DB adapter for the same project", () => {
70
+ const ws1 = createWorkspace(projectDir);
71
+ const ws2 = createWorkspace(projectDir);
72
+
73
+ const ok1 = openDatabaseByWorkspace(ws1);
74
+ assert.ok(ok1, "first open should succeed");
75
+ const adapter1 = _getAdapter();
76
+
77
+ const ok2 = openDatabaseByWorkspace(ws2);
78
+ assert.ok(ok2, "second open should succeed");
79
+ const adapter2 = _getAdapter();
80
+
81
+ assert.equal(adapter1, adapter2, "same project → same DB adapter instance");
82
+ assert.equal(_getDbCache().size, 1, "only one cache entry for same project");
83
+ });
84
+ });
85
+
86
+ // ─── Suite: different projects → different DB instances ──────────────────────
87
+
88
+ describe("openDatabaseByWorkspace: different projects get separate connections", () => {
89
+ let projectA: string;
90
+ let projectB: string;
91
+
92
+ beforeEach(() => {
93
+ projectA = makeProjectDir();
94
+ projectB = makeProjectDir();
95
+ });
96
+
97
+ afterEach(() => {
98
+ closeAllDatabases();
99
+ rmSync(projectA, { recursive: true, force: true });
100
+ rmSync(projectB, { recursive: true, force: true });
101
+ });
102
+
103
+ test("two different projects produce different identityKeys", () => {
104
+ const wsA = createWorkspace(projectA);
105
+ const wsB = createWorkspace(projectB);
106
+ assert.notEqual(wsA.identityKey, wsB.identityKey);
107
+ });
108
+
109
+ test("opening two different projects stores two cache entries", () => {
110
+ const wsA = createWorkspace(projectA);
111
+ const wsB = createWorkspace(projectB);
112
+
113
+ openDatabaseByWorkspace(wsA);
114
+ const adapterAfterA = _getAdapter();
115
+
116
+ openDatabaseByWorkspace(wsB);
117
+ const adapterAfterB = _getAdapter();
118
+
119
+ assert.notEqual(adapterAfterA, adapterAfterB, "different projects → different adapter instances");
120
+ assert.equal(_getDbCache().size, 2, "two cache entries for two distinct projects");
121
+ });
122
+ });
123
+
124
+ // ─── Suite: sibling worktrees share the same DB instance ─────────────────────
125
+
126
+ describe("openDatabaseByWorkspace: sibling worktrees share DB connection", () => {
127
+ let projectDir: string;
128
+
129
+ beforeEach(() => {
130
+ projectDir = makeProjectDir();
131
+ });
132
+
133
+ afterEach(() => {
134
+ closeAllDatabases();
135
+ rmSync(projectDir, { recursive: true, force: true });
136
+ });
137
+
138
+ test("worktree path resolves to same identityKey as project root", () => {
139
+ const worktreeDir = makeWorktreeDir(projectDir, "M001");
140
+ const wsProject = createWorkspace(projectDir);
141
+ const wsWorktree = createWorkspace(worktreeDir);
142
+ assert.equal(
143
+ wsProject.identityKey,
144
+ wsWorktree.identityKey,
145
+ "project root and sibling worktree share identityKey",
146
+ );
147
+ });
148
+
149
+ test("opening via project path and via worktree path yields the same DB adapter", () => {
150
+ const worktreeDir = makeWorktreeDir(projectDir, "M001");
151
+ const wsProject = createWorkspace(projectDir);
152
+ const wsWorktree = createWorkspace(worktreeDir);
153
+
154
+ openDatabaseByWorkspace(wsProject);
155
+ const adapterProject = _getAdapter();
156
+
157
+ openDatabaseByWorkspace(wsWorktree);
158
+ const adapterWorktree = _getAdapter();
159
+
160
+ assert.equal(
161
+ adapterProject,
162
+ adapterWorktree,
163
+ "sibling worktree reuses the same DB adapter as the project root",
164
+ );
165
+ assert.equal(_getDbCache().size, 1, "only one cache entry for project + sibling worktree");
166
+ });
167
+ });
168
+
169
+ // ─── Suite: closing removes only the targeted cache entry ─────────────────────
170
+
171
+ describe("closeDatabaseByWorkspace: removes only the targeted cache entry", () => {
172
+ let projectA: string;
173
+ let projectB: string;
174
+
175
+ beforeEach(() => {
176
+ projectA = makeProjectDir();
177
+ projectB = makeProjectDir();
178
+ });
179
+
180
+ afterEach(() => {
181
+ closeAllDatabases();
182
+ rmSync(projectA, { recursive: true, force: true });
183
+ rmSync(projectB, { recursive: true, force: true });
184
+ });
185
+
186
+ test("closing workspace A removes only A from the cache", () => {
187
+ const wsA = createWorkspace(projectA);
188
+ const wsB = createWorkspace(projectB);
189
+
190
+ openDatabaseByWorkspace(wsA);
191
+ openDatabaseByWorkspace(wsB);
192
+ assert.equal(_getDbCache().size, 2, "precondition: two cache entries");
193
+
194
+ closeDatabaseByWorkspace(wsA);
195
+
196
+ assert.equal(_getDbCache().size, 1, "one entry remains after closing A");
197
+ assert.ok(!_getDbCache().has(wsA.identityKey), "A's entry is gone");
198
+ assert.ok(_getDbCache().has(wsB.identityKey), "B's entry is still present");
199
+ });
200
+
201
+ test("closing the active workspace via closeDatabaseByWorkspace nulls currentDb", () => {
202
+ const wsA = createWorkspace(projectA);
203
+
204
+ openDatabaseByWorkspace(wsA);
205
+ assert.ok(_getAdapter() !== null, "precondition: adapter is open");
206
+
207
+ // Make wsA the active connection explicitly.
208
+ openDatabaseByWorkspace(wsA);
209
+ closeDatabaseByWorkspace(wsA);
210
+
211
+ // After closing the active connection, the global adapter should be null.
212
+ assert.equal(_getAdapter(), null, "currentDb should be null after closing active workspace");
213
+ assert.equal(_getDbCache().size, 0, "cache should be empty after closing sole entry");
214
+ });
215
+
216
+ test("openDatabaseByScope delegates to workspace correctly", () => {
217
+ const ws = createWorkspace(projectA);
218
+ const scope = scopeMilestone(ws, "M001");
219
+
220
+ const ok = openDatabaseByScope(scope);
221
+ assert.ok(ok, "openDatabaseByScope should succeed");
222
+ assert.ok(_getDbCache().has(ws.identityKey), "cache entry exists after openDatabaseByScope");
223
+
224
+ closeDatabaseByWorkspace(ws);
225
+ });
226
+ });
@@ -0,0 +1,66 @@
1
+ // GSD-2 + gsd-root-canonical: gsdRoot() result is realpath-canonicalized before caching
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ realpathSync,
9
+ rmSync,
10
+ symlinkSync,
11
+ } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { randomUUID } from "node:crypto";
15
+
16
+ import { gsdRoot, _clearGsdRootCache } from "../paths.ts";
17
+
18
+ // ─── Tests ───────────────────────────────────────────────────────────────────
19
+
20
+ describe("gsdRoot: returns realpath-canonicalized result", () => {
21
+ let projectDir: string;
22
+
23
+ beforeEach(() => {
24
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-root-canon-")));
25
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
26
+ _clearGsdRootCache();
27
+ });
28
+
29
+ afterEach(() => {
30
+ _clearGsdRootCache();
31
+ rmSync(projectDir, { recursive: true, force: true });
32
+ });
33
+
34
+ test("gsdRoot from a canonical project path returns a realpath-canonicalized result", () => {
35
+ const result = gsdRoot(projectDir);
36
+ const canonical = realpathSync(join(projectDir, ".gsd"));
37
+ assert.equal(result, canonical, "gsdRoot must return the realpath of the .gsd directory");
38
+ });
39
+
40
+ test("gsdRoot via a symlinked project path returns the realpath-canonicalized .gsd", (t) => {
41
+ // Create a symlink pointing to projectDir
42
+ const linkPath = join(tmpdir(), `gsd-root-link-${randomUUID()}`);
43
+ symlinkSync(projectDir, linkPath);
44
+ t.after(() => {
45
+ try { rmSync(linkPath); } catch { /* ignore */ }
46
+ });
47
+
48
+ _clearGsdRootCache();
49
+
50
+ const result = gsdRoot(linkPath);
51
+ // The canonical .gsd is under the realpath of projectDir, not the symlink
52
+ const canonicalGsd = realpathSync(join(projectDir, ".gsd"));
53
+
54
+ assert.equal(
55
+ result,
56
+ canonicalGsd,
57
+ `gsdRoot via symlink ("${linkPath}") must return the realpath'd .gsd ("${canonicalGsd}"), not a symlink-based path`,
58
+ );
59
+
60
+ // Also verify that the result does NOT contain the symlink in its path
61
+ assert.ok(
62
+ !result.startsWith(linkPath),
63
+ `gsdRoot result must not start with the symlink path "${linkPath}"`,
64
+ );
65
+ });
66
+ });
@@ -1,10 +1,13 @@
1
1
  /**
2
- * GSD2 — regression test for #5187: gsdRoot() must refuse to use the global
3
- * GSD home (~/.gsd) as a project .gsd directory when basePath resolves to
4
- * $HOME. Paths under ~/.gsd/projects/<hash>/ remain valid.
2
+ * GSD2 — regression tests for #5187 and git-root anchor guard:
5
3
  *
6
- * Before the fix, gsdRoot(homedir()) returned ~/.gsd silently and downstream
7
- * writes polluted the user's global state directory. After the fix, it throws.
4
+ * #5187: gsdRoot() must refuse to use the global GSD home (~/.gsd) as a
5
+ * project .gsd directory when basePath resolves to $HOME. Paths under
6
+ * ~/.gsd/projects/<hash>/ remain valid.
7
+ *
8
+ * git-root anchor guard: when $HOME is itself a git repo and ~/.gsd exists,
9
+ * gsdRoot() must NOT return ~/.gsd for a subdir basePath like ~/projects/foo.
10
+ * It should fall through to step 4 (creation fallback) instead.
8
11
  */
9
12
 
10
13
  import { describe, test, beforeEach, afterEach } from 'node:test';
@@ -12,6 +15,7 @@ import assert from 'node:assert/strict';
12
15
  import { mkdtempSync, mkdirSync, rmSync, realpathSync } from 'node:fs';
13
16
  import { tmpdir } from 'node:os';
14
17
  import { join } from 'node:path';
18
+ import { spawnSync } from 'node:child_process';
15
19
 
16
20
  import { gsdRoot, _clearGsdRootCache } from '../paths.ts';
17
21
 
@@ -84,3 +88,62 @@ describe('gsdRoot() refuses ~/.gsd as project state when basePath is $HOME (#518
84
88
  }
85
89
  });
86
90
  });
91
+
92
+ describe('git-root anchor guard: subdir basePath must not resolve to ~/.gsd', () => {
93
+ let fakeHome: string;
94
+ let subDir: string;
95
+ let savedHome: string | undefined;
96
+ let savedUserProfile: string | undefined;
97
+ let savedGsdHome: string | undefined;
98
+
99
+ beforeEach(() => {
100
+ // Create a tmpdir that will act as both $HOME and a git repo root.
101
+ fakeHome = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-anchor-guard-')));
102
+ // Init a bare-minimum git repo so git rev-parse --show-toplevel returns fakeHome.
103
+ spawnSync('git', ['init', fakeHome], { encoding: 'utf-8' });
104
+ // Create ~/.gsd (the global home that must NOT be used for project subdirs).
105
+ mkdirSync(join(fakeHome, '.gsd'), { recursive: true });
106
+ // Create a subdir inside the git repo — this is the project basePath.
107
+ subDir = join(fakeHome, 'projects', 'foo');
108
+ mkdirSync(subDir, { recursive: true });
109
+
110
+ savedHome = process.env.HOME;
111
+ savedUserProfile = process.env.USERPROFILE;
112
+ savedGsdHome = process.env.GSD_HOME;
113
+
114
+ process.env.HOME = fakeHome;
115
+ process.env.USERPROFILE = fakeHome;
116
+ delete process.env.GSD_HOME;
117
+
118
+ _clearGsdRootCache();
119
+ });
120
+
121
+ afterEach(() => {
122
+ if (savedHome === undefined) delete process.env.HOME;
123
+ else process.env.HOME = savedHome;
124
+ if (savedUserProfile === undefined) delete process.env.USERPROFILE;
125
+ else process.env.USERPROFILE = savedUserProfile;
126
+ if (savedGsdHome === undefined) delete process.env.GSD_HOME;
127
+ else process.env.GSD_HOME = savedGsdHome;
128
+
129
+ _clearGsdRootCache();
130
+ rmSync(fakeHome, { recursive: true, force: true });
131
+ });
132
+
133
+ test('does NOT return ~/.gsd when $HOME is a git repo and basePath is a subdir', () => {
134
+ // fakeHome IS the git root AND $HOME, so git rev-parse returns fakeHome,
135
+ // and ~/.gsd (fakeHome/.gsd) exists. The guard must skip that candidate
136
+ // and fall through to the creation fallback: subDir/.gsd.
137
+ const result = gsdRoot(subDir);
138
+ assert.notEqual(
139
+ result,
140
+ join(fakeHome, '.gsd'),
141
+ 'gsdRoot must not return ~/.gsd for a subdir basePath',
142
+ );
143
+ assert.equal(
144
+ result,
145
+ join(subDir, '.gsd'),
146
+ 'gsdRoot should fall through to the creation fallback for a subdir',
147
+ );
148
+ });
149
+ });
@@ -86,8 +86,8 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
86
86
  assert.ok(prompt.includes(TID), "must mention task id");
87
87
  assert.ok(prompt.includes(T_TITLE), "must mention task title");
88
88
  assert.ok(
89
- prompt.includes("gsd_complete_task"),
90
- "must instruct calling the canonical gsd_complete_task tool",
89
+ prompt.includes("gsd_task_complete"),
90
+ "must instruct calling the canonical gsd_task_complete tool",
91
91
  );
92
92
  assert.ok(
93
93
  prompt.includes(base),
@@ -110,8 +110,8 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
110
110
  assert.ok(prompt.includes(SID), "must mention slice id");
111
111
  assert.ok(prompt.includes(S_TITLE), "must mention slice title");
112
112
  assert.ok(
113
- prompt.includes("gsd_complete_slice"),
114
- "must instruct calling gsd_complete_slice (was in guided-complete-slice.md)",
113
+ prompt.includes("gsd_slice_complete"),
114
+ "must instruct calling gsd_slice_complete (was in guided-complete-slice.md)",
115
115
  );
116
116
  assert.ok(
117
117
  prompt.includes(base),
@@ -160,7 +160,7 @@ describe("auto-worktree lifecycle", () => {
160
160
  assert.ok(realWtPath.startsWith(storage), "git registered the symlink-resolved worktree path");
161
161
 
162
162
  _resetAutoWorktreeOriginalBaseForTests();
163
- process.chdir(join(realWtPath, ".gsd", "milestones", "M001"));
163
+ process.chdir(realWtPath);
164
164
 
165
165
  assert.ok(isInAutoWorktree(tempDir), "structural detection works without module originalBase");
166
166
  const resolved = getAutoWorktreePath(realWtPath, "M001");
@@ -169,7 +169,7 @@ describe("auto-worktree lifecycle", () => {
169
169
  assert.equal(existsSync(join(realWtPath, ".gsd", "worktrees", "M001")), false);
170
170
 
171
171
  enterAutoWorktree(tempDir, "M001");
172
- process.chdir(join(realWtPath, ".gsd", "milestones", "M001"));
172
+ process.chdir(realWtPath);
173
173
  assert.deepStrictEqual(
174
174
  getActiveAutoWorktreeContext(),
175
175
  {
@@ -282,7 +282,7 @@ describe("auto-worktree lifecycle", () => {
282
282
  teardownAutoWorktree(tempDir, "M010");
283
283
  });
284
284
 
285
- test("#778: reconcile plan checkboxes on re-attach", async () => {
285
+ test("#778: re-attach does not reconcile plan checkboxes into a worktree-local .gsd projection", async () => {
286
286
  tempDir = createTempRepo();
287
287
  const msDir = join(tempDir, ".gsd", "milestones", "M003");
288
288
  mkdirSync(msDir, { recursive: true });
@@ -322,23 +322,31 @@ describe("auto-worktree lifecycle", () => {
322
322
  "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
323
323
  );
324
324
 
325
- // Create worktree re-attached to existing milestone branch (T02 still [ ] in branch)
325
+ // Re-attaching the worktree should not reconcile the branch copy from the
326
+ // project-root plan. The project root stays canonical; the worktree keeps
327
+ // the milestone branch's tracked file contents until a git operation
328
+ // changes them.
326
329
  const wtPath = createAutoWorktree(tempDir, "M004");
327
330
 
328
331
  try {
329
332
  const wtPlanPath = join(wtPath, planRelPath);
330
- assert.ok(existsSync(wtPlanPath), "plan file exists in worktree after re-attach");
333
+ assert.ok(existsSync(wtPlanPath), "tracked plan file remains present in the worktree branch");
331
334
 
332
335
  const wtPlan = read(wtPlanPath, "utf-8");
333
- assert.ok(wtPlan.includes("- [x] **T02:"), "T02 should be [x] after reconciliation (was [ ] on branch)");
334
- assert.ok(wtPlan.includes("- [x] **T01:"), "T01 stays [x]");
335
- assert.ok(wtPlan.includes("- [ ] **T03:"), "T03 stays [ ] (not in root either)");
336
+ assert.ok(wtPlan.includes("- [ ] **T02:"), "worktree branch should retain its unreconciled T02 [ ] state");
337
+ assert.ok(wtPlan.includes("- [x] **T01:"), "worktree branch should retain T01 [x]");
338
+ assert.ok(wtPlan.includes("- [ ] **T03:"), "worktree branch should retain T03 [ ]");
339
+
340
+ const rootPlan = read(join(tempDir, planRelPath), "utf-8");
341
+ assert.ok(rootPlan.includes("- [x] **T02:"), "canonical root plan retains the newer T02 [x] state");
342
+ assert.ok(rootPlan.includes("- [x] **T01:"), "canonical root plan retains T01 [x]");
343
+ assert.ok(rootPlan.includes("- [ ] **T03:"), "canonical root plan retains T03 [ ]");
336
344
  } finally {
337
345
  teardownAutoWorktree(tempDir, "M004");
338
346
  }
339
347
  });
340
348
 
341
- test("#2791: mcp.json copied into worktree via copyPlanningArtifacts", () => {
349
+ test("#2791: mcp.json is not copied into worktree on creation after copyPlanningArtifacts removal", () => {
342
350
  tempDir = createTempRepo();
343
351
  const msDir = join(tempDir, ".gsd", "milestones", "M003");
344
352
  mkdirSync(msDir, { recursive: true });
@@ -347,7 +355,8 @@ describe("auto-worktree lifecycle", () => {
347
355
  run("git commit -m \"add milestone\"", tempDir);
348
356
 
349
357
  // Create mcp.json in .gsd/ AFTER the commit (untracked, like real usage).
350
- // copyPlanningArtifacts should copy it into the worktree's .gsd/.
358
+ // Phase C removed copyPlanningArtifacts, so creation should not seed a
359
+ // second worktree-local copy.
351
360
  writeFileSync(
352
361
  join(tempDir, ".gsd", "mcp.json"),
353
362
  JSON.stringify({ servers: { test: { command: "echo" } } }),
@@ -356,9 +365,10 @@ describe("auto-worktree lifecycle", () => {
356
365
  const wtPath = createAutoWorktree(tempDir, "M003");
357
366
 
358
367
  try {
359
- assert.ok(
368
+ assert.equal(
360
369
  existsSync(join(wtPath, ".gsd", "mcp.json")),
361
- "mcp.json should be copied into worktree .gsd/ on creation",
370
+ false,
371
+ "mcp.json should not be copied into worktree .gsd/ on creation",
362
372
  );
363
373
  } finally {
364
374
  teardownAutoWorktree(tempDir, "M003");
@@ -240,17 +240,31 @@ describe('doctor-proactive', async () => {
240
240
  cleanups.push(dir);
241
241
  mkdirSync(join(dir, ".gsd"), { recursive: true });
242
242
 
243
- // Write a stale lock
244
- writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify({
245
- pid: 9999999, startedAt: "2026-03-10T00:00:00Z",
246
- unitType: "execute-task", unitId: "M001/S01/T01",
247
- unitStartedAt: "2026-03-10T00:01:00Z", completedUnits: 3,
248
- }));
243
+ // Phase C pt 2: stale lock state lives in the workers table now.
244
+ // Open the DB, insert a fake stale worker row directly (PID 9999999
245
+ // is functionally guaranteed dead), then close — the doctor will
246
+ // re-open via its own path.
247
+ const { openDatabase, _getAdapter } = await import("../../gsd-db.ts");
248
+ const { randomUUID } = await import("node:crypto");
249
+ openDatabase(join(dir, ".gsd", "gsd.db"));
250
+ const db = _getAdapter()!;
251
+ db.prepare(
252
+ `INSERT INTO workers (worker_id, host, pid, started_at, version, last_heartbeat_at, status, project_root_realpath)
253
+ VALUES (:w, 'test-host', 9999999, '2026-03-10T00:00:00Z', 'test', '1970-01-01T00:00:00.000Z', 'active', :root)`,
254
+ ).run({ ":w": `test-fake-${randomUUID().slice(0, 8)}`, ":root": dir });
255
+ const { closeDatabase } = await import("../../gsd-db.ts");
256
+ closeDatabase();
249
257
 
250
- const result = await preDispatchHealthGate(dir);
251
- assert.ok(result.proceed, "gate passes after auto-clearing stale lock");
252
- assert.ok(result.fixesApplied.some(f => f.includes("cleared stale auto.lock")), "reports lock cleared");
253
- assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "lock file removed");
258
+ try {
259
+ const result = await preDispatchHealthGate(dir);
260
+ assert.ok(result.proceed, "gate passes after auto-clearing stale lock");
261
+ assert.ok(
262
+ result.fixesApplied.some(f => f.includes("cleared stale") || f.includes("cleared stale auto.lock")),
263
+ `reports lock cleared (got: ${result.fixesApplied.join(", ")})`,
264
+ );
265
+ } finally {
266
+ closeDatabase();
267
+ }
254
268
  });
255
269
 
256
270
  test('health gate: corrupt merge state auto-healed', async () => {
@@ -60,20 +60,24 @@ describe('doctor-runtime', async () => {
60
60
 
61
61
  try {
62
62
  // ─── Test 1: Stale crash lock detection & fix ─────────────────────
63
- test('stale_crash_lock', async () => {
63
+ test('stale_crash_lock', async (t) => {
64
64
  const dir = createMinimalProject();
65
65
  cleanups.push(dir);
66
66
 
67
- // Write a lock file with a PID that is definitely dead (use PID 1 million+)
68
- const lockData = {
69
- pid: 9999999,
70
- startedAt: "2026-03-10T00:00:00Z",
71
- unitType: "execute-task",
72
- unitId: "M001/S01/T01",
73
- unitStartedAt: "2026-03-10T00:01:00Z",
74
- completedUnits: 3,
75
- };
76
- writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
67
+ // Phase C pt 2: stale lock state lives in the workers table now.
68
+ // Insert a fake stale worker row directly (PID 9999999 is dead).
69
+ const { openDatabase, _getAdapter } = await import("../../gsd-db.ts");
70
+ const gsdDb = await import("../../gsd-db.ts");
71
+ t.after(() => { gsdDb.closeDatabase(); });
72
+ const { randomUUID } = await import("node:crypto");
73
+ openDatabase(join(dir, ".gsd", "gsd.db"));
74
+ const db = _getAdapter()!;
75
+ db.prepare(
76
+ `INSERT INTO workers (worker_id, host, pid, started_at, version, last_heartbeat_at, status, project_root_realpath)
77
+ VALUES (:w, 'test-host', 9999999, '2026-03-10T00:00:00Z', 'test', '1970-01-01T00:00:00.000Z', 'active', :root)`,
78
+ ).run({ ":w": `test-fake-${randomUUID().slice(0, 8)}`, ":root": dir });
79
+ // Leave DB open — runGSDDoctor's readCrashLock relies on the
80
+ // currently-open DB connection (it does not open one of its own).
77
81
 
78
82
  const detect = await runGSDDoctor(dir);
79
83
  const lockIssues = detect.issues.filter(i => i.code === "stale_crash_lock");
@@ -82,8 +86,15 @@ describe('doctor-runtime', async () => {
82
86
  assert.ok(lockIssues[0]?.fixable === true, "stale lock is fixable");
83
87
 
84
88
  const fixed = await runGSDDoctor(dir, { fix: true });
85
- assert.ok(fixed.fixesApplied.some(f => f.includes("cleared stale auto.lock")), "fix clears stale lock");
86
- assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "auto.lock removed after fix");
89
+ assert.ok(
90
+ fixed.fixesApplied.some(f => f.includes("cleared stale")),
91
+ `fix clears stale lock (got: ${fixed.fixesApplied.join(", ")})`,
92
+ );
93
+
94
+ // Close DB so subsequent tests in this file (which expect a clean
95
+ // state) don't see this test's connection lingering.
96
+ const { closeDatabase } = await import("../../gsd-db.ts");
97
+ closeDatabase();
87
98
  });
88
99
 
89
100
  // ─── Test 2: No false positive for missing lock ───────────────────
@@ -417,18 +428,19 @@ node_modules/
417
428
  const dir = createMinimalProject();
418
429
  cleanups.push(dir);
419
430
 
420
- // Create lock dir + auto.lock with PID 1 (init/launchd — always alive, never our own PID)
431
+ // Create lock dir + insert a live worker row (PID 1 = init/launchd —
432
+ // always alive, never our own PID). Phase C pt 2: worker liveness
433
+ // lives in the workers table. last_heartbeat_at = now → not stale.
421
434
  const lockDir = join(dir, ".gsd.lock");
422
435
  mkdirSync(lockDir, { recursive: true });
423
- const liveLockData = {
424
- pid: 1,
425
- startedAt: new Date().toISOString(),
426
- unitType: "execute-task",
427
- unitId: "M001/S01/T01",
428
- unitStartedAt: new Date().toISOString(),
429
- completedUnits: 1,
430
- };
431
- writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(liveLockData, null, 2));
436
+ const { openDatabase, _getAdapter } = await import("../../gsd-db.ts");
437
+ const { randomUUID } = await import("node:crypto");
438
+ openDatabase(join(dir, ".gsd", "gsd.db"));
439
+ const db = _getAdapter()!;
440
+ db.prepare(
441
+ `INSERT INTO workers (worker_id, host, pid, started_at, version, last_heartbeat_at, status, project_root_realpath)
442
+ VALUES (:w, 'test-host', 1, :now, 'test', :now, 'active', :root)`,
443
+ ).run({ ":w": `test-fake-${randomUUID().slice(0, 8)}`, ":now": new Date().toISOString(), ":root": dir });
432
444
 
433
445
  const detect = await runGSDDoctor(dir);
434
446
  const strandedIssues = detect.issues.filter(i => i.code === "stranded_lock_directory");