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
@@ -22,8 +22,14 @@ import type { GSDState } from "./types.js";
22
22
  import {
23
23
  assessInterruptedSession,
24
24
  readPausedSessionMetadata,
25
+ PAUSED_SESSION_KV_KEY,
25
26
  type InterruptedSessionAssessment,
27
+ type PausedSessionMetadata,
26
28
  } from "./interrupted-session.js";
29
+ import {
30
+ setRuntimeKv,
31
+ deleteRuntimeKv,
32
+ } from "./db/runtime-kv.js";
27
33
  import { getManifestStatus } from "./files.js";
28
34
  export { inlinePriorMilestoneSummary } from "./files.js";
29
35
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
@@ -244,6 +250,7 @@ import type {
244
250
  CurrentUnit,
245
251
  UnitRouting,
246
252
  StartModel,
253
+ AutoSession,
247
254
  } from "./auto/session.js";
248
255
  export {
249
256
  STUB_RECOVERY_THRESHOLD,
@@ -256,6 +263,10 @@ export type {
256
263
  } from "./auto/session.js";
257
264
  import { autoSession as s } from "./auto-runtime-state.js";
258
265
  import { gsdHome } from "./gsd-home.js";
266
+ import { createWorkspace, scopeMilestone } from "./workspace.js";
267
+ import { registerAutoWorker, markWorkerStopping } from "./db/auto-workers.js";
268
+ import { releaseMilestoneLease } from "./db/milestone-leases.js";
269
+ import { normalizeRealPath } from "./paths.js";
259
270
 
260
271
  // ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
261
272
  // ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
@@ -273,6 +284,28 @@ import { gsdHome } from "./gsd-home.js";
273
284
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
274
285
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
275
286
 
287
+ /**
288
+ * Phase B — register this auto-mode process in the workers table so other
289
+ * workers and janitors can detect liveness via heartbeat. Best-effort: if
290
+ * the DB is unavailable (e.g. fresh project before init) we skip registration
291
+ * silently rather than blocking session start.
292
+ */
293
+ function registerAutoWorkerForSession(session: AutoSession): void {
294
+ if (session.workerId) return; // already registered (e.g. resume re-runs)
295
+ try {
296
+ const projectRootRealpath = normalizeRealPath(
297
+ session.scope?.workspace.projectRoot
298
+ ?? (session.originalBasePath || session.basePath),
299
+ );
300
+ session.workerId = registerAutoWorker({ projectRootRealpath });
301
+ } catch (err) {
302
+ debugLog("autoLoop", {
303
+ phase: "register-worker-failed",
304
+ error: err instanceof Error ? err.message : String(err),
305
+ });
306
+ }
307
+ }
308
+
276
309
  function captureProjectRootEnv(projectRoot: string): void {
277
310
  if (!s.projectRootEnvCaptured) {
278
311
  s.hadProjectRootEnv = Object.prototype.hasOwnProperty.call(process.env, "GSD_PROJECT_ROOT");
@@ -324,6 +357,32 @@ function restoreMilestoneLockEnv(): void {
324
357
  s.milestoneLockEnvCaptured = false;
325
358
  }
326
359
 
360
+ /**
361
+ * Rebuild s.scope from the current s.basePath / s.originalBasePath / s.currentMilestoneId.
362
+ *
363
+ * Pass the worktree path as rawPath when entering a worktree so createWorkspace
364
+ * can detect the worktree layout and set mode="worktree". When no worktree is
365
+ * active, rawPath should equal the project root.
366
+ *
367
+ * Clears s.scope when milestoneId is absent — scope is only meaningful when a
368
+ * milestone is active.
369
+ *
370
+ * TODO(C8): remove basePath/originalBasePath once all readers use s.scope.
371
+ */
372
+ function rebuildScope(rawPath: string, milestoneId: string | null): void {
373
+ if (!milestoneId) {
374
+ s.scope = null;
375
+ return;
376
+ }
377
+ try {
378
+ const workspace = createWorkspace(rawPath);
379
+ s.scope = scopeMilestone(workspace, milestoneId);
380
+ } catch {
381
+ // Non-fatal — scope is additive. Existing readers still use basePath.
382
+ s.scope = null;
383
+ }
384
+ }
385
+
327
386
  function normalizeSessionFilePath(raw: unknown): string | null {
328
387
  if (typeof raw !== "string") return null;
329
388
  const trimmed = raw.trim();
@@ -509,6 +568,26 @@ export function _setAutoActiveForTest(active: boolean): void {
509
568
  s.active = active;
510
569
  }
511
570
 
571
+ /**
572
+ * Test-only seam: emit the missing-worktree warning exactly as the resume path
573
+ * does. Allows unit tests to verify the warning is produced without
574
+ * bootstrapping the full auto-mode entry point. Do not use in production code.
575
+ */
576
+ export function _warnIfWorktreeMissingForTest(
577
+ worktreePath: string | null | undefined,
578
+ milestoneId: string,
579
+ ): boolean {
580
+ if (worktreePath && !existsSync(worktreePath)) {
581
+ logWarning(
582
+ "session",
583
+ `Worktree was expected at ${worktreePath} but is missing. Continuing in project-root mode. To restart with a fresh worktree, run /gsd-debug or recreate the milestone.`,
584
+ { file: "auto.ts", milestoneId },
585
+ );
586
+ return true;
587
+ }
588
+ return false;
589
+ }
590
+
512
591
  export function isAutoPaused(): boolean {
513
592
  return s.paused;
514
593
  }
@@ -864,6 +943,21 @@ export async function stopAuto(
864
943
  debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
865
944
  }
866
945
 
946
+ // ── Step 1b: Coordination cleanup (Phase B) ──
947
+ // Release any active milestone lease so other workers don't have to
948
+ // wait for TTL expiry, then mark this worker as stopping. Best-effort:
949
+ // DB unavailability or stale state must not block shutdown.
950
+ try {
951
+ if (s.workerId && s.currentMilestoneId && s.milestoneLeaseToken) {
952
+ releaseMilestoneLease(s.workerId, s.currentMilestoneId, s.milestoneLeaseToken);
953
+ }
954
+ if (s.workerId) {
955
+ markWorkerStopping(s.workerId);
956
+ }
957
+ } catch (e) {
958
+ debugLog("stop-cleanup-coordination", { error: e instanceof Error ? e.message : String(e) });
959
+ }
960
+
867
961
  // ── Step 1b: Flush queued follow-up messages (#3512) ──
868
962
  // Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
869
963
  // extra LLM turns after stop. Flush them the same way run-unit.ts does.
@@ -1048,11 +1142,11 @@ export async function stopAuto(
1048
1142
  }
1049
1143
 
1050
1144
  // ── Step 12: Remove paused-session metadata (#1383) ──
1145
+ // Phase C pt 2: deleteRuntimeKv replaces unlinkSync(paused-session.json).
1051
1146
  try {
1052
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
1053
- if (existsSync(pausedPath)) unlinkSync(pausedPath);
1147
+ deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
1054
1148
  } catch (err) { /* non-fatal */
1055
- logWarning("engine", `file unlink failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1149
+ logWarning("engine", `paused-session DB delete failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1056
1150
  }
1057
1151
 
1058
1152
  // ── Step 13: Restore original model + thinking (before reset clears IDs) ──
@@ -1153,10 +1247,12 @@ export async function pauseAuto(
1153
1247
  s.pausedSessionFile = normalizeSessionFilePath(ctx?.sessionManager?.getSessionFile() ?? null);
1154
1248
 
1155
1249
  // Persist paused-session metadata so resume survives /exit (#1383).
1156
- // The fresh-start bootstrap checks for this file and restores worktree context.
1250
+ // Phase C pt 2: persisted to runtime_kv (global scope, key
1251
+ // PAUSED_SESSION_KV_KEY) instead of runtime/paused-session.json. The
1252
+ // fresh-start bootstrap below reads from the same key.
1157
1253
  try {
1158
- const pausedMeta = {
1159
- milestoneId: s.currentMilestoneId,
1254
+ const pausedMeta: PausedSessionMetadata = {
1255
+ milestoneId: s.currentMilestoneId ?? undefined,
1160
1256
  worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
1161
1257
  originalBasePath: s.originalBasePath,
1162
1258
  stepMode: s.stepMode,
@@ -1164,20 +1260,15 @@ export async function pauseAuto(
1164
1260
  sessionFile: s.pausedSessionFile,
1165
1261
  unitType: s.currentUnit?.type ?? undefined,
1166
1262
  unitId: s.currentUnit?.id ?? undefined,
1167
- activeEngineId: s.activeEngineId,
1263
+ activeEngineId: s.activeEngineId ?? undefined,
1168
1264
  activeRunDir: s.activeRunDir,
1169
1265
  autoStartTime: s.autoStartTime,
1170
1266
  milestoneLock: s.sessionMilestoneLock ?? undefined,
1171
1267
  };
1172
- const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
1173
- atomicWriteSync(
1174
- join(runtimeDir, "paused-session.json"),
1175
- JSON.stringify(pausedMeta, null, 2),
1176
- "utf-8",
1177
- );
1268
+ setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, pausedMeta);
1178
1269
  } catch (err) {
1179
1270
  // Non-fatal — resume will still work via full bootstrap, just without worktree context
1180
- logWarning("engine", `paused-session file write failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1271
+ logWarning("engine", `paused-session DB write failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1181
1272
  }
1182
1273
 
1183
1274
  // Close out the current unit so its runtime record doesn't stay at "dispatched"
@@ -1460,8 +1551,10 @@ export async function startAuto(
1460
1551
  ctx.ui.notify("Recovered unfinished migration (.gsd.migrating → .gsd).", "info");
1461
1552
  }
1462
1553
 
1463
- const freshStartAssessment = interruptedAssessment
1464
- ?? await assessInterruptedSession(base);
1554
+ const freshStartAssessment = await (interruptedAssessment
1555
+ ?? (() => {
1556
+ return ensureDbOpen(base).then(() => assessInterruptedSession(base));
1557
+ })());
1465
1558
 
1466
1559
  if (freshStartAssessment.classification === "running") {
1467
1560
  const pid = freshStartAssessment.lock?.pid;
@@ -1476,10 +1569,20 @@ export async function startAuto(
1476
1569
 
1477
1570
  // If resuming from paused state, just re-activate and dispatch next unit.
1478
1571
  // Check persisted paused-session first (#1383) — survives /exit.
1572
+ // Phase C pt 2: persisted in runtime_kv (global scope) instead of
1573
+ // runtime/paused-session.json. The `clearPausedSession` helper
1574
+ // replaces every prior unlinkSync(pausedPath) call.
1575
+ const clearPausedSession = (logTag: string): void => {
1576
+ try {
1577
+ deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
1578
+ } catch (err) {
1579
+ logWarning("session", `${logTag}: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1580
+ }
1581
+ };
1582
+
1479
1583
  if (!s.paused) {
1480
1584
  try {
1481
1585
  const meta = freshStartAssessment.pausedSession ?? readPausedSessionMetadata(base);
1482
- const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
1483
1586
  if (meta?.activeEngineId && meta.activeEngineId !== "dev") {
1484
1587
  // Custom workflow resume — restore engine state
1485
1588
  s.activeEngineId = meta.activeEngineId;
@@ -1489,11 +1592,6 @@ export async function startAuto(
1489
1592
  s.autoStartTime = meta.autoStartTime || Date.now();
1490
1593
  s.sessionMilestoneLock = meta.milestoneLock ?? null;
1491
1594
  s.paused = true;
1492
- try { unlinkSync(pausedPath); } catch (e) {
1493
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
1494
- logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
1495
- }
1496
- }
1497
1595
  ctx.ui.notify(
1498
1596
  `Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`,
1499
1597
  "info",
@@ -1534,11 +1632,7 @@ export async function startAuto(
1534
1632
  }
1535
1633
  }
1536
1634
  if (!mDir || summaryIsTerminal) {
1537
- try { unlinkSync(pausedPath); } catch (err) {
1538
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
1539
- logWarning("session", `pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1540
- }
1541
- }
1635
+ clearPausedSession("paused-session DB cleanup failed (milestone gone/complete)");
1542
1636
  ctx.ui.notify(
1543
1637
  `Paused milestone ${meta.milestoneId} is ${!mDir ? "missing" : "already complete"}. Starting fresh.`,
1544
1638
  "info",
@@ -1553,22 +1647,31 @@ export async function startAuto(
1553
1647
  s.autoStartTime = meta.autoStartTime || Date.now();
1554
1648
  s.sessionMilestoneLock = meta.milestoneLock ?? null;
1555
1649
  s.paused = true;
1556
- try { unlinkSync(pausedPath); } catch (e) {
1557
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
1558
- logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
1650
+ // Build scope from persisted state. Use worktreePath when present and
1651
+ // still on disk so mode is detected correctly; fall back to project root.
1652
+ {
1653
+ const persistedWorktreePath = meta.worktreePath ?? null;
1654
+ if (persistedWorktreePath && !existsSync(persistedWorktreePath)) {
1655
+ logWarning(
1656
+ "session",
1657
+ `Worktree was expected at ${persistedWorktreePath} but is missing. Continuing in project-root mode. To restart with a fresh worktree, run /gsd-debug or recreate the milestone.`,
1658
+ { file: "auto.ts", milestoneId: meta.milestoneId ?? "" },
1659
+ );
1559
1660
  }
1661
+ const rawForScope = (persistedWorktreePath && existsSync(persistedWorktreePath))
1662
+ ? persistedWorktreePath
1663
+ : (s.originalBasePath || base);
1664
+ rebuildScope(rawForScope, s.currentMilestoneId);
1560
1665
  }
1561
1666
  ctx.ui.notify(
1562
1667
  `Resuming paused session for ${meta.milestoneId}${meta.worktreePath && existsSync(meta.worktreePath) ? ` (worktree)` : ""}.`,
1563
1668
  "info",
1564
1669
  );
1565
1670
  }
1566
- } else if (existsSync(pausedPath)) {
1567
- try { unlinkSync(pausedPath); } catch (e) {
1568
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
1569
- logWarning("session", `stale pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
1570
- }
1571
- }
1671
+ } else if (meta) {
1672
+ // Stale paused-session metadata that the assessment chose not to
1673
+ // resume clean it up so the next bootstrap starts fresh.
1674
+ clearPausedSession("stale paused-session DB cleanup failed");
1572
1675
  }
1573
1676
  }
1574
1677
  } catch (err) {
@@ -1637,10 +1740,19 @@ export async function startAuto(
1637
1740
  // session (e.g. isolation mode changed, detectWorktreeName differs across
1638
1741
  // process restarts). We guard with existsSync so a stale or deleted
1639
1742
  // worktree directory safely falls back to the project root.
1640
- const resumeWorktreePath = freshStartAssessment.pausedSession?.worktreePath;
1743
+ const resumeWorktreePath = freshStartAssessment.pausedSession?.worktreePath ?? null;
1744
+ if (resumeWorktreePath && !existsSync(resumeWorktreePath)) {
1745
+ logWarning(
1746
+ "session",
1747
+ `Worktree was expected at ${resumeWorktreePath} but is missing. Continuing in project-root mode. To restart with a fresh worktree, run /gsd-debug or recreate the milestone.`,
1748
+ { file: "auto.ts", milestoneId: s.currentMilestoneId ?? "" },
1749
+ );
1750
+ }
1641
1751
  if (resumeWorktreePath && existsSync(resumeWorktreePath)) {
1642
1752
  s.basePath = resumeWorktreePath;
1643
1753
  }
1754
+ // Rebuild scope now that s.basePath reflects the actual worktree (or project root).
1755
+ rebuildScope(s.basePath, s.currentMilestoneId);
1644
1756
  // Ensure the workflow-logger audit log is pinned to the project root
1645
1757
  // even when auto-mode is entered via a path that bypasses the
1646
1758
  // bootstrap/dynamic-tools ensureDbOpen() → setLogBasePath() chain
@@ -1669,6 +1781,8 @@ export async function startAuto(
1669
1781
  buildResolver().enterMilestone(s.currentMilestoneId, {
1670
1782
  notify: ctx.ui.notify.bind(ctx.ui),
1671
1783
  });
1784
+ // s.basePath may have been updated to a worktree path by enterMilestone.
1785
+ rebuildScope(s.basePath, s.currentMilestoneId);
1672
1786
  }
1673
1787
 
1674
1788
  registerSigtermHandler(lockBase());
@@ -1737,19 +1851,23 @@ export async function startAuto(
1737
1851
  s.pausedSessionFile = null;
1738
1852
  }
1739
1853
 
1854
+ captureProjectRootEnv(s.originalBasePath || s.basePath);
1855
+ registerAutoWorkerForSession(s);
1740
1856
  updateSessionLock(
1741
1857
  lockBase(),
1742
1858
  "resuming",
1743
1859
  s.currentMilestoneId ?? "unknown",
1744
1860
  );
1745
- writeLock(
1746
- lockBase(),
1747
- "resuming",
1748
- s.currentMilestoneId ?? "unknown",
1749
- );
1861
+ if (s.workerId) {
1862
+ writeLock(
1863
+ lockBase(),
1864
+ "resuming",
1865
+ s.currentMilestoneId ?? "unknown",
1866
+ );
1867
+ clearPausedSession("paused-session DB cleanup failed (resume activation)");
1868
+ }
1750
1869
  pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", level: "progress" });
1751
1870
 
1752
- captureProjectRootEnv(s.originalBasePath || s.basePath);
1753
1871
  startAutoCommandPolling(s.basePath);
1754
1872
  await runAutoLoopWithUok({
1755
1873
  ctx,
@@ -1783,7 +1901,12 @@ export async function startAuto(
1783
1901
  );
1784
1902
  if (!ready) return;
1785
1903
 
1904
+ // Build scope after bootstrap has populated s.basePath / s.originalBasePath /
1905
+ // s.currentMilestoneId (including worktree setup inside bootstrapAutoSession).
1906
+ rebuildScope(s.basePath, s.currentMilestoneId);
1907
+
1786
1908
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1909
+ registerAutoWorkerForSession(s);
1787
1910
  try {
1788
1911
  pi.events.emit(CMUX_CHANNELS.SIDEBAR, { action: "sync" as const, preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, state: await deriveState(s.basePath) });
1789
1912
  } catch (err) {
@@ -115,7 +115,7 @@ export async function handleAgentEnd(
115
115
  }
116
116
 
117
117
  if (checkAutoStartAfterDiscuss()) {
118
- clearDiscussionFlowState();
118
+ clearDiscussionFlowState(resolveAgentEndBasePath() ?? process.cwd());
119
119
  return;
120
120
  }
121
121
 
@@ -76,7 +76,7 @@ export function registerHooks(
76
76
  const { initHealthWidget } = await import("../health-widget.js");
77
77
  initHealthWidget(ctx);
78
78
  }
79
- resetWriteGateState();
79
+ resetWriteGateState(process.cwd());
80
80
  resetToolCallLoopGuard();
81
81
  approvalQuestionAbortInFlight = false;
82
82
  await resetAskUserQuestionsTurnCache();
@@ -126,10 +126,10 @@ export function registerHooks(
126
126
  pi.on("session_switch", async (_event, ctx) => {
127
127
  initNotificationStore(process.cwd());
128
128
  installNotifyInterceptor(ctx);
129
- resetWriteGateState();
129
+ resetWriteGateState(process.cwd());
130
130
  resetToolCallLoopGuard();
131
131
  await resetAskUserQuestionsTurnCache();
132
- clearDiscussionFlowState();
132
+ clearDiscussionFlowState(process.cwd());
133
133
  await syncServiceTierStatus(ctx);
134
134
  await applyDisabledModelProviderPolicy(ctx);
135
135
  // Skip MCP auto-prep when running inside an auto-worktree. The worktree
@@ -155,12 +155,13 @@ export function registerHooks(
155
155
  const { getEcosystemReadyPromise } = await import("../ecosystem/loader.js");
156
156
  await getEcosystemReadyPromise();
157
157
 
158
+ const beforeAgentBasePath = process.cwd();
158
159
  const pendingApprovalGate = getPendingGate();
159
160
  if (pendingApprovalGate && isExplicitApprovalResponse(event.prompt, pendingApprovalGate)) {
160
- markApprovalGateVerified(pendingApprovalGate);
161
+ markApprovalGateVerified(pendingApprovalGate, beforeAgentBasePath);
161
162
  const milestoneId = extractDepthVerificationMilestoneId(pendingApprovalGate);
162
- if (milestoneId) markDepthVerified(milestoneId);
163
- clearPendingGate();
163
+ if (milestoneId) markDepthVerified(milestoneId, beforeAgentBasePath);
164
+ clearPendingGate(beforeAgentBasePath);
164
165
  }
165
166
 
166
167
  // GSD's own context injection (existing behavior — unchanged).
@@ -346,7 +347,7 @@ export function registerHooks(
346
347
  if (!shouldPauseForUserApprovalQuestion(unitType, [event.message])) return;
347
348
 
348
349
  const gateId = approvalGateIdForUnit(unitType, unitId);
349
- if (gateId) setPendingGate(gateId);
350
+ if (gateId) setPendingGate(gateId, process.cwd());
350
351
 
351
352
  approvalQuestionAbortInFlight = true;
352
353
  ctx.ui.notify(
@@ -393,7 +394,7 @@ export function registerHooks(
393
394
  const questions: any[] = (event.input as any)?.questions ?? [];
394
395
  const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
395
396
  if (typeof questionId === "string") {
396
- setPendingGate(questionId);
397
+ setPendingGate(questionId, discussionBasePath);
397
398
  }
398
399
  }
399
400
 
@@ -555,7 +556,8 @@ export function registerHooks(
555
556
  }
556
557
  const toolName = canonicalToolName(event.toolName);
557
558
  if (toolName !== "ask_user_questions") return;
558
- const milestoneId = await getDiscussionMilestoneIdFor(process.cwd());
559
+ const basePath = process.cwd();
560
+ const milestoneId = await getDiscussionMilestoneIdFor(basePath);
559
561
  const queueActive = isQueuePhaseActive();
560
562
 
561
563
  const details = event.details as any;
@@ -569,8 +571,13 @@ export function registerHooks(
569
571
  const currentPendingGate = getPendingGate();
570
572
  if (currentPendingGate) {
571
573
  if (details?.cancelled || !details?.response) {
572
- // Gate stays pending. Return a hard instruction as the tool result so
573
- // the model cannot reinterpret a cancelled prompt as prior approval.
574
+ // Gate stays pending. Direct the agent to the most reliable recovery
575
+ // path re-calling ask_user_questions with the same gate id — without
576
+ // misrepresenting the plain-text path. The plain-text path also works
577
+ // (isExplicitApprovalResponse on the next before_agent_start clears
578
+ // the gate when the user replies with an approval keyword), but the
579
+ // structured re-ask is more deterministic and gives the user a clear UI.
580
+ resetToolCallLoopGuard();
574
581
  return {
575
582
  content: [{
576
583
  type: "text" as const,
@@ -578,8 +585,8 @@ export function registerHooks(
578
585
  `HARD BLOCK: approval gate "${currentPendingGate}" is still pending.`,
579
586
  "No user response was received for the confirmation question.",
580
587
  "Do not infer approval from earlier or prior messages.",
581
- "Do not proceed, write files, save artifacts, or call more tools.",
582
- "Ask the user to confirm in plain chat, then stop and wait for their next message.",
588
+ "Do not proceed, write files, save artifacts, or call other tools.",
589
+ `Re-call ask_user_questions with the same gate question id ("${currentPendingGate}") and wait for the user's response.`,
583
590
  ].join(" "),
584
591
  }],
585
592
  };
@@ -588,10 +595,10 @@ export function registerHooks(
588
595
  if (pendingQuestion) {
589
596
  const answer = details.response?.answers?.[currentPendingGate];
590
597
  if (isDepthConfirmationAnswer(answer?.selected, pendingQuestion.options)) {
591
- markApprovalGateVerified(currentPendingGate);
598
+ markApprovalGateVerified(currentPendingGate, basePath);
592
599
  const milestoneIdFromGate = extractDepthVerificationMilestoneId(currentPendingGate);
593
- if (milestoneIdFromGate) markDepthVerified(milestoneIdFromGate);
594
- clearPendingGate();
600
+ if (milestoneIdFromGate) markDepthVerified(milestoneIdFromGate, basePath);
601
+ clearPendingGate(basePath);
595
602
  }
596
603
  }
597
604
  }
@@ -607,9 +614,9 @@ export function registerHooks(
607
614
  const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
608
615
  if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
609
616
  if (currentPendingGate && question.id !== currentPendingGate) break;
610
- markApprovalGateVerified(question.id);
611
- markDepthVerified(inferredMilestoneId);
612
- clearPendingGate();
617
+ markApprovalGateVerified(question.id, basePath);
618
+ markDepthVerified(inferredMilestoneId, basePath);
619
+ clearPendingGate(basePath);
613
620
  }
614
621
  break;
615
622
  }
@@ -617,8 +624,6 @@ export function registerHooks(
617
624
 
618
625
  if (!milestoneId && !queueActive) return;
619
626
  if (!milestoneId) return;
620
-
621
- const basePath = process.cwd();
622
627
  const milestoneDir = resolveMilestonePath(basePath, milestoneId);
623
628
  if (!milestoneDir) return;
624
629
 
@@ -0,0 +1,103 @@
1
+ // GSD-2 write-gate bootstrap — regression test for required basePath (commit A3)
2
+ //
3
+ // Verifies that persistWriteGateSnapshot / loadWriteGateSnapshot are pinned to
4
+ // the basePath argument and do not silently fall back to process.cwd(). The
5
+ // underlying bug: both functions defaulted `basePath = process.cwd()`, so a
6
+ // persist in cwd-A followed by a chdir to cwd-B and a load (which also
7
+ // defaulted to process.cwd(), now cwd-B) missed the persisted file entirely —
8
+ // the depth-verification state became invisible across cwd boundaries.
9
+
10
+ import { test, describe, before, after } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, rmSync, existsSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ import {
17
+ markDepthVerified,
18
+ loadWriteGateSnapshot,
19
+ clearDiscussionFlowState,
20
+ } from "../write-gate.js";
21
+
22
+ // ─── Helpers ────────────────────────────────────────────────────────────────
23
+
24
+ function makeTempDir(): string {
25
+ return mkdtempSync(join(tmpdir(), "wg-basepath-test-"));
26
+ }
27
+
28
+ // Save and restore process.cwd() across tests to avoid cross-test pollution.
29
+ let originalCwd: string;
30
+ before(() => {
31
+ originalCwd = process.cwd();
32
+ });
33
+ after(() => {
34
+ if (process.cwd() !== originalCwd) {
35
+ process.chdir(originalCwd);
36
+ }
37
+ });
38
+
39
+ // ─── Scenario: persist with basePath=A, chdir, load with basePath=A ─────────
40
+ //
41
+ // This is the exact failure mode from the bug: persist used process.cwd() and
42
+ // load used process.cwd(), and they resolved to different directories after a
43
+ // chdir. With the fix, both calls receive an explicit basePath so cwd changes
44
+ // have no effect.
45
+
46
+ describe("write-gate basePath regression", () => {
47
+ let baseDirA: string;
48
+ let baseDirB: string;
49
+
50
+ before(() => {
51
+ baseDirA = makeTempDir();
52
+ baseDirB = makeTempDir();
53
+ });
54
+
55
+ after(() => {
56
+ // Restore cwd before cleanup to avoid issues on Windows.
57
+ process.chdir(originalCwd);
58
+ rmSync(baseDirA, { recursive: true, force: true });
59
+ rmSync(baseDirB, { recursive: true, force: true });
60
+ });
61
+
62
+ test("snapshot persisted to basePath=A is readable after chdir to basePath=B", (t) => {
63
+ // Arrange: enable persistence (the default when env var is not set to "0"/"false").
64
+ const prev = process.env.GSD_PERSIST_WRITE_GATE_STATE;
65
+ t.after(() => {
66
+ if (prev === undefined) {
67
+ delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
68
+ } else {
69
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = prev;
70
+ }
71
+ });
72
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
73
+
74
+ // Reset state and clear any stale snapshot files from both dirs.
75
+ clearDiscussionFlowState(baseDirA);
76
+ clearDiscussionFlowState(baseDirB);
77
+
78
+ // Act: persist a milestone as depth-verified into baseDirA.
79
+ markDepthVerified("M001", baseDirA);
80
+
81
+ // Confirm the snapshot file was written under baseDirA.
82
+ const snapshotPath = join(baseDirA, ".gsd", "runtime", "write-gate-state.json");
83
+ assert.ok(existsSync(snapshotPath), "snapshot file should exist under baseDirA");
84
+
85
+ // Simulate what happens when cwd changes to a different project root.
86
+ process.chdir(baseDirB);
87
+ assert.notEqual(process.cwd(), baseDirA, "cwd should differ from baseDirA after chdir");
88
+
89
+ // Load snapshot using the explicit baseDirA — must see the persisted state.
90
+ const snapshot = loadWriteGateSnapshot(baseDirA);
91
+ assert.ok(
92
+ snapshot.verifiedDepthMilestones.includes("M001"),
93
+ "loadWriteGateSnapshot(baseDirA) must return the persisted milestone despite cwd being baseDirB",
94
+ );
95
+
96
+ // Loading with baseDirB must NOT see the state from baseDirA.
97
+ const snapshotB = loadWriteGateSnapshot(baseDirB);
98
+ assert.ok(
99
+ !snapshotB.verifiedDepthMilestones.includes("M001"),
100
+ "loadWriteGateSnapshot(baseDirB) must not bleed state from baseDirA",
101
+ );
102
+ });
103
+ });