vibecodingmachine-cli 2026.1.3-2209 → 2026.1.23-1010

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 (40) hide show
  1. package/__tests__/antigravity-js-handler.test.js +23 -0
  2. package/__tests__/provider-manager.test.js +84 -0
  3. package/__tests__/provider-rate-cache.test.js +27 -0
  4. package/bin/vibecodingmachine.js +8 -0
  5. package/package.json +2 -2
  6. package/reset_provider_order.js +21 -0
  7. package/scripts/convert-requirements.js +35 -0
  8. package/scripts/debug-parse.js +24 -0
  9. package/src/commands/auto-direct.js +679 -120
  10. package/src/commands/auto.js +200 -45
  11. package/src/commands/ide.js +108 -3
  12. package/src/commands/requirements-remote.js +10 -1
  13. package/src/commands/status.js +39 -1
  14. package/src/utils/antigravity-js-handler.js +13 -4
  15. package/src/utils/auth.js +37 -13
  16. package/src/utils/compliance-check.js +10 -0
  17. package/src/utils/config.js +29 -1
  18. package/src/utils/date-formatter.js +44 -0
  19. package/src/utils/interactive.js +1006 -537
  20. package/src/utils/kiro-js-handler.js +188 -0
  21. package/src/utils/provider-rate-cache.js +31 -0
  22. package/src/utils/provider-registry.js +42 -1
  23. package/src/utils/requirements-converter.js +107 -0
  24. package/src/utils/requirements-parser.js +144 -0
  25. package/tests/antigravity-js-handler.test.js +23 -0
  26. package/tests/integration/health-tracking.integration.test.js +284 -0
  27. package/tests/provider-manager.test.js +92 -0
  28. package/tests/rate-limit-display.test.js +44 -0
  29. package/tests/requirements-bullet-parsing.test.js +15 -0
  30. package/tests/requirements-converter.test.js +42 -0
  31. package/tests/requirements-heading-count.test.js +27 -0
  32. package/tests/requirements-legacy-parsing.test.js +15 -0
  33. package/tests/requirements-parse-integration.test.js +44 -0
  34. package/tests/wait-for-ide-completion.test.js +56 -0
  35. package/tests/wait-for-ide-quota-detection-cursor-screenshot.test.js +61 -0
  36. package/tests/wait-for-ide-quota-detection-cursor.test.js +60 -0
  37. package/tests/wait-for-ide-quota-detection-negative.test.js +45 -0
  38. package/tests/wait-for-ide-quota-detection.test.js +59 -0
  39. package/verify_fix.js +36 -0
  40. package/verify_ui.js +38 -0
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  const chalk = require('chalk');
8
- const { DirectLLMManager, AppleScriptManager, t, detectLocale, setLocale } = require('vibecodingmachine-core');
8
+ const { DirectLLMManager, AppleScriptManager, QuotaDetector, IDEHealthTracker, t, detectLocale, setLocale } = require('vibecodingmachine-core');
9
9
 
10
10
  // Initialize locale detection for auto mode
11
11
  const detectedLocale = detectLocale();
@@ -17,11 +17,13 @@ const path = require('path');
17
17
  const { spawn } = require('child_process');
18
18
  const chokidar = require('chokidar');
19
19
  const stringWidth = require('string-width');
20
- const { getProviderPreferences, getProviderDefinition } = require('../utils/provider-registry');
20
+ const { getProviderPreferences, getProviderDefinition, saveProviderPreferences } = require('../utils/provider-registry');
21
21
  const { createKeyboardHandler } = require('../utils/keyboard-handler');
22
22
  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
+ const { checkKiroRateLimit, handleKiroRateLimit } = require('../utils/kiro-js-handler');
26
+ const { startAutoMode, stopAutoMode, updateAutoModeStatus } = require('../utils/auto-mode');
25
27
 
26
28
  // Status management will use in-process tracking instead of external file
27
29
  const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
@@ -29,6 +31,16 @@ const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
29
31
  // CRITICAL: Shared ProviderManager instance to track rate limits across all function calls
30
32
  const sharedProviderManager = new ProviderManager();
31
33
 
34
+ // CRITICAL: Shared IDEHealthTracker instance to track IDE reliability across all iterations
35
+ const sharedHealthTracker = new IDEHealthTracker();
36
+
37
+ // Listen for consecutive failures to warn user
38
+ sharedHealthTracker.on('consecutive-failures', ({ ideId, count, lastError }) => {
39
+ console.log(chalk.yellow(`\n⚠️ WARNING: ${ideId} has failed ${count} times consecutively`));
40
+ console.log(chalk.yellow(` Last error: ${lastError}`));
41
+ console.log(chalk.yellow(` Consider switching to a different IDE\n`));
42
+ });
43
+
32
44
  // Configured stages (will be loaded in handleAutoStart)
33
45
  let configuredStages = DEFAULT_STAGES;
34
46
 
@@ -45,6 +57,22 @@ function getTimestamp() {
45
57
  }) + ' MST';
46
58
  }
47
59
 
60
+ /**
61
+ * Get a human-friendly timestamp for log prefixes that includes date, time, and timezone
62
+ * Example: "2025-01-02 3:45 PM MST"
63
+ */
64
+ function getLogTimestamp(date = new Date()) {
65
+ const datePart = date.toISOString().split('T')[0]; // YYYY-MM-DD
66
+ const timePart = date.toLocaleTimeString('en-US', {
67
+ hour: 'numeric',
68
+ minute: '2-digit',
69
+ hour12: true,
70
+ timeZone: 'America/Denver',
71
+ timeZoneName: 'short'
72
+ });
73
+ return `${datePart} ${timePart}`;
74
+ }
75
+
48
76
  /**
49
77
  * Translate workflow stage names
50
78
  */
@@ -378,7 +406,7 @@ async function moveRequirementToVerify(repoPath, requirementText) {
378
406
 
379
407
  const content = await fs.readFile(reqPath, 'utf8');
380
408
  const lines = content.split('\n');
381
-
409
+
382
410
  // Get computer filter from config
383
411
  const { getComputerFilter } = require('../utils/config');
384
412
  const computerFilter = await getComputerFilter();
@@ -401,95 +429,94 @@ async function moveRequirementToVerify(repoPath, requirementText) {
401
429
  continue;
402
430
  }
403
431
 
404
- // Check if we're leaving TODO section
405
- if (inTodoSection && trimmed.startsWith('##') && !trimmed.startsWith('###') && !trimmed.includes('Requirements not yet completed')) {
406
- inTodoSection = false;
407
- }
432
+ // Check if we're leaving TODO section
433
+ if (inTodoSection && trimmed.startsWith('##') && !trimmed.startsWith('###') && !trimmed.includes('Requirements not yet completed')) {
434
+ inTodoSection = false;
435
+ }
408
436
 
409
- // Only look for requirements in TODO section
410
- if (inTodoSection && trimmed.startsWith('###')) {
411
- const title = trimmed.replace(/^###\s*/, '').trim();
412
- if (title) {
413
- // Try multiple matching strategies
414
- const normalizedTitle = title.trim();
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();
415
443
 
416
- // Exact match
417
- if (normalizedTitle === normalizedRequirement) {
418
- requirementStartIndex = i;
419
- }
420
- // Check if either contains the other (for partial matches)
421
- else if (normalizedTitle.includes(normalizedRequirement) || normalizedRequirement.includes(normalizedTitle)) {
422
- requirementStartIndex = i;
423
- }
424
- // Check snippet matches
425
- else if (normalizedTitle.includes(snippet) || snippet.includes(normalizedTitle.substring(0, 80))) {
426
- requirementStartIndex = i;
427
- }
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
+ }
428
456
 
429
- if (requirementStartIndex !== -1) {
430
- // Find the end of this requirement (next ### or ## header)
431
- for (let j = i + 1; j < lines.length; j++) {
432
- const nextLine = lines[j].trim();
433
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
434
- requirementEndIndex = j;
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
+ }
435
469
  break;
436
470
  }
437
471
  }
438
- if (requirementEndIndex === -1) {
439
- requirementEndIndex = lines.length;
440
- }
441
- break;
442
472
  }
443
473
  }
444
- }
445
- }
446
474
 
447
- if (requirementStartIndex === -1) {
448
- console.log(chalk.yellow(`⚠️ ${t('auto.direct.requirement.not.found.todo', { requirement: requirementText.substring(0, 60) + '...' })}`));
449
- return false;
450
- }
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
+ }
451
479
 
452
- // Extract the entire requirement block
453
- const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
480
+ // Extract the entire requirement block
481
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
454
482
 
455
- // Remove the requirement from its current location
456
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
483
+ // Remove the requirement from its current location
484
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
457
485
 
458
- // Check if there are any more requirements in TODO section after removal
459
- let hasMoreTodoRequirements = false;
460
- inTodoSection = false; // Reset the existing variable
461
- for (let i = 0; i < lines.length; i++) {
462
- const line = lines[i].trim();
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();
463
491
 
464
- // Check if we're entering TODO section
465
- if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
466
- inTodoSection = true;
467
- continue;
468
- }
492
+ // Check if we're entering TODO section
493
+ if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
494
+ inTodoSection = true;
495
+ continue;
496
+ }
469
497
 
470
- // Check if we're leaving TODO section
471
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
472
- break;
473
- }
498
+ // Check if we're leaving TODO section
499
+ if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
500
+ break;
501
+ }
474
502
 
475
- // Check if we found a requirement in TODO section
476
- if (inTodoSection && line.startsWith('###')) {
477
- const title = line.replace(/^###\s*/, '').trim();
478
- if (title && title.includes(computerFilter)) {
479
- hasMoreTodoRequirements = true;
480
- break;
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
+ }
481
511
  }
482
- }
483
- }
484
-
485
- // If no more TODO requirements, log message
486
- if (!hasMoreTodoRequirements) {
487
- console.log(chalk.green(`🎉 ${t('auto.direct.requirement.no.more.todo')}`));
488
- // Add a new requirement to the TODO section
489
- const newRequirement = '### R14: TESTREQ1';
490
- lines.push(newRequirement);
491
- }
492
512
 
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
+ }
493
520
  // Find or create TO VERIFY BY HUMAN section with conflict resolution
494
521
  // IMPORTANT: Do NOT match VERIFIED sections - only match TO VERIFY sections
495
522
  const verifySectionVariants = [
@@ -499,7 +526,8 @@ const verifySectionVariants = [
499
526
  '## ✅ TO VERIFY',
500
527
  '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG',
501
528
  '## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT',
502
- '## 🖥️ COMPUTER FOCUS AREA MANAGEMENT'
529
+ '## 🖥️ COMPUTER FOCUS AREA MANAGEMENT',
530
+ '## 🔍 spec-kit: TO VERIFY BY HUMAN'
503
531
  ];
504
532
 
505
533
  let verifyIndex = -1;
@@ -514,7 +542,7 @@ for (let i = 0; i < lines.length; i++) {
514
542
  if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
515
543
  // Double-check: make sure it's NOT a VERIFIED section (without TO VERIFY)
516
544
  if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) &&
517
- (trimmed.includes('TO VERIFY') || trimmed.includes('Verified by AI screenshot') || trimmed.includes('CROSS-COMPUTER REQUIREMENT ASSIGNMENT') || trimmed.includes('COMPUTER FOCUS AREA MANAGEMENT'))) {
545
+ (trimmed.includes('TO VERIFY') || trimmed.includes('Verified by AI screenshot') || trimmed.includes('CROSS-COMPUTER REQUIREMENT ASSIGNMENT') || trimmed.includes('COMPUTER FOCUS AREA MANAGEMENT') || trimmed.includes('spec-kit'))) {
518
546
  verifyIndex = i;
519
547
  break;
520
548
  }
@@ -547,13 +575,13 @@ if (verifyIndex === -1) {
547
575
  if (insertionIndex > 0 && lines[insertionIndex - 1].trim() !== '') {
548
576
  block.push('');
549
577
  }
550
- block.push('## 🔍 TO VERIFY BY HUMAN', '### Automatic Registration and Tracking', '### User Registration', '');
578
+ block.push('## 🔍 spec-kit: TO VERIFY BY HUMAN', '### Automatic Registration and Tracking', '### User Registration', '');
551
579
  block.push('## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT');
552
580
  block.push('## 🖥️ COMPUTER FOCUS AREA MANAGEMENT');
553
581
  lines.splice(insertionIndex, 0, ...block);
554
582
  verifyIndex = lines.findIndex(line => {
555
583
  const trimmed = line.trim();
556
- return trimmed === '## 🔍 TO VERIFY BY HUMAN' || trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN');
584
+ return trimmed === '## 🔍 spec-kit: TO VERIFY BY HUMAN' || trimmed.startsWith('## 🔍 spec-kit: TO VERIFY BY HUMAN');
557
585
  });
558
586
 
559
587
  // Safety check: verifyIndex should be valid
@@ -616,7 +644,6 @@ for (let i = verifyIndex + 1; i < nextSectionIndex; i++) {
616
644
  }
617
645
 
618
646
  // Insert requirement block at TOP of TO VERIFY section (right after section header)
619
- // Always insert immediately after the section header with proper blank line spacing
620
647
  let insertIndex = verifyIndex + 1;
621
648
 
622
649
  // Ensure there's a blank line after the section header
@@ -625,18 +652,77 @@ if (lines[insertIndex]?.trim() !== '') {
625
652
  insertIndex++;
626
653
  }
627
654
 
628
- // Insert the requirement block with conflict resolution
629
- const conflictResolution = '### Conflict Resolution: ';
630
- lines.splice(insertIndex, 0, conflictResolution);
631
- insertIndex++;
655
+ // If a Conflict Resolution header already exists immediately after the section header, reuse it
656
+ const conflictHeader = '### Conflict Resolution:';
657
+ if (lines[insertIndex]?.trim().startsWith(conflictHeader)) {
658
+ // Move insertion point to after the existing header
659
+ insertIndex++;
660
+ // Ensure there's a blank line after the header before inserting the requirement
661
+ if (lines[insertIndex]?.trim() !== '') {
662
+ lines.splice(insertIndex, 0, '');
663
+ insertIndex++;
664
+ }
665
+ } else {
666
+ // Insert the conflict header
667
+ lines.splice(insertIndex, 0, conflictHeader);
668
+ insertIndex++;
669
+ // Ensure a blank line after the header
670
+ if (lines[insertIndex]?.trim() !== '') {
671
+ lines.splice(insertIndex, 0, '');
672
+ insertIndex++;
673
+ }
674
+ }
675
+
676
+ // Insert the requirement block
632
677
  lines.splice(insertIndex, 0, ...requirementBlock);
633
678
 
634
679
  // Ensure there's a blank line after the requirement block
635
- const afterIndex = insertIndex + requirementBlock.length + 1;
680
+ const afterIndex = insertIndex + requirementBlock.length;
636
681
  if (afterIndex < lines.length && lines[afterIndex]?.trim() !== '') {
637
682
  lines.splice(afterIndex, 0, '');
638
683
  }
639
684
 
685
+ // Move the requirement to the VERIFIED section
686
+ const verifiedSectionVariants = [
687
+ '## 📝 VERIFIED',
688
+ '## VERIFIED'
689
+ ];
690
+ let verifiedIndex = -1;
691
+ for (let i = 0; i < lines.length; i++) {
692
+ const line = lines[i];
693
+ const trimmed = line.trim();
694
+
695
+ // Check each variant more carefully
696
+ for (const variant of verifiedSectionVariants) {
697
+ const variantTrimmed = variant.trim();
698
+ // Exact match or line starts with variant
699
+ if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
700
+ verifiedIndex = i;
701
+ break;
702
+ }
703
+ }
704
+ if (verifiedIndex !== -1) break;
705
+ }
706
+
707
+ if (verifiedIndex === -1) {
708
+ // Create VERIFIED section - place it after TO VERIFY section
709
+ const block = [];
710
+ block.push('## 📝 VERIFIED');
711
+ lines.splice(verifyIndex + 1, 0, ...block);
712
+ verifiedIndex = lines.findIndex(line => {
713
+ const trimmed = line.trim();
714
+ return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED');
715
+ });
716
+ }
717
+
718
+ // Insert the requirement block at the end of the VERIFIED section
719
+ let insertIndexVerified = verifiedIndex + 1;
720
+ while (insertIndexVerified < lines.length && lines[insertIndexVerified].trim().startsWith('###')) {
721
+ insertIndexVerified++;
722
+ }
723
+ lines.splice(insertIndexVerified, 0, ...requirementBlock);
724
+
725
+
640
726
 
641
727
  // Write the file
642
728
  await fs.writeFile(reqPath, lines.join('\n'));
@@ -792,8 +878,31 @@ async function getAllAvailableProviders() {
792
878
  case 'windsurf':
793
879
  case 'vscode':
794
880
  case 'kiro':
795
- case 'antigravity': {
796
- providers.push(base);
881
+ case 'antigravity':
882
+ case 'github-copilot':
883
+ case 'amazon-q':
884
+ case 'replit': {
885
+ console.log(chalk.gray(`[DEBUG] Processing provider: ${providerId}, extension: ${def.extension}`));
886
+ if (Array.isArray(def.subAgents) && def.subAgents.length > 0) {
887
+ for (const sub of def.subAgents) {
888
+ providers.push({
889
+ ...base,
890
+ model: sub.model,
891
+ subAgentId: sub.id,
892
+ subAgentName: sub.name,
893
+ displayName: `${def.name} (${sub.name})`,
894
+ extension: def.extension
895
+ });
896
+ }
897
+ } else {
898
+ const providerObj = {
899
+ ...base,
900
+ model: def.defaultModel || providerId,
901
+ extension: def.extension
902
+ };
903
+ console.log(chalk.gray(`[DEBUG] Created provider object for ${providerId}:`, JSON.stringify(providerObj, null, 2)));
904
+ providers.push(providerObj);
905
+ }
797
906
  break;
798
907
  }
799
908
  case 'ollama': {
@@ -829,7 +938,32 @@ async function getProviderConfig(excludeProvider = null) {
829
938
  const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
830
939
  const providers = await getAllAvailableProviders();
831
940
  const prefs = await getProviderPreferences();
832
-
941
+
942
+ // Clear any incorrect rate limits for web-based IDEs that were marked due to platform issues
943
+ for (const provider of providers) {
944
+ if (provider.provider === 'replit' && providerManager.isRateLimited('replit', 'replit')) {
945
+ const rateLimitInfo = providerManager.rateLimits['replit:replit'];
946
+ if (rateLimitInfo) {
947
+ const now = Date.now();
948
+ const timeSinceMarked = now - (rateLimitInfo.markedAt || 0);
949
+ const minutesSinceMarked = timeSinceMarked / (1000 * 60);
950
+
951
+ // Clear rate limit if:
952
+ // 1. It was marked due to platform issues, OR
953
+ // 2. It was marked within the last 5 minutes (likely a recent platform issue)
954
+ const isPlatformError = rateLimitInfo.reason &&
955
+ (rateLimitInfo.reason.includes('xdg-open') ||
956
+ rateLimitInfo.reason.includes('command not found') ||
957
+ rateLimitInfo.reason.includes('Unable to find application'));
958
+
959
+ if (isPlatformError || minutesSinceMarked < 5) {
960
+ console.log(chalk.yellow(`⚠️ Clearing incorrect rate limit for Replit (marked ${minutesSinceMarked.toFixed(1)} minutes ago: ${isPlatformError ? 'platform error' : 'recent error'})`));
961
+ delete providerManager.rateLimits['replit:replit'];
962
+ }
963
+ }
964
+ }
965
+ }
966
+
833
967
  // Get first ENABLED agent from provider preferences (same logic as interactive menu)
834
968
  let firstEnabledAgent = null;
835
969
  for (const agentId of prefs.order) {
@@ -839,6 +973,13 @@ async function getProviderConfig(excludeProvider = null) {
839
973
  }
840
974
  }
841
975
  const savedAgent = firstEnabledAgent || config.agent || config.ide;
976
+
977
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
978
+ console.log(chalk.gray(`[DEBUG] firstEnabledAgent: ${firstEnabledAgent}`));
979
+ console.log(chalk.gray(`[DEBUG] config.agent: ${config.agent}`));
980
+ console.log(chalk.gray(`[DEBUG] config.ide: ${config.ide}`));
981
+ console.log(chalk.gray(`[DEBUG] savedAgent: ${savedAgent}`));
982
+ }
842
983
 
843
984
  if (providers.length === 0) {
844
985
  return { status: 'no_providers', providers: [] };
@@ -854,17 +995,147 @@ async function getProviderConfig(excludeProvider = null) {
854
995
  const availableProviders = enabledProviders.filter(p => {
855
996
  // Exclude the specified provider and rate-limited providers
856
997
  if (excludeProvider && p.provider === excludeProvider) {
998
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
999
+ console.log(chalk.gray(`[DEBUG] Excluding ${p.provider} due to excludeProvider=${excludeProvider}`));
1000
+ }
857
1001
  return false;
858
1002
  }
859
- return !providerManager.isRateLimited(p.provider, p.model);
1003
+
1004
+ // For IDE providers, only consider them rate limited if they've actually been used
1005
+ // IDE providers that haven't been used yet should always be available for launch/installation
1006
+ if (p.type === 'ide') {
1007
+ // Check if this IDE provider has been used before by looking for any rate limit entries
1008
+ const hasBeenUsed = providerManager.rateLimits[`${p.provider}:`] ||
1009
+ Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`));
1010
+
1011
+ // Debug: Log IDE provider status
1012
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
1013
+ console.log(chalk.gray(`[DEBUG] IDE Provider ${p.provider}: hasBeenUsed=${hasBeenUsed}, isRateLimited=${providerManager.isRateLimited(p.provider, p.model)}`));
1014
+ console.log(chalk.gray(`[DEBUG] Rate limits for ${p.provider}:`, JSON.stringify(providerManager.rateLimits, null, 2)));
1015
+ }
1016
+
1017
+ // If it has been used, check if it's currently rate limited
1018
+ if (hasBeenUsed) {
1019
+ const isRateLimited = providerManager.isRateLimited(p.provider, p.model);
1020
+ if (process.env.DEBUG_PROVIDER_SELECTION && isRateLimited) {
1021
+ console.log(chalk.gray(`[DEBUG] ${p.provider} is rate limited, excluding`));
1022
+ }
1023
+ return !isRateLimited;
1024
+ }
1025
+ // If it hasn't been used yet, it's always available
1026
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
1027
+ console.log(chalk.gray(`[DEBUG] ${p.provider} hasn't been used yet, including`));
1028
+ }
1029
+ return true;
1030
+ }
1031
+
1032
+ // For non-IDE providers, check rate limits normally
1033
+ const isRateLimited = providerManager.isRateLimited(p.provider, p.model);
1034
+ if (process.env.DEBUG_PROVIDER_SELECTION && isRateLimited) {
1035
+ console.log(chalk.gray(`[DEBUG] Non-IDE provider ${p.provider} is rate limited, excluding`));
1036
+ }
1037
+ return !isRateLimited;
860
1038
  });
861
1039
 
1040
+ // If no providers are available, try to enable some IDE providers that haven't been used yet
1041
+ if (availableProviders.length === 0) {
1042
+ const allProviders = await getAllAvailableProviders();
1043
+ const unusedIdeProviders = allProviders.filter(p =>
1044
+ p.type === 'ide' &&
1045
+ !enabledProviders.some(ep => ep.provider === p.provider) &&
1046
+ !providerManager.rateLimits[`${p.provider}:`] &&
1047
+ !Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`))
1048
+ );
1049
+
1050
+ if (unusedIdeProviders.length > 0) {
1051
+ console.log(chalk.yellow(`⚠️ No enabled providers available. Auto-enabling unused IDE providers...`));
1052
+ const prefs = await getProviderPreferences();
1053
+
1054
+ // Enable the first few unused IDE providers
1055
+ const providersToEnable = unusedIdeProviders.slice(0, 3);
1056
+ for (const provider of providersToEnable) {
1057
+ prefs.enabled[provider.provider] = true;
1058
+ console.log(chalk.gray(` ✓ Enabled ${provider.displayName}`));
1059
+ }
1060
+
1061
+ await saveProviderPreferences(prefs.order, prefs.enabled);
1062
+
1063
+ // Recalculate available providers with the newly enabled ones
1064
+ const newlyEnabledProviders = allProviders.filter(p =>
1065
+ prefs.enabled[p.provider] !== false
1066
+ );
1067
+
1068
+ const newlyAvailableProviders = newlyEnabledProviders.filter(p => {
1069
+ // Exclude the specified provider
1070
+ if (excludeProvider && p.provider === excludeProvider) {
1071
+ return false;
1072
+ }
1073
+
1074
+ // For newly enabled IDE providers, they haven't been used yet, so they're always available
1075
+ if (p.type === 'ide') {
1076
+ return true;
1077
+ }
1078
+
1079
+ // For non-IDE providers, check rate limits normally
1080
+ return !providerManager.isRateLimited(p.provider, p.model);
1081
+ });
1082
+
1083
+ // Add the newly available providers to the list
1084
+ availableProviders.push(...newlyAvailableProviders);
1085
+ }
1086
+ }
1087
+
1088
+ // Debug: Log provider selection details
1089
+ if (process.env.DEBUG_PROVIDER_SELECTION || true) { // Force debug for now
1090
+ console.log(chalk.gray(`[DEBUG] Total providers: ${providers.length}`));
1091
+ console.log(chalk.gray(`[DEBUG] Enabled providers: ${enabledProviders.length}`));
1092
+ console.log(chalk.gray(`[DEBUG] Available providers: ${availableProviders.length}`));
1093
+ console.log(chalk.gray(`[DEBUG] Available provider names: ${availableProviders.map(p => p.provider).join(', ')}`));
1094
+ console.log(chalk.gray(`[DEBUG] Enabled provider names: ${enabledProviders.map(p => p.provider).join(', ')}`));
1095
+ console.log(chalk.gray(`[DEBUG] Excluding provider: ${excludeProvider}`));
1096
+ console.log(chalk.gray(`[DEBUG] Looking for savedAgent: ${savedAgent}`));
1097
+ }
1098
+
862
1099
  let selection = null;
863
1100
  if (savedAgent && savedAgent !== excludeProvider) {
864
1101
  selection = availableProviders.find(p => p.provider === savedAgent);
1102
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
1103
+ console.log(chalk.gray(`[DEBUG] Looking for savedAgent: ${savedAgent}`));
1104
+ console.log(chalk.gray(`[DEBUG] Found selection: ${selection ? selection.provider : 'null'}`));
1105
+ }
865
1106
  }
866
- if (!selection) {
867
- selection = availableProviders[0] || null;
1107
+
1108
+ // If no selection or the selected provider is rate limited, try to find an unused IDE provider
1109
+ if (!selection || (selection.type === 'ide' && providerManager.isRateLimited(selection.provider, selection.model))) {
1110
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
1111
+ console.log(chalk.gray(`[DEBUG] No valid selection, trying unused IDE providers`));
1112
+ console.log(chalk.gray(`[DEBUG] Reason: ${!selection ? 'no selection' : 'selection is rate limited'}`));
1113
+ }
1114
+
1115
+ // Prioritize newly enabled IDE providers that haven't been used
1116
+ const unusedIdeProviders = availableProviders.filter(p =>
1117
+ p.type === 'ide' &&
1118
+ !providerManager.rateLimits[`${p.provider}:`] &&
1119
+ !Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`))
1120
+ );
1121
+
1122
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
1123
+ console.log(chalk.gray(`[DEBUG] Unused IDE providers: ${unusedIdeProviders.map(p => p.provider).join(', ')}`));
1124
+ }
1125
+
1126
+ if (unusedIdeProviders.length > 0) {
1127
+ selection = unusedIdeProviders[0];
1128
+ console.log(chalk.green(`✓ Selected unused IDE provider: ${selection.displayName}`));
1129
+ } else if (availableProviders.length > 0) {
1130
+ selection = availableProviders[0];
1131
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
1132
+ console.log(chalk.gray(`[DEBUG] Selected first available provider: ${selection.provider}`));
1133
+ }
1134
+ }
1135
+ } else if (selection) {
1136
+ if (process.env.DEBUG_PROVIDER_SELECTION) {
1137
+ console.log(chalk.gray(`[DEBUG] Using savedAgent selection: ${selection.provider}`));
1138
+ }
868
1139
  }
869
1140
 
870
1141
  if (selection) {
@@ -888,10 +1159,16 @@ async function getProviderConfig(excludeProvider = null) {
888
1159
  };
889
1160
  }
890
1161
 
891
- async function acquireProviderConfig(excludeProvider = null) {
1162
+ async function acquireProviderConfig(excludeProvider = null, excludeModel = null) {
892
1163
  while (true) {
893
1164
  const selection = await getProviderConfig(excludeProvider);
894
1165
  if (selection.status === 'ok') {
1166
+ // If we have a specific model to exclude (for same-IDE failover), skip it
1167
+ if (excludeModel && selection.provider.model === excludeModel) {
1168
+ console.log(chalk.yellow(`⚠️ Excluding rate-limited sub-agent: ${selection.provider.displayName}\n`));
1169
+ // Retry with the same provider excluded to force picking another sub-agent
1170
+ return acquireProviderConfig(selection.provider.provider, selection.provider.model);
1171
+ }
895
1172
  return selection.provider;
896
1173
  }
897
1174
 
@@ -1338,6 +1615,13 @@ async function runIdeProviderIteration(providerConfig, repoPath) {
1338
1615
  console.log(chalk.cyan(`⚙️ ${t('auto.direct.provider.launching', { provider: providerConfig.displayName })}\n`));
1339
1616
 
1340
1617
  const args = [CLI_ENTRY_POINT, 'auto:start', '--ide', providerConfig.ide || providerConfig.provider, '--max-chats', String(providerConfig.maxChats || 1)];
1618
+ if (providerConfig.model) {
1619
+ args.push('--ide-model', String(providerConfig.model));
1620
+ }
1621
+ // Pass extension information for VS Code extensions
1622
+ if (providerConfig.extension) {
1623
+ args.push('--extension', String(providerConfig.extension));
1624
+ }
1341
1625
  const child = spawn(process.execPath, args, {
1342
1626
  cwd: repoPath,
1343
1627
  env: process.env,
@@ -1372,13 +1656,15 @@ async function runIdeProviderIteration(providerConfig, repoPath) {
1372
1656
  } else {
1373
1657
  const message = `${providerConfig.displayName} exited with code ${code}`;
1374
1658
  const antigravityRateLimit = checkAntigravityRateLimit(combinedOutput);
1659
+ const kiroRateLimit = checkKiroRateLimit(combinedOutput);
1375
1660
 
1376
1661
  resolve({
1377
1662
  success: false,
1378
1663
  error: combinedOutput ? `${message}\n${combinedOutput}` : message,
1379
1664
  output: combinedOutput,
1380
- rateLimited: isRateLimitMessage(combinedOutput) || antigravityRateLimit.isRateLimited,
1381
- antigravityRateLimited: antigravityRateLimit.isRateLimited
1665
+ rateLimited: isRateLimitMessage(combinedOutput) || antigravityRateLimit.isRateLimited || kiroRateLimit.isRateLimited,
1666
+ antigravityRateLimited: antigravityRateLimit.isRateLimited,
1667
+ kiroRateLimited: kiroRateLimit.isRateLimited
1382
1668
  });
1383
1669
  }
1384
1670
  });
@@ -1421,7 +1707,7 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1421
1707
  let foundInVerified = false;
1422
1708
 
1423
1709
  for (const line of lines) {
1424
- if (line.includes('## ✅ Verified by AI screenshot')) {
1710
+ if (line.includes('## ✅ Verified by AI')) {
1425
1711
  inVerifiedSection = true;
1426
1712
  continue;
1427
1713
  }
@@ -1451,6 +1737,10 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1451
1737
  for (const line of lines) {
1452
1738
  if (line.includes('🚦 Current Status')) {
1453
1739
  inStatusSection = true;
1740
+ if (line.includes('DONE')) {
1741
+ statusContainsDone = true;
1742
+ break;
1743
+ }
1454
1744
  continue;
1455
1745
  }
1456
1746
 
@@ -1472,21 +1762,72 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1472
1762
  return;
1473
1763
  }
1474
1764
 
1475
- // Check 3: Quota limit detection for Antigravity (after 2 minutes of waiting)
1476
- const elapsed = Date.now() - startTime;
1477
- if (ideType === 'antigravity' && !quotaHandled && elapsed >= 120000) {
1478
- console.log(chalk.yellow('\n⚠️ Antigravity quota limit detected after 2 minutes\n'));
1479
- console.log(chalk.cyan(' Switching to next available IDE agent...\n'));
1765
+ // Check 3: Detect rate-limit messages written into the REQUIREMENTS file
1766
+ // Examples:
1767
+ // - "You have reached the quota limit for this model. You can resume using this model at 1/19/2026, 4:07:27 PM."
1768
+ // - "Please try again in 15m5.472s"
1769
+ // - "Spending cap reached resets Jan 17 at 12pm"
1770
+ // - "Usage cap reached. Try again in 15 minutes."
1771
+ // - "You've reached your monthly chat messages quota" (GitHub Copilot)
1772
+ // - "Upgrade to Copilot Pro" (GitHub Copilot)
1773
+ // - "wait for your allowance to renew" (GitHub Copilot)
1774
+ const rateLimitPattern = /(quota limit|you have reached( the)? quota|you can resume using this model at|please try again in|try again in|spending cap reached|usage cap( reached)?|you\'ve hit( the)? usage limit|you\u2019ve hit( the)? usage limit|cap reached|limit exceeded|exceeded (quota|limit)|monthly.*quota|upgrade to.*pro|allowance to renew|chat messages quota)/i;
1775
+ // Avoid matching requirement headings or bullets (these may mention "rate limit" as part of the requirement text)
1776
+ const matchedLine = lines.find(l => rateLimitPattern.test(l) && !l.trim().startsWith('###') && !l.trim().startsWith('-'));
1777
+ if (matchedLine) {
1778
+ // Mark the provider as rate limited (if we can) and signal a rate-limited completion
1779
+ try {
1780
+ if (ideType && sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
1781
+ sharedProviderManager.markRateLimited(ideType, undefined, matchedLine);
1782
+ }
1783
+ } catch (e) {
1784
+ // Ignore errors from marking rate-limited
1785
+ }
1786
+
1480
1787
  watcher.close();
1481
- resolve({
1482
- success: false,
1483
- reason: 'antigravity-quota',
1484
- antigravityRateLimited: true // This triggers switching to next provider
1485
- });
1788
+ console.log(chalk.yellow(`\n⚠️ Rate limit message detected from IDE: ${matchedLine}\n`));
1789
+ // Return a generic rateLimited flag and include the matched line for diagnostics
1790
+ resolve({ success: false, rateLimited: true, providerRateLimited: ideType || undefined, matchedLine });
1486
1791
  return;
1487
1792
  }
1488
1793
 
1489
- // Check 4: Timeout
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')) {
1797
+ try {
1798
+ const quotaDetector = new QuotaDetector();
1799
+ const ideToCheck = ideType === 'github-copilot' || ideType === 'amazon-q' ? 'vscode' : ideType;
1800
+ const quotaResult = await quotaDetector.detectQuotaWarning(ideToCheck);
1801
+
1802
+ if (quotaResult && quotaResult.hasQuotaWarning) {
1803
+ quotaHandled = true;
1804
+ const quotaMessage = quotaResult.matchedText || 'Quota limit detected in IDE UI';
1805
+
1806
+ // Mark the provider as rate limited
1807
+ try {
1808
+ if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
1809
+ sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
1810
+ }
1811
+ } catch (e) {
1812
+ // Ignore errors from marking rate-limited
1813
+ }
1814
+
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;
1819
+ }
1820
+ } 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
+ }
1826
+ }
1827
+ }
1828
+
1829
+ // Check 5: Timeout
1830
+ const elapsed = Date.now() - startTime;
1490
1831
  if (elapsed >= timeoutMs) {
1491
1832
  watcher.close();
1492
1833
  console.log(chalk.yellow(`\n⚠️ Timeout after ${Math.floor(elapsed / 60000)} minutes\n`));
@@ -1541,30 +1882,110 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
1541
1882
 
1542
1883
  if (!ideResult.success) {
1543
1884
  if (ideResult.antigravityRateLimited) {
1544
- await handleAntigravityRateLimit();
1545
- return { success: false, error: 'Antigravity rate limit detected, retrying with next provider.', shouldRetry: true };
1885
+ providerManager.markRateLimited(providerConfig.provider, providerConfig.model, 'Quota limit reached');
1886
+ const switchResult = await handleAntigravityRateLimit();
1887
+ if (switchResult && switchResult.modelSwitched) {
1888
+ return { success: false, error: `Antigravity switched to ${switchResult.nextModel}, retrying.`, shouldRetry: true };
1889
+ }
1890
+ return { success: false, error: 'Antigravity rate limit reached, retrying with next provider.', shouldRetry: true };
1546
1891
  }
1892
+
1547
1893
  // CRITICAL: Mark provider as unavailable for ANY error so acquireProviderConfig() will skip it
1548
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, ideResult.output || ideResult.error || 'IDE provider failed');
1894
+ // EXCEPT for web-based IDEs where the error is platform/browser related
1895
+ const error = ideResult.output || ideResult.error || 'IDE provider failed';
1896
+ const isWebBasedIDE = providerConfig.provider === 'replit';
1897
+ const isPlatformError = error.includes('xdg-open') || error.includes('command not found') || error.includes('Unable to find application');
1898
+
1899
+ if (!isWebBasedIDE || !isPlatformError) {
1900
+ providerManager.markRateLimited(providerConfig.provider, providerConfig.model, error);
1901
+ } else {
1902
+ // For web-based IDEs with platform errors, don't mark as rate limited
1903
+ // Just log the error and let the system try the next provider
1904
+ console.log(chalk.yellow(`⚠️ Web-based IDE ${providerConfig.provider} failed due to platform issue: ${error}`));
1905
+ }
1906
+
1549
1907
  return { success: false, error: ideResult.error || 'IDE provider failed' };
1550
1908
  }
1551
1909
 
1552
1910
  console.log(chalk.green(`✓ ${t('auto.direct.ide.prompt.sent')}`));
1553
1911
 
1554
1912
  // Wait for IDE agent to complete the work (IDE will update status to DONE itself)
1913
+ const ideCompletionStartTime = Date.now();
1555
1914
  const completionResult = await waitForIdeCompletion(repoPath, requirement.text, providerConfig.ide || providerConfig.provider);
1915
+ const ideResponseTime = Date.now() - ideCompletionStartTime;
1916
+
1917
+ // Track IDE health metrics based on completion result
1918
+ const ideType = providerConfig.ide || providerConfig.provider;
1919
+ if (completionResult.success) {
1920
+ // Record success with response time
1921
+ await sharedHealthTracker.recordSuccess(ideType, ideResponseTime, {
1922
+ requirementId: requirement.id || requirement.text.substring(0, 50),
1923
+ continuationPromptsDetected: 0, // Will be updated when continuation detection is implemented
1924
+ });
1925
+ } else if (completionResult.rateLimited) {
1926
+ // Record quota event (does NOT increment success/failure counters per FR-008)
1927
+ await sharedHealthTracker.recordQuota(ideType, completionResult.matchedLine || 'Quota limit detected', {
1928
+ requirementId: requirement.id || requirement.text.substring(0, 50),
1929
+ });
1930
+ } else if (completionResult.reason === 'timeout') {
1931
+ // Record failure due to timeout
1932
+ await sharedHealthTracker.recordFailure(ideType, 'Timeout exceeded', {
1933
+ timeoutUsed: 30 * 60 * 1000, // Default 30 minutes, will be adaptive later
1934
+ requirementId: requirement.id || requirement.text.substring(0, 50),
1935
+ });
1936
+ } else {
1937
+ // Record other failure
1938
+ await sharedHealthTracker.recordFailure(ideType, completionResult.error || 'Unknown error', {
1939
+ requirementId: requirement.id || requirement.text.substring(0, 50),
1940
+ });
1941
+ }
1556
1942
 
1557
1943
  if (!completionResult.success) {
1944
+ // Special-case behavior for Antigravity CLI installs (they have special handling)
1558
1945
  if (completionResult.antigravityRateLimited) {
1559
1946
  console.log(chalk.yellow(`⚠️ ${t('auto.direct.provider.quota.exhausted', { provider: 'Antigravity' })}\n`));
1560
1947
  providerManager.markRateLimited(providerConfig.provider, providerConfig.model, 'Quota limit reached');
1948
+
1949
+ const switchResult = await handleAntigravityRateLimit();
1950
+ if (switchResult && switchResult.modelSwitched) {
1951
+ return { success: false, error: `Antigravity switched to ${switchResult.nextModel}, retrying.`, shouldRetry: true };
1952
+ }
1953
+
1561
1954
  return { success: false, error: 'Antigravity quota limit', shouldRetry: true };
1562
1955
  }
1563
1956
 
1957
+ // Special-case behavior for Kiro IDE (they have special handling)
1958
+ if (completionResult.kiroRateLimited) {
1959
+ console.log(chalk.yellow(`⚠️ ${t('auto.direct.provider.quota.exhausted', { provider: 'AWS Kiro' })}\n`));
1960
+ providerManager.markRateLimited(providerConfig.provider, providerConfig.model, 'Quota limit reached');
1961
+
1962
+ const switchResult = await handleKiroRateLimit();
1963
+ if (switchResult && switchResult.success) {
1964
+ return { success: false, error: `AWS Kiro switched to ${switchResult.nextProvider}, retrying.`, shouldRetry: true };
1965
+ }
1966
+
1967
+ return { success: false, error: 'AWS Kiro quota limit', shouldRetry: true };
1968
+ }
1969
+
1970
+ // Generic rate-limited behavior: if the completion detected a rate-limited message in REQUIREMENTS,
1971
+ // mark the provider as rate-limited and retry with the next available provider.
1972
+ if (completionResult.rateLimited) {
1973
+ console.log(chalk.yellow(`⚠️ Provider ${providerConfig.provider} reported quota/exhaustion: ${completionResult.matchedLine || ''}\n`));
1974
+ providerManager.markRateLimited(providerConfig.provider, providerConfig.model, completionResult.matchedLine || 'Quota limit detected');
1975
+ return { success: false, error: 'Provider quota limit', shouldRetry: true };
1976
+ }
1977
+
1564
1978
  const errorMsg = completionResult.reason === 'timeout'
1565
1979
  ? 'IDE agent timed out'
1566
1980
  : 'IDE agent failed to complete';
1567
1981
  providerManager.markRateLimited(providerConfig.provider, providerConfig.model, errorMsg);
1982
+
1983
+ // Automatically retry with next IDE on timeout (T024: automatic IDE switching)
1984
+ if (completionResult.reason === 'timeout') {
1985
+ console.log(chalk.yellow(`⏰ Timeout detected - switching to next available IDE\n`));
1986
+ return { success: false, error: errorMsg, shouldRetry: true };
1987
+ }
1988
+
1568
1989
  return { success: false, error: errorMsg };
1569
1990
  }
1570
1991
 
@@ -1633,7 +2054,10 @@ async function runIteration(requirement, providerConfig, repoPath) {
1633
2054
  // ═══════════════════════════════════════════════════════════
1634
2055
  printStatusCard(requirement.text, 'ACT');
1635
2056
 
1636
- console.log(chalk.cyan('🤖 Asking LLM for implementation...\n'));
2057
+ console.log(chalk.cyan(` ${getLogTimestamp()} - 🤖 Asking LLM for implementation...\n`));
2058
+ console.log(chalk.gray('─'.repeat(80)));
2059
+ console.log(chalk.yellow('💭 LLM Response (streaming):'));
2060
+ console.log(chalk.gray('─'.repeat(80)));
1637
2061
 
1638
2062
  // Build context with actual file snippets
1639
2063
  let contextSection = '';
@@ -1752,27 +2176,97 @@ if (counts.todoCount === 0) {
1752
2176
  Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
1753
2177
 
1754
2178
  let fullResponse = '';
2179
+ let chunkCount = 0;
2180
+ let totalChars = 0;
2181
+
2182
+ // Show spinner while waiting for first chunk
2183
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
2184
+ let spinnerIndex = 0;
2185
+ let spinnerInterval = null;
2186
+ let receivedFirstChunk = false;
2187
+
2188
+ const startSpinner = () => {
2189
+ process.stdout.write(chalk.cyan('⏳ Waiting for response'));
2190
+ spinnerInterval = setInterval(() => {
2191
+ process.stdout.write(`\r${chalk.cyan('⏳ Waiting for response')} ${chalk.yellow(spinnerFrames[spinnerIndex])}`);
2192
+ spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
2193
+ }, 100);
2194
+ };
2195
+
2196
+ const stopSpinner = () => {
2197
+ if (spinnerInterval) {
2198
+ clearInterval(spinnerInterval);
2199
+ spinnerInterval = null;
2200
+ process.stdout.write('\r\x1b[K'); // Clear the line
2201
+ }
2202
+ };
2203
+
2204
+ startSpinner();
1755
2205
 
1756
2206
  const result = await llm.call(providerConfig, prompt, {
1757
2207
  temperature: 0.1,
1758
2208
  maxTokens: 4096,
1759
2209
  onChunk: (chunk) => {
1760
- process.stdout.write(chalk.gray(chunk));
2210
+ chunkCount++;
2211
+ totalChars += chunk.length;
2212
+ // Show first chunk arrival to confirm streaming started
2213
+ if (chunkCount === 1) {
2214
+ stopSpinner();
2215
+ receivedFirstChunk = true;
2216
+ process.stdout.write(chalk.green('✓ Streaming started...\n'));
2217
+ }
2218
+ // Use white text for better visibility instead of gray
2219
+ process.stdout.write(chalk.white(chunk));
1761
2220
  fullResponse += chunk;
1762
2221
  },
1763
2222
  onComplete: () => {
1764
- console.log('\n');
2223
+ stopSpinner();
2224
+ if (chunkCount > 0) {
2225
+ console.log(chalk.green(`\n✓ Received ${totalChars} characters in ${chunkCount} chunks`));
2226
+ } else {
2227
+ console.log(chalk.yellow('\n⚠️ No streaming response received (using complete response)'));
2228
+ }
2229
+ console.log(chalk.gray('─'.repeat(80)));
2230
+ console.log();
1765
2231
  },
1766
2232
  onError: (error) => {
2233
+ stopSpinner();
1767
2234
  console.error(chalk.red(`\n✗ Error: ${error}`));
1768
2235
  }
1769
2236
  });
1770
2237
 
2238
+ // Ensure spinner is stopped even if callbacks didn't fire
2239
+ stopSpinner();
2240
+
1771
2241
  if (!result.success) {
1772
2242
  const combinedError = [result.error, fullResponse].filter(Boolean).join('\n').trim() || 'Unknown error';
1773
2243
  console.log(chalk.red(`\n✗ LLM call failed: ${combinedError}`));
1774
2244
  // CRITICAL: Mark provider as rate-limited for ANY error so acquireProviderConfig() will skip it
1775
2245
  providerManager.markRateLimited(providerConfig.provider, providerConfig.model, combinedError);
2246
+
2247
+ // Track health metrics for failed direct providers
2248
+ const providerType = providerConfig.provider;
2249
+ const duration = Date.now() - startTime;
2250
+ try {
2251
+ // Detect quota/rate limit errors vs other failures
2252
+ const quotaPattern = /(quota|rate.?limit|usage.?limit|spending.?cap|allowance|exceeded|overloaded)/i;
2253
+ const isQuotaError = quotaPattern.test(combinedError);
2254
+
2255
+ if (isQuotaError) {
2256
+ await sharedHealthTracker.recordQuota(providerType, combinedError, {
2257
+ requirementId: requirement.id || requirement.text.substring(0, 50),
2258
+ });
2259
+ } else {
2260
+ await sharedHealthTracker.recordFailure(providerType, combinedError, {
2261
+ timeoutUsed: duration,
2262
+ requirementId: requirement.id || requirement.text.substring(0, 50),
2263
+ });
2264
+ }
2265
+ } catch (healthError) {
2266
+ // Don't fail the iteration if health tracking fails
2267
+ console.log(chalk.gray(`⚠️ Health tracking error: ${healthError.message}`));
2268
+ }
2269
+
1776
2270
  return { success: false, error: combinedError };
1777
2271
  }
1778
2272
 
@@ -1899,6 +2393,18 @@ Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
1899
2393
  const duration = Date.now() - startTime;
1900
2394
  providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
1901
2395
 
2396
+ // Track health metrics for direct providers (IDE providers tracked in runIdeFallbackIteration)
2397
+ const providerType = providerConfig.provider;
2398
+ try {
2399
+ await sharedHealthTracker.recordSuccess(providerType, duration, {
2400
+ requirementId: requirement.id || requirement.text.substring(0, 50),
2401
+ continuationPromptsDetected: 0,
2402
+ });
2403
+ } catch (healthError) {
2404
+ // Don't fail the iteration if health tracking fails
2405
+ console.log(chalk.gray(`⚠️ Health tracking error: ${healthError.message}`));
2406
+ }
2407
+
1902
2408
  console.log();
1903
2409
  console.log(chalk.gray('─'.repeat(80)));
1904
2410
  console.log();
@@ -1929,10 +2435,6 @@ async function handleAutoStart(options) {
1929
2435
  console.log(chalk.gray('═'.repeat(80)));
1930
2436
  console.log();
1931
2437
 
1932
- // Get repo path
1933
- // Load configured stages
1934
- configuredStages = await getStages();
1935
-
1936
2438
  const repoPath = await getRepoPath();
1937
2439
  if (!repoPath) {
1938
2440
  console.log(chalk.red('✗ No repository configured'));
@@ -1940,6 +2442,20 @@ async function handleAutoStart(options) {
1940
2442
  return;
1941
2443
  }
1942
2444
 
2445
+ // Start Auto Mode status tracking
2446
+ const config = await getAutoConfig();
2447
+
2448
+ // Save extension to config if provided
2449
+ if (options.extension) {
2450
+ config.extension = options.extension;
2451
+ await setAutoConfig(config);
2452
+ }
2453
+
2454
+ await startAutoMode(repoPath, { ide: options.ide || config.ide });
2455
+
2456
+ // Also load configured stages here since we already have the config
2457
+ configuredStages = await getStages();
2458
+
1943
2459
  console.log(chalk.white(t('auto.repository')), chalk.cyan(repoPath));
1944
2460
 
1945
2461
  // Use the agent that was already determined by provider preferences in interactive.js
@@ -1954,11 +2470,13 @@ async function handleAutoStart(options) {
1954
2470
 
1955
2471
  console.log(chalk.white('Provider:'), chalk.cyan(providerConfig.displayName));
1956
2472
 
1957
- // Get max chats
1958
- const config = await getAutoConfig();
2473
+ // Get max chats (use already loaded config)
1959
2474
  const unlimited = !options.maxChats && !config.maxChats && config.neverStop;
1960
2475
  const maxChats = unlimited ? Number.MAX_SAFE_INTEGER : (options.maxChats || config.maxChats || 1);
1961
2476
  console.log(chalk.white(`${t('auto.direct.config.max.iterations')}`), unlimited ? chalk.cyan('∞ (never stop)') : chalk.cyan(maxChats));
2477
+
2478
+ // Update initial status
2479
+ await updateAutoModeStatus(repoPath, { chatCount: 0, maxChats: unlimited ? 0 : maxChats });
1962
2480
  console.log();
1963
2481
  console.log(chalk.gray('═'.repeat(80)));
1964
2482
 
@@ -1969,6 +2487,9 @@ async function handleAutoStart(options) {
1969
2487
  // Main loop
1970
2488
  let completedCount = 0;
1971
2489
  let failedCount = 0;
2490
+ let providerAttempts = 0; // Track attempts for current requirement
2491
+ let lastRequirementText = null; // Track which requirement we're on
2492
+ const MAX_PROVIDER_ATTEMPTS = 3; // Maximum times to try different providers for same requirement
1972
2493
 
1973
2494
  for (let i = 0; i < maxChats; i++) {
1974
2495
  // Get current requirement first to check if there are any TODO items
@@ -1979,6 +2500,32 @@ async function handleAutoStart(options) {
1979
2500
  break;
1980
2501
  }
1981
2502
 
2503
+ // Check if this is a new requirement or the same one we're retrying
2504
+ if (requirement.text !== lastRequirementText) {
2505
+ // New requirement - reset attempt counter
2506
+ providerAttempts = 0;
2507
+ lastRequirementText = requirement.text;
2508
+ }
2509
+
2510
+ // Increment attempt counter
2511
+ providerAttempts++;
2512
+
2513
+ // Check if we've exceeded maximum attempts for this requirement
2514
+ if (providerAttempts > MAX_PROVIDER_ATTEMPTS) {
2515
+ console.log(chalk.red(`\n✗ Maximum provider attempts (${MAX_PROVIDER_ATTEMPTS}) reached for this requirement`));
2516
+ console.log(chalk.yellow(' All available providers have failed or are rate limited'));
2517
+ console.log(chalk.gray(' Skipping this requirement and moving to next...\n'));
2518
+
2519
+ // Mark requirement as failed and move on
2520
+ failedCount++;
2521
+ providerAttempts = 0;
2522
+ lastRequirementText = null;
2523
+
2524
+ // Move requirement to a "Failed" or "Needs Review" section
2525
+ // For now, just continue to next iteration without decrementing i
2526
+ continue;
2527
+ }
2528
+
1982
2529
  // Calculate current requirement number consistently (before processing)
1983
2530
  // This represents which requirement we're working on (1-based)
1984
2531
  const currentReqNumber = completedCount + failedCount + 1;
@@ -1987,11 +2534,16 @@ async function handleAutoStart(options) {
1987
2534
  console.log(chalk.bold.magenta(` ${t('auto.direct.requirement.header', { current: currentReqNumber, total: initialEffectiveMax })}`));
1988
2535
  console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
1989
2536
 
2537
+ // Update Auto Mode status with current iteration
2538
+ await updateAutoModeStatus(repoPath, { chatCount: currentReqNumber });
2539
+
1990
2540
  // Run iteration with full workflow
1991
2541
  const result = await runIteration(requirement, providerConfig, repoPath);
1992
2542
 
1993
2543
  if (result.success) {
1994
2544
  completedCount++;
2545
+ providerAttempts = 0; // Reset attempts on success
2546
+ lastRequirementText = null;
1995
2547
  console.log(chalk.bold.green(`✅ Requirement ${currentReqNumber}/${initialEffectiveMax} COMPLETE`));
1996
2548
  console.log(chalk.gray('Moving to next requirement...\n'));
1997
2549
 
@@ -2027,6 +2579,7 @@ async function handleAutoStart(options) {
2027
2579
  await new Promise(resolve => setTimeout(resolve, 300));
2028
2580
 
2029
2581
  // Exit this process - child continues with terminal
2582
+ await stopAutoMode('restarting');
2030
2583
  process.exit(0);
2031
2584
  } else {
2032
2585
  // Small delay before next iteration (if not restarting)
@@ -2044,7 +2597,7 @@ async function handleAutoStart(options) {
2044
2597
 
2045
2598
  console.log(chalk.yellow(`⚠️ ${errorType} detected, switching to next provider in your list...\n`));
2046
2599
 
2047
- const newProviderConfig = await acquireProviderConfig(providerConfig.provider);
2600
+ const newProviderConfig = await acquireProviderConfig(providerConfig.provider, providerConfig.model);
2048
2601
  if (newProviderConfig) {
2049
2602
  providerConfig = newProviderConfig;
2050
2603
  console.log(chalk.yellow(`⚠️ ${failedProvider} hit ${errorType.toLowerCase()}`));
@@ -2085,14 +2638,20 @@ async function handleAutoStart(options) {
2085
2638
  console.log(chalk.bold.green(`🎉 ${t('auto.direct.summary.final.message', { count: completedCount, plural: completedCount > 1 ? 's' : '' })}`));
2086
2639
  }
2087
2640
 
2641
+ // Stop Auto Mode status tracking
2642
+ await stopAutoMode('completed');
2643
+
2088
2644
  } catch (error) {
2089
2645
  console.error(chalk.red('\n' + t('auto.fatal.error')), error.message);
2090
2646
  if (error.stack) {
2091
2647
  console.log(chalk.gray(error.stack));
2092
2648
  }
2649
+
2650
+ // Stop Auto Mode status tracking on fatal error
2651
+ await stopAutoMode('error');
2093
2652
  process.exit(1);
2094
2653
  }
2095
2654
  }
2096
2655
 
2097
- module.exports = { handleAutoStart };
2656
+ module.exports = { handleAutoStart, waitForIdeCompletion, acquireProviderConfig };
2098
2657