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
@@ -24,38 +24,38 @@ import {
24
24
  // ─── shouldBlockQueueExecution ────────────────────────────────────────────
25
25
 
26
26
  test('shouldBlockQueueExecution: queue inactive → allow write to user source', (t) => {
27
- t.after(() => clearDiscussionFlowState());
28
- setQueuePhaseActive(false);
27
+ t.after(() => clearDiscussionFlowState(process.cwd()));
28
+ setQueuePhaseActive(false, process.cwd());
29
29
  const r = shouldBlockQueueExecution('write', 'src/main.ts', false);
30
30
  assert.strictEqual(r.block, false);
31
31
  });
32
32
 
33
33
  test('shouldBlockQueueExecution: queue active → block write to user source', (t) => {
34
- t.after(() => clearDiscussionFlowState());
35
- setQueuePhaseActive(true);
34
+ t.after(() => clearDiscussionFlowState(process.cwd()));
35
+ setQueuePhaseActive(true, process.cwd());
36
36
  const r = shouldBlockQueueExecution('write', 'src/main.ts', true);
37
37
  assert.strictEqual(r.block, true);
38
38
  assert.ok(r.reason);
39
39
  });
40
40
 
41
41
  test('shouldBlockQueueExecution: queue active → allow write to .gsd/ path', (t) => {
42
- t.after(() => clearDiscussionFlowState());
43
- setQueuePhaseActive(true);
42
+ t.after(() => clearDiscussionFlowState(process.cwd()));
43
+ setQueuePhaseActive(true, process.cwd());
44
44
  const r = shouldBlockQueueExecution('write', '.gsd/milestones/M001/M001-CONTEXT.md', true);
45
45
  assert.strictEqual(r.block, false);
46
46
  });
47
47
 
48
48
  test('shouldBlockQueueExecution: queue active → block mutating bash', (t) => {
49
- t.after(() => clearDiscussionFlowState());
50
- setQueuePhaseActive(true);
49
+ t.after(() => clearDiscussionFlowState(process.cwd()));
50
+ setQueuePhaseActive(true, process.cwd());
51
51
  const r = shouldBlockQueueExecution('bash', 'npm run build', true);
52
52
  assert.strictEqual(r.block, true);
53
53
  assert.ok(r.reason);
54
54
  });
55
55
 
56
56
  test('shouldBlockQueueExecution: queue active → allow read-only bash', (t) => {
57
- t.after(() => clearDiscussionFlowState());
58
- setQueuePhaseActive(true);
57
+ t.after(() => clearDiscussionFlowState(process.cwd()));
58
+ setQueuePhaseActive(true, process.cwd());
59
59
  const r = shouldBlockQueueExecution('bash', 'git log --oneline -5', true);
60
60
  assert.strictEqual(r.block, false);
61
61
  });
@@ -63,30 +63,30 @@ test('shouldBlockQueueExecution: queue active → allow read-only bash', (t) =>
63
63
  // ─── shouldBlockPendingGate ───────────────────────────────────────────────
64
64
 
65
65
  test('shouldBlockPendingGate: no pending gate → allow any tool', (t) => {
66
- t.after(() => clearDiscussionFlowState());
67
- clearPendingGate();
66
+ t.after(() => clearDiscussionFlowState(process.cwd()));
67
+ clearPendingGate(process.cwd());
68
68
  const r = shouldBlockPendingGate('write', 'M001');
69
69
  assert.strictEqual(r.block, false);
70
70
  });
71
71
 
72
72
  test('shouldBlockPendingGate: pending gate → block write', (t) => {
73
- t.after(() => clearDiscussionFlowState());
74
- setPendingGate('depth_verification_M001');
73
+ t.after(() => clearDiscussionFlowState(process.cwd()));
74
+ setPendingGate('depth_verification_M001', process.cwd());
75
75
  const r = shouldBlockPendingGate('write', 'M001');
76
76
  assert.strictEqual(r.block, true);
77
77
  assert.ok(r.reason?.includes('depth_verification_M001'));
78
78
  });
79
79
 
80
80
  test('shouldBlockPendingGate: pending gate → allow ask_user_questions', (t) => {
81
- t.after(() => clearDiscussionFlowState());
82
- setPendingGate('depth_verification_M001');
81
+ t.after(() => clearDiscussionFlowState(process.cwd()));
82
+ setPendingGate('depth_verification_M001', process.cwd());
83
83
  const r = shouldBlockPendingGate('ask_user_questions', 'M001');
84
84
  assert.strictEqual(r.block, false);
85
85
  });
86
86
 
87
87
  test('shouldBlockPendingGate: pending gate → block read so approval question stays visible', (t) => {
88
- t.after(() => clearDiscussionFlowState());
89
- setPendingGate('depth_verification_M001');
88
+ t.after(() => clearDiscussionFlowState(process.cwd()));
89
+ setPendingGate('depth_verification_M001', process.cwd());
90
90
  const r = shouldBlockPendingGate('read', 'M001');
91
91
  assert.strictEqual(r.block, true);
92
92
  assert.ok(r.reason?.includes('already asked for user confirmation'));
@@ -95,31 +95,31 @@ test('shouldBlockPendingGate: pending gate → block read so approval question s
95
95
  // ─── shouldBlockPendingGateBash ───────────────────────────────────────────
96
96
 
97
97
  test('shouldBlockPendingGateBash: no pending gate → allow mutating bash', (t) => {
98
- t.after(() => clearDiscussionFlowState());
99
- clearPendingGate();
98
+ t.after(() => clearDiscussionFlowState(process.cwd()));
99
+ clearPendingGate(process.cwd());
100
100
  const r = shouldBlockPendingGateBash('npm run build', 'M001');
101
101
  assert.strictEqual(r.block, false);
102
102
  });
103
103
 
104
104
  test('shouldBlockPendingGateBash: pending gate → block mutating bash', (t) => {
105
- t.after(() => clearDiscussionFlowState());
106
- setPendingGate('depth_verification_M001');
105
+ t.after(() => clearDiscussionFlowState(process.cwd()));
106
+ setPendingGate('depth_verification_M001', process.cwd());
107
107
  const r = shouldBlockPendingGateBash('npm run build', 'M001');
108
108
  assert.strictEqual(r.block, true);
109
109
  assert.ok(r.reason?.includes('depth_verification_M001'));
110
110
  });
111
111
 
112
112
  test('shouldBlockPendingGateBash: pending gate → block read-only bash (cat)', (t) => {
113
- t.after(() => clearDiscussionFlowState());
114
- setPendingGate('depth_verification_M001');
113
+ t.after(() => clearDiscussionFlowState(process.cwd()));
114
+ setPendingGate('depth_verification_M001', process.cwd());
115
115
  const r = shouldBlockPendingGateBash('cat README.md', 'M001');
116
116
  assert.strictEqual(r.block, true);
117
117
  assert.ok(r.reason?.includes('already asked for user confirmation'));
118
118
  });
119
119
 
120
120
  test('shouldBlockPendingGateBash: pending gate → block read-only bash (git log)', (t) => {
121
- t.after(() => clearDiscussionFlowState());
122
- setPendingGate('depth_verification_M001');
121
+ t.after(() => clearDiscussionFlowState(process.cwd()));
122
+ setPendingGate('depth_verification_M001', process.cwd());
123
123
  const r = shouldBlockPendingGateBash('git log --oneline -10', 'M001');
124
124
  assert.strictEqual(r.block, true);
125
125
  });
@@ -127,26 +127,26 @@ test('shouldBlockPendingGateBash: pending gate → block read-only bash (git log
127
127
  // ─── shouldBlockContextWrite ──────────────────────────────────────────────
128
128
 
129
129
  test('shouldBlockContextWrite: non-write tool → allow', (t) => {
130
- t.after(() => clearDiscussionFlowState());
130
+ t.after(() => clearDiscussionFlowState(process.cwd()));
131
131
  const r = shouldBlockContextWrite('read', '.gsd/milestones/M001/M001-CONTEXT.md', 'M001');
132
132
  assert.strictEqual(r.block, false);
133
133
  });
134
134
 
135
135
  test('shouldBlockContextWrite: write to non-CONTEXT file → allow', (t) => {
136
- t.after(() => clearDiscussionFlowState());
136
+ t.after(() => clearDiscussionFlowState(process.cwd()));
137
137
  const r = shouldBlockContextWrite('write', 'src/index.ts', 'M001');
138
138
  assert.strictEqual(r.block, false);
139
139
  });
140
140
 
141
141
  test('shouldBlockContextWrite: write to CONTEXT.md without verification → block', (t) => {
142
- t.after(() => clearDiscussionFlowState());
142
+ t.after(() => clearDiscussionFlowState(process.cwd()));
143
143
  const r = shouldBlockContextWrite('write', '.gsd/milestones/M007/M007-CONTEXT.md', 'M007');
144
144
  assert.strictEqual(r.block, true);
145
145
  assert.ok(r.reason);
146
146
  });
147
147
 
148
148
  test('shouldBlockContextWrite: write to CONTEXT.md after verification → allow', (t) => {
149
- t.after(() => clearDiscussionFlowState());
149
+ t.after(() => clearDiscussionFlowState(process.cwd()));
150
150
  markDepthVerified('M008');
151
151
  const r = shouldBlockContextWrite('write', '.gsd/milestones/M008/M008-CONTEXT.md', 'M008');
152
152
  assert.strictEqual(r.block, false);
@@ -155,33 +155,33 @@ test('shouldBlockContextWrite: write to CONTEXT.md after verification → allow'
155
155
  // ─── shouldBlockContextArtifactSave ───────────────────────────────────────
156
156
 
157
157
  test('shouldBlockContextArtifactSave: non-CONTEXT artifact type → allow', (t) => {
158
- t.after(() => clearDiscussionFlowState());
158
+ t.after(() => clearDiscussionFlowState(process.cwd()));
159
159
  const r = shouldBlockContextArtifactSave('CONTEXT-DRAFT', 'M001');
160
160
  assert.strictEqual(r.block, false);
161
161
  });
162
162
 
163
163
  test('shouldBlockContextArtifactSave: slice-level CONTEXT → allow', (t) => {
164
- t.after(() => clearDiscussionFlowState());
164
+ t.after(() => clearDiscussionFlowState(process.cwd()));
165
165
  const r = shouldBlockContextArtifactSave('CONTEXT', 'M001', 'S01');
166
166
  assert.strictEqual(r.block, false);
167
167
  });
168
168
 
169
169
  test('shouldBlockContextArtifactSave: milestone CONTEXT without verification → block', (t) => {
170
- t.after(() => clearDiscussionFlowState());
170
+ t.after(() => clearDiscussionFlowState(process.cwd()));
171
171
  const r = shouldBlockContextArtifactSave('CONTEXT', 'M009');
172
172
  assert.strictEqual(r.block, true);
173
173
  assert.ok(r.reason?.includes('M009'));
174
174
  });
175
175
 
176
176
  test('shouldBlockContextArtifactSave: milestone CONTEXT after verification → allow', (t) => {
177
- t.after(() => clearDiscussionFlowState());
177
+ t.after(() => clearDiscussionFlowState(process.cwd()));
178
178
  markDepthVerified('M010');
179
179
  const r = shouldBlockContextArtifactSave('CONTEXT', 'M010');
180
180
  assert.strictEqual(r.block, false);
181
181
  });
182
182
 
183
183
  test('shouldBlockContextArtifactSave: CONTEXT with no milestoneId → block', (t) => {
184
- t.after(() => clearDiscussionFlowState());
184
+ t.after(() => clearDiscussionFlowState(process.cwd()));
185
185
  const r = shouldBlockContextArtifactSave('CONTEXT', null);
186
186
  assert.strictEqual(r.block, true);
187
187
  assert.ok(r.reason);
@@ -9,14 +9,16 @@
9
9
  * (e) else → block with actionable reason
10
10
  */
11
11
 
12
- import test from 'node:test';
12
+ import test, { afterEach } from 'node:test';
13
13
  import assert from 'node:assert/strict';
14
14
  import { mkdirSync, writeFileSync, unlinkSync, existsSync, rmSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  import { tmpdir } from 'node:os';
17
17
  import { randomUUID } from 'node:crypto';
18
18
  import {
19
+ isDepthVerified,
19
20
  isDepthConfirmationAnswer,
21
+ isQueuePhaseActive,
20
22
  shouldBlockContextWrite,
21
23
  setQueuePhaseActive,
22
24
  } from '../index.ts';
@@ -32,6 +34,10 @@ import {
32
34
  loadWriteGateSnapshot,
33
35
  } from '../bootstrap/write-gate.ts';
34
36
 
37
+ afterEach(() => {
38
+ clearDiscussionFlowState(process.cwd());
39
+ });
40
+
35
41
  // ─── Scenario 1: Blocks CONTEXT.md write during discussion without depth verification (absolute path) ──
36
42
 
37
43
  test('write-gate: blocks CONTEXT.md write during discussion without depth verification (absolute path)', () => {
@@ -61,7 +67,7 @@ test('write-gate: blocks CONTEXT.md write during discussion without depth verifi
61
67
  // ─── Scenario 3: Allows CONTEXT.md write after depth verification ──
62
68
 
63
69
  test('write-gate: allows CONTEXT.md write after depth verification', () => {
64
- clearDiscussionFlowState();
70
+ clearDiscussionFlowState(process.cwd());
65
71
  markDepthVerified('M001');
66
72
  const result = shouldBlockContextWrite(
67
73
  'write',
@@ -70,7 +76,6 @@ test('write-gate: allows CONTEXT.md write after depth verification', () => {
70
76
  );
71
77
  assert.strictEqual(result.block, false, 'should not block after depth verification');
72
78
  assert.strictEqual(result.reason, undefined, 'should have no reason');
73
- clearDiscussionFlowState();
74
79
  });
75
80
 
76
81
  // ─── Scenario 4: Ambiguous session context no longer bypasses the gate ──
@@ -154,7 +159,7 @@ test('write-gate: blocks CONTEXT.md write in queue mode without depth verificati
154
159
  // ─── Scenario 9: Queue mode allows CONTEXT.md write after depth verification ──
155
160
 
156
161
  test('write-gate: allows CONTEXT.md write in queue mode after depth verification', () => {
157
- clearDiscussionFlowState();
162
+ clearDiscussionFlowState(process.cwd());
158
163
  markDepthVerified('M001');
159
164
  const result = shouldBlockContextWrite(
160
165
  'write',
@@ -163,13 +168,12 @@ test('write-gate: allows CONTEXT.md write in queue mode after depth verification
163
168
  true, // queue phase active
164
169
  );
165
170
  assert.strictEqual(result.block, false, 'should not block in queue mode after depth verification');
166
- clearDiscussionFlowState();
167
171
  });
168
172
 
169
173
  // ─── Scenario 10: depth verification is scoped per milestone, not global ──
170
174
 
171
175
  test('write-gate: markDepthVerified unlocks only the matching milestone', () => {
172
- clearDiscussionFlowState();
176
+ clearDiscussionFlowState(process.cwd());
173
177
  markDepthVerified('M001');
174
178
 
175
179
  const allowed = shouldBlockContextWrite(
@@ -187,14 +191,12 @@ test('write-gate: markDepthVerified unlocks only the matching milestone', () =>
187
191
  assert.strictEqual(blockedOther.block, true, 'other milestones should remain blocked');
188
192
  assert.strictEqual(isMilestoneDepthVerified('M001'), true);
189
193
  assert.strictEqual(isMilestoneDepthVerified('M002'), false);
190
-
191
- clearDiscussionFlowState();
192
194
  });
193
195
 
194
196
  // ─── Scenario 11: gsd_summary_save CONTEXT contract is milestone-scoped ──
195
197
 
196
198
  test('write-gate: gsd_summary_save only blocks final milestone CONTEXT writes', () => {
197
- clearDiscussionFlowState();
199
+ clearDiscussionFlowState(process.cwd());
198
200
 
199
201
  assert.strictEqual(
200
202
  shouldBlockContextArtifactSave('CONTEXT-DRAFT', 'M001').block,
@@ -218,8 +220,6 @@ test('write-gate: gsd_summary_save only blocks final milestone CONTEXT writes',
218
220
  false,
219
221
  'final milestone CONTEXT should pass after verification',
220
222
  );
221
-
222
- clearDiscussionFlowState();
223
223
  });
224
224
 
225
225
  test('write-gate: root PROJECT/REQUIREMENTS final saves block behind pending approval gate', () => {
@@ -299,33 +299,39 @@ test('write-gate: deep root PROJECT/REQUIREMENTS final saves require verified ap
299
299
  });
300
300
 
301
301
  test('write-gate: reopening a gate revokes its previous verified approval', () => {
302
- clearDiscussionFlowState();
303
-
304
- markApprovalGateVerified('depth_verification_project_confirm');
305
- assert.strictEqual(
306
- shouldBlockRootArtifactSaveInSnapshot(
307
- loadWriteGateSnapshot(),
308
- 'PROJECT',
309
- { requireVerifiedApproval: true },
310
- ).block,
311
- false,
312
- 'precondition: verified approval unlocks the final project artifact',
313
- );
314
-
315
- setPendingGate('depth_verification_project_confirm');
316
- clearPendingGate();
317
-
318
- assert.strictEqual(
319
- shouldBlockRootArtifactSaveInSnapshot(
320
- loadWriteGateSnapshot(),
321
- 'PROJECT',
322
- { requireVerifiedApproval: true },
323
- ).block,
324
- true,
325
- 'a re-asked gate must require a fresh approval',
326
- );
302
+ const base = join(tmpdir(), `gsd-write-gate-reopen-${randomUUID()}`);
303
+ mkdirSync(base, { recursive: true });
327
304
 
328
- clearDiscussionFlowState();
305
+ try {
306
+ clearDiscussionFlowState(base);
307
+
308
+ markApprovalGateVerified('depth_verification_project_confirm', base);
309
+ assert.strictEqual(
310
+ shouldBlockRootArtifactSaveInSnapshot(
311
+ loadWriteGateSnapshot(base),
312
+ 'PROJECT',
313
+ { requireVerifiedApproval: true },
314
+ ).block,
315
+ false,
316
+ 'precondition: verified approval unlocks the final project artifact',
317
+ );
318
+
319
+ setPendingGate('depth_verification_project_confirm', base);
320
+ clearPendingGate(base);
321
+
322
+ assert.strictEqual(
323
+ shouldBlockRootArtifactSaveInSnapshot(
324
+ loadWriteGateSnapshot(base),
325
+ 'PROJECT',
326
+ { requireVerifiedApproval: true },
327
+ ).block,
328
+ true,
329
+ 'a re-asked gate must require a fresh approval',
330
+ );
331
+ } finally {
332
+ clearDiscussionFlowState(base);
333
+ rmSync(base, { recursive: true, force: true });
334
+ }
329
335
  });
330
336
 
331
337
  // ═══════════════════════════════════════════════════════════════════════
@@ -357,26 +363,26 @@ test('write-gate: isGateQuestionId recognizes all gate patterns', () => {
357
363
  // ─── Scenario 20: setPendingGate / getPendingGate / clearPendingGate lifecycle ──
358
364
 
359
365
  test('write-gate: pending gate lifecycle (set, get, clear)', () => {
360
- clearDiscussionFlowState();
366
+ clearDiscussionFlowState(process.cwd());
361
367
  assert.strictEqual(getPendingGate(), null, 'starts null');
362
368
 
363
- setPendingGate('depth_verification');
369
+ setPendingGate('depth_verification', process.cwd());
364
370
  assert.strictEqual(getPendingGate(), 'depth_verification', 'set correctly');
365
371
 
366
- clearPendingGate();
372
+ clearPendingGate(process.cwd());
367
373
  assert.strictEqual(getPendingGate(), null, 'cleared correctly');
368
374
 
369
375
  // clearDiscussionFlowState also clears pending gate
370
- setPendingGate('depth_verification_M002');
371
- clearDiscussionFlowState();
376
+ setPendingGate('depth_verification_M002', process.cwd());
377
+ clearDiscussionFlowState(process.cwd());
372
378
  assert.strictEqual(getPendingGate(), null, 'clearDiscussionFlowState clears pending gate');
373
379
  });
374
380
 
375
381
  // ─── Scenario 21: shouldBlockPendingGate blocks non-safe tools when gate is pending ──
376
382
 
377
383
  test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate', () => {
378
- clearDiscussionFlowState();
379
- setPendingGate('depth_verification');
384
+ clearDiscussionFlowState(process.cwd());
385
+ setPendingGate('depth_verification', process.cwd());
380
386
 
381
387
  // write should be blocked during discussion
382
388
  const writeResult = shouldBlockPendingGate('write', 'M001', false);
@@ -390,15 +396,13 @@ test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate',
390
396
  // gsd tools should be blocked
391
397
  const gsdResult = shouldBlockPendingGate('gsd_plan_milestone', 'M001', false);
392
398
  assert.strictEqual(gsdResult.block, true, 'gsd tools should be blocked');
393
-
394
- clearDiscussionFlowState();
395
399
  });
396
400
 
397
401
  // ─── Scenario 22: shouldBlockPendingGate allows only re-asking when gate is pending ──
398
402
 
399
403
  test('write-gate: shouldBlockPendingGate blocks read-only tools and allows ask_user_questions during pending gate', () => {
400
- clearDiscussionFlowState();
401
- setPendingGate('depth_verification');
404
+ clearDiscussionFlowState(process.cwd());
405
+ setPendingGate('depth_verification', process.cwd());
402
406
 
403
407
  // ask_user_questions is always safe (model needs to re-ask)
404
408
  assert.strictEqual(shouldBlockPendingGate('ask_user_questions', 'M001').block, false);
@@ -407,67 +411,57 @@ test('write-gate: shouldBlockPendingGate blocks read-only tools and allows ask_u
407
411
  assert.strictEqual(shouldBlockPendingGate('grep', 'M001').block, true);
408
412
  assert.strictEqual(shouldBlockPendingGate('glob', 'M001').block, true);
409
413
  assert.strictEqual(shouldBlockPendingGate('ls', 'M001').block, true);
410
-
411
- clearDiscussionFlowState();
412
414
  });
413
415
 
414
416
  // ─── Scenario 23: shouldBlockPendingGate still blocks when the session is ambiguous ──
415
417
 
416
418
  test('write-gate: shouldBlockPendingGate blocks outside discussion when a gate is pending', () => {
417
- clearDiscussionFlowState();
418
- setPendingGate('depth_verification');
419
+ clearDiscussionFlowState(process.cwd());
420
+ setPendingGate('depth_verification', process.cwd());
419
421
 
420
422
  // No milestoneId and no queue phase — still block because the gate is pending
421
423
  const result = shouldBlockPendingGate('write', null, false);
422
424
  assert.strictEqual(result.block, true, 'should block even when milestoneId is null');
423
-
424
- clearDiscussionFlowState();
425
425
  });
426
426
 
427
427
  // ─── Scenario 24: shouldBlockPendingGate blocks in queue mode ──
428
428
 
429
429
  test('write-gate: shouldBlockPendingGate blocks in queue mode when gate is pending', () => {
430
- clearDiscussionFlowState();
431
- setQueuePhaseActive(true);
432
- setPendingGate('depth_verification');
430
+ clearDiscussionFlowState(process.cwd());
431
+ setQueuePhaseActive(true, process.cwd());
432
+ setPendingGate('depth_verification', process.cwd());
433
433
 
434
434
  const result = shouldBlockPendingGate('write', null, true);
435
435
  assert.strictEqual(result.block, true, 'should block in queue mode');
436
-
437
- clearDiscussionFlowState();
438
436
  });
439
437
 
440
438
  // ─── Scenario 25: shouldBlockPendingGateBash blocks read-only commands ──
441
439
 
442
440
  test('write-gate: shouldBlockPendingGateBash blocks read-only commands during pending gate', () => {
443
- clearDiscussionFlowState();
444
- setPendingGate('depth_verification');
441
+ clearDiscussionFlowState(process.cwd());
442
+ setPendingGate('depth_verification', process.cwd());
445
443
 
446
444
  assert.strictEqual(shouldBlockPendingGateBash('cat file.txt', 'M001').block, true);
447
445
  assert.strictEqual(shouldBlockPendingGateBash('git log --oneline', 'M001').block, true);
448
446
  assert.strictEqual(shouldBlockPendingGateBash('grep -r pattern .', 'M001').block, true);
449
447
  assert.strictEqual(shouldBlockPendingGateBash('ls -la', 'M001').block, true);
450
-
451
- clearDiscussionFlowState();
452
448
  });
453
449
 
454
450
  // ─── Scenario 26: shouldBlockPendingGateBash blocks mutating commands ──
455
451
 
456
452
  test('write-gate: shouldBlockPendingGateBash blocks mutating commands during pending gate', () => {
457
- clearDiscussionFlowState();
458
- setPendingGate('depth_verification');
453
+ clearDiscussionFlowState(process.cwd());
454
+ setPendingGate('depth_verification', process.cwd());
459
455
 
460
456
  const result = shouldBlockPendingGateBash('npm run build', 'M001');
461
457
  assert.strictEqual(result.block, true, 'mutating bash should be blocked');
462
458
  assert.ok(result.reason!.includes('depth_verification'));
463
-
464
- clearDiscussionFlowState();
465
459
  });
466
460
 
467
461
  // ─── Scenario 27: no pending gate means no blocking ──
468
462
 
469
463
  test('write-gate: no pending gate means no blocking', () => {
470
- clearDiscussionFlowState();
464
+ clearDiscussionFlowState(process.cwd());
471
465
 
472
466
  assert.strictEqual(shouldBlockPendingGate('write', 'M001').block, false);
473
467
  assert.strictEqual(shouldBlockPendingGateBash('npm run build', 'M001').block, false);
@@ -476,11 +470,40 @@ test('write-gate: no pending gate means no blocking', () => {
476
470
  // ─── Scenario 28: resetWriteGateState clears pending gate ──
477
471
 
478
472
  test('write-gate: resetWriteGateState clears pending gate', () => {
479
- setPendingGate('depth_verification');
480
- resetWriteGateState();
473
+ setPendingGate('depth_verification', process.cwd());
474
+ resetWriteGateState(process.cwd());
481
475
  assert.strictEqual(getPendingGate(), null);
482
476
  });
483
477
 
478
+ test('write-gate: in-memory state is scoped by basePath', () => {
479
+ const workspaceA = join(tmpdir(), `gsd-write-gate-isolation-a-${randomUUID()}`);
480
+ const workspaceB = join(tmpdir(), `gsd-write-gate-isolation-b-${randomUUID()}`);
481
+
482
+ try {
483
+ clearDiscussionFlowState(workspaceA);
484
+ clearDiscussionFlowState(workspaceB);
485
+
486
+ setPendingGate('depth_verification_M777', workspaceA);
487
+ assert.strictEqual(getPendingGate(workspaceA), 'depth_verification_M777', 'workspace A should see its pending gate');
488
+ assert.strictEqual(getPendingGate(workspaceB), null, 'workspace B should not see workspace A pending gate');
489
+
490
+ clearPendingGate(workspaceA);
491
+ setQueuePhaseActive(true, workspaceA);
492
+ assert.strictEqual(isQueuePhaseActive(workspaceA), true, 'workspace A should see queue mode active');
493
+ assert.strictEqual(isQueuePhaseActive(workspaceB), false, 'workspace B should not see workspace A queue mode');
494
+
495
+ markDepthVerified('M777', workspaceA);
496
+ assert.strictEqual(isMilestoneDepthVerified('M777', workspaceA), true, 'workspace A should see its verified milestone');
497
+ assert.strictEqual(isMilestoneDepthVerified('M777', workspaceB), false, 'workspace B should not see workspace A milestone verification');
498
+ assert.strictEqual(isDepthVerified(workspaceB), false, 'workspace B should have no verified depth state');
499
+ } finally {
500
+ clearDiscussionFlowState(workspaceA);
501
+ clearDiscussionFlowState(workspaceB);
502
+ rmSync(workspaceA, { recursive: true, force: true });
503
+ rmSync(workspaceB, { recursive: true, force: true });
504
+ }
505
+ });
506
+
484
507
  // ─── Standard options fixture used across depth confirmation tests ──
485
508
 
486
509
  const STANDARD_OPTIONS = [
@@ -656,7 +679,7 @@ test('write-gate: loadWriteGateSnapshot returns empty default when persist file
656
679
  } else {
657
680
  process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
658
681
  }
659
- clearDiscussionFlowState();
682
+ clearDiscussionFlowState(base);
660
683
  try {
661
684
  rmSync(base, { recursive: true, force: true });
662
685
  } catch { /* swallow */ }
@@ -71,6 +71,6 @@ test('write-intercept: BLOCKED_WRITE_ERROR is a non-empty string', () => {
71
71
  });
72
72
 
73
73
  test('write-intercept: BLOCKED_WRITE_ERROR mentions engine tool calls', () => {
74
- assert.ok(BLOCKED_WRITE_ERROR.includes('gsd_complete_task'), 'should mention gsd_complete_task');
74
+ assert.ok(BLOCKED_WRITE_ERROR.includes('gsd_task_complete'), 'should mention gsd_task_complete');
75
75
  assert.ok(BLOCKED_WRITE_ERROR.includes('engine tool calls'), 'should mention engine tool calls');
76
76
  });
@@ -349,9 +349,9 @@ export function getRequiredWorkflowToolsForAutoUnit(unitType: string): string[]
349
349
  case "execute-task":
350
350
  case "execute-task-simple":
351
351
  case "reactive-execute":
352
- return ["gsd_complete_task"];
352
+ return ["gsd_task_complete"];
353
353
  case "complete-slice":
354
- return ["gsd_complete_slice"];
354
+ return ["gsd_slice_complete"];
355
355
  case "replan-slice":
356
356
  return ["gsd_replan_slice"];
357
357
  case "reassess-roadmap":
@@ -0,0 +1,95 @@
1
+ // GSD-2 + Workspace handle: single source of truth for path resolution per milestone
2
+
3
+ import { join, resolve } from "node:path";
4
+ import { type GsdPathContract, resolveGsdPathContract, normalizeRealPath } from "./paths.js";
5
+ import { isGsdWorktreePath, resolveWorktreeProjectRoot } from "./worktree-root.js";
6
+
7
+ export type GsdWorkspaceMode = "project" | "worktree";
8
+
9
+ export interface GsdWorkspace {
10
+ readonly projectRoot: string; // realpath-normalized absolute
11
+ readonly worktreeRoot: string | null; // realpath-normalized absolute, null when no worktree
12
+ readonly mode: GsdWorkspaceMode;
13
+ readonly contract: GsdPathContract; // pre-resolved, frozen
14
+ readonly identityKey: string; // canonical key (realpath of projectRoot) for dedup/cache
15
+ readonly lockRoot: string; // where auto.lock and {MID}-META.json live (always projectRoot)
16
+ }
17
+
18
+ export interface MilestoneScope {
19
+ readonly workspace: GsdWorkspace;
20
+ readonly milestoneId: string;
21
+ // path methods:
22
+ readonly contextFile: () => string;
23
+ readonly roadmapFile: () => string;
24
+ readonly stateFile: () => string;
25
+ readonly dbPath: () => string;
26
+ readonly milestoneDir: () => string;
27
+ readonly metaJson: () => string; // {MID}-META.json on lockRoot
28
+ }
29
+
30
+ function tryRealpath(p: string): string {
31
+ return normalizeRealPath(p);
32
+ }
33
+
34
+ /**
35
+ * Create an immutable GsdWorkspace handle from a raw base path.
36
+ * Resolves both the project root and (when applicable) the worktree root,
37
+ * normalizes them via realpath, and freezes the result.
38
+ */
39
+ export function createWorkspace(rawBasePath: string): GsdWorkspace {
40
+ const resolvedBase = resolve(rawBasePath);
41
+ const isWorktree = isGsdWorktreePath(resolvedBase);
42
+
43
+ const projectRootRaw = resolveWorktreeProjectRoot(resolvedBase);
44
+ const projectRoot = tryRealpath(resolve(projectRootRaw));
45
+
46
+ const worktreeRoot = isWorktree ? tryRealpath(resolvedBase) : null;
47
+
48
+ // Derive a canonical base from the already-realpath-normalized paths so that
49
+ // resolveGsdPathContract always receives a canonical path. Using the raw
50
+ // resolvedBase here can produce a non-canonical projectGsd when the input
51
+ // path contains symlinks, causing contract.projectGsd to diverge from the
52
+ // realpath-normalized projectRoot / identityKey.
53
+ const canonicalBase = isWorktree ? (worktreeRoot ?? resolvedBase) : projectRoot;
54
+ const contract = Object.freeze(resolveGsdPathContract(canonicalBase));
55
+
56
+ const identityKey = tryRealpath(projectRoot);
57
+
58
+ const mode: GsdWorkspaceMode = isWorktree ? "worktree" : "project";
59
+
60
+ const workspace: GsdWorkspace = Object.freeze({
61
+ projectRoot,
62
+ worktreeRoot,
63
+ mode,
64
+ contract,
65
+ identityKey,
66
+ lockRoot: projectRoot,
67
+ });
68
+
69
+ return workspace;
70
+ }
71
+
72
+ /**
73
+ * Bind a milestoneId to a workspace, producing an immutable MilestoneScope
74
+ * with path-returning closures that resolve via the authoritative projectGsd.
75
+ *
76
+ * All milestone-content paths route to contract.projectGsd (canonical),
77
+ * since that is the authoritative source of truth regardless of worktree mode.
78
+ */
79
+ export function scopeMilestone(workspace: GsdWorkspace, milestoneId: string): MilestoneScope {
80
+ const { contract } = workspace;
81
+ const gsd = contract.projectGsd;
82
+
83
+ const scope: MilestoneScope = Object.freeze({
84
+ workspace,
85
+ milestoneId,
86
+ contextFile: () => join(gsd, "milestones", milestoneId, `${milestoneId}-CONTEXT.md`),
87
+ roadmapFile: () => join(gsd, "milestones", milestoneId, `${milestoneId}-ROADMAP.md`),
88
+ stateFile: () => join(gsd, "STATE.md"),
89
+ dbPath: () => contract.projectDb,
90
+ milestoneDir: () => join(gsd, "milestones", milestoneId),
91
+ metaJson: () => join(gsd, `${milestoneId}-META.json`),
92
+ });
93
+
94
+ return scope;
95
+ }