minovative-mind-cli 2.7.0 → 2.8.1

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.
package/README.md CHANGED
@@ -165,7 +165,7 @@ _⚠️\*Support is limited or requires custom local system tooling/environment
165
165
 
166
166
  1. **Dependency Tracing:** Fully supported across language families. It uses lightning-fast static regex pattern matching to resolve local import structures and determine the "blast radius" of code changes without requiring local compilations.
167
167
  2. **Performance Auditing:** Executed natively across 9 major language families (JavaScript/TypeScript, Python, Go, Rust, PHP, C#, Java, C/C++, and Ruby). It uses zero-dependency, ultra-fast (<50ms) language-aware regex heuristics with comment/string stripping and line-preserving offset tracking to detect severe runtime anti-patterns (such as unbounded loops, synchronous I/O, chained array allocations, unnecessary allocations, and unclosed resources).
168
- 3. **Ephemeral Analysis & Property-Based Probing:** Runs temporary files in sandbox directories to parse and extract local symbols, execute ephemeral Property-Based Testing (PBT) probes to discover and shrink counterexamples, perform infrastructure probing (detecting available runtimes, checking ports, identifying project type), post-change validation (importing modified modules and asserting expected behavior), and lightweight ML-powered analysis (e.g., TF-IDF file ranking, anomaly detection, Big-O estimation). Defaults to Node.js as a safe baseline, but natively leverages host-installed runtimes (Python, Rust, Go compilers) and standard libraries whenever available in the workspace.
168
+ 3. **Ephemeral Analysis & Property-Based Probing:** Defaults to Node.js as a safe baseline, but natively leverages host-installed runtimes (Python, Rust, Go compilers) and standard libraries whenever available in the workspace.
169
169
 
170
170
  ---
171
171
 
@@ -223,20 +223,27 @@ export async function handleSlashCommand(command, context) {
223
223
  try {
224
224
  const { getInvestigationCacheStats } = await import('../orchestration/investigationCache.js');
225
225
  const invStats = getInvestigationCacheStats(workspaceRoot);
226
- const memBankSizeKB = (invStats.sizeBytes / 1024).toFixed(1);
227
- console.log(`${pc.bold('Memory Bank:')} ${pc.cyan(`${invStats.entries} cached investigations (${memBankSizeKB} KB / 5 MB)`)}`);
226
+ const formatBytesToSize = (bytes) => {
227
+ if (bytes >= 1024 * 1024) {
228
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
229
+ }
230
+ return `${(bytes / 1024).toFixed(1)} KB`;
231
+ };
232
+ const memBankSizeStr = formatBytesToSize(invStats.sizeBytes);
233
+ console.log(`${pc.bold('Memory Bank:')} ${pc.cyan(`${invStats.entries} cached investigations (${memBankSizeStr} / 5 MB)`)}`);
228
234
  const { readCache } = await import('../../utils/projectStorage.js');
229
235
  const contextCache = readCache(workspaceRoot, 'context_cache.json');
230
236
  const contextEntries = contextCache ? Object.keys(contextCache).length : 0;
231
- let contextSizeKB = '0.0';
237
+ let contextSizeBytes = 0;
232
238
  try {
233
239
  const fs = await import('node:fs');
234
240
  const path = await import('node:path');
235
241
  const stat = await fs.promises.stat(path.join(workspaceRoot, '.minovativemind', 'context_cache.json'));
236
- contextSizeKB = (stat.size / 1024).toFixed(1);
242
+ contextSizeBytes = stat.size;
237
243
  }
238
244
  catch (e) { }
239
- console.log(`${pc.bold('Context Cache:')} ${pc.cyan(`${contextEntries} file summaries (${contextSizeKB} KB / 5 MB)`)}`);
245
+ const contextSizeStr = formatBytesToSize(contextSizeBytes);
246
+ console.log(`${pc.bold('Context Cache:')} ${pc.cyan(`${contextEntries} file summaries (${contextSizeStr} / 5 MB)`)}`);
240
247
  }
241
248
  catch (e) {
242
249
  // Ignored if cache stats fail
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import * as p from '@clack/prompts';
2
3
  import pc from 'picocolors';
3
4
  import { debugLog } from '../../utils/logger.js';
@@ -24,6 +25,9 @@ const TOOL_ICONS = {
24
25
  find_dependencies: '🔗',
25
26
  run_analysis_script: '🔬',
26
27
  run_debug_script: '🧪',
28
+ run_fuzz_probe: '🎯',
29
+ check_heap_delta: '📊',
30
+ check_behavioral_drift: '🔀',
27
31
  };
28
32
  /**
29
33
  * Human-readable translations for tool actions.
@@ -41,6 +45,9 @@ const TOOL_LABELS = {
41
45
  find_dependencies: 'Tracing dependencies',
42
46
  run_analysis_script: 'Analyzing workspace',
43
47
  run_debug_script: 'Executing debug/validation script',
48
+ run_fuzz_probe: 'Executing fuzz testing probe',
49
+ check_heap_delta: 'Inspecting memory heap delta',
50
+ check_behavioral_drift: 'Comparing behavioral drift',
44
51
  };
45
52
  // ─── Helpers ─────────────────────────────────────────────────────────
46
53
  /**
@@ -52,8 +59,20 @@ const TOOL_LABELS = {
52
59
  * @returns A fully colorized and formatted ANSI console string.
53
60
  */
54
61
  export function formatToolCall(name, args) {
55
- const icon = TOOL_ICONS[name] ?? '🔧';
56
- const label = TOOL_LABELS[name] ?? name;
62
+ let icon = TOOL_ICONS[name] ?? '🔧';
63
+ let label = TOOL_LABELS[name] ?? name;
64
+ if (name === 'rename_file') {
65
+ const src = String(args.sourcePath ?? '');
66
+ const tgt = String(args.targetPath ?? '');
67
+ if (src && tgt && path.dirname(src) === path.dirname(tgt)) {
68
+ label = 'Renaming file';
69
+ icon = '🏷️';
70
+ }
71
+ else {
72
+ label = 'Moving file';
73
+ icon = '🚚';
74
+ }
75
+ }
57
76
  const formatPath = (path) => pc.cyan(path);
58
77
  const argsMap = {
59
78
  read_file: () => {
@@ -86,6 +105,37 @@ export function formatToolCall(name, args) {
86
105
  find_dependencies: () => `: ${formatPath(String(args.filePath))}${args.direction ? ` (${args.direction})` : ''}`,
87
106
  run_analysis_script: () => `: ${formatPath(String(args.targetFile ?? 'workspace'))}`,
88
107
  run_debug_script: () => `: ${pc.dim(`(${String(args.language ?? 'script')})`)}`,
108
+ run_fuzz_probe: () => {
109
+ const lang = String(args.language ?? 'node');
110
+ const extras = [];
111
+ if (args.iterations !== undefined)
112
+ extras.push(`${args.iterations} iterations`);
113
+ if (args.seed !== undefined)
114
+ extras.push(`seed: ${args.seed}`);
115
+ const meta = extras.length > 0 ? `, ${extras.join(', ')}` : '';
116
+ return `: ${pc.dim(`(${lang}${meta})`)}`;
117
+ },
118
+ check_heap_delta: () => {
119
+ const lang = String(args.language ?? 'node');
120
+ const extras = [];
121
+ if (args.testIterations !== undefined)
122
+ extras.push(`${args.testIterations} iterations`);
123
+ if (args.samplingIntervalMs !== undefined)
124
+ extras.push(`${args.samplingIntervalMs}ms interval`);
125
+ const meta = extras.length > 0 ? `, ${extras.join(', ')}` : '';
126
+ return `: ${pc.dim(`(${lang}${meta})`)}`;
127
+ },
128
+ check_behavioral_drift: () => {
129
+ const lang = String(args.language ?? 'node');
130
+ const extras = [];
131
+ if (args.tolerance !== undefined)
132
+ extras.push(`tol: ${args.tolerance}`);
133
+ if (Array.isArray(args.compareKeys) && args.compareKeys.length > 0) {
134
+ extras.push(`keys: ${args.compareKeys.join(', ')}`);
135
+ }
136
+ const meta = extras.length > 0 ? `, ${extras.join(', ')}` : '';
137
+ return `: ${pc.dim(`(${lang}${meta})`)}`;
138
+ },
89
139
  };
90
140
  const formatter = argsMap[name];
91
141
  const details = formatter ? formatter() : '';
@@ -183,6 +183,32 @@ export declare function findRecentChanges(workspaceRoot: string, dirPath?: strin
183
183
  * @returns A promise resolving to a {@link ToolResult} containing execution output.
184
184
  */
185
185
  export declare function runDebugScript(workspaceRoot: string, language: string, code: string, abortSignal?: AbortSignal): Promise<ToolResult>;
186
+ /**
187
+ * Executes a fuzz probing analysis script and formats the resulting pass/fail and crash statistics.
188
+ */
189
+ export declare function executeFuzzProbe(workspaceRoot: string, code: string, language?: string, config?: {
190
+ iterations?: number;
191
+ maxInputLength?: number;
192
+ seed?: number;
193
+ timeoutMs?: number;
194
+ }, abortSignal?: AbortSignal): Promise<ToolResult>;
195
+ /**
196
+ * Executes a heap memory inspection script and formats memory consumption and growth delta metrics.
197
+ */
198
+ export declare function executeHeapDelta(workspaceRoot: string, code: string, language?: string, config?: {
199
+ warmupIterations?: number;
200
+ testIterations?: number;
201
+ samplingIntervalMs?: number;
202
+ timeoutMs?: number;
203
+ }, abortSignal?: AbortSignal): Promise<ToolResult>;
204
+ /**
205
+ * Executes baseline and candidate scripts, returning output diffs and behavioral drift findings.
206
+ */
207
+ export declare function executeBehavioralDrift(workspaceRoot: string, baselineCode: string, candidateCode: string, language?: string, config?: {
208
+ tolerance?: number;
209
+ compareKeys?: string[];
210
+ timeoutMs?: number;
211
+ }, abortSignal?: AbortSignal): Promise<ToolResult>;
186
212
  /**
187
213
  * Central tool dispatcher that resolves multi-workspace paths, executes the requested tool
188
214
  * with the provided arguments, records metrics, and returns the standardized {@link ToolResult}.
@@ -24,6 +24,7 @@ import { extractSymbols } from '../utils/symbolExtractor.js';
24
24
  import { getMetricCollector } from './metrics.js';
25
25
  import { getCurrentAgentId } from '../utils/asyncContext.js';
26
26
  import { recordFileRead, hasAgentReadFile } from '../utils/fileReadGuard.js';
27
+ import { runFuzzProbe, checkHeapDelta as runCheckHeapDelta, checkBehavioralDrift as runCheckBehavioralDrift, } from '../utils/analysisRunner.js';
27
28
  import ignore from 'ignore';
28
29
  const execAsync = promisify(exec);
29
30
  // ─── Tool Declarations for Gemini Function Calling ───────────────────
@@ -341,6 +342,109 @@ export const toolDeclarations = [
341
342
  required: ['summary'],
342
343
  },
343
344
  },
345
+ {
346
+ name: 'run_fuzz_probe',
347
+ description: 'Execute a fuzz testing probe script to detect edge-case failures, unhandled exceptions, or unexpected crashes under randomized or boundary inputs.',
348
+ parameters: {
349
+ type: SchemaType.OBJECT,
350
+ properties: {
351
+ code: {
352
+ type: SchemaType.STRING,
353
+ description: 'The fuzz test script or probe code to execute.',
354
+ },
355
+ language: {
356
+ type: SchemaType.STRING,
357
+ description: 'Runtime environment: "node", "ts-node", "python", "bash", "go", or "rust". Defaults to "node".',
358
+ },
359
+ iterations: {
360
+ type: SchemaType.NUMBER,
361
+ description: 'Optional. Number of fuzz test iterations to run.',
362
+ },
363
+ maxInputLength: {
364
+ type: SchemaType.NUMBER,
365
+ description: 'Optional. Maximum length/size for generated fuzz input vectors.',
366
+ },
367
+ seed: {
368
+ type: SchemaType.NUMBER,
369
+ description: 'Optional. Random seed for deterministic fuzz generation.',
370
+ },
371
+ timeoutMs: {
372
+ type: SchemaType.NUMBER,
373
+ description: 'Optional. Execution timeout in milliseconds.',
374
+ },
375
+ },
376
+ required: ['code'],
377
+ },
378
+ },
379
+ {
380
+ name: 'check_heap_delta',
381
+ description: 'Execute a heap memory inspection script to measure memory consumption, detect heap growth/leaks, and evaluate memory delta statistics.',
382
+ parameters: {
383
+ type: SchemaType.OBJECT,
384
+ properties: {
385
+ code: {
386
+ type: SchemaType.STRING,
387
+ description: 'The script or code snippet to execute for heap memory analysis.',
388
+ },
389
+ language: {
390
+ type: SchemaType.STRING,
391
+ description: 'Runtime environment: "node", "ts-node", "python", "bash", "go", or "rust". Defaults to "node".',
392
+ },
393
+ warmupIterations: {
394
+ type: SchemaType.NUMBER,
395
+ description: 'Optional. Number of warmup iterations prior to initial heap snapshot.',
396
+ },
397
+ testIterations: {
398
+ type: SchemaType.NUMBER,
399
+ description: 'Optional. Number of test iterations executed between snapshots.',
400
+ },
401
+ samplingIntervalMs: {
402
+ type: SchemaType.NUMBER,
403
+ description: 'Optional. Memory sampling interval in milliseconds.',
404
+ },
405
+ timeoutMs: {
406
+ type: SchemaType.NUMBER,
407
+ description: 'Optional. Execution timeout in milliseconds.',
408
+ },
409
+ },
410
+ required: ['code'],
411
+ },
412
+ },
413
+ {
414
+ name: 'check_behavioral_drift',
415
+ description: 'Execute baseline and candidate scripts in parallel or sequence to detect output differences, return value mismatches, or behavioral drift.',
416
+ parameters: {
417
+ type: SchemaType.OBJECT,
418
+ properties: {
419
+ baselineCode: {
420
+ type: SchemaType.STRING,
421
+ description: 'The baseline implementation script source code.',
422
+ },
423
+ candidateCode: {
424
+ type: SchemaType.STRING,
425
+ description: 'The candidate implementation script source code to compare against baseline.',
426
+ },
427
+ language: {
428
+ type: SchemaType.STRING,
429
+ description: 'Runtime environment: "node", "ts-node", "python", "bash", "go", or "rust". Defaults to "node".',
430
+ },
431
+ tolerance: {
432
+ type: SchemaType.NUMBER,
433
+ description: 'Optional. Numerical tolerance threshold for floating point comparison.',
434
+ },
435
+ compareKeys: {
436
+ type: SchemaType.ARRAY,
437
+ items: { type: SchemaType.STRING },
438
+ description: 'Optional. Specific JSON object keys/fields to compare for drift.',
439
+ },
440
+ timeoutMs: {
441
+ type: SchemaType.NUMBER,
442
+ description: 'Optional. Execution timeout in milliseconds.',
443
+ },
444
+ },
445
+ required: ['baselineCode', 'candidateCode'],
446
+ },
447
+ },
344
448
  ];
345
449
  let currentApprovalMode = 'ask';
346
450
  /**
@@ -1312,6 +1416,112 @@ export async function runDebugScript(workspaceRoot, language, code, abortSignal)
1312
1416
  }
1313
1417
  }
1314
1418
  }
1419
+ /**
1420
+ * Executes a fuzz probing analysis script and formats the resulting pass/fail and crash statistics.
1421
+ */
1422
+ export async function executeFuzzProbe(workspaceRoot, code, language = 'node', config, abortSignal) {
1423
+ try {
1424
+ const res = await runFuzzProbe(workspaceRoot, code, language, {
1425
+ ...config,
1426
+ abortSignal,
1427
+ });
1428
+ let output = `Fuzz Probe Result: ${res.passed ? 'PASSED' : 'FAILED'}\n`;
1429
+ if (res.totalFuzzRuns > 0)
1430
+ output += `Total Fuzz Runs: ${res.totalFuzzRuns}\n`;
1431
+ if (res.seed !== undefined)
1432
+ output += `Seed: ${res.seed}\n`;
1433
+ if (res.failingInputs && res.failingInputs.length > 0) {
1434
+ output += `Failing Inputs / Counterexamples:\n${res.failingInputs.map((inp) => ` - ${inp}`).join('\n')}\n`;
1435
+ }
1436
+ if (res.errorSummary) {
1437
+ output += `Error Summary:\n${res.errorSummary}\n`;
1438
+ }
1439
+ if (res.stdout)
1440
+ output += `\n[STDOUT]\n${res.stdout}\n`;
1441
+ if (res.stderr)
1442
+ output += `\n[STDERR]\n${res.stderr}\n`;
1443
+ return {
1444
+ output: output.trim(),
1445
+ ...(res.exitCode !== 0 && !res.passed ? { error: `Fuzz probe failed with exit code ${res.exitCode}` } : {}),
1446
+ };
1447
+ }
1448
+ catch (err) {
1449
+ return { output: '', error: `Fuzz probe error: ${err.message || String(err)}` };
1450
+ }
1451
+ }
1452
+ /**
1453
+ * Executes a heap memory inspection script and formats memory consumption and growth delta metrics.
1454
+ */
1455
+ export async function executeHeapDelta(workspaceRoot, code, language = 'node', config, abortSignal) {
1456
+ try {
1457
+ const res = await runCheckHeapDelta(workspaceRoot, code, language, {
1458
+ ...config,
1459
+ abortSignal,
1460
+ });
1461
+ let output = `Heap Delta Analysis Result:\n`;
1462
+ output += `Initial Heap: ${res.initialHeapBytes} bytes\n`;
1463
+ output += `Final Heap: ${res.finalHeapBytes} bytes\n`;
1464
+ output += `Delta: ${res.deltaBytes} bytes\n`;
1465
+ output += `Leak Detected: ${res.leakDetected ? 'YES' : 'NO'}\n`;
1466
+ if (res.growthRateBytesPerIter !== undefined) {
1467
+ output += `Growth Rate: ${res.growthRateBytesPerIter} bytes/iter\n`;
1468
+ }
1469
+ if (res.samples && res.samples.length > 0) {
1470
+ output += `Samples (${res.samples.length}):\n`;
1471
+ for (const s of res.samples) {
1472
+ output += ` Iter ${s.iteration}: ${s.heapBytes} bytes\n`;
1473
+ }
1474
+ }
1475
+ if (res.stdout)
1476
+ output += `\n[STDOUT]\n${res.stdout}\n`;
1477
+ if (res.stderr)
1478
+ output += `\n[STDERR]\n${res.stderr}\n`;
1479
+ return {
1480
+ output: output.trim(),
1481
+ ...(res.exitCode !== 0 ? { error: `Heap delta failed with exit code ${res.exitCode}` } : {}),
1482
+ };
1483
+ }
1484
+ catch (err) {
1485
+ return { output: '', error: `Heap delta error: ${err.message || String(err)}` };
1486
+ }
1487
+ }
1488
+ /**
1489
+ * Executes baseline and candidate scripts, returning output diffs and behavioral drift findings.
1490
+ */
1491
+ export async function executeBehavioralDrift(workspaceRoot, baselineCode, candidateCode, language = 'node', config, abortSignal) {
1492
+ try {
1493
+ const res = await runCheckBehavioralDrift(workspaceRoot, baselineCode, candidateCode, language, {
1494
+ ...config,
1495
+ abortSignal,
1496
+ });
1497
+ let output = `Behavioral Drift Analysis Result:\n`;
1498
+ output += `Has Drift: ${res.hasDrift ? 'YES' : 'NO'}\n`;
1499
+ if (res.diffSummary) {
1500
+ output += `Diff Summary:\n${res.diffSummary}\n`;
1501
+ }
1502
+ if (res.driftDetails && res.driftDetails.length > 0) {
1503
+ output += `Drift Details:\n`;
1504
+ for (const detail of res.driftDetails) {
1505
+ output += ` - ${detail.field ? `[${detail.field}] ` : ''}Baseline: ${JSON.stringify(detail.baseline)} vs Candidate: ${JSON.stringify(detail.candidate)}${detail.diff ? ` (${detail.diff})` : ''}\n`;
1506
+ }
1507
+ }
1508
+ if (res.baselineOutput)
1509
+ output += `\n[BASELINE OUTPUT]\n${res.baselineOutput}\n`;
1510
+ if (res.candidateOutput)
1511
+ output += `\n[CANDIDATE OUTPUT]\n${res.candidateOutput}\n`;
1512
+ if (res.stdout)
1513
+ output += `\n[STDOUT]\n${res.stdout}\n`;
1514
+ if (res.stderr)
1515
+ output += `\n[STDERR]\n${res.stderr}\n`;
1516
+ return {
1517
+ output: output.trim(),
1518
+ ...(res.exitCode !== 0 ? { error: `Behavioral drift check failed with exit code ${res.exitCode}` } : {}),
1519
+ };
1520
+ }
1521
+ catch (err) {
1522
+ return { output: '', error: `Behavioral drift error: ${err.message || String(err)}` };
1523
+ }
1524
+ }
1315
1525
  /**
1316
1526
  * Resolves `@alias/` prefixed paths in tool arguments to the correct workspace root
1317
1527
  * and relative path. Returns the effective workspaceRoot and the cleaned arguments.
@@ -1424,6 +1634,15 @@ export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
1424
1634
  else if (normalizedToolName === 'finish_investigation') {
1425
1635
  normalizedToolName = 'finish_task';
1426
1636
  }
1637
+ else if (normalizedToolName === 'fuzz_probe' || normalizedToolName === 'run_fuzz') {
1638
+ normalizedToolName = 'run_fuzz_probe';
1639
+ }
1640
+ else if (normalizedToolName === 'heap_delta' || normalizedToolName === 'check_heap') {
1641
+ normalizedToolName = 'check_heap_delta';
1642
+ }
1643
+ else if (normalizedToolName === 'behavioral_drift' || normalizedToolName === 'check_drift') {
1644
+ normalizedToolName = 'check_behavioral_drift';
1645
+ }
1427
1646
  // ─── Multi-Workspace Path Resolution ─────────────────────────────
1428
1647
  // Intercept @alias/ prefixed paths and swap workspaceRoot + relative path
1429
1648
  // before dispatching to the underlying tool functions (which remain unchanged).
@@ -1620,6 +1839,42 @@ export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
1620
1839
  case 'find_recent_changes':
1621
1840
  result = await findRecentChanges(effectiveRoot, resolvedArgs.dirPath, resolvedArgs.minutes, resolvedArgs.maxDepth);
1622
1841
  break;
1842
+ case 'run_fuzz_probe': {
1843
+ const code = resolvedArgs.code;
1844
+ const language = resolvedArgs.language || 'node';
1845
+ const config = {
1846
+ iterations: resolvedArgs.iterations,
1847
+ maxInputLength: resolvedArgs.maxInputLength,
1848
+ seed: resolvedArgs.seed,
1849
+ timeoutMs: resolvedArgs.timeoutMs,
1850
+ };
1851
+ result = await executeFuzzProbe(effectiveRoot, code, language, config, abortSignal);
1852
+ break;
1853
+ }
1854
+ case 'check_heap_delta': {
1855
+ const code = resolvedArgs.code;
1856
+ const language = resolvedArgs.language || 'node';
1857
+ const config = {
1858
+ warmupIterations: resolvedArgs.warmupIterations,
1859
+ testIterations: resolvedArgs.testIterations,
1860
+ samplingIntervalMs: resolvedArgs.samplingIntervalMs,
1861
+ timeoutMs: resolvedArgs.timeoutMs,
1862
+ };
1863
+ result = await executeHeapDelta(effectiveRoot, code, language, config, abortSignal);
1864
+ break;
1865
+ }
1866
+ case 'check_behavioral_drift': {
1867
+ const baselineCode = resolvedArgs.baselineCode;
1868
+ const candidateCode = resolvedArgs.candidateCode;
1869
+ const language = resolvedArgs.language || 'node';
1870
+ const config = {
1871
+ tolerance: resolvedArgs.tolerance,
1872
+ compareKeys: resolvedArgs.compareKeys,
1873
+ timeoutMs: resolvedArgs.timeoutMs,
1874
+ };
1875
+ result = await executeBehavioralDrift(effectiveRoot, baselineCode, candidateCode, language, config, abortSignal);
1876
+ break;
1877
+ }
1623
1878
  default:
1624
1879
  result = { output: '', error: `Unknown tool: "${toolName}"` };
1625
1880
  break;
@@ -26,7 +26,7 @@ import { markedTerminal } from 'marked-terminal';
26
26
  const execAsync = promisify(exec);
27
27
  import { debugLog, isDebugOn } from '../utils/logger.js';
28
28
  import { ensureProjectStorage, ensureIgnored, readCache, writeCache, invalidateCacheForDependents, } from '../utils/projectStorage.js';
29
- import { GEMINI_MODELS, isByokEnabled } from '../utils/config.js';
29
+ import { GEMINI_MODELS, isByokEnabled, TPM_COOLING_DELAYS } from '../utils/config.js';
30
30
  import { createSharedChatSession, getGeneralChatConfig, getPlanExecutionConfig, compressTextUsingFlashLite, generateChatTitle, getGlobalActiveModel, setGlobalActiveModel, summarizeChatHistory, } from './ai.js';
31
31
  import { getAndResetTurnUsage } from './proxyClient.js';
32
32
  import { changeLogger } from './changeLogger.js';
@@ -317,6 +317,8 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
317
317
  inputHandler.start(spinner);
318
318
  changeLogger.startChangeSet(userInput);
319
319
  let finalInput = userInput;
320
+ // Inter-turn cooling-off delay to allow API token buckets to settle before running intent routing
321
+ await new Promise((resolve) => setTimeout(resolve, TPM_COOLING_DELAYS.INTER_TURN_MS));
320
322
  // Stage 0: Summarize chat history if length threshold is met
321
323
  if (!cachedContextResult) {
322
324
  const isDebug = isDebugOn();
@@ -359,6 +361,8 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
359
361
  toolLogs.forEach((log) => p.log.step(log));
360
362
  spinner.start(`🔍 Context gathered successfully.`);
361
363
  }
364
+ // Post-investigation cooling-off delay to allow API Tokens-Per-Minute (TPM) sliding window to settle
365
+ await new Promise((resolve) => setTimeout(resolve, TPM_COOLING_DELAYS.POST_INVESTIGATION_MS));
362
366
  }
363
367
  let latestUsage = undefined;
364
368
  // Collect any inputs that were queued while the Context Agent was investigating
@@ -481,7 +485,8 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
481
485
  modifiedFiles.forEach((file) => p.log.message(` ${pc.dim('•')} ${file}`));
482
486
  }
483
487
  else {
484
- summaryMsg = 'Generation was stopped by the user before completion. No code files were modified before stopping.';
488
+ summaryMsg =
489
+ 'Generation was stopped by the user before completion. No code files were modified before stopping.';
485
490
  p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion (no files were modified).'));
486
491
  }
487
492
  chat.addTurn({
@@ -657,8 +662,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
657
662
  const turnDuration = ((turnEndTime - turnStartTime) / 1000).toFixed(1);
658
663
  p.log.info(`${pc.dim('Generated in')} ${pc.cyan(turnDuration + 's')}`);
659
664
  // Check if generation was stopped by user
660
- const isStopped = finalText.includes('Generation stopped') ||
661
- finalText.includes('[Generation stopped');
665
+ const isStopped = finalText.includes('Generation stopped') || finalText.includes('[Generation stopped');
662
666
  if (isStopped) {
663
667
  const currentChanges = changeLogger.getCurrentChangeSet()?.changes || [];
664
668
  const modifiedFiles = currentChanges
@@ -672,7 +676,8 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
672
676
  modifiedFiles.forEach((file) => p.log.message(` ${pc.dim('•')} ${file}`));
673
677
  }
674
678
  else {
675
- summaryMsg = 'Generation was stopped by the user before completion. No code files were modified before stopping.';
679
+ summaryMsg =
680
+ 'Generation was stopped by the user before completion. No code files were modified before stopping.';
676
681
  p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion (no files were modified).'));
677
682
  }
678
683
  chat.addTurn({
@@ -25,6 +25,7 @@ import { InvestigationAgentRunner } from './investigationAgent.js';
25
25
  import { readFile } from '../agent-tools.js';
26
26
  import { buildDependencyGraph } from '../../utils/dependencyTracer.js';
27
27
  import { debugLog } from '../../utils/logger.js';
28
+ import { TPM_COOLING_DELAYS } from '../../utils/config.js';
28
29
  // ─── Constants ───────────────────────────────────────────────────────
29
30
  /** Maximum total relevant files across all agents after merge. */
30
31
  const MAX_TOTAL_FILES = 30;
@@ -57,6 +58,8 @@ export class InvestigationOrchestrator {
57
58
  const agents = agentAssignments.map((assignment) => new InvestigationAgentRunner(assignment.agentLabel, assignment.domains, workspaceRoot, readCache, projectTree, projectType));
58
59
  // 3. Run all agents in parallel
59
60
  const startTime = Date.now();
61
+ // Cooling-off pause to prevent burst 429 errors when initiating parallel agent dispatch
62
+ await new Promise((resolve) => setTimeout(resolve, TPM_COOLING_DELAYS.PARALLEL_DISPATCH_MS));
60
63
  // Limit parallel investigation agents to 2 to avoid Vertex AI RESOURCE_EXHAUSTED errors
61
64
  const MAX_CONCURRENT = 2;
62
65
  const results = [];
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import { executeTool, getToolDeclarations as getBaseToolDeclarations } from '../agent-tools.js';
2
3
  import { MessageBus } from './messageBus.js';
3
4
  import { debugLog } from '../../utils/logger.js';
@@ -149,6 +150,31 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
149
150
  const matchCount = (outputText.match(/\.\/[^:]+:\d+:/g) || []).length;
150
151
  resultSummary = `${matchCount} matches`;
151
152
  }
153
+ else if (name === 'rename_file') {
154
+ const src = String(args.sourcePath ?? '');
155
+ const tgt = String(args.targetPath ?? '');
156
+ targetDesc = `${src} -> ${tgt}`;
157
+ actionDesc = (src && tgt && path.dirname(src) === path.dirname(tgt)) ? 'Renamed' : 'Moved';
158
+ }
159
+ else if (name === 'delete_file') {
160
+ targetDesc = String(args.filePath);
161
+ actionDesc = 'Deleted';
162
+ }
163
+ else if (name === 'run_fuzz_probe') {
164
+ targetDesc = String(args.language ?? 'node');
165
+ actionDesc = 'Ran fuzz probe';
166
+ resultSummary = typeof result === 'object' && result?.error ? result.error : 'Completed fuzz probe';
167
+ }
168
+ else if (name === 'check_heap_delta') {
169
+ targetDesc = String(args.language ?? 'node');
170
+ actionDesc = 'Inspected heap delta';
171
+ resultSummary = typeof result === 'object' && result?.error ? result.error : 'Completed heap analysis';
172
+ }
173
+ else if (name === 'check_behavioral_drift') {
174
+ targetDesc = String(args.language ?? 'node');
175
+ actionDesc = 'Checked behavioral drift';
176
+ resultSummary = typeof result === 'object' && result?.error ? result.error : 'Completed drift comparison';
177
+ }
152
178
  return result;
153
179
  }
154
180
  catch (error) {
@@ -72,7 +72,7 @@ export class SubAgentRunner {
72
72
  `3. EXECUTION WORKFLOW:\n` +
73
73
  ` - Step 1 (Investigate): Use 'grep_search', 'read_file', or 'list_directory' to inspect the codebase as needed.\n` +
74
74
  ` - Step 2 (Execute): Use 'modify_file' or 'write_file' to implement the required changes.\n` +
75
- ` - Step 3 (Verify): Use 'run_debug_script' or 'run_command' to test your changes if necessary.\n` +
75
+ ` - Step 3 (Verify): Use 'run_debug_script', 'run_fuzz_probe', 'check_heap_delta', 'check_behavioral_drift', or 'run_command' to test your changes if necessary.\n` +
76
76
  ` - Step 4 (Conclude): Once your specific objective is fully met, stop calling tools and return a text summary of your changes.\n` +
77
77
  `4. Coordinate: Use 'read_messages' to check for updates from other agents. Use 'post_message' if you discover breaking changes affecting others.\n` +
78
78
  `</critical_guidelines>`);
@@ -41,7 +41,7 @@ let globalSessionAccumulatedUsage = {
41
41
  totalTokenCount: 0,
42
42
  creditsUsed: 0,
43
43
  remainingBalance: undefined,
44
- modelsUsed: {}
44
+ modelsUsed: {},
45
45
  };
46
46
  export function getAndResetTurnUsage() {
47
47
  const current = { ...globalSessionAccumulatedUsage };
@@ -52,7 +52,7 @@ export function getAndResetTurnUsage() {
52
52
  totalTokenCount: 0,
53
53
  creditsUsed: 0,
54
54
  remainingBalance: undefined,
55
- modelsUsed: {}
55
+ modelsUsed: {},
56
56
  };
57
57
  return current;
58
58
  }
@@ -117,6 +117,7 @@ export class ProxyClient {
117
117
  if ((response.status === 429 || response.status === 503) && attempt < MAX_RETRIES) {
118
118
  const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
119
119
  const delayTime = Math.round(exponentialDelay * (0.5 + Math.random() * 0.5));
120
+ process.stdout.write('\n');
120
121
  console.warn(`Rate limit or service unavailable hit (${response.status}). Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
121
122
  await delay(delayTime, abortSignal);
122
123
  attempt++;
@@ -198,7 +199,7 @@ export class ProxyClient {
198
199
  collector?.accumulateUsage({
199
200
  promptTokens: data.usage.promptTokens || 0,
200
201
  candidatesTokens: data.usage.candidatesTokens || 0,
201
- cachedTokens: data.usage.cachedTokens || 0
202
+ cachedTokens: data.usage.cachedTokens || 0,
202
203
  });
203
204
  globalSessionAccumulatedUsage.promptTokens += data.usage.promptTokens || 0;
204
205
  globalSessionAccumulatedUsage.candidatesTokens += data.usage.candidatesTokens || 0;
@@ -209,7 +210,8 @@ export class ProxyClient {
209
210
  if (data.usage.remainingBalance !== undefined) {
210
211
  globalSessionAccumulatedUsage.remainingBalance = data.usage.remainingBalance;
211
212
  }
212
- globalSessionAccumulatedUsage.modelsUsed[modelName] = (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
213
+ globalSessionAccumulatedUsage.modelsUsed[modelName] =
214
+ (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
213
215
  }
214
216
  if (data.groundingMetadata) {
215
217
  groundingMetadata = data.groundingMetadata;
@@ -236,6 +238,7 @@ export class ProxyClient {
236
238
  if (attempt < MAX_RETRIES) {
237
239
  const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
238
240
  const delayTime = Math.round(exponentialDelay * (0.5 + Math.random() * 0.5));
241
+ process.stdout.write('\n');
239
242
  console.warn(`Rate limit or service unavailable hit during stream. Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
240
243
  await delay(delayTime, abortSignal);
241
244
  attempt++;