gsd-pi 2.67.0-dev.4fb8afe → 2.67.0-dev.5399650

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 (133) hide show
  1. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  2. package/dist/resources/extensions/gsd/auto.js +27 -0
  3. package/dist/resources/extensions/gsd/commands/handlers/core.js +38 -24
  4. package/dist/resources/extensions/gsd/commands/index.js +8 -1
  5. package/dist/resources/extensions/gsd/init-wizard.js +34 -0
  6. package/dist/resources/extensions/gsd/workflow-logger.js +18 -3
  7. package/dist/resources/extensions/gsd/workflow-mcp.js +38 -7
  8. package/dist/web/standalone/.next/BUILD_ID +1 -1
  9. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  10. package/dist/web/standalone/.next/build-manifest.json +2 -2
  11. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  12. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.html +1 -1
  29. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  36. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  37. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  38. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  39. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  40. package/package.json +4 -2
  41. package/packages/mcp-server/dist/cli.d.ts +9 -0
  42. package/packages/mcp-server/dist/cli.d.ts.map +1 -0
  43. package/packages/mcp-server/dist/cli.js +58 -0
  44. package/packages/mcp-server/dist/cli.js.map +1 -0
  45. package/packages/mcp-server/dist/index.d.ts +20 -0
  46. package/packages/mcp-server/dist/index.d.ts.map +1 -0
  47. package/packages/mcp-server/dist/index.js +14 -0
  48. package/packages/mcp-server/dist/index.js.map +1 -0
  49. package/packages/mcp-server/dist/readers/captures.d.ts +25 -0
  50. package/packages/mcp-server/dist/readers/captures.d.ts.map +1 -0
  51. package/packages/mcp-server/dist/readers/captures.js +67 -0
  52. package/packages/mcp-server/dist/readers/captures.js.map +1 -0
  53. package/packages/mcp-server/dist/readers/doctor-lite.d.ts +20 -0
  54. package/packages/mcp-server/dist/readers/doctor-lite.d.ts.map +1 -0
  55. package/packages/mcp-server/dist/readers/doctor-lite.js +173 -0
  56. package/packages/mcp-server/dist/readers/doctor-lite.js.map +1 -0
  57. package/packages/mcp-server/dist/readers/index.d.ts +14 -0
  58. package/packages/mcp-server/dist/readers/index.d.ts.map +1 -0
  59. package/packages/mcp-server/dist/readers/index.js +10 -0
  60. package/packages/mcp-server/dist/readers/index.js.map +1 -0
  61. package/packages/mcp-server/dist/readers/knowledge.d.ts +18 -0
  62. package/packages/mcp-server/dist/readers/knowledge.d.ts.map +1 -0
  63. package/packages/mcp-server/dist/readers/knowledge.js +82 -0
  64. package/packages/mcp-server/dist/readers/knowledge.js.map +1 -0
  65. package/packages/mcp-server/dist/readers/metrics.d.ts +32 -0
  66. package/packages/mcp-server/dist/readers/metrics.d.ts.map +1 -0
  67. package/packages/mcp-server/dist/readers/metrics.js +74 -0
  68. package/packages/mcp-server/dist/readers/metrics.js.map +1 -0
  69. package/packages/mcp-server/dist/readers/paths.d.ts +42 -0
  70. package/packages/mcp-server/dist/readers/paths.d.ts.map +1 -0
  71. package/packages/mcp-server/dist/readers/paths.js +199 -0
  72. package/packages/mcp-server/dist/readers/paths.js.map +1 -0
  73. package/packages/mcp-server/dist/readers/roadmap.d.ts +26 -0
  74. package/packages/mcp-server/dist/readers/roadmap.d.ts.map +1 -0
  75. package/packages/mcp-server/dist/readers/roadmap.js +194 -0
  76. package/packages/mcp-server/dist/readers/roadmap.js.map +1 -0
  77. package/packages/mcp-server/dist/readers/state.d.ts +43 -0
  78. package/packages/mcp-server/dist/readers/state.d.ts.map +1 -0
  79. package/packages/mcp-server/dist/readers/state.js +184 -0
  80. package/packages/mcp-server/dist/readers/state.js.map +1 -0
  81. package/packages/mcp-server/dist/server.d.ts +28 -0
  82. package/packages/mcp-server/dist/server.d.ts.map +1 -0
  83. package/packages/mcp-server/dist/server.js +319 -0
  84. package/packages/mcp-server/dist/server.js.map +1 -0
  85. package/packages/mcp-server/dist/session-manager.d.ts +54 -0
  86. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -0
  87. package/packages/mcp-server/dist/session-manager.js +284 -0
  88. package/packages/mcp-server/dist/session-manager.js.map +1 -0
  89. package/packages/mcp-server/dist/types.d.ts +61 -0
  90. package/packages/mcp-server/dist/types.d.ts.map +1 -0
  91. package/packages/mcp-server/dist/types.js +11 -0
  92. package/packages/mcp-server/dist/types.js.map +1 -0
  93. package/packages/mcp-server/dist/workflow-tools.d.ts +9 -0
  94. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -0
  95. package/packages/mcp-server/dist/workflow-tools.js +526 -0
  96. package/packages/mcp-server/dist/workflow-tools.js.map +1 -0
  97. package/packages/mcp-server/tsconfig.json +1 -1
  98. package/packages/rpc-client/dist/index.d.ts +10 -0
  99. package/packages/rpc-client/dist/index.d.ts.map +1 -0
  100. package/packages/rpc-client/dist/index.js +9 -0
  101. package/packages/rpc-client/dist/index.js.map +1 -0
  102. package/packages/rpc-client/dist/jsonl.d.ts +17 -0
  103. package/packages/rpc-client/dist/jsonl.d.ts.map +1 -0
  104. package/packages/rpc-client/dist/jsonl.js +54 -0
  105. package/packages/rpc-client/dist/jsonl.js.map +1 -0
  106. package/packages/rpc-client/dist/rpc-client.d.ts +259 -0
  107. package/packages/rpc-client/dist/rpc-client.d.ts.map +1 -0
  108. package/packages/rpc-client/dist/rpc-client.js +541 -0
  109. package/packages/rpc-client/dist/rpc-client.js.map +1 -0
  110. package/packages/rpc-client/dist/rpc-client.test.d.ts +2 -0
  111. package/packages/rpc-client/dist/rpc-client.test.d.ts.map +1 -0
  112. package/packages/rpc-client/dist/rpc-client.test.js +477 -0
  113. package/packages/rpc-client/dist/rpc-client.test.js.map +1 -0
  114. package/packages/rpc-client/dist/rpc-types.d.ts +566 -0
  115. package/packages/rpc-client/dist/rpc-types.d.ts.map +1 -0
  116. package/packages/rpc-client/dist/rpc-types.js +12 -0
  117. package/packages/rpc-client/dist/rpc-types.js.map +1 -0
  118. package/scripts/ensure-workspace-builds.cjs +2 -0
  119. package/scripts/link-workspace-packages.cjs +21 -14
  120. package/src/resources/extensions/gsd/auto/session.ts +6 -0
  121. package/src/resources/extensions/gsd/auto.ts +29 -1
  122. package/src/resources/extensions/gsd/commands/handlers/core.ts +52 -25
  123. package/src/resources/extensions/gsd/commands/index.ts +7 -1
  124. package/src/resources/extensions/gsd/init-wizard.ts +34 -0
  125. package/src/resources/extensions/gsd/tests/auto-project-root-env.test.ts +29 -0
  126. package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +101 -0
  127. package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +121 -0
  128. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +16 -0
  129. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +39 -1
  130. package/src/resources/extensions/gsd/workflow-logger.ts +19 -3
  131. package/src/resources/extensions/gsd/workflow-mcp.ts +41 -7
  132. /package/dist/web/standalone/.next/static/{IBTC_HlEpTBAa4HXMoV_A → 6_QPFhgX0DQnDhhquheRc}/_buildManifest.js +0 -0
  133. /package/dist/web/standalone/.next/static/{IBTC_HlEpTBAa4HXMoV_A → 6_QPFhgX0DQnDhhquheRc}/_ssgManifest.js +0 -0
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * link-workspace-packages.cjs
4
4
  *
5
- * Creates node_modules/@gsd/* symlinks pointing to packages/* directories.
5
+ * Creates node_modules/@gsd/* and node_modules/@gsd-build/* symlinks pointing
6
+ * to shipped packages/* directories.
6
7
  *
7
8
  * During development, npm workspaces creates these automatically. But in the
8
9
  * published tarball, workspace packages are shipped under packages/ (via the
@@ -20,27 +21,33 @@ const { resolve, join } = require('path')
20
21
 
21
22
  const root = resolve(__dirname, '..')
22
23
  const packagesDir = join(root, 'packages')
23
- const nodeModulesGsd = join(root, 'node_modules', '@gsd')
24
+ const scopeDirs = {
25
+ '@gsd': join(root, 'node_modules', '@gsd'),
26
+ '@gsd-build': join(root, 'node_modules', '@gsd-build'),
27
+ }
24
28
 
25
- // Map directory names to package names
29
+ // Map directory names to scoped package names
26
30
  const packageMap = {
27
- 'native': 'native',
28
- 'pi-agent-core': 'pi-agent-core',
29
- 'pi-ai': 'pi-ai',
30
- 'pi-coding-agent': 'pi-coding-agent',
31
- 'pi-tui': 'pi-tui',
31
+ 'native': { scope: '@gsd', name: 'native' },
32
+ 'pi-agent-core': { scope: '@gsd', name: 'pi-agent-core' },
33
+ 'pi-ai': { scope: '@gsd', name: 'pi-ai' },
34
+ 'pi-coding-agent': { scope: '@gsd', name: 'pi-coding-agent' },
35
+ 'pi-tui': { scope: '@gsd', name: 'pi-tui' },
36
+ 'rpc-client': { scope: '@gsd-build', name: 'rpc-client' },
32
37
  }
33
38
 
34
- // Ensure @gsd scope directory exists
35
- if (!existsSync(nodeModulesGsd)) {
36
- mkdirSync(nodeModulesGsd, { recursive: true })
39
+ for (const scopeDir of Object.values(scopeDirs)) {
40
+ if (!existsSync(scopeDir)) {
41
+ mkdirSync(scopeDir, { recursive: true })
42
+ }
37
43
  }
38
44
 
39
45
  let linked = 0
40
46
  let copied = 0
41
- for (const [dir, name] of Object.entries(packageMap)) {
47
+ for (const [dir, pkg] of Object.entries(packageMap)) {
42
48
  const source = join(packagesDir, dir)
43
- const target = join(nodeModulesGsd, name)
49
+ const scopeDir = scopeDirs[pkg.scope]
50
+ const target = join(scopeDir, pkg.name)
44
51
 
45
52
  if (!existsSync(source)) continue
46
53
 
@@ -50,7 +57,7 @@ for (const [dir, name] of Object.entries(packageMap)) {
50
57
  const stat = lstatSync(target)
51
58
  if (stat.isSymbolicLink()) {
52
59
  const linkTarget = readlinkSync(target)
53
- if (resolve(join(nodeModulesGsd, linkTarget)) === source || linkTarget === source) {
60
+ if (resolve(join(scopeDir, linkTarget)) === source || linkTarget === source) {
54
61
  continue // Already correct
55
62
  }
56
63
  unlinkSync(target) // Wrong target, relink
@@ -84,6 +84,9 @@ export class AutoSession {
84
84
  // ── Paths ────────────────────────────────────────────────────────────────
85
85
  basePath = "";
86
86
  originalBasePath = "";
87
+ previousProjectRootEnv: string | null = null;
88
+ hadProjectRootEnv = false;
89
+ projectRootEnvCaptured = false;
87
90
  gitService: GitServiceImpl | null = null;
88
91
 
89
92
  // ── Dispatch counters ────────────────────────────────────────────────────
@@ -192,6 +195,9 @@ export class AutoSession {
192
195
  // Paths
193
196
  this.basePath = "";
194
197
  this.originalBasePath = "";
198
+ this.previousProjectRootEnv = null;
199
+ this.hadProjectRootEnv = false;
200
+ this.projectRootEnvCaptured = false;
195
201
  this.gitService = null;
196
202
 
197
203
  // Dispatch
@@ -241,6 +241,29 @@ const s = new AutoSession();
241
241
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
242
242
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
243
243
 
244
+ function captureProjectRootEnv(projectRoot: string): void {
245
+ if (!s.projectRootEnvCaptured) {
246
+ s.hadProjectRootEnv = Object.prototype.hasOwnProperty.call(process.env, "GSD_PROJECT_ROOT");
247
+ s.previousProjectRootEnv = process.env.GSD_PROJECT_ROOT ?? null;
248
+ s.projectRootEnvCaptured = true;
249
+ }
250
+ process.env.GSD_PROJECT_ROOT = projectRoot;
251
+ }
252
+
253
+ function restoreProjectRootEnv(): void {
254
+ if (!s.projectRootEnvCaptured) return;
255
+
256
+ if (s.hadProjectRootEnv && s.previousProjectRootEnv !== null) {
257
+ process.env.GSD_PROJECT_ROOT = s.previousProjectRootEnv;
258
+ } else {
259
+ delete process.env.GSD_PROJECT_ROOT;
260
+ }
261
+
262
+ s.previousProjectRootEnv = null;
263
+ s.hadProjectRootEnv = false;
264
+ s.projectRootEnvCaptured = false;
265
+ }
266
+
244
267
  export function shouldUseWorktreeIsolation(): boolean {
245
268
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
246
269
  if (prefs?.isolation === "worktree") return true;
@@ -542,6 +565,7 @@ function handleLostSessionLock(
542
565
  s.active = false;
543
566
  s.paused = false;
544
567
  clearUnitTimeout();
568
+ restoreProjectRootEnv();
545
569
  deregisterSigtermHandler();
546
570
  clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
547
571
  const base = lockBase();
@@ -577,6 +601,7 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
577
601
  s.currentUnit = null;
578
602
  s.active = false;
579
603
  clearUnitTimeout();
604
+ restoreProjectRootEnv();
580
605
 
581
606
  // Clear crash lock and release session lock so the next `/gsd next` does
582
607
  // not see a stale lock with the current PID and treat it as a "remote"
@@ -846,6 +871,7 @@ export async function stopAuto(
846
871
  ctx?.ui.setStatus("gsd-auto", undefined);
847
872
  ctx?.ui.setWidget("gsd-progress", undefined);
848
873
  ctx?.ui.setFooter(undefined);
874
+ restoreProjectRootEnv();
849
875
 
850
876
  // Reset all session state in one call
851
877
  s.reset();
@@ -934,6 +960,7 @@ export async function pauseAuto(
934
960
 
935
961
  s.active = false;
936
962
  s.paused = true;
963
+ restoreProjectRootEnv();
937
964
  s.pendingVerificationRetry = null;
938
965
  s.verificationRetryCount.clear();
939
966
  ctx?.ui.setStatus("gsd-auto", "paused");
@@ -1305,6 +1332,7 @@ export async function startAuto(
1305
1332
  );
1306
1333
  logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1307
1334
 
1335
+ captureProjectRootEnv(s.originalBasePath || s.basePath);
1308
1336
  await autoLoop(ctx, pi, s, buildLoopDeps());
1309
1337
  cleanupAfterLoopExit(ctx);
1310
1338
  return;
@@ -1329,6 +1357,7 @@ export async function startAuto(
1329
1357
  );
1330
1358
  if (!ready) return;
1331
1359
 
1360
+ captureProjectRootEnv(s.originalBasePath || s.basePath);
1332
1361
  try {
1333
1362
  syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1334
1363
  } catch (err) {
@@ -1569,4 +1598,3 @@ export {
1569
1598
  buildLoopRemediationSteps,
1570
1599
  } from "./auto-recovery.js";
1571
1600
  export { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
1572
-
@@ -194,6 +194,56 @@ function sortModelsForSelection(models: Model<any>[], currentModel: Model<any> |
194
194
  });
195
195
  }
196
196
 
197
+ function buildProviderModelGroups(
198
+ models: Model<any>[],
199
+ currentModel: Model<any> | undefined,
200
+ ): Map<string, Model<any>[]> {
201
+ const byProvider = new Map<string, Model<any>[]>();
202
+
203
+ for (const model of sortModelsForSelection(models, currentModel)) {
204
+ let group = byProvider.get(model.provider);
205
+ if (!group) {
206
+ group = [];
207
+ byProvider.set(model.provider, group);
208
+ }
209
+ group.push(model);
210
+ }
211
+ return byProvider;
212
+ }
213
+
214
+ async function selectModelByProvider(
215
+ title: string,
216
+ models: Model<any>[],
217
+ ctx: ExtensionCommandContext,
218
+ currentModel: Model<any> | undefined,
219
+ ): Promise<Model<any> | undefined> {
220
+ const byProvider = buildProviderModelGroups(models, currentModel);
221
+ const providerOptions = Array.from(byProvider.entries()).map(([provider, group]) =>
222
+ `${provider} (${group.length} model${group.length === 1 ? "" : "s"})`,
223
+ );
224
+ providerOptions.push("(cancel)");
225
+
226
+ const providerChoice = await ctx.ui.select(`${title} — choose provider:`, providerOptions);
227
+ if (!providerChoice || typeof providerChoice !== "string" || providerChoice === "(cancel)") return undefined;
228
+
229
+ const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
230
+ const providerModels = byProvider.get(providerName);
231
+ if (!providerModels || providerModels.length === 0) return undefined;
232
+
233
+ const optionToModel = new Map<string, Model<any>>();
234
+ const modelOptions = providerModels.map((model) => {
235
+ const isCurrent = currentModel && model.provider === currentModel.provider && model.id === currentModel.id;
236
+ const label = `${isCurrent ? "* " : ""}${model.id}`;
237
+ optionToModel.set(label, model);
238
+ return label;
239
+ });
240
+ modelOptions.push("(cancel)");
241
+
242
+ const modelChoice = await ctx.ui.select(`${title} — ${providerName}:`, modelOptions);
243
+ if (!modelChoice || typeof modelChoice !== "string" || modelChoice === "(cancel)") return undefined;
244
+ return optionToModel.get(modelChoice);
245
+ }
246
+
197
247
  async function resolveRequestedModel(
198
248
  query: string,
199
249
  ctx: ExtensionCommandContext,
@@ -211,19 +261,7 @@ async function resolveRequestedModel(
211
261
 
212
262
  if (partialMatches.length === 1) return partialMatches[0];
213
263
  if (partialMatches.length === 0 || !ctx.hasUI) return undefined;
214
-
215
- const sorted = sortModelsForSelection(partialMatches, ctx.model);
216
- const optionToModel = new Map<string, Model<any>>();
217
- const options = sorted.map((model) => {
218
- const label = `${model.provider}/${model.id}`;
219
- optionToModel.set(label, model);
220
- return label;
221
- });
222
- options.push("(cancel)");
223
-
224
- const choice = await ctx.ui.select(`Multiple models match "${query}" — choose one:`, options);
225
- if (!choice || typeof choice !== "string" || choice === "(cancel)") return undefined;
226
- return optionToModel.get(choice);
264
+ return selectModelByProvider(`Multiple models match "${query}"`, partialMatches, ctx, ctx.model);
227
265
  }
228
266
 
229
267
  async function handleModel(trimmedArgs: string, ctx: ExtensionCommandContext, pi: ExtensionAPI | undefined): Promise<void> {
@@ -247,18 +285,7 @@ async function handleModel(trimmedArgs: string, ctx: ExtensionCommandContext, pi
247
285
  return;
248
286
  }
249
287
 
250
- const optionToModel = new Map<string, Model<any>>();
251
- const options = sortModelsForSelection(availableModels, ctx.model).map((model) => {
252
- const isCurrent = ctx.model && model.provider === ctx.model.provider && model.id === ctx.model.id;
253
- const label = `${isCurrent ? "* " : ""}${model.provider}/${model.id}`;
254
- optionToModel.set(label, model);
255
- return label;
256
- });
257
- options.push("(cancel)");
258
-
259
- const choice = await ctx.ui.select("Select session model:", options);
260
- if (!choice || typeof choice !== "string" || choice === "(cancel)") return;
261
- targetModel = optionToModel.get(choice);
288
+ targetModel = await selectModelByProvider("Select session model:", availableModels, ctx, ctx.model);
262
289
  } else {
263
290
  targetModel = await resolveRequestedModel(trimmed, ctx);
264
291
  }
@@ -8,7 +8,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
8
8
  getArgumentCompletions: getGsdArgumentCompletions,
9
9
  handler: async (args: string, ctx: ExtensionCommandContext) => {
10
10
  const { handleGSDCommand } = await import("./dispatcher.js");
11
- await handleGSDCommand(args, ctx, pi);
11
+ const { setStderrLoggingEnabled } = await import("../workflow-logger.js");
12
+ const previousStderrSetting = setStderrLoggingEnabled(false);
13
+ try {
14
+ await handleGSDCommand(args, ctx, pi);
15
+ } finally {
16
+ setStderrLoggingEnabled(previousStderrSetting);
17
+ }
12
18
  },
13
19
  });
14
20
  }
@@ -235,6 +235,20 @@ export async function showProjectInit(
235
235
  // ── Step 9: Bootstrap .gsd/ ────────────────────────────────────────────────
236
236
  bootstrapGsdDirectory(basePath, prefs, signals);
237
237
 
238
+ // Initialize SQLite database so GSD starts in full-capability mode (#3880).
239
+ // Without this, isDbAvailable() returns false and GSD enters degraded
240
+ // markdown-only mode until a tool handler happens to call ensureDbOpen().
241
+ let dbReady = false;
242
+ try {
243
+ const { ensureDbOpen } = await import("./bootstrap/dynamic-tools.js");
244
+ dbReady = await ensureDbOpen(basePath);
245
+ } catch {
246
+ // Swallowed — warning surfaced below
247
+ }
248
+ if (!dbReady) {
249
+ ctx.ui.notify("Warning: database initialization failed — GSD will run in degraded mode until the next /gsd invocation.", "warning");
250
+ }
251
+
238
252
  // Ensure .gitignore
239
253
  ensureGitignore(basePath);
240
254
  untrackRuntimeFiles(basePath);
@@ -250,6 +264,25 @@ export async function showProjectInit(
250
264
  // Non-fatal — codebase map generation failure should never block project init
251
265
  }
252
266
 
267
+ // Write initial STATE.md so it exists before the first /gsd invocation.
268
+ // The explicit /gsd init path (ops.ts) returns without entering showSmartEntry(),
269
+ // which would otherwise generate STATE.md at guided-flow.ts:1358.
270
+ let stateReady = false;
271
+ try {
272
+ const { deriveState } = await import("./state.js");
273
+ const { buildStateMarkdown } = await import("./doctor.js");
274
+ const { saveFile } = await import("./files.js");
275
+ const { resolveGsdRootFile } = await import("./paths.js");
276
+ const state = await deriveState(basePath);
277
+ await saveFile(resolveGsdRootFile(basePath, "STATE"), buildStateMarkdown(state));
278
+ stateReady = true;
279
+ } catch {
280
+ // Swallowed — warning surfaced below
281
+ }
282
+ if (!stateReady) {
283
+ ctx.ui.notify("Warning: initial STATE.md generation failed — it will be created on the next /gsd invocation.", "warning");
284
+ }
285
+
253
286
  ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
254
287
 
255
288
  return { completed: true, bootstrapped: true };
@@ -433,6 +466,7 @@ function bootstrapGsdDirectory(
433
466
 
434
467
  const gsd = gsdRoot(basePath);
435
468
  mkdirSync(join(gsd, "milestones"), { recursive: true });
469
+ mkdirSync(join(gsd, "runtime"), { recursive: true });
436
470
 
437
471
  // Write PREFERENCES.md from wizard answers
438
472
  const preferencesContent = buildPreferencesFile(prefs);
@@ -0,0 +1,29 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ const sourcePath = join(import.meta.dirname, "..", "auto.ts");
7
+ const source = readFileSync(sourcePath, "utf-8");
8
+
9
+ test("auto-mode captures GSD_PROJECT_ROOT before entering the dispatch loop", () => {
10
+ const captureDeclIdx = source.indexOf("function captureProjectRootEnv(projectRoot: string): void {");
11
+ assert.ok(captureDeclIdx > -1, "auto.ts should define captureProjectRootEnv()");
12
+
13
+ const resumeCallIdx = source.indexOf("captureProjectRootEnv(s.originalBasePath || s.basePath);");
14
+ assert.ok(resumeCallIdx > -1, "auto.ts should capture GSD_PROJECT_ROOT before resume autoLoop");
15
+
16
+ const firstAutoLoopIdx = source.indexOf("await autoLoop(ctx, pi, s, buildLoopDeps());");
17
+ assert.ok(firstAutoLoopIdx > -1, "auto.ts should invoke autoLoop()");
18
+ assert.ok(
19
+ resumeCallIdx < firstAutoLoopIdx,
20
+ "auto.ts must set GSD_PROJECT_ROOT before the first autoLoop() call",
21
+ );
22
+ });
23
+
24
+ test("auto-mode restores GSD_PROJECT_ROOT when execution stops or pauses", () => {
25
+ assert.match(source, /function restoreProjectRootEnv\(\): void \{/);
26
+ assert.match(source, /cleanupAfterLoopExit\(ctx: ExtensionContext\): void \{[\s\S]*restoreProjectRootEnv\(\);/);
27
+ assert.match(source, /export async function pauseAuto\([\s\S]*restoreProjectRootEnv\(\);/);
28
+ assert.match(source, /\} finally \{[\s\S]*restoreProjectRootEnv\(\);[\s\S]*s\.reset\(\);/);
29
+ });
@@ -74,3 +74,104 @@ test("model command resolves and persists exact provider-qualified selection", a
74
74
  assert.deepEqual(applied, selectedModel);
75
75
  assert.match(notices[0]!.message, /openai\/gpt-5\.4/);
76
76
  });
77
+
78
+ test("interactive model picker chooses provider first, then model", async () => {
79
+ const selectedModel = { provider: "openai", id: "gpt-5.4" };
80
+ let applied: typeof selectedModel | null = null;
81
+ const selects: Array<{ title: string; options: string[] }> = [];
82
+ const notices: Array<{ message: string; type?: string }> = [];
83
+
84
+ const ctx = {
85
+ hasUI: true,
86
+ model: { provider: "anthropic", id: "claude-sonnet-4-6" },
87
+ modelRegistry: {
88
+ getAvailable: () => [
89
+ { provider: "openai", id: "gpt-5.4" },
90
+ { provider: "anthropic", id: "claude-opus-4-6" },
91
+ { provider: "openai", id: "gpt-5.3-mini" },
92
+ { provider: "anthropic", id: "claude-sonnet-4-6" },
93
+ ],
94
+ },
95
+ ui: {
96
+ select: async (title: string, options: string[]) => {
97
+ selects.push({ title, options });
98
+ return selects.length === 1 ? "openai (2 models)" : "gpt-5.4";
99
+ },
100
+ notify: (message: string, type?: string) => {
101
+ notices.push({ message, type });
102
+ },
103
+ },
104
+ } as any;
105
+
106
+ const pi = {
107
+ setModel: async (model: typeof selectedModel) => {
108
+ applied = model;
109
+ return true;
110
+ },
111
+ } as any;
112
+
113
+ const handled = await handleCoreCommand("model", ctx, pi);
114
+ assert.equal(handled, true);
115
+ assert.deepEqual(selects, [
116
+ {
117
+ title: "Select session model: — choose provider:",
118
+ options: ["anthropic (2 models)", "openai (2 models)", "(cancel)"],
119
+ },
120
+ {
121
+ title: "Select session model: — openai:",
122
+ options: ["gpt-5.3-mini", "gpt-5.4", "(cancel)"],
123
+ },
124
+ ]);
125
+ assert.deepEqual(applied, selectedModel);
126
+ assert.match(notices[0]!.message, /openai\/gpt-5\.4/);
127
+ });
128
+
129
+ test("ambiguous typed model selection chooses provider first, then model", async () => {
130
+ const selectedModel = { provider: "github-copilot", id: "gpt-5" };
131
+ let applied: typeof selectedModel | null = null;
132
+ const selects: Array<{ title: string; options: string[] }> = [];
133
+ const notices: Array<{ message: string; type?: string }> = [];
134
+
135
+ const ctx = {
136
+ hasUI: true,
137
+ model: { provider: "anthropic", id: "claude-sonnet-4-6" },
138
+ modelRegistry: {
139
+ getAvailable: () => [
140
+ { provider: "openai", id: "gpt-5" },
141
+ { provider: "github-copilot", id: "gpt-5" },
142
+ { provider: "openai", id: "gpt-5-mini" },
143
+ ],
144
+ },
145
+ ui: {
146
+ select: async (title: string, options: string[]) => {
147
+ selects.push({ title, options });
148
+ return selects.length === 1 ? "github-copilot (1 model)" : "gpt-5";
149
+ },
150
+ notify: (message: string, type?: string) => {
151
+ notices.push({ message, type });
152
+ },
153
+ },
154
+ } as any;
155
+
156
+ const pi = {
157
+ setModel: async (model: typeof selectedModel) => {
158
+ applied = model;
159
+ return true;
160
+ },
161
+ } as any;
162
+
163
+ const handled = await handleCoreCommand("model gpt", ctx, pi);
164
+ assert.equal(handled, true);
165
+ assert.deepEqual(selects, [
166
+ {
167
+ title: "Multiple models match \"gpt\" — choose provider:",
168
+ options: ["github-copilot (1 model)", "openai (2 models)", "(cancel)"],
169
+ },
170
+ {
171
+ title: "Multiple models match \"gpt\" — github-copilot:",
172
+ options: ["gpt-5", "(cancel)"],
173
+ },
174
+ ]);
175
+ assert.deepEqual(applied, selectedModel);
176
+ assert.match(notices[0]!.message, /github-copilot\/gpt-5/);
177
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * GSD Init Wizard — Bootstrap completeness regression tests
3
+ *
4
+ * Regression test for #3880 — fresh install never creates gsd.db.
5
+ *
6
+ * The init wizard must create all artifacts needed for full-capability
7
+ * mode: gsd.db (via ensureDbOpen), runtime/ directory, and STATE.md
8
+ * (via deriveState + buildStateMarkdown). Without these, GSD enters
9
+ * degraded markdown-only mode on every fresh install.
10
+ *
11
+ * These are structural tests that verify the init-wizard.ts source
12
+ * contains the required calls in the correct order.
13
+ */
14
+
15
+ import { describe, test } from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { readFileSync } from "node:fs";
18
+ import { fileURLToPath } from "node:url";
19
+ import { dirname, join } from "node:path";
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+
24
+ const wizardSrc = readFileSync(
25
+ join(__dirname, "..", "init-wizard.ts"),
26
+ "utf-8",
27
+ );
28
+
29
+ describe("init-wizard bootstrap completeness (#3880)", () => {
30
+ // ── Gap 1: gsd.db must be created during init ─────────────────────────
31
+
32
+ test("bootstrapGsdDirectory is followed by ensureDbOpen", () => {
33
+ const bootstrapIdx = wizardSrc.indexOf("bootstrapGsdDirectory(basePath");
34
+ const ensureDbIdx = wizardSrc.indexOf("ensureDbOpen(basePath)");
35
+ assert.ok(bootstrapIdx > -1, "bootstrapGsdDirectory call should exist");
36
+ assert.ok(ensureDbIdx > -1, "ensureDbOpen(basePath) call should exist");
37
+ assert.ok(
38
+ ensureDbIdx > bootstrapIdx,
39
+ "ensureDbOpen must appear after bootstrapGsdDirectory so .gsd/ exists first",
40
+ );
41
+ });
42
+
43
+ test("ensureDbOpen is imported from dynamic-tools", () => {
44
+ assert.match(
45
+ wizardSrc,
46
+ /import.*dynamic-tools/,
47
+ "init-wizard should import from dynamic-tools for ensureDbOpen",
48
+ );
49
+ });
50
+
51
+ // ── Gap 2: runtime/ directory must be created during init ──────────────
52
+
53
+ test("bootstrapGsdDirectory creates runtime/ directory", () => {
54
+ // Find the bootstrapGsdDirectory function body
55
+ const fnStart = wizardSrc.indexOf("function bootstrapGsdDirectory(");
56
+ assert.ok(fnStart > -1, "bootstrapGsdDirectory function should exist");
57
+
58
+ // Find the next function definition to bound the search
59
+ const fnBody = wizardSrc.slice(fnStart, wizardSrc.indexOf("\nfunction ", fnStart + 1));
60
+
61
+ assert.match(
62
+ fnBody,
63
+ /mkdirSync\(.*"runtime"/,
64
+ 'bootstrapGsdDirectory should create "runtime" directory',
65
+ );
66
+ });
67
+
68
+ // ── Gap 3: STATE.md must be written during init ────────────────────────
69
+
70
+ test("showProjectInit generates STATE.md after bootstrap", () => {
71
+ const bootstrapIdx = wizardSrc.indexOf("bootstrapGsdDirectory(basePath");
72
+ const deriveIdx = wizardSrc.indexOf("deriveState(basePath)");
73
+ const stateIdx = wizardSrc.indexOf("buildStateMarkdown(state)");
74
+ const saveIdx = wizardSrc.indexOf('resolveGsdRootFile(basePath, "STATE")');
75
+
76
+ assert.ok(deriveIdx > -1, "deriveState call should exist in init-wizard");
77
+ assert.ok(stateIdx > -1, "buildStateMarkdown call should exist in init-wizard");
78
+ assert.ok(saveIdx > -1, "resolveGsdRootFile STATE call should exist in init-wizard");
79
+ assert.ok(
80
+ deriveIdx > bootstrapIdx,
81
+ "deriveState must appear after bootstrapGsdDirectory",
82
+ );
83
+ });
84
+
85
+ // ── Ordering: DB must be open before deriveState ───────────────────────
86
+
87
+ test("ensureDbOpen appears before deriveState", () => {
88
+ const ensureDbIdx = wizardSrc.indexOf("ensureDbOpen(basePath)");
89
+ const deriveIdx = wizardSrc.indexOf("deriveState(basePath)");
90
+ assert.ok(ensureDbIdx > -1, "ensureDbOpen should exist");
91
+ assert.ok(deriveIdx > -1, "deriveState should exist");
92
+ assert.ok(
93
+ ensureDbIdx < deriveIdx,
94
+ "ensureDbOpen must appear before deriveState so DB is ready for state derivation",
95
+ );
96
+ });
97
+
98
+ // ── Failure visibility: user must be warned on partial bootstrap ───────
99
+
100
+ test("ensureDbOpen failure surfaces a warning to the user", () => {
101
+ assert.match(
102
+ wizardSrc,
103
+ /if\s*\(\s*!dbReady\s*\)/,
104
+ "init-wizard should check dbReady and warn the user on failure",
105
+ );
106
+ // The warning must reference degraded mode so the user knows what happened
107
+ assert.match(
108
+ wizardSrc,
109
+ /degraded mode/,
110
+ "DB failure warning should mention degraded mode",
111
+ );
112
+ });
113
+
114
+ test("STATE.md failure surfaces a warning to the user", () => {
115
+ assert.match(
116
+ wizardSrc,
117
+ /if\s*\(\s*!stateReady\s*\)/,
118
+ "init-wizard should check stateReady and warn the user on failure",
119
+ );
120
+ });
121
+ });
@@ -18,6 +18,7 @@ import {
18
18
  summarizeLogs,
19
19
  formatForNotification,
20
20
  setLogBasePath,
21
+ setStderrLoggingEnabled,
21
22
  _resetLogs,
22
23
  } from "../workflow-logger.ts";
23
24
 
@@ -375,5 +376,20 @@ describe("workflow-logger", () => {
375
376
  logError("tool", "failed", { cmd: "complete_task" });
376
377
  assert.ok(written[0].includes('"cmd":"complete_task"'));
377
378
  });
379
+
380
+ test("suppresses stderr when disabled", (t) => {
381
+ const written: string[] = [];
382
+ const orig = process.stderr.write.bind(process.stderr);
383
+ const previous = setStderrLoggingEnabled(false);
384
+ // @ts-ignore — patching for test
385
+ process.stderr.write = (chunk: string) => { written.push(chunk); return true; };
386
+ t.after(() => {
387
+ process.stderr.write = orig;
388
+ setStderrLoggingEnabled(previous);
389
+ });
390
+
391
+ logWarning("engine", "hidden warning");
392
+ assert.deepEqual(written, []);
393
+ });
378
394
  });
379
395
  });
@@ -1,7 +1,8 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { readFileSync } from "node:fs";
3
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
+ import { tmpdir } from "node:os";
5
6
  import { fileURLToPath } from "node:url";
6
7
 
7
8
  import {
@@ -70,6 +71,43 @@ test("buildWorkflowMcpServers mirrors explicit launch config", () => {
70
71
  });
71
72
  });
72
73
 
74
+ test("detectWorkflowMcpLaunchConfig resolves the bundled server from GSD_PROJECT_ROOT", () => {
75
+ const repoRoot = mkdtempSync(join(tmpdir(), "gsd-workflow-root-"));
76
+ const worktreeRoot = mkdtempSync(join(tmpdir(), "gsd-workflow-worktree-"));
77
+ const cliPath = join(repoRoot, "packages", "mcp-server", "dist", "cli.js");
78
+
79
+ mkdirSync(join(repoRoot, "packages", "mcp-server", "dist"), { recursive: true });
80
+ writeFileSync(cliPath, "#!/usr/bin/env node\n", "utf-8");
81
+
82
+ const launch = detectWorkflowMcpLaunchConfig(worktreeRoot, {
83
+ GSD_PROJECT_ROOT: repoRoot,
84
+ });
85
+
86
+ assert.deepEqual(launch, {
87
+ name: "gsd-workflow",
88
+ command: process.execPath,
89
+ args: [cliPath],
90
+ cwd: repoRoot,
91
+ env: {
92
+ GSD_PERSIST_WRITE_GATE_STATE: "1",
93
+ GSD_WORKFLOW_PROJECT_ROOT: repoRoot,
94
+ },
95
+ });
96
+ });
97
+
98
+ test("detectWorkflowMcpLaunchConfig resolves the bundled server relative to the installed GSD package", () => {
99
+ const launch = detectWorkflowMcpLaunchConfig("/tmp/project", {
100
+ GSD_BIN_PATH: "/tmp/gsd-loader.js",
101
+ });
102
+
103
+ assert.equal(launch?.command, process.execPath);
104
+ assert.equal(launch?.cwd, "/tmp/project");
105
+ assert.equal(launch?.env?.GSD_CLI_PATH, "/tmp/gsd-loader.js");
106
+ assert.equal(launch?.env?.GSD_WORKFLOW_PROJECT_ROOT, "/tmp/project");
107
+ assert.equal(typeof launch?.args?.[0], "string");
108
+ assert.match(launch?.args?.[0] ?? "", /packages[\/\\]mcp-server[\/\\]dist[\/\\]cli\.js$/);
109
+ });
110
+
73
111
  test("usesWorkflowMcpTransport matches local externalCli providers", () => {
74
112
  assert.equal(usesWorkflowMcpTransport("externalCli", "local://claude-code"), true);
75
113
  assert.equal(usesWorkflowMcpTransport("externalCli", "https://api.example.com"), false);