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
@@ -0,0 +1,453 @@
1
+ // GSD-2 + Sync-layer scope variants: tests for ByScope wrappers in auto-worktree.ts
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ writeFileSync,
9
+ existsSync,
10
+ realpathSync,
11
+ rmSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
17
+ import {
18
+ syncProjectRootToWorktree,
19
+ syncProjectRootToWorktreeByScope,
20
+ syncStateToProjectRoot,
21
+ syncStateToProjectRootByScope,
22
+ syncGsdStateToWorktree,
23
+ syncGsdStateToWorktreeByScope,
24
+ reconcilePlanCheckboxesByScope,
25
+ } from "../auto-worktree.ts";
26
+
27
+ // ─── Helpers ────────────────────────────────────────────────────────────────
28
+
29
+ const MID = "M001-abc123";
30
+
31
+ /**
32
+ * Build a minimal project+worktree layout in a temp dir.
33
+ *
34
+ * Layout:
35
+ * <root>/
36
+ * .gsd/
37
+ * milestones/<MID>/
38
+ * <MID>-CONTEXT.md
39
+ * metrics.json
40
+ * completed-units.json
41
+ * runtime/units/
42
+ * .gsd/worktrees/<MID>/
43
+ * .gsd/ ← worktree-local .gsd projection
44
+ * milestones/<MID>/
45
+ *
46
+ * Returns { projectDir, worktreeDir }.
47
+ */
48
+ function makeProjectAndWorktree(base: string): {
49
+ projectDir: string;
50
+ worktreeDir: string;
51
+ } {
52
+ const projectDir = realpathSync(base);
53
+
54
+ // Project .gsd layout
55
+ mkdirSync(join(projectDir, ".gsd", "milestones", MID), { recursive: true });
56
+ mkdirSync(join(projectDir, ".gsd", "runtime", "units"), { recursive: true });
57
+ writeFileSync(join(projectDir, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), "context");
58
+ writeFileSync(join(projectDir, ".gsd", "metrics.json"), '{"tokens":0}');
59
+ writeFileSync(join(projectDir, ".gsd", "completed-units.json"), "[]");
60
+
61
+ // Worktree directory inside .gsd/worktrees/<MID> so isGsdWorktreePath recognises it
62
+ const worktreeDir = join(projectDir, ".gsd", "worktrees", MID);
63
+ mkdirSync(join(worktreeDir, ".gsd", "milestones", MID), { recursive: true });
64
+ mkdirSync(join(worktreeDir, ".gsd", "runtime", "units"), { recursive: true });
65
+
66
+ return { projectDir, worktreeDir };
67
+ }
68
+
69
+ // ─── Suite: identity check (throws on mismatched workspace) ─────────────────
70
+
71
+ describe("ByScope variants: mismatched-workspace identity assertion", () => {
72
+ let tmpA: string;
73
+ let tmpB: string;
74
+
75
+ beforeEach(() => {
76
+ tmpA = mkdtempSync(join(tmpdir(), "gsd-sync-scope-a-"));
77
+ tmpB = mkdtempSync(join(tmpdir(), "gsd-sync-scope-b-"));
78
+ });
79
+
80
+ afterEach(() => {
81
+ rmSync(tmpA, { recursive: true, force: true });
82
+ rmSync(tmpB, { recursive: true, force: true });
83
+ });
84
+
85
+ test("syncProjectRootToWorktreeByScope throws when identityKeys differ", () => {
86
+ mkdirSync(join(tmpA, ".gsd"), { recursive: true });
87
+ mkdirSync(join(tmpB, ".gsd"), { recursive: true });
88
+ const wsA = createWorkspace(tmpA);
89
+ const wsB = createWorkspace(tmpB);
90
+ const scopeA = scopeMilestone(wsA, MID);
91
+ const scopeB = scopeMilestone(wsB, MID);
92
+
93
+ assert.throws(
94
+ () => syncProjectRootToWorktreeByScope(scopeA, scopeB),
95
+ /scope identity mismatch/,
96
+ );
97
+ });
98
+
99
+ test("syncStateToProjectRootByScope throws when identityKeys differ", () => {
100
+ mkdirSync(join(tmpA, ".gsd"), { recursive: true });
101
+ mkdirSync(join(tmpB, ".gsd"), { recursive: true });
102
+ const wsA = createWorkspace(tmpA);
103
+ const wsB = createWorkspace(tmpB);
104
+ const scopeA = scopeMilestone(wsA, MID);
105
+ const scopeB = scopeMilestone(wsB, MID);
106
+
107
+ assert.throws(
108
+ () => syncStateToProjectRootByScope(scopeA, scopeB),
109
+ /scope identity mismatch/,
110
+ );
111
+ });
112
+
113
+ test("syncGsdStateToWorktreeByScope throws when identityKeys differ", () => {
114
+ mkdirSync(join(tmpA, ".gsd"), { recursive: true });
115
+ mkdirSync(join(tmpB, ".gsd"), { recursive: true });
116
+ const wsA = createWorkspace(tmpA);
117
+ const wsB = createWorkspace(tmpB);
118
+ const scopeA = scopeMilestone(wsA, MID);
119
+ const scopeB = scopeMilestone(wsB, MID);
120
+
121
+ assert.throws(
122
+ () => syncGsdStateToWorktreeByScope(scopeA, scopeB),
123
+ /scope identity mismatch/,
124
+ );
125
+ });
126
+
127
+ test("reconcilePlanCheckboxesByScope throws when identityKeys differ", () => {
128
+ mkdirSync(join(tmpA, ".gsd"), { recursive: true });
129
+ mkdirSync(join(tmpB, ".gsd"), { recursive: true });
130
+ const wsA = createWorkspace(tmpA);
131
+ const wsB = createWorkspace(tmpB);
132
+ const scopeA = scopeMilestone(wsA, MID);
133
+ const scopeB = scopeMilestone(wsB, MID);
134
+
135
+ assert.throws(
136
+ () => reconcilePlanCheckboxesByScope(scopeA, scopeB),
137
+ /scope identity mismatch/,
138
+ );
139
+ });
140
+ });
141
+
142
+ // ─── Suite: same-milestone, same-workspace path identity ────────────────────
143
+
144
+ describe("ByScope variants: same-workspace produces same paths regardless of scope side", () => {
145
+ let tmp: string;
146
+
147
+ beforeEach(() => {
148
+ tmp = mkdtempSync(join(tmpdir(), "gsd-sync-scope-id-"));
149
+ });
150
+
151
+ afterEach(() => {
152
+ rmSync(tmp, { recursive: true, force: true });
153
+ });
154
+
155
+ test("rootScope.workspace.identityKey equals worktreeScope.workspace.identityKey for same project", () => {
156
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
157
+
158
+ const rootWs = createWorkspace(projectDir);
159
+ const worktreeWs = createWorkspace(worktreeDir);
160
+
161
+ assert.equal(
162
+ rootWs.identityKey,
163
+ worktreeWs.identityKey,
164
+ "both scopes from same project must share identityKey",
165
+ );
166
+ });
167
+
168
+ test("rootScope paths and worktreeScope.workspace.projectRoot resolve to the same project root", () => {
169
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
170
+
171
+ const rootWs = createWorkspace(projectDir);
172
+ const worktreeWs = createWorkspace(worktreeDir);
173
+
174
+ assert.equal(
175
+ rootWs.projectRoot,
176
+ worktreeWs.projectRoot,
177
+ "projectRoot must be identical for root and worktree scopes",
178
+ );
179
+ });
180
+ });
181
+
182
+ // ─── Suite: disk-effect parity (scope variants == legacy path variants) ─────
183
+
184
+ describe("syncProjectRootToWorktreeByScope: disk-effect parity with legacy", () => {
185
+ let tmp1: string;
186
+ let tmp2: string;
187
+
188
+ beforeEach(() => {
189
+ tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-legacy-"));
190
+ tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-scope-"));
191
+ });
192
+
193
+ afterEach(() => {
194
+ rmSync(tmp1, { recursive: true, force: true });
195
+ rmSync(tmp2, { recursive: true, force: true });
196
+ });
197
+
198
+ test("scope variant copies milestone dir into worktree identical to legacy variant", () => {
199
+ const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
200
+ const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
201
+
202
+ // Add a source file in project root milestone dir (not yet in worktree)
203
+ const srcFile = "extra-artifact.md";
204
+ writeFileSync(join(proj1, ".gsd", "milestones", MID, srcFile), "artifact");
205
+ writeFileSync(join(proj2, ".gsd", "milestones", MID, srcFile), "artifact");
206
+
207
+ // Remove destination files so something needs to be copied
208
+ rmSync(join(wt1, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), { force: true });
209
+ rmSync(join(wt2, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`), { force: true });
210
+
211
+ // Run legacy on tmp1, scope variant on tmp2
212
+ syncProjectRootToWorktree(proj1, wt1, MID);
213
+
214
+ const rootWs2 = createWorkspace(proj2);
215
+ const worktreeWs2 = createWorkspace(wt2);
216
+ const rootScope2 = scopeMilestone(rootWs2, MID);
217
+ const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
218
+ syncProjectRootToWorktreeByScope(rootScope2, worktreeScope2);
219
+
220
+ // Both worktrees should now have the CONTEXT.md
221
+ assert.ok(
222
+ existsSync(join(wt1, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`)),
223
+ "legacy: CONTEXT.md should be copied into worktree",
224
+ );
225
+ assert.ok(
226
+ existsSync(join(wt2, ".gsd", "milestones", MID, `${MID}-CONTEXT.md`)),
227
+ "scope: CONTEXT.md should be copied into worktree",
228
+ );
229
+ });
230
+ });
231
+
232
+ describe("syncStateToProjectRootByScope: disk-effect parity with legacy", () => {
233
+ let tmp1: string;
234
+ let tmp2: string;
235
+
236
+ beforeEach(() => {
237
+ tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-stpr-legacy-"));
238
+ tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-stpr-scope-"));
239
+ });
240
+
241
+ afterEach(() => {
242
+ rmSync(tmp1, { recursive: true, force: true });
243
+ rmSync(tmp2, { recursive: true, force: true });
244
+ });
245
+
246
+ test("scope variant copies metrics.json from worktree to project root identical to legacy variant", () => {
247
+ const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
248
+ const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
249
+
250
+ // Write metrics.json into each worktree .gsd
251
+ const metricsContent = '{"tokens":42}';
252
+ writeFileSync(join(wt1, ".gsd", "metrics.json"), metricsContent);
253
+ writeFileSync(join(wt2, ".gsd", "metrics.json"), metricsContent);
254
+
255
+ // Run legacy on tmp1, scope variant on tmp2
256
+ syncStateToProjectRoot(wt1, proj1, MID);
257
+
258
+ const rootWs2 = createWorkspace(proj2);
259
+ const worktreeWs2 = createWorkspace(wt2);
260
+ const rootScope2 = scopeMilestone(rootWs2, MID);
261
+ const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
262
+ syncStateToProjectRootByScope(worktreeScope2, rootScope2);
263
+
264
+ // Both project roots should now have the updated metrics.json
265
+ assert.ok(
266
+ existsSync(join(proj1, ".gsd", "metrics.json")),
267
+ "legacy: metrics.json should be synced to project root",
268
+ );
269
+ assert.ok(
270
+ existsSync(join(proj2, ".gsd", "metrics.json")),
271
+ "scope: metrics.json should be synced to project root",
272
+ );
273
+ });
274
+ });
275
+
276
+ describe("syncGsdStateToWorktreeByScope: disk-effect parity with legacy", () => {
277
+ let tmp1: string;
278
+ let tmp2: string;
279
+
280
+ beforeEach(() => {
281
+ tmp1 = mkdtempSync(join(tmpdir(), "gsd-sync-gsd-legacy-"));
282
+ tmp2 = mkdtempSync(join(tmpdir(), "gsd-sync-gsd-scope-"));
283
+ });
284
+
285
+ afterEach(() => {
286
+ rmSync(tmp1, { recursive: true, force: true });
287
+ rmSync(tmp2, { recursive: true, force: true });
288
+ });
289
+
290
+ test("scope variant syncs root state files into worktree identical to legacy variant", () => {
291
+ const { projectDir: proj1, worktreeDir: wt1 } = makeProjectAndWorktree(tmp1);
292
+ const { projectDir: proj2, worktreeDir: wt2 } = makeProjectAndWorktree(tmp2);
293
+
294
+ // Add a root state file in each project .gsd (not yet in worktree)
295
+ writeFileSync(join(proj1, ".gsd", "DECISIONS.md"), "decisions");
296
+ writeFileSync(join(proj2, ".gsd", "DECISIONS.md"), "decisions");
297
+
298
+ // Run legacy on tmp1, scope variant on tmp2
299
+ syncGsdStateToWorktree(proj1, wt1);
300
+
301
+ const rootWs2 = createWorkspace(proj2);
302
+ const worktreeWs2 = createWorkspace(wt2);
303
+ const rootScope2 = scopeMilestone(rootWs2, MID);
304
+ const worktreeScope2 = scopeMilestone(worktreeWs2, MID);
305
+ syncGsdStateToWorktreeByScope(rootScope2, worktreeScope2);
306
+
307
+ // Both worktrees should now have DECISIONS.md
308
+ assert.ok(
309
+ existsSync(join(wt1, ".gsd", "DECISIONS.md")),
310
+ "legacy: DECISIONS.md should be copied into worktree",
311
+ );
312
+ assert.ok(
313
+ existsSync(join(wt2, ".gsd", "DECISIONS.md")),
314
+ "scope: DECISIONS.md should be copied into worktree",
315
+ );
316
+ });
317
+ });
318
+
319
+ // ─── Suite: direction tests ──────────────────────────────────────────────────
320
+
321
+ describe("sync direction: project→worktree variants only write to worktree side", () => {
322
+ let tmp: string;
323
+
324
+ beforeEach(() => {
325
+ tmp = mkdtempSync(join(tmpdir(), "gsd-sync-dir-"));
326
+ });
327
+
328
+ afterEach(() => {
329
+ rmSync(tmp, { recursive: true, force: true });
330
+ });
331
+
332
+ test("syncProjectRootToWorktreeByScope: new file appears in worktree, not duplicated to project root", () => {
333
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
334
+
335
+ // New file in project root milestone — not yet in worktree
336
+ const marker = "direction-test.md";
337
+ writeFileSync(join(projectDir, ".gsd", "milestones", MID, marker), "marker");
338
+
339
+ // Remove from worktree so we can detect it being added
340
+ const wtDst = join(worktreeDir, ".gsd", "milestones", MID, marker);
341
+ rmSync(wtDst, { force: true });
342
+
343
+ const rootWs = createWorkspace(projectDir);
344
+ const worktreeWs = createWorkspace(worktreeDir);
345
+ const rootScope = scopeMilestone(rootWs, MID);
346
+ const worktreeScope = scopeMilestone(worktreeWs, MID);
347
+
348
+ syncProjectRootToWorktreeByScope(rootScope, worktreeScope);
349
+
350
+ // File should now be in worktree
351
+ assert.ok(existsSync(wtDst), "marker file should appear in worktree after project→worktree sync");
352
+
353
+ // The original in project root should still be there (not removed)
354
+ assert.ok(
355
+ existsSync(join(projectDir, ".gsd", "milestones", MID, marker)),
356
+ "project root marker file should not be removed",
357
+ );
358
+ });
359
+
360
+ test("syncStateToProjectRootByScope: new file appears in project root from worktree", () => {
361
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
362
+
363
+ // Write a runtime unit file into worktree
364
+ const unitFile = "some-unit-M001.json";
365
+ writeFileSync(join(worktreeDir, ".gsd", "runtime", "units", unitFile), '{"status":"done"}');
366
+
367
+ const rootWs = createWorkspace(projectDir);
368
+ const worktreeWs = createWorkspace(worktreeDir);
369
+ const rootScope = scopeMilestone(rootWs, MID);
370
+ const worktreeScope = scopeMilestone(worktreeWs, MID);
371
+
372
+ syncStateToProjectRootByScope(worktreeScope, rootScope);
373
+
374
+ // Unit file should now be in project root
375
+ assert.ok(
376
+ existsSync(join(projectDir, ".gsd", "runtime", "units", unitFile)),
377
+ "runtime unit file should appear in project root after worktree→root sync",
378
+ );
379
+
380
+ // Worktree side should still have the file (not removed)
381
+ assert.ok(
382
+ existsSync(join(worktreeDir, ".gsd", "runtime", "units", unitFile)),
383
+ "worktree runtime unit file should not be removed",
384
+ );
385
+ });
386
+ });
387
+
388
+ // ─── Suite: milestoneId mismatch guard ───────────────────────────────────────
389
+
390
+ describe("ByScope variants: milestoneId mismatch throws for milestone-aware wrappers", () => {
391
+ let tmp: string;
392
+
393
+ beforeEach(() => {
394
+ tmp = mkdtempSync(join(tmpdir(), "gsd-sync-mid-mismatch-"));
395
+ });
396
+
397
+ afterEach(() => {
398
+ rmSync(tmp, { recursive: true, force: true });
399
+ });
400
+
401
+ test("syncProjectRootToWorktreeByScope throws when milestoneIds differ", () => {
402
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
403
+ const rootWs = createWorkspace(projectDir);
404
+ const worktreeWs = createWorkspace(worktreeDir);
405
+ // Same workspace identity, different milestoneId
406
+ const rootScope = scopeMilestone(rootWs, "M001-abc123");
407
+ const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
408
+
409
+ assert.throws(
410
+ () => syncProjectRootToWorktreeByScope(rootScope, worktreeScope),
411
+ /milestoneId mismatch/,
412
+ );
413
+ });
414
+
415
+ test("syncStateToProjectRootByScope throws when milestoneIds differ", () => {
416
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
417
+ const rootWs = createWorkspace(projectDir);
418
+ const worktreeWs = createWorkspace(worktreeDir);
419
+ const rootScope = scopeMilestone(rootWs, "M001-abc123");
420
+ const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
421
+
422
+ assert.throws(
423
+ () => syncStateToProjectRootByScope(worktreeScope, rootScope),
424
+ /milestoneId mismatch/,
425
+ );
426
+ });
427
+
428
+ test("reconcilePlanCheckboxesByScope throws when milestoneIds differ", () => {
429
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
430
+ const rootWs = createWorkspace(projectDir);
431
+ const worktreeWs = createWorkspace(worktreeDir);
432
+ const rootScope = scopeMilestone(rootWs, "M001-abc123");
433
+ const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
434
+
435
+ assert.throws(
436
+ () => reconcilePlanCheckboxesByScope(rootScope, worktreeScope),
437
+ /milestoneId mismatch/,
438
+ );
439
+ });
440
+
441
+ test("syncGsdStateToWorktreeByScope does NOT throw when milestoneIds differ (workspace-only wrapper)", () => {
442
+ const { projectDir, worktreeDir } = makeProjectAndWorktree(tmp);
443
+ const rootWs = createWorkspace(projectDir);
444
+ const worktreeWs = createWorkspace(worktreeDir);
445
+ // Different milestoneIds — syncGsdStateToWorktreeByScope must not guard milestoneId
446
+ const rootScope = scopeMilestone(rootWs, "M001-abc123");
447
+ const worktreeScope = scopeMilestone(worktreeWs, "M002-def456");
448
+
449
+ assert.doesNotThrow(
450
+ () => syncGsdStateToWorktreeByScope(rootScope, worktreeScope),
451
+ );
452
+ });
453
+ });
@@ -0,0 +1,162 @@
1
+ // GSD-2 + Regression test: teardownAutoWorktree clears activeWorkspace even when process.chdir throws
2
+
3
+ /**
4
+ * Regression (H3 broadened scope): `teardownAutoWorktree` must clear `activeWorkspace`
5
+ * (and therefore `getAutoWorktreeOriginalBase()` / `getActiveAutoWorktreeContext()`)
6
+ * unconditionally — regardless of where in the function body an error occurs.
7
+ *
8
+ * The original H3 fix (d1276b021) wrapped only `removeWorktree(...)` in a
9
+ * try/finally. But `process.chdir(originalBasePath)` at the top of the function
10
+ * can throw a GSDError if the target directory no longer exists. In that case
11
+ * execution exits the function before ever reaching the inner try/finally, leaving
12
+ * `activeWorkspace` stale.
13
+ *
14
+ * The fix: a single outer try/finally wraps the entire teardown body so
15
+ * `setActiveWorkspace(null)` runs regardless of which step throws.
16
+ *
17
+ * Test strategy:
18
+ * 1. Populate the registry via `createAutoWorktree` on a real temp git repo.
19
+ * 2. Delete the repo directory so `process.chdir(originalBasePath)` throws.
20
+ * 3. Assert `teardownAutoWorktree` re-throws (chdir failure still propagates).
21
+ * 4. Assert `getActiveWorkspace()` is null — the broadened finally caught it.
22
+ * 5. Regression: success path still clears activeWorkspace (same guarantee).
23
+ */
24
+
25
+ import { describe, test, beforeEach, afterEach } from "node:test";
26
+ import assert from "node:assert/strict";
27
+ import {
28
+ mkdirSync,
29
+ mkdtempSync,
30
+ writeFileSync,
31
+ rmSync,
32
+ realpathSync,
33
+ existsSync,
34
+ } from "node:fs";
35
+ import { join } from "node:path";
36
+ import { tmpdir } from "node:os";
37
+ import { execFileSync } from "node:child_process";
38
+
39
+ import {
40
+ createAutoWorktree,
41
+ teardownAutoWorktree,
42
+ getAutoWorktreeOriginalBase,
43
+ getActiveAutoWorktreeContext,
44
+ _resetAutoWorktreeOriginalBaseForTests,
45
+ } from "../auto-worktree.ts";
46
+
47
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
48
+
49
+ function git(args: string[], cwd: string): void {
50
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
51
+ }
52
+
53
+ function createTempRepo(): string {
54
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-chdir-fail-")));
55
+ git(["init"], dir);
56
+ git(["config", "user.email", "test@gsd.test"], dir);
57
+ git(["config", "user.name", "Test"], dir);
58
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
59
+ writeFileSync(join(dir, "README.md"), "# test\n");
60
+ git(["add", "."], dir);
61
+ git(["commit", "-m", "init"], dir);
62
+ git(["branch", "-M", "main"], dir);
63
+ return dir;
64
+ }
65
+
66
+ function seedMilestone(repoDir: string, milestoneId: string): void {
67
+ const msDir = join(repoDir, ".gsd", "milestones", milestoneId);
68
+ mkdirSync(msDir, { recursive: true });
69
+ writeFileSync(join(msDir, "CONTEXT.md"), `# ${milestoneId} Context\n`);
70
+ git(["add", "."], repoDir);
71
+ git(["commit", "-m", `add ${milestoneId}`], repoDir);
72
+ }
73
+
74
+ // ─── Tests ───────────────────────────────────────────────────────────────────
75
+
76
+ describe("teardown chdir failure clears registry", () => {
77
+ const savedCwd = process.cwd();
78
+ let repoDir: string;
79
+
80
+ beforeEach(() => {
81
+ _resetAutoWorktreeOriginalBaseForTests();
82
+ process.chdir(savedCwd);
83
+ repoDir = "";
84
+ });
85
+
86
+ afterEach(() => {
87
+ _resetAutoWorktreeOriginalBaseForTests();
88
+ process.chdir(savedCwd);
89
+ if (repoDir && existsSync(repoDir)) {
90
+ rmSync(repoDir, { recursive: true, force: true });
91
+ }
92
+ repoDir = "";
93
+ });
94
+
95
+ // ── chdir failure path (the new coverage) ──────────────────────────────────
96
+
97
+ test("registry is null after teardown throws due to chdir failure (H3 broadened scope)", () => {
98
+ repoDir = createTempRepo();
99
+ seedMilestone(repoDir, "M001");
100
+
101
+ // Populate the registry by entering the worktree
102
+ createAutoWorktree(repoDir, "M001");
103
+
104
+ assert.strictEqual(
105
+ getAutoWorktreeOriginalBase(),
106
+ repoDir,
107
+ "registry is populated after createAutoWorktree",
108
+ );
109
+ assert.notStrictEqual(
110
+ getActiveAutoWorktreeContext(),
111
+ null,
112
+ "context is non-null after createAutoWorktree",
113
+ );
114
+
115
+ // Move back to a safe cwd so we can delete the repo dir
116
+ process.chdir(savedCwd);
117
+
118
+ // Delete the repo directory — process.chdir(repoDir) inside teardown will throw
119
+ const capturedRepoDir = repoDir;
120
+ rmSync(repoDir, { recursive: true, force: true });
121
+ repoDir = ""; // afterEach cleanup no longer needed
122
+
123
+ // teardownAutoWorktree must throw (chdir to deleted dir fails)
124
+ assert.throws(
125
+ () => teardownAutoWorktree(capturedRepoDir, "M001"),
126
+ "teardownAutoWorktree should throw when originalBasePath does not exist",
127
+ );
128
+
129
+ // The broadened outer finally must have cleared the registry despite the throw
130
+ assert.strictEqual(
131
+ getAutoWorktreeOriginalBase(),
132
+ null,
133
+ "getAutoWorktreeOriginalBase() is null after chdir-failure teardown (H3)",
134
+ );
135
+ assert.strictEqual(
136
+ getActiveAutoWorktreeContext(),
137
+ null,
138
+ "getActiveAutoWorktreeContext() is null after chdir-failure teardown (H3)",
139
+ );
140
+ });
141
+
142
+ // ── Success path (regression guard) ───────────────────────────────────────
143
+
144
+ test("registry is null after successful teardown (success path regression)", () => {
145
+ repoDir = createTempRepo();
146
+ seedMilestone(repoDir, "M002");
147
+
148
+ // Confirm baseline
149
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry null before entering worktree");
150
+
151
+ createAutoWorktree(repoDir, "M002");
152
+
153
+ assert.strictEqual(getAutoWorktreeOriginalBase(), repoDir, "registry set after createAutoWorktree");
154
+ assert.notStrictEqual(getActiveAutoWorktreeContext(), null, "context non-null after createAutoWorktree");
155
+
156
+ // Normal teardown — finally block must still clear registry
157
+ teardownAutoWorktree(repoDir, "M002");
158
+
159
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry null after successful teardown");
160
+ assert.strictEqual(getActiveAutoWorktreeContext(), null, "context null after successful teardown");
161
+ });
162
+ });