vibecodingmachine-cli 2026.2.20-438 → 2026.2.26-1739

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 (101) hide show
  1. package/bin/auth/auth-compliance.js +126 -0
  2. package/bin/cli-program.js +104 -0
  3. package/bin/cli-setup.js +52 -0
  4. package/bin/commands/agent-commands.js +310 -0
  5. package/bin/commands/auto-commands.js +70 -0
  6. package/bin/commands/command-aliases.js +118 -0
  7. package/bin/commands/repo-commands.js +39 -0
  8. package/bin/commands/rui-commands.js +152 -0
  9. package/bin/config/cli-config.js +394 -0
  10. package/bin/init/environment-setup.js +84 -0
  11. package/bin/update/update-checker.js +126 -0
  12. package/bin/vibecodingmachine-new.js +50 -0
  13. package/bin/vibecodingmachine.js +29 -663
  14. package/package.json +8 -2
  15. package/src/commands/agents/add.js +277 -0
  16. package/src/commands/agents/check.js +380 -0
  17. package/src/commands/agents/list.js +471 -0
  18. package/src/commands/agents/remove.js +351 -0
  19. package/src/commands/analyze-file-sizes.js +428 -0
  20. package/src/commands/auto-direct/code-processor.js +282 -0
  21. package/src/commands/auto-direct/file-scanner.js +266 -0
  22. package/src/commands/auto-direct/provider-config.js +178 -0
  23. package/src/commands/auto-direct/provider-manager.js +219 -0
  24. package/src/commands/auto-direct/requirement-manager.js +172 -0
  25. package/src/commands/auto-direct/status-display.js +91 -0
  26. package/src/commands/auto-direct/utils.js +106 -0
  27. package/src/commands/auto-direct.js +875 -488
  28. package/src/commands/auto-execution.js +342 -0
  29. package/src/commands/auto-provider-management.js +102 -0
  30. package/src/commands/auto-requirement-management.js +161 -0
  31. package/src/commands/auto-status-helpers.js +141 -0
  32. package/src/commands/auto.js +105 -5155
  33. package/src/commands/check-compliance.js +536 -0
  34. package/src/commands/continuous-scan.js +119 -0
  35. package/src/commands/ide.js +16 -4
  36. package/src/commands/refactor-file.js +486 -0
  37. package/src/commands/requirements.js +301 -2
  38. package/src/commands/timeout.js +290 -0
  39. package/src/trui/TruiInterface.js +108 -0
  40. package/src/trui/agents/AgentInterface.js +580 -0
  41. package/src/utils/antigravity-installer.js +60 -6
  42. package/src/utils/clarification-actions.js +290 -0
  43. package/src/utils/config.js +123 -2
  44. package/src/utils/first-run.js +5 -5
  45. package/src/utils/ide-handlers.js +212 -0
  46. package/src/utils/interactive/clarification-actions.js +348 -0
  47. package/src/utils/interactive/core-ui.js +265 -0
  48. package/src/utils/interactive/file-backup.js +237 -0
  49. package/src/utils/interactive/file-import-export.js +305 -0
  50. package/src/utils/interactive/file-operations.js +49 -0
  51. package/src/utils/interactive/file-validation.js +276 -0
  52. package/src/utils/interactive/interactive-prompts.js +480 -0
  53. package/src/utils/interactive/requirement-actions.js +127 -0
  54. package/src/utils/interactive/requirement-crud.js +356 -0
  55. package/src/utils/interactive/requirements-navigation.js +286 -0
  56. package/src/utils/interactive.js +390 -3459
  57. package/src/utils/provider-checker/agent-checker.js +250 -0
  58. package/src/utils/provider-checker/agent-runner.js +450 -0
  59. package/src/utils/provider-checker/cli-installer.js +123 -0
  60. package/src/utils/provider-checker/cli-utils.js +15 -0
  61. package/src/utils/provider-checker/format-utils.js +32 -0
  62. package/src/utils/provider-checker/ide-manager.js +72 -0
  63. package/src/utils/provider-checker/ide-utils.js +71 -0
  64. package/src/utils/provider-checker/node-detector.js +56 -0
  65. package/src/utils/provider-checker/node-utils.js +61 -0
  66. package/src/utils/provider-checker/process-spawn.js +22 -0
  67. package/src/utils/provider-checker/process-utils.js +37 -0
  68. package/src/utils/provider-checker/provider-validator.js +160 -0
  69. package/src/utils/provider-checker/quota-checker.js +54 -0
  70. package/src/utils/provider-checker/quota-detector.js +44 -0
  71. package/src/utils/provider-checker/requirements-manager.js +94 -0
  72. package/src/utils/provider-checker/test-requirements.js +95 -0
  73. package/src/utils/provider-checker/time-formatter.js +18 -0
  74. package/src/utils/provider-checker-new.js +14 -0
  75. package/src/utils/provider-checker.js +12 -407
  76. package/src/utils/provider-checkers/ide-manager.js +128 -0
  77. package/src/utils/provider-checkers/node-executable-finder.js +51 -0
  78. package/src/utils/provider-checkers/provider-checker-core.js +172 -0
  79. package/src/utils/provider-checkers/provider-checker-main.js +107 -0
  80. package/src/utils/provider-manager.js +60 -4
  81. package/src/utils/provider-registry.js +26 -3
  82. package/src/utils/provider-utils.js +173 -0
  83. package/src/utils/quota-detectors.js +212 -0
  84. package/src/utils/requirement-action-handlers.js +288 -0
  85. package/src/utils/requirement-actions/clarification-actions.js +229 -0
  86. package/src/utils/requirement-actions/confirmation-prompts.js +93 -0
  87. package/src/utils/requirement-actions/file-operations.js +92 -0
  88. package/src/utils/requirement-actions/helpers.js +40 -0
  89. package/src/utils/requirement-actions/requirement-operations.js +335 -0
  90. package/src/utils/requirement-actions.js +46 -856
  91. package/src/utils/requirement-file-operations.js +259 -0
  92. package/src/utils/requirement-helpers.js +128 -0
  93. package/src/utils/requirement-management.js +279 -0
  94. package/src/utils/requirement-navigation.js +146 -0
  95. package/src/utils/requirement-organization.js +271 -0
  96. package/src/utils/simple-trui.js +75 -1
  97. package/src/utils/trui-navigation.js +28 -2
  98. package/src/utils/trui-req-tree.js +196 -11
  99. package/src/utils/trui-specifications.js +31 -1
  100. package/src/utils/interactive-backup.js +0 -5664
  101. package/src/utils/trui-provider-manager.js +0 -182
@@ -5,7 +5,25 @@
5
5
  */
6
6
 
7
7
  const chalk = require('chalk');
8
- const { DirectLLMManager, AppleScriptManager, QuotaDetector, IDEHealthTracker, t, detectLocale, setLocale } = require('vibecodingmachine-core');
8
+ const vibecodingmachineCore = require('vibecodingmachine-core');
9
+ const {
10
+ DirectLLMManager,
11
+ AppleScriptManager,
12
+ QuotaDetector,
13
+ IDEHealthTracker,
14
+ TimeoutCalculator,
15
+ t,
16
+ detectLocale,
17
+ setLocale
18
+ } = vibecodingmachineCore;
19
+
20
+ // Extract requirement management classes
21
+ const {
22
+ RequirementFileParser,
23
+ RequirementSequencer,
24
+ DefaultRequirementManager,
25
+ JSONStorage
26
+ } = vibecodingmachineCore;
9
27
 
10
28
  // Initialize locale detection for auto mode
11
29
  const detectedLocale = detectLocale();
@@ -26,6 +44,9 @@ const { checkKiroRateLimit, handleKiroRateLimit } = require('../utils/kiro-js-ha
26
44
  const { checkClineRateLimit, handleClineRateLimit } = require('../utils/cline-js-handler');
27
45
  const { startAutoMode, stopAutoMode, updateAutoModeStatus } = require('../utils/auto-mode');
28
46
 
47
+ // Global keyboard handler reference for cleanup
48
+ let keyboardHandler = null;
49
+
29
50
  // Status management will use in-process tracking instead of external file
30
51
  const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
31
52
 
@@ -37,7 +58,7 @@ const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
37
58
  async function ensureClineInstalled(forceInstall = false) {
38
59
  const { DirectLLMManager } = require('vibecodingmachine-core');
39
60
  const llm = new DirectLLMManager();
40
-
61
+
41
62
  // Check if already available
42
63
  if (!forceInstall && await llm.isClineAvailable()) {
43
64
  return true;
@@ -45,16 +66,16 @@ async function ensureClineInstalled(forceInstall = false) {
45
66
 
46
67
  const ora = require('ora');
47
68
  const { execSync } = require('child_process');
48
-
69
+
49
70
  const spinner = ora('Installing Cline CLI...').start();
50
-
71
+
51
72
  try {
52
73
  // Install Cline CLI globally
53
74
  execSync('npm install -g cline', { stdio: 'pipe', encoding: 'utf8' });
54
-
75
+
55
76
  // Verify installation
56
77
  const isAvailable = await llm.isClineAvailable();
57
-
78
+
58
79
  if (isAvailable) {
59
80
  spinner.succeed('Cline CLI installed successfully');
60
81
  console.log(chalk.green('✓ Cline CLI is now ready to use'));
@@ -459,317 +480,317 @@ async function moveRequirementToVerify(repoPath, requirementText) {
459
480
 
460
481
  const content = await fs.readFile(reqPath, 'utf8');
461
482
  const lines = content.split('\n');
462
- // Find the requirement by its title (in ### header format)
463
- // Only look in TODO section
464
- const normalizedRequirement = requirementText.trim();
465
- const snippet = normalizedRequirement.substring(0, 80);
466
- let requirementStartIndex = -1;
467
- let requirementEndIndex = -1;
468
- let inTodoSection = false;
469
-
470
- for (let i = 0; i < lines.length; i++) {
471
- const line = lines[i];
472
- const trimmed = line.trim();
473
-
474
- // Check if we're entering TODO section
475
- if (trimmed.startsWith('##') && trimmed.includes('Requirements not yet completed')) {
476
- inTodoSection = true;
477
- continue;
478
- }
479
-
480
- // Check if we're leaving TODO section
481
- if (inTodoSection && trimmed.startsWith('##') && !trimmed.startsWith('###') && !trimmed.includes('Requirements not yet completed')) {
482
- inTodoSection = false;
483
- }
483
+ // Find the requirement by its title (in ### header format)
484
+ // Only look in TODO section
485
+ const normalizedRequirement = requirementText.trim();
486
+ const snippet = normalizedRequirement.substring(0, 80);
487
+ let requirementStartIndex = -1;
488
+ let requirementEndIndex = -1;
489
+ let inTodoSection = false;
484
490
 
485
- // Only look for requirements in TODO section
486
- if (inTodoSection && trimmed.startsWith('###')) {
487
- const title = trimmed.replace(/^###\s*/, '').trim();
488
- if (title) {
489
- // Try multiple matching strategies
490
- const normalizedTitle = title.trim();
491
+ for (let i = 0; i < lines.length; i++) {
492
+ const line = lines[i];
493
+ const trimmed = line.trim();
491
494
 
492
- // Exact match
493
- if (normalizedTitle === normalizedRequirement) {
494
- requirementStartIndex = i;
495
- }
496
- // Check if either contains the other (for partial matches)
497
- else if (normalizedTitle.includes(normalizedRequirement) || normalizedRequirement.includes(normalizedTitle)) {
498
- requirementStartIndex = i;
495
+ // Check if we're entering TODO section
496
+ if (trimmed.startsWith('##') && trimmed.includes('Requirements not yet completed')) {
497
+ inTodoSection = true;
498
+ continue;
499
499
  }
500
- // Check snippet matches
501
- else if (normalizedTitle.includes(snippet) || snippet.includes(normalizedTitle.substring(0, 80))) {
502
- requirementStartIndex = i;
500
+
501
+ // Check if we're leaving TODO section
502
+ if (inTodoSection && trimmed.startsWith('##') && !trimmed.startsWith('###') && !trimmed.includes('Requirements not yet completed')) {
503
+ inTodoSection = false;
503
504
  }
504
505
 
505
- if (requirementStartIndex !== -1) {
506
- // Find the end of this requirement (next ### or ## header)
507
- for (let j = i + 1; j < lines.length; j++) {
508
- const nextLine = lines[j].trim();
509
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
510
- requirementEndIndex = j;
506
+ // Only look for requirements in TODO section
507
+ if (inTodoSection && trimmed.startsWith('###')) {
508
+ const title = trimmed.replace(/^###\s*/, '').trim();
509
+ if (title) {
510
+ // Try multiple matching strategies
511
+ const normalizedTitle = title.trim();
512
+
513
+ // Exact match
514
+ if (normalizedTitle === normalizedRequirement) {
515
+ requirementStartIndex = i;
516
+ }
517
+ // Check if either contains the other (for partial matches)
518
+ else if (normalizedTitle.includes(normalizedRequirement) || normalizedRequirement.includes(normalizedTitle)) {
519
+ requirementStartIndex = i;
520
+ }
521
+ // Check snippet matches
522
+ else if (normalizedTitle.includes(snippet) || snippet.includes(normalizedTitle.substring(0, 80))) {
523
+ requirementStartIndex = i;
524
+ }
525
+
526
+ if (requirementStartIndex !== -1) {
527
+ // Find the end of this requirement (next ### or ## header)
528
+ for (let j = i + 1; j < lines.length; j++) {
529
+ const nextLine = lines[j].trim();
530
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
531
+ requirementEndIndex = j;
532
+ break;
533
+ }
534
+ }
535
+ if (requirementEndIndex === -1) {
536
+ requirementEndIndex = lines.length;
537
+ }
511
538
  break;
512
539
  }
513
540
  }
514
- if (requirementEndIndex === -1) {
515
- requirementEndIndex = lines.length;
516
- }
517
- break;
518
541
  }
519
542
  }
520
- }
521
- }
522
543
 
523
- if (requirementStartIndex === -1) {
524
- console.log(chalk.yellow(`⚠️ ${t('auto.direct.requirement.not.found.todo', { requirement: requirementText.substring(0, 60) + '...' })}`));
525
- return false;
526
- }
544
+ if (requirementStartIndex === -1) {
545
+ console.log(chalk.yellow(`⚠️ ${t('auto.direct.requirement.not.found.todo', { requirement: requirementText.substring(0, 60) + '...' })}`));
546
+ return false;
547
+ }
527
548
 
528
- // Extract the entire requirement block
529
- const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
549
+ // Extract the entire requirement block
550
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
530
551
 
531
- // Remove the requirement from its current location
532
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
552
+ // Remove the requirement from its current location
553
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
533
554
 
534
- // Check if there are any more requirements in TODO section after removal
535
- let hasMoreTodoRequirements = false;
536
- inTodoSection = false; // Reset the existing variable
537
- for (let i = 0; i < lines.length; i++) {
538
- const line = lines[i].trim();
555
+ // Check if there are any more requirements in TODO section after removal
556
+ let hasMoreTodoRequirements = false;
557
+ inTodoSection = false; // Reset the existing variable
558
+ for (let i = 0; i < lines.length; i++) {
559
+ const line = lines[i].trim();
539
560
 
540
- // Check if we're entering TODO section
541
- if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
542
- inTodoSection = true;
543
- continue;
544
- }
561
+ // Check if we're entering TODO section
562
+ if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
563
+ inTodoSection = true;
564
+ continue;
565
+ }
545
566
 
546
- // Check if we're leaving TODO section
547
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
548
- break;
549
- }
567
+ // Check if we're leaving TODO section
568
+ if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
569
+ break;
570
+ }
550
571
 
551
- // Check if we found a requirement in TODO section
552
- if (inTodoSection && line.startsWith('###')) {
553
- const title = line.replace(/^###\s*/, '').trim();
554
- if (title) {
555
- hasMoreTodoRequirements = true;
556
- break;
572
+ // Check if we found a requirement in TODO section
573
+ if (inTodoSection && line.startsWith('###')) {
574
+ const title = line.replace(/^###\s*/, '').trim();
575
+ if (title) {
576
+ hasMoreTodoRequirements = true;
577
+ break;
578
+ }
579
+ }
557
580
  }
558
- }
559
- }
560
581
 
561
- // If no more TODO requirements, log message
562
- if (!hasMoreTodoRequirements) {
563
- console.log(chalk.green(`🎉 ${t('auto.direct.requirement.no.more.todo')}`));
564
- // Add a new requirement to the TODO section
565
- const newRequirement = '### R14: TESTREQ1 with promo code FRIENDSANDFAMILYROCK';
566
- lines.push(newRequirement);
567
- }
582
+ // If no more TODO requirements, log message
583
+ if (!hasMoreTodoRequirements) {
584
+ console.log(chalk.green(`🎉 ${t('auto.direct.requirement.no.more.todo')}`));
585
+ // Add a new requirement to the TODO section
586
+ const newRequirement = '### R14: TESTREQ1 with promo code FRIENDSANDFAMILYROCK';
587
+ lines.push(newRequirement);
588
+ }
589
+
590
+ // Find or create TO VERIFY BY HUMAN section with conflict resolution
591
+ // IMPORTANT: Do NOT match VERIFIED sections - only match TO VERIFY sections
592
+ const verifySectionVariants = [
593
+ '## 🔍 TO VERIFY BY HUMAN',
594
+ '## 🔍 TO VERIFY',
595
+ '## TO VERIFY',
596
+ '## ✅ TO VERIFY',
597
+ '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG',
598
+ '## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT',
599
+ '## 🖥️ COMPUTER FOCUS AREA MANAGEMENT',
600
+ '## 🔍 spec-kit: TO VERIFY BY HUMAN'
601
+ ];
602
+
603
+ let verifyIndex = -1;
604
+ for (let i = 0; i < lines.length; i++) {
605
+ const line = lines[i];
606
+ const trimmed = line.trim();
607
+
608
+ // Check each variant more carefully
609
+ for (const variant of verifySectionVariants) {
610
+ const variantTrimmed = variant.trim();
611
+ // Exact match or line starts with variant
612
+ if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
613
+ // Double-check: make sure it's NOT a VERIFIED section (without TO VERIFY)
614
+ if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) &&
615
+ (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'))) {
616
+ verifyIndex = i;
617
+ break;
618
+ }
619
+ }
620
+ }
621
+ if (verifyIndex !== -1) break;
622
+ }
568
623
 
569
- // Find or create TO VERIFY BY HUMAN section with conflict resolution
570
- // IMPORTANT: Do NOT match VERIFIED sections - only match TO VERIFY sections
571
- const verifySectionVariants = [
572
- '## 🔍 TO VERIFY BY HUMAN',
573
- '## 🔍 TO VERIFY',
574
- '## TO VERIFY',
575
- '## ✅ TO VERIFY',
576
- '## Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG',
577
- '## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT',
578
- '## 🖥️ COMPUTER FOCUS AREA MANAGEMENT',
579
- '## 🔍 spec-kit: TO VERIFY BY HUMAN'
580
- ];
581
-
582
- let verifyIndex = -1;
583
- for (let i = 0; i < lines.length; i++) {
584
- const line = lines[i];
585
- const trimmed = line.trim();
586
-
587
- // Check each variant more carefully
588
- for (const variant of verifySectionVariants) {
589
- const variantTrimmed = variant.trim();
590
- // Exact match or line starts with variant
591
- if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
592
- // Double-check: make sure it's NOT a VERIFIED section (without TO VERIFY)
593
- if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) &&
594
- (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'))) {
595
- verifyIndex = i;
624
+ if (verifyIndex === -1) {
625
+ // Create TO VERIFY section - place it BEFORE VERIFIED section if one exists, otherwise before CHANGELOG
626
+ const verifiedIndex = lines.findIndex(line => {
627
+ const trimmed = line.trim();
628
+ return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED') ||
629
+ (trimmed.startsWith('##') && trimmed.includes('VERIFIED') && !trimmed.includes('TO VERIFY'));
630
+ });
631
+ const changelogIndex = lines.findIndex(line => line.includes('## CHANGELOG'));
632
+ const manualFeedbackIndex = lines.findIndex(line => line.trim().startsWith('## ❓'));
633
+
634
+ // Prefer: before VERIFIED > before CHANGELOG > before manual feedback > at end
635
+ let insertionIndex = lines.length;
636
+ if (verifiedIndex > 0) {
637
+ insertionIndex = verifiedIndex;
638
+ } else if (changelogIndex > 0) {
639
+ insertionIndex = changelogIndex;
640
+ } else if (manualFeedbackIndex > 0) {
641
+ insertionIndex = manualFeedbackIndex;
642
+ }
643
+
644
+ const block = [];
645
+ if (insertionIndex > 0 && lines[insertionIndex - 1].trim() !== '') {
646
+ block.push('');
647
+ }
648
+ block.push('## 🔍 spec-kit: TO VERIFY BY HUMAN', '### Automatic Registration and Tracking', '### User Registration', '');
649
+ block.push('## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT');
650
+ block.push('## 🖥️ COMPUTER FOCUS AREA MANAGEMENT');
651
+ lines.splice(insertionIndex, 0, ...block);
652
+ verifyIndex = lines.findIndex(line => {
653
+ const trimmed = line.trim();
654
+ return trimmed === '## 🔍 spec-kit: TO VERIFY BY HUMAN' || trimmed.startsWith('## 🔍 spec-kit: TO VERIFY BY HUMAN');
655
+ });
656
+
657
+ // Safety check: verifyIndex should be valid
658
+ if (verifyIndex === -1) {
659
+ console.error(t('auto.direct.verify.section.create.failed'));
660
+ return false;
661
+ }
662
+ }
663
+
664
+ // Safety check: verify we're not inserting into a VERIFIED section
665
+ const verifyLine = lines[verifyIndex] || '';
666
+ if (verifyLine.includes('## 📝 VERIFIED') || (verifyLine.trim().startsWith('##') && verifyLine.includes('VERIFIED') && !verifyLine.includes('TO VERIFY'))) {
667
+ console.error('ERROR: Attempted to insert into VERIFIED section instead of TO VERIFY');
668
+ return false;
669
+ }
670
+
671
+ // Remove any existing duplicate of this requirement in TO VERIFY section
672
+ // Find the next section header after TO VERIFY
673
+ let nextSectionIndex = lines.length;
674
+ for (let i = verifyIndex + 1; i < lines.length; i++) {
675
+ const trimmed = lines[i].trim();
676
+ if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
677
+ nextSectionIndex = i;
596
678
  break;
597
679
  }
598
680
  }
599
- }
600
- if (verifyIndex !== -1) break;
601
- }
602
681
 
603
- if (verifyIndex === -1) {
604
- // Create TO VERIFY section - place it BEFORE VERIFIED section if one exists, otherwise before CHANGELOG
605
- const verifiedIndex = lines.findIndex(line => {
606
- const trimmed = line.trim();
607
- return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED') ||
608
- (trimmed.startsWith('##') && trimmed.includes('VERIFIED') && !trimmed.includes('TO VERIFY'));
609
- });
610
- const changelogIndex = lines.findIndex(line => line.includes('## CHANGELOG'));
611
- const manualFeedbackIndex = lines.findIndex(line => line.trim().startsWith('## ❓'));
612
-
613
- // Prefer: before VERIFIED > before CHANGELOG > before manual feedback > at end
614
- let insertionIndex = lines.length;
615
- if (verifiedIndex > 0) {
616
- insertionIndex = verifiedIndex;
617
- } else if (changelogIndex > 0) {
618
- insertionIndex = changelogIndex;
619
- } else if (manualFeedbackIndex > 0) {
620
- insertionIndex = manualFeedbackIndex;
621
- }
682
+ // Search for duplicate requirement in TO VERIFY section
683
+ const requirementTitle = requirementBlock[0].trim().replace(/^###\s*/, '').trim();
684
+ for (let i = verifyIndex + 1; i < nextSectionIndex; i++) {
685
+ const line = lines[i];
686
+ const trimmed = line.trim();
622
687
 
623
- const block = [];
624
- if (insertionIndex > 0 && lines[insertionIndex - 1].trim() !== '') {
625
- block.push('');
626
- }
627
- block.push('## 🔍 spec-kit: TO VERIFY BY HUMAN', '### Automatic Registration and Tracking', '### User Registration', '');
628
- block.push('## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT');
629
- block.push('## 🖥️ COMPUTER FOCUS AREA MANAGEMENT');
630
- lines.splice(insertionIndex, 0, ...block);
631
- verifyIndex = lines.findIndex(line => {
632
- const trimmed = line.trim();
633
- return trimmed === '## 🔍 spec-kit: TO VERIFY BY HUMAN' || trimmed.startsWith('## 🔍 spec-kit: TO VERIFY BY HUMAN');
634
- });
688
+ if (trimmed.startsWith('###')) {
689
+ const existingTitle = trimmed.replace(/^###\s*/, '').trim();
635
690
 
636
- // Safety check: verifyIndex should be valid
637
- if (verifyIndex === -1) {
638
- console.error(t('auto.direct.verify.section.create.failed'));
639
- return false;
640
- }
641
- }
691
+ // Check if this is a duplicate (exact match or contains/contained by)
692
+ if (existingTitle === requirementTitle ||
693
+ existingTitle.includes(requirementTitle) ||
694
+ requirementTitle.includes(existingTitle)) {
642
695
 
643
- // Safety check: verify we're not inserting into a VERIFIED section
644
- const verifyLine = lines[verifyIndex] || '';
645
- if (verifyLine.includes('## 📝 VERIFIED') || (verifyLine.trim().startsWith('##') && verifyLine.includes('VERIFIED') && !verifyLine.includes('TO VERIFY'))) {
646
- console.error('ERROR: Attempted to insert into VERIFIED section instead of TO VERIFY');
647
- return false;
648
- }
696
+ // Find the end of this existing requirement
697
+ let existingEndIndex = nextSectionIndex;
698
+ for (let j = i + 1; j < nextSectionIndex; j++) {
699
+ const nextLine = lines[j].trim();
700
+ if (nextLine.startsWith('###') || nextLine.startsWith('##')) {
701
+ existingEndIndex = j;
702
+ break;
703
+ }
704
+ }
649
705
 
650
- // Remove any existing duplicate of this requirement in TO VERIFY section
651
- // Find the next section header after TO VERIFY
652
- let nextSectionIndex = lines.length;
653
- for (let i = verifyIndex + 1; i < lines.length; i++) {
654
- const trimmed = lines[i].trim();
655
- if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
656
- nextSectionIndex = i;
657
- break;
658
- }
659
- }
706
+ // Remove the duplicate
707
+ lines.splice(i, existingEndIndex - i);
660
708
 
661
- // Search for duplicate requirement in TO VERIFY section
662
- const requirementTitle = requirementBlock[0].trim().replace(/^###\s*/, '').trim();
663
- for (let i = verifyIndex + 1; i < nextSectionIndex; i++) {
664
- const line = lines[i];
665
- const trimmed = line.trim();
666
-
667
- if (trimmed.startsWith('###')) {
668
- const existingTitle = trimmed.replace(/^###\s*/, '').trim();
669
-
670
- // Check if this is a duplicate (exact match or contains/contained by)
671
- if (existingTitle === requirementTitle ||
672
- existingTitle.includes(requirementTitle) ||
673
- requirementTitle.includes(existingTitle)) {
674
-
675
- // Find the end of this existing requirement
676
- let existingEndIndex = nextSectionIndex;
677
- for (let j = i + 1; j < nextSectionIndex; j++) {
678
- const nextLine = lines[j].trim();
679
- if (nextLine.startsWith('###') || nextLine.startsWith('##')) {
680
- existingEndIndex = j;
709
+ // Adjust nextSectionIndex if needed
710
+ nextSectionIndex -= (existingEndIndex - i);
681
711
  break;
682
712
  }
683
713
  }
684
-
685
- // Remove the duplicate
686
- lines.splice(i, existingEndIndex - i);
687
-
688
- // Adjust nextSectionIndex if needed
689
- nextSectionIndex -= (existingEndIndex - i);
690
- break;
691
714
  }
692
- }
693
- }
694
715
 
695
- // Insert requirement block at TOP of TO VERIFY section (right after section header)
696
- let insertIndex = verifyIndex + 1;
716
+ // Insert requirement block at TOP of TO VERIFY section (right after section header)
717
+ let insertIndex = verifyIndex + 1;
697
718
 
698
- // Ensure there's a blank line after the section header
699
- if (lines[insertIndex]?.trim() !== '') {
700
- lines.splice(insertIndex, 0, '');
701
- insertIndex++;
702
- }
719
+ // Ensure there's a blank line after the section header
720
+ if (lines[insertIndex]?.trim() !== '') {
721
+ lines.splice(insertIndex, 0, '');
722
+ insertIndex++;
723
+ }
703
724
 
704
- // If a Conflict Resolution header already exists immediately after the section header, reuse it
705
- const conflictHeader = '### Conflict Resolution:';
706
- if (lines[insertIndex]?.trim().startsWith(conflictHeader)) {
707
- // Move insertion point to after the existing header
708
- insertIndex++;
709
- // Ensure there's a blank line after the header before inserting the requirement
710
- if (lines[insertIndex]?.trim() !== '') {
711
- lines.splice(insertIndex, 0, '');
712
- insertIndex++;
713
- }
714
- } else {
715
- // Insert the conflict header
716
- lines.splice(insertIndex, 0, conflictHeader);
717
- insertIndex++;
718
- // Ensure a blank line after the header
719
- if (lines[insertIndex]?.trim() !== '') {
720
- lines.splice(insertIndex, 0, '');
721
- insertIndex++;
722
- }
723
- }
725
+ // If a Conflict Resolution header already exists immediately after the section header, reuse it
726
+ const conflictHeader = '### Conflict Resolution:';
727
+ if (lines[insertIndex]?.trim().startsWith(conflictHeader)) {
728
+ // Move insertion point to after the existing header
729
+ insertIndex++;
730
+ // Ensure there's a blank line after the header before inserting the requirement
731
+ if (lines[insertIndex]?.trim() !== '') {
732
+ lines.splice(insertIndex, 0, '');
733
+ insertIndex++;
734
+ }
735
+ } else {
736
+ // Insert the conflict header
737
+ lines.splice(insertIndex, 0, conflictHeader);
738
+ insertIndex++;
739
+ // Ensure a blank line after the header
740
+ if (lines[insertIndex]?.trim() !== '') {
741
+ lines.splice(insertIndex, 0, '');
742
+ insertIndex++;
743
+ }
744
+ }
724
745
 
725
- // Insert the requirement block
726
- lines.splice(insertIndex, 0, ...requirementBlock);
746
+ // Insert the requirement block
747
+ lines.splice(insertIndex, 0, ...requirementBlock);
727
748
 
728
- // Ensure there's a blank line after the requirement block
729
- const afterIndex = insertIndex + requirementBlock.length;
730
- if (afterIndex < lines.length && lines[afterIndex]?.trim() !== '') {
731
- lines.splice(afterIndex, 0, '');
732
- }
749
+ // Ensure there's a blank line after the requirement block
750
+ const afterIndex = insertIndex + requirementBlock.length;
751
+ if (afterIndex < lines.length && lines[afterIndex]?.trim() !== '') {
752
+ lines.splice(afterIndex, 0, '');
753
+ }
733
754
 
734
- // Move the requirement to the VERIFIED section
735
- const verifiedSectionVariants = [
736
- '## 📝 VERIFIED',
737
- '## VERIFIED'
738
- ];
739
- let verifiedIndex = -1;
740
- for (let i = 0; i < lines.length; i++) {
741
- const line = lines[i];
742
- const trimmed = line.trim();
743
-
744
- // Check each variant more carefully
745
- for (const variant of verifiedSectionVariants) {
746
- const variantTrimmed = variant.trim();
747
- // Exact match or line starts with variant
748
- if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
749
- verifiedIndex = i;
750
- break;
755
+ // Move the requirement to the VERIFIED section
756
+ const verifiedSectionVariants = [
757
+ '## 📝 VERIFIED',
758
+ '## VERIFIED'
759
+ ];
760
+ let verifiedIndex = -1;
761
+ for (let i = 0; i < lines.length; i++) {
762
+ const line = lines[i];
763
+ const trimmed = line.trim();
764
+
765
+ // Check each variant more carefully
766
+ for (const variant of verifiedSectionVariants) {
767
+ const variantTrimmed = variant.trim();
768
+ // Exact match or line starts with variant
769
+ if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
770
+ verifiedIndex = i;
771
+ break;
772
+ }
773
+ }
774
+ if (verifiedIndex !== -1) break;
751
775
  }
752
- }
753
- if (verifiedIndex !== -1) break;
754
- }
755
776
 
756
- if (verifiedIndex === -1) {
757
- // Create VERIFIED section - place it after TO VERIFY section
758
- const block = [];
759
- block.push('## 📝 VERIFIED');
760
- lines.splice(verifyIndex + 1, 0, ...block);
761
- verifiedIndex = lines.findIndex(line => {
762
- const trimmed = line.trim();
763
- return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED');
764
- });
765
- }
777
+ if (verifiedIndex === -1) {
778
+ // Create VERIFIED section - place it after TO VERIFY section
779
+ const block = [];
780
+ block.push('## 📝 VERIFIED');
781
+ lines.splice(verifyIndex + 1, 0, ...block);
782
+ verifiedIndex = lines.findIndex(line => {
783
+ const trimmed = line.trim();
784
+ return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED');
785
+ });
786
+ }
766
787
 
767
- // Insert the requirement block at the end of the VERIFIED section
768
- let insertIndexVerified = verifiedIndex + 1;
769
- while (insertIndexVerified < lines.length && lines[insertIndexVerified].trim().startsWith('###')) {
770
- insertIndexVerified++;
771
- }
772
- lines.splice(insertIndexVerified, 0, ...requirementBlock);
788
+ // Insert the requirement block at the end of the VERIFIED section
789
+ let insertIndexVerified = verifiedIndex + 1;
790
+ while (insertIndexVerified < lines.length && lines[insertIndexVerified].trim().startsWith('###')) {
791
+ insertIndexVerified++;
792
+ }
793
+ lines.splice(insertIndexVerified, 0, ...requirementBlock);
773
794
 
774
795
 
775
796
 
@@ -862,6 +883,7 @@ async function getAllAvailableProviders() {
862
883
  const awsSecretKey = process.env.AWS_SECRET_ACCESS_KEY || config.awsSecretAccessKey;
863
884
  const claudeCodeAvailable = await llm.isClaudeCodeAvailable();
864
885
  const clineAvailable = await llm.isClineAvailable();
886
+ const openCodeAvailable = await llm.isOpenCodeAvailable();
865
887
  const ollamaAvailable = await llm.isOllamaAvailable();
866
888
  let ollamaModels = [];
867
889
  if (ollamaAvailable) {
@@ -944,7 +966,7 @@ async function getAllAvailableProviders() {
944
966
  console.log(chalk.yellow(`\n🔧 Cline CLI not found, auto-installing...`));
945
967
  clineAvailable = await ensureClineInstalled();
946
968
  }
947
-
969
+
948
970
  if (!clineAvailable) {
949
971
  skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'Cline CLI not installed — run: npm install -g cline' });
950
972
  continue;
@@ -955,6 +977,17 @@ async function getAllAvailableProviders() {
955
977
  });
956
978
  break;
957
979
  }
980
+ case 'opencode': {
981
+ if (!openCodeAvailable) {
982
+ skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'OpenCode CLI not installed — visit https://opencode.ai to install' });
983
+ continue;
984
+ }
985
+ providers.push({
986
+ ...base,
987
+ model: def.defaultModel
988
+ });
989
+ break;
990
+ }
958
991
  case 'cursor':
959
992
  case 'windsurf':
960
993
  case 'vscode':
@@ -1030,15 +1063,15 @@ async function getProviderConfig(excludeProvider = null) {
1030
1063
  const now = Date.now();
1031
1064
  const timeSinceMarked = now - (rateLimitInfo.markedAt || 0);
1032
1065
  const minutesSinceMarked = timeSinceMarked / (1000 * 60);
1033
-
1066
+
1034
1067
  // Clear rate limit if:
1035
1068
  // 1. It was marked due to platform issues, OR
1036
1069
  // 2. It was marked within the last 5 minutes (likely a recent platform issue)
1037
- const isPlatformError = rateLimitInfo.reason &&
1038
- (rateLimitInfo.reason.includes('xdg-open') ||
1039
- rateLimitInfo.reason.includes('command not found') ||
1040
- rateLimitInfo.reason.includes('Unable to find application'));
1041
-
1070
+ const isPlatformError = rateLimitInfo.reason &&
1071
+ (rateLimitInfo.reason.includes('xdg-open') ||
1072
+ rateLimitInfo.reason.includes('command not found') ||
1073
+ rateLimitInfo.reason.includes('Unable to find application'));
1074
+
1042
1075
  if (isPlatformError || minutesSinceMarked < 5) {
1043
1076
  console.log(chalk.yellow(`⚠️ Clearing incorrect rate limit for Replit (marked ${minutesSinceMarked.toFixed(1)} minutes ago: ${isPlatformError ? 'platform error' : 'recent error'})`));
1044
1077
  delete providerManager.rateLimits['replit:replit'];
@@ -1056,7 +1089,7 @@ async function getProviderConfig(excludeProvider = null) {
1056
1089
  }
1057
1090
  }
1058
1091
  const savedAgent = firstEnabledAgent || config.agent || config.ide;
1059
-
1092
+
1060
1093
  if (process.env.DEBUG_PROVIDER_SELECTION) {
1061
1094
  console.log(chalk.gray(`[DEBUG] firstEnabledAgent: ${firstEnabledAgent}`));
1062
1095
  console.log(chalk.gray(`[DEBUG] config.agent: ${config.agent}`));
@@ -1076,42 +1109,33 @@ async function getProviderConfig(excludeProvider = null) {
1076
1109
  }
1077
1110
 
1078
1111
  const availableProviders = enabledProviders.filter(p => {
1079
- // Exclude the specified provider and rate-limited providers
1112
+ // Exclude specified provider and rate-limited providers
1080
1113
  if (excludeProvider && p.provider === excludeProvider) {
1081
1114
  if (process.env.DEBUG_PROVIDER_SELECTION) {
1082
1115
  console.log(chalk.gray(`[DEBUG] Excluding ${p.provider} due to excludeProvider=${excludeProvider}`));
1083
1116
  }
1084
1117
  return false;
1085
1118
  }
1086
-
1119
+
1087
1120
  // For IDE providers, only consider them rate limited if they've actually been used
1088
- // IDE providers that haven't been used yet should always be available for launch/installation
1089
1121
  if (p.type === 'ide') {
1090
- // Check if this IDE provider has been used before by looking for any rate limit entries
1091
- const hasBeenUsed = providerManager.rateLimits[`${p.provider}:`] ||
1092
- Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`));
1093
-
1094
- // Debug: Log IDE provider status
1095
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1096
- console.log(chalk.gray(`[DEBUG] IDE Provider ${p.provider}: hasBeenUsed=${hasBeenUsed}, isRateLimited=${providerManager.isRateLimited(p.provider, p.model)}`));
1097
- console.log(chalk.gray(`[DEBUG] Rate limits for ${p.provider}:`, JSON.stringify(providerManager.rateLimits, null, 2)));
1098
- }
1099
-
1100
- // If it has been used, check if it's currently rate limited
1122
+ const hasBeenUsed = providerManager.rateLimits[`${p.provider}:`] ||
1123
+ Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`));
1124
+
1101
1125
  if (hasBeenUsed) {
1102
1126
  const isRateLimited = providerManager.isRateLimited(p.provider, p.model);
1103
1127
  if (process.env.DEBUG_PROVIDER_SELECTION && isRateLimited) {
1104
- console.log(chalk.gray(`[DEBUG] ${p.provider} is rate limited, excluding`));
1128
+ console.log(chalk.gray(`[DEBUG] IDE Provider ${p.provider} is rate limited, excluding`));
1105
1129
  }
1106
1130
  return !isRateLimited;
1107
1131
  }
1108
1132
  // If it hasn't been used yet, it's always available
1109
1133
  if (process.env.DEBUG_PROVIDER_SELECTION) {
1110
- console.log(chalk.gray(`[DEBUG] ${p.provider} hasn't been used yet, including`));
1134
+ console.log(chalk.gray(`[DEBUG] IDE Provider ${p.provider} hasn't been used yet, including`));
1111
1135
  }
1112
1136
  return true;
1113
1137
  }
1114
-
1138
+
1115
1139
  // For non-IDE providers, check rate limits normally
1116
1140
  const isRateLimited = providerManager.isRateLimited(p.provider, p.model);
1117
1141
  if (process.env.DEBUG_PROVIDER_SELECTION && isRateLimited) {
@@ -1120,53 +1144,7 @@ async function getProviderConfig(excludeProvider = null) {
1120
1144
  return !isRateLimited;
1121
1145
  });
1122
1146
 
1123
- // If no providers are available, try to enable some IDE providers that haven't been used yet
1124
- if (availableProviders.length === 0) {
1125
- const { providers: allProviders } = await getAllAvailableProviders();
1126
- const unusedIdeProviders = allProviders.filter(p =>
1127
- p.type === 'ide' &&
1128
- !enabledProviders.some(ep => ep.provider === p.provider) &&
1129
- !providerManager.rateLimits[`${p.provider}:`] &&
1130
- !Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`))
1131
- );
1132
-
1133
- if (unusedIdeProviders.length > 0) {
1134
- console.log(chalk.yellow(`⚠️ No enabled providers available. Auto-enabling unused IDE providers...`));
1135
- const prefs = await getProviderPreferences();
1136
-
1137
- // Enable the first few unused IDE providers
1138
- const providersToEnable = unusedIdeProviders.slice(0, 3);
1139
- for (const provider of providersToEnable) {
1140
- prefs.enabled[provider.provider] = true;
1141
- console.log(chalk.gray(` ✓ Enabled ${provider.displayName}`));
1142
- }
1143
-
1144
- await saveProviderPreferences(prefs.order, prefs.enabled);
1145
-
1146
- // Recalculate available providers with the newly enabled ones
1147
- const newlyEnabledProviders = allProviders.filter(p =>
1148
- prefs.enabled[p.provider] !== false
1149
- );
1150
-
1151
- const newlyAvailableProviders = newlyEnabledProviders.filter(p => {
1152
- // Exclude the specified provider
1153
- if (excludeProvider && p.provider === excludeProvider) {
1154
- return false;
1155
- }
1156
-
1157
- // For newly enabled IDE providers, they haven't been used yet, so they're always available
1158
- if (p.type === 'ide') {
1159
- return true;
1160
- }
1161
-
1162
- // For non-IDE providers, check rate limits normally
1163
- return !providerManager.isRateLimited(p.provider, p.model);
1164
- });
1165
-
1166
- // Add the newly available providers to the list
1167
- availableProviders.push(...newlyAvailableProviders);
1168
- }
1169
- }
1147
+
1170
1148
 
1171
1149
  // Debug: Log provider selection details
1172
1150
  if (process.env.DEBUG_PROVIDER_SELECTION) {
@@ -1187,25 +1165,25 @@ async function getProviderConfig(excludeProvider = null) {
1187
1165
  console.log(chalk.gray(`[DEBUG] Found selection: ${selection ? selection.provider : 'null'}`));
1188
1166
  }
1189
1167
  }
1190
-
1168
+
1191
1169
  // If no selection or the selected provider is rate limited, try to find an unused IDE provider
1192
1170
  if (!selection || (selection.type === 'ide' && providerManager.isRateLimited(selection.provider, selection.model))) {
1193
1171
  if (process.env.DEBUG_PROVIDER_SELECTION) {
1194
1172
  console.log(chalk.gray(`[DEBUG] No valid selection, trying unused IDE providers`));
1195
1173
  console.log(chalk.gray(`[DEBUG] Reason: ${!selection ? 'no selection' : 'selection is rate limited'}`));
1196
1174
  }
1197
-
1175
+
1198
1176
  // Prioritize newly enabled IDE providers that haven't been used
1199
- const unusedIdeProviders = availableProviders.filter(p =>
1200
- p.type === 'ide' &&
1201
- !providerManager.rateLimits[`${p.provider}:`] &&
1177
+ const unusedIdeProviders = availableProviders.filter(p =>
1178
+ p.type === 'ide' &&
1179
+ !providerManager.rateLimits[`${p.provider}:`] &&
1202
1180
  !Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`))
1203
1181
  );
1204
-
1182
+
1205
1183
  if (process.env.DEBUG_PROVIDER_SELECTION) {
1206
1184
  console.log(chalk.gray(`[DEBUG] Unused IDE providers: ${unusedIdeProviders.map(p => p.provider).join(', ')}`));
1207
1185
  }
1208
-
1186
+
1209
1187
  if (unusedIdeProviders.length > 0) {
1210
1188
  selection = unusedIdeProviders[0];
1211
1189
  console.log(chalk.green(`✓ Selected unused IDE provider: ${selection.displayName}`));
@@ -1229,16 +1207,30 @@ async function getProviderConfig(excludeProvider = null) {
1229
1207
  return { status: 'ok', provider: selection, disabledProviders };
1230
1208
  }
1231
1209
 
1232
- const waits = enabledProviders
1210
+ const waits = availableProviders
1233
1211
  .map(p => providerManager.getTimeUntilReset(p.provider, p.model))
1234
1212
  .filter(Boolean);
1235
1213
  const nextResetMs = waits.length ? Math.min(...waits) : null;
1236
1214
 
1215
+ // Only return all_rate_limited if there are available providers and ALL of them are rate limited
1216
+ const allAvailableRateLimited = availableProviders.length > 0 &&
1217
+ availableProviders.every(p => providerManager.isRateLimited(p.provider, p.model));
1218
+
1219
+ if (allAvailableRateLimited) {
1220
+ return {
1221
+ status: 'all_rate_limited',
1222
+ enabledProviders,
1223
+ disabledProviders,
1224
+ nextResetMs
1225
+ };
1226
+ }
1227
+
1228
+ // If we reach here, no providers are available (filtered out by excludeProvider or other conditions)
1237
1229
  return {
1238
- status: 'all_rate_limited',
1230
+ status: 'no_available',
1239
1231
  enabledProviders,
1240
1232
  disabledProviders,
1241
- nextResetMs
1233
+ skipped
1242
1234
  };
1243
1235
  }
1244
1236
 
@@ -1249,7 +1241,7 @@ async function acquireProviderConfig(excludeProvider = null, excludeModel = null
1249
1241
  if (forcedProvider === 'cline') {
1250
1242
  const { DirectLLMManager } = require('vibecodingmachine-core');
1251
1243
  const llm = new DirectLLMManager();
1252
-
1244
+
1253
1245
  if (!await llm.isClineAvailable()) {
1254
1246
  console.log(chalk.yellow(`\n🔧 Cline CLI not found, auto-installing...`));
1255
1247
  const installed = await ensureClineInstalled();
@@ -1259,7 +1251,7 @@ async function acquireProviderConfig(excludeProvider = null, excludeModel = null
1259
1251
  }
1260
1252
  }
1261
1253
  }
1262
-
1254
+
1263
1255
  const { providers } = await getAllAvailableProviders();
1264
1256
  const match = providers.find(p => p.provider === forcedProvider);
1265
1257
  if (match) return match;
@@ -1270,6 +1262,13 @@ async function acquireProviderConfig(excludeProvider = null, excludeModel = null
1270
1262
 
1271
1263
  while (true) {
1272
1264
  const selection = await getProviderConfig(excludeProvider);
1265
+
1266
+ // Handle case where getProviderConfig returns undefined/null
1267
+ if (!selection) {
1268
+ console.log(chalk.red(`\n✗ ${t('auto.direct.provider.none.available')}\n`));
1269
+ return null;
1270
+ }
1271
+
1273
1272
  if (selection.status === 'ok') {
1274
1273
  // If we have a specific model to exclude (for same-IDE failover), skip it
1275
1274
  if (excludeModel && selection.provider.model === excludeModel) {
@@ -1317,6 +1316,14 @@ async function acquireProviderConfig(excludeProvider = null, excludeModel = null
1317
1316
  continue;
1318
1317
  }
1319
1318
 
1319
+ if (selection.status === 'no_available') {
1320
+ console.log(chalk.red(`\n✗ No available providers after filtering\n`));
1321
+ if (selection.disabledProviders && selection.disabledProviders.length > 0) {
1322
+ console.log(chalk.gray(` Enable more providers in Settings to continue\n`));
1323
+ }
1324
+ return null;
1325
+ }
1326
+
1320
1327
  return null;
1321
1328
  }
1322
1329
  }
@@ -1963,7 +1970,7 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1963
1970
  if (antigravityQuotaResult && antigravityQuotaResult.isRateLimited) {
1964
1971
  quotaHandled = true;
1965
1972
  const quotaMessage = antigravityQuotaResult.message || 'Antigravity quota limit detected via AppleScript';
1966
-
1973
+
1967
1974
  // Mark the provider as rate limited
1968
1975
  try {
1969
1976
  if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
@@ -1978,20 +1985,20 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
1978
1985
  resolve({ success: false, rateLimited: true, antigravityRateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
1979
1986
  return;
1980
1987
  }
1981
- } catch (appleScriptError) {
1982
- console.log(chalk.red(`❌ AppleScript quota detection failed for Antigravity: ${appleScriptError.message}`));
1983
- }
1988
+ } catch (appleScriptError) {
1989
+ console.log(chalk.red(`❌ AppleScript quota detection failed for Antigravity: ${appleScriptError.message}`));
1990
+ }
1984
1991
  }
1985
1992
  // For Windsurf: AppleScript quota detection
1986
1993
  else if (ideType === 'windsurf') {
1987
1994
  try {
1988
1995
  const appleScriptManager = new AppleScriptManager();
1989
1996
  const windsurfQuotaResult = await appleScriptManager.checkWindsurfQuotaLimit();
1990
-
1997
+
1991
1998
  if (windsurfQuotaResult && windsurfQuotaResult.hasQuotaWarning) {
1992
1999
  quotaHandled = true;
1993
2000
  const quotaMessage = windsurfQuotaResult.matchedText || 'Windsurf quota limit detected';
1994
-
2001
+
1995
2002
  // Mark the provider as rate limited
1996
2003
  try {
1997
2004
  if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
@@ -2000,7 +2007,7 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
2000
2007
  } catch (e) {
2001
2008
  // Ignore errors from marking rate-limited
2002
2009
  }
2003
-
2010
+
2004
2011
  watcher.close();
2005
2012
  console.log(chalk.yellow(`\n⚠️ Windsurf quota warning detected: ${quotaMessage.substring(0, 100)}...\n`));
2006
2013
  resolve({ success: false, rateLimited: true, windsurfRateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
@@ -2019,7 +2026,7 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
2019
2026
  if (cursorQuotaResult && cursorQuotaResult.isRateLimited) {
2020
2027
  quotaHandled = true;
2021
2028
  const quotaMessage = cursorQuotaResult.message || 'Cursor quota limit detected via AppleScript';
2022
-
2029
+
2023
2030
  // Mark the provider as rate limited
2024
2031
  try {
2025
2032
  if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
@@ -2042,7 +2049,7 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
2042
2049
  else if (ideType === 'vscode' || ideType === 'github-copilot' || ideType === 'amazon-q') {
2043
2050
  const quotaDetector = new QuotaDetector();
2044
2051
  const ideToCheck = ideType === 'github-copilot' || ideType === 'amazon-q' ? 'vscode' : ideType;
2045
-
2052
+
2046
2053
  try {
2047
2054
  const quotaResult = await quotaDetector.detectQuotaWarning(ideToCheck);
2048
2055
 
@@ -2051,9 +2058,9 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
2051
2058
  const quotaMessage = quotaResult.matchedText || 'Quota limit detected in IDE UI';
2052
2059
 
2053
2060
  // Check if this might be Kiro quota warning even when ideType is amazon-q
2054
- const isKiroPattern = quotaMessage.toLowerCase().includes('out of credits') ||
2055
- quotaMessage.toLowerCase().includes('upgrade plan') ||
2056
- quotaMessage.toLowerCase().includes('monthly usage limit');
2061
+ const isKiroPattern = quotaMessage.toLowerCase().includes('out of credits') ||
2062
+ quotaMessage.toLowerCase().includes('upgrade plan') ||
2063
+ quotaMessage.toLowerCase().includes('monthly usage limit');
2057
2064
 
2058
2065
  // Mark the provider as rate limited
2059
2066
  try {
@@ -2066,20 +2073,20 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
2066
2073
 
2067
2074
  watcher.close();
2068
2075
  console.log(chalk.yellow(`\n⚠️ Quota warning detected via CDP in ${ideToCheck} UI: ${quotaMessage.substring(0, 100)}...\n`));
2069
-
2076
+
2070
2077
  // If this looks like Kiro quota, set kiroRateLimited flag too
2071
- const resolveObj = {
2072
- success: false,
2073
- rateLimited: true,
2074
- providerRateLimited: ideType,
2075
- matchedLine: quotaMessage
2078
+ const resolveObj = {
2079
+ success: false,
2080
+ rateLimited: true,
2081
+ providerRateLimited: ideType,
2082
+ matchedLine: quotaMessage
2076
2083
  };
2077
-
2084
+
2078
2085
  if (ideType === 'amazon-q' && isKiroPattern) {
2079
2086
  resolveObj.kiroRateLimited = true;
2080
2087
  console.log(chalk.magenta(`💡 Detected potential Kiro quota warning despite amazon-q ideType`));
2081
2088
  }
2082
-
2089
+
2083
2090
  resolve(resolveObj);
2084
2091
  return;
2085
2092
  }
@@ -2093,7 +2100,76 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
2093
2100
  }
2094
2101
  }
2095
2102
 
2096
- // Check 5: Timeout
2103
+ // Check 5: Continuation detection (check every 2 seconds)
2104
+ if (elapsed > 30000 && elapsed % 2000 < 2000) { // Start checking after 30 seconds, every 2 seconds
2105
+ try {
2106
+ let continuationDetected = false;
2107
+ let continuationClicked = false;
2108
+
2109
+ // Use CDP for web-based IDEs
2110
+ if (ideType === 'vscode' || ideType === 'cursor' || ideType === 'windsurf') {
2111
+ const { CDPManager } = require('vibecodingmachine-core');
2112
+ const cdpManager = new CDPManager();
2113
+
2114
+ try {
2115
+ const continuationResult = await cdpManager.checkForContinuation(ideType);
2116
+ if (continuationResult.continuationDetected) {
2117
+ console.log(chalk.yellow(`🔄 Continuation prompt detected in ${ideType}, clicking button...`));
2118
+ const clickResult = await cdpManager.clickContinuationButton(ideType);
2119
+ if (clickResult.success && clickResult.clicked) {
2120
+ continuationDetected = true;
2121
+ continuationClicked = true;
2122
+ console.log(chalk.green(`✅ Continuation button clicked successfully`));
2123
+ } else {
2124
+ console.log(chalk.yellow(`⚠️ Failed to click continuation button: ${clickResult.message || 'Unknown error'}`));
2125
+ }
2126
+ }
2127
+ } catch (cdpError) {
2128
+ console.log(chalk.gray(` Continuation detection via CDP failed: ${cdpError.message}`));
2129
+ }
2130
+ }
2131
+ // Use AppleScript for desktop IDEs
2132
+ else if (ideType === 'cline' || ideType === 'claude-code') {
2133
+ const { AppleScriptManager } = require('vibecodingmachine-core');
2134
+ const appleScriptManager = new AppleScriptManager();
2135
+
2136
+ try {
2137
+ const continuationResult = await appleScriptManager.checkForContinuation(ideType);
2138
+ if (continuationResult.continuationDetected) {
2139
+ console.log(chalk.yellow(`🔄 Continuation prompt detected in ${ideType}, clicking button...`));
2140
+ const clickResult = await appleScriptManager.clickContinuationButton(ideType);
2141
+ if (clickResult.success && clickResult.clicked) {
2142
+ continuationDetected = true;
2143
+ continuationClicked = true;
2144
+ console.log(chalk.green(`✅ Continuation button clicked successfully`));
2145
+ } else {
2146
+ console.log(chalk.yellow(`⚠️ Failed to click continuation button: ${clickResult.error || 'Unknown error'}`));
2147
+ }
2148
+ }
2149
+ } catch (appleScriptError) {
2150
+ console.log(chalk.gray(` Continuation detection via AppleScript failed: ${appleScriptError.message}`));
2151
+ }
2152
+ }
2153
+
2154
+ // Record continuation detection in health tracker
2155
+ if (continuationDetected) {
2156
+ try {
2157
+ // Update the interaction record to include continuation detection
2158
+ const currentMetrics = sharedHealthTracker.getHealthMetrics(ideType);
2159
+ if (currentMetrics && currentMetrics.interactions && currentMetrics.interactions.length > 0) {
2160
+ const lastInteraction = currentMetrics.interactions[currentMetrics.interactions.length - 1];
2161
+ lastInteraction.continuationPromptsDetected = (lastInteraction.continuationPromptsDetected || 0) + 1;
2162
+ }
2163
+ } catch (trackerError) {
2164
+ console.log(chalk.gray(` Failed to record continuation detection: ${trackerError.message}`));
2165
+ }
2166
+ }
2167
+ } catch (error) {
2168
+ console.log(chalk.gray(` Continuation detection error: ${error.message}`));
2169
+ }
2170
+ }
2171
+
2172
+ // Check 6: Timeout
2097
2173
  const elapsed = Date.now() - startTime;
2098
2174
  if (elapsed >= timeoutMs) {
2099
2175
  watcher.close();
@@ -2102,6 +2178,45 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
2102
2178
  return;
2103
2179
  }
2104
2180
 
2181
+ // Check 7: Text pattern matching fallback for continuation prompts
2182
+ const fileContent = await fs.readFile(reqPath, 'utf-8');
2183
+ const continuationPatterns = [
2184
+ /continue.*generation/i,
2185
+ /keep.*going/i,
2186
+ /continue.*writing/i,
2187
+ /continue.*coding/i,
2188
+ /proceed/i,
2189
+ /next.*step/i,
2190
+ /continue/i
2191
+ ];
2192
+
2193
+ const hasContinuationPrompt = continuationPatterns.some(pattern => pattern.test(fileContent));
2194
+ if (hasContinuationPrompt && elapsed > 30000) { // Only check for text patterns after 30 seconds
2195
+ console.log(chalk.yellow(`🔄 Continuation prompt detected in text, attempting to continue...`));
2196
+ // This is a fallback - we can't click buttons via text patterns, but we can log it
2197
+ try {
2198
+ const currentMetrics = sharedHealthTracker.getHealthMetrics(ideType);
2199
+ if (currentMetrics && currentMetrics.interactions && currentMetrics.interactions.length > 0) {
2200
+ const lastInteraction = currentMetrics.interactions[currentMetrics.interactions.length - 1];
2201
+ lastInteraction.continuationPromptsDetected = (lastInteraction.continuationPromptsDetected || 0) + 1;
2202
+ }
2203
+ } catch (trackerError) {
2204
+ console.log(chalk.gray(` Failed to record text continuation detection: ${trackerError.message}`));
2205
+ }
2206
+ }
2207
+ // Check 8: Memory monitoring
2208
+ if (elapsed > 60000 && elapsed % 30000 < 30000) { // Check memory every 30 seconds after 1 minute
2209
+ const memoryUsage = process.memoryUsage();
2210
+ const heapUsedMB = Math.round(memoryUsage.heapUsed / 1024 / 1024);
2211
+ const heapTotalMB = Math.round(memoryUsage.heapTotal / 1024 / 1024);
2212
+ const heapUsedPercent = Math.round((memoryUsage.heapUsed / memoryUsage.heapTotal) * 100);
2213
+
2214
+ if (heapUsedMB > 500) { // Warn at 500MB
2215
+ console.log(chalk.yellow(`⚠️ High memory usage detected: ${heapUsedMB}MB heap (${heapUsedPercent}% of ${heapTotalMB}MB)`));
2216
+ console.log(chalk.gray(` RSS: ${Math.round(memoryUsage.rss / 1024 / 1024)}MB, External: ${Math.round(memoryUsage.external / 1024 / 1024)}MB`));
2217
+ }
2218
+ }
2219
+
2105
2220
  // Log progress every 30 seconds
2106
2221
  if (Date.now() - lastCheckTime >= 30000) {
2107
2222
  const elapsedMin = Math.floor(elapsed / 60000);
@@ -2156,13 +2271,13 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
2156
2271
  }
2157
2272
  return { success: false, error: 'Antigravity rate limit reached, retrying with next provider.', shouldRetry: true };
2158
2273
  }
2159
-
2274
+
2160
2275
  // CRITICAL: Mark provider as unavailable for ANY error so acquireProviderConfig() will skip it
2161
2276
  // EXCEPT for web-based IDEs where the error is platform/browser related
2162
2277
  const error = ideResult.output || ideResult.error || 'IDE provider failed';
2163
2278
  const isWebBasedIDE = providerConfig.provider === 'replit';
2164
2279
  const isPlatformError = error.includes('xdg-open') || error.includes('command not found') || error.includes('Unable to find application');
2165
-
2280
+
2166
2281
  if (!isWebBasedIDE || !isPlatformError) {
2167
2282
  providerManager.markRateLimited(providerConfig.provider, providerConfig.model, error);
2168
2283
  } else {
@@ -2170,19 +2285,37 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
2170
2285
  // Just log the error and let the system try the next provider
2171
2286
  console.log(chalk.yellow(`⚠️ Web-based IDE ${providerConfig.provider} failed due to platform issue: ${error}`));
2172
2287
  }
2173
-
2288
+
2174
2289
  return { success: false, error: ideResult.error || 'IDE provider failed' };
2175
2290
  }
2176
2291
 
2177
2292
  console.log(chalk.green(`✓ ${t('auto.direct.ide.prompt.sent')}`));
2178
2293
 
2294
+ // Calculate adaptive timeout based on IDE's historical performance
2295
+ const ideType = providerConfig.provider || providerConfig.ide;
2296
+ const healthMetrics = sharedHealthTracker.getHealthMetrics(ideType);
2297
+ let adaptiveTimeoutMs = 30 * 60 * 1000; // Default 30 minutes
2298
+
2299
+ if (healthMetrics && healthMetrics.responseTimes.length > 0) {
2300
+ // Use adaptive timeout calculation if we have historical data
2301
+ const stats = TimeoutCalculator.calculateTimeout(healthMetrics.responseTimes, {
2302
+ defaultTimeout: 30 * 60 * 1000,
2303
+ minTimeout: 5 * 60 * 1000, // 5 minutes minimum
2304
+ maxTimeout: 60 * 60 * 1000, // 60 minutes maximum
2305
+ bufferPercentage: 0.5 // 50% buffer
2306
+ });
2307
+ adaptiveTimeoutMs = stats.timeout;
2308
+ console.log(chalk.gray(` Using adaptive timeout: ${Math.floor(adaptiveTimeoutMs / 60000)} minutes (based on ${healthMetrics.responseTimes.length} historical response times)`));
2309
+ } else {
2310
+ console.log(chalk.gray(` Using default timeout: ${Math.floor(adaptiveTimeoutMs / 60000)} minutes (no historical data for ${ideType})`));
2311
+ }
2312
+
2179
2313
  // Wait for IDE agent to complete the work (IDE will update status to DONE itself)
2180
2314
  const ideCompletionStartTime = Date.now();
2181
- const completionResult = await waitForIdeCompletion(repoPath, requirement.text, providerConfig.provider || providerConfig.ide);
2315
+ const completionResult = await waitForIdeCompletion(repoPath, requirement.text, ideType, adaptiveTimeoutMs);
2182
2316
  const ideResponseTime = Date.now() - ideCompletionStartTime;
2183
2317
 
2184
2318
  // Track IDE health metrics based on completion result
2185
- const ideType = providerConfig.provider || providerConfig.ide;
2186
2319
  if (completionResult.success) {
2187
2320
  // Record success with response time
2188
2321
  await sharedHealthTracker.recordSuccess(ideType, ideResponseTime, {
@@ -2197,7 +2330,7 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
2197
2330
  } else if (completionResult.reason === 'timeout') {
2198
2331
  // Record failure due to timeout
2199
2332
  await sharedHealthTracker.recordFailure(ideType, 'Timeout exceeded', {
2200
- timeoutUsed: 30 * 60 * 1000, // Default 30 minutes, will be adaptive later
2333
+ timeoutUsed: adaptiveTimeoutMs, // Use the adaptive timeout that was actually used
2201
2334
  requirementId: requirement.id || requirement.text.substring(0, 50),
2202
2335
  });
2203
2336
  } else {
@@ -2293,6 +2426,13 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
2293
2426
  * Sends the full spec instruction to the IDE and polls tasks.md for
2294
2427
  * progress (any new checkbox ticked), instead of spawning vcm auto:start.
2295
2428
  *
2429
+ * COORDINATION STRATEGY:
2430
+ * - Wait for agent completion signals instead of just detecting any progress
2431
+ * - Implement 2-minute cooldown after progress detection to prevent rapid spawning
2432
+ * - Only consider iteration complete when all tasks are done AND agent signals completion
2433
+ * - Use STATUS:WAITING and PLEASE RESPOND signals from agent to coordinate handoff
2434
+ * - Prevent multiple agents from working on the same spec simultaneously
2435
+ *
2296
2436
  * @param {Object} spec - Spec object with .path, .directory, .hasTasks, .hasPlan, .hasPlanPrompt
2297
2437
  * @param {string} taskText - Text of the NEXT unchecked task (for display)
2298
2438
  * @param {string} taskLine - Full line from tasks.md (used if this is a continuation)
@@ -2304,107 +2444,40 @@ async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2304
2444
  const { done: doneBefore, total: totalBefore } = countSpecCheckboxes(spec.path);
2305
2445
  const pctBefore = totalBefore > 0 ? Math.round((doneBefore / totalBefore) * 100) : 0;
2306
2446
 
2447
+ // Get configurable timeouts from auto config
2448
+ const autoConfig = await getAutoConfig();
2449
+ const PROGRESS_TIMEOUT_MS = (autoConfig.specProgressTimeoutMinutes || 15) * 60 * 1000; // Configurable, default 15 minutes
2450
+ const MAX_IDE_ATTEMPTS = autoConfig.maxIdeAttempts || 3; // Configurable, default 3 attempts
2451
+
2307
2452
  // Build the full spec instruction (mirrors buildSpecInstruction from Electron app)
2308
2453
  // This lets the agent work through multiple tasks autonomously rather than one at a time.
2309
2454
  let instruction;
2310
2455
  if (spec.hasTasks) {
2311
- instruction = `Implement all remaining tasks in ${spec.path}/tasks.md. Current progress: ${doneBefore}/${totalBefore} tasks (${pctBefore}%) complete. Check off each task (change "[ ]" to "[x]") as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. When ALL tasks are checked off, report "COMPLETION: 100%".`;
2456
+ instruction = `Read AGENTS.md and INSTRUCTIONS.md in the repo root for workflow guidelines. Implement all remaining tasks in ${spec.path}/tasks.md. Current progress: ${doneBefore}/${totalBefore} tasks (${pctBefore}%) complete. Check off each task (change "[ ]" to "[x]") as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. Whenever you stop and wait for user response, say "STATUS:WAITING" followed by "PLEASE RESPOND". When ALL tasks are checked off, report "COMPLETION: 100%", then say "STATUS:WAITING" and "PLEASE RESPOND".`;
2312
2457
  } else if (spec.hasPlan) {
2313
- instruction = `Read ${spec.path}/plan.md, generate tasks.md for this spec, then implement all tasks. Check off each task as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. When ALL tasks are done, report "COMPLETION: 100%".`;
2458
+ instruction = `Read AGENTS.md and INSTRUCTIONS.md in the repo root for workflow guidelines. Read ${spec.path}/plan.md, generate tasks.md for this spec, then implement all tasks. Check off each task as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. Whenever you stop and wait for user response, say "STATUS:WAITING" followed by "PLEASE RESPOND". When ALL tasks are done, report "COMPLETION: 100%", then say "STATUS:WAITING" and "PLEASE RESPOND".`;
2314
2459
  } else {
2315
2460
  const planPromptNote = spec.hasPlanPrompt
2316
2461
  ? `Use plan prompt from ${spec.path}/plan-prompt.md to guide planning. ` : '';
2317
- instruction = `Read ${spec.path}/spec.md. ${planPromptNote}Run speckit workflow: (1) read spec.md, (2) generate plan.md, (3) generate tasks.md, (4) implement all tasks. Check off each task as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. When ALL tasks are done, report "COMPLETION: 100%".`;
2462
+ instruction = `Read AGENTS.md and INSTRUCTIONS.md in the repo root for workflow guidelines. Read ${spec.path}/spec.md. ${planPromptNote}Run speckit workflow: (1) read spec.md, (2) generate plan.md, (3) generate tasks.md, (4) implement all tasks. Check off each task as you complete it. Report progress as "X/Y tasks (Z%) complete" after each task. Whenever you stop and wait for user response, say "STATUS:WAITING" followed by "PLEASE RESPOND". When ALL tasks are done, report "COMPLETION: 100%", then say "STATUS:WAITING" and "PLEASE RESPOND".`;
2318
2463
  }
2319
2464
 
2320
2465
  // Send the spec instruction to the IDE via AppleScript.
2321
- // For Windsurf: use a single combined synchronous AppleScript that activates Windsurf,
2322
- // opens a fresh Cascade conversation, pastes the instruction, and submits — all in one
2323
- // atomic execSync call. This eliminates the timing race where a detached AppleScript
2324
- // fires 3 seconds later after VS Code has regained focus.
2466
+ // Uses the shared AppleScriptManager with detached execution for proper timing.
2325
2467
  console.log(chalk.cyan(`📤 Sending spec instruction to ${providerConfig.displayName}...\n`));
2326
2468
 
2327
2469
  if (ideType === 'windsurf') {
2328
2470
  try {
2329
- const { execSync } = require('child_process');
2330
- const { writeFileSync, unlinkSync: _unlinkSync } = require('fs');
2331
- const { tmpdir } = require('os');
2332
-
2333
- // Write instruction to a temp file so we can read it from AppleScript
2334
- // without any escaping issues with quotes or special characters.
2335
- const ts = Date.now();
2336
- const tmpTextFile = path.join(tmpdir(), `spec_instr_${ts}.txt`);
2337
- const tmpScpt = path.join(tmpdir(), `send_cascade_${ts}.scpt`);
2338
-
2339
- writeFileSync(tmpTextFile, instruction, 'utf8');
2340
-
2341
- // Single combined script: activate Windsurf → verify it is frontmost →
2342
- // focus Cascade → paste → submit.
2343
- // Both Windsurf AND VS Code report process name "Electron" to System Events,
2344
- // so we MUST use bundle identifier "com.exafunction.windsurf" to distinguish them.
2345
- const combinedScript = `
2346
- set instructionText to (do shell script "cat " & quoted form of "${tmpTextFile}")
2347
-
2348
- -- Step 1: bring Windsurf to front
2349
- tell application "Windsurf"
2350
- activate
2351
- delay 1.5
2352
- end tell
2353
-
2354
- -- Step 2: verify Windsurf is the frontmost app by BUNDLE ID (not name — both
2355
- -- Windsurf and VS Code report process name "Electron" to System Events).
2356
- set maxTries to 3
2357
- set tries to 0
2358
- repeat
2359
- set tries to tries + 1
2360
- tell application "System Events"
2361
- set frontBundleID to bundle identifier of first process whose frontmost is true
2362
- end tell
2363
- if frontBundleID is "com.exafunction.windsurf" then exit repeat
2364
- if tries >= maxTries then
2365
- error "Windsurf did not become frontmost (frontmost bundle ID: " & frontBundleID & ")"
2366
- end if
2367
- tell application "Windsurf"
2368
- activate
2369
- end tell
2370
- delay 1.0
2371
- end repeat
2372
-
2373
- -- Step 3: send keystrokes to Windsurf using bundle ID (required to distinguish
2374
- -- Windsurf from VS Code since both run as "Electron" processes)
2375
- tell application "System Events"
2376
- set windsurfProc to first process whose bundle identifier is "com.exafunction.windsurf"
2377
- tell windsurfProc
2378
- set frontmost to true
2379
- delay 0.5
2380
- -- ESC: defocus any editor/terminal input that might intercept keystrokes
2381
- key code 53
2382
- delay 0.5
2383
- -- Cmd+Shift+L: focus Cascade chat input (proven approach from sendText())
2384
- keystroke "l" using {command down, shift down}
2385
- delay 2.0
2386
- -- Clear any auto-inserted context text (e.g. @terminal:zsh)
2387
- keystroke "a" using {command down}
2388
- delay 0.3
2389
- key code 51
2390
- delay 0.3
2391
- -- Paste the spec instruction
2392
- set the clipboard to instructionText
2393
- keystroke "v" using {command down}
2394
- delay 0.5
2395
- -- Submit
2396
- key code 36
2397
- delay 1.0
2398
- end tell
2399
- end tell
2400
- `;
2401
-
2402
- writeFileSync(tmpScpt, combinedScript, 'utf8');
2403
- try {
2404
- execSync(`osascript "${tmpScpt}"`, { stdio: 'pipe', timeout: 30000 });
2405
- } finally {
2406
- try { _unlinkSync(tmpScpt); } catch (_) {}
2407
- try { _unlinkSync(tmpTextFile); } catch (_) {}
2471
+ const appleScriptManager = new AppleScriptManager();
2472
+ const sendResult = await appleScriptManager.sendText(instruction, 'windsurf');
2473
+
2474
+ if (sendResult && sendResult.success) {
2475
+ console.log(chalk.green(`✓ Spec instruction sent to ${providerConfig.displayName}`));
2476
+ console.log(chalk.gray(`⏳ Polling tasks.md every 30s for checkbox progress...\n`));
2477
+ console.log(chalk.gray(`🔄 Coordination: Will wait for agent completion signals before sending new tasks\n`));
2478
+ } else {
2479
+ console.log(chalk.red(`✗ Failed to send instruction to Windsurf: ${sendResult?.error || 'Unknown error'}`));
2480
+ return { success: false, error: sendResult?.error || 'Unknown error' };
2408
2481
  }
2409
2482
  } catch (err) {
2410
2483
  console.log(chalk.red(`✗ AppleScript error: ${err.message}`));
@@ -2434,13 +2507,19 @@ end tell
2434
2507
 
2435
2508
  console.log(chalk.green(`✓ Spec instruction sent to ${providerConfig.displayName}`));
2436
2509
  console.log(chalk.gray(`⏳ Polling tasks.md every 30s for checkbox progress...\n`));
2510
+ console.log(chalk.gray(`🔄 Coordination: Will wait for agent completion signals before sending new tasks\n`));
2437
2511
 
2438
2512
  const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds
2439
2513
  const CONTINUE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
2440
- const TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
2514
+ const TIMEOUT_MS = PROGRESS_TIMEOUT_MS; // Use configurable progress timeout
2515
+ const PROGRESS_COOLDOWN_MS = 2 * 60 * 1000; // 2 minutes cooldown after progress
2441
2516
 
2442
2517
  const startTime = Date.now();
2443
2518
  let lastContinueSent = Date.now();
2519
+ let lastProgressTime = Date.now(); // Track when we last saw progress
2520
+ let lastProgressDetectionTime = 0; // Track when we last detected progress to implement cooldown
2521
+ let totalAttempts = (spec.totalIdeAttempts || 0) + 1; // Track attempts across IDEs
2522
+ let agentSignaledCompletion = false; // Track if agent signaled completion
2444
2523
 
2445
2524
  return new Promise((resolve) => {
2446
2525
  let interval = null;
@@ -2464,27 +2543,91 @@ end tell
2464
2543
  const elapsed = Date.now() - startTime;
2465
2544
 
2466
2545
  if (elapsed >= TIMEOUT_MS) {
2467
- cleanup({ success: false, error: 'timeout', shouldRetry: true });
2468
- console.log(chalk.yellow(`⏰ Timeout (30min) ${providerConfig.displayName} did not check off any tasks\n`));
2546
+ console.log(chalk.yellow(`⏰ Overall timeout reached - but continuing to monitor existing Windsurf instance\n`));
2547
+ // Don't create new instance - just reset timer and continue monitoring
2548
+ startTime = Date.now(); // Reset the overall timer
2469
2549
  return;
2470
2550
  }
2471
2551
 
2472
- // Detect any new checkbox ticked since we started
2552
+ // Detect progress AND check if IDE agent is done working
2473
2553
  const { done: doneNow, total: totalNow } = countSpecCheckboxes(spec.path);
2554
+ const currentTime = Date.now();
2555
+
2474
2556
  if (doneNow > doneBefore) {
2475
2557
  const pctNow = totalNow > 0 ? Math.round((doneNow / totalNow) * 100) : 0;
2476
2558
  console.log(chalk.green(`✓ Progress detected: ${doneNow}/${totalNow} tasks (${pctNow}%) complete\n`));
2559
+
2560
+ // Update progress tracking
2561
+ lastProgressTime = currentTime;
2562
+ lastProgressDetectionTime = currentTime;
2563
+
2564
+ // Check if all tasks are complete - if so, wait for agent completion signal
2565
+ if (doneNow >= totalNow && totalNow > 0) {
2566
+ console.log(chalk.green(`🎯 All tasks checked off! Waiting for agent completion signal...\n`));
2567
+ // Don't immediately return - wait for STATUS:WAITING signal or timeout
2568
+ agentSignaledCompletion = true; // Flag that we're waiting for completion signal
2569
+ }
2570
+
2571
+ // If progress detected but not all tasks done, continue waiting for agent to finish current work
2572
+ return; // Continue polling, don't send new task yet
2573
+ }
2574
+
2575
+ // If agent signaled completion (all tasks done) and we've waited a reasonable time, consider it complete
2576
+ if (agentSignaledCompletion && (currentTime - lastProgressDetectionTime) >= 30 * 1000) {
2577
+ console.log(chalk.green(`✅ Agent completed all tasks! Finishing iteration.\n`));
2477
2578
  cleanup({ success: true, changes: [] });
2478
2579
  return;
2479
2580
  }
2480
2581
 
2481
- // Send continuation prompt every 5 minutes if no progress
2582
+ // Implement cooldown period after progress detection to prevent rapid spawning
2583
+ if (lastProgressDetectionTime > 0 && (currentTime - lastProgressDetectionTime) < PROGRESS_COOLDOWN_MS) {
2584
+ const cooldownRemaining = Math.round((PROGRESS_COOLDOWN_MS - (currentTime - lastProgressDetectionTime)) / 1000);
2585
+ console.log(chalk.gray(`⏱️ In cooldown period (${cooldownRemaining}s remaining) - allowing agent time to complete work\n`));
2586
+ return;
2587
+ }
2588
+
2589
+ // Check for no progress timeout (configurable, default 15 minutes)
2590
+ const timeSinceLastProgress = Date.now() - lastProgressTime;
2591
+ if (timeSinceLastProgress >= PROGRESS_TIMEOUT_MS) {
2592
+ console.log(chalk.yellow(`⚠️ No progress detected for ${Math.round(timeSinceLastProgress / 60000)} minutes on ${providerConfig.displayName}\n`));
2593
+
2594
+ // Check if we've exceeded max IDE attempts
2595
+ if (totalAttempts >= MAX_IDE_ATTEMPTS) {
2596
+ console.log(chalk.red(`✗ Maximum IDE attempts (${MAX_IDE_ATTEMPTS}) reached. Stopping auto mode.\n`));
2597
+ // Stop auto mode - call async function properly
2598
+ stopAutoMode().then(() => {
2599
+ cleanup({ success: false, error: 'max_ide_attempts_exceeded', shouldRetry: false });
2600
+ }).catch((err) => {
2601
+ console.error('Error stopping auto mode:', err);
2602
+ cleanup({ success: false, error: 'max_ide_attempts_exceeded', shouldRetry: false });
2603
+ });
2604
+ return;
2605
+ }
2606
+
2607
+ // Don't create new instance - just continue monitoring and send continuation messages
2608
+ // Reset the progress timer to allow more time for the existing instance
2609
+ lastProgressTime = Date.now();
2610
+ totalAttempts++;
2611
+ console.log(chalk.yellow(`⚠️ Allowing more time for existing Windsurf instance (attempt ${totalAttempts}/${MAX_IDE_ATTEMPTS})\n`));
2612
+ return;
2613
+ }
2614
+
2615
+ // Send continuation prompt every 5 minutes if no progress OR if waiting for completion signal
2482
2616
  const sinceLastContinue = Date.now() - lastContinueSent;
2483
2617
  if (sinceLastContinue >= CONTINUE_INTERVAL_MS) {
2484
2618
  lastContinueSent = Date.now();
2485
2619
  const pctNow = totalNow > 0 ? Math.round((doneNow / totalNow) * 100) : 0;
2486
2620
  const mins = Math.round(elapsed / 60000);
2487
- const continueMsg = `Continue implementing spec "${spec.directory}". Current progress: ${doneNow}/${totalNow} tasks (${pctNow}%) complete. Keep working until ALL tasks in tasks.md are checked off. Next task: "${taskText}". (${mins}min elapsed)`;
2621
+
2622
+ let continueMsg;
2623
+ if (agentSignaledCompletion) {
2624
+ // All tasks done, waiting for completion signal
2625
+ continueMsg = `All tasks appear to be completed (${doneNow}/${totalNow} tasks). If you are truly finished, please report "COMPLETION: 100%" followed by "STATUS:WAITING" and "PLEASE RESPOND". If you need more time, continue working and report progress. (${mins}min elapsed) PLEASE RESPOND`;
2626
+ } else {
2627
+ // Normal continuation prompt
2628
+ continueMsg = `Continue implementing spec "${spec.directory}". Current progress: ${doneNow}/${totalNow} tasks (${pctNow}%) complete. Keep working until ALL tasks in tasks.md are checked off. Next task: "${taskText}". (${mins}min elapsed) PLEASE RESPOND`;
2629
+ }
2630
+
2488
2631
  try {
2489
2632
  const appleScriptManager = new AppleScriptManager();
2490
2633
  // Use plain sendText for continuations (no need to open new thread)
@@ -2956,10 +3099,88 @@ async function loadEnabledIncompleteSpecs(repoPath) {
2956
3099
 
2957
3100
  // ─────────────────────────────────────────────────────────────────────────────
2958
3101
 
3102
+ /**
3103
+ * Process default requirement until completion
3104
+ * @param {RequirementSequencer} sequencer - Requirement sequencer
3105
+ * @param {DefaultRequirementManager} defaultManager - Default requirement manager
3106
+ * @param {Object} providerConfig - Provider configuration
3107
+ * @param {string} repoPath - Repository path
3108
+ */
3109
+ async function processDefaultRequirement(sequencer, defaultManager, providerConfig, repoPath) {
3110
+ const requirementsPath = await getRequirementsPath(repoPath);
3111
+ let iterationCount = 0;
3112
+
3113
+ while (true) {
3114
+ // Check if default requirement should stop
3115
+ const shouldStop = await defaultManager.shouldStop();
3116
+ if (shouldStop) {
3117
+ const status = await defaultManager.getStatus();
3118
+ console.log(chalk.bold.green(`\n✅ Default Requirement Complete`));
3119
+ console.log(chalk.gray(`Status: ${status.completionReason || 'Completed'}\n`));
3120
+ break;
3121
+ }
3122
+
3123
+ // Check if regular requirements were added (should pause default)
3124
+ const currentState = await sequencer.getCurrentState(requirementsPath);
3125
+ if (currentState.hasRegularRequirements) {
3126
+ console.log(chalk.bold.yellow('\n⏸️ Default Requirement Paused'));
3127
+ console.log(chalk.gray('New regular requirements detected, pausing default requirement...\n'));
3128
+ await defaultManager.pause();
3129
+ break;
3130
+ }
3131
+
3132
+ // Get current default requirement status
3133
+ const defaultStatus = await defaultManager.getStatus();
3134
+ iterationCount = defaultStatus.iterationCount + 1;
3135
+
3136
+ console.log(chalk.bold.cyan(`\n${'━'.repeat(80)}`));
3137
+ console.log(chalk.bold.cyan(` ⭐ DEFAULT REQUIREMENT - Iteration ${iterationCount}/${defaultStatus.maxIterations}`));
3138
+ console.log(chalk.bold.cyan(` ${defaultStatus.title}`));
3139
+ console.log(chalk.bold.cyan(`${'━'.repeat(80)}\n`));
3140
+
3141
+ // Create a synthetic requirement object for the default requirement
3142
+ const defaultRequirement = {
3143
+ text: defaultStatus.description,
3144
+ number: iterationCount,
3145
+ isDefault: true
3146
+ };
3147
+
3148
+ // Run iteration with default requirement
3149
+ const result = await runIteration(defaultRequirement, providerConfig, repoPath);
3150
+
3151
+ if (result.success) {
3152
+ // Increment iteration count
3153
+ await defaultManager.incrementIteration();
3154
+ console.log(chalk.bold.green(`✅ Default Requirement Iteration ${iterationCount}/${defaultStatus.maxIterations} COMPLETE`));
3155
+
3156
+ // Small delay between iterations
3157
+ await new Promise(resolve => setTimeout(resolve, 3000));
3158
+ } else {
3159
+ // Record failure
3160
+ await defaultManager.recordFailure();
3161
+ console.log(chalk.bold.red(`❌ Default Requirement Iteration ${iterationCount}/${defaultStatus.maxIterations} FAILED`));
3162
+ console.log(chalk.red(`Error: ${result.error}\n`));
3163
+
3164
+ // Check consecutive failures
3165
+ const updatedStatus = await defaultManager.getStatus();
3166
+ if (updatedStatus.consecutiveFailures >= 5) {
3167
+ console.log(chalk.bold.yellow('\n⚠️ Maximum consecutive failures reached'));
3168
+ await defaultManager.markDone('Stopped due to consecutive failures');
3169
+ break;
3170
+ }
3171
+
3172
+ // Delay longer after failure
3173
+ await new Promise(resolve => setTimeout(resolve, 5000));
3174
+ }
3175
+ }
3176
+ }
3177
+
2959
3178
  /**
2960
3179
  * Main auto mode command handler
2961
3180
  */
2962
3181
  async function handleAutoStart(options) {
3182
+ let stopMonitoring = null; // Declare outside try block so it's accessible in catch
3183
+
2963
3184
  try {
2964
3185
  // STRICT AUTH CHECK
2965
3186
  const auth = require('../utils/auth');
@@ -2986,20 +3207,68 @@ async function handleAutoStart(options) {
2986
3207
  return;
2987
3208
  }
2988
3209
 
3210
+ // Create keyboard handler for 'x' key exit
3211
+ keyboardHandler = createKeyboardHandler({
3212
+ onExit: () => {
3213
+ // Update status to stopped
3214
+ updateAutoModeStatus(repoPath, { running: false });
3215
+ console.log(chalk.gray('Auto mode stopped'));
3216
+ }
3217
+ });
3218
+
3219
+ // Start keyboard handler
3220
+ keyboardHandler.start();
3221
+
3222
+ // Ensure keyboard handler is cleaned up on exit
3223
+ const cleanup = () => {
3224
+ if (keyboardHandler) {
3225
+ keyboardHandler.stop();
3226
+ }
3227
+ };
3228
+
3229
+ // Handle process termination
3230
+ process.on('exit', cleanup);
3231
+ process.on('SIGINT', () => {
3232
+ cleanup();
3233
+ process.exit(0);
3234
+ });
3235
+ process.on('SIGTERM', () => {
3236
+ cleanup();
3237
+ process.exit(0);
3238
+ });
3239
+
2989
3240
  // Start Auto Mode status tracking
2990
3241
  const config = await getAutoConfig();
2991
-
3242
+
2992
3243
  // Save extension to config if provided
2993
3244
  if (options.extension) {
2994
3245
  config.extension = options.extension;
2995
3246
  await setAutoConfig(config);
2996
3247
  }
2997
-
3248
+
2998
3249
  await startAutoMode(repoPath, { ide: options.ide || config.ide });
2999
3250
 
3000
3251
  // Also load configured stages here since we already have the config
3001
3252
  configuredStages = await getStages();
3002
3253
 
3254
+ // Initialize requirement monitoring for default requirement management
3255
+ const requirementsPath = await getRequirementsPath(repoPath);
3256
+ const storage = new JSONStorage();
3257
+ const parser = new RequirementFileParser();
3258
+ const defaultManager = new DefaultRequirementManager(storage);
3259
+ const sequencer = new RequirementSequencer(parser, defaultManager);
3260
+
3261
+ // Start monitoring requirements file for changes
3262
+ stopMonitoring = sequencer.monitorRequirements(requirementsPath, async (changeType, change) => {
3263
+ if (changeType === 'regular-requirements-added') {
3264
+ console.log(chalk.bold.yellow('\n📝 New regular requirements detected'));
3265
+ console.log(chalk.gray('Default requirement will be paused to process regular requirements first\n'));
3266
+ } else if (changeType === 'regular-requirements-completed') {
3267
+ console.log(chalk.bold.green('\n✅ All regular requirements completed'));
3268
+ console.log(chalk.gray('Default requirement will be resumed\n'));
3269
+ }
3270
+ });
3271
+
3003
3272
  console.log(chalk.white(t('auto.repository')), chalk.cyan(repoPath));
3004
3273
 
3005
3274
  // Use the agent that was already determined by provider preferences in interactive.js
@@ -3038,6 +3307,9 @@ async function handleAutoStart(options) {
3038
3307
  console.log(chalk.bold.cyan(`\n📋 Processing ${incompleteSpecs.length} incomplete spec(s) before requirements...\n`));
3039
3308
 
3040
3309
  for (const spec of incompleteSpecs) {
3310
+ // Initialize IDE attempts tracking for this spec
3311
+ spec.totalIdeAttempts = 0;
3312
+
3041
3313
  const { done: doneStart, total: totalStart } = countSpecCheckboxes(spec.path);
3042
3314
  console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
3043
3315
  console.log(chalk.bold.magenta(` 📋 SPEC: ${spec.directory} (${spec.title || spec.directory})`));
@@ -3126,8 +3398,8 @@ end tell
3126
3398
  _execSync2(`osascript "${_tmpScpt2}"`, { stdio: 'pipe', timeout: 30000 });
3127
3399
  return { success: true };
3128
3400
  } finally {
3129
- try { _unlinkSync2(_tmpScpt2); } catch (_) {}
3130
- try { _unlinkSync2(_tmpText2); } catch (_) {}
3401
+ try { _unlinkSync2(_tmpScpt2); } catch (_) { }
3402
+ try { _unlinkSync2(_tmpText2); } catch (_) { }
3131
3403
  }
3132
3404
  } else {
3133
3405
  const appleScriptManager = new AppleScriptManager();
@@ -3186,6 +3458,58 @@ end tell
3186
3458
  }
3187
3459
  }
3188
3460
 
3461
+ // Verify tasks.md has checkbox-format tasks
3462
+ const { done: doneCheck, total: totalCheck } = countSpecCheckboxes(spec.path);
3463
+ if (totalCheck === 0 && spec.hasTasks) {
3464
+ console.log(chalk.yellow(`\n⚠️ tasks.md exists but has no checkbox tasks (narrative format detected)`));
3465
+ console.log(chalk.yellow(` Regenerating tasks.md with /speckit.tasks...\n`));
3466
+
3467
+ // Send task regeneration instruction to IDE
3468
+ const tasksRegenerationText = `Run /speckit.tasks to regenerate tasks.md for spec "${spec.directory}" in checkbox format (- [ ] task).`;
3469
+
3470
+ let regenResult;
3471
+ if (providerConfig.type === 'ide') {
3472
+ console.log(chalk.cyan(`📤 Sending tasks regeneration request to ${providerConfig.displayName}...\n`));
3473
+ const ideType = providerConfig.provider || providerConfig.ide;
3474
+ const appleScriptManager = new AppleScriptManager();
3475
+ try {
3476
+ const sendResult = typeof appleScriptManager.sendTextWithThreadClosure === 'function'
3477
+ ? await appleScriptManager.sendTextWithThreadClosure(tasksRegenerationText, ideType)
3478
+ : await appleScriptManager.sendText(tasksRegenerationText, ideType);
3479
+
3480
+ if (!sendResult || !sendResult.success) {
3481
+ console.log(chalk.red(`✗ Failed to send to ${providerConfig.displayName}`));
3482
+ regenResult = { success: false };
3483
+ } else {
3484
+ console.log(chalk.green(`✓ Regeneration request sent. Waiting for tasks.md to be updated...`));
3485
+ // Give IDE time to regenerate (30 seconds)
3486
+ await new Promise(resolve => setTimeout(resolve, 30000));
3487
+
3488
+ // Check if tasks now have checkboxes
3489
+ const { total: totalAfterRegen } = countSpecCheckboxes(spec.path);
3490
+ if (totalAfterRegen > 0) {
3491
+ console.log(chalk.green(`✓ tasks.md regenerated with ${totalAfterRegen} checkbox tasks!\n`));
3492
+ regenResult = { success: true };
3493
+ } else {
3494
+ console.log(chalk.yellow(`⚠️ tasks.md still has no checkbox tasks after regeneration\n`));
3495
+ regenResult = { success: false };
3496
+ }
3497
+ }
3498
+ } catch (err) {
3499
+ regenResult = { success: false, error: err.message };
3500
+ }
3501
+ } else {
3502
+ const regenRequirement = { text: tasksRegenerationText, package: null, disabled: false };
3503
+ regenResult = await runIteration(regenRequirement, providerConfig, repoPath);
3504
+ }
3505
+
3506
+ if (!regenResult.success) {
3507
+ console.log(chalk.red('✗ Failed to regenerate tasks.md — skipping spec\n'));
3508
+ failedCount++;
3509
+ continue; // Skip to next spec
3510
+ }
3511
+ }
3512
+
3189
3513
  // Loop until spec is done or stalled
3190
3514
  while (true) {
3191
3515
  const task = getNextSpecTask(spec.path);
@@ -3237,27 +3561,53 @@ end tell
3237
3561
 
3238
3562
  if (result.success) {
3239
3563
  specProviderAttempts = 0;
3564
+ spec.totalIdeAttempts = 0; // Reset IDE attempts on success
3240
3565
  completedCount++;
3241
3566
  const { done, total } = countSpecCheckboxes(spec.path);
3242
3567
  const pct = total > 0 ? Math.round((done / total) * 100) : 0;
3243
3568
  console.log(chalk.bold.green(`📊 Spec progress: ${done}/${total} tasks (${pct}%) complete`));
3244
3569
  } else {
3245
3570
  const isRateLimitError = isRateLimitMessage(result.error);
3246
- const errorType = isRateLimitError ? 'Rate limit' : 'Error';
3571
+ const isMaxAttemptsExceeded = result.error === 'max_ide_attempts_exceeded';
3572
+ const isNoProgress = result.error === 'no_progress';
3573
+
3574
+ let errorType = 'Error';
3575
+ if (isRateLimitError) errorType = 'Rate limit';
3576
+ else if (isMaxAttemptsExceeded) errorType = 'Max IDE attempts exceeded';
3577
+ else if (isNoProgress) errorType = 'No progress';
3578
+
3579
+ // Update total IDE attempts tracking
3580
+ if (result.totalAttempts) {
3581
+ spec.totalIdeAttempts = result.totalAttempts;
3582
+ }
3247
3583
 
3248
3584
  specProviderAttempts++;
3249
3585
  failedCount++;
3250
3586
 
3587
+ if (isMaxAttemptsExceeded) {
3588
+ // Auto mode was stopped, exit completely
3589
+ console.log(chalk.red(`\n✗ ${errorType} — auto mode stopped\n`));
3590
+ return { completedCount, failedCount };
3591
+ }
3592
+
3251
3593
  if (specProviderAttempts > MAX_SPEC_TASK_ATTEMPTS) {
3252
3594
  console.log(chalk.red(`\n✗ Max provider attempts reached — moving to next spec\n`));
3253
3595
  break;
3254
3596
  }
3255
3597
 
3256
- console.log(chalk.yellow(`⚠️ ${errorType} on spec task, switching provider...`));
3598
+ if (isNoProgress) {
3599
+ console.log(chalk.yellow(`⚠️ ${errorType} on ${providerConfig.displayName}, switching IDE...`));
3600
+ } else {
3601
+ console.log(chalk.yellow(`⚠️ ${errorType} on spec task, switching provider...`));
3602
+ }
3603
+
3257
3604
  const newProviderConfig = await acquireProviderConfig(providerConfig.provider, providerConfig.model);
3258
3605
  if (newProviderConfig) {
3259
3606
  providerConfig = newProviderConfig;
3260
3607
  console.log(chalk.green(`✓ Switched to: ${providerConfig.displayName}\n`));
3608
+ } else {
3609
+ console.log(chalk.red(`✗ No alternative providers available\n`));
3610
+ break;
3261
3611
  }
3262
3612
  }
3263
3613
  }
@@ -3276,12 +3626,29 @@ end tell
3276
3626
  // Get current requirement first to check if there are any TODO items
3277
3627
  const requirement = await getCurrentRequirement(repoPath);
3278
3628
  if (!requirement) {
3279
- if (completedCount > 0 || failedCount > 0) {
3280
- console.log(chalk.bold.yellow('\n🎉 All requirements completed!'));
3629
+ // Check if we should process default requirement
3630
+ const requirementsPath = await getRequirementsPath(repoPath);
3631
+ const storage = new JSONStorage();
3632
+ const parser = new RequirementFileParser();
3633
+ const defaultManager = new DefaultRequirementManager(storage);
3634
+ const sequencer = new RequirementSequencer(parser, defaultManager);
3635
+
3636
+ const shouldProcessDefault = await sequencer.shouldProcessDefault(requirementsPath);
3637
+
3638
+ if (shouldProcessDefault) {
3639
+ console.log(chalk.bold.cyan('\n⭐ Default Requirement Mode'));
3640
+ console.log(chalk.gray('All regular requirements completed, processing default requirement...\n'));
3641
+
3642
+ // Process default requirement until completion or max iterations
3643
+ await processDefaultRequirement(sequencer, defaultManager, providerConfig, repoPath);
3281
3644
  } else {
3282
- console.log(chalk.bold.yellow('\n🎉 No requirements to process.'));
3645
+ if (completedCount > 0 || failedCount > 0) {
3646
+ console.log(chalk.bold.yellow('\n🎉 All requirements completed!'));
3647
+ } else {
3648
+ console.log(chalk.bold.yellow('\n🎉 No requirements to process.'));
3649
+ }
3650
+ console.log(chalk.gray(`${t('auto.direct.no.more.todo.items')}\n`));
3283
3651
  }
3284
- console.log(chalk.gray(`${t('auto.direct.no.more.todo.items')}\n`));
3285
3652
  break;
3286
3653
  }
3287
3654
 
@@ -3426,12 +3793,32 @@ end tell
3426
3793
  // Stop Auto Mode status tracking
3427
3794
  await stopAutoMode('completed');
3428
3795
 
3796
+ // Clean up keyboard handler
3797
+ if (keyboardHandler) {
3798
+ keyboardHandler.stop();
3799
+ }
3800
+
3801
+ // Stop requirement monitoring
3802
+ if (stopMonitoring) {
3803
+ stopMonitoring();
3804
+ }
3805
+
3429
3806
  } catch (error) {
3430
3807
  console.error(chalk.red('\n' + t('auto.fatal.error')), error.message);
3431
3808
  if (error.stack) {
3432
3809
  console.log(chalk.gray(error.stack));
3433
3810
  }
3434
3811
 
3812
+ // Clean up keyboard handler
3813
+ if (keyboardHandler) {
3814
+ keyboardHandler.stop();
3815
+ }
3816
+
3817
+ // Stop requirement monitoring
3818
+ if (stopMonitoring) {
3819
+ stopMonitoring();
3820
+ }
3821
+
3435
3822
  // Stop Auto Mode status tracking on fatal error
3436
3823
  await stopAutoMode('error');
3437
3824
  process.exit(1);