ft-scout 6.0.5 → 6.0.9

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.
Files changed (65) hide show
  1. package/.firebase/hosting.d2Vi.cache +2 -2
  2. package/README.md +61 -38
  3. package/bin/ft.js +1 -1
  4. package/bin/src/commands/agent.d.ts.map +1 -1
  5. package/bin/src/commands/agent.js +439 -39
  6. package/bin/src/commands/agent.js.map +1 -1
  7. package/bin/src/commands/audit.js.map +1 -1
  8. package/bin/src/commands/authCommand.js.map +1 -1
  9. package/bin/src/commands/brief.js.map +1 -1
  10. package/bin/src/commands/configCommand.d.ts.map +1 -1
  11. package/bin/src/commands/configCommand.js.map +1 -1
  12. package/bin/src/commands/dashboard.js.map +1 -1
  13. package/bin/src/commands/history.js.map +1 -1
  14. package/bin/src/commands/init.js.map +1 -1
  15. package/bin/src/commands/login.d.ts.map +1 -1
  16. package/bin/src/commands/login.js.map +1 -1
  17. package/bin/src/commands/logout.js.map +1 -1
  18. package/bin/src/commands/owners.js.map +1 -1
  19. package/bin/src/commands/quota.js.map +1 -1
  20. package/bin/src/commands/recommend.js.map +1 -1
  21. package/bin/src/commands/risky.d.ts.map +1 -1
  22. package/bin/src/commands/risky.js.map +1 -1
  23. package/bin/src/commands/setup.js.map +1 -1
  24. package/bin/src/commands/signup.d.ts.map +1 -1
  25. package/bin/src/commands/signup.js.map +1 -1
  26. package/bin/src/commands/undo.js.map +1 -1
  27. package/bin/src/commands/web.js.map +1 -1
  28. package/bin/src/discovery/config.d.ts.map +1 -1
  29. package/bin/src/discovery/config.js.map +1 -1
  30. package/bin/src/discovery/display.js.map +1 -1
  31. package/bin/src/discovery/messages.js.map +1 -1
  32. package/bin/src/discovery/signals.d.ts.map +1 -1
  33. package/bin/src/discovery/signals.js.map +1 -1
  34. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  35. package/bin/src/engine/agentEngine.js +369 -33
  36. package/bin/src/engine/agentEngine.js.map +1 -1
  37. package/bin/src/engine/git.d.ts.map +1 -1
  38. package/bin/src/engine/git.js.map +1 -1
  39. package/bin/src/engine/llm.d.ts.map +1 -1
  40. package/bin/src/engine/llm.js +57 -6
  41. package/bin/src/engine/llm.js.map +1 -1
  42. package/bin/src/engine/verifier.d.ts.map +1 -1
  43. package/bin/src/engine/verifier.js.map +1 -1
  44. package/bin/src/engine/web.d.ts.map +1 -1
  45. package/bin/src/engine/web.js.map +1 -1
  46. package/bin/src/index.js +4 -4
  47. package/bin/src/index.js.map +1 -1
  48. package/bin/src/server/server.js.map +1 -1
  49. package/bin/src/utils/auth.d.ts.map +1 -1
  50. package/bin/src/utils/auth.js.map +1 -1
  51. package/bin/src/utils/branding.js +7 -7
  52. package/bin/src/utils/branding.js.map +1 -1
  53. package/bin/src/utils/config.d.ts.map +1 -1
  54. package/bin/src/utils/config.js.map +1 -1
  55. package/bin/src/utils/email.d.ts.map +1 -1
  56. package/bin/src/utils/email.js.map +1 -1
  57. package/bin/src/utils/firebaseAuth.d.ts.map +1 -1
  58. package/bin/src/utils/firebaseAuth.js.map +1 -1
  59. package/bin/src/utils/holoseneAuth.d.ts.map +1 -1
  60. package/bin/src/utils/holoseneAuth.js.map +1 -1
  61. package/bin/src/utils/language.d.ts.map +1 -1
  62. package/bin/src/utils/language.js.map +1 -1
  63. package/bin/src/utils/markdown.js.map +1 -1
  64. package/bin/src/utils/updateCheck.js.map +1 -1
  65. package/package.json +2 -2
@@ -1,16 +1,18 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import readline from 'readline';
3
4
  import { spinner, text, isCancel, select, confirm } from '@clack/prompts';
4
5
  import chalk from 'chalk';
5
6
  import { simpleGit } from 'simple-git';
6
- import { generateMultiCodeFix, identifyTargetFiles, analyzeCommandError, generateCodeScanReport, generateFeaturePlan, generateCodeFile, askRepoQuestion, } from '../engine/llm.js';
7
+ import { generateMultiCodeFix, identifyTargetFiles, analyzeCommandError, generateCodeScanReport, generateFeaturePlan, generateCodeFile, askRepoQuestion, getActiveModelInfo, } from '../engine/llm.js';
7
8
  import { loadContext, recordHistoryAction, getDirectoryFiles } from '../utils/config.js';
8
9
  import { renderMarkdown, safeNote } from '../utils/markdown.js';
9
10
  import { normalizeLanguage } from '../utils/language.js';
10
11
  import { createPullRequest } from '../engine/git.js';
12
+ import { consumeCredit, checkTierAccess } from '../utils/auth.js';
11
13
  import { handleUndo } from './undo.js';
12
- import { AgentExecutionLoop } from '../engine/agentEngine.js';
13
- import { verifyAndSelfHealFiles, executeSmartCommand } from '../engine/verifier.js';
14
+ import { AgentExecutionLoop, buildDirectoryTree } from '../engine/agentEngine.js';
15
+ import { verifyAndSelfHealFiles, executeSmartCommand, runProjectVerification } from '../engine/verifier.js';
14
16
  import { exec } from 'child_process';
15
17
  import { promisify } from 'util';
16
18
  const execAsync = promisify(exec);
@@ -226,7 +228,78 @@ export function findTargetFilesFromPrompt(userPrompt, specifiedFile, filesList)
226
228
  }
227
229
  return [];
228
230
  }
229
- import { consumeCredit, checkTierAccess } from '../utils/auth.js';
231
+ export async function getAgentGoalInput(placeholderStr) {
232
+ if (!process.stdin.isTTY) {
233
+ return await text({
234
+ message: chalk.bold('Agent Prompt:'),
235
+ placeholder: placeholderStr,
236
+ validate: (val) => (!val || !val.trim() ? 'Please enter a prompt or /exit to quit' : undefined),
237
+ });
238
+ }
239
+ return new Promise((resolve) => {
240
+ let buffer = '';
241
+ let isDone = false;
242
+ const rl = readline.createInterface({
243
+ input: process.stdin,
244
+ output: process.stdout,
245
+ terminal: true,
246
+ });
247
+ readline.emitKeypressEvents(process.stdin, rl);
248
+ if (process.stdin.setRawMode) {
249
+ process.stdin.setRawMode(true);
250
+ }
251
+ process.stdout.write(`\n${chalk.cyan('◇')} ${chalk.bold('Agent Prompt:')}\n${chalk.cyan('│')} `);
252
+ const cleanup = () => {
253
+ process.stdin.removeListener('keypress', onKeypress);
254
+ if (process.stdin.setRawMode) {
255
+ process.stdin.setRawMode(false);
256
+ }
257
+ rl.close();
258
+ };
259
+ const onKeypress = (str, key) => {
260
+ if (isDone)
261
+ return;
262
+ if (str === '\u0003' || (key && key.ctrl && key.name === 'c')) {
263
+ isDone = true;
264
+ cleanup();
265
+ process.stdout.write('\n');
266
+ resolve(Symbol.for('clack:cancel'));
267
+ return;
268
+ }
269
+ // Immediately catch '/' keypress as the very first typed character without requiring Enter!
270
+ if (buffer.length === 0 && str === '/') {
271
+ isDone = true;
272
+ cleanup();
273
+ readline.cursorTo(process.stdout, 0);
274
+ readline.clearLine(process.stdout, 0);
275
+ process.stdout.write(`${chalk.cyan('│')} /\n`);
276
+ resolve('/');
277
+ return;
278
+ }
279
+ if (key && (key.name === 'return' || key.name === 'enter')) {
280
+ isDone = true;
281
+ cleanup();
282
+ process.stdout.write('\n');
283
+ resolve(buffer.trim());
284
+ return;
285
+ }
286
+ if (key && key.name === 'backspace') {
287
+ if (buffer.length > 0) {
288
+ buffer = buffer.slice(0, -1);
289
+ readline.cursorTo(process.stdout, 0);
290
+ readline.clearLine(process.stdout, 0);
291
+ process.stdout.write(`${chalk.cyan('│')} ${buffer}`);
292
+ }
293
+ return;
294
+ }
295
+ if (str && !key.ctrl && !key.meta && key.name !== 'escape') {
296
+ buffer += str;
297
+ process.stdout.write(str);
298
+ }
299
+ };
300
+ process.stdin.on('keypress', onKeypress);
301
+ });
302
+ }
230
303
  export async function handleAgent(goalInput, options) {
231
304
  const tierCheck = checkTierAccess('agent');
232
305
  if (!tierCheck.allowed) {
@@ -267,17 +340,26 @@ export async function handleAgent(goalInput, options) {
267
340
  language: targetLang,
268
341
  projectName,
269
342
  });
343
+ const modelInfo = getActiveModelInfo();
344
+ const fallbackPipeline = modelInfo.fallbackChain.slice(1).join(' ➔ ');
270
345
  const welcomeBanner = `
271
346
  ${chalk.bold.cyan('Scout Autonomous AI Agent')} ${targetLang ? `(${targetLang})` : ''}
347
+ ${chalk.bold('Active Model:')} ${chalk.bold.green(modelInfo.currentActiveModel)}
348
+ ${chalk.bold('Fallback Pipeline:')} ${chalk.dim(fallbackPipeline)}
349
+
272
350
  ${chalk.dim('Type your goal, or use slash commands:')}
273
- ${chalk.cyan('/help')} - Show agent tools & commands.
274
- ${chalk.cyan('/diff')} - Show git diff for uncommited changes.
275
- ${chalk.cyan('/clear')} - Reset conversation context.
276
- ${chalk.cyan('/undo')} - Revert last file modifications.
277
- ${chalk.cyan('/scan')} - Run vulnerability scan.
278
- ${chalk.cyan('/plan')} - Generate implementation roadmap.
279
- ${chalk.cyan('/exit')} - Exit session.
280
- `.trim();
351
+ ${chalk.cyan('/help')} - Show agent tools & commands (14 tools)
352
+ ${chalk.cyan('/tree')} - View workspace directory tree
353
+ ${chalk.cyan('/stats')} - View real-time session & model telemetry
354
+ ${chalk.cyan('/test')} - Run automated project test suite
355
+ ${chalk.cyan('/diff')} - Show git diff of uncommitted changes
356
+ ${chalk.cyan('/export')} - Save session report to .ft/
357
+ ${chalk.cyan('/undo')} - Revert last file modifications
358
+ ${chalk.cyan('/clear')} - Reset conversation context
359
+ ${chalk.cyan('/scan')} - Run vulnerability scan
360
+ ${chalk.cyan('/plan')} - Generate implementation roadmap
361
+ ${chalk.cyan('/exit')} - Exit session
362
+ `.trim();
281
363
  safeNote(welcomeBanner, 'Scout Agent Active Session');
282
364
  while (true) {
283
365
  let rawGoal = undefined;
@@ -288,43 +370,258 @@ ${chalk.cyan('/clear')} - Reset conversation context.
288
370
  else {
289
371
  isFirstRun = false;
290
372
  options = {};
291
- const userGoalInput = await text({
292
- message: chalk.bold('Agent Prompt:'),
293
- placeholder: 'Ask question, fix bug, run command, scan issues, create file... (/help /exit)',
294
- validate: (val) => (!val || !val.trim() ? 'Please enter a prompt or /exit to quit' : undefined),
295
- });
296
- if (isCancel(userGoalInput)) {
373
+ const userGoalInput = await getAgentGoalInput('Ask question, fix bug, run command, scan issues, create file... (/help /exit)');
374
+ if (isCancel(userGoalInput) || typeof userGoalInput !== 'string') {
297
375
  safeNote(chalk.yellow('Exiting Scout Agent session. see you soon!'), 'Scout Agent Ended');
298
376
  break;
299
377
  }
300
- const trimmedInput = userGoalInput.trim();
378
+ let trimmedInput = userGoalInput.trim();
379
+ const KNOWN_SLASH_COMMANDS = [
380
+ '/help', '/tree', '/status', '/stats', '/test', '/explain',
381
+ '/export', '/save-session', '/diff', '/clear', '/compact',
382
+ '/undo', '/history', '/scan', '/plan', '/exit', '/quit'
383
+ ];
384
+ if (trimmedInput.startsWith('/')) {
385
+ const cmdName = trimmedInput.split(/\s+/)[0].toLowerCase();
386
+ if (!KNOWN_SLASH_COMMANDS.includes(cmdName)) {
387
+ const filterStr = trimmedInput.toLowerCase();
388
+ let availableOptions = [
389
+ { value: '/help', label: '/help', hint: 'Show agent tools & commands guide' },
390
+ { value: '/tree', label: '/tree', hint: 'Display workspace directory tree' },
391
+ { value: '/stats', label: '/stats', hint: 'Show session telemetry & model statistics' },
392
+ { value: '/test', label: '/test', hint: 'Run project verification test suite' },
393
+ { value: '/diff', label: '/diff', hint: 'Display git diff of uncommitted changes' },
394
+ { value: '/scan', label: '/scan', hint: 'Run read-only vulnerability scan' },
395
+ { value: '/plan', label: '/plan', hint: 'Generate feature roadmap & plan' },
396
+ { value: '/explain', label: '/explain', hint: 'Architectural breakdown of a file' },
397
+ { value: '/export', label: '/export', hint: 'Save session report to .ft/' },
398
+ { value: '/undo', label: '/undo', hint: 'Revert last file modifications' },
399
+ { value: '/history', label: '/history', hint: 'View recent action history logs' },
400
+ { value: '/clear', label: '/clear', hint: 'Reset conversation context' },
401
+ { value: '/exit', label: '/exit', hint: 'Exit Scout Agent session' },
402
+ ];
403
+ if (filterStr !== '/') {
404
+ const filtered = availableOptions.filter(o => o.value.startsWith(filterStr));
405
+ if (filtered.length > 0) {
406
+ availableOptions = filtered;
407
+ }
408
+ }
409
+ const pickedCmd = await select({
410
+ message: chalk.bold('Select a Slash Command:'),
411
+ options: availableOptions,
412
+ });
413
+ if (isCancel(pickedCmd)) {
414
+ continue;
415
+ }
416
+ let chosen = pickedCmd;
417
+ if (chosen === '/explain') {
418
+ const filePath = await text({
419
+ message: chalk.bold('Enter file path to explain (e.g. bin/src/index.ts):'),
420
+ placeholder: 'bin/src/index.ts',
421
+ validate: (val) => (!val || !val.trim() ? 'File path is required' : undefined),
422
+ });
423
+ if (isCancel(filePath))
424
+ continue;
425
+ chosen = `/explain ${filePath.trim()}`;
426
+ }
427
+ else if (chosen === '/plan') {
428
+ const idea = await text({
429
+ message: chalk.bold('Enter feature idea or goal for the plan:'),
430
+ placeholder: 'Add authentication middleware...',
431
+ validate: (val) => (!val || !val.trim() ? 'Goal description is required' : undefined),
432
+ });
433
+ if (isCancel(idea))
434
+ continue;
435
+ chosen = `/plan ${idea.trim()}`;
436
+ }
437
+ else if (chosen === '/tree') {
438
+ const dirInput = await text({
439
+ message: chalk.bold('Enter directory path & optional depth (default "." 4):'),
440
+ placeholder: '. 4',
441
+ });
442
+ if (!isCancel(dirInput) && typeof dirInput === 'string' && dirInput.trim()) {
443
+ chosen = `/tree ${dirInput.trim()}`;
444
+ }
445
+ }
446
+ else if (chosen === '/test') {
447
+ const testCmd = await text({
448
+ message: chalk.bold('Enter custom test command (press Enter for default):'),
449
+ placeholder: 'npm test',
450
+ });
451
+ if (!isCancel(testCmd) && typeof testCmd === 'string' && testCmd.trim()) {
452
+ chosen = `/test ${testCmd.trim()}`;
453
+ }
454
+ }
455
+ trimmedInput = chosen;
456
+ }
457
+ }
301
458
  if (['exit', 'quit', 'q', ':q', '/exit', '/quit'].includes(trimmedInput.toLowerCase())) {
302
459
  safeNote(chalk.yellow('Exiting Scout Agent session. see you soon!'), 'Scout Agent Ended');
303
460
  break;
304
461
  }
305
462
  if (trimmedInput.toLowerCase() === '/help') {
306
463
  const helpText = `
307
- ### ️ Scout Agent Tools
308
- - **read_file**: Read content & line ranges of codebase files.
309
- - **write_file**: Create new files or overwrite existing code.
310
- - **edit_file**: Search-and-replace exact code snippets.
311
- - **run_command**: Execute shell commands live (\`npm test\`, \`build\`).
312
- - **grep_search**: Regex/string search across all files.
313
- - **glob_search**: Pattern search for workspace files.
464
+ ### 🛠️ Scout Autonomous Agent Tools (14 Available)
465
+ - **read_file**: Read content & line ranges of workspace files.
466
+ - **write_file**: Create new files or overwrite code directly.
467
+ - **edit_file**: Search & replace exact target code snippets.
468
+ - **multi_edit_file**: Apply batch snippet replacements in a single step.
469
+ - **run_command**: Execute live terminal commands (\`npm test\`, \`build\`).
470
+ - **tree_view**: Generate visual ASCII directory tree structures.
471
+ - **file_info**: Get file statistics, size, lines, & modification time.
472
+ - **grep_search**: String/regex pattern search across codebase.
473
+ - **glob_search**: Filter workspace files by file pattern.
474
+ - **fetch_url**: Fetch web API documentation or URL content.
475
+ - **git_diff**: Retrieve uncommitted workspace git diff.
476
+ - **ask_user**: Request missing secret credentials or API keys.
477
+ - **task_completed**: Conclude autonomous goal execution with teardown.
314
478
 
315
- ### Slash Commands
316
- - \`/help\`: View this assistance screen.
317
- - \`/diff\`: Display colorized git diff of uncommitted workspace changes.
318
- - \`/clear\` or \`/compact\`: Clear session context window.
319
- - \`/undo\`: Revert last changes applied by Scout Agent.
320
- - \`/scan\`: Perform read-only security & bug scan.
321
- - \`/plan <idea>\`: Generate feature feasibility roadmap.
322
- - \`/history\`: View recent Scout actions.
323
- - \`/exit\`: Quit Scout Agent session.
479
+ ### 🚀 Interactive Slash Commands
480
+ - \`/help\`: Show this tools & commands guide.
481
+ - \`/tree [dir]\`: Display workspace directory tree structure.
482
+ - \`/status\` or \`/stats\`: Show session telemetry & statistics.
483
+ - \`/test [cmd]\`: Run project verification test suite.
484
+ - \`/explain <file>\`: Get deep architectural breakdown of a file.
485
+ - \`/export\` or \`/save-session\`: Save session report to \`.ft/session-<timestamp>.md\`.
486
+ - \`/diff\`: Display colorized git diff of uncommitted changes.
487
+ - \`/clear\` or \`/compact\`: Reset agent conversation context.
488
+ - \`/undo\`: Revert last file modifications.
489
+ - \`/history\`: View recent Scout action logs.
490
+ - \`/exit\`: End Scout Agent session.
324
491
  `.trim();
325
492
  safeNote(renderMarkdown(helpText), ' Scout Slash Commands & Tools');
326
493
  continue;
327
494
  }
495
+ if (trimmedInput.toLowerCase().startsWith('/tree')) {
496
+ const parts = trimmedInput.split(/\s+/).slice(1);
497
+ let targetDir = '.';
498
+ let maxDepth = 4;
499
+ if (parts.length === 1) {
500
+ if (/^\d+$/.test(parts[0])) {
501
+ maxDepth = parseInt(parts[0], 10);
502
+ }
503
+ else {
504
+ targetDir = parts[0];
505
+ }
506
+ }
507
+ else if (parts.length >= 2) {
508
+ if (/^\d+$/.test(parts[0])) {
509
+ maxDepth = parseInt(parts[0], 10);
510
+ targetDir = parts[1];
511
+ }
512
+ else {
513
+ targetDir = parts[0];
514
+ if (/^\d+$/.test(parts[1])) {
515
+ maxDepth = parseInt(parts[1], 10);
516
+ }
517
+ }
518
+ }
519
+ const fullDir = path.resolve(process.cwd(), targetDir);
520
+ if (!fs.existsSync(fullDir) || !fs.statSync(fullDir).isDirectory()) {
521
+ safeNote(chalk.yellow(`Directory not found: "${targetDir}"`), '️ Directory Tree Error');
522
+ }
523
+ else {
524
+ const treeStr = buildDirectoryTree(fullDir, '', 0, maxDepth);
525
+ safeNote(`Directory Tree for "${targetDir}" (depth ${maxDepth}):\n${targetDir}/\n${treeStr}`, ' Workspace Directory Tree');
526
+ }
527
+ continue;
528
+ }
529
+ if (['/status', '/stats'].includes(trimmedInput.toLowerCase())) {
530
+ const stats = agentLoop.getStats();
531
+ const modifiedFiles = agentLoop.getModifiedFiles();
532
+ const backups = agentLoop.getBackups();
533
+ const git = simpleGit();
534
+ let currentBranch = 'main';
535
+ try {
536
+ const branchSummary = await git.branch();
537
+ currentBranch = branchSummary.current || 'main';
538
+ }
539
+ catch { }
540
+ const elapsedSec = Math.round(stats.elapsedMs / 1000);
541
+ const activeModel = getActiveModelInfo();
542
+ const fallbackStatus = activeModel.lastFallbackInfo
543
+ ? `Switched from \`${activeModel.lastFallbackInfo.previousModel}\` ➔ \`${activeModel.lastFallbackInfo.fallbackModel}\` (${activeModel.lastFallbackInfo.reason})`
544
+ : 'None (Primary model actively responding)';
545
+ const statsMd = `
546
+ ### 📊 Scout Session & AI Model Telemetry
547
+ - **Active AI Model**: \`${activeModel.currentActiveModel}\`
548
+ - **Primary AI Model**: \`${activeModel.primaryModel}\`
549
+ - **Fallback Chain**: \`${activeModel.fallbackChain.join(' ➔ ')}\`
550
+ - **Last Fallback Event**: ${fallbackStatus}
551
+ - **Active Branch**: \`${currentBranch}\`
552
+ - **Elapsed Session Time**: \`${elapsedSec}s\`
553
+ - **Steps Executed**: \`${stats.totalSteps}\`
554
+ - **Total Tool Calls**: \`${stats.totalToolCalls}\`
555
+ - **Modified Files**: \`${modifiedFiles.length}\` (${modifiedFiles.map(f => `\`${f}\``).join(', ') || 'None'})
556
+ - **Stored File Backups**: \`${backups.length}\`
557
+ `.trim();
558
+ safeNote(renderMarkdown(statsMd), ' Scout Session & Model Telemetry');
559
+ continue;
560
+ }
561
+ if (trimmedInput.toLowerCase().startsWith('/test')) {
562
+ const parts = trimmedInput.split(/\s+/);
563
+ const customCmd = parts.slice(1).join(' ') || options?.verifyCmd;
564
+ const testSpin = spinner();
565
+ testSpin.start(`Running project verification test suite ${customCmd ? `(\`${customCmd}\`)` : ''}...`);
566
+ const testRes = await runProjectVerification(process.cwd(), customCmd);
567
+ if (testRes.valid) {
568
+ testSpin.stop(chalk.green('Project verification tests passed cleanly!'));
569
+ safeNote(`${chalk.green(' Verification Passed:')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Suite Clean');
570
+ }
571
+ else {
572
+ testSpin.stop(chalk.red('Project verification test suite failed.'));
573
+ safeNote(`${chalk.red('️ Verification Failed:')}\n${testRes.errors.join('\n')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Failure Warning');
574
+ }
575
+ continue;
576
+ }
577
+ if (trimmedInput.toLowerCase().startsWith('/explain')) {
578
+ const parts = trimmedInput.split(/\s+/);
579
+ const targetPath = parts[1];
580
+ if (!targetPath) {
581
+ safeNote(chalk.yellow('Usage: /explain <filePath> (e.g. /explain bin/src/index.ts)'), ' Explain Command');
582
+ continue;
583
+ }
584
+ const fullP = path.resolve(process.cwd(), targetPath);
585
+ if (!fs.existsSync(fullP)) {
586
+ safeNote(chalk.yellow(`File not found: ${targetPath}`), ' Explain Error');
587
+ continue;
588
+ }
589
+ const explainSpin = spinner();
590
+ explainSpin.start(`Analyzing architecture and structure of ${targetPath}...`);
591
+ const content = fs.readFileSync(fullP, 'utf-8');
592
+ const explanation = await askRepoQuestion(`Provide a deep architectural breakdown, primary purpose, component breakdown, and code explanation for file "${targetPath}":\n\`\`\`\n${content.slice(0, 6000)}\n\`\`\``, projectName, filesList, '', [], { language: targetLang });
593
+ explainSpin.stop('Analysis complete.');
594
+ safeNote(renderMarkdown(explanation), ` Architectural Breakdown: ${targetPath}`);
595
+ continue;
596
+ }
597
+ if (['/export', '/save-session'].includes(trimmedInput.toLowerCase())) {
598
+ const ftDir = path.join(process.cwd(), '.ft');
599
+ if (!fs.existsSync(ftDir))
600
+ fs.mkdirSync(ftDir, { recursive: true });
601
+ const timeStamp = new Date().toISOString().replace(/[:.]/g, '-');
602
+ const sessionPath = path.join(ftDir, `session-${timeStamp}.md`);
603
+ const stats = agentLoop.getStats();
604
+ const modifiedFiles = agentLoop.getModifiedFiles();
605
+ const history = (await import('../utils/config.js')).loadHistory();
606
+ const exportContent = `# Scout Agent Session Report
607
+ Date: ${new Date().toLocaleString()}
608
+ Project: ${projectName}
609
+
610
+ ## Session Statistics
611
+ - Total Steps: ${stats.totalSteps}
612
+ - Tool Calls: ${stats.totalToolCalls}
613
+ - Modified Files Count: ${modifiedFiles.length}
614
+
615
+ ## Modified Files
616
+ ${modifiedFiles.map(f => `- ${f}`).join('\n') || 'None'}
617
+
618
+ ## Action History
619
+ ${history.slice(0, 15).map(h => `- [${h.timestamp}] ${h.command}: ${h.description}`).join('\n')}
620
+ `;
621
+ fs.writeFileSync(sessionPath, exportContent, 'utf-8');
622
+ safeNote(chalk.green(`Session report saved to: ${chalk.bold.cyan(path.relative(process.cwd(), sessionPath))}`), ' Session Exported');
623
+ continue;
624
+ }
328
625
  if (trimmedInput.toLowerCase() === '/diff') {
329
626
  try {
330
627
  const { execSync } = await import('child_process');
@@ -619,14 +916,74 @@ ${filePreviews}
619
916
  safeNote(chalk.yellow('No active goal specified.'), ' Scout Agent');
620
917
  continue;
621
918
  }
919
+ const git = simpleGit();
920
+ const isRepo = await git.checkIsRepo();
921
+ let currentBranch = 'main';
922
+ if (isRepo) {
923
+ try {
924
+ const branchSummary = await git.branch();
925
+ currentBranch = branchSummary.current || 'main';
926
+ }
927
+ catch { }
928
+ }
929
+ // Fast Multi-File Fix path via generateMultiCodeFix if --issue is set
930
+ if (options?.issue) {
931
+ const idSpinner = spinner();
932
+ idSpinner.start('Scout AI: Identifying affected target files...');
933
+ const targetFiles = await identifyTargetFiles(finalGoal, projectName, filesList, targetLang, options?.native);
934
+ idSpinner.stop(targetFiles.length > 0 ? `Identified ${targetFiles.length} target file(s): ${targetFiles.join(', ')}` : 'Target identification complete.');
935
+ if (targetFiles.length > 0) {
936
+ const targetMap = {};
937
+ for (const tf of targetFiles) {
938
+ const absP = path.resolve(cwd, tf);
939
+ if (fs.existsSync(absP)) {
940
+ targetMap[tf] = fs.readFileSync(absP, 'utf-8');
941
+ }
942
+ }
943
+ if (Object.keys(targetMap).length > 0) {
944
+ const fixSpinner = spinner();
945
+ fixSpinner.start(`Scout AI: Generating multi-file fixes for ${Object.keys(targetMap).join(', ')}...`);
946
+ const multiFixRes = await generateMultiCodeFix(finalGoal, projectName, filesList, targetMap, targetLang, options?.native, (done, total, curFile) => {
947
+ fixSpinner.message(`Generating fix for ${curFile} (${done}/${total})...`);
948
+ });
949
+ fixSpinner.stop('Multi-file fixes generated.');
950
+ const backups = [];
951
+ const affectedFiles = [];
952
+ for (const item of multiFixRes.files) {
953
+ const absPath = path.resolve(cwd, item.targetFile);
954
+ if (fs.existsSync(absPath)) {
955
+ backups.push({
956
+ filePath: item.targetFile,
957
+ originalContent: fs.readFileSync(absPath, 'utf-8'),
958
+ });
959
+ }
960
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
961
+ fs.writeFileSync(absPath, item.fixedCode, 'utf-8');
962
+ affectedFiles.push(item.targetFile);
963
+ }
964
+ recordHistoryAction({
965
+ command: `scout agent --issue ${options.issue}`,
966
+ description: `Auto-fixed issue on ${affectedFiles.length} file(s)`,
967
+ previousBranch: currentBranch,
968
+ affectedFiles,
969
+ backups,
970
+ });
971
+ safeNote(`### Fixed Issue\n${multiFixRes.problem}\n\n### Fix Applied\n${multiFixRes.howFixed}\n\n### Modified Files\n${affectedFiles.map(f => ` • \`${f}\``).join('\n')}`, ' Scout Multi-File Auto-Fix Complete');
972
+ continue;
973
+ }
974
+ }
975
+ }
976
+ // Standard Agent Goal Execution: Pre-scan & identify target files
977
+ const idSpinner = spinner();
978
+ idSpinner.start('Scout AI: Analyzing codebase file tree & resolving target files...');
979
+ const targetFiles = await identifyTargetFiles(finalGoal, projectName, filesList, targetLang, options?.native);
980
+ idSpinner.stop(targetFiles.length > 0 ? `Target files resolved: ${targetFiles.map(f => `\`${f}\``).join(', ')}` : 'Analysis complete.');
622
981
  safeNote(`${chalk.bold.cyan(' Scout Autonomous Execution Initialized')}\n` +
623
- `${chalk.dim('Goal:')} ${finalGoal}`, ' Scout Agent Execution');
982
+ `${chalk.dim('Goal:')} ${finalGoal}` +
983
+ (targetFiles.length > 0 ? `\n${chalk.dim('Target Files:')} ${targetFiles.map(f => `\`${f}\``).join(', ')}` : ''), ' Scout Agent Execution');
624
984
  const execResult = await agentLoop.executeGoal(finalGoal);
625
985
  const modifiedFiles = agentLoop.getModifiedFiles();
626
986
  const backups = agentLoop.getBackups();
627
- const git = simpleGit();
628
- const isRepo = await git.checkIsRepo();
629
- let currentBranch = 'main';
630
987
  if (isRepo) {
631
988
  try {
632
989
  const branchSummary = await git.branch();
@@ -665,7 +1022,9 @@ ${execResult.summary}
665
1022
  message: chalk.bold('Review Scout Agent result. What would you like to do?'),
666
1023
  options: [
667
1024
  { value: 'keep', label: ' Keep Changes', hint: `Retain agent changes on current branch (${currentBranch})` },
1025
+ { value: 'test', label: ' Run Verification Tests', hint: 'Execute test suite to confirm zero regressions' },
668
1026
  { value: 'pr', label: ' Create Pull Request', hint: 'Push fix to a new branch & open a Pull Request' },
1027
+ { value: 'export', label: ' Export Session Report', hint: 'Save detailed session teardown report to .ft/' },
669
1028
  { value: 'revert', label: '️ Revert Changes', hint: 'Restore original code immediately' },
670
1029
  ],
671
1030
  });
@@ -676,6 +1035,47 @@ ${execResult.summary}
676
1035
  revSpinner.stop('Changes reverted successfully.');
677
1036
  continue;
678
1037
  }
1038
+ if (actionChoice === 'test') {
1039
+ const testSpin = spinner();
1040
+ testSpin.start('Running project verification test suite...');
1041
+ const testRes = await runProjectVerification(process.cwd(), options?.verifyCmd);
1042
+ if (testRes.valid) {
1043
+ testSpin.stop(chalk.green('Project verification tests passed cleanly!'));
1044
+ safeNote(`${chalk.green(' Verification Passed:')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Suite Clean');
1045
+ }
1046
+ else {
1047
+ testSpin.stop(chalk.red('Project verification test suite failed.'));
1048
+ safeNote(`${chalk.red('️ Verification Failed:')}\n${testRes.errors.join('\n')}\n\`\`\`\n${testRes.output.slice(0, 3000)}\n\`\`\``, ' Test Failure Warning');
1049
+ }
1050
+ continue;
1051
+ }
1052
+ if (actionChoice === 'export') {
1053
+ const ftDir = path.join(process.cwd(), '.ft');
1054
+ if (!fs.existsSync(ftDir))
1055
+ fs.mkdirSync(ftDir, { recursive: true });
1056
+ const timeStamp = new Date().toISOString().replace(/[:.]/g, '-');
1057
+ const sessionPath = path.join(ftDir, `session-${timeStamp}.md`);
1058
+ const stats = agentLoop.getStats();
1059
+ const exportContent = `# Scout Agent Session Report
1060
+ Date: ${new Date().toLocaleString()}
1061
+ Project: ${projectName}
1062
+ Goal: ${finalGoal}
1063
+
1064
+ ## Session Statistics
1065
+ - Total Steps: ${stats.totalSteps}
1066
+ - Tool Calls: ${stats.totalToolCalls}
1067
+ - Modified Files Count: ${modifiedFiles.length}
1068
+
1069
+ ## Modified Files
1070
+ ${modifiedFiles.map(f => `- ${f}`).join('\n') || 'None'}
1071
+
1072
+ ## Execution Summary
1073
+ ${execResult.summary}
1074
+ `;
1075
+ fs.writeFileSync(sessionPath, exportContent, 'utf-8');
1076
+ safeNote(chalk.green(`Session report saved to: ${chalk.bold.cyan(path.relative(process.cwd(), sessionPath))}`), ' Session Exported');
1077
+ continue;
1078
+ }
679
1079
  if (actionChoice === 'pr') {
680
1080
  const prSpinner = spinner();
681
1081
  prSpinner.start('Creating Pull Request...');