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