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
@@ -74,7 +74,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
74
74
 
75
75
  if (parts[0] === "prefs" && parts.length <= 2) {
76
76
  const subPrefix = parts[1] ?? "";
77
- return ["global", "project", "status"]
77
+ return ["global", "project", "status", "wizard", "setup"]
78
78
  .filter((cmd) => cmd.startsWith(subPrefix))
79
79
  .map((cmd) => ({ value: `prefs ${cmd}`, label: cmd }));
80
80
  }
@@ -168,7 +168,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
168
168
  }
169
169
 
170
170
  ctx.ui.notify(
171
- `Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status], /gsd doctor [audit|fix|heal] [M###/S##], /gsd migrate <path>, or /gsd remote [slack|discord|status|disconnect].`,
171
+ `Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status|wizard|setup], /gsd doctor [audit|fix|heal] [M###/S##], /gsd migrate <path>, or /gsd remote [slack|discord|status|disconnect].`,
172
172
  "warning",
173
173
  );
174
174
  },
@@ -219,6 +219,13 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<
219
219
  return;
220
220
  }
221
221
 
222
+ if (trimmed === "wizard" || trimmed === "setup" || trimmed === "wizard global" || trimmed === "setup global"
223
+ || trimmed === "wizard project" || trimmed === "setup project") {
224
+ const scope = trimmed.includes("project") ? "project" : "global";
225
+ await handlePrefsWizard(ctx, scope);
226
+ return;
227
+ }
228
+
222
229
  if (trimmed === "status") {
223
230
  const globalPrefs = loadGlobalGSDPreferences();
224
231
  const projectPrefs = loadProjectGSDPreferences();
@@ -249,7 +256,7 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<
249
256
  return;
250
257
  }
251
258
 
252
- ctx.ui.notify("Usage: /gsd prefs [global|project|status]", "info");
259
+ ctx.ui.notify("Usage: /gsd prefs [global|project|status|wizard|setup]", "info");
253
260
  }
254
261
 
255
262
  async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
@@ -290,6 +297,214 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte
290
297
  }
291
298
  }
292
299
 
300
+ // ─── Preferences Wizard ───────────────────────────────────────────────────────
301
+
302
+ async function handlePrefsWizard(
303
+ ctx: ExtensionCommandContext,
304
+ scope: "global" | "project",
305
+ ): Promise<void> {
306
+ const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath();
307
+ const existing = scope === "project" ? loadProjectGSDPreferences() : loadGlobalGSDPreferences();
308
+ const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : {};
309
+
310
+ ctx.ui.notify(`GSD preferences wizard (${scope}) — press Escape at any prompt to skip it.`, "info");
311
+
312
+ // ─── Models ──────────────────────────────────────────────────────────────
313
+ const modelPhases = ["research", "planning", "execution", "completion"] as const;
314
+ const models: Record<string, string> = (prefs.models as Record<string, string>) ?? {};
315
+
316
+ for (const phase of modelPhases) {
317
+ const current = models[phase] ?? "";
318
+ const input = await ctx.ui.input(
319
+ `Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`,
320
+ current || "e.g. claude-sonnet-4-20250514",
321
+ );
322
+ if (input !== null && input !== undefined) {
323
+ const val = input.trim();
324
+ if (val) {
325
+ models[phase] = val;
326
+ } else if (current) {
327
+ // User cleared it — remove
328
+ delete models[phase];
329
+ }
330
+ }
331
+ // null/undefined = Escape/skip — keep existing value
332
+ }
333
+ if (Object.keys(models).length > 0) {
334
+ prefs.models = models;
335
+ }
336
+
337
+ // ─── Auto-supervisor timeouts ────────────────────────────────────────────
338
+ const autoSup: Record<string, unknown> = (prefs.auto_supervisor as Record<string, unknown>) ?? {};
339
+ const timeoutFields = [
340
+ { key: "soft_timeout_minutes", label: "Soft timeout (minutes)", defaultVal: "20" },
341
+ { key: "idle_timeout_minutes", label: "Idle timeout (minutes)", defaultVal: "10" },
342
+ { key: "hard_timeout_minutes", label: "Hard timeout (minutes)", defaultVal: "30" },
343
+ ] as const;
344
+
345
+ for (const field of timeoutFields) {
346
+ const current = autoSup[field.key];
347
+ const currentStr = current !== undefined && current !== null ? String(current) : "";
348
+ const input = await ctx.ui.input(
349
+ `${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
350
+ currentStr || field.defaultVal,
351
+ );
352
+ if (input !== null && input !== undefined) {
353
+ const val = input.trim();
354
+ if (val && /^\d+$/.test(val)) {
355
+ autoSup[field.key] = Number(val);
356
+ } else if (val && !/^\d+$/.test(val)) {
357
+ ctx.ui.notify(`Invalid value "${val}" for ${field.label} — must be a whole number. Keeping previous value.`, "warning");
358
+ } else if (!val && currentStr) {
359
+ delete autoSup[field.key];
360
+ }
361
+ }
362
+ }
363
+ if (Object.keys(autoSup).length > 0) {
364
+ prefs.auto_supervisor = autoSup;
365
+ }
366
+
367
+ // ─── Git main branch ────────────────────────────────────────────────────
368
+ const git: Record<string, unknown> = (prefs.git as Record<string, unknown>) ?? {};
369
+ const currentBranch = git.main_branch ? String(git.main_branch) : "";
370
+ const branchInput = await ctx.ui.input(
371
+ `Git main branch${currentBranch ? ` (current: ${currentBranch})` : ""}:`,
372
+ currentBranch || "main",
373
+ );
374
+ if (branchInput !== null && branchInput !== undefined) {
375
+ const val = branchInput.trim();
376
+ if (val) {
377
+ git.main_branch = val;
378
+ } else if (currentBranch) {
379
+ delete git.main_branch;
380
+ }
381
+ }
382
+ if (Object.keys(git).length > 0) {
383
+ prefs.git = git;
384
+ }
385
+
386
+ // ─── Skill discovery mode ───────────────────────────────────────────────
387
+ const currentDiscovery = (prefs.skill_discovery as string) ?? "";
388
+ const discoveryChoice = await ctx.ui.select(
389
+ `Skill discovery mode${currentDiscovery ? ` (current: ${currentDiscovery})` : ""}:`,
390
+ ["auto", "suggest", "off", "(keep current)"],
391
+ );
392
+ if (discoveryChoice && discoveryChoice !== "(keep current)") {
393
+ prefs.skill_discovery = discoveryChoice;
394
+ }
395
+
396
+ // ─── Serialize to frontmatter ───────────────────────────────────────────
397
+ prefs.version = prefs.version || 1;
398
+ const frontmatter = serializePreferencesToFrontmatter(prefs);
399
+
400
+ // Preserve existing body content (everything after closing ---)
401
+ let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
402
+ if (existsSync(path)) {
403
+ const existingContent = readFileSync(path, "utf-8");
404
+ const closingIdx = existingContent.indexOf("\n---", existingContent.indexOf("---"));
405
+ if (closingIdx !== -1) {
406
+ const afterFrontmatter = existingContent.slice(closingIdx + 4); // skip past "\n---"
407
+ if (afterFrontmatter.trim()) {
408
+ body = afterFrontmatter;
409
+ }
410
+ }
411
+ }
412
+
413
+ const content = `---\n${frontmatter}---${body}`;
414
+
415
+ await saveFile(path, content);
416
+ await ctx.waitForIdle();
417
+ await ctx.reload();
418
+ ctx.ui.notify(`Saved ${scope} preferences to ${path}`, "info");
419
+ }
420
+
421
+ /** Wrap a YAML value in double quotes if it contains special characters. */
422
+ function yamlSafeString(val: unknown): string {
423
+ if (typeof val !== "string") return String(val);
424
+ if (/[:#{\[\]'"`,|>&*!?@%]/.test(val) || val.trim() !== val || val === "") {
425
+ return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
426
+ }
427
+ return val;
428
+ }
429
+
430
+ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): string {
431
+ const lines: string[] = [];
432
+
433
+ function serializeValue(key: string, value: unknown, indent: number): void {
434
+ const prefix = " ".repeat(indent);
435
+ if (value === null || value === undefined) return;
436
+
437
+ if (Array.isArray(value)) {
438
+ if (value.length === 0) {
439
+ lines.push(`${prefix}${key}: []`);
440
+ return;
441
+ }
442
+ lines.push(`${prefix}${key}:`);
443
+ for (const item of value) {
444
+ if (typeof item === "object" && item !== null) {
445
+ const entries = Object.entries(item as Record<string, unknown>);
446
+ if (entries.length > 0) {
447
+ const [firstKey, firstVal] = entries[0];
448
+ lines.push(`${prefix} - ${firstKey}: ${yamlSafeString(firstVal)}`);
449
+ for (let i = 1; i < entries.length; i++) {
450
+ const [k, v] = entries[i];
451
+ if (Array.isArray(v)) {
452
+ lines.push(`${prefix} ${k}:`);
453
+ for (const arrItem of v) {
454
+ lines.push(`${prefix} - ${yamlSafeString(arrItem)}`);
455
+ }
456
+ } else {
457
+ lines.push(`${prefix} ${k}: ${yamlSafeString(v)}`);
458
+ }
459
+ }
460
+ }
461
+ } else {
462
+ lines.push(`${prefix} - ${yamlSafeString(item)}`);
463
+ }
464
+ }
465
+ return;
466
+ }
467
+
468
+ if (typeof value === "object") {
469
+ const entries = Object.entries(value as Record<string, unknown>);
470
+ if (entries.length === 0) {
471
+ lines.push(`${prefix}${key}: {}`);
472
+ return;
473
+ }
474
+ lines.push(`${prefix}${key}:`);
475
+ for (const [k, v] of entries) {
476
+ serializeValue(k, v, indent + 1);
477
+ }
478
+ return;
479
+ }
480
+
481
+ lines.push(`${prefix}${key}: ${yamlSafeString(value)}`);
482
+ }
483
+
484
+ // Ordered keys for consistent output
485
+ const orderedKeys = [
486
+ "version", "always_use_skills", "prefer_skills", "avoid_skills",
487
+ "skill_rules", "custom_instructions", "models", "skill_discovery",
488
+ "auto_supervisor", "uat_dispatch", "budget_ceiling", "remote_questions", "git",
489
+ ];
490
+
491
+ const seen = new Set<string>();
492
+ for (const key of orderedKeys) {
493
+ if (key in prefs) {
494
+ serializeValue(key, prefs[key], 0);
495
+ seen.add(key);
496
+ }
497
+ }
498
+ // Any remaining keys not in the ordered list
499
+ for (const [key, value] of Object.entries(prefs)) {
500
+ if (!seen.has(key)) {
501
+ serializeValue(key, value, 0);
502
+ }
503
+ }
504
+
505
+ return lines.join("\n") + "\n";
506
+ }
507
+
293
508
  async function ensurePreferencesFile(
294
509
  path: string,
295
510
  ctx: ExtensionCommandContext,
@@ -79,7 +79,7 @@ function validatePreferenceShape(preferences: GSDPreferences): string[] {
79
79
  issues.push(`skill_rules[${index}].when must be a string`);
80
80
  }
81
81
  for (const key of ["use", "prefer", "avoid"] as const) {
82
- const value = (rule as Record<string, unknown>)[key];
82
+ const value = (rule as unknown as Record<string, unknown>)[key];
83
83
  if (value !== undefined && !Array.isArray(value)) {
84
84
  issues.push(`skill_rules[${index}].${key} must be a list`);
85
85
  }
@@ -9,7 +9,8 @@
9
9
  */
10
10
 
11
11
  import { execSync } from "node:child_process";
12
- import { sep } from "node:path";
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join, sep } from "node:path";
13
14
 
14
15
  import {
15
16
  detectWorktreeName,
@@ -68,6 +69,86 @@ export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
68
69
  ".gsd/STATE.md",
69
70
  ];
70
71
 
72
+ // ─── Integration Branch Metadata ───────────────────────────────────────────
73
+
74
+ /**
75
+ * Path to the milestone metadata file that stores the integration branch.
76
+ * Format: .gsd/milestones/<MID>/<MID>-META.json
77
+ */
78
+ function milestoneMetaPath(basePath: string, milestoneId: string): string {
79
+ return join(basePath, ".gsd", "milestones", milestoneId, `${milestoneId}-META.json`);
80
+ }
81
+
82
+ /**
83
+ * Read the integration branch recorded for a milestone.
84
+ * Returns null if no metadata file exists or the branch isn't set.
85
+ */
86
+ export function readIntegrationBranch(basePath: string, milestoneId: string): string | null {
87
+ try {
88
+ const metaFile = milestoneMetaPath(basePath, milestoneId);
89
+ if (!existsSync(metaFile)) return null;
90
+ const data = JSON.parse(readFileSync(metaFile, "utf-8"));
91
+ const branch = data?.integrationBranch;
92
+ if (typeof branch === "string" && branch.trim() !== "" && VALID_BRANCH_NAME.test(branch)) {
93
+ return branch;
94
+ }
95
+ return null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Persist the integration branch for a milestone.
103
+ *
104
+ * Called once when auto-mode starts on a milestone. Records the branch
105
+ * the user was on at that point, so that slice branches merge back to it
106
+ * instead of the repo's default branch.
107
+ *
108
+ * The file is committed immediately so it survives branch switches — the
109
+ * pre-switch auto-commit excludes `.gsd/` to avoid merge conflicts, and
110
+ * uncommitted `.gsd/` files are discarded during checkout.
111
+ *
112
+ * Skips writing if an integration branch is already recorded (idempotent
113
+ * across restarts) or if the current branch is already a GSD slice branch.
114
+ */
115
+ export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string): void {
116
+ // Don't record slice branches as the integration target
117
+ if (SLICE_BRANCH_RE.test(branch)) return;
118
+ // Don't overwrite an existing integration branch
119
+ if (readIntegrationBranch(basePath, milestoneId) !== null) return;
120
+ // Validate
121
+ if (!VALID_BRANCH_NAME.test(branch)) return;
122
+
123
+ const metaFile = milestoneMetaPath(basePath, milestoneId);
124
+ mkdirSync(join(basePath, ".gsd", "milestones", milestoneId), { recursive: true });
125
+
126
+ // Merge with existing metadata if present
127
+ let existing: Record<string, unknown> = {};
128
+ try {
129
+ if (existsSync(metaFile)) {
130
+ existing = JSON.parse(readFileSync(metaFile, "utf-8"));
131
+ }
132
+ } catch { /* corrupt file — overwrite */ }
133
+
134
+ existing.integrationBranch = branch;
135
+ writeFileSync(metaFile, JSON.stringify(existing, null, 2) + "\n", "utf-8");
136
+
137
+ // Commit immediately — .gsd/ files are discarded during branch switches
138
+ // (ensureSliceBranch excludes .gsd/ from pre-switch auto-commit and runs
139
+ // git checkout -- .gsd/ to prevent checkout conflicts). Without this
140
+ // commit, the metadata would be lost on the first branch switch.
141
+ try {
142
+ runGit(basePath, ["add", "--force", metaFile]);
143
+ runGit(basePath, ["commit", "-F", "-"], {
144
+ input: `chore(${milestoneId}): record integration branch`,
145
+ });
146
+ } catch {
147
+ // Non-fatal — file is on disk even if commit fails (e.g. nothing to commit
148
+ // because the file was already tracked with identical content)
149
+ }
150
+ }
151
+
71
152
  // ─── Git Helper ────────────────────────────────────────────────────────────
72
153
 
73
154
  /**
@@ -115,11 +196,23 @@ export class GitServiceImpl {
115
196
  readonly basePath: string;
116
197
  readonly prefs: GitPreferences;
117
198
 
199
+ /** Active milestone ID — used to resolve the integration branch. */
200
+ private _milestoneId: string | null = null;
201
+
118
202
  constructor(basePath: string, prefs: GitPreferences = {}) {
119
203
  this.basePath = basePath;
120
204
  this.prefs = prefs;
121
205
  }
122
206
 
207
+ /**
208
+ * Set the active milestone ID for integration branch resolution.
209
+ * When set, getMainBranch() will check the milestone's metadata file
210
+ * for a recorded integration branch before falling back to repo defaults.
211
+ */
212
+ setMilestoneId(milestoneId: string | null): void {
213
+ this._milestoneId = milestoneId;
214
+ }
215
+
123
216
  /** Convenience wrapper: run git in this repo's basePath. */
124
217
  private git(args: string[], options: { allowFailure?: boolean; input?: string } = {}): string {
125
218
  return runGit(this.basePath, args, options);
@@ -212,9 +305,18 @@ export class GitServiceImpl {
212
305
  // ─── Branch Queries ────────────────────────────────────────────────────
213
306
 
214
307
  /**
215
- * Get the "main" branch for this repo.
216
- * In a worktree: returns worktree/<name> (the worktree's base branch).
217
- * In the main tree: origin/HEAD symbolic-ref → main/master fallback → current branch.
308
+ * Get the "main" (integration) branch for this repo.
309
+ *
310
+ * Resolution order:
311
+ * 1. Explicit `main_branch` preference (user override, highest priority)
312
+ * 2. Milestone integration branch from metadata file (recorded at milestone start)
313
+ * 3. Worktree base branch (worktree/<name>)
314
+ * 4. origin/HEAD symbolic-ref → main/master fallback → current branch
315
+ *
316
+ * The integration branch (step 2) is what makes feature-branch workflows
317
+ * work correctly: when a user starts GSD on `f-123-new-thing`, that branch
318
+ * is recorded as the integration target, and all slice branches merge back
319
+ * to it instead of the repo's default branch.
218
320
  */
219
321
  getMainBranch(): string {
220
322
  // Explicit preference takes priority (double-check validity as defense-in-depth)
@@ -222,6 +324,16 @@ export class GitServiceImpl {
222
324
  return this.prefs.main_branch;
223
325
  }
224
326
 
327
+ // Check milestone integration branch — recorded when auto-mode starts
328
+ if (this._milestoneId) {
329
+ const integrationBranch = readIntegrationBranch(this.basePath, this._milestoneId);
330
+ if (integrationBranch) {
331
+ // Verify the branch still exists locally (could have been deleted)
332
+ const exists = this.git(["show-ref", "--verify", `refs/heads/${integrationBranch}`], { allowFailure: true });
333
+ if (exists) return integrationBranch;
334
+ }
335
+ }
336
+
225
337
  const wtName = detectWorktreeName(this.basePath);
226
338
  if (wtName) {
227
339
  const wtBranch = `worktree/${wtName}`;
@@ -527,6 +639,16 @@ export class GitServiceImpl {
527
639
  // Pull latest main before merging to avoid conflicts from remote changes
528
640
  this.git(["pull", "--rebase", "origin", mainBranch], { allowFailure: true });
529
641
 
642
+ // Untrack runtime files that may have been manually committed (e.g. via `gsd queue`)
643
+ // to prevent merge conflicts on files that belong in .gitignore (#189)
644
+ for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
645
+ this.git(["rm", "--cached", "-r", "--ignore-unmatch", exclusion], { allowFailure: true });
646
+ }
647
+ const untrackDiff = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
648
+ if (untrackDiff && untrackDiff.trim()) {
649
+ this.git(["commit", "-m", "chore: untrack .gsd/ runtime files before merge"], { allowFailure: true });
650
+ }
651
+
530
652
  // Merge slice branch — strategy is configurable via git.merge_strategy
531
653
  // preference. Default: "squash" (preserves existing behavior).
532
654
  // "merge" uses --no-ff which is more resilient to conflicts from
@@ -539,15 +661,43 @@ export class GitServiceImpl {
539
661
  try {
540
662
  this.git(mergeArgs);
541
663
  } catch (mergeError) {
542
- // Merge exits non-zero on conflict. Reset to restore a clean state.
543
- this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
544
- const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
545
- throw new Error(
546
- `${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed with conflicts. ` +
547
- `Working tree has been reset to a clean state. ` +
548
- `Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
549
- `Original error: ${msg}`,
550
- );
664
+ // Check if conflicts are limited to runtime files we can auto-resolve (#189)
665
+ const conflicted = this.git(["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
666
+ if (conflicted) {
667
+ const conflictedFiles = conflicted.split("\n").filter(Boolean);
668
+ const allRuntime = conflictedFiles.every(f =>
669
+ RUNTIME_EXCLUSION_PATHS.some(excl => f.startsWith(excl.replace(/\/$/, ""))),
670
+ );
671
+ if (allRuntime) {
672
+ // Runtime-only conflicts: take ours and remove from index
673
+ for (const f of conflictedFiles) {
674
+ this.git(["checkout", "--ours", "--", f], { allowFailure: true });
675
+ this.git(["rm", "--cached", "--ignore-unmatch", f], { allowFailure: true });
676
+ }
677
+ this.git(["add", "-A"], { allowFailure: true });
678
+ // Don't throw — let the merge proceed
679
+ } else {
680
+ // Non-runtime conflicts: reset and throw as before
681
+ this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
682
+ const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
683
+ throw new Error(
684
+ `${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed with conflicts. ` +
685
+ `Working tree has been reset to a clean state. ` +
686
+ `Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
687
+ `Original error: ${msg}`,
688
+ );
689
+ }
690
+ } else {
691
+ // No conflicted files detected but merge still failed — reset and throw
692
+ this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
693
+ const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
694
+ throw new Error(
695
+ `${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed. ` +
696
+ `Working tree has been reset to a clean state. ` +
697
+ `Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
698
+ `Original error: ${msg}`,
699
+ );
700
+ }
551
701
  }
552
702
 
553
703
  // Squash merge needs a separate commit; --no-ff merge already committed
@@ -112,6 +112,19 @@ function findMilestoneIds(basePath: string): string[] {
112
112
  }
113
113
  }
114
114
 
115
+ /** Return the highest numeric suffix among milestone IDs (0 when the list is empty or has no numeric IDs). */
116
+ export function maxMilestoneNum(milestoneIds: string[]): number {
117
+ return milestoneIds.reduce((max, id) => {
118
+ const num = parseInt(id.replace(/^M/, ""), 10);
119
+ return num > max ? num : max;
120
+ }, 0);
121
+ }
122
+
123
+ /** Derive the next milestone ID from existing IDs using max-based approach to avoid collisions after deletions. */
124
+ export function nextMilestoneId(milestoneIds: string[]): string {
125
+ return `M${String(maxMilestoneNum(milestoneIds) + 1).padStart(3, "0")}`;
126
+ }
127
+
115
128
  // ─── Queue ─────────────────────────────────────────────────────────────────────
116
129
 
117
130
  /**
@@ -153,12 +166,9 @@ export async function showQueue(
153
166
  const existingContext = await buildExistingMilestonesContext(basePath, milestoneIds, state);
154
167
 
155
168
  // ── Determine next milestone ID ─────────────────────────────────────
156
- const maxNum = milestoneIds.reduce((max, id) => {
157
- const num = parseInt(id.replace(/^M/, ""), 10);
158
- return num > max ? num : max;
159
- }, 0);
160
- const nextId = `M${String(maxNum + 1).padStart(3, "0")}`;
161
- const nextIdPlus1 = `M${String(maxNum + 2).padStart(3, "0")}`;
169
+ const max = maxMilestoneNum(milestoneIds);
170
+ const nextId = `M${String(max + 1).padStart(3, "0")}`;
171
+ const nextIdPlus1 = `M${String(max + 2).padStart(3, "0")}`;
162
172
 
163
173
  // ── Build preamble ──────────────────────────────────────────────────
164
174
  const activePart = state.activeMilestone
@@ -508,7 +518,7 @@ export async function showSmartEntry(
508
518
  }
509
519
 
510
520
  const milestoneIds = findMilestoneIds(basePath);
511
- const nextId = `M${String(milestoneIds.length + 1).padStart(3, "0")}`;
521
+ const nextId = nextMilestoneId(milestoneIds);
512
522
  const isFirst = milestoneIds.length === 0;
513
523
 
514
524
  if (isFirst) {
@@ -570,7 +580,7 @@ export async function showSmartEntry(
570
580
 
571
581
  if (choice === "new_milestone") {
572
582
  const milestoneIds = findMilestoneIds(basePath);
573
- const nextId = `M${String(milestoneIds.length + 1).padStart(3, "0")}`;
583
+ const nextId = nextMilestoneId(milestoneIds);
574
584
 
575
585
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
576
586
  dispatchWorkflow(pi, buildDiscussPrompt(nextId,
@@ -638,7 +648,7 @@ export async function showSmartEntry(
638
648
  }));
639
649
  } else if (choice === "skip_milestone") {
640
650
  const milestoneIds = findMilestoneIds(basePath);
641
- const nextId = `M${String(milestoneIds.length + 1).padStart(3, "0")}`;
651
+ const nextId = nextMilestoneId(milestoneIds);
642
652
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
643
653
  dispatchWorkflow(pi, buildDiscussPrompt(nextId,
644
654
  `New milestone ${nextId}.`,
@@ -127,7 +127,7 @@ export default function (pi: ExtensionAPI) {
127
127
  ...params,
128
128
  timeout: params.timeout ?? DEFAULT_BASH_TIMEOUT_SECS,
129
129
  };
130
- return baseBash.execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx);
130
+ return (baseBash as any).execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx);
131
131
  },
132
132
  };
133
133
  pi.registerTool(dynamicBash as any);
@@ -148,7 +148,7 @@ export default function (pi: ExtensionAPI) {
148
148
  ctx?: any,
149
149
  ) => {
150
150
  const fresh = createWriteTool(process.cwd());
151
- return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
151
+ return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
152
152
  },
153
153
  };
154
154
  pi.registerTool(dynamicWrite as any);
@@ -164,7 +164,7 @@ export default function (pi: ExtensionAPI) {
164
164
  ctx?: any,
165
165
  ) => {
166
166
  const fresh = createReadTool(process.cwd());
167
- return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
167
+ return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
168
168
  },
169
169
  };
170
170
  pi.registerTool(dynamicRead as any);
@@ -180,7 +180,7 @@ export default function (pi: ExtensionAPI) {
180
180
  ctx?: any,
181
181
  ) => {
182
182
  const fresh = createEditTool(process.cwd());
183
- return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
183
+ return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
184
184
  },
185
185
  };
186
186
  pi.registerTool(dynamicEdit as any);
@@ -325,14 +325,24 @@ export default function (pi: ExtensionAPI) {
325
325
  // If auto-mode is already running, advance to next unit
326
326
  if (!isAutoActive()) return;
327
327
 
328
- // If the agent was aborted (user pressed Escape), pause auto-mode
329
- // instead of advancing. This preserves the conversation so the user
330
- // can inspect what happened, interact with the agent, or resume.
328
+ // If the agent was aborted (user pressed Escape) or hit a provider
329
+ // error (fetch failure, rate limit, etc.), pause auto-mode instead of
330
+ // advancing. This preserves the conversation so the user can inspect
331
+ // what happened, interact with the agent, or resume.
331
332
  const lastMsg = event.messages[event.messages.length - 1];
332
333
  if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
333
334
  await pauseAuto(ctx, pi);
334
335
  return;
335
336
  }
337
+ if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
338
+ const errorDetail =
339
+ "errorMessage" in lastMsg && lastMsg.errorMessage
340
+ ? `: ${lastMsg.errorMessage}`
341
+ : "";
342
+ (ctx as any).log(`Auto-mode paused due to provider error${errorDetail}`);
343
+ await pauseAuto(ctx, pi);
344
+ return;
345
+ }
336
346
 
337
347
  await handleAgentEnd(ctx, pi);
338
348
  });
@@ -625,7 +625,7 @@ function validatePreferences(preferences: GSDPreferences): {
625
625
  }
626
626
  const validatedRule: GSDSkillRule = { when };
627
627
  for (const action of SKILL_ACTIONS) {
628
- const values = normalizeStringList((rule as Record<string, unknown>)[action]);
628
+ const values = normalizeStringList((rule as unknown as Record<string, unknown>)[action]);
629
629
  if (values.length > 0) {
630
630
  validatedRule[action as keyof GSDSkillRule] = values as never;
631
631
  }