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,331 @@
1
+ // GSD-2 + Tests for MilestoneScope threading through AutoSession state (C2)
2
+ //
3
+ // Strategy: construct AutoSession directly + call createWorkspace/scopeMilestone
4
+ // to mirror the rebuildScope() helper in auto.ts — avoids importing the full
5
+ // auto.ts module (too many .js resolved imports).
6
+
7
+ import { describe, test, beforeEach, afterEach } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+
13
+ import { AutoSession } from "../auto/session.ts";
14
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
15
+ import type { MilestoneScope } from "../workspace.ts";
16
+
17
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
18
+
19
+ function makeProjectDir(): string {
20
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-scope-test-")));
21
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
22
+ return dir;
23
+ }
24
+
25
+ function makeWorktreeDir(projectDir: string, milestoneId: string): string {
26
+ const wt = join(projectDir, ".gsd", "worktrees", milestoneId);
27
+ mkdirSync(wt, { recursive: true });
28
+ return wt;
29
+ }
30
+
31
+ /**
32
+ * Mirror the rebuildScope() helper from auto.ts — computes s.scope from the
33
+ * same inputs so tests can verify the behaviour without importing auto.ts.
34
+ */
35
+ function applyRebuildScope(
36
+ s: AutoSession,
37
+ rawPath: string,
38
+ milestoneId: string | null,
39
+ ): void {
40
+ if (!milestoneId) {
41
+ s.scope = null;
42
+ return;
43
+ }
44
+ try {
45
+ const workspace = createWorkspace(rawPath);
46
+ s.scope = scopeMilestone(workspace, milestoneId);
47
+ } catch {
48
+ s.scope = null;
49
+ }
50
+ }
51
+
52
+ // ─── Tests ───────────────────────────────────────────────────────────────────
53
+
54
+ describe("AutoSession.scope — project mode (basePath equals originalBasePath)", () => {
55
+ let s: AutoSession;
56
+ let projectDir: string;
57
+
58
+ beforeEach(() => {
59
+ projectDir = makeProjectDir();
60
+ s = new AutoSession();
61
+ });
62
+
63
+ afterEach(() => {
64
+ rmSync(projectDir, { recursive: true, force: true });
65
+ });
66
+
67
+ test("scope is null when milestoneId is null", () => {
68
+ s.basePath = projectDir;
69
+ s.originalBasePath = projectDir;
70
+ s.currentMilestoneId = null;
71
+
72
+ applyRebuildScope(s, projectDir, null);
73
+
74
+ assert.equal(s.scope, null);
75
+ });
76
+
77
+ test("scope mode is 'project' when basePath equals originalBasePath", () => {
78
+ const mid = "M001";
79
+ s.basePath = projectDir;
80
+ s.originalBasePath = projectDir;
81
+ s.currentMilestoneId = mid;
82
+
83
+ applyRebuildScope(s, projectDir, mid);
84
+
85
+ assert.ok(s.scope, "scope should be set");
86
+ assert.equal(s.scope.workspace.mode, "project");
87
+ assert.equal(s.scope.milestoneId, mid);
88
+ });
89
+
90
+ test("scope projectRoot matches realpath of projectDir", () => {
91
+ const mid = "M001";
92
+ s.basePath = projectDir;
93
+ s.originalBasePath = projectDir;
94
+ s.currentMilestoneId = mid;
95
+
96
+ applyRebuildScope(s, projectDir, mid);
97
+
98
+ assert.ok(s.scope, "scope should be set");
99
+ assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
100
+ });
101
+
102
+ test("scope worktreeRoot is null in project mode", () => {
103
+ const mid = "M001";
104
+ s.basePath = projectDir;
105
+ s.originalBasePath = projectDir;
106
+ s.currentMilestoneId = mid;
107
+
108
+ applyRebuildScope(s, projectDir, mid);
109
+
110
+ assert.ok(s.scope, "scope should be set");
111
+ assert.equal(s.scope.workspace.worktreeRoot, null);
112
+ });
113
+
114
+ test("scope path methods resolve under the .gsd directory", () => {
115
+ const mid = "M002";
116
+ s.basePath = projectDir;
117
+ s.originalBasePath = projectDir;
118
+ s.currentMilestoneId = mid;
119
+
120
+ applyRebuildScope(s, projectDir, mid);
121
+
122
+ assert.ok(s.scope, "scope should be set");
123
+ const gsd = join(projectDir, ".gsd");
124
+ assert.equal(s.scope.contextFile(), join(gsd, "milestones", mid, `${mid}-CONTEXT.md`));
125
+ assert.equal(s.scope.roadmapFile(), join(gsd, "milestones", mid, `${mid}-ROADMAP.md`));
126
+ assert.equal(s.scope.stateFile(), join(gsd, "STATE.md"));
127
+ });
128
+ });
129
+
130
+ describe("AutoSession.scope — worktree mode (basePath differs from originalBasePath)", () => {
131
+ let s: AutoSession;
132
+ let projectDir: string;
133
+ let worktreeDir: string;
134
+
135
+ beforeEach(() => {
136
+ projectDir = makeProjectDir();
137
+ worktreeDir = makeWorktreeDir(projectDir, "M001");
138
+ s = new AutoSession();
139
+ });
140
+
141
+ afterEach(() => {
142
+ rmSync(projectDir, { recursive: true, force: true });
143
+ });
144
+
145
+ test("scope mode is 'worktree' when basePath is the worktree path", () => {
146
+ const mid = "M001";
147
+ s.basePath = worktreeDir;
148
+ s.originalBasePath = projectDir;
149
+ s.currentMilestoneId = mid;
150
+
151
+ applyRebuildScope(s, worktreeDir, mid);
152
+
153
+ assert.ok(s.scope, "scope should be set");
154
+ assert.equal(s.scope.workspace.mode, "worktree");
155
+ });
156
+
157
+ test("scope worktreeRoot matches realpath of worktreeDir", () => {
158
+ const mid = "M001";
159
+ s.basePath = worktreeDir;
160
+ s.originalBasePath = projectDir;
161
+ s.currentMilestoneId = mid;
162
+
163
+ applyRebuildScope(s, worktreeDir, mid);
164
+
165
+ assert.ok(s.scope, "scope should be set");
166
+ assert.equal(s.scope.workspace.worktreeRoot, realpathSync(worktreeDir));
167
+ });
168
+
169
+ test("scope projectRoot resolves to project root (not worktree)", () => {
170
+ const mid = "M001";
171
+ s.basePath = worktreeDir;
172
+ s.originalBasePath = projectDir;
173
+ s.currentMilestoneId = mid;
174
+
175
+ applyRebuildScope(s, worktreeDir, mid);
176
+
177
+ assert.ok(s.scope, "scope should be set");
178
+ assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
179
+ });
180
+
181
+ test("scope milestoneId matches the milestone being tracked", () => {
182
+ const mid = "M001";
183
+ s.basePath = worktreeDir;
184
+ s.originalBasePath = projectDir;
185
+ s.currentMilestoneId = mid;
186
+
187
+ applyRebuildScope(s, worktreeDir, mid);
188
+
189
+ assert.ok(s.scope, "scope should be set");
190
+ assert.equal(s.scope.milestoneId, mid);
191
+ });
192
+ });
193
+
194
+ describe("AutoSession.scope — milestoneId change rebuilds scope", () => {
195
+ let s: AutoSession;
196
+ let projectDir: string;
197
+
198
+ beforeEach(() => {
199
+ projectDir = makeProjectDir();
200
+ s = new AutoSession();
201
+ });
202
+
203
+ afterEach(() => {
204
+ rmSync(projectDir, { recursive: true, force: true });
205
+ });
206
+
207
+ test("scope reflects the new milestoneId after rebuild", () => {
208
+ s.basePath = projectDir;
209
+ s.originalBasePath = projectDir;
210
+ s.currentMilestoneId = "M001";
211
+ applyRebuildScope(s, projectDir, "M001");
212
+
213
+ assert.ok(s.scope, "initial scope should be set");
214
+ assert.equal(s.scope.milestoneId, "M001");
215
+
216
+ // Simulate milestone transition mid-session
217
+ s.currentMilestoneId = "M002";
218
+ applyRebuildScope(s, projectDir, "M002");
219
+
220
+ assert.ok(s.scope, "scope should be set after transition");
221
+ assert.equal(s.scope.milestoneId, "M002");
222
+ });
223
+
224
+ test("scope contextFile changes when milestoneId changes", () => {
225
+ s.basePath = projectDir;
226
+ s.originalBasePath = projectDir;
227
+
228
+ s.currentMilestoneId = "M001";
229
+ applyRebuildScope(s, projectDir, "M001");
230
+ const ctxM001 = s.scope?.contextFile();
231
+
232
+ s.currentMilestoneId = "M002";
233
+ applyRebuildScope(s, projectDir, "M002");
234
+ const ctxM002 = s.scope?.contextFile();
235
+
236
+ assert.ok(ctxM001, "M001 contextFile should be set");
237
+ assert.ok(ctxM002, "M002 contextFile should be set");
238
+ assert.notEqual(ctxM001, ctxM002, "contextFile must differ between milestone IDs");
239
+ assert.ok(ctxM001.includes("M001"), "M001 path should contain M001");
240
+ assert.ok(ctxM002.includes("M002"), "M002 path should contain M002");
241
+ });
242
+ });
243
+
244
+ describe("AutoSession.scope — resume from persisted state", () => {
245
+ let s: AutoSession;
246
+ let projectDir: string;
247
+ let worktreeDir: string;
248
+
249
+ beforeEach(() => {
250
+ projectDir = makeProjectDir();
251
+ worktreeDir = makeWorktreeDir(projectDir, "M003");
252
+ s = new AutoSession();
253
+ });
254
+
255
+ afterEach(() => {
256
+ rmSync(projectDir, { recursive: true, force: true });
257
+ });
258
+
259
+ test("resume without worktree: scope mode is project, projectRoot is base", () => {
260
+ // Mirror the paused-session resume path:
261
+ // s.currentMilestoneId = meta.milestoneId
262
+ // s.originalBasePath = meta.originalBasePath || base
263
+ // rawPath = originalBasePath (no worktreePath present)
264
+ const mid = "M003";
265
+ s.currentMilestoneId = mid;
266
+ s.originalBasePath = projectDir;
267
+ s.basePath = projectDir;
268
+
269
+ applyRebuildScope(s, s.originalBasePath, s.currentMilestoneId);
270
+
271
+ assert.ok(s.scope, "scope should be reconstructed");
272
+ assert.equal(s.scope.milestoneId, mid);
273
+ assert.equal(s.scope.workspace.mode, "project");
274
+ assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
275
+ assert.equal(s.scope.workspace.worktreeRoot, null);
276
+ });
277
+
278
+ test("resume with valid worktree path: scope mode is worktree", () => {
279
+ // Mirror the paused-session resume path where worktreePath exists on disk:
280
+ // rawPath = worktreePath (existsSync true)
281
+ const mid = "M003";
282
+ s.currentMilestoneId = mid;
283
+ s.originalBasePath = projectDir;
284
+ s.basePath = worktreeDir;
285
+
286
+ assert.ok(existsSync(worktreeDir), "worktreeDir must exist for this test");
287
+
288
+ applyRebuildScope(s, worktreeDir, s.currentMilestoneId);
289
+
290
+ assert.ok(s.scope, "scope should be reconstructed");
291
+ assert.equal(s.scope.milestoneId, mid);
292
+ assert.equal(s.scope.workspace.mode, "worktree");
293
+ assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
294
+ assert.equal(s.scope.workspace.worktreeRoot, realpathSync(worktreeDir));
295
+ });
296
+
297
+ test("scope is consistent with direct createWorkspace + scopeMilestone for same inputs", () => {
298
+ const mid = "M003";
299
+ s.currentMilestoneId = mid;
300
+ s.originalBasePath = projectDir;
301
+ s.basePath = projectDir;
302
+
303
+ applyRebuildScope(s, projectDir, mid);
304
+ assert.ok(s.scope, "scope should be set");
305
+
306
+ // Build expected scope via lower-level API to verify equivalence
307
+ const ws = createWorkspace(projectDir);
308
+ const expected = scopeMilestone(ws, mid);
309
+
310
+ assert.equal(s.scope.milestoneId, expected.milestoneId);
311
+ assert.equal(s.scope.contextFile(), expected.contextFile());
312
+ assert.equal(s.scope.roadmapFile(), expected.roadmapFile());
313
+ assert.equal(s.scope.stateFile(), expected.stateFile());
314
+ assert.equal(s.scope.dbPath(), expected.dbPath());
315
+ assert.equal(s.scope.milestoneDir(), expected.milestoneDir());
316
+ assert.equal(s.scope.metaJson(), expected.metaJson());
317
+ });
318
+
319
+ test("reset() clears scope", () => {
320
+ const mid = "M003";
321
+ s.basePath = projectDir;
322
+ s.originalBasePath = projectDir;
323
+ s.currentMilestoneId = mid;
324
+
325
+ applyRebuildScope(s, projectDir, mid);
326
+ assert.ok(s.scope, "scope should be set before reset");
327
+
328
+ s.reset();
329
+ assert.equal(s.scope, null, "scope must be null after reset()");
330
+ });
331
+ });
@@ -0,0 +1,105 @@
1
+ // gsd-2 + Auto-mode worker registry tests (Phase B coordination)
2
+
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+
9
+ import { openDatabase, closeDatabase } from "../gsd-db.ts";
10
+ import { _getAdapter } from "../gsd-db.ts";
11
+ import {
12
+ registerAutoWorker,
13
+ heartbeatAutoWorker,
14
+ markWorkerCrashed,
15
+ markWorkerStopping,
16
+ getActiveAutoWorkers,
17
+ getAutoWorker,
18
+ } from "../db/auto-workers.ts";
19
+
20
+ function makeBase(): string {
21
+ const base = mkdtempSync(join(tmpdir(), "gsd-auto-workers-"));
22
+ mkdirSync(join(base, ".gsd"), { recursive: true });
23
+ return base;
24
+ }
25
+
26
+ function cleanup(base: string): void {
27
+ try { closeDatabase(); } catch { /* noop */ }
28
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
29
+ }
30
+
31
+ test("registerAutoWorker creates a row with active status and heartbeat", (t) => {
32
+ const base = makeBase();
33
+ t.after(() => cleanup(base));
34
+ openDatabase(join(base, ".gsd", "gsd.db"));
35
+
36
+ const id = registerAutoWorker({ projectRootRealpath: base });
37
+ assert.match(id, /^auto-/, "worker_id has expected prefix");
38
+
39
+ const row = getAutoWorker(id);
40
+ assert.ok(row, "row exists");
41
+ assert.equal(row!.status, "active");
42
+ assert.equal(row!.project_root_realpath, base);
43
+ assert.equal(row!.pid, process.pid);
44
+ });
45
+
46
+ test("heartbeatAutoWorker updates last_heartbeat_at", async (t) => {
47
+ const base = makeBase();
48
+ t.after(() => cleanup(base));
49
+ openDatabase(join(base, ".gsd", "gsd.db"));
50
+
51
+ const id = registerAutoWorker({ projectRootRealpath: base });
52
+ const initial = getAutoWorker(id)!;
53
+ await new Promise(r => setTimeout(r, 10));
54
+ heartbeatAutoWorker(id);
55
+ const after = getAutoWorker(id)!;
56
+ const initialTs = Date.parse(initial.last_heartbeat_at);
57
+ const afterTs = Date.parse(after.last_heartbeat_at);
58
+ assert.ok(Number.isFinite(initialTs), "initial heartbeat parses");
59
+ assert.ok(Number.isFinite(afterTs), "updated heartbeat parses");
60
+ assert.ok(afterTs > initialTs, "heartbeat advanced");
61
+ });
62
+
63
+ test("markWorkerStopping flips status to stopping", (t) => {
64
+ const base = makeBase();
65
+ t.after(() => cleanup(base));
66
+ openDatabase(join(base, ".gsd", "gsd.db"));
67
+
68
+ const id = registerAutoWorker({ projectRootRealpath: base });
69
+ markWorkerStopping(id);
70
+ const row = getAutoWorker(id)!;
71
+ assert.equal(row.status, "stopping");
72
+ });
73
+
74
+ test("markWorkerCrashed flips status to crashed", (t) => {
75
+ const base = makeBase();
76
+ t.after(() => cleanup(base));
77
+ openDatabase(join(base, ".gsd", "gsd.db"));
78
+
79
+ const id = registerAutoWorker({ projectRootRealpath: base });
80
+ markWorkerCrashed(id);
81
+ const row = getAutoWorker(id)!;
82
+ assert.equal(row.status, "crashed");
83
+ });
84
+
85
+ test("getActiveAutoWorkers filters by status and TTL", (t) => {
86
+ const base = makeBase();
87
+ t.after(() => cleanup(base));
88
+ openDatabase(join(base, ".gsd", "gsd.db"));
89
+
90
+ const a = registerAutoWorker({ projectRootRealpath: base });
91
+ const b = registerAutoWorker({ projectRootRealpath: base });
92
+
93
+ const active = getActiveAutoWorkers();
94
+ assert.equal(active.length, 2);
95
+ assert.ok(active.find(w => w.worker_id === a));
96
+ assert.ok(active.find(w => w.worker_id === b));
97
+
98
+ _getAdapter()!.prepare(
99
+ `UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :worker_id`,
100
+ ).run({ ":worker_id": a });
101
+
102
+ const after = getActiveAutoWorkers();
103
+ assert.equal(after.length, 1);
104
+ assert.equal(after[0].worker_id, b);
105
+ });
@@ -0,0 +1,176 @@
1
+ // GSD-2 + Unit tests for the workspace registry that replaced the originalBase singleton
2
+
3
+ import { describe, test, beforeEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { execFileSync } from "node:child_process";
9
+
10
+ import {
11
+ getAutoWorktreeOriginalBase,
12
+ getActiveAutoWorktreeContext,
13
+ _resetAutoWorktreeOriginalBaseForTests,
14
+ createAutoWorktree,
15
+ enterAutoWorktree,
16
+ teardownAutoWorktree,
17
+ } from "../auto-worktree.ts";
18
+
19
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
20
+
21
+ // Safe: all inputs below are hardcoded test strings, not user input.
22
+ function git(subArgs: string[], cwd: string): void {
23
+ execFileSync("git", subArgs, { cwd, stdio: ["ignore", "pipe", "pipe"] });
24
+ }
25
+
26
+ function createTempRepo(t: { after: (fn: () => void) => void }): string {
27
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "awreg-test-")));
28
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
29
+ git(["init"], dir);
30
+ git(["config", "user.email", "test@test.com"], dir);
31
+ git(["config", "user.name", "Test"], dir);
32
+ writeFileSync(join(dir, "README.md"), "# test\n");
33
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
34
+ git(["add", "."], dir);
35
+ git(["commit", "-m", "init"], dir);
36
+ git(["branch", "-M", "main"], dir);
37
+ return dir;
38
+ }
39
+
40
+ // ─── Tests ───────────────────────────────────────────────────────────────────
41
+
42
+ describe("auto-worktree workspace registry", () => {
43
+ const savedCwd = process.cwd();
44
+
45
+ beforeEach(() => {
46
+ _resetAutoWorktreeOriginalBaseForTests();
47
+ process.chdir(savedCwd);
48
+ });
49
+
50
+ test("getAutoWorktreeOriginalBase() is null at baseline", () => {
51
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
52
+ });
53
+
54
+ test("getActiveAutoWorktreeContext() is null at baseline", () => {
55
+ assert.strictEqual(getActiveAutoWorktreeContext(), null);
56
+ });
57
+
58
+ test("_resetAutoWorktreeOriginalBaseForTests() clears the registry — idempotent", () => {
59
+ _resetAutoWorktreeOriginalBaseForTests();
60
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
61
+ _resetAutoWorktreeOriginalBaseForTests();
62
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
63
+ });
64
+
65
+ test("behavioral equivalence: createAutoWorktree populates registry; teardown clears it", (t) => {
66
+ const tempDir = createTempRepo(t);
67
+ const msDir = join(tempDir, ".gsd", "milestones", "M001");
68
+ mkdirSync(msDir, { recursive: true });
69
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M001 Context\n");
70
+ git(["add", "."], tempDir);
71
+ git(["commit", "-m", "add milestone"], tempDir);
72
+
73
+ // Before entering: registry must be empty
74
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
75
+ "originalBase is null before entering worktree");
76
+
77
+ createAutoWorktree(tempDir, "M001");
78
+
79
+ // After enter: getAutoWorktreeOriginalBase must equal tempDir
80
+ assert.strictEqual(
81
+ getAutoWorktreeOriginalBase(),
82
+ tempDir,
83
+ "getAutoWorktreeOriginalBase() returns projectRoot after createAutoWorktree",
84
+ );
85
+
86
+ // getActiveAutoWorktreeContext must return the correct shape
87
+ const ctx = getActiveAutoWorktreeContext();
88
+ assert.ok(ctx !== null, "context is non-null inside worktree");
89
+ assert.strictEqual(ctx.originalBase, tempDir, "context.originalBase matches tempDir");
90
+ assert.strictEqual(ctx.worktreeName, "M001", "context.worktreeName is M001");
91
+ assert.strictEqual(ctx.branch, "milestone/M001", "context.branch is milestone/M001");
92
+
93
+ // Teardown: registry must be cleared
94
+ teardownAutoWorktree(tempDir, "M001");
95
+
96
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
97
+ "getAutoWorktreeOriginalBase() is null after teardown");
98
+ assert.strictEqual(getActiveAutoWorktreeContext(), null,
99
+ "getActiveAutoWorktreeContext() is null after teardown");
100
+
101
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
102
+ });
103
+
104
+ test("behavioral equivalence: enterAutoWorktree also populates registry", (t) => {
105
+ const tempDir = createTempRepo(t);
106
+ const msDir = join(tempDir, ".gsd", "milestones", "M002");
107
+ mkdirSync(msDir, { recursive: true });
108
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M002 Context\n");
109
+ git(["add", "."], tempDir);
110
+ git(["commit", "-m", "add milestone"], tempDir);
111
+
112
+ createAutoWorktree(tempDir, "M002");
113
+
114
+ // Simulate leaving the worktree (crash/pause)
115
+ _resetAutoWorktreeOriginalBaseForTests();
116
+ process.chdir(tempDir);
117
+
118
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
119
+ "registry is empty after manual reset");
120
+
121
+ // Re-enter via enterAutoWorktree
122
+ enterAutoWorktree(tempDir, "M002");
123
+
124
+ assert.strictEqual(
125
+ getAutoWorktreeOriginalBase(),
126
+ tempDir,
127
+ "getAutoWorktreeOriginalBase() returns projectRoot after enterAutoWorktree",
128
+ );
129
+ const ctx = getActiveAutoWorktreeContext();
130
+ assert.ok(ctx !== null, "context is non-null after re-entry");
131
+ assert.strictEqual(ctx.originalBase, tempDir);
132
+ assert.strictEqual(ctx.worktreeName, "M002");
133
+ assert.strictEqual(ctx.branch, "milestone/M002");
134
+
135
+ teardownAutoWorktree(tempDir, "M002");
136
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
137
+ });
138
+
139
+ test("single-occupancy: entering a new workspace replaces the previous one", (t) => {
140
+ const dir1 = createTempRepo(t);
141
+ const dir2 = createTempRepo(t);
142
+
143
+ // Set up milestone in dir1
144
+ const ms1Dir = join(dir1, ".gsd", "milestones", "M010");
145
+ mkdirSync(ms1Dir, { recursive: true });
146
+ writeFileSync(join(ms1Dir, "CONTEXT.md"), "# M010\n");
147
+ git(["add", "."], dir1);
148
+ git(["commit", "-m", "add milestone"], dir1);
149
+
150
+ // Set up milestone in dir2
151
+ const ms2Dir = join(dir2, ".gsd", "milestones", "M020");
152
+ mkdirSync(ms2Dir, { recursive: true });
153
+ writeFileSync(join(ms2Dir, "CONTEXT.md"), "# M020\n");
154
+ git(["add", "."], dir2);
155
+ git(["commit", "-m", "add milestone"], dir2);
156
+
157
+ // Enter dir1/M010
158
+ createAutoWorktree(dir1, "M010");
159
+ assert.strictEqual(getAutoWorktreeOriginalBase(), dir1,
160
+ "registry holds dir1 after entering M010");
161
+
162
+ // Tear down dir1 cleanly
163
+ teardownAutoWorktree(dir1, "M010");
164
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry cleared after M010 teardown");
165
+
166
+ // Enter dir2/M020 — registry should now hold dir2 only
167
+ createAutoWorktree(dir2, "M020");
168
+ assert.strictEqual(getAutoWorktreeOriginalBase(), dir2,
169
+ "registry holds dir2 after entering M020 (single-occupancy preserved)");
170
+ assert.notStrictEqual(getAutoWorktreeOriginalBase(), dir1,
171
+ "dir1 is no longer in the registry");
172
+
173
+ teardownAutoWorktree(dir2, "M020");
174
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
175
+ });
176
+ });