gsd-pi 2.38.0-dev.bc2e21e → 2.38.0-dev.d533afb

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 (99) hide show
  1. package/dist/resource-loader.js +34 -1
  2. package/dist/resources/extensions/github-sync/cli.js +284 -0
  3. package/dist/resources/extensions/github-sync/index.js +73 -0
  4. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  5. package/dist/resources/extensions/github-sync/sync.js +424 -0
  6. package/dist/resources/extensions/github-sync/templates.js +118 -0
  7. package/dist/resources/extensions/github-sync/types.js +7 -0
  8. package/dist/resources/extensions/gsd/auto/session.js +3 -23
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  10. package/dist/resources/extensions/gsd/auto-loop.js +292 -263
  11. package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
  12. package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
  13. package/dist/resources/extensions/gsd/auto-start.js +7 -1
  14. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  15. package/dist/resources/extensions/gsd/auto.js +143 -80
  16. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  17. package/dist/resources/extensions/gsd/commands.js +2 -1
  18. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  19. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  20. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  21. package/dist/resources/extensions/gsd/doctor.js +20 -1
  22. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  23. package/dist/resources/extensions/gsd/files.js +4 -0
  24. package/dist/resources/extensions/gsd/git-service.js +15 -12
  25. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  26. package/dist/resources/extensions/gsd/index.js +22 -19
  27. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  28. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  29. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  30. package/dist/resources/extensions/gsd/preferences-validation.js +58 -10
  31. package/dist/resources/extensions/gsd/preferences.js +4 -2
  32. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
  33. package/dist/resources/extensions/gsd/repo-identity.js +19 -3
  34. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  35. package/dist/resources/extensions/mcp-client/index.js +14 -1
  36. package/package.json +1 -1
  37. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  38. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  39. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  40. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  42. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  43. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  44. package/src/resources/extensions/github-sync/cli.ts +364 -0
  45. package/src/resources/extensions/github-sync/index.ts +93 -0
  46. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  47. package/src/resources/extensions/github-sync/sync.ts +556 -0
  48. package/src/resources/extensions/github-sync/templates.ts +183 -0
  49. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  50. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  51. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  52. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  53. package/src/resources/extensions/github-sync/types.ts +47 -0
  54. package/src/resources/extensions/gsd/auto/session.ts +3 -25
  55. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  56. package/src/resources/extensions/gsd/auto-loop.ts +382 -360
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  58. package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
  59. package/src/resources/extensions/gsd/auto-start.ts +11 -1
  60. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  61. package/src/resources/extensions/gsd/auto.ts +139 -86
  62. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  63. package/src/resources/extensions/gsd/commands.ts +2 -2
  64. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  65. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  66. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  67. package/src/resources/extensions/gsd/doctor.ts +22 -1
  68. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  69. package/src/resources/extensions/gsd/files.ts +3 -1
  70. package/src/resources/extensions/gsd/git-service.ts +20 -10
  71. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  72. package/src/resources/extensions/gsd/index.ts +21 -16
  73. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  74. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  75. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  76. package/src/resources/extensions/gsd/preferences-validation.ts +50 -10
  77. package/src/resources/extensions/gsd/preferences.ts +3 -2
  78. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
  79. package/src/resources/extensions/gsd/repo-identity.ts +20 -3
  80. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  82. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  83. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  84. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  85. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  86. package/src/resources/extensions/gsd/types.ts +0 -1
  87. package/src/resources/extensions/mcp-client/index.ts +17 -1
  88. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  89. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  90. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  91. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  92. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  93. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  94. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  95. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  96. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  97. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  98. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  99. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -51,10 +51,12 @@ function modelToProviderId(model: string): string | null {
51
51
  const prefix = model.split("/")[0].toLowerCase();
52
52
  // Map known prefixes to registry IDs
53
53
  const prefixMap: Record<string, string> = {
54
+ "anthropic-vertex": "anthropic-vertex",
54
55
  openrouter: "openrouter",
55
56
  groq: "groq",
56
57
  mistral: "mistral",
57
58
  google: "google",
59
+ "google-vertex": "google-vertex",
58
60
  anthropic: "anthropic",
59
61
  openai: "openai",
60
62
  "github-copilot": "github-copilot",
@@ -88,11 +90,20 @@ function collectConfiguredModelProviders(): Set<string> {
88
90
 
89
91
  const modelEntries = typeof models === "object" ? Object.values(models) : [];
90
92
  for (const entry of modelEntries) {
91
- const modelId = typeof entry === "string" ? entry
92
- : typeof entry === "object" && entry !== null && "model" in entry
93
- ? String((entry as { model: unknown }).model)
94
- : null;
95
- if (modelId) {
93
+ if (typeof entry === "string") {
94
+ const pid = modelToProviderId(entry);
95
+ if (pid) providers.add(pid);
96
+ continue;
97
+ }
98
+
99
+ if (typeof entry === "object" && entry !== null && "model" in entry) {
100
+ const configuredProvider = "provider" in entry ? (entry as { provider?: unknown }).provider : undefined;
101
+ if (typeof configuredProvider === "string" && configuredProvider.trim().length > 0) {
102
+ providers.add(configuredProvider);
103
+ continue;
104
+ }
105
+
106
+ const modelId = String((entry as { model: unknown }).model);
96
107
  const pid = modelToProviderId(modelId);
97
108
  if (pid) providers.add(pid);
98
109
  }
@@ -175,7 +186,9 @@ function checkLlmProviders(): ProviderCheckResult[] {
175
186
 
176
187
  for (const providerId of required) {
177
188
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
178
- const label = info?.label ?? providerId;
189
+ const label = providerId === "anthropic-vertex"
190
+ ? "Anthropic Vertex"
191
+ : info?.label ?? providerId;
179
192
  const lookup = resolveKey(providerId);
180
193
 
181
194
  if (!lookup.found) {
@@ -196,14 +209,18 @@ function checkLlmProviders(): ProviderCheckResult[] {
196
209
  continue;
197
210
  }
198
211
 
199
- const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
212
+ const envVar = providerId === "anthropic-vertex"
213
+ ? "ANTHROPIC_VERTEX_PROJECT_ID"
214
+ : info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
200
215
  results.push({
201
216
  name: providerId,
202
217
  label,
203
218
  category: "llm",
204
219
  status: "error",
205
- message: `${label} — no API key found`,
206
- detail: info?.hasOAuth
220
+ message: `${label} — not configured`,
221
+ detail: providerId === "anthropic-vertex"
222
+ ? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC"
223
+ : info?.hasOAuth
207
224
  ? `Run /gsd keys to authenticate`
208
225
  : `Set ${envVar} or run /gsd keys`,
209
226
  required: true,
@@ -280,9 +280,24 @@ async function markSliceDoneInRoadmap(basePath: string, milestoneId: string, sli
280
280
  }
281
281
  }
282
282
 
283
+ async function markSliceUndoneInRoadmap(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise<void> {
284
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
285
+ if (!roadmapPath) return;
286
+ const content = await loadFile(roadmapPath);
287
+ if (!content) return;
288
+ const updated = content.replace(
289
+ new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"),
290
+ `$1[ ] **${sliceId}:`,
291
+ );
292
+ if (updated !== content) {
293
+ await saveFile(roadmapPath, updated);
294
+ fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`);
295
+ }
296
+ }
297
+
283
298
  function matchesScope(unitId: string, scope?: string): boolean {
284
299
  if (!scope) return true;
285
- return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
300
+ return unitId === scope || unitId.startsWith(`${scope}/`);
286
301
  }
287
302
 
288
303
  function auditRequirements(content: string | null): DoctorIssue[] {
@@ -863,6 +878,12 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
863
878
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
864
879
  fixable: true,
865
880
  });
881
+ if (!allTasksDone) {
882
+ dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`);
883
+ if (shouldFix("slice_checked_missing_summary")) {
884
+ await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
885
+ }
886
+ }
866
887
  }
867
888
 
868
889
  if (slice.done && !hasSliceUat) {
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
1
+ import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
2
 
3
3
  type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI, reason?: string) => Promise<void>;
4
4
 
@@ -10,7 +10,7 @@ export function registerExitCommand(
10
10
  description: "Exit GSD gracefully",
11
11
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
12
12
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
13
- const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
13
+ const stopAuto = deps.stopAuto ?? (await importExtensionModule<typeof import("./auto.js")>(import.meta.url, "./auto.js")).stopAuto;
14
14
  await stopAuto(ctx, pi, "Graceful exit");
15
15
  ctx.shutdown();
16
16
  },
@@ -775,7 +775,7 @@ export function parseTaskPlanIO(content: string): { inputFiles: string[]; output
775
775
  * The four UAT classification types recognised by GSD auto-mode.
776
776
  * `undefined` is returned (not this union) when no type can be determined.
777
777
  */
778
- export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed';
778
+ export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' | 'mixed' | 'browser-executable' | 'runtime-executable';
779
779
 
780
780
  /**
781
781
  * Extract the UAT type from a UAT file's raw content.
@@ -799,6 +799,8 @@ export function extractUatType(content: string): UatType | undefined {
799
799
  const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
800
800
 
801
801
  if (rawValue.startsWith('artifact-driven')) return 'artifact-driven';
802
+ if (rawValue.startsWith('browser-executable')) return 'browser-executable';
803
+ if (rawValue.startsWith('runtime-executable')) return 'runtime-executable';
802
804
  if (rawValue.startsWith('live-runtime')) return 'live-runtime';
803
805
  if (rawValue.startsWith('human-experience')) return 'human-experience';
804
806
  if (rawValue.startsWith('mixed')) return 'mixed';
@@ -24,7 +24,7 @@ import {
24
24
  nativeDetectMainBranch,
25
25
  nativeBranchExists,
26
26
  nativeHasChanges,
27
- nativeAddAll,
27
+ nativeAddAllWithExclusions,
28
28
  nativeResetPaths,
29
29
  nativeHasStagedChanges,
30
30
  nativeCommit,
@@ -95,6 +95,8 @@ export interface TaskCommitContext {
95
95
  oneLiner?: string;
96
96
  /** Files modified by this task (from task summary frontmatter) */
97
97
  keyFiles?: string[];
98
+ /** GitHub issue number — appends "Resolves #N" trailer when set. */
99
+ issueNumber?: number;
98
100
  }
99
101
 
100
102
  /**
@@ -118,12 +120,22 @@ export function buildTaskCommitMessage(ctx: TaskCommitContext): string {
118
120
  const subject = `${type}(${scope}): ${truncated}`;
119
121
 
120
122
  // Build body with key files if available
123
+ const bodyParts: string[] = [];
124
+
121
125
  if (ctx.keyFiles && ctx.keyFiles.length > 0) {
122
126
  const fileLines = ctx.keyFiles
123
127
  .slice(0, 8) // cap at 8 files to keep commit concise
124
128
  .map(f => `- ${f}`)
125
129
  .join("\n");
126
- return `${subject}\n\n${fileLines}`;
130
+ bodyParts.push(fileLines);
131
+ }
132
+
133
+ if (ctx.issueNumber) {
134
+ bodyParts.push(`Resolves #${ctx.issueNumber}`);
135
+ }
136
+
137
+ if (bodyParts.length > 0) {
138
+ return `${subject}\n\n${bodyParts.join("\n\n")}`;
127
139
  }
128
140
 
129
141
  return subject;
@@ -373,7 +385,9 @@ export class GitServiceImpl {
373
385
  this._runtimeFilesCleanedUp = true;
374
386
  }
375
387
 
376
- // Stage everything, then unstage excluded paths.
388
+ // Stage everything using pathspec exclusions so excluded paths are never
389
+ // hashed by git. The old approach of `git add -A` followed by unstaging
390
+ // hangs indefinitely on repos with large untracked artifact trees (#1605).
377
391
  //
378
392
  // Exclude only RUNTIME paths from staging — not the entire .gsd/ directory.
379
393
  // When .gsd/milestones/ files are already tracked in the index (projects
@@ -383,13 +397,9 @@ export class GitServiceImpl {
383
397
  // the second half of a milestone's artifacts are never committed (#1326).
384
398
  //
385
399
  // If .gsd/ IS in .gitignore (the default for external state projects),
386
- // git add -A already skips it and the reset is a harmless no-op.
387
- nativeAddAll(this.basePath);
388
-
389
- const runtimeExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
390
- for (const exclusion of runtimeExclusions) {
391
- try { nativeResetPaths(this.basePath, [exclusion]); } catch { /* path not staged — ignore */ }
392
- }
400
+ // git add -A already skips it and the exclusions are harmless no-ops.
401
+ const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
402
+ nativeAddAllWithExclusions(this.basePath, allExclusions);
393
403
  }
394
404
 
395
405
  /** Tracks whether runtime file cleanup has run this session. */
@@ -34,6 +34,7 @@ import { showConfirm } from "../shared/mod.js";
34
34
  import { debugLog } from "./debug-logger.js";
35
35
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
36
36
  import { parkMilestone, discardMilestone } from "./milestone-actions.js";
37
+ import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
37
38
 
38
39
  // ─── Re-exports (preserve public API for existing importers) ────────────────
39
40
  export {
@@ -190,8 +191,40 @@ type UIContext = ExtensionContext;
190
191
  /**
191
192
  * Read GSD-WORKFLOW.md and dispatch it to the LLM with a contextual note.
192
193
  * This is the only way the wizard triggers work — everything else is the LLM's job.
194
+ *
195
+ * When a unitType is provided, resolves the user's model preference for that
196
+ * phase (e.g., models.planning → "plan-milestone") and applies it before
197
+ * dispatching. This ensures guided-flow dispatches respect the same
198
+ * per-phase model preferences that auto-mode uses.
193
199
  */
194
- function dispatchWorkflow(pi: ExtensionAPI, note: string, customType = "gsd-run"): void {
200
+ async function dispatchWorkflow(
201
+ pi: ExtensionAPI,
202
+ note: string,
203
+ customType = "gsd-run",
204
+ ctx?: ExtensionContext,
205
+ unitType?: string,
206
+ ): Promise<void> {
207
+ // Apply model preference for this unit type (if configured)
208
+ if (ctx && unitType) {
209
+ const modelConfig = resolveModelWithFallbacksForUnit(unitType);
210
+ if (modelConfig) {
211
+ const availableModels = ctx.modelRegistry.getAvailable();
212
+ const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
213
+
214
+ for (const modelId of modelsToTry) {
215
+ // Resolve model from available models (same logic as auto-model-selection)
216
+ const model = resolveAvailableModel(modelId, availableModels, ctx.model?.provider);
217
+ if (!model) continue;
218
+
219
+ const ok = await pi.setModel(model, { persist: false });
220
+ if (ok) {
221
+ debugLog("guided-flow-model-applied", { unitType, model: `${model.provider}/${model.id}` });
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ }
227
+
195
228
  const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
196
229
  const workflow = readFileSync(workflowPath, "utf-8");
197
230
 
@@ -205,6 +238,45 @@ function dispatchWorkflow(pi: ExtensionAPI, note: string, customType = "gsd-run"
205
238
  );
206
239
  }
207
240
 
241
+ /**
242
+ * Resolve a model ID string to a model object from available models.
243
+ * Handles "provider/model" and bare ID formats.
244
+ */
245
+ function resolveAvailableModel<T extends { id: string; provider: string }>(
246
+ modelId: string,
247
+ availableModels: T[],
248
+ currentProvider: string | undefined,
249
+ ): T | undefined {
250
+ const slashIdx = modelId.indexOf("/");
251
+
252
+ if (slashIdx !== -1) {
253
+ const maybeProvider = modelId.substring(0, slashIdx);
254
+ const id = modelId.substring(slashIdx + 1);
255
+
256
+ const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
257
+ if (knownProviders.has(maybeProvider.toLowerCase())) {
258
+ const match = availableModels.find(
259
+ m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
260
+ && m.id.toLowerCase() === id.toLowerCase(),
261
+ );
262
+ if (match) return match;
263
+ }
264
+
265
+ // Try matching the full string as a model ID (OpenRouter-style)
266
+ const lower = modelId.toLowerCase();
267
+ return availableModels.find(
268
+ m => m.id.toLowerCase() === lower
269
+ || `${m.provider}/${m.id}`.toLowerCase() === lower,
270
+ );
271
+ }
272
+
273
+ // Bare ID — prefer current provider, then first available
274
+ const exactProviderMatch = availableModels.find(
275
+ m => m.id === modelId && m.provider === currentProvider,
276
+ );
277
+ return exactProviderMatch ?? availableModels.find(m => m.id === modelId);
278
+ }
279
+
208
280
  /**
209
281
  * Build the discuss-and-plan prompt for a new milestone.
210
282
  * Used by all three "new milestone" paths (first ever, no active, all complete).
@@ -301,8 +373,8 @@ export async function showHeadlessMilestoneCreation(
301
373
  // Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
302
374
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
303
375
 
304
- // Dispatch
305
- dispatchWorkflow(pi, prompt);
376
+ // Dispatch — headless milestone creation is a planning activity
377
+ await dispatchWorkflow(pi, prompt, "gsd-run", ctx, "plan-milestone");
306
378
  }
307
379
 
308
380
 
@@ -467,21 +539,21 @@ export async function showDiscuss(
467
539
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
468
540
  : basePrompt;
469
541
  pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
470
- dispatchWorkflow(pi, seed, "gsd-discuss");
542
+ await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
471
543
  } else if (choice === "discuss_fresh") {
472
544
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
473
545
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
474
546
  pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
475
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
547
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
476
548
  milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
477
549
  commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
478
- }), "gsd-discuss");
550
+ }), "gsd-discuss", ctx, "plan-milestone");
479
551
  } else if (choice === "skip_milestone") {
480
552
  const milestoneIds = findMilestoneIds(basePath);
481
553
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
482
554
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
483
555
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };
484
- dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
556
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
485
557
  }
486
558
  return;
487
559
  }
@@ -580,7 +652,7 @@ export async function showDiscuss(
580
652
  }
581
653
 
582
654
  const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss });
583
- dispatchWorkflow(pi, prompt, "gsd-discuss");
655
+ await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-slice");
584
656
 
585
657
  // Wait for the discuss session to finish, then loop back to the picker
586
658
  await ctx.waitForIdle();
@@ -722,10 +794,10 @@ async function handleMilestoneActions(
722
794
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
723
795
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
724
796
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
725
- dispatchWorkflow(pi, buildDiscussPrompt(nextId,
797
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
726
798
  `New milestone ${nextId}.`,
727
799
  basePath
728
- ));
800
+ ), "gsd-run", ctx, "plan-milestone");
729
801
  return true;
730
802
  }
731
803
 
@@ -866,10 +938,10 @@ export async function showSmartEntry(
866
938
  if (isFirst) {
867
939
  // First ever — skip wizard, just ask directly
868
940
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
869
- dispatchWorkflow(pi, buildDiscussPrompt(nextId,
941
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
870
942
  `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`,
871
943
  basePath
872
- ));
944
+ ), "gsd-run", ctx, "plan-milestone");
873
945
  } else {
874
946
  const choice = await showNextAction(ctx, {
875
947
  title: "GSD — Get Shit Done",
@@ -887,10 +959,10 @@ export async function showSmartEntry(
887
959
 
888
960
  if (choice === "new_milestone") {
889
961
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
890
- dispatchWorkflow(pi, buildDiscussPrompt(nextId,
962
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
891
963
  `New milestone ${nextId}.`,
892
964
  basePath
893
- ));
965
+ ), "gsd-run", ctx, "plan-milestone");
894
966
  }
895
967
  }
896
968
  return;
@@ -926,10 +998,10 @@ export async function showSmartEntry(
926
998
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
927
999
 
928
1000
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
929
- dispatchWorkflow(pi, buildDiscussPrompt(nextId,
1001
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
930
1002
  `New milestone ${nextId}.`,
931
1003
  basePath
932
- ));
1004
+ ), "gsd-run", ctx, "plan-milestone");
933
1005
  } else if (choice === "status") {
934
1006
  const { fireStatusViaCommand } = await import("./commands.js");
935
1007
  await fireStatusViaCommand(ctx);
@@ -977,24 +1049,24 @@ export async function showSmartEntry(
977
1049
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
978
1050
  : basePrompt;
979
1051
  pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
980
- dispatchWorkflow(pi, seed, "gsd-discuss");
1052
+ await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
981
1053
  } else if (choice === "discuss_fresh") {
982
1054
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
983
1055
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
984
1056
  pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
985
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
1057
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
986
1058
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
987
1059
  commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
988
- }), "gsd-discuss");
1060
+ }), "gsd-discuss", ctx, "plan-milestone");
989
1061
  } else if (choice === "skip_milestone") {
990
1062
  const milestoneIds = findMilestoneIds(basePath);
991
1063
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
992
1064
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
993
1065
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
994
- dispatchWorkflow(pi, buildDiscussPrompt(nextId,
1066
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
995
1067
  `New milestone ${nextId}.`,
996
1068
  basePath
997
- ));
1069
+ ), "gsd-run", ctx, "plan-milestone");
998
1070
  }
999
1071
  return;
1000
1072
  }
@@ -1051,25 +1123,25 @@ export async function showSmartEntry(
1051
1123
  inlineTemplate("secrets-manifest", "Secrets Manifest"),
1052
1124
  ].join("\n\n---\n\n");
1053
1125
  const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
1054
- dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
1126
+ await dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
1055
1127
  milestoneId, milestoneTitle, secretsOutputPath, inlinedTemplates: planMilestoneTemplates,
1056
- }));
1128
+ }), "gsd-run", ctx, "plan-milestone");
1057
1129
  } else if (choice === "discuss") {
1058
1130
  const discussMilestoneTemplates = inlineTemplate("context", "Context");
1059
1131
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
1060
- dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
1132
+ await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
1061
1133
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
1062
1134
  commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
1063
- }));
1135
+ }), "gsd-run", ctx, "plan-milestone");
1064
1136
  } else if (choice === "skip_milestone") {
1065
1137
  const milestoneIds = findMilestoneIds(basePath);
1066
1138
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1067
1139
  const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
1068
1140
  pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
1069
- dispatchWorkflow(pi, buildDiscussPrompt(nextId,
1141
+ await dispatchWorkflow(pi, buildDiscussPrompt(nextId,
1070
1142
  `New milestone ${nextId}.`,
1071
1143
  basePath
1072
- ));
1144
+ ), "gsd-run", ctx, "plan-milestone");
1073
1145
  } else if (choice === "discard_milestone") {
1074
1146
  const confirmed = await showConfirm(ctx, {
1075
1147
  title: "Discard milestone?",
@@ -1181,16 +1253,16 @@ export async function showSmartEntry(
1181
1253
  inlineTemplate("plan", "Slice Plan"),
1182
1254
  inlineTemplate("task-plan", "Task Plan"),
1183
1255
  ].join("\n\n---\n\n");
1184
- dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
1256
+ await dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
1185
1257
  milestoneId, sliceId, sliceTitle, inlinedTemplates: planSliceTemplates,
1186
- }));
1258
+ }), "gsd-run", ctx, "plan-slice");
1187
1259
  } else if (choice === "discuss") {
1188
- dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }));
1260
+ await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "plan-slice");
1189
1261
  } else if (choice === "research") {
1190
1262
  const researchTemplates = inlineTemplate("research", "Research");
1191
- dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
1263
+ await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
1192
1264
  milestoneId, sliceId, sliceTitle, inlinedTemplates: researchTemplates,
1193
- }));
1265
+ }), "gsd-run", ctx, "research-slice");
1194
1266
  } else if (choice === "status") {
1195
1267
  const { fireStatusViaCommand } = await import("./commands.js");
1196
1268
  await fireStatusViaCommand(ctx);
@@ -1232,9 +1304,9 @@ export async function showSmartEntry(
1232
1304
  inlineTemplate("slice-summary", "Slice Summary"),
1233
1305
  inlineTemplate("uat", "UAT"),
1234
1306
  ].join("\n\n---\n\n");
1235
- dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
1307
+ await dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
1236
1308
  workingDirectory: basePath, milestoneId, sliceId, sliceTitle, inlinedTemplates: completeSliceTemplates,
1237
- }));
1309
+ }), "gsd-run", ctx, "complete-slice");
1238
1310
  } else if (choice === "status") {
1239
1311
  const { fireStatusViaCommand } = await import("./commands.js");
1240
1312
  await fireStatusViaCommand(ctx);
@@ -1297,14 +1369,14 @@ export async function showSmartEntry(
1297
1369
 
1298
1370
  if (choice === "execute") {
1299
1371
  if (hasInterrupted) {
1300
- dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
1372
+ await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
1301
1373
  milestoneId, sliceId,
1302
- }));
1374
+ }), "gsd-run", ctx, "execute-task");
1303
1375
  } else {
1304
1376
  const executeTaskTemplates = inlineTemplate("task-summary", "Task Summary");
1305
- dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
1377
+ await dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
1306
1378
  milestoneId, sliceId, taskId, taskTitle, inlinedTemplates: executeTaskTemplates,
1307
- }));
1379
+ }), "gsd-run", ctx, "execute-task");
1308
1380
  }
1309
1381
  } else if (choice === "status") {
1310
1382
  const { fireStatusViaCommand } = await import("./commands.js");
@@ -92,6 +92,23 @@ function warnDeprecatedAgentInstructions(): void {
92
92
  // ── Depth verification state ──────────────────────────────────────────────
93
93
  let depthVerificationDone = false;
94
94
 
95
+ // ── DB lazy-open helper ───────────────────────────────────────────────────
96
+ // In manual sessions (no auto-mode), the DB is never opened by bootstrapAutoSession.
97
+ // This helper ensures the DB is lazily opened on first tool call that needs it.
98
+ async function ensureDbOpen(): Promise<boolean> {
99
+ try {
100
+ const db = await import("./gsd-db.js");
101
+ if (db.isDbAvailable()) return true;
102
+ const dbPath = join(process.cwd(), ".gsd", "gsd.db");
103
+ if (existsSync(dbPath)) {
104
+ return db.openDatabase(dbPath);
105
+ }
106
+ return false;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
95
112
  // ── Queue phase tracking ──────────────────────────────────────────────────
96
113
  // When true, the LLM is in a queue flow writing CONTEXT.md files.
97
114
  // The write-gate applies during queue flows just like discussion flows.
@@ -300,12 +317,8 @@ export default function (pi: ExtensionAPI) {
300
317
  when_context: Type.Optional(Type.String({ description: "When/context for the decision (e.g. milestone ID)" })),
301
318
  }),
302
319
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
303
- // Check DB availability
304
- let dbAvailable = false;
305
- try {
306
- const db = await import("./gsd-db.js");
307
- dbAvailable = db.isDbAvailable();
308
- } catch { /* dynamic import failed */ }
320
+ // Ensure DB is open (lazy-open on first tool call in manual sessions)
321
+ const dbAvailable = await ensureDbOpen();
309
322
 
310
323
  if (!dbAvailable) {
311
324
  return {
@@ -367,11 +380,7 @@ export default function (pi: ExtensionAPI) {
367
380
  supporting_slices: Type.Optional(Type.String({ description: "Supporting slices" })),
368
381
  }),
369
382
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
370
- let dbAvailable = false;
371
- try {
372
- const db = await import("./gsd-db.js");
373
- dbAvailable = db.isDbAvailable();
374
- } catch { /* dynamic import failed */ }
383
+ const dbAvailable = await ensureDbOpen();
375
384
 
376
385
  if (!dbAvailable) {
377
386
  return {
@@ -441,11 +450,7 @@ export default function (pi: ExtensionAPI) {
441
450
  content: Type.String({ description: "The full markdown content of the artifact" }),
442
451
  }),
443
452
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
444
- let dbAvailable = false;
445
- try {
446
- const db = await import("./gsd-db.js");
447
- dbAvailable = db.isDbAvailable();
448
- } catch { /* dynamic import failed */ }
453
+ const dbAvailable = await ensureDbOpen();
449
454
 
450
455
  if (!dbAvailable) {
451
456
  return {
@@ -671,6 +671,43 @@ export function nativeAddAll(basePath: string): void {
671
671
  gitFileExec(basePath, ["add", "-A"]);
672
672
  }
673
673
 
674
+ /**
675
+ * Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
676
+ * Excluded paths are never hashed by git, preventing hangs on large
677
+ * untracked artifact trees (57GB+, 11K+ files). See #1605.
678
+ *
679
+ * Falls back to plain `git add -A` when no exclusions are provided.
680
+ * Always uses the CLI path (not libgit2) because libgit2's add_all
681
+ * does not support pathspec exclusion syntax.
682
+ *
683
+ * When excluded paths are already covered by .gitignore, git may exit
684
+ * with code 1 and an "ignored by .gitignore" warning. This is harmless
685
+ * (the staging succeeds for all non-ignored files) and is suppressed.
686
+ */
687
+ export function nativeAddAllWithExclusions(basePath: string, exclusions: readonly string[]): void {
688
+ if (exclusions.length === 0) {
689
+ nativeAddAll(basePath);
690
+ return;
691
+ }
692
+ const pathspecs = exclusions.map(e => `:!${e}`);
693
+ try {
694
+ execFileSync("git", ["add", "-A", "--", ...pathspecs], {
695
+ cwd: basePath,
696
+ stdio: ["ignore", "pipe", "pipe"],
697
+ encoding: "utf-8",
698
+ env: GIT_NO_PROMPT_ENV,
699
+ });
700
+ } catch (err: unknown) {
701
+ // git exits 1 when pathspec exclusions reference paths already covered
702
+ // by .gitignore. The staging itself succeeds — only suppress that case.
703
+ const stderr = (err as { stderr?: string })?.stderr ?? "";
704
+ if (stderr.includes("ignored by one of your .gitignore files")) {
705
+ return;
706
+ }
707
+ throw new GSDError(GSD_GIT_ERROR, `git add -A with exclusions failed in ${basePath}: ${getErrorMessage(err)}`);
708
+ }
709
+ }
710
+
674
711
  /**
675
712
  * Stage specific files.
676
713
  * Native: libgit2 index add.
@@ -295,18 +295,6 @@ export function resolveInlineLevel(): InlineLevel {
295
295
  }
296
296
  }
297
297
 
298
- /**
299
- * Resolve the compression strategy from the active token profile.
300
- * budget/balanced -> "compress", quality -> "truncate".
301
- * Explicit preference always wins.
302
- */
303
- export function resolveCompressionStrategy(): import("./types.js").CompressionStrategy {
304
- const prefs = loadEffectiveGSDPreferences();
305
- if (prefs?.preferences.compression_strategy) return prefs.preferences.compression_strategy;
306
- const profile = resolveEffectiveProfile();
307
- return profile === "quality" ? "truncate" : "compress";
308
- }
309
-
310
298
  /**
311
299
  * Resolve the context selection mode from the active token profile.
312
300
  * budget -> "smart", balanced/quality -> "full".