vibecodingmachine-cli 2025.12.25-25 → 2026.1.22-1441

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 (52) 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 +92 -118
  5. package/logs/audit/2025-12-27.jsonl +1 -0
  6. package/logs/audit/2026-01-03.jsonl +2 -0
  7. package/package.json +2 -2
  8. package/reset_provider_order.js +21 -0
  9. package/scripts/convert-requirements.js +35 -0
  10. package/scripts/debug-parse.js +24 -0
  11. package/src/commands/auth.js +5 -1
  12. package/src/commands/auto-direct.js +747 -182
  13. package/src/commands/auto.js +206 -48
  14. package/src/commands/computers.js +9 -0
  15. package/src/commands/feature.js +123 -0
  16. package/src/commands/ide.js +108 -3
  17. package/src/commands/repo.js +27 -22
  18. package/src/commands/requirements-remote.js +34 -2
  19. package/src/commands/requirements.js +129 -9
  20. package/src/commands/setup.js +2 -1
  21. package/src/commands/status.js +39 -1
  22. package/src/commands/sync.js +7 -1
  23. package/src/utils/antigravity-js-handler.js +13 -4
  24. package/src/utils/auth.js +56 -25
  25. package/src/utils/compliance-check.js +10 -0
  26. package/src/utils/config.js +42 -1
  27. package/src/utils/date-formatter.js +44 -0
  28. package/src/utils/first-run.js +8 -6
  29. package/src/utils/interactive.js +1363 -334
  30. package/src/utils/kiro-js-handler.js +188 -0
  31. package/src/utils/prompt-helper.js +64 -0
  32. package/src/utils/provider-rate-cache.js +31 -0
  33. package/src/utils/provider-registry.js +42 -1
  34. package/src/utils/requirements-converter.js +107 -0
  35. package/src/utils/requirements-parser.js +144 -0
  36. package/tests/antigravity-js-handler.test.js +23 -0
  37. package/tests/home-bootstrap.test.js +76 -0
  38. package/tests/integration/health-tracking.integration.test.js +284 -0
  39. package/tests/provider-manager.test.js +92 -0
  40. package/tests/rate-limit-display.test.js +44 -0
  41. package/tests/requirements-bullet-parsing.test.js +15 -0
  42. package/tests/requirements-converter.test.js +42 -0
  43. package/tests/requirements-heading-count.test.js +27 -0
  44. package/tests/requirements-legacy-parsing.test.js +15 -0
  45. package/tests/requirements-parse-integration.test.js +44 -0
  46. package/tests/wait-for-ide-completion.test.js +56 -0
  47. package/tests/wait-for-ide-quota-detection-cursor-screenshot.test.js +61 -0
  48. package/tests/wait-for-ide-quota-detection-cursor.test.js +60 -0
  49. package/tests/wait-for-ide-quota-detection-negative.test.js +45 -0
  50. package/tests/wait-for-ide-quota-detection.test.js +59 -0
  51. package/verify_fix.js +36 -0
  52. 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,6 +406,11 @@ async function moveRequirementToVerify(repoPath, requirementText) {
378
406
 
379
407
  const content = await fs.readFile(reqPath, 'utf8');
380
408
  const lines = content.split('\n');
409
+
410
+ // Get computer filter from config
411
+ const { getComputerFilter } = require('../utils/config');
412
+ const computerFilter = await getComputerFilter();
413
+
381
414
  // Find the requirement by its title (in ### header format)
382
415
  // Only look in TODO section
383
416
  const normalizedRequirement = requirementText.trim();
@@ -469,175 +502,227 @@ async function moveRequirementToVerify(repoPath, requirementText) {
469
502
 
470
503
  // Check if we found a requirement in TODO section
471
504
  if (inTodoSection && line.startsWith('###')) {
472
- hasMoreTodoRequirements = true;
473
- break;
505
+ const title = line.replace(/^###\s*/, '').trim();
506
+ if (title && title.includes(computerFilter)) {
507
+ hasMoreTodoRequirements = true;
508
+ break;
509
+ }
474
510
  }
475
511
  }
476
512
 
477
513
  // If no more TODO requirements, log message
478
514
  if (!hasMoreTodoRequirements) {
479
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);
480
519
  }
481
-
482
-
483
-
484
- // Add AWS Kiro AI IDE GUI and CLI support
485
- const kiroAiIdeGuiSupport = '### AWS Kiro AI IDE GUI Support\n\nThis requirement has been moved to the TO VERIFY BY HUMAN section for further review.';
486
- const kiroAiIdeCliSupport = '### AWS Kiro AI IDE CLI Support\n\nThis requirement has been moved to the TO VERIFY BY HUMAN section for further review using the CLI.';
487
- lines.push(kiroAiIdeGuiSupport);
488
- lines.push(kiroAiIdeCliSupport);
489
-
490
- // Check if the requirement is in the Recycled section
491
- const recycledIndex = lines.findIndex(line => line.trim() === '## 📦 RECYCLED');
492
- if (recycledIndex !== -1) {
493
- const recycledBlock = lines.slice(recycledIndex + 1);
494
- const requirementIndexInRecycled = recycledBlock.findIndex(line => line.trim().startsWith('###') && line.trim().includes(snippet));
495
- if (requirementIndexInRecycled !== -1) {
496
- lines.splice(recycledIndex + 1 + requirementIndexInRecycled, 1);
520
+ // Find or create TO VERIFY BY HUMAN section with conflict resolution
521
+ // IMPORTANT: Do NOT match VERIFIED sections - only match TO VERIFY sections
522
+ const verifySectionVariants = [
523
+ '## 🔍 TO VERIFY BY HUMAN',
524
+ '## 🔍 TO VERIFY',
525
+ '## TO VERIFY',
526
+ '## ✅ TO VERIFY',
527
+ '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG',
528
+ '## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT',
529
+ '## 🖥️ COMPUTER FOCUS AREA MANAGEMENT',
530
+ '## 🔍 spec-kit: TO VERIFY BY HUMAN'
531
+ ];
532
+
533
+ let verifyIndex = -1;
534
+ for (let i = 0; i < lines.length; i++) {
535
+ const line = lines[i];
536
+ const trimmed = line.trim();
537
+
538
+ // Check each variant more carefully
539
+ for (const variant of verifySectionVariants) {
540
+ const variantTrimmed = variant.trim();
541
+ // Exact match or line starts with variant
542
+ if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
543
+ // Double-check: make sure it's NOT a VERIFIED section (without TO VERIFY)
544
+ if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) &&
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'))) {
546
+ verifyIndex = i;
547
+ break;
497
548
  }
498
549
  }
550
+ }
551
+ if (verifyIndex !== -1) break;
552
+ }
499
553
 
500
- // Find or create TO VERIFY BY HUMAN section
501
- // IMPORTANT: Do NOT match VERIFIED sections - only match TO VERIFY sections
502
- const verifySectionVariants = [
503
- '## 🔍 TO VERIFY BY HUMAN',
504
- '## 🔍 TO VERIFY',
505
- '## TO VERIFY',
506
- '## ✅ TO VERIFY',
507
- '## Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG'
508
- ];
554
+ if (verifyIndex === -1) {
555
+ // Create TO VERIFY section - place it BEFORE VERIFIED section if one exists, otherwise before CHANGELOG
556
+ const verifiedIndex = lines.findIndex(line => {
557
+ const trimmed = line.trim();
558
+ return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED') ||
559
+ (trimmed.startsWith('##') && trimmed.includes('VERIFIED') && !trimmed.includes('TO VERIFY'));
560
+ });
561
+ const changelogIndex = lines.findIndex(line => line.includes('## CHANGELOG'));
562
+ const manualFeedbackIndex = lines.findIndex(line => line.trim().startsWith('## ❓'));
563
+
564
+ // Prefer: before VERIFIED > before CHANGELOG > before manual feedback > at end
565
+ let insertionIndex = lines.length;
566
+ if (verifiedIndex > 0) {
567
+ insertionIndex = verifiedIndex;
568
+ } else if (changelogIndex > 0) {
569
+ insertionIndex = changelogIndex;
570
+ } else if (manualFeedbackIndex > 0) {
571
+ insertionIndex = manualFeedbackIndex;
572
+ }
509
573
 
510
- let verifyIndex = -1;
511
- for (let i = 0; i < lines.length; i++) {
512
- const line = lines[i];
513
- const trimmed = line.trim();
574
+ const block = [];
575
+ if (insertionIndex > 0 && lines[insertionIndex - 1].trim() !== '') {
576
+ block.push('');
577
+ }
578
+ block.push('## 🔍 spec-kit: TO VERIFY BY HUMAN', '### Automatic Registration and Tracking', '### User Registration', '');
579
+ block.push('## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT');
580
+ block.push('## 🖥️ COMPUTER FOCUS AREA MANAGEMENT');
581
+ lines.splice(insertionIndex, 0, ...block);
582
+ verifyIndex = lines.findIndex(line => {
583
+ const trimmed = line.trim();
584
+ return trimmed === '## 🔍 spec-kit: TO VERIFY BY HUMAN' || trimmed.startsWith('## 🔍 spec-kit: TO VERIFY BY HUMAN');
585
+ });
514
586
 
515
- // Check each variant more carefully
516
- for (const variant of verifySectionVariants) {
517
- const variantTrimmed = variant.trim();
518
- // Exact match or line starts with variant
519
- if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
520
- // Double-check: make sure it's NOT a VERIFIED section (without TO VERIFY)
521
- if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) &&
522
- (trimmed.includes('TO VERIFY') || trimmed.includes('Verified by AI screenshot'))) {
523
- verifyIndex = i;
524
- break;
525
- }
526
- }
527
- }
528
- if (verifyIndex !== -1) break;
529
- }
587
+ // Safety check: verifyIndex should be valid
588
+ if (verifyIndex === -1) {
589
+ console.error(t('auto.direct.verify.section.create.failed'));
590
+ return false;
591
+ }
592
+ }
530
593
 
531
- if (verifyIndex === -1) {
532
- // Create TO VERIFY section - place it BEFORE VERIFIED section if one exists, otherwise before CHANGELOG
533
- const verifiedIndex = lines.findIndex(line => {
534
- const trimmed = line.trim();
535
- return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED') ||
536
- (trimmed.startsWith('##') && trimmed.includes('VERIFIED') && !trimmed.includes('TO VERIFY'));
537
- });
538
- const changelogIndex = lines.findIndex(line => line.includes('## CHANGELOG'));
539
- const manualFeedbackIndex = lines.findIndex(line => line.trim().startsWith('## ❓'));
540
-
541
- // Prefer: before VERIFIED > before CHANGELOG > before manual feedback > at end
542
- let insertionIndex = lines.length;
543
- if (verifiedIndex > 0) {
544
- insertionIndex = verifiedIndex;
545
- } else if (changelogIndex > 0) {
546
- insertionIndex = changelogIndex;
547
- } else if (manualFeedbackIndex > 0) {
548
- insertionIndex = manualFeedbackIndex;
549
- }
594
+ // Safety check: verify we're not inserting into a VERIFIED section
595
+ const verifyLine = lines[verifyIndex] || '';
596
+ if (verifyLine.includes('## 📝 VERIFIED') || (verifyLine.trim().startsWith('##') && verifyLine.includes('VERIFIED') && !verifyLine.includes('TO VERIFY'))) {
597
+ console.error('ERROR: Attempted to insert into VERIFIED section instead of TO VERIFY');
598
+ return false;
599
+ }
550
600
 
551
- const block = [];
552
- if (insertionIndex > 0 && lines[insertionIndex - 1].trim() !== '') {
553
- block.push('');
554
- }
555
- block.push('## 🔍 TO VERIFY BY HUMAN', '');
556
- lines.splice(insertionIndex, 0, ...block);
557
- verifyIndex = lines.findIndex(line => {
558
- const trimmed = line.trim();
559
- return trimmed === '## 🔍 TO VERIFY BY HUMAN' || trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN');
560
- });
601
+ // Remove any existing duplicate of this requirement in TO VERIFY section
602
+ // Find the next section header after TO VERIFY
603
+ let nextSectionIndex = lines.length;
604
+ for (let i = verifyIndex + 1; i < lines.length; i++) {
605
+ const trimmed = lines[i].trim();
606
+ if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
607
+ nextSectionIndex = i;
608
+ break;
609
+ }
610
+ }
561
611
 
562
- // Safety check: verifyIndex should be valid
563
- if (verifyIndex === -1) {
564
- console.error(t('auto.direct.verify.section.create.failed'));
565
- return false;
612
+ // Search for duplicate requirement in TO VERIFY section
613
+ const requirementTitle = requirementBlock[0].trim().replace(/^###\s*/, '').trim();
614
+ for (let i = verifyIndex + 1; i < nextSectionIndex; i++) {
615
+ const line = lines[i];
616
+ const trimmed = line.trim();
617
+
618
+ if (trimmed.startsWith('###')) {
619
+ const existingTitle = trimmed.replace(/^###\s*/, '').trim();
620
+
621
+ // Check if this is a duplicate (exact match or contains/contained by)
622
+ if (existingTitle === requirementTitle ||
623
+ existingTitle.includes(requirementTitle) ||
624
+ requirementTitle.includes(existingTitle)) {
625
+
626
+ // Find the end of this existing requirement
627
+ let existingEndIndex = nextSectionIndex;
628
+ for (let j = i + 1; j < nextSectionIndex; j++) {
629
+ const nextLine = lines[j].trim();
630
+ if (nextLine.startsWith('###') || nextLine.startsWith('##')) {
631
+ existingEndIndex = j;
632
+ break;
633
+ }
566
634
  }
567
- }
568
-
569
635
 
570
- // Safety check: verify we're not inserting into a VERIFIED section
571
- const verifyLine = lines[verifyIndex] || '';
572
- if (verifyLine.includes('## 📝 VERIFIED') || (verifyLine.trim().startsWith('##') && verifyLine.includes('VERIFIED') && !verifyLine.includes('TO VERIFY'))) {
573
- console.error('ERROR: Attempted to insert into VERIFIED section instead of TO VERIFY');
574
- return false;
575
- }
636
+ // Remove the duplicate
637
+ lines.splice(i, existingEndIndex - i);
576
638
 
577
- // Remove any existing duplicate of this requirement in TO VERIFY section
578
- // Find the next section header after TO VERIFY
579
- let nextSectionIndex = lines.length;
580
- for (let i = verifyIndex + 1; i < lines.length; i++) {
581
- const trimmed = lines[i].trim();
582
- if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
583
- nextSectionIndex = i;
584
- break;
585
- }
639
+ // Adjust nextSectionIndex if needed
640
+ nextSectionIndex -= (existingEndIndex - i);
641
+ break;
586
642
  }
643
+ }
644
+ }
587
645
 
588
- // Search for duplicate requirement in TO VERIFY section
589
- const requirementTitle = requirementBlock[0].trim().replace(/^###\s*/, '').trim();
590
- for (let i = verifyIndex + 1; i < nextSectionIndex; i++) {
591
- const line = lines[i];
592
- const trimmed = line.trim();
646
+ // Insert requirement block at TOP of TO VERIFY section (right after section header)
647
+ let insertIndex = verifyIndex + 1;
593
648
 
594
- if (trimmed.startsWith('###')) {
595
- const existingTitle = trimmed.replace(/^###\s*/, '').trim();
649
+ // Ensure there's a blank line after the section header
650
+ if (lines[insertIndex]?.trim() !== '') {
651
+ lines.splice(insertIndex, 0, '');
652
+ insertIndex++;
653
+ }
596
654
 
597
- // Check if this is a duplicate (exact match or contains/contained by)
598
- if (existingTitle === requirementTitle ||
599
- existingTitle.includes(requirementTitle) ||
600
- requirementTitle.includes(existingTitle)) {
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
+ }
601
675
 
602
- // Find the end of this existing requirement
603
- let existingEndIndex = nextSectionIndex;
604
- for (let j = i + 1; j < nextSectionIndex; j++) {
605
- const nextLine = lines[j].trim();
606
- if (nextLine.startsWith('###') || nextLine.startsWith('##')) {
607
- existingEndIndex = j;
608
- break;
609
- }
610
- }
676
+ // Insert the requirement block
677
+ lines.splice(insertIndex, 0, ...requirementBlock);
611
678
 
612
- // Remove the duplicate
613
- lines.splice(i, existingEndIndex - i);
679
+ // Ensure there's a blank line after the requirement block
680
+ const afterIndex = insertIndex + requirementBlock.length;
681
+ if (afterIndex < lines.length && lines[afterIndex]?.trim() !== '') {
682
+ lines.splice(afterIndex, 0, '');
683
+ }
614
684
 
615
- // Adjust nextSectionIndex if needed
616
- nextSectionIndex -= (existingEndIndex - i);
617
- break;
618
- }
619
- }
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;
620
702
  }
703
+ }
704
+ if (verifiedIndex !== -1) break;
705
+ }
621
706
 
622
- // Insert requirement block at TOP of TO VERIFY section (right after section header)
623
- // Always insert immediately after the section header with proper blank line spacing
624
- let insertIndex = verifyIndex + 1;
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
+ }
625
717
 
626
- // Ensure there's a blank line after the section header
627
- if (lines[insertIndex]?.trim() !== '') {
628
- lines.splice(insertIndex, 0, '');
629
- insertIndex++;
630
- }
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);
631
724
 
632
- // Insert the requirement block with image paste ability
633
- const imagePaste = '### Image Paste\n\nYou can paste an image here by using the following format: ![image](image-url)\n\nExample: ![image](https://example.com/image.png)';
634
- lines.splice(insertIndex, 0, ...requirementBlock, imagePaste);
635
725
 
636
- // Ensure there's a blank line after the requirement block
637
- const afterIndex = insertIndex + requirementBlock.length + 1;
638
- if (afterIndex < lines.length && lines[afterIndex]?.trim() !== '') {
639
- lines.splice(afterIndex, 0, '');
640
- }
641
726
 
642
727
  // Write the file
643
728
  await fs.writeFile(reqPath, lines.join('\n'));
@@ -793,8 +878,31 @@ async function getAllAvailableProviders() {
793
878
  case 'windsurf':
794
879
  case 'vscode':
795
880
  case 'kiro':
796
- case 'antigravity': {
797
- 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
+ }
798
906
  break;
799
907
  }
800
908
  case 'ollama': {
@@ -829,7 +937,49 @@ async function getProviderConfig(excludeProvider = null) {
829
937
  const config = await getAutoConfig();
830
938
  const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
831
939
  const providers = await getAllAvailableProviders();
832
- const savedAgent = config.agent || config.ide;
940
+ const prefs = await getProviderPreferences();
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
+
967
+ // Get first ENABLED agent from provider preferences (same logic as interactive menu)
968
+ let firstEnabledAgent = null;
969
+ for (const agentId of prefs.order) {
970
+ if (prefs.enabled[agentId] !== false) {
971
+ firstEnabledAgent = agentId;
972
+ break;
973
+ }
974
+ }
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
+ }
833
983
 
834
984
  if (providers.length === 0) {
835
985
  return { status: 'no_providers', providers: [] };
@@ -845,17 +995,147 @@ async function getProviderConfig(excludeProvider = null) {
845
995
  const availableProviders = enabledProviders.filter(p => {
846
996
  // Exclude the specified provider and rate-limited providers
847
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
+ }
848
1001
  return false;
849
1002
  }
850
- 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;
851
1038
  });
852
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
+
853
1099
  let selection = null;
854
1100
  if (savedAgent && savedAgent !== excludeProvider) {
855
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
+ }
856
1106
  }
857
- if (!selection) {
858
- 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
+ }
859
1139
  }
860
1140
 
861
1141
  if (selection) {
@@ -879,10 +1159,16 @@ async function getProviderConfig(excludeProvider = null) {
879
1159
  };
880
1160
  }
881
1161
 
882
- async function acquireProviderConfig(excludeProvider = null) {
1162
+ async function acquireProviderConfig(excludeProvider = null, excludeModel = null) {
883
1163
  while (true) {
884
1164
  const selection = await getProviderConfig(excludeProvider);
885
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
+ }
886
1172
  return selection.provider;
887
1173
  }
888
1174
 
@@ -1329,6 +1615,13 @@ async function runIdeProviderIteration(providerConfig, repoPath) {
1329
1615
  console.log(chalk.cyan(`⚙️ ${t('auto.direct.provider.launching', { provider: providerConfig.displayName })}\n`));
1330
1616
 
1331
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
+ }
1332
1625
  const child = spawn(process.execPath, args, {
1333
1626
  cwd: repoPath,
1334
1627
  env: process.env,
@@ -1363,13 +1656,15 @@ async function runIdeProviderIteration(providerConfig, repoPath) {
1363
1656
  } else {
1364
1657
  const message = `${providerConfig.displayName} exited with code ${code}`;
1365
1658
  const antigravityRateLimit = checkAntigravityRateLimit(combinedOutput);
1659
+ const kiroRateLimit = checkKiroRateLimit(combinedOutput);
1366
1660
 
1367
1661
  resolve({
1368
1662
  success: false,
1369
1663
  error: combinedOutput ? `${message}\n${combinedOutput}` : message,
1370
1664
  output: combinedOutput,
1371
- rateLimited: isRateLimitMessage(combinedOutput) || antigravityRateLimit.isRateLimited,
1372
- antigravityRateLimited: antigravityRateLimit.isRateLimited
1665
+ rateLimited: isRateLimitMessage(combinedOutput) || antigravityRateLimit.isRateLimited || kiroRateLimit.isRateLimited,
1666
+ antigravityRateLimited: antigravityRateLimit.isRateLimited,
1667
+ kiroRateLimited: kiroRateLimit.isRateLimited
1373
1668
  });
1374
1669
  }
1375
1670
  });
@@ -1412,7 +1707,7 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1412
1707
  let foundInVerified = false;
1413
1708
 
1414
1709
  for (const line of lines) {
1415
- if (line.includes('## ✅ Verified by AI screenshot')) {
1710
+ if (line.includes('## ✅ Verified by AI')) {
1416
1711
  inVerifiedSection = true;
1417
1712
  continue;
1418
1713
  }
@@ -1442,6 +1737,10 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1442
1737
  for (const line of lines) {
1443
1738
  if (line.includes('🚦 Current Status')) {
1444
1739
  inStatusSection = true;
1740
+ if (line.includes('DONE')) {
1741
+ statusContainsDone = true;
1742
+ break;
1743
+ }
1445
1744
  continue;
1446
1745
  }
1447
1746
 
@@ -1463,21 +1762,72 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1463
1762
  return;
1464
1763
  }
1465
1764
 
1466
- // Check 3: Quota limit detection for Antigravity (after 2 minutes of waiting)
1467
- const elapsed = Date.now() - startTime;
1468
- if (ideType === 'antigravity' && !quotaHandled && elapsed >= 120000) {
1469
- console.log(chalk.yellow('\n⚠️ Antigravity quota limit detected after 2 minutes\n'));
1470
- 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
+
1471
1787
  watcher.close();
1472
- resolve({
1473
- success: false,
1474
- reason: 'antigravity-quota',
1475
- antigravityRateLimited: true // This triggers switching to next provider
1476
- });
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 });
1477
1791
  return;
1478
1792
  }
1479
1793
 
1480
- // 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;
1481
1831
  if (elapsed >= timeoutMs) {
1482
1832
  watcher.close();
1483
1833
  console.log(chalk.yellow(`\n⚠️ Timeout after ${Math.floor(elapsed / 60000)} minutes\n`));
@@ -1532,30 +1882,110 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
1532
1882
 
1533
1883
  if (!ideResult.success) {
1534
1884
  if (ideResult.antigravityRateLimited) {
1535
- await handleAntigravityRateLimit();
1536
- 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 };
1537
1891
  }
1892
+
1538
1893
  // CRITICAL: Mark provider as unavailable for ANY error so acquireProviderConfig() will skip it
1539
- 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
+
1540
1907
  return { success: false, error: ideResult.error || 'IDE provider failed' };
1541
1908
  }
1542
1909
 
1543
1910
  console.log(chalk.green(`✓ ${t('auto.direct.ide.prompt.sent')}`));
1544
1911
 
1545
1912
  // Wait for IDE agent to complete the work (IDE will update status to DONE itself)
1913
+ const ideCompletionStartTime = Date.now();
1546
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
+ }
1547
1942
 
1548
1943
  if (!completionResult.success) {
1944
+ // Special-case behavior for Antigravity CLI installs (they have special handling)
1549
1945
  if (completionResult.antigravityRateLimited) {
1550
1946
  console.log(chalk.yellow(`⚠️ ${t('auto.direct.provider.quota.exhausted', { provider: 'Antigravity' })}\n`));
1551
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
+
1552
1954
  return { success: false, error: 'Antigravity quota limit', shouldRetry: true };
1553
1955
  }
1554
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
+
1555
1978
  const errorMsg = completionResult.reason === 'timeout'
1556
1979
  ? 'IDE agent timed out'
1557
1980
  : 'IDE agent failed to complete';
1558
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
+
1559
1989
  return { success: false, error: errorMsg };
1560
1990
  }
1561
1991
 
@@ -1624,7 +2054,10 @@ async function runIteration(requirement, providerConfig, repoPath) {
1624
2054
  // ═══════════════════════════════════════════════════════════
1625
2055
  printStatusCard(requirement.text, 'ACT');
1626
2056
 
1627
- 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)));
1628
2061
 
1629
2062
  // Build context with actual file snippets
1630
2063
  let contextSection = '';
@@ -1743,27 +2176,97 @@ if (counts.todoCount === 0) {
1743
2176
  Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
1744
2177
 
1745
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();
1746
2205
 
1747
2206
  const result = await llm.call(providerConfig, prompt, {
1748
2207
  temperature: 0.1,
1749
2208
  maxTokens: 4096,
1750
2209
  onChunk: (chunk) => {
1751
- 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));
1752
2220
  fullResponse += chunk;
1753
2221
  },
1754
2222
  onComplete: () => {
1755
- 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();
1756
2231
  },
1757
2232
  onError: (error) => {
2233
+ stopSpinner();
1758
2234
  console.error(chalk.red(`\n✗ Error: ${error}`));
1759
2235
  }
1760
2236
  });
1761
2237
 
2238
+ // Ensure spinner is stopped even if callbacks didn't fire
2239
+ stopSpinner();
2240
+
1762
2241
  if (!result.success) {
1763
2242
  const combinedError = [result.error, fullResponse].filter(Boolean).join('\n').trim() || 'Unknown error';
1764
2243
  console.log(chalk.red(`\n✗ LLM call failed: ${combinedError}`));
1765
2244
  // CRITICAL: Mark provider as rate-limited for ANY error so acquireProviderConfig() will skip it
1766
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
+
1767
2270
  return { success: false, error: combinedError };
1768
2271
  }
1769
2272
 
@@ -1890,6 +2393,18 @@ Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
1890
2393
  const duration = Date.now() - startTime;
1891
2394
  providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
1892
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
+
1893
2408
  console.log();
1894
2409
  console.log(chalk.gray('─'.repeat(80)));
1895
2410
  console.log();
@@ -1920,10 +2435,6 @@ async function handleAutoStart(options) {
1920
2435
  console.log(chalk.gray('═'.repeat(80)));
1921
2436
  console.log();
1922
2437
 
1923
- // Get repo path
1924
- // Load configured stages
1925
- configuredStages = await getStages();
1926
-
1927
2438
  const repoPath = await getRepoPath();
1928
2439
  if (!repoPath) {
1929
2440
  console.log(chalk.red('✗ No repository configured'));
@@ -1931,14 +2442,25 @@ async function handleAutoStart(options) {
1931
2442
  return;
1932
2443
  }
1933
2444
 
1934
- console.log(chalk.white(t('auto.repository')), chalk.cyan(repoPath));
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();
1935
2458
 
1936
- // Get effective agent using centralized selector
1937
- const { getEffectiveAgent } = require('../utils/agent-selector');
1938
- const { PROVIDER_DEFINITIONS } = require('../utils/provider-registry');
1939
- const PROVIDER_DEFINITION_MAP = new Map(PROVIDER_DEFINITIONS.map(def => [def.id, def]));
2459
+ console.log(chalk.white(t('auto.repository')), chalk.cyan(repoPath));
1940
2460
 
1941
- const { effectiveAgent } = await getEffectiveAgent(options, PROVIDER_DEFINITIONS, PROVIDER_DEFINITION_MAP);
2461
+ // Use the agent that was already determined by provider preferences in interactive.js
2462
+ // No need to call getEffectiveAgent since we already have the correct agent
2463
+ const effectiveAgent = options.ide;
1942
2464
 
1943
2465
  // Get provider configuration for the selected agent
1944
2466
  let providerConfig = await acquireProviderConfig(effectiveAgent);
@@ -1948,11 +2470,13 @@ async function handleAutoStart(options) {
1948
2470
 
1949
2471
  console.log(chalk.white('Provider:'), chalk.cyan(providerConfig.displayName));
1950
2472
 
1951
- // Get max chats
1952
- const config = await getAutoConfig();
2473
+ // Get max chats (use already loaded config)
1953
2474
  const unlimited = !options.maxChats && !config.maxChats && config.neverStop;
1954
2475
  const maxChats = unlimited ? Number.MAX_SAFE_INTEGER : (options.maxChats || config.maxChats || 1);
1955
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 });
1956
2480
  console.log();
1957
2481
  console.log(chalk.gray('═'.repeat(80)));
1958
2482
 
@@ -1963,6 +2487,9 @@ async function handleAutoStart(options) {
1963
2487
  // Main loop
1964
2488
  let completedCount = 0;
1965
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
1966
2493
 
1967
2494
  for (let i = 0; i < maxChats; i++) {
1968
2495
  // Get current requirement first to check if there are any TODO items
@@ -1973,6 +2500,32 @@ async function handleAutoStart(options) {
1973
2500
  break;
1974
2501
  }
1975
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
+
1976
2529
  // Calculate current requirement number consistently (before processing)
1977
2530
  // This represents which requirement we're working on (1-based)
1978
2531
  const currentReqNumber = completedCount + failedCount + 1;
@@ -1981,11 +2534,16 @@ async function handleAutoStart(options) {
1981
2534
  console.log(chalk.bold.magenta(` ${t('auto.direct.requirement.header', { current: currentReqNumber, total: initialEffectiveMax })}`));
1982
2535
  console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
1983
2536
 
2537
+ // Update Auto Mode status with current iteration
2538
+ await updateAutoModeStatus(repoPath, { chatCount: currentReqNumber });
2539
+
1984
2540
  // Run iteration with full workflow
1985
2541
  const result = await runIteration(requirement, providerConfig, repoPath);
1986
2542
 
1987
2543
  if (result.success) {
1988
2544
  completedCount++;
2545
+ providerAttempts = 0; // Reset attempts on success
2546
+ lastRequirementText = null;
1989
2547
  console.log(chalk.bold.green(`✅ Requirement ${currentReqNumber}/${initialEffectiveMax} COMPLETE`));
1990
2548
  console.log(chalk.gray('Moving to next requirement...\n'));
1991
2549
 
@@ -2021,6 +2579,7 @@ async function handleAutoStart(options) {
2021
2579
  await new Promise(resolve => setTimeout(resolve, 300));
2022
2580
 
2023
2581
  // Exit this process - child continues with terminal
2582
+ await stopAutoMode('restarting');
2024
2583
  process.exit(0);
2025
2584
  } else {
2026
2585
  // Small delay before next iteration (if not restarting)
@@ -2038,7 +2597,7 @@ async function handleAutoStart(options) {
2038
2597
 
2039
2598
  console.log(chalk.yellow(`⚠️ ${errorType} detected, switching to next provider in your list...\n`));
2040
2599
 
2041
- const newProviderConfig = await acquireProviderConfig(providerConfig.provider);
2600
+ const newProviderConfig = await acquireProviderConfig(providerConfig.provider, providerConfig.model);
2042
2601
  if (newProviderConfig) {
2043
2602
  providerConfig = newProviderConfig;
2044
2603
  console.log(chalk.yellow(`⚠️ ${failedProvider} hit ${errorType.toLowerCase()}`));
@@ -2079,14 +2638,20 @@ async function handleAutoStart(options) {
2079
2638
  console.log(chalk.bold.green(`🎉 ${t('auto.direct.summary.final.message', { count: completedCount, plural: completedCount > 1 ? 's' : '' })}`));
2080
2639
  }
2081
2640
 
2641
+ // Stop Auto Mode status tracking
2642
+ await stopAutoMode('completed');
2643
+
2082
2644
  } catch (error) {
2083
2645
  console.error(chalk.red('\n' + t('auto.fatal.error')), error.message);
2084
2646
  if (error.stack) {
2085
2647
  console.log(chalk.gray(error.stack));
2086
2648
  }
2649
+
2650
+ // Stop Auto Mode status tracking on fatal error
2651
+ await stopAutoMode('error');
2087
2652
  process.exit(1);
2088
2653
  }
2089
2654
  }
2090
2655
 
2091
- module.exports = { handleAutoStart };
2656
+ module.exports = { handleAutoStart, waitForIdeCompletion, acquireProviderConfig };
2092
2657