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
@@ -1,10 +1,13 @@
1
1
  /**
2
- * GSD2 — regression test for #5187: gsdRoot() must refuse to use the global
3
- * GSD home (~/.gsd) as a project .gsd directory when basePath resolves to
4
- * $HOME. Paths under ~/.gsd/projects/<hash>/ remain valid.
2
+ * GSD2 — regression tests for #5187 and git-root anchor guard:
5
3
  *
6
- * Before the fix, gsdRoot(homedir()) returned ~/.gsd silently and downstream
7
- * writes polluted the user's global state directory. After the fix, it throws.
4
+ * #5187: gsdRoot() must refuse to use the global GSD home (~/.gsd) as a
5
+ * project .gsd directory when basePath resolves to $HOME. Paths under
6
+ * ~/.gsd/projects/<hash>/ remain valid.
7
+ *
8
+ * git-root anchor guard: when $HOME is itself a git repo and ~/.gsd exists,
9
+ * gsdRoot() must NOT return ~/.gsd for a subdir basePath like ~/projects/foo.
10
+ * It should fall through to step 4 (creation fallback) instead.
8
11
  */
9
12
 
10
13
  import { describe, test, beforeEach, afterEach } from 'node:test';
@@ -12,6 +15,7 @@ import assert from 'node:assert/strict';
12
15
  import { mkdtempSync, mkdirSync, rmSync, realpathSync } from 'node:fs';
13
16
  import { tmpdir } from 'node:os';
14
17
  import { join } from 'node:path';
18
+ import { spawnSync } from 'node:child_process';
15
19
 
16
20
  import { gsdRoot, _clearGsdRootCache } from '../paths.ts';
17
21
 
@@ -84,3 +88,62 @@ describe('gsdRoot() refuses ~/.gsd as project state when basePath is $HOME (#518
84
88
  }
85
89
  });
86
90
  });
91
+
92
+ describe('git-root anchor guard: subdir basePath must not resolve to ~/.gsd', () => {
93
+ let fakeHome: string;
94
+ let subDir: string;
95
+ let savedHome: string | undefined;
96
+ let savedUserProfile: string | undefined;
97
+ let savedGsdHome: string | undefined;
98
+
99
+ beforeEach(() => {
100
+ // Create a tmpdir that will act as both $HOME and a git repo root.
101
+ fakeHome = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-anchor-guard-')));
102
+ // Init a bare-minimum git repo so git rev-parse --show-toplevel returns fakeHome.
103
+ spawnSync('git', ['init', fakeHome], { encoding: 'utf-8' });
104
+ // Create ~/.gsd (the global home that must NOT be used for project subdirs).
105
+ mkdirSync(join(fakeHome, '.gsd'), { recursive: true });
106
+ // Create a subdir inside the git repo — this is the project basePath.
107
+ subDir = join(fakeHome, 'projects', 'foo');
108
+ mkdirSync(subDir, { recursive: true });
109
+
110
+ savedHome = process.env.HOME;
111
+ savedUserProfile = process.env.USERPROFILE;
112
+ savedGsdHome = process.env.GSD_HOME;
113
+
114
+ process.env.HOME = fakeHome;
115
+ process.env.USERPROFILE = fakeHome;
116
+ delete process.env.GSD_HOME;
117
+
118
+ _clearGsdRootCache();
119
+ });
120
+
121
+ afterEach(() => {
122
+ if (savedHome === undefined) delete process.env.HOME;
123
+ else process.env.HOME = savedHome;
124
+ if (savedUserProfile === undefined) delete process.env.USERPROFILE;
125
+ else process.env.USERPROFILE = savedUserProfile;
126
+ if (savedGsdHome === undefined) delete process.env.GSD_HOME;
127
+ else process.env.GSD_HOME = savedGsdHome;
128
+
129
+ _clearGsdRootCache();
130
+ rmSync(fakeHome, { recursive: true, force: true });
131
+ });
132
+
133
+ test('does NOT return ~/.gsd when $HOME is a git repo and basePath is a subdir', () => {
134
+ // fakeHome IS the git root AND $HOME, so git rev-parse returns fakeHome,
135
+ // and ~/.gsd (fakeHome/.gsd) exists. The guard must skip that candidate
136
+ // and fall through to the creation fallback: subDir/.gsd.
137
+ const result = gsdRoot(subDir);
138
+ assert.notEqual(
139
+ result,
140
+ join(fakeHome, '.gsd'),
141
+ 'gsdRoot must not return ~/.gsd for a subdir basePath',
142
+ );
143
+ assert.equal(
144
+ result,
145
+ join(subDir, '.gsd'),
146
+ 'gsdRoot should fall through to the creation fallback for a subdir',
147
+ );
148
+ });
149
+ });
@@ -86,8 +86,8 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
86
86
  assert.ok(prompt.includes(TID), "must mention task id");
87
87
  assert.ok(prompt.includes(T_TITLE), "must mention task title");
88
88
  assert.ok(
89
- prompt.includes("gsd_complete_task"),
90
- "must instruct calling the canonical gsd_complete_task tool",
89
+ prompt.includes("gsd_task_complete"),
90
+ "must instruct calling the canonical gsd_task_complete tool",
91
91
  );
92
92
  assert.ok(
93
93
  prompt.includes(base),
@@ -110,8 +110,8 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
110
110
  assert.ok(prompt.includes(SID), "must mention slice id");
111
111
  assert.ok(prompt.includes(S_TITLE), "must mention slice title");
112
112
  assert.ok(
113
- prompt.includes("gsd_complete_slice"),
114
- "must instruct calling gsd_complete_slice (was in guided-complete-slice.md)",
113
+ prompt.includes("gsd_slice_complete"),
114
+ "must instruct calling gsd_slice_complete (was in guided-complete-slice.md)",
115
115
  );
116
116
  assert.ok(
117
117
  prompt.includes(base),
@@ -0,0 +1,371 @@
1
+ // GSD-2 + Integration regression suite for workspace collapse (feat/workspace-collapse)
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ writeFileSync,
9
+ existsSync,
10
+ rmSync,
11
+ realpathSync,
12
+ } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+ import { execFileSync } from "node:child_process";
16
+
17
+ import { createWorkspace, scopeMilestone } from "../../workspace.ts";
18
+ import {
19
+ gsdRoot,
20
+ clearPathCache,
21
+ _clearGsdRootCache,
22
+ } from "../../paths.ts";
23
+ import {
24
+ loadWriteGateSnapshot,
25
+ markDepthVerified,
26
+ clearDiscussionFlowState,
27
+ } from "../../bootstrap/write-gate.ts";
28
+ import {
29
+ teardownAutoWorktree,
30
+ _resetAutoWorktreeOriginalBaseForTests,
31
+ } from "../../auto-worktree.ts";
32
+ import {
33
+ openDatabaseByWorkspace,
34
+ closeAllDatabases,
35
+ _getDbCache,
36
+ } from "../../gsd-db.ts";
37
+
38
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
39
+
40
+ function makeProjectDir(): string {
41
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-collapse-int-")));
42
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
43
+ return dir;
44
+ }
45
+
46
+ function git(args: string[], cwd: string): void {
47
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
48
+ }
49
+
50
+ function makeGitRepo(): string {
51
+ const dir = makeProjectDir();
52
+ git(["init"], dir);
53
+ git(["config", "user.email", "test@gsd.test"], dir);
54
+ git(["config", "user.name", "GSD Test"], dir);
55
+ writeFileSync(join(dir, "README.md"), "# test\n");
56
+ git(["add", "README.md"], dir);
57
+ git(["commit", "-m", "init"], dir);
58
+ git(["branch", "-M", "main"], dir);
59
+ return dir;
60
+ }
61
+
62
+ // ─── Test 1: Writer/validator path agreement under cwd-drift ─────────────────
63
+
64
+ describe("workspace-collapse integration: Test 1 — cwd-drift path agreement", () => {
65
+ let projectDir: string;
66
+ let otherDir: string;
67
+ const savedCwd = process.cwd();
68
+
69
+ beforeEach(() => {
70
+ projectDir = makeProjectDir();
71
+ otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cwd-drift-other-")));
72
+ _clearGsdRootCache();
73
+ });
74
+
75
+ afterEach(() => {
76
+ process.chdir(savedCwd);
77
+ rmSync(projectDir, { recursive: true, force: true });
78
+ rmSync(otherDir, { recursive: true, force: true });
79
+ _clearGsdRootCache();
80
+ });
81
+
82
+ test("contextFile() returns same absolute path before and after cwd change", () => {
83
+ const worktreeDir = join(projectDir, ".gsd", "worktrees", "M001");
84
+ mkdirSync(worktreeDir, { recursive: true });
85
+
86
+ const ws = createWorkspace(projectDir);
87
+ const scope = scopeMilestone(ws, "M001");
88
+
89
+ // Record the path the "writer" would use
90
+ const writerPath = scope.contextFile();
91
+ assert.ok(writerPath.startsWith(projectDir), "writer path is under projectDir");
92
+
93
+ // Simulate cwd drift
94
+ process.chdir(otherDir);
95
+ assert.notEqual(process.cwd(), projectDir, "cwd has drifted away from projectDir");
96
+
97
+ // The "validator" recomputes via the same scope
98
+ const validatorPath = scope.contextFile();
99
+
100
+ assert.equal(
101
+ validatorPath,
102
+ writerPath,
103
+ "contextFile() must return the same absolute path regardless of cwd drift",
104
+ );
105
+ });
106
+
107
+ test("scopeMilestone paths are stable across cwd changes (roadmap, state, db)", () => {
108
+ const ws = createWorkspace(projectDir);
109
+ const scope = scopeMilestone(ws, "M001");
110
+
111
+ const before = {
112
+ roadmap: scope.roadmapFile(),
113
+ state: scope.stateFile(),
114
+ db: scope.dbPath(),
115
+ milestoneDir: scope.milestoneDir(),
116
+ };
117
+
118
+ process.chdir(otherDir);
119
+
120
+ assert.equal(scope.roadmapFile(), before.roadmap, "roadmapFile() stable after cwd drift");
121
+ assert.equal(scope.stateFile(), before.state, "stateFile() stable after cwd drift");
122
+ assert.equal(scope.dbPath(), before.db, "dbPath() stable after cwd drift");
123
+ assert.equal(scope.milestoneDir(), before.milestoneDir, "milestoneDir() stable after cwd drift");
124
+ });
125
+ });
126
+
127
+ // ─── Test 2: Abort path leaves no stale state ────────────────────────────────
128
+
129
+ describe("workspace-collapse integration: Test 2 — abort teardown clears stale state", () => {
130
+ let repoDir: string;
131
+ const savedCwd = process.cwd();
132
+
133
+ beforeEach(() => {
134
+ repoDir = makeGitRepo();
135
+ _resetAutoWorktreeOriginalBaseForTests();
136
+ });
137
+
138
+ afterEach(() => {
139
+ process.chdir(savedCwd);
140
+ _resetAutoWorktreeOriginalBaseForTests();
141
+ rmSync(repoDir, { recursive: true, force: true });
142
+ });
143
+
144
+ test("STATE.md, auto.lock, and M001-META.json are removed by teardownAutoWorktree", () => {
145
+ const gsdDir = join(repoDir, ".gsd");
146
+ const milestonesDir = join(gsdDir, "milestones", "M001");
147
+ mkdirSync(milestonesDir, { recursive: true });
148
+
149
+ const stateMd = join(gsdDir, "STATE.md");
150
+ const autoLock = join(gsdDir, "auto.lock");
151
+ const metaJson = join(milestonesDir, "M001-META.json");
152
+
153
+ writeFileSync(stateMd, "# State\nactive\n");
154
+ writeFileSync(autoLock, JSON.stringify({ pid: process.pid, unitType: "plan-milestone", unitId: "M001" }));
155
+ writeFileSync(metaJson, JSON.stringify({ milestoneId: "M001" }));
156
+
157
+ assert.ok(existsSync(stateMd), "STATE.md exists before teardown");
158
+ assert.ok(existsSync(autoLock), "auto.lock exists before teardown");
159
+ assert.ok(existsSync(metaJson), "M001-META.json exists before teardown");
160
+
161
+ // teardownAutoWorktree clears state files before the git step; git removal
162
+ // may fail in a minimal test repo — that is acceptable.
163
+ try {
164
+ teardownAutoWorktree(repoDir, "M001");
165
+ } catch {
166
+ // git worktree removal may fail when no worktree was created — non-fatal for this assertion
167
+ }
168
+
169
+ assert.ok(!existsSync(stateMd), "STATE.md removed by teardownAutoWorktree (regression: A5)");
170
+ assert.ok(!existsSync(autoLock), "auto.lock removed by teardownAutoWorktree (regression: A5)");
171
+ assert.ok(!existsSync(metaJson), "M001-META.json removed by teardownAutoWorktree (regression: A5)");
172
+ });
173
+ });
174
+
175
+ // ─── Test 3: Cwd drift between persist and load of write-gate state ──────────
176
+
177
+ describe("workspace-collapse integration: Test 3 — write-gate snapshot survives cwd drift", () => {
178
+ let projectDir: string;
179
+ let otherDir: string;
180
+ const savedCwd = process.cwd();
181
+
182
+ beforeEach(() => {
183
+ projectDir = makeProjectDir();
184
+ otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-wg-other-")));
185
+ // Start with a clean write-gate state for projectDir
186
+ clearDiscussionFlowState(projectDir);
187
+ });
188
+
189
+ afterEach(() => {
190
+ process.chdir(savedCwd);
191
+ clearDiscussionFlowState(projectDir);
192
+ try { clearDiscussionFlowState(otherDir); } catch { /* best-effort */ }
193
+ rmSync(projectDir, { recursive: true, force: true });
194
+ rmSync(otherDir, { recursive: true, force: true });
195
+ });
196
+
197
+ test("loadWriteGateSnapshot returns persisted state after cwd drift", () => {
198
+ // Persist a snapshot: mark M001 depth-verified for projectDir
199
+ markDepthVerified("M001", projectDir);
200
+
201
+ // Drift cwd away from projectDir
202
+ process.chdir(otherDir);
203
+ assert.notEqual(process.cwd(), projectDir, "cwd has drifted");
204
+
205
+ // Load the snapshot using the explicit basePath — must not be affected by cwd
206
+ const snapshot = loadWriteGateSnapshot(projectDir);
207
+
208
+ assert.ok(
209
+ snapshot.verifiedDepthMilestones.includes("M001"),
210
+ "snapshot loaded from projectDir includes M001 despite cwd drift",
211
+ );
212
+ });
213
+
214
+ test("loadWriteGateSnapshot from different basePath does not bleed state", () => {
215
+ markDepthVerified("M001", projectDir);
216
+
217
+ process.chdir(otherDir);
218
+
219
+ // otherDir has no persisted state — should return empty snapshot
220
+ const snapshot = loadWriteGateSnapshot(otherDir);
221
+
222
+ assert.ok(
223
+ !snapshot.verifiedDepthMilestones.includes("M001"),
224
+ "otherDir snapshot must not bleed M001 state from projectDir",
225
+ );
226
+ });
227
+ });
228
+
229
+ // ─── Test 4: Sibling worktrees share DB connection ───────────────────────────
230
+
231
+ describe("workspace-collapse integration: Test 4 — sibling worktrees share DB connection", () => {
232
+ let projectDir: string;
233
+
234
+ beforeEach(() => {
235
+ projectDir = makeProjectDir();
236
+ });
237
+
238
+ afterEach(() => {
239
+ closeAllDatabases();
240
+ rmSync(projectDir, { recursive: true, force: true });
241
+ });
242
+
243
+ test("ws1 and ws2 (sibling worktrees) have same identityKey", () => {
244
+ const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
245
+ const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
246
+ mkdirSync(wt1, { recursive: true });
247
+ mkdirSync(wt2, { recursive: true });
248
+
249
+ const ws1 = createWorkspace(wt1);
250
+ const ws2 = createWorkspace(wt2);
251
+
252
+ assert.equal(
253
+ ws1.identityKey,
254
+ ws2.identityKey,
255
+ "sibling worktrees M001 and M002 must share the same identityKey",
256
+ );
257
+ assert.equal(
258
+ ws1.identityKey,
259
+ realpathSync(projectDir),
260
+ "identityKey is the realpath of the project root",
261
+ );
262
+ });
263
+
264
+ test("openDatabaseByWorkspace for sibling worktrees resolves to the same DB path", () => {
265
+ const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
266
+ const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
267
+ mkdirSync(wt1, { recursive: true });
268
+ mkdirSync(wt2, { recursive: true });
269
+
270
+ const ws1 = createWorkspace(wt1);
271
+ const ws2 = createWorkspace(wt2);
272
+
273
+ const ok1 = openDatabaseByWorkspace(ws1);
274
+ assert.ok(ok1, "openDatabaseByWorkspace(ws1) must succeed");
275
+ const cacheAfterWs1 = _getDbCache();
276
+ const entry1 = cacheAfterWs1.get(ws1.identityKey);
277
+ assert.ok(entry1, "cache entry for ws1.identityKey must exist");
278
+ const dbPath1 = entry1.dbPath;
279
+
280
+ const ok2 = openDatabaseByWorkspace(ws2);
281
+ assert.ok(ok2, "openDatabaseByWorkspace(ws2) must succeed");
282
+ const cacheAfterWs2 = _getDbCache();
283
+ const entry2 = cacheAfterWs2.get(ws2.identityKey);
284
+ assert.ok(entry2, "cache entry for ws2.identityKey must exist");
285
+ const dbPath2 = entry2.dbPath;
286
+
287
+ assert.equal(
288
+ dbPath1,
289
+ dbPath2,
290
+ "sibling worktrees must resolve to the same DB path (shared WAL)",
291
+ );
292
+ assert.equal(
293
+ cacheAfterWs2.size,
294
+ 1,
295
+ "only one cache entry for project + two sibling worktrees",
296
+ );
297
+ });
298
+ });
299
+
300
+ // ─── Test 5: gsdRootCache normalization survives trailing-slash inputs ────────
301
+
302
+ describe("workspace-collapse integration: Test 5 — gsdRootCache normalization deduplicates trailing-slash inputs", () => {
303
+ let projectDir: string;
304
+ let fakeHome: string;
305
+ let savedHome: string | undefined;
306
+ let savedUserProfile: string | undefined;
307
+ let savedGsdHome: string | undefined;
308
+
309
+ beforeEach(() => {
310
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-")));
311
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
312
+
313
+ fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-home-")));
314
+
315
+ savedHome = process.env.HOME;
316
+ savedUserProfile = process.env.USERPROFILE;
317
+ savedGsdHome = process.env.GSD_HOME;
318
+
319
+ // Prevent ~/.gsd interference
320
+ process.env.HOME = fakeHome;
321
+ process.env.USERPROFILE = fakeHome;
322
+ process.env.GSD_HOME = join(fakeHome, ".gsd");
323
+
324
+ clearPathCache();
325
+ });
326
+
327
+ afterEach(() => {
328
+ if (savedHome === undefined) delete process.env.HOME;
329
+ else process.env.HOME = savedHome;
330
+ if (savedUserProfile === undefined) delete process.env.USERPROFILE;
331
+ else process.env.USERPROFILE = savedUserProfile;
332
+ if (savedGsdHome === undefined) delete process.env.GSD_HOME;
333
+ else process.env.GSD_HOME = savedGsdHome;
334
+
335
+ clearPathCache();
336
+ rmSync(projectDir, { recursive: true, force: true });
337
+ rmSync(fakeHome, { recursive: true, force: true });
338
+ });
339
+
340
+ test("gsdRoot('/path/to/project') and gsdRoot('/path/to/project/') return identical paths", () => {
341
+ const withoutSlash = gsdRoot(projectDir);
342
+ const withSlash = gsdRoot(projectDir + "/");
343
+
344
+ assert.equal(
345
+ withoutSlash,
346
+ withSlash,
347
+ "gsdRoot must return identical paths for inputs with and without trailing slash",
348
+ );
349
+ assert.equal(
350
+ withoutSlash,
351
+ join(projectDir, ".gsd"),
352
+ "both calls must resolve to projectDir/.gsd",
353
+ );
354
+ });
355
+
356
+ test("both calls after clearPathCache() return identical paths (no duplicate cache entries)", () => {
357
+ // Start clean
358
+ clearPathCache();
359
+
360
+ const r1 = gsdRoot(projectDir);
361
+ const r2 = gsdRoot(projectDir + "/");
362
+
363
+ assert.equal(r1, r2, "r1 and r2 must be the same string after normalization");
364
+ // The cache normalizes both inputs to the same key — no duplicate entries.
365
+ // We can't inspect the cache size directly, but the behavioral proof is
366
+ // that a second call after clearPathCache re-probes and still matches.
367
+ clearPathCache();
368
+ const r3 = gsdRoot(projectDir + "/");
369
+ assert.equal(r3, r1, "re-probe after clearPathCache must produce the same result");
370
+ });
371
+ });