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
@@ -0,0 +1,239 @@
1
+ // GSD-2 + Tests verifying writer/validator path parity via MilestoneScope (C3)
2
+ //
3
+ // Critical invariant: a writer that constructs paths via scope.contextFile() /
4
+ // scope.roadmapFile() and a validator that resolves paths via the scope-based
5
+ // wrappers in guided-flow.ts must produce IDENTICAL absolute paths for the same
6
+ // logical inputs. If they diverge, writes go to a different location than the
7
+ // validator checks, causing silent failures.
8
+
9
+ import { describe, test, beforeEach, afterEach } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdtempSync, mkdirSync, rmSync, realpathSync, writeFileSync, unlinkSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+
15
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
16
+ import {
17
+ verifyExpectedArtifactForScope,
18
+ resolveExpectedArtifactPathForScope,
19
+ isGhostMilestoneByScope,
20
+ } from "../guided-flow.ts";
21
+
22
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
23
+
24
+ function makeProjectDir(label = "gsd-vsp-"): string {
25
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), label)));
26
+ mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true });
27
+ return dir;
28
+ }
29
+
30
+ // ─── Suite: writer/validator path parity ─────────────────────────────────────
31
+
32
+ describe("validator-scope-parity: writer and validator produce identical paths", () => {
33
+ let base: string;
34
+
35
+ beforeEach(() => {
36
+ base = makeProjectDir();
37
+ });
38
+
39
+ afterEach(() => {
40
+ if (base) rmSync(base, { recursive: true, force: true });
41
+ });
42
+
43
+ test("resolveExpectedArtifactPathForScope('discuss-milestone') matches scope.contextFile()", () => {
44
+ const ws = createWorkspace(base);
45
+ const scope = scopeMilestone(ws, "M001");
46
+
47
+ // Writer path: what the discuss/plan agent writes to
48
+ const writerPath = scope.contextFile();
49
+
50
+ // Validator path: what the validator checks
51
+ const validatorPath = resolveExpectedArtifactPathForScope(scope, "discuss-milestone", "M001");
52
+
53
+ assert.equal(
54
+ validatorPath,
55
+ writerPath,
56
+ "discuss-milestone artifact path must match scope.contextFile()",
57
+ );
58
+ });
59
+
60
+ test("resolveExpectedArtifactPathForScope('plan-milestone') matches scope.roadmapFile()", () => {
61
+ const ws = createWorkspace(base);
62
+ const scope = scopeMilestone(ws, "M001");
63
+
64
+ const writerPath = scope.roadmapFile();
65
+ const validatorPath = resolveExpectedArtifactPathForScope(scope, "plan-milestone", "M001");
66
+
67
+ assert.equal(
68
+ validatorPath,
69
+ writerPath,
70
+ "plan-milestone artifact path must match scope.roadmapFile()",
71
+ );
72
+ });
73
+
74
+ test("resolveExpectedArtifactPathForScope returns an absolute path", () => {
75
+ const ws = createWorkspace(base);
76
+ const scope = scopeMilestone(ws, "M001");
77
+
78
+ const path = resolveExpectedArtifactPathForScope(scope, "discuss-milestone", "M001");
79
+ assert.ok(path, "path must be non-null for a milestone unit");
80
+ assert.ok(path!.startsWith("/"), "path must be absolute");
81
+ });
82
+ });
83
+
84
+ // ─── Suite: cwd-drift immunity ────────────────────────────────────────────────
85
+
86
+ describe("validator-scope-parity: scope-based validators are immune to cwd-drift", () => {
87
+ let base: string;
88
+
89
+ beforeEach(() => {
90
+ base = makeProjectDir();
91
+ });
92
+
93
+ afterEach(() => {
94
+ if (base) rmSync(base, { recursive: true, force: true });
95
+ });
96
+
97
+ test("resolveExpectedArtifactPathForScope path is unchanged after process.chdir", (t) => {
98
+ const ws = createWorkspace(base);
99
+ const scope = scopeMilestone(ws, "M001");
100
+
101
+ const pathBefore = resolveExpectedArtifactPathForScope(scope, "plan-milestone", "M001");
102
+
103
+ const originalCwd = process.cwd();
104
+ const altDir = mkdtempSync(join(tmpdir(), "gsd-cwd-alt-"));
105
+ t.after(() => {
106
+ process.chdir(originalCwd);
107
+ rmSync(altDir, { recursive: true, force: true });
108
+ });
109
+
110
+ process.chdir(altDir);
111
+
112
+ const pathAfter = resolveExpectedArtifactPathForScope(scope, "plan-milestone", "M001");
113
+
114
+ assert.equal(
115
+ pathAfter,
116
+ pathBefore,
117
+ "artifact path must not change after cwd drift",
118
+ );
119
+ });
120
+
121
+ test("isGhostMilestoneByScope result is consistent before and after process.chdir", (t) => {
122
+ const ws = createWorkspace(base);
123
+ const scope = scopeMilestone(ws, "M001");
124
+
125
+ // No DB, no content files — should be ghost
126
+ const resultBefore = isGhostMilestoneByScope(scope);
127
+
128
+ const originalCwd = process.cwd();
129
+ const altDir = mkdtempSync(join(tmpdir(), "gsd-cwd-alt2-"));
130
+ t.after(() => {
131
+ process.chdir(originalCwd);
132
+ rmSync(altDir, { recursive: true, force: true });
133
+ });
134
+
135
+ process.chdir(altDir);
136
+
137
+ const resultAfter = isGhostMilestoneByScope(scope);
138
+
139
+ assert.equal(
140
+ resultAfter,
141
+ resultBefore,
142
+ "isGhostMilestoneByScope result must be consistent across cwd change",
143
+ );
144
+ });
145
+ });
146
+
147
+ // ─── Suite: isGhostMilestoneByScope behavior ─────────────────────────────────
148
+
149
+ describe("validator-scope-parity: isGhostMilestoneByScope correctness", () => {
150
+ let base: string;
151
+
152
+ beforeEach(() => {
153
+ base = makeProjectDir();
154
+ });
155
+
156
+ afterEach(() => {
157
+ if (base) rmSync(base, { recursive: true, force: true });
158
+ });
159
+
160
+ test("isGhostMilestoneByScope returns true for milestone dir with no content files", () => {
161
+ const ws = createWorkspace(base);
162
+ const scope = scopeMilestone(ws, "M001");
163
+
164
+ // M001 dir exists (created in beforeEach) but has no CONTEXT/ROADMAP/SUMMARY
165
+ assert.equal(
166
+ isGhostMilestoneByScope(scope),
167
+ true,
168
+ "empty milestone dir with no content files should be ghost",
169
+ );
170
+ });
171
+
172
+ test("isGhostMilestoneByScope returns false when CONTEXT.md exists", (t) => {
173
+ const ws = createWorkspace(base);
174
+ const scope = scopeMilestone(ws, "M001");
175
+
176
+ // Write CONTEXT.md so the milestone is no longer a ghost
177
+ writeFileSync(scope.contextFile(), "# M001: Test\n\nContext.\n");
178
+ t.after(() => {
179
+ try { unlinkSync(scope.contextFile()); } catch {}
180
+ });
181
+
182
+ assert.equal(
183
+ isGhostMilestoneByScope(scope),
184
+ false,
185
+ "milestone with CONTEXT.md should not be ghost",
186
+ );
187
+ });
188
+ });
189
+
190
+ // ─── Suite: worktree path resolves to canonical project root ─────────────────
191
+
192
+ describe("validator-scope-parity: scope uses canonical projectRoot not worktree path", () => {
193
+ let base: string;
194
+
195
+ beforeEach(() => {
196
+ base = makeProjectDir("gsd-wt-parity-");
197
+ });
198
+
199
+ afterEach(() => {
200
+ if (base) rmSync(base, { recursive: true, force: true });
201
+ });
202
+
203
+ test("scope.workspace.projectRoot equals the input base for a non-worktree project", () => {
204
+ const ws = createWorkspace(base);
205
+ // For a plain project (not a worktree), projectRoot should be the realpath of base
206
+ assert.equal(
207
+ ws.projectRoot,
208
+ realpathSync(base),
209
+ "projectRoot must be realpath of the input base for a non-worktree project",
210
+ );
211
+ });
212
+
213
+ test("validator wrapper paths are rooted at scope.workspace.projectRoot, not at a worktree dir", () => {
214
+ // Simulate calling with a workspace that has projectRoot set.
215
+ // The validator should use projectRoot, not a different runtime path.
216
+ const ws = createWorkspace(base);
217
+ const scope = scopeMilestone(ws, "M001");
218
+
219
+ const artifactPath = resolveExpectedArtifactPathForScope(scope, "plan-milestone", "M001");
220
+
221
+ assert.ok(
222
+ artifactPath!.startsWith(scope.workspace.projectRoot),
223
+ `artifact path '${artifactPath}' must be rooted at projectRoot '${scope.workspace.projectRoot}'`,
224
+ );
225
+ });
226
+
227
+ test("verifyExpectedArtifactForScope uses projectRoot: returns false for non-existent artifact", () => {
228
+ const ws = createWorkspace(base);
229
+ const scope = scopeMilestone(ws, "M001");
230
+
231
+ // The artifact does not exist on disk yet
232
+ const ready = verifyExpectedArtifactForScope(scope, "plan-milestone", "M001");
233
+ assert.equal(
234
+ ready,
235
+ false,
236
+ "verifyExpectedArtifactForScope should return false when artifact is absent",
237
+ );
238
+ });
239
+ });
@@ -50,8 +50,8 @@ test("guided execute-task requires canonical task completion tool", () => {
50
50
  assert.deepEqual(getRequiredWorkflowToolsForGuidedUnit("execute-task"), ["gsd_task_complete"]);
51
51
  });
52
52
 
53
- test("auto execute-task requires legacy completion alias until prompt contract is aligned", () => {
54
- assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("execute-task"), ["gsd_complete_task"]);
53
+ test("auto execute-task requires canonical task completion tool", () => {
54
+ assert.deepEqual(getRequiredWorkflowToolsForAutoUnit("execute-task"), ["gsd_task_complete"]);
55
55
  });
56
56
 
57
57
  test("deep project setup units declare required workflow MCP tools", () => {
@@ -677,7 +677,7 @@ test("executeSummarySave removes sibling CONTEXT-DRAFT when writing milestone CO
677
677
  "CONTEXT-DRAFT.md should be removed after final CONTEXT.md is written",
678
678
  );
679
679
  } finally {
680
- clearDiscussionFlowState();
680
+ clearDiscussionFlowState(base);
681
681
  closeDatabase();
682
682
  cleanup(base);
683
683
  }
@@ -741,9 +741,7 @@ test("executeSummarySave blocks final root artifacts while approval gate is pend
741
741
  const base = makeTmpBase();
742
742
  try {
743
743
  openTestDb(base);
744
- await inProjectDir(base, async () => {
745
- setPendingGate("depth_verification_requirements_confirm");
746
- });
744
+ setPendingGate("depth_verification_requirements_confirm", base);
747
745
 
748
746
  const result = await inProjectDir(base, () => executeSummarySave({
749
747
  artifact_type: "REQUIREMENTS",
@@ -762,7 +760,7 @@ test("executeSummarySave blocks final root artifacts while approval gate is pend
762
760
  assert.equal(draft.isError, undefined);
763
761
  assert.ok(existsSync(join(base, ".gsd", "REQUIREMENTS-DRAFT.md")));
764
762
  } finally {
765
- clearDiscussionFlowState();
763
+ clearDiscussionFlowState(base);
766
764
  closeDatabase();
767
765
  cleanup(base);
768
766
  }
@@ -784,9 +782,7 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
784
782
  assert.match(blocked.content[0].text, /fail-closed/);
785
783
  assert.equal(existsSync(join(base, ".gsd", "PROJECT.md")), false);
786
784
 
787
- await inProjectDir(base, async () => {
788
- markApprovalGateVerified("depth_verification_project_confirm", base);
789
- });
785
+ markApprovalGateVerified("depth_verification_project_confirm", base);
790
786
 
791
787
  const unblocked = await inProjectDir(base, () => executeSummarySave({
792
788
  artifact_type: "PROJECT",
@@ -797,7 +793,7 @@ test("executeSummarySave requires verified root approval in deep mode", async ()
797
793
  assert.equal(unblocked.details.path, "PROJECT.md");
798
794
  assert.ok(existsSync(join(base, ".gsd", "PROJECT.md")));
799
795
  } finally {
800
- clearDiscussionFlowState();
796
+ clearDiscussionFlowState(base);
801
797
  closeDatabase();
802
798
  cleanup(base);
803
799
  }
@@ -807,9 +803,7 @@ test("executeSummarySave renders final REQUIREMENTS from the DB source of truth"
807
803
  const base = makeTmpBase();
808
804
  try {
809
805
  openTestDb(base);
810
- await inProjectDir(base, async () => {
811
- markApprovalGateVerified("depth_verification_requirements_confirm", base);
812
- });
806
+ markApprovalGateVerified("depth_verification_requirements_confirm", base);
813
807
 
814
808
  upsertRequirement({
815
809
  id: "R001",
@@ -882,7 +876,7 @@ test("executeSummarySave renders final REQUIREMENTS from the DB source of truth"
882
876
  .get("REQUIREMENTS.md") as Record<string, unknown>;
883
877
  assert.equal(artifact.full_content, content);
884
878
  } finally {
885
- clearDiscussionFlowState();
879
+ clearDiscussionFlowState(base);
886
880
  closeDatabase();
887
881
  cleanup(base);
888
882
  }
@@ -992,7 +986,7 @@ test("executeSummarySave CONTEXT HARD BLOCK clears after write-gate state file i
992
986
  process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
993
987
  try {
994
988
  openTestDb(base);
995
- clearDiscussionFlowState();
989
+ clearDiscussionFlowState(base);
996
990
 
997
991
  // First call: CONTEXT artifact without depth verification → HARD BLOCK
998
992
  const blocked = await inProjectDir(base, () => executeSummarySave({
@@ -1043,7 +1037,7 @@ test("executeSummarySave CONTEXT HARD BLOCK clears after write-gate state file i
1043
1037
  } else {
1044
1038
  process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
1045
1039
  }
1046
- clearDiscussionFlowState();
1040
+ clearDiscussionFlowState(base);
1047
1041
  closeDatabase();
1048
1042
  cleanup(base);
1049
1043
  }
@@ -0,0 +1,196 @@
1
+ // GSD-2 + Workspace handle tests: createWorkspace and scopeMilestone
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ rmSync,
9
+ realpathSync,
10
+ symlinkSync,
11
+ } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
16
+
17
+ // ─── Helpers ────────────────────────────────────────────────────────────────
18
+
19
+ function makeProjectDir(): string {
20
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-ws-test-")));
21
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
22
+ return dir;
23
+ }
24
+
25
+ // ─── Tests ──────────────────────────────────────────────────────────────────
26
+
27
+ describe("createWorkspace", () => {
28
+ let projectDir: string;
29
+
30
+ beforeEach(() => {
31
+ projectDir = makeProjectDir();
32
+ });
33
+
34
+ afterEach(() => {
35
+ rmSync(projectDir, { recursive: true, force: true });
36
+ });
37
+
38
+ test("from a project root produces mode=project and worktreeRoot=null", () => {
39
+ const ws = createWorkspace(projectDir);
40
+ assert.equal(ws.mode, "project");
41
+ assert.equal(ws.worktreeRoot, null);
42
+ assert.equal(ws.projectRoot, realpathSync(projectDir));
43
+ });
44
+
45
+ test("from a worktree path produces mode=worktree, worktreeRoot=realpath, projectRoot=realpath of project", () => {
46
+ // Construct a worktree path: <projectDir>/.gsd/worktrees/M001
47
+ const worktreePath = join(projectDir, ".gsd", "worktrees", "M001");
48
+ mkdirSync(worktreePath, { recursive: true });
49
+
50
+ const ws = createWorkspace(worktreePath);
51
+ assert.equal(ws.mode, "worktree");
52
+ assert.equal(ws.worktreeRoot, realpathSync(worktreePath));
53
+ assert.equal(ws.projectRoot, realpathSync(projectDir));
54
+ });
55
+
56
+ test("normalizes /foo and /foo/ to identical identityKey", () => {
57
+ const wsTrailing = createWorkspace(projectDir + "/");
58
+ const wsNoTrailing = createWorkspace(projectDir);
59
+ assert.equal(wsTrailing.identityKey, wsNoTrailing.identityKey);
60
+ });
61
+
62
+ test("follows symlinks — identityKey matches realpath of target", (t) => {
63
+ const linkParent = mkdtempSync(join(tmpdir(), "gsd-ws-link-"));
64
+ const linkPath = join(linkParent, "project");
65
+ t.after(() => {
66
+ rmSync(linkParent, { recursive: true, force: true });
67
+ });
68
+ symlinkSync(projectDir, linkPath, "junction");
69
+
70
+ const ws = createWorkspace(linkPath);
71
+ assert.equal(ws.identityKey, realpathSync(projectDir));
72
+ });
73
+ });
74
+
75
+ describe("GsdWorkspace and MilestoneScope are frozen", () => {
76
+ let projectDir: string;
77
+
78
+ beforeEach(() => {
79
+ projectDir = makeProjectDir();
80
+ });
81
+
82
+ afterEach(() => {
83
+ rmSync(projectDir, { recursive: true, force: true });
84
+ });
85
+
86
+ test("workspace is frozen", () => {
87
+ const ws = createWorkspace(projectDir);
88
+ assert.ok(Object.isFrozen(ws), "workspace should be frozen");
89
+ assert.throws(() => {
90
+ (ws as { mode: string }).mode = "worktree";
91
+ }, /Cannot assign/);
92
+ });
93
+
94
+ test("scope is frozen", () => {
95
+ const ws = createWorkspace(projectDir);
96
+ const scope = scopeMilestone(ws, "M001");
97
+ assert.ok(Object.isFrozen(scope), "scope should be frozen");
98
+ assert.throws(() => {
99
+ (scope as { milestoneId: string }).milestoneId = "M999";
100
+ }, /Cannot assign/);
101
+ });
102
+
103
+ test("contract inside workspace is frozen", () => {
104
+ const ws = createWorkspace(projectDir);
105
+ assert.ok(Object.isFrozen(ws.contract), "contract should be frozen");
106
+ });
107
+ });
108
+
109
+ describe("scopeMilestone path methods", () => {
110
+ let projectDir: string;
111
+ const MID = "M001";
112
+
113
+ beforeEach(() => {
114
+ projectDir = makeProjectDir();
115
+ });
116
+
117
+ afterEach(() => {
118
+ rmSync(projectDir, { recursive: true, force: true });
119
+ });
120
+
121
+ test("produces correct paths for a known milestone ID", () => {
122
+ const ws = createWorkspace(projectDir);
123
+ const scope = scopeMilestone(ws, MID);
124
+ const gsd = ws.contract.projectGsd;
125
+
126
+ assert.equal(scope.milestoneId, MID);
127
+ assert.equal(scope.contextFile(), join(gsd, "milestones", MID, `${MID}-CONTEXT.md`));
128
+ assert.equal(scope.roadmapFile(), join(gsd, "milestones", MID, `${MID}-ROADMAP.md`));
129
+ assert.equal(scope.stateFile(), join(gsd, "STATE.md"));
130
+ assert.equal(scope.dbPath(), ws.contract.projectDb);
131
+ assert.equal(scope.milestoneDir(), join(gsd, "milestones", MID));
132
+ assert.equal(scope.metaJson(), join(gsd, `${MID}-META.json`));
133
+ });
134
+
135
+ test("two scopes from same workspace + same MID produce identical paths", () => {
136
+ const ws = createWorkspace(projectDir);
137
+ const scope1 = scopeMilestone(ws, MID);
138
+ const scope2 = scopeMilestone(ws, MID);
139
+
140
+ assert.equal(scope1.contextFile(), scope2.contextFile());
141
+ assert.equal(scope1.roadmapFile(), scope2.roadmapFile());
142
+ assert.equal(scope1.stateFile(), scope2.stateFile());
143
+ assert.equal(scope1.dbPath(), scope2.dbPath());
144
+ assert.equal(scope1.milestoneDir(), scope2.milestoneDir());
145
+ assert.equal(scope1.metaJson(), scope2.metaJson());
146
+ });
147
+ });
148
+
149
+ describe("createWorkspace: contract.projectGsd is realpath-canonicalized when basePath is a symlink", () => {
150
+ let projectDir = "";
151
+ let linkParent = "";
152
+ let linkPath = "";
153
+
154
+ beforeEach(() => {
155
+ projectDir = makeProjectDir();
156
+ linkParent = mkdtempSync(join(tmpdir(), "gsd-ws-symlink-"));
157
+ linkPath = join(linkParent, "project");
158
+ symlinkSync(projectDir, linkPath, "junction");
159
+ });
160
+
161
+ afterEach(() => {
162
+ if (linkParent) rmSync(linkParent, { recursive: true, force: true });
163
+ if (projectDir) rmSync(projectDir, { recursive: true, force: true });
164
+ linkParent = "";
165
+ linkPath = "";
166
+ projectDir = "";
167
+ });
168
+
169
+ test("contract.projectGsd matches realpath of projectRoot when workspace is created via symlink", () => {
170
+ const ws = createWorkspace(linkPath);
171
+
172
+ const canonicalProjectRoot = realpathSync(projectDir);
173
+
174
+ // identityKey must be the realpath of the canonical project root
175
+ assert.equal(ws.identityKey, canonicalProjectRoot);
176
+ assert.equal(ws.projectRoot, canonicalProjectRoot);
177
+
178
+ // contract.projectGsd must start with the canonical project root —
179
+ // not with the symlink path. If the bug is present, contract.projectGsd
180
+ // would be linkPath + "/.gsd" instead of canonicalProjectRoot + "/.gsd".
181
+ assert.ok(
182
+ ws.contract.projectGsd.startsWith(canonicalProjectRoot),
183
+ `contract.projectGsd ("${ws.contract.projectGsd}") must be under the realpath'd projectRoot ("${canonicalProjectRoot}"), not the symlink path`,
184
+ );
185
+ });
186
+
187
+ test("contract.projectDb matches realpath of projectRoot when workspace is created via symlink", () => {
188
+ const ws = createWorkspace(linkPath);
189
+ const canonicalProjectRoot = realpathSync(projectDir);
190
+
191
+ assert.ok(
192
+ ws.contract.projectDb.startsWith(canonicalProjectRoot),
193
+ `contract.projectDb ("${ws.contract.projectDb}") must be under the realpath'd projectRoot ("${canonicalProjectRoot}")`,
194
+ );
195
+ });
196
+ });