gsd-pi 2.37.1-dev.d3ace49 → 2.38.0-dev.29edcdc

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 (188) hide show
  1. package/dist/app-paths.js +1 -1
  2. package/dist/cli.js +9 -0
  3. package/dist/extension-discovery.d.ts +5 -3
  4. package/dist/extension-discovery.js +14 -9
  5. package/dist/extension-registry.js +2 -2
  6. package/dist/remote-questions-config.js +2 -2
  7. package/dist/resource-loader.js +34 -1
  8. package/dist/resources/extensions/browser-tools/package.json +3 -1
  9. package/dist/resources/extensions/cmux/index.js +55 -1
  10. package/dist/resources/extensions/context7/package.json +1 -1
  11. package/dist/resources/extensions/env-utils.js +29 -0
  12. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  13. package/dist/resources/extensions/github-sync/cli.js +284 -0
  14. package/dist/resources/extensions/github-sync/index.js +73 -0
  15. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  16. package/dist/resources/extensions/github-sync/sync.js +424 -0
  17. package/dist/resources/extensions/github-sync/templates.js +118 -0
  18. package/dist/resources/extensions/github-sync/types.js +7 -0
  19. package/dist/resources/extensions/google-search/package.json +3 -1
  20. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  21. package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
  22. package/dist/resources/extensions/gsd/auto-loop.js +597 -588
  23. package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
  24. package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
  25. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  26. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  27. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  28. package/dist/resources/extensions/gsd/auto.js +143 -96
  29. package/dist/resources/extensions/gsd/captures.js +9 -1
  30. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  31. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  32. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  33. package/dist/resources/extensions/gsd/commands.js +24 -3
  34. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  35. package/dist/resources/extensions/gsd/detection.js +1 -2
  36. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  37. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  38. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  39. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  40. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  41. package/dist/resources/extensions/gsd/doctor.js +204 -12
  42. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  43. package/dist/resources/extensions/gsd/export.js +1 -1
  44. package/dist/resources/extensions/gsd/files.js +6 -2
  45. package/dist/resources/extensions/gsd/forensics.js +1 -1
  46. package/dist/resources/extensions/gsd/git-service.js +15 -12
  47. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  48. package/dist/resources/extensions/gsd/index.js +24 -20
  49. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  50. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  51. package/dist/resources/extensions/gsd/package.json +1 -1
  52. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  53. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  54. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  55. package/dist/resources/extensions/gsd/preferences.js +8 -5
  56. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  57. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  59. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  60. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  61. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  62. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  63. package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
  64. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  65. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  66. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  67. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  68. package/dist/resources/extensions/gsd/state.js +1 -1
  69. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  70. package/dist/resources/extensions/gsd/worktree.js +35 -16
  71. package/dist/resources/extensions/mcp-client/index.js +14 -1
  72. package/dist/resources/extensions/remote-questions/status.js +2 -1
  73. package/dist/resources/extensions/remote-questions/store.js +2 -1
  74. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  75. package/dist/resources/extensions/subagent/index.js +12 -3
  76. package/dist/resources/extensions/subagent/isolation.js +2 -1
  77. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  78. package/dist/resources/extensions/universal-config/package.json +1 -1
  79. package/dist/welcome-screen.d.ts +12 -0
  80. package/dist/welcome-screen.js +53 -0
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  83. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  84. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  85. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  87. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  90. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  91. package/packages/pi-coding-agent/package.json +1 -1
  92. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  93. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  94. package/pkg/package.json +1 -1
  95. package/src/resources/extensions/cmux/index.ts +57 -1
  96. package/src/resources/extensions/env-utils.ts +31 -0
  97. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  98. package/src/resources/extensions/github-sync/cli.ts +364 -0
  99. package/src/resources/extensions/github-sync/index.ts +93 -0
  100. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  101. package/src/resources/extensions/github-sync/sync.ts +556 -0
  102. package/src/resources/extensions/github-sync/templates.ts +183 -0
  103. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  104. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  105. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  106. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  107. package/src/resources/extensions/github-sync/types.ts +47 -0
  108. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  109. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
  110. package/src/resources/extensions/gsd/auto-loop.ts +484 -546
  111. package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
  112. package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
  113. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  114. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  115. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  116. package/src/resources/extensions/gsd/auto.ts +139 -101
  117. package/src/resources/extensions/gsd/captures.ts +10 -1
  118. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  119. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  120. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  121. package/src/resources/extensions/gsd/commands.ts +26 -4
  122. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  123. package/src/resources/extensions/gsd/detection.ts +2 -2
  124. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  125. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  126. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  127. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  128. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  129. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  130. package/src/resources/extensions/gsd/doctor.ts +199 -14
  131. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  132. package/src/resources/extensions/gsd/export.ts +1 -1
  133. package/src/resources/extensions/gsd/files.ts +5 -3
  134. package/src/resources/extensions/gsd/forensics.ts +1 -1
  135. package/src/resources/extensions/gsd/git-service.ts +20 -10
  136. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  137. package/src/resources/extensions/gsd/index.ts +24 -17
  138. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  139. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  140. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  141. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  142. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  143. package/src/resources/extensions/gsd/preferences.ts +8 -5
  144. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  145. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  146. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  147. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  148. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  149. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  150. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  151. package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
  152. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  153. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  154. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  155. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  156. package/src/resources/extensions/gsd/state.ts +1 -1
  157. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  158. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  159. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  160. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  161. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  162. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  163. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  164. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  165. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  166. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  167. package/src/resources/extensions/gsd/types.ts +0 -1
  168. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  169. package/src/resources/extensions/gsd/worktree.ts +35 -15
  170. package/src/resources/extensions/mcp-client/index.ts +17 -1
  171. package/src/resources/extensions/remote-questions/status.ts +3 -1
  172. package/src/resources/extensions/remote-questions/store.ts +3 -1
  173. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  174. package/src/resources/extensions/subagent/index.ts +12 -3
  175. package/src/resources/extensions/subagent/isolation.ts +3 -1
  176. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  177. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  178. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  179. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  180. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  181. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  182. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  183. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  184. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  185. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  186. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  187. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  188. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -28,6 +28,7 @@ import { showConfirm } from "../shared/mod.js";
28
28
  import { debugLog } from "./debug-logger.js";
29
29
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
30
30
  import { parkMilestone, discardMilestone } from "./milestone-actions.js";
31
+ import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
31
32
  // ─── Re-exports (preserve public API for existing importers) ────────────────
32
33
  export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, } from "./milestone-ids.js";
33
34
  export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
@@ -153,8 +154,32 @@ function parseMilestoneSequenceFromProject(content) {
153
154
  /**
154
155
  * Read GSD-WORKFLOW.md and dispatch it to the LLM with a contextual note.
155
156
  * This is the only way the wizard triggers work — everything else is the LLM's job.
157
+ *
158
+ * When a unitType is provided, resolves the user's model preference for that
159
+ * phase (e.g., models.planning → "plan-milestone") and applies it before
160
+ * dispatching. This ensures guided-flow dispatches respect the same
161
+ * per-phase model preferences that auto-mode uses.
156
162
  */
157
- function dispatchWorkflow(pi, note, customType = "gsd-run") {
163
+ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType) {
164
+ // Apply model preference for this unit type (if configured)
165
+ if (ctx && unitType) {
166
+ const modelConfig = resolveModelWithFallbacksForUnit(unitType);
167
+ if (modelConfig) {
168
+ const availableModels = ctx.modelRegistry.getAvailable();
169
+ const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
170
+ for (const modelId of modelsToTry) {
171
+ // Resolve model from available models (same logic as auto-model-selection)
172
+ const model = resolveAvailableModel(modelId, availableModels, ctx.model?.provider);
173
+ if (!model)
174
+ continue;
175
+ const ok = await pi.setModel(model, { persist: false });
176
+ if (ok) {
177
+ debugLog("guided-flow-model-applied", { unitType, model: `${model.provider}/${model.id}` });
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ }
158
183
  const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
159
184
  const workflow = readFileSync(workflowPath, "utf-8");
160
185
  pi.sendMessage({
@@ -163,6 +188,31 @@ function dispatchWorkflow(pi, note, customType = "gsd-run") {
163
188
  display: false,
164
189
  }, { triggerTurn: true });
165
190
  }
191
+ /**
192
+ * Resolve a model ID string to a model object from available models.
193
+ * Handles "provider/model" and bare ID formats.
194
+ */
195
+ function resolveAvailableModel(modelId, availableModels, currentProvider) {
196
+ const slashIdx = modelId.indexOf("/");
197
+ if (slashIdx !== -1) {
198
+ const maybeProvider = modelId.substring(0, slashIdx);
199
+ const id = modelId.substring(slashIdx + 1);
200
+ const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
201
+ if (knownProviders.has(maybeProvider.toLowerCase())) {
202
+ const match = availableModels.find(m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
203
+ && m.id.toLowerCase() === id.toLowerCase());
204
+ if (match)
205
+ return match;
206
+ }
207
+ // Try matching the full string as a model ID (OpenRouter-style)
208
+ const lower = modelId.toLowerCase();
209
+ return availableModels.find(m => m.id.toLowerCase() === lower
210
+ || `${m.provider}/${m.id}`.toLowerCase() === lower);
211
+ }
212
+ // Bare ID — prefer current provider, then first available
213
+ const exactProviderMatch = availableModels.find(m => m.id === modelId && m.provider === currentProvider);
214
+ return exactProviderMatch ?? availableModels.find(m => m.id === modelId);
215
+ }
166
216
  /**
167
217
  * Build the discuss-and-plan prompt for a new milestone.
168
218
  * Used by all three "new milestone" paths (first ever, no active, all complete).
@@ -244,8 +294,8 @@ export async function showHeadlessMilestoneCreation(ctx, pi, basePath, seedConte
244
294
  const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath);
245
295
  // Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
246
296
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
247
- // Dispatch
248
- dispatchWorkflow(pi, prompt);
297
+ // Dispatch — headless milestone creation is a planning activity
298
+ await dispatchWorkflow(pi, prompt, "gsd-run", ctx, "plan-milestone");
249
299
  }
250
300
  // ─── Discuss Flow ─────────────────────────────────────────────────────────────
251
301
  /**
@@ -381,23 +431,23 @@ export async function showDiscuss(ctx, pi, basePath) {
381
431
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
382
432
  : basePrompt;
383
433
  pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
384
- dispatchWorkflow(pi, seed, "gsd-discuss");
434
+ await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
385
435
  }
386
436
  else if (choice === "discuss_fresh") {
387
437
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
388
438
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
389
439
  pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
390
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
440
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
391
441
  milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
392
442
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
393
- }), "gsd-discuss");
443
+ }), "gsd-discuss", ctx, "plan-milestone");
394
444
  }
395
445
  else if (choice === "skip_milestone") {
396
446
  const milestoneIds = findMilestoneIds(basePath);
397
447
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
398
448
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
399
449
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };
400
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
450
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
401
451
  }
402
452
  return;
403
453
  }
@@ -484,7 +534,7 @@ export async function showDiscuss(ctx, pi, basePath) {
484
534
  continue;
485
535
  }
486
536
  const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss });
487
- dispatchWorkflow(pi, prompt, "gsd-discuss");
537
+ await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-slice");
488
538
  // Wait for the discuss session to finish, then loop back to the picker
489
539
  await ctx.waitForIdle();
490
540
  invalidateAllCaches();
@@ -611,7 +661,7 @@ async function handleMilestoneActions(ctx, pi, basePath, milestoneId, milestoneT
611
661
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
612
662
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
613
663
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
614
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
664
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
615
665
  return true;
616
666
  }
617
667
  // "back" or null
@@ -729,7 +779,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
729
779
  if (isFirst) {
730
780
  // First ever — skip wizard, just ask directly
731
781
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
732
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath));
782
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath), "gsd-run", ctx, "plan-milestone");
733
783
  }
734
784
  else {
735
785
  const choice = await showNextAction(ctx, {
@@ -747,7 +797,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
747
797
  });
748
798
  if (choice === "new_milestone") {
749
799
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
750
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
800
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
751
801
  }
752
802
  }
753
803
  return;
@@ -779,7 +829,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
779
829
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
780
830
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
781
831
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
782
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
832
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
783
833
  }
784
834
  else if (choice === "status") {
785
835
  const { fireStatusViaCommand } = await import("./commands.js");
@@ -825,23 +875,23 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
825
875
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
826
876
  : basePrompt;
827
877
  pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
828
- dispatchWorkflow(pi, seed, "gsd-discuss");
878
+ await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
829
879
  }
830
880
  else if (choice === "discuss_fresh") {
831
881
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
832
882
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
833
883
  pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
834
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
884
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
835
885
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
836
886
  commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
837
- }), "gsd-discuss");
887
+ }), "gsd-discuss", ctx, "plan-milestone");
838
888
  }
839
889
  else if (choice === "skip_milestone") {
840
890
  const milestoneIds = findMilestoneIds(basePath);
841
891
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
842
892
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
843
893
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
844
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
894
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
845
895
  }
846
896
  return;
847
897
  }
@@ -893,24 +943,24 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
893
943
  inlineTemplate("secrets-manifest", "Secrets Manifest"),
894
944
  ].join("\n\n---\n\n");
895
945
  const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
896
- dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
946
+ await dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
897
947
  milestoneId, milestoneTitle, secretsOutputPath, inlinedTemplates: planMilestoneTemplates,
898
- }));
948
+ }), "gsd-run", ctx, "plan-milestone");
899
949
  }
900
950
  else if (choice === "discuss") {
901
951
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
902
952
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
903
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
953
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
904
954
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
905
955
  commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
906
- }));
956
+ }), "gsd-run", ctx, "plan-milestone");
907
957
  }
908
958
  else if (choice === "skip_milestone") {
909
959
  const milestoneIds = findMilestoneIds(basePath);
910
960
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
911
961
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
912
962
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
913
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
963
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
914
964
  }
915
965
  else if (choice === "discard_milestone") {
916
966
  const confirmed = await showConfirm(ctx, {
@@ -1021,18 +1071,18 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1021
1071
  inlineTemplate("plan", "Slice Plan"),
1022
1072
  inlineTemplate("task-plan", "Task Plan"),
1023
1073
  ].join("\n\n---\n\n");
1024
- dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
1074
+ await dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
1025
1075
  milestoneId, sliceId, sliceTitle, inlinedTemplates: planSliceTemplates,
1026
- }));
1076
+ }), "gsd-run", ctx, "plan-slice");
1027
1077
  }
1028
1078
  else if (choice === "discuss") {
1029
- dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }));
1079
+ await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "plan-slice");
1030
1080
  }
1031
1081
  else if (choice === "research") {
1032
1082
  const researchTemplates = inlineTemplate("research", "Research");
1033
- dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
1083
+ await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
1034
1084
  milestoneId, sliceId, sliceTitle, inlinedTemplates: researchTemplates,
1035
- }));
1085
+ }), "gsd-run", ctx, "research-slice");
1036
1086
  }
1037
1087
  else if (choice === "status") {
1038
1088
  const { fireStatusViaCommand } = await import("./commands.js");
@@ -1075,9 +1125,9 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1075
1125
  inlineTemplate("slice-summary", "Slice Summary"),
1076
1126
  inlineTemplate("uat", "UAT"),
1077
1127
  ].join("\n\n---\n\n");
1078
- dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
1128
+ await dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
1079
1129
  workingDirectory: basePath, milestoneId, sliceId, sliceTitle, inlinedTemplates: completeSliceTemplates,
1080
- }));
1130
+ }), "gsd-run", ctx, "complete-slice");
1081
1131
  }
1082
1132
  else if (choice === "status") {
1083
1133
  const { fireStatusViaCommand } = await import("./commands.js");
@@ -1138,15 +1188,15 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1138
1188
  }
1139
1189
  if (choice === "execute") {
1140
1190
  if (hasInterrupted) {
1141
- dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
1191
+ await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
1142
1192
  milestoneId, sliceId,
1143
- }));
1193
+ }), "gsd-run", ctx, "execute-task");
1144
1194
  }
1145
1195
  else {
1146
1196
  const executeTaskTemplates = inlineTemplate("task-summary", "Task Summary");
1147
- dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
1197
+ await dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
1148
1198
  milestoneId, sliceId, taskId, taskTitle, inlinedTemplates: executeTaskTemplates,
1149
- }));
1199
+ }), "gsd-run", ctx, "execute-task");
1150
1200
  }
1151
1201
  }
1152
1202
  else if (choice === "status") {
@@ -41,6 +41,7 @@ import { join } from "node:path";
41
41
  import { existsSync, readFileSync } from "node:fs";
42
42
  import { homedir } from "node:os";
43
43
  import { shortcutDesc } from "../shared/mod.js";
44
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
44
45
  import { Text } from "@gsd/pi-tui";
45
46
  import { pauseAutoForProviderError, classifyProviderError } from "./provider-error-pause.js";
46
47
  import { toPosixPath } from "../shared/mod.js";
@@ -52,7 +53,7 @@ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js"
52
53
  // Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
53
54
  function warnDeprecatedAgentInstructions() {
54
55
  const paths = [
55
- join(homedir(), ".gsd", "agent-instructions.md"),
56
+ join(gsdHome, "agent-instructions.md"),
56
57
  join(process.cwd(), ".gsd", "agent-instructions.md"),
57
58
  ];
58
59
  for (const p of paths) {
@@ -65,6 +66,24 @@ function warnDeprecatedAgentInstructions() {
65
66
  }
66
67
  // ── Depth verification state ──────────────────────────────────────────────
67
68
  let depthVerificationDone = false;
69
+ // ── DB lazy-open helper ───────────────────────────────────────────────────
70
+ // In manual sessions (no auto-mode), the DB is never opened by bootstrapAutoSession.
71
+ // This helper ensures the DB is lazily opened on first tool call that needs it.
72
+ async function ensureDbOpen() {
73
+ try {
74
+ const db = await import("./gsd-db.js");
75
+ if (db.isDbAvailable())
76
+ return true;
77
+ const dbPath = join(process.cwd(), ".gsd", "gsd.db");
78
+ if (existsSync(dbPath)) {
79
+ return db.openDatabase(dbPath);
80
+ }
81
+ return false;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
68
87
  // ── Queue phase tracking ──────────────────────────────────────────────────
69
88
  // When true, the LLM is in a queue flow writing CONTEXT.md files.
70
89
  // The write-gate applies during queue flows just like discussion flows.
@@ -227,13 +246,8 @@ export default function (pi) {
227
246
  when_context: Type.Optional(Type.String({ description: "When/context for the decision (e.g. milestone ID)" })),
228
247
  }),
229
248
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
230
- // Check DB availability
231
- let dbAvailable = false;
232
- try {
233
- const db = await import("./gsd-db.js");
234
- dbAvailable = db.isDbAvailable();
235
- }
236
- catch { /* dynamic import failed */ }
249
+ // Ensure DB is open (lazy-open on first tool call in manual sessions)
250
+ const dbAvailable = await ensureDbOpen();
237
251
  if (!dbAvailable) {
238
252
  return {
239
253
  content: [{ type: "text", text: "Error: GSD database is not available. Cannot save decision." }],
@@ -289,12 +303,7 @@ export default function (pi) {
289
303
  supporting_slices: Type.Optional(Type.String({ description: "Supporting slices" })),
290
304
  }),
291
305
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
292
- let dbAvailable = false;
293
- try {
294
- const db = await import("./gsd-db.js");
295
- dbAvailable = db.isDbAvailable();
296
- }
297
- catch { /* dynamic import failed */ }
306
+ const dbAvailable = await ensureDbOpen();
298
307
  if (!dbAvailable) {
299
308
  return {
300
309
  content: [{ type: "text", text: "Error: GSD database is not available. Cannot update requirement." }],
@@ -364,12 +373,7 @@ export default function (pi) {
364
373
  content: Type.String({ description: "The full markdown content of the artifact" }),
365
374
  }),
366
375
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
367
- let dbAvailable = false;
368
- try {
369
- const db = await import("./gsd-db.js");
370
- dbAvailable = db.isDbAvailable();
371
- }
372
- catch { /* dynamic import failed */ }
376
+ const dbAvailable = await ensureDbOpen();
373
377
  if (!dbAvailable) {
374
378
  return {
375
379
  content: [{ type: "text", text: "Error: GSD database is not available. Cannot save artifact." }],
@@ -2,7 +2,7 @@
2
2
  // Pure functions that take file content (string) and return typed data.
3
3
  // Zero Pi dependencies — uses only exported helpers from files.ts.
4
4
  import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.js';
5
- import { normalizeStringArray } from '../../shared/mod.js';
5
+ import { normalizeStringArray } from '../../shared/format-utils.js';
6
6
  // Re-export PlanningProjectMeta — not in types.ts yet, use string for project field
7
7
  // Actually PlanningProjectMeta isn't in types.ts — project is stored as string | null.
8
8
  // We'll keep parseOldProject returning a simple shape.
@@ -518,6 +518,43 @@ export function nativeAddAll(basePath) {
518
518
  }
519
519
  gitFileExec(basePath, ["add", "-A"]);
520
520
  }
521
+ /**
522
+ * Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
523
+ * Excluded paths are never hashed by git, preventing hangs on large
524
+ * untracked artifact trees (57GB+, 11K+ files). See #1605.
525
+ *
526
+ * Falls back to plain `git add -A` when no exclusions are provided.
527
+ * Always uses the CLI path (not libgit2) because libgit2's add_all
528
+ * does not support pathspec exclusion syntax.
529
+ *
530
+ * When excluded paths are already covered by .gitignore, git may exit
531
+ * with code 1 and an "ignored by .gitignore" warning. This is harmless
532
+ * (the staging succeeds for all non-ignored files) and is suppressed.
533
+ */
534
+ export function nativeAddAllWithExclusions(basePath, exclusions) {
535
+ if (exclusions.length === 0) {
536
+ nativeAddAll(basePath);
537
+ return;
538
+ }
539
+ const pathspecs = exclusions.map(e => `:!${e}`);
540
+ try {
541
+ execFileSync("git", ["add", "-A", "--", ...pathspecs], {
542
+ cwd: basePath,
543
+ stdio: ["ignore", "pipe", "pipe"],
544
+ encoding: "utf-8",
545
+ env: GIT_NO_PROMPT_ENV,
546
+ });
547
+ }
548
+ catch (err) {
549
+ // git exits 1 when pathspec exclusions reference paths already covered
550
+ // by .gitignore. The staging itself succeeds — only suppress that case.
551
+ const stderr = err?.stderr ?? "";
552
+ if (stderr.includes("ignored by one of your .gitignore files")) {
553
+ return;
554
+ }
555
+ throw new GSDError(GSD_GIT_ERROR, `git add -A with exclusions failed in ${basePath}: ${getErrorMessage(err)}`);
556
+ }
557
+ }
521
558
  /**
522
559
  * Stage specific files.
523
560
  * Native: libgit2 index add.
@@ -5,7 +5,7 @@
5
5
  "type": "module",
6
6
  "pi": {
7
7
  "extensions": [
8
- "./index.ts"
8
+ "./index.js"
9
9
  ]
10
10
  }
11
11
  }
@@ -260,18 +260,6 @@ export function resolveInlineLevel() {
260
260
  case "quality": return "full";
261
261
  }
262
262
  }
263
- /**
264
- * Resolve the compression strategy from the active token profile.
265
- * budget/balanced -> "compress", quality -> "truncate".
266
- * Explicit preference always wins.
267
- */
268
- export function resolveCompressionStrategy() {
269
- const prefs = loadEffectiveGSDPreferences();
270
- if (prefs?.preferences.compression_strategy)
271
- return prefs.preferences.compression_strategy;
272
- const profile = resolveEffectiveProfile();
273
- return profile === "quality" ? "truncate" : "compress";
274
- }
275
263
  /**
276
264
  * Resolve the context selection mode from the active token profile.
277
265
  * budget -> "smart", balanced/quality -> "full".
@@ -62,10 +62,10 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
62
62
  "verification_auto_fix",
63
63
  "verification_max_retries",
64
64
  "search_provider",
65
- "compression_strategy",
66
65
  "context_selection",
67
66
  "widget_mode",
68
67
  "reactive_execution",
68
+ "github",
69
69
  ]);
70
70
  /** Canonical list of all dispatch unit types. */
71
71
  export const KNOWN_UNIT_TYPES = [
@@ -6,7 +6,7 @@
6
6
  * together with any errors and warnings.
7
7
  */
8
8
  import { VALID_BRANCH_NAME } from "./git-service.js";
9
- import { normalizeStringArray } from "../shared/mod.js";
9
+ import { normalizeStringArray } from "../shared/format-utils.js";
10
10
  import { KNOWN_PREFERENCE_KEYS, KNOWN_UNIT_TYPES, SKILL_ACTIONS, } from "./preferences-types.js";
11
11
  const VALID_TOKEN_PROFILES = new Set(["budget", "balanced", "quality"]);
12
12
  export function validatePreferences(preferences) {
@@ -707,16 +707,6 @@ export function validatePreferences(preferences) {
707
707
  errors.push("auto_report must be a boolean");
708
708
  }
709
709
  }
710
- // ─── Compression Strategy ───────────────────────────────────────────
711
- if (preferences.compression_strategy !== undefined) {
712
- const validStrategies = new Set(["truncate", "compress"]);
713
- if (typeof preferences.compression_strategy === "string" && validStrategies.has(preferences.compression_strategy)) {
714
- validated.compression_strategy = preferences.compression_strategy;
715
- }
716
- else {
717
- errors.push(`compression_strategy must be one of: truncate, compress`);
718
- }
719
- }
720
710
  // ─── Context Selection ──────────────────────────────────────────────
721
711
  if (preferences.context_selection !== undefined) {
722
712
  const validModes = new Set(["full", "smart"]);
@@ -727,5 +717,63 @@ export function validatePreferences(preferences) {
727
717
  errors.push(`context_selection must be one of: full, smart`);
728
718
  }
729
719
  }
720
+ // ─── GitHub Sync ────────────────────────────────────────────────────────
721
+ if (preferences.github !== undefined) {
722
+ if (typeof preferences.github === "object" && preferences.github !== null) {
723
+ const gh = preferences.github;
724
+ const validGh = {};
725
+ if (gh.enabled !== undefined) {
726
+ if (typeof gh.enabled === "boolean")
727
+ validGh.enabled = gh.enabled;
728
+ else
729
+ errors.push("github.enabled must be a boolean");
730
+ }
731
+ if (gh.repo !== undefined) {
732
+ if (typeof gh.repo === "string" && gh.repo.includes("/"))
733
+ validGh.repo = gh.repo;
734
+ else
735
+ errors.push('github.repo must be a string in "owner/repo" format');
736
+ }
737
+ if (gh.project !== undefined) {
738
+ const p = typeof gh.project === "number" ? gh.project : Number(gh.project);
739
+ if (Number.isFinite(p) && p > 0)
740
+ validGh.project = Math.floor(p);
741
+ else
742
+ errors.push("github.project must be a positive number");
743
+ }
744
+ if (gh.labels !== undefined) {
745
+ if (Array.isArray(gh.labels) && gh.labels.every((l) => typeof l === "string")) {
746
+ validGh.labels = gh.labels;
747
+ }
748
+ else {
749
+ errors.push("github.labels must be an array of strings");
750
+ }
751
+ }
752
+ if (gh.auto_link_commits !== undefined) {
753
+ if (typeof gh.auto_link_commits === "boolean")
754
+ validGh.auto_link_commits = gh.auto_link_commits;
755
+ else
756
+ errors.push("github.auto_link_commits must be a boolean");
757
+ }
758
+ if (gh.slice_prs !== undefined) {
759
+ if (typeof gh.slice_prs === "boolean")
760
+ validGh.slice_prs = gh.slice_prs;
761
+ else
762
+ errors.push("github.slice_prs must be a boolean");
763
+ }
764
+ const knownGhKeys = new Set(["enabled", "repo", "project", "labels", "auto_link_commits", "slice_prs"]);
765
+ for (const key of Object.keys(gh)) {
766
+ if (!knownGhKeys.has(key)) {
767
+ warnings.push(`unknown github key "${key}" — ignored`);
768
+ }
769
+ }
770
+ if (Object.keys(validGh).length > 0) {
771
+ validated.github = validGh;
772
+ }
773
+ }
774
+ else {
775
+ errors.push("github must be an object");
776
+ }
777
+ }
730
778
  return { preferences: validated, errors, warnings };
731
779
  }
@@ -12,9 +12,10 @@
12
12
  import { existsSync, readFileSync } from "node:fs";
13
13
  import { homedir } from "node:os";
14
14
  import { join } from "node:path";
15
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
16
  import { gsdRoot } from "./paths.js";
16
17
  import { parse as parseYaml } from "yaml";
17
- import { normalizeStringArray } from "../shared/mod.js";
18
+ import { normalizeStringArray } from "../shared/format-utils.js";
18
19
  import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
19
20
  import { MODE_DEFAULTS, } from "./preferences-types.js";
20
21
  import { validatePreferences } from "./preferences-validation.js";
@@ -24,16 +25,16 @@ export { validatePreferences } from "./preferences-validation.js";
24
25
  // ─── Re-exports: skills ─────────────────────────────────────────────────────
25
26
  export { resolveAllSkillReferences, resolveSkillDiscoveryMode, resolveSkillStalenessDays, } from "./preferences-skills.js";
26
27
  // ─── Re-exports: models ─────────────────────────────────────────────────────
27
- export { resolveModelForUnit, resolveModelWithFallbacksForUnit, getNextFallbackModel, isTransientNetworkError, validateModelId, updatePreferencesModels, resolveDynamicRoutingConfig, resolveAutoSupervisorConfig, resolveProfileDefaults, resolveEffectiveProfile, resolveInlineLevel, resolveCompressionStrategy, resolveContextSelection, resolveSearchProviderFromPreferences, } from "./preferences-models.js";
28
+ export { resolveModelForUnit, resolveModelWithFallbacksForUnit, getNextFallbackModel, isTransientNetworkError, validateModelId, updatePreferencesModels, resolveDynamicRoutingConfig, resolveAutoSupervisorConfig, resolveProfileDefaults, resolveEffectiveProfile, resolveInlineLevel, resolveContextSelection, resolveSearchProviderFromPreferences, } from "./preferences-models.js";
28
29
  // ─── Path Constants & Getters ───────────────────────────────────────────────
29
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
30
+ const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
30
31
  const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
31
32
  function projectPreferencesPath() {
32
33
  return join(gsdRoot(process.cwd()), "preferences.md");
33
34
  }
34
35
  // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
35
36
  // Check uppercase as a fallback so those files aren't silently ignored.
36
- const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(homedir(), ".gsd", "PREFERENCES.md");
37
+ const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
37
38
  function projectPreferencesPathUppercase() {
38
39
  return join(gsdRoot(process.cwd()), "PREFERENCES.md");
39
40
  }
@@ -199,10 +200,12 @@ function mergePreferences(base, override) {
199
200
  verification_auto_fix: override.verification_auto_fix ?? base.verification_auto_fix,
200
201
  verification_max_retries: override.verification_max_retries ?? base.verification_max_retries,
201
202
  search_provider: override.search_provider ?? base.search_provider,
202
- compression_strategy: override.compression_strategy ?? base.compression_strategy,
203
203
  context_selection: override.context_selection ?? base.context_selection,
204
204
  auto_visualize: override.auto_visualize ?? base.auto_visualize,
205
205
  auto_report: override.auto_report ?? base.auto_report,
206
+ github: (base.github || override.github)
207
+ ? { ...(base.github ?? {}), ...(override.github ?? {}) }
208
+ : undefined,
206
209
  };
207
210
  }
208
211
  function mergeStringLists(base, override) {