minovative-mind-cli 2.14.1 → 2.14.3

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 (47) hide show
  1. package/README.md +40 -59
  2. package/dist/services/agent/slashCommands.js +144 -13
  3. package/dist/services/agent/toolLoop.js +11 -2
  4. package/dist/services/agent/types.d.ts +9 -2
  5. package/dist/services/agent-tools.d.ts +20 -1
  6. package/dist/services/agent-tools.js +272 -47
  7. package/dist/services/agent.js +43 -18
  8. package/dist/services/ai.d.ts +9 -0
  9. package/dist/services/ai.js +71 -21
  10. package/dist/services/chatHistoryService.d.ts +5 -0
  11. package/dist/services/contextAgent.d.ts +14 -6
  12. package/dist/services/contextAgent.js +36 -10
  13. package/dist/services/orchestration/investigationAgent.js +31 -7
  14. package/dist/services/orchestration/investigationCache.js +19 -9
  15. package/dist/services/orchestration/readCache.d.ts +1 -0
  16. package/dist/services/orchestration/readCache.js +5 -2
  17. package/dist/services/orchestration/scopedTools.js +37 -10
  18. package/dist/services/orchestration/subAgent.js +16 -2
  19. package/dist/services/proxyClient.d.ts +6 -0
  20. package/dist/services/proxyClient.js +24 -10
  21. package/dist/services/sessionSettings.d.ts +66 -0
  22. package/dist/services/sessionSettings.js +126 -0
  23. package/dist/services/userProfileService.d.ts +14 -0
  24. package/dist/services/userProfileService.js +105 -3
  25. package/dist/services/verificationService.js +24 -2
  26. package/dist/utils/analysisRunner.d.ts +120 -8
  27. package/dist/utils/analysisRunner.js +946 -125
  28. package/dist/utils/antiCheatingGuard.d.ts +21 -0
  29. package/dist/utils/antiCheatingGuard.js +554 -0
  30. package/dist/utils/contextPrompts.d.ts +39 -0
  31. package/dist/utils/contextPrompts.js +81 -9
  32. package/dist/utils/contextRanker.d.ts +216 -0
  33. package/dist/utils/contextRanker.js +603 -0
  34. package/dist/utils/dependencyTracer/modules/graph.d.ts +4 -1
  35. package/dist/utils/dependencyTracer/modules/graph.js +11 -0
  36. package/dist/utils/dependencyTracer/modules/types.d.ts +10 -0
  37. package/dist/utils/dependencyTracer.d.ts +40 -2
  38. package/dist/utils/dependencyTracer.js +95 -3
  39. package/dist/utils/fileReadCache.d.ts +58 -0
  40. package/dist/utils/fileReadCache.js +162 -0
  41. package/dist/utils/projectStorage.js +2 -1
  42. package/dist/utils/symbolExtractor.d.ts +12 -0
  43. package/dist/utils/symbolExtractor.js +111 -15
  44. package/dist/utils/systemPrompts.d.ts +4 -3
  45. package/dist/utils/systemPrompts.js +66 -8
  46. package/oclif.manifest.json +1 -1
  47. package/package.json +1 -1
@@ -1,10 +1,8 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { exec } from 'node:child_process';
5
- import { promisify } from 'node:util';
4
+ import { spawn } from 'node:child_process';
6
5
  import { createHash } from 'node:crypto';
7
- const execAsync = promisify(exec);
8
6
  /** Supported language aliases map. */
9
7
  const LANGUAGE_ALIASES = {
10
8
  js: 'node',
@@ -56,6 +54,85 @@ export const SUPPORTED_LANGUAGES = [
56
54
  'php',
57
55
  'java',
58
56
  ];
57
+ /**
58
+ * High-performance Least-Recently-Used (LRU) environment and runner cache with per-item TTL.
59
+ * Prevents repetitive filesystem traversal and binary probing across ephemeral executions.
60
+ */
61
+ export class EnvironmentLRUCache {
62
+ cache = new Map();
63
+ max;
64
+ ttlMs;
65
+ constructor(max = 100, ttlMs = 30_000) {
66
+ this.max = max;
67
+ this.ttlMs = ttlMs;
68
+ }
69
+ get size() {
70
+ return this.cache.size;
71
+ }
72
+ clear() {
73
+ this.cache.clear();
74
+ }
75
+ delete(key) {
76
+ return this.cache.delete(key);
77
+ }
78
+ get(key) {
79
+ const entry = this.cache.get(key);
80
+ if (!entry)
81
+ return undefined;
82
+ if (Date.now() > entry.expiresAt) {
83
+ this.cache.delete(key);
84
+ return undefined;
85
+ }
86
+ // Refresh LRU order (delete & re-insert)
87
+ this.cache.delete(key);
88
+ this.cache.set(key, entry);
89
+ return entry.value;
90
+ }
91
+ has(key) {
92
+ return this.get(key) !== undefined;
93
+ }
94
+ set(key, value, ttlMs) {
95
+ if (this.cache.has(key)) {
96
+ this.cache.delete(key);
97
+ }
98
+ else if (this.cache.size >= this.max) {
99
+ const oldestKey = this.cache.keys().next().value;
100
+ if (oldestKey !== undefined) {
101
+ this.cache.delete(oldestKey);
102
+ }
103
+ }
104
+ this.cache.set(key, {
105
+ expiresAt: Date.now() + (ttlMs ?? this.ttlMs),
106
+ value,
107
+ });
108
+ }
109
+ }
110
+ // Global LRU cache instances for workspace runner probes
111
+ const tsRunnerCache = new EnvironmentLRUCache(100, 30_000);
112
+ const pyEnvCache = new EnvironmentLRUCache(100, 30_000);
113
+ const moduleTypeCache = new EnvironmentLRUCache(100, 30_000);
114
+ const projectRuntimeCache = new EnvironmentLRUCache(100, 30_000);
115
+ /**
116
+ * Clears cached environment and runner resolution data.
117
+ * If workspaceRoot is provided, invalidates cache entries for that workspace;
118
+ * otherwise, clears all cached runner environments globally.
119
+ *
120
+ * @param workspaceRoot - Optional workspace root directory to invalidate.
121
+ */
122
+ export function clearRunnerCache(workspaceRoot) {
123
+ if (workspaceRoot) {
124
+ tsRunnerCache.delete(workspaceRoot);
125
+ pyEnvCache.delete(workspaceRoot);
126
+ moduleTypeCache.delete(workspaceRoot);
127
+ projectRuntimeCache.delete(workspaceRoot);
128
+ }
129
+ else {
130
+ tsRunnerCache.clear();
131
+ pyEnvCache.clear();
132
+ moduleTypeCache.clear();
133
+ projectRuntimeCache.clear();
134
+ }
135
+ }
59
136
  /**
60
137
  * Normalizes user/AI provided language string to a standard runtime identifier.
61
138
  */
@@ -141,11 +218,15 @@ export function detectLanguageFromCode(code) {
141
218
  }
142
219
  /**
143
220
  * Detects the dominant programming language / runtime for a workspace based on project manifest files.
221
+ * Caches results per workspace root via LRU cache.
144
222
  *
145
223
  * @param workspaceRoot - Path to the workspace root directory.
146
224
  * @returns Detected runtime identifier (e.g., 'rust', 'go', 'python', 'cpp', 'ts-node', 'node').
147
225
  */
148
226
  export async function detectProjectRuntime(workspaceRoot) {
227
+ const cached = projectRuntimeCache.get(workspaceRoot);
228
+ if (cached)
229
+ return cached;
149
230
  const probeFiles = [
150
231
  { file: 'Cargo.toml', runtime: 'rust' },
151
232
  { file: 'go.mod', runtime: 'go' },
@@ -164,12 +245,14 @@ export async function detectProjectRuntime(workspaceRoot) {
164
245
  for (const { file, runtime } of probeFiles) {
165
246
  try {
166
247
  await fs.access(path.join(workspaceRoot, file));
248
+ projectRuntimeCache.set(workspaceRoot, runtime);
167
249
  return runtime;
168
250
  }
169
251
  catch {
170
252
  // Continue searching
171
253
  }
172
254
  }
255
+ projectRuntimeCache.set(workspaceRoot, 'node');
173
256
  return 'node';
174
257
  }
175
258
  /**
@@ -194,16 +277,22 @@ export async function resolveEffectiveRuntime(workspaceRoot, language, code) {
194
277
  }
195
278
  /**
196
279
  * Detects whether the workspace package.json specifies `"type": "module"`.
197
- * Returns `'module'` or `'commonjs'`.
280
+ * Returns `'module'` or `'commonjs'`. Caches results per workspace root via LRU cache.
198
281
  */
199
282
  export async function detectWorkspaceModuleType(workspaceRoot) {
283
+ const cached = moduleTypeCache.get(workspaceRoot);
284
+ if (cached)
285
+ return cached;
200
286
  try {
201
287
  const pkgPath = path.join(workspaceRoot, 'package.json');
202
288
  const content = await fs.readFile(pkgPath, 'utf-8');
203
289
  const pkg = JSON.parse(content);
204
- return pkg.type === 'module' ? 'module' : 'commonjs';
290
+ const result = pkg.type === 'module' ? 'module' : 'commonjs';
291
+ moduleTypeCache.set(workspaceRoot, result);
292
+ return result;
205
293
  }
206
294
  catch {
295
+ moduleTypeCache.set(workspaceRoot, 'commonjs');
207
296
  return 'commonjs';
208
297
  }
209
298
  }
@@ -254,44 +343,574 @@ function buildTempPath(code, ext) {
254
343
  return path.join(os.tmpdir(), `.mino-analysis-${hash}-${nonce}${ext}`);
255
344
  }
256
345
  /**
257
- * Constructs the shell command for executing the given language runtime.
258
- * Returns null for unsupported languages.
346
+ * Probes the workspace for available TypeScript runners (tsx, ts-node-esm, ts-node)
347
+ * and resolves tsconfig.json configuration for path aliases.
348
+ * Caches results per workspace root via LRU cache.
349
+ *
350
+ * @param workspaceRoot - Path to the root directory of the workspace.
351
+ * @returns Runner info with executable command and tsconfig path if available.
352
+ */
353
+ export async function findTypeScriptRunner(workspaceRoot) {
354
+ const cached = tsRunnerCache.get(workspaceRoot);
355
+ if (cached)
356
+ return cached;
357
+ const binDir = path.join(workspaceRoot, 'node_modules', '.bin');
358
+ const isWin = os.platform() === 'win32';
359
+ const tsconfigPath = path.join(workspaceRoot, 'tsconfig.json');
360
+ let hasTsconfig = false;
361
+ try {
362
+ await fs.access(tsconfigPath);
363
+ hasTsconfig = true;
364
+ }
365
+ catch {
366
+ // tsconfig not present in root
367
+ }
368
+ // 1. Prefer local workspace tsx if installed (fastest, native ESM/CJS and tsconfig paths support)
369
+ const tsxBin = path.join(binDir, isWin ? 'tsx.cmd' : 'tsx');
370
+ try {
371
+ await fs.access(tsxBin);
372
+ const result = {
373
+ runnerType: 'local-tsx',
374
+ command: `"${tsxBin}"`,
375
+ binaryPath: tsxBin,
376
+ tsconfigPath: hasTsconfig ? tsconfigPath : undefined,
377
+ };
378
+ tsRunnerCache.set(workspaceRoot, result);
379
+ return result;
380
+ }
381
+ catch {
382
+ // Continue probing
383
+ }
384
+ // 2. Check workspace module type for ts-node variant
385
+ const moduleType = await detectWorkspaceModuleType(workspaceRoot);
386
+ if (moduleType === 'module') {
387
+ const tsNodeEsmBin = path.join(binDir, isWin ? 'ts-node-esm.cmd' : 'ts-node-esm');
388
+ try {
389
+ await fs.access(tsNodeEsmBin);
390
+ const result = {
391
+ runnerType: 'local-ts-node-esm',
392
+ command: `"${tsNodeEsmBin}"`,
393
+ binaryPath: tsNodeEsmBin,
394
+ tsconfigPath: hasTsconfig ? tsconfigPath : undefined,
395
+ };
396
+ tsRunnerCache.set(workspaceRoot, result);
397
+ return result;
398
+ }
399
+ catch {
400
+ // Continue probing
401
+ }
402
+ }
403
+ // 3. Check local ts-node binary
404
+ const tsNodeBin = path.join(binDir, isWin ? 'ts-node.cmd' : 'ts-node');
405
+ try {
406
+ await fs.access(tsNodeBin);
407
+ const extraFlag = moduleType === 'module' ? '--esm ' : '';
408
+ const result = {
409
+ runnerType: 'local-ts-node',
410
+ command: `"${tsNodeBin}" ${extraFlag}`.trim(),
411
+ binaryPath: tsNodeBin,
412
+ tsconfigPath: hasTsconfig ? tsconfigPath : undefined,
413
+ };
414
+ tsRunnerCache.set(workspaceRoot, result);
415
+ return result;
416
+ }
417
+ catch {
418
+ // Continue probing
419
+ }
420
+ // 4. Default fallback: npx tsx (preferred) or npx ts-node
421
+ const result = {
422
+ runnerType: 'npx-tsx',
423
+ command: 'npx tsx',
424
+ binaryPath: 'npx',
425
+ tsconfigPath: hasTsconfig ? tsconfigPath : undefined,
426
+ };
427
+ tsRunnerCache.set(workspaceRoot, result);
428
+ return result;
429
+ }
430
+ /**
431
+ * Probes the workspace for virtual environments (.venv, venv, env, .env, virtualenv)
432
+ * and resolves the appropriate Python binary path and environment directories.
433
+ * Caches results per workspace root via LRU cache.
434
+ *
435
+ * @param workspaceRoot - Path to workspace root directory.
436
+ * @returns Detected Python interpreter binary and virtual environment metadata.
259
437
  */
260
- function buildCommand(language, scriptPath) {
438
+ export async function findPythonBinary(workspaceRoot) {
439
+ const cached = pyEnvCache.get(workspaceRoot);
440
+ if (cached)
441
+ return cached;
442
+ const isWin = os.platform() === 'win32';
443
+ const candidateDirs = ['.venv', 'venv', 'env', '.env', 'virtualenv', '.virtualenv'];
444
+ for (const dir of candidateDirs) {
445
+ const venvDir = path.join(workspaceRoot, dir);
446
+ const probeBinaries = isWin
447
+ ? [
448
+ path.join(venvDir, 'Scripts', 'python.exe'),
449
+ path.join(venvDir, 'python.exe'),
450
+ path.join(venvDir, 'Scripts', 'python.cmd'),
451
+ ]
452
+ : [
453
+ path.join(venvDir, 'bin', 'python'),
454
+ path.join(venvDir, 'bin', 'python3'),
455
+ path.join(venvDir, 'bin', 'python.exe'),
456
+ ];
457
+ for (const binPath of probeBinaries) {
458
+ try {
459
+ await fs.access(binPath);
460
+ const result = {
461
+ pythonBin: binPath,
462
+ isVenv: true,
463
+ venvDir,
464
+ binDir: path.dirname(binPath),
465
+ };
466
+ pyEnvCache.set(workspaceRoot, result);
467
+ return result;
468
+ }
469
+ catch {
470
+ // Continue probing
471
+ }
472
+ }
473
+ }
474
+ const fallback = {
475
+ pythonBin: 'python3',
476
+ isVenv: false,
477
+ };
478
+ pyEnvCache.set(workspaceRoot, fallback);
479
+ return fallback;
480
+ }
481
+ /**
482
+ * Injects sandbox query helpers into user/agent script source code.
483
+ * Provides preloaded `emitResult` / `__emitResult` (structured JSON emission via dedicated FD-3 with stdout fallback)
484
+ * and `inspectSymbols` / `inspectObject` (reflection & AST property analysis).
485
+ *
486
+ * Preserves 1:1 source line offsets so compiler/runtime stack traces and syntax diagnostics
487
+ * accurately align with original source lines.
488
+ *
489
+ * @param code - Raw source code of the script.
490
+ * @param language - Target runtime language.
491
+ * @returns Source code augmented with runtime-specific helper preambles.
492
+ */
493
+ export function injectSandboxHelpers(code, language) {
261
494
  const normLang = normalizeLanguage(language);
262
- const binExt = os.platform() === 'win32' ? '.exe' : '';
263
- switch (normLang) {
264
- case 'node':
265
- return `node "${scriptPath}"`;
266
- case 'ts-node':
267
- return `npx ts-node "${scriptPath}"`;
268
- case 'python':
269
- return `python3 "${scriptPath}"`;
270
- case 'bash':
271
- return `bash "${scriptPath}"`;
272
- case 'go':
273
- return `go run "${scriptPath}"`;
274
- case 'rust': {
275
- const binPath = scriptPath.replace(/\.rs$/, binExt);
276
- return `rustc "${scriptPath}" -o "${binPath}" && "${binPath}"`;
495
+ if (code.includes('__MINO_PREAMBLE_START__')) {
496
+ return code;
497
+ }
498
+ if (normLang === 'node' || normLang === 'ts-node') {
499
+ // Single-line preamble preserving exact 1:1 line offsets
500
+ const preamble = `/* __MINO_PREAMBLE_START__ */ if (typeof globalThis.emitResult === 'undefined') { globalThis.emitResult = (data) => { const payload = typeof data === 'string' ? data : JSON.stringify(data); try { const _fs = typeof require !== 'undefined' ? require('node:fs') : (typeof process !== 'undefined' && process.getBuiltinModule ? process.getBuiltinModule('node:fs') : null); if (_fs && _fs.writeSync) { _fs.writeSync(3, payload + '\\n'); return; } } catch {} console.log('__MINO_RESULT__' + payload + '__MINO_RESULT__'); }; globalThis.__emitResult = globalThis.emitResult; } if (typeof globalThis.inspectSymbols === 'undefined') { globalThis.inspectSymbols = (target) => { if (target === null || target === undefined) return []; const symbols = []; const seen = new Set(); let curr = target; let depth = 0; while (curr && depth < 2) { for (const key of Object.getOwnPropertyNames(curr)) { if (key === 'constructor' && depth > 0) continue; if (seen.has(key)) continue; seen.add(key); let val; let type = 'unknown'; let isFunc = false; try { val = curr[key]; type = typeof val; isFunc = type === 'function'; } catch { type = 'inaccessible'; } symbols.push({ name: key, type, isFunction: isFunc, inherited: depth > 0 }); } curr = Object.getPrototypeOf(curr); depth++; if (curr === Object.prototype) break; } return symbols; }; globalThis.inspectObject = globalThis.inspectSymbols; } /* __MINO_PREAMBLE_END__ */ `;
501
+ if (code.startsWith('#!')) {
502
+ const firstLineEnd = code.indexOf('\n');
503
+ if (firstLineEnd !== -1) {
504
+ const shebang = code.slice(0, firstLineEnd + 1);
505
+ const rest = code.slice(firstLineEnd + 1);
506
+ return shebang + preamble + rest;
507
+ }
277
508
  }
278
- case 'c': {
279
- const binPath = scriptPath.replace(/\.c$/, binExt);
280
- return `gcc "${scriptPath}" -o "${binPath}" && "${binPath}"`;
509
+ return preamble + code;
510
+ }
511
+ if (normLang === 'python') {
512
+ const preamble = `# __MINO_PREAMBLE_START__
513
+ import json as _mino_json
514
+ import inspect as _mino_inspect
515
+ import os as _mino_os
516
+
517
+ def emitResult(data):
518
+ payload = data if isinstance(data, str) else _mino_json.dumps(data)
519
+ try:
520
+ _mino_os.write(3, (payload + '\\n').encode('utf-8'))
521
+ except Exception:
522
+ print(f"__MINO_RESULT__{payload}__MINO_RESULT__")
523
+
524
+ __emitResult = emitResult
525
+
526
+ def inspect_symbols(target):
527
+ symbols = []
528
+ if target is None:
529
+ return symbols
530
+ for name in dir(target):
531
+ if name.startswith('__') and name.endswith('__'):
532
+ continue
533
+ try:
534
+ val = getattr(target, name)
535
+ is_fn = _mino_inspect.isfunction(val) or _mino_inspect.ismethod(val) or _mino_inspect.isbuiltin(val)
536
+ is_cls = _mino_inspect.isclass(val)
537
+ t_name = type(val).__name__
538
+ symbols.append({
539
+ "name": name,
540
+ "type": t_name,
541
+ "is_callable": callable(val),
542
+ "is_function": is_fn,
543
+ "is_class": is_cls
544
+ })
545
+ except Exception:
546
+ symbols.append({"name": name, "type": "unknown", "is_callable": False})
547
+ return symbols
548
+
549
+ inspectSymbols = inspect_symbols
550
+ inspect_object = inspect_symbols
551
+ inspectObject = inspect_symbols
552
+ # __MINO_PREAMBLE_END__\n`;
553
+ const lines = code.split('\n');
554
+ const headerLines = [];
555
+ const restLines = [];
556
+ let inHeader = true;
557
+ for (const line of lines) {
558
+ const stripped = line.trim();
559
+ if (inHeader &&
560
+ (stripped.startsWith('#') ||
561
+ stripped.startsWith('from __future__') ||
562
+ stripped === '')) {
563
+ headerLines.push(line);
564
+ }
565
+ else {
566
+ inHeader = false;
567
+ restLines.push(line);
568
+ }
281
569
  }
282
- case 'cpp': {
283
- const binPath = scriptPath.replace(/\.cpp$/, binExt);
284
- return `g++ -std=c++17 "${scriptPath}" -o "${binPath}" && "${binPath}"`;
570
+ if (headerLines.length > 0) {
571
+ return headerLines.join('\n') + '\n' + preamble + restLines.join('\n');
285
572
  }
286
- case 'ruby':
287
- return `ruby "${scriptPath}"`;
288
- case 'php':
289
- return `php "${scriptPath}"`;
290
- case 'java':
291
- return `java "${scriptPath}"`;
292
- default:
293
- return null;
573
+ return preamble + code;
574
+ }
575
+ return code;
576
+ }
577
+ /**
578
+ * Parses universal structured result markers (__MINO_RESULT__) from stdout stream.
579
+ * Returns the cleaned human-readable stdout and the parsed structured result object/primitive.
580
+ *
581
+ * @param stdout - Captured standard output text.
582
+ */
583
+ export function parseStructuredResult(stdout) {
584
+ const resultRegex = /__MINO_RESULT__([\s\S]*?)__MINO_RESULT__/g;
585
+ let match;
586
+ let lastPayload;
587
+ while ((match = resultRegex.exec(stdout)) !== null) {
588
+ lastPayload = match[1].trim();
589
+ }
590
+ let structuredResult;
591
+ if (lastPayload !== undefined) {
592
+ try {
593
+ structuredResult = JSON.parse(lastPayload);
594
+ }
595
+ catch {
596
+ structuredResult = lastPayload;
597
+ }
598
+ }
599
+ const cleanedStdout = stdout
600
+ .replace(/__MINO_RESULT__[\s\S]*?__MINO_RESULT__/g, '')
601
+ .replace(/\n{3,}/g, '\n\n')
602
+ .trim();
603
+ return { cleanedStdout, structuredResult };
604
+ }
605
+ /**
606
+ * Diagnostic-first error middleware for sandbox script execution.
607
+ * Intercepts common sandbox execution traps (module resolution, ESM/CJS mismatch,
608
+ * Python import errors, TypeScript compile failures, linker errors) and produces
609
+ * actionable remediation advisories.
610
+ *
611
+ * @param stderr - Standard error output.
612
+ * @param stdout - Standard output text.
613
+ * @param language - Runtime language identifier.
614
+ * @param workspaceRoot - Path to workspace root directory.
615
+ * @returns Formatted diagnostic advisory string or null if no matching pattern.
616
+ */
617
+ export function diagnoseSandboxError(stderr, stdout, language, workspaceRoot) {
618
+ const combined = `${stderr}\n${stdout}`;
619
+ const diagnostics = [];
620
+ // 1. Module not found (Node/TypeScript)
621
+ if (/ERR_MODULE_NOT_FOUND|Cannot find module\s+'([^']+)'|Cannot find package\s+'([^']+)'/i.test(combined)) {
622
+ const match = combined.match(/Cannot find (?:module|package)\s+'([^']+)'/i);
623
+ const mod = match ? match[1] : 'the requested package';
624
+ diagnostics.push(`• Cause: Module resolution failure for '${mod}'.\n` +
625
+ `• Action: Ensure '${mod}' is installed in workspace node_modules (e.g. npm install ${mod}).\n` +
626
+ ` If referencing a local file, ensure relative path and extension (.js, .ts, .mjs) are correct.\n` +
627
+ ` Note: NODE_PATH is configured to search ${workspaceRoot ? path.join(workspaceRoot, 'node_modules') : 'workspace node_modules'}.`);
628
+ }
629
+ // 2. ESM / CommonJS mismatch
630
+ if (/ERR_REQUIRE_ESM|Must use import to load ES Module|require\(\) of ES Module/i.test(combined)) {
631
+ diagnostics.push(`• Cause: CommonJS require() attempted to load an ES Module.\n` +
632
+ `• Action: Use dynamic import(...) or standard 'import ... from ...' syntax with .mjs or tsx runtime.`);
633
+ }
634
+ else if (/Cannot use import statement outside a module/i.test(combined)) {
635
+ diagnostics.push(`• Cause: Static ES Module import used in CommonJS execution context.\n` +
636
+ `• Action: Rename script to use .mjs or execute with ts-node/tsx runtime.`);
637
+ }
638
+ // 3. Python ModuleNotFoundError / ImportError
639
+ if (/ModuleNotFoundError:\s*No module named\s+'([^']+)'|ImportError:\s*(?:cannot import name\s+'([^']+)'|No module named\s+'([^']+)')/i.test(combined)) {
640
+ const match = combined.match(/No module named\s+'([^']+)'|cannot import name\s+'([^']+)'/i);
641
+ const mod = match ? match[1] || match[2] : 'requested module';
642
+ diagnostics.push(`• Cause: Python module '${mod}' could not be imported.\n` +
643
+ `• Action: Verify that '${mod}' is installed in your virtual environment (.venv / venv) or requirements.txt.\n` +
644
+ ` Note: PYTHONPATH is pre-configured to include workspace root and src directory.`);
645
+ }
646
+ // 4. TypeScript compiler error
647
+ if (/error TS\d{4}:|TS\d{4}:/i.test(combined)) {
648
+ const tsCode = combined.match(/TS(\d{4})/i)?.[0] || 'TS Error';
649
+ diagnostics.push(`• Cause: TypeScript compiler reported an issue (${tsCode}).\n` +
650
+ `• Action: Verify type definitions and tsconfig.json path mappings.\n` +
651
+ ` For ephemeral debugging scripts, add '// @ts-nocheck' at the top of the script to bypass static type checks.`);
652
+ }
653
+ // 5. Rust crate / import errors
654
+ if (/error\[E0432\]|error\[E0463\]|can't find crate/i.test(combined)) {
655
+ diagnostics.push(`• Cause: Rust crate or unresolved import.\n` +
656
+ `• Action: Standalone rustc scripts only have access to std.\n` +
657
+ ` For external crates, compile via cargo or test against compiled workspace libraries.`);
658
+ }
659
+ // 6. Go package errors
660
+ if (/cannot find package\s+"([^"]+)"|no required module provides package/i.test(combined)) {
661
+ const match = combined.match(/cannot find package\s+"([^"]+)"/i);
662
+ const pkg = match ? match[1] : 'package';
663
+ diagnostics.push(`• Cause: Go package '${pkg}' not found in GOPATH/workspace module.\n` +
664
+ `• Action: Ensure '${pkg}' is declared in go.mod or run 'go get ${pkg}'.`);
665
+ }
666
+ // 7. C/C++ header / linker errors
667
+ if (/fatal error:\s*([^:]+):\s*No such file or directory/i.test(combined)) {
668
+ const match = combined.match(/fatal error:\s*([^:]+):\s*No such file or directory/i);
669
+ const hdr = match ? match[1] : 'header';
670
+ diagnostics.push(`• Cause: C/C++ header file '${hdr}' not found.\n` +
671
+ `• Action: Verify system library installation or specify include path.`);
672
+ }
673
+ else if (/undefined reference to|ld returned \d+ exit status/i.test(combined)) {
674
+ diagnostics.push(`• Cause: C/C++ linker unresolved symbol reference.\n` +
675
+ `• Action: Link required libraries (e.g., -lm, -lpthread, -lstdc++).`);
676
+ }
677
+ // 8. Timeout
678
+ if (/Execution timed out after \d+ms/i.test(combined)) {
679
+ diagnostics.push(`• Cause: Execution timed out.\n` +
680
+ `• Action: Increase timeoutMs in options, reduce iteration count, or check for infinite loops / blocking I/O.`);
681
+ }
682
+ if (diagnostics.length === 0)
683
+ return null;
684
+ return `[SANDBOX DIAGNOSTIC]\n` + diagnostics.join('\n\n');
685
+ }
686
+ /**
687
+ * Spawns a direct subprocess with dedicated FD-3 payload streaming,
688
+ * zero-shell overhead, and clean signal propagation.
689
+ */
690
+ function spawnSubprocess(executable, args, options) {
691
+ return new Promise((resolve) => {
692
+ let stdoutData = '';
693
+ let stderrData = '';
694
+ let fd3Data = '';
695
+ let isSettled = false;
696
+ let killedByTimeout = false;
697
+ let aborted = false;
698
+ let timer = null;
699
+ let killTimer = null;
700
+ // Windows compatibility for .cmd / .bat executables
701
+ const isWin = os.platform() === 'win32';
702
+ const useShell = isWin && (executable.endsWith('.cmd') || executable.endsWith('.bat'));
703
+ let child;
704
+ try {
705
+ child = spawn(executable, args, {
706
+ cwd: options.cwd,
707
+ env: options.env,
708
+ stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
709
+ shell: useShell,
710
+ windowsHide: true,
711
+ });
712
+ }
713
+ catch (err) {
714
+ resolve({
715
+ stdout: '',
716
+ stderr: err.message || String(err),
717
+ fd3: '',
718
+ exitCode: 1,
719
+ });
720
+ return;
721
+ }
722
+ const cleanupTimers = () => {
723
+ if (timer) {
724
+ clearTimeout(timer);
725
+ timer = null;
726
+ }
727
+ if (killTimer) {
728
+ clearTimeout(killTimer);
729
+ killTimer = null;
730
+ }
731
+ };
732
+ const abortHandler = () => {
733
+ aborted = true;
734
+ cleanupTimers();
735
+ try {
736
+ child.kill('SIGTERM');
737
+ }
738
+ catch {
739
+ // Ignore kill errors
740
+ }
741
+ killTimer = setTimeout(() => {
742
+ try {
743
+ child.kill('SIGKILL');
744
+ }
745
+ catch {
746
+ // Ignore
747
+ }
748
+ }, 500);
749
+ };
750
+ if (options.abortSignal) {
751
+ if (options.abortSignal.aborted) {
752
+ abortHandler();
753
+ }
754
+ else {
755
+ options.abortSignal.addEventListener('abort', abortHandler, { once: true });
756
+ }
757
+ }
758
+ if (options.timeoutMs > 0) {
759
+ timer = setTimeout(() => {
760
+ killedByTimeout = true;
761
+ try {
762
+ child.kill('SIGTERM');
763
+ }
764
+ catch {
765
+ // Ignore
766
+ }
767
+ killTimer = setTimeout(() => {
768
+ try {
769
+ child.kill('SIGKILL');
770
+ }
771
+ catch {
772
+ // Ignore
773
+ }
774
+ }, 1000);
775
+ }, options.timeoutMs);
776
+ }
777
+ if (child.stdout) {
778
+ child.stdout.setEncoding('utf-8');
779
+ child.stdout.on('data', (chunk) => {
780
+ if (stdoutData.length < options.maxOutputChars * 2) {
781
+ stdoutData += chunk;
782
+ }
783
+ });
784
+ }
785
+ if (child.stderr) {
786
+ child.stderr.setEncoding('utf-8');
787
+ child.stderr.on('data', (chunk) => {
788
+ if (stderrData.length < options.maxOutputChars * 2) {
789
+ stderrData += chunk;
790
+ }
791
+ });
792
+ }
793
+ const fd3Stream = child.stdio[3];
794
+ if (fd3Stream) {
795
+ fd3Stream.setEncoding('utf-8');
796
+ fd3Stream.on('data', (chunk) => {
797
+ fd3Data += chunk;
798
+ });
799
+ }
800
+ if (options.stdinContent !== undefined && child.stdin) {
801
+ child.stdin.on('error', () => {
802
+ // Avoid unhandled EPIPE if child process exits early
803
+ });
804
+ try {
805
+ child.stdin.write(options.stdinContent, 'utf-8');
806
+ child.stdin.end();
807
+ }
808
+ catch {
809
+ // Ignore write errors
810
+ }
811
+ }
812
+ else if (child.stdin) {
813
+ try {
814
+ child.stdin.end();
815
+ }
816
+ catch {
817
+ // Ignore
818
+ }
819
+ }
820
+ const finish = (code, signal) => {
821
+ if (isSettled)
822
+ return;
823
+ isSettled = true;
824
+ cleanupTimers();
825
+ if (options.abortSignal) {
826
+ options.abortSignal.removeEventListener('abort', abortHandler);
827
+ }
828
+ let exitCode = code ?? (signal ? 128 + 15 : 0);
829
+ if (killedByTimeout) {
830
+ exitCode = 124;
831
+ }
832
+ else if (aborted) {
833
+ exitCode = 130;
834
+ }
835
+ resolve({
836
+ stdout: stdoutData,
837
+ stderr: stderrData,
838
+ fd3: fd3Data,
839
+ exitCode,
840
+ killedByTimeout,
841
+ aborted,
842
+ });
843
+ };
844
+ child.on('error', (err) => {
845
+ stderrData += `\n${err.message}`;
846
+ finish(1, null);
847
+ });
848
+ child.on('close', (code, signal) => {
849
+ finish(code, signal);
850
+ });
851
+ });
852
+ }
853
+ /**
854
+ * Formats subprocess execution results, resolving structured results from FD-3 or stdout fallback.
855
+ */
856
+ function handleExecutionResult(res, normLang, workspaceRoot, options) {
857
+ if (res.aborted) {
858
+ return {
859
+ exitCode: 130,
860
+ stderr: 'Analysis script aborted by user.',
861
+ stdout: '',
862
+ };
863
+ }
864
+ if (res.killedByTimeout) {
865
+ const tMs = options.timeoutMs ?? 60_000;
866
+ const diag = diagnoseSandboxError(`Execution timed out after ${tMs}ms.`, '', normLang, workspaceRoot);
867
+ const errOut = diag
868
+ ? `Execution timed out after ${tMs}ms.\n\n${diag}`
869
+ : `Execution timed out after ${tMs}ms.`;
870
+ return {
871
+ exitCode: 124,
872
+ stderr: errOut,
873
+ stdout: '',
874
+ };
294
875
  }
876
+ const rawStdout = res.stdout.trim();
877
+ const rawStderr = res.stderr.trim();
878
+ // 1. Check FD-3 structured payload first
879
+ let structuredResult;
880
+ let hasFd3Payload = false;
881
+ if (res.fd3 && res.fd3.trim()) {
882
+ const lines = res.fd3.trim().split('\n').map((l) => l.trim()).filter(Boolean);
883
+ if (lines.length > 0) {
884
+ const lastPayload = lines[lines.length - 1];
885
+ try {
886
+ structuredResult = JSON.parse(lastPayload);
887
+ hasFd3Payload = true;
888
+ }
889
+ catch {
890
+ structuredResult = lastPayload;
891
+ hasFd3Payload = true;
892
+ }
893
+ }
894
+ }
895
+ // 2. Fallback to stdout regex extraction
896
+ const parsed = parseStructuredResult(rawStdout);
897
+ if (!hasFd3Payload && parsed.structuredResult !== undefined) {
898
+ structuredResult = parsed.structuredResult;
899
+ }
900
+ const cleanedStdout = parsed.cleanedStdout;
901
+ let finalStderr = rawStderr;
902
+ if (finalStderr) {
903
+ const diag = diagnoseSandboxError(finalStderr, rawStdout, normLang, workspaceRoot);
904
+ if (diag) {
905
+ finalStderr = `${finalStderr}\n\n${diag}`.trim();
906
+ }
907
+ }
908
+ return {
909
+ exitCode: res.exitCode,
910
+ stderr: truncateOutput(finalStderr, options.maxOutputChars),
911
+ stdout: truncateOutput(cleanedStdout || (structuredResult !== undefined ? '' : rawStdout), options.maxOutputChars),
912
+ structuredResult,
913
+ };
295
914
  }
296
915
  /**
297
916
  * Truncates a string to the specified maximum length, appending an ellipsis marker if truncated.
@@ -302,15 +921,15 @@ function truncateOutput(text, max) {
302
921
  return text.substring(0, max) + '\n... (output truncated)';
303
922
  }
304
923
  /**
305
- * Write a disposable analysis script to a temporary file, execute it using the
924
+ * Write a disposable analysis script or stream it directly via stdin, execute it using the
306
925
  * specified language runtime, capture its output, and guarantee cleanup.
307
926
  *
308
- * Scripts are written to `os.tmpdir()` never to the workspace directory to
309
- * prevent pollution, `.gitignore` conflicts, and interference with the user's project.
927
+ * For interpreted runtimes (Node, Python, Bash, Ruby, PHP), zero-disk stdin streaming is used
928
+ * to eliminate disk I/O, watcher churn, and temporary file artifacts.
310
929
  *
311
930
  * @param workspaceRoot - The workspace root, used as the `cwd` for script execution
312
931
  * so relative file paths in the script resolve correctly.
313
- * @param language - The runtime to use: "node", "ts-node", "python", "bash", "go", or "rust".
932
+ * @param language - The runtime to use: "node", "ts-node", "python", "bash", "go", "rust", etc.
314
933
  * @param code - The script source code to execute.
315
934
  * @param options - Optional timeout, output cap, and abort signal.
316
935
  * @returns Captured stdout, stderr, and exit code.
@@ -320,65 +939,250 @@ export async function runEphemeralScript(workspaceRoot, language, code, options)
320
939
  const maxOutputChars = options?.maxOutputChars ?? 100_000;
321
940
  const normLang = await resolveEffectiveRuntime(workspaceRoot, language, code);
322
941
  const ext = await resolveScriptExtension(workspaceRoot, normLang, code);
323
- const scriptPath = buildTempPath(code, ext);
324
- const cmd = buildCommand(normLang, scriptPath);
325
- if (!cmd) {
326
- return {
327
- stdout: '',
328
- stderr: `Unsupported language runtime: "${language}". Supported: ${SUPPORTED_LANGUAGES.join(', ')}`,
329
- exitCode: 1,
330
- };
942
+ const [tsRunnerInfo, pyEnvInfo, moduleType] = await Promise.all([
943
+ findTypeScriptRunner(workspaceRoot),
944
+ findPythonBinary(workspaceRoot),
945
+ detectWorkspaceModuleType(workspaceRoot),
946
+ ]);
947
+ const finalCode = options?.injectHelpers !== false
948
+ ? injectSandboxHelpers(code, normLang)
949
+ : code;
950
+ const mergedEnv = {
951
+ ...process.env,
952
+ ...options?.env,
953
+ };
954
+ // 1. NODE_PATH injection for workspace packages
955
+ const workspaceNodeModules = path.join(workspaceRoot, 'node_modules');
956
+ if (mergedEnv.NODE_PATH) {
957
+ mergedEnv.NODE_PATH = `${workspaceNodeModules}${path.delimiter}${mergedEnv.NODE_PATH}`;
958
+ }
959
+ else {
960
+ mergedEnv.NODE_PATH = workspaceNodeModules;
961
+ }
962
+ // 2. TypeScript configuration
963
+ if (tsRunnerInfo.tsconfigPath) {
964
+ mergedEnv.TS_NODE_PROJECT = tsRunnerInfo.tsconfigPath;
965
+ mergedEnv.TSCONFIG_PATH = tsRunnerInfo.tsconfigPath;
966
+ }
967
+ // 3. Python environment & PYTHONPATH injection
968
+ const workspaceSrc = path.join(workspaceRoot, 'src');
969
+ let hasSrc = false;
970
+ try {
971
+ await fs.access(workspaceSrc);
972
+ hasSrc = true;
331
973
  }
332
- // Collect all temp files created so we can clean them up unconditionally
333
- const tempFiles = [scriptPath];
334
- const binExt = os.platform() === 'win32' ? '.exe' : '';
335
- if (normLang === 'rust') {
336
- tempFiles.push(scriptPath.replace(/\.rs$/, binExt));
337
- tempFiles.push(scriptPath.replace(/\.rs$/, '.pdb'));
974
+ catch {
975
+ // no src dir
976
+ }
977
+ const pythonPaths = [workspaceRoot];
978
+ if (hasSrc) {
979
+ pythonPaths.push(workspaceSrc);
980
+ }
981
+ if (mergedEnv.PYTHONPATH) {
982
+ mergedEnv.PYTHONPATH = `${pythonPaths.join(path.delimiter)}${path.delimiter}${mergedEnv.PYTHONPATH}`;
338
983
  }
339
- else if (normLang === 'c') {
340
- tempFiles.push(scriptPath.replace(/\.c$/, binExt));
984
+ else {
985
+ mergedEnv.PYTHONPATH = pythonPaths.join(path.delimiter);
341
986
  }
342
- else if (normLang === 'cpp') {
343
- tempFiles.push(scriptPath.replace(/\.cpp$/, binExt));
987
+ mergedEnv.PYTHONUNBUFFERED = '1';
988
+ if (pyEnvInfo.isVenv && pyEnvInfo.venvDir && pyEnvInfo.binDir) {
989
+ mergedEnv.VIRTUAL_ENV = pyEnvInfo.venvDir;
990
+ mergedEnv.PATH = `${pyEnvInfo.binDir}${path.delimiter}${mergedEnv.PATH || ''}`;
344
991
  }
992
+ // 4. Ruby RUBYLIB injection
993
+ const workspaceLib = path.join(workspaceRoot, 'lib');
994
+ let hasLib = false;
345
995
  try {
346
- await fs.writeFile(scriptPath, code, 'utf-8');
347
- const mergedEnv = options?.env ? { ...process.env, ...options.env } : process.env;
348
- const { stdout, stderr } = await execAsync(cmd, {
349
- cwd: workspaceRoot,
350
- timeout: timeoutMs,
351
- maxBuffer: 1024 * 1024, // 1 MB buffer
352
- signal: options?.abortSignal,
353
- env: mergedEnv,
354
- });
355
- return {
356
- stdout: truncateOutput(stdout.trim(), maxOutputChars),
357
- stderr: truncateOutput(stderr.trim(), maxOutputChars),
358
- exitCode: 0,
359
- };
996
+ await fs.access(workspaceLib);
997
+ hasLib = true;
998
+ }
999
+ catch {
1000
+ // no lib dir
1001
+ }
1002
+ const rubyPaths = [workspaceRoot];
1003
+ if (hasLib) {
1004
+ rubyPaths.push(workspaceLib);
360
1005
  }
361
- catch (err) {
362
- // AbortError — propagate cleanly
363
- if (err.name === 'AbortError' || err.message?.includes('abort')) {
364
- return { stdout: '', stderr: 'Analysis script aborted by user.', exitCode: 130 };
1006
+ if (mergedEnv.RUBYLIB) {
1007
+ mergedEnv.RUBYLIB = `${rubyPaths.join(path.delimiter)}${path.delimiter}${mergedEnv.RUBYLIB}`;
1008
+ }
1009
+ else {
1010
+ mergedEnv.RUBYLIB = rubyPaths.join(path.delimiter);
1011
+ }
1012
+ const binExt = os.platform() === 'win32' ? '.exe' : '';
1013
+ const tempFiles = [];
1014
+ let executable = null;
1015
+ let args = [];
1016
+ let stdinContent;
1017
+ let isZeroDisk = false;
1018
+ switch (normLang) {
1019
+ case 'node': {
1020
+ executable = process.execPath || 'node';
1021
+ const hasCJS = /\b(require\s*\(|module\\.exports|exports\.)/.test(code);
1022
+ const hasESM = /\b(import\s+|import\(|export\s+|export\{|export\s+default)\b/.test(code) || /^\s*await\s+/m.test(code);
1023
+ const isESM = (hasESM && !hasCJS) || (!hasCJS && moduleType === 'module');
1024
+ args = isESM ? ['--input-type=module', '-'] : ['--input-type=commonjs', '-'];
1025
+ stdinContent = finalCode;
1026
+ isZeroDisk = true;
1027
+ break;
1028
+ }
1029
+ case 'python': {
1030
+ executable = pyEnvInfo.pythonBin;
1031
+ args = ['-u', '-'];
1032
+ stdinContent = finalCode;
1033
+ isZeroDisk = true;
1034
+ break;
1035
+ }
1036
+ case 'bash': {
1037
+ executable = 'bash';
1038
+ args = ['-s'];
1039
+ stdinContent = finalCode;
1040
+ isZeroDisk = true;
1041
+ break;
1042
+ }
1043
+ case 'ruby': {
1044
+ executable = 'ruby';
1045
+ args = ['-'];
1046
+ stdinContent = finalCode;
1047
+ isZeroDisk = true;
1048
+ break;
1049
+ }
1050
+ case 'php': {
1051
+ executable = 'php';
1052
+ args = [];
1053
+ stdinContent = finalCode.trim().startsWith('<?php') ? finalCode : `<?php\n${finalCode}`;
1054
+ isZeroDisk = true;
1055
+ break;
1056
+ }
1057
+ case 'ts-node': {
1058
+ const scriptPath = buildTempPath(code, ext);
1059
+ tempFiles.push(scriptPath);
1060
+ await fs.writeFile(scriptPath, finalCode, 'utf-8');
1061
+ if (tsRunnerInfo.runnerType === 'local-tsx') {
1062
+ executable = tsRunnerInfo.binaryPath || 'tsx';
1063
+ args = [scriptPath];
1064
+ }
1065
+ else if (tsRunnerInfo.runnerType === 'local-ts-node-esm') {
1066
+ executable = tsRunnerInfo.binaryPath || 'ts-node-esm';
1067
+ args = [scriptPath];
1068
+ }
1069
+ else if (tsRunnerInfo.runnerType === 'local-ts-node') {
1070
+ executable = tsRunnerInfo.binaryPath || 'ts-node';
1071
+ args = moduleType === 'module' ? ['--esm', scriptPath] : [scriptPath];
1072
+ }
1073
+ else if (tsRunnerInfo.runnerType === 'npx-ts-node') {
1074
+ executable = 'npx';
1075
+ args = ['ts-node', scriptPath];
1076
+ }
1077
+ else {
1078
+ executable = 'npx';
1079
+ args = ['tsx', scriptPath];
1080
+ }
1081
+ break;
1082
+ }
1083
+ case 'go': {
1084
+ const scriptPath = buildTempPath(code, ext);
1085
+ tempFiles.push(scriptPath);
1086
+ await fs.writeFile(scriptPath, finalCode, 'utf-8');
1087
+ executable = 'go';
1088
+ args = ['run', scriptPath];
1089
+ break;
1090
+ }
1091
+ case 'java': {
1092
+ const scriptPath = buildTempPath(code, ext);
1093
+ tempFiles.push(scriptPath);
1094
+ await fs.writeFile(scriptPath, finalCode, 'utf-8');
1095
+ executable = 'java';
1096
+ args = [scriptPath];
1097
+ break;
1098
+ }
1099
+ case 'rust': {
1100
+ const scriptPath = buildTempPath(code, ext);
1101
+ const binPath = scriptPath.replace(/\.rs$/, binExt);
1102
+ tempFiles.push(scriptPath, binPath, scriptPath.replace(/\.rs$/, '.pdb'));
1103
+ await fs.writeFile(scriptPath, finalCode, 'utf-8');
1104
+ // Compile step
1105
+ const compileRes = await spawnSubprocess('rustc', [scriptPath, '-o', binPath], {
1106
+ cwd: workspaceRoot,
1107
+ env: mergedEnv,
1108
+ timeoutMs: Math.min(timeoutMs, 30_000),
1109
+ maxOutputChars,
1110
+ abortSignal: options?.abortSignal,
1111
+ });
1112
+ if (compileRes.exitCode !== 0) {
1113
+ return handleExecutionResult(compileRes, normLang, workspaceRoot, { maxOutputChars, timeoutMs });
1114
+ }
1115
+ executable = binPath;
1116
+ args = [];
1117
+ break;
365
1118
  }
366
- if (err.killed && err.signal === 'SIGTERM') {
367
- return { stdout: '', stderr: `Execution timed out after ${timeoutMs}ms.`, exitCode: 124 };
1119
+ case 'c': {
1120
+ const scriptPath = buildTempPath(code, ext);
1121
+ const binPath = scriptPath.replace(/\.c$/, binExt);
1122
+ tempFiles.push(scriptPath, binPath);
1123
+ await fs.writeFile(scriptPath, finalCode, 'utf-8');
1124
+ // Compile step
1125
+ const compileRes = await spawnSubprocess('gcc', [scriptPath, '-o', binPath], {
1126
+ cwd: workspaceRoot,
1127
+ env: mergedEnv,
1128
+ timeoutMs: Math.min(timeoutMs, 30_000),
1129
+ maxOutputChars,
1130
+ abortSignal: options?.abortSignal,
1131
+ });
1132
+ if (compileRes.exitCode !== 0) {
1133
+ return handleExecutionResult(compileRes, normLang, workspaceRoot, { maxOutputChars, timeoutMs });
1134
+ }
1135
+ executable = binPath;
1136
+ args = [];
1137
+ break;
368
1138
  }
369
- const stdout = truncateOutput((err.stdout || '').trim(), maxOutputChars);
370
- const stderr = truncateOutput((err.stderr || '').trim(), maxOutputChars);
371
- const exitCode = typeof err.code === 'number' ? err.code : 1;
372
- return { stdout, stderr, exitCode };
1139
+ case 'cpp': {
1140
+ const scriptPath = buildTempPath(code, ext);
1141
+ const binPath = scriptPath.replace(/\.cpp$/, binExt);
1142
+ tempFiles.push(scriptPath, binPath);
1143
+ await fs.writeFile(scriptPath, finalCode, 'utf-8');
1144
+ // Compile step
1145
+ const compileRes = await spawnSubprocess('g++', ['-std=c++17', scriptPath, '-o', binPath], {
1146
+ cwd: workspaceRoot,
1147
+ env: mergedEnv,
1148
+ timeoutMs: Math.min(timeoutMs, 30_000),
1149
+ maxOutputChars,
1150
+ abortSignal: options?.abortSignal,
1151
+ });
1152
+ if (compileRes.exitCode !== 0) {
1153
+ return handleExecutionResult(compileRes, normLang, workspaceRoot, { maxOutputChars, timeoutMs });
1154
+ }
1155
+ executable = binPath;
1156
+ args = [];
1157
+ break;
1158
+ }
1159
+ default:
1160
+ return {
1161
+ stdout: '',
1162
+ stderr: `Unsupported language runtime: "${language}". Supported: ${SUPPORTED_LANGUAGES.join(', ')}`,
1163
+ exitCode: 1,
1164
+ };
1165
+ }
1166
+ try {
1167
+ const runRes = await spawnSubprocess(executable, args, {
1168
+ cwd: workspaceRoot,
1169
+ env: mergedEnv,
1170
+ timeoutMs,
1171
+ maxOutputChars,
1172
+ abortSignal: options?.abortSignal,
1173
+ stdinContent,
1174
+ });
1175
+ return handleExecutionResult(runRes, normLang, workspaceRoot, { maxOutputChars, timeoutMs });
373
1176
  }
374
1177
  finally {
375
- // Guarantee temp file cleanup regardless of success or failure
376
- for (const tmpFile of tempFiles) {
377
- try {
378
- await fs.rm(tmpFile, { force: true, recursive: true });
379
- }
380
- catch {
381
- // Ignore cleanup errors — temp files will be reaped by the OS eventually
1178
+ if (!isZeroDisk && tempFiles.length > 0) {
1179
+ for (const tmpFile of tempFiles) {
1180
+ try {
1181
+ await fs.rm(tmpFile, { force: true, recursive: true });
1182
+ }
1183
+ catch {
1184
+ // ignore cleanup errors
1185
+ }
382
1186
  }
383
1187
  }
384
1188
  }
@@ -486,7 +1290,8 @@ export async function runPropertyBasedTest(workspaceRoot, code, language = 'node
486
1290
  }
487
1291
  const scriptResult = await runEphemeralScript(workspaceRoot, lang, code, opts);
488
1292
  const combinedOutput = `${scriptResult.stdout}\n${scriptResult.stderr}`;
489
- const isPassed = combinedOutput.includes('[PBT PASSED]') || (scriptResult.exitCode === 0 && !combinedOutput.includes('[PBT FAILED]'));
1293
+ const isPassed = combinedOutput.includes('[PBT PASSED]') ||
1294
+ (scriptResult.exitCode === 0 && !combinedOutput.includes('[PBT FAILED]'));
490
1295
  let counterexample;
491
1296
  let shrunkInput;
492
1297
  let seed;
@@ -504,20 +1309,20 @@ export async function runPropertyBasedTest(workspaceRoot, code, language = 'node
504
1309
  // Extract seed
505
1310
  const seedMatch = combinedOutput.match(/Seed(?:|\/Iteration):\s*(\d+)/i);
506
1311
  if (seedMatch) {
507
- seed = parseInt(seedMatch[1], 10);
1312
+ seed = Number.parseInt(seedMatch[1], 10);
508
1313
  }
509
1314
  // Extract completed runs
510
1315
  const runsMatch = combinedOutput.match(/Completed\s+(\d+)\s+runs/i);
511
1316
  if (runsMatch) {
512
- numRunsCompleted = parseInt(runsMatch[1], 10);
1317
+ numRunsCompleted = Number.parseInt(runsMatch[1], 10);
513
1318
  }
514
1319
  return {
515
1320
  ...scriptResult,
516
- passed: isPassed,
517
1321
  counterexample,
518
- shrunkInput,
519
- seed,
520
1322
  numRunsCompleted,
1323
+ passed: isPassed,
1324
+ seed,
1325
+ shrunkInput,
521
1326
  };
522
1327
  }
523
1328
  /**
@@ -710,28 +1515,31 @@ export async function runFuzzProbe(workspaceRoot, code, language = 'node', confi
710
1515
  failingInputs.push(match[1].trim());
711
1516
  }
712
1517
  let totalFuzzRuns = 0;
713
- const runsMatch = combinedOutput.match(/(?:Total Runs|Runs):\s*(\d+)/i) || combinedOutput.match(/Completed\s+(\d+)\s+(?:fuzz\s+)?runs/i);
1518
+ const runsMatch = combinedOutput.match(/(?:Total Runs|Runs):\s*(\d+)/i) ||
1519
+ combinedOutput.match(/Completed\s+(\d+)\s+(?:fuzz\s+)?runs/i);
714
1520
  if (runsMatch) {
715
- totalFuzzRuns = parseInt(runsMatch[1], 10);
1521
+ totalFuzzRuns = Number.parseInt(runsMatch[1], 10);
716
1522
  }
717
1523
  let seed;
718
1524
  const seedMatch = combinedOutput.match(/Seed:\s*(\d+)/i);
719
1525
  if (seedMatch) {
720
- seed = parseInt(seedMatch[1], 10);
1526
+ seed = Number.parseInt(seedMatch[1], 10);
721
1527
  }
722
1528
  let errorSummary;
723
1529
  if (!isPassed) {
724
1530
  const errLine = scriptResult.stderr.split('\n').find((l) => l.trim().length > 0) ||
725
- combinedOutput.split('\n').find((l) => l.toLowerCase().includes('error') || l.includes('[FUZZ FAILED]'));
1531
+ combinedOutput
1532
+ .split('\n')
1533
+ .find((l) => l.toLowerCase().includes('error') || l.includes('[FUZZ FAILED]'));
726
1534
  errorSummary = errLine?.trim() || 'Fuzzing probe encountered unexpected failure or crash.';
727
1535
  }
728
1536
  return {
729
1537
  ...scriptResult,
730
- passed: isPassed,
731
- totalFuzzRuns,
732
- failingInputs: failingInputs.length > 0 ? failingInputs : undefined,
733
1538
  errorSummary,
1539
+ failingInputs: failingInputs.length > 0 ? failingInputs : undefined,
1540
+ passed: isPassed,
734
1541
  seed,
1542
+ totalFuzzRuns,
735
1543
  };
736
1544
  }
737
1545
  /**
@@ -837,7 +1645,8 @@ const testIters = ${testIters};
837
1645
  for (let i = 0; i < testIters; i++) {
838
1646
  if (i % Math.max(1, Math.floor(testIters / 5)) === 0) {
839
1647
  if (global.gc) global.gc();
840
- console.log(\`[HEAP_SAMPLE] \${i}, \${process.memoryUsage().heapUsed}\`);
1648
+ const curr = process.memoryUsage().heapUsed;
1649
+ console.log(\`[HEAP_SAMPLE] \${i}, \${curr}\`);
841
1650
  }
842
1651
  }
843
1652
 
@@ -866,26 +1675,26 @@ export async function checkHeapDelta(workspaceRoot, code, language = 'node', con
866
1675
  let initialHeapBytes = 0;
867
1676
  const startMatch = combinedOutput.match(/(?:\[HEAP_START\]|Heap start:)\s*(-?\d+)/i);
868
1677
  if (startMatch) {
869
- initialHeapBytes = parseInt(startMatch[1], 10);
1678
+ initialHeapBytes = Number.parseInt(startMatch[1], 10);
870
1679
  }
871
1680
  let finalHeapBytes = 0;
872
1681
  const finalMatch = combinedOutput.match(/(?:\[HEAP_FINAL\]|Heap final:)\s*(-?\d+)/i);
873
1682
  if (finalMatch) {
874
- finalHeapBytes = parseInt(finalMatch[1], 10);
1683
+ finalHeapBytes = Number.parseInt(finalMatch[1], 10);
875
1684
  }
876
1685
  let deltaBytes = finalHeapBytes - initialHeapBytes;
877
1686
  const deltaMatch = combinedOutput.match(/(?:\[HEAP_DELTA\]|Heap delta:)\s*(-?\d+)/i);
878
1687
  if (deltaMatch) {
879
- deltaBytes = parseInt(deltaMatch[1], 10);
1688
+ deltaBytes = Number.parseInt(deltaMatch[1], 10);
880
1689
  }
881
1690
  const samples = [];
882
1691
  const sampleRegex = /(?:\[HEAP_SAMPLE\]|Sample\s+(\d+):?)\s*(\d+)(?:,\s*(\d+))?/gi;
883
1692
  let sMatch;
884
1693
  while ((sMatch = sampleRegex.exec(combinedOutput)) !== null) {
885
- const iter = parseInt(sMatch[1], 10);
886
- const bytes = sMatch[3] ? parseInt(sMatch[3], 10) : parseInt(sMatch[2], 10);
887
- if (!isNaN(iter) && !isNaN(bytes)) {
888
- samples.push({ iteration: iter, heapBytes: bytes });
1694
+ const iter = Number.parseInt(sMatch[1], 10);
1695
+ const bytes = sMatch[3] ? Number.parseInt(sMatch[3], 10) : Number.parseInt(sMatch[2], 10);
1696
+ if (!Number.isNaN(iter) && !Number.isNaN(bytes)) {
1697
+ samples.push({ heapBytes: bytes, iteration: iter });
889
1698
  }
890
1699
  }
891
1700
  let growthRateBytesPerIter;
@@ -905,11 +1714,11 @@ export async function checkHeapDelta(workspaceRoot, code, language = 'node', con
905
1714
  (growthRateBytesPerIter !== undefined && growthRateBytesPerIter > 10_000);
906
1715
  return {
907
1716
  ...scriptResult,
908
- initialHeapBytes,
909
- finalHeapBytes,
910
1717
  deltaBytes,
911
- leakDetected,
1718
+ finalHeapBytes,
912
1719
  growthRateBytesPerIter,
1720
+ initialHeapBytes,
1721
+ leakDetected,
913
1722
  samples: samples.length > 0 ? samples : undefined,
914
1723
  };
915
1724
  }
@@ -922,30 +1731,30 @@ function compareJsonValues(val1, val2, pathStr, tolerance, details) {
922
1731
  if (typeof val1 === 'number' && typeof val2 === 'number') {
923
1732
  if (Math.abs(val1 - val2) > tolerance) {
924
1733
  details.push({
925
- field: pathStr,
926
1734
  baseline: val1,
927
1735
  candidate: val2,
928
1736
  diff: `Numeric difference exceeds tolerance (${tolerance}): |${val1} - ${val2}| = ${Math.abs(val1 - val2)}`,
1737
+ field: pathStr,
929
1738
  });
930
1739
  }
931
1740
  return;
932
1741
  }
933
1742
  if (typeof val1 !== typeof val2 || val1 === null || val2 === null) {
934
1743
  details.push({
935
- field: pathStr,
936
1744
  baseline: val1,
937
1745
  candidate: val2,
938
1746
  diff: `Type mismatch: baseline is ${typeof val1}, candidate is ${typeof val2}`,
1747
+ field: pathStr,
939
1748
  });
940
1749
  return;
941
1750
  }
942
1751
  if (Array.isArray(val1) && Array.isArray(val2)) {
943
1752
  if (val1.length !== val2.length) {
944
1753
  details.push({
945
- field: `${pathStr}.length`,
946
1754
  baseline: val1.length,
947
1755
  candidate: val2.length,
948
1756
  diff: `Array length mismatch: baseline length ${val1.length}, candidate length ${val2.length}`,
1757
+ field: `${pathStr}.length`,
949
1758
  });
950
1759
  }
951
1760
  const minLen = Math.min(val1.length, val2.length);
@@ -962,18 +1771,18 @@ function compareJsonValues(val1, val2, pathStr, tolerance, details) {
962
1771
  const subPath = pathStr ? `${pathStr}.${key}` : key;
963
1772
  if (!(key in val1)) {
964
1773
  details.push({
965
- field: subPath,
966
1774
  baseline: undefined,
967
1775
  candidate: val2[key],
968
1776
  diff: `Key "${key}" missing in baseline`,
1777
+ field: subPath,
969
1778
  });
970
1779
  }
971
1780
  else if (!(key in val2)) {
972
1781
  details.push({
973
- field: subPath,
974
1782
  baseline: val1[key],
975
1783
  candidate: undefined,
976
1784
  diff: `Key "${key}" missing in candidate`,
1785
+ field: subPath,
977
1786
  });
978
1787
  }
979
1788
  else {
@@ -983,10 +1792,10 @@ function compareJsonValues(val1, val2, pathStr, tolerance, details) {
983
1792
  return;
984
1793
  }
985
1794
  details.push({
986
- field: pathStr,
987
1795
  baseline: val1,
988
1796
  candidate: val2,
989
1797
  diff: `Value mismatch: baseline="${val1}", candidate="${val2}"`,
1798
+ field: pathStr,
990
1799
  });
991
1800
  }
992
1801
  /**
@@ -1057,10 +1866,10 @@ export async function checkBehavioralDrift(workspaceRoot, baselineCode, candidat
1057
1866
  }
1058
1867
  if (baselineResult.exitCode !== candidateResult.exitCode) {
1059
1868
  driftDetails.push({
1060
- field: 'exitCode',
1061
1869
  baseline: baselineResult.exitCode,
1062
1870
  candidate: candidateResult.exitCode,
1063
1871
  diff: `Exit code mismatch: baseline exited with ${baselineResult.exitCode}, candidate with ${candidateResult.exitCode}`,
1872
+ field: 'exitCode',
1064
1873
  });
1065
1874
  }
1066
1875
  const hasDrift = driftDetails.length > 0;
@@ -1068,13 +1877,25 @@ export async function checkBehavioralDrift(workspaceRoot, baselineCode, candidat
1068
1877
  const combinedStdout = `--- Baseline Stdout ---\n${baselineResult.stdout}\n--- Candidate Stdout ---\n${candidateResult.stdout}`;
1069
1878
  const combinedStderr = `--- Baseline Stderr ---\n${baselineResult.stderr}\n--- Candidate Stderr ---\n${candidateResult.stderr}`;
1070
1879
  return {
1071
- stdout: combinedStdout,
1072
- stderr: combinedStderr,
1073
- exitCode: hasDrift ? 1 : 0,
1074
- hasDrift,
1075
1880
  baselineOutput: baselineOut,
1076
1881
  candidateOutput: candidateOut,
1077
1882
  diffSummary,
1078
1883
  driftDetails: driftDetails.length > 0 ? driftDetails : undefined,
1884
+ exitCode: hasDrift ? 1 : 0,
1885
+ hasDrift,
1886
+ stderr: combinedStderr,
1887
+ stdout: combinedStdout,
1079
1888
  };
1080
1889
  }
1890
+ /**
1891
+ * Runs a heap delta memory profiling script and evaluates memory growth characteristics.
1892
+ */
1893
+ export async function runHeapDeltaCheck(workspaceRoot, code, language = 'node', config) {
1894
+ return checkHeapDelta(workspaceRoot, code, language, config);
1895
+ }
1896
+ /**
1897
+ * Runs two scripts (baseline vs candidate) and compares output for behavioral drift.
1898
+ */
1899
+ export async function runBehavioralDriftCheck(workspaceRoot, baselineCode, candidateCode, language = 'node', config) {
1900
+ return checkBehavioralDrift(workspaceRoot, baselineCode, candidateCode, language, config);
1901
+ }