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,152 @@
1
+ // GSD-2 + db-writer path containment: regression tests for path.relative-based traversal guard
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+
9
+ import { openDatabase, closeDatabase } from "../gsd-db.ts";
10
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
11
+ import {
12
+ saveArtifactToDbForWorkspace,
13
+ saveArtifactToDbByScope,
14
+ } from "../db-writer.ts";
15
+
16
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
17
+
18
+ function makeProjectDir(base: string): string {
19
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
20
+ return base;
21
+ }
22
+
23
+ // ─── Tests ───────────────────────────────────────────────────────────────────
24
+
25
+ describe("saveArtifactToDbForWorkspace: path.relative containment guard", () => {
26
+ let tmp: string;
27
+ let projectDir: string;
28
+
29
+ beforeEach(() => {
30
+ tmp = mkdtempSync(join(tmpdir(), "gsd-path-contain-fw-"));
31
+ projectDir = makeProjectDir(tmp);
32
+ openDatabase(join(projectDir, ".gsd", "gsd.db"));
33
+ });
34
+
35
+ afterEach(() => {
36
+ closeDatabase();
37
+ rmSync(tmp, { recursive: true, force: true });
38
+ });
39
+
40
+ // Attack: /foo/.gsd-other/file resolves to a path that startsWith("/foo/.gsd")
41
+ // but is NOT inside /foo/.gsd/. The path.relative fix correctly detects this.
42
+ test("rejects sibling directory that startsWith would have accepted", async () => {
43
+ // Create a sibling directory next to .gsd that shares the prefix
44
+ const sibling = join(projectDir, ".gsd-other");
45
+ mkdirSync(sibling, { recursive: true });
46
+
47
+ const ws = createWorkspace(projectDir);
48
+ // Craft an opts.path that traverses out of .gsd into .gsd-other
49
+ // resolve(gsdDir, "../.gsd-other/evil.md") === projectDir + "/.gsd-other/evil.md"
50
+ // which startsWith(projectDir + "/.gsd") because ".gsd-other" starts with ".gsd"
51
+ const traversalPath = "../.gsd-other/evil.md";
52
+
53
+ await assert.rejects(
54
+ () =>
55
+ saveArtifactToDbForWorkspace(ws, {
56
+ path: traversalPath,
57
+ artifact_type: "CONTEXT",
58
+ content: "attack",
59
+ }),
60
+ /path escapes \.gsd\/ directory/,
61
+ );
62
+ });
63
+
64
+ test("rejects absolute path input", async () => {
65
+ const ws = createWorkspace(projectDir);
66
+ await assert.rejects(
67
+ () =>
68
+ saveArtifactToDbForWorkspace(ws, {
69
+ path: "/etc/passwd",
70
+ artifact_type: "CONTEXT",
71
+ content: "attack",
72
+ }),
73
+ /path escapes \.gsd\/ directory/,
74
+ );
75
+ });
76
+
77
+ test("accepts a legitimate path inside .gsd/", async () => {
78
+ const ws = createWorkspace(projectDir);
79
+ // Should not throw — CONTEXT.md inside .gsd is valid
80
+ await assert.doesNotReject(() =>
81
+ saveArtifactToDbForWorkspace(ws, {
82
+ path: "CONTEXT.md",
83
+ artifact_type: "CONTEXT",
84
+ content: "# Context\n",
85
+ }),
86
+ );
87
+ });
88
+ });
89
+
90
+ describe("saveArtifactToDbByScope: path.relative containment guard", () => {
91
+ let tmp: string;
92
+ let projectDir: string;
93
+
94
+ beforeEach(() => {
95
+ tmp = mkdtempSync(join(tmpdir(), "gsd-path-contain-bs-"));
96
+ projectDir = makeProjectDir(tmp);
97
+ openDatabase(join(projectDir, ".gsd", "gsd.db"));
98
+ });
99
+
100
+ afterEach(() => {
101
+ closeDatabase();
102
+ rmSync(tmp, { recursive: true, force: true });
103
+ });
104
+
105
+ test("rejects sibling directory that startsWith would have accepted", async () => {
106
+ const sibling = join(projectDir, ".gsd-other");
107
+ mkdirSync(sibling, { recursive: true });
108
+
109
+ const ws = createWorkspace(projectDir);
110
+ const scope = scopeMilestone(ws, "M001");
111
+ const traversalPath = "../.gsd-other/evil.md";
112
+
113
+ await assert.rejects(
114
+ () =>
115
+ saveArtifactToDbByScope(scope, {
116
+ path: traversalPath,
117
+ artifact_type: "CONTEXT",
118
+ content: "attack",
119
+ }),
120
+ /path escapes \.gsd\/ directory/,
121
+ );
122
+ });
123
+
124
+ test("rejects absolute path input", async () => {
125
+ const ws = createWorkspace(projectDir);
126
+ const scope = scopeMilestone(ws, "M001");
127
+ await assert.rejects(
128
+ () =>
129
+ saveArtifactToDbByScope(scope, {
130
+ path: "/etc/passwd",
131
+ artifact_type: "CONTEXT",
132
+ content: "attack",
133
+ }),
134
+ /path escapes \.gsd\/ directory/,
135
+ );
136
+ });
137
+
138
+ test("accepts a legitimate milestone-relative path inside .gsd/", async () => {
139
+ mkdirSync(join(projectDir, ".gsd", "milestones", "M001"), {
140
+ recursive: true,
141
+ });
142
+ const ws = createWorkspace(projectDir);
143
+ const scope = scopeMilestone(ws, "M001");
144
+ await assert.doesNotReject(() =>
145
+ saveArtifactToDbByScope(scope, {
146
+ path: "milestones/M001/M001-CONTEXT.md",
147
+ artifact_type: "CONTEXT",
148
+ content: "# Context\n",
149
+ }),
150
+ );
151
+ });
152
+ });
@@ -0,0 +1,221 @@
1
+ // GSD-2 + db-writer root-artifact path guard: regression tests for M1 fix
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ existsSync,
9
+ readFileSync,
10
+ realpathSync,
11
+ rmSync,
12
+ } from "node:fs";
13
+ import { join, resolve } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
17
+ import {
18
+ saveArtifactToDb,
19
+ saveArtifactToDbByScope,
20
+ saveArtifactToDbForWorkspace,
21
+ } from "../db-writer.ts";
22
+ import { openDatabase, closeDatabase } from "../gsd-db.ts";
23
+
24
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
25
+
26
+ function makeProjectDir(): string {
27
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-dbwriter-root-")));
28
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
29
+ return dir;
30
+ }
31
+
32
+ // ─── Suite 1: saveArtifactToDb with undefined milestone_id writes to .gsd/ root, not milestones/ ──
33
+
34
+ describe("saveArtifactToDb: root artifact (no milestone_id) routes to workspace .gsd root", () => {
35
+ let tmp: string;
36
+
37
+ beforeEach(() => {
38
+ tmp = makeProjectDir();
39
+ openDatabase(join(tmp, ".gsd", "gsd.db"));
40
+ });
41
+
42
+ afterEach(() => {
43
+ closeDatabase();
44
+ rmSync(tmp, { recursive: true, force: true });
45
+ });
46
+
47
+ test("opts.milestone_id = undefined writes artifact at .gsd/REQUIREMENTS.md, not inside milestones/", async () => {
48
+ const content = "# Requirements\n\nTest root artifact.\n";
49
+ const opts = {
50
+ path: "REQUIREMENTS.md",
51
+ artifact_type: "REQUIREMENTS_DRAFT",
52
+ content,
53
+ milestone_id: undefined,
54
+ };
55
+
56
+ await saveArtifactToDb(opts, tmp);
57
+
58
+ const ws = createWorkspace(tmp);
59
+ const expectedPath = resolve(ws.contract.projectGsd, "REQUIREMENTS.md");
60
+
61
+ assert.ok(existsSync(expectedPath), "root artifact written at .gsd/REQUIREMENTS.md");
62
+ assert.equal(readFileSync(expectedPath, "utf-8"), content, "content matches");
63
+
64
+ // Must NOT be inside milestones/ — the latent trap being fixed
65
+ const wrongPath = resolve(ws.contract.projectGsd, "milestones", "", "REQUIREMENTS.md");
66
+ assert.ok(!existsSync(wrongPath), "artifact NOT written inside milestones/");
67
+ });
68
+
69
+ test("opts.milestone_id = null writes artifact at .gsd/ root", async () => {
70
+ const content = "# Project\n\nRoot project doc.\n";
71
+ const opts = {
72
+ path: "PROJECT.md",
73
+ artifact_type: "PROJECT",
74
+ content,
75
+ milestone_id: undefined,
76
+ };
77
+
78
+ await saveArtifactToDb(opts, tmp);
79
+
80
+ const ws = createWorkspace(tmp);
81
+ const expectedPath = resolve(ws.contract.projectGsd, "PROJECT.md");
82
+
83
+ assert.ok(existsSync(expectedPath), "PROJECT.md written at .gsd/PROJECT.md");
84
+ assert.equal(readFileSync(expectedPath, "utf-8"), content, "content matches");
85
+ });
86
+
87
+ test("path resolves via workspace.contract.projectGsd, not a hand-rolled join", async () => {
88
+ const content = "# Knowledge\n";
89
+ const opts = {
90
+ path: "KNOWLEDGE.md",
91
+ artifact_type: "KNOWLEDGE",
92
+ content,
93
+ milestone_id: undefined,
94
+ };
95
+
96
+ await saveArtifactToDb(opts, tmp);
97
+
98
+ const ws = createWorkspace(tmp);
99
+ // The canonical path must equal contract.projectGsd + '/KNOWLEDGE.md'
100
+ const canonicalPath = join(ws.contract.projectGsd, "KNOWLEDGE.md");
101
+ assert.ok(existsSync(canonicalPath), "file at contract.projectGsd/KNOWLEDGE.md");
102
+ assert.equal(
103
+ canonicalPath,
104
+ join(ws.projectRoot, ".gsd", "KNOWLEDGE.md"),
105
+ "contract.projectGsd-based path equals projectRoot/.gsd/KNOWLEDGE.md",
106
+ );
107
+ });
108
+ });
109
+
110
+ // ─── Suite 2: saveArtifactToDb with a real milestone_id still works (no regression) ──
111
+
112
+ describe("saveArtifactToDb: milestone_id present routes to milestones/ (no regression)", () => {
113
+ let tmp: string;
114
+
115
+ beforeEach(() => {
116
+ tmp = makeProjectDir();
117
+ openDatabase(join(tmp, ".gsd", "gsd.db"));
118
+ });
119
+
120
+ afterEach(() => {
121
+ closeDatabase();
122
+ rmSync(tmp, { recursive: true, force: true });
123
+ });
124
+
125
+ test("milestone_id = 'M001' writes to .gsd/milestones/M001/...", async () => {
126
+ const relPath = "milestones/M001/M001-CONTEXT.md";
127
+ const content = "# M001 Context\n";
128
+ const opts = {
129
+ path: relPath,
130
+ artifact_type: "CONTEXT",
131
+ content,
132
+ milestone_id: "M001",
133
+ };
134
+
135
+ await saveArtifactToDb(opts, tmp);
136
+
137
+ const ws = createWorkspace(tmp);
138
+ const expectedPath = resolve(ws.contract.projectGsd, relPath);
139
+
140
+ assert.ok(existsSync(expectedPath), "milestone artifact written at correct path");
141
+ assert.equal(readFileSync(expectedPath, "utf-8"), content, "content matches");
142
+ });
143
+ });
144
+
145
+ // ─── Suite 3: saveArtifactToDbByScope with empty milestoneId throws a clear error ──
146
+
147
+ describe("saveArtifactToDbByScope: empty milestoneId throws defensive error", () => {
148
+ let tmp: string;
149
+
150
+ beforeEach(() => {
151
+ tmp = makeProjectDir();
152
+ openDatabase(join(tmp, ".gsd", "gsd.db"));
153
+ });
154
+
155
+ afterEach(() => {
156
+ closeDatabase();
157
+ rmSync(tmp, { recursive: true, force: true });
158
+ });
159
+
160
+ test("scope with empty milestoneId throws GSDError mentioning saveArtifactToDbForWorkspace", async () => {
161
+ const ws = createWorkspace(tmp);
162
+ const emptyScope = scopeMilestone(ws, "");
163
+ const opts = {
164
+ path: "REQUIREMENTS.md",
165
+ artifact_type: "REQUIREMENTS_DRAFT",
166
+ content: "# req\n",
167
+ };
168
+
169
+ await assert.rejects(
170
+ () => saveArtifactToDbByScope(emptyScope, opts),
171
+ (err: unknown) => {
172
+ assert.ok(err instanceof Error, "thrown value is an Error");
173
+ assert.ok(
174
+ err.message.includes("milestoneId is empty"),
175
+ `error message should mention 'milestoneId is empty', got: ${err.message}`,
176
+ );
177
+ assert.ok(
178
+ err.message.includes("saveArtifactToDbForWorkspace"),
179
+ `error message should mention 'saveArtifactToDbForWorkspace', got: ${err.message}`,
180
+ );
181
+ return true;
182
+ },
183
+ );
184
+ });
185
+ });
186
+
187
+ // ─── Suite 4: saveArtifactToDbForWorkspace writes at contract.projectGsd, not milestones/ ──
188
+
189
+ describe("saveArtifactToDbForWorkspace: writes directly to .gsd root via workspace contract", () => {
190
+ let tmp: string;
191
+
192
+ beforeEach(() => {
193
+ tmp = makeProjectDir();
194
+ openDatabase(join(tmp, ".gsd", "gsd.db"));
195
+ });
196
+
197
+ afterEach(() => {
198
+ closeDatabase();
199
+ rmSync(tmp, { recursive: true, force: true });
200
+ });
201
+
202
+ test("root artifact lands at contract.projectGsd/path, not milestones/", async () => {
203
+ const ws = createWorkspace(tmp);
204
+ const content = "# Requirements\n";
205
+ const opts = {
206
+ path: "REQUIREMENTS.md",
207
+ artifact_type: "REQUIREMENTS_DRAFT",
208
+ content,
209
+ };
210
+
211
+ await saveArtifactToDbForWorkspace(ws, opts);
212
+
213
+ const expectedPath = resolve(ws.contract.projectGsd, "REQUIREMENTS.md");
214
+ assert.ok(existsSync(expectedPath), "artifact written at contract.projectGsd/REQUIREMENTS.md");
215
+ assert.equal(readFileSync(expectedPath, "utf-8"), content, "content matches");
216
+
217
+ // Must not have landed inside milestones/
218
+ const milestonePath = join(ws.contract.projectGsd, "milestones", "", "REQUIREMENTS.md");
219
+ assert.ok(!existsSync(milestonePath), "artifact NOT inside milestones/empty-string/");
220
+ });
221
+ });
@@ -0,0 +1,230 @@
1
+ // GSD-2 + db-writer saveArtifactToDbByScope: workspace-contract path routing tests
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ existsSync,
9
+ readFileSync,
10
+ realpathSync,
11
+ rmSync,
12
+ } from "node:fs";
13
+ import { join, resolve } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
17
+ import { saveArtifactToDb, saveArtifactToDbByScope } from "../db-writer.ts";
18
+ import { openDatabase, closeDatabase } from "../gsd-db.ts";
19
+
20
+ // ─── Helpers ────────────────────────────────────────────────────────────────
21
+
22
+ function makeProjectDir(): string {
23
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-dbwriter-scope-")));
24
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
25
+ return dir;
26
+ }
27
+
28
+ // ─── Suite 1: scope variant writes to the same canonical path as legacy ──────
29
+
30
+ describe("saveArtifactToDbByScope: path parity with legacy saveArtifactToDb", () => {
31
+ let tmp1: string;
32
+ let tmp2: string;
33
+
34
+ beforeEach(() => {
35
+ tmp1 = makeProjectDir();
36
+ tmp2 = makeProjectDir();
37
+ });
38
+
39
+ afterEach(() => {
40
+ closeDatabase();
41
+ rmSync(tmp1, { recursive: true, force: true });
42
+ rmSync(tmp2, { recursive: true, force: true });
43
+ });
44
+
45
+ test("scope variant writes artifact to same canonical path as legacy variant", async () => {
46
+ const relPath = "milestones/M001/slices/S01/tasks/T01-SUMMARY.md";
47
+ const content = "# T01 Summary\n\nTest content.\n";
48
+ const opts = {
49
+ path: relPath,
50
+ artifact_type: "SUMMARY",
51
+ content,
52
+ milestone_id: "M001",
53
+ slice_id: "S01",
54
+ task_id: "T01",
55
+ };
56
+
57
+ // Legacy path: basePath + '.gsd' join
58
+ const legacyExpectedPath = resolve(tmp1, ".gsd", relPath);
59
+
60
+ // Scope path: contract.projectGsd
61
+ const ws = createWorkspace(tmp2);
62
+ const scope = scopeMilestone(ws, "M001");
63
+ const scopeExpectedPath = resolve(ws.contract.projectGsd, relPath);
64
+
65
+ // Both should resolve to the same relative structure
66
+ // (though under different temp dirs — so we compare structure, not absolute path)
67
+ assert.equal(
68
+ scopeExpectedPath,
69
+ resolve(ws.contract.projectGsd, relPath),
70
+ "scope path must be contract.projectGsd + relPath",
71
+ );
72
+ assert.equal(
73
+ legacyExpectedPath,
74
+ resolve(tmp1, ".gsd", relPath),
75
+ "legacy path must be basePath/.gsd + relPath",
76
+ );
77
+
78
+ // Open DB for tmp1 and write via legacy
79
+ const dbPath1 = join(tmp1, ".gsd", "gsd.db");
80
+ openDatabase(dbPath1);
81
+ await saveArtifactToDb(opts, tmp1);
82
+ closeDatabase();
83
+
84
+ // Open DB for tmp2 and write via scope variant
85
+ const dbPath2 = join(tmp2, ".gsd", "gsd.db");
86
+ openDatabase(dbPath2);
87
+ await saveArtifactToDbByScope(scope, opts);
88
+ closeDatabase();
89
+
90
+ // Both should have written to the correct location under their respective .gsd dirs
91
+ assert.ok(existsSync(legacyExpectedPath), "legacy: artifact written at basePath/.gsd/relPath");
92
+ assert.ok(existsSync(scopeExpectedPath), "scope: artifact written at contract.projectGsd/relPath");
93
+
94
+ // Content must match
95
+ assert.equal(readFileSync(legacyExpectedPath, "utf-8"), content, "legacy: content matches");
96
+ assert.equal(readFileSync(scopeExpectedPath, "utf-8"), content, "scope: content matches");
97
+ });
98
+ });
99
+
100
+ // ─── Suite 2: scope variant uses contract.projectGsd, not a basePath join ────
101
+
102
+ describe("saveArtifactToDbByScope: uses contract.projectGsd, not hand-rolled basePath join", () => {
103
+ let tmp: string;
104
+
105
+ beforeEach(() => {
106
+ tmp = makeProjectDir();
107
+ });
108
+
109
+ afterEach(() => {
110
+ closeDatabase();
111
+ rmSync(tmp, { recursive: true, force: true });
112
+ });
113
+
114
+ test("scope.workspace.contract.projectGsd is used as the .gsd root, not basePath/.gsd", async () => {
115
+ const ws = createWorkspace(tmp);
116
+ const scope = scopeMilestone(ws, "M001");
117
+
118
+ // The contract.projectGsd must equal the canonical join(projectRoot, '.gsd')
119
+ assert.equal(
120
+ ws.contract.projectGsd,
121
+ join(ws.projectRoot, ".gsd"),
122
+ "contract.projectGsd must equal join(projectRoot, '.gsd')",
123
+ );
124
+
125
+ // It must NOT be a hand-rolled resolution from an arbitrary basePath string
126
+ // (i.e., contract.projectGsd routes through the workspace contract)
127
+ assert.ok(
128
+ ws.contract.projectGsd.startsWith(ws.projectRoot),
129
+ "contract.projectGsd must be rooted at projectRoot",
130
+ );
131
+
132
+ const relPath = "milestones/M001/M001-CONTEXT.md";
133
+ const content = "# M001 Context\n";
134
+ const opts = {
135
+ path: relPath,
136
+ artifact_type: "CONTEXT",
137
+ content,
138
+ milestone_id: "M001",
139
+ };
140
+
141
+ openDatabase(join(tmp, ".gsd", "gsd.db"));
142
+ await saveArtifactToDbByScope(scope, opts);
143
+
144
+ // File must be at contract.projectGsd/relPath
145
+ const expectedPath = resolve(ws.contract.projectGsd, relPath);
146
+ assert.ok(existsSync(expectedPath), "artifact written at contract.projectGsd/relPath");
147
+ assert.equal(readFileSync(expectedPath, "utf-8"), content, "content matches");
148
+
149
+ // And must NOT be at some other location
150
+ const handRolledPath = resolve(tmp, ".gsd", relPath);
151
+ // Both should be the same path in project mode (they should agree)
152
+ assert.equal(
153
+ expectedPath,
154
+ handRolledPath,
155
+ "in project mode, contract.projectGsd resolves same as basePath/.gsd",
156
+ );
157
+ });
158
+ });
159
+
160
+ // ─── Suite 3: worktree-mode scope routes to project root's .gsd/ ─────────────
161
+
162
+ describe("saveArtifactToDbByScope: worktree scope writes to project root .gsd/", () => {
163
+ let tmp: string;
164
+
165
+ beforeEach(() => {
166
+ tmp = realpathSync(mkdtempSync(join(tmpdir(), "gsd-dbwriter-wt-scope-")));
167
+ // Create project .gsd directory
168
+ mkdirSync(join(tmp, ".gsd"), { recursive: true });
169
+ });
170
+
171
+ afterEach(() => {
172
+ closeDatabase();
173
+ rmSync(tmp, { recursive: true, force: true });
174
+ });
175
+
176
+ test("worktree-mode scope: contract.projectGsd resolves to project root's .gsd/, not worktree .gsd/", async () => {
177
+ // Construct a worktree path inside the project's .gsd/worktrees/<MID>
178
+ const worktreePath = join(tmp, ".gsd", "worktrees", "M001");
179
+ mkdirSync(join(worktreePath, ".gsd"), { recursive: true });
180
+
181
+ const projectWs = createWorkspace(tmp);
182
+ const worktreeWs = createWorkspace(worktreePath);
183
+
184
+ // Both should share the same projectRoot (worktree-root resolution)
185
+ assert.equal(
186
+ worktreeWs.projectRoot,
187
+ projectWs.projectRoot,
188
+ "worktree workspace must have same projectRoot as project workspace",
189
+ );
190
+
191
+ // contract.projectGsd for the worktree workspace must point to the PROJECT root's .gsd/
192
+ assert.equal(
193
+ worktreeWs.contract.projectGsd,
194
+ join(projectWs.projectRoot, ".gsd"),
195
+ "worktree contract.projectGsd must equal project root's .gsd/",
196
+ );
197
+
198
+ // Must NOT be the worktree-local .gsd/
199
+ assert.notEqual(
200
+ worktreeWs.contract.projectGsd,
201
+ join(worktreePath, ".gsd"),
202
+ "worktree contract.projectGsd must NOT be the worktree-local .gsd/",
203
+ );
204
+
205
+ // Write via the worktree-mode scope
206
+ const scope = scopeMilestone(worktreeWs, "M001");
207
+ const relPath = "milestones/M001/M001-CONTEXT.md";
208
+ const content = "# M001 Context from worktree scope\n";
209
+ const opts = {
210
+ path: relPath,
211
+ artifact_type: "CONTEXT",
212
+ content,
213
+ milestone_id: "M001",
214
+ };
215
+
216
+ openDatabase(join(tmp, ".gsd", "gsd.db"));
217
+ await saveArtifactToDbByScope(scope, opts);
218
+
219
+ // File must land in the PROJECT root's .gsd/, not in the worktree's .gsd/
220
+ const projectPath = resolve(projectWs.contract.projectGsd, relPath);
221
+ const worktreeLocalPath = resolve(worktreePath, ".gsd", relPath);
222
+
223
+ assert.ok(existsSync(projectPath), "artifact written to project root's .gsd/");
224
+ assert.ok(
225
+ !existsSync(worktreeLocalPath),
226
+ "artifact must NOT be written to worktree-local .gsd/",
227
+ );
228
+ assert.equal(readFileSync(projectPath, "utf-8"), content, "content at project root matches");
229
+ });
230
+ });