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.
@@ -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 extractCreateTargetFile(rawGoal) {
34
+ export function extractCreateTargetFiles(rawGoal) {
33
35
  if (!rawGoal || !rawGoal.trim())
34
- return undefined;
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 undefined;
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
- return token.replace(/^\.\//, '');
69
+ const cleanPath = token.replace(/^\.\//, '');
70
+ if (!foundFiles.includes(cleanPath)) {
71
+ foundFiles.push(cleanPath);
72
+ }
59
73
  }
60
74
  }
61
75
  }
62
- return undefined;
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 targetFile = extractCreateTargetFile(cleaned) || extractCreateTargetFile(rawGoal);
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(targetFile && /\b(?:create|generate|make|build|write|add|edit|modify|update|populate|fill|implement|snippet|game|code)\b/i.test(lowerCleaned));
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 && targetFile) {
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 Session Active')} ${targetLang ? `(${targetLang})` : ''}
215
- ${chalk.dim('Type your goal, task, or question. Press Ctrl+C or type "exit" to quit.')}
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... (Ctrl+C / exit to quit)',
229
- validate: (val) => (!val || !val.trim() ? 'Please enter a prompt or Ctrl+C / exit to quit' : undefined),
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 targetCreateFile = options?.create || (parsedIntent.intentType === 'create' ? parsedIntent.targetFile : extractCreateTargetFile(effectiveGoal));
397
- if (targetCreateFile) {
398
- const targetFilePath = targetCreateFile.trim();
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(targetFilePath)}?`),
403
- placeholder: 'e.g. Create a utility module with functions for string formatting and timestamp parsing',
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 resolvedPath = path.resolve(cwd, targetFilePath);
413
- const fileExists = fs.existsSync(resolvedPath);
414
- const originalContent = fileExists ? fs.readFileSync(resolvedPath, 'utf-8') : '__FT_WAS_NEW__';
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 code for ${targetFilePath}...`);
427
- let genResult;
428
- try {
429
- genResult = await generateCodeFile(targetFilePath, finalPrompt, projectName, filesList, targetLang, options?.native);
430
- }
431
- catch (err) {
432
- s.stop(chalk.red('Failed to generate code file.'));
433
- safeNote(chalk.red(`Error: ${err?.message || String(err)}`), '⚠️ Code Generation Error');
434
- continue;
435
- }
436
- if (!genResult.code || !genResult.code.trim()) {
437
- s.stop(chalk.red('Failed to generate code file.'));
438
- safeNote(chalk.yellow(`Scout AI returned empty code content for ${targetFilePath}. Please refine your prompt describing what code to write.`), '⚠️ Empty Code Generated');
439
- continue;
440
- }
441
- const parentDir = path.dirname(resolvedPath);
442
- if (!fs.existsSync(parentDir)) {
443
- fs.mkdirSync(parentDir, { recursive: true });
444
- }
445
- try {
446
- fs.writeFileSync(resolvedPath, genResult.code, 'utf-8');
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
- catch (writeErr) {
449
- s.stop(chalk.red('Failed to write generated code to disk.'));
450
- safeNote(chalk.red(`Write error: ${writeErr?.message || String(writeErr)}`), '⚠️ File Write Error');
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 "${targetFilePath}" "${finalPrompt}"`,
455
- description: `Scout created file ${targetFilePath}`,
507
+ command: `scout agent --create "${affectedFiles.join(',')}" "${finalPrompt}"`,
508
+ description: `Scout created ${createdFilesSummary.length} file(s): ${affectedFiles.join(', ')}`,
456
509
  previousBranch: currentBranch,
457
- affectedFiles: [targetFilePath],
458
- backups: [{ filePath: targetFilePath, originalContent }],
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
- s.stop(chalk.green(`File ${targetFilePath} created successfully!`));
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 File
463
- \`${targetFilePath}\`
464
-
465
- ### 💡 Description & Overview
466
- ${genResult.description}
529
+ ### 📄 Created Files (${createdFilesSummary.length})
530
+ ${createdFilesSummary.map(f => ` • \`${f.path}\``).join('\n')}
467
531
 
468
- ### 💻 Preview
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(targetFilePath)} on branch ${chalk.bold.green(currentBranch)}.\n` +
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 File', hint: `Retain the newly created file directly on current branch (${currentBranch})` },
483
- { value: 'pr', label: '🔀 Create Pull Request', hint: 'Push created file to a new branch & open a Pull Request' },
484
- { value: 'revert', label: '↩️ Revert / Delete File', hint: 'Remove the created file immediately' },
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 creation...');
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: Add ${targetFilePath}`,
500
- affectedFiles: [targetFilePath],
501
- commitMessage: `feat: Create ${targetFilePath}`,
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(targetFilePath)} on branch ${chalk.bold.green(currentBranch)}.\n` +
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 & Code Fix Mode (Default)
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
- // File Identification
531
- let targetFiles = [];
532
- if (specifiedTargetFile) {
533
- targetFiles = findTargetFilesFromPrompt(finalGoal, specifiedTargetFile, filesList);
534
- if (targetFiles.length === 0 && fs.existsSync(path.resolve(cwd, specifiedTargetFile))) {
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
- ### 🔴 Problem Identified
692
- ${fixResult.problem}
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 Autonomous Teardown');
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 Fix Confirmed');
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({