minovative-mind-cli 2.9.1 → 2.11.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 -6
- package/dist/services/agent/commandApproval.js +5 -2
- package/dist/services/agent/slashCommands.js +90 -53
- package/dist/services/agent-tools.d.ts +4 -3
- package/dist/services/agent-tools.js +32 -79
- package/dist/services/agent.d.ts +5 -6
- package/dist/services/agent.js +11 -15
- package/dist/services/ai.d.ts +21 -1
- package/dist/services/ai.js +236 -11
- package/dist/services/chatHistoryService.d.ts +95 -2
- package/dist/services/chatHistoryService.js +236 -9
- package/dist/services/contextAgent.js +196 -81
- package/dist/services/investigationComplexity.d.ts +1 -1
- package/dist/services/investigationComplexity.js +1 -1
- package/dist/services/orchestration/investigationAgent.js +101 -84
- package/dist/services/orchestration/investigationCache.d.ts +80 -5
- package/dist/services/orchestration/investigationCache.js +570 -41
- package/dist/services/orchestration/investigationOrchestrator.js +17 -4
- package/dist/services/orchestration/orchestrator.js +6 -3
- package/dist/services/orchestration/scopedTools.js +5 -0
- package/dist/services/orchestration/subAgent.d.ts +31 -1
- package/dist/services/orchestration/subAgent.js +153 -2
- package/dist/utils/analysisRunner.d.ts +29 -0
- package/dist/utils/analysisRunner.js +200 -5
- package/dist/utils/contextPrompts.d.ts +20 -4
- package/dist/utils/contextPrompts.js +158 -23
- package/dist/utils/historyPrompt.d.ts +92 -1
- package/dist/utils/historyPrompt.js +166 -2
- package/dist/utils/symbolExtractor.d.ts +12 -0
- package/dist/utils/symbolExtractor.js +946 -0
- package/dist/utils/systemPrompts.d.ts +5 -4
- package/dist/utils/systemPrompts.js +46 -14
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
|
@@ -64,6 +64,8 @@ export class InvestigationOrchestrator {
|
|
|
64
64
|
const MAX_CONCURRENT = 2;
|
|
65
65
|
const results = [];
|
|
66
66
|
for (let i = 0; i < agents.length; i += MAX_CONCURRENT) {
|
|
67
|
+
if (abortSignal.aborted)
|
|
68
|
+
break;
|
|
67
69
|
const chunk = agents.slice(i, i + MAX_CONCURRENT);
|
|
68
70
|
const chunkAssignments = agentAssignments.slice(i, i + MAX_CONCURRENT);
|
|
69
71
|
const chunkPromises = chunk.map((agent, chunkIndex) => {
|
|
@@ -107,8 +109,10 @@ export class InvestigationOrchestrator {
|
|
|
107
109
|
// 5. Reduce: Merge results
|
|
108
110
|
const mergedResult = this.reduceResults(successfulResults, projectTree, projectType);
|
|
109
111
|
// 6. Auto-trace reverse dependencies (same logic as contextAgent.ts)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
if (!abortSignal.aborted) {
|
|
113
|
+
const allSelectedFiles = Array.from(mergedResult.relevantFiles.keys());
|
|
114
|
+
await this.autoTraceReverseDeps(workspaceRoot, allSelectedFiles, mergedResult.relevantFiles, onProgress);
|
|
115
|
+
}
|
|
112
116
|
// 7. Log summary
|
|
113
117
|
const cacheStats = readCache.getStats();
|
|
114
118
|
const totalTokens = results.reduce((sum, r) => sum + r.creditsUsed, 0);
|
|
@@ -121,8 +125,17 @@ export class InvestigationOrchestrator {
|
|
|
121
125
|
`Cache hits: ${cacheStats.hitCount}\n` +
|
|
122
126
|
` Duration: ${duration}s | Tokens: ${totalTokens.toLocaleString()} ${pc.dim(`(Input: ${totalInputTokens.toLocaleString()}, Output: ${totalOutputTokens.toLocaleString()})`)}`);
|
|
123
127
|
if (mergedResult && mergedResult.relevantFiles.size > 0) {
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
try {
|
|
129
|
+
const { saveInvestigation } = await import('./investigationCache.js');
|
|
130
|
+
const allDomains = Array.from(new Set(agentAssignments.flatMap((a) => a.domains)));
|
|
131
|
+
await saveInvestigation(workspaceRoot, userRequest, Array.from(mergedResult.relevantFiles.keys()), mergedResult.summary, { topics: allDomains }, abortSignal);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err?.name === 'AbortError' || abortSignal?.aborted) {
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
debugLog(`Failed to save parallel investigation to cache: ${err?.message || err}`);
|
|
138
|
+
}
|
|
126
139
|
}
|
|
127
140
|
return mergedResult;
|
|
128
141
|
}
|
|
@@ -27,12 +27,13 @@ You must output ONLY raw JSON representing the TaskGraph object. Do not wrap in
|
|
|
27
27
|
</identity>
|
|
28
28
|
|
|
29
29
|
<requirements>
|
|
30
|
-
- Break the work down logically based on file isolation and dependency chains.
|
|
31
|
-
- Tasks that don't depend on each other will run in parallel.
|
|
30
|
+
- Break the work down logically based on file isolation, domain boundaries, and dependency chains.
|
|
31
|
+
- Tasks that don't depend on each other will run in parallel (up to 2 concurrent agents per wave).
|
|
32
32
|
- If a task depends on another, it must list its ID in "dependsOn".
|
|
33
33
|
- Do NOT create cyclic dependencies (A -> B -> A).
|
|
34
34
|
- If the entire objective is very simple and only requires modifying 1-2 files sequentially, output a graph with exactly 1 task.
|
|
35
|
-
- Ensure 'targetFiles' lists all files the task will modify.
|
|
35
|
+
- Ensure 'targetFiles' lists all files the task will modify. Isolate targetFiles across parallel tasks in the same wave to avoid file lock contention.
|
|
36
|
+
- Use 'readOnlyFiles' for files the task needs to inspect without modifying.
|
|
36
37
|
</requirements>
|
|
37
38
|
|
|
38
39
|
<json_schema>
|
|
@@ -123,6 +124,8 @@ export class Orchestrator {
|
|
|
123
124
|
const MAX_CONCURRENT = 2;
|
|
124
125
|
const toolLogs = [];
|
|
125
126
|
for (let i = 0; i < wave.taskIds.length; i += MAX_CONCURRENT) {
|
|
127
|
+
if (signal.aborted)
|
|
128
|
+
break;
|
|
126
129
|
const chunk = wave.taskIds.slice(i, i + MAX_CONCURRENT);
|
|
127
130
|
const wavePromises = chunk.map((taskId) => {
|
|
128
131
|
const taskDef = graph.tasks.find((t) => t.id === taskId);
|
|
@@ -160,6 +160,11 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
|
|
|
160
160
|
targetDesc = String(args.filePath);
|
|
161
161
|
actionDesc = 'Deleted';
|
|
162
162
|
}
|
|
163
|
+
else if (name === 'run_debug_script') {
|
|
164
|
+
targetDesc = String(args.language ?? 'node');
|
|
165
|
+
actionDesc = 'Ran debug script';
|
|
166
|
+
resultSummary = typeof result === 'object' && result?.error ? result.error : 'Completed debug script';
|
|
167
|
+
}
|
|
163
168
|
else if (name === 'run_fuzz_probe') {
|
|
164
169
|
targetDesc = String(args.language ?? 'node');
|
|
165
170
|
actionDesc = 'Ran fuzz probe';
|
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { MessageBus } from './messageBus.js';
|
|
8
8
|
import { FileLockRegistry } from './fileLockRegistry.js';
|
|
9
|
+
/**
|
|
10
|
+
* Scopes workspace file blocks within a context string by replacing full implementation
|
|
11
|
+
* bodies with compact AST declaration outlines (types, classes, function signatures),
|
|
12
|
+
* drastically conserving token overhead for parallel sub-agents.
|
|
13
|
+
*
|
|
14
|
+
* @param contextStr The raw context string containing workspace_file blocks.
|
|
15
|
+
* @returns Context string with scoped workspace_file blocks.
|
|
16
|
+
*/
|
|
17
|
+
export declare function scopeContext(contextStr: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Generates a concise reference string for completed historical tool executions
|
|
20
|
+
* to eliminate token bloat while keeping execution history coherent for the model.
|
|
21
|
+
*
|
|
22
|
+
* @param toolName Name of the executed tool.
|
|
23
|
+
* @param output Output text or object from the tool execution.
|
|
24
|
+
* @param args Arguments passed to the tool call.
|
|
25
|
+
* @returns Compacted tool execution reference string.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createCompactToolReference(toolName: string, output: any, args?: Record<string, any>): string;
|
|
9
28
|
/**
|
|
10
29
|
* Result returned when a sub-agent completes its execution.
|
|
11
30
|
*/
|
|
@@ -43,7 +62,9 @@ export declare class SubAgentRunner {
|
|
|
43
62
|
static readonly STALL_TIMEOUT_MS = 300000;
|
|
44
63
|
constructor(taskId: string, intent: string, workspaceRoot: string, bus: MessageBus, locks: FileLockRegistry, globalContext: string, onProgress?: ((msg: string) => void) | undefined, onTool?: ((msg: string) => void) | undefined);
|
|
45
64
|
/**
|
|
46
|
-
* Constructs the base system instruction for this specific agent
|
|
65
|
+
* Constructs the base system instruction for this specific agent,
|
|
66
|
+
* injecting scoped AST outlines for referenced workspace files to eliminate
|
|
67
|
+
* multi-megabyte context bloat during parallel sub-agent execution.
|
|
47
68
|
*/
|
|
48
69
|
private buildSystemInstruction;
|
|
49
70
|
/**
|
|
@@ -57,6 +78,15 @@ export declare class SubAgentRunner {
|
|
|
57
78
|
* an error occurs, or the stall timeout is hit.
|
|
58
79
|
*/
|
|
59
80
|
execute(signal: AbortSignal): Promise<SubAgentResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Compacts older tool responses in the chat session history into concise references
|
|
83
|
+
* (e.g. `[Read X lines from file.ts - tool execution completed]`), drastically reducing
|
|
84
|
+
* context window bloat across multi-turn sub-agent executions while preserving
|
|
85
|
+
* Gemini's functionCall <-> functionResponse protocol.
|
|
86
|
+
*
|
|
87
|
+
* @param turnsToKeep Number of recent turns to preserve in full (default: 1).
|
|
88
|
+
*/
|
|
89
|
+
compactOlderToolResponses(turnsToKeep?: number): void;
|
|
60
90
|
/**
|
|
61
91
|
* Accumulates token usage from the chat session.
|
|
62
92
|
*/
|
|
@@ -11,6 +11,108 @@ import { debugLog } from '../../utils/logger.js';
|
|
|
11
11
|
import { runWithAgentId } from '../../utils/asyncContext.js';
|
|
12
12
|
import { clearReadHistory } from '../../utils/fileReadGuard.js';
|
|
13
13
|
import { formatToolCall } from '../agent/toolLoop.js';
|
|
14
|
+
import { extractDeclarationsOutline } from '../../utils/symbolExtractor.js';
|
|
15
|
+
import { sanitizeForCDATA } from '../../utils/contextPrompts.js';
|
|
16
|
+
/**
|
|
17
|
+
* Scopes workspace file blocks within a context string by replacing full implementation
|
|
18
|
+
* bodies with compact AST declaration outlines (types, classes, function signatures),
|
|
19
|
+
* drastically conserving token overhead for parallel sub-agents.
|
|
20
|
+
*
|
|
21
|
+
* @param contextStr The raw context string containing workspace_file blocks.
|
|
22
|
+
* @returns Context string with scoped workspace_file blocks.
|
|
23
|
+
*/
|
|
24
|
+
export function scopeContext(contextStr) {
|
|
25
|
+
if (!contextStr)
|
|
26
|
+
return '';
|
|
27
|
+
return contextStr.replace(/<workspace_file path="([^"]+)"([^>]*)>[\s\r\n]*<content_data><!\[CDATA\[([\s\S]*?)\]\](?:\\u200B)?>[\s\r\n]*<\/content_data>[\s\r\n]*<\/workspace_file>/g, (match, filePath, attrs, rawContent) => {
|
|
28
|
+
if (attrs.includes('scoped="outline"'))
|
|
29
|
+
return match;
|
|
30
|
+
const unescaped = rawContent.replace(/\]\]\\u200B>/g, ']]>');
|
|
31
|
+
const outline = extractDeclarationsOutline(unescaped, filePath) || unescaped;
|
|
32
|
+
const sanitized = sanitizeForCDATA(outline);
|
|
33
|
+
return `<workspace_file path="${filePath}"${attrs} scoped="outline">\n<content_data><![CDATA[\n${sanitized}\n]]\\u200B></content_data>\n</workspace_file>`;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Locates the matching functionCall in the preceding model response entry.
|
|
38
|
+
*
|
|
39
|
+
* @param modelEntry The preceding Content entry from the model.
|
|
40
|
+
* @param toolName The name of the function call.
|
|
41
|
+
* @param partIndex Index of the function response part.
|
|
42
|
+
* @returns The matching functionCall object with args if found.
|
|
43
|
+
*/
|
|
44
|
+
function findMatchingFunctionCall(modelEntry, toolName, partIndex) {
|
|
45
|
+
if (!modelEntry || modelEntry.role !== 'model' || !Array.isArray(modelEntry.parts))
|
|
46
|
+
return null;
|
|
47
|
+
const callParts = modelEntry.parts.filter((p) => p && typeof p === 'object' && 'functionCall' in p && p.functionCall);
|
|
48
|
+
if (partIndex < callParts.length) {
|
|
49
|
+
const call = callParts[partIndex].functionCall;
|
|
50
|
+
if (call && call.name === toolName)
|
|
51
|
+
return call;
|
|
52
|
+
}
|
|
53
|
+
// Fallback: search by toolName
|
|
54
|
+
const match = callParts.find((p) => p.functionCall?.name === toolName);
|
|
55
|
+
return match ? match.functionCall : null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generates a concise reference string for completed historical tool executions
|
|
59
|
+
* to eliminate token bloat while keeping execution history coherent for the model.
|
|
60
|
+
*
|
|
61
|
+
* @param toolName Name of the executed tool.
|
|
62
|
+
* @param output Output text or object from the tool execution.
|
|
63
|
+
* @param args Arguments passed to the tool call.
|
|
64
|
+
* @returns Compacted tool execution reference string.
|
|
65
|
+
*/
|
|
66
|
+
export function createCompactToolReference(toolName, output, args) {
|
|
67
|
+
if (typeof output !== 'string') {
|
|
68
|
+
return `[${toolName} tool execution completed]`;
|
|
69
|
+
}
|
|
70
|
+
// If already compacted, return as-is
|
|
71
|
+
if (output.startsWith('[') && output.endsWith('completed]')) {
|
|
72
|
+
return output;
|
|
73
|
+
}
|
|
74
|
+
const lineCount = output.split('\n').length;
|
|
75
|
+
if (toolName === 'read_file') {
|
|
76
|
+
const file = args?.filePath ? `from ${args.filePath}` : 'from file';
|
|
77
|
+
return `[Read ${lineCount} lines ${file} - tool execution completed]`;
|
|
78
|
+
}
|
|
79
|
+
if (toolName === 'grep_search' || toolName === 'search_codebase') {
|
|
80
|
+
const pattern = args?.pattern ? `for "${args.pattern}"` : '';
|
|
81
|
+
return `[Grep search ${pattern} completed - ${lineCount} lines returned]`.replace(/\s+/g, ' ');
|
|
82
|
+
}
|
|
83
|
+
if (toolName === 'list_directory') {
|
|
84
|
+
const dir = args?.dirPath ? `for "${args.dirPath}"` : '';
|
|
85
|
+
return `[Directory listing ${dir} completed - ${lineCount} entries]`.replace(/\s+/g, ' ');
|
|
86
|
+
}
|
|
87
|
+
if (toolName === 'modify_file') {
|
|
88
|
+
const file = args?.filePath ? `${args.filePath}` : 'file';
|
|
89
|
+
return `[Modified ${file} - tool execution completed]`;
|
|
90
|
+
}
|
|
91
|
+
if (toolName === 'write_file') {
|
|
92
|
+
const file = args?.filePath ? `${args.filePath}` : 'file';
|
|
93
|
+
return `[Wrote ${file} - tool execution completed]`;
|
|
94
|
+
}
|
|
95
|
+
if (toolName === 'find_dependencies') {
|
|
96
|
+
const file = args?.filePath ? `for ${args.filePath}` : '';
|
|
97
|
+
return `[Dependency trace ${file} completed]`.replace(/\s+/g, ' ');
|
|
98
|
+
}
|
|
99
|
+
if (toolName === 'find_recent_changes') {
|
|
100
|
+
return `[Recent changes lookup completed]`;
|
|
101
|
+
}
|
|
102
|
+
if (toolName === 'run_command' ||
|
|
103
|
+
toolName === 'run_debug_script' ||
|
|
104
|
+
toolName === 'run_analysis_script' ||
|
|
105
|
+
toolName === 'run_fuzz_probe' ||
|
|
106
|
+
toolName === 'check_heap_delta' ||
|
|
107
|
+
toolName === 'check_behavioral_drift') {
|
|
108
|
+
const cmd = args?.command ? ` "${args.command.slice(0, 50)}"` : '';
|
|
109
|
+
return `[Command/Script${cmd} execution completed (${lineCount} lines output)]`;
|
|
110
|
+
}
|
|
111
|
+
if (output.length > 300) {
|
|
112
|
+
return `[${toolName} tool execution completed (${lineCount} lines)]`;
|
|
113
|
+
}
|
|
114
|
+
return output;
|
|
115
|
+
}
|
|
14
116
|
/**
|
|
15
117
|
* Executes a sub-agent task with full health monitoring, tool wrapping,
|
|
16
118
|
* and orchestration integration.
|
|
@@ -52,9 +154,12 @@ export class SubAgentRunner {
|
|
|
52
154
|
});
|
|
53
155
|
}
|
|
54
156
|
/**
|
|
55
|
-
* Constructs the base system instruction for this specific agent
|
|
157
|
+
* Constructs the base system instruction for this specific agent,
|
|
158
|
+
* injecting scoped AST outlines for referenced workspace files to eliminate
|
|
159
|
+
* multi-megabyte context bloat during parallel sub-agent execution.
|
|
56
160
|
*/
|
|
57
161
|
buildSystemInstruction() {
|
|
162
|
+
const scopedContext = scopeContext(this.globalContext);
|
|
58
163
|
return (`<identity>\n` +
|
|
59
164
|
`You are an autonomous Senior software developer sub-agent executing a specific portion of a larger task.\n` +
|
|
60
165
|
`</identity>\n\n` +
|
|
@@ -64,7 +169,7 @@ export class SubAgentRunner {
|
|
|
64
169
|
`</task_info>\n\n` +
|
|
65
170
|
`<reference_context>\n` +
|
|
66
171
|
`DO NOT IMPLEMENT THIS FULL REQUEST. THIS IS JUST FOR CONTEXT.\n\n` +
|
|
67
|
-
`${
|
|
172
|
+
`${scopedContext}\n` +
|
|
68
173
|
`</reference_context>\n\n` +
|
|
69
174
|
`<critical_guidelines>\n` +
|
|
70
175
|
`1. You are ONE worker in a team. Focus EXCLUSIVELY on your specific objective: "${this.intent}".\n` +
|
|
@@ -162,6 +267,10 @@ export class SubAgentRunner {
|
|
|
162
267
|
}
|
|
163
268
|
if (crashed || signal.aborted)
|
|
164
269
|
break;
|
|
270
|
+
// Compact older tool responses in chat history before sending new turn
|
|
271
|
+
if (turns > 0) {
|
|
272
|
+
this.compactOlderToolResponses(1);
|
|
273
|
+
}
|
|
165
274
|
this.pingHeartbeat();
|
|
166
275
|
// Feed tool results back to the model
|
|
167
276
|
turnResult = await this.chat.sendMessage(toolResponses, undefined, signal);
|
|
@@ -199,6 +308,48 @@ export class SubAgentRunner {
|
|
|
199
308
|
};
|
|
200
309
|
});
|
|
201
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Compacts older tool responses in the chat session history into concise references
|
|
313
|
+
* (e.g. `[Read X lines from file.ts - tool execution completed]`), drastically reducing
|
|
314
|
+
* context window bloat across multi-turn sub-agent executions while preserving
|
|
315
|
+
* Gemini's functionCall <-> functionResponse protocol.
|
|
316
|
+
*
|
|
317
|
+
* @param turnsToKeep Number of recent turns to preserve in full (default: 1).
|
|
318
|
+
*/
|
|
319
|
+
compactOlderToolResponses(turnsToKeep = 1) {
|
|
320
|
+
const rawHistory = this.chat.getRawHistory();
|
|
321
|
+
// 1 turn = 1 user Content + 1 model Content pair (2 entries)
|
|
322
|
+
const cutoffIndex = Math.max(0, rawHistory.length - turnsToKeep * 2);
|
|
323
|
+
if (cutoffIndex <= 0)
|
|
324
|
+
return;
|
|
325
|
+
for (let i = 0; i < cutoffIndex; i++) {
|
|
326
|
+
const entry = rawHistory[i];
|
|
327
|
+
if (entry && entry.role === 'user' && Array.isArray(entry.parts)) {
|
|
328
|
+
const prevModelEntry = i > 0 ? rawHistory[i - 1] : undefined;
|
|
329
|
+
let funcRespIdx = 0;
|
|
330
|
+
for (const part of entry.parts) {
|
|
331
|
+
if (part && typeof part === 'object' && 'functionResponse' in part && part.functionResponse) {
|
|
332
|
+
const funcResp = part.functionResponse;
|
|
333
|
+
const toolName = funcResp.name || 'tool';
|
|
334
|
+
const matchingCall = findMatchingFunctionCall(prevModelEntry, toolName, funcRespIdx);
|
|
335
|
+
const args = matchingCall?.args;
|
|
336
|
+
if (funcResp.response && typeof funcResp.response === 'object') {
|
|
337
|
+
const respObj = funcResp.response;
|
|
338
|
+
if (typeof respObj.output === 'string') {
|
|
339
|
+
respObj.output = createCompactToolReference(toolName, respObj.output, args);
|
|
340
|
+
}
|
|
341
|
+
if (typeof respObj.result === 'string') {
|
|
342
|
+
respObj.result = createCompactToolReference(toolName, respObj.result, args);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
funcRespIdx++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Also trigger proxy chat session pruning to collapse any remaining long fields
|
|
351
|
+
this.chat.pruneToolOutputHistory();
|
|
352
|
+
}
|
|
202
353
|
/**
|
|
203
354
|
* Accumulates token usage from the chat session.
|
|
204
355
|
*/
|
|
@@ -10,6 +10,8 @@ export interface EphemeralScriptOptions {
|
|
|
10
10
|
maxOutputChars?: number;
|
|
11
11
|
/** AbortSignal to cancel execution. */
|
|
12
12
|
abortSignal?: AbortSignal;
|
|
13
|
+
/** Optional custom environment variables to merge into execution environment. */
|
|
14
|
+
env?: Record<string, string>;
|
|
13
15
|
}
|
|
14
16
|
export interface PropertyTestConfig {
|
|
15
17
|
numRuns?: number;
|
|
@@ -23,10 +25,37 @@ export interface PropertyTestResult extends EphemeralScriptResult {
|
|
|
23
25
|
seed?: number;
|
|
24
26
|
numRunsCompleted?: number;
|
|
25
27
|
}
|
|
28
|
+
/** Supported canonical language names list. */
|
|
29
|
+
export declare const SUPPORTED_LANGUAGES: readonly ["node", "ts-node", "python", "bash", "go", "rust", "c", "cpp", "ruby", "php", "java"];
|
|
30
|
+
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
|
26
31
|
/**
|
|
27
32
|
* Normalizes user/AI provided language string to a standard runtime identifier.
|
|
28
33
|
*/
|
|
29
34
|
export declare function normalizeLanguage(lang: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Detects programming language heuristic markers directly from script source code syntax.
|
|
37
|
+
* Useful when language is omitted or set to 'auto'.
|
|
38
|
+
*
|
|
39
|
+
* @param code - The source code to analyze.
|
|
40
|
+
* @returns Detected runtime identifier (e.g., 'python', 'rust', 'go', 'cpp', 'c', 'bash', 'ruby', 'php', 'java', 'ts-node', 'node').
|
|
41
|
+
*/
|
|
42
|
+
export declare function detectLanguageFromCode(code: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Detects the dominant programming language / runtime for a workspace based on project manifest files.
|
|
45
|
+
*
|
|
46
|
+
* @param workspaceRoot - Path to the workspace root directory.
|
|
47
|
+
* @returns Detected runtime identifier (e.g., 'rust', 'go', 'python', 'cpp', 'ts-node', 'node').
|
|
48
|
+
*/
|
|
49
|
+
export declare function detectProjectRuntime(workspaceRoot: string): Promise<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the effective runtime language by combining explicit language input,
|
|
52
|
+
* source code syntax heuristics, and workspace project manifests.
|
|
53
|
+
*
|
|
54
|
+
* @param workspaceRoot - Path to workspace root directory.
|
|
55
|
+
* @param language - Optional language parameter passed by user/agent.
|
|
56
|
+
* @param code - Optional source code string to inspect.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveEffectiveRuntime(workspaceRoot: string, language?: string, code?: string): Promise<string>;
|
|
30
59
|
/**
|
|
31
60
|
* Detects whether the workspace package.json specifies `"type": "module"`.
|
|
32
61
|
* Returns `'module'` or `'commonjs'`.
|
|
@@ -10,17 +10,52 @@ const LANGUAGE_ALIASES = {
|
|
|
10
10
|
js: 'node',
|
|
11
11
|
javascript: 'node',
|
|
12
12
|
node: 'node',
|
|
13
|
+
cjs: 'node',
|
|
14
|
+
mjs: 'node',
|
|
13
15
|
ts: 'ts-node',
|
|
14
16
|
typescript: 'ts-node',
|
|
15
17
|
'ts-node': 'ts-node',
|
|
18
|
+
tsx: 'ts-node',
|
|
19
|
+
mts: 'ts-node',
|
|
20
|
+
cts: 'ts-node',
|
|
16
21
|
py: 'python',
|
|
22
|
+
py3: 'python',
|
|
17
23
|
python: 'python',
|
|
24
|
+
python3: 'python',
|
|
18
25
|
sh: 'bash',
|
|
19
26
|
bash: 'bash',
|
|
27
|
+
shell: 'bash',
|
|
28
|
+
zsh: 'bash',
|
|
20
29
|
go: 'go',
|
|
30
|
+
golang: 'go',
|
|
21
31
|
rs: 'rust',
|
|
22
32
|
rust: 'rust',
|
|
33
|
+
rustc: 'rust',
|
|
34
|
+
c: 'c',
|
|
35
|
+
cpp: 'cpp',
|
|
36
|
+
'c++': 'cpp',
|
|
37
|
+
cc: 'cpp',
|
|
38
|
+
cxx: 'cpp',
|
|
39
|
+
cplusplus: 'cpp',
|
|
40
|
+
rb: 'ruby',
|
|
41
|
+
ruby: 'ruby',
|
|
42
|
+
php: 'php',
|
|
43
|
+
java: 'java',
|
|
23
44
|
};
|
|
45
|
+
/** Supported canonical language names list. */
|
|
46
|
+
export const SUPPORTED_LANGUAGES = [
|
|
47
|
+
'node',
|
|
48
|
+
'ts-node',
|
|
49
|
+
'python',
|
|
50
|
+
'bash',
|
|
51
|
+
'go',
|
|
52
|
+
'rust',
|
|
53
|
+
'c',
|
|
54
|
+
'cpp',
|
|
55
|
+
'ruby',
|
|
56
|
+
'php',
|
|
57
|
+
'java',
|
|
58
|
+
];
|
|
24
59
|
/**
|
|
25
60
|
* Normalizes user/AI provided language string to a standard runtime identifier.
|
|
26
61
|
*/
|
|
@@ -28,6 +63,135 @@ export function normalizeLanguage(lang) {
|
|
|
28
63
|
const key = lang.toLowerCase().trim();
|
|
29
64
|
return LANGUAGE_ALIASES[key] || key;
|
|
30
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Detects programming language heuristic markers directly from script source code syntax.
|
|
68
|
+
* Useful when language is omitted or set to 'auto'.
|
|
69
|
+
*
|
|
70
|
+
* @param code - The source code to analyze.
|
|
71
|
+
* @returns Detected runtime identifier (e.g., 'python', 'rust', 'go', 'cpp', 'c', 'bash', 'ruby', 'php', 'java', 'ts-node', 'node').
|
|
72
|
+
*/
|
|
73
|
+
export function detectLanguageFromCode(code) {
|
|
74
|
+
const trimmed = code.trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
return 'node';
|
|
77
|
+
// Shebang check
|
|
78
|
+
if (trimmed.startsWith('#!')) {
|
|
79
|
+
const firstLine = trimmed.split('\n')[0].toLowerCase();
|
|
80
|
+
if (firstLine.includes('python'))
|
|
81
|
+
return 'python';
|
|
82
|
+
if (firstLine.includes('bash') || firstLine.includes('sh') || firstLine.includes('zsh'))
|
|
83
|
+
return 'bash';
|
|
84
|
+
if (firstLine.includes('node'))
|
|
85
|
+
return 'node';
|
|
86
|
+
if (firstLine.includes('ruby'))
|
|
87
|
+
return 'ruby';
|
|
88
|
+
if (firstLine.includes('php'))
|
|
89
|
+
return 'php';
|
|
90
|
+
}
|
|
91
|
+
// PHP marker
|
|
92
|
+
if (trimmed.startsWith('<?php') || trimmed.includes('<?php')) {
|
|
93
|
+
return 'php';
|
|
94
|
+
}
|
|
95
|
+
// Rust markers
|
|
96
|
+
if (/\b(fn\s+main\s*\(|println!\s*\(|eprintln!\s*\(|use\s+std::|impl\s+[A-Z]\w*|pub\s+fn\b|match\s+\w+\s*\{)/.test(code)) {
|
|
97
|
+
return 'rust';
|
|
98
|
+
}
|
|
99
|
+
// Go markers
|
|
100
|
+
if (/\bpackage\s+main\b/.test(code) ||
|
|
101
|
+
(/\bfunc\s+main\s*\(/.test(code) && /\bimport\s+(\(|")/.test(code))) {
|
|
102
|
+
return 'go';
|
|
103
|
+
}
|
|
104
|
+
// C++ markers
|
|
105
|
+
if (/#\s*include\s*<(iostream|vector|string|memory|algorithm|map|set|utility)>/.test(code) ||
|
|
106
|
+
/\bstd::(cout|cin|cerr|endl|vector|string|make_unique|make_shared)\b/.test(code) ||
|
|
107
|
+
/\btemplate\s*<\s*typename\b/.test(code)) {
|
|
108
|
+
return 'cpp';
|
|
109
|
+
}
|
|
110
|
+
// C markers
|
|
111
|
+
if (/#\s*include\s*<(stdio\.h|stdlib\.h|string\.h|unistd\.h|math\.h)>/.test(code) ||
|
|
112
|
+
(/\bprintf\s*\(/.test(code) && /\bint\s+main\s*\(/.test(code))) {
|
|
113
|
+
return 'c';
|
|
114
|
+
}
|
|
115
|
+
// Java markers
|
|
116
|
+
if (/\bpublic\s+class\s+[A-Z]\w*/.test(code) ||
|
|
117
|
+
/\bpublic\s+static\s+void\s+main\s*\(/.test(code) ||
|
|
118
|
+
/\bSystem\.(out|err)\.println\b/.test(code)) {
|
|
119
|
+
return 'java';
|
|
120
|
+
}
|
|
121
|
+
// Python markers
|
|
122
|
+
if (/\b(def\s+[a-zA-Z_]\w*\s*\(|import\s+[a-zA-Z_]\w*|from\s+[a-zA-Z_]\w*\s+import|if\s+__name__\s*==\s*['"]__main__['"])/.test(code) ||
|
|
123
|
+
(/:\s*$/.test(trimmed) && /\b(elif|except|finally|pass|yield)\b/.test(code))) {
|
|
124
|
+
return 'python';
|
|
125
|
+
}
|
|
126
|
+
// Ruby markers
|
|
127
|
+
if (/\b(def\s+[a-zA-Z_]\w*(\s*\(|\s*\n)|puts\s+|require_relative\s+|attr_accessor\s+:)/.test(code) &&
|
|
128
|
+
/\bend\b/.test(code)) {
|
|
129
|
+
return 'ruby';
|
|
130
|
+
}
|
|
131
|
+
// TypeScript markers
|
|
132
|
+
if (/\b(interface\s+[A-Z]\w*|type\s+[A-Z]\w*\s*=|:\s*(string|number|boolean|Record<|Array<|Promise<|void|unknown|never)\b|as\s+const\b)/.test(code)) {
|
|
133
|
+
return 'ts-node';
|
|
134
|
+
}
|
|
135
|
+
// Bash markers
|
|
136
|
+
if (/\b(echo\s+['"].*['"]|if\s+\[\s+.*\s+\];\s*then|fi\b|done\b|export\s+[A-Z_]+=\S+)/.test(code) &&
|
|
137
|
+
!code.includes('console.log')) {
|
|
138
|
+
return 'bash';
|
|
139
|
+
}
|
|
140
|
+
return 'node';
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Detects the dominant programming language / runtime for a workspace based on project manifest files.
|
|
144
|
+
*
|
|
145
|
+
* @param workspaceRoot - Path to the workspace root directory.
|
|
146
|
+
* @returns Detected runtime identifier (e.g., 'rust', 'go', 'python', 'cpp', 'ts-node', 'node').
|
|
147
|
+
*/
|
|
148
|
+
export async function detectProjectRuntime(workspaceRoot) {
|
|
149
|
+
const probeFiles = [
|
|
150
|
+
{ file: 'Cargo.toml', runtime: 'rust' },
|
|
151
|
+
{ file: 'go.mod', runtime: 'go' },
|
|
152
|
+
{ file: 'pyproject.toml', runtime: 'python' },
|
|
153
|
+
{ file: 'requirements.txt', runtime: 'python' },
|
|
154
|
+
{ file: 'Pipfile', runtime: 'python' },
|
|
155
|
+
{ file: 'setup.py', runtime: 'python' },
|
|
156
|
+
{ file: 'CMakeLists.txt', runtime: 'cpp' },
|
|
157
|
+
{ file: 'Gemfile', runtime: 'ruby' },
|
|
158
|
+
{ file: 'composer.json', runtime: 'php' },
|
|
159
|
+
{ file: 'pom.xml', runtime: 'java' },
|
|
160
|
+
{ file: 'build.gradle', runtime: 'java' },
|
|
161
|
+
{ file: 'tsconfig.json', runtime: 'ts-node' },
|
|
162
|
+
{ file: 'package.json', runtime: 'node' },
|
|
163
|
+
];
|
|
164
|
+
for (const { file, runtime } of probeFiles) {
|
|
165
|
+
try {
|
|
166
|
+
await fs.access(path.join(workspaceRoot, file));
|
|
167
|
+
return runtime;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Continue searching
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return 'node';
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolves the effective runtime language by combining explicit language input,
|
|
177
|
+
* source code syntax heuristics, and workspace project manifests.
|
|
178
|
+
*
|
|
179
|
+
* @param workspaceRoot - Path to workspace root directory.
|
|
180
|
+
* @param language - Optional language parameter passed by user/agent.
|
|
181
|
+
* @param code - Optional source code string to inspect.
|
|
182
|
+
*/
|
|
183
|
+
export async function resolveEffectiveRuntime(workspaceRoot, language, code) {
|
|
184
|
+
if (language && language.toLowerCase().trim() !== 'auto') {
|
|
185
|
+
return normalizeLanguage(language);
|
|
186
|
+
}
|
|
187
|
+
if (code && code.trim()) {
|
|
188
|
+
const detectedFromCode = detectLanguageFromCode(code);
|
|
189
|
+
if (detectedFromCode !== 'node') {
|
|
190
|
+
return detectedFromCode;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return detectProjectRuntime(workspaceRoot);
|
|
194
|
+
}
|
|
31
195
|
/**
|
|
32
196
|
* Detects whether the workspace package.json specifies `"type": "module"`.
|
|
33
197
|
* Returns `'module'` or `'commonjs'`.
|
|
@@ -72,6 +236,11 @@ export async function resolveScriptExtension(workspaceRoot, language, code) {
|
|
|
72
236
|
bash: '.sh',
|
|
73
237
|
go: '.go',
|
|
74
238
|
rust: '.rs',
|
|
239
|
+
c: '.c',
|
|
240
|
+
cpp: '.cpp',
|
|
241
|
+
ruby: '.rb',
|
|
242
|
+
php: '.php',
|
|
243
|
+
java: '.java',
|
|
75
244
|
};
|
|
76
245
|
return defaultExts[normLang] || '.js';
|
|
77
246
|
}
|
|
@@ -89,6 +258,7 @@ function buildTempPath(code, ext) {
|
|
|
89
258
|
*/
|
|
90
259
|
function buildCommand(language, scriptPath) {
|
|
91
260
|
const normLang = normalizeLanguage(language);
|
|
261
|
+
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
92
262
|
switch (normLang) {
|
|
93
263
|
case 'node':
|
|
94
264
|
return `node "${scriptPath}"`;
|
|
@@ -101,10 +271,23 @@ function buildCommand(language, scriptPath) {
|
|
|
101
271
|
case 'go':
|
|
102
272
|
return `go run "${scriptPath}"`;
|
|
103
273
|
case 'rust': {
|
|
104
|
-
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
105
274
|
const binPath = scriptPath.replace(/\.rs$/, binExt);
|
|
106
275
|
return `rustc "${scriptPath}" -o "${binPath}" && "${binPath}"`;
|
|
107
276
|
}
|
|
277
|
+
case 'c': {
|
|
278
|
+
const binPath = scriptPath.replace(/\.c$/, binExt);
|
|
279
|
+
return `gcc "${scriptPath}" -o "${binPath}" && "${binPath}"`;
|
|
280
|
+
}
|
|
281
|
+
case 'cpp': {
|
|
282
|
+
const binPath = scriptPath.replace(/\.cpp$/, binExt);
|
|
283
|
+
return `g++ -std=c++17 "${scriptPath}" -o "${binPath}" && "${binPath}"`;
|
|
284
|
+
}
|
|
285
|
+
case 'ruby':
|
|
286
|
+
return `ruby "${scriptPath}"`;
|
|
287
|
+
case 'php':
|
|
288
|
+
return `php "${scriptPath}"`;
|
|
289
|
+
case 'java':
|
|
290
|
+
return `java "${scriptPath}"`;
|
|
108
291
|
default:
|
|
109
292
|
return null;
|
|
110
293
|
}
|
|
@@ -134,30 +317,39 @@ function truncateOutput(text, max) {
|
|
|
134
317
|
export async function runEphemeralScript(workspaceRoot, language, code, options) {
|
|
135
318
|
const timeoutMs = options?.timeoutMs ?? 60_000;
|
|
136
319
|
const maxOutputChars = options?.maxOutputChars ?? 100_000;
|
|
137
|
-
const normLang =
|
|
320
|
+
const normLang = await resolveEffectiveRuntime(workspaceRoot, language, code);
|
|
138
321
|
const ext = await resolveScriptExtension(workspaceRoot, normLang, code);
|
|
139
322
|
const scriptPath = buildTempPath(code, ext);
|
|
140
323
|
const cmd = buildCommand(normLang, scriptPath);
|
|
141
324
|
if (!cmd) {
|
|
142
325
|
return {
|
|
143
326
|
stdout: '',
|
|
144
|
-
stderr: `Unsupported language runtime: "${language}". Supported:
|
|
327
|
+
stderr: `Unsupported language runtime: "${language}". Supported: ${SUPPORTED_LANGUAGES.join(', ')}`,
|
|
145
328
|
exitCode: 1,
|
|
146
329
|
};
|
|
147
330
|
}
|
|
148
331
|
// Collect all temp files created so we can clean them up unconditionally
|
|
149
332
|
const tempFiles = [scriptPath];
|
|
333
|
+
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
150
334
|
if (normLang === 'rust') {
|
|
151
|
-
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
152
335
|
tempFiles.push(scriptPath.replace(/\.rs$/, binExt));
|
|
336
|
+
tempFiles.push(scriptPath.replace(/\.rs$/, '.pdb'));
|
|
337
|
+
}
|
|
338
|
+
else if (normLang === 'c') {
|
|
339
|
+
tempFiles.push(scriptPath.replace(/\.c$/, binExt));
|
|
340
|
+
}
|
|
341
|
+
else if (normLang === 'cpp') {
|
|
342
|
+
tempFiles.push(scriptPath.replace(/\.cpp$/, binExt));
|
|
153
343
|
}
|
|
154
344
|
try {
|
|
155
345
|
await fs.writeFile(scriptPath, code, 'utf-8');
|
|
346
|
+
const mergedEnv = options?.env ? { ...process.env, ...options.env } : process.env;
|
|
156
347
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
157
348
|
cwd: workspaceRoot,
|
|
158
349
|
timeout: timeoutMs,
|
|
159
350
|
maxBuffer: 1024 * 1024, // 1 MB buffer
|
|
160
351
|
signal: options?.abortSignal,
|
|
352
|
+
env: mergedEnv,
|
|
161
353
|
});
|
|
162
354
|
return {
|
|
163
355
|
stdout: truncateOutput(stdout.trim(), maxOutputChars),
|
|
@@ -170,6 +362,9 @@ export async function runEphemeralScript(workspaceRoot, language, code, options)
|
|
|
170
362
|
if (err.name === 'AbortError' || err.message?.includes('abort')) {
|
|
171
363
|
return { stdout: '', stderr: 'Analysis script aborted by user.', exitCode: 130 };
|
|
172
364
|
}
|
|
365
|
+
if (err.killed && err.signal === 'SIGTERM') {
|
|
366
|
+
return { stdout: '', stderr: `Execution timed out after ${timeoutMs}ms.`, exitCode: 124 };
|
|
367
|
+
}
|
|
173
368
|
const stdout = truncateOutput((err.stdout || '').trim(), maxOutputChars);
|
|
174
369
|
const stderr = truncateOutput((err.stderr || '').trim(), maxOutputChars);
|
|
175
370
|
const exitCode = typeof err.code === 'number' ? err.code : 1;
|
|
@@ -179,7 +374,7 @@ export async function runEphemeralScript(workspaceRoot, language, code, options)
|
|
|
179
374
|
// Guarantee temp file cleanup regardless of success or failure
|
|
180
375
|
for (const tmpFile of tempFiles) {
|
|
181
376
|
try {
|
|
182
|
-
await fs.rm(tmpFile, { force: true });
|
|
377
|
+
await fs.rm(tmpFile, { force: true, recursive: true });
|
|
183
378
|
}
|
|
184
379
|
catch {
|
|
185
380
|
// Ignore cleanup errors — temp files will be reaped by the OS eventually
|