gsd-pi 2.17.0 → 2.19.0

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 (217) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  9. package/dist/resources/extensions/gsd/auto.ts +399 -29
  10. package/dist/resources/extensions/gsd/captures.ts +384 -0
  11. package/dist/resources/extensions/gsd/commands.ts +382 -23
  12. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  16. package/dist/resources/extensions/gsd/files.ts +123 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  18. package/dist/resources/extensions/gsd/index.ts +47 -3
  19. package/dist/resources/extensions/gsd/metrics.ts +48 -0
  20. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  21. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  22. package/dist/resources/extensions/gsd/paths.ts +9 -0
  23. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  24. package/dist/resources/extensions/gsd/preferences.ts +132 -1
  25. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  28. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  29. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  30. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  31. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  32. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  33. package/dist/resources/extensions/gsd/state.ts +15 -3
  34. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  35. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  37. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  38. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  41. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  42. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  43. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  44. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  45. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  46. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  47. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  48. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  49. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  50. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  51. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  52. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  56. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  57. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  58. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  59. package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
  60. package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  61. package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
  62. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  63. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  64. package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  65. package/dist/resources/extensions/remote-questions/format.ts +12 -6
  66. package/dist/resources/extensions/remote-questions/manager.ts +8 -0
  67. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  68. package/package.json +1 -1
  69. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  70. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  72. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  74. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  76. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  78. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  86. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  102. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  110. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  115. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  117. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/index.js +4 -1
  119. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/main.js +17 -2
  122. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  137. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  138. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  139. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  140. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  141. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  142. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  143. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  144. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  145. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  146. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  147. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  148. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  149. package/packages/pi-coding-agent/src/index.ts +5 -0
  150. package/packages/pi-coding-agent/src/main.ts +19 -2
  151. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  152. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  153. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  154. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  155. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  156. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  157. package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
  158. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  159. package/src/resources/extensions/gsd/auto.ts +399 -29
  160. package/src/resources/extensions/gsd/captures.ts +384 -0
  161. package/src/resources/extensions/gsd/commands.ts +382 -23
  162. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  163. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  164. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  165. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  166. package/src/resources/extensions/gsd/files.ts +123 -1
  167. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  168. package/src/resources/extensions/gsd/index.ts +47 -3
  169. package/src/resources/extensions/gsd/metrics.ts +48 -0
  170. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  171. package/src/resources/extensions/gsd/model-router.ts +256 -0
  172. package/src/resources/extensions/gsd/paths.ts +9 -0
  173. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  174. package/src/resources/extensions/gsd/preferences.ts +132 -1
  175. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  176. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  177. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  178. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  179. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  180. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  181. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  182. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  183. package/src/resources/extensions/gsd/state.ts +15 -3
  184. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  185. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  186. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  187. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  188. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  189. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  190. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  192. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  193. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  194. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  195. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  197. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  198. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  199. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  200. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  201. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  202. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  203. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  204. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  205. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  206. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  207. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  208. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  209. package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
  210. package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  211. package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
  212. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  213. package/src/resources/extensions/gsd/worktree.ts +22 -0
  214. package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  215. package/src/resources/extensions/remote-questions/format.ts +12 -6
  216. package/src/resources/extensions/remote-questions/manager.ts +8 -0
  217. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -1,9 +1,15 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { parseSlackReply, parseDiscordResponse } from "../../remote-questions/format.ts";
3
+ import { readFileSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseSlackReply, parseDiscordResponse, formatForDiscord } from "../../remote-questions/format.ts";
4
7
  import { resolveRemoteConfig, isValidChannelId } from "../../remote-questions/config.ts";
5
8
  import { sanitizeError } from "../../remote-questions/manager.ts";
6
9
 
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
7
13
  test("parseSlackReply handles single-number single-question answers", () => {
8
14
  const result = parseSlackReply("2", [{
9
15
  id: "choice",
@@ -153,3 +159,223 @@ test("sanitizeError preserves short safe messages", () => {
153
159
  assert.equal(sanitizeError("Connection refused"), "Connection refused");
154
160
  });
155
161
 
162
+
163
+ // ═══════════════════════════════════════════════════════════════════════════
164
+ // Discord Parity Tests
165
+ // ═══════════════════════════════════════════════════════════════════════════
166
+
167
+ test("formatForDiscord includes context source in footer when present", () => {
168
+ const prompt = {
169
+ id: "test-1",
170
+ channel: "discord" as const,
171
+ createdAt: Date.now(),
172
+ timeoutAt: Date.now() + 60000,
173
+ pollIntervalMs: 5000,
174
+ context: { source: "auto-mode-dispatch" },
175
+ questions: [{
176
+ id: "q1",
177
+ header: "Confirm",
178
+ question: "Proceed?",
179
+ options: [
180
+ { label: "Yes", description: "Continue" },
181
+ { label: "No", description: "Stop" },
182
+ ],
183
+ allowMultiple: false,
184
+ }],
185
+ };
186
+
187
+ const { embeds } = formatForDiscord(prompt);
188
+ assert.equal(embeds.length, 1);
189
+ assert.ok(embeds[0].footer?.text.includes("auto-mode-dispatch"), "footer should include context source");
190
+ });
191
+
192
+ test("formatForDiscord omits source from footer when context is absent", () => {
193
+ const prompt = {
194
+ id: "test-2",
195
+ channel: "discord" as const,
196
+ createdAt: Date.now(),
197
+ timeoutAt: Date.now() + 60000,
198
+ pollIntervalMs: 5000,
199
+ questions: [{
200
+ id: "q1",
201
+ header: "Choice",
202
+ question: "Pick one",
203
+ options: [
204
+ { label: "A", description: "Alpha" },
205
+ { label: "B", description: "Beta" },
206
+ ],
207
+ allowMultiple: false,
208
+ }],
209
+ };
210
+
211
+ const { embeds } = formatForDiscord(prompt);
212
+ assert.ok(!embeds[0].footer?.text.includes("Source:"), "footer should not include Source when context absent");
213
+ });
214
+
215
+ test("formatForDiscord multi-question footer includes question position", () => {
216
+ const prompt = {
217
+ id: "test-3",
218
+ channel: "discord" as const,
219
+ createdAt: Date.now(),
220
+ timeoutAt: Date.now() + 60000,
221
+ pollIntervalMs: 5000,
222
+ questions: [
223
+ {
224
+ id: "q1",
225
+ header: "First",
226
+ question: "Pick",
227
+ options: [{ label: "A", description: "a" }],
228
+ allowMultiple: false,
229
+ },
230
+ {
231
+ id: "q2",
232
+ header: "Second",
233
+ question: "Pick",
234
+ options: [{ label: "B", description: "b" }],
235
+ allowMultiple: false,
236
+ },
237
+ ],
238
+ };
239
+
240
+ const { embeds } = formatForDiscord(prompt);
241
+ assert.equal(embeds.length, 2);
242
+ assert.ok(embeds[0].footer?.text.includes("1/2"), "first embed footer should show 1/2");
243
+ assert.ok(embeds[1].footer?.text.includes("2/2"), "second embed footer should show 2/2");
244
+ });
245
+
246
+ test("formatForDiscord single-question generates reaction emojis", () => {
247
+ const prompt = {
248
+ id: "test-4",
249
+ channel: "discord" as const,
250
+ createdAt: Date.now(),
251
+ timeoutAt: Date.now() + 60000,
252
+ pollIntervalMs: 5000,
253
+ questions: [{
254
+ id: "q1",
255
+ header: "Pick",
256
+ question: "Choose",
257
+ options: [
258
+ { label: "A", description: "a" },
259
+ { label: "B", description: "b" },
260
+ { label: "C", description: "c" },
261
+ ],
262
+ allowMultiple: false,
263
+ }],
264
+ };
265
+
266
+ const { reactionEmojis } = formatForDiscord(prompt);
267
+ assert.equal(reactionEmojis.length, 3, "should generate 3 reaction emojis for 3 options");
268
+ assert.equal(reactionEmojis[0], "1️⃣");
269
+ assert.equal(reactionEmojis[1], "2️⃣");
270
+ assert.equal(reactionEmojis[2], "3️⃣");
271
+ });
272
+
273
+ test("formatForDiscord multi-question generates no reaction emojis", () => {
274
+ const prompt = {
275
+ id: "test-5",
276
+ channel: "discord" as const,
277
+ createdAt: Date.now(),
278
+ timeoutAt: Date.now() + 60000,
279
+ pollIntervalMs: 5000,
280
+ questions: [
281
+ {
282
+ id: "q1",
283
+ header: "First",
284
+ question: "Pick",
285
+ options: [{ label: "A", description: "a" }],
286
+ allowMultiple: false,
287
+ },
288
+ {
289
+ id: "q2",
290
+ header: "Second",
291
+ question: "Pick",
292
+ options: [{ label: "B", description: "b" }],
293
+ allowMultiple: false,
294
+ },
295
+ ],
296
+ };
297
+
298
+ const { reactionEmojis } = formatForDiscord(prompt);
299
+ assert.equal(reactionEmojis.length, 0, "multi-question should not generate reaction emojis");
300
+ });
301
+
302
+ test("parseDiscordResponse handles multi-question text reply via semicolons", () => {
303
+ const result = parseDiscordResponse([], "1;2", [
304
+ {
305
+ id: "first",
306
+ header: "First",
307
+ question: "Pick one",
308
+ allowMultiple: false,
309
+ options: [
310
+ { label: "Alpha", description: "A" },
311
+ { label: "Beta", description: "B" },
312
+ ],
313
+ },
314
+ {
315
+ id: "second",
316
+ header: "Second",
317
+ question: "Pick one",
318
+ allowMultiple: false,
319
+ options: [
320
+ { label: "Gamma", description: "G" },
321
+ { label: "Delta", description: "D" },
322
+ ],
323
+ },
324
+ ]);
325
+
326
+ assert.deepEqual(result.answers.first.answers, ["Alpha"]);
327
+ assert.deepEqual(result.answers.second.answers, ["Delta"]);
328
+ });
329
+
330
+ test("parseDiscordResponse handles multiple reactions for allowMultiple question", () => {
331
+ const result = parseDiscordResponse(
332
+ [{ emoji: "1️⃣", count: 1 }, { emoji: "3️⃣", count: 1 }],
333
+ null,
334
+ [{
335
+ id: "choice",
336
+ header: "Choice",
337
+ question: "Pick any",
338
+ allowMultiple: true,
339
+ options: [
340
+ { label: "Alpha", description: "A" },
341
+ { label: "Beta", description: "B" },
342
+ { label: "Gamma", description: "G" },
343
+ ],
344
+ }],
345
+ );
346
+
347
+ assert.deepEqual(result.answers.choice.answers, ["Alpha", "Gamma"]);
348
+ });
349
+
350
+ test("DiscordAdapter source-level: acknowledgeAnswer method exists", () => {
351
+ const adapterSrc = readFileSync(
352
+ join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
353
+ "utf-8",
354
+ );
355
+ assert.ok(adapterSrc.includes("async acknowledgeAnswer"), "should have acknowledgeAnswer method");
356
+ assert.ok(adapterSrc.includes("✅"), "should use checkmark emoji for acknowledgement");
357
+ });
358
+
359
+ test("DiscordAdapter source-level: resolves guild ID for message URLs", () => {
360
+ const adapterSrc = readFileSync(
361
+ join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
362
+ "utf-8",
363
+ );
364
+ assert.ok(adapterSrc.includes("guildId"), "should track guild ID");
365
+ assert.ok(adapterSrc.includes("guild_id"), "should read guild_id from channel info");
366
+ assert.ok(
367
+ adapterSrc.includes("discord.com/channels/"),
368
+ "should construct message URL with guild/channel/message format",
369
+ );
370
+ });
371
+
372
+ test("DiscordAdapter source-level: sendPrompt sets threadUrl in ref", () => {
373
+ const adapterSrc = readFileSync(
374
+ join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
375
+ "utf-8",
376
+ );
377
+ assert.ok(
378
+ adapterSrc.includes("threadUrl: messageUrl"),
379
+ "sendPrompt should set threadUrl to the constructed message URL",
380
+ );
381
+ });
@@ -1,87 +1,240 @@
1
- /**
2
- * Routing History — structural tests for adaptive learning module.
3
- *
4
- * Verifies routing-history.ts exports and structure from #579.
5
- * Uses source-level checks to avoid @gsd/pi-coding-agent import chain.
6
- */
7
-
8
1
  import test from "node:test";
9
2
  import assert from "node:assert/strict";
10
- import { readFileSync } from "node:fs";
11
- import { join, dirname } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const historySrc = readFileSync(join(__dirname, "..", "routing-history.ts"), "utf-8");
16
-
17
- // ═══════════════════════════════════════════════════════════════════════════
18
- // Module Exports
19
- // ═══════════════════════════════════════════════════════════════════════════
20
-
21
- test("routing-history: exports initRoutingHistory", () => {
22
- assert.ok(historySrc.includes("export function initRoutingHistory"), "should export initRoutingHistory");
3
+ import { mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import {
8
+ initRoutingHistory,
9
+ resetRoutingHistory,
10
+ recordOutcome,
11
+ recordFeedback,
12
+ getAdaptiveTierAdjustment,
13
+ clearRoutingHistory,
14
+ getRoutingHistory,
15
+ } from "../routing-history.js";
16
+
17
+ // ─── Test Setup ──────────────────────────────────────────────────────────────
18
+
19
+ function makeTmpDir(): string {
20
+ const dir = join(tmpdir(), `gsd-routing-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
21
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
22
+ return dir;
23
+ }
24
+
25
+ function cleanup(dir: string): void {
26
+ try { rmSync(dir, { recursive: true, force: true }); } catch {}
27
+ resetRoutingHistory();
28
+ }
29
+
30
+ // ─── recordOutcome ───────────────────────────────────────────────────────────
31
+
32
+ test("recordOutcome tracks success and failure counts", () => {
33
+ const dir = makeTmpDir();
34
+ try {
35
+ initRoutingHistory(dir);
36
+ recordOutcome("execute-task", "standard", true);
37
+ recordOutcome("execute-task", "standard", true);
38
+ recordOutcome("execute-task", "standard", false);
39
+
40
+ const history = getRoutingHistory();
41
+ assert.ok(history);
42
+ const pattern = history.patterns["execute-task"];
43
+ assert.ok(pattern);
44
+ assert.equal(pattern.standard.success, 2);
45
+ assert.equal(pattern.standard.fail, 1);
46
+ } finally {
47
+ cleanup(dir);
48
+ }
23
49
  });
24
50
 
25
- test("routing-history: exports recordOutcome", () => {
26
- assert.ok(historySrc.includes("export function recordOutcome"), "should export recordOutcome");
51
+ test("recordOutcome tracks tag-specific patterns", () => {
52
+ const dir = makeTmpDir();
53
+ try {
54
+ initRoutingHistory(dir);
55
+ recordOutcome("execute-task", "light", true, ["docs"]);
56
+
57
+ const history = getRoutingHistory();
58
+ assert.ok(history);
59
+ assert.ok(history.patterns["execute-task:docs"]);
60
+ assert.equal(history.patterns["execute-task:docs"].light.success, 1);
61
+ } finally {
62
+ cleanup(dir);
63
+ }
27
64
  });
28
65
 
29
- test("routing-history: exports recordFeedback", () => {
30
- assert.ok(historySrc.includes("export function recordFeedback"), "should export recordFeedback");
66
+ test("recordOutcome applies rolling window", () => {
67
+ const dir = makeTmpDir();
68
+ try {
69
+ initRoutingHistory(dir);
70
+ // Record 60 successes — should be capped to 50
71
+ for (let i = 0; i < 60; i++) {
72
+ recordOutcome("execute-task", "standard", true);
73
+ }
74
+
75
+ const history = getRoutingHistory();
76
+ assert.ok(history);
77
+ const total = history.patterns["execute-task"].standard.success +
78
+ history.patterns["execute-task"].standard.fail;
79
+ assert.ok(total <= 50, `total ${total} should be <= 50`);
80
+ } finally {
81
+ cleanup(dir);
82
+ }
31
83
  });
32
84
 
33
- test("routing-history: exports getAdaptiveTierAdjustment", () => {
34
- assert.ok(historySrc.includes("export function getAdaptiveTierAdjustment"), "should export getAdaptiveTierAdjustment");
85
+ // ─── getAdaptiveTierAdjustment ───────────────────────────────────────────────
86
+
87
+ test("no adjustment when insufficient data", () => {
88
+ const dir = makeTmpDir();
89
+ try {
90
+ initRoutingHistory(dir);
91
+ recordOutcome("execute-task", "light", false);
92
+ // Only 1 data point — not enough
93
+ const adj = getAdaptiveTierAdjustment("execute-task", "light");
94
+ assert.equal(adj, null);
95
+ } finally {
96
+ cleanup(dir);
97
+ }
35
98
  });
36
99
 
37
- test("routing-history: exports resetRoutingHistory", () => {
38
- assert.ok(historySrc.includes("export function resetRoutingHistory"), "should export resetRoutingHistory");
100
+ test("bumps tier when failure rate exceeds threshold", () => {
101
+ const dir = makeTmpDir();
102
+ try {
103
+ initRoutingHistory(dir);
104
+ // Record high failure rate at light tier
105
+ recordOutcome("execute-task", "light", false);
106
+ recordOutcome("execute-task", "light", false);
107
+ recordOutcome("execute-task", "light", true);
108
+ // 2/3 = 66% failure rate > 20% threshold
109
+
110
+ const adj = getAdaptiveTierAdjustment("execute-task", "light");
111
+ assert.equal(adj, "standard");
112
+ } finally {
113
+ cleanup(dir);
114
+ }
39
115
  });
40
116
 
41
- // ═══════════════════════════════════════════════════════════════════════════
42
- // Design Constants
43
- // ═══════════════════════════════════════════════════════════════════════════
44
-
45
- test("routing-history: uses rolling window of 50 entries", () => {
46
- assert.ok(historySrc.includes("ROLLING_WINDOW = 50"), "should use 50-entry rolling window");
117
+ test("no adjustment when success rate is high", () => {
118
+ const dir = makeTmpDir();
119
+ try {
120
+ initRoutingHistory(dir);
121
+ for (let i = 0; i < 10; i++) {
122
+ recordOutcome("execute-task", "light", true);
123
+ }
124
+ const adj = getAdaptiveTierAdjustment("execute-task", "light");
125
+ assert.equal(adj, null);
126
+ } finally {
127
+ cleanup(dir);
128
+ }
47
129
  });
48
130
 
49
- test("routing-history: failure threshold is 20%", () => {
50
- assert.ok(historySrc.includes("FAILURE_THRESHOLD = 0.20"), "should use 20% failure threshold");
131
+ test("tag-specific patterns take precedence", () => {
132
+ const dir = makeTmpDir();
133
+ try {
134
+ initRoutingHistory(dir);
135
+ // Base pattern has high success rate (tagged calls also count toward base)
136
+ for (let i = 0; i < 15; i++) {
137
+ recordOutcome("execute-task", "light", true);
138
+ }
139
+ // But docs-tagged tasks fail at light
140
+ recordOutcome("execute-task", "light", false, ["docs"]);
141
+ recordOutcome("execute-task", "light", false, ["docs"]);
142
+ recordOutcome("execute-task", "light", true, ["docs"]);
143
+
144
+ // With tags, should bump (docs pattern: 1/3 success = 66% failure)
145
+ const adj = getAdaptiveTierAdjustment("execute-task", "light", ["docs"]);
146
+ assert.equal(adj, "standard");
147
+
148
+ // Without tags, should not bump (base: 16/18 success = 11% failure)
149
+ const adjBase = getAdaptiveTierAdjustment("execute-task", "light");
150
+ assert.equal(adjBase, null);
151
+ } finally {
152
+ cleanup(dir);
153
+ }
51
154
  });
52
155
 
53
- test("routing-history: feedback weight is 2x", () => {
54
- assert.ok(historySrc.includes("FEEDBACK_WEIGHT = 2"), "feedback should count 2x");
156
+ // ─── recordFeedback ──────────────────────────────────────────────────────────
157
+
158
+ test("recordFeedback stores feedback entries", () => {
159
+ const dir = makeTmpDir();
160
+ try {
161
+ initRoutingHistory(dir);
162
+ recordFeedback("execute-task", "M001/S01/T01", "standard", "over");
163
+
164
+ const history = getRoutingHistory();
165
+ assert.ok(history);
166
+ assert.equal(history.feedback.length, 1);
167
+ assert.equal(history.feedback[0].rating, "over");
168
+ assert.equal(history.feedback[0].tier, "standard");
169
+ } finally {
170
+ cleanup(dir);
171
+ }
55
172
  });
56
173
 
57
- // ═══════════════════════════════════════════════════════════════════════════
58
- // Type Structure
59
- // ═══════════════════════════════════════════════════════════════════════════
60
-
61
- test("routing-history: imports ComplexityTier from types.ts", () => {
62
- assert.ok(
63
- historySrc.includes('from "./types.js"') && historySrc.includes("ComplexityTier"),
64
- "should import ComplexityTier from types.ts",
65
- );
66
- });
67
-
68
- test("routing-history: defines RoutingHistoryData interface", () => {
69
- assert.ok(historySrc.includes("interface RoutingHistoryData"), "should define RoutingHistoryData");
174
+ test("recordFeedback 'under' increases failure count at tier", () => {
175
+ const dir = makeTmpDir();
176
+ try {
177
+ initRoutingHistory(dir);
178
+ recordFeedback("execute-task", "M001/S01/T01", "light", "under");
179
+
180
+ const history = getRoutingHistory();
181
+ assert.ok(history);
182
+ // "under" adds 2 (FEEDBACK_WEIGHT) failures
183
+ assert.equal(history.patterns["execute-task"].light.fail, 2);
184
+ } finally {
185
+ cleanup(dir);
186
+ }
70
187
  });
71
188
 
72
- test("routing-history: defines FeedbackEntry interface", () => {
73
- assert.ok(historySrc.includes("interface FeedbackEntry"), "should define FeedbackEntry");
189
+ test("recordFeedback 'over' increases success count at lower tier", () => {
190
+ const dir = makeTmpDir();
191
+ try {
192
+ initRoutingHistory(dir);
193
+ recordFeedback("execute-task", "M001/S01/T01", "standard", "over");
194
+
195
+ const history = getRoutingHistory();
196
+ assert.ok(history);
197
+ // "over" at standard → adds 2 successes at light
198
+ assert.equal(history.patterns["execute-task"].light.success, 2);
199
+ } finally {
200
+ cleanup(dir);
201
+ }
74
202
  });
75
203
 
76
- // ═══════════════════════════════════════════════════════════════════════════
77
- // Persistence
78
- // ═══════════════════════════════════════════════════════════════════════════
79
-
80
- test("routing-history: persists to routing-history.json", () => {
81
- assert.ok(historySrc.includes("routing-history.json"), "should persist to routing-history.json");
204
+ // ─── clearRoutingHistory ─────────────────────────────────────────────────────
205
+
206
+ test("clearRoutingHistory resets all data", () => {
207
+ const dir = makeTmpDir();
208
+ try {
209
+ initRoutingHistory(dir);
210
+ recordOutcome("execute-task", "light", true);
211
+ clearRoutingHistory(dir);
212
+
213
+ const history = getRoutingHistory();
214
+ assert.ok(history);
215
+ assert.deepEqual(history.patterns, {});
216
+ assert.deepEqual(history.feedback, []);
217
+ } finally {
218
+ cleanup(dir);
219
+ }
82
220
  });
83
221
 
84
- test("routing-history: has save and load functions", () => {
85
- assert.ok(historySrc.includes("saveHistory") || historySrc.includes("function save"), "should have save");
86
- assert.ok(historySrc.includes("loadHistory") || historySrc.includes("function load"), "should have load");
222
+ // ─── Persistence ─────────────────────────────────────────────────────────────
223
+
224
+ test("routing history persists to disk and reloads", () => {
225
+ const dir = makeTmpDir();
226
+ try {
227
+ initRoutingHistory(dir);
228
+ recordOutcome("execute-task", "standard", true);
229
+ recordOutcome("execute-task", "standard", true);
230
+ resetRoutingHistory();
231
+
232
+ // Reload from disk
233
+ initRoutingHistory(dir);
234
+ const history = getRoutingHistory();
235
+ assert.ok(history);
236
+ assert.equal(history.patterns["execute-task"].standard.success, 2);
237
+ } finally {
238
+ cleanup(dir);
239
+ }
87
240
  });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * stale-worktree-cwd.test.ts — Tests for #608 fix.
3
+ *
4
+ * Verifies that when process.cwd() is inside a stale .gsd/worktrees/ path,
5
+ * startAuto escapes back to the project root before proceeding.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdtempSync, mkdirSync, rmSync, existsSync, realpathSync, writeFileSync } from "node:fs";
11
+ import { join, sep } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ import { execSync } from "node:child_process";
14
+
15
+ import {
16
+ createAutoWorktree,
17
+ teardownAutoWorktree,
18
+ mergeMilestoneToMain,
19
+ } from "../auto-worktree.ts";
20
+
21
+ function run(command: string, cwd: string): string {
22
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
23
+ }
24
+
25
+ function createTempRepo(): string {
26
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "stale-wt-test-")));
27
+ run("git init", dir);
28
+ run("git config user.email test@test.com", dir);
29
+ run("git config user.name Test", dir);
30
+ writeFileSync(join(dir, "README.md"), "# test\n");
31
+ run("git add .", dir);
32
+ run("git commit -m init", dir);
33
+ run("git branch -M main", dir);
34
+ return dir;
35
+ }
36
+
37
+ // ─── escapeStaleWorktree is called by startAuto, test the detection logic ────
38
+
39
+ test("detects stale worktree path and extracts project root", () => {
40
+ // Simulate the path pattern: /project/.gsd/worktrees/M004/...
41
+ const projectRoot = "/Users/test/myproject";
42
+ const stalePath = `${projectRoot}${sep}.gsd${sep}worktrees${sep}M004`;
43
+
44
+ const marker = `${sep}.gsd${sep}worktrees${sep}`;
45
+ const idx = stalePath.indexOf(marker);
46
+
47
+ assert.ok(idx !== -1, "marker found in stale path");
48
+ assert.equal(stalePath.slice(0, idx), projectRoot, "project root extracted correctly");
49
+ });
50
+
51
+ test("does not trigger on normal project path", () => {
52
+ const normalPath = "/Users/test/myproject";
53
+ const marker = `${sep}.gsd${sep}worktrees${sep}`;
54
+ const idx = normalPath.indexOf(marker);
55
+
56
+ assert.equal(idx, -1, "marker not found in normal path");
57
+ });
58
+
59
+ // ─── Integration: mergeMilestoneToMain restores cwd ─────────────────────────
60
+
61
+ test("mergeMilestoneToMain restores cwd to project root", () => {
62
+ const savedCwd = process.cwd();
63
+ let tempDir = "";
64
+
65
+ try {
66
+ tempDir = createTempRepo();
67
+
68
+ // Create milestone planning artifacts
69
+ const msDir = join(tempDir, ".gsd", "milestones", "M050");
70
+ mkdirSync(msDir, { recursive: true });
71
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M050 Context\n");
72
+ const roadmap = [
73
+ "# M050: Test Milestone",
74
+ "**Vision**: testing",
75
+ "## Success Criteria",
76
+ "- It works",
77
+ "## Slices",
78
+ "- [x] S01 — First slice",
79
+ ].join("\n");
80
+ writeFileSync(join(msDir, "ROADMAP.md"), roadmap);
81
+ run("git add .", tempDir);
82
+ run("git commit -m \"add milestone\"", tempDir);
83
+
84
+ // Create auto-worktree (enters the worktree dir)
85
+ const wtPath = createAutoWorktree(tempDir, "M050");
86
+ assert.equal(process.cwd(), wtPath, "cwd is in worktree after create");
87
+
88
+ // Add a change in the worktree
89
+ writeFileSync(join(wtPath, "feature.txt"), "new feature\n");
90
+ run("git add .", wtPath);
91
+ run("git commit -m \"feat: add feature\"", wtPath);
92
+
93
+ // Merge back — should restore cwd to tempDir
94
+ mergeMilestoneToMain(tempDir, "M050", roadmap);
95
+
96
+ assert.equal(process.cwd(), tempDir, "cwd restored to project root after merge");
97
+ assert.ok(!existsSync(wtPath), "worktree directory removed after merge");
98
+ } finally {
99
+ process.chdir(savedCwd);
100
+ if (tempDir && existsSync(tempDir)) {
101
+ rmSync(tempDir, { recursive: true, force: true });
102
+ }
103
+ }
104
+ });
105
+
106
+ // ─── Integration: stale worktree directory is detectable ────────────────────
107
+
108
+ test("process.cwd() inside removed worktree is recoverable", () => {
109
+ const savedCwd = process.cwd();
110
+ let tempDir = "";
111
+
112
+ try {
113
+ tempDir = createTempRepo();
114
+
115
+ // Create a .gsd/worktrees/M099 directory to simulate stale state
116
+ const staleWtDir = join(tempDir, ".gsd", "worktrees", "M099");
117
+ mkdirSync(staleWtDir, { recursive: true });
118
+
119
+ // Enter the stale directory
120
+ process.chdir(staleWtDir);
121
+ const cwdBefore = process.cwd();
122
+ assert.ok(cwdBefore.includes(`${sep}.gsd${sep}worktrees${sep}`), "cwd is inside worktree dir");
123
+
124
+ // Simulate escapeStaleWorktree logic
125
+ const marker = `${sep}.gsd${sep}worktrees${sep}`;
126
+ const idx = cwdBefore.indexOf(marker);
127
+ assert.ok(idx !== -1, "marker found");
128
+
129
+ const projectRoot = cwdBefore.slice(0, idx);
130
+ process.chdir(projectRoot);
131
+
132
+ assert.equal(process.cwd(), tempDir, "successfully escaped to project root");
133
+ } finally {
134
+ process.chdir(savedCwd);
135
+ if (tempDir && existsSync(tempDir)) {
136
+ rmSync(tempDir, { recursive: true, force: true });
137
+ }
138
+ }
139
+ });