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,369 @@
1
+ // GSD-2 + Integration regression suite for workspace collapse (feat/workspace-collapse)
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ writeFileSync,
9
+ existsSync,
10
+ rmSync,
11
+ realpathSync,
12
+ } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+ import { execFileSync } from "node:child_process";
16
+
17
+ import { createWorkspace, scopeMilestone } from "../../workspace.ts";
18
+ import {
19
+ gsdRoot,
20
+ clearPathCache,
21
+ _clearGsdRootCache,
22
+ } from "../../paths.ts";
23
+ import {
24
+ loadWriteGateSnapshot,
25
+ markDepthVerified,
26
+ clearDiscussionFlowState,
27
+ } from "../../bootstrap/write-gate.ts";
28
+ import {
29
+ teardownAutoWorktree,
30
+ _resetAutoWorktreeOriginalBaseForTests,
31
+ } from "../../auto-worktree.ts";
32
+ import {
33
+ openDatabaseByWorkspace,
34
+ closeAllDatabases,
35
+ _getDbCache,
36
+ } from "../../gsd-db.ts";
37
+
38
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
39
+
40
+ function makeProjectDir(): string {
41
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-collapse-int-")));
42
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
43
+ return dir;
44
+ }
45
+
46
+ function git(args: string[], cwd: string): void {
47
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
48
+ }
49
+
50
+ function makeGitRepo(): string {
51
+ const dir = makeProjectDir();
52
+ git(["init"], dir);
53
+ git(["config", "user.email", "test@gsd.test"], dir);
54
+ git(["config", "user.name", "GSD Test"], dir);
55
+ writeFileSync(join(dir, "README.md"), "# test\n");
56
+ git(["add", "README.md"], dir);
57
+ git(["commit", "-m", "init"], dir);
58
+ git(["branch", "-M", "main"], dir);
59
+ return dir;
60
+ }
61
+
62
+ // ─── Test 1: Writer/validator path agreement under cwd-drift ─────────────────
63
+
64
+ describe("workspace-collapse integration: Test 1 — cwd-drift path agreement", () => {
65
+ let projectDir: string;
66
+ let otherDir: string;
67
+ const savedCwd = process.cwd();
68
+
69
+ beforeEach(() => {
70
+ projectDir = makeProjectDir();
71
+ otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cwd-drift-other-")));
72
+ _clearGsdRootCache();
73
+ });
74
+
75
+ afterEach(() => {
76
+ process.chdir(savedCwd);
77
+ rmSync(projectDir, { recursive: true, force: true });
78
+ rmSync(otherDir, { recursive: true, force: true });
79
+ _clearGsdRootCache();
80
+ });
81
+
82
+ test("contextFile() returns same absolute path before and after cwd change", () => {
83
+ const worktreeDir = join(projectDir, ".gsd", "worktrees", "M001");
84
+ mkdirSync(worktreeDir, { recursive: true });
85
+
86
+ const ws = createWorkspace(projectDir);
87
+ const scope = scopeMilestone(ws, "M001");
88
+
89
+ // Record the path the "writer" would use
90
+ const writerPath = scope.contextFile();
91
+ assert.ok(writerPath.startsWith(projectDir), "writer path is under projectDir");
92
+
93
+ // Simulate cwd drift
94
+ process.chdir(otherDir);
95
+ assert.notEqual(process.cwd(), projectDir, "cwd has drifted away from projectDir");
96
+
97
+ // The "validator" recomputes via the same scope
98
+ const validatorPath = scope.contextFile();
99
+
100
+ assert.equal(
101
+ validatorPath,
102
+ writerPath,
103
+ "contextFile() must return the same absolute path regardless of cwd drift",
104
+ );
105
+ });
106
+
107
+ test("scopeMilestone paths are stable across cwd changes (roadmap, state, db)", () => {
108
+ const ws = createWorkspace(projectDir);
109
+ const scope = scopeMilestone(ws, "M001");
110
+
111
+ const before = {
112
+ roadmap: scope.roadmapFile(),
113
+ state: scope.stateFile(),
114
+ db: scope.dbPath(),
115
+ milestoneDir: scope.milestoneDir(),
116
+ };
117
+
118
+ process.chdir(otherDir);
119
+
120
+ assert.equal(scope.roadmapFile(), before.roadmap, "roadmapFile() stable after cwd drift");
121
+ assert.equal(scope.stateFile(), before.state, "stateFile() stable after cwd drift");
122
+ assert.equal(scope.dbPath(), before.db, "dbPath() stable after cwd drift");
123
+ assert.equal(scope.milestoneDir(), before.milestoneDir, "milestoneDir() stable after cwd drift");
124
+ });
125
+ });
126
+
127
+ // ─── Test 2: Abort path leaves no stale state ────────────────────────────────
128
+
129
+ describe("workspace-collapse integration: Test 2 — abort teardown clears stale state", () => {
130
+ let repoDir: string;
131
+ const savedCwd = process.cwd();
132
+
133
+ beforeEach(() => {
134
+ repoDir = makeGitRepo();
135
+ _resetAutoWorktreeOriginalBaseForTests();
136
+ });
137
+
138
+ afterEach(() => {
139
+ process.chdir(savedCwd);
140
+ _resetAutoWorktreeOriginalBaseForTests();
141
+ rmSync(repoDir, { recursive: true, force: true });
142
+ });
143
+
144
+ test("STATE.md and M001-META.json are removed by teardownAutoWorktree", () => {
145
+ // Phase C pt 2: auto.lock no longer exists as a file — it migrated
146
+ // to the workers + unit_dispatches tables.
147
+ const gsdDir = join(repoDir, ".gsd");
148
+ const milestonesDir = join(gsdDir, "milestones", "M001");
149
+ mkdirSync(milestonesDir, { recursive: true });
150
+
151
+ const stateMd = join(gsdDir, "STATE.md");
152
+ const metaJson = join(milestonesDir, "M001-META.json");
153
+
154
+ writeFileSync(stateMd, "# State\nactive\n");
155
+ writeFileSync(metaJson, JSON.stringify({ milestoneId: "M001" }));
156
+
157
+ assert.ok(existsSync(stateMd), "STATE.md exists before teardown");
158
+ assert.ok(existsSync(metaJson), "M001-META.json exists before teardown");
159
+
160
+ // teardownAutoWorktree clears state files before the git step; git removal
161
+ // may fail in a minimal test repo — that is acceptable.
162
+ try {
163
+ teardownAutoWorktree(repoDir, "M001");
164
+ } catch {
165
+ // git worktree removal may fail when no worktree was created — non-fatal for this assertion
166
+ }
167
+
168
+ assert.ok(!existsSync(stateMd), "STATE.md removed by teardownAutoWorktree (regression: A5)");
169
+ assert.ok(!existsSync(metaJson), "M001-META.json removed by teardownAutoWorktree (regression: A5)");
170
+ });
171
+ });
172
+
173
+ // ─── Test 3: Cwd drift between persist and load of write-gate state ──────────
174
+
175
+ describe("workspace-collapse integration: Test 3 — write-gate snapshot survives cwd drift", () => {
176
+ let projectDir: string;
177
+ let otherDir: string;
178
+ const savedCwd = process.cwd();
179
+
180
+ beforeEach(() => {
181
+ projectDir = makeProjectDir();
182
+ otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-wg-other-")));
183
+ // Start with a clean write-gate state for projectDir
184
+ clearDiscussionFlowState(projectDir);
185
+ });
186
+
187
+ afterEach(() => {
188
+ process.chdir(savedCwd);
189
+ clearDiscussionFlowState(projectDir);
190
+ try { clearDiscussionFlowState(otherDir); } catch { /* best-effort */ }
191
+ rmSync(projectDir, { recursive: true, force: true });
192
+ rmSync(otherDir, { recursive: true, force: true });
193
+ });
194
+
195
+ test("loadWriteGateSnapshot returns persisted state after cwd drift", () => {
196
+ // Persist a snapshot: mark M001 depth-verified for projectDir
197
+ markDepthVerified("M001", projectDir);
198
+
199
+ // Drift cwd away from projectDir
200
+ process.chdir(otherDir);
201
+ assert.notEqual(process.cwd(), projectDir, "cwd has drifted");
202
+
203
+ // Load the snapshot using the explicit basePath — must not be affected by cwd
204
+ const snapshot = loadWriteGateSnapshot(projectDir);
205
+
206
+ assert.ok(
207
+ snapshot.verifiedDepthMilestones.includes("M001"),
208
+ "snapshot loaded from projectDir includes M001 despite cwd drift",
209
+ );
210
+ });
211
+
212
+ test("loadWriteGateSnapshot from different basePath does not bleed state", () => {
213
+ markDepthVerified("M001", projectDir);
214
+
215
+ process.chdir(otherDir);
216
+
217
+ // otherDir has no persisted state — should return empty snapshot
218
+ const snapshot = loadWriteGateSnapshot(otherDir);
219
+
220
+ assert.ok(
221
+ !snapshot.verifiedDepthMilestones.includes("M001"),
222
+ "otherDir snapshot must not bleed M001 state from projectDir",
223
+ );
224
+ });
225
+ });
226
+
227
+ // ─── Test 4: Sibling worktrees share DB connection ───────────────────────────
228
+
229
+ describe("workspace-collapse integration: Test 4 — sibling worktrees share DB connection", () => {
230
+ let projectDir: string;
231
+
232
+ beforeEach(() => {
233
+ projectDir = makeProjectDir();
234
+ });
235
+
236
+ afterEach(() => {
237
+ closeAllDatabases();
238
+ rmSync(projectDir, { recursive: true, force: true });
239
+ });
240
+
241
+ test("ws1 and ws2 (sibling worktrees) have same identityKey", () => {
242
+ const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
243
+ const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
244
+ mkdirSync(wt1, { recursive: true });
245
+ mkdirSync(wt2, { recursive: true });
246
+
247
+ const ws1 = createWorkspace(wt1);
248
+ const ws2 = createWorkspace(wt2);
249
+
250
+ assert.equal(
251
+ ws1.identityKey,
252
+ ws2.identityKey,
253
+ "sibling worktrees M001 and M002 must share the same identityKey",
254
+ );
255
+ assert.equal(
256
+ ws1.identityKey,
257
+ realpathSync(projectDir),
258
+ "identityKey is the realpath of the project root",
259
+ );
260
+ });
261
+
262
+ test("openDatabaseByWorkspace for sibling worktrees resolves to the same DB path", () => {
263
+ const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
264
+ const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
265
+ mkdirSync(wt1, { recursive: true });
266
+ mkdirSync(wt2, { recursive: true });
267
+
268
+ const ws1 = createWorkspace(wt1);
269
+ const ws2 = createWorkspace(wt2);
270
+
271
+ const ok1 = openDatabaseByWorkspace(ws1);
272
+ assert.ok(ok1, "openDatabaseByWorkspace(ws1) must succeed");
273
+ const cacheAfterWs1 = _getDbCache();
274
+ const entry1 = cacheAfterWs1.get(ws1.identityKey);
275
+ assert.ok(entry1, "cache entry for ws1.identityKey must exist");
276
+ const dbPath1 = entry1.dbPath;
277
+
278
+ const ok2 = openDatabaseByWorkspace(ws2);
279
+ assert.ok(ok2, "openDatabaseByWorkspace(ws2) must succeed");
280
+ const cacheAfterWs2 = _getDbCache();
281
+ const entry2 = cacheAfterWs2.get(ws2.identityKey);
282
+ assert.ok(entry2, "cache entry for ws2.identityKey must exist");
283
+ const dbPath2 = entry2.dbPath;
284
+
285
+ assert.equal(
286
+ dbPath1,
287
+ dbPath2,
288
+ "sibling worktrees must resolve to the same DB path (shared WAL)",
289
+ );
290
+ assert.equal(
291
+ cacheAfterWs2.size,
292
+ 1,
293
+ "only one cache entry for project + two sibling worktrees",
294
+ );
295
+ });
296
+ });
297
+
298
+ // ─── Test 5: gsdRootCache normalization survives trailing-slash inputs ────────
299
+
300
+ describe("workspace-collapse integration: Test 5 — gsdRootCache normalization deduplicates trailing-slash inputs", () => {
301
+ let projectDir: string;
302
+ let fakeHome: string;
303
+ let savedHome: string | undefined;
304
+ let savedUserProfile: string | undefined;
305
+ let savedGsdHome: string | undefined;
306
+
307
+ beforeEach(() => {
308
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-")));
309
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
310
+
311
+ fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-home-")));
312
+
313
+ savedHome = process.env.HOME;
314
+ savedUserProfile = process.env.USERPROFILE;
315
+ savedGsdHome = process.env.GSD_HOME;
316
+
317
+ // Prevent ~/.gsd interference
318
+ process.env.HOME = fakeHome;
319
+ process.env.USERPROFILE = fakeHome;
320
+ process.env.GSD_HOME = join(fakeHome, ".gsd");
321
+
322
+ clearPathCache();
323
+ });
324
+
325
+ afterEach(() => {
326
+ if (savedHome === undefined) delete process.env.HOME;
327
+ else process.env.HOME = savedHome;
328
+ if (savedUserProfile === undefined) delete process.env.USERPROFILE;
329
+ else process.env.USERPROFILE = savedUserProfile;
330
+ if (savedGsdHome === undefined) delete process.env.GSD_HOME;
331
+ else process.env.GSD_HOME = savedGsdHome;
332
+
333
+ clearPathCache();
334
+ rmSync(projectDir, { recursive: true, force: true });
335
+ rmSync(fakeHome, { recursive: true, force: true });
336
+ });
337
+
338
+ test("gsdRoot('/path/to/project') and gsdRoot('/path/to/project/') return identical paths", () => {
339
+ const withoutSlash = gsdRoot(projectDir);
340
+ const withSlash = gsdRoot(projectDir + "/");
341
+
342
+ assert.equal(
343
+ withoutSlash,
344
+ withSlash,
345
+ "gsdRoot must return identical paths for inputs with and without trailing slash",
346
+ );
347
+ assert.equal(
348
+ withoutSlash,
349
+ join(projectDir, ".gsd"),
350
+ "both calls must resolve to projectDir/.gsd",
351
+ );
352
+ });
353
+
354
+ test("both calls after clearPathCache() return identical paths (no duplicate cache entries)", () => {
355
+ // Start clean
356
+ clearPathCache();
357
+
358
+ const r1 = gsdRoot(projectDir);
359
+ const r2 = gsdRoot(projectDir + "/");
360
+
361
+ assert.equal(r1, r2, "r1 and r2 must be the same string after normalization");
362
+ // The cache normalizes both inputs to the same key — no duplicate entries.
363
+ // We can't inspect the cache size directly, but the behavioral proof is
364
+ // that a second call after clearPathCache re-probes and still matches.
365
+ clearPathCache();
366
+ const r3 = gsdRoot(projectDir + "/");
367
+ assert.equal(r3, r1, "re-probe after clearPathCache must produce the same result");
368
+ });
369
+ });
@@ -6,6 +6,21 @@ import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
8
  import { assessInterruptedSession } from "../interrupted-session.ts";
9
+ import {
10
+ openDatabase,
11
+ closeDatabase,
12
+ insertMilestone,
13
+ _getAdapter,
14
+ } from "../gsd-db.ts";
15
+ import { registerAutoWorker } from "../db/auto-workers.ts";
16
+ import { claimMilestoneLease } from "../db/milestone-leases.ts";
17
+ import { recordDispatchClaim } from "../db/unit-dispatches.ts";
18
+ import { setRuntimeKv } from "../db/runtime-kv.ts";
19
+ import {
20
+ PAUSED_SESSION_KV_KEY,
21
+ type PausedSessionMetadata,
22
+ } from "../interrupted-session.ts";
23
+ import { normalizeRealPath } from "../paths.ts";
9
24
 
10
25
  function makeTmpBase(): string {
11
26
  const base = join(tmpdir(), `gsd-auto-interrupted-${randomUUID()}`);
@@ -14,9 +29,61 @@ function makeTmpBase(): string {
14
29
  }
15
30
 
16
31
  function cleanup(base: string): void {
32
+ try { closeDatabase(); } catch { /* */ }
17
33
  try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
18
34
  }
19
35
 
36
+ function openFixtureDb(base: string): void {
37
+ openDatabase(join(base, ".gsd", "gsd.db"));
38
+ }
39
+
40
+ function expireWorker(workerId: string): void {
41
+ const db = _getAdapter()!;
42
+ db.prepare(
43
+ `UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :worker_id`,
44
+ ).run({ ":worker_id": workerId });
45
+ }
46
+
47
+ function writeLock(base: string, unitType: string, unitId: string): void {
48
+ openFixtureDb(base);
49
+ insertMilestone({
50
+ id: "M001",
51
+ title: "Test Milestone",
52
+ status: unitType === "complete-slice" ? "complete" : "active",
53
+ });
54
+ const workerId = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
55
+ const lease = claimMilestoneLease(workerId, "M001");
56
+ assert.equal(lease.ok, true);
57
+ if (lease.ok) {
58
+ const [, sliceId = null, taskId = null] = unitId.split("/");
59
+ const claimed = recordDispatchClaim({
60
+ traceId: `trace-${randomUUID().slice(0, 8)}`,
61
+ workerId,
62
+ milestoneLeaseToken: lease.token,
63
+ milestoneId: "M001",
64
+ sliceId,
65
+ taskId,
66
+ unitType,
67
+ unitId,
68
+ });
69
+ assert.equal(claimed.ok, true);
70
+ }
71
+ _getAdapter()!
72
+ .prepare(`UPDATE workers SET pid = 99999 WHERE worker_id = :worker_id`)
73
+ .run({ ":worker_id": workerId });
74
+ expireWorker(workerId);
75
+ }
76
+
77
+ function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
78
+ openFixtureDb(base);
79
+ const meta: PausedSessionMetadata = {
80
+ milestoneId,
81
+ originalBasePath: base,
82
+ stepMode,
83
+ };
84
+ setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, meta);
85
+ }
86
+
20
87
  function writeRoadmap(base: string, checked = false): void {
21
88
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
22
89
  mkdirSync(join(milestoneDir, "slices", "S01", "tasks"), { recursive: true });
@@ -51,42 +118,22 @@ function writeRoadmap(base: string, checked = false): void {
51
118
  function writeCompleteArtifacts(base: string): void {
52
119
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
53
120
  const sliceDir = join(milestoneDir, "slices", "S01");
121
+ const tasksDir = join(sliceDir, "tasks");
54
122
  mkdirSync(sliceDir, { recursive: true });
123
+ mkdirSync(tasksDir, { recursive: true });
124
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01: Test Slice\n\n## Tasks\n- [x] **T01: Do thing** `est:10m`\n", "utf-8");
125
+ writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "# Task Summary\nDone.\n", "utf-8");
55
126
  writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n", "utf-8");
56
127
  writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n", "utf-8");
57
128
  writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
58
129
  }
59
130
 
60
- function writeLock(base: string, unitType: string, unitId: string): void {
61
- writeFileSync(
62
- join(base, ".gsd", "auto.lock"),
63
- JSON.stringify({
64
- pid: 999999999,
65
- startedAt: new Date().toISOString(),
66
- unitType,
67
- unitId,
68
- unitStartedAt: new Date().toISOString(),
69
- }, null, 2),
70
- "utf-8",
71
- );
72
- }
73
-
74
- function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
75
- const runtimeDir = join(base, ".gsd", "runtime");
76
- mkdirSync(runtimeDir, { recursive: true });
77
- writeFileSync(
78
- join(runtimeDir, "paused-session.json"),
79
- JSON.stringify({ milestoneId, originalBasePath: base, stepMode }, null, 2),
80
- "utf-8",
81
- );
82
- }
83
-
84
131
  test("direct /gsd auto stale complete repo yields stale classification with no recovery payload", async () => {
85
132
  const base = makeTmpBase();
86
133
  try {
87
134
  writeRoadmap(base, true);
88
135
  writeCompleteArtifacts(base);
89
- writeLock(base, "execute-task", "M001/S01/T01");
136
+ writeLock(base, "complete-slice", "M001/S01");
90
137
 
91
138
  const assessment = await assessInterruptedSession(base);
92
139
  assert.equal(assessment.classification, "stale");
@@ -6,6 +6,21 @@ import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
8
  import { assessInterruptedSession } from "../interrupted-session.ts";
9
+ import {
10
+ openDatabase,
11
+ closeDatabase,
12
+ insertMilestone,
13
+ _getAdapter,
14
+ } from "../gsd-db.ts";
15
+ import { registerAutoWorker } from "../db/auto-workers.ts";
16
+ import { claimMilestoneLease } from "../db/milestone-leases.ts";
17
+ import { recordDispatchClaim } from "../db/unit-dispatches.ts";
18
+ import { setRuntimeKv } from "../db/runtime-kv.ts";
19
+ import {
20
+ PAUSED_SESSION_KV_KEY,
21
+ type PausedSessionMetadata,
22
+ } from "../interrupted-session.ts";
23
+ import { normalizeRealPath } from "../paths.ts";
9
24
 
10
25
  function makeTmpBase(): string {
11
26
  const base = join(tmpdir(), `gsd-smart-entry-${randomUUID()}`);
@@ -14,9 +29,61 @@ function makeTmpBase(): string {
14
29
  }
15
30
 
16
31
  function cleanup(base: string): void {
32
+ try { closeDatabase(); } catch { /* */ }
17
33
  try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
18
34
  }
19
35
 
36
+ function openFixtureDb(base: string): void {
37
+ openDatabase(join(base, ".gsd", "gsd.db"));
38
+ }
39
+
40
+ function expireWorker(workerId: string): void {
41
+ const db = _getAdapter()!;
42
+ db.prepare(
43
+ `UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :worker_id`,
44
+ ).run({ ":worker_id": workerId });
45
+ }
46
+
47
+ function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
48
+ openFixtureDb(base);
49
+ const meta: PausedSessionMetadata = {
50
+ milestoneId,
51
+ originalBasePath: base,
52
+ stepMode,
53
+ };
54
+ setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, meta);
55
+ }
56
+
57
+ function writeLock(base: string, unitType: string, unitId: string): void {
58
+ openFixtureDb(base);
59
+ insertMilestone({
60
+ id: "M001",
61
+ title: "Test Milestone",
62
+ status: unitType === "complete-slice" ? "complete" : "active",
63
+ });
64
+ const workerId = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
65
+ const lease = claimMilestoneLease(workerId, "M001");
66
+ assert.equal(lease.ok, true);
67
+ if (lease.ok) {
68
+ const [, sliceId = null, taskId = null] = unitId.split("/");
69
+ const claimed = recordDispatchClaim({
70
+ traceId: `trace-${randomUUID().slice(0, 8)}`,
71
+ workerId,
72
+ milestoneLeaseToken: lease.token,
73
+ milestoneId: "M001",
74
+ sliceId,
75
+ taskId,
76
+ unitType,
77
+ unitId,
78
+ });
79
+ assert.equal(claimed.ok, true);
80
+ }
81
+ _getAdapter()!
82
+ .prepare(`UPDATE workers SET pid = 99999 WHERE worker_id = :worker_id`)
83
+ .run({ ":worker_id": workerId });
84
+ expireWorker(workerId);
85
+ }
86
+
20
87
  function writeRoadmap(base: string, checked = false): void {
21
88
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
22
89
  mkdirSync(join(milestoneDir, "slices", "S01", "tasks"), { recursive: true });
@@ -51,42 +118,22 @@ function writeRoadmap(base: string, checked = false): void {
51
118
  function writeCompleteArtifacts(base: string): void {
52
119
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
53
120
  const sliceDir = join(milestoneDir, "slices", "S01");
121
+ const tasksDir = join(sliceDir, "tasks");
54
122
  mkdirSync(sliceDir, { recursive: true });
123
+ mkdirSync(tasksDir, { recursive: true });
124
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01: Test Slice\n\n## Tasks\n- [x] **T01: Do thing** `est:10m`\n", "utf-8");
125
+ writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "# Task Summary\nDone.\n", "utf-8");
55
126
  writeFileSync(join(sliceDir, "S01-SUMMARY.md"), "# Summary\nDone.\n", "utf-8");
56
127
  writeFileSync(join(sliceDir, "S01-UAT.md"), "# UAT\nPassed.\n", "utf-8");
57
128
  writeFileSync(join(milestoneDir, "M001-SUMMARY.md"), "# Milestone Summary\nDone.\n", "utf-8");
58
129
  }
59
130
 
60
- function writePausedSession(base: string, milestoneId = "M001", stepMode = false): void {
61
- const runtimeDir = join(base, ".gsd", "runtime");
62
- mkdirSync(runtimeDir, { recursive: true });
63
- writeFileSync(
64
- join(runtimeDir, "paused-session.json"),
65
- JSON.stringify({ milestoneId, originalBasePath: base, stepMode }, null, 2),
66
- "utf-8",
67
- );
68
- }
69
-
70
- function writeLock(base: string, unitType: string, unitId: string): void {
71
- writeFileSync(
72
- join(base, ".gsd", "auto.lock"),
73
- JSON.stringify({
74
- pid: 999999999,
75
- startedAt: new Date().toISOString(),
76
- unitType,
77
- unitId,
78
- unitStartedAt: new Date().toISOString(),
79
- }, null, 2),
80
- "utf-8",
81
- );
82
- }
83
-
84
131
  test("guided-flow stale complete scenario classifies as stale so the resume prompt can be suppressed", async () => {
85
132
  const base = makeTmpBase();
86
133
  try {
87
134
  writeRoadmap(base, true);
88
135
  writeCompleteArtifacts(base);
89
- writeLock(base, "execute-task", "M001/S01/T01");
136
+ writeLock(base, "complete-slice", "M001/S01");
90
137
 
91
138
  const assessment = await assessInterruptedSession(base);
92
139
  assert.equal(assessment.classification, "stale");
@@ -40,23 +40,26 @@ describe("stuck detection persistence (#3704)", () => {
40
40
  assert.match(loopSource, /function saveStuckState/);
41
41
  });
42
42
 
43
+ // Phase C: API changed from (basePath) to (session) — recentUnits is
44
+ // now reconstructed from unit_dispatches and stuckRecoveryAttempts
45
+ // persists in runtime_kv (worker scope).
43
46
  test("loopState initialized from persisted state", () => {
44
- assert.match(loopSource, /loadStuckState\(s\.basePath\)/);
47
+ assert.match(loopSource, /loadStuckState\(s\)/);
45
48
  });
46
49
 
47
50
  test("stuck state saved after each iteration", () => {
48
- assert.match(loopSource, /saveStuckState\(s\.basePath,\s*loopState\)/);
51
+ assert.match(loopSource, /saveStuckState\(s,\s*loopState\)/);
49
52
  });
50
53
 
51
- test("stuck state file path uses runtime directory", () => {
52
- assert.match(loopSource, /stuck-state\.json/);
53
- });
54
+ // Phase C: stuck-state.json file IO deleted; persistence moved to
55
+ // unit_dispatches (recentUnits) + runtime_kv (stuckRecoveryAttempts).
56
+ // The stuck-state-via-db.test.ts suite covers the round-trip.
54
57
 
55
58
  test("saveStuckState called in standard dev path as well as custom engine path (#4382)", () => {
56
59
  // Count all call-sites of saveStuckState (excluding the function definition itself).
57
60
  // After the fix, both the custom-engine path and the standard dev path must each
58
61
  // call saveStuckState so stuckRecoveryAttempts survives session restarts.
59
- const callMatches = loopSource.match(/saveStuckState\(s\.basePath,\s*loopState\)/g) ?? [];
62
+ const callMatches = loopSource.match(/saveStuckState\(s,\s*loopState\)/g) ?? [];
60
63
  assert.ok(
61
64
  callMatches.length >= 2,
62
65
  `saveStuckState must be called in both the custom-engine path and the standard dev path ` +