ft-scout 9.0.6 → 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.
@@ -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({
@@ -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)) {
@@ -1983,6 +2050,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1983
2050
  const res = await this.computerController.openApplication(appName, args.args);
1984
2051
  this.taskStateManager.setActiveApplication(appName);
1985
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';
1986
2056
  const structuredRes = {
1987
2057
  success: res.success,
1988
2058
  action: 'open_app',
@@ -1991,6 +2061,9 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1991
2061
  recoverable: res.recoverable ?? true,
1992
2062
  activeWindow: res.activeWindow || appName,
1993
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
+ } : {}),
1994
2067
  };
1995
2068
  return { result: JSON.stringify(structuredRes, null, 2) };
1996
2069
  }
@@ -1998,6 +2071,12 @@ You MUST output exactly ONE JSON code block per tool call. Do NOT describe what
1998
2071
  const appName = String(args.appName || args.app || '');
1999
2072
  const res = await this.computerController.closeApplication(appName);
2000
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
+ }
2001
2080
  return { result: res.output };
2002
2081
  }
2003
2082
  case 'computer_switch_window': {