ft-scout 9.0.6 → 9.0.8

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 (84) hide show
  1. package/.firebase/hosting.d2Vi.cache +9 -9
  2. package/AUTONOMOUS_FEATURES.md +1 -1
  3. package/README.md +128 -21
  4. package/bin/src/commands/agent.d.ts.map +1 -1
  5. package/bin/src/commands/agent.js +45 -4
  6. package/bin/src/commands/agent.js.map +1 -1
  7. package/bin/src/commands/audit.js +1 -1
  8. package/bin/src/commands/audit.js.map +1 -1
  9. package/bin/src/commands/dashboard.js +1 -1
  10. package/bin/src/commands/dashboard.js.map +1 -1
  11. package/bin/src/commands/mcp.d.ts.map +1 -0
  12. package/bin/src/commands/mcp.js +393 -0
  13. package/bin/src/commands/mcp.js.map +1 -0
  14. package/bin/src/commands/owners.js +1 -1
  15. package/bin/src/commands/owners.js.map +1 -1
  16. package/bin/src/discovery/messages.js +2 -2
  17. package/bin/src/discovery/messages.js.map +1 -1
  18. package/bin/src/discovery/signals.js +3 -3
  19. package/bin/src/discovery/signals.js.map +1 -1
  20. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  21. package/bin/src/engine/agentEngine.js +214 -93
  22. package/bin/src/engine/agentEngine.js.map +1 -1
  23. package/bin/src/engine/planner.d.ts.map +1 -1
  24. package/bin/src/engine/planner.js +81 -2
  25. package/bin/src/engine/planner.js.map +1 -1
  26. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
  27. package/bin/src/engine/runtime/agentRuntime.js +79 -5
  28. package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
  29. package/bin/src/engine/scoutAudioRecorder.cs +215 -0
  30. package/bin/src/engine/scout_audio_recorder.exe +0 -0
  31. package/bin/src/engine/toolRegistry.d.ts.map +1 -1
  32. package/bin/src/engine/toolRegistry.js +13 -0
  33. package/bin/src/engine/toolRegistry.js.map +1 -1
  34. package/bin/src/engine/verifier.js +2 -2
  35. package/bin/src/engine/verifier.js.map +1 -1
  36. package/bin/src/engine/voiceEngine.d.ts.map +1 -1
  37. package/bin/src/engine/voiceEngine.js +333 -13
  38. package/bin/src/engine/voiceEngine.js.map +1 -1
  39. package/bin/src/index.js +85 -7
  40. package/bin/src/index.js.map +1 -1
  41. package/bin/src/mcp/client.d.ts.map +1 -0
  42. package/bin/src/mcp/client.js +127 -0
  43. package/bin/src/mcp/client.js.map +1 -0
  44. package/bin/src/mcp/discovery.d.ts.map +1 -0
  45. package/bin/src/mcp/discovery.js +91 -0
  46. package/bin/src/mcp/discovery.js.map +1 -0
  47. package/bin/src/mcp/health.d.ts.map +1 -0
  48. package/bin/src/mcp/health.js +44 -0
  49. package/bin/src/mcp/health.js.map +1 -0
  50. package/bin/src/mcp/index.d.ts.map +1 -0
  51. package/bin/src/mcp/index.js +15 -0
  52. package/bin/src/mcp/index.js.map +1 -0
  53. package/bin/src/mcp/manager.d.ts.map +1 -0
  54. package/bin/src/mcp/manager.js +535 -0
  55. package/bin/src/mcp/manager.js.map +1 -0
  56. package/bin/src/mcp/permissions.d.ts.map +1 -0
  57. package/bin/src/mcp/permissions.js +50 -0
  58. package/bin/src/mcp/permissions.js.map +1 -0
  59. package/bin/src/mcp/persistence.d.ts.map +1 -0
  60. package/bin/src/mcp/persistence.js +156 -0
  61. package/bin/src/mcp/persistence.js.map +1 -0
  62. package/bin/src/mcp/recovery.d.ts.map +1 -0
  63. package/bin/src/mcp/recovery.js +75 -0
  64. package/bin/src/mcp/recovery.js.map +1 -0
  65. package/bin/src/mcp/telemetry.d.ts.map +1 -0
  66. package/bin/src/mcp/telemetry.js +82 -0
  67. package/bin/src/mcp/telemetry.js.map +1 -0
  68. package/bin/src/mcp/toolRouter.d.ts.map +1 -0
  69. package/bin/src/mcp/toolRouter.js +127 -0
  70. package/bin/src/mcp/toolRouter.js.map +1 -0
  71. package/bin/src/mcp/transport.d.ts.map +1 -0
  72. package/bin/src/mcp/transport.js +328 -0
  73. package/bin/src/mcp/transport.js.map +1 -0
  74. package/bin/src/mcp/types.d.ts.map +1 -0
  75. package/bin/src/mcp/types.js +6 -0
  76. package/bin/src/mcp/types.js.map +1 -0
  77. package/bin/src/server/server.d.ts.map +1 -1
  78. package/bin/src/server/server.js +207 -0
  79. package/bin/src/server/server.js.map +1 -1
  80. package/bin/src/utils/branding.js +1 -1
  81. package/bin/src/utils/email.js +4 -4
  82. package/package.json +1 -1
  83. package/web/app.js +1255 -59
  84. package/web/styles.css +1474 -15
@@ -14,12 +14,13 @@ import { TaskStateManager } from './agentState.js';
14
14
  import { SemanticBrowserController } from './browser/index.js';
15
15
  import { ActionVerifier } from './actionVerifier.js';
16
16
  import { ApprovalManager, RiskClassifier } from './safety.js';
17
- import { TaskPlanner } from './planner.js';
17
+ import { TaskPlanner, checkSimpleAppAction } from './planner.js';
18
18
  import { UnifiedToolRegistry } from './toolRegistry.js';
19
19
  import { SystemComputerController, MockComputerController } from './computer/index.js';
20
20
  import { AgentWorkspace, inferExecutionIntent, resolveDesktopPath } from './workspace/index.js';
21
21
  import { AgentRuntime } from './runtime/index.js';
22
22
  import { loadAuthConfig, isProOrHigher } from '../utils/auth.js';
23
+ import { getMCPManager } from '../mcp/index.js';
23
24
  export { AgentRuntime, AgentWorkspace, inferExecutionIntent, resolveDesktopPath, SystemComputerController, MockComputerController };
24
25
  const execAsync = promisify(exec);
25
26
  export function robustSnippetReplace(origContent, target, replacement) {
@@ -157,7 +158,7 @@ export const AGENT_TOOLS = [
157
158
  },
158
159
  {
159
160
  name: 'run_command',
160
- description: 'Execute a terminal/shell command in the workspace (e.g. npm test, npm run build, git status). WARNING: Do NOT use run_command to create files or folders (e.g. mkdir, touch, echo >)—call write_file directly instead!',
161
+ description: 'Execute a terminal/shell command in the workspace (e.g. npm test, npm run build, git status). WARNING: Do NOT use run_command to create files or folders (e.g. mkdir, touch, echo >)call write_file directly instead!',
161
162
  parameters: {
162
163
  type: 'object',
163
164
  properties: {
@@ -270,7 +271,7 @@ export const AGENT_TOOLS = [
270
271
  },
271
272
  {
272
273
  name: 'ask_user',
273
- description: 'ONLY use for missing API keys or secret credentials. DO NOT use to ask the user what code to write or to re-confirm their prompt—take immediate autonomous tool action instead.',
274
+ description: 'ONLY use for missing API keys or secret credentials. DO NOT use to ask the user what code to write or to re-confirm their prompttake immediate autonomous tool action instead.',
274
275
  parameters: {
275
276
  type: 'object',
276
277
  properties: {
@@ -374,6 +375,7 @@ export class AgentExecutionLoop {
374
375
  cancelReason;
375
376
  executionIntent;
376
377
  userTier;
378
+ currentGoal = '';
377
379
  constructor(options) {
378
380
  this.cwd = process.cwd();
379
381
  this.autoApprove = Boolean(options?.autoApprove);
@@ -582,6 +584,8 @@ PLAN TIER & CAPABILITIES: DEVELOPER FREE (BASIC DEVELOPER OPS ONLY)
582
584
  4. Reading & exploring: use read_file, list_dir, grep_search to inspect files.
583
585
  • Do NOT attempt to use scratchpad, computer, or browser tools. Execute the requested file edit, file creation, or command directly and call task_completed immediately.
584
586
  `;
587
+ const mcpRouting = getMCPManager().routeTools(this.currentGoal);
588
+ const mcpCapabilitiesSection = mcpRouting.capabilitiesSummary ? `\n${mcpRouting.capabilitiesSummary}\n` : '';
585
589
  return `You are Scout Agent, an elite AI Principal Software Engineer & Autonomous Computer-Use Operator (developed by FrontTerrain).
586
590
 
587
591
  Working Directory: "${this.cwd}"
@@ -589,6 +593,7 @@ Host OS Platform: "${process.platform}"
589
593
  Current Date & Time: "${new Date().toISOString()}" (Current Year: ${new Date().getFullYear()})
590
594
  ${tierDirective}
591
595
  ${intentInfo}
596
+ ${mcpCapabilitiesSection}
592
597
  Workspace Overview (first 100 files):
593
598
  ${filesList.join('\n')}
594
599
 
@@ -615,8 +620,10 @@ CORE ARCHITECTURAL PRINCIPLES:
615
620
  6. MULTILINGUAL & INTENT COMPREHENSION: Understand user intent in any language (English, Hinglish like "likho", "bnao", "code karo", "fix karo", "samjha do", Hindi, Spanish, etc.).
616
621
  7. FOR CODE & REPO TASKS: When the task is a coding, project refactoring, or repository maintenance task, use direct code modification tools (write_file, edit_file, run_command, read_file, grep_search).
617
622
  8. RUN & VERIFY COMMANDS: Execute shell commands, tests, builds, and formatters using run_command.
618
- 9. CRITICAL TERMINATION RULE: As soon as you have finished creating/modifying the necessary files, running verification checks, or completing the desktop application workflow, YOU MUST CALL task_completed tool immediately with a clear summary of your work.
623
+ 9. CRITICAL TERMINATION & CONVERSATIONAL VOICE RULE: As soon as you have finished creating/modifying the necessary files, running verification checks, or completing the desktop application workflow, YOU MUST CALL task_completed tool immediately.
624
+ - CONVERSATIONAL CO-PILOT TONE: In your 'summary' parameter for task_completed, speak directly and warmly to the developer like an elite human co-pilot (e.g. "I've launched Chrome for you and it's active on your screen. What should we tackle next?" or "I've created the authentication controller and all unit tests pass with zero regressions!"). Never output dry mechanical robot logs or third-person phrases like "Objective achieved with 1 tool call". Talk directly to the user!
619
625
  10. THOUGHT TRANSPARENCY PROTOCOL: In EVERY step before calling any tools or ending a turn, you MUST provide a clear 1-2 sentence explanation in your text response describing your current reasoning, what file/action you are taking, and why you are taking it.
626
+ 11. APPLICATION LAUNCH & SIMPLE ACTION TERMINATION: When the user's objective is simply to open, launch, close, or focus an application (e.g. "Open Chrome", "Launch Notepad", "Open Calculator"): Immediately after calling computer_open_app or computer_close_app, your goal is 100% complete! You MUST call task_completed on the very next step with your friendly conversational summary. NEVER explore the repository or call list_dir, read_file, grep_search, or edit_file for app launching tasks!
620
627
 
621
628
  When returning tool calls, use standard OpenAI function calling format or JSON tool call payload format:
622
629
  \`\`\`json
@@ -629,6 +636,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
629
636
  }
630
637
  async executeGoal(userGoal) {
631
638
  this.resetContext();
639
+ this.currentGoal = userGoal;
632
640
  this.executionIntent = inferExecutionIntent(userGoal);
633
641
  this.taskStateManager.setObjective(userGoal);
634
642
  if (this.computerController.setExpectedApplication && this.executionIntent.requiredApplications.length > 0) {
@@ -636,7 +644,9 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
636
644
  }
637
645
  const structuredPlan = this.taskPlanner.createPlan(userGoal);
638
646
  this.taskStateManager.setPlan(structuredPlan.steps);
639
- this.taskStateManager.setAvailableTools(this.toolRegistry.getToolsForTier(this.userTier).map((t) => t.name));
647
+ const mcpRouted = getMCPManager().routeTools(userGoal);
648
+ const mcpNames = mcpRouted.tools.map((t) => t.originalName);
649
+ this.taskStateManager.setAvailableTools([...this.toolRegistry.getToolsForTier(this.userTier).map((t) => t.name), ...mcpNames]);
640
650
  if (this.mockMode) {
641
651
  for (let i = 1; i <= structuredPlan.steps.length; i++) {
642
652
  this.taskStateManager.startStep(i);
@@ -657,97 +667,100 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
657
667
  content: recentSessionsMemory,
658
668
  });
659
669
  }
660
- // Robust Target File Discovery: Extract file paths/names directly from userGoal without destroying backslashes
670
+ // Robust Target File Discovery: Only run file discovery if the goal is NOT a simple desktop application launch/action
671
+ const simpleAppCheck = checkSimpleAppAction(userGoal);
661
672
  const filesList = getDirectoryFiles(this.cwd);
662
673
  const matchedFiles = [];
663
- const isIgnoredFile = (filePath) => {
664
- const norm = filePath.replace(/\\/g, '/').toLowerCase();
665
- const ext = path.extname(norm);
666
- const ignoredExts = ['.pyc', '.pyo', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.exe', '.bin', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz'];
667
- return (norm.includes('__pycache__/') ||
668
- norm.includes('.git/') ||
669
- norm.includes('node_modules/') ||
670
- norm.includes('.ft/') ||
671
- ignoredExts.includes(ext));
672
- };
673
- // Match potential paths
674
- const pathMatches = userGoal.match(/(?:[a-zA-Z]:[\\/][^\s"':;,]+|\.?[\\/][^\s"':;,]+\.[a-zA-Z0-9]+|[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)/g) || [];
675
- for (const rawPath of pathMatches) {
676
- const cleanP = rawPath.replace(/^["'()]+|["'()]+$/g, '').trim();
677
- if (!cleanP || cleanP.length < 3)
678
- continue;
679
- const fullP = path.isAbsolute(cleanP) ? cleanP : path.resolve(this.cwd, cleanP);
680
- if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
681
- const relP = path.relative(this.cwd, fullP).replace(/\\/g, '/');
682
- if (!isIgnoredFile(relP) && !matchedFiles.includes(relP))
683
- matchedFiles.push(relP);
674
+ if (!simpleAppCheck.isSimple) {
675
+ const isIgnoredFile = (filePath) => {
676
+ const norm = filePath.replace(/\\/g, '/').toLowerCase();
677
+ const ext = path.extname(norm);
678
+ const ignoredExts = ['.pyc', '.pyo', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.exe', '.bin', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz'];
679
+ return (norm.includes('__pycache__/') ||
680
+ norm.includes('.git/') ||
681
+ norm.includes('node_modules/') ||
682
+ norm.includes('.ft/') ||
683
+ ignoredExts.includes(ext));
684
+ };
685
+ // Match potential paths
686
+ const pathMatches = userGoal.match(/(?:[a-zA-Z]:[\\/][^\s"':;,]+|\.?[\\/][^\s"':;,]+\.[a-zA-Z0-9]+|[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)/g) || [];
687
+ for (const rawPath of pathMatches) {
688
+ const cleanP = rawPath.replace(/^["'()]+|["'()]+$/g, '').trim();
689
+ if (!cleanP || cleanP.length < 3)
690
+ continue;
691
+ const fullP = path.isAbsolute(cleanP) ? cleanP : path.resolve(this.cwd, cleanP);
692
+ if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
693
+ const relP = path.relative(this.cwd, fullP).replace(/\\/g, '/');
694
+ if (!isIgnoredFile(relP) && !matchedFiles.includes(relP))
695
+ matchedFiles.push(relP);
696
+ }
697
+ else {
698
+ // Check if any file in filesList matches or ends with the extracted relative path or filename
699
+ const normToken = cleanP.replace(/\\/g, '/').toLowerCase();
700
+ const baseName = path.basename(normToken);
701
+ for (const f of filesList) {
702
+ if (isIgnoredFile(f))
703
+ continue;
704
+ const normF = f.replace(/\\/g, '/').toLowerCase();
705
+ if (normF === normToken || normF.endsWith('/' + normToken) || (baseName.includes('.') && normF.endsWith('/' + baseName))) {
706
+ if (!matchedFiles.includes(f))
707
+ matchedFiles.push(f);
708
+ }
709
+ }
710
+ }
684
711
  }
685
- else {
686
- // Check if any file in filesList matches or ends with the extracted relative path or filename
687
- const normToken = cleanP.replace(/\\/g, '/').toLowerCase();
688
- const baseName = path.basename(normToken);
712
+ // Keyword fallback if no direct path matched
713
+ if (matchedFiles.length === 0) {
714
+ const stopWords = new Set([
715
+ 'the', 'a', 'an', 'in', 'is', 'for', 'to', 'of', 'and', 'or', 'on', 'at', 'by', 'from',
716
+ 'with', 'this', 'that', 'it', 'code', 'file', 'files', 'fix', 'add', 'make', 'create',
717
+ 'update', 'change', 'modify', 'edit', 'write', 'implement', 'bro', 'please', 'scout',
718
+ 'agent', 'bug', 'error', 'issue', 'help', 'app', 'project', 'script', 'function',
719
+ 'folder', 'dir', 'directory', 'testing', 'inside', 'open', 'vs', 'vscode', 'py', 'python',
720
+ 'new', 'build', 'run', 'test', 'suite'
721
+ ]);
722
+ const words = userGoal.split(/[\s,;:\/\\]+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
723
+ const keywords = words.filter((w) => w.length >= 3 && !stopWords.has(w.toLowerCase()));
689
724
  for (const f of filesList) {
690
725
  if (isIgnoredFile(f))
691
726
  continue;
727
+ if (matchedFiles.length >= 3)
728
+ break;
692
729
  const normF = f.replace(/\\/g, '/').toLowerCase();
693
- if (normF === normToken || normF.endsWith('/' + normToken) || (baseName.includes('.') && normF.endsWith('/' + baseName))) {
694
- if (!matchedFiles.includes(f))
695
- matchedFiles.push(f);
730
+ const baseName = path.basename(normF, path.extname(normF));
731
+ for (const kw of keywords) {
732
+ const kwLower = kw.toLowerCase();
733
+ if (baseName === kwLower || normF.endsWith('/' + kwLower + path.extname(normF))) {
734
+ if (!matchedFiles.includes(f))
735
+ matchedFiles.push(f);
736
+ break;
737
+ }
696
738
  }
697
739
  }
698
740
  }
699
- }
700
- // Keyword fallback if no direct path matched
701
- if (matchedFiles.length === 0) {
702
- const stopWords = new Set([
703
- 'the', 'a', 'an', 'in', 'is', 'for', 'to', 'of', 'and', 'or', 'on', 'at', 'by', 'from',
704
- 'with', 'this', 'that', 'it', 'code', 'file', 'files', 'fix', 'add', 'make', 'create',
705
- 'update', 'change', 'modify', 'edit', 'write', 'implement', 'bro', 'please', 'scout',
706
- 'agent', 'bug', 'error', 'issue', 'help', 'app', 'project', 'script', 'function',
707
- 'folder', 'dir', 'directory', 'testing', 'inside', 'open', 'vs', 'vscode', 'py', 'python',
708
- 'new', 'build', 'run', 'test', 'suite'
709
- ]);
710
- const words = userGoal.split(/[\s,;:\/\\]+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
711
- const keywords = words.filter((w) => w.length >= 3 && !stopWords.has(w.toLowerCase()));
712
- for (const f of filesList) {
713
- if (isIgnoredFile(f))
714
- continue;
715
- if (matchedFiles.length >= 3)
716
- break;
717
- const normF = f.replace(/\\/g, '/').toLowerCase();
718
- const baseName = path.basename(normF, path.extname(normF));
719
- for (const kw of keywords) {
720
- const kwLower = kw.toLowerCase();
721
- if (baseName === kwLower || normF.endsWith('/' + kwLower + path.extname(normF))) {
722
- if (!matchedFiles.includes(f))
723
- matchedFiles.push(f);
724
- break;
741
+ // Safely inject high-confidence existing file previews without prompt-hijacking
742
+ for (const file of matchedFiles.slice(0, 3)) {
743
+ const fullP = path.resolve(this.cwd, file);
744
+ if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
745
+ const analyzeSpin = spinner();
746
+ analyzeSpin.start(chalk.cyan(` Auto-discovered target file: ${file}. Analyzing...`));
747
+ try {
748
+ const content = fs.readFileSync(fullP, 'utf-8');
749
+ const lines = content.split('\n');
750
+ analyzeSpin.stop(chalk.green(` Auto-discovered & analyzed ${file} (${lines.length} lines)`));
751
+ const previewSnippet = lines.length <= 250
752
+ ? lines.map((l, i) => `${i + 1} | ${l}`).join('\n')
753
+ : lines.slice(0, 100).map((l, i) => `${i + 1} | ${l}`).join('\n') + `\n... [truncated, file has ${lines.length} lines. Call read_file to view remaining lines]`;
754
+ this.readFilesHistory.add(file);
755
+ this.readFilesHistory.add(file.replace(/\\/g, '/').toLowerCase());
756
+ this.historyMessages.push({
757
+ role: 'user',
758
+ content: `[Reference Workspace Context - Pre-loaded target file "${file}" (${lines.length} lines)]:\n\`\`\`\n${previewSnippet}\n\`\`\`\nCRITICAL DIRECTIVE: The target file "${file}" is ALREADY fully pre-loaded into your reference context above. DO NOT call read_file or list_dir for this file again! Immediately begin calling write_file, edit_file, or run_command to satisfy the user goal.`,
759
+ });
760
+ }
761
+ catch {
762
+ analyzeSpin.stop(chalk.yellow(`Could not read file ${file}`));
725
763
  }
726
- }
727
- }
728
- }
729
- // Safely inject high-confidence existing file previews without prompt-hijacking
730
- for (const file of matchedFiles.slice(0, 3)) {
731
- const fullP = path.resolve(this.cwd, file);
732
- if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
733
- const analyzeSpin = spinner();
734
- analyzeSpin.start(chalk.cyan(` Auto-discovered target file: ${file}. Analyzing...`));
735
- try {
736
- const content = fs.readFileSync(fullP, 'utf-8');
737
- const lines = content.split('\n');
738
- analyzeSpin.stop(chalk.green(` Auto-discovered & analyzed ${file} (${lines.length} lines)`));
739
- const previewSnippet = lines.length <= 250
740
- ? lines.map((l, i) => `${i + 1} | ${l}`).join('\n')
741
- : lines.slice(0, 100).map((l, i) => `${i + 1} | ${l}`).join('\n') + `\n... [truncated, file has ${lines.length} lines. Call read_file to view remaining lines]`;
742
- this.readFilesHistory.add(file);
743
- this.readFilesHistory.add(file.replace(/\\/g, '/').toLowerCase());
744
- this.historyMessages.push({
745
- role: 'user',
746
- content: `[Reference Workspace Context - Pre-loaded target file "${file}" (${lines.length} lines)]:\n\`\`\`\n${previewSnippet}\n\`\`\`\nCRITICAL DIRECTIVE: The target file "${file}" is ALREADY fully pre-loaded into your reference context above. DO NOT call read_file or list_dir for this file again! Immediately begin calling write_file, edit_file, or run_command to satisfy the user goal.`,
747
- });
748
- }
749
- catch {
750
- analyzeSpin.stop(chalk.yellow(`Could not read file ${file}`));
751
764
  }
752
765
  }
753
766
  }
@@ -786,12 +799,26 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
786
799
  stepSpinner.start(chalk.cyan(`Scout is Working (Step ${step}/${this.maxSteps} • Est. completion: ~${estLeftStr} left)`));
787
800
  try {
788
801
  let response;
802
+ const mcpRouting = getMCPManager().routeTools(userGoal);
803
+ const mcpToolDefs = mcpRouting.tools.map((mt) => ({
804
+ name: mt.originalName,
805
+ description: `[MCP: ${mt.serverName}] ${mt.description}`,
806
+ parameters: {
807
+ type: 'object',
808
+ properties: mt.inputSchema.properties || {},
809
+ required: mt.inputSchema.required || [],
810
+ },
811
+ source: 'mcp',
812
+ mcpServerId: mt.serverId,
813
+ mcpServerName: mt.serverName,
814
+ }));
815
+ const activeLoopTools = [...this.toolRegistry.getToolsForTier(this.userTier), ...mcpToolDefs];
789
816
  try {
790
817
  response = await callOpenAIWithRetry(async (model) => {
791
818
  return await openai.chat.completions.create({
792
819
  model,
793
820
  messages: sanitizeMessages(this.historyMessages),
794
- tools: this.toolRegistry.getToolsForTier(this.userTier).map((t) => ({ type: 'function', function: t })),
821
+ tools: activeLoopTools.map((t) => ({ type: 'function', function: t })),
795
822
  tool_choice: 'auto',
796
823
  temperature: 0.1,
797
824
  presence_penalty: 0.1,
@@ -802,7 +829,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
802
829
  catch (err) {
803
830
  const errStr = String(err?.message || err?.error || err || '').toLowerCase();
804
831
  if (errStr.includes('tool') || errStr.includes('400') || errStr.includes('not supported') || errStr.includes('reasoning')) {
805
- // Model doesn't support native function calling — inject tool-call formatting hint
832
+ // Model doesn't support native function calling inject tool-call formatting hint
806
833
  // so extractJsonToolCall can parse the response as a structured tool invocation
807
834
  const toolHintMsg = {
808
835
  role: 'user',
@@ -811,7 +838,7 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
811
838
  { "tool": "tool_name", "args": { ... } }
812
839
  \`\`\`
813
840
  Available tools: read_file, write_file, edit_file, run_command, list_dir, grep_search, glob_search, tree_view, file_info, multi_edit_file, fetch_url, git_diff, ask_user, speak_text, task_completed.
814
- You MUST output exactly ONE JSON code block per tool call. Do NOT describe what you would do in plain text—output the JSON tool call directly!`,
841
+ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what you would do in plain textoutput the JSON tool call directly!`,
815
842
  };
816
843
  const fallbackMessages = [...this.historyMessages, toolHintMsg];
817
844
  response = await callOpenAIWithRetry(async (model) => {
@@ -858,6 +885,29 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
858
885
  content: `Tool Execution Result (${fnName}):\n${toolResult.result}`,
859
886
  });
860
887
  this.handleToolConsecutiveTracking(fnName, args, step);
888
+ // Automatic immediate completion for simple app actions (open/close/switch window)
889
+ if (simpleAppCheck.isSimple && (fnName === 'computer_open_app' || fnName === 'computer_close_app' || fnName === 'computer_switch_window')) {
890
+ let callSucceeded = false;
891
+ try {
892
+ const parsed = JSON.parse(toolResult.result);
893
+ callSucceeded = parsed.success !== false;
894
+ }
895
+ catch {
896
+ callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
897
+ }
898
+ if (callSucceeded) {
899
+ const appName = simpleAppCheck.appName || 'application';
900
+ const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
901
+ finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
902
+ safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
903
+ saveAgentSession({
904
+ goal: userGoal,
905
+ summary: finalSummary,
906
+ modifiedFiles: [],
907
+ });
908
+ return { success: true, summary: finalSummary };
909
+ }
910
+ }
861
911
  if (fnName === 'task_completed') {
862
912
  finalSummary = args.summary || toolResult.result;
863
913
  if (this.modifiedFiles.size > 0) {
@@ -891,6 +941,29 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
891
941
  content: `Tool Execution Result (${parsedJsonTool.tool}):\n${toolResult.result}`,
892
942
  });
893
943
  this.handleToolConsecutiveTracking(parsedJsonTool.tool, parsedJsonTool.args, step);
944
+ // Automatic immediate completion for simple app actions (open/close/switch window)
945
+ if (simpleAppCheck.isSimple && (parsedJsonTool.tool === 'computer_open_app' || parsedJsonTool.tool === 'computer_close_app' || parsedJsonTool.tool === 'computer_switch_window')) {
946
+ let callSucceeded = false;
947
+ try {
948
+ const parsed = JSON.parse(toolResult.result);
949
+ callSucceeded = parsed.success !== false;
950
+ }
951
+ catch {
952
+ callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
953
+ }
954
+ if (callSucceeded) {
955
+ const appName = simpleAppCheck.appName || 'application';
956
+ const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
957
+ finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
958
+ safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
959
+ saveAgentSession({
960
+ goal: userGoal,
961
+ summary: finalSummary,
962
+ modifiedFiles: [],
963
+ });
964
+ return { success: true, summary: finalSummary };
965
+ }
966
+ }
894
967
  if (parsedJsonTool.tool === 'task_completed') {
895
968
  finalSummary = parsedJsonTool.args?.summary || toolResult.result;
896
969
  saveAgentSession({
@@ -1178,6 +1251,22 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1178
1251
  }
1179
1252
  }
1180
1253
  // Validate against execution intent (USER INTENT > TOOL CONVENIENCE)
1254
+ const simpleCheckInDispatch = checkSimpleAppAction(this.taskStateManager.getState()?.objective || '');
1255
+ if (simpleCheckInDispatch.isSimple) {
1256
+ if (['list_dir', 'read_file', 'grep_search', 'glob_search', 'file_info', 'tree_view', 'write_file', 'edit_file', 'multi_edit_file'].includes(name)) {
1257
+ const blockedExplorationMsg = `[UNNECESSARY WORKSPACE EXPLORATION BLOCKED]: The user only requested to ${simpleCheckInDispatch.action || 'launch'} ${simpleCheckInDispatch.appName || 'the application'}. Reading repository code or listing files (${name}) is forbidden for this task. You must call task_completed immediately.`;
1258
+ this.taskStateManager.addObservation(name, blockedExplorationMsg, false);
1259
+ return {
1260
+ result: JSON.stringify({
1261
+ success: false,
1262
+ action: name,
1263
+ error: 'UnnecessaryWorkspaceExplorationBlocked',
1264
+ message: blockedExplorationMsg,
1265
+ directive: "Call 'task_completed' NOW with your completion summary."
1266
+ }, null, 2)
1267
+ };
1268
+ }
1269
+ }
1181
1270
  if (this.executionIntent && !this.executionIntent.allowAlternativeTools) {
1182
1271
  if (this.executionIntent.requiredInteraction === 'computer') {
1183
1272
  if (['write_file', 'edit_file', 'multi_edit_file'].includes(name)) {
@@ -1356,13 +1445,13 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1356
1445
  checkSpin.start(chalk.cyan(` Checking for error: edited ${targetPath}...`));
1357
1446
  const syntaxCheck = await checkFileSyntax(targetPath, this.cwd);
1358
1447
  if (!syntaxCheck.valid) {
1359
- checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath} — Compiler/Syntax Error!`));
1448
+ checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath} Compiler/Syntax Error!`));
1360
1449
  this.taskStateManager.recordError(`Syntax error in "${targetPath}": ${syntaxCheck.errors.join('; ')}`, 'write_file');
1361
1450
  return {
1362
1451
  result: `Successfully wrote ${content.length} characters (${numberOfLinesChanged} lines changed) to "${targetPath}".\n\n️ AUTOMATED SYNTAX / COMPILER ERROR DETECTED IN "${targetPath}":\n${syntaxCheck.errors.join('\n')}\n\nPLEASE IMMEDIATELY FIX THIS SYNTAX ERROR IN YOUR NEXT TOOL CALL.`
1363
1452
  };
1364
1453
  }
1365
- checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath} — Syntax & build clean.`));
1454
+ checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath} Syntax & build clean.`));
1366
1455
  this.taskStateManager.recordArtifact({
1367
1456
  filename: path.basename(targetPath),
1368
1457
  path: targetPath,
@@ -1422,12 +1511,12 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1422
1511
  checkSpin.start(chalk.cyan(` Checking for error: edited ${targetPath}...`));
1423
1512
  const syntaxCheck = await checkFileSyntax(targetPath, this.cwd);
1424
1513
  if (!syntaxCheck.valid) {
1425
- checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath} — Compiler/Syntax Error!`));
1514
+ checkSpin.stop(chalk.red(`️ Checking for error: edited ${targetPath} Compiler/Syntax Error!`));
1426
1515
  return {
1427
1516
  result: `Successfully replaced snippet (${numberOfLinesChanged} lines changed) in "${targetPath}".\n\n️ AUTOMATED SYNTAX / COMPILER ERROR DETECTED IN "${targetPath}":\n${syntaxCheck.errors.join('\n')}\n\nPLEASE IMMEDIATELY FIX THIS SYNTAX ERROR IN YOUR NEXT TOOL CALL.`
1428
1517
  };
1429
1518
  }
1430
- checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath} — Syntax & build clean.`));
1519
+ checkSpin.stop(chalk.green(` Checking for error: edited ${targetPath} Syntax & build clean.`));
1431
1520
  return { result: `Successfully replaced target snippet (${numberOfLinesChanged} lines changed) in "${targetPath}". Rationale: ${reason}. Syntax check passed cleanly.` };
1432
1521
  }
1433
1522
  case 'run_command': {
@@ -1983,6 +2072,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1983
2072
  const res = await this.computerController.openApplication(appName, args.args);
1984
2073
  this.taskStateManager.setActiveApplication(appName);
1985
2074
  this.taskStateManager.addObservation('computer_open_app', res.output, res.success);
2075
+ const currentObj = this.taskStateManager.getState()?.objective || '';
2076
+ const simpleCheck = checkSimpleAppAction(currentObj);
2077
+ const isOnlyOpening = simpleCheck.isSimple && simpleCheck.action !== 'close';
1986
2078
  const structuredRes = {
1987
2079
  success: res.success,
1988
2080
  action: 'open_app',
@@ -1991,6 +2083,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1991
2083
  recoverable: res.recoverable ?? true,
1992
2084
  activeWindow: res.activeWindow || appName,
1993
2085
  expectedWindow: res.expectedWindow || appName,
2086
+ ...(isOnlyOpening && res.success ? {
2087
+ directive: "CRITICAL: The requested application is now opened. Your objective is 100% satisfied. You MUST call 'task_completed' immediately on your next turn. DO NOT call list_dir, read_file, or explore the repository."
2088
+ } : {}),
1994
2089
  };
1995
2090
  return { result: JSON.stringify(structuredRes, null, 2) };
1996
2091
  }
@@ -1998,6 +2093,12 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1998
2093
  const appName = String(args.appName || args.app || '');
1999
2094
  const res = await this.computerController.closeApplication(appName);
2000
2095
  this.taskStateManager.addObservation('computer_close_app', res.output, res.success);
2096
+ const currentObj = this.taskStateManager.getState()?.objective || '';
2097
+ const simpleCheck = checkSimpleAppAction(currentObj);
2098
+ const isOnlyClosing = simpleCheck.isSimple && simpleCheck.action === 'close';
2099
+ if (isOnlyClosing && res.success) {
2100
+ return { result: `${res.output}\nCRITICAL: Application closed. Objective complete. Call task_completed now.` };
2101
+ }
2001
2102
  return { result: res.output };
2002
2103
  }
2003
2104
  case 'computer_switch_window': {
@@ -2053,8 +2154,28 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
2053
2154
  this.taskStateManager.completeStep(this.currentStep, summaryStr);
2054
2155
  return { result: summaryStr };
2055
2156
  }
2056
- default:
2157
+ default: {
2158
+ // Check if this tool is provided by a connected MCP server
2159
+ const mcpManager = getMCPManager();
2160
+ const mcpTarget = mcpManager.findTool(name);
2161
+ if (mcpTarget) {
2162
+ const mcpRes = await mcpManager.executeTool(name, args);
2163
+ const isSuccess = mcpRes.status === 'success';
2164
+ const outputStr = typeof mcpRes.result === 'string'
2165
+ ? mcpRes.result
2166
+ : (mcpRes.result?.text ? String(mcpRes.result.text) : JSON.stringify(mcpRes.result ?? { status: mcpRes.status, error: mcpRes.error }));
2167
+ this.taskStateManager.addObservation(name, mcpRes.outputSummary || outputStr, isSuccess);
2168
+ // Check if MCP result generated or referenced local files/artifacts
2169
+ if (isSuccess && typeof mcpRes.result === 'object' && mcpRes.result !== null) {
2170
+ const possiblePath = mcpRes.result.path || mcpRes.result.filePath || mcpRes.result.file || args.destPath || args.path || args.outputPath;
2171
+ if (typeof possiblePath === 'string' && fs.existsSync(path.resolve(this.cwd, possiblePath))) {
2172
+ this.modifiedFiles.add(path.relative(this.cwd, path.resolve(this.cwd, possiblePath)).replace(/\\/g, '/'));
2173
+ }
2174
+ }
2175
+ return { result: outputStr };
2176
+ }
2057
2177
  return { result: `Unknown tool call name: "${name}"` };
2178
+ }
2058
2179
  }
2059
2180
  }
2060
2181
  }