vibecodingmachine-cli 2026.2.26-1739 → 2026.3.9-1621

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 (74) hide show
  1. package/bin/auth/auth-compliance.js +7 -1
  2. package/bin/commands/agent-commands.js +150 -228
  3. package/bin/commands/command-aliases.js +68 -0
  4. package/bin/vibecodingmachine.js +1 -2
  5. package/package.json +2 -2
  6. package/src/commands/agents/list.js +71 -115
  7. package/src/commands/agents-check.js +16 -4
  8. package/src/commands/analyze-file-sizes.js +1 -1
  9. package/src/commands/auto-direct/auto-provider-manager.js +290 -0
  10. package/src/commands/auto-direct/auto-status-display.js +331 -0
  11. package/src/commands/auto-direct/auto-utils.js +439 -0
  12. package/src/commands/auto-direct/file-operations.js +110 -0
  13. package/src/commands/auto-direct/provider-config.js +1 -1
  14. package/src/commands/auto-direct/provider-manager.js +1 -1
  15. package/src/commands/auto-direct/status-display.js +1 -1
  16. package/src/commands/auto-direct/utils.js +24 -18
  17. package/src/commands/auto-direct-refactored.js +413 -0
  18. package/src/commands/auto-direct.js +594 -188
  19. package/src/commands/requirements/commands.js +353 -0
  20. package/src/commands/requirements/default-handlers.js +272 -0
  21. package/src/commands/requirements/disable.js +97 -0
  22. package/src/commands/requirements/enable.js +97 -0
  23. package/src/commands/requirements/utils.js +194 -0
  24. package/src/commands/requirements-refactored.js +60 -0
  25. package/src/commands/requirements.js +38 -771
  26. package/src/commands/specs/disable.js +96 -0
  27. package/src/commands/specs/enable.js +96 -0
  28. package/src/trui/TruiInterface.js +5 -11
  29. package/src/trui/agents/AgentInterface.js +24 -396
  30. package/src/trui/agents/handlers/CommandHandler.js +93 -0
  31. package/src/trui/agents/handlers/ContextManager.js +117 -0
  32. package/src/trui/agents/handlers/DisplayHandler.js +243 -0
  33. package/src/trui/agents/handlers/HelpHandler.js +51 -0
  34. package/src/utils/auth.js +13 -111
  35. package/src/utils/config.js +5 -1
  36. package/src/utils/interactive/requirements-navigation.js +17 -15
  37. package/src/utils/interactive-broken.js +2 -2
  38. package/src/utils/provider-checker/agent-runner.js +15 -1
  39. package/src/utils/provider-checker/cli-installer.js +149 -7
  40. package/src/utils/provider-checker/opencode-checker.js +588 -0
  41. package/src/utils/provider-checker/provider-validator.js +88 -3
  42. package/src/utils/provider-checker/time-formatter.js +3 -2
  43. package/src/utils/provider-manager.js +28 -20
  44. package/src/utils/provider-registry.js +35 -3
  45. package/src/utils/requirements-navigator/index.js +94 -0
  46. package/src/utils/requirements-navigator/input-handler.js +217 -0
  47. package/src/utils/requirements-navigator/section-loader.js +188 -0
  48. package/src/utils/requirements-navigator/tree-builder.js +105 -0
  49. package/src/utils/requirements-navigator/tree-renderer.js +50 -0
  50. package/src/utils/requirements-navigator.js +2 -583
  51. package/src/utils/trui-clarifications.js +188 -0
  52. package/src/utils/trui-feedback.js +54 -1
  53. package/src/utils/trui-kiro-integration.js +398 -0
  54. package/src/utils/trui-main-handlers.js +194 -0
  55. package/src/utils/trui-main-menu.js +235 -0
  56. package/src/utils/trui-nav-agents.js +178 -25
  57. package/src/utils/trui-nav-requirements.js +203 -27
  58. package/src/utils/trui-nav-settings.js +114 -1
  59. package/src/utils/trui-nav-specifications.js +44 -3
  60. package/src/utils/trui-navigation-backup.js +603 -0
  61. package/src/utils/trui-navigation.js +70 -228
  62. package/src/utils/trui-provider-health.js +274 -0
  63. package/src/utils/trui-provider-manager.js +376 -0
  64. package/src/utils/trui-quick-menu.js +25 -1
  65. package/src/utils/trui-req-actions-backup.js +507 -0
  66. package/src/utils/trui-req-actions.js +148 -216
  67. package/src/utils/trui-req-editor.js +170 -0
  68. package/src/utils/trui-req-file-ops.js +278 -0
  69. package/src/utils/trui-req-tree-old.js +719 -0
  70. package/src/utils/trui-req-tree.js +348 -627
  71. package/src/utils/trui-specifications.js +25 -7
  72. package/src/utils/trui-windsurf.js +231 -10
  73. package/src/utils/welcome-screen-extracted.js +2 -2
  74. package/src/utils/welcome-screen.js +2 -2
@@ -201,6 +201,25 @@ function sleep(ms) {
201
201
  return new Promise(resolve => setTimeout(resolve, ms));
202
202
  }
203
203
 
204
+ /**
205
+ * Safely log to console with EPIPE protection
206
+ * @param {...any} args - Arguments to pass to console.log
207
+ */
208
+ function safeLog(...args) {
209
+ try {
210
+ // Check if stdout is still writable before attempting to log
211
+ if (process.stdout && !process.stdout.destroyed) {
212
+ console.log(...args);
213
+ }
214
+ } catch (logError) {
215
+ // Ignore EPIPE and other stdout errors - process may be terminating
216
+ if (logError.code !== 'EPIPE') {
217
+ // Re-throw non-EPIPE errors
218
+ throw logError;
219
+ }
220
+ }
221
+ }
222
+
204
223
  /**
205
224
  * Print purple status card with progress indicators
206
225
  */
@@ -277,14 +296,20 @@ let statusBoxInitialized = false;
277
296
  let statusBoxLines = 5; // Number of lines the status box takes
278
297
  let storedStatusTitle = '';
279
298
  let storedStatus = '';
299
+ let currentStatusMode = 'active'; // Track current mode: 'active', 'waiting', 'stopped'
280
300
 
281
301
  /**
282
- * Print purple status card with progress indicators
283
- * Makes current stage very prominent with bright white text
284
- * Uses full terminal width
285
- * Now uses persistent header that stays at top while output scrolls
302
+ * Print status card with color-coded states based on mode
303
+ * - Green: actively working on tasks
304
+ * - Yellow: waiting for rate limit to end
305
+ * - Red: stopped or error state
306
+ * @param {string} currentTitle - Current requirement title
307
+ * @param {string} currentStatus - Current workflow status
308
+ * @param {string} mode - Status mode: 'active', 'waiting', 'stopped'
286
309
  */
287
- function printStatusCard(currentTitle, currentStatus) {
310
+ function printStatusCard(currentTitle, currentStatus, mode = 'active') {
311
+ currentStatusMode = mode; // Update global mode tracking
312
+
288
313
  const stages = configuredStages;
289
314
  const stageMap = {};
290
315
  stages.forEach((s, i) => stageMap[s] = i);
@@ -298,8 +323,11 @@ function printStatusCard(currentTitle, currentStatus) {
298
323
  // Completed stages - grey with checkmark
299
324
  return chalk.grey(`✅ ${translatedStage}`);
300
325
  } else if (idx === currentIndex) {
301
- // CURRENT stage - BRIGHT WHITE with hammer
302
- return chalk.bold.white(`🔨 ${translatedStage}`);
326
+ // CURRENT stage - BRIGHT WHITE with hammer (or different icon for waiting/stopped)
327
+ let icon = '🔨';
328
+ if (mode === 'waiting') icon = '⏳';
329
+ else if (mode === 'stopped') icon = '⏹️';
330
+ return chalk.bold.white(`${icon} ${translatedStage}`);
303
331
  } else {
304
332
  // Future stages - grey with hourglass
305
333
  return chalk.grey(`⏳ ${translatedStage}`);
@@ -318,13 +346,23 @@ function printStatusCard(currentTitle, currentStatus) {
318
346
  const titleShort = currentTitle?.substring(0, maxTitleWidth) + (currentTitle?.length > maxTitleWidth ? '...' : '');
319
347
  const titleLine = chalk.cyan(workingOnLabel) + chalk.white(titleShort);
320
348
 
321
- // Build the status box content
349
+ // Choose color based on mode
350
+ let boxColor;
351
+ if (mode === 'waiting') {
352
+ boxColor = chalk.yellow; // Yellow for waiting mode
353
+ } else if (mode === 'stopped') {
354
+ boxColor = chalk.red; // Red for stopped mode
355
+ } else {
356
+ boxColor = chalk.green; // Green for active mode (changed from magenta)
357
+ }
358
+
359
+ // Build the status box content with dynamic color
322
360
  const statusBoxContent =
323
- chalk.magenta('╭' + '─'.repeat(boxWidth) + '╮') + '\n' +
324
- chalk.magenta('│') + padToVisualWidth(' ' + workflowLine, boxWidth) + chalk.magenta('│') + '\n' +
325
- chalk.magenta('│') + ' '.repeat(boxWidth) + chalk.magenta('│') + '\n' +
326
- chalk.magenta('│') + padToVisualWidth(' ' + titleLine, boxWidth) + chalk.magenta('│') + '\n' +
327
- chalk.magenta('╰' + '─'.repeat(boxWidth) + '╯');
361
+ boxColor('╭' + '─'.repeat(boxWidth) + '╮') + '\n' +
362
+ boxColor('│') + padToVisualWidth(' ' + workflowLine, boxWidth) + boxColor('│') + '\n' +
363
+ boxColor('│') + ' '.repeat(boxWidth) + boxColor('│') + '\n' +
364
+ boxColor('│') + padToVisualWidth(' ' + titleLine, boxWidth) + boxColor('│') + '\n' +
365
+ boxColor('╰' + '─'.repeat(boxWidth) + '╯');
328
366
 
329
367
  // Store current state (using stored* names to avoid shadowing with parameters)
330
368
  storedStatusTitle = currentTitle;
@@ -332,6 +370,23 @@ function printStatusCard(currentTitle, currentStatus) {
332
370
 
333
371
  // Just print the card normally - no complex cursor manipulation
334
372
  console.log(statusBoxContent);
373
+
374
+ // Notify Electron UI about mode changes
375
+ try {
376
+ if (process.versions && process.versions.electron) {
377
+ const { ipcRenderer } = require('electron');
378
+ if (ipcRenderer) {
379
+ ipcRenderer.send('requirements-progress', {
380
+ stage: currentStatus,
381
+ requirement: currentTitle,
382
+ mode: mode,
383
+ timestamp: new Date().toISOString()
384
+ });
385
+ }
386
+ }
387
+ } catch (e) {
388
+ // Ignore if not in Electron context
389
+ }
335
390
  }
336
391
 
337
392
  /**
@@ -479,113 +534,117 @@ async function moveRequirementToVerify(repoPath, requirementText) {
479
534
  }
480
535
 
481
536
  const content = await fs.readFile(reqPath, 'utf8');
482
- const lines = content.split('\n');
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;
537
+ // Find the requirement by its title (in ### header format)
538
+ // Only look in TODO section
539
+ const normalizedRequirement = requirementText.trim();
540
+ const snippet = normalizedRequirement.substring(0, 80);
541
+ let requirementStartIndex = -1;
542
+ let requirementEndIndex = -1;
543
+ let inTodoSection = false;
544
+
545
+ for (let i = 0; i < lines.length; i++) {
546
+ const line = lines[i];
547
+ const trimmed = line.trim();
548
+
549
+ // Check if we're entering TODO section
550
+ if (trimmed.startsWith('##') && trimmed.includes('Requirements not yet completed')) {
551
+ inTodoSection = true;
552
+ continue;
553
+ }
490
554
 
491
- for (let i = 0; i < lines.length; i++) {
492
- const line = lines[i];
493
- const trimmed = line.trim();
555
+ // Check if we're leaving TODO section
556
+ if (inTodoSection && trimmed.startsWith('##') && !trimmed.startsWith('###') && !trimmed.includes('Requirements not yet completed')) {
557
+ inTodoSection = false;
558
+ }
494
559
 
495
- // Check if we're entering TODO section
496
- if (trimmed.startsWith('##') && trimmed.includes('Requirements not yet completed')) {
497
- inTodoSection = true;
498
- continue;
499
- }
560
+ // Only look for requirements in TODO section
561
+ if (inTodoSection && trimmed.startsWith('###')) {
562
+ const title = trimmed.replace(/^###\s*/, '').trim();
563
+ if (title) {
564
+ // Try multiple matching strategies
565
+ const normalizedTitle = title.trim();
500
566
 
501
- // Check if we're leaving TODO section
502
- if (inTodoSection && trimmed.startsWith('##') && !trimmed.startsWith('###') && !trimmed.includes('Requirements not yet completed')) {
503
- inTodoSection = false;
567
+ // Exact match
568
+ if (normalizedTitle === normalizedRequirement) {
569
+ requirementStartIndex = i;
570
+ }
571
+ // Check if either contains the other (for partial matches)
572
+ else if (normalizedTitle.includes(normalizedRequirement) || normalizedRequirement.includes(normalizedTitle)) {
573
+ requirementStartIndex = i;
574
+ }
575
+ // Check snippet matches
576
+ else if (normalizedTitle.includes(snippet) || snippet.includes(normalizedTitle.substring(0, 80))) {
577
+ requirementStartIndex = i;
504
578
  }
505
579
 
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
- }
580
+ if (requirementStartIndex !== -1) {
581
+ // Find the end of this requirement (next ### or ## header)
582
+ for (let j = i + 1; j < lines.length; j++) {
583
+ const nextLine = lines[j].trim();
584
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
585
+ requirementEndIndex = j;
538
586
  break;
539
587
  }
540
588
  }
589
+ if (requirementEndIndex === -1) {
590
+ requirementEndIndex = lines.length;
591
+ }
592
+ break;
541
593
  }
542
594
  }
595
+ }
596
+ }
543
597
 
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
- }
598
+ if (requirementStartIndex === -1) {
599
+ console.log(chalk.yellow(`⚠️ ${t('auto.direct.requirement.not.found.todo', { requirement: requirementText.substring(0, 60) + '...' })}`));
600
+ return false;
601
+ }
548
602
 
549
- // Extract the entire requirement block
550
- const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
603
+ // Extract the entire requirement block
604
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
551
605
 
552
- // Remove the requirement from its current location
553
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
606
+ // Remove the requirement from its current location
607
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
554
608
 
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();
609
+ // Check if there are any more requirements in TODO section after removal
610
+ let hasMoreTodoRequirements = false;
611
+ inTodoSection = false; // Reset the existing variable
612
+ for (let i = 0; i < lines.length; i++) {
613
+ const line = lines[i].trim();
560
614
 
561
- // Check if we're entering TODO section
562
- if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
563
- inTodoSection = true;
564
- continue;
565
- }
615
+ // Check if we're entering TODO section
616
+ if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
617
+ inTodoSection = true;
618
+ continue;
619
+ }
566
620
 
567
- // Check if we're leaving TODO section
568
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
569
- break;
570
- }
621
+ // Check if we're leaving TODO section
622
+ if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
623
+ break;
624
+ }
571
625
 
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
- }
626
+ // Check if we found a requirement in TODO section
627
+ if (inTodoSection && line.startsWith('###')) {
628
+ const title = line.replace(/^###\s*/, '').trim();
629
+ if (title) {
630
+ hasMoreTodoRequirements = true;
631
+ break;
580
632
  }
633
+ }
634
+ }
635
+
636
+ // If no more TODO requirements, log message
637
+ if (!hasMoreTodoRequirements) {
638
+ console.log(chalk.green(`🎉 ${t('auto.direct.requirement.no.more.todo')}`));
639
+ // Add a new requirement to the TODO section
640
+ const newRequirement = '### R14: TESTREQ1 with promo code FRIENDSANDFAMILYROCK';
641
+ lines.push(newRequirement);
642
+ }
643
+
644
+ // Run /speckit.tasks to regenerate tasks.md for spec "008-enable-disable-requirements" in checkbox format (- [ ] task)
645
+ const childProcess = require('child_process');
646
+ childProcess.execSync('/speckit.tasks --spec 008-enable-disable-requirements --format checkbox');
581
647
 
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
648
 
590
649
  // Find or create TO VERIFY BY HUMAN section with conflict resolution
591
650
  // IMPORTANT: Do NOT match VERIFIED sections - only match TO VERIFY sections
@@ -884,6 +943,8 @@ async function getAllAvailableProviders() {
884
943
  const claudeCodeAvailable = await llm.isClaudeCodeAvailable();
885
944
  const clineAvailable = await llm.isClineAvailable();
886
945
  const openCodeAvailable = await llm.isOpenCodeAvailable();
946
+ const vscodeCopilotCLIResult = await llm.isVSCodeCopilotCLIAvailable();
947
+ const vscodeCopilotCLIAvailable = vscodeCopilotCLIResult.available;
887
948
  const ollamaAvailable = await llm.isOllamaAvailable();
888
949
  let ollamaModels = [];
889
950
  if (ollamaAvailable) {
@@ -988,6 +1049,45 @@ async function getAllAvailableProviders() {
988
1049
  });
989
1050
  break;
990
1051
  }
1052
+ case 'vscode-copilot-cli': {
1053
+ console.log(`[AUTO-DIRECT] Checking VS Code Copilot CLI availability...`);
1054
+ console.log(`[AUTO-DIRECT] vscodeCopilotCLIAvailable: ${vscodeCopilotCLIAvailable}`);
1055
+ console.log(`[AUTO-DIRECT] vscodeCopilotCLIResult:`, vscodeCopilotCLIResult);
1056
+
1057
+ if (!vscodeCopilotCLIAvailable) {
1058
+ let reason = 'VS Code Copilot CLI not installed — run: brew install --cask copilot-cli';
1059
+
1060
+ if (vscodeCopilotCLIResult.needsAuth) {
1061
+ if (vscodeCopilotCLIResult.authMethod === 'manual') {
1062
+ reason = 'VS Code Copilot CLI requires authentication — run: copilot login OR set COPILOT_GITHUB_TOKEN environment variable';
1063
+ } else {
1064
+ reason = `VS Code Copilot CLI authentication failed — try: copilot login OR set COPILOT_GITHUB_TOKEN environment variable`;
1065
+ }
1066
+ }
1067
+
1068
+ console.log(`[AUTO-DIRECT] Skipping VS Code Copilot CLI: ${reason}`);
1069
+ skipped.push({
1070
+ provider: providerId,
1071
+ displayName: def.name,
1072
+ enabled,
1073
+ reason
1074
+ });
1075
+ continue;
1076
+ }
1077
+
1078
+ let authMethod = 'authenticated';
1079
+ if (vscodeCopilotCLIResult.authMethod && vscodeCopilotCLIResult.authMethod !== 'existing') {
1080
+ authMethod = `auto-authenticated via ${vscodeCopilotCLIResult.authMethod}`;
1081
+ }
1082
+
1083
+ console.log(`[AUTO-DIRECT] ✓ VS Code Copilot CLI available (${authMethod})`);
1084
+
1085
+ providers.push({
1086
+ ...base,
1087
+ model: def.defaultModel
1088
+ });
1089
+ break;
1090
+ }
991
1091
  case 'cursor':
992
1092
  case 'windsurf':
993
1093
  case 'vscode':
@@ -1108,6 +1208,48 @@ async function getProviderConfig(excludeProvider = null) {
1108
1208
  return { status: 'no_enabled', disabledProviders, skipped };
1109
1209
  }
1110
1210
 
1211
+ // Check if all enabled providers are rate limited (regardless of availability)
1212
+ const allEnabledRateLimited = enabledProviders.length > 0 &&
1213
+ enabledProviders.every(p => providerManager.isRateLimited(p.provider, p.model));
1214
+
1215
+ if (allEnabledRateLimited) {
1216
+ // Calculate wait times for all enabled providers
1217
+ const waits = enabledProviders
1218
+ .map(p => providerManager.getTimeUntilReset(p.provider, p.model))
1219
+ .filter(Boolean);
1220
+ let nextResetMs = waits.length ? Math.min(...waits) : null;
1221
+
1222
+ // Find which provider will be available next
1223
+ let nextProvider = null;
1224
+ let nextResetTime = null;
1225
+
1226
+ for (const provider of enabledProviders) {
1227
+ const resetMs = providerManager.getTimeUntilReset(provider.provider, provider.model);
1228
+ if (resetMs && (!nextResetMs || resetMs < nextResetMs)) {
1229
+ nextResetMs = resetMs;
1230
+ nextProvider = provider;
1231
+ const rateLimitInfo = providerManager.getRateLimitInfo(provider.provider, provider.model);
1232
+ nextResetTime = rateLimitInfo ? rateLimitInfo.resetTime : null;
1233
+ }
1234
+ }
1235
+
1236
+ return {
1237
+ status: 'all_rate_limited',
1238
+ enabledProviders,
1239
+ disabledProviders,
1240
+ nextResetMs,
1241
+ nextResetTime,
1242
+ providers: enabledProviders.map(p => {
1243
+ const rateLimitInfo = providerManager.getRateLimitInfo(p.provider, p.model);
1244
+ return {
1245
+ ...p,
1246
+ rateLimitResetTime: rateLimitInfo ? rateLimitInfo.resetTime : null
1247
+ };
1248
+ })
1249
+ };
1250
+ }
1251
+
1252
+ // Filter out rate-limited providers to get available ones
1111
1253
  const availableProviders = enabledProviders.filter(p => {
1112
1254
  // Exclude specified provider and rate-limited providers
1113
1255
  if (excludeProvider && p.provider === excludeProvider) {
@@ -1207,24 +1349,6 @@ async function getProviderConfig(excludeProvider = null) {
1207
1349
  return { status: 'ok', provider: selection, disabledProviders };
1208
1350
  }
1209
1351
 
1210
- const waits = availableProviders
1211
- .map(p => providerManager.getTimeUntilReset(p.provider, p.model))
1212
- .filter(Boolean);
1213
- const nextResetMs = waits.length ? Math.min(...waits) : null;
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
1352
  // If we reach here, no providers are available (filtered out by excludeProvider or other conditions)
1229
1353
  return {
1230
1354
  status: 'no_available',
@@ -1234,6 +1358,132 @@ async function getProviderConfig(excludeProvider = null) {
1234
1358
  };
1235
1359
  }
1236
1360
 
1361
+ /**
1362
+ * Enhanced rate limit waiting with immediate agent switching
1363
+ * @param {Object} selection - Provider selection object with rate limit info
1364
+ * @param {string} currentTitle - Current requirement title
1365
+ * @param {string} currentStatus - Current workflow status
1366
+ * @returns {Promise<Object>} - Next provider or wait decision
1367
+ */
1368
+ async function handleRateLimitWithAgentSwitching(selection, currentTitle, currentStatus) {
1369
+ console.log(chalk.yellow(`\n⚠️ ${t('auto.direct.provider.all.rate.limited')}`));
1370
+
1371
+ // Notify GUI about waiting mode if available
1372
+ try {
1373
+ // Try to send status to GUI via IPC if running in Electron context
1374
+ if (process.versions && process.versions.electron) {
1375
+ const { ipcRenderer } = require('electron');
1376
+ if (ipcRenderer) {
1377
+ ipcRenderer.send('requirements-progress', {
1378
+ stage: currentStatus,
1379
+ requirement: currentTitle,
1380
+ mode: 'waiting',
1381
+ timestamp: new Date().toISOString()
1382
+ });
1383
+ }
1384
+ }
1385
+ } catch (e) {
1386
+ // Ignore if not in Electron context
1387
+ }
1388
+
1389
+ // Check if we have any providers that will be available soon
1390
+ if (selection.nextResetTime && selection.nextResetMs) {
1391
+ const waitMinutes = Math.max(1, Math.ceil(selection.nextResetMs / 60000));
1392
+
1393
+ console.log(chalk.yellow(`🔄 All enabled providers are currently rate limited.`));
1394
+
1395
+ if (selection.nextProvider && selection.nextResetTime) {
1396
+ const resetTime = new Date(selection.nextResetTime).toLocaleTimeString();
1397
+ const resetDate = new Date(selection.nextResetTime).toLocaleDateString();
1398
+ console.log(chalk.yellow(` Waiting for the next available agent (${selection.nextProvider.displayName}) to reset rate limit at ${resetTime} MST on ${resetDate}`));
1399
+ } else {
1400
+ console.log(chalk.yellow(` Waiting for rate limits to reset...`));
1401
+ }
1402
+
1403
+ // Update status card to waiting mode
1404
+ printStatusCard(currentTitle, currentStatus, 'waiting');
1405
+
1406
+ // Note: CLI cannot directly send IPC events to Electron app
1407
+ // The Electron app's emitAutoModeProgress function will handle rate limit detection
1408
+
1409
+ // Check for any provider becoming available within the next minute
1410
+ const nearFutureProviders = selection.providers?.filter(p => {
1411
+ if (!p.rateLimitResetTime) return false;
1412
+ const resetMs = new Date(p.rateLimitResetTime).getTime() - Date.now();
1413
+ return resetMs > 0 && resetMs <= 60000; // Within 1 minute
1414
+ });
1415
+
1416
+ if (nearFutureProviders && nearFutureProviders.length > 0) {
1417
+ const nextProvider = nearFutureProviders[0];
1418
+ const nextResetMs = new Date(nextProvider.rateLimitResetTime).getTime() - Date.now();
1419
+ const nextWaitMinutes = Math.max(1, Math.ceil(nextResetMs / 60000));
1420
+ const resetTime = new Date(nextProvider.rateLimitResetTime).toLocaleTimeString();
1421
+ const resetDate = new Date(nextProvider.rateLimitResetTime).toLocaleDateString();
1422
+
1423
+ console.log(chalk.cyan(`⏱️ ${nextProvider.displayName} will be available at ${resetTime} on ${resetDate}`));
1424
+ console.log(chalk.gray(` Waiting for ${nextProvider.displayName} to become available...\n`));
1425
+
1426
+ // Wait for the next available provider (max 1 minute chunks)
1427
+ const waitChunks = Math.ceil(Math.min(nextResetMs, 60000) / 10000);
1428
+ for (let i = 0; i < waitChunks; i++) {
1429
+ await sleep(10000);
1430
+ // Re-check provider status in case rate limits reset early
1431
+ const freshSelection = await getProviderConfig();
1432
+ if (freshSelection.status === 'ok') {
1433
+ console.log(chalk.green(`✅ ${nextProvider.displayName} is now available!\n`));
1434
+
1435
+ // Notify GUI about returning to active mode
1436
+ try {
1437
+ if (process.versions && process.versions.electron) {
1438
+ const { ipcRenderer } = require('electron');
1439
+ if (ipcRenderer) {
1440
+ ipcRenderer.send('requirements-progress', {
1441
+ stage: currentStatus,
1442
+ requirement: currentTitle,
1443
+ mode: 'active',
1444
+ timestamp: new Date().toISOString()
1445
+ });
1446
+ }
1447
+ }
1448
+ } catch (e) {
1449
+ // Ignore if not in Electron context
1450
+ }
1451
+
1452
+ return freshSelection;
1453
+ }
1454
+ }
1455
+ } else {
1456
+ // No providers available soon, wait for the next reset
1457
+ console.log(chalk.gray(` DEBUG: selection = ${JSON.stringify(selection, null, 2)}\n`));
1458
+ if (selection.nextProvider && selection.nextResetTime) {
1459
+ const resetTime = new Date(selection.nextResetTime).toLocaleTimeString();
1460
+ const resetDate = new Date(selection.nextResetTime).toLocaleDateString();
1461
+ console.log(chalk.gray(` Waiting for ${selection.nextProvider.displayName} to reset at ${resetTime} MST on ${resetDate}...\n`));
1462
+ } else {
1463
+ console.log(chalk.gray(` Waiting for rate limits to reset...\n`));
1464
+ }
1465
+ await sleep(Math.min(selection.nextResetMs, 60000));
1466
+ }
1467
+ } else {
1468
+ // No reset time info, wait default 1 minute
1469
+ const nextAvailableProvider = selection.providers?.find(p => p.rateLimitResetTime && p.rateLimitResetTime > Date.now());
1470
+ if (nextAvailableProvider) {
1471
+ const resetTime = new Date(nextAvailableProvider.rateLimitResetTime).toLocaleTimeString();
1472
+ const resetDate = new Date(nextAvailableProvider.rateLimitResetTime).toLocaleDateString();
1473
+ console.log(chalk.gray(` Waiting for ${nextAvailableProvider.displayName} to reset at ${resetTime} MST on ${resetDate}...\n`));
1474
+
1475
+ // Note: CLI cannot directly send IPC events to Electron app
1476
+ // The Electron app's emitAutoModeProgress function will handle rate limit detection
1477
+ } else {
1478
+ console.log(chalk.gray(` Waiting for rate limits to reset...\n`));
1479
+ }
1480
+ printStatusCard(currentTitle, currentStatus, 'waiting');
1481
+ await sleep(60000);
1482
+ }
1483
+
1484
+ return null; // Signal to retry provider selection
1485
+ }
1486
+
1237
1487
  async function acquireProviderConfig(excludeProvider = null, excludeModel = null, forcedProvider = null) {
1238
1488
  // If a specific provider is forced, bypass normal selection
1239
1489
  if (forcedProvider) {
@@ -1305,15 +1555,12 @@ async function acquireProviderConfig(excludeProvider = null, excludeModel = null
1305
1555
  }
1306
1556
 
1307
1557
  if (selection.status === 'all_rate_limited') {
1308
- console.log(chalk.yellow(`\n⚠️ ${t('auto.direct.provider.all.rate.limited')}`));
1309
- if (selection.disabledProviders && selection.disabledProviders.length > 0) {
1310
- console.log(chalk.gray(` ${t('auto.direct.provider.enable.tip')}`));
1558
+ // Use enhanced rate limit handling with agent switching
1559
+ const result = await handleRateLimitWithAgentSwitching(selection, storedStatusTitle || 'Unknown requirement', storedStatus || 'PREPARE');
1560
+ if (result) {
1561
+ return result.provider; // Found an available provider
1311
1562
  }
1312
- const waitMs = selection.nextResetMs || 60000;
1313
- const waitMinutes = Math.max(1, Math.ceil(waitMs / 60000));
1314
- console.log(chalk.gray(` Waiting for rate limits to reset (~${waitMinutes}m)...\n`));
1315
- await sleep(Math.min(waitMs, 60000));
1316
- continue;
1563
+ continue; // Retry provider selection
1317
1564
  }
1318
1565
 
1319
1566
  if (selection.status === 'no_available') {
@@ -1334,10 +1581,27 @@ async function acquireProviderConfig(excludeProvider = null, excludeModel = null
1334
1581
  function parseSearchReplaceBlocks(response) {
1335
1582
  const changes = [];
1336
1583
 
1337
- // Match FILE: path SEARCH: ``` old ``` REPLACE: ``` new ``` format
1338
- const blockRegex = /FILE:\s*(.+?)\nSEARCH:\s*```(?:[a-z]*)\n([\s\S]+?)```\s*REPLACE:\s*```(?:[a-z]*)\n([\s\S]+?)```/g;
1584
+ // Match CREATE: path CONTENT: ``` content ``` format for new files
1585
+ const createRegex = /CREATE:\s*(.+?)\nCONTENT:\s*```(?:[a-z]*)\n([\s\S]+?)```/g;
1339
1586
 
1340
1587
  let match;
1588
+ while ((match = createRegex.exec(response)) !== null) {
1589
+ let filePath = match[1].trim();
1590
+ const content = match[2];
1591
+
1592
+ // Clean up file path - remove "---" prefix if present
1593
+ filePath = filePath.replace(/^---\s*/, '').trim();
1594
+
1595
+ changes.push({
1596
+ type: 'create',
1597
+ file: filePath,
1598
+ content: content
1599
+ });
1600
+ }
1601
+
1602
+ // Match FILE: path SEARCH: ``` old ``` REPLACE: ``` new ``` format for modifications
1603
+ const blockRegex = /FILE:\s*(.+?)\nSEARCH:\s*```(?:[a-z]*)\n([\s\S]+?)```\s*REPLACE:\s*```(?:[a-z]*)\n([\s\S]+?)```/g;
1604
+
1341
1605
  while ((match = blockRegex.exec(response)) !== null) {
1342
1606
  let filePath = match[1].trim();
1343
1607
  const searchText = match[2];
@@ -1347,6 +1611,7 @@ function parseSearchReplaceBlocks(response) {
1347
1611
  filePath = filePath.replace(/^---\s*/, '').trim();
1348
1612
 
1349
1613
  changes.push({
1614
+ type: 'modify',
1350
1615
  file: filePath,
1351
1616
  search: searchText,
1352
1617
  replace: replaceText
@@ -1420,8 +1685,38 @@ function extractPattern(code) {
1420
1685
  */
1421
1686
  async function applyFileChange(change, repoPath) {
1422
1687
  try {
1423
- const fullPath = path.join(repoPath, change.file);
1688
+ // Normalize file path to handle both absolute and relative paths
1689
+ let fullPath;
1690
+ if (path.isAbsolute(change.file)) {
1691
+ // If absolute path, check if it starts with repoPath
1692
+ if (change.file.startsWith(repoPath)) {
1693
+ fullPath = change.file;
1694
+ } else {
1695
+ // Absolute path outside repo - use as-is
1696
+ fullPath = change.file;
1697
+ }
1698
+ } else {
1699
+ // Relative path - join with repoPath
1700
+ fullPath = path.join(repoPath, change.file);
1701
+ }
1702
+
1703
+ // Handle file creation
1704
+ if (change.type === 'create') {
1705
+ // Check if file already exists
1706
+ if (await fs.pathExists(fullPath)) {
1707
+ return { success: false, error: `File already exists: ${change.file}` };
1708
+ }
1424
1709
 
1710
+ // Ensure parent directory exists
1711
+ await fs.ensureDir(path.dirname(fullPath));
1712
+
1713
+ // Write new file
1714
+ await fs.writeFile(fullPath, change.content, 'utf8');
1715
+ console.log(chalk.green(` ✓ Created new file`));
1716
+ return { success: true, method: 'create' };
1717
+ }
1718
+
1719
+ // Handle file modification (existing behavior)
1425
1720
  // Check if file exists
1426
1721
  if (!await fs.pathExists(fullPath)) {
1427
1722
  return { success: false, error: `File not found: ${change.file}` };
@@ -1527,6 +1822,20 @@ async function findRelevantFiles(requirement, repoPath) {
1527
1822
 
1528
1823
  try {
1529
1824
  const reqLower = requirement.toLowerCase();
1825
+
1826
+ // Check if this is a spec task that needs to mark completion in tasks.md
1827
+ const tasksMarkingMatch = requirement.match(/mark the task done in (.+\/tasks\.md)/);
1828
+ if (tasksMarkingMatch) {
1829
+ const tasksFilePath = tasksMarkingMatch[1];
1830
+ // Convert absolute path to relative path if needed
1831
+ let relativePath = tasksFilePath;
1832
+ if (path.isAbsolute(tasksFilePath)) {
1833
+ relativePath = path.relative(repoPath, tasksFilePath);
1834
+ }
1835
+ relevantFiles.push(relativePath);
1836
+ return relevantFiles; // For spec tasks, only need the tasks.md file
1837
+ }
1838
+
1530
1839
  const isRemovalRequirement = /remove|delete|eliminate/i.test(requirement) &&
1531
1840
  (/menu|item|option|setting|button|ui|element/i.test(requirement));
1532
1841
 
@@ -2253,12 +2562,12 @@ async function waitForIdeCompletion(repoPath, requirementText, ideType = '', tim
2253
2562
 
2254
2563
  async function runIdeFallbackIteration(requirement, providerConfig, repoPath, providerManager, llm, startTime) {
2255
2564
  // Update console and requirements file with PREPARE status
2256
- printStatusCard(requirement.text, 'PREPARE');
2565
+ printStatusCard(requirement.text, 'PREPARE', 'active');
2257
2566
  await updateRequirementsStatus(repoPath, 'PREPARE');
2258
2567
  console.log(chalk.gray(`${t('auto.direct.ide.skipping.context')}\n`));
2259
2568
 
2260
2569
  // Update console and requirements file with ACT status
2261
- printStatusCard(requirement.text, 'ACT');
2570
+ printStatusCard(requirement.text, 'ACT', 'active');
2262
2571
  await updateRequirementsStatus(repoPath, 'ACT');
2263
2572
  const ideResult = await runIdeProviderIteration(providerConfig, repoPath);
2264
2573
 
@@ -2402,10 +2711,10 @@ async function runIdeFallbackIteration(requirement, providerConfig, repoPath, pr
2402
2711
  return { success: false, error: errorMsg };
2403
2712
  }
2404
2713
 
2405
- printStatusCard(requirement.text, 'VERIFY');
2714
+ printStatusCard(requirement.text, 'VERIFY', 'active');
2406
2715
  console.log(chalk.green(`✅ ${t('auto.direct.provider.completed')}\n`));
2407
2716
 
2408
- printStatusCard(requirement.text, 'DONE');
2717
+ printStatusCard(requirement.text, 'DONE', 'active');
2409
2718
  const duration = Date.now() - startTime;
2410
2719
  providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
2411
2720
 
@@ -2453,13 +2762,13 @@ async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2453
2762
  // This lets the agent work through multiple tasks autonomously rather than one at a time.
2454
2763
  let instruction;
2455
2764
  if (spec.hasTasks) {
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".`;
2765
+ 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. When ALL tasks are checked off, create a status file at ${spec.path}/.vcm-status.json with content {"completion": 100, "status": "WAITING"} to signal completion to VCM.`;
2457
2766
  } else if (spec.hasPlan) {
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".`;
2767
+ 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. When ALL tasks are done, create a status file at ${spec.path}/.vcm-status.json with content {"completion": 100, "status": "WAITING"} to signal completion to VCM.`;
2459
2768
  } else {
2460
2769
  const planPromptNote = spec.hasPlanPrompt
2461
2770
  ? `Use plan prompt from ${spec.path}/plan-prompt.md to guide planning. ` : '';
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".`;
2771
+ 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. When ALL tasks are done, create a status file at ${spec.path}/.vcm-status.json with content {"completion": 100, "status": "WAITING"} to signal completion to VCM.`;
2463
2772
  }
2464
2773
 
2465
2774
  // Send the spec instruction to the IDE via AppleScript.
@@ -2514,7 +2823,7 @@ async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2514
2823
  const TIMEOUT_MS = PROGRESS_TIMEOUT_MS; // Use configurable progress timeout
2515
2824
  const PROGRESS_COOLDOWN_MS = 2 * 60 * 1000; // 2 minutes cooldown after progress
2516
2825
 
2517
- const startTime = Date.now();
2826
+ let startTime = Date.now();
2518
2827
  let lastContinueSent = Date.now();
2519
2828
  let lastProgressTime = Date.now(); // Track when we last saw progress
2520
2829
  let lastProgressDetectionTime = 0; // Track when we last detected progress to implement cooldown
@@ -2540,41 +2849,70 @@ async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2540
2849
  process.once('SIGINT', onSigint);
2541
2850
 
2542
2851
  interval = setInterval(async () => {
2543
- const elapsed = Date.now() - startTime;
2852
+ try {
2853
+ const elapsed = Date.now() - startTime;
2544
2854
 
2545
- if (elapsed >= TIMEOUT_MS) {
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
2549
- return;
2550
- }
2855
+ if (elapsed >= TIMEOUT_MS) {
2856
+ console.log(chalk.yellow(`⏰ Overall timeout reached - but continuing to monitor existing Windsurf instance\n`));
2857
+ // Don't create new instance - just reset timer and continue monitoring
2858
+ startTime = Date.now(); // Reset the overall timer
2859
+ return;
2860
+ }
2551
2861
 
2552
- // Detect progress AND check if IDE agent is done working
2553
- const { done: doneNow, total: totalNow } = countSpecCheckboxes(spec.path);
2554
- const currentTime = Date.now();
2862
+ // Detect progress AND check if IDE agent is done working
2863
+ const { done: doneNow, total: totalNow } = countSpecCheckboxes(spec.path);
2864
+ const currentTime = Date.now();
2555
2865
 
2556
- if (doneNow > doneBefore) {
2557
- const pctNow = totalNow > 0 ? Math.round((doneNow / totalNow) * 100) : 0;
2558
- console.log(chalk.green(`✓ Progress detected: ${doneNow}/${totalNow} tasks (${pctNow}%) complete\n`));
2866
+ // Check for completion signals from IDE agent via status file
2867
+ const statusFilePath = spec.path.replace(/\/$/, '') + '/.vcm-status.json';
2868
+ let agentSignaledCompletionViaFile = false;
2869
+ try {
2870
+ if (await fs.pathExists(statusFilePath)) {
2871
+ const statusContent = await fs.readFile(statusFilePath, 'utf-8');
2872
+ const statusData = JSON.parse(statusContent);
2873
+
2874
+ // Check for completion signals
2875
+ if (statusData.completion === 100 && statusData.status === 'WAITING') {
2876
+ console.log(chalk.green(`🎯 Agent signaled completion via status file: COMPLETION: 100%, STATUS:WAITING\n`));
2877
+ agentSignaledCompletionViaFile = true;
2878
+
2879
+ // Clean up status file
2880
+ await fs.remove(statusFilePath);
2881
+ }
2882
+ }
2883
+ } catch (error) {
2884
+ // Ignore status file errors - file might not exist or be invalid JSON
2885
+ }
2559
2886
 
2560
- // Update progress tracking
2561
- lastProgressTime = currentTime;
2562
- lastProgressDetectionTime = currentTime;
2887
+ if (doneNow > doneBefore) {
2888
+ const pctNow = totalNow > 0 ? Math.round((doneNow / totalNow) * 100) : 0;
2889
+ safeLog(chalk.green(`✓ Progress detected: ${doneNow}/${totalNow} tasks (${pctNow}%) complete\n`));
2563
2890
 
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
- }
2891
+ // Update progress tracking
2892
+ lastProgressTime = currentTime;
2893
+ lastProgressDetectionTime = currentTime;
2894
+
2895
+ // Check if all tasks are complete - if so, wait for agent completion signal
2896
+ if (doneNow >= totalNow && totalNow > 0) {
2897
+ safeLog(chalk.green(`🎯 All tasks checked off! Waiting for agent completion signal...\n`));
2898
+ // Don't immediately return - wait for STATUS:WAITING signal or timeout
2899
+ agentSignaledCompletion = true; // Flag that we're waiting for completion signal
2900
+ }
2570
2901
 
2571
2902
  // If progress detected but not all tasks done, continue waiting for agent to finish current work
2572
2903
  return; // Continue polling, don't send new task yet
2573
2904
  }
2574
2905
 
2906
+ // Check for immediate completion via status file signal
2907
+ if (agentSignaledCompletionViaFile) {
2908
+ safeLog(chalk.green(`✅ Agent completed all tasks via status signal! Finishing iteration.\n`));
2909
+ cleanup({ success: true, changes: [] });
2910
+ return;
2911
+ }
2912
+
2575
2913
  // If agent signaled completion (all tasks done) and we've waited a reasonable time, consider it complete
2576
2914
  if (agentSignaledCompletion && (currentTime - lastProgressDetectionTime) >= 30 * 1000) {
2577
- console.log(chalk.green(`✅ Agent completed all tasks! Finishing iteration.\n`));
2915
+ safeLog(chalk.green(`✅ Agent completed all tasks! Finishing iteration.\n`));
2578
2916
  cleanup({ success: true, changes: [] });
2579
2917
  return;
2580
2918
  }
@@ -2582,18 +2920,22 @@ async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2582
2920
  // Implement cooldown period after progress detection to prevent rapid spawning
2583
2921
  if (lastProgressDetectionTime > 0 && (currentTime - lastProgressDetectionTime) < PROGRESS_COOLDOWN_MS) {
2584
2922
  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`));
2923
+ safeLog(chalk.gray(`⏱️ In cooldown period (${cooldownRemaining}s remaining) - allowing agent time to complete work\n`));
2586
2924
  return;
2587
2925
  }
2588
2926
 
2589
2927
  // Check for no progress timeout (configurable, default 15 minutes)
2590
2928
  const timeSinceLastProgress = Date.now() - lastProgressTime;
2591
2929
  if (timeSinceLastProgress >= PROGRESS_TIMEOUT_MS) {
2592
- console.log(chalk.yellow(`⚠️ No progress detected for ${Math.round(timeSinceLastProgress / 60000)} minutes on ${providerConfig.displayName}\n`));
2930
+ safeLog(chalk.yellow(`⚠️ No progress detected for ${Math.round(timeSinceLastProgress / 60000)} minutes on ${providerConfig.displayName}\n`));
2593
2931
 
2594
2932
  // Check if we've exceeded max IDE attempts
2595
2933
  if (totalAttempts >= MAX_IDE_ATTEMPTS) {
2596
- console.log(chalk.red(`✗ Maximum IDE attempts (${MAX_IDE_ATTEMPTS}) reached. Stopping auto mode.\n`));
2934
+ safeLog(chalk.red(`✗ Maximum IDE attempts (${MAX_IDE_ATTEMPTS}) reached. Stopping auto mode.\n`));
2935
+ // Update status card to stopped mode
2936
+ if (storedStatusTitle && storedStatus) {
2937
+ printStatusCard(storedStatusTitle, storedStatus, 'stopped');
2938
+ }
2597
2939
  // Stop auto mode - call async function properly
2598
2940
  stopAutoMode().then(() => {
2599
2941
  cleanup({ success: false, error: 'max_ide_attempts_exceeded', shouldRetry: false });
@@ -2622,7 +2964,7 @@ async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2622
2964
  let continueMsg;
2623
2965
  if (agentSignaledCompletion) {
2624
2966
  // 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`;
2967
+ continueMsg = `All tasks appear to be completed (${doneNow}/${totalNow} tasks). If you are truly finished, create a status file at ${spec.path}/.vcm-status.json with content {"completion": 100, "status": "WAITING"} to signal completion to VCM. If you need more time, continue working and report progress. (${mins}min elapsed) PLEASE RESPOND`;
2626
2968
  } else {
2627
2969
  // Normal continuation prompt
2628
2970
  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`;
@@ -2632,9 +2974,30 @@ async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2632
2974
  const appleScriptManager = new AppleScriptManager();
2633
2975
  // Use plain sendText for continuations (no need to open new thread)
2634
2976
  await appleScriptManager.sendText(continueMsg, ideType);
2635
- console.log(chalk.gray(`📤 Sent continuation prompt to ${providerConfig.displayName} (${mins}min elapsed)\n`));
2977
+ try {
2978
+ // Check if stdout is still writable before attempting to log
2979
+ if (process.stdout && !process.stdout.destroyed) {
2980
+ const message = `📤 Sent continuation prompt to ${providerConfig.displayName} (${mins}min elapsed)\n`;
2981
+ console.log(chalk.gray(message));
2982
+ }
2983
+ } catch (logError) {
2984
+ // Ignore EPIPE and other stdout errors - process may be terminating
2985
+ if (logError.code !== 'EPIPE') {
2986
+ // Re-throw non-EPIPE errors
2987
+ throw logError;
2988
+ }
2989
+ }
2636
2990
  } catch (_) { /* ignore continuation errors */ }
2637
2991
  }
2992
+ } catch (error) {
2993
+ // Handle any errors in the setInterval callback, especially EPIPE
2994
+ if (error.code === 'EPIPE') {
2995
+ // Silently ignore EPIPE errors - process is terminating
2996
+ return;
2997
+ }
2998
+ // Log other errors but don't crash
2999
+ console.error('Error in polling interval:', error.message);
3000
+ }
2638
3001
  }, POLL_INTERVAL_MS);
2639
3002
  });
2640
3003
  }
@@ -2654,7 +3017,7 @@ async function runIteration(requirement, providerConfig, repoPath) {
2654
3017
  // ═══════════════════════════════════════════════════════════
2655
3018
  // PREPARE PHASE - SEARCH AND READ ACTUAL FILES
2656
3019
  // ═══════════════════════════════════════════════════════════
2657
- printStatusCard(requirement.text, 'PREPARE');
3020
+ printStatusCard(requirement.text, 'PREPARE', 'active');
2658
3021
 
2659
3022
  console.log(chalk.bold.white('📋 REQUIREMENT:'));
2660
3023
  console.log(chalk.cyan(` ${requirement.text}\n`));
@@ -2681,19 +3044,24 @@ async function runIteration(requirement, providerConfig, repoPath) {
2681
3044
  // ═══════════════════════════════════════════════════════════
2682
3045
  // ACT PHASE - GET STRUCTURED CHANGES FROM LLM
2683
3046
  // ═══════════════════════════════════════════════════════════
2684
- printStatusCard(requirement.text, 'ACT');
3047
+ printStatusCard(requirement.text, 'ACT', 'active');
2685
3048
 
2686
3049
  console.log(chalk.cyan(` ${getLogTimestamp()} - 🤖 Asking LLM for implementation...\n`));
2687
3050
  console.log(chalk.gray('─'.repeat(80)));
2688
3051
  console.log(chalk.yellow('💭 LLM Response (streaming):'));
2689
3052
  console.log(chalk.gray('─'.repeat(80)));
2690
3053
 
2691
- // Build context with actual file snippets
3054
+ // Build context with actual file snippets (use relative paths)
2692
3055
  let contextSection = '';
2693
3056
  if (fileSnippets.length > 0) {
2694
3057
  contextSection = '\n\nCURRENT CODE CONTEXT:\n';
2695
3058
  fileSnippets.forEach(({ file, snippet, startLine }) => {
2696
- contextSection += `\n--- ${file} (around line ${startLine}) ---\n${snippet}\n`;
3059
+ // Convert to relative path if absolute
3060
+ let displayPath = file;
3061
+ if (path.isAbsolute(file)) {
3062
+ displayPath = path.relative(repoPath, file);
3063
+ }
3064
+ contextSection += `\n--- ${displayPath} (around line ${startLine}) ---\n${snippet}\n`;
2697
3065
  });
2698
3066
  }
2699
3067
 
@@ -2755,7 +3123,8 @@ ${isRemovalRequirement ? '5. **REMOVE ALL CODE** related to the item - do NOT co
2755
3123
 
2756
3124
  OUTPUT FORMAT:
2757
3125
 
2758
- FILE: <exact path from the "---" line>
3126
+ For modifying existing files:
3127
+ FILE: <exact path from the "---" line - must be relative to repo root>
2759
3128
  SEARCH: \`\`\`javascript
2760
3129
  <COPY 10+ lines EXACTLY - preserve indentation, spacing, comments>
2761
3130
  \`\`\`
@@ -2763,22 +3132,31 @@ REPLACE: \`\`\`javascript
2763
3132
  <SAME lines but with necessary modifications>
2764
3133
  \`\`\`
2765
3134
 
3135
+ For creating new files:
3136
+ CREATE: <relative path to new file>
3137
+ CONTENT: \`\`\`javascript
3138
+ <full content of new file>
3139
+ \`\`\`
3140
+
2766
3141
  CRITICAL RULES - READ CAREFULLY:
2767
- 1. SEARCH block must be COPIED CHARACTER-BY-CHARACTER from CURRENT CODE CONTEXT above
2768
- 2. Use the EXACT code as it appears NOW - do NOT use old/outdated code from memory
2769
- 3. Include ALL property values EXACTLY as shown (e.g., if context shows 'type: "info"', use 'type: "info"', NOT 'type: "setting"')
2770
- 4. Include indentation EXACTLY as shown (count the spaces!)
2771
- 5. Include AT LEAST 20-30 LINES of context:
3142
+ 1. If the file does NOT exist, use CREATE: format to create it
3143
+ 2. If the file DOES exist and you need to modify it, use FILE:/SEARCH:/REPLACE: format
3144
+ 3. For FILE: blocks, SEARCH block must be COPIED CHARACTER-BY-CHARACTER from CURRENT CODE CONTEXT above
3145
+ 4. Use the EXACT code as it appears NOW - do NOT use old/outdated code from memory
3146
+ 5. Include ALL property values EXACTLY as shown (e.g., if context shows 'type: "info"', use 'type: "info"', NOT 'type: "setting"')
3147
+ 6. Include indentation EXACTLY as shown (count the spaces!)
3148
+ 7. For modifications, include AT LEAST 20-30 LINES of context:
2772
3149
  - Start 10-15 lines BEFORE the code you need to change
2773
3150
  - End 10-15 lines AFTER the code you need to change
2774
3151
  - MORE context is BETTER than less - include extra lines if unsure
2775
- 6. Do NOT paraphrase or rewrite - COPY EXACTLY from CURRENT CODE CONTEXT
2776
- 7. FILE path must match exactly what's after "---" in CURRENT CODE CONTEXT (DO NOT include the "---" itself)
2777
- 8. Output ONLY the FILE/SEARCH/REPLACE block
2778
- 9. NO explanations, NO markdown outside the blocks, NO additional text
2779
- 10. If you cannot find the exact code to modify, output: ERROR: CANNOT LOCATE CODE IN CONTEXT
2780
- 11. IMPORTANT: Include ALL intermediate code between the before/after context - do NOT skip lines
2781
- 12. DOUBLE-CHECK: Compare your SEARCH block line-by-line with CURRENT CODE CONTEXT to ensure they match
3152
+ 8. Do NOT paraphrase or rewrite - COPY EXACTLY from CURRENT CODE CONTEXT
3153
+ 9. FILE path must match exactly what's after "---" in CURRENT CODE CONTEXT (DO NOT include the "---" itself)
3154
+ 10. All paths must be RELATIVE to repo root (e.g., "specs/005-beta-pricing/tasks.md" NOT "/Users/.../tasks.md")
3155
+ 11. Output ONLY the FILE/SEARCH/REPLACE or CREATE/CONTENT blocks
3156
+ 12. NO explanations, NO markdown outside the blocks, NO additional text
3157
+ 13. If you cannot find the exact code to modify, output: ERROR: CANNOT LOCATE CODE IN CONTEXT
3158
+ 14. IMPORTANT: Include ALL intermediate code between the before/after context - do NOT skip lines
3159
+ 15. DOUBLE-CHECK: Compare your SEARCH block line-by-line with CURRENT CODE CONTEXT to ensure they match
2782
3160
 
2783
3161
  EXAMPLE (notice EXACT copying of indentation and spacing):
2784
3162
 
@@ -2802,7 +3180,24 @@ if (counts.todoCount === 0) {
2802
3180
  }
2803
3181
  \`\`\`
2804
3182
 
2805
- Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
3183
+ CREATE EXAMPLE (for new files):
3184
+
3185
+ CREATE: packages/web/src/utils/paypal-config.js
3186
+ CONTENT: \`\`\`javascript
3187
+ /**
3188
+ * PayPal Configuration Utility
3189
+ */
3190
+
3191
+ export const getPayPalClientId = () => {
3192
+ return import.meta.env.VITE_PAYPAL_CLIENT_ID;
3193
+ };
3194
+
3195
+ export const getPayPalEnvironment = () => {
3196
+ return import.meta.env.VITE_PAYPAL_ENVIRONMENT || 'sandbox';
3197
+ };
3198
+ \`\`\`
3199
+
3200
+ Now implement the requirement. Remember: For modifications, COPY THE SEARCH BLOCK EXACTLY! For new files, use CREATE: format.`;
2806
3201
 
2807
3202
  let fullResponse = '';
2808
3203
  let chunkCount = 0;
@@ -2902,7 +3297,7 @@ Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
2902
3297
  // ═══════════════════════════════════════════════════════════
2903
3298
  // CLEAN UP PHASE - APPLY CHANGES TO ACTUAL FILES
2904
3299
  // ═══════════════════════════════════════════════════════════
2905
- printStatusCard(requirement.text, 'CLEAN UP');
3300
+ printStatusCard(requirement.text, 'CLEAN UP', 'active');
2906
3301
 
2907
3302
  console.log(chalk.cyan('🧹 Parsing and applying changes...\n'));
2908
3303
 
@@ -2972,7 +3367,7 @@ Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
2972
3367
  // ═══════════════════════════════════════════════════════════
2973
3368
  // VERIFY PHASE - CONFIRM CHANGES WERE APPLIED
2974
3369
  // ═══════════════════════════════════════════════════════════
2975
- printStatusCard(requirement.text, 'VERIFY');
3370
+ printStatusCard(requirement.text, 'VERIFY', 'active');
2976
3371
 
2977
3372
  console.log(chalk.cyan('✓ Verifying changes...\n'));
2978
3373
 
@@ -2982,7 +3377,14 @@ Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
2982
3377
  const fullPath = path.join(repoPath, change.file);
2983
3378
  if (await fs.pathExists(fullPath)) {
2984
3379
  const content = await fs.readFile(fullPath, 'utf8');
2985
- const hasChange = content.includes(change.replace.trim());
3380
+
3381
+ // Handle both create and modify types
3382
+ let hasChange = false;
3383
+ if (change.type === 'create') {
3384
+ hasChange = change.content && content.includes(change.content.trim());
3385
+ } else {
3386
+ hasChange = change.replace && content.includes(change.replace.trim());
3387
+ }
2986
3388
 
2987
3389
  if (hasChange) {
2988
3390
  console.log(chalk.green(` ✓ ${change.file}`));
@@ -2999,7 +3401,7 @@ Now implement the requirement. Remember: COPY THE SEARCH BLOCK EXACTLY!`;
2999
3401
  // ═══════════════════════════════════════════════════════════
3000
3402
  // DONE PHASE
3001
3403
  // ═══════════════════════════════════════════════════════════
3002
- printStatusCard(requirement.text, 'DONE');
3404
+ printStatusCard(requirement.text, 'DONE', 'active');
3003
3405
 
3004
3406
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
3005
3407
 
@@ -3213,6 +3615,10 @@ async function handleAutoStart(options) {
3213
3615
  // Update status to stopped
3214
3616
  updateAutoModeStatus(repoPath, { running: false });
3215
3617
  console.log(chalk.gray('Auto mode stopped'));
3618
+ // Update status card to stopped mode
3619
+ if (storedStatusTitle && storedStatus) {
3620
+ printStatusCard(storedStatusTitle, storedStatus, 'stopped');
3621
+ }
3216
3622
  }
3217
3623
  });
3218
3624