minovative-mind-cli 2.11.5 → 2.12.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 +14 -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/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 +147 -9
- package/dist/services/contextAgent.d.ts +1 -0
- package/dist/services/contextAgent.js +29 -6
- package/dist/services/metrics.d.ts +10 -0
- package/dist/services/metrics.js +24 -0
- 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/systemPrompts.d.ts +1 -1
- package/dist/utils/systemPrompts.js +8 -2
- 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`:
|
|
@@ -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
|
@@ -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';
|
|
@@ -1110,6 +1110,137 @@ export async function listDirectory(workspaceRoot, dirPath, maxDepth = 3) {
|
|
|
1110
1110
|
* @param abortSignal - Optional AbortSignal to cancel execution.
|
|
1111
1111
|
* @returns A promise resolving to a {@link ToolResult} containing stdout/stderr or an error.
|
|
1112
1112
|
*/
|
|
1113
|
+
/**
|
|
1114
|
+
* High-signal error patterns across major languages and compiler toolchains.
|
|
1115
|
+
* Linker notes and candidate diagnostic details are explicitly retained.
|
|
1116
|
+
*/
|
|
1117
|
+
const HIGH_SIGNAL_ERROR_PATTERNS = [
|
|
1118
|
+
// C / C++ / Cython / Clang / GCC
|
|
1119
|
+
/fatal error:/i,
|
|
1120
|
+
/#error/i,
|
|
1121
|
+
/\berror:\b/i,
|
|
1122
|
+
/undefined symbol:/i,
|
|
1123
|
+
/undefined reference/i,
|
|
1124
|
+
/ld: symbol\(s\) not found/i,
|
|
1125
|
+
/clang: error:/i,
|
|
1126
|
+
/referenced from:/i,
|
|
1127
|
+
/note: expanded from macro/i,
|
|
1128
|
+
/note: candidate:/i,
|
|
1129
|
+
// Python / Pytest
|
|
1130
|
+
/Traceback \(most recent call last\):/i,
|
|
1131
|
+
/AssertionError:/i,
|
|
1132
|
+
/TypeError:/i,
|
|
1133
|
+
/ImportError:/i,
|
|
1134
|
+
/ModuleNotFoundError:/i,
|
|
1135
|
+
/FAILED \(failures=/i,
|
|
1136
|
+
/\bE\s{3}\b/,
|
|
1137
|
+
/\bFAIL\b/,
|
|
1138
|
+
// Node.js / TypeScript / Jest / Mocha
|
|
1139
|
+
/SyntaxError:/i,
|
|
1140
|
+
/ReferenceError:/i,
|
|
1141
|
+
/Error: Cannot find module/i,
|
|
1142
|
+
/\bFAIL\b/,
|
|
1143
|
+
/✕\s/,
|
|
1144
|
+
/TS\d{4}:/,
|
|
1145
|
+
// Rust / Cargo
|
|
1146
|
+
/error\[E\d+\]:/i,
|
|
1147
|
+
/fatal runtime error:/i,
|
|
1148
|
+
/panicked at/i,
|
|
1149
|
+
// Go
|
|
1150
|
+
/panic:/i,
|
|
1151
|
+
/cannot find package/i,
|
|
1152
|
+
/undefined:/i,
|
|
1153
|
+
/FAIL\t/,
|
|
1154
|
+
// Java / JVM
|
|
1155
|
+
/Exception in thread/i,
|
|
1156
|
+
/java\.lang\./i,
|
|
1157
|
+
/error: cannot find symbol/i,
|
|
1158
|
+
// Generic Fallbacks
|
|
1159
|
+
/\b(cannot|unable to|not found|command not found|no such file)\b/i,
|
|
1160
|
+
];
|
|
1161
|
+
/**
|
|
1162
|
+
* Noise filter pattern for compiler warning churn when total output is extensive.
|
|
1163
|
+
*/
|
|
1164
|
+
const COMPILER_WARNING_NOISE_PATTERN = /-W(deprecated-declarations|unused-variable|unused-parameter|unused-function|sign-compare|ignored-qualifiers|nullability-completeness)/i;
|
|
1165
|
+
/**
|
|
1166
|
+
* Start boundary patterns for common multi-line stack traces.
|
|
1167
|
+
*/
|
|
1168
|
+
const STACK_TRACE_START_PATTERNS = [
|
|
1169
|
+
/Traceback \(most recent call last\):/i,
|
|
1170
|
+
/Exception in thread/i,
|
|
1171
|
+
/panic:/i,
|
|
1172
|
+
/Error:\s*$/i,
|
|
1173
|
+
];
|
|
1174
|
+
/**
|
|
1175
|
+
* Condenses verbose command and test failure outputs into high-signal diagnostic error logs
|
|
1176
|
+
* with dynamic tail sizing and stack trace boundary snapping.
|
|
1177
|
+
*
|
|
1178
|
+
* @param rawError - The full error or standard error string.
|
|
1179
|
+
* @returns The condensed high-signal diagnostic output.
|
|
1180
|
+
*/
|
|
1181
|
+
export function extractHighSignalError(rawError) {
|
|
1182
|
+
if (!rawError || typeof rawError !== 'string')
|
|
1183
|
+
return '';
|
|
1184
|
+
// Threshold passthrough: if < 3000 chars or < 50 lines, return untouched
|
|
1185
|
+
const lines = rawError.split(/\r?\n/);
|
|
1186
|
+
if (rawError.length < 3000 && lines.length < 50) {
|
|
1187
|
+
return rawError;
|
|
1188
|
+
}
|
|
1189
|
+
// Filter out noise lines (e.g. repetitive compiler warning flags)
|
|
1190
|
+
const nonNoiseLines = lines.filter((l) => !COMPILER_WARNING_NOISE_PATTERN.test(l));
|
|
1191
|
+
// Find high-signal error lines
|
|
1192
|
+
const highSignalLines = [];
|
|
1193
|
+
const highSignalIndices = new Set();
|
|
1194
|
+
for (let i = 0; i < nonNoiseLines.length; i++) {
|
|
1195
|
+
const line = nonNoiseLines[i];
|
|
1196
|
+
if (HIGH_SIGNAL_ERROR_PATTERNS.some((p) => p.test(line))) {
|
|
1197
|
+
highSignalLines.push(line);
|
|
1198
|
+
highSignalIndices.add(i);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
// Dynamic tail sizing: Math.min(50, Math.max(15, Math.floor(totalLines * 0.15)))
|
|
1202
|
+
const dynamicTailCount = Math.min(50, Math.max(15, Math.floor(nonNoiseLines.length * 0.15)));
|
|
1203
|
+
let tailStartIndex = Math.max(0, nonNoiseLines.length - dynamicTailCount);
|
|
1204
|
+
// Stack-trace boundary snapping: if a stack trace starts up to 15 lines above tailStartIndex, snap upward
|
|
1205
|
+
const lookbackMax = Math.max(0, tailStartIndex - 15);
|
|
1206
|
+
for (let i = tailStartIndex; i >= lookbackMax; i--) {
|
|
1207
|
+
if (STACK_TRACE_START_PATTERNS.some((p) => p.test(nonNoiseLines[i]))) {
|
|
1208
|
+
tailStartIndex = i;
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const tailLines = nonNoiseLines.slice(tailStartIndex);
|
|
1213
|
+
// If high-signal lines are all already present within the tail, return tail directly (with note if truncated)
|
|
1214
|
+
const isHighSignalInTail = Array.from(highSignalIndices).every((idx) => idx >= tailStartIndex);
|
|
1215
|
+
if (isHighSignalInTail && tailStartIndex === 0) {
|
|
1216
|
+
return nonNoiseLines.join('\n');
|
|
1217
|
+
}
|
|
1218
|
+
const resultBlocks = [];
|
|
1219
|
+
// Add high signal summary if there are high-signal lines occurring before the tail
|
|
1220
|
+
const precedingHighSignal = highSignalLines.filter((_, idx) => {
|
|
1221
|
+
const originalIndex = Array.from(highSignalIndices)[idx];
|
|
1222
|
+
return originalIndex < tailStartIndex;
|
|
1223
|
+
});
|
|
1224
|
+
if (precedingHighSignal.length > 0) {
|
|
1225
|
+
resultBlocks.push(`--- [High-Signal Diagnostic Highlights (${precedingHighSignal.length} findings)] ---\n` +
|
|
1226
|
+
precedingHighSignal.join('\n'));
|
|
1227
|
+
}
|
|
1228
|
+
if (tailStartIndex > 0) {
|
|
1229
|
+
resultBlocks.push(`--- [Output Tail (last ${tailLines.length} of ${nonNoiseLines.length} lines)] ---\n` +
|
|
1230
|
+
tailLines.join('\n'));
|
|
1231
|
+
}
|
|
1232
|
+
else {
|
|
1233
|
+
resultBlocks.push(tailLines.join('\n'));
|
|
1234
|
+
}
|
|
1235
|
+
const condensedResult = resultBlocks.join('\n\n');
|
|
1236
|
+
const condensedLines = condensedResult.split(/\r?\n/).length;
|
|
1237
|
+
const linesPruned = Math.max(0, lines.length - condensedLines);
|
|
1238
|
+
const charsPruned = Math.max(0, rawError.length - condensedResult.length);
|
|
1239
|
+
if (linesPruned > 0 || charsPruned > 0) {
|
|
1240
|
+
recordRecoveryPrunedLogVolume(linesPruned, charsPruned);
|
|
1241
|
+
}
|
|
1242
|
+
return condensedResult;
|
|
1243
|
+
}
|
|
1113
1244
|
export async function runCommand(workspaceRoot, command, abortSignal) {
|
|
1114
1245
|
// Reject sudo or interactive root commands to prevent non-interactive subshell deadlocks and security escalation
|
|
1115
1246
|
if (/\bsudo\b/i.test(command)) {
|
|
@@ -1137,12 +1268,13 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
|
|
|
1137
1268
|
}
|
|
1138
1269
|
catch (err) {
|
|
1139
1270
|
const message = err instanceof Error ? err.message : String(err);
|
|
1140
|
-
//
|
|
1271
|
+
// Condense error output with high-signal extraction and dynamic tail sizing
|
|
1272
|
+
const condensedError = extractHighSignalError(message);
|
|
1141
1273
|
const MAX_ERR_OUTPUT = 30_000;
|
|
1142
|
-
const truncatedMsg =
|
|
1143
|
-
?
|
|
1144
|
-
`\n\n... (Error output truncated: ${
|
|
1145
|
-
:
|
|
1274
|
+
const truncatedMsg = condensedError.length > MAX_ERR_OUTPUT
|
|
1275
|
+
? condensedError.substring(0, MAX_ERR_OUTPUT) +
|
|
1276
|
+
`\n\n... (Error output truncated: ${condensedError.length} bytes exceeded 30KB limit. Pipe to a file if you need full logs.)`
|
|
1277
|
+
: condensedError;
|
|
1146
1278
|
return { output: '', error: `Command failed: ${truncatedMsg}` };
|
|
1147
1279
|
}
|
|
1148
1280
|
}
|
|
@@ -1389,15 +1521,21 @@ export async function runDebugScript(workspaceRoot, language, code, abortSignal)
|
|
|
1389
1521
|
if (!finalOutput.trim()) {
|
|
1390
1522
|
finalOutput = 'Script executed successfully with no output.';
|
|
1391
1523
|
}
|
|
1524
|
+
const condensedOutput = extractHighSignalError(finalOutput.trim());
|
|
1392
1525
|
return {
|
|
1393
|
-
output: `<test_results>\n${sanitizeForCDATA(
|
|
1394
|
-
...(res.exitCode !== 0
|
|
1526
|
+
output: `<test_results>\n${sanitizeForCDATA(condensedOutput)}\n</test_results>`,
|
|
1527
|
+
...(res.exitCode !== 0
|
|
1528
|
+
? {
|
|
1529
|
+
error: `Script failed with exit code ${res.exitCode}:\n${extractHighSignalError(res.stderr || res.stdout || '')}`,
|
|
1530
|
+
}
|
|
1531
|
+
: {}),
|
|
1395
1532
|
};
|
|
1396
1533
|
}
|
|
1397
1534
|
catch (err) {
|
|
1535
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1398
1536
|
return {
|
|
1399
1537
|
output: '',
|
|
1400
|
-
error: `Failed to execute debug script: ${
|
|
1538
|
+
error: `Failed to execute debug script: ${extractHighSignalError(msg)}`,
|
|
1401
1539
|
};
|
|
1402
1540
|
}
|
|
1403
1541
|
}
|
|
@@ -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';
|
|
@@ -51,8 +51,15 @@ function boundToolOutput(output) {
|
|
|
51
51
|
truncationMarker: `\n\n... [Tool output truncated: exceeded ${MAX_TOOL_OUTPUT_TOKENS} tokens limit] ...\n`,
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
-
async function detectProjectType(workspaceRoot) {
|
|
54
|
+
export async function detectProjectType(workspaceRoot) {
|
|
55
55
|
const types = [];
|
|
56
|
+
// Detect Host Platform & Architecture
|
|
57
|
+
const platformName = process.platform === 'darwin' ? 'Darwin' : process.platform === 'win32' ? 'Windows' : 'Linux';
|
|
58
|
+
const isAppleSilicon = process.platform === 'darwin' && process.arch === 'arm64';
|
|
59
|
+
const hostDesc = isAppleSilicon
|
|
60
|
+
? `Host: ${platformName} ${process.arch} (Apple Silicon)`
|
|
61
|
+
: `Host: ${platformName} ${process.arch}`;
|
|
62
|
+
types.push(hostDesc);
|
|
56
63
|
const fileExists = async (fileName) => {
|
|
57
64
|
try {
|
|
58
65
|
await fs.access(path.join(workspaceRoot, fileName));
|
|
@@ -62,6 +69,13 @@ async function detectProjectType(workspaceRoot) {
|
|
|
62
69
|
return false;
|
|
63
70
|
}
|
|
64
71
|
};
|
|
72
|
+
// C / C++ / Native Build Toolchains
|
|
73
|
+
if (await fileExists('CMakeLists.txt'))
|
|
74
|
+
types.push('CMake');
|
|
75
|
+
if (await fileExists('Makefile'))
|
|
76
|
+
types.push('Makefile');
|
|
77
|
+
if (await fileExists('meson.build'))
|
|
78
|
+
types.push('Meson');
|
|
65
79
|
// Node.js Ecosystem
|
|
66
80
|
if (await fileExists('package.json')) {
|
|
67
81
|
types.push('Node.js');
|
|
@@ -93,17 +107,17 @@ async function detectProjectType(workspaceRoot) {
|
|
|
93
107
|
types.push('NestJS');
|
|
94
108
|
if (deps['vite'])
|
|
95
109
|
types.push('Vite');
|
|
96
|
-
if (deps['tailwindcss'])
|
|
97
|
-
types.push('Tailwind CSS');
|
|
98
|
-
if (deps['firebase'])
|
|
99
|
-
types.push('Firebase');
|
|
100
110
|
}
|
|
101
111
|
catch { }
|
|
102
112
|
if (await fileExists('tsconfig.json'))
|
|
103
113
|
types.push('TypeScript');
|
|
104
114
|
}
|
|
105
115
|
// Python Ecosystem
|
|
106
|
-
if ((await fileExists('pyproject.toml')) ||
|
|
116
|
+
if ((await fileExists('pyproject.toml')) ||
|
|
117
|
+
(await fileExists('requirements.txt')) ||
|
|
118
|
+
(await fileExists('setup.py')) ||
|
|
119
|
+
(await fileExists('setup.cfg')) ||
|
|
120
|
+
(await fileExists('Pipfile'))) {
|
|
107
121
|
types.push('Python');
|
|
108
122
|
try {
|
|
109
123
|
const reqs = (await fileExists('requirements.txt'))
|
|
@@ -119,6 +133,15 @@ async function detectProjectType(workspaceRoot) {
|
|
|
119
133
|
types.push('Flask');
|
|
120
134
|
if (combined.includes('fastapi'))
|
|
121
135
|
types.push('FastAPI');
|
|
136
|
+
if (combined.includes('cython') || (await fileExists('setup.py'))) {
|
|
137
|
+
try {
|
|
138
|
+
const files = await fs.readdir(workspaceRoot);
|
|
139
|
+
if (files.some((f) => f.endsWith('.pyx') || f.endsWith('.pxd') || f.endsWith('.c') || f.endsWith('.cpp'))) {
|
|
140
|
+
types.push('Cython/C-Extensions');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch { }
|
|
144
|
+
}
|
|
122
145
|
}
|
|
123
146
|
catch { }
|
|
124
147
|
}
|
|
@@ -19,6 +19,8 @@ export interface MetricCollector {
|
|
|
19
19
|
recordCacheHit(cacheType?: 'investigation' | 'read'): void;
|
|
20
20
|
recordCacheMiss(cacheType?: 'investigation' | 'read'): void;
|
|
21
21
|
recordCachePerformance(cacheType: 'investigation' | 'read', durationMs: number): void;
|
|
22
|
+
recordCircuitBreakerTrip?(): void;
|
|
23
|
+
recordPrunedLogVolume?(lines: number, chars: number): void;
|
|
22
24
|
recordWriteFailure?(): void;
|
|
23
25
|
recordModifyFailure?(): void;
|
|
24
26
|
recordToolFailure?(toolName?: string): void;
|
|
@@ -33,3 +35,11 @@ export declare function getTurnTotals(): {
|
|
|
33
35
|
outputTokens: number;
|
|
34
36
|
cachedTokens: number;
|
|
35
37
|
};
|
|
38
|
+
export declare function recordRecoveryCircuitBreakerTrip(): void;
|
|
39
|
+
export declare function recordRecoveryPrunedLogVolume(lines: number, chars: number): void;
|
|
40
|
+
export declare function getRecoveryMetrics(): {
|
|
41
|
+
circuitBreakerTrips: number;
|
|
42
|
+
prunedLines: number;
|
|
43
|
+
prunedChars: number;
|
|
44
|
+
};
|
|
45
|
+
export declare function resetRecoveryMetrics(): void;
|