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
@@ -0,0 +1,326 @@
1
+ // GSD Exec Sandbox — tool-output sandboxing for sub-sessions.
2
+ //
3
+ // Runs a script in a subprocess and persists stdout/stderr to
4
+ // `.gsd/exec/<id>.{stdout,stderr,meta.json}`. Only a short digest is
5
+ // returned to the calling agent's context, keeping large outputs
6
+ // (e.g. Playwright snapshots, issue dumps) out of the window.
7
+ //
8
+ // Inspired by mksglu/context-mode (Elastic License 2.0). Independent
9
+ // implementation — no upstream code incorporated.
10
+
11
+ import { spawn } from "node:child_process";
12
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
13
+ import { randomUUID } from "node:crypto";
14
+ import { resolve } from "node:path";
15
+
16
+ export interface ExecSandboxRequest {
17
+ /** Interpreter to use. */
18
+ runtime: "bash" | "node" | "python";
19
+ /** Script body. Executed via the runtime's -c equivalent. */
20
+ script: string;
21
+ /** Optional purpose/label recorded in meta.json. */
22
+ purpose?: string;
23
+ /** Per-invocation timeout in ms. Clamped to `clamp_timeout_ms`. */
24
+ timeout_ms?: number;
25
+ }
26
+
27
+ export interface ExecSandboxOptions {
28
+ /** Project root. stdout/stderr persist under `<baseDir>/.gsd/exec/`. */
29
+ baseDir: string;
30
+ /** Absolute upper bound for the timeout. */
31
+ clamp_timeout_ms: number;
32
+ /** Default timeout if request omits one. */
33
+ default_timeout_ms: number;
34
+ /** Cap on persisted stdout bytes. Further output is truncated with a marker. */
35
+ stdout_cap_bytes: number;
36
+ /** Cap on persisted stderr bytes. */
37
+ stderr_cap_bytes: number;
38
+ /** Number of trailing stdout chars returned as the digest. */
39
+ digest_chars: number;
40
+ /** Env var allowlist (case-sensitive). PATH/HOME always forwarded. */
41
+ env_allowlist: readonly string[];
42
+ /** Optional override of process.env for tests. */
43
+ env?: NodeJS.ProcessEnv;
44
+ /** Optional override for the current time (tests). */
45
+ now?: () => Date;
46
+ /** Optional override for id generation (tests). */
47
+ generateId?: () => string;
48
+ }
49
+
50
+ export interface ExecSandboxResult {
51
+ id: string;
52
+ runtime: ExecSandboxRequest["runtime"];
53
+ exit_code: number | null;
54
+ signal: NodeJS.Signals | null;
55
+ timed_out: boolean;
56
+ duration_ms: number;
57
+ stdout_bytes: number;
58
+ stderr_bytes: number;
59
+ stdout_truncated: boolean;
60
+ stderr_truncated: boolean;
61
+ stdout_path: string;
62
+ stderr_path: string;
63
+ meta_path: string;
64
+ digest: string;
65
+ }
66
+
67
+ const ALWAYS_FORWARD_ENV = ["PATH", "HOME"] as const;
68
+
69
+ export const EXEC_DEFAULTS = {
70
+ clampTimeoutMs: 600_000,
71
+ defaultTimeoutMs: 30_000,
72
+ stdoutCapBytes: 1_048_576,
73
+ stderrCapBytes: 262_144,
74
+ digestChars: 300,
75
+ envAllowlist: [
76
+ "LANG",
77
+ "LC_ALL",
78
+ "TERM",
79
+ "TZ",
80
+ "SHELL",
81
+ "USER",
82
+ "LOGNAME",
83
+ "TMPDIR",
84
+ "NODE_OPTIONS",
85
+ "PYTHONPATH",
86
+ "PYTHONIOENCODING",
87
+ ] as const,
88
+ } as const;
89
+
90
+ function buildChildEnv(opts: ExecSandboxOptions): NodeJS.ProcessEnv {
91
+ const source = opts.env ?? process.env;
92
+ const out: NodeJS.ProcessEnv = {};
93
+ const allowed = new Set<string>([...ALWAYS_FORWARD_ENV, ...opts.env_allowlist]);
94
+ for (const key of allowed) {
95
+ const value = source[key];
96
+ if (typeof value === "string") out[key] = value;
97
+ }
98
+ return out;
99
+ }
100
+
101
+ function clampTimeout(request: ExecSandboxRequest, opts: ExecSandboxOptions): number {
102
+ const requested = typeof request.timeout_ms === "number" && Number.isFinite(request.timeout_ms)
103
+ ? Math.floor(request.timeout_ms)
104
+ : opts.default_timeout_ms;
105
+ if (requested < 1) return 1;
106
+ if (requested > opts.clamp_timeout_ms) return opts.clamp_timeout_ms;
107
+ return requested;
108
+ }
109
+
110
+ function resolveCommand(runtime: ExecSandboxRequest["runtime"]): { cmd: string; args: string[] } {
111
+ switch (runtime) {
112
+ case "bash":
113
+ return { cmd: "bash", args: ["-c"] };
114
+ case "node":
115
+ return { cmd: process.execPath, args: ["-e"] };
116
+ case "python":
117
+ return { cmd: "python3", args: ["-c"] };
118
+ }
119
+ }
120
+
121
+ function tail(buf: Buffer, chars: number): string {
122
+ if (chars <= 0) return "";
123
+ const text = buf.toString("utf-8");
124
+ return text.length <= chars ? text : text.slice(text.length - chars);
125
+ }
126
+
127
+ /**
128
+ * Run a script in a subprocess, capture stdout/stderr to files under
129
+ * `.gsd/exec/<id>.{stdout,stderr,meta.json}`, and return an `ExecSandboxResult`
130
+ * containing the digest plus metadata.
131
+ *
132
+ * Errors from spawn failures resolve (not reject) with `exit_code=null`.
133
+ * The function is pure with respect to its inputs — no global state beyond
134
+ * filesystem writes under `baseDir`.
135
+ */
136
+ export function runExecSandbox(
137
+ request: ExecSandboxRequest,
138
+ opts: ExecSandboxOptions,
139
+ ): Promise<ExecSandboxResult> {
140
+ return new Promise((resolveP) => {
141
+ const id = (opts.generateId ?? defaultGenerateId)();
142
+ const now = (opts.now ?? (() => new Date()))();
143
+ const execDir = resolve(opts.baseDir, ".gsd", "exec");
144
+ if (!existsSync(execDir)) mkdirSync(execDir, { recursive: true });
145
+ const stdoutPath = resolve(execDir, `${id}.stdout`);
146
+ const stderrPath = resolve(execDir, `${id}.stderr`);
147
+ const metaPath = resolve(execDir, `${id}.meta.json`);
148
+
149
+ const timeoutMs = clampTimeout(request, opts);
150
+ const { cmd, args } = resolveCommand(request.runtime);
151
+ const env = buildChildEnv(opts);
152
+ const useProcessGroup = process.platform !== "win32";
153
+
154
+ const started = Date.now();
155
+ let child;
156
+ try {
157
+ child = spawn(cmd, [...args, request.script], {
158
+ cwd: opts.baseDir,
159
+ env,
160
+ stdio: ["ignore", "pipe", "pipe"],
161
+ ...(useProcessGroup ? { detached: true } : {}),
162
+ });
163
+ } catch (err) {
164
+ const duration = Date.now() - started;
165
+ const message = err instanceof Error ? err.message : String(err);
166
+ writeFileSync(stdoutPath, "");
167
+ writeFileSync(stderrPath, `spawn error: ${message}\n`);
168
+ const result: ExecSandboxResult = {
169
+ id,
170
+ runtime: request.runtime,
171
+ exit_code: null,
172
+ signal: null,
173
+ timed_out: false,
174
+ duration_ms: duration,
175
+ stdout_bytes: 0,
176
+ stderr_bytes: Buffer.byteLength(`spawn error: ${message}\n`),
177
+ stdout_truncated: false,
178
+ stderr_truncated: false,
179
+ stdout_path: stdoutPath,
180
+ stderr_path: stderrPath,
181
+ meta_path: metaPath,
182
+ digest: `[spawn error: ${message}]`,
183
+ };
184
+ writeMeta(metaPath, result, request, now);
185
+ resolveP(result);
186
+ return;
187
+ }
188
+
189
+ const stdoutChunks: Buffer[] = [];
190
+ const stderrChunks: Buffer[] = [];
191
+ let stdoutBytes = 0;
192
+ let stderrBytes = 0;
193
+ let stdoutTruncated = false;
194
+ let stderrTruncated = false;
195
+
196
+ child.stdout?.on("data", (chunk: Buffer) => {
197
+ const remaining = opts.stdout_cap_bytes - stdoutBytes;
198
+ if (remaining <= 0) {
199
+ stdoutTruncated = true;
200
+ return;
201
+ }
202
+ if (chunk.length <= remaining) {
203
+ stdoutChunks.push(chunk);
204
+ stdoutBytes += chunk.length;
205
+ } else {
206
+ stdoutChunks.push(chunk.subarray(0, remaining));
207
+ stdoutBytes += remaining;
208
+ stdoutTruncated = true;
209
+ }
210
+ });
211
+ child.stderr?.on("data", (chunk: Buffer) => {
212
+ const remaining = opts.stderr_cap_bytes - stderrBytes;
213
+ if (remaining <= 0) {
214
+ stderrTruncated = true;
215
+ return;
216
+ }
217
+ if (chunk.length <= remaining) {
218
+ stderrChunks.push(chunk);
219
+ stderrBytes += chunk.length;
220
+ } else {
221
+ stderrChunks.push(chunk.subarray(0, remaining));
222
+ stderrBytes += remaining;
223
+ stderrTruncated = true;
224
+ }
225
+ });
226
+
227
+ let timedOut = false;
228
+ const timer = setTimeout(() => {
229
+ timedOut = true;
230
+ if (useProcessGroup && child.pid != null) {
231
+ try {
232
+ process.kill(-child.pid, "SIGKILL");
233
+ } catch {
234
+ child.kill("SIGKILL");
235
+ }
236
+ } else {
237
+ child.kill("SIGKILL");
238
+ }
239
+ }, timeoutMs);
240
+ timer.unref?.();
241
+
242
+ const finalize = (exitCode: number | null, signal: NodeJS.Signals | null) => {
243
+ clearTimeout(timer);
244
+ const duration = Date.now() - started;
245
+ const stdoutBuf = Buffer.concat(stdoutChunks);
246
+ const stderrBuf = Buffer.concat(stderrChunks);
247
+ const stdoutSuffix = stdoutTruncated ? "\n[truncated: stdout cap reached]\n" : "";
248
+ const stderrSuffix = stderrTruncated ? "\n[truncated: stderr cap reached]\n" : "";
249
+ writeFileSync(stdoutPath, Buffer.concat([stdoutBuf, Buffer.from(stdoutSuffix, "utf-8")]));
250
+ writeFileSync(stderrPath, Buffer.concat([stderrBuf, Buffer.from(stderrSuffix, "utf-8")]));
251
+
252
+ const digestBody = tail(stdoutBuf, opts.digest_chars);
253
+ const digest =
254
+ digestBody.length > 0
255
+ ? digestBody
256
+ : timedOut
257
+ ? "[no stdout — timed out]"
258
+ : stderrBuf.length > 0
259
+ ? `[no stdout — tail of stderr]\n${tail(stderrBuf, opts.digest_chars)}`
260
+ : "[no output]";
261
+
262
+ const result: ExecSandboxResult = {
263
+ id,
264
+ runtime: request.runtime,
265
+ exit_code: exitCode,
266
+ signal,
267
+ timed_out: timedOut,
268
+ duration_ms: duration,
269
+ stdout_bytes: stdoutBytes,
270
+ stderr_bytes: stderrBytes,
271
+ stdout_truncated: stdoutTruncated,
272
+ stderr_truncated: stderrTruncated,
273
+ stdout_path: stdoutPath,
274
+ stderr_path: stderrPath,
275
+ meta_path: metaPath,
276
+ digest,
277
+ };
278
+ writeMeta(metaPath, result, request, now);
279
+ resolveP(result);
280
+ };
281
+
282
+ child.on("error", (err) => {
283
+ const message = err instanceof Error ? err.message : String(err);
284
+ const line = `child error: ${message}\n`;
285
+ const remaining = opts.stderr_cap_bytes - stderrBytes;
286
+ if (remaining > 0) {
287
+ const chunk = Buffer.from(line, "utf-8").subarray(0, remaining);
288
+ stderrChunks.push(chunk);
289
+ stderrBytes += chunk.length;
290
+ if (chunk.length < Buffer.byteLength(line, "utf-8")) stderrTruncated = true;
291
+ }
292
+ });
293
+ child.on("close", (code, signal) => finalize(code, signal));
294
+ });
295
+ }
296
+
297
+ function defaultGenerateId(): string {
298
+ return randomUUID();
299
+ }
300
+
301
+ function writeMeta(
302
+ path: string,
303
+ result: ExecSandboxResult,
304
+ request: ExecSandboxRequest,
305
+ now: Date,
306
+ ): void {
307
+ const meta = {
308
+ id: result.id,
309
+ runtime: result.runtime,
310
+ purpose: request.purpose ?? null,
311
+ script_chars: request.script.length,
312
+ started_at: now.toISOString(),
313
+ finished_at: new Date(now.getTime() + result.duration_ms).toISOString(),
314
+ exit_code: result.exit_code,
315
+ signal: result.signal,
316
+ timed_out: result.timed_out,
317
+ duration_ms: result.duration_ms,
318
+ stdout_bytes: result.stdout_bytes,
319
+ stderr_bytes: result.stderr_bytes,
320
+ stdout_truncated: result.stdout_truncated,
321
+ stderr_truncated: result.stderr_truncated,
322
+ stdout_path: result.stdout_path,
323
+ stderr_path: result.stderr_path,
324
+ };
325
+ writeFileSync(path, `${JSON.stringify(meta, null, 2)}\n`);
326
+ }
@@ -564,7 +564,9 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
564
564
  // indexes so old DBs can open far enough for the normal migration chain.
565
565
  ensureBootstrapIndexColumns(db);
566
566
 
567
- db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
567
+ if (columnExists(db, "memories", "scope")) {
568
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
569
+ }
568
570
  db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_kind ON memory_sources(kind)");
569
571
  db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_scope ON memory_sources(scope)");
570
572
  db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_from ON memory_relations(from_id)");
@@ -1199,6 +1201,8 @@ let currentPath: string | null = null;
1199
1201
  let currentPid: number = 0;
1200
1202
  let _exitHandlerRegistered = false;
1201
1203
  let _dbOpenAttempted = false;
1204
+ let _lastDbError: Error | null = null;
1205
+ let _lastDbPhase: "open" | "initSchema" | "vacuum-recovery" | null = null;
1202
1206
 
1203
1207
  export function getDbProvider(): ProviderName | null {
1204
1208
  loadProvider();
@@ -1219,12 +1223,58 @@ export function wasDbOpenAttempted(): boolean {
1219
1223
  return _dbOpenAttempted;
1220
1224
  }
1221
1225
 
1226
+ export function getDbStatus(): {
1227
+ available: boolean;
1228
+ provider: ProviderName | null;
1229
+ attempted: boolean;
1230
+ lastError: Error | null;
1231
+ lastPhase: "open" | "initSchema" | "vacuum-recovery" | null;
1232
+ } {
1233
+ loadProvider();
1234
+ return {
1235
+ available: currentDb !== null,
1236
+ provider: providerName,
1237
+ attempted: _dbOpenAttempted,
1238
+ lastError: _lastDbError,
1239
+ lastPhase: _lastDbPhase,
1240
+ };
1241
+ }
1242
+
1222
1243
  export function openDatabase(path: string): boolean {
1223
1244
  _dbOpenAttempted = true;
1224
1245
  if (currentDb && currentPath !== path) closeDatabase();
1225
1246
  if (currentDb && currentPath === path) return true;
1226
1247
 
1227
- const rawDb = openRawDb(path);
1248
+ // Reset error state only when a new open attempt is actually going to run.
1249
+ _lastDbError = null;
1250
+ _lastDbPhase = null;
1251
+
1252
+ let rawDb: unknown;
1253
+ let fallbackProvider: ProviderName | null = null;
1254
+ let fallbackModule: unknown = null;
1255
+ try {
1256
+ rawDb = openRawDb(path);
1257
+ } catch (primaryErr) {
1258
+ _lastDbPhase = "open";
1259
+ _lastDbError = primaryErr instanceof Error ? primaryErr : new Error(String(primaryErr));
1260
+ // node:sqlite loaded but failed to open this file — try better-sqlite3 as fallback.
1261
+ if (providerName === "node:sqlite") {
1262
+ try {
1263
+ const mod = _require("better-sqlite3");
1264
+ const Db = (mod && mod.default) ? mod.default : mod;
1265
+ if (typeof Db === "function") {
1266
+ rawDb = new Db(path);
1267
+ fallbackProvider = "better-sqlite3";
1268
+ fallbackModule = Db;
1269
+ _lastDbError = null;
1270
+ _lastDbPhase = null;
1271
+ }
1272
+ } catch {
1273
+ // fallback unavailable; surface original error
1274
+ }
1275
+ }
1276
+ if (!rawDb) throw primaryErr;
1277
+ }
1228
1278
  if (!rawDb) return false;
1229
1279
 
1230
1280
  const adapter = createAdapter(rawDb);
@@ -1240,15 +1290,25 @@ export function openDatabase(path: string): boolean {
1240
1290
  initSchema(adapter, fileBacked);
1241
1291
  process.stderr.write("gsd-db: recovered corrupt database via VACUUM\n");
1242
1292
  } catch (retryErr) {
1293
+ _lastDbPhase = "vacuum-recovery";
1294
+ _lastDbError = retryErr instanceof Error ? retryErr : new Error(String(retryErr));
1243
1295
  try { adapter.close(); } catch (e) { logWarning("db", `close after VACUUM failed: ${(e as Error).message}`); }
1244
1296
  throw retryErr;
1245
1297
  }
1246
1298
  } else {
1247
- try { adapter.close(); } catch (e) { logWarning("db", `close after VACUUM failed: ${(e as Error).message}`); }
1299
+ _lastDbPhase = "initSchema";
1300
+ _lastDbError = err instanceof Error ? err : new Error(String(err));
1301
+ try { adapter.close(); } catch (e) { logWarning("db", `close after initSchema failed: ${(e as Error).message}`); }
1248
1302
  throw err;
1249
1303
  }
1250
1304
  }
1251
1305
 
1306
+ // Commit fallback provider switch only after open + schema both succeeded.
1307
+ if (fallbackProvider) {
1308
+ providerName = fallbackProvider;
1309
+ providerModule = fallbackModule;
1310
+ }
1311
+
1252
1312
  currentDb = adapter;
1253
1313
  currentPath = path;
1254
1314
  currentPid = process.pid;
@@ -1276,8 +1336,12 @@ export function closeDatabase(): void {
1276
1336
  currentDb = null;
1277
1337
  currentPath = null;
1278
1338
  currentPid = 0;
1279
- _dbOpenAttempted = false;
1280
1339
  }
1340
+ // Reset session-scoped state unconditionally so stale error info from a
1341
+ // failed open doesn't persist into the next open attempt or status check.
1342
+ _dbOpenAttempted = false;
1343
+ _lastDbError = null;
1344
+ _lastDbPhase = null;
1281
1345
  }
1282
1346
 
1283
1347
  /** Run a full VACUUM — call sparingly (e.g. after milestone completion). */
@@ -10,7 +10,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
10
10
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { showNextAction } from "../shared/tui.js";
13
- import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
13
+ import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
14
14
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
15
15
  import { gsdRoot } from "./paths.js";
16
16
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -74,6 +74,7 @@ export async function showProjectInit(
74
74
  }
75
75
 
76
76
  // ── Step 2: Git setup ──────────────────────────────────────────────────────
77
+ let didInitGit = false;
77
78
  if (!signals.isGitRepo) {
78
79
  const gitChoice = await showNextAction(ctx, {
79
80
  title: "GSD — Project Setup",
@@ -89,6 +90,7 @@ export async function showProjectInit(
89
90
 
90
91
  if (gitChoice === "init_git") {
91
92
  nativeInit(basePath, prefs.mainBranch);
93
+ didInitGit = true;
92
94
  }
93
95
  } else {
94
96
  // Auto-detect main branch from existing repo
@@ -295,6 +297,18 @@ export async function showProjectInit(
295
297
  ensureGitignore(basePath);
296
298
  untrackRuntimeFiles(basePath);
297
299
 
300
+ // Create initial commit so git log and git worktree work immediately (#4530).
301
+ // Without this, the branch is "unborn" (zero commits) and downstream operations
302
+ // like `git log` and `git worktree add` fail.
303
+ if (didInitGit) {
304
+ try {
305
+ nativeAddAll(basePath);
306
+ nativeCommit(basePath, "chore: init project");
307
+ } catch {
308
+ // Non-fatal — user can commit manually; don't block project init
309
+ }
310
+ }
311
+
298
312
  // Auto-generate codebase map for instant agent orientation
299
313
  try {
300
314
  const result = generateCodebaseMap(basePath);
@@ -35,6 +35,12 @@ export interface ProviderInfo {
35
35
  export const PROVIDER_REGISTRY: ProviderInfo[] = [
36
36
  // LLM Providers
37
37
  { id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], hasOAuth: true, dashboardUrl: "console.anthropic.com" },
38
+ // Claude Code CLI: routes through the local `claude` binary — no API key,
39
+ // authentication is handled by the CLI's own OAuth flow.
40
+ // Referenced by doctor-providers.ts, auto-model-selection.ts, and others;
41
+ // must be in the canonical registry so all consumers see the same catalog.
42
+ // See: https://github.com/gsd-build/gsd-2/issues/4541
43
+ { id: "claude-code", label: "Claude Code CLI", category: "llm", hasOAuth: true },
38
44
  { id: "openai", label: "OpenAI", category: "llm", envVar: "OPENAI_API_KEY", prefixes: ["sk-"], dashboardUrl: "platform.openai.com/api-keys" },
39
45
  { id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", hasOAuth: true },
40
46
  { id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)",category: "llm", hasOAuth: true },
@@ -91,8 +91,13 @@ export function extractPackageReferences(description: string): string[] {
91
91
  }
92
92
  }
93
93
 
94
- // require('pkg') or import from 'pkg' in code blocks
95
- const importPattern = /(?:require\s*\(\s*['"]|from\s+['"])([a-zA-Z0-9@/_-]+)['"\)]/g;
94
+ // require('pkg') or `import ... from 'pkg'` in code blocks.
95
+ // The `from\s+['"]` branch MUST be preceded by an `import` keyword so that
96
+ // natural-language prose like `from "What's Next"` or `from 'master'` does
97
+ // not produce false package-existence failures. Requiring the leading import
98
+ // keyword anchors the match to JavaScript/TypeScript syntax.
99
+ // See: https://github.com/gsd-build/gsd-2/issues/4388
100
+ const importPattern = /(?:require\s*\(\s*['"]|import\b[\s\S]*?\bfrom\s+['"])([a-zA-Z0-9@/_-]+)['"\)]/g;
96
101
  let importMatch: RegExpExecArray | null;
97
102
  while ((importMatch = importPattern.exec(description)) !== null) {
98
103
  // Skip relative imports and node builtins
@@ -325,7 +330,12 @@ function extractPathFromAnnotation(raw: string): string {
325
330
 
326
331
  const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/);
327
332
  if (annotatedMatch) {
328
- return annotatedMatch[1].trim();
333
+ const prefix = annotatedMatch[1].trim();
334
+ const prefixBacktickMatch = prefix.match(/`([^`]+)`/);
335
+ if (prefixBacktickMatch && looksLikePathOrUrl(prefixBacktickMatch[1].trim())) {
336
+ return prefixBacktickMatch[1].trim();
337
+ }
338
+ return prefix.replace(/`/g, "").trim();
329
339
  }
330
340
 
331
341
  // Fallback: scan all backticked tokens and return the first one that looks
@@ -28,6 +28,37 @@ export interface ContextManagementConfig {
28
28
  compaction_threshold_percent?: number; // default: 0.70, range: 0.5-0.95
29
29
  tool_result_max_chars?: number; // default: 800, range: 200-10000
30
30
  }
31
+
32
+ /**
33
+ * Opt-in tool-output sandboxing for sub-sessions. When enabled, the gsd_exec
34
+ * MCP tool runs scripts in an isolated subprocess and returns only a short
35
+ * digest to the calling agent's context window; full stdout/stderr persist
36
+ * in the project memory store and can be retrieved by id later.
37
+ *
38
+ * Inspired by mksglu/context-mode (Elastic License 2.0). This is an
39
+ * independent implementation — no upstream code is incorporated.
40
+ */
41
+ export interface ContextModeConfig {
42
+ /** Master switch. Default: true (opt-out via `enabled: false`). */
43
+ enabled?: boolean;
44
+ /** Per-invocation timeout in milliseconds. Default: 30_000. Range: 1_000–600_000. */
45
+ exec_timeout_ms?: number;
46
+ /** Cap on persisted stdout bytes per invocation. Default: 1_048_576 (1 MiB). Range: 4_096–16_777_216. */
47
+ exec_stdout_cap_bytes?: number;
48
+ /** Number of trailing stdout characters returned in the digest. Default: 300. Range: 0–4_000. */
49
+ exec_digest_chars?: number;
50
+ /** Environment variables forwarded to sandboxed processes (case-sensitive names). PATH and HOME are always forwarded. */
51
+ exec_env_allowlist?: string[];
52
+ }
53
+
54
+ /**
55
+ * Resolve whether context-mode features (gsd_exec sandbox + compaction
56
+ * snapshot) should be active. Default is ON: missing config or missing
57
+ * `enabled` is treated as true. Only `enabled: false` disables.
58
+ */
59
+ export function isContextModeEnabled(prefs: { context_mode?: ContextModeConfig } | null | undefined): boolean {
60
+ return prefs?.context_mode?.enabled !== false;
61
+ }
31
62
  import type { GitHubSyncConfig } from "../github-sync/types.js";
32
63
 
33
64
  // ─── Workflow Modes ──────────────────────────────────────────────────────────
@@ -117,6 +148,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
117
148
  "flat_rate_providers",
118
149
  "language",
119
150
  "context_window_override",
151
+ "context_mode",
120
152
  ]);
121
153
 
122
154
  /** Canonical list of all dispatch unit types. */
@@ -300,6 +332,12 @@ export interface GSDPreferences {
300
332
  */
301
333
  context_window_override?: number;
302
334
  context_management?: ContextManagementConfig;
335
+ /**
336
+ * Tool-output sandboxing via gsd_exec. Keeps sub-session context windows
337
+ * clean by running scripts in a subprocess and only surfacing a short
338
+ * digest. See `ContextModeConfig`. Default: disabled.
339
+ */
340
+ context_mode?: ContextModeConfig;
303
341
  token_profile?: TokenProfile;
304
342
  phases?: PhaseSkipPreferences;
305
343
  auto_visualize?: boolean;
@@ -644,6 +644,50 @@ export function validatePreferences(preferences: GSDPreferences): {
644
644
  }
645
645
  }
646
646
 
647
+ // ─── Context Mode (gsd_exec sandbox) ────────────────────────────────────
648
+ if (preferences.context_mode !== undefined) {
649
+ if (typeof preferences.context_mode === "object" && preferences.context_mode !== null) {
650
+ const cmode = preferences.context_mode as unknown as Record<string, unknown>;
651
+ const validCmode: Record<string, unknown> = {};
652
+
653
+ if (cmode.enabled !== undefined) {
654
+ if (typeof cmode.enabled === "boolean") validCmode.enabled = cmode.enabled;
655
+ else errors.push("context_mode.enabled must be a boolean");
656
+ }
657
+ if (cmode.exec_timeout_ms !== undefined) {
658
+ const t = cmode.exec_timeout_ms;
659
+ if (typeof t === "number" && t >= 1000 && t <= 600_000) validCmode.exec_timeout_ms = Math.floor(t);
660
+ else errors.push("context_mode.exec_timeout_ms must be a number between 1000 and 600000");
661
+ }
662
+ if (cmode.exec_stdout_cap_bytes !== undefined) {
663
+ const b = cmode.exec_stdout_cap_bytes;
664
+ if (typeof b === "number" && b >= 4096 && b <= 16_777_216) validCmode.exec_stdout_cap_bytes = Math.floor(b);
665
+ else errors.push("context_mode.exec_stdout_cap_bytes must be a number between 4096 and 16777216");
666
+ }
667
+ if (cmode.exec_digest_chars !== undefined) {
668
+ const c = cmode.exec_digest_chars;
669
+ if (typeof c === "number" && c >= 0 && c <= 4000) validCmode.exec_digest_chars = Math.floor(c);
670
+ else errors.push("context_mode.exec_digest_chars must be a number between 0 and 4000");
671
+ }
672
+ if (cmode.exec_env_allowlist !== undefined) {
673
+ if (
674
+ Array.isArray(cmode.exec_env_allowlist) &&
675
+ cmode.exec_env_allowlist.every((v) => typeof v === "string" && /^[A-Z_][A-Z0-9_]*$/i.test(v))
676
+ ) {
677
+ validCmode.exec_env_allowlist = cmode.exec_env_allowlist;
678
+ } else {
679
+ errors.push("context_mode.exec_env_allowlist must be an array of valid env var names");
680
+ }
681
+ }
682
+
683
+ if (Object.keys(validCmode).length > 0) {
684
+ validated.context_mode = validCmode as any;
685
+ }
686
+ } else {
687
+ errors.push("context_mode must be an object");
688
+ }
689
+ }
690
+
647
691
  // ─── Parallel Config ────────────────────────────────────────────────────
648
692
  if (preferences.parallel && typeof preferences.parallel === "object") {
649
693
  const p = preferences.parallel as unknown as Record<string, unknown>;
@@ -697,6 +741,41 @@ export function validatePreferences(preferences: GSDPreferences): {
697
741
  }
698
742
  }
699
743
 
744
+ // ─── Slice Parallel Config ───────────────────────────────────────────────
745
+ if (preferences.slice_parallel !== undefined) {
746
+ if (typeof preferences.slice_parallel === "object" && preferences.slice_parallel !== null) {
747
+ const sp = preferences.slice_parallel as Record<string, unknown>;
748
+ const validSp: NonNullable<GSDPreferences["slice_parallel"]> = {};
749
+
750
+ if (sp.enabled !== undefined) {
751
+ if (typeof sp.enabled === "boolean") validSp.enabled = sp.enabled;
752
+ else errors.push("slice_parallel.enabled must be a boolean");
753
+ }
754
+
755
+ if (sp.max_workers !== undefined) {
756
+ const maxWorkers = typeof sp.max_workers === "number" ? sp.max_workers : Number(sp.max_workers);
757
+ if (Number.isFinite(maxWorkers) && maxWorkers >= 1 && maxWorkers <= 8) {
758
+ validSp.max_workers = Math.floor(maxWorkers);
759
+ } else {
760
+ errors.push("slice_parallel.max_workers must be a number between 1 and 8");
761
+ }
762
+ }
763
+
764
+ const knownSliceParallelKeys = new Set(["enabled", "max_workers"]);
765
+ for (const key of Object.keys(sp)) {
766
+ if (!knownSliceParallelKeys.has(key)) {
767
+ warnings.push(`unknown slice_parallel key "${key}" — ignored`);
768
+ }
769
+ }
770
+
771
+ if (Object.keys(validSp).length > 0) {
772
+ validated.slice_parallel = validSp;
773
+ }
774
+ } else {
775
+ errors.push("slice_parallel must be an object");
776
+ }
777
+ }
778
+
700
779
  // ─── Reactive Execution ─────────────────────────────────────────────────
701
780
  if (preferences.reactive_execution !== undefined) {
702
781
  if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {