gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445

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 (155) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +7 -2
  3. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
  5. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
  7. package/dist/resources/extensions/gsd/auto.js +62 -1
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  9. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
  10. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  11. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  12. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  13. package/dist/resources/extensions/gsd/gsd-db.js +194 -0
  14. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  15. package/dist/resources/extensions/gsd/guided-flow.js +117 -25
  16. package/dist/resources/extensions/gsd/metrics.js +287 -1
  17. package/dist/resources/extensions/gsd/paths.js +79 -8
  18. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  19. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  20. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  23. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  24. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  25. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  26. package/dist/resources/extensions/gsd/workspace.js +59 -0
  27. package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
  28. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  29. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  30. package/dist/web/standalone/.next/BUILD_ID +1 -1
  31. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  32. package/dist/web/standalone/.next/build-manifest.json +2 -2
  33. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  34. package/dist/web/standalone/.next/required-server-files.json +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.html +1 -1
  52. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  59. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  61. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  62. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/dist/web/standalone/server.js +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/README.md +2 -11
  66. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  67. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  68. package/packages/mcp-server/dist/remote-questions.js +28 -0
  69. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  70. package/packages/mcp-server/dist/server.d.ts +28 -0
  71. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  72. package/packages/mcp-server/dist/server.js +94 -4
  73. package/packages/mcp-server/dist/server.js.map +1 -1
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  76. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  77. package/packages/mcp-server/src/remote-questions.ts +35 -0
  78. package/packages/mcp-server/src/server.ts +129 -6
  79. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  81. package/src/resources/extensions/gsd/auto/phases.ts +8 -2
  82. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  83. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
  84. package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
  85. package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
  86. package/src/resources/extensions/gsd/auto.ts +79 -1
  87. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  88. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
  89. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  90. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  91. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  92. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  93. package/src/resources/extensions/gsd/gsd-db.ts +184 -0
  94. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  95. package/src/resources/extensions/gsd/guided-flow.ts +154 -25
  96. package/src/resources/extensions/gsd/metrics.ts +321 -1
  97. package/src/resources/extensions/gsd/paths.ts +67 -8
  98. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  99. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  100. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  101. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  102. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  103. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  104. package/src/resources/extensions/gsd/templates/project.md +10 -0
  105. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  106. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  107. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  108. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  109. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  110. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  111. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  112. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  113. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  114. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  115. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  116. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  117. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  118. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  119. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  120. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  121. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
  123. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  124. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  125. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  126. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  127. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  128. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  129. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  130. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  131. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  132. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  133. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  134. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  135. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
  136. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
  137. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  138. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
  139. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  140. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
  141. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  142. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  143. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  144. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  145. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  146. package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
  147. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  148. package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
  149. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  150. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  151. package/src/resources/extensions/gsd/workspace.ts +95 -0
  152. package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
  153. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  154. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
  155. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
@@ -76,6 +76,7 @@ import {
76
76
  nativeMergeAbort,
77
77
  } from "./native-git-bridge.js";
78
78
  import { gsdHome } from "./gsd-home.js";
79
+ import { type MilestoneScope, type GsdWorkspace, createWorkspace } from "./workspace.js";
79
80
 
80
81
  const PROJECT_PREFERENCES_FILE = "PREFERENCES.md";
81
82
  const LEGACY_PROJECT_PREFERENCES_FILE = "preferences.md";
@@ -254,8 +255,16 @@ function forceOverwriteAssessmentsWithVerdict(
254
255
 
255
256
  // ─── Module State ──────────────────────────────────────────────────────────
256
257
 
257
- /** Original project root before chdir into auto-worktree. */
258
- let originalBase: string | null = null;
258
+ /** Active workspace registry replaces the legacy `originalBase` singleton. */
259
+ let activeWorkspace: GsdWorkspace | null = null;
260
+
261
+ function setActiveWorkspace(ws: GsdWorkspace | null): void {
262
+ activeWorkspace = ws;
263
+ }
264
+
265
+ function getActiveWorkspace(): GsdWorkspace | null {
266
+ return activeWorkspace;
267
+ }
259
268
 
260
269
  function clearProjectRootStateFiles(basePath: string, milestoneId: string): void {
261
270
  const gsdDir = gsdRoot(basePath);
@@ -345,6 +354,41 @@ export const isSafeToAutoResolve = (filePath: string): boolean =>
345
354
  * gsd.db in the worktree so it rebuilds from fresh disk state (#853).
346
355
  * Non-fatal — sync failure should never block dispatch.
347
356
  */
357
+ /**
358
+ * Scope-typed variant of syncProjectRootToWorktree.
359
+ *
360
+ * Takes an explicit (rootScope, worktreeScope) pair where rootScope is the
361
+ * project root and worktreeScope is the auto-worktree. Direction is encoded
362
+ * in argument order. Asserts both scopes belong to the same workspace identity
363
+ * to prevent silent mismatch bugs.
364
+ */
365
+ export function syncProjectRootToWorktreeByScope(
366
+ rootScope: MilestoneScope,
367
+ worktreeScope: MilestoneScope,
368
+ ): void {
369
+ if (rootScope.workspace.identityKey !== worktreeScope.workspace.identityKey) {
370
+ throw new Error(
371
+ `syncProjectRootToWorktreeByScope: scope identity mismatch — ` +
372
+ `rootScope.identityKey="${rootScope.workspace.identityKey}" ` +
373
+ `worktreeScope.identityKey="${worktreeScope.workspace.identityKey}"`,
374
+ );
375
+ }
376
+ if (rootScope.milestoneId !== worktreeScope.milestoneId) {
377
+ throw new Error(
378
+ `syncProjectRootToWorktreeByScope: milestoneId mismatch — ` +
379
+ `rootScope.milestoneId="${rootScope.milestoneId}" worktreeScope.milestoneId="${worktreeScope.milestoneId}"`,
380
+ );
381
+ }
382
+ const projectRoot = rootScope.workspace.projectRoot;
383
+ const worktreePath_ = worktreeScope.workspace.worktreeRoot ?? worktreeScope.workspace.projectRoot;
384
+ const milestoneId = rootScope.milestoneId;
385
+ syncProjectRootToWorktree(projectRoot, worktreePath_, milestoneId);
386
+ }
387
+
388
+ /**
389
+ * @deprecated Use syncProjectRootToWorktreeByScope instead.
390
+ * TODO(C-future): remove once all callers migrated.
391
+ */
348
392
  export function syncProjectRootToWorktree(
349
393
  projectRoot: string,
350
394
  worktreePath_: string,
@@ -430,12 +474,44 @@ export function syncProjectRootToWorktree(
430
474
  }
431
475
  }
432
476
 
477
+ /**
478
+ * Scope-typed variant of syncStateToProjectRoot.
479
+ *
480
+ * Takes an explicit (worktreeScope, rootScope) pair. Direction is encoded in
481
+ * argument order (worktree → root). Asserts both scopes belong to the same
482
+ * workspace identity to prevent silent mismatch bugs.
483
+ */
484
+ export function syncStateToProjectRootByScope(
485
+ worktreeScope: MilestoneScope,
486
+ rootScope: MilestoneScope,
487
+ ): void {
488
+ if (worktreeScope.workspace.identityKey !== rootScope.workspace.identityKey) {
489
+ throw new Error(
490
+ `syncStateToProjectRootByScope: scope identity mismatch — ` +
491
+ `worktreeScope.identityKey="${worktreeScope.workspace.identityKey}" ` +
492
+ `rootScope.identityKey="${rootScope.workspace.identityKey}"`,
493
+ );
494
+ }
495
+ if (worktreeScope.milestoneId !== rootScope.milestoneId) {
496
+ throw new Error(
497
+ `syncStateToProjectRootByScope: milestoneId mismatch — ` +
498
+ `worktreeScope.milestoneId="${worktreeScope.milestoneId}" rootScope.milestoneId="${rootScope.milestoneId}"`,
499
+ );
500
+ }
501
+ const worktreePath_ = worktreeScope.workspace.worktreeRoot ?? worktreeScope.workspace.projectRoot;
502
+ const projectRoot = rootScope.workspace.projectRoot;
503
+ const milestoneId = worktreeScope.milestoneId;
504
+ syncStateToProjectRoot(worktreePath_, projectRoot, milestoneId);
505
+ }
506
+
433
507
  /**
434
508
  * Sync worktree diagnostics from worktree to project root.
435
509
  * Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
436
510
  * DB/project-root state remains authoritative; markdown projections are not
437
511
  * copied from the worktree back to the project root.
438
512
  * Non-fatal — sync failure should never block dispatch.
513
+ * @deprecated Use syncStateToProjectRootByScope instead.
514
+ * TODO(C-future): remove once all callers migrated.
439
515
  */
440
516
  export function syncStateToProjectRoot(
441
517
  worktreePath_: string,
@@ -625,6 +701,30 @@ export function cleanStaleRuntimeUnits(
625
701
 
626
702
  // ─── Worktree ↔ Main Repo Sync (#1311) ──────────────────────────────────────
627
703
 
704
+ /**
705
+ * Scope-typed variant of syncGsdStateToWorktree.
706
+ *
707
+ * Takes an explicit (rootScope, worktreeScope) pair. Note: milestoneId is not
708
+ * used by syncGsdStateToWorktree — this variant only requires workspace
709
+ * identity. Asserts both scopes belong to the same workspace identity to
710
+ * prevent silent mismatch bugs.
711
+ */
712
+ export function syncGsdStateToWorktreeByScope(
713
+ rootScope: MilestoneScope,
714
+ worktreeScope: MilestoneScope,
715
+ ): { synced: string[] } {
716
+ if (rootScope.workspace.identityKey !== worktreeScope.workspace.identityKey) {
717
+ throw new Error(
718
+ `syncGsdStateToWorktreeByScope: scope identity mismatch — ` +
719
+ `rootScope.identityKey="${rootScope.workspace.identityKey}" ` +
720
+ `worktreeScope.identityKey="${worktreeScope.workspace.identityKey}"`,
721
+ );
722
+ }
723
+ const mainBasePath = rootScope.workspace.projectRoot;
724
+ const worktreePath_ = worktreeScope.workspace.worktreeRoot ?? worktreeScope.workspace.projectRoot;
725
+ return syncGsdStateToWorktree(mainBasePath, worktreePath_);
726
+ }
727
+
628
728
  /**
629
729
  * Sync .gsd/ state from the main repo into the worktree.
630
730
  *
@@ -639,6 +739,8 @@ export function cleanStaleRuntimeUnits(
639
739
  * Only adds missing content — never overwrites existing files in the worktree.
640
740
  * Worktree files are compatibility projections; DB/project root remains
641
741
  * authoritative for runtime state.
742
+ * @deprecated Use syncGsdStateToWorktreeByScope instead.
743
+ * TODO(C-future): remove once all callers migrated.
642
744
  */
643
745
  export function syncGsdStateToWorktree(
644
746
  mainBasePath: string,
@@ -1042,6 +1144,40 @@ export function enterBranchModeForMilestone(
1042
1144
  * remains authoritative; this never downgrades checked boxes in a local
1043
1145
  * worktree projection.
1044
1146
  */
1147
+ /**
1148
+ * Scope-typed variant of reconcilePlanCheckboxes.
1149
+ *
1150
+ * Takes an explicit (rootScope, worktreeScope) pair. milestoneId is taken
1151
+ * from rootScope. Asserts both scopes belong to the same workspace identity
1152
+ * to prevent silent mismatch bugs.
1153
+ */
1154
+ export function reconcilePlanCheckboxesByScope(
1155
+ rootScope: MilestoneScope,
1156
+ worktreeScope: MilestoneScope,
1157
+ ): void {
1158
+ if (rootScope.workspace.identityKey !== worktreeScope.workspace.identityKey) {
1159
+ throw new Error(
1160
+ `reconcilePlanCheckboxesByScope: scope identity mismatch — ` +
1161
+ `rootScope.identityKey="${rootScope.workspace.identityKey}" ` +
1162
+ `worktreeScope.identityKey="${worktreeScope.workspace.identityKey}"`,
1163
+ );
1164
+ }
1165
+ if (rootScope.milestoneId !== worktreeScope.milestoneId) {
1166
+ throw new Error(
1167
+ `reconcilePlanCheckboxesByScope: milestoneId mismatch — ` +
1168
+ `rootScope.milestoneId="${rootScope.milestoneId}" worktreeScope.milestoneId="${worktreeScope.milestoneId}"`,
1169
+ );
1170
+ }
1171
+ const projectRoot = rootScope.workspace.projectRoot;
1172
+ const wtPath = worktreeScope.workspace.worktreeRoot ?? worktreeScope.workspace.projectRoot;
1173
+ const milestoneId = rootScope.milestoneId;
1174
+ reconcilePlanCheckboxes(projectRoot, wtPath, milestoneId);
1175
+ }
1176
+
1177
+ /**
1178
+ * @deprecated Use reconcilePlanCheckboxesByScope instead.
1179
+ * TODO(C-future): remove once all callers migrated.
1180
+ */
1045
1181
  function reconcilePlanCheckboxes(
1046
1182
  projectRoot: string,
1047
1183
  wtPath: string,
@@ -1218,10 +1354,10 @@ export function createAutoWorktree(
1218
1354
 
1219
1355
  try {
1220
1356
  process.chdir(info.path);
1221
- originalBase = basePath;
1357
+ setActiveWorkspace(createWorkspace(basePath));
1222
1358
  } catch (err) {
1223
1359
  // If chdir fails, the worktree was created but we couldn't enter it.
1224
- // Don't store originalBase -- caller can retry or clean up.
1360
+ // Don't set activeWorkspace -- caller can retry or clean up.
1225
1361
  throw new GSDError(
1226
1362
  GSD_IO_ERROR,
1227
1363
  `Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -1302,48 +1438,87 @@ export function teardownAutoWorktree(
1302
1438
  const { preserveBranch = false } = opts;
1303
1439
  const previousCwd = process.cwd();
1304
1440
 
1441
+ // Wrap the entire teardown body in a single try/finally so activeWorkspace
1442
+ // is ALWAYS cleared — even if process.chdir throws (e.g. originalBasePath
1443
+ // was deleted before teardown ran). Previously the finally only covered
1444
+ // removeWorktree, leaving the registry stale on a chdir failure (H3 fix).
1305
1445
  try {
1306
- process.chdir(originalBasePath);
1307
- originalBase = null;
1308
- } catch (err) {
1309
- throw new GSDError(
1310
- GSD_IO_ERROR,
1311
- `Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`,
1312
- );
1313
- }
1446
+ try {
1447
+ process.chdir(originalBasePath);
1448
+ } catch (err) {
1449
+ throw new GSDError(
1450
+ GSD_IO_ERROR,
1451
+ `Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`,
1452
+ );
1453
+ }
1314
1454
 
1315
- nudgeGitBranchCache(previousCwd);
1316
- removeWorktree(originalBasePath, milestoneId, {
1317
- branch,
1318
- deleteBranch: !preserveBranch,
1319
- });
1455
+ // Mirror cleanup steps from mergeMilestoneToMain abort path:
1320
1456
 
1321
- // Verify cleanup succeeded warn if the worktree directory is still on disk.
1322
- // On Windows, bash-based cleanup can silently fail when paths contain
1323
- // backslashes (#1436), leaving ~1 GB+ orphaned directories.
1324
- const wtDir = worktreePath(originalBasePath, milestoneId);
1325
- if (existsSync(wtDir)) {
1326
- logWarning(
1327
- "reconcile",
1328
- `Worktree directory still exists after teardown: ${wtDir}. ` +
1329
- `This is likely an orphaned directory consuming disk space. ` +
1330
- `Remove it manually with: rm -rf "${wtDir.replaceAll("\\", "/")}"`,
1331
- { worktree: milestoneId },
1332
- );
1333
- // Attempt a direct filesystem removal as a fallback — but ONLY if the
1334
- // path is safely inside .gsd/worktrees/ to prevent #2365 data loss.
1335
- if (isInsideWorktreesDir(originalBasePath, wtDir)) {
1457
+ // 1. Remove transient state files (STATE.md, auto.lock, {MID}-META.json).
1458
+ // Non-fatal must not block teardown.
1459
+ try {
1460
+ clearProjectRootStateFiles(originalBasePath, milestoneId);
1461
+ } catch (err) {
1462
+ logWarning("worktree", `clearProjectRootStateFiles failed during teardown: ${err instanceof Error ? err.message : String(err)}`);
1463
+ }
1464
+
1465
+ // 2. Reconcile worktree-local gsd.db into project root DB if both exist.
1466
+ // Non-fatal handles legacy worktrees that have a local copy.
1467
+ if (isDbAvailable()) {
1336
1468
  try {
1337
- rmSync(wtDir, { recursive: true, force: true });
1469
+ const contract = resolveGsdPathContract(previousCwd, originalBasePath);
1470
+ const worktreeDbPath = join(contract.worktreeGsd ?? join(previousCwd, ".gsd"), "gsd.db");
1471
+ const mainDbPath = contract.projectDb;
1472
+ if (existsSync(worktreeDbPath) && !isSamePath(worktreeDbPath, mainDbPath)) {
1473
+ reconcileWorktreeDb(mainDbPath, worktreeDbPath);
1474
+ }
1338
1475
  } catch (err) {
1339
- // Non-fatal — the warning above tells the user how to clean up
1340
- logWarning("worktree", `worktree directory removal failed: ${err instanceof Error ? err.message : String(err)}`);
1476
+ /* non-fatal */
1477
+ logError("worktree", `DB reconciliation failed during teardown: ${err instanceof Error ? err.message : String(err)}`);
1341
1478
  }
1342
- } else {
1343
- console.error(
1344
- `[GSD] REFUSING fallback rmSync — path is outside .gsd/worktrees/: ${wtDir}`,
1479
+ }
1480
+
1481
+ nudgeGitBranchCache(previousCwd);
1482
+
1483
+ // 3. Remove the worktree. Errors propagate naturally — the outer finally
1484
+ // ensures activeWorkspace is cleared regardless.
1485
+ removeWorktree(originalBasePath, milestoneId, {
1486
+ branch,
1487
+ deleteBranch: !preserveBranch,
1488
+ });
1489
+
1490
+ // Verify cleanup succeeded — warn if the worktree directory is still on disk.
1491
+ // On Windows, bash-based cleanup can silently fail when paths contain
1492
+ // backslashes (#1436), leaving ~1 GB+ orphaned directories.
1493
+ const wtDir = worktreePath(originalBasePath, milestoneId);
1494
+ if (existsSync(wtDir)) {
1495
+ logWarning(
1496
+ "reconcile",
1497
+ `Worktree directory still exists after teardown: ${wtDir}. ` +
1498
+ `This is likely an orphaned directory consuming disk space. ` +
1499
+ `Remove it manually with: rm -rf "${wtDir.replaceAll("\\", "/")}"`,
1500
+ { worktree: milestoneId },
1345
1501
  );
1502
+ // Attempt a direct filesystem removal as a fallback — but ONLY if the
1503
+ // path is safely inside .gsd/worktrees/ to prevent #2365 data loss.
1504
+ if (isInsideWorktreesDir(originalBasePath, wtDir)) {
1505
+ try {
1506
+ rmSync(wtDir, { recursive: true, force: true });
1507
+ } catch (err) {
1508
+ // Non-fatal — the warning above tells the user how to clean up
1509
+ logWarning("worktree", `worktree directory removal failed: ${err instanceof Error ? err.message : String(err)}`);
1510
+ }
1511
+ } else {
1512
+ console.error(
1513
+ `[GSD] REFUSING fallback rmSync — path is outside .gsd/worktrees/: ${wtDir}`,
1514
+ );
1515
+ }
1346
1516
  }
1517
+ } finally {
1518
+ // Clear module state unconditionally — regardless of which step above
1519
+ // failed. A stale activeWorkspace causes getActiveAutoWorktreeContext()
1520
+ // to return wrong data for subsequent operations.
1521
+ setActiveWorkspace(null);
1347
1522
  }
1348
1523
  }
1349
1524
 
@@ -1356,8 +1531,9 @@ export function isInAutoWorktree(basePath: string): boolean {
1356
1531
  const targetPath = isGsdWorktreePath(basePath) ? basePath : process.cwd();
1357
1532
  if (!isGsdWorktreePath(targetPath)) return false;
1358
1533
 
1359
- const projectRoot = resolveWorktreeProjectRoot(basePath, originalBase);
1360
- const targetProjectRoot = resolveWorktreeProjectRoot(targetPath, originalBase);
1534
+ const storedBase = getAutoWorktreeOriginalBase();
1535
+ const projectRoot = resolveWorktreeProjectRoot(basePath, storedBase);
1536
+ const targetProjectRoot = resolveWorktreeProjectRoot(targetPath, storedBase);
1361
1537
  if (
1362
1538
  normalizeWorktreePathForCompare(projectRoot) !==
1363
1539
  normalizeWorktreePathForCompare(targetProjectRoot)
@@ -1453,7 +1629,7 @@ export function enterAutoWorktree(
1453
1629
 
1454
1630
  try {
1455
1631
  process.chdir(p);
1456
- originalBase = basePath;
1632
+ setActiveWorkspace(createWorkspace(basePath));
1457
1633
  } catch (err) {
1458
1634
  throw new GSDError(
1459
1635
  GSD_IO_ERROR,
@@ -1470,11 +1646,11 @@ export function enterAutoWorktree(
1470
1646
  * Returns null if not currently in an auto-worktree.
1471
1647
  */
1472
1648
  export function getAutoWorktreeOriginalBase(): string | null {
1473
- return originalBase;
1649
+ return getActiveWorkspace()?.projectRoot ?? null;
1474
1650
  }
1475
1651
 
1476
1652
  export function _resetAutoWorktreeOriginalBaseForTests(): void {
1477
- originalBase = null;
1653
+ setActiveWorkspace(null);
1478
1654
  }
1479
1655
 
1480
1656
  export function getActiveAutoWorktreeContext(): {
@@ -1482,7 +1658,9 @@ export function getActiveAutoWorktreeContext(): {
1482
1658
  worktreeName: string;
1483
1659
  branch: string;
1484
1660
  } | null {
1485
- if (!originalBase) return null;
1661
+ const ws = getActiveWorkspace();
1662
+ if (!ws) return null;
1663
+ const originalBase = ws.projectRoot;
1486
1664
  const cwd = process.cwd();
1487
1665
  if (!isGsdWorktreePath(cwd)) return null;
1488
1666
  const cwdProjectRoot = resolveWorktreeProjectRoot(cwd, originalBase);
@@ -1563,11 +1741,11 @@ export function mergeMilestoneToMain(
1563
1741
  // integration branch captures dirty files from OTHER milestones under a
1564
1742
  // misleading commit message, contaminating the main branch (#2929).
1565
1743
  //
1566
- // When originalBase is null (branch mode, no worktree), autoCommitDirtyState
1744
+ // When activeWorkspace is null (branch mode, no worktree), autoCommitDirtyState
1567
1745
  // runs unconditionally — the caller is responsible for cwd placement.
1568
1746
  {
1569
1747
  let shouldAutoCommit = true;
1570
- if (originalBase !== null) {
1748
+ if (getActiveWorkspace() !== null) {
1571
1749
  try {
1572
1750
  const currentBranch = nativeGetCurrentBranch(worktreeCwd);
1573
1751
  shouldAutoCommit = currentBranch === milestoneBranch;
@@ -2302,7 +2480,7 @@ export function mergeMilestoneToMain(
2302
2480
  }
2303
2481
 
2304
2482
  // 14. Clear module state
2305
- originalBase = null;
2483
+ setActiveWorkspace(null);
2306
2484
  nudgeGitBranchCache(previousCwd);
2307
2485
 
2308
2486
  // 15. Anchor cwd at the project root on success-return. Step 12 removed
@@ -256,6 +256,7 @@ export type {
256
256
  } from "./auto/session.js";
257
257
  import { autoSession as s } from "./auto-runtime-state.js";
258
258
  import { gsdHome } from "./gsd-home.js";
259
+ import { createWorkspace, scopeMilestone } from "./workspace.js";
259
260
 
260
261
  // ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
261
262
  // ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
@@ -324,6 +325,32 @@ function restoreMilestoneLockEnv(): void {
324
325
  s.milestoneLockEnvCaptured = false;
325
326
  }
326
327
 
328
+ /**
329
+ * Rebuild s.scope from the current s.basePath / s.originalBasePath / s.currentMilestoneId.
330
+ *
331
+ * Pass the worktree path as rawPath when entering a worktree so createWorkspace
332
+ * can detect the worktree layout and set mode="worktree". When no worktree is
333
+ * active, rawPath should equal the project root.
334
+ *
335
+ * Clears s.scope when milestoneId is absent — scope is only meaningful when a
336
+ * milestone is active.
337
+ *
338
+ * TODO(C8): remove basePath/originalBasePath once all readers use s.scope.
339
+ */
340
+ function rebuildScope(rawPath: string, milestoneId: string | null): void {
341
+ if (!milestoneId) {
342
+ s.scope = null;
343
+ return;
344
+ }
345
+ try {
346
+ const workspace = createWorkspace(rawPath);
347
+ s.scope = scopeMilestone(workspace, milestoneId);
348
+ } catch {
349
+ // Non-fatal — scope is additive. Existing readers still use basePath.
350
+ s.scope = null;
351
+ }
352
+ }
353
+
327
354
  function normalizeSessionFilePath(raw: unknown): string | null {
328
355
  if (typeof raw !== "string") return null;
329
356
  const trimmed = raw.trim();
@@ -509,6 +536,26 @@ export function _setAutoActiveForTest(active: boolean): void {
509
536
  s.active = active;
510
537
  }
511
538
 
539
+ /**
540
+ * Test-only seam: emit the missing-worktree warning exactly as the resume path
541
+ * does. Allows unit tests to verify the warning is produced without
542
+ * bootstrapping the full auto-mode entry point. Do not use in production code.
543
+ */
544
+ export function _warnIfWorktreeMissingForTest(
545
+ worktreePath: string | null | undefined,
546
+ milestoneId: string,
547
+ ): boolean {
548
+ if (worktreePath && !existsSync(worktreePath)) {
549
+ logWarning(
550
+ "session",
551
+ `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.`,
552
+ { file: "auto.ts", milestoneId },
553
+ );
554
+ return true;
555
+ }
556
+ return false;
557
+ }
558
+
512
559
  export function isAutoPaused(): boolean {
513
560
  return s.paused;
514
561
  }
@@ -1553,6 +1600,22 @@ export async function startAuto(
1553
1600
  s.autoStartTime = meta.autoStartTime || Date.now();
1554
1601
  s.sessionMilestoneLock = meta.milestoneLock ?? null;
1555
1602
  s.paused = true;
1603
+ // Build scope from persisted state. Use worktreePath when present and
1604
+ // still on disk so mode is detected correctly; fall back to project root.
1605
+ {
1606
+ const persistedWorktreePath = meta.worktreePath ?? null;
1607
+ if (persistedWorktreePath && !existsSync(persistedWorktreePath)) {
1608
+ logWarning(
1609
+ "session",
1610
+ `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.`,
1611
+ { file: "auto.ts", milestoneId: meta.milestoneId ?? "" },
1612
+ );
1613
+ }
1614
+ const rawForScope = (persistedWorktreePath && existsSync(persistedWorktreePath))
1615
+ ? persistedWorktreePath
1616
+ : (s.originalBasePath || base);
1617
+ rebuildScope(rawForScope, s.currentMilestoneId);
1618
+ }
1556
1619
  try { unlinkSync(pausedPath); } catch (e) {
1557
1620
  if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
1558
1621
  logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
@@ -1637,10 +1700,19 @@ export async function startAuto(
1637
1700
  // session (e.g. isolation mode changed, detectWorktreeName differs across
1638
1701
  // process restarts). We guard with existsSync so a stale or deleted
1639
1702
  // worktree directory safely falls back to the project root.
1640
- const resumeWorktreePath = freshStartAssessment.pausedSession?.worktreePath;
1703
+ const resumeWorktreePath = freshStartAssessment.pausedSession?.worktreePath ?? null;
1704
+ if (resumeWorktreePath && !existsSync(resumeWorktreePath)) {
1705
+ logWarning(
1706
+ "session",
1707
+ `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.`,
1708
+ { file: "auto.ts", milestoneId: s.currentMilestoneId ?? "" },
1709
+ );
1710
+ }
1641
1711
  if (resumeWorktreePath && existsSync(resumeWorktreePath)) {
1642
1712
  s.basePath = resumeWorktreePath;
1643
1713
  }
1714
+ // Rebuild scope now that s.basePath reflects the actual worktree (or project root).
1715
+ rebuildScope(s.basePath, s.currentMilestoneId);
1644
1716
  // Ensure the workflow-logger audit log is pinned to the project root
1645
1717
  // even when auto-mode is entered via a path that bypasses the
1646
1718
  // bootstrap/dynamic-tools ensureDbOpen() → setLogBasePath() chain
@@ -1669,6 +1741,8 @@ export async function startAuto(
1669
1741
  buildResolver().enterMilestone(s.currentMilestoneId, {
1670
1742
  notify: ctx.ui.notify.bind(ctx.ui),
1671
1743
  });
1744
+ // s.basePath may have been updated to a worktree path by enterMilestone.
1745
+ rebuildScope(s.basePath, s.currentMilestoneId);
1672
1746
  }
1673
1747
 
1674
1748
  registerSigtermHandler(lockBase());
@@ -1783,6 +1857,10 @@ export async function startAuto(
1783
1857
  );
1784
1858
  if (!ready) return;
1785
1859
 
1860
+ // Build scope after bootstrap has populated s.basePath / s.originalBasePath /
1861
+ // s.currentMilestoneId (including worktree setup inside bootstrapAutoSession).
1862
+ rebuildScope(s.basePath, s.currentMilestoneId);
1863
+
1786
1864
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1787
1865
  try {
1788
1866
  pi.events.emit(CMUX_CHANNELS.SIDEBAR, { action: "sync" as const, preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, state: await deriveState(s.basePath) });
@@ -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;
@@ -588,10 +590,10 @@ export function registerHooks(
588
590
  if (pendingQuestion) {
589
591
  const answer = details.response?.answers?.[currentPendingGate];
590
592
  if (isDepthConfirmationAnswer(answer?.selected, pendingQuestion.options)) {
591
- markApprovalGateVerified(currentPendingGate);
593
+ markApprovalGateVerified(currentPendingGate, basePath);
592
594
  const milestoneIdFromGate = extractDepthVerificationMilestoneId(currentPendingGate);
593
- if (milestoneIdFromGate) markDepthVerified(milestoneIdFromGate);
594
- clearPendingGate();
595
+ if (milestoneIdFromGate) markDepthVerified(milestoneIdFromGate, basePath);
596
+ clearPendingGate(basePath);
595
597
  }
596
598
  }
597
599
  }
@@ -607,9 +609,9 @@ export function registerHooks(
607
609
  const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
608
610
  if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
609
611
  if (currentPendingGate && question.id !== currentPendingGate) break;
610
- markApprovalGateVerified(question.id);
611
- markDepthVerified(inferredMilestoneId);
612
- clearPendingGate();
612
+ markApprovalGateVerified(question.id, basePath);
613
+ markDepthVerified(inferredMilestoneId, basePath);
614
+ clearPendingGate(basePath);
613
615
  }
614
616
  break;
615
617
  }
@@ -617,8 +619,6 @@ export function registerHooks(
617
619
 
618
620
  if (!milestoneId && !queueActive) return;
619
621
  if (!milestoneId) return;
620
-
621
- const basePath = process.cwd();
622
622
  const milestoneDir = resolveMilestonePath(basePath, milestoneId);
623
623
  if (!milestoneDir) return;
624
624