gsd-pi 2.8.3 → 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 (169) 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/lsp/client.d.ts +46 -0
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  33. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  34. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  35. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  36. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
  37. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  38. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  39. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  41. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  43. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
  44. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  45. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  46. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  47. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
  48. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  49. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  50. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  51. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  52. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  53. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  54. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  55. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +46 -1
  56. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  57. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
  58. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
  59. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  60. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  61. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  62. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
  63. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  64. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  65. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  66. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
  67. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  68. package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
  69. package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
  70. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  71. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +59 -2
  72. package/package.json +1 -1
  73. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  74. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
  76. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  78. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
  80. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  82. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  84. package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  86. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  88. package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  90. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  96. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  98. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  100. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  102. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
  104. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  106. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  108. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  111. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  113. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
  115. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +46 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  124. package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
  125. package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
  126. package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  127. package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  128. package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  129. package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
  130. package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  131. package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  132. package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  133. package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
  134. package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  135. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  136. package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
  137. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  138. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +59 -2
  139. package/src/resources/extensions/ask-user-questions.ts +2 -2
  140. package/src/resources/extensions/bg-shell/index.ts +34 -37
  141. package/src/resources/extensions/browser-tools/core.d.ts +205 -0
  142. package/src/resources/extensions/browser-tools/index.ts +2 -2
  143. package/src/resources/extensions/browser-tools/refs.ts +1 -1
  144. package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
  145. package/src/resources/extensions/context7/index.ts +2 -2
  146. package/src/resources/extensions/get-secrets-from-user.ts +3 -2
  147. package/src/resources/extensions/google-search/index.ts +1 -1
  148. package/src/resources/extensions/gsd/auto.ts +41 -4
  149. package/src/resources/extensions/gsd/commands.ts +218 -3
  150. package/src/resources/extensions/gsd/doctor.ts +1 -1
  151. package/src/resources/extensions/gsd/git-service.ts +116 -4
  152. package/src/resources/extensions/gsd/guided-flow.ts +19 -9
  153. package/src/resources/extensions/gsd/index.ts +17 -7
  154. package/src/resources/extensions/gsd/preferences.ts +1 -1
  155. package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
  156. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
  157. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
  158. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
  159. package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
  160. package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
  161. package/src/resources/extensions/gsd/types.ts +1 -0
  162. package/src/resources/extensions/gsd/worktree.ts +20 -1
  163. package/src/resources/extensions/mac-tools/index.ts +1 -1
  164. package/src/resources/extensions/search-the-web/format.ts +1 -1
  165. package/src/resources/extensions/search-the-web/index.ts +5 -5
  166. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
  167. package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
  168. package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
  169. package/src/resources/extensions/shared/interview-ui.ts +2 -2
@@ -160,7 +160,7 @@ async function collectOneSecret(
160
160
  ): Promise<string | null> {
161
161
  if (!ctx.hasUI) return null;
162
162
 
163
- return ctx.ui.custom<string | null>((tui: any, theme: any, _kb: any, done: (r: string | null) => void) => {
163
+ return ctx.ui.custom((tui: any, theme: any, _kb: any, done: (r: string | null) => void) => {
164
164
  let value = "";
165
165
  let cachedLines: string[] | undefined;
166
166
 
@@ -286,7 +286,7 @@ export async function showSecretsSummary(
286
286
 
287
287
  const existingSet = new Set(existingKeys);
288
288
 
289
- await ctx.ui.custom<void>((tui: any, theme: Theme, _kb: any, done: () => void) => {
289
+ await (ctx.ui.custom as Function)((tui: any, theme: Theme, _kb: any, done: () => void) => {
290
290
  let cachedLines: string[] | undefined;
291
291
 
292
292
  function handleInput(_data: string) {
@@ -549,6 +549,7 @@ export default function secureEnv(pi: ExtensionAPI) {
549
549
  return {
550
550
  content: [{ type: "text", text: "Error: UI not available (interactive mode required for secure env collection)." }],
551
551
  isError: true,
552
+ details: undefined as unknown,
552
553
  };
553
554
  }
554
555
 
@@ -261,7 +261,7 @@ export default function (pi: ExtensionAPI) {
261
261
  const d = result.details as SearchDetails | undefined;
262
262
 
263
263
  if (isPartial) return new Text(theme.fg("warning", "Searching Google..."), 0, 0);
264
- if (result.isError || d?.error) {
264
+ if ((result as any).isError || d?.error) {
265
265
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
266
266
  }
267
267
 
@@ -60,10 +60,12 @@ import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from
60
60
  import { execSync, execFileSync } from "node:child_process";
61
61
  import {
62
62
  autoCommitCurrentBranch,
63
+ captureIntegrationBranch,
63
64
  ensureSliceBranch,
64
65
  getCurrentBranch,
65
66
  getMainBranch,
66
67
  parseSliceBranch,
68
+ setActiveMilestoneId,
67
69
  switchToMain,
68
70
  mergeSliceToMain,
69
71
  } from "./worktree.ts";
@@ -361,6 +363,8 @@ export async function startAuto(
361
363
  unitDispatchCount.clear();
362
364
  // Re-initialize metrics in case ledger was lost during pause
363
365
  if (!getLedger()) initMetrics(base);
366
+ // Ensure milestone ID is set on git service for integration branch resolution
367
+ if (currentMilestoneId) setActiveMilestoneId(base, currentMilestoneId);
364
368
  ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
365
369
  ctx.ui.setFooter(hideFooter);
366
370
  ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
@@ -468,6 +472,15 @@ export async function startAuto(
468
472
  originalModelId = ctx.model?.id ?? null;
469
473
  originalModelProvider = ctx.model?.provider ?? null;
470
474
 
475
+ // Capture the integration branch — records the branch the user was on when
476
+ // auto-mode started. Slice branches will merge back to this branch instead
477
+ // of the repo's default (main/master). Idempotent: only writes if not
478
+ // already recorded, so restarts/resumes don't overwrite.
479
+ if (currentMilestoneId) {
480
+ captureIntegrationBranch(base, currentMilestoneId);
481
+ setActiveMilestoneId(base, currentMilestoneId);
482
+ }
483
+
471
484
  // Initialize metrics — loads existing ledger from disk
472
485
  initMetrics(base);
473
486
 
@@ -1002,8 +1015,13 @@ async function dispatchNextUnit(
1002
1015
  // Reset stuck detection for new milestone
1003
1016
  unitDispatchCount.clear();
1004
1017
  unitRecoveryCount.clear();
1018
+ // Capture integration branch for the new milestone and update git service
1019
+ captureIntegrationBranch(basePath, mid);
1020
+ }
1021
+ if (mid) {
1022
+ currentMilestoneId = mid;
1023
+ setActiveMilestoneId(basePath, mid);
1005
1024
  }
1006
- if (mid) currentMilestoneId = mid;
1007
1025
 
1008
1026
  if (!mid) {
1009
1027
  // Save final session before stopping
@@ -1016,6 +1034,14 @@ async function dispatchNextUnit(
1016
1034
  return;
1017
1035
  }
1018
1036
 
1037
+ // Guard: mid/midTitle must be defined strings from this point onward.
1038
+ // The !mid check above returns early if mid is falsy; midTitle comes from
1039
+ // the same object so it should always be present when mid is.
1040
+ if (!midTitle) {
1041
+ await stopAuto(ctx, pi);
1042
+ return;
1043
+ }
1044
+
1019
1045
  // ── General merge guard: merge completed slice branches before advancing ──
1020
1046
  // If we're on a gsd/MID/SID branch and that slice is done (roadmap [x]),
1021
1047
  // merge to main before dispatching the next unit. This handles:
@@ -1088,6 +1114,17 @@ async function dispatchNextUnit(
1088
1114
  }
1089
1115
  }
1090
1116
 
1117
+ // After merge, mid/midTitle may have been re-derived and could be undefined
1118
+ if (!mid || !midTitle) {
1119
+ if (currentUnit) {
1120
+ const modelId = ctx.model?.id ?? "unknown";
1121
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1122
+ saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1123
+ }
1124
+ await stopAuto(ctx, pi);
1125
+ return;
1126
+ }
1127
+
1091
1128
  // Determine next unit
1092
1129
  let unitType: string;
1093
1130
  let unitId: string;
@@ -1522,9 +1559,9 @@ async function dispatchNextUnit(
1522
1559
  // soft timeout; only idle/stalled tasks pause early.
1523
1560
  clearUnitTimeout();
1524
1561
  const supervisor = resolveAutoSupervisorConfig();
1525
- const softTimeoutMs = supervisor.soft_timeout_minutes * 60 * 1000;
1526
- const idleTimeoutMs = supervisor.idle_timeout_minutes * 60 * 1000;
1527
- const hardTimeoutMs = supervisor.hard_timeout_minutes * 60 * 1000;
1562
+ const softTimeoutMs = (supervisor.soft_timeout_minutes ?? 0) * 60 * 1000;
1563
+ const idleTimeoutMs = (supervisor.idle_timeout_minutes ?? 0) * 60 * 1000;
1564
+ const hardTimeoutMs = (supervisor.hard_timeout_minutes ?? 0) * 60 * 1000;
1528
1565
 
1529
1566
  wrapupWarningHandle = setTimeout(() => {
1530
1567
  wrapupWarningHandle = null;
@@ -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}`;
@@ -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}.`,