minovative-mind-cli 2.11.5 โ†’ 2.13.0

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 (43) hide show
  1. package/README.md +27 -1
  2. package/dist/commands/chat.js +3 -1
  3. package/dist/commands/eval.d.ts +22 -0
  4. package/dist/commands/eval.js +141 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +1 -0
  7. package/dist/services/agent/slashCommands.js +4 -2
  8. package/dist/services/agent/toolLoop.d.ts +4 -0
  9. package/dist/services/agent/toolLoop.js +61 -10
  10. package/dist/services/agent-tools.d.ts +5 -5
  11. package/dist/services/agent-tools.js +150 -11
  12. package/dist/services/contextAgent.d.ts +1 -0
  13. package/dist/services/contextAgent.js +29 -6
  14. package/dist/services/ideOptimization.d.ts +15 -0
  15. package/dist/services/ideOptimization.js +169 -0
  16. package/dist/services/metrics.d.ts +10 -0
  17. package/dist/services/metrics.js +24 -0
  18. package/dist/services/orchestration/messageBus.d.ts +81 -41
  19. package/dist/services/orchestration/messageBus.js +242 -98
  20. package/dist/services/orchestration/orchestrator.d.ts +6 -6
  21. package/dist/services/orchestration/orchestrator.js +32 -21
  22. package/dist/services/orchestration/scopedTools.d.ts +7 -1
  23. package/dist/services/orchestration/scopedTools.js +45 -9
  24. package/dist/services/orchestration/subAgent.d.ts +19 -17
  25. package/dist/services/orchestration/subAgent.js +98 -81
  26. package/dist/services/swebench/gitDiffExtractor.d.ts +57 -0
  27. package/dist/services/swebench/gitDiffExtractor.js +209 -0
  28. package/dist/services/swebench/index.d.ts +4 -0
  29. package/dist/services/swebench/index.js +4 -0
  30. package/dist/services/swebench/instanceLoader.d.ts +21 -0
  31. package/dist/services/swebench/instanceLoader.js +171 -0
  32. package/dist/services/swebench/sweBenchRunnerService.d.ts +38 -0
  33. package/dist/services/swebench/sweBenchRunnerService.js +618 -0
  34. package/dist/services/swebench/types.d.ts +167 -0
  35. package/dist/services/swebench/types.js +7 -0
  36. package/dist/services/verificationService.js +3 -0
  37. package/dist/utils/fuzzyMatch.d.ts +51 -21
  38. package/dist/utils/fuzzyMatch.js +37 -122
  39. package/dist/utils/projectStorage.js +10 -5
  40. package/dist/utils/systemPrompts.d.ts +1 -1
  41. package/dist/utils/systemPrompts.js +10 -4
  42. package/oclif.manifest.json +137 -1
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -76,12 +76,25 @@ Works in any terminal โ€” SSH, Docker, CI pipelines, Vim, Windows Command Prompt
76
76
  - Solved one of the hardest logic puzzles on the platform by engineering a mathematically rigorous pruning algorithm based on the Rearrangement Inequality.
77
77
  - Completely bypassed naive backtracking (10! permutations).
78
78
  - Achieved sub-millisecond execution speeds (~140 microseconds) with zero heap allocations during recursion.
79
- - Autonomously navigated the Go toolchain (\`go test -bench=.\`).
79
+ - Autonomously navigated the Go toolchain (`go test -bench=.`).
80
80
  - Parsed its own architectural performance warnings to optimize the solution.
81
81
  - [Alphametics Summary](https://github.com/Quarantiine/polyglot-benchmark-mmcli/blob/main/go/exercises/practice/alphametics/minovative-tries/attempt_1.md)
82
82
 
83
+ ### ๐Ÿ”ฌ Automated SWE-bench Evaluation Runner
84
+
85
+ Evaluate the agent on SWE-bench Lite datasets with automated repo isolation, patch extraction, and public-ready telemetry reporting:
86
+
87
+ ```bash
88
+ # Run SWE-bench evaluation and generate JSON report
89
+ minovative-mind-cli eval -i test_instances.jsonl -o predictions.jsonl -r evaluation_report.json --autoClone
90
+
91
+ # Run with specific repository filter and concurrency
92
+ minovative-mind-cli eval -i instances.jsonl --repo astropy/astropy --concurrency 2
93
+ ```
94
+
83
95
  ---
84
96
 
97
+
85
98
  ## Models
86
99
 
87
100
  Hot-swap during a session using `/models`:
@@ -181,6 +194,19 @@ _โš ๏ธ\*Support is limited or requires custom local system tooling/environment
181
194
 
182
195
  ---
183
196
 
197
+ ## ๐Ÿ’ก Performance & IDE Optimization
198
+
199
+ During heavy, multi-turn AI agent sessions, Minovative Mind CLI includes built-in architectural optimizations to eliminate IDE lag, CPU spikes, and dev-server interference:
200
+
201
+ - **Ephemeral Scratch Isolation (`os.tmpdir()`)**: Diagnostic probes, validation scripts, and benchmark tests execute in a sandboxed OS temp directory (`os.tmpdir()`) rather than writing disposable files into your workspace root. This prevents file watcher churn (`fsevents`, `inotify`), avoids Language Server Protocol (LSP) re-indexing storms (TSServer, Pyright, rust-analyzer), and prevents running dev servers (Vite, Turbopack, Nodemon) from triggering unneeded full-page reloads.
202
+ - **Automatic IDE Watcher Exclusions (`.vscode/settings.json`)**: On session startup, the CLI automatically and non-destructively ensures that `.minovativemind/**`, `.tmp/**`, and `scratch/**` are added to `files.watcherExclude` and `search.exclude`. This stops IDE background processes from burning CPU on internal telemetry, chat session logs, and cache files.
203
+ - **Clean Git Status Synchronization**: The CLI automatically ensures `.minovativemind/`, `.tmp/`, and `scratch/` are excluded from `.gitignore`, `.dockerignore`, and `.npmignore`, keeping your IDE's Source Control pane fast and responsive.
204
+ - **Terminal Render Efficiency**: For long-running evaluation sweeps (`eval`) or massive test outputs, minimizing the terminal pane or running it in the background pauses Electron/xterm.js GPU canvas repaints and frees up system resources.
205
+
206
+ For full tuning recommendations and configuration details, see the [Performance & IDE Optimization Guide](file:///Users/danielward/Developer/Work%20Projects/minovative-mind-cli/docs/PERFORMANCE_GUIDE.md).
207
+
208
+ ---
209
+
184
210
  ## Legal
185
211
 
186
212
  [Terms of Service](https://www.minovativemind.dev/legal/cli-terms) ยท
@@ -8,6 +8,7 @@ import { startAgentLoop } from '../services/agent.js';
8
8
  import { getAuthorizedIdToken, login } from '../services/auth.js';
9
9
  import { printLogo, brandBg, brandFg } from '../utils/logo.js';
10
10
  import { updateWorkspaceStatus } from '../services/workspace.js';
11
+ import { optimizeWorkspaceIDESettings } from '../services/ideOptimization.js';
11
12
  /**
12
13
  * @class DefaultCommand
13
14
  * @extends Command
@@ -80,10 +81,11 @@ Chat Controls:
80
81
  }
81
82
  p.log.info(`${pc.dim('Workspace:')} ${brandFg(workspaceRoot)}`);
82
83
  p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu and "${pc.yellow('stop')}" to stop the ai generation. Type ${pc.yellow('exit')} to leave.`);
83
- // Update workspace status in the background
84
+ // Update workspace status and optimize IDE watcher settings in the background
84
85
  if (idToken) {
85
86
  updateWorkspaceStatus(idToken, workspaceRoot).catch(() => { });
86
87
  }
88
+ optimizeWorkspaceIDESettings(workspaceRoot).catch(() => { });
87
89
  const { workspaceRegistry } = await import('../services/workspaceRegistry.js');
88
90
  workspaceRegistry.init();
89
91
  await startAgentLoop(workspaceRoot, this.config.version);
@@ -0,0 +1,22 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class EvalCommand extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ instances: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ report: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ model: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ maxTurns: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
+ repo: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ instanceId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ limit: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ workspaceDir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ autoClone: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
19
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ };
21
+ run(): Promise<void>;
22
+ }
@@ -0,0 +1,141 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import * as path from 'node:path';
3
+ import * as fs from 'node:fs';
4
+ import { runSWEBenchEvaluation, formatEvaluationReportSummary, } from '../services/swebench/index.js';
5
+ export default class EvalCommand extends Command {
6
+ static description = 'Execute automated evaluation on SWE-bench Lite benchmark instances';
7
+ static examples = [
8
+ '<%= config.bin %> <%= command.id %> -i instances.json -o predictions.jsonl',
9
+ '<%= config.bin %> <%= command.id %> --instances ./test_instances.jsonl --output ./predictions.jsonl --report ./evaluation_report.json',
10
+ '<%= config.bin %> <%= command.id %> --instances test_instances.jsonl --repo astropy/astropy --limit 5',
11
+ '<%= config.bin %> <%= command.id %> --instances test_instances.jsonl --instance-id astropy__astropy-12907',
12
+ '<%= config.bin %> <%= command.id %> --instances test_instances.jsonl --dry-run',
13
+ ];
14
+ static flags = {
15
+ instances: Flags.string({
16
+ char: 'i',
17
+ description: 'Path to input instances file (JSON or JSONL format)',
18
+ required: true,
19
+ }),
20
+ output: Flags.string({
21
+ char: 'o',
22
+ description: 'Path to write generated predictions JSONL file',
23
+ default: 'predictions.jsonl',
24
+ }),
25
+ report: Flags.string({
26
+ char: 'r',
27
+ description: 'Path to export comprehensive evaluation report JSON',
28
+ }),
29
+ model: Flags.string({
30
+ char: 'm',
31
+ description: 'Model name tag for prediction metadata',
32
+ default: 'minovative-mind-agent',
33
+ }),
34
+ maxTurns: Flags.integer({
35
+ description: 'Maximum agent execution turns per benchmark instance',
36
+ default: 30,
37
+ }),
38
+ timeout: Flags.integer({
39
+ description: 'Timeout in milliseconds per instance (default: 600000ms = 10min)',
40
+ default: 600000,
41
+ }),
42
+ repo: Flags.string({
43
+ description: 'Filter instances by repository name (e.g. astropy/astropy)',
44
+ }),
45
+ instanceId: Flags.string({
46
+ description: 'Filter by specific instance ID (e.g. astropy__astropy-12907)',
47
+ }),
48
+ limit: Flags.integer({
49
+ description: 'Limit the number of instances to evaluate',
50
+ }),
51
+ workspaceDir: Flags.string({
52
+ char: 'w',
53
+ description: 'Base workspace directory where repo checkouts reside',
54
+ }),
55
+ autoClone: Flags.boolean({
56
+ description: 'Automatically clone repository if not found locally in workspace directory',
57
+ default: false,
58
+ }),
59
+ dryRun: Flags.boolean({
60
+ description: 'Validate instances and workspace setup without invoking the agent loop',
61
+ default: false,
62
+ }),
63
+ concurrency: Flags.integer({
64
+ char: 'c',
65
+ description: 'Number of instances to evaluate concurrently (default: 1)',
66
+ default: 1,
67
+ }),
68
+ verbose: Flags.boolean({
69
+ char: 'v',
70
+ description: 'Show detailed per-turn logs during evaluation',
71
+ default: false,
72
+ }),
73
+ };
74
+ async run() {
75
+ const { flags } = await this.parse(EvalCommand);
76
+ const instancesPath = path.isAbsolute(flags.instances)
77
+ ? flags.instances
78
+ : path.resolve(process.cwd(), flags.instances);
79
+ const outputPath = path.isAbsolute(flags.output)
80
+ ? flags.output
81
+ : path.resolve(process.cwd(), flags.output);
82
+ const reportPath = flags.report
83
+ ? path.isAbsolute(flags.report)
84
+ ? flags.report
85
+ : path.resolve(process.cwd(), flags.report)
86
+ : undefined;
87
+ if (!fs.existsSync(instancesPath)) {
88
+ this.error(`Instances file not found: ${instancesPath}`);
89
+ return;
90
+ }
91
+ this.log(`\n======================================================`);
92
+ this.log(` MINOVATIVE SWE-BENCH LITE EVALUATION RUNNER`);
93
+ this.log(`======================================================`);
94
+ this.log(`Instances File : ${instancesPath}`);
95
+ this.log(`Output Location : ${outputPath}`);
96
+ if (reportPath)
97
+ this.log(`Report Location : ${reportPath}`);
98
+ this.log(`Model Identifier : ${flags.model}`);
99
+ this.log(`Max Turns : ${flags.maxTurns}`);
100
+ this.log(`Timeout / Inst : ${flags.timeout}ms`);
101
+ if (flags.repo)
102
+ this.log(`Repo Filter : ${flags.repo}`);
103
+ if (flags.instanceId)
104
+ this.log(`Instance Filter : ${flags.instanceId}`);
105
+ if (flags.limit)
106
+ this.log(`Instance Limit : ${flags.limit}`);
107
+ if (flags.dryRun)
108
+ this.log(`Execution Mode : Dry Run (No agent invocation)`);
109
+ if (flags.autoClone)
110
+ this.log(`Auto-Clone Repos : Enabled`);
111
+ this.log(`======================================================\n`);
112
+ const evalOptions = {
113
+ instancesPath,
114
+ outputPredictionsPath: outputPath,
115
+ reportPath,
116
+ modelName: flags.model,
117
+ maxTurns: flags.maxTurns,
118
+ timeoutMsPerInstance: flags.timeout,
119
+ repo: flags.repo,
120
+ instanceIds: flags.instanceId ? [flags.instanceId] : undefined,
121
+ limit: flags.limit,
122
+ workspacesBaseDir: flags.workspaceDir,
123
+ autoCloneRepos: flags.autoClone,
124
+ dryRun: flags.dryRun,
125
+ concurrency: flags.concurrency,
126
+ verbose: flags.verbose,
127
+ onProgress: (progress) => {
128
+ const statusStr = progress.status === 'success' ? 'Completed' : progress.status === 'running' ? 'Running' : progress.status;
129
+ this.log(`[${progress.completed}/${progress.total}] Instance: ${progress.currentInstanceId} (${statusStr})`);
130
+ },
131
+ };
132
+ try {
133
+ const summary = await runSWEBenchEvaluation(evalOptions);
134
+ this.log(formatEvaluationReportSummary(summary));
135
+ }
136
+ catch (err) {
137
+ const msg = err instanceof Error ? err.message : String(err);
138
+ this.error(`SWE-bench evaluation failed: ${msg}`);
139
+ }
140
+ }
141
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { run } from '@oclif/core';
2
+ export * as swebench from './services/swebench/index.js';
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { run } from '@oclif/core';
2
+ export * as swebench from './services/swebench/index.js';
@@ -2,6 +2,7 @@ import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
3
  import { promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
+ import os from 'node:os';
5
6
  import { exec } from 'node:child_process';
6
7
  import { promisify } from 'node:util';
7
8
  import crypto from 'node:crypto';
@@ -1052,10 +1053,11 @@ Strict Formatting Rules:
1052
1053
  .replace(/```\s*$/gm, '')
1053
1054
  .trim();
1054
1055
  commitSpinner.message('Committing...');
1055
- const tmpMsgPath = path.join(workspaceRoot, '.gemini-commit-msg.tmp');
1056
+ const nonce = `${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
1057
+ const tmpMsgPath = path.join(os.tmpdir(), `.mino-commit-msg-${nonce}.tmp`);
1056
1058
  await fs.writeFile(tmpMsgPath, commitMsg, 'utf-8');
1057
1059
  try {
1058
- await execAsync(`git commit -F .gemini-commit-msg.tmp`, { cwd: workspaceRoot });
1060
+ await execAsync(`git commit -F "${tmpMsgPath}"`, { cwd: workspaceRoot });
1059
1061
  }
1060
1062
  finally {
1061
1063
  await fs.rm(tmpMsgPath, { force: true });
@@ -37,3 +37,7 @@ export declare function formatToolCall(name: string, args: Record<string, unknow
37
37
  * @returns A promise that resolves to the final text response.
38
38
  */
39
39
  export declare function processResponse(chat: ProxyChatSession, result: any, workspaceRoot: string, inputHandler: AsyncInputHandler, agentState: AgentState, abortSignal: AbortSignal): Promise<string>;
40
+ /**
41
+ * System advisory appended when consecutive command failures exceed the circuit breaker threshold.
42
+ */
43
+ export declare const CONSECUTIVE_COMMAND_FAILURE_ADVISORY = "\n\n\u26A0\uFE0F SYSTEM ADVISORY: Multiple consecutive command failures detected. If you are troubleshooting a build or test failure, inspect the build configuration file (e.g., package.json, setup.py, Cargo.toml, Makefile, CMakeLists.txt) or relevant source code to identify the root cause before retrying.";
@@ -6,7 +6,7 @@ import { executeTool } from '../agent-tools.js';
6
6
  import { getPlanExecutionConfig } from '../ai.js';
7
7
  import { routeIntent } from '../contextAgent.js';
8
8
  import { requestCommandApproval } from './commandApproval.js';
9
- import { getMetricCollector } from '../metrics.js';
9
+ import { getMetricCollector, recordRecoveryCircuitBreakerTrip } from '../metrics.js';
10
10
  import { trackTask } from '../../utils/taskVisualizer.js';
11
11
  // โ”€โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
12
12
  /**
@@ -175,6 +175,8 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
175
175
  // Recovery thresholds for handling unexpected empty API payloads
176
176
  let emptyRetryCount = 0;
177
177
  const MAX_EMPTY_RETRIES = 5;
178
+ // Consecutive command failure tracking for circuit breaker
179
+ const commandFailureState = { consecutiveFailures: 0 };
178
180
  // Loop while the model keeps requesting tool calls
179
181
  while (true) {
180
182
  turnCount++;
@@ -251,7 +253,7 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
251
253
  const collector = getMetricCollector();
252
254
  if (collector)
253
255
  collector.recordToolTurn();
254
- const execRes = await executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal);
256
+ const execRes = await executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal, commandFailureState);
255
257
  if (execRes.aborted) {
256
258
  return '[Generation stopped by user]';
257
259
  }
@@ -294,8 +296,25 @@ export async function processResponse(chat, result, workspaceRoot, inputHandler,
294
296
  response = followUp.response;
295
297
  }
296
298
  }
297
- async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal) {
299
+ /**
300
+ * System advisory appended when consecutive command failures exceed the circuit breaker threshold.
301
+ */
302
+ export const CONSECUTIVE_COMMAND_FAILURE_ADVISORY = '\n\nโš ๏ธ SYSTEM ADVISORY: Multiple consecutive command failures detected. If you are troubleshooting a build or test failure, inspect the build configuration file (e.g., package.json, setup.py, Cargo.toml, Makefile, CMakeLists.txt) or relevant source code to identify the root cause before retrying.';
303
+ /**
304
+ * Executes a sequence of tool calls returned by the AI model.
305
+ * Enforces security gates, tracks telemetry, applies circuit breakers for consecutive command failures,
306
+ * and passes the tool execution results back to the conversation loop.
307
+ *
308
+ * @param functionCalls - Array of function call parts from the Gemini API.
309
+ * @param workspaceRoot - Root directory of the primary active workspace.
310
+ * @param inputHandler - Async input listener for handling user steering/abort.
311
+ * @param abortSignal - Signal to cancel ongoing execution turns.
312
+ * @param commandFailureState - Mutable container tracking consecutive command failure counts across turns.
313
+ * @returns An object containing tool responses formatted for Gemini API, plus an aborted flag.
314
+ */
315
+ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abortSignal, commandFailureState) {
298
316
  const toolResponses = [];
317
+ const state = commandFailureState ?? { consecutiveFailures: 0 };
299
318
  for (const fc of functionCalls) {
300
319
  await inputHandler.waitForPrompt();
301
320
  if (abortSignal.aborted) {
@@ -330,24 +349,56 @@ async function executeToolCalls(functionCalls, workspaceRoot, inputHandler, abor
330
349
  return await executeTool(workspaceRoot, toolName, toolArgs, abortSignal);
331
350
  });
332
351
  await inputHandler.waitForPrompt();
333
- if (toolResult.error) {
334
- debugLog(`Raw Tool Error for ${toolName}: ${toolResult.error}`);
352
+ let toolError = toolResult.error;
353
+ // Circuit Breaker State Tracking:
354
+ // Reset counter to 0 immediately upon:
355
+ // 1. A successful (zero-exit / no error) run_command or run_debug_script.
356
+ // 2. Any investigative tool call (read_file, grep_search, list_directory, find_dependencies, run_analysis_script).
357
+ // 3. Any file mutation (modify_file, write_file, delete_file, rename_file).
358
+ const isCommandTool = toolName === 'run_command' || toolName === 'run_debug_script';
359
+ const isInvestigativeOrMutationTool = [
360
+ 'read_file',
361
+ 'grep_search',
362
+ 'list_directory',
363
+ 'find_dependencies',
364
+ 'find_recent_changes',
365
+ 'run_analysis_script',
366
+ 'modify_file',
367
+ 'write_file',
368
+ 'delete_file',
369
+ 'rename_file',
370
+ ].includes(toolName);
371
+ if (isCommandTool) {
372
+ if (toolError) {
373
+ state.consecutiveFailures++;
374
+ if (state.consecutiveFailures >= 2) {
375
+ toolError += CONSECUTIVE_COMMAND_FAILURE_ADVISORY;
376
+ recordRecoveryCircuitBreakerTrip();
377
+ }
378
+ }
379
+ else {
380
+ state.consecutiveFailures = 0;
381
+ }
382
+ }
383
+ else if (isInvestigativeOrMutationTool) {
384
+ state.consecutiveFailures = 0;
385
+ }
386
+ if (toolError) {
387
+ debugLog(`Raw Tool Error for ${toolName}: ${toolError}`);
335
388
  // Do not spam the user with expected chunking warnings
336
- if (!toolResult.error.includes('File is too large')) {
389
+ if (!toolError.includes('File is too large')) {
337
390
  // Truncate the error message for the terminal UI to prevent console clutter
338
391
  // (e.g. hiding the large file previews sent to the AI)
339
- const displayError = toolResult.error.split('\n')[0].substring(0, 100);
392
+ const displayError = toolError.split('\n')[0].substring(0, 100);
340
393
  p.log.warn(`${pc.red('Tool error:')} [${displayError}]`);
341
394
  }
342
395
  }
343
- if (!toolResult.error && (toolName === 'write_file' || toolName === 'modify_file' || toolName === 'delete_file')) {
344
- }
345
396
  toolResponses.push({
346
397
  functionResponse: {
347
398
  name: toolName,
348
399
  response: {
349
400
  output: toolResult.output,
350
- ...(toolResult.error ? { error: toolResult.error } : {}),
401
+ ...(toolError ? { error: toolError } : {}),
351
402
  ...(toolResult.inlineData ? { inlineData: toolResult.inlineData } : {}),
352
403
  },
353
404
  },
@@ -139,13 +139,13 @@ export declare function modifyFile(workspaceRoot: string, filePath: string, edit
139
139
  */
140
140
  export declare function listDirectory(workspaceRoot: string, dirPath: string, maxDepth?: number): Promise<ToolResult>;
141
141
  /**
142
- * Executes a shell command within the workspace root with a timeout and buffer limit.
142
+ * Condenses verbose command and test failure outputs into high-signal diagnostic error logs
143
+ * with dynamic tail sizing and stack trace boundary snapping.
143
144
  *
144
- * @param workspaceRoot - Absolute path to the workspace root directory.
145
- * @param command - The shell command string to execute.
146
- * @param abortSignal - Optional AbortSignal to cancel execution.
147
- * @returns A promise resolving to a {@link ToolResult} containing stdout/stderr or an error.
145
+ * @param rawError - The full error or standard error string.
146
+ * @returns The condensed high-signal diagnostic output.
148
147
  */
148
+ export declare function extractHighSignalError(rawError: string): string;
149
149
  export declare function runCommand(workspaceRoot: string, command: string, abortSignal?: AbortSignal): Promise<ToolResult>;
150
150
  /**
151
151
  * Performs a grep text or regular expression search across files in the workspace.
@@ -27,7 +27,7 @@ import { findDependencies, formatDependencyResult } from '../utils/dependencyTra
27
27
  import { atomicWriteFile } from '../utils/atomicWrite.js';
28
28
  import { EXCLUDED_EXTENSIONS } from '../utils/excludedExtensions.js';
29
29
  import { extractSymbols } from '../utils/symbolExtractor.js';
30
- import { getMetricCollector } from './metrics.js';
30
+ import { getMetricCollector, recordRecoveryPrunedLogVolume } from './metrics.js';
31
31
  import { getCurrentAgentId } from '../utils/asyncContext.js';
32
32
  import { recordFileRead, hasAgentReadFile } from '../utils/fileReadGuard.js';
33
33
  import { runFuzzProbe, checkHeapDelta as runCheckHeapDelta, checkBehavioralDrift as runCheckBehavioralDrift, runEphemeralScript, } from '../utils/analysisRunner.js';
@@ -91,7 +91,7 @@ export const toolDeclarations = [
91
91
  },
92
92
  {
93
93
  name: 'write_file',
94
- description: 'Create a new file or completely overwrite an existing file with the provided content. Use modify_file for targeted edits instead. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
94
+ description: 'Create a new permanent file or completely overwrite an existing file with the provided content. Use modify_file for targeted edits instead. NEVER use write_file to create disposable test or scratch scripts in the workspace root โ€” use run_debug_script instead to prevent IDE file watcher churn and dev-server reloads. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
95
95
  parameters: {
96
96
  type: SchemaType.OBJECT,
97
97
  properties: {
@@ -278,7 +278,7 @@ export const toolDeclarations = [
278
278
  },
279
279
  {
280
280
  name: 'run_debug_script',
281
- description: 'Write a disposable script to a temporary file, execute it using the specified runtime, and return the exact standard output and standard error. ' +
281
+ description: 'Write a disposable script to a sandboxed temporary file in os.tmpdir(), execute it using the specified runtime, and return the exact standard output and standard error without polluting the workspace or triggering IDE file watchers. ' +
282
282
  'Use this to: (1) actively debug the codebase by inspecting variables or logging values, ' +
283
283
  '(2) validate your changes by importing the modified module and asserting expected behavior with edge-case inputs, ' +
284
284
  '(3) run quick sanity checks (e.g., verify a config file parses correctly, confirm exports are intact after a refactor, or check that a function returns the expected output), ' +
@@ -510,6 +510,7 @@ const DEFAULT_IGNORED_DIRS = new Set([
510
510
  '.tmp',
511
511
  'temp',
512
512
  'tmp',
513
+ 'scratch',
513
514
  '.minovativemind',
514
515
  ]);
515
516
  const DEFAULT_IGNORED_FILES = new Set([
@@ -1110,6 +1111,137 @@ export async function listDirectory(workspaceRoot, dirPath, maxDepth = 3) {
1110
1111
  * @param abortSignal - Optional AbortSignal to cancel execution.
1111
1112
  * @returns A promise resolving to a {@link ToolResult} containing stdout/stderr or an error.
1112
1113
  */
1114
+ /**
1115
+ * High-signal error patterns across major languages and compiler toolchains.
1116
+ * Linker notes and candidate diagnostic details are explicitly retained.
1117
+ */
1118
+ const HIGH_SIGNAL_ERROR_PATTERNS = [
1119
+ // C / C++ / Cython / Clang / GCC
1120
+ /fatal error:/i,
1121
+ /#error/i,
1122
+ /\berror:\b/i,
1123
+ /undefined symbol:/i,
1124
+ /undefined reference/i,
1125
+ /ld: symbol\(s\) not found/i,
1126
+ /clang: error:/i,
1127
+ /referenced from:/i,
1128
+ /note: expanded from macro/i,
1129
+ /note: candidate:/i,
1130
+ // Python / Pytest
1131
+ /Traceback \(most recent call last\):/i,
1132
+ /AssertionError:/i,
1133
+ /TypeError:/i,
1134
+ /ImportError:/i,
1135
+ /ModuleNotFoundError:/i,
1136
+ /FAILED \(failures=/i,
1137
+ /\bE\s{3}\b/,
1138
+ /\bFAIL\b/,
1139
+ // Node.js / TypeScript / Jest / Mocha
1140
+ /SyntaxError:/i,
1141
+ /ReferenceError:/i,
1142
+ /Error: Cannot find module/i,
1143
+ /\bFAIL\b/,
1144
+ /โœ•\s/,
1145
+ /TS\d{4}:/,
1146
+ // Rust / Cargo
1147
+ /error\[E\d+\]:/i,
1148
+ /fatal runtime error:/i,
1149
+ /panicked at/i,
1150
+ // Go
1151
+ /panic:/i,
1152
+ /cannot find package/i,
1153
+ /undefined:/i,
1154
+ /FAIL\t/,
1155
+ // Java / JVM
1156
+ /Exception in thread/i,
1157
+ /java\.lang\./i,
1158
+ /error: cannot find symbol/i,
1159
+ // Generic Fallbacks
1160
+ /\b(cannot|unable to|not found|command not found|no such file)\b/i,
1161
+ ];
1162
+ /**
1163
+ * Noise filter pattern for compiler warning churn when total output is extensive.
1164
+ */
1165
+ const COMPILER_WARNING_NOISE_PATTERN = /-W(deprecated-declarations|unused-variable|unused-parameter|unused-function|sign-compare|ignored-qualifiers|nullability-completeness)/i;
1166
+ /**
1167
+ * Start boundary patterns for common multi-line stack traces.
1168
+ */
1169
+ const STACK_TRACE_START_PATTERNS = [
1170
+ /Traceback \(most recent call last\):/i,
1171
+ /Exception in thread/i,
1172
+ /panic:/i,
1173
+ /Error:\s*$/i,
1174
+ ];
1175
+ /**
1176
+ * Condenses verbose command and test failure outputs into high-signal diagnostic error logs
1177
+ * with dynamic tail sizing and stack trace boundary snapping.
1178
+ *
1179
+ * @param rawError - The full error or standard error string.
1180
+ * @returns The condensed high-signal diagnostic output.
1181
+ */
1182
+ export function extractHighSignalError(rawError) {
1183
+ if (!rawError || typeof rawError !== 'string')
1184
+ return '';
1185
+ // Threshold passthrough: if < 3000 chars or < 50 lines, return untouched
1186
+ const lines = rawError.split(/\r?\n/);
1187
+ if (rawError.length < 3000 && lines.length < 50) {
1188
+ return rawError;
1189
+ }
1190
+ // Filter out noise lines (e.g. repetitive compiler warning flags)
1191
+ const nonNoiseLines = lines.filter((l) => !COMPILER_WARNING_NOISE_PATTERN.test(l));
1192
+ // Find high-signal error lines
1193
+ const highSignalLines = [];
1194
+ const highSignalIndices = new Set();
1195
+ for (let i = 0; i < nonNoiseLines.length; i++) {
1196
+ const line = nonNoiseLines[i];
1197
+ if (HIGH_SIGNAL_ERROR_PATTERNS.some((p) => p.test(line))) {
1198
+ highSignalLines.push(line);
1199
+ highSignalIndices.add(i);
1200
+ }
1201
+ }
1202
+ // Dynamic tail sizing: Math.min(50, Math.max(15, Math.floor(totalLines * 0.15)))
1203
+ const dynamicTailCount = Math.min(50, Math.max(15, Math.floor(nonNoiseLines.length * 0.15)));
1204
+ let tailStartIndex = Math.max(0, nonNoiseLines.length - dynamicTailCount);
1205
+ // Stack-trace boundary snapping: if a stack trace starts up to 15 lines above tailStartIndex, snap upward
1206
+ const lookbackMax = Math.max(0, tailStartIndex - 15);
1207
+ for (let i = tailStartIndex; i >= lookbackMax; i--) {
1208
+ if (STACK_TRACE_START_PATTERNS.some((p) => p.test(nonNoiseLines[i]))) {
1209
+ tailStartIndex = i;
1210
+ break;
1211
+ }
1212
+ }
1213
+ const tailLines = nonNoiseLines.slice(tailStartIndex);
1214
+ // If high-signal lines are all already present within the tail, return tail directly (with note if truncated)
1215
+ const isHighSignalInTail = Array.from(highSignalIndices).every((idx) => idx >= tailStartIndex);
1216
+ if (isHighSignalInTail && tailStartIndex === 0) {
1217
+ return nonNoiseLines.join('\n');
1218
+ }
1219
+ const resultBlocks = [];
1220
+ // Add high signal summary if there are high-signal lines occurring before the tail
1221
+ const precedingHighSignal = highSignalLines.filter((_, idx) => {
1222
+ const originalIndex = Array.from(highSignalIndices)[idx];
1223
+ return originalIndex < tailStartIndex;
1224
+ });
1225
+ if (precedingHighSignal.length > 0) {
1226
+ resultBlocks.push(`--- [High-Signal Diagnostic Highlights (${precedingHighSignal.length} findings)] ---\n` +
1227
+ precedingHighSignal.join('\n'));
1228
+ }
1229
+ if (tailStartIndex > 0) {
1230
+ resultBlocks.push(`--- [Output Tail (last ${tailLines.length} of ${nonNoiseLines.length} lines)] ---\n` +
1231
+ tailLines.join('\n'));
1232
+ }
1233
+ else {
1234
+ resultBlocks.push(tailLines.join('\n'));
1235
+ }
1236
+ const condensedResult = resultBlocks.join('\n\n');
1237
+ const condensedLines = condensedResult.split(/\r?\n/).length;
1238
+ const linesPruned = Math.max(0, lines.length - condensedLines);
1239
+ const charsPruned = Math.max(0, rawError.length - condensedResult.length);
1240
+ if (linesPruned > 0 || charsPruned > 0) {
1241
+ recordRecoveryPrunedLogVolume(linesPruned, charsPruned);
1242
+ }
1243
+ return condensedResult;
1244
+ }
1113
1245
  export async function runCommand(workspaceRoot, command, abortSignal) {
1114
1246
  // Reject sudo or interactive root commands to prevent non-interactive subshell deadlocks and security escalation
1115
1247
  if (/\bsudo\b/i.test(command)) {
@@ -1137,12 +1269,13 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
1137
1269
  }
1138
1270
  catch (err) {
1139
1271
  const message = err instanceof Error ? err.message : String(err);
1140
- // Also truncate error output
1272
+ // Condense error output with high-signal extraction and dynamic tail sizing
1273
+ const condensedError = extractHighSignalError(message);
1141
1274
  const MAX_ERR_OUTPUT = 30_000;
1142
- const truncatedMsg = message.length > MAX_ERR_OUTPUT
1143
- ? message.substring(0, MAX_ERR_OUTPUT) +
1144
- `\n\n... (Error output truncated: ${message.length} bytes exceeded 30KB limit. Pipe to a file if you need full logs.)`
1145
- : message;
1275
+ const truncatedMsg = condensedError.length > MAX_ERR_OUTPUT
1276
+ ? condensedError.substring(0, MAX_ERR_OUTPUT) +
1277
+ `\n\n... (Error output truncated: ${condensedError.length} bytes exceeded 30KB limit. Pipe to a file if you need full logs.)`
1278
+ : condensedError;
1146
1279
  return { output: '', error: `Command failed: ${truncatedMsg}` };
1147
1280
  }
1148
1281
  }
@@ -1389,15 +1522,21 @@ export async function runDebugScript(workspaceRoot, language, code, abortSignal)
1389
1522
  if (!finalOutput.trim()) {
1390
1523
  finalOutput = 'Script executed successfully with no output.';
1391
1524
  }
1525
+ const condensedOutput = extractHighSignalError(finalOutput.trim());
1392
1526
  return {
1393
- output: `<test_results>\n${sanitizeForCDATA(finalOutput.trim())}\n</test_results>`,
1394
- ...(res.exitCode !== 0 ? { error: `Script failed with exit code ${res.exitCode}` } : {}),
1527
+ output: `<test_results>\n${sanitizeForCDATA(condensedOutput)}\n</test_results>`,
1528
+ ...(res.exitCode !== 0
1529
+ ? {
1530
+ error: `Script failed with exit code ${res.exitCode}:\n${extractHighSignalError(res.stderr || res.stdout || '')}`,
1531
+ }
1532
+ : {}),
1395
1533
  };
1396
1534
  }
1397
1535
  catch (err) {
1536
+ const msg = err instanceof Error ? err.message : String(err);
1398
1537
  return {
1399
1538
  output: '',
1400
- error: `Failed to execute debug script: ${err instanceof Error ? err.message : String(err)}`,
1539
+ error: `Failed to execute debug script: ${extractHighSignalError(msg)}`,
1401
1540
  };
1402
1541
  }
1403
1542
  }
@@ -10,6 +10,7 @@ export interface ContextAgentResult {
10
10
  fromMemoryBank?: boolean;
11
11
  isParallel?: boolean;
12
12
  }
13
+ export declare function detectProjectType(workspaceRoot: string): Promise<string>;
13
14
  export interface IntentRoute {
14
15
  needsContext: boolean;
15
16
  targetAgent: 'CHAT' | 'EXECUTE';