gsd-pi 2.76.0-dev.4100bd590 → 2.76.0-dev.4c866b677

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 (117) hide show
  1. package/dist/resource-loader.d.ts +1 -1
  2. package/dist/resource-loader.js +2 -8
  3. package/dist/resources/extensions/gsd/auto/phases.js +4 -1
  4. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +13 -2
  6. package/dist/resources/extensions/gsd/auto-start.js +28 -10
  7. package/dist/resources/extensions/gsd/auto.js +4 -1
  8. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  9. package/dist/resources/extensions/gsd/gsd-db.js +59 -3
  10. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  11. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  12. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  13. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  14. package/dist/web/standalone/.next/BUILD_ID +1 -1
  15. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  16. package/dist/web/standalone/.next/build-manifest.json +2 -2
  17. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  18. package/dist/web/standalone/.next/required-server-files.json +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.html +1 -1
  36. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  43. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  44. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  45. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  46. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  47. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  48. package/dist/web/standalone/server.js +1 -1
  49. package/package.json +1 -1
  50. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  51. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  52. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  53. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  54. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  55. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  56. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  57. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  58. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  59. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  60. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  61. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  62. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  63. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  64. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  65. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  66. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  68. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  70. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  72. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  74. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
  76. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  88. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  89. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  90. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  91. package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
  92. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  93. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  94. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  95. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  96. package/scripts/link-workspace-packages.cjs +1 -0
  97. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  98. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  99. package/src/resources/extensions/gsd/auto/session.ts +7 -1
  100. package/src/resources/extensions/gsd/auto-model-selection.ts +16 -1
  101. package/src/resources/extensions/gsd/auto-start.ts +28 -10
  102. package/src/resources/extensions/gsd/auto.ts +4 -1
  103. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  104. package/src/resources/extensions/gsd/gsd-db.ts +65 -3
  105. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  106. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  107. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  108. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  109. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  110. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  111. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  112. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  113. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +87 -0
  114. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  115. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  116. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → jDqWYbuP_CG6Kjc-uKwkN}/_buildManifest.js +0 -0
  117. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → jDqWYbuP_CG6Kjc-uKwkN}/_ssgManifest.js +0 -0
@@ -34,6 +34,7 @@ const packageMap = {
34
34
  'pi-coding-agent': { scope: '@gsd', name: 'pi-coding-agent' },
35
35
  'pi-tui': { scope: '@gsd', name: 'pi-tui' },
36
36
  'rpc-client': { scope: '@gsd-build', name: 'rpc-client' },
37
+ 'mcp-server': { scope: '@gsd-build', name: 'mcp-server' },
37
38
  }
38
39
 
39
40
  for (const scopeDir of Object.values(scopeDirs)) {
@@ -213,6 +213,7 @@ export interface LoopDeps {
213
213
  retryContext?: { isRetry: boolean; previousTier?: string },
214
214
  isAutoMode?: boolean,
215
215
  sessionModelOverride?: { provider: string; id: string } | null,
216
+ autoModeStartThinkingLevel?: ReturnType<ExtensionAPI["getThinkingLevel"]> | null,
216
217
  ) => Promise<{
217
218
  routing: { tier: string; modelDowngraded: boolean } | null;
218
219
  appliedModel: { provider: string; id: string } | null;
@@ -1471,6 +1471,7 @@ export async function runUnitPhase(
1471
1471
  sidecarItem ? undefined : { isRetry, previousTier },
1472
1472
  undefined,
1473
1473
  s.manualSessionModelOverride,
1474
+ s.autoModeStartThinkingLevel,
1474
1475
  );
1475
1476
  s.currentUnitRouting =
1476
1477
  modelResult.routing as AutoSession["currentUnitRouting"];
@@ -1485,6 +1486,9 @@ export async function runUnitPhase(
1485
1486
  if (match) {
1486
1487
  const ok = await pi.setModel(match, { persist: false });
1487
1488
  if (ok) {
1489
+ if (s.autoModeStartThinkingLevel) {
1490
+ pi.setThinkingLevel(s.autoModeStartThinkingLevel);
1491
+ }
1488
1492
  s.currentUnitModel = match as AutoSession["currentUnitModel"];
1489
1493
  ctx.ui.notify(`Hook model override: ${match.provider}/${match.id}`, "info");
1490
1494
  } else {
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { Api, Model } from "@gsd/pi-ai";
20
- import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
20
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
21
21
  import type { GitServiceImpl } from "../git-service.js";
22
22
  import type { CaptureEntry } from "../captures.js";
23
23
  import type { BudgetAlertLevel } from "../auto-budget.js";
@@ -40,6 +40,8 @@ export interface StartModel {
40
40
  id: string;
41
41
  }
42
42
 
43
+ export type ThinkingLevelSnapshot = ReturnType<ExtensionAPI["getThinkingLevel"]>;
44
+
43
45
  export interface PendingVerificationRetry {
44
46
  unitId: string;
45
47
  failureContext: string;
@@ -120,6 +122,8 @@ export class AutoSession {
120
122
  currentDispatchedModelId: string | null = null;
121
123
  originalModelId: string | null = null;
122
124
  originalModelProvider: string | null = null;
125
+ autoModeStartThinkingLevel: ThinkingLevelSnapshot | null = null;
126
+ originalThinkingLevel: ThinkingLevelSnapshot | null = null;
123
127
  lastBudgetAlertLevel: BudgetAlertLevel = 0;
124
128
 
125
129
  // ── Recovery ─────────────────────────────────────────────────────────────
@@ -241,6 +245,8 @@ export class AutoSession {
241
245
  this.currentDispatchedModelId = null;
242
246
  this.originalModelId = null;
243
247
  this.originalModelProvider = null;
248
+ this.autoModeStartThinkingLevel = null;
249
+ this.originalThinkingLevel = null;
244
250
  this.lastBudgetAlertLevel = 0;
245
251
 
246
252
  // Recovery
@@ -33,6 +33,14 @@ export interface PreferredModelConfig {
33
33
  source: "explicit" | "synthesized";
34
34
  }
35
35
 
36
+ function reapplyThinkingLevel(
37
+ pi: ExtensionAPI,
38
+ level: ReturnType<ExtensionAPI["getThinkingLevel"]> | null | undefined,
39
+ ): void {
40
+ if (!level) return;
41
+ pi.setThinkingLevel(level);
42
+ }
43
+
36
44
  export function resolvePreferredModelConfig(
37
45
  unitType: string,
38
46
  autoModeStartModel: { provider: string; id: string; flatRateCtx?: FlatRateContext } | null,
@@ -97,6 +105,8 @@ export async function selectAndApplyModel(
97
105
  isAutoMode = true,
98
106
  /** Explicit /gsd model pin captured at bootstrap for long-running auto loops. */
99
107
  sessionModelOverride?: { provider: string; id: string } | null,
108
+ /** Thinking level captured at auto-mode start and re-applied after model swaps. */
109
+ autoModeStartThinkingLevel?: ReturnType<ExtensionAPI["getThinkingLevel"]> | null,
100
110
  ): Promise<ModelSelectionResult> {
101
111
  const uokFlags = resolveUokFlags(prefs);
102
112
  const effectiveSessionModelOverride = sessionModelOverride === undefined
@@ -380,6 +390,7 @@ export async function selectAndApplyModel(
380
390
  const ok = await pi.setModel(model, { persist: false });
381
391
  if (ok) {
382
392
  appliedModel = model;
393
+ reapplyThinkingLevel(pi, autoModeStartThinkingLevel);
383
394
 
384
395
  // ADR-005: Adjust active tool set for the selected model's provider capabilities.
385
396
  // Hard-filter incompatible tools, then let extensions override via adjust_tool_set hook.
@@ -456,10 +467,14 @@ export async function selectAndApplyModel(
456
467
  );
457
468
  if (byId) {
458
469
  const fallbackOk = await pi.setModel(byId, { persist: false });
459
- if (fallbackOk) appliedModel = byId;
470
+ if (fallbackOk) {
471
+ appliedModel = byId;
472
+ reapplyThinkingLevel(pi, autoModeStartThinkingLevel);
473
+ }
460
474
  }
461
475
  } else {
462
476
  appliedModel = startModel;
477
+ reapplyThinkingLevel(pi, autoModeStartThinkingLevel);
463
478
  }
464
479
  }
465
480
  }
@@ -60,7 +60,7 @@ import { initRoutingHistory } from "./routing-history.js";
60
60
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
61
61
  import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
62
62
  import { snapshotSkills } from "./skill-discovery.js";
63
- import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js";
63
+ import { isDbAvailable, getMilestone, openDatabase, getDbStatus } from "./gsd-db.js";
64
64
  import { hideFooter } from "./auto-dashboard.js";
65
65
  import {
66
66
  debugLog,
@@ -273,8 +273,8 @@ export async function bootstrapAutoSession(
273
273
  //
274
274
  // Precedence:
275
275
  // 1) Explicit session override via /gsd model (this session)
276
- // 2) GSD model preferences from PREFERENCES.md (validated against live auth)
277
- // 3) Current session model from settings/session restore (if provider ready)
276
+ // 2) Current session model from settings/session restore (if provider ready)
277
+ // 3) GSD model preferences from PREFERENCES.md (validated against live auth)
278
278
  //
279
279
  // This preserves #3517 defaults while honoring explicit runtime model
280
280
  // selection for subsequent /gsd runs in the same session.
@@ -314,11 +314,14 @@ export async function bootstrapAutoSession(
314
314
  }
315
315
  const sessionModelReady =
316
316
  ctx.model && ctx.modelRegistry.isProviderRequestReady(ctx.model.provider);
317
+ const currentSessionModel = (sessionModelReady && ctx.model)
318
+ ? { provider: ctx.model.provider, id: ctx.model.id }
319
+ : null;
320
+ const startThinkingSnapshot = pi.getThinkingLevel();
317
321
  const startModelSnapshot = manualSessionOverride
322
+ ?? currentSessionModel
318
323
  ?? validatedPreferredModel
319
- ?? (sessionModelReady && ctx.model
320
- ? { provider: ctx.model.provider, id: ctx.model.id }
321
- : null);
324
+ ?? null;
322
325
 
323
326
  try {
324
327
  // Validate GSD_PROJECT_ID early so the user gets immediate feedback
@@ -664,8 +667,9 @@ export async function bootstrapAutoSession(
664
667
  s.pendingQuickTasks = [];
665
668
  s.currentUnit = null;
666
669
  s.currentMilestoneId = state.activeMilestone?.id ?? null;
667
- s.originalModelId = ctx.model?.id ?? null;
668
- s.originalModelProvider = ctx.model?.provider ?? null;
670
+ s.originalModelId = startModelSnapshot?.id ?? ctx.model?.id ?? null;
671
+ s.originalModelProvider = startModelSnapshot?.provider ?? ctx.model?.provider ?? null;
672
+ s.originalThinkingLevel = startThinkingSnapshot ?? null;
669
673
 
670
674
  // Register SIGTERM handler
671
675
  registerSigtermHandler(base);
@@ -758,9 +762,22 @@ export async function bootstrapAutoSession(
758
762
  // call returns "db_unavailable", triggering artifact-retry which
759
763
  // re-dispatches the same task — producing an infinite loop (#2419).
760
764
  if (existsSync(gsdDbPath) && !isDbAvailable()) {
765
+ const dbStatus = getDbStatus();
766
+ const phaseHint = dbStatus.lastPhase === "open"
767
+ ? "The database file could not be opened"
768
+ : dbStatus.lastPhase === "initSchema"
769
+ ? "The database schema could not be initialized"
770
+ : dbStatus.lastPhase === "vacuum-recovery"
771
+ ? "Corruption recovery (VACUUM) failed"
772
+ : dbStatus.attempted
773
+ ? "The database could not be opened (phase unknown)"
774
+ : "The database provider could not be loaded";
775
+ const errorDetail = dbStatus.lastError ? ` (${dbStatus.lastError.message})` : "";
776
+ const providerHint = dbStatus.provider
777
+ ? ` Provider: ${dbStatus.provider}.`
778
+ : " No SQLite provider available — check Node >= 22 or install better-sqlite3.";
761
779
  ctx.ui.notify(
762
- "SQLite database exists but failed to open. Auto-mode cannot proceed without a working database provider. " +
763
- "Check for corrupt gsd.db or missing native SQLite bindings.",
780
+ `SQLite database exists but failed to open: ${gsdDbPath}. ${phaseHint}${errorDetail}.${providerHint}`,
764
781
  "error",
765
782
  );
766
783
  return releaseLockAndReturn();
@@ -779,6 +796,7 @@ export async function bootstrapAutoSession(
779
796
  id: startModelSnapshot.id,
780
797
  };
781
798
  }
799
+ s.autoModeStartThinkingLevel = startThinkingSnapshot ?? null;
782
800
  s.manualSessionModelOverride = manualSessionOverride ?? null;
783
801
 
784
802
  // Apply worker model override from parallel orchestrator (#worker-model).
@@ -969,7 +969,7 @@ export async function stopAuto(
969
969
  logWarning("engine", `file unlink failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
970
970
  }
971
971
 
972
- // ── Step 13: Restore original model (before reset clears IDs) ──
972
+ // ── Step 13: Restore original model + thinking (before reset clears IDs) ──
973
973
  try {
974
974
  if (pi && ctx && s.originalModelId && s.originalModelProvider) {
975
975
  const original = ctx.modelRegistry.find(
@@ -978,6 +978,9 @@ export async function stopAuto(
978
978
  );
979
979
  if (original) await pi.setModel(original);
980
980
  }
981
+ if (pi && s.originalThinkingLevel) {
982
+ pi.setThinkingLevel(s.originalThinkingLevel);
983
+ }
981
984
  } catch (e) {
982
985
  debugLog("stop-cleanup-model", { error: e instanceof Error ? e.message : String(e) });
983
986
  }
@@ -32,11 +32,13 @@ export interface TaskMetadata {
32
32
  // ─── Unit Type → Default Tier Mapping ────────────────────────────────────────
33
33
 
34
34
  const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
35
- // Tier 1 — Light: structured summaries, completion, UAT
36
- "complete-slice": "light",
35
+ // Tier 1 — Light: compact verification turns
37
36
  "run-uat": "light",
38
37
 
39
- // Tier 2 — Standard: research, routine discussion
38
+ // Tier 2 — Standard: research, routine discussion, slice completion
39
+ // complete-slice can carry large inlined context; avoid routing it to the
40
+ // cheapest "light" model by default (#4520).
41
+ "complete-slice": "standard",
40
42
  "discuss-milestone": "standard",
41
43
  "discuss-slice": "standard",
42
44
  "research-milestone": "standard",
@@ -1199,6 +1199,8 @@ let currentPath: string | null = null;
1199
1199
  let currentPid: number = 0;
1200
1200
  let _exitHandlerRegistered = false;
1201
1201
  let _dbOpenAttempted = false;
1202
+ let _lastDbError: Error | null = null;
1203
+ let _lastDbPhase: "open" | "initSchema" | "vacuum-recovery" | null = null;
1202
1204
 
1203
1205
  export function getDbProvider(): ProviderName | null {
1204
1206
  loadProvider();
@@ -1219,12 +1221,58 @@ export function wasDbOpenAttempted(): boolean {
1219
1221
  return _dbOpenAttempted;
1220
1222
  }
1221
1223
 
1224
+ export function getDbStatus(): {
1225
+ available: boolean;
1226
+ provider: ProviderName | null;
1227
+ attempted: boolean;
1228
+ lastError: Error | null;
1229
+ lastPhase: "open" | "initSchema" | "vacuum-recovery" | null;
1230
+ } {
1231
+ loadProvider();
1232
+ return {
1233
+ available: currentDb !== null,
1234
+ provider: providerName,
1235
+ attempted: _dbOpenAttempted,
1236
+ lastError: _lastDbError,
1237
+ lastPhase: _lastDbPhase,
1238
+ };
1239
+ }
1240
+
1222
1241
  export function openDatabase(path: string): boolean {
1223
1242
  _dbOpenAttempted = true;
1224
1243
  if (currentDb && currentPath !== path) closeDatabase();
1225
1244
  if (currentDb && currentPath === path) return true;
1226
1245
 
1227
- const rawDb = openRawDb(path);
1246
+ // Reset error state only when a new open attempt is actually going to run.
1247
+ _lastDbError = null;
1248
+ _lastDbPhase = null;
1249
+
1250
+ let rawDb: unknown;
1251
+ let fallbackProvider: ProviderName | null = null;
1252
+ let fallbackModule: unknown = null;
1253
+ try {
1254
+ rawDb = openRawDb(path);
1255
+ } catch (primaryErr) {
1256
+ _lastDbPhase = "open";
1257
+ _lastDbError = primaryErr instanceof Error ? primaryErr : new Error(String(primaryErr));
1258
+ // node:sqlite loaded but failed to open this file — try better-sqlite3 as fallback.
1259
+ if (providerName === "node:sqlite") {
1260
+ try {
1261
+ const mod = _require("better-sqlite3");
1262
+ const Db = (mod && mod.default) ? mod.default : mod;
1263
+ if (typeof Db === "function") {
1264
+ rawDb = new Db(path);
1265
+ fallbackProvider = "better-sqlite3";
1266
+ fallbackModule = Db;
1267
+ _lastDbError = null;
1268
+ _lastDbPhase = null;
1269
+ }
1270
+ } catch {
1271
+ // fallback unavailable; surface original error
1272
+ }
1273
+ }
1274
+ if (!rawDb) throw primaryErr;
1275
+ }
1228
1276
  if (!rawDb) return false;
1229
1277
 
1230
1278
  const adapter = createAdapter(rawDb);
@@ -1240,15 +1288,25 @@ export function openDatabase(path: string): boolean {
1240
1288
  initSchema(adapter, fileBacked);
1241
1289
  process.stderr.write("gsd-db: recovered corrupt database via VACUUM\n");
1242
1290
  } catch (retryErr) {
1291
+ _lastDbPhase = "vacuum-recovery";
1292
+ _lastDbError = retryErr instanceof Error ? retryErr : new Error(String(retryErr));
1243
1293
  try { adapter.close(); } catch (e) { logWarning("db", `close after VACUUM failed: ${(e as Error).message}`); }
1244
1294
  throw retryErr;
1245
1295
  }
1246
1296
  } else {
1247
- try { adapter.close(); } catch (e) { logWarning("db", `close after VACUUM failed: ${(e as Error).message}`); }
1297
+ _lastDbPhase = "initSchema";
1298
+ _lastDbError = err instanceof Error ? err : new Error(String(err));
1299
+ try { adapter.close(); } catch (e) { logWarning("db", `close after initSchema failed: ${(e as Error).message}`); }
1248
1300
  throw err;
1249
1301
  }
1250
1302
  }
1251
1303
 
1304
+ // Commit fallback provider switch only after open + schema both succeeded.
1305
+ if (fallbackProvider) {
1306
+ providerName = fallbackProvider;
1307
+ providerModule = fallbackModule;
1308
+ }
1309
+
1252
1310
  currentDb = adapter;
1253
1311
  currentPath = path;
1254
1312
  currentPid = process.pid;
@@ -1276,8 +1334,12 @@ export function closeDatabase(): void {
1276
1334
  currentDb = null;
1277
1335
  currentPath = null;
1278
1336
  currentPid = 0;
1279
- _dbOpenAttempted = false;
1280
1337
  }
1338
+ // Reset session-scoped state unconditionally so stale error info from a
1339
+ // failed open doesn't persist into the next open attempt or status check.
1340
+ _dbOpenAttempted = false;
1341
+ _lastDbError = null;
1342
+ _lastDbPhase = null;
1281
1343
  }
1282
1344
 
1283
1345
  /** Run a full VACUUM — call sparingly (e.g. after milestone completion). */
@@ -10,7 +10,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
10
10
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { showNextAction } from "../shared/tui.js";
13
- import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
13
+ import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
14
14
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
15
15
  import { gsdRoot } from "./paths.js";
16
16
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -74,6 +74,7 @@ export async function showProjectInit(
74
74
  }
75
75
 
76
76
  // ── Step 2: Git setup ──────────────────────────────────────────────────────
77
+ let didInitGit = false;
77
78
  if (!signals.isGitRepo) {
78
79
  const gitChoice = await showNextAction(ctx, {
79
80
  title: "GSD — Project Setup",
@@ -89,6 +90,7 @@ export async function showProjectInit(
89
90
 
90
91
  if (gitChoice === "init_git") {
91
92
  nativeInit(basePath, prefs.mainBranch);
93
+ didInitGit = true;
92
94
  }
93
95
  } else {
94
96
  // Auto-detect main branch from existing repo
@@ -295,6 +297,18 @@ export async function showProjectInit(
295
297
  ensureGitignore(basePath);
296
298
  untrackRuntimeFiles(basePath);
297
299
 
300
+ // Create initial commit so git log and git worktree work immediately (#4530).
301
+ // Without this, the branch is "unborn" (zero commits) and downstream operations
302
+ // like `git log` and `git worktree add` fail.
303
+ if (didInitGit) {
304
+ try {
305
+ nativeAddAll(basePath);
306
+ nativeCommit(basePath, "chore: init project");
307
+ } catch {
308
+ // Non-fatal — user can commit manually; don't block project init
309
+ }
310
+ }
311
+
298
312
  // Auto-generate codebase map for instant agent orientation
299
313
  try {
300
314
  const result = generateCodebaseMap(basePath);
@@ -24,6 +24,35 @@ import { fileURLToPath } from "node:url";
24
24
  import { homedir } from "node:os";
25
25
  import { logWarning } from "./workflow-logger.js";
26
26
 
27
+ type ExistsFn = (path: string) => boolean;
28
+
29
+ function hasRequiredExtensionAssets(rootDir: string, exists: ExistsFn = existsSync): boolean {
30
+ return (
31
+ exists(join(rootDir, "prompts")) &&
32
+ exists(join(rootDir, "templates", "task-summary.md"))
33
+ );
34
+ }
35
+
36
+ export function resolveExtensionDirFromCandidates(
37
+ moduleDir: string,
38
+ agentGsdDir: string,
39
+ exists: ExistsFn = existsSync,
40
+ ): string {
41
+ const moduleUsable = hasRequiredExtensionAssets(moduleDir, exists);
42
+ const agentUsable = hasRequiredExtensionAssets(agentGsdDir, exists);
43
+
44
+ // Prefer the user-local extension tree when both are valid. This avoids
45
+ // leaking npm/global-install paths into prompts on Windows.
46
+ if (agentUsable) return agentGsdDir;
47
+ if (moduleUsable) return moduleDir;
48
+
49
+ // Degraded fallback: if required template is missing in both locations,
50
+ // keep previous behavior and prefer whichever still has prompts/.
51
+ if (exists(join(moduleDir, "prompts"))) return moduleDir;
52
+ if (exists(join(agentGsdDir, "prompts"))) return agentGsdDir;
53
+ return moduleDir;
54
+ }
55
+
27
56
  /**
28
57
  * Resolve the GSD extension directory.
29
58
  *
@@ -36,15 +65,9 @@ import { logWarning } from "./workflow-logger.js";
36
65
  */
37
66
  function resolveExtensionDir(): string {
38
67
  const moduleDir = dirname(fileURLToPath(import.meta.url));
39
- if (existsSync(join(moduleDir, "prompts"))) return moduleDir;
40
-
41
- // Fallback: user-local agent directory
42
68
  const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
43
69
  const agentGsdDir = join(gsdHome, "agent", "extensions", "gsd");
44
- if (existsSync(join(agentGsdDir, "prompts"))) return agentGsdDir;
45
-
46
- // Last resort: return the module dir (warmCache will silently handle the miss)
47
- return moduleDir;
70
+ return resolveExtensionDirFromCandidates(moduleDir, agentGsdDir);
48
71
  }
49
72
 
50
73
  const __extensionDir = resolveExtensionDir();
@@ -100,7 +100,7 @@ function getChangedFilesFromLastCommit(basePath: string): string[] | null {
100
100
  try {
101
101
  const result = execFileSync(
102
102
  "git",
103
- ["diff", "--name-only", "HEAD~1", "HEAD"],
103
+ ["diff-tree", "--root", "--no-commit-id", "-r", "--name-only", "HEAD"],
104
104
  { cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
105
105
  ).trim();
106
106
  return result ? result.split("\n").filter(Boolean) : [];
@@ -341,6 +341,18 @@ test("dynamic routing passes provider-qualified model keys to the router", () =>
341
341
  );
342
342
  });
343
343
 
344
+ test("selectAndApplyModel re-applies captured thinking level after setModel success", () => {
345
+ const src = readFileSync(join(__dirname, "..", "auto-model-selection.ts"), "utf-8");
346
+ assert.ok(
347
+ src.includes("autoModeStartThinkingLevel?: ReturnType<ExtensionAPI[\"getThinkingLevel\"]> | null"),
348
+ "selectAndApplyModel should accept an autoModeStartThinkingLevel parameter",
349
+ );
350
+ assert.ok(
351
+ src.includes("reapplyThinkingLevel(pi, autoModeStartThinkingLevel)"),
352
+ "selectAndApplyModel should re-apply captured thinking level after model changes",
353
+ );
354
+ });
355
+
344
356
  test("resolveModelId: anthropic wins over claude-code when session provider is not claude-code", () => {
345
357
  const availableModels = [
346
358
  { id: "claude-sonnet-4-6", provider: "claude-code" },
@@ -52,9 +52,7 @@ test("bootstrapAutoSession checks manual session override before preferences", (
52
52
  "manual override and preference fallback must be resolved before building startModelSnapshot",
53
53
  );
54
54
 
55
- // The validated preferred model must still appear as one of the snapshot
56
- // sources so PREFERENCES.md continues to win over a stale settings.json
57
- // default for built-in providers.
55
+ // Preferred model should still be part of fallback resolution.
58
56
  const snapshotBlock = source.slice(snapshotIdx, snapshotIdx + 400);
59
57
  assert.ok(
60
58
  snapshotBlock.includes("validatedPreferredModel") || snapshotBlock.includes("preferredModel"),
@@ -62,6 +60,22 @@ test("bootstrapAutoSession checks manual session override before preferences", (
62
60
  );
63
61
  });
64
62
 
63
+ test("bootstrapAutoSession prioritizes current session model over PREFERENCES.md default", () => {
64
+ const snapshotIdx = source.indexOf("const startModelSnapshot = manualSessionOverride");
65
+ assert.ok(snapshotIdx > -1, "auto-start.ts should build startModelSnapshot");
66
+
67
+ const snapshotBlock = source.slice(snapshotIdx, snapshotIdx + 500);
68
+ const currentIdx = snapshotBlock.indexOf("currentSessionModel");
69
+ const preferredIdx = snapshotBlock.indexOf("validatedPreferredModel");
70
+
71
+ assert.ok(currentIdx > -1, "startModelSnapshot should include currentSessionModel");
72
+ assert.ok(preferredIdx > -1, "startModelSnapshot should include validatedPreferredModel");
73
+ assert.ok(
74
+ currentIdx < preferredIdx,
75
+ "startModelSnapshot should prefer currentSessionModel before validatedPreferredModel",
76
+ );
77
+ });
78
+
65
79
  test("bootstrapAutoSession prefers session model over PREFERENCES.md when provider is custom (#4122)", () => {
66
80
  // Custom providers (Ollama, vLLM, OpenAI-compatible proxies) live in
67
81
  // ~/.gsd/agent/models.json, not PREFERENCES.md. When the user picks one
@@ -111,3 +125,19 @@ test("bootstrapAutoSession validates preferred model against live registry auth
111
125
  const warningIdx = source.indexOf("is not configured; falling back to session default");
112
126
  assert.ok(warningIdx > -1, "auto-start.ts should warn when preferred model is unconfigured");
113
127
  });
128
+
129
+ test("bootstrapAutoSession snapshots and persists thinking level for auto-mode lifecycle", () => {
130
+ const captureIdx = source.indexOf("const startThinkingSnapshot = pi.getThinkingLevel()");
131
+ assert.ok(captureIdx > -1, "auto-start.ts should snapshot thinking level at bootstrap start");
132
+
133
+ const originalThinkingIdx = source.indexOf("s.originalThinkingLevel = startThinkingSnapshot ?? null");
134
+ assert.ok(originalThinkingIdx > -1, "auto-start.ts should store originalThinkingLevel from snapshot");
135
+
136
+ const autoThinkingIdx = source.indexOf("s.autoModeStartThinkingLevel = startThinkingSnapshot ?? null");
137
+ assert.ok(autoThinkingIdx > -1, "auto-start.ts should store autoModeStartThinkingLevel from snapshot");
138
+
139
+ assert.ok(
140
+ captureIdx < originalThinkingIdx && captureIdx < autoThinkingIdx,
141
+ "thinking snapshot must be captured before session state assignment",
142
+ );
143
+ });
@@ -0,0 +1,38 @@
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 autoSrc = readFileSync(join(import.meta.dirname, "..", "auto.ts"), "utf-8");
7
+ const phasesSrc = readFileSync(join(import.meta.dirname, "..", "auto", "phases.ts"), "utf-8");
8
+
9
+ test("stopAuto restores original thinking level", () => {
10
+ assert.ok(
11
+ autoSrc.includes("if (pi && s.originalThinkingLevel)"),
12
+ "auto.ts should conditionally restore original thinking level in stopAuto",
13
+ );
14
+ assert.ok(
15
+ autoSrc.includes("pi.setThinkingLevel(s.originalThinkingLevel)"),
16
+ "auto.ts should call pi.setThinkingLevel with originalThinkingLevel",
17
+ );
18
+ });
19
+
20
+ test("runUnitPhase threads captured thinking level into selectAndApplyModel", () => {
21
+ const callIdx = phasesSrc.indexOf("deps.selectAndApplyModel(");
22
+ assert.ok(callIdx > -1, "phases.ts should call selectAndApplyModel");
23
+ const callBlock = phasesSrc.slice(callIdx, callIdx + 600);
24
+ assert.ok(
25
+ callBlock.includes("s.autoModeStartThinkingLevel"),
26
+ "runUnitPhase should pass autoModeStartThinkingLevel to selectAndApplyModel",
27
+ );
28
+ });
29
+
30
+ test("hook model override preserves captured thinking level", () => {
31
+ const hookIdx = phasesSrc.indexOf("const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride;");
32
+ assert.ok(hookIdx > -1, "phases.ts should include hook model override handling");
33
+ const hookBlock = phasesSrc.slice(hookIdx, hookIdx + 600);
34
+ assert.ok(
35
+ hookBlock.includes("pi.setThinkingLevel(s.autoModeStartThinkingLevel)"),
36
+ "hook model override should re-apply captured thinking level after setModel",
37
+ );
38
+ });
@@ -21,9 +21,9 @@ test("tierOrdinal returns correct ordering", () => {
21
21
 
22
22
  // ─── Unit Type Classification ────────────────────────────────────────────────
23
23
 
24
- test("complete-slice classifies as light", () => {
24
+ test("complete-slice classifies as standard", () => {
25
25
  const result = classifyUnitComplexity("complete-slice", "M001/S01", "/tmp/fake");
26
- assert.equal(result.tier, "light");
26
+ assert.equal(result.tier, "standard");
27
27
  });
28
28
 
29
29
  test("run-uat classifies as light", () => {
@@ -145,7 +145,7 @@ test("budget pressure at 90% downgrades standard to light", () => {
145
145
  assert.equal(result.downgraded, true);
146
146
  });
147
147
 
148
- test("budget pressure at 90% downgrades light stays light", () => {
148
+ test("budget pressure at 90% downgrades complete-slice standard to light", () => {
149
149
  const result = classifyUnitComplexity("complete-slice", "M001/S01", "/tmp/fake", 0.95);
150
150
  assert.equal(result.tier, "light");
151
151
  });
@@ -15,6 +15,26 @@ function git(cwd: string, ...args: string[]): string {
15
15
  }).trim();
16
16
  }
17
17
 
18
+ test("validateFileChanges works on repos with a single commit (no HEAD~1)", (t) => {
19
+ const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
20
+ t.after(() => rmSync(base, { recursive: true, force: true }));
21
+
22
+ git(base, "init");
23
+ git(base, "config", "user.email", "test@example.com");
24
+ git(base, "config", "user.name", "Test User");
25
+
26
+ writeFileSync(join(base, "foo.ts"), "export const x = 1;\n");
27
+ git(base, "add", ".");
28
+ git(base, "commit", "-m", "initial");
29
+
30
+ // With only one commit, HEAD~1 doesn't exist — this must not throw
31
+ const audit = validateFileChanges(base, ["foo.ts"], []);
32
+
33
+ assert.ok(audit, "audit should be produced for single-commit repo");
34
+ assert.deepEqual(audit.unexpectedFiles, []);
35
+ assert.deepEqual(audit.missingFiles, []);
36
+ });
37
+
18
38
  test("validateFileChanges ignores inline descriptions in expected output paths", (t) => {
19
39
  const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
20
40
  t.after(() => rmSync(base, { recursive: true, force: true }));