gsd-pi 2.61.0-dev.7aed0bf → 2.62.0-dev.a987556

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 (119) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +47 -3
  2. package/dist/resources/extensions/gsd/auto-start.js +11 -6
  3. package/dist/resources/extensions/gsd/auto-timers.js +8 -2
  4. package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
  5. package/dist/resources/extensions/gsd/auto.js +24 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
  7. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
  8. package/dist/resources/extensions/gsd/commands-handlers.js +18 -7
  9. package/dist/resources/extensions/gsd/db-writer.js +64 -28
  10. package/dist/resources/extensions/gsd/preferences-models.js +74 -0
  11. package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
  12. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  13. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  14. package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
  15. package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
  16. package/dist/resources/extensions/gsd/skill-health.js +7 -3
  17. package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
  18. package/dist/web/standalone/.next/BUILD_ID +1 -1
  19. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  20. package/dist/web/standalone/.next/build-manifest.json +2 -2
  21. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  22. package/dist/web/standalone/.next/required-server-files.json +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  24. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.html +1 -1
  40. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  47. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  48. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  49. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  50. package/dist/web/standalone/server.js +1 -1
  51. package/package.json +1 -1
  52. package/packages/mcp-server/src/cli.ts +1 -1
  53. package/packages/mcp-server/src/index.ts +15 -1
  54. package/packages/mcp-server/src/readers/captures.ts +119 -0
  55. package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
  56. package/packages/mcp-server/src/readers/index.ts +16 -0
  57. package/packages/mcp-server/src/readers/knowledge.ts +111 -0
  58. package/packages/mcp-server/src/readers/metrics.ts +118 -0
  59. package/packages/mcp-server/src/readers/paths.ts +217 -0
  60. package/packages/mcp-server/src/readers/readers.test.ts +509 -0
  61. package/packages/mcp-server/src/readers/roadmap.ts +263 -0
  62. package/packages/mcp-server/src/readers/state.ts +223 -0
  63. package/packages/mcp-server/src/server.ts +134 -3
  64. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
  65. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
  66. package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
  67. package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
  68. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
  69. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
  70. package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
  71. package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
  72. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
  73. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
  75. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
  78. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
  80. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
  82. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
  84. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  85. package/packages/pi-coding-agent/package.json +1 -1
  86. package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
  87. package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
  88. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
  89. package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
  90. package/pkg/package.json +1 -1
  91. package/src/resources/extensions/ask-user-questions.ts +60 -4
  92. package/src/resources/extensions/gsd/auto-start.ts +11 -6
  93. package/src/resources/extensions/gsd/auto-timers.ts +8 -2
  94. package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
  95. package/src/resources/extensions/gsd/auto.ts +25 -0
  96. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
  97. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
  98. package/src/resources/extensions/gsd/commands-handlers.ts +20 -7
  99. package/src/resources/extensions/gsd/db-writer.ts +67 -30
  100. package/src/resources/extensions/gsd/preferences-models.ts +78 -0
  101. package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
  102. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  103. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  104. package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
  105. package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
  106. package/src/resources/extensions/gsd/skill-health.ts +7 -3
  107. package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
  108. package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
  109. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
  110. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
  111. package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
  112. package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
  113. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
  114. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +108 -0
  115. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
  116. package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
  117. package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
  118. /package/dist/web/standalone/.next/static/{b7FOoMHaUb3FPoLNbxar4 → AsCFY1___4XTI6GkTQkFb}/_buildManifest.js +0 -0
  119. /package/dist/web/standalone/.next/static/{b7FOoMHaUb3FPoLNbxar4 → AsCFY1___4XTI6GkTQkFb}/_ssgManifest.js +0 -0
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Tests for model config isolation between concurrent instances (#650, #1065).
2
+ * Tests for model config isolation between concurrent instances (#650, #1065)
3
+ * and GSD preferences override of settings.json defaults (#3517).
3
4
  */
4
5
 
5
6
  import { describe, it, beforeEach, afterEach } from "node:test";
@@ -155,3 +156,76 @@ describe("session model recovery on error (#1065)", () => {
155
156
  "Recovery should be skipped when no session model was captured");
156
157
  });
157
158
  });
159
+
160
+ // ─── GSD Preferences override settings.json (#3517) ─────────────────────────
161
+
162
+ describe("GSD preferences override settings.json for session model (#3517)", () => {
163
+ it("preferredModel takes priority over ctx.model when both are available", () => {
164
+ // Simulates auto-start.ts logic: preferredModel ?? ctx.model snapshot
165
+ const preferredModel = { provider: "openai-codex", id: "gpt-5.4" };
166
+ const ctxModel = { provider: "claude-code", id: "claude-sonnet-4-6" };
167
+
168
+ const startModelSnapshot = preferredModel
169
+ ?? { provider: ctxModel.provider, id: ctxModel.id };
170
+
171
+ assert.equal(startModelSnapshot.provider, "openai-codex",
172
+ "preferredModel provider should win over ctx.model");
173
+ assert.equal(startModelSnapshot.id, "gpt-5.4",
174
+ "preferredModel id should win over ctx.model");
175
+ });
176
+
177
+ it("falls back to ctx.model when no GSD preferences are configured", () => {
178
+ const preferredModel: { provider: string; id: string } | undefined = undefined;
179
+ const ctxModel = { provider: "claude-code", id: "claude-sonnet-4-6" };
180
+
181
+ const startModelSnapshot = preferredModel
182
+ ?? { provider: ctxModel.provider, id: ctxModel.id };
183
+
184
+ assert.equal(startModelSnapshot.provider, "claude-code",
185
+ "should fall back to ctx.model provider when no preferences");
186
+ assert.equal(startModelSnapshot.id, "claude-sonnet-4-6",
187
+ "should fall back to ctx.model id when no preferences");
188
+ });
189
+
190
+ it("handles null ctx.model with no preferences gracefully", () => {
191
+ const preferredModel: { provider: string; id: string } | undefined = undefined;
192
+ // Use a function to prevent TS from narrowing to `never` in the ternary
193
+ function getCtxModel(): { provider: string; id: string } | null { return null; }
194
+ const ctxModel = getCtxModel();
195
+
196
+ const startModelSnapshot = preferredModel
197
+ ?? (ctxModel ? { provider: ctxModel.provider, id: ctxModel.id } : null);
198
+
199
+ assert.equal(startModelSnapshot, null,
200
+ "should be null when neither preferences nor ctx.model exist");
201
+ });
202
+
203
+ it("bare model ID uses session provider when available", () => {
204
+ // Simulates: PREFERENCES.md has "gpt-5.4" (no provider), session is openai-codex
205
+ const preferredModel = { provider: "openai-codex", id: "gpt-5.4" }; // from resolveDefaultSessionModel("openai-codex")
206
+ const ctxModel = { provider: "openai-codex", id: "claude-sonnet-4-6" };
207
+
208
+ const startModelSnapshot = preferredModel
209
+ ?? { provider: ctxModel.provider, id: ctxModel.id };
210
+
211
+ assert.equal(startModelSnapshot.provider, "openai-codex");
212
+ assert.equal(startModelSnapshot.id, "gpt-5.4",
213
+ "bare model ID from preferences should still override ctx.model");
214
+ });
215
+
216
+ it("stale settings.json does not leak when preferences are set", () => {
217
+ // Scenario: settings.json has claude-code, PREFERENCES.md has openai-codex
218
+ const settingsJsonDefault = { provider: "claude-code", id: "claude-sonnet-4-6" };
219
+ const preferencesModel = { provider: "openai-codex", id: "gpt-5.4" };
220
+
221
+ // auto-start.ts captures preferredModel first, which preempts settingsJsonDefault
222
+ const startModelSnapshot = preferencesModel ?? settingsJsonDefault;
223
+
224
+ assert.equal(startModelSnapshot.provider, "openai-codex",
225
+ "PREFERENCES.md must override stale settings.json provider");
226
+ assert.equal(startModelSnapshot.id, "gpt-5.4",
227
+ "PREFERENCES.md must override stale settings.json model");
228
+ assert.notEqual(startModelSnapshot.provider, settingsJsonDefault.provider,
229
+ "settings.json provider must NOT leak through");
230
+ });
231
+ });
@@ -0,0 +1,108 @@
1
+ // GSD Extension - Steer Worktree Path Resolution Test
2
+ // Regression test for #3476: /gsd steer must write overrides to the worktree .gsd/,
3
+ // not the project root .gsd/, when a worktree is active.
4
+
5
+ import { describe, test, beforeEach, afterEach } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdtempSync, mkdirSync, existsSync, readFileSync, rmSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { tmpdir } from "node:os";
10
+ import { appendOverride, loadActiveOverrides } from "../files.ts";
11
+ import { getAutoWorktreePath } from "../auto-worktree.ts";
12
+
13
+ describe("steer worktree path resolution (#3476)", () => {
14
+ let projectRoot: string;
15
+ let worktreePath: string;
16
+
17
+ beforeEach(() => {
18
+ projectRoot = mkdtempSync(join(tmpdir(), "gsd-steer-wt-"));
19
+ mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
20
+
21
+ // Simulate a worktree with its own .gsd directory
22
+ worktreePath = join(projectRoot, ".gsd", "worktrees", "M001");
23
+ mkdirSync(join(worktreePath, ".gsd"), { recursive: true });
24
+ });
25
+
26
+ afterEach(() => {
27
+ rmSync(projectRoot, { recursive: true, force: true });
28
+ });
29
+
30
+ test("appendOverride writes to worktree .gsd/ when worktree path is used", async () => {
31
+ await appendOverride(worktreePath, "Use Postgres instead of SQLite", "M001/S01/T01");
32
+
33
+ // Override should be in the worktree .gsd/
34
+ const wtOverrides = join(worktreePath, ".gsd", "OVERRIDES.md");
35
+ assert.ok(existsSync(wtOverrides), "override file exists in worktree .gsd/");
36
+
37
+ const content = readFileSync(wtOverrides, "utf-8");
38
+ assert.ok(content.includes("Use Postgres instead of SQLite"), "override content is correct");
39
+
40
+ // Override should NOT be in the project root .gsd/
41
+ const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
42
+ assert.ok(!existsSync(rootOverrides), "no override file in project root .gsd/");
43
+ });
44
+
45
+ test("loadActiveOverrides reads from worktree .gsd/ when worktree path is used", async () => {
46
+ await appendOverride(worktreePath, "Switch to JWT auth", "M001/S02/T01");
47
+
48
+ // Loading from worktree should find the override
49
+ const wtOverrides = await loadActiveOverrides(worktreePath);
50
+ assert.equal(wtOverrides.length, 1, "one active override in worktree");
51
+ assert.equal(wtOverrides[0].change, "Switch to JWT auth");
52
+
53
+ // Loading from project root should find nothing
54
+ const rootOverrides = await loadActiveOverrides(projectRoot);
55
+ assert.equal(rootOverrides.length, 0, "no overrides in project root");
56
+ });
57
+
58
+ test("appendOverride falls back to project root when no worktree exists", async () => {
59
+ await appendOverride(projectRoot, "Use Redis cache", "M001/S01/T01");
60
+
61
+ const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
62
+ assert.ok(existsSync(rootOverrides), "override file exists in project root .gsd/");
63
+
64
+ const content = readFileSync(rootOverrides, "utf-8");
65
+ assert.ok(content.includes("Use Redis cache"), "override content is correct");
66
+ });
67
+
68
+ test("getAutoWorktreePath returns null for worktree without valid .git file", () => {
69
+ // The worktree directory exists but has no .git file — this is an inactive/
70
+ // leftover worktree. getAutoWorktreePath must return null so handleSteer
71
+ // does not route overrides to a dead worktree.
72
+ const result = getAutoWorktreePath(projectRoot, "M001");
73
+ assert.equal(result, null, "returns null for worktree without .git file");
74
+ });
75
+
76
+ test("override routing: inactive worktree directory should not receive overrides", async () => {
77
+ // Simulate the handleSteer path-resolution logic:
78
+ // When no auto-mode is running, even if a worktree dir exists,
79
+ // overrides must go to the project root.
80
+ const autoRunning = false; // no live session
81
+ const wtPath = autoRunning ? getAutoWorktreePath(projectRoot, "M001") : null;
82
+ const targetPath = wtPath ?? projectRoot;
83
+
84
+ await appendOverride(targetPath, "Should go to project root", "M001/S01/T01");
85
+
86
+ const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
87
+ const wtOverrides = join(worktreePath, ".gsd", "OVERRIDES.md");
88
+
89
+ assert.ok(existsSync(rootOverrides), "override written to project root");
90
+ assert.ok(!existsSync(wtOverrides), "override NOT written to inactive worktree");
91
+ });
92
+
93
+ test("override routing: active worktree with valid .git should receive overrides", async () => {
94
+ // Simulate the handleSteer path-resolution logic with active auto-mode.
95
+ // getAutoWorktreePath requires a valid .git file, so even with autoRunning=true,
96
+ // it returns null for our test worktree (no real .git). This confirms the
97
+ // double-gate: both autoRunning AND valid worktree must be true.
98
+ const autoRunning = true;
99
+ const wtPath = autoRunning ? getAutoWorktreePath(projectRoot, "M001") : null;
100
+ const targetPath = wtPath ?? projectRoot;
101
+
102
+ // Without a valid .git file, falls back to project root
103
+ await appendOverride(targetPath, "Falls back without .git", "M001/S01/T01");
104
+
105
+ const rootOverrides = join(projectRoot, ".gsd", "OVERRIDES.md");
106
+ assert.ok(existsSync(rootOverrides), "override written to project root (no valid .git in worktree)");
107
+ });
108
+ });
@@ -136,17 +136,30 @@ console.log('\n── Loop guard: nested args are not stripped ──');
136
136
  assert.deepStrictEqual(getToolCallLoopCount(), 1, `Each unique nested call should reset count to 1`);
137
137
  }
138
138
 
139
- // Truly identical nested calls should still be detected
139
+ // Truly identical nested calls should still be detected.
140
+ // ask_user_questions has a strict threshold of 1, so the 2nd identical call is blocked.
141
+ resetToolCallLoopGuard();
142
+ const first = checkToolCallLoop('ask_user_questions', {
143
+ questions: [{ id: 'same', question: 'Same?' }],
144
+ });
145
+ assert.ok(first.block === false, 'First ask_user_questions call should be allowed');
146
+ const blocked = checkToolCallLoop('ask_user_questions', {
147
+ questions: [{ id: 'same', question: 'Same?' }],
148
+ });
149
+ assert.ok(blocked.block === true, '2nd identical ask_user_questions call should be blocked (strict threshold)');
150
+
151
+ // Non-strict tools still allow up to 4 identical calls
140
152
  resetToolCallLoopGuard();
141
153
  for (let i = 1; i <= 4; i++) {
142
- checkToolCallLoop('ask_user_questions', {
154
+ const r = checkToolCallLoop('web_search', {
143
155
  questions: [{ id: 'same', question: 'Same?' }],
144
156
  });
157
+ assert.ok(r.block === false, `web_search call ${i} should be allowed (normal threshold)`);
145
158
  }
146
- const blocked = checkToolCallLoop('ask_user_questions', {
159
+ const blockedNormal = checkToolCallLoop('web_search', {
147
160
  questions: [{ id: 'same', question: 'Same?' }],
148
161
  });
149
- assert.ok(blocked.block === true, 'Identical nested calls should still be blocked');
162
+ assert.ok(blockedNormal.block === true, '5th identical web_search call should be blocked');
150
163
  }
151
164
 
152
165
  // ═══════════════════════════════════════════════════════════════════════════
@@ -100,8 +100,87 @@ describe('worktree-db-respawn-truncation (#2815)', async () => {
100
100
  }
101
101
  }
102
102
 
103
- // ─── 3. Milestone artifacts still synced when DB is preserved ────────
104
- console.log('\n=== 3. milestone artifacts still synced even when DB preserved ===');
103
+ // ─── 3. WAL/SHM sidecar files cleaned up when empty DB is deleted (#2478) ──
104
+ console.log('\n=== 3. orphaned WAL/SHM cleaned up alongside empty gsd.db (#2478) ===');
105
+ {
106
+ const mainBase = createBase('main');
107
+ const wtBase = createBase('wt');
108
+
109
+ try {
110
+ const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
111
+ mkdirSync(m001Dir, { recursive: true });
112
+ writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
113
+
114
+ // Create an empty (0-byte) gsd.db plus orphaned WAL and SHM files —
115
+ // this is the exact state that causes Node 24 node:sqlite CPU spin (#2478).
116
+ const wtGsd = join(wtBase, '.gsd');
117
+ writeFileSync(join(wtGsd, 'gsd.db'), '');
118
+ writeFileSync(join(wtGsd, 'gsd.db-wal'), Buffer.alloc(605672, 0xAA));
119
+ writeFileSync(join(wtGsd, 'gsd.db-shm'), Buffer.alloc(32768, 0xBB));
120
+
121
+ assert.ok(existsSync(join(wtGsd, 'gsd.db')), 'gsd.db exists before sync');
122
+ assert.ok(existsSync(join(wtGsd, 'gsd.db-wal')), 'gsd.db-wal exists before sync');
123
+ assert.ok(existsSync(join(wtGsd, 'gsd.db-shm')), 'gsd.db-shm exists before sync');
124
+
125
+ syncProjectRootToWorktree(mainBase, wtBase, 'M001');
126
+
127
+ assert.ok(
128
+ !existsSync(join(wtGsd, 'gsd.db')),
129
+ '#2478: empty gsd.db must be deleted',
130
+ );
131
+ assert.ok(
132
+ !existsSync(join(wtGsd, 'gsd.db-wal')),
133
+ '#2478: orphaned gsd.db-wal must be deleted alongside gsd.db',
134
+ );
135
+ assert.ok(
136
+ !existsSync(join(wtGsd, 'gsd.db-shm')),
137
+ '#2478: orphaned gsd.db-shm must be deleted alongside gsd.db',
138
+ );
139
+ } finally {
140
+ cleanup(mainBase);
141
+ cleanup(wtBase);
142
+ }
143
+ }
144
+
145
+ // ─── 4. Orphaned WAL/SHM cleaned up even when gsd.db already missing (#2478) ──
146
+ console.log('\n=== 4. orphaned WAL/SHM cleaned up even without gsd.db (#2478) ===');
147
+ {
148
+ const mainBase = createBase('main');
149
+ const wtBase = createBase('wt');
150
+
151
+ try {
152
+ const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
153
+ mkdirSync(m001Dir, { recursive: true });
154
+ writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
155
+
156
+ // Orphaned WAL/SHM with NO gsd.db at all — can happen from a previous
157
+ // partial cleanup. These must still be cleaned up.
158
+ const wtGsd = join(wtBase, '.gsd');
159
+ writeFileSync(join(wtGsd, 'gsd.db-wal'), Buffer.alloc(1024, 0xAA));
160
+ writeFileSync(join(wtGsd, 'gsd.db-shm'), Buffer.alloc(1024, 0xBB));
161
+
162
+ assert.ok(!existsSync(join(wtGsd, 'gsd.db')), 'gsd.db does not exist');
163
+ assert.ok(existsSync(join(wtGsd, 'gsd.db-wal')), 'orphaned gsd.db-wal exists');
164
+ assert.ok(existsSync(join(wtGsd, 'gsd.db-shm')), 'orphaned gsd.db-shm exists');
165
+
166
+ syncProjectRootToWorktree(mainBase, wtBase, 'M001');
167
+
168
+ assert.ok(
169
+ !existsSync(join(wtGsd, 'gsd.db-wal')),
170
+ '#2478: orphaned gsd.db-wal must be deleted even without main db file',
171
+ );
172
+ assert.ok(
173
+ !existsSync(join(wtGsd, 'gsd.db-shm')),
174
+ '#2478: orphaned gsd.db-shm must be deleted even without main db file',
175
+ );
176
+ } finally {
177
+ cleanup(mainBase);
178
+ cleanup(wtBase);
179
+ }
180
+ }
181
+
182
+ // ─── 5. Milestone artifacts still synced when DB is preserved ────────
183
+ console.log('\n=== 5. milestone artifacts still synced even when DB preserved ===');
105
184
  {
106
185
  const mainBase = createBase('main');
107
186
  const wtBase = createBase('wt');
@@ -10,12 +10,13 @@
10
10
  * triggers a follow-up free-text input prompt via ctx.ui.input().
11
11
  */
12
12
 
13
- import { describe, it } from "node:test";
13
+ import { describe, it, beforeEach } from "node:test";
14
14
  import assert from "node:assert/strict";
15
15
 
16
16
  // The ask-user-questions extension registers a tool via pi.registerTool().
17
17
  // We capture that registration and call execute() directly with a mock context.
18
18
  import AskUserQuestions from "../../ask-user-questions.js";
19
+ import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
19
20
 
20
21
  interface CapturedTool {
21
22
  name: string;
@@ -73,6 +74,10 @@ function makeMockCtx(opts: {
73
74
  }
74
75
 
75
76
  describe("ask-user-questions RPC fallback free-text", () => {
77
+ beforeEach(() => {
78
+ resetAskUserQuestionsCache();
79
+ });
80
+
76
81
  it("prompts for free-text input when user selects 'None of the above'", async () => {
77
82
  const tool = captureTool();
78
83
  const { ctx, selectCalls, inputCalls } = makeMockCtx({