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
@@ -330,8 +330,8 @@ export function startAutoDetached(
330
330
  }
331
331
 
332
332
  /** Returns true if the project is configured for `isolation:worktree` mode. */
333
- export function shouldUseWorktreeIsolation(): boolean {
334
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
333
+ export function shouldUseWorktreeIsolation(basePath?: string): boolean {
334
+ const prefs = loadEffectiveGSDPreferences(basePath)?.preferences?.git;
335
335
  if (prefs?.isolation === "worktree") return true;
336
336
  // Default is false — worktree isolation requires explicit opt-in
337
337
  return false;
@@ -424,7 +424,7 @@ export function getAutoDashboardData(): AutoDashboardData {
424
424
  const rtkSavings = sessionId && s.basePath
425
425
  ? getRtkSessionSavings(s.basePath, sessionId)
426
426
  : null;
427
- const rtkEnabled = loadEffectiveGSDPreferences()?.preferences.experimental?.rtk === true;
427
+ const rtkEnabled = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences.experimental?.rtk === true;
428
428
  // Pending capture count — lazy check, non-fatal
429
429
  let pendingCaptureCount = 0;
430
430
  try {
@@ -648,7 +648,7 @@ function buildSnapshotOpts(
648
648
  gitStatus?: "ok" | "failed";
649
649
  gitError?: string;
650
650
  } & Record<string, unknown> {
651
- const prefs = loadEffectiveGSDPreferences()?.preferences;
651
+ const prefs = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
652
652
  const uokFlags = resolveUokFlags(prefs);
653
653
  return {
654
654
  ...(s.autoStartTime > 0 ? { autoSessionKey: String(s.autoStartTime) } : {}),
@@ -686,7 +686,7 @@ function handleLostSessionLock(
686
686
  restoreProjectRootEnv();
687
687
  restoreMilestoneLockEnv();
688
688
  deregisterSigtermHandler();
689
- clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
689
+ clearCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences);
690
690
  const base = lockBase();
691
691
  const lockFilePath = base ? join(gsdRoot(base), "auto.lock") : "unknown";
692
692
  const recoverySuggestion = "\nTo recover, run: gsd doctor --fix";
@@ -764,7 +764,7 @@ export async function stopAuto(
764
764
  reason?: string,
765
765
  ): Promise<void> {
766
766
  if (!s.active && !s.paused) return;
767
- const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
767
+ const loadedPreferences = loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences;
768
768
  const reasonSuffix = reason ? ` — ${reason}` : "";
769
769
 
770
770
  try {
@@ -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
  }
@@ -1492,7 +1495,7 @@ export async function startAuto(
1492
1495
  // ── Auto-worktree / branch-mode: re-enter on resume ──
1493
1496
  if (
1494
1497
  s.currentMilestoneId &&
1495
- getIsolationMode() !== "none" &&
1498
+ getIsolationMode(s.originalBasePath || s.basePath) !== "none" &&
1496
1499
  s.originalBasePath &&
1497
1500
  !isInAutoWorktree(s.basePath) &&
1498
1501
  !detectWorktreeName(s.basePath) &&
@@ -1531,7 +1534,7 @@ export async function startAuto(
1531
1534
  await openProjectDbIfPresent(s.basePath);
1532
1535
  try {
1533
1536
  await rebuildState(s.basePath);
1534
- syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1537
+ syncCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, await deriveState(s.basePath));
1535
1538
  } catch (e) {
1536
1539
  debugLog("resume-rebuild-state-failed", {
1537
1540
  error: e instanceof Error ? e.message : String(e),
@@ -1581,7 +1584,7 @@ export async function startAuto(
1581
1584
  "resuming",
1582
1585
  s.currentMilestoneId ?? "unknown",
1583
1586
  );
1584
- logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1587
+ logCmuxEvent(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
1585
1588
 
1586
1589
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1587
1590
  startAutoCommandPolling(s.basePath);
@@ -1619,12 +1622,12 @@ export async function startAuto(
1619
1622
 
1620
1623
  captureProjectRootEnv(s.originalBasePath || s.basePath);
1621
1624
  try {
1622
- syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
1625
+ syncCmuxSidebar(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, await deriveState(s.basePath));
1623
1626
  } catch (err) {
1624
1627
  // Best-effort only — sidebar sync must never block auto-mode startup
1625
1628
  logWarning("engine", `cmux sync failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
1626
1629
  }
1627
- logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1630
+ logCmuxEvent(loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
1628
1631
 
1629
1632
  startAutoCommandPolling(s.basePath);
1630
1633
 
@@ -0,0 +1,98 @@
1
+ // GSD — Persistent per-project blocklist of provider/model pairs that the
2
+ // provider has rejected at request time for account entitlement reasons.
3
+ //
4
+ // Lives at `.gsd/runtime/blocked-models.json` so the block survives /gsd auto
5
+ // restarts. Auto-mode model selection skips blocked entries; agent-end
6
+ // recovery adds entries when a runtime rejection is classified as
7
+ // `unsupported-model`. See issue #4513.
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { gsdRoot } from "./paths.js";
12
+ import { withFileLockSync } from "./file-lock.js";
13
+
14
+ export interface BlockedModelEntry {
15
+ provider: string;
16
+ id: string;
17
+ reason: string;
18
+ blockedAt: number;
19
+ }
20
+
21
+ interface BlockedModelsFile {
22
+ version: 1;
23
+ blocked: BlockedModelEntry[];
24
+ }
25
+
26
+ function blockedModelsPath(basePath: string): string {
27
+ return join(gsdRoot(basePath), "runtime", "blocked-models.json");
28
+ }
29
+
30
+ function modelKey(provider: string, id: string): string {
31
+ return `${provider.toLowerCase()}/${id.toLowerCase()}`;
32
+ }
33
+
34
+ function readFileSafe(path: string): BlockedModelsFile {
35
+ if (!existsSync(path)) return { version: 1, blocked: [] };
36
+ try {
37
+ const raw = readFileSync(path, "utf-8");
38
+ const parsed = JSON.parse(raw) as Partial<BlockedModelsFile>;
39
+ if (!parsed || !Array.isArray(parsed.blocked)) {
40
+ return { version: 1, blocked: [] };
41
+ }
42
+ const blocked = parsed.blocked.filter(
43
+ (e): e is BlockedModelEntry =>
44
+ !!e && typeof e.provider === "string" && typeof e.id === "string",
45
+ );
46
+ return { version: 1, blocked };
47
+ } catch {
48
+ // Corrupted JSON: treat as empty so a bad file never blocks dispatch.
49
+ return { version: 1, blocked: [] };
50
+ }
51
+ }
52
+
53
+ export function loadBlockedModels(basePath: string): BlockedModelEntry[] {
54
+ return readFileSafe(blockedModelsPath(basePath)).blocked;
55
+ }
56
+
57
+ export function isModelBlocked(
58
+ basePath: string,
59
+ provider: string | undefined,
60
+ id: string | undefined,
61
+ ): boolean {
62
+ if (!provider || !id) return false;
63
+ const target = modelKey(provider, id);
64
+ return loadBlockedModels(basePath).some(
65
+ (e) => modelKey(e.provider, e.id) === target,
66
+ );
67
+ }
68
+
69
+ export function blockModel(
70
+ basePath: string,
71
+ provider: string,
72
+ id: string,
73
+ reason: string,
74
+ ): void {
75
+ const path = blockedModelsPath(basePath);
76
+ mkdirSync(dirname(path), { recursive: true });
77
+ // Ensure the file exists before we try to lock it — proper-lockfile requires
78
+ // the target path to exist (file-lock.ts falls through to an unlocked call
79
+ // otherwise).
80
+ if (!existsSync(path)) {
81
+ writeFileSync(path, JSON.stringify({ version: 1, blocked: [] }, null, 2) + "\n", "utf-8");
82
+ }
83
+ withFileLockSync(path, () => {
84
+ const current = readFileSafe(path);
85
+ const target = modelKey(provider, id);
86
+ if (current.blocked.some((e) => modelKey(e.provider, e.id) === target)) {
87
+ return;
88
+ }
89
+ const next: BlockedModelsFile = {
90
+ version: 1,
91
+ blocked: [
92
+ ...current.blocked,
93
+ { provider, id, reason, blockedAt: Date.now() },
94
+ ],
95
+ };
96
+ writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
97
+ });
98
+ }
@@ -16,6 +16,7 @@ import {
16
16
  isTransient,
17
17
  type ErrorClass,
18
18
  } from "../error-classifier.js";
19
+ import { blockModel, isModelBlocked } from "../blocked-models.js";
19
20
 
20
21
  const retryState = createRetryState();
21
22
  const MAX_NETWORK_RETRIES = 2;
@@ -124,6 +125,102 @@ export async function handleAgentEnd(
124
125
  // ── 1. Classify using rawErrorMsg to avoid prose false-positives ────
125
126
  const cls = classifyError(rawErrorMsg, explicitRetryAfterMs);
126
127
 
128
+ // ── 1a. Unsupported-model: provider rejected this model for the current
129
+ // account/plan at request time (#4513). Persist a block so the
130
+ // same dead model isn't reselected on the next /gsd auto restart,
131
+ // then try a fallback before pausing.
132
+ if (cls.kind === "unsupported-model") {
133
+ const dash = getAutoDashboardData();
134
+ const rejectedProvider = ctx.model?.provider;
135
+ const rejectedId = ctx.model?.id;
136
+ if (dash.basePath && rejectedProvider && rejectedId) {
137
+ try {
138
+ blockModel(dash.basePath, rejectedProvider, rejectedId, rawErrorMsg || "unsupported for account");
139
+ ctx.ui.notify(
140
+ `Blocked ${rejectedProvider}/${rejectedId} for this project — provider rejected it for the current account.`,
141
+ "warning",
142
+ );
143
+ } catch (err) {
144
+ const m = err instanceof Error ? err.message : String(err);
145
+ logWarning("bootstrap", `Failed to persist blocked model: ${m}`);
146
+ }
147
+ }
148
+
149
+ // Try configured fallback chain, skipping anything already blocked.
150
+ if (dash.currentUnit && dash.basePath) {
151
+ const modelConfig = resolveModelWithFallbacksForUnit(dash.currentUnit.type);
152
+ if (modelConfig && modelConfig.fallbacks.length > 0) {
153
+ const availableModels = ctx.modelRegistry.getAvailable();
154
+ let cursorModelId: string | undefined = ctx.model?.id;
155
+ while (true) {
156
+ const nextModelId = getNextFallbackModel(cursorModelId, modelConfig);
157
+ if (!nextModelId) break;
158
+ const candidate = resolveModelId(nextModelId, availableModels, ctx.model?.provider);
159
+ if (candidate && !isModelBlocked(dash.basePath, candidate.provider, candidate.id)) {
160
+ const ok = await pi.setModel(candidate, { persist: false });
161
+ if (ok) {
162
+ setCurrentDispatchedModelId({ provider: candidate.provider, id: candidate.id });
163
+ ctx.ui.notify(
164
+ `Switched to fallback ${candidate.provider}/${candidate.id} after account entitlement rejection.`,
165
+ "warning",
166
+ );
167
+ pi.sendMessage(
168
+ { customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false },
169
+ { triggerTurn: true },
170
+ );
171
+ return;
172
+ }
173
+ }
174
+ cursorModelId = nextModelId;
175
+ }
176
+ }
177
+
178
+ // Fallback chain exhausted — try the auto-mode start model if it isn't
179
+ // the same one we just blocked and isn't itself blocked.
180
+ const sessionModel = getAutoModeStartModel();
181
+ if (
182
+ sessionModel &&
183
+ !(sessionModel.provider === rejectedProvider && sessionModel.id === rejectedId) &&
184
+ !isModelBlocked(dash.basePath, sessionModel.provider, sessionModel.id)
185
+ ) {
186
+ const startModel = ctx.modelRegistry
187
+ .getAvailable()
188
+ .find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
189
+ if (startModel) {
190
+ const ok = await pi.setModel(startModel, { persist: false });
191
+ if (ok) {
192
+ setCurrentDispatchedModelId({ provider: startModel.provider, id: startModel.id });
193
+ ctx.ui.notify(
194
+ `Restored auto-mode start model ${startModel.provider}/${startModel.id} after entitlement rejection.`,
195
+ "warning",
196
+ );
197
+ pi.sendMessage(
198
+ { customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false },
199
+ { triggerTurn: true },
200
+ );
201
+ return;
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // No usable fallback — pause with a clearly named message.
208
+ const blockedLabel = rejectedProvider && rejectedId ? `${rejectedProvider}/${rejectedId}` : "current model";
209
+ const pauseDetail = `Model ${blockedLabel} blocked for this account${errorDetail}. Configure a different model and restart /gsd auto.`;
210
+ await pauseAutoForProviderError(ctx.ui, pauseDetail, () =>
211
+ pauseAuto(ctx, pi, {
212
+ message: pauseDetail,
213
+ category: "provider",
214
+ isTransient: false,
215
+ }),
216
+ {
217
+ isRateLimit: false,
218
+ isTransient: false,
219
+ retryAfterMs: 0,
220
+ });
221
+ return;
222
+ }
223
+
127
224
  // ── 1b. Defer to Core RetryHandler for most transient errors ────────
128
225
  // Core retries transient failures in-session after this handler.
129
226
  // Keep that behavior for non-rate-limit classes to avoid pause/retry races,
@@ -35,6 +35,19 @@ function registerAlias(pi: ExtensionAPI, toolDef: any, aliasName: string, canoni
35
35
  });
36
36
  }
37
37
 
38
+ /**
39
+ * Read a tool result's structured payload, accommodating MCP's `details` →
40
+ * `structuredContent` rename (#4472, #4477). In-process executions still
41
+ * deliver the payload on `result.details`; MCP-routed executions deliver it
42
+ * on `result.structuredContent` (post `adaptExecutorResult` transform). All
43
+ * `renderResult` callbacks in this file route through this helper so a future
44
+ * field rename only needs to be applied in one place.
45
+ */
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- result shape varies by tool
47
+ function readDetails(result: any): any {
48
+ return result?.details ?? result?.structuredContent;
49
+ }
50
+
38
51
  export function registerDbTools(pi: ExtensionAPI): void {
39
52
  // ─── gsd_decision_save (formerly gsd_save_decision) ─────────────────────
40
53
 
@@ -110,7 +123,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
110
123
  return new Text(text, 0, 0);
111
124
  },
112
125
  renderResult(result: any, _options: any, theme: any) {
113
- const d = result.details;
126
+ const d = readDetails(result);
114
127
  if (result.isError || d?.error) {
115
128
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
116
129
  }
@@ -188,7 +201,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
188
201
  return new Text(text, 0, 0);
189
202
  },
190
203
  renderResult(result: any, _options: any, theme: any) {
191
- const d = result.details;
204
+ const d = readDetails(result);
192
205
  if (result.isError || d?.error) {
193
206
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
194
207
  }
@@ -273,7 +286,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
273
286
  return new Text(text, 0, 0);
274
287
  },
275
288
  renderResult(result: any, _options: any, theme: any) {
276
- const d = result.details;
289
+ const d = readDetails(result);
277
290
  if (result.isError || d?.error) {
278
291
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
279
292
  }
@@ -322,7 +335,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
322
335
  return new Text(text, 0, 0);
323
336
  },
324
337
  renderResult(result: any, _options: any, theme: any) {
325
- const d = result.details;
338
+ const d = readDetails(result);
326
339
  if (result.isError || d?.error) {
327
340
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
328
341
  }
@@ -406,7 +419,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
406
419
  return new Text(theme.fg("toolTitle", theme.bold("milestone_generate_id")), 0, 0);
407
420
  },
408
421
  renderResult(result: any, _options: any, theme: any) {
409
- const d = result.details;
422
+ const d = readDetails(result);
410
423
  if (result.isError || d?.error) {
411
424
  return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
412
425
  }
@@ -1074,13 +1087,31 @@ export function registerDbTools(pi: ExtensionAPI): void {
1074
1087
  text += theme.fg("dim", ` → ${args.verdict ?? ""}`);
1075
1088
  return new Text(text, 0, 0);
1076
1089
  },
1090
+ /**
1091
+ * Render the save_gate_result tool output for the TUI.
1092
+ *
1093
+ * Prefers structured fields, but falls back to `content[0].text` when the
1094
+ * structured payload is empty. Defensive: the structural fix on this
1095
+ * branch plumbs `details` through MCP via `structuredContent`, but older
1096
+ * hosts, a future handler that forgets `structuredContent`, or any drop
1097
+ * of non-standard return fields would otherwise render as
1098
+ * "undefined: undefined". Same fallback applies to error rendering, and
1099
+ * we strip a leading `Error:` from the fallback text to avoid producing
1100
+ * `Error: Error: ...`.
1101
+ */
1077
1102
  renderResult(result: any, _options: any, theme: any) {
1078
- const d = result.details;
1103
+ const d = readDetails(result);
1079
1104
  if (result.isError || d?.error) {
1080
- return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
1105
+ const rawMsg = d?.error ?? result.content?.[0]?.text ?? "unknown";
1106
+ const msg = rawMsg.replace(/^\s*Error:\s*/i, "");
1107
+ return new Text(theme.fg("error", `Error: ${msg}`), 0, 0);
1108
+ }
1109
+ if (!d?.gateId || !d?.verdict) {
1110
+ const text = result.content?.[0]?.text ?? "Gate result saved";
1111
+ return new Text(theme.fg("success", text), 0, 0);
1081
1112
  }
1082
- const color = d?.verdict === "flag" ? "warning" : "success";
1083
- return new Text(theme.fg(color, `${d?.gateId}: ${d?.verdict}`), 0, 0);
1113
+ const color = d.verdict === "flag" ? "warning" : "success";
1114
+ return new Text(theme.fg(color, `${d.gateId}: ${d.verdict}`), 0, 0);
1084
1115
  },
1085
1116
  };
1086
1117
 
@@ -0,0 +1,109 @@
1
+ // GSD2 — Exec (context-mode) tool registration.
2
+ //
3
+ // Exposes the `gsd_exec` tool over MCP. Opt-in: disabled unless
4
+ // `context_mode.enabled: true` is set in preferences.
5
+
6
+ import { Type } from "@sinclair/typebox";
7
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
8
+
9
+ import { executeGsdExec } from "../tools/exec-tool.js";
10
+ import { executeExecSearch } from "../tools/exec-search-tool.js";
11
+ import { executeResume } from "../tools/resume-tool.js";
12
+ import { loadEffectiveGSDPreferences } from "../preferences.js";
13
+ import { logWarning } from "../workflow-logger.js";
14
+
15
+ export function registerExecTools(pi: ExtensionAPI): void {
16
+ pi.registerTool({
17
+ name: "gsd_exec",
18
+ label: "Exec (Sandboxed)",
19
+ description:
20
+ "Run a short script (bash/node/python) in a subprocess. Full stdout/stderr persist to " +
21
+ ".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
22
+ "this instead of reading many files or emitting large tool outputs — e.g. have the script " +
23
+ "count/grep/summarize and log the finding. Enabled by default; opt out via " +
24
+ "preferences.context_mode.enabled=false.",
25
+ promptSnippet:
26
+ "Run a bash/node/python script in a sandbox; full output is saved to disk and only a digest returns",
27
+ promptGuidelines: [
28
+ "Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
29
+ "Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
30
+ "The digest is the last ~300 chars of stdout — size your log output accordingly.",
31
+ "Need the full output? Read the stdout_path returned in details (file on local disk).",
32
+ ],
33
+ parameters: Type.Object({
34
+ runtime: Type.Union(
35
+ [Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")],
36
+ { description: "Interpreter: bash (-c), node (-e), or python3 (-c)." },
37
+ ),
38
+ script: Type.String({ description: "Script body. Keep output small (log the finding, not the data)." }),
39
+ purpose: Type.Optional(Type.String({ description: "Short label recorded in meta.json for later review." })),
40
+ timeout_ms: Type.Optional(
41
+ Type.Number({
42
+ description: "Per-invocation timeout (ms). Capped at 600000. Default from preferences.",
43
+ minimum: 1_000,
44
+ maximum: 600_000,
45
+ }),
46
+ ),
47
+ }),
48
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
49
+ let prefs: Awaited<ReturnType<typeof loadEffectiveGSDPreferences>> | null = null;
50
+ try {
51
+ prefs = loadEffectiveGSDPreferences();
52
+ } catch (err) {
53
+ logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
54
+ }
55
+ return executeGsdExec(params as Parameters<typeof executeGsdExec>[0], {
56
+ baseDir: process.cwd(),
57
+ preferences: prefs?.preferences ?? null,
58
+ });
59
+ },
60
+ });
61
+
62
+ pi.registerTool({
63
+ name: "gsd_exec_search",
64
+ label: "Search gsd_exec History",
65
+ description:
66
+ "List prior gsd_exec runs (most recent first) from .gsd/exec/*.meta.json. Useful for " +
67
+ "rediscovering the stdout_path of an earlier run without re-executing it. Read-only.",
68
+ promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
69
+ promptGuidelines: [
70
+ "Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
71
+ "The preview shows the trailing ~300 chars of stdout; read stdout_path for the full transcript.",
72
+ ],
73
+ parameters: Type.Object({
74
+ query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
75
+ runtime: Type.Optional(
76
+ Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], {
77
+ description: "Restrict to one runtime.",
78
+ }),
79
+ ),
80
+ failing_only: Type.Optional(Type.Boolean({ description: "Only non-zero exit codes and timeouts." })),
81
+ limit: Type.Optional(Type.Number({ description: "Max results (default 20, cap 200)", minimum: 1, maximum: 200 })),
82
+ }),
83
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
84
+ return executeExecSearch(params as Parameters<typeof executeExecSearch>[0], {
85
+ baseDir: process.cwd(),
86
+ });
87
+ },
88
+ });
89
+
90
+ pi.registerTool({
91
+ name: "gsd_resume",
92
+ label: "Resume (Read Snapshot)",
93
+ description:
94
+ "Return the contents of .gsd/last-snapshot.md — a ≤2 KB digest of top memories, recent " +
95
+ "gsd_exec runs, and active context, written automatically on session_before_compact. Use " +
96
+ "this after compaction or session resume to re-orient quickly.",
97
+ promptSnippet: "Read the pre-compaction snapshot to re-orient after context loss",
98
+ promptGuidelines: [
99
+ "Call this right after a session resumes if you feel you've lost durable context.",
100
+ "The snapshot is a summary — use memory_query or gsd_exec_search for detail.",
101
+ ],
102
+ parameters: Type.Object({}),
103
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
104
+ return executeResume(params as Parameters<typeof executeResume>[0], {
105
+ baseDir: process.cwd(),
106
+ });
107
+ },
108
+ });
109
+ }
@@ -8,6 +8,7 @@ import type { GSDEcosystemBeforeAgentStartHandler } from "../ecosystem/gsd-exten
8
8
  import { loadEcosystemExtensions } from "../ecosystem/loader.js";
9
9
  import { registerDbTools } from "./db-tools.js";
10
10
  import { registerDynamicTools } from "./dynamic-tools.js";
11
+ import { registerExecTools } from "./exec-tools.js";
11
12
  import { registerJournalTools } from "./journal-tools.js";
12
13
  import { registerMemoryTools } from "./memory-tools.js";
13
14
  import { registerQueryTools } from "./query-tools.js";
@@ -100,6 +101,7 @@ export function registerGsdExtension(pi: ExtensionAPI): void {
100
101
  ["journal-tools", () => registerJournalTools(pi)],
101
102
  ["query-tools", () => registerQueryTools(pi)],
102
103
  ["memory-tools", () => registerMemoryTools(pi)],
104
+ ["exec-tools", () => registerExecTools(pi)],
103
105
  ["shortcuts", () => registerShortcuts(pi)],
104
106
  ["hooks", () => registerHooks(pi, ecosystemHandlers)],
105
107
  ["ecosystem", () => {
@@ -225,6 +225,42 @@ export function registerHooks(
225
225
  }));
226
226
  });
227
227
 
228
+ // Context-mode snapshot: write .gsd/last-snapshot.md before compaction so
229
+ // agents can call gsd_resume (or Read the file) to re-orient. Opt-in via
230
+ // preferences.context_mode.enabled. Runs after the auto-cancel handler
231
+ // above — if that one returned cancel:true, pi still fires us but the
232
+ // compaction won't actually happen; the snapshot is still useful then,
233
+ // since auto may pause and resume later.
234
+ pi.on("session_before_compact", async () => {
235
+ try {
236
+ const { loadEffectiveGSDPreferences } = await import("../preferences.js");
237
+ const { isContextModeEnabled } = await import("../preferences-types.js");
238
+ const prefs = loadEffectiveGSDPreferences();
239
+ if (!isContextModeEnabled(prefs?.preferences)) return;
240
+ const { writeCompactionSnapshot } = await import("../compaction-snapshot.js");
241
+ const { ensureDbOpen } = await import("./dynamic-tools.js");
242
+ await ensureDbOpen();
243
+ const basePath = process.cwd();
244
+ let activeContext: string | null = null;
245
+ try {
246
+ const state = await deriveState(basePath);
247
+ if (state.activeMilestone && state.activeSlice && state.activeTask) {
248
+ activeContext =
249
+ `Active: ${state.activeMilestone.id} / ${state.activeSlice.id} / ${state.activeTask.id}` +
250
+ (state.activeTask.title ? ` — ${state.activeTask.title}` : "");
251
+ }
252
+ } catch {
253
+ /* non-fatal */
254
+ }
255
+ writeCompactionSnapshot(basePath, { activeContext });
256
+ } catch (err) {
257
+ safetyLogWarning(
258
+ "context-mode",
259
+ `failed to write compaction snapshot: ${err instanceof Error ? err.message : String(err)}`,
260
+ );
261
+ }
262
+ });
263
+
228
264
  pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => {
229
265
  if (isParallelActive()) {
230
266
  try {