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.
- package/README.md +27 -1
- package/dist/commands/chat.js +3 -1
- package/dist/commands/eval.d.ts +22 -0
- package/dist/commands/eval.js +141 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/agent/slashCommands.js +4 -2
- package/dist/services/agent/toolLoop.d.ts +4 -0
- package/dist/services/agent/toolLoop.js +61 -10
- package/dist/services/agent-tools.d.ts +5 -5
- package/dist/services/agent-tools.js +150 -11
- package/dist/services/contextAgent.d.ts +1 -0
- package/dist/services/contextAgent.js +29 -6
- package/dist/services/ideOptimization.d.ts +15 -0
- package/dist/services/ideOptimization.js +169 -0
- package/dist/services/metrics.d.ts +10 -0
- package/dist/services/metrics.js +24 -0
- package/dist/services/orchestration/messageBus.d.ts +81 -41
- package/dist/services/orchestration/messageBus.js +242 -98
- package/dist/services/orchestration/orchestrator.d.ts +6 -6
- package/dist/services/orchestration/orchestrator.js +32 -21
- package/dist/services/orchestration/scopedTools.d.ts +7 -1
- package/dist/services/orchestration/scopedTools.js +45 -9
- package/dist/services/orchestration/subAgent.d.ts +19 -17
- package/dist/services/orchestration/subAgent.js +98 -81
- package/dist/services/swebench/gitDiffExtractor.d.ts +57 -0
- package/dist/services/swebench/gitDiffExtractor.js +209 -0
- package/dist/services/swebench/index.d.ts +4 -0
- package/dist/services/swebench/index.js +4 -0
- package/dist/services/swebench/instanceLoader.d.ts +21 -0
- package/dist/services/swebench/instanceLoader.js +171 -0
- package/dist/services/swebench/sweBenchRunnerService.d.ts +38 -0
- package/dist/services/swebench/sweBenchRunnerService.js +618 -0
- package/dist/services/swebench/types.d.ts +167 -0
- package/dist/services/swebench/types.js +7 -0
- package/dist/services/verificationService.js +3 -0
- package/dist/utils/fuzzyMatch.d.ts +51 -21
- package/dist/utils/fuzzyMatch.js +37 -122
- package/dist/utils/projectStorage.js +10 -5
- package/dist/utils/systemPrompts.d.ts +1 -1
- package/dist/utils/systemPrompts.js +10 -4
- package/oclif.manifest.json +137 -1
- 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 (
|
|
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) ยท
|
package/dist/commands/chat.js
CHANGED
|
@@ -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
package/dist/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
-
...(
|
|
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
|
-
*
|
|
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
|
|
145
|
-
* @
|
|
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
|
-
//
|
|
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 =
|
|
1143
|
-
?
|
|
1144
|
-
`\n\n... (Error output truncated: ${
|
|
1145
|
-
:
|
|
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(
|
|
1394
|
-
...(res.exitCode !== 0
|
|
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: ${
|
|
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';
|