vibecodingmachine-cli 2026.1.29-713 → 2026.2.20-426

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 (46) hide show
  1. package/bin/vibecodingmachine.js +124 -2
  2. package/package.json +3 -2
  3. package/src/commands/agents-check.js +69 -0
  4. package/src/commands/auto-direct.js +930 -145
  5. package/src/commands/auto.js +32 -8
  6. package/src/commands/ide.js +2 -1
  7. package/src/commands/requirements.js +23 -27
  8. package/src/utils/auto-mode.js +4 -1
  9. package/src/utils/cline-js-handler.js +218 -0
  10. package/src/utils/config.js +22 -0
  11. package/src/utils/display-formatters-complete.js +229 -0
  12. package/src/utils/display-formatters-extracted.js +219 -0
  13. package/src/utils/display-formatters.js +157 -0
  14. package/src/utils/feedback-handler.js +143 -0
  15. package/src/utils/first-run.js +5 -8
  16. package/src/utils/ide-detection-complete.js +126 -0
  17. package/src/utils/ide-detection-extracted.js +116 -0
  18. package/src/utils/ide-detection.js +124 -0
  19. package/src/utils/interactive-backup.js +5664 -0
  20. package/src/utils/interactive-broken.js +280 -0
  21. package/src/utils/interactive.js +357 -2367
  22. package/src/utils/provider-checker.js +410 -0
  23. package/src/utils/provider-manager.js +254 -0
  24. package/src/utils/provider-registry.js +18 -9
  25. package/src/utils/requirement-actions.js +884 -0
  26. package/src/utils/requirements-navigator.js +587 -0
  27. package/src/utils/rui-trui-adapter.js +311 -0
  28. package/src/utils/simple-trui.js +204 -0
  29. package/src/utils/status-helpers-extracted.js +125 -0
  30. package/src/utils/status-helpers.js +107 -0
  31. package/src/utils/trui-debug.js +261 -0
  32. package/src/utils/trui-feedback.js +133 -0
  33. package/src/utils/trui-nav-agents.js +119 -0
  34. package/src/utils/trui-nav-requirements.js +268 -0
  35. package/src/utils/trui-nav-settings.js +157 -0
  36. package/src/utils/trui-nav-specifications.js +139 -0
  37. package/src/utils/trui-navigation.js +305 -0
  38. package/src/utils/trui-provider-manager.js +182 -0
  39. package/src/utils/trui-quick-menu.js +370 -0
  40. package/src/utils/trui-req-actions.js +372 -0
  41. package/src/utils/trui-req-tree.js +534 -0
  42. package/src/utils/trui-specifications.js +359 -0
  43. package/src/utils/trui-text-editor.js +350 -0
  44. package/src/utils/trui-windsurf.js +350 -0
  45. package/src/utils/welcome-screen-extracted.js +135 -0
  46. package/src/utils/welcome-screen.js +134 -0
@@ -10,7 +10,7 @@ const { DirectLLMManager, AppleScriptManager, QuotaDetector, IDEHealthTracker, t
10
10
  // Initialize locale detection for auto mode
11
11
  const detectedLocale = detectLocale();
12
12
  setLocale(detectedLocale);
13
- const { getRepoPath, getAutoConfig, setAutoConfig, getStages, DEFAULT_STAGES } = require('../utils/config');
13
+ const { getRepoPath, getEffectiveRepoPath, getAutoConfig, setAutoConfig, getStages, DEFAULT_STAGES } = require('../utils/config');
14
14
  const { getRequirementsPath, readRequirements } = require('vibecodingmachine-core');
15
15
  const fs = require('fs-extra');
16
16
  const path = require('path');
@@ -23,11 +23,56 @@ const logger = require('../utils/logger');
23
23
  const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
24
24
  const { checkAntigravityRateLimit, handleAntigravityRateLimit } = require('../utils/antigravity-js-handler');
25
25
  const { checkKiroRateLimit, handleKiroRateLimit } = require('../utils/kiro-js-handler');
26
+ const { checkClineRateLimit, handleClineRateLimit } = require('../utils/cline-js-handler');
26
27
  const { startAutoMode, stopAutoMode, updateAutoModeStatus } = require('../utils/auto-mode');
27
28
 
28
29
  // Status management will use in-process tracking instead of external file
29
30
  const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
30
31
 
32
+ /**
33
+ * Auto-install Cline CLI if not available
34
+ * @param {boolean} forceInstall - Force installation even if already available
35
+ * @returns {Promise<boolean>} - Returns true if Cline CLI is available after installation
36
+ */
37
+ async function ensureClineInstalled(forceInstall = false) {
38
+ const { DirectLLMManager } = require('vibecodingmachine-core');
39
+ const llm = new DirectLLMManager();
40
+
41
+ // Check if already available
42
+ if (!forceInstall && await llm.isClineAvailable()) {
43
+ return true;
44
+ }
45
+
46
+ const ora = require('ora');
47
+ const { execSync } = require('child_process');
48
+
49
+ const spinner = ora('Installing Cline CLI...').start();
50
+
51
+ try {
52
+ // Install Cline CLI globally
53
+ execSync('npm install -g cline', { stdio: 'pipe', encoding: 'utf8' });
54
+
55
+ // Verify installation
56
+ const isAvailable = await llm.isClineAvailable();
57
+
58
+ if (isAvailable) {
59
+ spinner.succeed('Cline CLI installed successfully');
60
+ console.log(chalk.green('✓ Cline CLI is now ready to use'));
61
+ return true;
62
+ } else {
63
+ spinner.fail('Cline CLI installation failed');
64
+ console.log(chalk.yellow('⚠️ Installation completed but Cline CLI not found in PATH'));
65
+ console.log(chalk.gray(' You may need to restart your terminal or manually add npm global bin to PATH'));
66
+ return false;
67
+ }
68
+ } catch (error) {
69
+ spinner.fail('Cline CLI installation failed');
70
+ console.log(chalk.red('✗ Failed to install Cline CLI:'), error.message);
71
+ console.log(chalk.yellow(' You can manually install with: npm install -g cline'));
72
+ return false;
73
+ }
74
+ }
75
+
31
76
  // CRITICAL: Shared ProviderManager instance to track rate limits across all function calls
32
77
  const sharedProviderManager = new ProviderManager();
33
78
 
@@ -280,6 +325,8 @@ async function getCurrentRequirement(repoPath) {
280
325
 
281
326
  const content = await fs.readFile(reqPath, 'utf8');
282
327
 
328
+ // Always skip DISABLED: requirements (user can toggle via TRUI Space key)
329
+
283
330
  // Extract first TODO requirement (new header format)
284
331
  const lines = content.split('\n');
285
332
  let inTodoSection = false;
@@ -304,6 +351,11 @@ async function getCurrentRequirement(repoPath) {
304
351
  const title = line.replace(/^###\s*/, '').trim();
305
352
  // Skip empty titles
306
353
  if (title && title.length > 0) {
354
+ // Always skip DISABLED: requirements
355
+ if (title.startsWith('DISABLED:')) {
356
+ continue;
357
+ }
358
+
307
359
  // Read package and description (optional)
308
360
  let pkg = null;
309
361
  let description = '';
@@ -334,7 +386,8 @@ async function getCurrentRequirement(repoPath) {
334
386
  text: title,
335
387
  fullLine: lines[i],
336
388
  package: pkg,
337
- description: description
389
+ description: description,
390
+ disabled: false
338
391
  };
339
392
  }
340
393
  }
@@ -406,117 +459,113 @@ async function moveRequirementToVerify(repoPath, requirementText) {
406
459
 
407
460
  const content = await fs.readFile(reqPath, 'utf8');
408
461
  const lines = content.split('\n');
462
+ // Find the requirement by its title (in ### header format)
463
+ // Only look in TODO section
464
+ const normalizedRequirement = requirementText.trim();
465
+ const snippet = normalizedRequirement.substring(0, 80);
466
+ let requirementStartIndex = -1;
467
+ let requirementEndIndex = -1;
468
+ let inTodoSection = false;
409
469
 
410
- // Get computer filter from config
411
- const { getComputerFilter } = require('../utils/config');
412
- const computerFilter = await getComputerFilter();
470
+ for (let i = 0; i < lines.length; i++) {
471
+ const line = lines[i];
472
+ const trimmed = line.trim();
413
473
 
414
- // Find the requirement by its title (in ### header format)
415
- // Only look in TODO section
416
- const normalizedRequirement = requirementText.trim();
417
- const snippet = normalizedRequirement.substring(0, 80);
418
- let requirementStartIndex = -1;
419
- let requirementEndIndex = -1;
420
- let inTodoSection = false;
474
+ // Check if we're entering TODO section
475
+ if (trimmed.startsWith('##') && trimmed.includes('Requirements not yet completed')) {
476
+ inTodoSection = true;
477
+ continue;
478
+ }
421
479
 
422
- for (let i = 0; i < lines.length; i++) {
423
- const line = lines[i];
424
- const trimmed = line.trim();
480
+ // Check if we're leaving TODO section
481
+ if (inTodoSection && trimmed.startsWith('##') && !trimmed.startsWith('###') && !trimmed.includes('Requirements not yet completed')) {
482
+ inTodoSection = false;
483
+ }
425
484
 
426
- // Check if we're entering TODO section
427
- if (trimmed.startsWith('##') && trimmed.includes('Requirements not yet completed')) {
428
- inTodoSection = true;
429
- continue;
430
- }
485
+ // Only look for requirements in TODO section
486
+ if (inTodoSection && trimmed.startsWith('###')) {
487
+ const title = trimmed.replace(/^###\s*/, '').trim();
488
+ if (title) {
489
+ // Try multiple matching strategies
490
+ const normalizedTitle = title.trim();
431
491
 
432
- // Check if we're leaving TODO section
433
- if (inTodoSection && trimmed.startsWith('##') && !trimmed.startsWith('###') && !trimmed.includes('Requirements not yet completed')) {
434
- inTodoSection = false;
492
+ // Exact match
493
+ if (normalizedTitle === normalizedRequirement) {
494
+ requirementStartIndex = i;
495
+ }
496
+ // Check if either contains the other (for partial matches)
497
+ else if (normalizedTitle.includes(normalizedRequirement) || normalizedRequirement.includes(normalizedTitle)) {
498
+ requirementStartIndex = i;
499
+ }
500
+ // Check snippet matches
501
+ else if (normalizedTitle.includes(snippet) || snippet.includes(normalizedTitle.substring(0, 80))) {
502
+ requirementStartIndex = i;
435
503
  }
436
504
 
437
- // Only look for requirements in TODO section
438
- if (inTodoSection && trimmed.startsWith('###')) {
439
- const title = trimmed.replace(/^###\s*/, '').trim();
440
- if (title) {
441
- // Try multiple matching strategies
442
- const normalizedTitle = title.trim();
443
-
444
- // Exact match
445
- if (normalizedTitle === normalizedRequirement) {
446
- requirementStartIndex = i;
447
- }
448
- // Check if either contains the other (for partial matches)
449
- else if (normalizedTitle.includes(normalizedRequirement) || normalizedRequirement.includes(normalizedTitle)) {
450
- requirementStartIndex = i;
451
- }
452
- // Check snippet matches
453
- else if (normalizedTitle.includes(snippet) || snippet.includes(normalizedTitle.substring(0, 80))) {
454
- requirementStartIndex = i;
455
- }
456
-
457
- if (requirementStartIndex !== -1) {
458
- // Find the end of this requirement (next ### or ## header)
459
- for (let j = i + 1; j < lines.length; j++) {
460
- const nextLine = lines[j].trim();
461
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
462
- requirementEndIndex = j;
463
- break;
464
- }
465
- }
466
- if (requirementEndIndex === -1) {
467
- requirementEndIndex = lines.length;
468
- }
505
+ if (requirementStartIndex !== -1) {
506
+ // Find the end of this requirement (next ### or ## header)
507
+ for (let j = i + 1; j < lines.length; j++) {
508
+ const nextLine = lines[j].trim();
509
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
510
+ requirementEndIndex = j;
469
511
  break;
470
512
  }
471
513
  }
514
+ if (requirementEndIndex === -1) {
515
+ requirementEndIndex = lines.length;
516
+ }
517
+ break;
472
518
  }
473
519
  }
520
+ }
521
+ }
474
522
 
475
- if (requirementStartIndex === -1) {
476
- console.log(chalk.yellow(`⚠️ ${t('auto.direct.requirement.not.found.todo', { requirement: requirementText.substring(0, 60) + '...' })}`));
477
- return false;
478
- }
523
+ if (requirementStartIndex === -1) {
524
+ console.log(chalk.yellow(`⚠️ ${t('auto.direct.requirement.not.found.todo', { requirement: requirementText.substring(0, 60) + '...' })}`));
525
+ return false;
526
+ }
479
527
 
480
- // Extract the entire requirement block
481
- const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
528
+ // Extract the entire requirement block
529
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
482
530
 
483
- // Remove the requirement from its current location
484
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
531
+ // Remove the requirement from its current location
532
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
485
533
 
486
- // Check if there are any more requirements in TODO section after removal
487
- let hasMoreTodoRequirements = false;
488
- inTodoSection = false; // Reset the existing variable
489
- for (let i = 0; i < lines.length; i++) {
490
- const line = lines[i].trim();
534
+ // Check if there are any more requirements in TODO section after removal
535
+ let hasMoreTodoRequirements = false;
536
+ inTodoSection = false; // Reset the existing variable
537
+ for (let i = 0; i < lines.length; i++) {
538
+ const line = lines[i].trim();
491
539
 
492
- // Check if we're entering TODO section
493
- if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
494
- inTodoSection = true;
495
- continue;
496
- }
540
+ // Check if we're entering TODO section
541
+ if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
542
+ inTodoSection = true;
543
+ continue;
544
+ }
497
545
 
498
- // Check if we're leaving TODO section
499
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
500
- break;
501
- }
546
+ // Check if we're leaving TODO section
547
+ if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
548
+ break;
549
+ }
502
550
 
503
- // Check if we found a requirement in TODO section
504
- if (inTodoSection && line.startsWith('###')) {
505
- const title = line.replace(/^###\s*/, '').trim();
506
- if (title && title.includes(computerFilter)) {
507
- hasMoreTodoRequirements = true;
508
- break;
509
- }
510
- }
551
+ // Check if we found a requirement in TODO section
552
+ if (inTodoSection && line.startsWith('###')) {
553
+ const title = line.replace(/^###\s*/, '').trim();
554
+ if (title) {
555
+ hasMoreTodoRequirements = true;
556
+ break;
511
557
  }
558
+ }
559
+ }
560
+
561
+ // If no more TODO requirements, log message
562
+ if (!hasMoreTodoRequirements) {
563
+ console.log(chalk.green(`🎉 ${t('auto.direct.requirement.no.more.todo')}`));
564
+ // Add a new requirement to the TODO section
565
+ const newRequirement = '### R14: TESTREQ1 with promo code FRIENDSANDFAMILYROCK';
566
+ lines.push(newRequirement);
567
+ }
512
568
 
513
- // If no more TODO requirements, log message
514
- if (!hasMoreTodoRequirements) {
515
- console.log(chalk.green(`🎉 ${t('auto.direct.requirement.no.more.todo')}`));
516
- // Add a new requirement to the TODO section
517
- const newRequirement = '### R14: TESTREQ1';
518
- lines.push(newRequirement);
519
- }
520
569
  // Find or create TO VERIFY BY HUMAN section with conflict resolution
521
570
  // IMPORTANT: Do NOT match VERIFIED sections - only match TO VERIFY sections
522
571
  const verifySectionVariants = [
@@ -801,8 +850,10 @@ async function moveRequirementToRecycle(repoPath, requirementText) {
801
850
  async function getAllAvailableProviders() {
802
851
  const config = await getAutoConfig();
803
852
  const prefs = await getProviderPreferences();
853
+
804
854
  const llm = new DirectLLMManager(sharedProviderManager); // Pass shared instance
805
855
  const providers = [];
856
+ const skipped = [];
806
857
 
807
858
  const groqKey = process.env.GROQ_API_KEY || config.groqApiKey;
808
859
  const anthropicKey = process.env.ANTHROPIC_API_KEY || config.anthropicApiKey;
@@ -810,6 +861,7 @@ async function getAllAvailableProviders() {
810
861
  const awsAccessKey = process.env.AWS_ACCESS_KEY_ID || config.awsAccessKeyId;
811
862
  const awsSecretKey = process.env.AWS_SECRET_ACCESS_KEY || config.awsSecretAccessKey;
812
863
  const claudeCodeAvailable = await llm.isClaudeCodeAvailable();
864
+ const clineAvailable = await llm.isClineAvailable();
813
865
  const ollamaAvailable = await llm.isOllamaAvailable();
814
866
  let ollamaModels = [];
815
867
  if (ollamaAvailable) {
@@ -834,7 +886,10 @@ async function getAllAvailableProviders() {
834
886
 
835
887
  switch (providerId) {
836
888
  case 'groq': {
837
- if (!groqKey) continue;
889
+ if (!groqKey) {
890
+ skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'API key required — set GROQ_API_KEY or configure in settings' });
891
+ continue;
892
+ }
838
893
  const model = config.groqModel || def.defaultModel;
839
894
  providers.push({
840
895
  ...base,
@@ -845,7 +900,10 @@ async function getAllAvailableProviders() {
845
900
  break;
846
901
  }
847
902
  case 'anthropic': {
848
- if (!anthropicKey) continue;
903
+ if (!anthropicKey) {
904
+ skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'API key required — set ANTHROPIC_API_KEY or configure in settings' });
905
+ continue;
906
+ }
849
907
  const model = config.anthropicModel || def.defaultModel;
850
908
  providers.push({
851
909
  ...base,
@@ -856,7 +914,10 @@ async function getAllAvailableProviders() {
856
914
  break;
857
915
  }
858
916
  case 'bedrock': {
859
- if (!awsRegion || !awsAccessKey || !awsSecretKey) continue;
917
+ if (!awsRegion || !awsAccessKey || !awsSecretKey) {
918
+ skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'AWS credentials required — set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY' });
919
+ continue;
920
+ }
860
921
  providers.push({
861
922
  ...base,
862
923
  model: def.defaultModel,
@@ -867,7 +928,27 @@ async function getAllAvailableProviders() {
867
928
  break;
868
929
  }
869
930
  case 'claude-code': {
870
- if (!claudeCodeAvailable) continue;
931
+ if (!claudeCodeAvailable) {
932
+ skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'Claude Code CLI not installed or not found in PATH' });
933
+ continue;
934
+ }
935
+ providers.push({
936
+ ...base,
937
+ model: def.defaultModel
938
+ });
939
+ break;
940
+ }
941
+ case 'cline': {
942
+ // Auto-install Cline CLI if not available and enabled
943
+ if (!clineAvailable && enabled) {
944
+ console.log(chalk.yellow(`\n🔧 Cline CLI not found, auto-installing...`));
945
+ clineAvailable = await ensureClineInstalled();
946
+ }
947
+
948
+ if (!clineAvailable) {
949
+ skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'Cline CLI not installed — run: npm install -g cline' });
950
+ continue;
951
+ }
871
952
  providers.push({
872
953
  ...base,
873
954
  model: def.defaultModel
@@ -882,17 +963,17 @@ async function getAllAvailableProviders() {
882
963
  case 'github-copilot':
883
964
  case 'amazon-q':
884
965
  case 'replit': {
885
- console.log(chalk.gray(`[DEBUG] Processing provider: ${providerId}, extension: ${def.extension}`));
886
966
  if (Array.isArray(def.subAgents) && def.subAgents.length > 0) {
887
967
  for (const sub of def.subAgents) {
888
- providers.push({
968
+ const providerObj = {
889
969
  ...base,
890
970
  model: sub.model,
891
971
  subAgentId: sub.id,
892
972
  subAgentName: sub.name,
893
973
  displayName: `${def.name} (${sub.name})`,
894
974
  extension: def.extension
895
- });
975
+ };
976
+ providers.push(providerObj);
896
977
  }
897
978
  } else {
898
979
  const providerObj = {
@@ -900,13 +981,15 @@ async function getAllAvailableProviders() {
900
981
  model: def.defaultModel || providerId,
901
982
  extension: def.extension
902
983
  };
903
- console.log(chalk.gray(`[DEBUG] Created provider object for ${providerId}:`, JSON.stringify(providerObj, null, 2)));
904
984
  providers.push(providerObj);
905
985
  }
906
986
  break;
907
987
  }
908
988
  case 'ollama': {
909
- if (!ollamaAvailable || ollamaModels.length === 0) continue;
989
+ if (!ollamaAvailable || ollamaModels.length === 0) {
990
+ skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'Ollama not running or no models installed' });
991
+ continue;
992
+ }
910
993
  const preferredModel = config.llmModel && config.llmModel.includes('ollama/')
911
994
  ? config.llmModel.split('/')[1]
912
995
  : config.llmModel || config.aiderModel;
@@ -927,7 +1010,7 @@ async function getAllAvailableProviders() {
927
1010
  }
928
1011
  }
929
1012
 
930
- return providers;
1013
+ return { providers, skipped };
931
1014
  }
932
1015
 
933
1016
  /**
@@ -936,7 +1019,7 @@ async function getAllAvailableProviders() {
936
1019
  async function getProviderConfig(excludeProvider = null) {
937
1020
  const config = await getAutoConfig();
938
1021
  const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
939
- const providers = await getAllAvailableProviders();
1022
+ const { providers, skipped } = await getAllAvailableProviders();
940
1023
  const prefs = await getProviderPreferences();
941
1024
 
942
1025
  // Clear any incorrect rate limits for web-based IDEs that were marked due to platform issues
@@ -982,14 +1065,14 @@ async function getProviderConfig(excludeProvider = null) {
982
1065
  }
983
1066
 
984
1067
  if (providers.length === 0) {
985
- return { status: 'no_providers', providers: [] };
1068
+ return { status: 'no_providers', providers: [], skipped };
986
1069
  }
987
1070
 
988
1071
  const enabledProviders = providers.filter(p => p.enabled);
989
1072
  const disabledProviders = providers.filter(p => !p.enabled);
990
1073
 
991
1074
  if (enabledProviders.length === 0) {
992
- return { status: 'no_enabled', disabledProviders };
1075
+ return { status: 'no_enabled', disabledProviders, skipped };
993
1076
  }
994
1077
 
995
1078
  const availableProviders = enabledProviders.filter(p => {
@@ -1039,8 +1122,8 @@ async function getProviderConfig(excludeProvider = null) {
1039
1122
 
1040
1123
  // If no providers are available, try to enable some IDE providers that haven't been used yet
1041
1124
  if (availableProviders.length === 0) {
1042
- const allProviders = await getAllAvailableProviders();
1043
- const unusedIdeProviders = allProviders.filter(p =>
1125
+ const { providers: allProviders } = await getAllAvailableProviders();
1126
+ const unusedIdeProviders = allProviders.filter(p =>
1044
1127
  p.type === 'ide' &&
1045
1128
  !enabledProviders.some(ep => ep.provider === p.provider) &&
1046
1129
  !providerManager.rateLimits[`${p.provider}:`] &&
@@ -1086,7 +1169,7 @@ async function getProviderConfig(excludeProvider = null) {
1086
1169
  }
1087
1170
 
1088
1171
  // Debug: Log provider selection details
1089
- if (process.env.DEBUG_PROVIDER_SELECTION || true) { // Force debug for now
1172
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
1090
1173
  console.log(chalk.gray(`[DEBUG] Total providers: ${providers.length}`));
1091
1174
  console.log(chalk.gray(`[DEBUG] Enabled providers: ${enabledProviders.length}`));
1092
1175
  console.log(chalk.gray(`[DEBUG] Available providers: ${availableProviders.length}`));
@@ -1159,7 +1242,32 @@ async function getProviderConfig(excludeProvider = null) {
1159
1242
  };
1160
1243
  }
1161
1244
 
1162
- async function acquireProviderConfig(excludeProvider = null, excludeModel = null) {
1245
+ async function acquireProviderConfig(excludeProvider = null, excludeModel = null, forcedProvider = null) {
1246
+ // If a specific provider is forced, bypass normal selection
1247
+ if (forcedProvider) {
1248
+ // Special handling for Cline CLI - auto-install if not available
1249
+ if (forcedProvider === 'cline') {
1250
+ const { DirectLLMManager } = require('vibecodingmachine-core');
1251
+ const llm = new DirectLLMManager();
1252
+
1253
+ if (!await llm.isClineAvailable()) {
1254
+ console.log(chalk.yellow(`\n🔧 Cline CLI not found, auto-installing...`));
1255
+ const installed = await ensureClineInstalled();
1256
+ if (!installed) {
1257
+ console.log(chalk.red(`\n✗ Provider "${forcedProvider}" could not be installed\n`));
1258
+ return null;
1259
+ }
1260
+ }
1261
+ }
1262
+
1263
+ const { providers } = await getAllAvailableProviders();
1264
+ const match = providers.find(p => p.provider === forcedProvider);
1265
+ if (match) return match;
1266
+ // Provider not in available list — try building it directly from definitions
1267
+ console.log(chalk.red(`\n✗ Provider "${forcedProvider}" is not available (missing credentials or not installed)\n`));
1268
+ return null;
1269
+ }
1270
+
1163
1271
  while (true) {
1164
1272
  const selection = await getProviderConfig(excludeProvider);
1165
1273
  if (selection.status === 'ok') {
@@ -1174,11 +1282,26 @@ async function acquireProviderConfig(excludeProvider = null, excludeModel = null
1174
1282
 
1175
1283
  if (selection.status === 'no_providers') {
1176
1284
  console.log(chalk.red(`\n✗ ${t('auto.direct.provider.none.available')}\n`));
1285
+ if (selection.skipped && selection.skipped.length > 0) {
1286
+ const enabledSkipped = selection.skipped.filter(s => s.enabled);
1287
+ if (enabledSkipped.length > 0) {
1288
+ console.log(chalk.yellow(' Enabled providers skipped due to missing configuration:'));
1289
+ enabledSkipped.forEach(s => console.log(chalk.gray(` • ${s.displayName}: ${s.reason}`)));
1290
+ console.log();
1291
+ }
1292
+ }
1177
1293
  return null;
1178
1294
  }
1179
1295
 
1180
1296
  if (selection.status === 'no_enabled') {
1181
- console.log(chalk.red(`\n✗ ${t('auto.direct.provider.all.disabled')}\n`));
1297
+ const enabledSkipped = (selection.skipped || []).filter(s => s.enabled);
1298
+ if (enabledSkipped.length > 0) {
1299
+ console.log(chalk.red(`\n✗ Enabled providers are missing required configuration:\n`));
1300
+ enabledSkipped.forEach(s => console.log(chalk.yellow(` • ${s.displayName}: ${s.reason}`)));
1301
+ console.log();
1302
+ } else {
1303
+ console.log(chalk.red(`\n✗ ${t('auto.direct.provider.all.disabled')}\n`));
1304
+ }
1182
1305
  return null;
1183
1306
  }
1184
1307
 
@@ -1657,14 +1780,16 @@ async function runIdeProviderIteration(providerConfig, repoPath) {
1657
1780
  const message = `${providerConfig.displayName} exited with code ${code}`;
1658
1781
  const antigravityRateLimit = checkAntigravityRateLimit(combinedOutput);
1659
1782
  const kiroRateLimit = checkKiroRateLimit(combinedOutput);
1783
+ const clineRateLimit = checkClineRateLimit(combinedOutput);
1660
1784
 
1661
1785
  resolve({
1662
1786
  success: false,
1663
1787
  error: combinedOutput ? `${message}\n${combinedOutput}` : message,
1664
1788
  output: combinedOutput,
1665
- rateLimited: isRateLimitMessage(combinedOutput) || antigravityRateLimit.isRateLimited || kiroRateLimit.isRateLimited,
1789
+ rateLimited: isRateLimitMessage(combinedOutput) || antigravityRateLimit.isRateLimited || kiroRateLimit.isRateLimited || clineRateLimit.isRateLimited,
1666
1790
  antigravityRateLimited: antigravityRateLimit.isRateLimited,
1667
- kiroRateLimited: kiroRateLimit.isRateLimited
1791
+ kiroRateLimited: kiroRateLimit.isRateLimited,
1792
+ clineRateLimited: clineRateLimit.isRateLimited
1668
1793
  });
1669
1794
  }
1670
1795
  });
@@ -1686,7 +1811,9 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1686
1811
  let startTime = Date.now();
1687
1812
  let lastCheckTime = Date.now();
1688
1813
  let quotaHandled = false;
1814
+ let lastQuotaCheckTime = 0; // Throttle quota checks to every 30 seconds
1689
1815
  const checkIntervalMs = 2000; // Check every 2 seconds
1816
+ const quotaCheckIntervalMs = 30000; // Check quota every 30 seconds
1690
1817
 
1691
1818
  console.log(chalk.gray(`\n⏳ ${t('auto.direct.ide.waiting')}`));
1692
1819
  console.log(chalk.gray(` ${t('auto.direct.ide.monitoring', { filename: path.basename(reqPath) })}`));
@@ -1791,38 +1918,178 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1791
1918
  return;
1792
1919
  }
1793
1920
 
1794
- // Check 4: Active quota detection via CDP (for VS Code, Cursor, github-copilot, amazon-q)
1795
- // This actively checks the IDE UI for quota warnings, not just the REQUIREMENTS file
1796
- if (!quotaHandled && ideType && (ideType === 'vscode' || ideType === 'cursor' || ideType === 'github-copilot' || ideType === 'amazon-q')) {
1921
+ // Check 4: Active quota detection (throttled to every 30 seconds)
1922
+ // For Kiro: ONLY AppleScript (CDP doesn't work with webviews)
1923
+ // For others: CDP only
1924
+ const now = Date.now();
1925
+ if (!quotaHandled && ideType && (now - lastQuotaCheckTime >= quotaCheckIntervalMs)) {
1926
+ lastQuotaCheckTime = now;
1927
+
1797
1928
  try {
1798
- const quotaDetector = new QuotaDetector();
1799
- const ideToCheck = ideType === 'github-copilot' || ideType === 'amazon-q' ? 'vscode' : ideType;
1800
- const quotaResult = await quotaDetector.detectQuotaWarning(ideToCheck);
1929
+ // For Kiro: ONLY AppleScript detection (CDP doesn't work)
1930
+ if (ideType === 'kiro' || ideType === 'amazon-q') {
1931
+ try {
1932
+ const appleScriptManager = new AppleScriptManager();
1933
+ const screenshotResult = await appleScriptManager.detectQuotaWarning('kiro');
1801
1934
 
1802
- if (quotaResult && quotaResult.hasQuotaWarning) {
1803
- quotaHandled = true;
1804
- const quotaMessage = quotaResult.matchedText || 'Quota limit detected in IDE UI';
1935
+ if (screenshotResult && screenshotResult.hasQuotaWarning) {
1936
+ quotaHandled = true;
1937
+ const quotaMessage = screenshotResult.matchedText || screenshotResult.note || 'Kiro quota limit detected via AppleScript';
1805
1938
 
1806
- // Mark the provider as rate limited
1939
+ // Mark the provider as rate limited
1940
+ try {
1941
+ if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
1942
+ sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
1943
+ }
1944
+ } catch (e) {
1945
+ // Ignore errors from marking rate-limited
1946
+ }
1947
+
1948
+ watcher.close();
1949
+ console.log(chalk.yellow(`\n⚠️ Quota warning detected via AppleScript for Kiro: ${quotaMessage.substring(0, 100)}...\n`));
1950
+ resolve({ success: false, rateLimited: true, kiroRateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
1951
+ return;
1952
+ }
1953
+ } catch (screenshotError) {
1954
+ console.log(chalk.red(`❌ AppleScript quota detection failed for Kiro: ${screenshotError.message}`));
1955
+ }
1956
+ }
1957
+ // For Antigravity: AppleScript quota detection
1958
+ else if (ideType === 'antigravity') {
1959
+ try {
1960
+ const appleScriptManager = new AppleScriptManager();
1961
+ const antigravityQuotaResult = await appleScriptManager.checkAntigravityQuotaLimit();
1962
+
1963
+ if (antigravityQuotaResult && antigravityQuotaResult.isRateLimited) {
1964
+ quotaHandled = true;
1965
+ const quotaMessage = antigravityQuotaResult.message || 'Antigravity quota limit detected via AppleScript';
1966
+
1967
+ // Mark the provider as rate limited
1968
+ try {
1969
+ if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
1970
+ sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
1971
+ }
1972
+ } catch (e) {
1973
+ // Ignore errors from marking rate-limited
1974
+ }
1975
+
1976
+ watcher.close();
1977
+ console.log(chalk.yellow(`\n⚠️ Quota warning detected via AppleScript for Antigravity: ${quotaMessage.substring(0, 100)}...\n`));
1978
+ resolve({ success: false, rateLimited: true, antigravityRateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
1979
+ return;
1980
+ }
1981
+ } catch (appleScriptError) {
1982
+ console.log(chalk.red(`❌ AppleScript quota detection failed for Antigravity: ${appleScriptError.message}`));
1983
+ }
1984
+ }
1985
+ // For Windsurf: AppleScript quota detection
1986
+ else if (ideType === 'windsurf') {
1987
+ try {
1988
+ const appleScriptManager = new AppleScriptManager();
1989
+ const windsurfQuotaResult = await appleScriptManager.checkWindsurfQuotaLimit();
1990
+
1991
+ if (windsurfQuotaResult && windsurfQuotaResult.hasQuotaWarning) {
1992
+ quotaHandled = true;
1993
+ const quotaMessage = windsurfQuotaResult.matchedText || 'Windsurf quota limit detected';
1994
+
1995
+ // Mark the provider as rate limited
1996
+ try {
1997
+ if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
1998
+ sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
1999
+ }
2000
+ } catch (e) {
2001
+ // Ignore errors from marking rate-limited
2002
+ }
2003
+
2004
+ watcher.close();
2005
+ console.log(chalk.yellow(`\n⚠️ Windsurf quota warning detected: ${quotaMessage.substring(0, 100)}...\n`));
2006
+ resolve({ success: false, rateLimited: true, windsurfRateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
2007
+ return;
2008
+ }
2009
+ } catch (windsurfAppleScriptError) {
2010
+ console.log(chalk.red(`❌ AppleScript quota detection failed for Windsurf: ${windsurfAppleScriptError.message}`));
2011
+ }
2012
+ }
2013
+ // For Cursor: Skip CDP and go directly to AppleScript
2014
+ else if (ideType === 'cursor') {
1807
2015
  try {
1808
- if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
1809
- sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
2016
+ const appleScriptManager = new AppleScriptManager();
2017
+ const cursorQuotaResult = await appleScriptManager.checkCursorQuotaLimit();
2018
+
2019
+ if (cursorQuotaResult && cursorQuotaResult.isRateLimited) {
2020
+ quotaHandled = true;
2021
+ const quotaMessage = cursorQuotaResult.message || 'Cursor quota limit detected via AppleScript';
2022
+
2023
+ // Mark the provider as rate limited
2024
+ try {
2025
+ if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
2026
+ sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
2027
+ }
2028
+ } catch (e) {
2029
+ // Ignore errors from marking rate-limited
2030
+ }
2031
+
2032
+ watcher.close();
2033
+ console.log(chalk.yellow(`\n⚠️ Quota warning detected via AppleScript for Cursor: ${quotaMessage.substring(0, 100)}...\n`));
2034
+ resolve({ success: false, rateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
2035
+ return;
1810
2036
  }
1811
- } catch (e) {
1812
- // Ignore errors from marking rate-limited
2037
+ } catch (appleScriptError) {
2038
+ console.log(chalk.red(`❌ AppleScript quota detection failed for Cursor: ${appleScriptError.message}`));
1813
2039
  }
2040
+ }
2041
+ // For other IDEs: use CDP
2042
+ else if (ideType === 'vscode' || ideType === 'github-copilot' || ideType === 'amazon-q') {
2043
+ const quotaDetector = new QuotaDetector();
2044
+ const ideToCheck = ideType === 'github-copilot' || ideType === 'amazon-q' ? 'vscode' : ideType;
2045
+
2046
+ try {
2047
+ const quotaResult = await quotaDetector.detectQuotaWarning(ideToCheck);
2048
+
2049
+ if (quotaResult && quotaResult.hasQuotaWarning) {
2050
+ quotaHandled = true;
2051
+ const quotaMessage = quotaResult.matchedText || 'Quota limit detected in IDE UI';
2052
+
2053
+ // Check if this might be Kiro quota warning even when ideType is amazon-q
2054
+ const isKiroPattern = quotaMessage.toLowerCase().includes('out of credits') ||
2055
+ quotaMessage.toLowerCase().includes('upgrade plan') ||
2056
+ quotaMessage.toLowerCase().includes('monthly usage limit');
2057
+
2058
+ // Mark the provider as rate limited
2059
+ try {
2060
+ if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
2061
+ sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
2062
+ }
2063
+ } catch (e) {
2064
+ // Ignore errors from marking rate-limited
2065
+ }
1814
2066
 
1815
- watcher.close();
1816
- console.log(chalk.yellow(`\n⚠️ Quota warning detected in ${ideToCheck} UI: ${quotaMessage.substring(0, 100)}...\n`));
1817
- resolve({ success: false, rateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
1818
- return;
2067
+ watcher.close();
2068
+ console.log(chalk.yellow(`\n⚠️ Quota warning detected via CDP in ${ideToCheck} UI: ${quotaMessage.substring(0, 100)}...\n`));
2069
+
2070
+ // If this looks like Kiro quota, set kiroRateLimited flag too
2071
+ const resolveObj = {
2072
+ success: false,
2073
+ rateLimited: true,
2074
+ providerRateLimited: ideType,
2075
+ matchedLine: quotaMessage
2076
+ };
2077
+
2078
+ if (ideType === 'amazon-q' && isKiroPattern) {
2079
+ resolveObj.kiroRateLimited = true;
2080
+ console.log(chalk.magenta(`💡 Detected potential Kiro quota warning despite amazon-q ideType`));
2081
+ }
2082
+
2083
+ resolve(resolveObj);
2084
+ return;
2085
+ }
2086
+ } catch (cdpError) {
2087
+ console.log(chalk.yellow(`⚠️ CDP quota detection failed for ${ideToCheck}: ${cdpError.message}`));
2088
+ }
1819
2089
  }
1820
2090
  } catch (quotaError) {
1821
- // Quota detection failed - this is non-fatal, continue waiting
1822
- // Only log errors every 30 seconds to avoid spam
1823
- if (Date.now() - lastCheckTime >= 30000) {
1824
- console.log(chalk.gray(` (Quota detection check failed: ${quotaError.message})`));
1825
- }
2091
+ console.log(chalk.red(`❌ [DEBUG] Quota detection check failed: ${quotaError.message}`));
2092
+ console.log(chalk.gray(` Stack: ${quotaError.stack}`));
1826
2093
  }
1827
2094
  }
1828
2095
 
@@ -1911,11 +2178,11 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
1911
2178
 
1912
2179
  // Wait for IDE agent to complete the work (IDE will update status to DONE itself)
1913
2180
  const ideCompletionStartTime = Date.now();
1914
- const completionResult = await waitForIdeCompletion(repoPath, requirement.text, providerConfig.ide || providerConfig.provider);
2181
+ const completionResult = await waitForIdeCompletion(repoPath, requirement.text, providerConfig.provider || providerConfig.ide);
1915
2182
  const ideResponseTime = Date.now() - ideCompletionStartTime;
1916
2183
 
1917
2184
  // Track IDE health metrics based on completion result
1918
- const ideType = providerConfig.ide || providerConfig.provider;
2185
+ const ideType = providerConfig.provider || providerConfig.ide;
1919
2186
  if (completionResult.success) {
1920
2187
  // Record success with response time
1921
2188
  await sharedHealthTracker.recordSuccess(ideType, ideResponseTime, {
@@ -1967,6 +2234,19 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
1967
2234
  return { success: false, error: 'AWS Kiro quota limit', shouldRetry: true };
1968
2235
  }
1969
2236
 
2237
+ // Special-case behavior for Cline IDE
2238
+ if (completionResult.clineRateLimited) {
2239
+ console.log(chalk.yellow(`⚠️ ${t('auto.direct.provider.quota.exhausted', { provider: 'Cline' })}\n`));
2240
+ providerManager.markRateLimited(providerConfig.provider, providerConfig.model, 'Quota limit reached');
2241
+
2242
+ const switchResult = await handleClineRateLimit();
2243
+ if (switchResult && switchResult.success) {
2244
+ return { success: false, error: `Cline switched to ${switchResult.nextProvider}, retrying.`, shouldRetry: true };
2245
+ }
2246
+
2247
+ return { success: false, error: 'Cline quota limit', shouldRetry: true };
2248
+ }
2249
+
1970
2250
  // Generic rate-limited behavior: if the completion detected a rate-limited message in REQUIREMENTS,
1971
2251
  // mark the provider as rate-limited and retry with the next available provider.
1972
2252
  if (completionResult.rateLimited) {
@@ -1999,8 +2279,6 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
1999
2279
  const moved = await moveRequirementToVerify(repoPath, requirement.text);
2000
2280
  if (moved) {
2001
2281
  console.log(chalk.green(`✓ ${t('auto.direct.status.verification.pending')}`));
2002
- } else {
2003
- console.log(chalk.yellow('⚠️ Requirement still pending verification in REQUIREMENTS file'));
2004
2282
  }
2005
2283
 
2006
2284
  console.log();
@@ -2010,6 +2288,214 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
2010
2288
  return { success: true, changes: [] };
2011
2289
  }
2012
2290
 
2291
+ /**
2292
+ * Run a spec iteration using an IDE provider (AppleScript).
2293
+ * Sends the full spec instruction to the IDE and polls tasks.md for
2294
+ * progress (any new checkbox ticked), instead of spawning vcm auto:start.
2295
+ *
2296
+ * @param {Object} spec - Spec object with .path, .directory, .hasTasks, .hasPlan, .hasPlanPrompt
2297
+ * @param {string} taskText - Text of the NEXT unchecked task (for display)
2298
+ * @param {string} taskLine - Full line from tasks.md (used if this is a continuation)
2299
+ * @param {Object} providerConfig - Provider config with .provider/.ide and .displayName
2300
+ * @returns {Promise<{success: boolean, error?: string, shouldRetry?: boolean}>}
2301
+ */
2302
+ async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2303
+ const ideType = providerConfig.provider || providerConfig.ide;
2304
+ const { done: doneBefore, total: totalBefore } = countSpecCheckboxes(spec.path);
2305
+ const pctBefore = totalBefore > 0 ? Math.round((doneBefore / totalBefore) * 100) : 0;
2306
+
2307
+ // Build the full spec instruction (mirrors buildSpecInstruction from Electron app)
2308
+ // This lets the agent work through multiple tasks autonomously rather than one at a time.
2309
+ let instruction;
2310
+ if (spec.hasTasks) {
2311
+ instruction = `Implement all remaining tasks in ${spec.path}/tasks.md. Current progress: ${doneBefore}/${totalBefore} tasks (${pctBefore}%) complete. Check off each task (change "[ ]" to "[x]") as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. When ALL tasks are checked off, report "COMPLETION: 100%".`;
2312
+ } else if (spec.hasPlan) {
2313
+ instruction = `Read ${spec.path}/plan.md, generate tasks.md for this spec, then implement all tasks. Check off each task as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. When ALL tasks are done, report "COMPLETION: 100%".`;
2314
+ } else {
2315
+ const planPromptNote = spec.hasPlanPrompt
2316
+ ? `Use plan prompt from ${spec.path}/plan-prompt.md to guide planning. ` : '';
2317
+ instruction = `Read ${spec.path}/spec.md. ${planPromptNote}Run speckit workflow: (1) read spec.md, (2) generate plan.md, (3) generate tasks.md, (4) implement all tasks. Check off each task as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. When ALL tasks are done, report "COMPLETION: 100%".`;
2318
+ }
2319
+
2320
+ // Send the spec instruction to the IDE via AppleScript.
2321
+ // For Windsurf: use a single combined synchronous AppleScript that activates Windsurf,
2322
+ // opens a fresh Cascade conversation, pastes the instruction, and submits — all in one
2323
+ // atomic execSync call. This eliminates the timing race where a detached AppleScript
2324
+ // fires 3 seconds later after VS Code has regained focus.
2325
+ console.log(chalk.cyan(`📤 Sending spec instruction to ${providerConfig.displayName}...\n`));
2326
+
2327
+ if (ideType === 'windsurf') {
2328
+ try {
2329
+ const { execSync } = require('child_process');
2330
+ const { writeFileSync, unlinkSync: _unlinkSync } = require('fs');
2331
+ const { tmpdir } = require('os');
2332
+
2333
+ // Write instruction to a temp file so we can read it from AppleScript
2334
+ // without any escaping issues with quotes or special characters.
2335
+ const ts = Date.now();
2336
+ const tmpTextFile = path.join(tmpdir(), `spec_instr_${ts}.txt`);
2337
+ const tmpScpt = path.join(tmpdir(), `send_cascade_${ts}.scpt`);
2338
+
2339
+ writeFileSync(tmpTextFile, instruction, 'utf8');
2340
+
2341
+ // Single combined script: activate Windsurf → verify it is frontmost →
2342
+ // focus Cascade → paste → submit.
2343
+ // Both Windsurf AND VS Code report process name "Electron" to System Events,
2344
+ // so we MUST use bundle identifier "com.exafunction.windsurf" to distinguish them.
2345
+ const combinedScript = `
2346
+ set instructionText to (do shell script "cat " & quoted form of "${tmpTextFile}")
2347
+
2348
+ -- Step 1: bring Windsurf to front
2349
+ tell application "Windsurf"
2350
+ activate
2351
+ delay 1.5
2352
+ end tell
2353
+
2354
+ -- Step 2: verify Windsurf is the frontmost app by BUNDLE ID (not name — both
2355
+ -- Windsurf and VS Code report process name "Electron" to System Events).
2356
+ set maxTries to 3
2357
+ set tries to 0
2358
+ repeat
2359
+ set tries to tries + 1
2360
+ tell application "System Events"
2361
+ set frontBundleID to bundle identifier of first process whose frontmost is true
2362
+ end tell
2363
+ if frontBundleID is "com.exafunction.windsurf" then exit repeat
2364
+ if tries >= maxTries then
2365
+ error "Windsurf did not become frontmost (frontmost bundle ID: " & frontBundleID & ")"
2366
+ end if
2367
+ tell application "Windsurf"
2368
+ activate
2369
+ end tell
2370
+ delay 1.0
2371
+ end repeat
2372
+
2373
+ -- Step 3: send keystrokes to Windsurf using bundle ID (required to distinguish
2374
+ -- Windsurf from VS Code since both run as "Electron" processes)
2375
+ tell application "System Events"
2376
+ set windsurfProc to first process whose bundle identifier is "com.exafunction.windsurf"
2377
+ tell windsurfProc
2378
+ set frontmost to true
2379
+ delay 0.5
2380
+ -- ESC: defocus any editor/terminal input that might intercept keystrokes
2381
+ key code 53
2382
+ delay 0.5
2383
+ -- Cmd+Shift+L: focus Cascade chat input (proven approach from sendText())
2384
+ keystroke "l" using {command down, shift down}
2385
+ delay 2.0
2386
+ -- Clear any auto-inserted context text (e.g. @terminal:zsh)
2387
+ keystroke "a" using {command down}
2388
+ delay 0.3
2389
+ key code 51
2390
+ delay 0.3
2391
+ -- Paste the spec instruction
2392
+ set the clipboard to instructionText
2393
+ keystroke "v" using {command down}
2394
+ delay 0.5
2395
+ -- Submit
2396
+ key code 36
2397
+ delay 1.0
2398
+ end tell
2399
+ end tell
2400
+ `;
2401
+
2402
+ writeFileSync(tmpScpt, combinedScript, 'utf8');
2403
+ try {
2404
+ execSync(`osascript "${tmpScpt}"`, { stdio: 'pipe', timeout: 30000 });
2405
+ } finally {
2406
+ try { _unlinkSync(tmpScpt); } catch (_) {}
2407
+ try { _unlinkSync(tmpTextFile); } catch (_) {}
2408
+ }
2409
+ } catch (err) {
2410
+ console.log(chalk.red(`✗ AppleScript error: ${err.message}`));
2411
+ return { success: false, error: err.message };
2412
+ }
2413
+ } else {
2414
+ // Non-Windsurf IDEs: use AppleScriptManager
2415
+ try {
2416
+ const appleScriptManager = new AppleScriptManager();
2417
+ let sendResult;
2418
+ // Prefer sendTextWithThreadClosure when available (.js build), fall back to sendText (.cjs build)
2419
+ if (typeof appleScriptManager.sendTextWithThreadClosure === 'function') {
2420
+ sendResult = await appleScriptManager.sendTextWithThreadClosure(instruction, ideType);
2421
+ } else {
2422
+ sendResult = await appleScriptManager.sendText(instruction, ideType);
2423
+ }
2424
+ if (!sendResult || !sendResult.success) {
2425
+ const errorMsg = (sendResult && sendResult.error) || `Failed to send text to ${providerConfig.displayName}`;
2426
+ console.log(chalk.red(`✗ ${errorMsg}`));
2427
+ return { success: false, error: errorMsg };
2428
+ }
2429
+ } catch (err) {
2430
+ console.log(chalk.red(`✗ AppleScript error: ${err.message}`));
2431
+ return { success: false, error: err.message };
2432
+ }
2433
+ }
2434
+
2435
+ console.log(chalk.green(`✓ Spec instruction sent to ${providerConfig.displayName}`));
2436
+ console.log(chalk.gray(`⏳ Polling tasks.md every 30s for checkbox progress...\n`));
2437
+
2438
+ const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds
2439
+ const CONTINUE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
2440
+ const TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
2441
+
2442
+ const startTime = Date.now();
2443
+ let lastContinueSent = Date.now();
2444
+
2445
+ return new Promise((resolve) => {
2446
+ let interval = null;
2447
+
2448
+ const cleanup = (result) => {
2449
+ if (interval) { clearInterval(interval); interval = null; }
2450
+ process.off('SIGINT', onSigint);
2451
+ resolve(result);
2452
+ };
2453
+
2454
+ // Handle Ctrl+C gracefully: stop polling and exit cleanly
2455
+ const onSigint = () => {
2456
+ console.log(chalk.yellow('\n⚠️ Interrupted — stopping spec task polling\n'));
2457
+ cleanup({ success: false, error: 'interrupted' });
2458
+ // Re-emit to trigger normal process exit after cleanup
2459
+ process.exit(0);
2460
+ };
2461
+ process.once('SIGINT', onSigint);
2462
+
2463
+ interval = setInterval(async () => {
2464
+ const elapsed = Date.now() - startTime;
2465
+
2466
+ if (elapsed >= TIMEOUT_MS) {
2467
+ cleanup({ success: false, error: 'timeout', shouldRetry: true });
2468
+ console.log(chalk.yellow(`⏰ Timeout (30min) — ${providerConfig.displayName} did not check off any tasks\n`));
2469
+ return;
2470
+ }
2471
+
2472
+ // Detect any new checkbox ticked since we started
2473
+ const { done: doneNow, total: totalNow } = countSpecCheckboxes(spec.path);
2474
+ if (doneNow > doneBefore) {
2475
+ const pctNow = totalNow > 0 ? Math.round((doneNow / totalNow) * 100) : 0;
2476
+ console.log(chalk.green(`✓ Progress detected: ${doneNow}/${totalNow} tasks (${pctNow}%) complete\n`));
2477
+ cleanup({ success: true, changes: [] });
2478
+ return;
2479
+ }
2480
+
2481
+ // Send continuation prompt every 5 minutes if no progress
2482
+ const sinceLastContinue = Date.now() - lastContinueSent;
2483
+ if (sinceLastContinue >= CONTINUE_INTERVAL_MS) {
2484
+ lastContinueSent = Date.now();
2485
+ const pctNow = totalNow > 0 ? Math.round((doneNow / totalNow) * 100) : 0;
2486
+ const mins = Math.round(elapsed / 60000);
2487
+ const continueMsg = `Continue implementing spec "${spec.directory}". Current progress: ${doneNow}/${totalNow} tasks (${pctNow}%) complete. Keep working until ALL tasks in tasks.md are checked off. Next task: "${taskText}". (${mins}min elapsed)`;
2488
+ try {
2489
+ const appleScriptManager = new AppleScriptManager();
2490
+ // Use plain sendText for continuations (no need to open new thread)
2491
+ await appleScriptManager.sendText(continueMsg, ideType);
2492
+ console.log(chalk.gray(`📤 Sent continuation prompt to ${providerConfig.displayName} (${mins}min elapsed)\n`));
2493
+ } catch (_) { /* ignore continuation errors */ }
2494
+ }
2495
+ }, POLL_INTERVAL_MS);
2496
+ });
2497
+ }
2498
+
2013
2499
  /**
2014
2500
  * Run one iteration of autonomous mode with full workflow
2015
2501
  */
@@ -2412,6 +2898,64 @@ Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
2412
2898
  return { success: true, changes };
2413
2899
  }
2414
2900
 
2901
+ // ─── Spec mode helpers ────────────────────────────────────────────────────────
2902
+
2903
+ /**
2904
+ * Count done/total checkboxes in a spec's tasks.md.
2905
+ */
2906
+ function countSpecCheckboxes(specPath) {
2907
+ try {
2908
+ const tasksFile = path.join(specPath, 'tasks.md');
2909
+ if (!fs.existsSync(tasksFile)) return { done: 0, total: 0 };
2910
+ const content = fs.readFileSync(tasksFile, 'utf8');
2911
+ const totalMatches = content.match(/^- \[[ x]\]/gmi) || [];
2912
+ const doneMatches = content.match(/^- \[x\]/gmi) || [];
2913
+ return { done: doneMatches.length, total: totalMatches.length };
2914
+ } catch (_) {
2915
+ return { done: 0, total: 0 };
2916
+ }
2917
+ }
2918
+
2919
+ /**
2920
+ * Get the next unchecked task line from tasks.md.
2921
+ * Returns { text, line } or null when all done.
2922
+ */
2923
+ function getNextSpecTask(specPath) {
2924
+ try {
2925
+ const tasksFile = path.join(specPath, 'tasks.md');
2926
+ if (!fs.existsSync(tasksFile)) return null;
2927
+ const content = fs.readFileSync(tasksFile, 'utf8');
2928
+ for (const line of content.split('\n')) {
2929
+ if (/^- \[ \]/.test(line)) {
2930
+ return { text: line.replace(/^- \[ \]\s*/, '').trim(), line: line.trim() };
2931
+ }
2932
+ }
2933
+ return null;
2934
+ } catch (_) {
2935
+ return null;
2936
+ }
2937
+ }
2938
+
2939
+ /**
2940
+ * Load all enabled specs that have incomplete tasks (or no tasks.md yet).
2941
+ */
2942
+ async function loadEnabledIncompleteSpecs(repoPath) {
2943
+ try {
2944
+ const { getAllSpecifications } = require('vibecodingmachine-core');
2945
+ const specs = await getAllSpecifications(repoPath, { skipDisabled: true });
2946
+ return specs.filter(spec => {
2947
+ if (!spec.hasTasks) return true; // No tasks.md = needs planning + implementation
2948
+ const counts = countSpecCheckboxes(spec.path);
2949
+ return counts.total === 0 || counts.done < counts.total;
2950
+ });
2951
+ } catch (err) {
2952
+ console.log(chalk.yellow(`⚠️ Error loading specs: ${err.message}`));
2953
+ return [];
2954
+ }
2955
+ }
2956
+
2957
+ // ─────────────────────────────────────────────────────────────────────────────
2958
+
2415
2959
  /**
2416
2960
  * Main auto mode command handler
2417
2961
  */
@@ -2435,7 +2979,7 @@ async function handleAutoStart(options) {
2435
2979
  console.log(chalk.gray('═'.repeat(80)));
2436
2980
  console.log();
2437
2981
 
2438
- const repoPath = await getRepoPath();
2982
+ const repoPath = await getEffectiveRepoPath();
2439
2983
  if (!repoPath) {
2440
2984
  console.log(chalk.red('✗ No repository configured'));
2441
2985
  console.log(chalk.gray(t('auto.direct.config.repo.not.set')));
@@ -2462,8 +3006,8 @@ async function handleAutoStart(options) {
2462
3006
  // No need to call getEffectiveAgent since we already have the correct agent
2463
3007
  const effectiveAgent = options.ide;
2464
3008
 
2465
- // Get provider configuration for the selected agent
2466
- let providerConfig = await acquireProviderConfig(effectiveAgent);
3009
+ // Get provider configuration options.provider forces a specific provider
3010
+ let providerConfig = await acquireProviderConfig(null, null, options.provider || null);
2467
3011
  if (!providerConfig) {
2468
3012
  return;
2469
3013
  }
@@ -2484,9 +3028,246 @@ async function handleAutoStart(options) {
2484
3028
  const initialTodoCount = await countTodoRequirements(repoPath);
2485
3029
  const initialEffectiveMax = unlimited ? initialTodoCount : Math.min(maxChats, initialTodoCount);
2486
3030
 
2487
- // Main loop
3031
+ // Main loop counters (shared across spec + requirements phases)
2488
3032
  let completedCount = 0;
2489
3033
  let failedCount = 0;
3034
+
3035
+ // ── Phase 1: Process incomplete enabled specs ─────────────────────────────
3036
+ const incompleteSpecs = await loadEnabledIncompleteSpecs(repoPath);
3037
+ if (incompleteSpecs.length > 0) {
3038
+ console.log(chalk.bold.cyan(`\n📋 Processing ${incompleteSpecs.length} incomplete spec(s) before requirements...\n`));
3039
+
3040
+ for (const spec of incompleteSpecs) {
3041
+ const { done: doneStart, total: totalStart } = countSpecCheckboxes(spec.path);
3042
+ console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
3043
+ console.log(chalk.bold.magenta(` 📋 SPEC: ${spec.directory} (${spec.title || spec.directory})`));
3044
+ if (totalStart > 0) {
3045
+ const pctStart = Math.round((doneStart / totalStart) * 100);
3046
+ console.log(chalk.bold.magenta(` Progress: ${doneStart}/${totalStart} tasks (${pctStart}%) complete`));
3047
+ } else {
3048
+ console.log(chalk.bold.magenta(' No tasks.md yet — will plan and implement'));
3049
+ }
3050
+ console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
3051
+
3052
+ let specProviderAttempts = 0;
3053
+ const MAX_SPEC_TASK_ATTEMPTS = 3;
3054
+ let lastSpecTaskText = null;
3055
+ let sameTaskAttempts = 0;
3056
+
3057
+ // If spec has no tasks.md yet, add a special planning task
3058
+ if (!spec.hasTasks) {
3059
+ console.log(chalk.cyan('📝 No tasks.md yet — planning spec first...\n'));
3060
+ const planningText = [
3061
+ `Plan and create tasks.md for spec "${spec.directory}".`,
3062
+ `Read ${spec.path}/spec.md${spec.hasPlanPrompt ? ` and ${spec.path}/plan-prompt.md` : ''}.`,
3063
+ `Then create ${spec.path}/tasks.md with implementation tasks as checkboxes (- [ ] task).`
3064
+ ].join('\n');
3065
+
3066
+ let planResult;
3067
+ if (providerConfig.type === 'ide') {
3068
+ // Send planning instruction via AppleScript, poll for tasks.md creation
3069
+ console.log(chalk.cyan(`📤 Sending planning task to ${providerConfig.displayName}...\n`));
3070
+ const ideTypeForPlan = providerConfig.provider || providerConfig.ide;
3071
+ // For Windsurf: single combined synchronous AppleScript to avoid timing race
3072
+ const sendPlanToIde = async () => {
3073
+ if (ideTypeForPlan === 'windsurf') {
3074
+ const { execSync: _execSync2 } = require('child_process');
3075
+ const { writeFileSync: _writeFileSync2, unlinkSync: _unlinkSync2 } = require('fs');
3076
+ const { tmpdir: _tmpdir2 } = require('os');
3077
+ const _ts2 = Date.now();
3078
+ const _tmpText2 = path.join(_tmpdir2(), `plan_instr_${_ts2}.txt`);
3079
+ const _tmpScpt2 = path.join(_tmpdir2(), `send_cascade_plan_${_ts2}.scpt`);
3080
+ _writeFileSync2(_tmpText2, planningText, 'utf8');
3081
+ const _combinedPlanScript = `
3082
+ set planText to (do shell script "cat " & quoted form of "${_tmpText2}")
3083
+ tell application "Windsurf"
3084
+ activate
3085
+ delay 1.5
3086
+ end tell
3087
+ set maxTries to 3
3088
+ set tries to 0
3089
+ repeat
3090
+ set tries to tries + 1
3091
+ tell application "System Events"
3092
+ set frontBundleID to bundle identifier of first process whose frontmost is true
3093
+ end tell
3094
+ if frontBundleID is "com.exafunction.windsurf" then exit repeat
3095
+ if tries >= maxTries then
3096
+ error "Windsurf did not become frontmost (frontmost bundle ID: " & frontBundleID & ")"
3097
+ end if
3098
+ tell application "Windsurf"
3099
+ activate
3100
+ end tell
3101
+ delay 1.0
3102
+ end repeat
3103
+ tell application "System Events"
3104
+ set windsurfProc to first process whose bundle identifier is "com.exafunction.windsurf"
3105
+ tell windsurfProc
3106
+ set frontmost to true
3107
+ delay 0.5
3108
+ key code 53
3109
+ delay 0.5
3110
+ keystroke "l" using {command down, shift down}
3111
+ delay 2.0
3112
+ keystroke "a" using {command down}
3113
+ delay 0.3
3114
+ key code 51
3115
+ delay 0.3
3116
+ set the clipboard to planText
3117
+ keystroke "v" using {command down}
3118
+ delay 0.5
3119
+ key code 36
3120
+ delay 1.0
3121
+ end tell
3122
+ end tell
3123
+ `;
3124
+ _writeFileSync2(_tmpScpt2, _combinedPlanScript, 'utf8');
3125
+ try {
3126
+ _execSync2(`osascript "${_tmpScpt2}"`, { stdio: 'pipe', timeout: 30000 });
3127
+ return { success: true };
3128
+ } finally {
3129
+ try { _unlinkSync2(_tmpScpt2); } catch (_) {}
3130
+ try { _unlinkSync2(_tmpText2); } catch (_) {}
3131
+ }
3132
+ } else {
3133
+ const appleScriptManager = new AppleScriptManager();
3134
+ const sendResult = typeof appleScriptManager.sendTextWithThreadClosure === 'function'
3135
+ ? await appleScriptManager.sendTextWithThreadClosure(planningText, ideTypeForPlan)
3136
+ : await appleScriptManager.sendText(planningText, ideTypeForPlan);
3137
+ if (!sendResult || !sendResult.success) {
3138
+ throw new Error((sendResult && sendResult.error) || 'send failed');
3139
+ }
3140
+ return { success: true };
3141
+ }
3142
+ };
3143
+ try {
3144
+ const sendPlanResult = await sendPlanToIde();
3145
+ if (!sendPlanResult || !sendPlanResult.success) {
3146
+ console.log(chalk.red(`✗ Failed to send to ${providerConfig.displayName}`));
3147
+ planResult = { success: false, error: 'send failed' };
3148
+ } else {
3149
+ console.log(chalk.green(`✓ Planning task sent. Waiting for tasks.md to be created...`));
3150
+ // Poll for tasks.md existence
3151
+ const tasksFilePath = path.join(spec.path, 'tasks.md');
3152
+ const POLL_MS = 30 * 1000;
3153
+ const TIMEOUT_MS = 30 * 60 * 1000;
3154
+ const planStart = Date.now();
3155
+ planResult = await new Promise((resolve) => {
3156
+ const iv = setInterval(() => {
3157
+ if (fs.existsSync(tasksFilePath)) {
3158
+ clearInterval(iv);
3159
+ console.log(chalk.green('✓ tasks.md created!\n'));
3160
+ resolve({ success: true });
3161
+ } else if (Date.now() - planStart >= TIMEOUT_MS) {
3162
+ clearInterval(iv);
3163
+ console.log(chalk.yellow('⏰ Timeout waiting for tasks.md\n'));
3164
+ resolve({ success: false, error: 'timeout' });
3165
+ }
3166
+ }, POLL_MS);
3167
+ });
3168
+ }
3169
+ } catch (err) {
3170
+ planResult = { success: false, error: err.message };
3171
+ }
3172
+ } else {
3173
+ const planRequirement = { text: planningText, package: null, disabled: false };
3174
+ planResult = await runIteration(planRequirement, providerConfig, repoPath);
3175
+ }
3176
+
3177
+ if (planResult.success) {
3178
+ completedCount++;
3179
+ console.log(chalk.green('✓ tasks.md created — proceeding with implementation\n'));
3180
+ // Re-check if spec now has tasks
3181
+ spec.hasTasks = fs.existsSync(path.join(spec.path, 'tasks.md'));
3182
+ } else {
3183
+ console.log(chalk.red('✗ Failed to create tasks.md — skipping spec\n'));
3184
+ failedCount++;
3185
+ continue; // Skip to next spec
3186
+ }
3187
+ }
3188
+
3189
+ // Loop until spec is done or stalled
3190
+ while (true) {
3191
+ const task = getNextSpecTask(spec.path);
3192
+ if (!task) {
3193
+ // All tasks checked off
3194
+ const { done: doneFinal, total: totalFinal } = countSpecCheckboxes(spec.path);
3195
+ console.log(chalk.bold.green(`\n✅ Spec "${spec.directory}" complete! (${doneFinal}/${totalFinal} tasks)\n`));
3196
+ break;
3197
+ }
3198
+
3199
+ // Detect same task repeated (LLM not checking it off)
3200
+ if (task.text === lastSpecTaskText) {
3201
+ sameTaskAttempts++;
3202
+ if (sameTaskAttempts >= MAX_SPEC_TASK_ATTEMPTS) {
3203
+ console.log(chalk.red(`\n✗ Task "${task.text}" not completing after ${MAX_SPEC_TASK_ATTEMPTS} attempts — skipping spec\n`));
3204
+ break;
3205
+ }
3206
+ } else {
3207
+ sameTaskAttempts = 0;
3208
+ lastSpecTaskText = task.text;
3209
+ }
3210
+
3211
+ const { done: doneCurrent, total: totalCurrent } = countSpecCheckboxes(spec.path);
3212
+ const pctCurrent = totalCurrent > 0 ? Math.round((doneCurrent / totalCurrent) * 100) : 0;
3213
+
3214
+ console.log(chalk.cyan(`\n📌 Task ${doneCurrent + 1}/${totalCurrent || '?'}: ${task.text}`));
3215
+ console.log(chalk.gray(` Spec progress: ${doneCurrent}/${totalCurrent} (${pctCurrent}%)\n`));
3216
+
3217
+ // Route spec tasks differently depending on provider type.
3218
+ // IDE providers (Windsurf, Cursor, etc.) must NOT go through runIdeFallbackIteration
3219
+ // because that spawns "vcm auto:start" which is designed for requirements mode only.
3220
+ // Instead, use runSpecIdeIteration which sends text directly via AppleScript
3221
+ // and polls tasks.md for checkbox completion.
3222
+ let result;
3223
+ if (providerConfig.type === 'ide') {
3224
+ result = await runSpecIdeIteration(spec, task.text, task.line, providerConfig);
3225
+ } else {
3226
+ // Direct LLM providers: wrap task in a requirement object and use standard iteration
3227
+ const specTaskText = [
3228
+ task.text,
3229
+ `[Spec: ${spec.directory} — task ${doneCurrent + 1}/${totalCurrent || '?'}]`,
3230
+ `[After implementing, mark the task done in ${spec.path}/tasks.md:`,
3231
+ ` change: ${task.line}`,
3232
+ ` to: - [x] ${task.text}]`
3233
+ ].join('\n');
3234
+ const specRequirement = { text: specTaskText, package: null, disabled: false };
3235
+ result = await runIteration(specRequirement, providerConfig, repoPath);
3236
+ }
3237
+
3238
+ if (result.success) {
3239
+ specProviderAttempts = 0;
3240
+ completedCount++;
3241
+ const { done, total } = countSpecCheckboxes(spec.path);
3242
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
3243
+ console.log(chalk.bold.green(`📊 Spec progress: ${done}/${total} tasks (${pct}%) complete`));
3244
+ } else {
3245
+ const isRateLimitError = isRateLimitMessage(result.error);
3246
+ const errorType = isRateLimitError ? 'Rate limit' : 'Error';
3247
+
3248
+ specProviderAttempts++;
3249
+ failedCount++;
3250
+
3251
+ if (specProviderAttempts > MAX_SPEC_TASK_ATTEMPTS) {
3252
+ console.log(chalk.red(`\n✗ Max provider attempts reached — moving to next spec\n`));
3253
+ break;
3254
+ }
3255
+
3256
+ console.log(chalk.yellow(`⚠️ ${errorType} on spec task, switching provider...`));
3257
+ const newProviderConfig = await acquireProviderConfig(providerConfig.provider, providerConfig.model);
3258
+ if (newProviderConfig) {
3259
+ providerConfig = newProviderConfig;
3260
+ console.log(chalk.green(`✓ Switched to: ${providerConfig.displayName}\n`));
3261
+ }
3262
+ }
3263
+ }
3264
+ }
3265
+
3266
+ console.log(chalk.bold.cyan('\n📋 All specs processed. Continuing to requirements...\n'));
3267
+ console.log(chalk.gray('═'.repeat(80)));
3268
+ }
3269
+
3270
+ // ── Phase 2: Requirements mode ─────────────────────────────────────────────
2490
3271
  let providerAttempts = 0; // Track attempts for current requirement
2491
3272
  let lastRequirementText = null; // Track which requirement we're on
2492
3273
  const MAX_PROVIDER_ATTEMPTS = 3; // Maximum times to try different providers for same requirement
@@ -2495,7 +3276,11 @@ async function handleAutoStart(options) {
2495
3276
  // Get current requirement first to check if there are any TODO items
2496
3277
  const requirement = await getCurrentRequirement(repoPath);
2497
3278
  if (!requirement) {
2498
- console.log(chalk.bold.yellow('\n🎉 All requirements completed!'));
3279
+ if (completedCount > 0 || failedCount > 0) {
3280
+ console.log(chalk.bold.yellow('\n🎉 All requirements completed!'));
3281
+ } else {
3282
+ console.log(chalk.bold.yellow('\n🎉 No requirements to process.'));
3283
+ }
2499
3284
  console.log(chalk.gray(`${t('auto.direct.no.more.todo.items')}\n`));
2500
3285
  break;
2501
3286
  }
@@ -2653,5 +3438,5 @@ async function handleAutoStart(options) {
2653
3438
  }
2654
3439
  }
2655
3440
 
2656
- module.exports = { handleAutoStart, waitForIdeCompletion, acquireProviderConfig };
3441
+ module.exports = { handleAutoStart, waitForIdeCompletion, acquireProviderConfig, getCurrentRequirement };
2657
3442