ft-scout 9.0.5 → 9.0.7

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 (50) hide show
  1. package/.firebase/hosting.d2Vi.cache +9 -8
  2. package/AUTONOMOUS_FEATURES.md +192 -0
  3. package/README.md +82 -40
  4. package/bin/src/commands/agent.d.ts.map +1 -1
  5. package/bin/src/commands/agent.js +43 -2
  6. package/bin/src/commands/agent.js.map +1 -1
  7. package/bin/src/commands/login.d.ts.map +1 -1
  8. package/bin/src/commands/login.js +3 -1
  9. package/bin/src/commands/login.js.map +1 -1
  10. package/bin/src/engine/actionVerifier.d.ts.map +1 -1
  11. package/bin/src/engine/actionVerifier.js +9 -1
  12. package/bin/src/engine/actionVerifier.js.map +1 -1
  13. package/bin/src/engine/agentEngine.d.ts.map +1 -1
  14. package/bin/src/engine/agentEngine.js +230 -85
  15. package/bin/src/engine/agentEngine.js.map +1 -1
  16. package/bin/src/engine/browser/browserController.d.ts.map +1 -1
  17. package/bin/src/engine/browser/browserController.js +53 -13
  18. package/bin/src/engine/browser/browserController.js.map +1 -1
  19. package/bin/src/engine/computer/computerController.d.ts.map +1 -1
  20. package/bin/src/engine/computer/computerController.js +43 -11
  21. package/bin/src/engine/computer/computerController.js.map +1 -1
  22. package/bin/src/engine/computer/scoutDriver.cs +325 -37
  23. package/bin/src/engine/computer/scout_driver.exe +0 -0
  24. package/bin/src/engine/planner.d.ts.map +1 -1
  25. package/bin/src/engine/planner.js +81 -2
  26. package/bin/src/engine/planner.js.map +1 -1
  27. package/bin/src/engine/runtime/agentRuntime.d.ts.map +1 -1
  28. package/bin/src/engine/runtime/agentRuntime.js +66 -5
  29. package/bin/src/engine/runtime/agentRuntime.js.map +1 -1
  30. package/bin/src/engine/safety.d.ts.map +1 -1
  31. package/bin/src/engine/safety.js +4 -3
  32. package/bin/src/engine/safety.js.map +1 -1
  33. package/bin/src/engine/scoutAudioRecorder.cs +215 -0
  34. package/bin/src/engine/scout_audio_recorder.exe +0 -0
  35. package/bin/src/engine/toolRegistry.d.ts.map +1 -1
  36. package/bin/src/engine/toolRegistry.js +65 -0
  37. package/bin/src/engine/toolRegistry.js.map +1 -1
  38. package/bin/src/engine/voiceEngine.d.ts.map +1 -1
  39. package/bin/src/engine/voiceEngine.js +333 -13
  40. package/bin/src/engine/voiceEngine.js.map +1 -1
  41. package/bin/src/index.js +5 -4
  42. package/bin/src/index.js.map +1 -1
  43. package/bin/src/utils/branding.js +1 -1
  44. package/bin/src/utils/holoseneAuth.d.ts.map +1 -1
  45. package/bin/src/utils/holoseneAuth.js +169 -41
  46. package/bin/src/utils/holoseneAuth.js.map +1 -1
  47. package/firebase.json +8 -0
  48. package/package.json +1 -1
  49. package/web/app.js +3 -3
  50. package/web/styles.css +2 -2
@@ -14,7 +14,7 @@ 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';
@@ -615,8 +615,10 @@ CORE ARCHITECTURAL PRINCIPLES:
615
615
  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
616
  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
617
  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.
618
+ 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.
619
+ - 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
620
  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.
621
+ 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
622
 
621
623
  When returning tool calls, use standard OpenAI function calling format or JSON tool call payload format:
622
624
  \`\`\`json
@@ -657,97 +659,100 @@ When returning tool calls, use standard OpenAI function calling format or JSON t
657
659
  content: recentSessionsMemory,
658
660
  });
659
661
  }
660
- // Robust Target File Discovery: Extract file paths/names directly from userGoal without destroying backslashes
662
+ // Robust Target File Discovery: Only run file discovery if the goal is NOT a simple desktop application launch/action
663
+ const simpleAppCheck = checkSimpleAppAction(userGoal);
661
664
  const filesList = getDirectoryFiles(this.cwd);
662
665
  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);
666
+ if (!simpleAppCheck.isSimple) {
667
+ const isIgnoredFile = (filePath) => {
668
+ const norm = filePath.replace(/\\/g, '/').toLowerCase();
669
+ const ext = path.extname(norm);
670
+ const ignoredExts = ['.pyc', '.pyo', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.exe', '.bin', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz'];
671
+ return (norm.includes('__pycache__/') ||
672
+ norm.includes('.git/') ||
673
+ norm.includes('node_modules/') ||
674
+ norm.includes('.ft/') ||
675
+ ignoredExts.includes(ext));
676
+ };
677
+ // Match potential paths
678
+ const pathMatches = userGoal.match(/(?:[a-zA-Z]:[\\/][^\s"':;,]+|\.?[\\/][^\s"':;,]+\.[a-zA-Z0-9]+|[a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)/g) || [];
679
+ for (const rawPath of pathMatches) {
680
+ const cleanP = rawPath.replace(/^["'()]+|["'()]+$/g, '').trim();
681
+ if (!cleanP || cleanP.length < 3)
682
+ continue;
683
+ const fullP = path.isAbsolute(cleanP) ? cleanP : path.resolve(this.cwd, cleanP);
684
+ if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
685
+ const relP = path.relative(this.cwd, fullP).replace(/\\/g, '/');
686
+ if (!isIgnoredFile(relP) && !matchedFiles.includes(relP))
687
+ matchedFiles.push(relP);
688
+ }
689
+ else {
690
+ // Check if any file in filesList matches or ends with the extracted relative path or filename
691
+ const normToken = cleanP.replace(/\\/g, '/').toLowerCase();
692
+ const baseName = path.basename(normToken);
693
+ for (const f of filesList) {
694
+ if (isIgnoredFile(f))
695
+ continue;
696
+ const normF = f.replace(/\\/g, '/').toLowerCase();
697
+ if (normF === normToken || normF.endsWith('/' + normToken) || (baseName.includes('.') && normF.endsWith('/' + baseName))) {
698
+ if (!matchedFiles.includes(f))
699
+ matchedFiles.push(f);
700
+ }
701
+ }
702
+ }
684
703
  }
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);
704
+ // Keyword fallback if no direct path matched
705
+ if (matchedFiles.length === 0) {
706
+ const stopWords = new Set([
707
+ 'the', 'a', 'an', 'in', 'is', 'for', 'to', 'of', 'and', 'or', 'on', 'at', 'by', 'from',
708
+ 'with', 'this', 'that', 'it', 'code', 'file', 'files', 'fix', 'add', 'make', 'create',
709
+ 'update', 'change', 'modify', 'edit', 'write', 'implement', 'bro', 'please', 'scout',
710
+ 'agent', 'bug', 'error', 'issue', 'help', 'app', 'project', 'script', 'function',
711
+ 'folder', 'dir', 'directory', 'testing', 'inside', 'open', 'vs', 'vscode', 'py', 'python',
712
+ 'new', 'build', 'run', 'test', 'suite'
713
+ ]);
714
+ const words = userGoal.split(/[\s,;:\/\\]+/).map((w) => w.replace(/["'(),;:!]/g, '').trim());
715
+ const keywords = words.filter((w) => w.length >= 3 && !stopWords.has(w.toLowerCase()));
689
716
  for (const f of filesList) {
690
717
  if (isIgnoredFile(f))
691
718
  continue;
719
+ if (matchedFiles.length >= 3)
720
+ break;
692
721
  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);
722
+ const baseName = path.basename(normF, path.extname(normF));
723
+ for (const kw of keywords) {
724
+ const kwLower = kw.toLowerCase();
725
+ if (baseName === kwLower || normF.endsWith('/' + kwLower + path.extname(normF))) {
726
+ if (!matchedFiles.includes(f))
727
+ matchedFiles.push(f);
728
+ break;
729
+ }
696
730
  }
697
731
  }
698
732
  }
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;
733
+ // Safely inject high-confidence existing file previews without prompt-hijacking
734
+ for (const file of matchedFiles.slice(0, 3)) {
735
+ const fullP = path.resolve(this.cwd, file);
736
+ if (fs.existsSync(fullP) && fs.statSync(fullP).isFile()) {
737
+ const analyzeSpin = spinner();
738
+ analyzeSpin.start(chalk.cyan(` Auto-discovered target file: ${file}. Analyzing...`));
739
+ try {
740
+ const content = fs.readFileSync(fullP, 'utf-8');
741
+ const lines = content.split('\n');
742
+ analyzeSpin.stop(chalk.green(` Auto-discovered & analyzed ${file} (${lines.length} lines)`));
743
+ const previewSnippet = lines.length <= 250
744
+ ? lines.map((l, i) => `${i + 1} | ${l}`).join('\n')
745
+ : 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]`;
746
+ this.readFilesHistory.add(file);
747
+ this.readFilesHistory.add(file.replace(/\\/g, '/').toLowerCase());
748
+ this.historyMessages.push({
749
+ role: 'user',
750
+ 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.`,
751
+ });
752
+ }
753
+ catch {
754
+ analyzeSpin.stop(chalk.yellow(`Could not read file ${file}`));
725
755
  }
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
756
  }
752
757
  }
753
758
  }
@@ -858,6 +863,29 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
858
863
  content: `Tool Execution Result (${fnName}):\n${toolResult.result}`,
859
864
  });
860
865
  this.handleToolConsecutiveTracking(fnName, args, step);
866
+ // Automatic immediate completion for simple app actions (open/close/switch window)
867
+ if (simpleAppCheck.isSimple && (fnName === 'computer_open_app' || fnName === 'computer_close_app' || fnName === 'computer_switch_window')) {
868
+ let callSucceeded = false;
869
+ try {
870
+ const parsed = JSON.parse(toolResult.result);
871
+ callSucceeded = parsed.success !== false;
872
+ }
873
+ catch {
874
+ callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
875
+ }
876
+ if (callSucceeded) {
877
+ const appName = simpleAppCheck.appName || 'application';
878
+ const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
879
+ finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
880
+ safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
881
+ saveAgentSession({
882
+ goal: userGoal,
883
+ summary: finalSummary,
884
+ modifiedFiles: [],
885
+ });
886
+ return { success: true, summary: finalSummary };
887
+ }
888
+ }
861
889
  if (fnName === 'task_completed') {
862
890
  finalSummary = args.summary || toolResult.result;
863
891
  if (this.modifiedFiles.size > 0) {
@@ -891,6 +919,29 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
891
919
  content: `Tool Execution Result (${parsedJsonTool.tool}):\n${toolResult.result}`,
892
920
  });
893
921
  this.handleToolConsecutiveTracking(parsedJsonTool.tool, parsedJsonTool.args, step);
922
+ // Automatic immediate completion for simple app actions (open/close/switch window)
923
+ if (simpleAppCheck.isSimple && (parsedJsonTool.tool === 'computer_open_app' || parsedJsonTool.tool === 'computer_close_app' || parsedJsonTool.tool === 'computer_switch_window')) {
924
+ let callSucceeded = false;
925
+ try {
926
+ const parsed = JSON.parse(toolResult.result);
927
+ callSucceeded = parsed.success !== false;
928
+ }
929
+ catch {
930
+ callSucceeded = !toolResult.result.toLowerCase().includes('failed') && !toolResult.result.toLowerCase().includes('error');
931
+ }
932
+ if (callSucceeded) {
933
+ const appName = simpleAppCheck.appName || 'application';
934
+ const actionVerb = simpleAppCheck.action === 'close' ? 'closed' : (simpleAppCheck.action === 'switch' ? 'focused' : 'launched');
935
+ finalSummary = `I've ${actionVerb} ${appName} for you. It's ready on your screen!`;
936
+ safeNote(chalk.green(` ${finalSummary}`), 'Task Completed');
937
+ saveAgentSession({
938
+ goal: userGoal,
939
+ summary: finalSummary,
940
+ modifiedFiles: [],
941
+ });
942
+ return { success: true, summary: finalSummary };
943
+ }
944
+ }
894
945
  if (parsedJsonTool.tool === 'task_completed') {
895
946
  finalSummary = parsedJsonTool.args?.summary || toolResult.result;
896
947
  saveAgentSession({
@@ -970,7 +1021,7 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
970
1021
  safeNote(chalk.bold.yellow(`⚡ Step limit reached while task is in progress. Automatically extending execution by +15 extra steps (New Max Limit: ${this.maxSteps})...`), ' Auto Extra Steps Extension');
971
1022
  }
972
1023
  else if (!isPaid) {
973
- safeNote(chalk.bold.yellow(`⚡ Free Tier limit reached (${this.maxSteps} steps).\nFree developers can perform basic code editing, creating files/folders, and running commands.\nTo unlock unlimited autonomous multi-step loops and scratchpad reasoning, upgrade to Pro at https://tryscout.web.app/plans or run 'scout dashboard'.`), ' Free Tier Step Limit');
1024
+ safeNote(chalk.bold.yellow(`⚡ Free Tier limit reached (${this.maxSteps} steps).\nFree developers can perform basic code editing, creating files/folders, and running commands.\nTo unlock unlimited autonomous multi-step loops and scratchpad reasoning, upgrade to Pro at https://scout.frontterrain.com/plans or run 'scout dashboard'.`), ' Free Tier Step Limit');
974
1025
  break;
975
1026
  }
976
1027
  }
@@ -1112,7 +1163,7 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1112
1163
  // 1. Scratchpad Feature Gating (Pro & Enterprise only)
1113
1164
  if (name === 'scratchpad') {
1114
1165
  if (!isPaid) {
1115
- const blockedMsg = `[PLAN RESTRICTION: PRO / ENTERPRISE ONLY] The Scratchpad reasoning memory is exclusive to Pro and Enterprise tiers. Free tier developers can perform basic code editing (edit_file), creating files and folders (write_file), and running commands (run_command). To unlock the AI scratchpad, upgrade at https://tryscout.web.app/plans or run 'scout dashboard'.`;
1166
+ const blockedMsg = `[PLAN RESTRICTION: PRO / ENTERPRISE ONLY] The Scratchpad reasoning memory is exclusive to Pro and Enterprise tiers. Free tier developers can perform basic code editing (edit_file), creating files and folders (write_file), and running commands (run_command). To unlock the AI scratchpad, upgrade at https://scout.frontterrain.com/plans or run 'scout dashboard'.`;
1116
1167
  console.log(chalk.red.bold(`\n⛔ ${blockedMsg}`));
1117
1168
  this.taskStateManager.addObservation(name, blockedMsg, false);
1118
1169
  return { result: blockedMsg };
@@ -1153,7 +1204,7 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1153
1204
  // 2. Autonomous Computer & Browser Automation Gating (Pro & Enterprise only)
1154
1205
  if (name === 'computer' || name.startsWith('computer_') || name.startsWith('browser_')) {
1155
1206
  if (!isPaid) {
1156
- const blockedMsg = `[PLAN RESTRICTION: PRO / ENTERPRISE ONLY] Autonomous Computer-Use & Browser Automation features are exclusive to Pro and Enterprise tiers. Free tier developers can perform basic code editing (edit_file), creating files and folders (write_file), and running commands (run_command) in the workspace. Upgrade to Pro at https://tryscout.web.app/plans or run 'scout dashboard'.`;
1207
+ const blockedMsg = `[PLAN RESTRICTION: PRO / ENTERPRISE ONLY] Autonomous Computer-Use & Browser Automation features are exclusive to Pro and Enterprise tiers. Free tier developers can perform basic code editing (edit_file), creating files and folders (write_file), and running commands (run_command) in the workspace. Upgrade to Pro at https://scout.frontterrain.com/plans or run 'scout dashboard'.`;
1157
1208
  console.log(chalk.red.bold(`\n⛔ ${blockedMsg}`));
1158
1209
  this.taskStateManager.addObservation(name, blockedMsg, false);
1159
1210
  return { result: blockedMsg };
@@ -1178,6 +1229,22 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1178
1229
  }
1179
1230
  }
1180
1231
  // Validate against execution intent (USER INTENT > TOOL CONVENIENCE)
1232
+ const simpleCheckInDispatch = checkSimpleAppAction(this.taskStateManager.getState()?.objective || '');
1233
+ if (simpleCheckInDispatch.isSimple) {
1234
+ if (['list_dir', 'read_file', 'grep_search', 'glob_search', 'file_info', 'tree_view', 'write_file', 'edit_file', 'multi_edit_file'].includes(name)) {
1235
+ 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.`;
1236
+ this.taskStateManager.addObservation(name, blockedExplorationMsg, false);
1237
+ return {
1238
+ result: JSON.stringify({
1239
+ success: false,
1240
+ action: name,
1241
+ error: 'UnnecessaryWorkspaceExplorationBlocked',
1242
+ message: blockedExplorationMsg,
1243
+ directive: "Call 'task_completed' NOW with your completion summary."
1244
+ }, null, 2)
1245
+ };
1246
+ }
1247
+ }
1181
1248
  if (this.executionIntent && !this.executionIntent.allowAlternativeTools) {
1182
1249
  if (this.executionIntent.requiredInteraction === 'computer') {
1183
1250
  if (['write_file', 'edit_file', 'multi_edit_file'].includes(name)) {
@@ -1256,6 +1323,24 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1256
1323
  name = 'computer_diagnostic';
1257
1324
  }
1258
1325
  }
1326
+ // Tier Gating Enforcement
1327
+ if (!isPaid) {
1328
+ if (name.startsWith('computer_') || name === 'computer') {
1329
+ const upgradeMsg = `[PLAN RESTRICTION]: Autonomous Computer Control tools (${name}) are exclusively available to Pro and Enterprise plan developers. Free tier developers can perform code editing, creating files/folders, and running terminal commands. Upgrade to Pro at https://scout.frontterrain.com to unlock autonomous computer & app control.`;
1330
+ this.taskStateManager.addObservation(name, upgradeMsg, false);
1331
+ return { result: upgradeMsg };
1332
+ }
1333
+ if (name.startsWith('browser_')) {
1334
+ const upgradeMsg = `[PLAN RESTRICTION]: Autonomous Browser Control tools (${name}) are exclusively available to Pro and Enterprise plan developers. Free tier developers can perform code editing, creating files/folders, and running terminal commands. Upgrade to Pro at https://scout.frontterrain.com to unlock browser automation.`;
1335
+ this.taskStateManager.addObservation(name, upgradeMsg, false);
1336
+ return { result: upgradeMsg };
1337
+ }
1338
+ if (name === 'scratchpad') {
1339
+ const upgradeMsg = `[PLAN RESTRICTION]: AI Scratchpad & Autonomous reasoning is an exclusive feature for Pro and Enterprise plan subscribers. Free tier developers can perform code editing, creating files/folders, and running commands. Upgrade to Pro at https://scout.frontterrain.com to unlock the AI Scratchpad.`;
1340
+ this.taskStateManager.addObservation(name, upgradeMsg, false);
1341
+ return { result: upgradeMsg };
1342
+ }
1343
+ }
1259
1344
  switch (name) {
1260
1345
  case 'read_file': {
1261
1346
  const targetPath = String(args.path || '').trim();
@@ -1716,6 +1801,54 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1716
1801
  this.taskStateManager.addObservation('browser_content', `Extracted content (${res.output.length} chars)`, res.success);
1717
1802
  return { result: res.output };
1718
1803
  }
1804
+ case 'browser_screenshot': {
1805
+ const res = await this.browserController.screenshot();
1806
+ this.taskStateManager.addObservation('browser_screenshot', res.output, res.success, res.url);
1807
+ return { result: res.output };
1808
+ }
1809
+ case 'browser_select': {
1810
+ const selector = String(args.selector || args.target || '');
1811
+ const val = String(args.value || args.val || '');
1812
+ const res = await this.browserController.select(selector, val);
1813
+ this.taskStateManager.addObservation('browser_select', res.output, res.success, selector);
1814
+ return { result: res.output };
1815
+ }
1816
+ case 'browser_scroll': {
1817
+ const direction = (args.direction || 'down');
1818
+ const amount = Number(args.amount) || 4;
1819
+ const res = await this.browserController.scroll(direction, amount);
1820
+ this.taskStateManager.addObservation('browser_scroll', res.output, res.success);
1821
+ return { result: res.output };
1822
+ }
1823
+ case 'scratchpad': {
1824
+ const action = String(args.action || 'read').toLowerCase().trim();
1825
+ const scratchpadPath = path.resolve(this.cwd, '.ft', 'scratchpad.md');
1826
+ fs.mkdirSync(path.dirname(scratchpadPath), { recursive: true });
1827
+ if (action === 'read') {
1828
+ const content = fs.existsSync(scratchpadPath) ? fs.readFileSync(scratchpadPath, 'utf-8') : '(Scratchpad is empty)';
1829
+ this.taskStateManager.addObservation('scratchpad', 'Read scratchpad content', true);
1830
+ return { result: `Scratchpad Content:\n${content || '(empty)'}` };
1831
+ }
1832
+ else if (action === 'write') {
1833
+ const newContent = String(args.content || '');
1834
+ fs.writeFileSync(scratchpadPath, newContent, 'utf-8');
1835
+ this.taskStateManager.addObservation('scratchpad', `Wrote scratchpad (${newContent.length} chars)`, true);
1836
+ return { result: `Scratchpad updated (${newContent.length} chars).` };
1837
+ }
1838
+ else if (action === 'append') {
1839
+ const appendContent = String(args.content || '');
1840
+ fs.appendFileSync(scratchpadPath, `\n${appendContent}`, 'utf-8');
1841
+ this.taskStateManager.addObservation('scratchpad', `Appended to scratchpad (${appendContent.length} chars)`, true);
1842
+ return { result: `Appended to scratchpad (${appendContent.length} chars).` };
1843
+ }
1844
+ else if (action === 'clear') {
1845
+ if (fs.existsSync(scratchpadPath))
1846
+ fs.unlinkSync(scratchpadPath);
1847
+ this.taskStateManager.addObservation('scratchpad', 'Cleared scratchpad', true);
1848
+ return { result: 'Scratchpad cleared.' };
1849
+ }
1850
+ return { result: `Unknown scratchpad action: "${action}". Supported: read, write, append, clear.` };
1851
+ }
1719
1852
  case 'computer_screenshot': {
1720
1853
  const res = await this.computerController.screenshot();
1721
1854
  if (res.path) {
@@ -1917,6 +2050,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1917
2050
  const res = await this.computerController.openApplication(appName, args.args);
1918
2051
  this.taskStateManager.setActiveApplication(appName);
1919
2052
  this.taskStateManager.addObservation('computer_open_app', res.output, res.success);
2053
+ const currentObj = this.taskStateManager.getState()?.objective || '';
2054
+ const simpleCheck = checkSimpleAppAction(currentObj);
2055
+ const isOnlyOpening = simpleCheck.isSimple && simpleCheck.action !== 'close';
1920
2056
  const structuredRes = {
1921
2057
  success: res.success,
1922
2058
  action: 'open_app',
@@ -1925,6 +2061,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1925
2061
  recoverable: res.recoverable ?? true,
1926
2062
  activeWindow: res.activeWindow || appName,
1927
2063
  expectedWindow: res.expectedWindow || appName,
2064
+ ...(isOnlyOpening && res.success ? {
2065
+ 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."
2066
+ } : {}),
1928
2067
  };
1929
2068
  return { result: JSON.stringify(structuredRes, null, 2) };
1930
2069
  }
@@ -1932,6 +2071,12 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1932
2071
  const appName = String(args.appName || args.app || '');
1933
2072
  const res = await this.computerController.closeApplication(appName);
1934
2073
  this.taskStateManager.addObservation('computer_close_app', res.output, res.success);
2074
+ const currentObj = this.taskStateManager.getState()?.objective || '';
2075
+ const simpleCheck = checkSimpleAppAction(currentObj);
2076
+ const isOnlyClosing = simpleCheck.isSimple && simpleCheck.action === 'close';
2077
+ if (isOnlyClosing && res.success) {
2078
+ return { result: `${res.output}\nCRITICAL: Application closed. Objective complete. Call task_completed now.` };
2079
+ }
1935
2080
  return { result: res.output };
1936
2081
  }
1937
2082
  case 'computer_switch_window': {