gsd-pi 2.8.2 → 2.9.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 (193) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +5 -0
  3. package/dist/loader.js +1 -1
  4. package/dist/update-check.d.ts +24 -0
  5. package/dist/update-check.js +93 -0
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  33. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  34. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  35. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  36. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  37. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  38. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  39. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  41. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  43. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  44. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  45. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  46. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
  47. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  48. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  49. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  50. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
  51. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  52. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  53. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  54. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  55. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  56. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  57. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  58. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
  59. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  60. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  61. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  62. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
  63. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  64. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  65. package/node_modules/@gsd/pi-coding-agent/src/core/extensions/types.ts +4 -2
  66. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
  67. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
  68. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  69. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  70. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  71. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
  72. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  73. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  74. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  75. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
  76. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  77. package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
  78. package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
  79. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  80. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
  81. package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
  82. package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
  83. package/package.json +1 -1
  84. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
  85. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  88. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
  90. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
  94. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  96. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  98. package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  100. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  102. package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  104. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
  106. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  110. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  112. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  114. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  116. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  117. package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
  118. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  121. package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  123. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  125. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  127. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
  129. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  139. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
  141. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  143. package/packages/pi-coding-agent/src/core/extensions/types.ts +4 -2
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
  145. package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
  146. package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  147. package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  148. package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  149. package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
  150. package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  151. package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  152. package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  153. package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
  154. package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  155. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  156. package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
  157. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  158. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
  159. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
  160. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
  161. package/src/resources/extensions/ask-user-questions.ts +42 -2
  162. package/src/resources/extensions/bg-shell/index.ts +34 -37
  163. package/src/resources/extensions/browser-tools/core.d.ts +205 -0
  164. package/src/resources/extensions/browser-tools/index.ts +2 -2
  165. package/src/resources/extensions/browser-tools/refs.ts +1 -1
  166. package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
  167. package/src/resources/extensions/context7/index.ts +2 -2
  168. package/src/resources/extensions/get-secrets-from-user.ts +3 -2
  169. package/src/resources/extensions/google-search/index.ts +1 -1
  170. package/src/resources/extensions/gsd/auto.ts +126 -12
  171. package/src/resources/extensions/gsd/commands.ts +218 -3
  172. package/src/resources/extensions/gsd/doctor.ts +1 -1
  173. package/src/resources/extensions/gsd/git-service.ts +163 -13
  174. package/src/resources/extensions/gsd/guided-flow.ts +19 -9
  175. package/src/resources/extensions/gsd/index.ts +17 -7
  176. package/src/resources/extensions/gsd/preferences.ts +1 -1
  177. package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
  178. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
  179. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
  180. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
  181. package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
  182. package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
  183. package/src/resources/extensions/gsd/types.ts +1 -0
  184. package/src/resources/extensions/gsd/worktree.ts +20 -1
  185. package/src/resources/extensions/mac-tools/index.ts +1 -1
  186. package/src/resources/extensions/search-the-web/command-search-provider.ts +1 -1
  187. package/src/resources/extensions/search-the-web/format.ts +1 -1
  188. package/src/resources/extensions/search-the-web/index.ts +5 -5
  189. package/src/resources/extensions/search-the-web/native-search.ts +5 -6
  190. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
  191. package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
  192. package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
  193. package/src/resources/extensions/shared/interview-ui.ts +2 -2
@@ -5,17 +5,21 @@ import { execSync } from "node:child_process";
5
5
 
6
6
  import {
7
7
  autoCommitCurrentBranch,
8
+ captureIntegrationBranch,
8
9
  detectWorktreeName,
9
10
  ensureSliceBranch,
10
11
  getActiveSliceBranch,
11
12
  getCurrentBranch,
13
+ getMainBranch,
12
14
  getSliceBranchName,
13
15
  isOnSliceBranch,
14
16
  mergeSliceToMain,
15
17
  parseSliceBranch,
18
+ setActiveMilestoneId,
16
19
  SLICE_BRANCH_RE,
17
20
  switchToMain,
18
21
  } from "../worktree.ts";
22
+ import { readIntegrationBranch } from "../git-service.ts";
19
23
  import { deriveState } from "../state.ts";
20
24
  import { indexWorkspace } from "../workspace-index.ts";
21
25
 
@@ -252,6 +256,354 @@ async function main(): Promise<void> {
252
256
 
253
257
  rmSync(base3, { recursive: true, force: true });
254
258
 
259
+ // ═══════════════════════════════════════════════════════════════════════
260
+ // Integration branch — facade-level tests
261
+ //
262
+ // These exercise the same codepath auto.ts uses:
263
+ // captureIntegrationBranch() → setActiveMilestoneId() → getMainBranch()
264
+ // → switchToMain() → mergeSliceToMain()
265
+ // ═══════════════════════════════════════════════════════════════════════
266
+
267
+ // ── captureIntegrationBranch on a feature branch ──────────────────────
268
+
269
+ console.log("\n=== captureIntegrationBranch: records current branch ===");
270
+
271
+ {
272
+ const repo = mkdtempSync(join(tmpdir(), "gsd-integ-facade-"));
273
+ run("git init -b main", repo);
274
+ run("git config user.name 'Pi Test'", repo);
275
+ run("git config user.email 'pi@example.com'", repo);
276
+ writeFileSync(join(repo, "README.md"), "init\n");
277
+ run("git add -A && git commit -m init", repo);
278
+
279
+ run("git checkout -b f-123-thing", repo);
280
+ assertEq(getCurrentBranch(repo), "f-123-thing", "on feature branch");
281
+
282
+ captureIntegrationBranch(repo, "M001");
283
+ assertEq(readIntegrationBranch(repo, "M001"), "f-123-thing",
284
+ "captureIntegrationBranch records the current branch");
285
+
286
+ // Verify it was committed (not just written to disk)
287
+ const logOut = run("git log --oneline -1", repo);
288
+ assert(logOut.includes("integration branch"), "metadata committed to git");
289
+
290
+ rmSync(repo, { recursive: true, force: true });
291
+ }
292
+
293
+ // ── captureIntegrationBranch is idempotent on same lineage ──────────
294
+
295
+ console.log("\n=== captureIntegrationBranch: idempotent ===");
296
+
297
+ {
298
+ const repo = mkdtempSync(join(tmpdir(), "gsd-integ-idem-"));
299
+ run("git init -b main", repo);
300
+ run("git config user.name 'Pi Test'", repo);
301
+ run("git config user.email 'pi@example.com'", repo);
302
+ writeFileSync(join(repo, "README.md"), "init\n");
303
+ run("git add -A && git commit -m init", repo);
304
+ run("git checkout -b f-first", repo);
305
+
306
+ captureIntegrationBranch(repo, "M001");
307
+ setActiveMilestoneId(repo, "M001");
308
+ assertEq(readIntegrationBranch(repo, "M001"), "f-first",
309
+ "first capture records f-first");
310
+
311
+ // Capture again on the same branch (simulates restart/resume) — should NOT overwrite
312
+ captureIntegrationBranch(repo, "M001");
313
+ assertEq(readIntegrationBranch(repo, "M001"), "f-first",
314
+ "second capture on same branch does not overwrite");
315
+
316
+ // After creating a slice branch (which inherits the metadata commit),
317
+ // capture should still be idempotent
318
+ ensureSliceBranch(repo, "M001", "S01");
319
+ // Now on gsd/M001/S01 — capture should be no-op (slice branch rejected)
320
+ captureIntegrationBranch(repo, "M001");
321
+ switchToMain(repo);
322
+ assertEq(readIntegrationBranch(repo, "M001"), "f-first",
323
+ "capture from slice branch is no-op, original preserved");
324
+ assertEq(getCurrentBranch(repo), "f-first",
325
+ "switchToMain returns to feature branch, confirming integration branch works");
326
+
327
+ rmSync(repo, { recursive: true, force: true });
328
+ }
329
+
330
+ // ── captureIntegrationBranch skips slice branches ─────────────────────
331
+
332
+ console.log("\n=== captureIntegrationBranch: skips slice branches ===");
333
+
334
+ {
335
+ const repo = mkdtempSync(join(tmpdir(), "gsd-integ-skip-"));
336
+ run("git init -b main", repo);
337
+ run("git config user.name 'Pi Test'", repo);
338
+ run("git config user.email 'pi@example.com'", repo);
339
+ writeFileSync(join(repo, "README.md"), "init\n");
340
+ run("git add -A && git commit -m init", repo);
341
+
342
+ run("git checkout -b gsd/M001/S01", repo);
343
+ captureIntegrationBranch(repo, "M001");
344
+
345
+ assertEq(readIntegrationBranch(repo, "M001"), null,
346
+ "capture from slice branch is a no-op");
347
+
348
+ rmSync(repo, { recursive: true, force: true });
349
+ }
350
+
351
+ // ── setActiveMilestoneId makes getMainBranch return integration branch ─
352
+
353
+ console.log("\n=== setActiveMilestoneId + getMainBranch ===");
354
+
355
+ {
356
+ const repo = mkdtempSync(join(tmpdir(), "gsd-integ-main-"));
357
+ run("git init -b main", repo);
358
+ run("git config user.name 'Pi Test'", repo);
359
+ run("git config user.email 'pi@example.com'", repo);
360
+ writeFileSync(join(repo, "README.md"), "init\n");
361
+ run("git add -A && git commit -m init", repo);
362
+
363
+ run("git checkout -b my-feature", repo);
364
+ captureIntegrationBranch(repo, "M001");
365
+
366
+ // Without milestone set, getMainBranch returns "main"
367
+ setActiveMilestoneId(repo, null);
368
+ assertEq(getMainBranch(repo), "main",
369
+ "getMainBranch returns main without milestone set");
370
+
371
+ // With milestone set, getMainBranch returns feature branch
372
+ setActiveMilestoneId(repo, "M001");
373
+ assertEq(getMainBranch(repo), "my-feature",
374
+ "getMainBranch returns integration branch with milestone set");
375
+
376
+ rmSync(repo, { recursive: true, force: true });
377
+ }
378
+
379
+ // ── Full multi-slice lifecycle on a feature branch ────────────────────
380
+ //
381
+ // Simulates what auto.ts does: start on feature branch, capture it,
382
+ // create S01, work, merge S01 back to feature branch, then S02 branches
383
+ // from feature branch (not main), works, merges to feature branch.
384
+ // Main stays untouched throughout.
385
+
386
+ console.log("\n=== Multi-slice lifecycle on feature branch ===");
387
+
388
+ {
389
+ const repo = mkdtempSync(join(tmpdir(), "gsd-integ-multi-"));
390
+ run("git init -b main", repo);
391
+ run("git config user.name 'Pi Test'", repo);
392
+ run("git config user.email 'pi@example.com'", repo);
393
+ writeFileSync(join(repo, "README.md"), "base\n");
394
+ run("git add -A && git commit -m init", repo);
395
+
396
+ // User creates feature branch
397
+ run("git checkout -b feature/big-change", repo);
398
+ writeFileSync(join(repo, "setup.txt"), "feature setup\n");
399
+ run("git add -A && git commit -m 'feat: initial setup'", repo);
400
+
401
+ // auto.ts startup: capture + set milestone
402
+ captureIntegrationBranch(repo, "M001");
403
+ setActiveMilestoneId(repo, "M001");
404
+
405
+ assertEq(getMainBranch(repo), "feature/big-change",
406
+ "multi: getMainBranch returns feature branch");
407
+
408
+ // ── S01 lifecycle ──────────────────────────────────────────────────
409
+ ensureSliceBranch(repo, "M001", "S01");
410
+ assertEq(getCurrentBranch(repo), "gsd/M001/S01", "multi: on S01");
411
+
412
+ // Verify S01 has feature branch content
413
+ assert(existsSync(join(repo, "setup.txt")),
414
+ "multi: S01 inherited feature branch content");
415
+
416
+ writeFileSync(join(repo, "s01-work.txt"), "s01 output\n");
417
+ run("git add -A && git commit -m 'feat(S01): work'", repo);
418
+
419
+ switchToMain(repo);
420
+ assertEq(getCurrentBranch(repo), "feature/big-change",
421
+ "multi: switchToMain goes to feature branch");
422
+
423
+ const s01merge = mergeSliceToMain(repo, "M001", "S01", "First slice");
424
+ assertEq(getCurrentBranch(repo), "feature/big-change",
425
+ "multi: after S01 merge, on feature branch");
426
+ assert(existsSync(join(repo, "s01-work.txt")),
427
+ "multi: S01 work merged to feature branch");
428
+ assert(s01merge.deletedBranch, "multi: S01 branch deleted");
429
+
430
+ // Main should NOT have S01 work
431
+ run("git stash", repo); // stash any .gsd changes
432
+ run("git checkout main", repo);
433
+ assert(!existsSync(join(repo, "s01-work.txt")),
434
+ "multi: main does NOT have S01 work");
435
+ run("git checkout feature/big-change", repo);
436
+ run("git stash pop || true", repo);
437
+
438
+ // ── S02 lifecycle ──────────────────────────────────────────────────
439
+ // S02 should branch from feature/big-change which now has S01's work
440
+ ensureSliceBranch(repo, "M001", "S02");
441
+ assertEq(getCurrentBranch(repo), "gsd/M001/S02", "multi: on S02");
442
+
443
+ // S02 should have S01's merged output (branched from feature branch)
444
+ assert(existsSync(join(repo, "s01-work.txt")),
445
+ "multi: S02 has S01 output (inherited via feature branch)");
446
+
447
+ writeFileSync(join(repo, "s02-work.txt"), "s02 output\n");
448
+ run("git add -A && git commit -m 'feat(S02): work'", repo);
449
+
450
+ switchToMain(repo);
451
+ assertEq(getCurrentBranch(repo), "feature/big-change",
452
+ "multi: switchToMain goes to feature branch after S02");
453
+
454
+ const s02merge = mergeSliceToMain(repo, "M001", "S02", "Second slice");
455
+ assertEq(getCurrentBranch(repo), "feature/big-change",
456
+ "multi: after S02 merge, on feature branch");
457
+ assert(existsSync(join(repo, "s02-work.txt")),
458
+ "multi: S02 work merged to feature branch");
459
+ assert(existsSync(join(repo, "s01-work.txt")),
460
+ "multi: S01 work still on feature branch after S02 merge");
461
+ assert(s02merge.deletedBranch, "multi: S02 branch deleted");
462
+
463
+ // Final check: main still untouched
464
+ run("git stash", repo);
465
+ run("git checkout main", repo);
466
+ assert(!existsSync(join(repo, "s01-work.txt")),
467
+ "multi: main still lacks S01 work at end");
468
+ assert(!existsSync(join(repo, "s02-work.txt")),
469
+ "multi: main still lacks S02 work at end");
470
+ assertEq(readFileSync(join(repo, "README.md"), "utf-8").trim(), "base",
471
+ "multi: main README unchanged");
472
+
473
+ rmSync(repo, { recursive: true, force: true });
474
+ }
475
+
476
+ // ── Resume scenario: milestone ID re-set after restart ────────────────
477
+ //
478
+ // Simulates crash + restart: the cached GitServiceImpl is lost, but the
479
+ // metadata file persists on disk. Re-calling setActiveMilestoneId should
480
+ // restore integration branch resolution.
481
+
482
+ console.log("\n=== Resume: milestone ID re-set restores integration branch ===");
483
+
484
+ {
485
+ const repo = mkdtempSync(join(tmpdir(), "gsd-integ-resume-"));
486
+ run("git init -b main", repo);
487
+ run("git config user.name 'Pi Test'", repo);
488
+ run("git config user.email 'pi@example.com'", repo);
489
+ writeFileSync(join(repo, "README.md"), "init\n");
490
+ run("git add -A && git commit -m init", repo);
491
+
492
+ run("git checkout -b my-feature", repo);
493
+ captureIntegrationBranch(repo, "M001");
494
+ setActiveMilestoneId(repo, "M001");
495
+
496
+ // Create a slice and do some work
497
+ ensureSliceBranch(repo, "M001", "S01");
498
+ writeFileSync(join(repo, "work.txt"), "wip\n");
499
+ run("git add -A && git commit -m 'wip'", repo);
500
+
501
+ // Simulate "restart" — clear milestone ID (fresh service instance)
502
+ setActiveMilestoneId(repo, null);
503
+ assertEq(getMainBranch(repo), "main",
504
+ "resume: getMainBranch returns main when milestone cleared");
505
+
506
+ // Re-set milestone ID (what auto.ts does on resume)
507
+ setActiveMilestoneId(repo, "M001");
508
+ assertEq(getMainBranch(repo), "my-feature",
509
+ "resume: getMainBranch returns feature branch after re-set");
510
+
511
+ // Full lifecycle still works after resume
512
+ switchToMain(repo);
513
+ assertEq(getCurrentBranch(repo), "my-feature",
514
+ "resume: switchToMain goes to feature branch after re-set");
515
+
516
+ const result = mergeSliceToMain(repo, "M001", "S01", "Resume slice");
517
+ assertEq(getCurrentBranch(repo), "my-feature",
518
+ "resume: merge lands on feature branch after re-set");
519
+ assert(existsSync(join(repo, "work.txt")),
520
+ "resume: merged work exists on feature branch");
521
+
522
+ rmSync(repo, { recursive: true, force: true });
523
+ }
524
+
525
+ // ── Backward compat: no metadata file, plain main workflow ────────────
526
+ //
527
+ // Simulates existing projects that were created before this feature.
528
+ // No metadata file exists, milestone ID is set — getMainBranch should
529
+ // still return "main" and the entire slice lifecycle works unchanged.
530
+
531
+ console.log("\n=== Backward compat: no metadata, main workflow ===");
532
+
533
+ {
534
+ const repo = mkdtempSync(join(tmpdir(), "gsd-integ-compat-"));
535
+ run("git init -b main", repo);
536
+ run("git config user.name 'Pi Test'", repo);
537
+ run("git config user.email 'pi@example.com'", repo);
538
+ writeFileSync(join(repo, "README.md"), "init\n");
539
+ run("git add -A && git commit -m init", repo);
540
+
541
+ // Set milestone but DON'T capture integration branch (simulates old project)
542
+ setActiveMilestoneId(repo, "M001");
543
+
544
+ assertEq(getMainBranch(repo), "main",
545
+ "compat: getMainBranch returns main without metadata");
546
+
547
+ // Full lifecycle on main still works
548
+ ensureSliceBranch(repo, "M001", "S01");
549
+ writeFileSync(join(repo, "feature.txt"), "new\n");
550
+ run("git add -A && git commit -m 'feat: work'", repo);
551
+
552
+ switchToMain(repo);
553
+ assertEq(getCurrentBranch(repo), "main",
554
+ "compat: switchToMain goes to main");
555
+
556
+ const result = mergeSliceToMain(repo, "M001", "S01", "Compat slice");
557
+ assertEq(getCurrentBranch(repo), "main",
558
+ "compat: merge lands on main");
559
+ assert(existsSync(join(repo, "feature.txt")),
560
+ "compat: merged work exists on main");
561
+ assert(result.deletedBranch, "compat: branch deleted");
562
+
563
+ rmSync(repo, { recursive: true, force: true });
564
+ }
565
+
566
+ // ── ensureSliceBranch from another slice with integration branch ──────
567
+ //
568
+ // When on gsd/M001/S01 and creating S02, the code falls back to
569
+ // getMainBranch() (not the current slice). With integration branch set,
570
+ // S02 should branch from the feature branch.
571
+
572
+ console.log("\n=== ensureSliceBranch: S02 from S01 uses integration branch as base ===");
573
+
574
+ {
575
+ const repo = mkdtempSync(join(tmpdir(), "gsd-integ-chain-"));
576
+ run("git init -b main", repo);
577
+ run("git config user.name 'Pi Test'", repo);
578
+ run("git config user.email 'pi@example.com'", repo);
579
+ writeFileSync(join(repo, "README.md"), "init\n");
580
+ run("git add -A && git commit -m init", repo);
581
+
582
+ run("git checkout -b dev-branch", repo);
583
+ writeFileSync(join(repo, "dev-only.txt"), "from dev\n");
584
+ run("git add -A && git commit -m 'dev setup'", repo);
585
+
586
+ captureIntegrationBranch(repo, "M001");
587
+ setActiveMilestoneId(repo, "M001");
588
+
589
+ // Create S01 (from dev-branch)
590
+ ensureSliceBranch(repo, "M001", "S01");
591
+ writeFileSync(join(repo, "s01.txt"), "s01\n");
592
+ run("git add -A && git commit -m 's01 work'", repo);
593
+
594
+ // While on S01, create S02 — should fall back to integration branch
595
+ ensureSliceBranch(repo, "M001", "S02");
596
+ assertEq(getCurrentBranch(repo), "gsd/M001/S02", "chain: on S02");
597
+
598
+ // S02 should be based on dev-branch (the integration branch)
599
+ assert(existsSync(join(repo, "dev-only.txt")),
600
+ "chain: S02 has dev-branch content");
601
+ assert(!existsSync(join(repo, "s01.txt")),
602
+ "chain: S02 does NOT have S01 content (not chained from S01)");
603
+
604
+ rmSync(repo, { recursive: true, force: true });
605
+ }
606
+
255
607
  rmSync(base, { recursive: true, force: true });
256
608
  console.log(`\nResults: ${passed} passed, ${failed} failed`);
257
609
  if (failed > 0) process.exit(1);
@@ -176,6 +176,7 @@ export interface GSDState {
176
176
  blockers: string[];
177
177
  nextAction: string;
178
178
  activeBranch?: string;
179
+ activeWorkspace?: string;
179
180
  registry: MilestoneRegistryEntry[];
180
181
  requirements?: RequirementCounts;
181
182
  progress?: {
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { sep } from "node:path";
19
19
 
20
- import { GitServiceImpl } from "./git-service.ts";
20
+ import { GitServiceImpl, writeIntegrationBranch } from "./git-service.ts";
21
21
  import { loadEffectiveGSDPreferences } from "./preferences.ts";
22
22
 
23
23
  // Re-export MergeSliceResult from the canonical source (D014 — type-only re-export)
@@ -43,6 +43,25 @@ function getService(basePath: string): GitServiceImpl {
43
43
  return cachedService;
44
44
  }
45
45
 
46
+ /**
47
+ * Set the active milestone ID on the cached GitServiceImpl.
48
+ * This enables integration branch resolution in getMainBranch().
49
+ */
50
+ export function setActiveMilestoneId(basePath: string, milestoneId: string | null): void {
51
+ getService(basePath).setMilestoneId(milestoneId);
52
+ }
53
+
54
+ /**
55
+ * Record the current branch as the integration branch for a milestone.
56
+ * Called once when auto-mode starts — captures where slice branches should
57
+ * merge back to. No-op if already recorded or if on a GSD slice branch.
58
+ */
59
+ export function captureIntegrationBranch(basePath: string, milestoneId: string): void {
60
+ const svc = getService(basePath);
61
+ const current = svc.getCurrentBranch();
62
+ writeIntegrationBranch(basePath, milestoneId, current);
63
+ }
64
+
46
65
  // ─── Pure Utility Functions (unchanged) ────────────────────────────────────
47
66
 
48
67
  /**
@@ -123,7 +123,7 @@ function execMacAgent(command: string, params?: Record<string, any>): MacAgentRe
123
123
  stdio: ["pipe", "pipe", "pipe"],
124
124
  maxBuffer: 5 * 1024 * 1024, // 5MB — needed for retina screenshot base64 payloads
125
125
  });
126
- stdout = typeof result === "string" ? result : result.toString();
126
+ stdout = typeof result === "string" ? result : String(result);
127
127
  } catch (err: any) {
128
128
  stderr = err.stderr?.toString() || "";
129
129
  const isTimeout = err.killed || err.signal === "SIGTERM";
@@ -81,7 +81,7 @@ export function registerSearchProviderCommand(pi: ExtensionAPI): void {
81
81
  return
82
82
  }
83
83
 
84
- chosen = parseSelectChoice(result)
84
+ chosen = parseSelectChoice(Array.isArray(result) ? result[0] : result)
85
85
  }
86
86
 
87
87
  setSearchProviderPreference(chosen)
@@ -3,7 +3,7 @@
3
3
  * and LLM context responses.
4
4
  */
5
5
 
6
- import { extractDomain } from "./url-utils";
6
+ import { extractDomain } from "./url-utils.js";
7
7
 
8
8
  export interface SearchResultFormatted {
9
9
  title: string;
@@ -45,11 +45,11 @@
45
45
  */
46
46
 
47
47
  import type { ExtensionAPI } from "@gsd/pi-coding-agent";
48
- import { registerSearchTool } from "./tool-search";
49
- import { registerFetchPageTool } from "./tool-fetch-page";
50
- import { registerLLMContextTool } from "./tool-llm-context";
51
- import { registerSearchProviderCommand } from "./command-search-provider.ts";
52
- import { registerNativeSearchHooks } from "./native-search";
48
+ import { registerSearchTool } from "./tool-search.js";
49
+ import { registerFetchPageTool } from "./tool-fetch-page.js";
50
+ import { registerLLMContextTool } from "./tool-llm-context.js";
51
+ import { registerSearchProviderCommand } from "./command-search-provider.js";
52
+ import { registerNativeSearchHooks } from "./native-search.js";
53
53
 
54
54
  export default function (pi: ExtensionAPI) {
55
55
  registerSearchTool(pi);
@@ -99,12 +99,11 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic:
99
99
  const payload = event.payload as Record<string, unknown>;
100
100
  if (!payload) return;
101
101
 
102
- // Detect Anthropic by model name prefix (works even before model_select fires)
103
- const model = payload.model as string | undefined;
104
- if (!model || !model.startsWith("claude")) return;
105
-
106
- // Keep provider tracking in sync
107
- isAnthropicProvider = true;
102
+ // Only inject native web search for confirmed Anthropic provider.
103
+ // model_select sets isAnthropicProvider via the provider field.
104
+ // Model name prefix is NOT sufficient — other providers (GitHub Copilot,
105
+ // AWS Bedrock, etc.) serve claude-* models through non-Anthropic APIs.
106
+ if (!isAnthropicProvider) return;
108
107
 
109
108
  // Strip thinking blocks from history to avoid signature validation errors
110
109
  // caused by the SDK dropping server_tool_use/web_search_tool_result blocks.
@@ -13,10 +13,10 @@ import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@gsd/pi-codi
13
13
  import { Text } from "@gsd/pi-tui";
14
14
  import { Type } from "@sinclair/typebox";
15
15
 
16
- import { LRUTTLCache } from "./cache";
17
- import { fetchSimple, HttpError } from "./http";
18
- import { extractDomain } from "./url-utils";
19
- import { formatPageContent, type FormatPageOptions } from "./format";
16
+ import { LRUTTLCache } from "./cache.js";
17
+ import { fetchSimple, HttpError } from "./http.js";
18
+ import { extractDomain } from "./url-utils.js";
19
+ import { formatPageContent, type FormatPageOptions } from "./format.js";
20
20
 
21
21
  // =============================================================================
22
22
  // Cache
@@ -336,7 +336,7 @@ export function registerFetchPageTool(pi: ExtensionAPI) {
336
336
 
337
337
  async execute(toolCallId, params, signal, onUpdate, ctx) {
338
338
  if (signal?.aborted) {
339
- return { content: [{ type: "text", text: "Fetch cancelled." }] };
339
+ return { content: [{ type: "text", text: "Fetch cancelled." }], details: undefined as unknown };
340
340
  }
341
341
 
342
342
  const maxChars = params.maxChars ?? 8000;
@@ -392,7 +392,7 @@ export function registerFetchPageTool(pi: ExtensionAPI) {
392
392
  }
393
393
 
394
394
  const domain = extractDomain(url);
395
- onUpdate?.({ content: [{ type: "text", text: `Fetching ${domain}...` }] });
395
+ onUpdate?.({ content: [{ type: "text", text: `Fetching ${domain}...` }], details: undefined as unknown });
396
396
 
397
397
  // ------------------------------------------------------------------
398
398
  // Fetch page content
@@ -439,7 +439,7 @@ export function registerFetchPageTool(pi: ExtensionAPI) {
439
439
  const finalTruncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
440
440
  let content = finalTruncation.content;
441
441
  if (finalTruncation.truncated) {
442
- const tempFile = await pi.writeTempFile(output, { prefix: "fetch-page-" });
442
+ const tempFile = await (pi as any).writeTempFile(output, { prefix: "fetch-page-" });
443
443
  content += `\n\n[Truncated to fit context. Full content: ${tempFile}]`;
444
444
  }
445
445
 
@@ -21,13 +21,13 @@ import { Text } from "@gsd/pi-tui";
21
21
  import { Type } from "@sinclair/typebox";
22
22
  import { StringEnum } from "@gsd/pi-ai";
23
23
 
24
- import { LRUTTLCache } from "./cache";
25
- import { fetchWithRetryTimed, HttpError, classifyError, type RateLimitInfo } from "./http";
26
- import { normalizeQuery, extractDomain } from "./url-utils";
27
- import { formatLLMContext, type LLMContextSnippet, type LLMContextSource } from "./format";
28
- import type { TavilyResult, TavilySearchResponse } from "./tavily";
29
- import { publishedDateToAge } from "./tavily";
30
- import { getTavilyApiKey, resolveSearchProvider } from "./provider";
24
+ import { LRUTTLCache } from "./cache.js";
25
+ import { fetchWithRetryTimed, HttpError, classifyError, type RateLimitInfo } from "./http.js";
26
+ import { normalizeQuery, extractDomain } from "./url-utils.js";
27
+ import { formatLLMContext, type LLMContextSnippet, type LLMContextSource } from "./format.js";
28
+ import type { TavilyResult, TavilySearchResponse } from "./tavily.js";
29
+ import { publishedDateToAge } from "./tavily.js";
30
+ import { getTavilyApiKey, resolveSearchProvider } from "./provider.js";
31
31
 
32
32
  // =============================================================================
33
33
  // Types
@@ -286,7 +286,7 @@ export function registerLLMContextTool(pi: ExtensionAPI) {
286
286
 
287
287
  async execute(toolCallId, params, signal, onUpdate, ctx) {
288
288
  if (signal?.aborted) {
289
- return { content: [{ type: "text", text: "Search cancelled." }] };
289
+ return { content: [{ type: "text", text: "Search cancelled." }], details: undefined as unknown };
290
290
  }
291
291
 
292
292
  // ------------------------------------------------------------------
@@ -321,7 +321,7 @@ export function registerLLMContextTool(pi: ExtensionAPI) {
321
321
  const truncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
322
322
  let content = truncation.content;
323
323
  if (truncation.truncated) {
324
- const tempFile = await pi.writeTempFile(output, { prefix: "llm-context-" });
324
+ const tempFile = await (pi as any).writeTempFile(output, { prefix: "llm-context-" });
325
325
  content += `\n\n[Truncated. Full content: ${tempFile}]`;
326
326
  }
327
327
 
@@ -340,7 +340,7 @@ export function registerLLMContextTool(pi: ExtensionAPI) {
340
340
  return { content: [{ type: "text", text: content }], details };
341
341
  }
342
342
 
343
- onUpdate?.({ content: [{ type: "text", text: `Searching & reading about "${params.query}"...` }] });
343
+ onUpdate?.({ content: [{ type: "text", text: `Searching & reading about "${params.query}"...` }], details: undefined as unknown });
344
344
 
345
345
  try {
346
346
  // ------------------------------------------------------------------
@@ -483,7 +483,7 @@ export function registerLLMContextTool(pi: ExtensionAPI) {
483
483
  let content = truncation.content;
484
484
 
485
485
  if (truncation.truncated) {
486
- const tempFile = await pi.writeTempFile(output, { prefix: "llm-context-" });
486
+ const tempFile = await (pi as any).writeTempFile(output, { prefix: "llm-context-" });
487
487
  content += `\n\n[Truncated. Full content: ${tempFile}]`;
488
488
  }
489
489
 
@@ -16,12 +16,12 @@ import { Text } from "@gsd/pi-tui";
16
16
  import { Type } from "@sinclair/typebox";
17
17
  import { StringEnum } from "@gsd/pi-ai";
18
18
 
19
- import { LRUTTLCache } from "./cache";
20
- import { fetchWithRetryTimed, fetchWithRetry, classifyError, type RateLimitInfo } from "./http";
21
- import { normalizeQuery, toDedupeKey, detectFreshness } from "./url-utils";
22
- import { formatSearchResults, type SearchResultFormatted, type FormatSearchOptions } from "./format";
23
- import { getTavilyApiKey, resolveSearchProvider } from "./provider";
24
- import { normalizeTavilyResult, mapFreshnessToTavily, type TavilySearchResponse } from "./tavily";
19
+ import { LRUTTLCache } from "./cache.js";
20
+ import { fetchWithRetryTimed, fetchWithRetry, classifyError, type RateLimitInfo } from "./http.js";
21
+ import { normalizeQuery, toDedupeKey, detectFreshness } from "./url-utils.js";
22
+ import { formatSearchResults, type SearchResultFormatted, type FormatSearchOptions } from "./format.js";
23
+ import { getTavilyApiKey, resolveSearchProvider } from "./provider.js";
24
+ import { normalizeTavilyResult, mapFreshnessToTavily, type TavilySearchResponse } from "./tavily.js";
25
25
 
26
26
  // =============================================================================
27
27
  // Types
@@ -291,7 +291,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
291
291
 
292
292
  async execute(toolCallId, params, signal, onUpdate, ctx) {
293
293
  if (signal?.aborted) {
294
- return { content: [{ type: "text", text: "Search cancelled." }] };
294
+ return { content: [{ type: "text", text: "Search cancelled." }], details: undefined as unknown };
295
295
  }
296
296
 
297
297
  // ------------------------------------------------------------------
@@ -365,7 +365,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
365
365
  const truncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
366
366
  let content = truncation.content;
367
367
  if (truncation.truncated) {
368
- const tempFile = await pi.writeTempFile(output, { prefix: "web-search-" });
368
+ const tempFile = await (pi as any).writeTempFile(output, { prefix: "web-search-" });
369
369
  content += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)}/${formatSize(truncation.totalBytes)}). Full results: ${tempFile}]`;
370
370
  }
371
371
 
@@ -387,7 +387,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
387
387
  return { content: [{ type: "text", text: content }], details };
388
388
  }
389
389
 
390
- onUpdate?.({ content: [{ type: "text", text: `Searching for "${params.query}"...` }] });
390
+ onUpdate?.({ content: [{ type: "text", text: `Searching for "${params.query}"...` }], details: undefined as unknown });
391
391
 
392
392
  try {
393
393
  // ------------------------------------------------------------------
@@ -484,7 +484,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
484
484
  let content = truncation.content;
485
485
 
486
486
  if (truncation.truncated) {
487
- const tempFile = await pi.writeTempFile(output, { prefix: "web-search-" });
487
+ const tempFile = await (pi as any).writeTempFile(output, { prefix: "web-search-" });
488
488
  content += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)}/${formatSize(truncation.totalBytes)}). Full results: ${tempFile}]`;
489
489
  }
490
490
 
@@ -105,7 +105,7 @@ export interface WrapUpOptions {
105
105
  // ─── Constants ────────────────────────────────────────────────────────────────
106
106
 
107
107
  const OTHER_OPTION_LABEL = "None of the above";
108
- const OTHER_OPTION_DESCRIPTION = "Optionally, add details in notes below.";
108
+ const OTHER_OPTION_DESCRIPTION = "Press TAB to add optional notes.";
109
109
 
110
110
  // ─── Wrap-up screen ───────────────────────────────────────────────────────────
111
111
 
@@ -593,7 +593,7 @@ export async function showInterviewRound(
593
593
  hints.push("tab to add notes");
594
594
  hints.push(isLast && allAnswered() ? "enter to review" : "enter to next");
595
595
  } else {
596
- if (st.committedIndex !== null || !isMultiQuestion) hints.push("tab to add notes");
596
+ hints.push("tab to add notes");
597
597
  if (isMultiQuestion) hints.push("←/→ navigate");
598
598
  hints.push(isLast && allAnswered() ? "enter to review" : "enter to next");
599
599
  }