ft-scout 3.0.8 → 4.0.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/bin/src/commands/agent.d.ts.map +1 -1
- package/bin/src/commands/agent.js +188 -264
- package/bin/src/commands/agent.js.map +1 -1
- package/bin/src/engine/agentEngine.d.ts.map +1 -0
- package/bin/src/engine/agentEngine.js +464 -0
- package/bin/src/engine/agentEngine.js.map +1 -0
- package/bin/src/engine/llm.d.ts.map +1 -1
- package/bin/src/engine/llm.js +63 -16
- package/bin/src/engine/llm.js.map +1 -1
- package/bin/src/engine/verifier.d.ts.map +1 -0
- package/bin/src/engine/verifier.js +195 -0
- package/bin/src/engine/verifier.js.map +1 -0
- package/bin/src/index.js +4 -4
- package/bin/src/utils/branding.js +1 -1
- package/package.json +48 -1
|
@@ -9,6 +9,8 @@ import { renderMarkdown, safeNote } from '../utils/markdown.js';
|
|
|
9
9
|
import { normalizeLanguage } from '../utils/language.js';
|
|
10
10
|
import { createPullRequest } from '../engine/git.js';
|
|
11
11
|
import { handleUndo } from './undo.js';
|
|
12
|
+
import { AgentExecutionLoop } from '../engine/agentEngine.js';
|
|
13
|
+
import { verifyAndSelfHealFiles } from '../engine/verifier.js';
|
|
12
14
|
import { exec } from 'child_process';
|
|
13
15
|
import { promisify } from 'util';
|
|
14
16
|
const execAsync = promisify(exec);
|
|
@@ -29,25 +31,34 @@ export function loadScanReport() {
|
|
|
29
31
|
return [];
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
|
-
export function
|
|
34
|
+
export function extractCreateTargetFiles(rawGoal) {
|
|
33
35
|
if (!rawGoal || !rawGoal.trim())
|
|
34
|
-
return
|
|
36
|
+
return [];
|
|
35
37
|
const goalLower = rawGoal.toLowerCase().trim();
|
|
36
38
|
const isCreateIntent = /^(?:create|generate|make|build|add|write|edit|modify|update|implement|populate|fill)\b/i.test(goalLower) ||
|
|
37
|
-
/\b(?:create|generate|make|build|add|new|edit|modify|update|implement)\s+(?:a\s+)?(?:new\s+)?(?:file|code|script)\b/i.test(goalLower) ||
|
|
38
|
-
/\b(?:in|into|to|called|named|file|edit|modify|update)\s+([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)\b/i.test(rawGoal);
|
|
39
|
+
/\b(?:create|generate|make|build|add|new|edit|modify|update|implement)\s+(?:a\s+)?(?:new\s+)?(?:file|files|code|script)\b/i.test(goalLower) ||
|
|
40
|
+
/\b(?:in|into|to|called|named|file|files|edit|modify|update)\s+([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)\b/i.test(rawGoal);
|
|
39
41
|
if (!isCreateIntent)
|
|
40
|
-
return
|
|
41
|
-
const directMatch = rawGoal.match(/(?:create|generate|make|build|add|write|edit|modify|update|implement|populate|fill)\s+(?:a\s+)?(?:new\s+)?(?:file\s+)?(?:in\s+)?(?:the\s+)?(?:root\s+)?(?:repo\s+|repository\s+|dir\s+|directory\s+)?(?:called\s+|named\s+)?([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)\b/i);
|
|
42
|
-
if (directMatch && directMatch[1] && directMatch[1].toLowerCase() !== 'package.json') {
|
|
43
|
-
return directMatch[1].replace(/^\.\//, '');
|
|
44
|
-
}
|
|
45
|
-
const tokens = rawGoal.split(/\s+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
|
|
42
|
+
return [];
|
|
46
43
|
const validExts = [
|
|
47
44
|
'ts', 'js', 'py', 'java', 'cpp', 'c', 'h', 'cs', 'go', 'rs', 'php',
|
|
48
45
|
'rb', 'html', 'css', 'json', 'md', 'sql', 'txt', 'sh', 'yaml', 'yml',
|
|
49
46
|
'xml', 'jsx', 'tsx', 'vue', 'svelte'
|
|
50
47
|
];
|
|
48
|
+
const foundFiles = [];
|
|
49
|
+
const directMatches = rawGoal.matchAll(/(?:create|generate|make|build|add|write|edit|modify|update|implement|populate|fill|file|called|named)\s+(?:a\s+)?(?:new\s+)?(?:file\s+)?(?:in\s+)?(?:the\s+)?(?:root\s+)?(?:repo\s+|repository\s+|dir\s+|directory\s+)?(?:called\s+|named\s+)?([a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+)\b/gi);
|
|
50
|
+
for (const match of directMatches) {
|
|
51
|
+
if (match && match[1]) {
|
|
52
|
+
const file = match[1].replace(/^\.\//, '');
|
|
53
|
+
if (file.toLowerCase() !== 'package.json' && !foundFiles.includes(file)) {
|
|
54
|
+
const ext = path.extname(file).replace('.', '').toLowerCase();
|
|
55
|
+
if (validExts.includes(ext)) {
|
|
56
|
+
foundFiles.push(file);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const tokens = rawGoal.split(/[\s,;]+/).map((w) => w.replace(/["'()]/g, '').trim());
|
|
51
62
|
for (const token of tokens) {
|
|
52
63
|
if (token.includes('.') &&
|
|
53
64
|
!token.startsWith('.') &&
|
|
@@ -55,11 +66,18 @@ export function extractCreateTargetFile(rawGoal) {
|
|
|
55
66
|
/^[a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+$/.test(token)) {
|
|
56
67
|
const ext = path.extname(token).replace('.', '').toLowerCase();
|
|
57
68
|
if (validExts.includes(ext) && token.toLowerCase() !== 'package.json') {
|
|
58
|
-
|
|
69
|
+
const cleanPath = token.replace(/^\.\//, '');
|
|
70
|
+
if (!foundFiles.includes(cleanPath)) {
|
|
71
|
+
foundFiles.push(cleanPath);
|
|
72
|
+
}
|
|
59
73
|
}
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
|
-
return
|
|
76
|
+
return foundFiles;
|
|
77
|
+
}
|
|
78
|
+
export function extractCreateTargetFile(rawGoal) {
|
|
79
|
+
const files = extractCreateTargetFiles(rawGoal);
|
|
80
|
+
return files.length > 0 ? files[0] : undefined;
|
|
63
81
|
}
|
|
64
82
|
export function parseUserPromptIntent(rawGoal, filesList) {
|
|
65
83
|
if (!rawGoal || !rawGoal.trim()) {
|
|
@@ -75,7 +93,11 @@ export function parseUserPromptIntent(rawGoal, filesList) {
|
|
|
75
93
|
if (!cleaned)
|
|
76
94
|
cleaned = rawGoal.trim();
|
|
77
95
|
const lowerCleaned = cleaned.toLowerCase();
|
|
78
|
-
let
|
|
96
|
+
let targetFiles = extractCreateTargetFiles(cleaned);
|
|
97
|
+
if (targetFiles.length === 0) {
|
|
98
|
+
targetFiles = extractCreateTargetFiles(rawGoal);
|
|
99
|
+
}
|
|
100
|
+
let targetFile = targetFiles.length > 0 ? targetFiles[0] : undefined;
|
|
79
101
|
if (!targetFile) {
|
|
80
102
|
const tokens = cleaned.split(/\s+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
|
|
81
103
|
const validExts = [
|
|
@@ -88,6 +110,7 @@ export function parseUserPromptIntent(rawGoal, filesList) {
|
|
|
88
110
|
const ext = path.extname(token).replace('.', '').toLowerCase();
|
|
89
111
|
if (validExts.includes(ext) && token.toLowerCase() !== 'package.json') {
|
|
90
112
|
targetFile = token.replace(/^\.\//, '');
|
|
113
|
+
targetFiles = [targetFile];
|
|
91
114
|
break;
|
|
92
115
|
}
|
|
93
116
|
}
|
|
@@ -97,33 +120,34 @@ export function parseUserPromptIntent(rawGoal, filesList) {
|
|
|
97
120
|
const matchedFiles = findTargetFilesFromPrompt(cleaned, undefined, filesList);
|
|
98
121
|
if (matchedFiles.length > 0) {
|
|
99
122
|
targetFile = matchedFiles[0];
|
|
123
|
+
targetFiles = matchedFiles;
|
|
100
124
|
}
|
|
101
125
|
}
|
|
102
126
|
const isScan = /\b(?:scan|bug scan|scan codebase|check for bugs|find bugs|security scan)\b/i.test(lowerCleaned);
|
|
103
127
|
if (isScan)
|
|
104
|
-
return { cleanGoal: cleaned, intentType: 'scan', targetFile };
|
|
128
|
+
return { cleanGoal: cleaned, intentType: 'scan', targetFile, targetFiles };
|
|
105
129
|
const isRun = /^(?:run|exec|execute|terminal)\b/i.test(lowerCleaned);
|
|
106
130
|
if (isRun)
|
|
107
|
-
return { cleanGoal: cleaned, intentType: 'run', targetFile };
|
|
131
|
+
return { cleanGoal: cleaned, intentType: 'run', targetFile, targetFiles };
|
|
108
132
|
const isPlan = /^(?:plan|roadmap|feasibility|design|architect)\b/i.test(lowerCleaned) || /^add\s+feature\b/i.test(lowerCleaned);
|
|
109
133
|
if (isPlan)
|
|
110
|
-
return { cleanGoal: cleaned, intentType: 'plan', targetFile };
|
|
111
|
-
const creationKeywordsRegex = /\b(?:create|generate|make|build|add\s+file|new\s+file|write\s+file|edit\s+file|populate|write\s+code|implement\s+code|write\s+snippet|add\s+snippet)\b/i;
|
|
134
|
+
return { cleanGoal: cleaned, intentType: 'plan', targetFile, targetFiles };
|
|
135
|
+
const creationKeywordsRegex = /\b(?:create|generate|make|build|add\s+file|new\s+file|write\s+file|edit\s+file|populate|write\s+code|implement\s+code|write\s+snippet|add\s+snippet|all\s+necessary\s+files)\b/i;
|
|
112
136
|
const actionVerbsRegex = /\b(?:create|generate|make|build|write|add|edit|modify|update|fix|refactor|change|delete|remove|patch|implement|populate|fill|code|put|setup)\b/i;
|
|
113
137
|
const hasActionVerb = actionVerbsRegex.test(lowerCleaned);
|
|
114
|
-
const isCreationIntent = creationKeywordsRegex.test(lowerCleaned) || Boolean(
|
|
138
|
+
const isCreationIntent = creationKeywordsRegex.test(lowerCleaned) || Boolean(targetFiles.length > 0 && /\b(?:create|generate|make|build|write|add|edit|modify|update|populate|fill|implement|snippet|game|code)\b/i.test(lowerCleaned));
|
|
115
139
|
const pureQuestionRegex = /^(?:what|why|where|who|explain|read|display|tell|describe|list|purpose\s+of|how\s+does|how\s+is|show\s+me\s+code)\b/i;
|
|
116
140
|
const isPureQuestion = pureQuestionRegex.test(lowerCleaned) && !hasActionVerb;
|
|
117
141
|
if (isPureQuestion) {
|
|
118
|
-
return { cleanGoal: cleaned, intentType: 'question', targetFile };
|
|
142
|
+
return { cleanGoal: cleaned, intentType: 'question', targetFile, targetFiles };
|
|
119
143
|
}
|
|
120
|
-
if (isCreationIntent
|
|
121
|
-
return { cleanGoal: cleaned, intentType: 'create', targetFile };
|
|
144
|
+
if (isCreationIntent) {
|
|
145
|
+
return { cleanGoal: cleaned, intentType: 'create', targetFile, targetFiles };
|
|
122
146
|
}
|
|
123
147
|
if (hasActionVerb || targetFile) {
|
|
124
|
-
return { cleanGoal: cleaned, intentType: 'modify', targetFile };
|
|
148
|
+
return { cleanGoal: cleaned, intentType: 'modify', targetFile, targetFiles };
|
|
125
149
|
}
|
|
126
|
-
return { cleanGoal: cleaned, intentType: 'question', targetFile };
|
|
150
|
+
return { cleanGoal: cleaned, intentType: 'question', targetFile, targetFiles };
|
|
127
151
|
}
|
|
128
152
|
export function findTargetFilesFromPrompt(userPrompt, specifiedFile, filesList) {
|
|
129
153
|
const norm = (p) => p.replace(/\\/g, '/').toLowerCase().trim();
|
|
@@ -210,11 +234,23 @@ export async function handleAgent(goalInput, options) {
|
|
|
210
234
|
const projectName = existingContext?.projectName || path.basename(cwd) || 'Codebase';
|
|
211
235
|
let initialGoal = goalInput || options?.goal;
|
|
212
236
|
let isFirstRun = true;
|
|
237
|
+
const agentLoop = new AgentExecutionLoop({
|
|
238
|
+
autoApprove: options?.yes,
|
|
239
|
+
maxSteps: options?.maxRetries || 15,
|
|
240
|
+
language: targetLang,
|
|
241
|
+
projectName,
|
|
242
|
+
});
|
|
213
243
|
const welcomeBanner = `
|
|
214
|
-
${chalk.bold.cyan('🤖 Scout Autonomous AI Agent
|
|
215
|
-
${chalk.dim('Type your goal,
|
|
244
|
+
${chalk.bold.cyan('🤖 Scout Autonomous AI Agent')} ${targetLang ? `(${targetLang})` : ''}
|
|
245
|
+
${chalk.dim('Type your goal, or use slash commands:')}
|
|
246
|
+
${chalk.cyan('/help')} - Show agent tools & commands
|
|
247
|
+
${chalk.cyan('/clear')} - Reset conversation context
|
|
248
|
+
${chalk.cyan('/undo')} - Revert last file modifications
|
|
249
|
+
${chalk.cyan('/scan')} - Run vulnerability scan
|
|
250
|
+
${chalk.cyan('/plan')} - Generate implementation roadmap
|
|
251
|
+
${chalk.cyan('/exit')} - Exit session
|
|
216
252
|
`.trim();
|
|
217
|
-
safeNote(welcomeBanner, '🤖 Scout Agent Session');
|
|
253
|
+
safeNote(welcomeBanner, '🤖 Scout Agent Active Session');
|
|
218
254
|
while (true) {
|
|
219
255
|
let rawGoal = undefined;
|
|
220
256
|
if (isFirstRun && (initialGoal || options?.scan || options?.add || options?.plan || options?.run || options?.create || options?.issue)) {
|
|
@@ -225,18 +261,55 @@ ${chalk.dim('Type your goal, task, or question. Press Ctrl+C or type "exit" to q
|
|
|
225
261
|
isFirstRun = false;
|
|
226
262
|
const userGoalInput = await text({
|
|
227
263
|
message: chalk.bold('Agent Prompt:'),
|
|
228
|
-
placeholder: 'Ask question, fix bug, run command, scan issues, create file... (
|
|
229
|
-
validate: (val) => (!val || !val.trim() ? 'Please enter a prompt or
|
|
264
|
+
placeholder: 'Ask question, fix bug, run command, scan issues, create file... (/help /exit)',
|
|
265
|
+
validate: (val) => (!val || !val.trim() ? 'Please enter a prompt or /exit to quit' : undefined),
|
|
230
266
|
});
|
|
231
267
|
if (isCancel(userGoalInput)) {
|
|
232
268
|
safeNote(chalk.yellow('Exiting Scout Agent session. Goodbye!'), '👋 Scout Agent Ended');
|
|
233
269
|
break;
|
|
234
270
|
}
|
|
235
271
|
const trimmedInput = userGoalInput.trim();
|
|
236
|
-
if (['exit', 'quit', 'q', ':q'].includes(trimmedInput.toLowerCase())) {
|
|
272
|
+
if (['exit', 'quit', 'q', ':q', '/exit', '/quit'].includes(trimmedInput.toLowerCase())) {
|
|
237
273
|
safeNote(chalk.yellow('Exiting Scout Agent session. Goodbye!'), '👋 Scout Agent Ended');
|
|
238
274
|
break;
|
|
239
275
|
}
|
|
276
|
+
if (trimmedInput.toLowerCase() === '/help') {
|
|
277
|
+
const helpText = `
|
|
278
|
+
### 🛠️ Scout Agent Tools
|
|
279
|
+
- **read_file**: Read content & line ranges of codebase files.
|
|
280
|
+
- **write_file**: Create new files or overwrite existing code.
|
|
281
|
+
- **edit_file**: Search-and-replace exact code snippets.
|
|
282
|
+
- **run_command**: Execute shell commands live (\`npm test\`, \`build\`).
|
|
283
|
+
- **grep_search**: Regex/string search across all files.
|
|
284
|
+
- **glob_search**: Pattern search for workspace files.
|
|
285
|
+
|
|
286
|
+
### ⚡ Slash Commands
|
|
287
|
+
- \`/help\`: View this assistance screen.
|
|
288
|
+
- \`/clear\` or \`/compact\`: Clear session context window.
|
|
289
|
+
- \`/undo\`: Revert last changes applied by Scout Agent.
|
|
290
|
+
- \`/scan\`: Perform read-only security & bug scan.
|
|
291
|
+
- \`/plan <idea>\`: Generate feature feasibility roadmap.
|
|
292
|
+
- \`/history\`: View recent Scout actions.
|
|
293
|
+
- \`/exit\`: Quit Scout Agent session.
|
|
294
|
+
`.trim();
|
|
295
|
+
safeNote(renderMarkdown(helpText), '⚡ Scout Slash Commands & Tools');
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (['/clear', '/compact'].includes(trimmedInput.toLowerCase())) {
|
|
299
|
+
agentLoop.resetContext();
|
|
300
|
+
safeNote(chalk.green('Agent context reset successfully.'), '🧹 Session Context Cleared');
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (trimmedInput.toLowerCase() === '/undo') {
|
|
304
|
+
await handleUndo();
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (trimmedInput.toLowerCase() === '/history') {
|
|
308
|
+
const history = (await import('../utils/config.js')).loadHistory();
|
|
309
|
+
const historyMd = history.slice(0, 10).map((h) => `- **${h.timestamp}**: \`${h.command}\` — ${h.description}`).join('\n') || 'No action history recorded yet.';
|
|
310
|
+
safeNote(renderMarkdown(historyMd), '📜 Scout History Log');
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
240
313
|
rawGoal = trimmedInput;
|
|
241
314
|
}
|
|
242
315
|
if (!rawGoal || !rawGoal.trim()) {
|
|
@@ -246,37 +319,6 @@ ${chalk.dim('Type your goal, task, or question. Press Ctrl+C or type "exit" to q
|
|
|
246
319
|
const effectiveGoal = parsedIntent.cleanGoal || rawGoal;
|
|
247
320
|
const goalLower = effectiveGoal.toLowerCase().trim();
|
|
248
321
|
// -------------------------------------------------------------
|
|
249
|
-
// Mode 0: Q&A / Read-Only Question & File Content Inspection Mode
|
|
250
|
-
// -------------------------------------------------------------
|
|
251
|
-
if (parsedIntent.intentType === 'question' && !options?.create && !options?.file && !options?.run) {
|
|
252
|
-
const s = spinner();
|
|
253
|
-
s.start('🤖 Scout Agent analyzing query & inspecting codebase...');
|
|
254
|
-
const matchedFiles = findTargetFilesFromPrompt(rawGoal, undefined, filesList);
|
|
255
|
-
let readfileOutput = '';
|
|
256
|
-
if (matchedFiles.length > 0) {
|
|
257
|
-
for (const f of matchedFiles.slice(0, 3)) {
|
|
258
|
-
const fullP = path.resolve(cwd, f);
|
|
259
|
-
if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
|
|
260
|
-
const fileContent = fs.readFileSync(fullP, 'utf-8');
|
|
261
|
-
readfileOutput += `### 📄 File: \`${f}\`\n\`\`\`ts\n${fileContent.slice(0, 8000)}${fileContent.length > 8000 ? '\n... (truncated)' : ''}\n\`\`\`\n\n`;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
let manifestContent = '';
|
|
266
|
-
const pkgPath = path.join(cwd, 'package.json');
|
|
267
|
-
if (fs.existsSync(pkgPath)) {
|
|
268
|
-
try {
|
|
269
|
-
manifestContent = fs.readFileSync(pkgPath, 'utf-8');
|
|
270
|
-
}
|
|
271
|
-
catch { }
|
|
272
|
-
}
|
|
273
|
-
const aiAnswer = await askRepoQuestion(rawGoal + (readfileOutput ? `\n\nReferenced File Content:\n${readfileOutput}` : ''), projectName, filesList, manifestContent, [], { language: targetLang, native: options?.native });
|
|
274
|
-
s.stop('Query analysis complete.');
|
|
275
|
-
const outputMd = readfileOutput ? `${readfileOutput}### 💡 AI Explanation & Answer\n${aiAnswer}` : aiAnswer;
|
|
276
|
-
safeNote(renderMarkdown(outputMd), '📖 Scout Agent Q&A Response');
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
// -------------------------------------------------------------
|
|
280
322
|
// Mode 1: Codebase Scan Mode
|
|
281
323
|
// -------------------------------------------------------------
|
|
282
324
|
const isScanIntent = options?.scan || /\b(?:scan|bug scan|scan codebase|check for bugs|find bugs|security scan)\b/i.test(goalLower);
|
|
@@ -393,14 +435,17 @@ ${analysis.targetFileToFix ? `\n### 🎯 Suspected File\n\`${analysis.targetFile
|
|
|
393
435
|
// -------------------------------------------------------------
|
|
394
436
|
// Mode 4: Create File Generation Mode (--create)
|
|
395
437
|
// -------------------------------------------------------------
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
438
|
+
const targetCreateFiles = options?.create
|
|
439
|
+
? options.create.split(',').map((s) => s.trim()).filter(Boolean)
|
|
440
|
+
: (parsedIntent.intentType === 'create' && parsedIntent.targetFiles && parsedIntent.targetFiles.length > 0
|
|
441
|
+
? parsedIntent.targetFiles
|
|
442
|
+
: extractCreateTargetFiles(effectiveGoal));
|
|
443
|
+
if (targetCreateFiles && targetCreateFiles.length > 0) {
|
|
399
444
|
let finalPrompt = effectiveGoal;
|
|
400
445
|
if (!finalPrompt || !finalPrompt.trim()) {
|
|
401
446
|
const userPromptInput = await text({
|
|
402
|
-
message: chalk.bold(`What should Scout generate inside ${chalk.cyan(
|
|
403
|
-
placeholder: 'e.g. Create
|
|
447
|
+
message: chalk.bold(`What should Scout generate inside ${chalk.cyan(targetCreateFiles.join(', '))}?`),
|
|
448
|
+
placeholder: 'e.g. Create source files with modules, styles, and logic',
|
|
404
449
|
validate: (val) => (!val || !val.trim() ? 'Please provide a prompt describing the file requirements' : undefined),
|
|
405
450
|
});
|
|
406
451
|
if (isCancel(userPromptInput)) {
|
|
@@ -409,9 +454,9 @@ ${analysis.targetFileToFix ? `\n### 🎯 Suspected File\n\`${analysis.targetFile
|
|
|
409
454
|
}
|
|
410
455
|
finalPrompt = userPromptInput.trim();
|
|
411
456
|
}
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
const
|
|
457
|
+
const createdFilesSummary = [];
|
|
458
|
+
const backups = [];
|
|
459
|
+
const affectedFiles = [];
|
|
415
460
|
const git = simpleGit();
|
|
416
461
|
const isRepo = await git.checkIsRepo();
|
|
417
462
|
let currentBranch = 'main';
|
|
@@ -423,70 +468,87 @@ ${analysis.targetFileToFix ? `\n### 🎯 Suspected File\n\`${analysis.targetFile
|
|
|
423
468
|
catch { }
|
|
424
469
|
}
|
|
425
470
|
const s = spinner();
|
|
426
|
-
s.start(`🤖 Scout Agent: Generating
|
|
427
|
-
let
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
471
|
+
s.start(`🤖 Scout Agent: Generating ${targetCreateFiles.length} file(s) (${targetCreateFiles.join(', ')})...`);
|
|
472
|
+
let hasError = false;
|
|
473
|
+
for (const targetFilePath of targetCreateFiles) {
|
|
474
|
+
const resolvedPath = path.resolve(cwd, targetFilePath);
|
|
475
|
+
const fileExists = fs.existsSync(resolvedPath);
|
|
476
|
+
const originalContent = fileExists ? fs.readFileSync(resolvedPath, 'utf-8') : '__FT_WAS_NEW__';
|
|
477
|
+
try {
|
|
478
|
+
const contextPrompt = finalPrompt + (createdFilesSummary.length > 0 ? `\n\nContext of already generated files in this batch:\n` + createdFilesSummary.map(f => `--- File: ${f.path} ---\n${f.code.slice(0, 1000)}`).join('\n') : '');
|
|
479
|
+
const genResult = await generateCodeFile(targetFilePath, contextPrompt, projectName, filesList, targetLang, options?.native);
|
|
480
|
+
if (!genResult.code || !genResult.code.trim()) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const parentDir = path.dirname(resolvedPath);
|
|
484
|
+
if (!fs.existsSync(parentDir)) {
|
|
485
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
486
|
+
}
|
|
487
|
+
fs.writeFileSync(resolvedPath, genResult.code, 'utf-8');
|
|
488
|
+
backups.push({ filePath: targetFilePath, originalContent });
|
|
489
|
+
affectedFiles.push(targetFilePath);
|
|
490
|
+
createdFilesSummary.push({ path: targetFilePath, code: genResult.code, description: genResult.description });
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
hasError = true;
|
|
494
|
+
s.stop(chalk.red(`Failed to generate code for ${targetFilePath}.`));
|
|
495
|
+
safeNote(chalk.red(`Error: ${err?.message || String(err)}`), '⚠️ Code Generation Error');
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
447
498
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
499
|
+
if (hasError || createdFilesSummary.length === 0) {
|
|
500
|
+
if (!hasError) {
|
|
501
|
+
s.stop(chalk.red('Failed to generate code files.'));
|
|
502
|
+
safeNote(chalk.yellow(`Scout AI returned empty code content. Please refine your prompt describing what code to write.`), '⚠️ Empty Code Generated');
|
|
503
|
+
}
|
|
451
504
|
continue;
|
|
452
505
|
}
|
|
453
506
|
recordHistoryAction({
|
|
454
|
-
command: `scout agent --create "${
|
|
455
|
-
description: `Scout created file ${
|
|
507
|
+
command: `scout agent --create "${affectedFiles.join(',')}" "${finalPrompt}"`,
|
|
508
|
+
description: `Scout created ${createdFilesSummary.length} file(s): ${affectedFiles.join(', ')}`,
|
|
456
509
|
previousBranch: currentBranch,
|
|
457
|
-
affectedFiles
|
|
458
|
-
backups
|
|
510
|
+
affectedFiles,
|
|
511
|
+
backups,
|
|
512
|
+
});
|
|
513
|
+
s.stop(chalk.green(`Successfully created ${createdFilesSummary.length} file(s)!`));
|
|
514
|
+
// Run automated syntax check and self-healing retries on created files
|
|
515
|
+
const healRes = await verifyAndSelfHealFiles(affectedFiles, cwd, projectName, filesList, {
|
|
516
|
+
verifyCmd: options?.verifyCmd,
|
|
517
|
+
maxRetries: options?.maxRetries || 3,
|
|
518
|
+
language: targetLang,
|
|
519
|
+
native: options?.native,
|
|
459
520
|
});
|
|
460
|
-
|
|
521
|
+
if (healRes.verifiedFiles.length > 0) {
|
|
522
|
+
safeNote(chalk.green(`✓ Automated Verification Confirmed: ${healRes.verifiedFiles.length} file(s) syntax & build verified clean!`), '✅ Code Verification Clean');
|
|
523
|
+
}
|
|
524
|
+
if (healRes.remainingErrors.length > 0) {
|
|
525
|
+
safeNote(chalk.yellow(`⚠️ Remaining verification issues:\n${healRes.remainingErrors.join('\n')}`), '⚠️ Verification Warning');
|
|
526
|
+
}
|
|
527
|
+
const filePreviews = createdFilesSummary.map((f) => `#### 📄 \`${f.path}\`\n${f.description}\n\`\`\`\n${f.code.slice(0, 400)}${f.code.length > 400 ? '\n... (truncated)' : ''}\n\`\`\``).join('\n\n');
|
|
461
528
|
const teardownContent = `
|
|
462
|
-
### 📄 Created
|
|
463
|
-
\`${
|
|
464
|
-
|
|
465
|
-
### 💡 Description & Overview
|
|
466
|
-
${genResult.description}
|
|
529
|
+
### 📄 Created Files (${createdFilesSummary.length})
|
|
530
|
+
${createdFilesSummary.map(f => ` • \`${f.path}\``).join('\n')}
|
|
467
531
|
|
|
468
|
-
###
|
|
469
|
-
|
|
470
|
-
${genResult.code.slice(0, 500)}${genResult.code.length > 500 ? '\n... (truncated)' : ''}
|
|
471
|
-
\`\`\`
|
|
532
|
+
### 💡 Overview & Previews
|
|
533
|
+
${filePreviews}
|
|
472
534
|
`.trim();
|
|
473
535
|
safeNote(renderMarkdown(teardownContent), '✨ Scout File Creation Teardown');
|
|
474
536
|
if (options?.yes || !process.stdout.isTTY) {
|
|
475
|
-
safeNote(`${chalk.green('✓')} Saved ${chalk.bold.cyan(
|
|
537
|
+
safeNote(`${chalk.green('✓')} Saved ${chalk.bold.cyan(affectedFiles.length + ' file(s)')} on branch ${chalk.bold.green(currentBranch)}.\n` +
|
|
476
538
|
`💡 ${chalk.dim('You can revert this creation anytime by running:')} ${chalk.cyan('scout undo')}`, '✅ Creation Confirmed');
|
|
477
539
|
continue;
|
|
478
540
|
}
|
|
479
541
|
const actionChoice = await select({
|
|
480
|
-
message: chalk.bold('Review Scout Agent\'s generated file. What would you like to do?'),
|
|
542
|
+
message: chalk.bold('Review Scout Agent\'s generated file(s). What would you like to do?'),
|
|
481
543
|
options: [
|
|
482
|
-
{ value: 'keep', label: '✅ Keep
|
|
483
|
-
{ value: 'pr', label: '🔀 Create Pull Request', hint: 'Push created
|
|
484
|
-
{ value: 'revert', label: '↩️ Revert / Delete
|
|
544
|
+
{ value: 'keep', label: '✅ Keep Files', hint: `Retain newly created files directly on current branch (${currentBranch})` },
|
|
545
|
+
{ value: 'pr', label: '🔀 Create Pull Request', hint: 'Push created files to a new branch & open a Pull Request' },
|
|
546
|
+
{ value: 'revert', label: '↩️ Revert / Delete Files', hint: 'Remove created files immediately' },
|
|
485
547
|
],
|
|
486
548
|
});
|
|
487
549
|
if (isCancel(actionChoice) || actionChoice === 'revert') {
|
|
488
550
|
const revSpinner = spinner();
|
|
489
|
-
revSpinner.start('Reverting Scout Agent\'s file
|
|
551
|
+
revSpinner.start('Reverting Scout Agent\'s file creations...');
|
|
490
552
|
await handleUndo();
|
|
491
553
|
revSpinner.stop('Creation reverted successfully.');
|
|
492
554
|
continue;
|
|
@@ -496,171 +558,40 @@ ${genResult.code.slice(0, 500)}${genResult.code.length > 500 ? '\n... (truncated
|
|
|
496
558
|
prSpinner.start('Creating Pull Request...');
|
|
497
559
|
const prRes = await createPullRequest({
|
|
498
560
|
branchPrefix: 'create',
|
|
499
|
-
title: `feat:
|
|
500
|
-
affectedFiles
|
|
501
|
-
commitMessage: `feat: Create ${
|
|
561
|
+
title: `feat: Create ${affectedFiles.join(', ')}`,
|
|
562
|
+
affectedFiles,
|
|
563
|
+
commitMessage: `feat: Create ${affectedFiles.join(', ')}`,
|
|
502
564
|
});
|
|
503
565
|
prSpinner.stop('Pull Request workflow processed.');
|
|
504
566
|
safeNote(`${chalk.green('✓')} ${prRes.message}\n` +
|
|
505
567
|
`💡 ${chalk.dim('You remain on active branch:')} ${chalk.cyan(currentBranch)}`, '🔀 Pull Request Status');
|
|
506
568
|
continue;
|
|
507
569
|
}
|
|
508
|
-
safeNote(`${chalk.green('✓')} Saved ${chalk.bold.cyan(
|
|
570
|
+
safeNote(`${chalk.green('✓')} Saved ${chalk.bold.cyan(affectedFiles.length + ' file(s)')} on branch ${chalk.bold.green(currentBranch)}.\n` +
|
|
509
571
|
`💡 ${chalk.dim('You can revert this creation anytime by running:')} ${chalk.cyan('scout undo')}`, '✅ Creation Confirmed');
|
|
510
572
|
continue;
|
|
511
573
|
}
|
|
512
574
|
// -------------------------------------------------------------
|
|
513
|
-
// Mode 5: Autonomous Agent Loop
|
|
575
|
+
// Mode 5: Autonomous ReAct Tool-Calling Agent Loop
|
|
514
576
|
// -------------------------------------------------------------
|
|
515
577
|
let finalGoal = effectiveGoal || rawGoal;
|
|
516
|
-
let specifiedTargetFile = options?.file || parsedIntent.targetFile;
|
|
517
578
|
if (!finalGoal && options?.issue) {
|
|
518
579
|
const report = loadScanReport();
|
|
519
580
|
const issueId = Number(options.issue);
|
|
520
581
|
const targetIssue = report.find((i) => i.id === issueId) || report[0];
|
|
521
582
|
if (targetIssue) {
|
|
522
583
|
finalGoal = `Fix issue #${targetIssue.id}: ${targetIssue.title} — ${targetIssue.description}`;
|
|
523
|
-
specifiedTargetFile = targetIssue.file;
|
|
524
584
|
}
|
|
525
585
|
}
|
|
526
586
|
if (!finalGoal || !finalGoal.trim()) {
|
|
527
587
|
safeNote(chalk.yellow('No active goal specified.'), '🤖 Scout Agent');
|
|
528
588
|
continue;
|
|
529
589
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
targetFiles = [specifiedTargetFile];
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
if (targetFiles.length === 0) {
|
|
539
|
-
targetFiles = findTargetFilesFromPrompt(finalGoal, undefined, filesList);
|
|
540
|
-
}
|
|
541
|
-
if (targetFiles.length === 0) {
|
|
542
|
-
const sIdentify = spinner();
|
|
543
|
-
sIdentify.start(`🤖 Scout Agent initializing autonomous workflow for: "${finalGoal}"...`);
|
|
544
|
-
const aiMatched = await identifyTargetFiles(finalGoal, projectName, filesList, targetLang, options?.native);
|
|
545
|
-
sIdentify.stop('Target files identified.');
|
|
546
|
-
if (aiMatched.length > 0) {
|
|
547
|
-
targetFiles = aiMatched;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
if (targetFiles.length === 0) {
|
|
551
|
-
let possibleNewFile = undefined;
|
|
552
|
-
if (specifiedTargetFile && specifiedTargetFile.trim()) {
|
|
553
|
-
const clean = specifiedTargetFile.trim();
|
|
554
|
-
if (!clean.startsWith('.') && !clean.startsWith('*') && clean.includes('.')) {
|
|
555
|
-
possibleNewFile = clean;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
if (!possibleNewFile && rawGoal) {
|
|
559
|
-
possibleNewFile = extractCreateTargetFile(rawGoal);
|
|
560
|
-
}
|
|
561
|
-
if (possibleNewFile) {
|
|
562
|
-
await handleAgent(finalGoal, { ...options, create: possibleNewFile });
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
safeNote(chalk.yellow(`Scout Agent could not identify specific files needing modification for goal: "${finalGoal}".\nPlease refine your request or specify target files with --file.`), '⚠️ Agent File Selection');
|
|
566
|
-
continue;
|
|
567
|
-
}
|
|
568
|
-
const s = spinner();
|
|
569
|
-
s.start(`🤖 Scout Agent processing: "${finalGoal}" across file(s): ${targetFiles.join(', ')}...`);
|
|
570
|
-
const targetFilesMap = {};
|
|
571
|
-
const backups = [];
|
|
572
|
-
const normKey = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '').trim();
|
|
573
|
-
for (const tf of targetFiles) {
|
|
574
|
-
const fullPath = path.resolve(cwd, tf);
|
|
575
|
-
if (fs.existsSync(fullPath)) {
|
|
576
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
577
|
-
targetFilesMap[normKey(tf)] = content;
|
|
578
|
-
backups.push({ filePath: normKey(tf), originalContent: content });
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// Code Modification
|
|
582
|
-
s.message('Applying initial code modifications...');
|
|
583
|
-
let fixResult = await generateMultiCodeFix(finalGoal, projectName, filesList, targetFilesMap, targetLang, options?.native, (completed, total, file) => {
|
|
584
|
-
s.message(`Modifying files (${completed}/${total}): ${file}...`);
|
|
585
|
-
});
|
|
586
|
-
const modifiedFiles = [];
|
|
587
|
-
for (const item of fixResult.files) {
|
|
588
|
-
if (item.fixedCode !== undefined && item.fixedCode.trim().length > 0) {
|
|
589
|
-
let k = normKey(item.targetFile);
|
|
590
|
-
if (!(k in targetFilesMap) && targetFiles.length > 0) {
|
|
591
|
-
const matchKey = Object.keys(targetFilesMap).find((tfKey) => tfKey.toLowerCase() === k.toLowerCase() || path.basename(tfKey).toLowerCase() === path.basename(k).toLowerCase());
|
|
592
|
-
if (matchKey)
|
|
593
|
-
k = matchKey;
|
|
594
|
-
else if (targetFiles.length === 1)
|
|
595
|
-
k = normKey(targetFiles[0]);
|
|
596
|
-
}
|
|
597
|
-
const fullPath = path.resolve(cwd, k);
|
|
598
|
-
const orig = targetFilesMap[k] ?? '';
|
|
599
|
-
const normalizedFixed = item.fixedCode.replace(/\r\n/g, '\n').trim();
|
|
600
|
-
const normalizedOrig = orig.replace(/\r\n/g, '\n').trim();
|
|
601
|
-
if (normalizedFixed !== normalizedOrig) {
|
|
602
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
603
|
-
fs.writeFileSync(fullPath, item.fixedCode, 'utf-8');
|
|
604
|
-
modifiedFiles.push(k);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
// Autonomous Verification & Self-Healing Loop
|
|
609
|
-
let verifyCommand = options?.verifyCmd;
|
|
610
|
-
if (!verifyCommand) {
|
|
611
|
-
const cmdInPromptMatch = finalGoal.match(/(?:deploy|verify|test|run|build)(?:\s+(?:by\s+running|with|using|and\s+run|via))?\s+['"`]([^'"`]+)['"`]/i) ||
|
|
612
|
-
finalGoal.match(/(?:deploy|verify|test|run)\s+(?:by\s+running|with|using)\s+(.+?)(?:$|\!|\.|\,)/i);
|
|
613
|
-
if (cmdInPromptMatch && cmdInPromptMatch[1]) {
|
|
614
|
-
verifyCommand = cmdInPromptMatch[1].trim();
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (!verifyCommand) {
|
|
618
|
-
const pkgPath = path.join(cwd, 'package.json');
|
|
619
|
-
if (fs.existsSync(pkgPath)) {
|
|
620
|
-
try {
|
|
621
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
622
|
-
if (pkg.scripts?.test && pkg.scripts.test !== 'echo "Error: no test specified" && exit 1') {
|
|
623
|
-
verifyCommand = 'npm test';
|
|
624
|
-
}
|
|
625
|
-
else if (pkg.scripts?.build) {
|
|
626
|
-
verifyCommand = 'npm run build';
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
catch { }
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
let verificationSuccess = true;
|
|
633
|
-
let attempts = 0;
|
|
634
|
-
const maxAttempts = options?.maxRetries || 3;
|
|
635
|
-
if (verifyCommand) {
|
|
636
|
-
s.message(`Running verification command: \`${verifyCommand}\`...`);
|
|
637
|
-
while (attempts < maxAttempts) {
|
|
638
|
-
attempts++;
|
|
639
|
-
try {
|
|
640
|
-
await execAsync(verifyCommand, { cwd });
|
|
641
|
-
verificationSuccess = true;
|
|
642
|
-
s.message(`Verification command \`${verifyCommand}\` passed!`);
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
catch (cmdErr) {
|
|
646
|
-
verificationSuccess = false;
|
|
647
|
-
const output = (cmdErr.stdout || '') + '\n' + (cmdErr.stderr || '');
|
|
648
|
-
s.message(`Attempt ${attempts}/${maxAttempts}: Command failed. Diagnosing & self-healing...`);
|
|
649
|
-
const diagnosis = await analyzeCommandError(verifyCommand, cmdErr.code || 1, output, projectName, filesList, targetLang, options?.native);
|
|
650
|
-
const fileToFix = diagnosis.targetFileToFix || targetFiles[0] || filesList[0] || '';
|
|
651
|
-
if (fileToFix && fs.existsSync(path.resolve(cwd, fileToFix))) {
|
|
652
|
-
const currentContent = fs.readFileSync(path.resolve(cwd, fileToFix), 'utf-8');
|
|
653
|
-
const healPrompt = `Fix error running \`${verifyCommand}\`: ${diagnosis.rootCause}. Suggestion: ${diagnosis.suggestedFix}`;
|
|
654
|
-
const healedRes = await generateMultiCodeFix(healPrompt, projectName, filesList, { [fileToFix]: currentContent }, targetLang, options?.native);
|
|
655
|
-
if (healedRes.files[0]?.fixedCode) {
|
|
656
|
-
fs.writeFileSync(path.resolve(cwd, fileToFix), healedRes.files[0].fixedCode, 'utf-8');
|
|
657
|
-
if (!modifiedFiles.includes(fileToFix))
|
|
658
|
-
modifiedFiles.push(fileToFix);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
590
|
+
safeNote(`${chalk.bold.cyan('🤖 Scout Autonomous Execution Initialized')}\n` +
|
|
591
|
+
`${chalk.dim('Goal:')} ${finalGoal}`, '⚡ Scout Agent Execution');
|
|
592
|
+
const execResult = await agentLoop.executeGoal(finalGoal);
|
|
593
|
+
const modifiedFiles = agentLoop.getModifiedFiles();
|
|
594
|
+
const backups = agentLoop.getBackups();
|
|
664
595
|
const git = simpleGit();
|
|
665
596
|
const isRepo = await git.checkIsRepo();
|
|
666
597
|
let currentBranch = 'main';
|
|
@@ -680,7 +611,6 @@ ${genResult.code.slice(0, 500)}${genResult.code.length > 500 ? '\n... (truncated
|
|
|
680
611
|
backups,
|
|
681
612
|
});
|
|
682
613
|
}
|
|
683
|
-
s.stop(chalk.green(`🤖 Scout Agent workflow completed for: "${finalGoal}"!`));
|
|
684
614
|
const teardownMd = `
|
|
685
615
|
### 🎯 Agent Goal
|
|
686
616
|
${finalGoal}
|
|
@@ -688,21 +618,15 @@ ${finalGoal}
|
|
|
688
618
|
### 📄 Modified File(s)
|
|
689
619
|
${modifiedFiles.map((f) => ` • \`${f}\``).join('\n') || ' • None'}
|
|
690
620
|
|
|
691
|
-
###
|
|
692
|
-
${
|
|
693
|
-
|
|
694
|
-
### 🛠️ Solution Applied
|
|
695
|
-
${fixResult.howFixed}
|
|
696
|
-
|
|
697
|
-
### ✅ Verification Status
|
|
698
|
-
${verifyCommand ? (verificationSuccess ? `\`${verifyCommand}\` PASSED 🎉` : `\`${verifyCommand}\` FAILED (max retries reached) ⚠️`) : 'No automated verification script detected'}
|
|
621
|
+
### 💡 Execution Summary
|
|
622
|
+
${execResult.summary}
|
|
699
623
|
`.trim();
|
|
700
|
-
safeNote(renderMarkdown(teardownMd), '🤖 Scout Agent
|
|
624
|
+
safeNote(renderMarkdown(teardownMd), '🤖 Scout Agent Teardown Report');
|
|
701
625
|
if (modifiedFiles.length === 0)
|
|
702
626
|
continue;
|
|
703
627
|
if (options?.yes || !process.stdout.isTTY) {
|
|
704
628
|
safeNote(`${chalk.green('✓')} Agent changes retained on ${modifiedFiles.length} file(s) (Branch: ${chalk.bold.green(currentBranch)}).\n` +
|
|
705
|
-
`💡 ${chalk.dim('You can revert anytime by running:')} ${chalk.cyan('scout undo')}`, '✅ Agent
|
|
629
|
+
`💡 ${chalk.dim('You can revert anytime by running:')} ${chalk.cyan('scout undo')}`, '✅ Agent Execution Confirmed');
|
|
706
630
|
continue;
|
|
707
631
|
}
|
|
708
632
|
const actionChoice = await select({
|