gsd-pi 2.76.0-dev.b072ebb73 → 2.76.0-dev.fe143342a

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 (200) hide show
  1. package/dist/mcp-server.d.ts +7 -0
  2. package/dist/mcp-server.js +35 -1
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +2 -8
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
  6. package/dist/resources/extensions/gsd/auto/phases.js +4 -1
  7. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  8. package/dist/resources/extensions/gsd/auto-model-selection.js +39 -13
  9. package/dist/resources/extensions/gsd/auto-start.js +39 -21
  10. package/dist/resources/extensions/gsd/auto.js +15 -12
  11. package/dist/resources/extensions/gsd/blocked-models.js +68 -0
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +76 -0
  13. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  14. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  15. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
  17. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  18. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  19. package/dist/resources/extensions/gsd/error-classifier.js +31 -3
  20. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  21. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  22. package/dist/resources/extensions/gsd/gsd-db.js +62 -4
  23. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  24. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  25. package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
  26. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  27. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  28. package/dist/resources/extensions/gsd/preferences.js +17 -17
  29. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  30. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  31. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  32. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  33. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  34. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  35. package/dist/resources/extensions/search-the-web/command-search-provider.js +5 -4
  36. package/dist/resources/extensions/search-the-web/native-search.js +45 -13
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/required-server-files.json +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.html +1 -1
  60. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  67. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  69. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  70. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  71. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  72. package/dist/web/standalone/server.js +1 -1
  73. package/package.json +1 -1
  74. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  75. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  76. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  77. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  78. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  79. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  80. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  81. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  82. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  83. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  84. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  85. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  86. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  87. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  88. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  89. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  90. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  91. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  92. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  93. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  94. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  95. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  96. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  98. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  100. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  104. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
  106. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  110. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  112. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  114. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  117. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  119. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  131. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  132. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  133. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  134. package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
  135. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  136. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  137. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  138. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  139. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  140. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  141. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  142. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  143. package/scripts/link-workspace-packages.cjs +1 -0
  144. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
  145. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
  146. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  147. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  148. package/src/resources/extensions/gsd/auto/session.ts +7 -1
  149. package/src/resources/extensions/gsd/auto-model-selection.ts +50 -12
  150. package/src/resources/extensions/gsd/auto-start.ts +40 -22
  151. package/src/resources/extensions/gsd/auto.ts +15 -12
  152. package/src/resources/extensions/gsd/blocked-models.ts +98 -0
  153. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +97 -0
  154. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  155. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  156. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  157. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
  158. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  159. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  160. package/src/resources/extensions/gsd/error-classifier.ts +36 -3
  161. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  162. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  163. package/src/resources/extensions/gsd/gsd-db.ts +68 -4
  164. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  165. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  166. package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
  167. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  168. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  169. package/src/resources/extensions/gsd/preferences.ts +17 -17
  170. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  171. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  172. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  173. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  174. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  175. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +98 -0
  176. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  177. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  178. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  179. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  180. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  181. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
  182. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  183. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  184. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  185. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  186. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
  187. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  188. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  189. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +91 -0
  190. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  191. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  192. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  193. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  194. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  195. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  196. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  197. package/src/resources/extensions/search-the-web/command-search-provider.ts +5 -4
  198. package/src/resources/extensions/search-the-web/native-search.ts +48 -12
  199. /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
  200. /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
@@ -68,13 +68,13 @@ export { resolveAllSkillReferences } from "./preferences-skills.js";
68
68
  // These lived in preferences-skills.ts but imported loadEffectiveGSDPreferences
69
69
  // back from this file, creating a circular dependency. Moved here since they
70
70
  // are trivial wrappers over loadEffectiveGSDPreferences.
71
- export function resolveSkillDiscoveryMode(): SkillDiscoveryMode {
72
- const prefs = loadEffectiveGSDPreferences();
71
+ export function resolveSkillDiscoveryMode(basePath?: string): SkillDiscoveryMode {
72
+ const prefs = loadEffectiveGSDPreferences(basePath);
73
73
  return prefs?.preferences.skill_discovery ?? "suggest";
74
74
  }
75
75
 
76
- export function resolveSkillStalenessDays(): number {
77
- const prefs = loadEffectiveGSDPreferences();
76
+ export function resolveSkillStalenessDays(basePath?: string): number {
77
+ const prefs = loadEffectiveGSDPreferences(basePath);
78
78
  return prefs?.preferences.skill_staleness_days ?? 60;
79
79
  }
80
80
 
@@ -109,16 +109,16 @@ function legacyGlobalPreferencesPath(): string {
109
109
  return join(homedir(), ".pi", "agent", "gsd-preferences.md");
110
110
  }
111
111
 
112
- function projectPreferencesPath(): string {
113
- return join(gsdRoot(process.cwd()), "PREFERENCES.md");
112
+ function projectPreferencesPath(basePath: string = process.cwd()): string {
113
+ return join(gsdRoot(basePath), "PREFERENCES.md");
114
114
  }
115
115
  // Legacy lowercase files can still exist in older projects. Keep them as a
116
116
  // compatibility-only fallback, but route new reads/writes through PREFERENCES.md.
117
117
  function legacyGlobalPreferencesPathLowercase(): string {
118
118
  return join(gsdHome(), "preferences.md");
119
119
  }
120
- function legacyProjectPreferencesPathLowercase(): string {
121
- return join(gsdRoot(process.cwd()), "preferences.md");
120
+ function legacyProjectPreferencesPathLowercase(basePath: string = process.cwd()): string {
121
+ return join(gsdRoot(basePath), "preferences.md");
122
122
  }
123
123
 
124
124
  export function getGlobalGSDPreferencesPath(): string {
@@ -129,8 +129,8 @@ export function getLegacyGlobalGSDPreferencesPath(): string {
129
129
  return legacyGlobalPreferencesPath();
130
130
  }
131
131
 
132
- export function getProjectGSDPreferencesPath(): string {
133
- return projectPreferencesPath();
132
+ export function getProjectGSDPreferencesPath(basePath?: string): string {
133
+ return projectPreferencesPath(basePath);
134
134
  }
135
135
 
136
136
  // ─── Loading ────────────────────────────────────────────────────────────────
@@ -141,14 +141,14 @@ export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null {
141
141
  ?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
142
142
  }
143
143
 
144
- export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
145
- return loadPreferencesFile(projectPreferencesPath(), "project")
146
- ?? loadPreferencesFile(legacyProjectPreferencesPathLowercase(), "project");
144
+ export function loadProjectGSDPreferences(basePath?: string): LoadedGSDPreferences | null {
145
+ return loadPreferencesFile(projectPreferencesPath(basePath), "project")
146
+ ?? loadPreferencesFile(legacyProjectPreferencesPathLowercase(basePath), "project");
147
147
  }
148
148
 
149
- export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null {
149
+ export function loadEffectiveGSDPreferences(basePath?: string): LoadedGSDPreferences | null {
150
150
  const globalPreferences = loadGlobalGSDPreferences();
151
- const projectPreferences = loadProjectGSDPreferences();
151
+ const projectPreferences = loadProjectGSDPreferences(basePath);
152
152
 
153
153
  if (!globalPreferences && !projectPreferences) return null;
154
154
 
@@ -603,8 +603,8 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
603
603
  * Worktree isolation requires explicit opt-in because it depends on git
604
604
  * branch infrastructure that must be set up before use.
605
605
  */
606
- export function getIsolationMode(): "none" | "worktree" | "branch" {
607
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
606
+ export function getIsolationMode(basePath?: string): "none" | "worktree" | "branch" {
607
+ const prefs = loadEffectiveGSDPreferences(basePath)?.preferences?.git;
608
608
  if (prefs?.isolation === "worktree") return "worktree";
609
609
  if (prefs?.isolation === "branch") return "branch";
610
610
  return "none"; // default — no isolation, work on current branch
@@ -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
+ });
@@ -0,0 +1,98 @@
1
+ // GSD — Tests for persistent blocked-models store (issue #4513)
2
+
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+
9
+ import {
10
+ blockModel,
11
+ isModelBlocked,
12
+ loadBlockedModels,
13
+ } from "../blocked-models.ts";
14
+
15
+ function mkBase(): string {
16
+ const base = mkdtempSync(join(tmpdir(), "gsd-blocked-models-"));
17
+ mkdirSync(join(base, ".gsd"), { recursive: true });
18
+ return base;
19
+ }
20
+
21
+ test("blocked-models: round-trip write and read", () => {
22
+ const base = mkBase();
23
+ try {
24
+ assert.equal(isModelBlocked(base, "openai-codex", "gpt-5.1-codex-max"), false);
25
+ blockModel(base, "openai-codex", "gpt-5.1-codex-max", "not supported for ChatGPT account");
26
+ assert.equal(isModelBlocked(base, "openai-codex", "gpt-5.1-codex-max"), true);
27
+
28
+ const entries = loadBlockedModels(base);
29
+ assert.equal(entries.length, 1);
30
+ assert.equal(entries[0].provider, "openai-codex");
31
+ assert.equal(entries[0].id, "gpt-5.1-codex-max");
32
+ assert.ok(entries[0].blockedAt > 0);
33
+ } finally {
34
+ rmSync(base, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ test("blocked-models: case-insensitive lookup", () => {
39
+ const base = mkBase();
40
+ try {
41
+ blockModel(base, "OpenAI-Codex", "GPT-5.1-Codex-Max", "reason");
42
+ assert.equal(isModelBlocked(base, "openai-codex", "gpt-5.1-codex-max"), true);
43
+ assert.equal(isModelBlocked(base, "OPENAI-CODEX", "GPT-5.1-CODEX-MAX"), true);
44
+ } finally {
45
+ rmSync(base, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test("blocked-models: dedupes repeated blocks", () => {
50
+ const base = mkBase();
51
+ try {
52
+ blockModel(base, "openai-codex", "gpt-5", "first");
53
+ blockModel(base, "openai-codex", "gpt-5", "second");
54
+ blockModel(base, "openai-codex", "gpt-5", "third");
55
+ assert.equal(loadBlockedModels(base).length, 1);
56
+ } finally {
57
+ rmSync(base, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ test("blocked-models: corrupted JSON recovers to empty", () => {
62
+ const base = mkBase();
63
+ try {
64
+ const path = join(base, ".gsd", "runtime", "blocked-models.json");
65
+ mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
66
+ writeFileSync(path, "{not valid json", "utf-8");
67
+
68
+ assert.equal(loadBlockedModels(base).length, 0);
69
+ assert.equal(isModelBlocked(base, "any", "model"), false);
70
+
71
+ // A subsequent write should still succeed (overwrites the corrupt file).
72
+ blockModel(base, "openai-codex", "gpt-5", "reason");
73
+ assert.equal(loadBlockedModels(base).length, 1);
74
+ } finally {
75
+ rmSync(base, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ test("blocked-models: returns false for missing provider or id", () => {
80
+ const base = mkBase();
81
+ try {
82
+ blockModel(base, "openai-codex", "gpt-5", "reason");
83
+ assert.equal(isModelBlocked(base, undefined, "gpt-5"), false);
84
+ assert.equal(isModelBlocked(base, "openai-codex", undefined), false);
85
+ } finally {
86
+ rmSync(base, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test("blocked-models: file created under .gsd/runtime/", () => {
91
+ const base = mkBase();
92
+ try {
93
+ blockModel(base, "openai-codex", "gpt-5", "reason");
94
+ assert.ok(existsSync(join(base, ".gsd", "runtime", "blocked-models.json")));
95
+ } finally {
96
+ rmSync(base, { recursive: true, force: true });
97
+ }
98
+ });
@@ -0,0 +1,123 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import {
8
+ buildSnapshot,
9
+ readCompactionSnapshot,
10
+ writeCompactionSnapshot,
11
+ DEFAULT_SNAPSHOT_BYTES,
12
+ } from '../compaction-snapshot.ts';
13
+ import { closeDatabase, openDatabase } from '../gsd-db.ts';
14
+ import { createMemory } from '../memory-store.ts';
15
+ import { executeResume } from '../tools/resume-tool.ts';
16
+
17
+ function freshBase(): string {
18
+ return mkdtempSync(join(tmpdir(), 'gsd-snap-'));
19
+ }
20
+
21
+ function cleanup(dir: string): void {
22
+ rmSync(dir, { recursive: true, force: true });
23
+ }
24
+
25
+ test('buildSnapshot: renders memories, exec history, and active context', () => {
26
+ const snap = buildSnapshot({
27
+ generatedAt: new Date('2026-04-20T12:00:00.000Z'),
28
+ activeContext: 'M001 / S01 / T01 — wire gsd_exec',
29
+ memories: [
30
+ { id: 'MEM001', category: 'gotcha', content: 'FTS5 needs Porter tokenizer', confidence: 0.9,
31
+ source_unit_type: null, source_unit_id: null, created_at: '', updated_at: '',
32
+ superseded_by: null, hit_count: 0, scope: 'project', seq: 1, tags: [], structured_fields: null },
33
+ ],
34
+ execHistory: [
35
+ {
36
+ id: 'abc',
37
+ runtime: 'bash',
38
+ purpose: 'count TODOs',
39
+ started_at: '', finished_at: '', duration_ms: 10,
40
+ exit_code: 0, signal: null, timed_out: false,
41
+ stdout_bytes: 1, stderr_bytes: 0, stdout_truncated: false, stderr_truncated: false,
42
+ stdout_path: '/tmp/abc.stdout', stderr_path: '/tmp/abc.stderr', meta_path: '/tmp/abc.meta.json',
43
+ },
44
+ ],
45
+ });
46
+ assert.match(snap, /Active context/);
47
+ assert.match(snap, /M001 \/ S01 \/ T01/);
48
+ assert.match(snap, /FTS5 needs Porter tokenizer/);
49
+ assert.match(snap, /\[abc\] bash exit:0 — count TODOs/);
50
+ });
51
+
52
+ test('buildSnapshot: enforces the byte cap with a truncation marker', () => {
53
+ const longMemories = Array.from({ length: 50 }, (_v, i) => ({
54
+ id: `MEM${String(i).padStart(3, '0')}`,
55
+ category: 'gotcha',
56
+ content: 'x'.repeat(200),
57
+ confidence: 0.8,
58
+ source_unit_type: null,
59
+ source_unit_id: null,
60
+ created_at: '',
61
+ updated_at: '',
62
+ superseded_by: null,
63
+ hit_count: 0,
64
+ scope: 'project',
65
+ seq: i,
66
+ tags: [] as string[],
67
+ structured_fields: null,
68
+ }));
69
+ const snap = buildSnapshot(
70
+ { generatedAt: new Date(), memories: longMemories, execHistory: [] },
71
+ { maxBytes: 512, maxMemories: 50 },
72
+ );
73
+ assert.ok(Buffer.byteLength(snap, 'utf-8') <= 512, 'should respect cap');
74
+ assert.match(snap, /\[truncated\]/, 'should include truncation marker');
75
+ });
76
+
77
+ test('buildSnapshot: handles empty state with an explanatory placeholder', () => {
78
+ const snap = buildSnapshot({ generatedAt: new Date(), memories: [], execHistory: [] });
79
+ assert.match(snap, /_No durable memories/);
80
+ assert.ok(Buffer.byteLength(snap, 'utf-8') <= DEFAULT_SNAPSHOT_BYTES);
81
+ });
82
+
83
+ test('writeCompactionSnapshot + readCompactionSnapshot + executeResume: end-to-end', () => {
84
+ const base = freshBase();
85
+ try {
86
+ openDatabase(':memory:');
87
+ createMemory({ category: 'architecture', content: 'Single-writer DB through gsd-db.ts', confidence: 0.95 });
88
+ createMemory({ category: 'convention', content: 'Prefer typed helpers over raw SQL', confidence: 0.9 });
89
+
90
+ const out = writeCompactionSnapshot(base, { activeContext: 'M099 resume check' });
91
+ assert.ok(out.path.endsWith('last-snapshot.md'));
92
+ assert.ok(out.bytes > 0);
93
+ assert.equal(out.memories, 2);
94
+
95
+ const contents = readCompactionSnapshot(base);
96
+ assert.ok(contents);
97
+ assert.match(contents!, /Single-writer DB through gsd-db\.ts/);
98
+ assert.match(contents!, /M099 resume check/);
99
+
100
+ const tool = executeResume({}, { baseDir: base });
101
+ assert.ok(!tool.isError);
102
+ assert.equal(tool.details.found, true);
103
+ assert.match(tool.content[0].text, /Single-writer DB through gsd-db\.ts/);
104
+
105
+ // also verify the file content matches (without trailing newline)
106
+ const raw = readFileSync(out.path, 'utf-8');
107
+ assert.ok(raw.endsWith('\n'));
108
+ } finally {
109
+ closeDatabase();
110
+ cleanup(base);
111
+ }
112
+ });
113
+
114
+ test('executeResume: reports friendly empty state when no snapshot exists', () => {
115
+ const base = freshBase();
116
+ try {
117
+ const result = executeResume({}, { baseDir: base });
118
+ assert.equal(result.details.found, false);
119
+ assert.match(result.content[0].text, /No snapshot found/);
120
+ } finally {
121
+ cleanup(base);
122
+ }
123
+ });
@@ -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
  });
@@ -0,0 +1,124 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { listExecHistory, searchExecHistory } from '../exec-history.ts';
8
+ import { executeExecSearch } from '../tools/exec-search-tool.ts';
9
+
10
+ function freshBase(): string {
11
+ return mkdtempSync(join(tmpdir(), 'gsd-exec-history-'));
12
+ }
13
+
14
+ function cleanup(dir: string): void {
15
+ rmSync(dir, { recursive: true, force: true });
16
+ }
17
+
18
+ function writeRun(base: string, id: string, overrides: Record<string, unknown> = {}): void {
19
+ const dir = join(base, '.gsd', 'exec');
20
+ mkdirSync(dir, { recursive: true });
21
+ const stdoutPath = join(dir, `${id}.stdout`);
22
+ const stderrPath = join(dir, `${id}.stderr`);
23
+ const metaPath = join(dir, `${id}.meta.json`);
24
+ writeFileSync(stdoutPath, (overrides.stdout as string | undefined) ?? `stdout for ${id}\n`);
25
+ writeFileSync(stderrPath, '');
26
+ writeFileSync(
27
+ metaPath,
28
+ JSON.stringify({
29
+ id,
30
+ runtime: 'bash',
31
+ purpose: `purpose for ${id}`,
32
+ started_at: '2026-04-20T12:00:00.000Z',
33
+ finished_at: '2026-04-20T12:00:00.100Z',
34
+ duration_ms: 100,
35
+ exit_code: 0,
36
+ signal: null,
37
+ timed_out: false,
38
+ stdout_bytes: 12,
39
+ stderr_bytes: 0,
40
+ stdout_truncated: false,
41
+ stderr_truncated: false,
42
+ stdout_path: stdoutPath,
43
+ stderr_path: stderrPath,
44
+ ...overrides,
45
+ }),
46
+ );
47
+ }
48
+
49
+ test('listExecHistory: returns empty list when .gsd/exec missing', () => {
50
+ const base = freshBase();
51
+ try {
52
+ assert.deepEqual(listExecHistory(base), []);
53
+ } finally {
54
+ cleanup(base);
55
+ }
56
+ });
57
+
58
+ test('listExecHistory: skips malformed meta files', () => {
59
+ const base = freshBase();
60
+ try {
61
+ const dir = join(base, '.gsd', 'exec');
62
+ mkdirSync(dir, { recursive: true });
63
+ writeFileSync(join(dir, 'bad.meta.json'), '{not-json');
64
+ writeRun(base, 'ok-1');
65
+ const list = listExecHistory(base);
66
+ assert.equal(list.length, 1);
67
+ assert.equal(list[0]!.id, 'ok-1');
68
+ } finally {
69
+ cleanup(base);
70
+ }
71
+ });
72
+
73
+ test('searchExecHistory: filters by query, runtime, and failing_only', () => {
74
+ const base = freshBase();
75
+ try {
76
+ writeRun(base, 'playwright-run', { purpose: 'playwright snapshot' });
77
+ writeRun(base, 'grep-run', { purpose: 'grep TODOs' });
78
+ writeRun(base, 'failing-run', { exit_code: 1, purpose: 'boom' });
79
+ writeRun(base, 'node-run', { runtime: 'node', purpose: 'dedupe' });
80
+
81
+ const playwrightHits = searchExecHistory(base, { query: 'playwright' });
82
+ assert.equal(playwrightHits.length, 1);
83
+ assert.equal(playwrightHits[0]!.entry.id, 'playwright-run');
84
+
85
+ const failingHits = searchExecHistory(base, { failing_only: true });
86
+ assert.equal(failingHits.length, 1);
87
+ assert.equal(failingHits[0]!.entry.id, 'failing-run');
88
+
89
+ const nodeHits = searchExecHistory(base, { runtime: 'node' });
90
+ assert.equal(nodeHits.length, 1);
91
+ assert.equal(nodeHits[0]!.entry.runtime, 'node');
92
+
93
+ const unlimited = searchExecHistory(base, {});
94
+ assert.equal(unlimited.length, 4);
95
+ } finally {
96
+ cleanup(base);
97
+ }
98
+ });
99
+
100
+ test('executeExecSearch: returns helpful empty-state message when no matches', () => {
101
+ const base = freshBase();
102
+ try {
103
+ const result = executeExecSearch({ query: 'missing' }, { baseDir: base });
104
+ assert.ok(!result.isError);
105
+ assert.match(result.content[0].text, /No prior gsd_exec runs/);
106
+ } finally {
107
+ cleanup(base);
108
+ }
109
+ });
110
+
111
+ test('executeExecSearch: includes stdout_path and preview in details', () => {
112
+ const base = freshBase();
113
+ try {
114
+ writeRun(base, 'summary-run', { stdout: 'found 42 TODOs\n' });
115
+ const result = executeExecSearch({ query: 'summary' }, { baseDir: base });
116
+ const details = result.details as { results: Array<{ id: string; stdout_path: string }> };
117
+ assert.equal(details.results.length, 1);
118
+ assert.equal(details.results[0]!.id, 'summary-run');
119
+ assert.match(details.results[0]!.stdout_path, /summary-run\.stdout$/);
120
+ assert.match(result.content[0].text, /found 42 TODOs/);
121
+ } finally {
122
+ cleanup(base);
123
+ }
124
+ });