gsd-pi 2.76.0-dev.4100bd590 → 2.76.0-dev.76f9a2dc5

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 (112) 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 +12 -7
  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/init-wizard.js +15 -1
  10. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  11. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  12. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  13. package/dist/web/standalone/.next/BUILD_ID +1 -1
  14. package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
  15. package/dist/web/standalone/.next/build-manifest.json +2 -2
  16. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  17. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.html +1 -1
  34. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
  41. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  42. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  43. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  44. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  45. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  46. package/package.json +1 -1
  47. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  48. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  49. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  50. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  51. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  52. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  53. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  54. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  55. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  56. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  57. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  58. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  59. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  60. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  61. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  62. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  63. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  65. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  67. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  69. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  71. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
  73. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  85. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  86. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  87. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  88. package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
  89. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  90. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  91. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  92. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  93. package/scripts/link-workspace-packages.cjs +1 -0
  94. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  95. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  96. package/src/resources/extensions/gsd/auto/session.ts +7 -1
  97. package/src/resources/extensions/gsd/auto-model-selection.ts +16 -1
  98. package/src/resources/extensions/gsd/auto-start.ts +12 -7
  99. package/src/resources/extensions/gsd/auto.ts +4 -1
  100. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  101. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  102. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  103. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  104. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  105. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  106. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  107. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  108. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  109. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  110. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  111. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → UMCfv_sVnLXawpUAjvArc}/_buildManifest.js +0 -0
  112. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → UMCfv_sVnLXawpUAjvArc}/_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
  }
@@ -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);
@@ -779,6 +783,7 @@ export async function bootstrapAutoSession(
779
783
  id: startModelSnapshot.id,
780
784
  };
781
785
  }
786
+ s.autoModeStartThinkingLevel = startThinkingSnapshot ?? null;
782
787
  s.manualSessionModelOverride = manualSessionOverride ?? null;
783
788
 
784
789
  // 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",
@@ -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 }));
@@ -178,6 +178,33 @@ test("init-wizard: multiple project files detected together", (t) => {
178
178
  }
179
179
  });
180
180
 
181
+ // ─── Git init + initial commit regression (#4530) ───────────────────────────
182
+
183
+ import { execFileSync } from "node:child_process";
184
+ import { nativeInit, nativeAddAll, nativeCommit } from "../native-git-bridge.ts";
185
+
186
+ test("init-wizard: nativeInit + nativeAddAll + nativeCommit produces a reachable HEAD (#4530)", (t) => {
187
+ // Regression: showProjectInit called nativeInit but never committed, leaving
188
+ // the branch unborn. git log and git worktree add both fail on zero-commit repos.
189
+ const dir = makeTempDir("git-init-commit");
190
+ t.after(() => { cleanup(dir); });
191
+
192
+ nativeInit(dir, "main");
193
+ execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: dir });
194
+ execFileSync("git", ["config", "user.name", "Test"], { cwd: dir });
195
+ writeFileSync(join(dir, ".gitignore"), "*.log\n", "utf-8");
196
+
197
+ nativeAddAll(dir);
198
+ nativeCommit(dir, "chore: init project");
199
+
200
+ // git log must succeed (was: fatal: your current branch 'main' does not have any commits yet)
201
+ const subject = execFileSync("git", ["log", "-1", "--format=%s"], {
202
+ cwd: dir,
203
+ encoding: "utf-8",
204
+ }).trim();
205
+ assert.equal(subject, "chore: init project");
206
+ });
207
+
181
208
  test("init-wizard: v1 with both .planning/ and .gsd/ prioritizes v2", (t) => {
182
209
  const dir = makeTempDir("both-v1-v2");
183
210
  try {
@@ -0,0 +1,49 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { join } from "node:path";
4
+
5
+ import { resolveExtensionDirFromCandidates } from "../prompt-loader.ts";
6
+
7
+ function makeExists(paths: Set<string>): (path: string) => boolean {
8
+ return (path: string) => paths.has(path);
9
+ }
10
+
11
+ test("resolveExtensionDirFromCandidates prefers user-local dir when both trees are valid", () => {
12
+ const moduleDir = "/npm/global/gsd";
13
+ const agentDir = "/home/user/.gsd/agent/extensions/gsd";
14
+ const paths = new Set<string>([
15
+ join(moduleDir, "prompts"),
16
+ join(moduleDir, "templates", "task-summary.md"),
17
+ join(agentDir, "prompts"),
18
+ join(agentDir, "templates", "task-summary.md"),
19
+ ]);
20
+
21
+ const resolved = resolveExtensionDirFromCandidates(moduleDir, agentDir, makeExists(paths));
22
+ assert.equal(resolved, agentDir);
23
+ });
24
+
25
+ test("resolveExtensionDirFromCandidates rejects module dir missing task-summary template", () => {
26
+ const moduleDir = "/npm/global/gsd";
27
+ const agentDir = "/home/user/.gsd/agent/extensions/gsd";
28
+ const paths = new Set<string>([
29
+ join(moduleDir, "prompts"),
30
+ // Missing module templates/task-summary.md on purpose.
31
+ join(agentDir, "prompts"),
32
+ join(agentDir, "templates", "task-summary.md"),
33
+ ]);
34
+
35
+ const resolved = resolveExtensionDirFromCandidates(moduleDir, agentDir, makeExists(paths));
36
+ assert.equal(resolved, agentDir);
37
+ });
38
+
39
+ test("resolveExtensionDirFromCandidates falls back to prompts-only dir when neither tree is fully valid", () => {
40
+ const moduleDir = "/npm/global/gsd";
41
+ const agentDir = "/home/user/.gsd/agent/extensions/gsd";
42
+ const paths = new Set<string>([
43
+ join(moduleDir, "prompts"),
44
+ // Neither side has templates/task-summary.md.
45
+ ]);
46
+
47
+ const resolved = resolveExtensionDirFromCandidates(moduleDir, agentDir, makeExists(paths));
48
+ assert.equal(resolved, moduleDir);
49
+ });