vibecodingmachine-cli 2026.3.14-1537 → 2026.6.17-1956

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 (162) hide show
  1. package/bin/auth/auth-compliance.js +7 -7
  2. package/bin/commands/agent-commands.js +15 -15
  3. package/bin/commands/auto-commands.js +3 -3
  4. package/bin/commands/command-aliases.js +13 -4
  5. package/bin/config/cli-config.js +15 -5
  6. package/bin/update/update-checker.js +5 -5
  7. package/bin/vibecodingmachine.js +2 -2
  8. package/package.json +2 -2
  9. package/src/commands/agents/add.js +5 -5
  10. package/src/commands/agents/check.js +19 -19
  11. package/src/commands/agents/list.js +24 -24
  12. package/src/commands/agents/remove.js +4 -4
  13. package/src/commands/agents-check.js +1 -1
  14. package/src/commands/analyze-file-sizes.js +43 -43
  15. package/src/commands/auto-direct/auto-provider-manager.js +19 -19
  16. package/src/commands/auto-direct/auto-start-phases.js +493 -0
  17. package/src/commands/auto-direct/auto-status-display.js +35 -35
  18. package/src/commands/auto-direct/auto-utils.js +50 -50
  19. package/src/commands/auto-direct/cline-installer.js +56 -0
  20. package/src/commands/auto-direct/code-processor.js +27 -27
  21. package/src/commands/auto-direct/file-scanner.js +19 -19
  22. package/src/commands/auto-direct/ide-completion-waiter.js +485 -0
  23. package/src/commands/auto-direct/ide-fallback-runner.js +226 -0
  24. package/src/commands/auto-direct/ide-provider-runner.js +103 -0
  25. package/src/commands/auto-direct/iteration-handlers.js +189 -0
  26. package/src/commands/auto-direct/iteration-runner.js +485 -0
  27. package/src/commands/auto-direct/provider-config.js +38 -7
  28. package/src/commands/auto-direct/provider-manager.js +132 -6
  29. package/src/commands/auto-direct/requirement-manager.js +169 -104
  30. package/src/commands/auto-direct/requirement-mover.js +350 -0
  31. package/src/commands/auto-direct/spec-handlers.js +155 -0
  32. package/src/commands/auto-direct/spec-ide-runner.js +318 -0
  33. package/src/commands/auto-direct/spec-processing.js +203 -0
  34. package/src/commands/auto-direct/status-display.js +9 -9
  35. package/src/commands/auto-direct/utils.js +83 -1
  36. package/src/commands/auto-direct-refactored.js +1 -413
  37. package/src/commands/auto-direct.js +127 -4119
  38. package/src/commands/auto-execution.js +21 -21
  39. package/src/commands/auto-status-helpers.js +0 -2
  40. package/src/commands/auto.js +22 -22
  41. package/src/commands/check-compliance.js +65 -65
  42. package/src/commands/computers.js +39 -39
  43. package/src/commands/continuous-scan.js +19 -19
  44. package/src/commands/ide.js +4 -4
  45. package/src/commands/locale.js +7 -7
  46. package/src/commands/refactor-file.js +59 -59
  47. package/src/commands/requirements/commands.js +17 -17
  48. package/src/commands/requirements/default-handlers.js +30 -30
  49. package/src/commands/requirements/disable.js +3 -3
  50. package/src/commands/requirements/enable.js +3 -3
  51. package/src/commands/requirements/utils.js +6 -6
  52. package/src/commands/requirements-refactored.js +3 -3
  53. package/src/commands/requirements-remote.js +38 -38
  54. package/src/commands/requirements.js +3 -3
  55. package/src/commands/settings.js +111 -0
  56. package/src/commands/specs/count.js +60 -0
  57. package/src/commands/specs/disable.js +3 -3
  58. package/src/commands/specs/enable.js +3 -3
  59. package/src/commands/status.js +10 -10
  60. package/src/commands/sync.js +25 -25
  61. package/src/commands/timeout.js +35 -35
  62. package/src/trui/TruiInterface.js +2 -2
  63. package/src/trui/agents/AgentInterface.js +4 -4
  64. package/src/trui/agents/handlers/CommandHandler.js +4 -4
  65. package/src/trui/agents/handlers/ContextManager.js +1 -1
  66. package/src/trui/agents/handlers/DisplayHandler.js +11 -11
  67. package/src/trui/agents/handlers/HelpHandler.js +1 -1
  68. package/src/utils/agent-selector.js +6 -6
  69. package/src/utils/antigravity-installer.js +4 -4
  70. package/src/utils/asset-cleanup.js +1 -1
  71. package/src/utils/auth.js +9 -12
  72. package/src/utils/clarification-actions.js +4 -4
  73. package/src/utils/cline-js-handler.js +5 -5
  74. package/src/utils/compliance-check.js +6 -6
  75. package/src/utils/config.js +12 -12
  76. package/src/utils/display-formatters-complete.js +2 -2
  77. package/src/utils/display-formatters-extracted.js +2 -2
  78. package/src/utils/display-formatters.js +2 -2
  79. package/src/utils/feedback-handler.js +2 -2
  80. package/src/utils/first-run.js +7 -7
  81. package/src/utils/ide-detection.js +1 -1
  82. package/src/utils/ide-handlers.js +6 -6
  83. package/src/utils/interactive/clarification-actions.js +3 -3
  84. package/src/utils/interactive/core-ui.js +7 -7
  85. package/src/utils/interactive/file-backup.js +6 -6
  86. package/src/utils/interactive/file-import-export.js +49 -49
  87. package/src/utils/interactive/file-operations.js +3 -3
  88. package/src/utils/interactive/file-validation.js +41 -41
  89. package/src/utils/interactive/interactive-prompts.js +41 -41
  90. package/src/utils/interactive/requirement-actions.js +5 -5
  91. package/src/utils/interactive/requirement-crud.js +4 -4
  92. package/src/utils/interactive/requirements-navigation.js +10 -10
  93. package/src/utils/interactive-broken.js +6 -6
  94. package/src/utils/interactive.js +37 -37
  95. package/src/utils/keyboard-handler.js +4 -4
  96. package/src/utils/prompt-helper.js +6 -6
  97. package/src/utils/provider-checker/agent-checker.js +1 -1
  98. package/src/utils/provider-checker/agent-runner.js +203 -314
  99. package/src/utils/provider-checker/agents-file-lock.js +134 -0
  100. package/src/utils/provider-checker/agents-manager.js +224 -36
  101. package/src/utils/provider-checker/cli-installer.js +28 -28
  102. package/src/utils/provider-checker/cli-utils.js +2 -2
  103. package/src/utils/provider-checker/cursor-approval-clicker.js +108 -0
  104. package/src/utils/provider-checker/format-utils.js +4 -4
  105. package/src/utils/provider-checker/ide-installer-helper.js +96 -0
  106. package/src/utils/provider-checker/ide-manager.js +19 -8
  107. package/src/utils/provider-checker/ide-quota-checker.js +120 -0
  108. package/src/utils/provider-checker/ide-utils.js +2 -2
  109. package/src/utils/provider-checker/node-detector.js +4 -4
  110. package/src/utils/provider-checker/node-utils.js +5 -5
  111. package/src/utils/provider-checker/opencode-checker.js +107 -73
  112. package/src/utils/provider-checker/process-utils.js +1 -1
  113. package/src/utils/provider-checker/provider-validator.js +11 -11
  114. package/src/utils/provider-checker/quota-checker.js +5 -5
  115. package/src/utils/provider-checker/quota-detector.js +5 -5
  116. package/src/utils/provider-checker/requirements-manager.js +6 -6
  117. package/src/utils/provider-checker/test-requirements.js +1 -1
  118. package/src/utils/provider-checker/vscode-approval-clicker.js +328 -0
  119. package/src/utils/provider-checker-new.js +6 -6
  120. package/src/utils/provider-checker.js +6 -6
  121. package/src/utils/provider-checkers/ide-manager.js +13 -13
  122. package/src/utils/provider-checkers/node-executable-finder.js +4 -4
  123. package/src/utils/provider-checkers/provider-checker-core.js +5 -5
  124. package/src/utils/provider-checkers/provider-checker-main.js +17 -17
  125. package/src/utils/provider-registry.js +5 -6
  126. package/src/utils/provider-utils.js +12 -12
  127. package/src/utils/quota-detectors.js +32 -32
  128. package/src/utils/requirement-action-handlers.js +12 -12
  129. package/src/utils/requirement-actions/requirement-operations.js +3 -3
  130. package/src/utils/requirement-actions.js +1 -1
  131. package/src/utils/requirement-file-operations.js +5 -5
  132. package/src/utils/requirement-helpers.js +1 -1
  133. package/src/utils/requirement-management.js +5 -5
  134. package/src/utils/requirement-navigation.js +2 -2
  135. package/src/utils/requirement-organization.js +3 -3
  136. package/src/utils/rui-trui-adapter.js +14 -14
  137. package/src/utils/simple-trui.js +3 -3
  138. package/src/utils/status-helpers-extracted.js +3 -3
  139. package/src/utils/trui-clarifications.js +11 -11
  140. package/src/utils/trui-debug.js +3 -2
  141. package/src/utils/trui-devin.js +217 -0
  142. package/src/utils/trui-feedback.js +7 -7
  143. package/src/utils/trui-kiro-integration.js +34 -34
  144. package/src/utils/trui-main-handlers.js +20 -21
  145. package/src/utils/trui-main-menu.js +19 -19
  146. package/src/utils/trui-nav-agents.js +59 -8
  147. package/src/utils/trui-nav-requirements.js +3 -3
  148. package/src/utils/trui-nav-settings.js +10 -10
  149. package/src/utils/trui-nav-specifications.js +1 -1
  150. package/src/utils/trui-navigation-backup.js +11 -11
  151. package/src/utils/trui-navigation.js +9 -9
  152. package/src/utils/trui-provider-health.js +25 -25
  153. package/src/utils/trui-provider-manager.js +28 -28
  154. package/src/utils/trui-quick-menu.js +2 -2
  155. package/src/utils/trui-req-actions-backup.js +21 -21
  156. package/src/utils/trui-req-actions.js +20 -20
  157. package/src/utils/trui-req-editor.js +10 -10
  158. package/src/utils/trui-req-file-ops.js +3 -3
  159. package/src/utils/trui-req-tree.js +7 -7
  160. package/src/utils/trui-windsurf.js +103 -103
  161. package/src/utils/user-tracking.js +15 -15
  162. package/src/utils/trui-req-tree-old.js +0 -719
@@ -44,4150 +44,159 @@ const { checkKiroRateLimit, handleKiroRateLimit } = require('../utils/kiro-js-ha
44
44
  const { checkClineRateLimit, handleClineRateLimit } = require('../utils/cline-js-handler');
45
45
  const { startAutoMode, stopAutoMode, updateAutoModeStatus } = require('../utils/auto-mode');
46
46
 
47
- // Global keyboard handler reference for cleanup
48
- let keyboardHandler = null;
49
-
50
- // Status management will use in-process tracking instead of external file
51
- const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
52
-
53
- /**
54
- * Auto-install Cline CLI if not available
55
- * @param {boolean} forceInstall - Force installation even if already available
56
- * @returns {Promise<boolean>} - Returns true if Cline CLI is available after installation
57
- */
58
- async function ensureClineInstalled(forceInstall = false) {
59
- const { DirectLLMManager } = require('vibecodingmachine-core');
60
- const llm = new DirectLLMManager();
61
-
62
- // Check if already available
63
- if (!forceInstall && await llm.isClineAvailable()) {
64
- return true;
65
- }
66
-
67
- const ora = require('ora');
68
- const { execSync } = require('child_process');
69
-
70
- const spinner = ora('Installing Cline CLI...').start();
71
-
72
- try {
73
- // Install Cline CLI globally
74
- execSync('npm install -g cline', { stdio: 'pipe', encoding: 'utf8' });
75
-
76
- // Verify installation
77
- const isAvailable = await llm.isClineAvailable();
78
-
79
- if (isAvailable) {
80
- spinner.succeed('Cline CLI installed successfully');
81
- console.log(chalk.green('✓ Cline CLI is now ready to use'));
82
- return true;
83
- } else {
84
- spinner.fail('Cline CLI installation failed');
85
- console.log(chalk.yellow('⚠️ Installation completed but Cline CLI not found in PATH'));
86
- console.log(chalk.gray(' You may need to restart your terminal or manually add npm global bin to PATH'));
87
- return false;
88
- }
89
- } catch (error) {
90
- spinner.fail('Cline CLI installation failed');
91
- console.log(chalk.red('✗ Failed to install Cline CLI:'), error.message);
92
- console.log(chalk.yellow(' You can manually install with: npm install -g cline'));
93
- return false;
94
- }
95
- }
96
-
97
- // CRITICAL: Shared ProviderManager instance to track rate limits across all function calls
98
- const sharedProviderManager = new ProviderManager();
99
-
100
- // CRITICAL: Shared IDEHealthTracker instance to track IDE reliability across all iterations
101
- const sharedHealthTracker = new IDEHealthTracker();
102
-
103
- // Listen for consecutive failures to warn user
104
- sharedHealthTracker.on('consecutive-failures', ({ ideId, count, lastError }) => {
105
- console.log(chalk.yellow(`\n⚠️ WARNING: ${ideId} has failed ${count} times consecutively`));
106
- console.log(chalk.yellow(` Last error: ${lastError}`));
107
- console.log(chalk.yellow(` Consider switching to a different IDE\n`));
108
- });
109
-
110
- // Configured stages (will be loaded in handleAutoStart)
111
- let configuredStages = DEFAULT_STAGES;
112
-
113
- /**
114
- * Get timestamp for logging
115
- */
116
- function getTimestamp() {
117
- const now = new Date();
118
- return now.toLocaleTimeString('en-US', {
119
- hour: '2-digit',
120
- minute: '2-digit',
121
- hour12: false,
122
- timeZone: 'America/Denver'
123
- }) + ' MST';
124
- }
125
-
126
- /**
127
- * Get a human-friendly timestamp for log prefixes that includes date, time, and timezone
128
- * Example: "2025-01-02 3:45 PM MST"
129
- */
130
- function getLogTimestamp(date = new Date()) {
131
- const datePart = date.toISOString().split('T')[0]; // YYYY-MM-DD
132
- const timePart = date.toLocaleTimeString('en-US', {
133
- hour: 'numeric',
134
- minute: '2-digit',
135
- hour12: true,
136
- timeZone: 'America/Denver',
137
- timeZoneName: 'short'
138
- });
139
- return `${datePart} ${timePart}`;
140
- }
141
-
142
- /**
143
- * Translate workflow stage names
144
- */
145
- function translateStage(stage) {
146
- const stageMap = {
147
- 'PREPARE': 'workflow.stage.prepare',
148
- 'REPRODUCE': 'workflow.stage.reproduce',
149
- 'CREATE UNIT TEST': 'workflow.stage.create.unit.test',
150
- 'ACT': 'workflow.stage.act',
151
- 'CLEAN UP': 'workflow.stage.clean.up',
152
- 'VERIFY': 'workflow.stage.verify',
153
- 'RUN UNIT TESTS': 'workflow.stage.run.unit.tests',
154
- 'DONE': 'workflow.stage.done'
155
- };
156
-
157
- const key = stageMap[stage];
158
- return key ? t(key) : stage;
159
- }
160
-
161
- /**
162
- * Strip ANSI escape codes from a string
163
- */
164
- function stripAnsi(str) {
165
- // eslint-disable-next-line no-control-regex
166
- return str.replace(/\x1B\[[0-9;]*[mGKH]/g, '');
167
- }
168
-
169
- /**
170
- * Get visual width of a string accounting for ANSI codes, emojis, and wide characters
171
- * Uses string-width library for accurate Unicode width calculation
172
- */
173
- function getVisualWidth(str) {
174
- return stringWidth(str);
175
- }
176
-
177
- /**
178
- * Pad string to visual width accounting for emojis, ANSI codes, and wide characters
179
- */
180
- function padToVisualWidth(str, targetWidth) {
181
- const visualWidth = getVisualWidth(str);
182
- const paddingNeeded = targetWidth - visualWidth;
183
- if (paddingNeeded <= 0) {
184
- return str;
185
- }
186
- return str + ' '.repeat(paddingNeeded);
187
- }
188
-
189
- function isRateLimitMessage(text) {
190
- if (!text) return false;
191
- const lower = text.toLowerCase();
192
- return lower.includes('rate limit') ||
193
- lower.includes('too many requests') ||
194
- lower.includes('429') ||
195
- lower.includes('weekly limit') ||
196
- lower.includes('daily limit') ||
197
- lower.includes('limit reached');
198
- }
199
-
200
- function sleep(ms) {
201
- return new Promise(resolve => setTimeout(resolve, ms));
202
- }
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
-
223
- /**
224
- * Print purple status card with progress indicators
225
- */
226
- /**
227
- * Update status section in requirements file
228
- * @param {string} repoPath - Repository path
229
- * @param {string} status - Status to set (PREPARE, ACT, CLEAN UP, VERIFY, DONE)
230
- */
231
- async function updateRequirementsStatus(repoPath, status) {
232
- try {
233
- const reqPath = await getRequirementsPath(repoPath);
234
- if (!reqPath || !await fs.pathExists(reqPath)) {
235
- return;
236
- }
237
-
238
- const content = await fs.readFile(reqPath, 'utf-8');
239
- const lines = content.split('\n');
240
- let inStatusSection = false;
241
- let statusLineIndex = -1;
242
-
243
- // Find the status section and the line with the status
244
- for (let i = 0; i < lines.length; i++) {
245
- const line = lines[i];
246
-
247
- if (line.includes('🚦 Current Status')) {
248
- inStatusSection = true;
249
- continue;
250
- }
251
-
252
- if (inStatusSection && line.startsWith('##') && !line.startsWith('###')) {
253
- // End of status section, status line not found
254
- break;
255
- }
256
-
257
- // Check against configured stages
258
- if (inStatusSection && configuredStages.includes(line.trim())) {
259
- statusLineIndex = i;
260
- break;
261
- }
262
- }
263
-
264
- // Update or add the status line
265
- if (statusLineIndex >= 0) {
266
- // Replace existing status
267
- lines[statusLineIndex] = status;
268
- } else if (inStatusSection) {
269
- // Add status after the section header
270
- for (let i = 0; i < lines.length; i++) {
271
- if (lines[i].includes('🚦 Current Status')) {
272
- lines.splice(i + 1, 0, status);
273
- break;
274
- }
275
- }
276
- } else {
277
- // Status section doesn't exist - find the requirement and add it
278
- for (let i = 0; i < lines.length; i++) {
279
- if (lines[i].startsWith('### ') && !lines[i].includes('🚦 Current Status')) {
280
- // Found a requirement header, add status section after it
281
- lines.splice(i + 1, 0, '', '#### 🚦 Current Status', status);
282
- break;
283
- }
284
- }
285
- }
286
-
287
- await fs.writeFile(reqPath, lines.join('\n'));
288
- } catch (error) {
289
- // Silently fail - don't break execution if status update fails
290
- console.error(chalk.gray(` ${t('auto.direct.status.update.warning')} ${error.message}`));
291
- }
292
- }
293
-
294
- // Persistent status box state
295
- let statusBoxInitialized = false;
296
- let statusBoxLines = 5; // Number of lines the status box takes
297
- let storedStatusTitle = '';
298
- let storedStatus = '';
299
- let currentStatusMode = 'active'; // Track current mode: 'active', 'waiting', 'stopped'
300
-
301
- /**
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'
309
- */
310
- function printStatusCard(currentTitle, currentStatus, mode = 'active') {
311
- currentStatusMode = mode; // Update global mode tracking
312
-
313
- const stages = configuredStages;
314
- const stageMap = {};
315
- stages.forEach((s, i) => stageMap[s] = i);
316
-
317
- const currentIndex = stageMap[currentStatus] || 0;
318
-
319
- // Build workflow line with visual prominence for current stage
320
- const stageParts = stages.map((stage, idx) => {
321
- const translatedStage = translateStage(stage);
322
- if (idx < currentIndex) {
323
- // Completed stages - grey with checkmark
324
- return chalk.grey(`✅ ${translatedStage}`);
325
- } else if (idx === currentIndex) {
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}`);
331
- } else {
332
- // Future stages - grey with hourglass
333
- return chalk.grey(`⏳ ${translatedStage}`);
334
- }
335
- });
336
-
337
- const workflowLine = stageParts.join(chalk.grey(' → '));
338
-
339
- // Get terminal width, default to 100 if not available
340
- const terminalWidth = process.stdout.columns || 100;
341
- const boxWidth = Math.max(terminalWidth - 4, 80); // Leave 4 chars margin, minimum 80
342
-
343
- // Truncate title if needed to fit in box
344
- const workingOnLabel = `🎯 ${t('auto.direct.requirement.working.on')} `;
345
- const maxTitleWidth = boxWidth - stringWidth(workingOnLabel) - 2; // Leave room for label
346
- const titleShort = currentTitle?.substring(0, maxTitleWidth) + (currentTitle?.length > maxTitleWidth ? '...' : '');
347
- const titleLine = chalk.cyan(workingOnLabel) + chalk.white(titleShort);
348
-
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
360
- const statusBoxContent =
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) + '╯');
366
-
367
- // Store current state (using stored* names to avoid shadowing with parameters)
368
- storedStatusTitle = currentTitle;
369
- storedStatus = currentStatus;
370
-
371
- // Just print the card normally - no complex cursor manipulation
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
- }
390
- }
391
-
392
- /**
393
- * Get current requirement from REQUIREMENTS file
394
- */
395
- async function getCurrentRequirement(repoPath) {
396
- try {
397
- const reqPath = await getRequirementsPath(repoPath);
398
- if (!reqPath || !await fs.pathExists(reqPath)) {
399
- return null;
400
- }
401
-
402
- const content = await fs.readFile(reqPath, 'utf8');
403
-
404
- // Always skip DISABLED: requirements (user can toggle via TRUI Space key)
405
-
406
- // Extract first TODO requirement (new header format)
407
- const lines = content.split('\n');
408
- let inTodoSection = false;
409
-
410
- for (let i = 0; i < lines.length; i++) {
411
- const line = lines[i].trim();
412
-
413
- // Check if we're in the TODO section
414
- if (line.includes('## ⏳ Requirements not yet completed') ||
415
- line.includes('Requirements not yet completed')) {
416
- inTodoSection = true;
417
- continue;
418
- }
419
-
420
- // If we hit another section header, stop looking
421
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###')) {
422
- break;
423
- }
424
-
425
- // If we're in TODO section and find a requirement header (###)
426
- if (inTodoSection && line.startsWith('###')) {
427
- const title = line.replace(/^###\s*/, '').trim();
428
- // Skip empty titles
429
- if (title && title.length > 0) {
430
- // Always skip DISABLED: requirements
431
- if (title.startsWith('DISABLED:')) {
432
- continue;
433
- }
434
-
435
- // Read package and description (optional)
436
- let pkg = null;
437
- let description = '';
438
- let j = i + 1;
439
-
440
- // Read next few lines for package and description
441
- while (j < lines.length && j < i + 20) {
442
- const nextLine = lines[j].trim();
443
- // Stop if we hit another requirement or section
444
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
445
- break;
446
- }
447
- // Check for PACKAGE line
448
- if (nextLine.startsWith('PACKAGE:')) {
449
- pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
450
- } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
451
- // Description line (not empty, not package)
452
- if (description) {
453
- description += '\n' + nextLine;
454
- } else {
455
- description = nextLine;
456
- }
457
- }
458
- j++;
459
- }
460
-
461
- return {
462
- text: title,
463
- fullLine: lines[i],
464
- package: pkg,
465
- description: description,
466
- disabled: false
467
- };
468
- }
469
- }
470
- }
471
-
472
- return null;
473
- } catch (err) {
474
- console.error(t('auto.direct.requirement.read.error'), err.message);
475
- return null;
476
- }
477
- }
478
-
479
- /**
480
- * Count total TODO requirements
481
- */
482
- async function countTodoRequirements(repoPath) {
483
- try {
484
- const reqPath = await getRequirementsPath(repoPath);
485
- if (!reqPath || !await fs.pathExists(reqPath)) {
486
- return 0;
487
- }
488
-
489
- const content = await fs.readFile(reqPath, 'utf8');
490
- const lines = content.split('\n');
491
- let inTodoSection = false;
492
- let count = 0;
493
-
494
- for (let i = 0; i < lines.length; i++) {
495
- const line = lines[i].trim();
496
-
497
- // Check if we're in the TODO section
498
- if (line.includes('## ⏳ Requirements not yet completed') ||
499
- line.includes('Requirements not yet completed')) {
500
- inTodoSection = true;
501
- continue;
502
- }
503
-
504
- // If we hit another section header, stop looking
505
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###')) {
506
- break;
507
- }
508
-
509
- // If we're in TODO section and find a requirement header (###)
510
- if (inTodoSection && line.startsWith('###')) {
511
- const title = line.replace(/^###\s*/, '').trim();
512
- // Only count non-empty titles
513
- if (title && title.length > 0) {
514
- count++;
515
- }
516
- }
517
- }
518
-
519
- return count;
520
- } catch (err) {
521
- console.error(t('auto.direct.requirement.count.error'), err.message);
522
- return 0;
523
- }
524
- }
525
-
526
- /**
527
- * Move requirement from TODO to TO VERIFY BY HUMAN
528
- */
529
- async function moveRequirementToVerify(repoPath, requirementText) {
530
- try {
531
- const reqPath = await getRequirementsPath(repoPath);
532
- if (!reqPath || !await fs.pathExists(reqPath)) {
533
- return false;
534
- }
535
-
536
- const content = await fs.readFile(reqPath, 'utf8');
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
- }
554
-
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
- }
559
-
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();
566
-
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;
578
- }
579
-
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;
586
- break;
587
- }
588
- }
589
- if (requirementEndIndex === -1) {
590
- requirementEndIndex = lines.length;
591
- }
592
- break;
593
- }
594
- }
595
- }
596
- }
597
-
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
- }
602
-
603
- // Extract the entire requirement block
604
- const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
605
-
606
- // Remove the requirement from its current location
607
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
608
-
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();
614
-
615
- // Check if we're entering TODO section
616
- if (line.startsWith('##') && line.includes('Requirements not yet completed')) {
617
- inTodoSection = true;
618
- continue;
619
- }
620
-
621
- // Check if we're leaving TODO section
622
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###') && !line.includes('Requirements not yet completed')) {
623
- break;
624
- }
625
-
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;
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');
647
-
648
-
649
- // Find or create TO VERIFY BY HUMAN section with conflict resolution
650
- // IMPORTANT: Do NOT match VERIFIED sections - only match TO VERIFY sections
651
- const verifySectionVariants = [
652
- '## 🔍 TO VERIFY BY HUMAN',
653
- '## 🔍 TO VERIFY',
654
- '## TO VERIFY',
655
- '## ✅ TO VERIFY',
656
- '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG',
657
- '## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT',
658
- '## 🖥️ COMPUTER FOCUS AREA MANAGEMENT',
659
- '## 🔍 spec-kit: TO VERIFY BY HUMAN'
660
- ];
661
-
662
- let verifyIndex = -1;
663
- for (let i = 0; i < lines.length; i++) {
664
- const line = lines[i];
665
- const trimmed = line.trim();
666
-
667
- // Check each variant more carefully
668
- for (const variant of verifySectionVariants) {
669
- const variantTrimmed = variant.trim();
670
- // Exact match or line starts with variant
671
- if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
672
- // Double-check: make sure it's NOT a VERIFIED section (without TO VERIFY)
673
- if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) &&
674
- (trimmed.includes('TO VERIFY') || trimmed.includes('Verified by AI screenshot') || trimmed.includes('CROSS-COMPUTER REQUIREMENT ASSIGNMENT') || trimmed.includes('COMPUTER FOCUS AREA MANAGEMENT') || trimmed.includes('spec-kit'))) {
675
- verifyIndex = i;
676
- break;
677
- }
678
- }
679
- }
680
- if (verifyIndex !== -1) break;
681
- }
682
-
683
- if (verifyIndex === -1) {
684
- // Create TO VERIFY section - place it BEFORE VERIFIED section if one exists, otherwise before CHANGELOG
685
- const verifiedIndex = lines.findIndex(line => {
686
- const trimmed = line.trim();
687
- return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED') ||
688
- (trimmed.startsWith('##') && trimmed.includes('VERIFIED') && !trimmed.includes('TO VERIFY'));
689
- });
690
- const changelogIndex = lines.findIndex(line => line.includes('## CHANGELOG'));
691
- const manualFeedbackIndex = lines.findIndex(line => line.trim().startsWith('## ❓'));
692
-
693
- // Prefer: before VERIFIED > before CHANGELOG > before manual feedback > at end
694
- let insertionIndex = lines.length;
695
- if (verifiedIndex > 0) {
696
- insertionIndex = verifiedIndex;
697
- } else if (changelogIndex > 0) {
698
- insertionIndex = changelogIndex;
699
- } else if (manualFeedbackIndex > 0) {
700
- insertionIndex = manualFeedbackIndex;
701
- }
702
-
703
- const block = [];
704
- if (insertionIndex > 0 && lines[insertionIndex - 1].trim() !== '') {
705
- block.push('');
706
- }
707
- block.push('## 🔍 spec-kit: TO VERIFY BY HUMAN', '### Automatic Registration and Tracking', '### User Registration', '');
708
- block.push('## 📊 CROSS-COMPUTER REQUIREMENT ASSIGNMENT');
709
- block.push('## 🖥️ COMPUTER FOCUS AREA MANAGEMENT');
710
- lines.splice(insertionIndex, 0, ...block);
711
- verifyIndex = lines.findIndex(line => {
712
- const trimmed = line.trim();
713
- return trimmed === '## 🔍 spec-kit: TO VERIFY BY HUMAN' || trimmed.startsWith('## 🔍 spec-kit: TO VERIFY BY HUMAN');
714
- });
715
-
716
- // Safety check: verifyIndex should be valid
717
- if (verifyIndex === -1) {
718
- console.error(t('auto.direct.verify.section.create.failed'));
719
- return false;
720
- }
721
- }
722
-
723
- // Safety check: verify we're not inserting into a VERIFIED section
724
- const verifyLine = lines[verifyIndex] || '';
725
- if (verifyLine.includes('## 📝 VERIFIED') || (verifyLine.trim().startsWith('##') && verifyLine.includes('VERIFIED') && !verifyLine.includes('TO VERIFY'))) {
726
- console.error('ERROR: Attempted to insert into VERIFIED section instead of TO VERIFY');
727
- return false;
728
- }
729
-
730
- // Remove any existing duplicate of this requirement in TO VERIFY section
731
- // Find the next section header after TO VERIFY
732
- let nextSectionIndex = lines.length;
733
- for (let i = verifyIndex + 1; i < lines.length; i++) {
734
- const trimmed = lines[i].trim();
735
- if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
736
- nextSectionIndex = i;
737
- break;
738
- }
739
- }
740
-
741
- // Search for duplicate requirement in TO VERIFY section
742
- const requirementTitle = requirementBlock[0].trim().replace(/^###\s*/, '').trim();
743
- for (let i = verifyIndex + 1; i < nextSectionIndex; i++) {
744
- const line = lines[i];
745
- const trimmed = line.trim();
746
-
747
- if (trimmed.startsWith('###')) {
748
- const existingTitle = trimmed.replace(/^###\s*/, '').trim();
749
-
750
- // Check if this is a duplicate (exact match or contains/contained by)
751
- if (existingTitle === requirementTitle ||
752
- existingTitle.includes(requirementTitle) ||
753
- requirementTitle.includes(existingTitle)) {
754
-
755
- // Find the end of this existing requirement
756
- let existingEndIndex = nextSectionIndex;
757
- for (let j = i + 1; j < nextSectionIndex; j++) {
758
- const nextLine = lines[j].trim();
759
- if (nextLine.startsWith('###') || nextLine.startsWith('##')) {
760
- existingEndIndex = j;
761
- break;
762
- }
763
- }
764
-
765
- // Remove the duplicate
766
- lines.splice(i, existingEndIndex - i);
767
-
768
- // Adjust nextSectionIndex if needed
769
- nextSectionIndex -= (existingEndIndex - i);
770
- break;
771
- }
772
- }
773
- }
774
-
775
- // Insert requirement block at TOP of TO VERIFY section (right after section header)
776
- let insertIndex = verifyIndex + 1;
777
-
778
- // Ensure there's a blank line after the section header
779
- if (lines[insertIndex]?.trim() !== '') {
780
- lines.splice(insertIndex, 0, '');
781
- insertIndex++;
782
- }
783
-
784
- // If a Conflict Resolution header already exists immediately after the section header, reuse it
785
- const conflictHeader = '### Conflict Resolution:';
786
- if (lines[insertIndex]?.trim().startsWith(conflictHeader)) {
787
- // Move insertion point to after the existing header
788
- insertIndex++;
789
- // Ensure there's a blank line after the header before inserting the requirement
790
- if (lines[insertIndex]?.trim() !== '') {
791
- lines.splice(insertIndex, 0, '');
792
- insertIndex++;
793
- }
794
- } else {
795
- // Insert the conflict header
796
- lines.splice(insertIndex, 0, conflictHeader);
797
- insertIndex++;
798
- // Ensure a blank line after the header
799
- if (lines[insertIndex]?.trim() !== '') {
800
- lines.splice(insertIndex, 0, '');
801
- insertIndex++;
802
- }
803
- }
804
-
805
- // Insert the requirement block
806
- lines.splice(insertIndex, 0, ...requirementBlock);
807
-
808
- // Ensure there's a blank line after the requirement block
809
- const afterIndex = insertIndex + requirementBlock.length;
810
- if (afterIndex < lines.length && lines[afterIndex]?.trim() !== '') {
811
- lines.splice(afterIndex, 0, '');
812
- }
813
-
814
- // Move the requirement to the VERIFIED section
815
- const verifiedSectionVariants = [
816
- '## 📝 VERIFIED',
817
- '## VERIFIED'
818
- ];
819
- let verifiedIndex = -1;
820
- for (let i = 0; i < lines.length; i++) {
821
- const line = lines[i];
822
- const trimmed = line.trim();
823
-
824
- // Check each variant more carefully
825
- for (const variant of verifiedSectionVariants) {
826
- const variantTrimmed = variant.trim();
827
- // Exact match or line starts with variant
828
- if (trimmed === variantTrimmed || trimmed.startsWith(variantTrimmed)) {
829
- verifiedIndex = i;
830
- break;
831
- }
832
- }
833
- if (verifiedIndex !== -1) break;
834
- }
835
-
836
- if (verifiedIndex === -1) {
837
- // Create VERIFIED section - place it after TO VERIFY section
838
- const block = [];
839
- block.push('## 📝 VERIFIED');
840
- lines.splice(verifyIndex + 1, 0, ...block);
841
- verifiedIndex = lines.findIndex(line => {
842
- const trimmed = line.trim();
843
- return trimmed === '## 📝 VERIFIED' || trimmed.startsWith('## 📝 VERIFIED');
844
- });
845
- }
846
-
847
- // Insert the requirement block at the end of the VERIFIED section
848
- let insertIndexVerified = verifiedIndex + 1;
849
- while (insertIndexVerified < lines.length && lines[insertIndexVerified].trim().startsWith('###')) {
850
- insertIndexVerified++;
851
- }
852
- lines.splice(insertIndexVerified, 0, ...requirementBlock);
853
-
854
-
855
-
856
- // Write the file
857
- await fs.writeFile(reqPath, lines.join('\n'));
858
- console.log(chalk.green(`✅ Moved requirement to TO VERIFY BY HUMAN: ${requirementText.substring(0, 80)}...`));
859
- return true;
860
- } catch (err) {
861
- console.error(t('auto.direct.requirement.move.verify.error'), err.message);
862
- console.error('⚠️ Requirement may have been lost. Please check the requirements file.');
863
- return false;
864
- }
865
- }
866
-
867
-
868
- /**
869
- * Move requirement to recycled section
870
- */
871
- async function moveRequirementToRecycle(repoPath, requirementText) {
872
- try {
873
- const reqPath = await getRequirementsPath(repoPath);
874
- if (!reqPath || !await fs.pathExists(reqPath)) {
875
- return false;
876
- }
877
-
878
- let content = await fs.readFile(reqPath, 'utf8');
879
-
880
- // Find and remove from any section
881
- const lines = content.split('\n');
882
- let requirementIndex = -1;
883
-
884
- for (let i = 0; i < lines.length; i++) {
885
- if (lines[i].includes(requirementText.substring(0, 50))) {
886
- requirementIndex = i;
887
- break;
888
- }
889
- }
890
-
891
- if (requirementIndex === -1) {
892
- console.log(chalk.yellow(`⚠️ ${t('auto.direct.requirement.not.found')}`));
893
- return false;
894
- }
895
-
896
- // Remove from any section
897
- const requirementLine = lines[requirementIndex];
898
- lines.splice(requirementIndex, 1);
899
-
900
- // Add to Recycled section (after "## ♻️ Recycled")
901
- let recycledIndex = -1;
902
- for (let i = 0; i < lines.length; i++) {
903
- if (lines[i].includes('## ♻️ Recycled')) {
904
- recycledIndex = i; // Move to the line of the header
905
- break;
906
- }
907
- }
908
-
909
- if (recycledIndex === -1) {
910
- lines.push('## ♻️ Recycled');
911
- recycledIndex = lines.length - 1;
912
- }
913
-
914
- // Add timestamp and insert at TOP of Recycled list
915
- const timestamp = new Date().toISOString().split('T')[0];
916
- lines.splice(recycledIndex + 1, 0, `- ${timestamp}: ${requirementLine.replace(/^- /, '')}`);
917
-
918
- // Save
919
- await fs.writeFile(reqPath, lines.join('\n'));
920
- return true;
921
- } catch (err) {
922
- console.error(t('auto.direct.requirement.move.error'), err.message);
923
- return false;
924
- }
925
- }
926
-
927
- /**
928
- * Get all available provider configurations
929
- */
930
- async function getAllAvailableProviders() {
931
- const config = await getAutoConfig();
932
- const prefs = await getProviderPreferences();
933
-
934
- const llm = new DirectLLMManager(sharedProviderManager); // Pass shared instance
935
- const providers = [];
936
- const skipped = [];
937
-
938
- const groqKey = process.env.GROQ_API_KEY || config.groqApiKey;
939
- const anthropicKey = process.env.ANTHROPIC_API_KEY || config.anthropicApiKey;
940
- const awsRegion = process.env.AWS_REGION || config.awsRegion;
941
- const awsAccessKey = process.env.AWS_ACCESS_KEY_ID || config.awsAccessKeyId;
942
- const awsSecretKey = process.env.AWS_SECRET_ACCESS_KEY || config.awsSecretAccessKey;
943
- const claudeCodeAvailable = await llm.isClaudeCodeAvailable();
944
- const clineAvailable = await llm.isClineAvailable();
945
- const openCodeAvailable = await llm.isOpenCodeAvailable();
946
- const vscodeCopilotCLIResult = await llm.isVSCodeCopilotCLIAvailable();
947
- const vscodeCopilotCLIAvailable = vscodeCopilotCLIResult.available;
948
- const ollamaAvailable = await llm.isOllamaAvailable();
949
- let ollamaModels = [];
950
- if (ollamaAvailable) {
951
- ollamaModels = await llm.getOllamaModels();
952
- }
953
-
954
- for (const providerId of prefs.order) {
955
- const def = getProviderDefinition(providerId);
956
- if (!def) continue;
957
-
958
- const enabled = prefs.enabled[providerId] !== false;
959
- const base = {
960
- provider: providerId,
961
- displayName: def.name,
962
- type: def.type,
963
- category: def.category,
964
- enabled,
965
- estimatedSpeed: def.estimatedSpeed,
966
- ide: def.ide,
967
- maxChats: def.type === 'ide' ? 1 : undefined
968
- };
969
-
970
- switch (providerId) {
971
- case 'groq': {
972
- if (!groqKey) {
973
- skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'API key required — set GROQ_API_KEY or configure in settings' });
974
- continue;
975
- }
976
- const model = config.groqModel || def.defaultModel;
977
- providers.push({
978
- ...base,
979
- model,
980
- apiKey: groqKey,
981
- displayName: `${def.name} (${model})`
982
- });
983
- break;
984
- }
985
- case 'anthropic': {
986
- if (!anthropicKey) {
987
- skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'API key required — set ANTHROPIC_API_KEY or configure in settings' });
988
- continue;
989
- }
990
- const model = config.anthropicModel || def.defaultModel;
991
- providers.push({
992
- ...base,
993
- model,
994
- apiKey: anthropicKey,
995
- displayName: `${def.name} (${model})`
996
- });
997
- break;
998
- }
999
- case 'bedrock': {
1000
- if (!awsRegion || !awsAccessKey || !awsSecretKey) {
1001
- skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'AWS credentials required — set AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY' });
1002
- continue;
1003
- }
1004
- providers.push({
1005
- ...base,
1006
- model: def.defaultModel,
1007
- region: awsRegion,
1008
- accessKeyId: awsAccessKey,
1009
- secretAccessKey: awsSecretKey
1010
- });
1011
- break;
1012
- }
1013
- case 'claude-code': {
1014
- if (!claudeCodeAvailable) {
1015
- skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'Claude Code CLI not installed or not found in PATH' });
1016
- continue;
1017
- }
1018
- providers.push({
1019
- ...base,
1020
- model: def.defaultModel
1021
- });
1022
- break;
1023
- }
1024
- case 'cline': {
1025
- // Auto-install Cline CLI if not available and enabled
1026
- if (!clineAvailable && enabled) {
1027
- console.log(chalk.yellow(`\n🔧 Cline CLI not found, auto-installing...`));
1028
- clineAvailable = await ensureClineInstalled();
1029
- }
1030
-
1031
- if (!clineAvailable) {
1032
- skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'Cline CLI not installed — run: npm install -g cline' });
1033
- continue;
1034
- }
1035
- providers.push({
1036
- ...base,
1037
- model: def.defaultModel
1038
- });
1039
- break;
1040
- }
1041
- case 'opencode': {
1042
- if (!openCodeAvailable) {
1043
- skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'OpenCode CLI not installed — visit https://opencode.ai to install' });
1044
- continue;
1045
- }
1046
- providers.push({
1047
- ...base,
1048
- model: def.defaultModel
1049
- });
1050
- break;
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
- }
1091
- case 'cursor':
1092
- case 'windsurf':
1093
- case 'vscode':
1094
- case 'kiro':
1095
- case 'antigravity':
1096
- case 'github-copilot':
1097
- case 'amazon-q':
1098
- case 'replit': {
1099
- if (Array.isArray(def.subAgents) && def.subAgents.length > 0) {
1100
- for (const sub of def.subAgents) {
1101
- const providerObj = {
1102
- ...base,
1103
- model: sub.model,
1104
- subAgentId: sub.id,
1105
- subAgentName: sub.name,
1106
- displayName: `${def.name} (${sub.name})`,
1107
- extension: def.extension
1108
- };
1109
- providers.push(providerObj);
1110
- }
1111
- } else {
1112
- const providerObj = {
1113
- ...base,
1114
- model: def.defaultModel || providerId,
1115
- extension: def.extension
1116
- };
1117
- providers.push(providerObj);
1118
- }
1119
- break;
1120
- }
1121
- case 'ollama': {
1122
- if (!ollamaAvailable || ollamaModels.length === 0) {
1123
- skipped.push({ provider: providerId, displayName: def.name, enabled, reason: 'Ollama not running or no models installed' });
1124
- continue;
1125
- }
1126
- const preferredModel = config.llmModel && config.llmModel.includes('ollama/')
1127
- ? config.llmModel.split('/')[1]
1128
- : config.llmModel || config.aiderModel;
1129
- let bestModel = preferredModel && ollamaModels.includes(preferredModel)
1130
- ? preferredModel
1131
- : ollamaModels.includes(def.defaultModel)
1132
- ? def.defaultModel
1133
- : ollamaModels[0];
1134
- providers.push({
1135
- ...base,
1136
- model: bestModel,
1137
- displayName: `${def.name} (${bestModel})`
1138
- });
1139
- break;
1140
- }
1141
- default:
1142
- break;
1143
- }
1144
- }
1145
-
1146
- return { providers, skipped };
1147
- }
1148
-
1149
- /**
1150
- * Get provider configuration with automatic failover
1151
- */
1152
- async function getProviderConfig(excludeProvider = null) {
1153
- const config = await getAutoConfig();
1154
- const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
1155
- const { providers, skipped } = await getAllAvailableProviders();
1156
- const prefs = await getProviderPreferences();
1157
-
1158
- // Clear any incorrect rate limits for web-based IDEs that were marked due to platform issues
1159
- for (const provider of providers) {
1160
- if (provider.provider === 'replit' && providerManager.isRateLimited('replit', 'replit')) {
1161
- const rateLimitInfo = providerManager.rateLimits['replit:replit'];
1162
- if (rateLimitInfo) {
1163
- const now = Date.now();
1164
- const timeSinceMarked = now - (rateLimitInfo.markedAt || 0);
1165
- const minutesSinceMarked = timeSinceMarked / (1000 * 60);
1166
-
1167
- // Clear rate limit if:
1168
- // 1. It was marked due to platform issues, OR
1169
- // 2. It was marked within the last 5 minutes (likely a recent platform issue)
1170
- const isPlatformError = rateLimitInfo.reason &&
1171
- (rateLimitInfo.reason.includes('xdg-open') ||
1172
- rateLimitInfo.reason.includes('command not found') ||
1173
- rateLimitInfo.reason.includes('Unable to find application'));
1174
-
1175
- if (isPlatformError || minutesSinceMarked < 5) {
1176
- console.log(chalk.yellow(`⚠️ Clearing incorrect rate limit for Replit (marked ${minutesSinceMarked.toFixed(1)} minutes ago: ${isPlatformError ? 'platform error' : 'recent error'})`));
1177
- delete providerManager.rateLimits['replit:replit'];
1178
- }
1179
- }
1180
- }
1181
- }
1182
-
1183
- // Get first ENABLED agent from provider preferences (same logic as interactive menu)
1184
- let firstEnabledAgent = null;
1185
- for (const agentId of prefs.order) {
1186
- if (prefs.enabled[agentId] !== false) {
1187
- firstEnabledAgent = agentId;
1188
- break;
1189
- }
1190
- }
1191
- const savedAgent = firstEnabledAgent || config.agent || config.ide;
1192
-
1193
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1194
- console.log(chalk.gray(`[DEBUG] firstEnabledAgent: ${firstEnabledAgent}`));
1195
- console.log(chalk.gray(`[DEBUG] config.agent: ${config.agent}`));
1196
- console.log(chalk.gray(`[DEBUG] config.ide: ${config.ide}`));
1197
- console.log(chalk.gray(`[DEBUG] savedAgent: ${savedAgent}`));
1198
- }
1199
-
1200
- if (providers.length === 0) {
1201
- return { status: 'no_providers', providers: [], skipped };
1202
- }
1203
-
1204
- const enabledProviders = providers.filter(p => p.enabled);
1205
- const disabledProviders = providers.filter(p => !p.enabled);
1206
-
1207
- if (enabledProviders.length === 0) {
1208
- return { status: 'no_enabled', disabledProviders, skipped };
1209
- }
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
1253
- const availableProviders = enabledProviders.filter(p => {
1254
- // Exclude specified provider and rate-limited providers
1255
- if (excludeProvider && p.provider === excludeProvider) {
1256
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1257
- console.log(chalk.gray(`[DEBUG] Excluding ${p.provider} due to excludeProvider=${excludeProvider}`));
1258
- }
1259
- return false;
1260
- }
1261
-
1262
- // For IDE providers, only consider them rate limited if they've actually been used
1263
- if (p.type === 'ide') {
1264
- const hasBeenUsed = providerManager.rateLimits[`${p.provider}:`] ||
1265
- Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`));
1266
-
1267
- if (hasBeenUsed) {
1268
- const isRateLimited = providerManager.isRateLimited(p.provider, p.model);
1269
- if (process.env.DEBUG_PROVIDER_SELECTION && isRateLimited) {
1270
- console.log(chalk.gray(`[DEBUG] IDE Provider ${p.provider} is rate limited, excluding`));
1271
- }
1272
- return !isRateLimited;
1273
- }
1274
- // If it hasn't been used yet, it's always available
1275
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1276
- console.log(chalk.gray(`[DEBUG] IDE Provider ${p.provider} hasn't been used yet, including`));
1277
- }
1278
- return true;
1279
- }
1280
-
1281
- // For non-IDE providers, check rate limits normally
1282
- const isRateLimited = providerManager.isRateLimited(p.provider, p.model);
1283
- if (process.env.DEBUG_PROVIDER_SELECTION && isRateLimited) {
1284
- console.log(chalk.gray(`[DEBUG] Non-IDE provider ${p.provider} is rate limited, excluding`));
1285
- }
1286
- return !isRateLimited;
1287
- });
1288
-
1289
-
1290
-
1291
- // Debug: Log provider selection details
1292
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1293
- console.log(chalk.gray(`[DEBUG] Total providers: ${providers.length}`));
1294
- console.log(chalk.gray(`[DEBUG] Enabled providers: ${enabledProviders.length}`));
1295
- console.log(chalk.gray(`[DEBUG] Available providers: ${availableProviders.length}`));
1296
- console.log(chalk.gray(`[DEBUG] Available provider names: ${availableProviders.map(p => p.provider).join(', ')}`));
1297
- console.log(chalk.gray(`[DEBUG] Enabled provider names: ${enabledProviders.map(p => p.provider).join(', ')}`));
1298
- console.log(chalk.gray(`[DEBUG] Excluding provider: ${excludeProvider}`));
1299
- console.log(chalk.gray(`[DEBUG] Looking for savedAgent: ${savedAgent}`));
1300
- }
1301
-
1302
- let selection = null;
1303
- if (savedAgent && savedAgent !== excludeProvider) {
1304
- selection = availableProviders.find(p => p.provider === savedAgent);
1305
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1306
- console.log(chalk.gray(`[DEBUG] Looking for savedAgent: ${savedAgent}`));
1307
- console.log(chalk.gray(`[DEBUG] Found selection: ${selection ? selection.provider : 'null'}`));
1308
- }
1309
- }
1310
-
1311
- // If no selection or the selected provider is rate limited, try to find an unused IDE provider
1312
- if (!selection || (selection.type === 'ide' && providerManager.isRateLimited(selection.provider, selection.model))) {
1313
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1314
- console.log(chalk.gray(`[DEBUG] No valid selection, trying unused IDE providers`));
1315
- console.log(chalk.gray(`[DEBUG] Reason: ${!selection ? 'no selection' : 'selection is rate limited'}`));
1316
- }
1317
-
1318
- // Prioritize newly enabled IDE providers that haven't been used
1319
- const unusedIdeProviders = availableProviders.filter(p =>
1320
- p.type === 'ide' &&
1321
- !providerManager.rateLimits[`${p.provider}:`] &&
1322
- !Object.keys(providerManager.rateLimits).some(key => key.startsWith(`${p.provider}:`))
1323
- );
1324
-
1325
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1326
- console.log(chalk.gray(`[DEBUG] Unused IDE providers: ${unusedIdeProviders.map(p => p.provider).join(', ')}`));
1327
- }
1328
-
1329
- if (unusedIdeProviders.length > 0) {
1330
- selection = unusedIdeProviders[0];
1331
- console.log(chalk.green(`✓ Selected unused IDE provider: ${selection.displayName}`));
1332
- } else if (availableProviders.length > 0) {
1333
- selection = availableProviders[0];
1334
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1335
- console.log(chalk.gray(`[DEBUG] Selected first available provider: ${selection.provider}`));
1336
- }
1337
- }
1338
- } else if (selection) {
1339
- if (process.env.DEBUG_PROVIDER_SELECTION) {
1340
- console.log(chalk.gray(`[DEBUG] Using savedAgent selection: ${selection.provider}`));
1341
- }
1342
- }
1343
-
1344
- if (selection) {
1345
- const perfKey = `${selection.provider}:${selection.model || ''}`;
1346
- const avgSpeed = providerManager.performance[perfKey]?.avgSpeed;
1347
- const speedInfo = avgSpeed ? ` (avg: ${(avgSpeed / 1000).toFixed(1)}s)` : '';
1348
- console.log(chalk.green(`✓ Selected: ${selection.displayName}${speedInfo}`));
1349
- return { status: 'ok', provider: selection, disabledProviders };
1350
- }
1351
-
1352
- // If we reach here, no providers are available (filtered out by excludeProvider or other conditions)
1353
- return {
1354
- status: 'no_available',
1355
- enabledProviders,
1356
- disabledProviders,
1357
- skipped
1358
- };
1359
- }
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
-
1487
- async function acquireProviderConfig(excludeProvider = null, excludeModel = null, forcedProvider = null) {
1488
- // If a specific provider is forced, bypass normal selection
1489
- if (forcedProvider) {
1490
- // Special handling for Cline CLI - auto-install if not available
1491
- if (forcedProvider === 'cline') {
1492
- const { DirectLLMManager } = require('vibecodingmachine-core');
1493
- const llm = new DirectLLMManager();
1494
-
1495
- if (!await llm.isClineAvailable()) {
1496
- console.log(chalk.yellow(`\n🔧 Cline CLI not found, auto-installing...`));
1497
- const installed = await ensureClineInstalled();
1498
- if (!installed) {
1499
- console.log(chalk.red(`\n✗ Provider "${forcedProvider}" could not be installed\n`));
1500
- return null;
1501
- }
1502
- }
1503
- }
1504
-
1505
- const { providers } = await getAllAvailableProviders();
1506
- const match = providers.find(p => p.provider === forcedProvider);
1507
- if (match) return match;
1508
- // Provider not in available list — try building it directly from definitions
1509
- console.log(chalk.red(`\n✗ Provider "${forcedProvider}" is not available (missing credentials or not installed)\n`));
1510
- return null;
1511
- }
1512
-
1513
- while (true) {
1514
- const selection = await getProviderConfig(excludeProvider);
1515
-
1516
- // Handle case where getProviderConfig returns undefined/null
1517
- if (!selection) {
1518
- console.log(chalk.red(`\n✗ ${t('auto.direct.provider.none.available')}\n`));
1519
- return null;
1520
- }
1521
-
1522
- if (selection.status === 'ok') {
1523
- // If we have a specific model to exclude (for same-IDE failover), skip it
1524
- if (excludeModel && selection.provider.model === excludeModel) {
1525
- console.log(chalk.yellow(`⚠️ Excluding rate-limited sub-agent: ${selection.provider.displayName}\n`));
1526
- // Retry with the same provider excluded to force picking another sub-agent
1527
- return acquireProviderConfig(selection.provider.provider, selection.provider.model);
1528
- }
1529
- return selection.provider;
1530
- }
1531
-
1532
- if (selection.status === 'no_providers') {
1533
- console.log(chalk.red(`\n✗ ${t('auto.direct.provider.none.available')}\n`));
1534
- if (selection.skipped && selection.skipped.length > 0) {
1535
- const enabledSkipped = selection.skipped.filter(s => s.enabled);
1536
- if (enabledSkipped.length > 0) {
1537
- console.log(chalk.yellow(' Enabled providers skipped due to missing configuration:'));
1538
- enabledSkipped.forEach(s => console.log(chalk.gray(` • ${s.displayName}: ${s.reason}`)));
1539
- console.log();
1540
- }
1541
- }
1542
- return null;
1543
- }
1544
-
1545
- if (selection.status === 'no_enabled') {
1546
- const enabledSkipped = (selection.skipped || []).filter(s => s.enabled);
1547
- if (enabledSkipped.length > 0) {
1548
- console.log(chalk.red(`\n✗ Enabled providers are missing required configuration:\n`));
1549
- enabledSkipped.forEach(s => console.log(chalk.yellow(` • ${s.displayName}: ${s.reason}`)));
1550
- console.log();
1551
- } else {
1552
- console.log(chalk.red(`\n✗ ${t('auto.direct.provider.all.disabled')}\n`));
1553
- }
1554
- return null;
1555
- }
1556
-
1557
- if (selection.status === 'all_rate_limited') {
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
1562
- }
1563
- continue; // Retry provider selection
1564
- }
1565
-
1566
- if (selection.status === 'no_available') {
1567
- console.log(chalk.red(`\n✗ No available providers after filtering\n`));
1568
- if (selection.disabledProviders && selection.disabledProviders.length > 0) {
1569
- console.log(chalk.gray(` Enable more providers in Settings to continue\n`));
1570
- }
1571
- return null;
1572
- }
1573
-
1574
- return null;
1575
- }
1576
- }
1577
-
1578
- /**
1579
- * Parse search/replace blocks from LLM response
1580
- */
1581
- function parseSearchReplaceBlocks(response) {
1582
- const changes = [];
1583
-
1584
- // Match CREATE: path CONTENT: ``` content ``` format for new files
1585
- const createRegex = /CREATE:\s*(.+?)\nCONTENT:\s*```(?:[a-z]*)\n([\s\S]+?)```/g;
1586
-
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
-
1605
- while ((match = blockRegex.exec(response)) !== null) {
1606
- let filePath = match[1].trim();
1607
- const searchText = match[2];
1608
- const replaceText = match[3];
1609
-
1610
- // Clean up file path - remove "---" prefix if present
1611
- filePath = filePath.replace(/^---\s*/, '').trim();
1612
-
1613
- changes.push({
1614
- type: 'modify',
1615
- file: filePath,
1616
- search: searchText,
1617
- replace: replaceText
1618
- });
1619
- }
1620
-
1621
- return changes;
1622
- }
1623
-
1624
- /**
1625
- * Normalize whitespace for comparison (convert all whitespace to single spaces)
1626
- */
1627
- function normalizeWhitespace(str) {
1628
- return str.replace(/\s+/g, ' ').trim();
1629
- }
1630
-
1631
- /**
1632
- * Extract key identifiers from code (variable names, function names, strings)
1633
- */
1634
- function extractIdentifiers(code) {
1635
- const identifiers = new Set();
1636
-
1637
- // Extract quoted strings
1638
- const stringMatches = code.match(/'([^']+)'|"([^"]+)"/g);
1639
- if (stringMatches) {
1640
- stringMatches.forEach(match => {
1641
- const str = match.slice(1, -1); // Remove quotes
1642
- if (str.length > 3) { // Only meaningful strings
1643
- identifiers.add(str);
1644
- }
1645
- });
1646
- }
1647
-
1648
- // Extract variable/function names (words followed by : or =)
1649
- const nameMatches = code.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[:=]/g);
1650
- if (nameMatches) {
1651
- nameMatches.forEach(match => {
1652
- const name = match.replace(/[:=].*$/, '').trim();
1653
- if (name.length > 2) {
1654
- identifiers.add(name);
1655
- }
1656
- });
1657
- }
1658
-
1659
- // Extract common patterns like 'type:', 'name:', 'value:'
1660
- const patternMatches = code.match(/(type|name|value|file|path|function|const|let|var)\s*:/gi);
1661
- if (patternMatches) {
1662
- patternMatches.forEach(match => {
1663
- identifiers.add(match.toLowerCase().replace(/\s*:/, ''));
1664
- });
1665
- }
1666
-
1667
- return Array.from(identifiers);
1668
- }
1669
-
1670
- /**
1671
- * Extract structural pattern from code (ignoring values)
1672
- */
1673
- function extractPattern(code) {
1674
- // Replace strings with placeholders
1675
- let pattern = code.replace(/'[^']+'|"[^"]+"/g, '"..."');
1676
- // Replace numbers with placeholders
1677
- pattern = pattern.replace(/\b\d+\b/g, 'N');
1678
- // Normalize whitespace
1679
- pattern = normalizeWhitespace(pattern);
1680
- return pattern;
1681
- }
1682
-
1683
- /**
1684
- * Apply a search/replace change to a file with fuzzy matching fallback
1685
- */
1686
- async function applyFileChange(change, repoPath) {
1687
- try {
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
- }
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)
1720
- // Check if file exists
1721
- if (!await fs.pathExists(fullPath)) {
1722
- return { success: false, error: `File not found: ${change.file}` };
1723
- }
1724
-
1725
- // Read file
1726
- let content = await fs.readFile(fullPath, 'utf8');
1727
-
1728
- // Try exact match first
1729
- console.log(chalk.gray(` 🔍 ${t('auto.direct.files.trying.exact')}`));
1730
- if (content.includes(change.search)) {
1731
- const newContent = content.replace(change.search, change.replace);
1732
- await fs.writeFile(fullPath, newContent, 'utf8');
1733
- console.log(chalk.green(` ✓ Exact match found`));
1734
- return { success: true, method: 'exact' };
1735
- }
1736
- console.log(chalk.gray(` ✗ Exact match failed`));
1737
-
1738
- // Try with normalized whitespace (fuzzy match)
1739
- console.log(chalk.gray(` 🔍 ${t('auto.direct.files.trying.fuzzy')}`));
1740
- const normalizedSearch = normalizeWhitespace(change.search);
1741
- const lines = content.split('\n');
1742
- const searchLines = change.search.split('\n');
1743
-
1744
- console.log(chalk.gray(` - Search block: ${searchLines.length} lines`));
1745
- console.log(chalk.gray(` - File total: ${lines.length} lines`));
1746
-
1747
- // Extract key identifiers from search text (function names, variable names, strings)
1748
- const searchIdentifiers = extractIdentifiers(change.search);
1749
-
1750
- // Try multiple window sizes (±5 lines) to account for LLM not including enough context
1751
- for (let sizeOffset = 0; sizeOffset <= 10; sizeOffset++) {
1752
- const windowSize = searchLines.length + sizeOffset;
1753
- if (sizeOffset > 0) {
1754
- console.log(chalk.gray(` 🔍 Trying window size +${sizeOffset} (${windowSize} lines)...`));
1755
- }
1756
-
1757
- // Try to find a sequence of lines that matches when normalized
1758
- for (let i = 0; i < lines.length; i++) {
1759
- if (i + windowSize > lines.length) break;
1760
-
1761
- const window = lines.slice(i, i + windowSize).join('\n');
1762
- const normalizedWindow = normalizeWhitespace(window);
1763
-
1764
- // Check if normalized versions match (or if normalized window contains normalized search)
1765
- if (normalizedWindow === normalizedSearch || normalizedWindow.includes(normalizedSearch)) {
1766
- // Found a match! Replace this section
1767
- const beforeLines = lines.slice(0, i);
1768
- const afterLines = lines.slice(i + windowSize);
1769
- const replaceLines = change.replace.split('\n');
1770
-
1771
- const newLines = [...beforeLines, ...replaceLines, ...afterLines];
1772
- const newContent = newLines.join('\n');
1773
-
1774
- await fs.writeFile(fullPath, newContent, 'utf8');
1775
- console.log(chalk.green(` ✓ Fuzzy match found at line ${i + 1} (window size: ${windowSize})`));
1776
- return { success: true, method: 'fuzzy', matchedAt: i + 1, windowSize };
1777
- }
1778
-
1779
- // Also try semantic matching - check if key identifiers match even if some values differ
1780
- if (searchIdentifiers.length > 0) {
1781
- const windowIdentifiers = extractIdentifiers(window);
1782
- const matchingIdentifiers = searchIdentifiers.filter(id => windowIdentifiers.includes(id));
1783
- // If 80% of identifiers match, consider it a potential match
1784
- if (matchingIdentifiers.length >= searchIdentifiers.length * 0.8) {
1785
- // Check if the structure is similar (same number of lines, similar patterns)
1786
- const searchPattern = extractPattern(change.search);
1787
- const windowPattern = extractPattern(window);
1788
- if (searchPattern === windowPattern) {
1789
- // Found a semantic match! Replace this section
1790
- const beforeLines = lines.slice(0, i);
1791
- const afterLines = lines.slice(i + windowSize);
1792
- const replaceLines = change.replace.split('\n');
1793
-
1794
- const newLines = [...beforeLines, ...replaceLines, ...afterLines];
1795
- const newContent = newLines.join('\n');
1796
-
1797
- await fs.writeFile(fullPath, newContent, 'utf8');
1798
- console.log(chalk.green(` ✓ Semantic match found at line ${i + 1} (window size: ${windowSize}, ${matchingIdentifiers.length}/${searchIdentifiers.length} identifiers)`));
1799
- return { success: true, method: 'semantic', matchedAt: i + 1, windowSize };
1800
- }
1801
- }
1802
- }
1803
- }
1804
- }
1805
- console.log(chalk.red(` ✗ No match found (tried exact + fuzzy with multiple window sizes)`));
1806
-
1807
- return {
1808
- success: false,
1809
- error: `Search text not found in ${change.file} (tried exact, fuzzy, and semantic matching with windows ${searchLines.length}-${searchLines.length + 10} lines)`
1810
- };
1811
-
1812
- } catch (error) {
1813
- return { success: false, error: error.message };
1814
- }
1815
- }
1816
-
1817
- /**
1818
- * Find relevant files based on requirement
1819
- */
1820
- async function findRelevantFiles(requirement, repoPath) {
1821
- const relevantFiles = [];
1822
-
1823
- try {
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
-
1839
- const isRemovalRequirement = /remove|delete|eliminate/i.test(requirement) &&
1840
- (/menu|item|option|setting|button|ui|element/i.test(requirement));
1841
-
1842
- // For removal requirements, search for the specific text/identifier mentioned
1843
- if (isRemovalRequirement) {
1844
- // Extract the text/identifier to search for (text in quotes or after "Remove")
1845
- const match = requirement.match(/remove\s+["']?([^"']+)["']?/i) ||
1846
- requirement.match(/remove:\s*["']?([^"']+)["']?/i) ||
1847
- requirement.match(/["']([^"']+)["']/);
1848
-
1849
- if (match && match[1]) {
1850
- const searchText = match[1].trim();
1851
- // Search for files containing this text
1852
- const searchLower = searchText.toLowerCase();
1853
-
1854
- // Always check interactive.js for menu items
1855
- relevantFiles.push('packages/cli/src/utils/interactive.js');
1856
-
1857
- // If it mentions CLI or auto, also check auto-direct.js
1858
- if (searchLower.includes('cli') || searchLower.includes('auto') || searchLower.includes('restart')) {
1859
- relevantFiles.push('packages/cli/src/commands/auto-direct.js');
1860
- }
1861
- } else {
1862
- // Fallback: check both files for removal requirements
1863
- relevantFiles.push('packages/cli/src/utils/interactive.js');
1864
- relevantFiles.push('packages/cli/src/commands/auto-direct.js');
1865
- }
1866
- } else if (reqLower.includes('completed') && reqLower.includes('verify')) {
1867
- // This is about the auto mode moving requirements
1868
- relevantFiles.push('packages/cli/src/commands/auto-direct.js');
1869
- } else if (reqLower.includes('requirements page') || reqLower.includes('requirements:')) {
1870
- // This is about the requirements menu/page
1871
- relevantFiles.push('packages/cli/src/utils/interactive.js');
1872
- } else if (reqLower.includes('main screen') || reqLower.includes('menu')) {
1873
- // This is about the main menu
1874
- relevantFiles.push('packages/cli/src/utils/interactive.js');
1875
- } else {
1876
- // Default: check both files
1877
- relevantFiles.push('packages/cli/src/commands/auto-direct.js');
1878
- relevantFiles.push('packages/cli/src/utils/interactive.js');
1879
- }
1880
- } catch (error) {
1881
- console.log(chalk.yellow(`⚠️ ${t('auto.direct.files.error')} ${error.message}`));
1882
- }
1883
-
1884
- return relevantFiles;
1885
- }
1886
-
1887
- /**
1888
- * Read file snippets to give LLM context
1889
- */
1890
- async function readFileSnippets(files, repoPath, requirement) {
1891
- const snippets = [];
1892
-
1893
- for (const file of files) {
1894
- try {
1895
- const fullPath = path.join(repoPath, file);
1896
- if (await fs.pathExists(fullPath)) {
1897
- const content = await fs.readFile(fullPath, 'utf8');
1898
- const lines = content.split('\n');
1899
-
1900
- let startLine = -1;
1901
- let endLine = -1;
1902
-
1903
- // For auto-direct.js, find the moveRequirementToVerify function
1904
- if (file.includes('auto-direct.js')) {
1905
- for (let i = 0; i < lines.length; i++) {
1906
- const line = lines[i];
1907
- if (line.includes('async function moveRequirementToVerify')) {
1908
- startLine = i;
1909
- // Find the end of the function
1910
- let braceCount = 0;
1911
- let foundStart = false;
1912
- for (let j = i; j < lines.length; j++) {
1913
- const l = lines[j];
1914
- // Count braces to find function end
1915
- for (const char of l) {
1916
- if (char === '{') {
1917
- braceCount++;
1918
- foundStart = true;
1919
- } else if (char === '}') {
1920
- braceCount--;
1921
- if (foundStart && braceCount === 0) {
1922
- endLine = j + 1;
1923
- break;
1924
- }
1925
- }
1926
- }
1927
- if (endLine > 0) break;
1928
- }
1929
- break;
1930
- }
1931
- }
1932
- }
1933
-
1934
- // For interactive.js, search based on requirement keywords
1935
- if (file.includes('interactive.js')) {
1936
- const reqLower = requirement.toLowerCase();
1937
-
1938
- // Search for specific sections based on requirement
1939
- if (reqLower.includes('current agent') || (reqLower.includes('└─') && reqLower.includes('current agent'))) {
1940
- // Find the Current Agent display code (with or without rate limit)
1941
- // First, try to find the exact "└─ Current Agent" pattern
1942
- for (let i = 0; i < lines.length; i++) {
1943
- // Look for the exact pattern with tree character
1944
- if (lines[i].includes('└─') && lines[i].includes('Current Agent')) {
1945
- startLine = Math.max(0, i - 15);
1946
- endLine = Math.min(lines.length, i + 20);
1947
- break;
1948
- }
1949
- }
1950
- // If not found, look for "Current Agent" in menu items
1951
- if (startLine === -1) {
1952
- for (let i = 0; i < lines.length; i++) {
1953
- if (lines[i].includes('Current Agent') ||
1954
- (lines[i].includes('currentAgent') && lines[i].includes('items.push'))) {
1955
- startLine = Math.max(0, i - 15);
1956
- endLine = Math.min(lines.length, i + 20);
1957
- break;
1958
- }
1959
- }
1960
- }
1961
- // If still not found, look for the setting item with Current Agent
1962
- if (startLine === -1) {
1963
- for (let i = 0; i < lines.length; i++) {
1964
- if (lines[i].includes('setting:current-agent') ||
1965
- (lines[i].includes('Current Agent') && lines[i].includes('type:') && lines[i].includes('setting'))) {
1966
- startLine = Math.max(0, i - 10);
1967
- endLine = Math.min(lines.length, i + 15);
1968
- break;
1969
- }
1970
- }
1971
- }
1972
- } else if (reqLower.includes('remove') || reqLower.includes('delete')) {
1973
- // Find the delete/remove confirmation code
1974
- for (let i = 0; i < lines.length; i++) {
1975
- // Look for confirmAction with 'Delete' or 'Are you sure'
1976
- if ((lines[i].includes('confirmAction') && lines[i].includes('Delete')) ||
1977
- (lines[i].includes('confirmDelete') && (i > 0 && lines[i - 5] && lines[i - 5].includes("'delete'")))) {
1978
- startLine = Math.max(0, i - 10);
1979
- endLine = Math.min(lines.length, i + 20);
1980
- break;
1981
- }
1982
- }
1983
- } else if (reqLower.includes('submenu') || reqLower.includes('menu')) {
1984
- // Find the showRequirementActions function
1985
- for (let i = 0; i < lines.length; i++) {
1986
- if (lines[i].includes('async function showRequirementActions')) {
1987
- startLine = i;
1988
- endLine = Math.min(lines.length, i + 80);
1989
- break;
1990
- }
1991
- }
1992
- } else if (reqLower.includes('next todo requirement') || reqLower.includes('next requirement') || (reqLower.includes('requirement') && reqLower.includes('indent'))) {
1993
- // Find the "Next TODO Requirement" section
1994
- for (let i = 0; i < lines.length; i++) {
1995
- const line = lines[i];
1996
- if (line.includes('Next TODO Requirement') || line.includes('Next Requirement') || (line.includes('nextReqText') && line.includes('items.push'))) {
1997
- // Get more context - look backwards for requirementsText and forwards for the items.push
1998
- startLine = Math.max(0, i - 30);
1999
- // Look for the items.push that contains Next TODO Requirement
2000
- for (let j = i; j < Math.min(lines.length, i + 20); j++) {
2001
- if (lines[j].includes('items.push') && (lines[j].includes('Next TODO Requirement') || lines[j].includes('Next Requirement') || (j > 0 && (lines[j - 1].includes('Next TODO Requirement') || lines[j - 1].includes('Next Requirement'))))) {
2002
- endLine = Math.min(lines.length, j + 10);
2003
- break;
2004
- }
2005
- }
2006
- if (endLine === -1) {
2007
- endLine = Math.min(lines.length, i + 60);
2008
- }
2009
- break;
2010
- }
2011
- }
2012
- // If not found, fall back to requirements section
2013
- if (startLine === -1) {
2014
- for (let i = 0; i < lines.length; i++) {
2015
- const line = lines[i];
2016
- if (line.includes('let requirementsText') || line.includes('requirementsText')) {
2017
- startLine = Math.max(0, i - 10);
2018
- endLine = Math.min(lines.length, i + 80);
2019
- break;
2020
- }
2021
- }
2022
- }
2023
- } else {
2024
- // Default: find menu/requirements section
2025
- for (let i = 0; i < lines.length; i++) {
2026
- const line = lines[i];
2027
- if (line.includes('let requirementsText') || line.includes('requirementsText')) {
2028
- startLine = Math.max(0, i - 10);
2029
- endLine = Math.min(lines.length, i + 80);
2030
- break;
2031
- }
2032
- }
2033
- }
2034
- }
2035
-
2036
- if (startLine >= 0 && endLine > startLine) {
2037
- const snippet = lines.slice(startLine, endLine).join('\n');
2038
- console.log(chalk.gray(` Found snippet at lines ${startLine + 1}-${endLine + 1}`));
2039
- snippets.push({ file, snippet, startLine: startLine + 1, endLine: endLine + 1 });
2040
- } else {
2041
- console.log(chalk.yellow(` ⚠️ Could not find relevant section in ${file}`));
2042
- }
2043
- }
2044
- } catch (error) {
2045
- console.log(chalk.yellow(`⚠️ Could not read ${file}: ${error.message}`));
2046
- }
2047
- }
2048
-
2049
- return snippets;
2050
- }
2051
-
2052
- async function runIdeProviderIteration(providerConfig, repoPath) {
2053
- return new Promise((resolve) => {
2054
- console.log(chalk.cyan(`⚙️ ${t('auto.direct.provider.launching', { provider: providerConfig.displayName })}\n`));
2055
-
2056
- const args = [CLI_ENTRY_POINT, 'auto:start', '--ide', providerConfig.ide || providerConfig.provider, '--max-chats', String(providerConfig.maxChats || 1)];
2057
- if (providerConfig.model) {
2058
- args.push('--ide-model', String(providerConfig.model));
2059
- }
2060
- // Pass extension information for VS Code extensions
2061
- if (providerConfig.extension) {
2062
- args.push('--extension', String(providerConfig.extension));
2063
- }
2064
- const child = spawn(process.execPath, args, {
2065
- cwd: repoPath,
2066
- env: process.env,
2067
- stdio: ['inherit', 'pipe', 'pipe']
2068
- });
2069
-
2070
- let combinedOutput = '';
2071
-
2072
- child.stdout.on('data', (data) => {
2073
- const text = data.toString();
2074
- combinedOutput += text;
2075
- process.stdout.write(text);
2076
- });
2077
-
2078
- child.stderr.on('data', (data) => {
2079
- const text = data.toString();
2080
- combinedOutput += text;
2081
- process.stderr.write(text);
2082
- });
2083
-
2084
- child.on('error', (error) => {
2085
- resolve({
2086
- success: false,
2087
- error: `Failed to start ${providerConfig.displayName}: ${error.message}`,
2088
- output: combinedOutput
2089
- });
2090
- });
2091
-
2092
- child.on('exit', (code) => {
2093
- if (code === 0) {
2094
- resolve({ success: true, output: combinedOutput });
2095
- } else {
2096
- const message = `${providerConfig.displayName} exited with code ${code}`;
2097
- const antigravityRateLimit = checkAntigravityRateLimit(combinedOutput);
2098
- const kiroRateLimit = checkKiroRateLimit(combinedOutput);
2099
- const clineRateLimit = checkClineRateLimit(combinedOutput);
2100
-
2101
- resolve({
2102
- success: false,
2103
- error: combinedOutput ? `${message}\n${combinedOutput}` : message,
2104
- output: combinedOutput,
2105
- rateLimited: isRateLimitMessage(combinedOutput) || antigravityRateLimit.isRateLimited || kiroRateLimit.isRateLimited || clineRateLimit.isRateLimited,
2106
- antigravityRateLimited: antigravityRateLimit.isRateLimited,
2107
- kiroRateLimited: kiroRateLimit.isRateLimited,
2108
- clineRateLimited: clineRateLimit.isRateLimited
2109
- });
2110
- }
2111
- });
2112
- });
2113
- }
2114
-
2115
- /**
2116
- * Wait for IDE agent to complete work by monitoring requirements file
2117
- * @param {string} repoPath - Repository path
2118
- * @param {string} requirementText - Requirement text to watch for
2119
- * @param {string} ideType - IDE type (e.g., 'antigravity') for quota limit handling
2120
- * @param {number} timeoutMs - Timeout in milliseconds (default: 30 minutes)
2121
- * @returns {Promise<{success: boolean, reason?: string}>}
2122
- */
2123
- async function waitForIdeCompletion(repoPath, requirementText, ideType = '', timeoutMs = 30 * 60 * 1000) {
2124
- const reqPath = await getRequirementsPath(repoPath);
2125
-
2126
- return new Promise(async (resolve) => {
2127
- let startTime = Date.now();
2128
- let lastCheckTime = Date.now();
2129
- let quotaHandled = false;
2130
- let lastQuotaCheckTime = 0; // Throttle quota checks to every 30 seconds
2131
- const checkIntervalMs = 2000; // Check every 2 seconds
2132
- const quotaCheckIntervalMs = 30000; // Check quota every 30 seconds
2133
-
2134
- console.log(chalk.gray(`\n⏳ ${t('auto.direct.ide.waiting')}`));
2135
- console.log(chalk.gray(` ${t('auto.direct.ide.monitoring', { filename: path.basename(reqPath) })}`));
2136
- console.log(chalk.gray(` ${t('auto.direct.ide.timeout', { minutes: Math.floor(timeoutMs / 60000) })}\n`));
2137
-
2138
- const watcher = chokidar.watch(reqPath, {
2139
- persistent: true,
2140
- ignoreInitial: false
2141
- });
2142
-
2143
- const checkCompletion = async () => {
2144
- try {
2145
- const content = await fs.readFile(reqPath, 'utf-8');
2146
- const lines = content.split('\n');
2147
-
2148
- // Check 1: Is requirement in "Verified by AI" section?
2149
- let inVerifiedSection = false;
2150
- let foundInVerified = false;
2151
-
2152
- for (const line of lines) {
2153
- if (line.includes('## ✅ Verified by AI')) {
2154
- inVerifiedSection = true;
2155
- continue;
2156
- }
2157
-
2158
- if (inVerifiedSection && line.startsWith('##') && !line.startsWith('###')) {
2159
- inVerifiedSection = false;
2160
- break;
2161
- }
2162
-
2163
- if (inVerifiedSection && line.includes(requirementText)) {
2164
- foundInVerified = true;
2165
- break;
2166
- }
2167
- }
2168
-
2169
- if (foundInVerified) {
2170
- watcher.close();
2171
- console.log(chalk.green('✓ IDE agent completed - requirement moved to Verified section\n'));
2172
- resolve({ success: true });
2173
- return;
2174
- }
2175
-
2176
- // Check 2: Does status section contain "DONE"?
2177
- let inStatusSection = false;
2178
- let statusContainsDone = false;
2179
-
2180
- for (const line of lines) {
2181
- if (line.includes('🚦 Current Status')) {
2182
- inStatusSection = true;
2183
- if (line.includes('DONE')) {
2184
- statusContainsDone = true;
2185
- break;
2186
- }
2187
- continue;
2188
- }
2189
-
2190
- if (inStatusSection && line.startsWith('##') && !line.startsWith('###')) {
2191
- inStatusSection = false;
2192
- break;
2193
- }
2194
-
2195
- if (inStatusSection && line.trim() === 'DONE') {
2196
- statusContainsDone = true;
2197
- break;
2198
- }
2199
- }
2200
-
2201
- if (statusContainsDone) {
2202
- watcher.close();
2203
- console.log(chalk.green('✓ IDE agent completed - status marked as DONE\n'));
2204
- resolve({ success: true });
2205
- return;
2206
- }
2207
-
2208
- // Check 3: Detect rate-limit messages written into the REQUIREMENTS file
2209
- // Examples:
2210
- // - "You have reached the quota limit for this model. You can resume using this model at 1/19/2026, 4:07:27 PM."
2211
- // - "Please try again in 15m5.472s"
2212
- // - "Spending cap reached resets Jan 17 at 12pm"
2213
- // - "Usage cap reached. Try again in 15 minutes."
2214
- // - "You've reached your monthly chat messages quota" (GitHub Copilot)
2215
- // - "Upgrade to Copilot Pro" (GitHub Copilot)
2216
- // - "wait for your allowance to renew" (GitHub Copilot)
2217
- const rateLimitPattern = /(quota limit|you have reached( the)? quota|you can resume using this model at|please try again in|try again in|spending cap reached|usage cap( reached)?|you\'ve hit( the)? usage limit|you\u2019ve hit( the)? usage limit|cap reached|limit exceeded|exceeded (quota|limit)|monthly.*quota|upgrade to.*pro|allowance to renew|chat messages quota)/i;
2218
- // Avoid matching requirement headings or bullets (these may mention "rate limit" as part of the requirement text)
2219
- const matchedLine = lines.find(l => rateLimitPattern.test(l) && !l.trim().startsWith('###') && !l.trim().startsWith('-'));
2220
- if (matchedLine) {
2221
- // Mark the provider as rate limited (if we can) and signal a rate-limited completion
2222
- try {
2223
- if (ideType && sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
2224
- sharedProviderManager.markRateLimited(ideType, undefined, matchedLine);
2225
- }
2226
- } catch (e) {
2227
- // Ignore errors from marking rate-limited
2228
- }
2229
-
2230
- watcher.close();
2231
- console.log(chalk.yellow(`\n⚠️ Rate limit message detected from IDE: ${matchedLine}\n`));
2232
- // Return a generic rateLimited flag and include the matched line for diagnostics
2233
- resolve({ success: false, rateLimited: true, providerRateLimited: ideType || undefined, matchedLine });
2234
- return;
2235
- }
2236
-
2237
- // Check 4: Active quota detection (throttled to every 30 seconds)
2238
- // For Kiro: ONLY AppleScript (CDP doesn't work with webviews)
2239
- // For others: CDP only
2240
- const now = Date.now();
2241
- if (!quotaHandled && ideType && (now - lastQuotaCheckTime >= quotaCheckIntervalMs)) {
2242
- lastQuotaCheckTime = now;
2243
-
2244
- try {
2245
- // For Kiro: ONLY AppleScript detection (CDP doesn't work)
2246
- if (ideType === 'kiro' || ideType === 'amazon-q') {
2247
- try {
2248
- const appleScriptManager = new AppleScriptManager();
2249
- const screenshotResult = await appleScriptManager.detectQuotaWarning('kiro');
2250
-
2251
- if (screenshotResult && screenshotResult.hasQuotaWarning) {
2252
- quotaHandled = true;
2253
- const quotaMessage = screenshotResult.matchedText || screenshotResult.note || 'Kiro quota limit detected via AppleScript';
2254
-
2255
- // Mark the provider as rate limited
2256
- try {
2257
- if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
2258
- sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
2259
- }
2260
- } catch (e) {
2261
- // Ignore errors from marking rate-limited
2262
- }
2263
-
2264
- watcher.close();
2265
- console.log(chalk.yellow(`\n⚠️ Quota warning detected via AppleScript for Kiro: ${quotaMessage.substring(0, 100)}...\n`));
2266
- resolve({ success: false, rateLimited: true, kiroRateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
2267
- return;
2268
- }
2269
- } catch (screenshotError) {
2270
- console.log(chalk.red(`❌ AppleScript quota detection failed for Kiro: ${screenshotError.message}`));
2271
- }
2272
- }
2273
- // For Antigravity: AppleScript quota detection
2274
- else if (ideType === 'antigravity') {
2275
- try {
2276
- const appleScriptManager = new AppleScriptManager();
2277
- const antigravityQuotaResult = await appleScriptManager.checkAntigravityQuotaLimit();
2278
-
2279
- if (antigravityQuotaResult && antigravityQuotaResult.isRateLimited) {
2280
- quotaHandled = true;
2281
- const quotaMessage = antigravityQuotaResult.message || 'Antigravity quota limit detected via AppleScript';
2282
-
2283
- // Mark the provider as rate limited
2284
- try {
2285
- if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
2286
- sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
2287
- }
2288
- } catch (e) {
2289
- // Ignore errors from marking rate-limited
2290
- }
2291
-
2292
- watcher.close();
2293
- console.log(chalk.yellow(`\n⚠️ Quota warning detected via AppleScript for Antigravity: ${quotaMessage.substring(0, 100)}...\n`));
2294
- resolve({ success: false, rateLimited: true, antigravityRateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
2295
- return;
2296
- }
2297
- } catch (appleScriptError) {
2298
- console.log(chalk.red(`❌ AppleScript quota detection failed for Antigravity: ${appleScriptError.message}`));
2299
- }
2300
- }
2301
- // For Windsurf: AppleScript quota detection
2302
- else if (ideType === 'windsurf') {
2303
- try {
2304
- const appleScriptManager = new AppleScriptManager();
2305
- const windsurfQuotaResult = await appleScriptManager.checkWindsurfQuotaLimit();
2306
-
2307
- if (windsurfQuotaResult && windsurfQuotaResult.hasQuotaWarning) {
2308
- quotaHandled = true;
2309
- const quotaMessage = windsurfQuotaResult.matchedText || 'Windsurf quota limit detected';
2310
-
2311
- // Mark the provider as rate limited
2312
- try {
2313
- if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
2314
- sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
2315
- }
2316
- } catch (e) {
2317
- // Ignore errors from marking rate-limited
2318
- }
2319
-
2320
- watcher.close();
2321
- console.log(chalk.yellow(`\n⚠️ Windsurf quota warning detected: ${quotaMessage.substring(0, 100)}...\n`));
2322
- resolve({ success: false, rateLimited: true, windsurfRateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
2323
- return;
2324
- }
2325
- } catch (windsurfAppleScriptError) {
2326
- console.log(chalk.red(`❌ AppleScript quota detection failed for Windsurf: ${windsurfAppleScriptError.message}`));
2327
- }
2328
- }
2329
- // For Cursor: Skip CDP and go directly to AppleScript
2330
- else if (ideType === 'cursor') {
2331
- try {
2332
- const appleScriptManager = new AppleScriptManager();
2333
- const cursorQuotaResult = await appleScriptManager.checkCursorQuotaLimit();
2334
-
2335
- if (cursorQuotaResult && cursorQuotaResult.isRateLimited) {
2336
- quotaHandled = true;
2337
- const quotaMessage = cursorQuotaResult.message || 'Cursor quota limit detected via AppleScript';
2338
-
2339
- // Mark the provider as rate limited
2340
- try {
2341
- if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
2342
- sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
2343
- }
2344
- } catch (e) {
2345
- // Ignore errors from marking rate-limited
2346
- }
2347
-
2348
- watcher.close();
2349
- console.log(chalk.yellow(`\n⚠️ Quota warning detected via AppleScript for Cursor: ${quotaMessage.substring(0, 100)}...\n`));
2350
- resolve({ success: false, rateLimited: true, providerRateLimited: ideType, matchedLine: quotaMessage });
2351
- return;
2352
- }
2353
- } catch (appleScriptError) {
2354
- console.log(chalk.red(`❌ AppleScript quota detection failed for Cursor: ${appleScriptError.message}`));
2355
- }
2356
- }
2357
- // For other IDEs: use CDP
2358
- else if (ideType === 'vscode' || ideType === 'github-copilot' || ideType === 'amazon-q') {
2359
- const quotaDetector = new QuotaDetector();
2360
- const ideToCheck = ideType === 'github-copilot' || ideType === 'amazon-q' ? 'vscode' : ideType;
2361
-
2362
- try {
2363
- const quotaResult = await quotaDetector.detectQuotaWarning(ideToCheck);
2364
-
2365
- if (quotaResult && quotaResult.hasQuotaWarning) {
2366
- quotaHandled = true;
2367
- const quotaMessage = quotaResult.matchedText || 'Quota limit detected in IDE UI';
2368
-
2369
- // Check if this might be Kiro quota warning even when ideType is amazon-q
2370
- const isKiroPattern = quotaMessage.toLowerCase().includes('out of credits') ||
2371
- quotaMessage.toLowerCase().includes('upgrade plan') ||
2372
- quotaMessage.toLowerCase().includes('monthly usage limit');
2373
-
2374
- // Mark the provider as rate limited
2375
- try {
2376
- if (sharedProviderManager && typeof sharedProviderManager.markRateLimited === 'function') {
2377
- sharedProviderManager.markRateLimited(ideType, undefined, quotaMessage);
2378
- }
2379
- } catch (e) {
2380
- // Ignore errors from marking rate-limited
2381
- }
2382
-
2383
- watcher.close();
2384
- console.log(chalk.yellow(`\n⚠️ Quota warning detected via CDP in ${ideToCheck} UI: ${quotaMessage.substring(0, 100)}...\n`));
2385
-
2386
- // If this looks like Kiro quota, set kiroRateLimited flag too
2387
- const resolveObj = {
2388
- success: false,
2389
- rateLimited: true,
2390
- providerRateLimited: ideType,
2391
- matchedLine: quotaMessage
2392
- };
2393
-
2394
- if (ideType === 'amazon-q' && isKiroPattern) {
2395
- resolveObj.kiroRateLimited = true;
2396
- console.log(chalk.magenta(`💡 Detected potential Kiro quota warning despite amazon-q ideType`));
2397
- }
2398
-
2399
- resolve(resolveObj);
2400
- return;
2401
- }
2402
- } catch (cdpError) {
2403
- console.log(chalk.yellow(`⚠️ CDP quota detection failed for ${ideToCheck}: ${cdpError.message}`));
2404
- }
2405
- }
2406
- } catch (quotaError) {
2407
- console.log(chalk.red(`❌ [DEBUG] Quota detection check failed: ${quotaError.message}`));
2408
- console.log(chalk.gray(` Stack: ${quotaError.stack}`));
2409
- }
2410
- }
2411
-
2412
- // Check 5: Continuation detection (check every 2 seconds)
2413
- if (elapsed > 30000 && elapsed % 2000 < 2000) { // Start checking after 30 seconds, every 2 seconds
2414
- try {
2415
- let continuationDetected = false;
2416
- let continuationClicked = false;
2417
-
2418
- // Use CDP for web-based IDEs
2419
- if (ideType === 'vscode' || ideType === 'cursor' || ideType === 'windsurf') {
2420
- const { CDPManager } = require('vibecodingmachine-core');
2421
- const cdpManager = new CDPManager();
2422
-
2423
- try {
2424
- const continuationResult = await cdpManager.checkForContinuation(ideType);
2425
- if (continuationResult.continuationDetected) {
2426
- console.log(chalk.yellow(`🔄 Continuation prompt detected in ${ideType}, clicking button...`));
2427
- const clickResult = await cdpManager.clickContinuationButton(ideType);
2428
- if (clickResult.success && clickResult.clicked) {
2429
- continuationDetected = true;
2430
- continuationClicked = true;
2431
- console.log(chalk.green(`✅ Continuation button clicked successfully`));
2432
- } else {
2433
- console.log(chalk.yellow(`⚠️ Failed to click continuation button: ${clickResult.message || 'Unknown error'}`));
2434
- }
2435
- }
2436
- } catch (cdpError) {
2437
- console.log(chalk.gray(` Continuation detection via CDP failed: ${cdpError.message}`));
2438
- }
2439
- }
2440
- // Use AppleScript for desktop IDEs
2441
- else if (ideType === 'cline' || ideType === 'claude-code') {
2442
- const { AppleScriptManager } = require('vibecodingmachine-core');
2443
- const appleScriptManager = new AppleScriptManager();
2444
-
2445
- try {
2446
- const continuationResult = await appleScriptManager.checkForContinuation(ideType);
2447
- if (continuationResult.continuationDetected) {
2448
- console.log(chalk.yellow(`🔄 Continuation prompt detected in ${ideType}, clicking button...`));
2449
- const clickResult = await appleScriptManager.clickContinuationButton(ideType);
2450
- if (clickResult.success && clickResult.clicked) {
2451
- continuationDetected = true;
2452
- continuationClicked = true;
2453
- console.log(chalk.green(`✅ Continuation button clicked successfully`));
2454
- } else {
2455
- console.log(chalk.yellow(`⚠️ Failed to click continuation button: ${clickResult.error || 'Unknown error'}`));
2456
- }
2457
- }
2458
- } catch (appleScriptError) {
2459
- console.log(chalk.gray(` Continuation detection via AppleScript failed: ${appleScriptError.message}`));
2460
- }
2461
- }
2462
-
2463
- // Record continuation detection in health tracker
2464
- if (continuationDetected) {
2465
- try {
2466
- // Update the interaction record to include continuation detection
2467
- const currentMetrics = sharedHealthTracker.getHealthMetrics(ideType);
2468
- if (currentMetrics && currentMetrics.interactions && currentMetrics.interactions.length > 0) {
2469
- const lastInteraction = currentMetrics.interactions[currentMetrics.interactions.length - 1];
2470
- lastInteraction.continuationPromptsDetected = (lastInteraction.continuationPromptsDetected || 0) + 1;
2471
- }
2472
- } catch (trackerError) {
2473
- console.log(chalk.gray(` Failed to record continuation detection: ${trackerError.message}`));
2474
- }
2475
- }
2476
- } catch (error) {
2477
- console.log(chalk.gray(` Continuation detection error: ${error.message}`));
2478
- }
2479
- }
2480
-
2481
- // Check 6: Timeout
2482
- const elapsed = Date.now() - startTime;
2483
- if (elapsed >= timeoutMs) {
2484
- watcher.close();
2485
- console.log(chalk.yellow(`\n⚠️ Timeout after ${Math.floor(elapsed / 60000)} minutes\n`));
2486
- resolve({ success: false, reason: 'timeout' });
2487
- return;
2488
- }
2489
-
2490
- // Check 7: Text pattern matching fallback for continuation prompts
2491
- const fileContent = await fs.readFile(reqPath, 'utf-8');
2492
- const continuationPatterns = [
2493
- /continue.*generation/i,
2494
- /keep.*going/i,
2495
- /continue.*writing/i,
2496
- /continue.*coding/i,
2497
- /proceed/i,
2498
- /next.*step/i,
2499
- /continue/i
2500
- ];
2501
-
2502
- const hasContinuationPrompt = continuationPatterns.some(pattern => pattern.test(fileContent));
2503
- if (hasContinuationPrompt && elapsed > 30000) { // Only check for text patterns after 30 seconds
2504
- console.log(chalk.yellow(`🔄 Continuation prompt detected in text, attempting to continue...`));
2505
- // This is a fallback - we can't click buttons via text patterns, but we can log it
2506
- try {
2507
- const currentMetrics = sharedHealthTracker.getHealthMetrics(ideType);
2508
- if (currentMetrics && currentMetrics.interactions && currentMetrics.interactions.length > 0) {
2509
- const lastInteraction = currentMetrics.interactions[currentMetrics.interactions.length - 1];
2510
- lastInteraction.continuationPromptsDetected = (lastInteraction.continuationPromptsDetected || 0) + 1;
2511
- }
2512
- } catch (trackerError) {
2513
- console.log(chalk.gray(` Failed to record text continuation detection: ${trackerError.message}`));
2514
- }
2515
- }
2516
- // Check 8: Memory monitoring
2517
- if (elapsed > 60000 && elapsed % 30000 < 30000) { // Check memory every 30 seconds after 1 minute
2518
- const memoryUsage = process.memoryUsage();
2519
- const heapUsedMB = Math.round(memoryUsage.heapUsed / 1024 / 1024);
2520
- const heapTotalMB = Math.round(memoryUsage.heapTotal / 1024 / 1024);
2521
- const heapUsedPercent = Math.round((memoryUsage.heapUsed / memoryUsage.heapTotal) * 100);
2522
-
2523
- if (heapUsedMB > 500) { // Warn at 500MB
2524
- console.log(chalk.yellow(`⚠️ High memory usage detected: ${heapUsedMB}MB heap (${heapUsedPercent}% of ${heapTotalMB}MB)`));
2525
- console.log(chalk.gray(` RSS: ${Math.round(memoryUsage.rss / 1024 / 1024)}MB, External: ${Math.round(memoryUsage.external / 1024 / 1024)}MB`));
2526
- }
2527
- }
2528
-
2529
- // Log progress every 30 seconds
2530
- if (Date.now() - lastCheckTime >= 30000) {
2531
- const elapsedMin = Math.floor(elapsed / 60000);
2532
- const remainingMin = Math.floor((timeoutMs - elapsed) / 60000);
2533
- console.log(chalk.gray(` ${t('auto.direct.ide.still.waiting', { elapsed: elapsedMin, remaining: remainingMin })}`));
2534
- lastCheckTime = Date.now();
2535
- }
2536
- } catch (error) {
2537
- console.error(chalk.red(`Error checking completion: ${error.message}`));
2538
- }
2539
- };
2540
-
2541
- // Check on file changes
2542
- watcher.on('change', () => {
2543
- checkCompletion();
2544
- });
2545
-
2546
- // Also check periodically in case file watcher misses changes
2547
- const interval = setInterval(() => {
2548
- checkCompletion();
2549
- }, checkIntervalMs);
2550
-
2551
- // Clean up interval when promise resolves
2552
- const originalResolve = resolve;
2553
- resolve = (result) => {
2554
- clearInterval(interval);
2555
- originalResolve(result);
2556
- };
2557
-
2558
- // Initial check
2559
- checkCompletion();
2560
- });
2561
- }
2562
-
2563
- async function runIdeFallbackIteration(requirement, providerConfig, repoPath, providerManager, llm, startTime) {
2564
- // Update console and requirements file with PREPARE status
2565
- printStatusCard(requirement.text, 'PREPARE', 'active');
2566
- await updateRequirementsStatus(repoPath, 'PREPARE');
2567
- console.log(chalk.gray(`${t('auto.direct.ide.skipping.context')}\n`));
2568
-
2569
- // Update console and requirements file with ACT status
2570
- printStatusCard(requirement.text, 'ACT', 'active');
2571
- await updateRequirementsStatus(repoPath, 'ACT');
2572
- const ideResult = await runIdeProviderIteration(providerConfig, repoPath);
2573
-
2574
- if (!ideResult.success) {
2575
- if (ideResult.antigravityRateLimited) {
2576
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, 'Quota limit reached');
2577
- const switchResult = await handleAntigravityRateLimit();
2578
- if (switchResult && switchResult.modelSwitched) {
2579
- return { success: false, error: `Antigravity switched to ${switchResult.nextModel}, retrying.`, shouldRetry: true };
2580
- }
2581
- return { success: false, error: 'Antigravity rate limit reached, retrying with next provider.', shouldRetry: true };
2582
- }
2583
-
2584
- // CRITICAL: Mark provider as unavailable for ANY error so acquireProviderConfig() will skip it
2585
- // EXCEPT for web-based IDEs where the error is platform/browser related
2586
- const error = ideResult.output || ideResult.error || 'IDE provider failed';
2587
- const isWebBasedIDE = providerConfig.provider === 'replit';
2588
- const isPlatformError = error.includes('xdg-open') || error.includes('command not found') || error.includes('Unable to find application');
2589
-
2590
- if (!isWebBasedIDE || !isPlatformError) {
2591
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, error);
2592
- } else {
2593
- // For web-based IDEs with platform errors, don't mark as rate limited
2594
- // Just log the error and let the system try the next provider
2595
- console.log(chalk.yellow(`⚠️ Web-based IDE ${providerConfig.provider} failed due to platform issue: ${error}`));
2596
- }
2597
-
2598
- return { success: false, error: ideResult.error || 'IDE provider failed' };
2599
- }
2600
-
2601
- console.log(chalk.green(`✓ ${t('auto.direct.ide.prompt.sent')}`));
2602
-
2603
- // Calculate adaptive timeout based on IDE's historical performance
2604
- const ideType = providerConfig.provider || providerConfig.ide;
2605
- const healthMetrics = sharedHealthTracker.getHealthMetrics(ideType);
2606
- let adaptiveTimeoutMs = 30 * 60 * 1000; // Default 30 minutes
2607
-
2608
- if (healthMetrics && healthMetrics.responseTimes.length > 0) {
2609
- // Use adaptive timeout calculation if we have historical data
2610
- const stats = TimeoutCalculator.calculateTimeout(healthMetrics.responseTimes, {
2611
- defaultTimeout: 30 * 60 * 1000,
2612
- minTimeout: 5 * 60 * 1000, // 5 minutes minimum
2613
- maxTimeout: 60 * 60 * 1000, // 60 minutes maximum
2614
- bufferPercentage: 0.5 // 50% buffer
2615
- });
2616
- adaptiveTimeoutMs = stats.timeout;
2617
- console.log(chalk.gray(` Using adaptive timeout: ${Math.floor(adaptiveTimeoutMs / 60000)} minutes (based on ${healthMetrics.responseTimes.length} historical response times)`));
2618
- } else {
2619
- console.log(chalk.gray(` Using default timeout: ${Math.floor(adaptiveTimeoutMs / 60000)} minutes (no historical data for ${ideType})`));
2620
- }
2621
-
2622
- // Wait for IDE agent to complete the work (IDE will update status to DONE itself)
2623
- const ideCompletionStartTime = Date.now();
2624
- const completionResult = await waitForIdeCompletion(repoPath, requirement.text, ideType, adaptiveTimeoutMs);
2625
- const ideResponseTime = Date.now() - ideCompletionStartTime;
2626
-
2627
- // Track IDE health metrics based on completion result
2628
- if (completionResult.success) {
2629
- // Record success with response time
2630
- await sharedHealthTracker.recordSuccess(ideType, ideResponseTime, {
2631
- requirementId: requirement.id || requirement.text.substring(0, 50),
2632
- continuationPromptsDetected: 0, // Will be updated when continuation detection is implemented
2633
- });
2634
- } else if (completionResult.rateLimited) {
2635
- // Record quota event (does NOT increment success/failure counters per FR-008)
2636
- await sharedHealthTracker.recordQuota(ideType, completionResult.matchedLine || 'Quota limit detected', {
2637
- requirementId: requirement.id || requirement.text.substring(0, 50),
2638
- });
2639
- } else if (completionResult.reason === 'timeout') {
2640
- // Record failure due to timeout
2641
- await sharedHealthTracker.recordFailure(ideType, 'Timeout exceeded', {
2642
- timeoutUsed: adaptiveTimeoutMs, // Use the adaptive timeout that was actually used
2643
- requirementId: requirement.id || requirement.text.substring(0, 50),
2644
- });
2645
- } else {
2646
- // Record other failure
2647
- await sharedHealthTracker.recordFailure(ideType, completionResult.error || 'Unknown error', {
2648
- requirementId: requirement.id || requirement.text.substring(0, 50),
2649
- });
2650
- }
2651
-
2652
- if (!completionResult.success) {
2653
- // Special-case behavior for Antigravity CLI installs (they have special handling)
2654
- if (completionResult.antigravityRateLimited) {
2655
- console.log(chalk.yellow(`⚠️ ${t('auto.direct.provider.quota.exhausted', { provider: 'Antigravity' })}\n`));
2656
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, 'Quota limit reached');
2657
-
2658
- const switchResult = await handleAntigravityRateLimit();
2659
- if (switchResult && switchResult.modelSwitched) {
2660
- return { success: false, error: `Antigravity switched to ${switchResult.nextModel}, retrying.`, shouldRetry: true };
2661
- }
2662
-
2663
- return { success: false, error: 'Antigravity quota limit', shouldRetry: true };
2664
- }
2665
-
2666
- // Special-case behavior for Kiro IDE (they have special handling)
2667
- if (completionResult.kiroRateLimited) {
2668
- console.log(chalk.yellow(`⚠️ ${t('auto.direct.provider.quota.exhausted', { provider: 'AWS Kiro' })}\n`));
2669
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, 'Quota limit reached');
2670
-
2671
- const switchResult = await handleKiroRateLimit();
2672
- if (switchResult && switchResult.success) {
2673
- return { success: false, error: `AWS Kiro switched to ${switchResult.nextProvider}, retrying.`, shouldRetry: true };
2674
- }
2675
-
2676
- return { success: false, error: 'AWS Kiro quota limit', shouldRetry: true };
2677
- }
2678
-
2679
- // Special-case behavior for Cline IDE
2680
- if (completionResult.clineRateLimited) {
2681
- console.log(chalk.yellow(`⚠️ ${t('auto.direct.provider.quota.exhausted', { provider: 'Cline' })}\n`));
2682
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, 'Quota limit reached');
2683
-
2684
- const switchResult = await handleClineRateLimit();
2685
- if (switchResult && switchResult.success) {
2686
- return { success: false, error: `Cline switched to ${switchResult.nextProvider}, retrying.`, shouldRetry: true };
2687
- }
2688
-
2689
- return { success: false, error: 'Cline quota limit', shouldRetry: true };
2690
- }
2691
-
2692
- // Generic rate-limited behavior: if the completion detected a rate-limited message in REQUIREMENTS,
2693
- // mark the provider as rate-limited and retry with the next available provider.
2694
- if (completionResult.rateLimited) {
2695
- console.log(chalk.yellow(`⚠️ Provider ${providerConfig.provider} reported quota/exhaustion: ${completionResult.matchedLine || ''}\n`));
2696
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, completionResult.matchedLine || 'Quota limit detected');
2697
- return { success: false, error: 'Provider quota limit', shouldRetry: true };
2698
- }
2699
-
2700
- const errorMsg = completionResult.reason === 'timeout'
2701
- ? 'IDE agent timed out'
2702
- : 'IDE agent failed to complete';
2703
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, errorMsg);
2704
-
2705
- // Automatically retry with next IDE on timeout (T024: automatic IDE switching)
2706
- if (completionResult.reason === 'timeout') {
2707
- console.log(chalk.yellow(`⏰ Timeout detected - switching to next available IDE\n`));
2708
- return { success: false, error: errorMsg, shouldRetry: true };
2709
- }
2710
-
2711
- return { success: false, error: errorMsg };
2712
- }
2713
-
2714
- printStatusCard(requirement.text, 'VERIFY', 'active');
2715
- console.log(chalk.green(`✅ ${t('auto.direct.provider.completed')}\n`));
2716
-
2717
- printStatusCard(requirement.text, 'DONE', 'active');
2718
- const duration = Date.now() - startTime;
2719
- providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
2720
-
2721
- const moved = await moveRequirementToVerify(repoPath, requirement.text);
2722
- if (moved) {
2723
- console.log(chalk.green(`✓ ${t('auto.direct.status.verification.pending')}`));
2724
- }
2725
-
2726
- console.log();
2727
- console.log(chalk.gray('─'.repeat(80)));
2728
- console.log();
2729
-
2730
- return { success: true, changes: [] };
2731
- }
2732
-
2733
- /**
2734
- * Run a spec iteration using an IDE provider (AppleScript).
2735
- * Sends the full spec instruction to the IDE and polls tasks.md for
2736
- * progress (any new checkbox ticked), instead of spawning vcm auto:start.
2737
- *
2738
- * COORDINATION STRATEGY:
2739
- * - Wait for agent completion signals instead of just detecting any progress
2740
- * - Implement 2-minute cooldown after progress detection to prevent rapid spawning
2741
- * - Only consider iteration complete when all tasks are done AND agent signals completion
2742
- * - Use STATUS:WAITING and PLEASE RESPOND signals from agent to coordinate handoff
2743
- * - Prevent multiple agents from working on the same spec simultaneously
2744
- *
2745
- * @param {Object} spec - Spec object with .path, .directory, .hasTasks, .hasPlan, .hasPlanPrompt
2746
- * @param {string} taskText - Text of the NEXT unchecked task (for display)
2747
- * @param {string} taskLine - Full line from tasks.md (used if this is a continuation)
2748
- * @param {Object} providerConfig - Provider config with .provider/.ide and .displayName
2749
- * @returns {Promise<{success: boolean, error?: string, shouldRetry?: boolean}>}
2750
- */
2751
- async function runSpecIdeIteration(spec, taskText, taskLine, providerConfig) {
2752
- const ideType = providerConfig.provider || providerConfig.ide;
2753
- const { done: doneBefore, total: totalBefore } = countSpecCheckboxes(spec.path);
2754
- const pctBefore = totalBefore > 0 ? Math.round((doneBefore / totalBefore) * 100) : 0;
2755
-
2756
- // Get configurable timeouts from auto config
2757
- const autoConfig = await getAutoConfig();
2758
- const PROGRESS_TIMEOUT_MS = (autoConfig.specProgressTimeoutMinutes || 15) * 60 * 1000; // Configurable, default 15 minutes
2759
- const MAX_IDE_ATTEMPTS = autoConfig.maxIdeAttempts || 3; // Configurable, default 3 attempts
2760
-
2761
- // Build the full spec instruction (mirrors buildSpecInstruction from Electron app)
2762
- // This lets the agent work through multiple tasks autonomously rather than one at a time.
2763
- let instruction;
2764
- if (spec.hasTasks) {
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.`;
2766
- } else if (spec.hasPlan) {
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.`;
2768
- } else {
2769
- const planPromptNote = spec.hasPlanPrompt
2770
- ? `Use plan prompt from ${spec.path}/plan-prompt.md to guide planning. ` : '';
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.`;
2772
- }
2773
-
2774
- // Send the spec instruction to the IDE via platform-specific automation.
2775
- // Uses the appropriate automation manager for the current platform.
2776
- console.log(chalk.cyan(`📤 Sending spec instruction to ${providerConfig.displayName}...\n`));
2777
-
2778
- if (ideType === 'windsurf') {
2779
- try {
2780
- let sendResult;
2781
-
2782
- // Use platform-specific automation
2783
- if (process.platform === 'win32') {
2784
- // Use Windows automation on Windows
2785
- const { WindowsAutomationManager } = require('vibecodingmachine-core');
2786
- const windowsManager = new WindowsAutomationManager();
2787
- sendResult = await windowsManager.sendTextToWindsurf(instruction);
2788
- } else {
2789
- // Use AppleScript on macOS
2790
- const appleScriptManager = new AppleScriptManager();
2791
- sendResult = await appleScriptManager.sendText(instruction, 'windsurf');
2792
- }
2793
-
2794
- if (sendResult && sendResult.success) {
2795
- console.log(chalk.green(`✓ Spec instruction sent to ${providerConfig.displayName}`));
2796
- console.log(chalk.gray(`⏳ Polling tasks.md every 30s for checkbox progress...\n`));
2797
- console.log(chalk.gray(`🔄 Coordination: Will wait for agent completion signals before sending new tasks\n`));
2798
- } else {
2799
- console.log(chalk.red(`✗ Failed to send instruction to Windsurf: ${sendResult?.error || 'Unknown error'}`));
2800
- return { success: false, error: sendResult?.error || 'Unknown error' };
2801
- }
2802
- } catch (err) {
2803
- console.log(chalk.red(`✗ Automation error: ${err.message}`));
2804
- return { success: false, error: err.message };
2805
- }
2806
- } else {
2807
- // Non-Windsurf IDEs: use AppleScriptManager
2808
- try {
2809
- const appleScriptManager = new AppleScriptManager();
2810
- let sendResult;
2811
- // Prefer sendTextWithThreadClosure when available (.js build), fall back to sendText (.cjs build)
2812
- if (typeof appleScriptManager.sendTextWithThreadClosure === 'function') {
2813
- sendResult = await appleScriptManager.sendTextWithThreadClosure(instruction, ideType);
2814
- } else {
2815
- sendResult = await appleScriptManager.sendText(instruction, ideType);
2816
- }
2817
- if (!sendResult || !sendResult.success) {
2818
- const errorMsg = (sendResult && sendResult.error) || `Failed to send text to ${providerConfig.displayName}`;
2819
- console.log(chalk.red(`✗ ${errorMsg}`));
2820
- return { success: false, error: errorMsg };
2821
- }
2822
- } catch (err) {
2823
- console.log(chalk.red(`✗ AppleScript error: ${err.message}`));
2824
- return { success: false, error: err.message };
2825
- }
2826
- }
2827
-
2828
- console.log(chalk.green(`✓ Spec instruction sent to ${providerConfig.displayName}`));
2829
- console.log(chalk.gray(`⏳ Polling tasks.md every 30s for checkbox progress...\n`));
2830
- console.log(chalk.gray(`🔄 Coordination: Will wait for agent completion signals before sending new tasks\n`));
2831
-
2832
- const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds
2833
- const CONTINUE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
2834
- const TIMEOUT_MS = PROGRESS_TIMEOUT_MS; // Use configurable progress timeout
2835
- const PROGRESS_COOLDOWN_MS = 2 * 60 * 1000; // 2 minutes cooldown after progress
2836
-
2837
- let startTime = Date.now();
2838
- let lastContinueSent = Date.now();
2839
- let lastProgressTime = Date.now(); // Track when we last saw progress
2840
- let lastProgressDetectionTime = 0; // Track when we last detected progress to implement cooldown
2841
- let totalAttempts = (spec.totalIdeAttempts || 0) + 1; // Track attempts across IDEs
2842
- let agentSignaledCompletion = false; // Track if agent signaled completion
2843
-
2844
- return new Promise((resolve) => {
2845
- let interval = null;
2846
-
2847
- const cleanup = (result) => {
2848
- if (interval) { clearInterval(interval); interval = null; }
2849
- process.off('SIGINT', onSigint);
2850
- resolve(result);
2851
- };
2852
-
2853
- // Handle Ctrl+C gracefully: stop polling and exit cleanly
2854
- const onSigint = () => {
2855
- console.log(chalk.yellow('\n⚠️ Interrupted — stopping spec task polling\n'));
2856
- cleanup({ success: false, error: 'interrupted' });
2857
- // Re-emit to trigger normal process exit after cleanup
2858
- process.exit(0);
2859
- };
2860
- process.once('SIGINT', onSigint);
2861
-
2862
- interval = setInterval(async () => {
2863
- try {
2864
- const elapsed = Date.now() - startTime;
2865
-
2866
- if (elapsed >= TIMEOUT_MS) {
2867
- console.log(chalk.yellow(`⏰ Overall timeout reached - but continuing to monitor existing Windsurf instance\n`));
2868
- // Don't create new instance - just reset timer and continue monitoring
2869
- startTime = Date.now(); // Reset the overall timer
2870
- return;
2871
- }
2872
-
2873
- // Detect progress AND check if IDE agent is done working
2874
- const { done: doneNow, total: totalNow } = countSpecCheckboxes(spec.path);
2875
- const currentTime = Date.now();
2876
-
2877
- // Check for completion signals from IDE agent via status file
2878
- const statusFilePath = spec.path.replace(/\/$/, '') + '/.vcm-status.json';
2879
- let agentSignaledCompletionViaFile = false;
2880
- try {
2881
- if (await fs.pathExists(statusFilePath)) {
2882
- const statusContent = await fs.readFile(statusFilePath, 'utf-8');
2883
- const statusData = JSON.parse(statusContent);
2884
-
2885
- // Check for completion signals
2886
- if (statusData.completion === 100 && statusData.status === 'WAITING') {
2887
- console.log(chalk.green(`🎯 Agent signaled completion via status file: COMPLETION: 100%, STATUS:WAITING\n`));
2888
- agentSignaledCompletionViaFile = true;
2889
-
2890
- // Clean up status file
2891
- await fs.remove(statusFilePath);
2892
- }
2893
- }
2894
- } catch (error) {
2895
- // Ignore status file errors - file might not exist or be invalid JSON
2896
- }
2897
-
2898
- if (doneNow > doneBefore) {
2899
- const pctNow = totalNow > 0 ? Math.round((doneNow / totalNow) * 100) : 0;
2900
- safeLog(chalk.green(`✓ Progress detected: ${doneNow}/${totalNow} tasks (${pctNow}%) complete\n`));
2901
-
2902
- // Update progress tracking
2903
- lastProgressTime = currentTime;
2904
- lastProgressDetectionTime = currentTime;
2905
-
2906
- // Check if all tasks are complete - if so, wait for agent completion signal
2907
- if (doneNow >= totalNow && totalNow > 0) {
2908
- safeLog(chalk.green(`🎯 All tasks checked off! Waiting for agent completion signal...\n`));
2909
- // Don't immediately return - wait for STATUS:WAITING signal or timeout
2910
- agentSignaledCompletion = true; // Flag that we're waiting for completion signal
2911
- }
2912
-
2913
- // If progress detected but not all tasks done, continue waiting for agent to finish current work
2914
- return; // Continue polling, don't send new task yet
2915
- }
2916
-
2917
- // Check for immediate completion via status file signal
2918
- if (agentSignaledCompletionViaFile) {
2919
- safeLog(chalk.green(`✅ Agent completed all tasks via status signal! Finishing iteration.\n`));
2920
- cleanup({ success: true, changes: [] });
2921
- return;
2922
- }
2923
-
2924
- // If agent signaled completion (all tasks done) and we've waited a reasonable time, consider it complete
2925
- if (agentSignaledCompletion && (currentTime - lastProgressDetectionTime) >= 30 * 1000) {
2926
- safeLog(chalk.green(`✅ Agent completed all tasks! Finishing iteration.\n`));
2927
- cleanup({ success: true, changes: [] });
2928
- return;
2929
- }
2930
-
2931
- // Implement cooldown period after progress detection to prevent rapid spawning
2932
- if (lastProgressDetectionTime > 0 && (currentTime - lastProgressDetectionTime) < PROGRESS_COOLDOWN_MS) {
2933
- const cooldownRemaining = Math.round((PROGRESS_COOLDOWN_MS - (currentTime - lastProgressDetectionTime)) / 1000);
2934
- safeLog(chalk.gray(`⏱️ In cooldown period (${cooldownRemaining}s remaining) - allowing agent time to complete work\n`));
2935
- return;
2936
- }
2937
-
2938
- // Check for no progress timeout (configurable, default 15 minutes)
2939
- const timeSinceLastProgress = Date.now() - lastProgressTime;
2940
- if (timeSinceLastProgress >= PROGRESS_TIMEOUT_MS) {
2941
- safeLog(chalk.yellow(`⚠️ No progress detected for ${Math.round(timeSinceLastProgress / 60000)} minutes on ${providerConfig.displayName}\n`));
2942
-
2943
- // Check if we've exceeded max IDE attempts
2944
- if (totalAttempts >= MAX_IDE_ATTEMPTS) {
2945
- safeLog(chalk.red(`✗ Maximum IDE attempts (${MAX_IDE_ATTEMPTS}) reached. Stopping auto mode.\n`));
2946
- // Update status card to stopped mode
2947
- if (storedStatusTitle && storedStatus) {
2948
- printStatusCard(storedStatusTitle, storedStatus, 'stopped');
2949
- }
2950
- // Stop auto mode - call async function properly
2951
- stopAutoMode().then(() => {
2952
- cleanup({ success: false, error: 'max_ide_attempts_exceeded', shouldRetry: false });
2953
- }).catch((err) => {
2954
- console.error('Error stopping auto mode:', err);
2955
- cleanup({ success: false, error: 'max_ide_attempts_exceeded', shouldRetry: false });
2956
- });
2957
- return;
2958
- }
2959
-
2960
- // Don't create new instance - just continue monitoring and send continuation messages
2961
- // Reset the progress timer to allow more time for the existing instance
2962
- lastProgressTime = Date.now();
2963
- totalAttempts++;
2964
- console.log(chalk.yellow(`⚠️ Allowing more time for existing Windsurf instance (attempt ${totalAttempts}/${MAX_IDE_ATTEMPTS})\n`));
2965
- return;
2966
- }
2967
-
2968
- // Send continuation prompt every 5 minutes if no progress OR if waiting for completion signal
2969
- const sinceLastContinue = Date.now() - lastContinueSent;
2970
- if (sinceLastContinue >= CONTINUE_INTERVAL_MS) {
2971
- lastContinueSent = Date.now();
2972
- const pctNow = totalNow > 0 ? Math.round((doneNow / totalNow) * 100) : 0;
2973
- const mins = Math.round(elapsed / 60000);
2974
-
2975
- let continueMsg;
2976
- if (agentSignaledCompletion) {
2977
- // All tasks done, waiting for completion signal
2978
- 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`;
2979
- } else {
2980
- // Normal continuation prompt
2981
- 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`;
2982
- }
2983
-
2984
- try {
2985
- const appleScriptManager = new AppleScriptManager();
2986
- // Use plain sendText for continuations (no need to open new thread)
2987
- await appleScriptManager.sendText(continueMsg, ideType);
2988
- try {
2989
- // Check if stdout is still writable before attempting to log
2990
- if (process.stdout && !process.stdout.destroyed) {
2991
- const message = `📤 Sent continuation prompt to ${providerConfig.displayName} (${mins}min elapsed)\n`;
2992
- console.log(chalk.gray(message));
2993
- }
2994
- } catch (logError) {
2995
- // Ignore EPIPE and other stdout errors - process may be terminating
2996
- if (logError.code !== 'EPIPE') {
2997
- // Re-throw non-EPIPE errors
2998
- throw logError;
2999
- }
3000
- }
3001
- } catch (_) { /* ignore continuation errors */ }
3002
- }
3003
- } catch (error) {
3004
- // Handle any errors in the setInterval callback, especially EPIPE
3005
- if (error.code === 'EPIPE') {
3006
- // Silently ignore EPIPE errors - process is terminating
3007
- return;
3008
- }
3009
- // Log other errors but don't crash
3010
- console.error('Error in polling interval:', error.message);
3011
- }
3012
- }, POLL_INTERVAL_MS);
3013
- });
3014
- }
3015
-
3016
- /**
3017
- * Run one iteration of autonomous mode with full workflow
3018
- */
3019
- async function runIteration(requirement, providerConfig, repoPath) {
3020
- const startTime = Date.now();
3021
- const providerManager = sharedProviderManager; // Use shared instance to persist rate limit state
3022
- const llm = new DirectLLMManager(sharedProviderManager); // Pass shared instance
3023
-
3024
- if (providerConfig.type === 'ide') {
3025
- return runIdeFallbackIteration(requirement, providerConfig, repoPath, providerManager, llm, startTime);
3026
- }
3027
-
3028
- // ═══════════════════════════════════════════════════════════
3029
- // PREPARE PHASE - SEARCH AND READ ACTUAL FILES
3030
- // ═══════════════════════════════════════════════════════════
3031
- printStatusCard(requirement.text, 'PREPARE', 'active');
3032
-
3033
- console.log(chalk.bold.white('📋 REQUIREMENT:'));
3034
- console.log(chalk.cyan(` ${requirement.text}\n`));
3035
- console.log(chalk.gray(t('auto.direct.summary.provider')), chalk.white(providerConfig.displayName));
3036
- console.log(chalk.gray(t('auto.repository')), chalk.white(repoPath));
3037
- console.log();
3038
-
3039
- console.log(chalk.cyan(`🔍 ${t('auto.direct.files.searching')}...\n`));
3040
- const relevantFiles = await findRelevantFiles(requirement.text, repoPath);
3041
-
3042
- if (relevantFiles.length > 0) {
3043
- console.log(chalk.white(`${t('auto.direct.files.found', { count: relevantFiles.length })}:`));
3044
- relevantFiles.forEach((file, i) => {
3045
- console.log(chalk.gray(` ${i + 1}. ${file}`));
3046
- });
3047
- console.log();
3048
- }
3049
-
3050
- console.log(chalk.cyan('📖 Reading file context...\n'));
3051
- const fileSnippets = await readFileSnippets(relevantFiles, repoPath, requirement.text);
3052
-
3053
- await new Promise(resolve => setTimeout(resolve, 500));
3054
-
3055
- // ═══════════════════════════════════════════════════════════
3056
- // ACT PHASE - GET STRUCTURED CHANGES FROM LLM
3057
- // ═══════════════════════════════════════════════════════════
3058
- printStatusCard(requirement.text, 'ACT', 'active');
3059
-
3060
- console.log(chalk.cyan(` ${getLogTimestamp()} - 🤖 Asking LLM for implementation...\n`));
3061
- console.log(chalk.gray('─'.repeat(80)));
3062
- console.log(chalk.yellow('💭 LLM Response (streaming):'));
3063
- console.log(chalk.gray('─'.repeat(80)));
3064
-
3065
- // Build context with actual file snippets (use relative paths)
3066
- let contextSection = '';
3067
- if (fileSnippets.length > 0) {
3068
- contextSection = '\n\nCURRENT CODE CONTEXT:\n';
3069
- fileSnippets.forEach(({ file, snippet, startLine }) => {
3070
- // Convert to relative path if absolute
3071
- let displayPath = file;
3072
- if (path.isAbsolute(file)) {
3073
- displayPath = path.relative(repoPath, file);
3074
- }
3075
- contextSection += `\n--- ${displayPath} (around line ${startLine}) ---\n${snippet}\n`;
3076
- });
3077
- }
3078
-
3079
- // Check if requirement involves removing menu items or UI elements
3080
- const isRemovalRequirement = /remove|delete|eliminate/i.test(requirement.text) &&
3081
- (/menu|item|option|setting|button|ui|element/i.test(requirement.text));
3082
-
3083
- let removalInstructions = '';
3084
- if (isRemovalRequirement) {
3085
- removalInstructions = `
3086
-
3087
- ⚠️ CRITICAL: THIS IS A REMOVAL REQUIREMENT ⚠️
3088
-
3089
- When removing menu items, UI elements, or settings, you MUST:
3090
-
3091
- 1. **FIND ALL OCCURRENCES**: Search the CURRENT CODE CONTEXT for:
3092
- - The exact text/identifier mentioned in the requirement
3093
- - Variations (e.g., "Restart CLI", "restart CLI", "restartCLI", "setting:restart-cli")
3094
- - Both display code (where it's shown) AND handler code (where it's processed)
3095
-
3096
- 2. **REMOVE ALL CODE, NOT JUST TOGGLE**:
3097
- - DELETE the code that displays/creates the menu item (e.g., items.push() calls)
3098
- - DELETE the handler code that processes the action (e.g., case 'setting:restart-cli': blocks)
3099
- - DELETE any configuration/state code related to the item (e.g., const restartCLI = ...)
3100
- - DELETE any variable declarations or references to the item
3101
-
3102
- 3. **MULTIPLE FILES MAY BE AFFECTED**:
3103
- - If you find references in multiple files, you MUST provide multiple FILE/SEARCH/REPLACE blocks
3104
- - Each file needs its own FILE/SEARCH/REPLACE block
3105
- - Remove ALL occurrences across ALL files shown in the context
3106
-
3107
- 4. **VERIFICATION**:
3108
- - After removal, the code should compile/run without errors
3109
- - The removed item should not appear anywhere in the codebase
3110
- - All related handlers and configuration should be removed
3111
-
3112
- EXAMPLE for removing "Restart CLI after each completed requirement":
3113
- - Remove: items.push({ type: 'setting', name: \`...Restart CLI...\`, value: 'setting:restart-cli' })
3114
- - Remove: case 'setting:restart-cli': { ... } handler block
3115
- - Remove: const restartCLI = ... variable declaration
3116
- - Remove: Any other references to restartCLI or 'setting:restart-cli'
3117
-
3118
- `;
3119
- }
3120
-
3121
- const prompt = `You are implementing a code change. Your task is to provide a SEARCH/REPLACE block that will modify the code.
3122
-
3123
- REQUIREMENT TO IMPLEMENT:
3124
- ${requirement.text}
3125
- ${removalInstructions}
3126
- ${contextSection}
3127
-
3128
- YOUR TASK:
3129
- 1. Read the CURRENT CODE CONTEXT carefully
3130
- 2. Find the EXACT location where changes are needed
3131
- 3. COPY AT LEAST 10 LINES EXACTLY as they appear (including indentation, spacing, comments)
3132
- 4. Show what the code should look like after your changes
3133
- ${isRemovalRequirement ? '5. **REMOVE ALL CODE** related to the item - do NOT comment it out, DELETE it completely' : ''}
3134
-
3135
- OUTPUT FORMAT:
3136
-
3137
- For modifying existing files:
3138
- FILE: <exact path from the "---" line - must be relative to repo root>
3139
- SEARCH: \`\`\`javascript
3140
- <COPY 10+ lines EXACTLY - preserve indentation, spacing, comments>
3141
- \`\`\`
3142
- REPLACE: \`\`\`javascript
3143
- <SAME lines but with necessary modifications>
3144
- \`\`\`
3145
-
3146
- For creating new files:
3147
- CREATE: <relative path to new file>
3148
- CONTENT: \`\`\`javascript
3149
- <full content of new file>
3150
- \`\`\`
3151
-
3152
- CRITICAL RULES - READ CAREFULLY:
3153
- 1. If the file does NOT exist, use CREATE: format to create it
3154
- 2. If the file DOES exist and you need to modify it, use FILE:/SEARCH:/REPLACE: format
3155
- 3. For FILE: blocks, SEARCH block must be COPIED CHARACTER-BY-CHARACTER from CURRENT CODE CONTEXT above
3156
- 4. Use the EXACT code as it appears NOW - do NOT use old/outdated code from memory
3157
- 5. Include ALL property values EXACTLY as shown (e.g., if context shows 'type: "info"', use 'type: "info"', NOT 'type: "setting"')
3158
- 6. Include indentation EXACTLY as shown (count the spaces!)
3159
- 7. For modifications, include AT LEAST 20-30 LINES of context:
3160
- - Start 10-15 lines BEFORE the code you need to change
3161
- - End 10-15 lines AFTER the code you need to change
3162
- - MORE context is BETTER than less - include extra lines if unsure
3163
- 8. Do NOT paraphrase or rewrite - COPY EXACTLY from CURRENT CODE CONTEXT
3164
- 9. FILE path must match exactly what's after "---" in CURRENT CODE CONTEXT (DO NOT include the "---" itself)
3165
- 10. All paths must be RELATIVE to repo root (e.g., "specs/005-beta-pricing/tasks.md" NOT "/Users/.../tasks.md")
3166
- 11. Output ONLY the FILE/SEARCH/REPLACE or CREATE/CONTENT blocks
3167
- 12. NO explanations, NO markdown outside the blocks, NO additional text
3168
- 13. If you cannot find the exact code to modify, output: ERROR: CANNOT LOCATE CODE IN CONTEXT
3169
- 14. IMPORTANT: Include ALL intermediate code between the before/after context - do NOT skip lines
3170
- 15. DOUBLE-CHECK: Compare your SEARCH block line-by-line with CURRENT CODE CONTEXT to ensure they match
3171
-
3172
- EXAMPLE (notice EXACT copying of indentation and spacing):
3173
-
3174
- FILE: packages/cli/src/utils/interactive.js
3175
- SEARCH: \`\`\`javascript
3176
- // Add warning if no TODO requirements
3177
- if (counts.todoCount === 0) {
3178
- requirementsText += \` \${chalk.red('⚠️ No requirements to work on')}\`;
3179
- }
3180
- } else {
3181
- requirementsText += \`\${chalk.yellow('0 TODO')}, \${chalk.cyan('0 TO VERIFY')}, \${chalk.green('0 VERIFIED')} \${chalk.red('⚠️ No requirements')}\`;
3182
- }
3183
- \`\`\`
3184
- REPLACE: \`\`\`javascript
3185
- // Add warning if no TODO requirements
3186
- if (counts.todoCount === 0) {
3187
- requirementsText += \` \${chalk.red('⚠️ No requirements')}\`;
3188
- }
3189
- } else {
3190
- requirementsText += \`\${chalk.yellow('0 TODO')}, \${chalk.cyan('0 TO VERIFY')}, \${chalk.green('0 VERIFIED')} \${chalk.red('⚠️ No requirements')}\`;
3191
- }
3192
- \`\`\`
3193
-
3194
- CREATE EXAMPLE (for new files):
3195
-
3196
- CREATE: packages/web/src/utils/paypal-config.js
3197
- CONTENT: \`\`\`javascript
3198
- /**
3199
- * PayPal Configuration Utility
3200
- */
3201
-
3202
- export const getPayPalClientId = () => {
3203
- return import.meta.env.VITE_PAYPAL_CLIENT_ID;
3204
- };
3205
-
3206
- export const getPayPalEnvironment = () => {
3207
- return import.meta.env.VITE_PAYPAL_ENVIRONMENT || 'sandbox';
3208
- };
3209
- \`\`\`
3210
-
3211
- Now implement the requirement. Remember: For modifications, COPY THE SEARCH BLOCK EXACTLY! For new files, use CREATE: format.`;
3212
-
3213
- let fullResponse = '';
3214
- let chunkCount = 0;
3215
- let totalChars = 0;
3216
-
3217
- // Show spinner while waiting for first chunk
3218
- const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
3219
- let spinnerIndex = 0;
3220
- let spinnerInterval = null;
3221
- let receivedFirstChunk = false;
3222
-
3223
- const startSpinner = () => {
3224
- process.stdout.write(chalk.cyan('⏳ Waiting for response'));
3225
- spinnerInterval = setInterval(() => {
3226
- process.stdout.write(`\r${chalk.cyan('⏳ Waiting for response')} ${chalk.yellow(spinnerFrames[spinnerIndex])}`);
3227
- spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
3228
- }, 100);
3229
- };
3230
-
3231
- const stopSpinner = () => {
3232
- if (spinnerInterval) {
3233
- clearInterval(spinnerInterval);
3234
- spinnerInterval = null;
3235
- process.stdout.write('\r\x1b[K'); // Clear the line
3236
- }
3237
- };
3238
-
3239
- startSpinner();
3240
-
3241
- const result = await llm.call(providerConfig, prompt, {
3242
- temperature: 0.1,
3243
- maxTokens: 4096,
3244
- onChunk: (chunk) => {
3245
- chunkCount++;
3246
- totalChars += chunk.length;
3247
- // Show first chunk arrival to confirm streaming started
3248
- if (chunkCount === 1) {
3249
- stopSpinner();
3250
- receivedFirstChunk = true;
3251
- process.stdout.write(chalk.green('✓ Streaming started...\n'));
3252
- }
3253
- // Use white text for better visibility instead of gray
3254
- process.stdout.write(chalk.white(chunk));
3255
- fullResponse += chunk;
3256
- },
3257
- onComplete: () => {
3258
- stopSpinner();
3259
- if (chunkCount > 0) {
3260
- console.log(chalk.green(`\n✓ Received ${totalChars} characters in ${chunkCount} chunks`));
3261
- } else {
3262
- console.log(chalk.yellow('\n⚠️ No streaming response received (using complete response)'));
3263
- }
3264
- console.log(chalk.gray('─'.repeat(80)));
3265
- console.log();
3266
- },
3267
- onError: (error) => {
3268
- stopSpinner();
3269
- console.error(chalk.red(`\n✗ Error: ${error}`));
3270
- }
3271
- });
3272
-
3273
- // Ensure spinner is stopped even if callbacks didn't fire
3274
- stopSpinner();
3275
-
3276
- if (!result.success) {
3277
- const combinedError = [result.error, fullResponse].filter(Boolean).join('\n').trim() || 'Unknown error';
3278
- console.log(chalk.red(`\n✗ LLM call failed: ${combinedError}`));
3279
- // CRITICAL: Mark provider as rate-limited for ANY error so acquireProviderConfig() will skip it
3280
- providerManager.markRateLimited(providerConfig.provider, providerConfig.model, combinedError);
3281
-
3282
- // Track health metrics for failed direct providers
3283
- const providerType = providerConfig.provider;
3284
- const duration = Date.now() - startTime;
3285
- try {
3286
- // Detect quota/rate limit errors vs other failures
3287
- const quotaPattern = /(quota|rate.?limit|usage.?limit|spending.?cap|allowance|exceeded|overloaded)/i;
3288
- const isQuotaError = quotaPattern.test(combinedError);
3289
-
3290
- if (isQuotaError) {
3291
- await sharedHealthTracker.recordQuota(providerType, combinedError, {
3292
- requirementId: requirement.id || requirement.text.substring(0, 50),
3293
- });
3294
- } else {
3295
- await sharedHealthTracker.recordFailure(providerType, combinedError, {
3296
- timeoutUsed: duration,
3297
- requirementId: requirement.id || requirement.text.substring(0, 50),
3298
- });
3299
- }
3300
- } catch (healthError) {
3301
- // Don't fail the iteration if health tracking fails
3302
- console.log(chalk.gray(`⚠️ Health tracking error: ${healthError.message}`));
3303
- }
3304
-
3305
- return { success: false, error: combinedError };
3306
- }
3307
-
3308
- // ═══════════════════════════════════════════════════════════
3309
- // CLEAN UP PHASE - APPLY CHANGES TO ACTUAL FILES
3310
- // ═══════════════════════════════════════════════════════════
3311
- printStatusCard(requirement.text, 'CLEAN UP', 'active');
3312
-
3313
- console.log(chalk.cyan('🧹 Parsing and applying changes...\n'));
3314
-
3315
- // Check if LLM said it cannot locate code
3316
- if (fullResponse.includes('ERROR: CANNOT LOCATE CODE') || fullResponse.includes('CANNOT LOCATE CODE')) {
3317
- console.log(chalk.red('\n✗ LLM could not locate the code to modify'));
3318
- console.log(chalk.yellow('The code context provided may not contain the relevant section\n'));
3319
- return { success: false, error: 'LLM could not locate code in context', changes: [] };
3320
- }
3321
-
3322
- const changes = parseSearchReplaceBlocks(fullResponse);
3323
-
3324
- if (changes.length === 0) {
3325
- if (fullResponse.includes('NO CHANGES NEEDED')) {
3326
- console.log(chalk.yellow('⚠️ LLM determined no code changes needed\n'));
3327
- return { success: false, error: 'No changes needed', changes: [] };
3328
- } else {
3329
- console.log(chalk.yellow('⚠️ Could not parse any search/replace blocks from LLM response\n'));
3330
- console.log(chalk.gray('This might be a documentation-only requirement or LLM formatting issue\n'));
3331
- return { success: false, error: 'No search/replace blocks found', changes: [] };
3332
- }
3333
- } else {
3334
- console.log(chalk.white(`Applying ${changes.length} change(s):\n`));
3335
-
3336
- let appliedCount = 0;
3337
- let failedCount = 0;
3338
-
3339
- for (let i = 0; i < changes.length; i++) {
3340
- const change = changes[i];
3341
- console.log(chalk.cyan(` ${i + 1}. ${change.file}...`));
3342
-
3343
- const applyResult = await applyFileChange(change, repoPath);
3344
-
3345
- if (applyResult.success) {
3346
- const methodInfo = applyResult.method === 'fuzzy'
3347
- ? ` (fuzzy match at line ${applyResult.matchedAt})`
3348
- : '';
3349
- console.log(chalk.green(` ✓ Applied successfully${methodInfo}`));
3350
- appliedCount++;
3351
- } else {
3352
- console.log(chalk.red(` ✗ Failed: ${applyResult.error}`));
3353
- failedCount++;
3354
- }
3355
- }
3356
-
3357
- console.log();
3358
- console.log(chalk.white(`Applied: ${chalk.green(appliedCount)}, Failed: ${chalk.red(failedCount)}`));
3359
- console.log();
3360
-
3361
- // CRITICAL: Fail if no changes were applied successfully
3362
- if (appliedCount === 0 && failedCount > 0) {
3363
- console.log(chalk.bold.red('\n❌ ITERATION FAILED\n'));
3364
- console.log(chalk.red('No changes were successfully applied'));
3365
- console.log(chalk.yellow('Common causes:'));
3366
- console.log(chalk.gray(' - LLM provided incorrect search text'));
3367
- console.log(chalk.gray(' - Code has changed since context was provided'));
3368
- console.log(chalk.gray(' - File path is incorrect'));
3369
- console.log();
3370
- console.log(chalk.cyan('💡 Tip: Check the search block matches the actual code in the file'));
3371
- console.log();
3372
- return { success: false, error: 'No changes applied', changes: [] };
3373
- }
3374
- }
3375
-
3376
- await new Promise(resolve => setTimeout(resolve, 500));
3377
-
3378
- // ═══════════════════════════════════════════════════════════
3379
- // VERIFY PHASE - CONFIRM CHANGES WERE APPLIED
3380
- // ═══════════════════════════════════════════════════════════
3381
- printStatusCard(requirement.text, 'VERIFY', 'active');
3382
-
3383
- console.log(chalk.cyan('✓ Verifying changes...\n'));
3384
-
3385
- if (changes.length > 0) {
3386
- console.log(chalk.white('Modified files:'));
3387
- for (const change of changes) {
3388
- const fullPath = path.join(repoPath, change.file);
3389
- if (await fs.pathExists(fullPath)) {
3390
- const content = await fs.readFile(fullPath, 'utf8');
3391
-
3392
- // Handle both create and modify types
3393
- let hasChange = false;
3394
- if (change.type === 'create') {
3395
- hasChange = change.content && content.includes(change.content.trim());
3396
- } else {
3397
- hasChange = change.replace && content.includes(change.replace.trim());
3398
- }
3399
-
3400
- if (hasChange) {
3401
- console.log(chalk.green(` ✓ ${change.file}`));
3402
- } else {
3403
- console.log(chalk.yellow(` ⚠️ ${change.file} (change may not have applied)`));
3404
- }
3405
- }
3406
- }
3407
- console.log();
3408
- }
3409
-
3410
- await new Promise(resolve => setTimeout(resolve, 500));
3411
-
3412
- // ═══════════════════════════════════════════════════════════
3413
- // DONE PHASE
3414
- // ═══════════════════════════════════════════════════════════
3415
- printStatusCard(requirement.text, 'DONE', 'active');
3416
-
3417
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
3418
-
3419
- console.log(chalk.bold.green(`✅ ${t('auto.direct.requirement.completed')}\n`));
3420
- console.log(chalk.white(`${t('auto.direct.requirement.title')}`), chalk.cyan(requirement.text));
3421
- console.log(chalk.white(t('auto.direct.summary.files.modified')), chalk.cyan(changes.length));
3422
- console.log(chalk.white(t('auto.direct.summary.status')), chalk.green(t('auto.direct.summary.moving.to.verify')));
3423
- console.log(chalk.white(t('auto.direct.summary.time')), chalk.gray(`${elapsed}s`));
3424
- console.log();
3425
-
3426
- // Move requirement to TO VERIFY section
3427
- const moved = await moveRequirementToVerify(repoPath, requirement.text);
3428
- if (moved) {
3429
- console.log(chalk.green(`✓ ${t('auto.direct.status.verification.pending')}`));
3430
- } else {
3431
- console.log(chalk.yellow('⚠️ Could not automatically move requirement'));
3432
- }
3433
-
3434
- // Record performance metric
3435
- const duration = Date.now() - startTime;
3436
- providerManager.recordPerformance(providerConfig.provider, providerConfig.model, duration);
3437
-
3438
- // Track health metrics for direct providers (IDE providers tracked in runIdeFallbackIteration)
3439
- const providerType = providerConfig.provider;
3440
- try {
3441
- await sharedHealthTracker.recordSuccess(providerType, duration, {
3442
- requirementId: requirement.id || requirement.text.substring(0, 50),
3443
- continuationPromptsDetected: 0,
3444
- });
3445
- } catch (healthError) {
3446
- // Don't fail the iteration if health tracking fails
3447
- console.log(chalk.gray(`⚠️ Health tracking error: ${healthError.message}`));
3448
- }
3449
-
3450
- console.log();
3451
- console.log(chalk.gray('─'.repeat(80)));
3452
- console.log();
47
+ // Import modular functions from auto-direct/ subdirectory
48
+ const {
49
+ getTimestamp,
50
+ getLogTimestamp,
51
+ translateStage,
52
+ stripAnsi,
53
+ getVisualWidth,
54
+ padToVisualWidth,
55
+ isRateLimitMessage,
56
+ sleep,
57
+ safeLog
58
+ } = require('./auto-direct/utils');
3453
59
 
3454
- return { success: true, changes };
3455
- }
60
+ const {
61
+ updateRequirementsStatus,
62
+ getCurrentRequirement,
63
+ countTodoRequirements,
64
+ moveRequirementToVerify,
65
+ moveRequirementToRecycle
66
+ } = require('./auto-direct/requirement-manager');
3456
67
 
3457
- // ─── Spec mode helpers ────────────────────────────────────────────────────────
68
+ const {
69
+ parseSearchReplaceBlocks,
70
+ normalizeWhitespace,
71
+ extractIdentifiers,
72
+ extractPattern,
73
+ applyFileChange
74
+ } = require('./auto-direct/code-processor');
3458
75
 
3459
- /**
3460
- * Count done/total checkboxes in a spec's tasks.md.
3461
- */
3462
- function countSpecCheckboxes(specPath) {
3463
- try {
3464
- const tasksFile = path.join(specPath, 'tasks.md');
3465
- if (!fs.existsSync(tasksFile)) return { done: 0, total: 0 };
3466
- const content = fs.readFileSync(tasksFile, 'utf8');
3467
- const totalMatches = content.match(/^- \[[ x]\]/gmi) || [];
3468
- const doneMatches = content.match(/^- \[x\]/gmi) || [];
3469
- return { done: doneMatches.length, total: totalMatches.length };
3470
- } catch (_) {
3471
- return { done: 0, total: 0 };
3472
- }
3473
- }
76
+ const {
77
+ findRelevantFiles,
78
+ readFileSnippets
79
+ } = require('./auto-direct/file-scanner');
3474
80
 
3475
- /**
3476
- * Get the next unchecked task line from tasks.md.
3477
- * Returns { text, line } or null when all done.
3478
- */
3479
- function getNextSpecTask(specPath) {
3480
- try {
3481
- const tasksFile = path.join(specPath, 'tasks.md');
3482
- if (!fs.existsSync(tasksFile)) return null;
3483
- const content = fs.readFileSync(tasksFile, 'utf8');
3484
- for (const line of content.split('\n')) {
3485
- if (/^- \[ \]/.test(line)) {
3486
- return { text: line.replace(/^- \[ \]\s*/, '').trim(), line: line.trim() };
3487
- }
3488
- }
3489
- return null;
3490
- } catch (_) {
3491
- return null;
3492
- }
3493
- }
81
+ const {
82
+ printStatusCard
83
+ } = require('./auto-direct/status-display');
3494
84
 
3495
- /**
3496
- * Load all enabled specs that have incomplete tasks (or no tasks.md yet).
3497
- */
3498
- async function loadEnabledIncompleteSpecs(repoPath) {
3499
- try {
3500
- const { getAllSpecifications } = require('vibecodingmachine-core');
3501
- const specs = await getAllSpecifications(repoPath, { skipDisabled: true });
3502
- return specs.filter(spec => {
3503
- if (!spec.hasTasks) return true; // No tasks.md = needs planning + implementation
3504
- const counts = countSpecCheckboxes(spec.path);
3505
- return counts.total === 0 || counts.done < counts.total;
3506
- });
3507
- } catch (err) {
3508
- console.log(chalk.yellow(`⚠️ Error loading specs: ${err.message}`));
3509
- return [];
3510
- }
3511
- }
85
+ // Import extracted iteration handlers
86
+ const {
87
+ ensureClineInstalled,
88
+ runIdeProviderIteration,
89
+ waitForIdeCompletion,
90
+ runIdeFallbackIteration,
91
+ runSpecIdeIteration,
92
+ runIteration,
93
+ initialize: initializeIterationHandlers
94
+ } = require('./auto-direct/iteration-handlers');
95
+
96
+ // Import spec handlers
97
+ const {
98
+ countSpecCheckboxes,
99
+ getNextSpecTask,
100
+ loadEnabledIncompleteSpecs,
101
+ processDefaultRequirement
102
+ } = require('./auto-direct/spec-handlers');
3512
103
 
3513
- // ─────────────────────────────────────────────────────────────────────────────
104
+ // Import spec management for auto-creation
105
+ const { countTodoSpecs, checkAndCreateSpecs } = require('vibecodingmachine-core');
3514
106
 
3515
- /**
3516
- * Process default requirement until completion
3517
- * @param {RequirementSequencer} sequencer - Requirement sequencer
3518
- * @param {DefaultRequirementManager} defaultManager - Default requirement manager
3519
- * @param {Object} providerConfig - Provider configuration
3520
- * @param {string} repoPath - Repository path
3521
- */
3522
- async function processDefaultRequirement(sequencer, defaultManager, providerConfig, repoPath) {
3523
- const requirementsPath = await getRequirementsPath(repoPath);
3524
- let iterationCount = 0;
107
+ // Import provider functions
108
+ const {
109
+ getAllAvailableProviders,
110
+ handleRateLimitWithAgentSwitching
111
+ } = require('./auto-direct/provider-manager');
3525
112
 
3526
- while (true) {
3527
- // Check if default requirement should stop
3528
- const shouldStop = await defaultManager.shouldStop();
3529
- if (shouldStop) {
3530
- const status = await defaultManager.getStatus();
3531
- console.log(chalk.bold.green(`\n✅ Default Requirement Complete`));
3532
- console.log(chalk.gray(`Status: ${status.completionReason || 'Completed'}\n`));
3533
- break;
3534
- }
113
+ const {
114
+ getProviderConfig,
115
+ acquireProviderConfig
116
+ } = require('./auto-direct/provider-config');
3535
117
 
3536
- // Check if regular requirements were added (should pause default)
3537
- const currentState = await sequencer.getCurrentState(requirementsPath);
3538
- if (currentState.hasRegularRequirements) {
3539
- console.log(chalk.bold.yellow('\n⏸️ Default Requirement Paused'));
3540
- console.log(chalk.gray('New regular requirements detected, pausing default requirement...\n'));
3541
- await defaultManager.pause();
3542
- break;
3543
- }
118
+ // Import phase handlers
119
+ const {
120
+ initializeAutoMode,
121
+ processSpecsLoop,
122
+ processRequirementsLoop
123
+ } = require('./auto-direct/auto-start-phases');
3544
124
 
3545
- // Get current default requirement status
3546
- const defaultStatus = await defaultManager.getStatus();
3547
- iterationCount = defaultStatus.iterationCount + 1;
125
+ // Global keyboard handler reference for cleanup
126
+ let keyboardHandler = null;
3548
127
 
3549
- console.log(chalk.bold.cyan(`\n${'━'.repeat(80)}`));
3550
- console.log(chalk.bold.cyan(` ⭐ DEFAULT REQUIREMENT - Iteration ${iterationCount}/${defaultStatus.maxIterations}`));
3551
- console.log(chalk.bold.cyan(` ${defaultStatus.title}`));
3552
- console.log(chalk.bold.cyan(`${'━'.repeat(80)}\n`));
128
+ // Status management will use in-process tracking instead of external file
129
+ const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
130
+ // CRITICAL: Shared ProviderManager instance to track rate limits across all function calls
131
+ const sharedProviderManager = new ProviderManager();
3553
132
 
3554
- // Create a synthetic requirement object for the default requirement
3555
- const defaultRequirement = {
3556
- text: defaultStatus.description,
3557
- number: iterationCount,
3558
- isDefault: true
3559
- };
133
+ // CRITICAL: Shared IDEHealthTracker instance to track IDE reliability across all iterations
134
+ const sharedHealthTracker = new IDEHealthTracker();
3560
135
 
3561
- // Run iteration with default requirement
3562
- const result = await runIteration(defaultRequirement, providerConfig, repoPath);
136
+ // Listen for consecutive failures to warn user
137
+ sharedHealthTracker.on('consecutive-failures', ({ ideId, count, lastError }) => {
138
+ console.log(chalk.yellow(`\n⚠️ WARNING: ${ideId} has failed ${count} times consecutively`));
139
+ console.log(chalk.yellow(` Last error: ${lastError}`));
140
+ console.log(chalk.yellow(` Consider switching to a different IDE\n`));
141
+ });
3563
142
 
3564
- if (result.success) {
3565
- // Increment iteration count
3566
- await defaultManager.incrementIteration();
3567
- console.log(chalk.bold.green(`✅ Default Requirement Iteration ${iterationCount}/${defaultStatus.maxIterations} COMPLETE`));
143
+ // Initialize iteration handlers with shared instances
144
+ initializeIterationHandlers({
145
+ providerManager: sharedProviderManager,
146
+ healthTracker: sharedHealthTracker,
147
+ cliEntryPoint: CLI_ENTRY_POINT,
148
+ statusVars: { storedStatusTitle: '', storedStatus: '' } // Will be updated by reference
149
+ });
3568
150
 
3569
- // Small delay between iterations
3570
- await new Promise(resolve => setTimeout(resolve, 3000));
3571
- } else {
3572
- // Record failure
3573
- await defaultManager.recordFailure();
3574
- console.log(chalk.bold.red(`❌ Default Requirement Iteration ${iterationCount}/${defaultStatus.maxIterations} FAILED`));
3575
- console.log(chalk.red(`Error: ${result.error}\n`));
3576
151
 
3577
- // Check consecutive failures
3578
- const updatedStatus = await defaultManager.getStatus();
3579
- if (updatedStatus.consecutiveFailures >= 5) {
3580
- console.log(chalk.bold.yellow('\n⚠️ Maximum consecutive failures reached'));
3581
- await defaultManager.markDone('Stopped due to consecutive failures');
3582
- break;
3583
- }
152
+ // Configured stages (will be loaded in handleAutoStart)
153
+ let configuredStages = DEFAULT_STAGES;
3584
154
 
3585
- // Delay longer after failure
3586
- await new Promise(resolve => setTimeout(resolve, 5000));
3587
- }
3588
- }
3589
- }
155
+ // Persistent status box state
156
+ let statusBoxInitialized = false;
157
+ let statusBoxLines = 5; // Number of lines the status box takes
158
+ let storedStatusTitle = '';
159
+ let storedStatus = '';
160
+ let currentStatusMode = 'active'; // Track current mode: 'active', 'waiting', 'stopped'
3590
161
 
3591
162
  /**
3592
163
  * Main auto mode command handler
164
+ * Delegates to phase handlers for constitutional compliance
3593
165
  */
3594
166
  async function handleAutoStart(options) {
3595
- let stopMonitoring = null; // Declare outside try block so it's accessible in catch
3596
-
3597
- try {
3598
- // STRICT AUTH CHECK
3599
- const auth = require('../utils/auth');
3600
- const isAuth = await auth.isAuthenticated();
3601
- if (!isAuth) {
3602
- console.log(chalk.cyan('\n🔐 Opening browser for authentication...\n'));
3603
- try {
3604
- await auth.login();
3605
- console.log(chalk.green('\n✓ Authentication successful!\n'));
3606
- } catch (error) {
3607
- console.log(chalk.red('\n✗ Authentication failed:'), error.message);
3608
- process.exit(1);
3609
- }
3610
- }
3611
-
3612
- console.log(chalk.bold.cyan('\n' + t('auto.direct.title') + '\n'));
3613
- console.log(chalk.gray('═'.repeat(80)));
3614
- console.log();
3615
-
3616
- const repoPath = await getEffectiveRepoPath();
3617
- if (!repoPath) {
3618
- console.log(chalk.red('✗ No repository configured'));
3619
- console.log(chalk.gray(t('auto.direct.config.repo.not.set')));
3620
- return;
3621
- }
3622
-
3623
- // Create keyboard handler for 'x' key exit
3624
- keyboardHandler = createKeyboardHandler({
3625
- onExit: () => {
3626
- // Update status to stopped
3627
- updateAutoModeStatus(repoPath, { running: false });
3628
- console.log(chalk.gray('Auto mode stopped'));
3629
- // Update status card to stopped mode
3630
- if (storedStatusTitle && storedStatus) {
3631
- printStatusCard(storedStatusTitle, storedStatus, 'stopped');
3632
- }
3633
- }
3634
- });
3635
-
3636
- // Start keyboard handler
3637
- keyboardHandler.start();
3638
-
3639
- // Ensure keyboard handler is cleaned up on exit
3640
- const cleanup = () => {
3641
- if (keyboardHandler) {
3642
- keyboardHandler.stop();
3643
- }
3644
- };
3645
-
3646
- // Handle process termination
3647
- process.on('exit', cleanup);
3648
- process.on('SIGINT', () => {
3649
- cleanup();
3650
- process.exit(0);
3651
- });
3652
- process.on('SIGTERM', () => {
3653
- cleanup();
3654
- process.exit(0);
3655
- });
3656
-
3657
- // Start Auto Mode status tracking
3658
- const config = await getAutoConfig();
3659
-
3660
- // Save extension to config if provided
3661
- if (options.extension) {
3662
- config.extension = options.extension;
3663
- await setAutoConfig(config);
3664
- }
3665
-
3666
- await startAutoMode(repoPath, { ide: options.ide || config.ide });
3667
-
3668
- // Also load configured stages here since we already have the config
3669
- configuredStages = await getStages();
3670
-
3671
- // Initialize requirement monitoring for default requirement management
3672
- const requirementsPath = await getRequirementsPath(repoPath);
3673
- const storage = new JSONStorage();
3674
- const parser = new RequirementFileParser();
3675
- const defaultManager = new DefaultRequirementManager(storage);
3676
- const sequencer = new RequirementSequencer(parser, defaultManager);
3677
-
3678
- // Start monitoring requirements file for changes
3679
- stopMonitoring = sequencer.monitorRequirements(requirementsPath, async (changeType, change) => {
3680
- if (changeType === 'regular-requirements-added') {
3681
- console.log(chalk.bold.yellow('\n📝 New regular requirements detected'));
3682
- console.log(chalk.gray('Default requirement will be paused to process regular requirements first\n'));
3683
- } else if (changeType === 'regular-requirements-completed') {
3684
- console.log(chalk.bold.green('\n✅ All regular requirements completed'));
3685
- console.log(chalk.gray('Default requirement will be resumed\n'));
3686
- }
3687
- });
3688
-
3689
- console.log(chalk.white(t('auto.repository')), chalk.cyan(repoPath));
3690
-
3691
- // Use the agent that was already determined by provider preferences in interactive.js
3692
- // No need to call getEffectiveAgent since we already have the correct agent
3693
- const effectiveAgent = options.ide;
3694
-
3695
- // Get provider configuration — options.provider forces a specific provider
3696
- let providerConfig = await acquireProviderConfig(null, null, options.provider || null);
3697
- if (!providerConfig) {
3698
- return;
3699
- }
3700
-
3701
- console.log(chalk.white('Provider:'), chalk.cyan(providerConfig.displayName));
167
+ // Shared references for keyboard handler and status
168
+ const sharedRefs = {
169
+ keyboardHandler: null,
170
+ storedStatusTitle: storedStatusTitle,
171
+ storedStatus: storedStatus
172
+ };
3702
173
 
3703
- // Get max chats (use already loaded config)
3704
- const unlimited = !options.maxChats && !config.maxChats && config.neverStop;
3705
- const maxChats = unlimited ? Number.MAX_SAFE_INTEGER : (options.maxChats || config.maxChats || 1);
3706
- console.log(chalk.white(`${t('auto.direct.config.max.iterations')}`), unlimited ? chalk.cyan('∞ (never stop)') : chalk.cyan(maxChats));
174
+ let state = null;
3707
175
 
3708
- // Update initial status
3709
- await updateAutoModeStatus(repoPath, { chatCount: 0, maxChats: unlimited ? 0 : maxChats });
3710
- console.log();
3711
- console.log(chalk.gray('═'.repeat(80)));
176
+ try {
177
+ // Phase 1: Initialize auto mode
178
+ state = await initializeAutoMode(options, sharedRefs);
3712
179
 
3713
- // Calculate initial total for consistent display throughout the session
3714
- const initialTodoCount = await countTodoRequirements(repoPath);
3715
- const initialEffectiveMax = unlimited ? initialTodoCount : Math.min(maxChats, initialTodoCount);
180
+ // Update global keyboard handler reference for cleanup
181
+ keyboardHandler = sharedRefs.keyboardHandler;
3716
182
 
3717
- // Main loop counters (shared across spec + requirements phases)
3718
183
  let completedCount = 0;
3719
184
  let failedCount = 0;
3720
185
 
3721
- // ── Phase 1: Process incomplete enabled specs ─────────────────────────────
3722
- const incompleteSpecs = await loadEnabledIncompleteSpecs(repoPath);
3723
- if (incompleteSpecs.length > 0) {
3724
- console.log(chalk.bold.cyan(`\n📋 Processing ${incompleteSpecs.length} incomplete spec(s) before requirements...\n`));
3725
-
3726
- for (const spec of incompleteSpecs) {
3727
- // Initialize IDE attempts tracking for this spec
3728
- spec.totalIdeAttempts = 0;
3729
-
3730
- const { done: doneStart, total: totalStart } = countSpecCheckboxes(spec.path);
3731
- console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
3732
- console.log(chalk.bold.magenta(` 📋 SPEC: ${spec.directory} (${spec.title || spec.directory})`));
3733
- if (totalStart > 0) {
3734
- const pctStart = Math.round((doneStart / totalStart) * 100);
3735
- console.log(chalk.bold.magenta(` Progress: ${doneStart}/${totalStart} tasks (${pctStart}%) complete`));
3736
- } else {
3737
- console.log(chalk.bold.magenta(' No tasks.md yet — will plan and implement'));
3738
- }
3739
- console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
3740
-
3741
- let specProviderAttempts = 0;
3742
- const MAX_SPEC_TASK_ATTEMPTS = 3;
3743
- let lastSpecTaskText = null;
3744
- let sameTaskAttempts = 0;
3745
-
3746
- // If spec has no tasks.md yet, add a special planning task
3747
- if (!spec.hasTasks) {
3748
- console.log(chalk.cyan('📝 No tasks.md yet — planning spec first...\n'));
3749
- const planningText = [
3750
- `Plan and create tasks.md for spec "${spec.directory}".`,
3751
- `Read ${spec.path}/spec.md${spec.hasPlanPrompt ? ` and ${spec.path}/plan-prompt.md` : ''}.`,
3752
- `Then create ${spec.path}/tasks.md with implementation tasks as checkboxes (- [ ] task).`
3753
- ].join('\n');
3754
-
3755
- let planResult;
3756
- if (providerConfig.type === 'ide') {
3757
- // Send planning instruction via AppleScript, poll for tasks.md creation
3758
- console.log(chalk.cyan(`📤 Sending planning task to ${providerConfig.displayName}...\n`));
3759
- const ideTypeForPlan = providerConfig.provider || providerConfig.ide;
3760
- // For Windsurf: single combined synchronous AppleScript to avoid timing race
3761
- const sendPlanToIde = async () => {
3762
- if (ideTypeForPlan === 'windsurf') {
3763
- const { execSync: _execSync2 } = require('child_process');
3764
- const { writeFileSync: _writeFileSync2, unlinkSync: _unlinkSync2 } = require('fs');
3765
- const { tmpdir: _tmpdir2 } = require('os');
3766
- const _ts2 = Date.now();
3767
- const _tmpText2 = path.join(_tmpdir2(), `plan_instr_${_ts2}.txt`);
3768
- const _tmpScpt2 = path.join(_tmpdir2(), `send_cascade_plan_${_ts2}.scpt`);
3769
- _writeFileSync2(_tmpText2, planningText, 'utf8');
3770
- const _combinedPlanScript = `
3771
- set planText to (do shell script "cat " & quoted form of "${_tmpText2}")
3772
- tell application "Windsurf"
3773
- activate
3774
- delay 1.5
3775
- end tell
3776
- set maxTries to 3
3777
- set tries to 0
3778
- repeat
3779
- set tries to tries + 1
3780
- tell application "System Events"
3781
- set frontBundleID to bundle identifier of first process whose frontmost is true
3782
- end tell
3783
- if frontBundleID is "com.exafunction.windsurf" then exit repeat
3784
- if tries >= maxTries then
3785
- error "Windsurf did not become frontmost (frontmost bundle ID: " & frontBundleID & ")"
3786
- end if
3787
- tell application "Windsurf"
3788
- activate
3789
- end tell
3790
- delay 1.0
3791
- end repeat
3792
- tell application "System Events"
3793
- set windsurfProc to first process whose bundle identifier is "com.exafunction.windsurf"
3794
- tell windsurfProc
3795
- set frontmost to true
3796
- delay 0.5
3797
- key code 53
3798
- delay 0.5
3799
- keystroke "l" using {command down, shift down}
3800
- delay 2.0
3801
- keystroke "a" using {command down}
3802
- delay 0.3
3803
- key code 51
3804
- delay 0.3
3805
- set the clipboard to planText
3806
- keystroke "v" using {command down}
3807
- delay 0.5
3808
- key code 36
3809
- delay 1.0
3810
- end tell
3811
- end tell
3812
- `;
3813
- _writeFileSync2(_tmpScpt2, _combinedPlanScript, 'utf8');
3814
- try {
3815
- _execSync2(`osascript "${_tmpScpt2}"`, { stdio: 'pipe', timeout: 30000 });
3816
- return { success: true };
3817
- } finally {
3818
- try { _unlinkSync2(_tmpScpt2); } catch (_) { }
3819
- try { _unlinkSync2(_tmpText2); } catch (_) { }
3820
- }
3821
- } else {
3822
- const appleScriptManager = new AppleScriptManager();
3823
- const sendResult = typeof appleScriptManager.sendTextWithThreadClosure === 'function'
3824
- ? await appleScriptManager.sendTextWithThreadClosure(planningText, ideTypeForPlan)
3825
- : await appleScriptManager.sendText(planningText, ideTypeForPlan);
3826
- if (!sendResult || !sendResult.success) {
3827
- throw new Error((sendResult && sendResult.error) || 'send failed');
3828
- }
3829
- return { success: true };
3830
- }
3831
- };
3832
- try {
3833
- const sendPlanResult = await sendPlanToIde();
3834
- if (!sendPlanResult || !sendPlanResult.success) {
3835
- console.log(chalk.red(`✗ Failed to send to ${providerConfig.displayName}`));
3836
- planResult = { success: false, error: 'send failed' };
3837
- } else {
3838
- console.log(chalk.green(`✓ Planning task sent. Waiting for tasks.md to be created...`));
3839
- // Poll for tasks.md existence
3840
- const tasksFilePath = path.join(spec.path, 'tasks.md');
3841
- const POLL_MS = 30 * 1000;
3842
- const TIMEOUT_MS = 30 * 60 * 1000;
3843
- const planStart = Date.now();
3844
- planResult = await new Promise((resolve) => {
3845
- const iv = setInterval(() => {
3846
- if (fs.existsSync(tasksFilePath)) {
3847
- clearInterval(iv);
3848
- console.log(chalk.green('✓ tasks.md created!\n'));
3849
- resolve({ success: true });
3850
- } else if (Date.now() - planStart >= TIMEOUT_MS) {
3851
- clearInterval(iv);
3852
- console.log(chalk.yellow('⏰ Timeout waiting for tasks.md\n'));
3853
- resolve({ success: false, error: 'timeout' });
3854
- }
3855
- }, POLL_MS);
3856
- });
3857
- }
3858
- } catch (err) {
3859
- planResult = { success: false, error: err.message };
3860
- }
3861
- } else {
3862
- const planRequirement = { text: planningText, package: null, disabled: false };
3863
- planResult = await runIteration(planRequirement, providerConfig, repoPath);
3864
- }
3865
-
3866
- if (planResult.success) {
3867
- completedCount++;
3868
- console.log(chalk.green('✓ tasks.md created — proceeding with implementation\n'));
3869
- // Re-check if spec now has tasks
3870
- spec.hasTasks = fs.existsSync(path.join(spec.path, 'tasks.md'));
3871
- } else {
3872
- console.log(chalk.red('✗ Failed to create tasks.md — skipping spec\n'));
3873
- failedCount++;
3874
- continue; // Skip to next spec
3875
- }
3876
- }
3877
-
3878
- // Verify tasks.md has checkbox-format tasks
3879
- const { done: doneCheck, total: totalCheck } = countSpecCheckboxes(spec.path);
3880
- if (totalCheck === 0 && spec.hasTasks) {
3881
- console.log(chalk.yellow(`\n⚠️ tasks.md exists but has no checkbox tasks (narrative format detected)`));
3882
- console.log(chalk.yellow(` Regenerating tasks.md with /speckit.tasks...\n`));
3883
-
3884
- // Send task regeneration instruction to IDE
3885
- const tasksRegenerationText = `Run /speckit.tasks to regenerate tasks.md for spec "${spec.directory}" in checkbox format (- [ ] task).`;
3886
-
3887
- let regenResult;
3888
- if (providerConfig.type === 'ide') {
3889
- console.log(chalk.cyan(`📤 Sending tasks regeneration request to ${providerConfig.displayName}...\n`));
3890
- const ideType = providerConfig.provider || providerConfig.ide;
3891
- const appleScriptManager = new AppleScriptManager();
3892
- try {
3893
- const sendResult = typeof appleScriptManager.sendTextWithThreadClosure === 'function'
3894
- ? await appleScriptManager.sendTextWithThreadClosure(tasksRegenerationText, ideType)
3895
- : await appleScriptManager.sendText(tasksRegenerationText, ideType);
3896
-
3897
- if (!sendResult || !sendResult.success) {
3898
- console.log(chalk.red(`✗ Failed to send to ${providerConfig.displayName}`));
3899
- regenResult = { success: false };
3900
- } else {
3901
- console.log(chalk.green(`✓ Regeneration request sent. Waiting for tasks.md to be updated...`));
3902
- // Give IDE time to regenerate (30 seconds)
3903
- await new Promise(resolve => setTimeout(resolve, 30000));
3904
-
3905
- // Check if tasks now have checkboxes
3906
- const { total: totalAfterRegen } = countSpecCheckboxes(spec.path);
3907
- if (totalAfterRegen > 0) {
3908
- console.log(chalk.green(`✓ tasks.md regenerated with ${totalAfterRegen} checkbox tasks!\n`));
3909
- regenResult = { success: true };
3910
- } else {
3911
- console.log(chalk.yellow(`⚠️ tasks.md still has no checkbox tasks after regeneration\n`));
3912
- regenResult = { success: false };
3913
- }
3914
- }
3915
- } catch (err) {
3916
- regenResult = { success: false, error: err.message };
3917
- }
3918
- } else {
3919
- const regenRequirement = { text: tasksRegenerationText, package: null, disabled: false };
3920
- regenResult = await runIteration(regenRequirement, providerConfig, repoPath);
3921
- }
3922
-
3923
- if (!regenResult.success) {
3924
- console.log(chalk.red('✗ Failed to regenerate tasks.md — skipping spec\n'));
3925
- failedCount++;
3926
- continue; // Skip to next spec
3927
- }
3928
- }
3929
-
3930
- // Loop until spec is done or stalled
3931
- while (true) {
3932
- const task = getNextSpecTask(spec.path);
3933
- if (!task) {
3934
- // All tasks checked off
3935
- const { done: doneFinal, total: totalFinal } = countSpecCheckboxes(spec.path);
3936
- console.log(chalk.bold.green(`\n✅ Spec "${spec.directory}" complete! (${doneFinal}/${totalFinal} tasks)\n`));
3937
- break;
3938
- }
3939
-
3940
- // Detect same task repeated (LLM not checking it off)
3941
- if (task.text === lastSpecTaskText) {
3942
- sameTaskAttempts++;
3943
- if (sameTaskAttempts >= MAX_SPEC_TASK_ATTEMPTS) {
3944
- console.log(chalk.red(`\n✗ Task "${task.text}" not completing after ${MAX_SPEC_TASK_ATTEMPTS} attempts — skipping spec\n`));
3945
- break;
3946
- }
3947
- } else {
3948
- sameTaskAttempts = 0;
3949
- lastSpecTaskText = task.text;
3950
- }
3951
-
3952
- const { done: doneCurrent, total: totalCurrent } = countSpecCheckboxes(spec.path);
3953
- const pctCurrent = totalCurrent > 0 ? Math.round((doneCurrent / totalCurrent) * 100) : 0;
3954
-
3955
- console.log(chalk.cyan(`\n📌 Task ${doneCurrent + 1}/${totalCurrent || '?'}: ${task.text}`));
3956
- console.log(chalk.gray(` Spec progress: ${doneCurrent}/${totalCurrent} (${pctCurrent}%)\n`));
3957
-
3958
- // Route spec tasks differently depending on provider type.
3959
- // IDE providers (Windsurf, Cursor, etc.) must NOT go through runIdeFallbackIteration
3960
- // because that spawns "vcm auto:start" which is designed for requirements mode only.
3961
- // Instead, use runSpecIdeIteration which sends text directly via AppleScript
3962
- // and polls tasks.md for checkbox completion.
3963
- let result;
3964
- if (providerConfig.type === 'ide') {
3965
- result = await runSpecIdeIteration(spec, task.text, task.line, providerConfig);
3966
- } else {
3967
- // Direct LLM providers: wrap task in a requirement object and use standard iteration
3968
- const specTaskText = [
3969
- task.text,
3970
- `[Spec: ${spec.directory} — task ${doneCurrent + 1}/${totalCurrent || '?'}]`,
3971
- `[After implementing, mark the task done in ${spec.path}/tasks.md:`,
3972
- ` change: ${task.line}`,
3973
- ` to: - [x] ${task.text}]`
3974
- ].join('\n');
3975
- const specRequirement = { text: specTaskText, package: null, disabled: false };
3976
- result = await runIteration(specRequirement, providerConfig, repoPath);
3977
- }
3978
-
3979
- if (result.success) {
3980
- specProviderAttempts = 0;
3981
- spec.totalIdeAttempts = 0; // Reset IDE attempts on success
3982
- completedCount++;
3983
- const { done, total } = countSpecCheckboxes(spec.path);
3984
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
3985
- console.log(chalk.bold.green(`📊 Spec progress: ${done}/${total} tasks (${pct}%) complete`));
3986
- } else {
3987
- const isRateLimitError = isRateLimitMessage(result.error);
3988
- const isMaxAttemptsExceeded = result.error === 'max_ide_attempts_exceeded';
3989
- const isNoProgress = result.error === 'no_progress';
3990
-
3991
- let errorType = 'Error';
3992
- if (isRateLimitError) errorType = 'Rate limit';
3993
- else if (isMaxAttemptsExceeded) errorType = 'Max IDE attempts exceeded';
3994
- else if (isNoProgress) errorType = 'No progress';
3995
-
3996
- // Update total IDE attempts tracking
3997
- if (result.totalAttempts) {
3998
- spec.totalIdeAttempts = result.totalAttempts;
3999
- }
4000
-
4001
- specProviderAttempts++;
4002
- failedCount++;
4003
-
4004
- if (isMaxAttemptsExceeded) {
4005
- // Auto mode was stopped, exit completely
4006
- console.log(chalk.red(`\n✗ ${errorType} — auto mode stopped\n`));
4007
- return { completedCount, failedCount };
4008
- }
186
+ // Phase 2: Process incomplete specs
187
+ const specsResult = await processSpecsLoop(state);
188
+ completedCount += specsResult.completedCount;
189
+ failedCount += specsResult.failedCount;
4009
190
 
4010
- if (specProviderAttempts > MAX_SPEC_TASK_ATTEMPTS) {
4011
- console.log(chalk.red(`\n✗ Max provider attempts reached — moving to next spec\n`));
4012
- break;
4013
- }
4014
-
4015
- if (isNoProgress) {
4016
- console.log(chalk.yellow(`⚠️ ${errorType} on ${providerConfig.displayName}, switching IDE...`));
4017
- } else {
4018
- console.log(chalk.yellow(`⚠️ ${errorType} on spec task, switching provider...`));
4019
- }
4020
-
4021
- const newProviderConfig = await acquireProviderConfig(providerConfig.provider, providerConfig.model);
4022
- if (newProviderConfig) {
4023
- providerConfig = newProviderConfig;
4024
- console.log(chalk.green(`✓ Switched to: ${providerConfig.displayName}\n`));
4025
- } else {
4026
- console.log(chalk.red(`✗ No alternative providers available\n`));
4027
- break;
4028
- }
4029
- }
4030
- }
4031
- }
4032
-
4033
- console.log(chalk.bold.cyan('\n📋 All specs processed. Continuing to requirements...\n'));
4034
- console.log(chalk.gray('═'.repeat(80)));
191
+ // Check if we should exit early (max IDE attempts exceeded)
192
+ if (specsResult.shouldExit) {
193
+ return { completedCount, failedCount };
4035
194
  }
4036
195
 
4037
- // ── Phase 2: Requirements mode ─────────────────────────────────────────────
4038
- let providerAttempts = 0; // Track attempts for current requirement
4039
- let lastRequirementText = null; // Track which requirement we're on
4040
- const MAX_PROVIDER_ATTEMPTS = 3; // Maximum times to try different providers for same requirement
4041
-
4042
- for (let i = 0; i < maxChats; i++) {
4043
- // Get current requirement first to check if there are any TODO items
4044
- const requirement = await getCurrentRequirement(repoPath);
4045
- if (!requirement) {
4046
- // Check if we should process default requirement
4047
- const requirementsPath = await getRequirementsPath(repoPath);
4048
- const storage = new JSONStorage();
4049
- const parser = new RequirementFileParser();
4050
- const defaultManager = new DefaultRequirementManager(storage);
4051
- const sequencer = new RequirementSequencer(parser, defaultManager);
4052
-
4053
- const shouldProcessDefault = await sequencer.shouldProcessDefault(requirementsPath);
4054
-
4055
- if (shouldProcessDefault) {
4056
- console.log(chalk.bold.cyan('\n⭐ Default Requirement Mode'));
4057
- console.log(chalk.gray('All regular requirements completed, processing default requirement...\n'));
4058
-
4059
- // Process default requirement until completion or max iterations
4060
- await processDefaultRequirement(sequencer, defaultManager, providerConfig, repoPath);
4061
- } else {
4062
- if (completedCount > 0 || failedCount > 0) {
4063
- console.log(chalk.bold.yellow('\n🎉 All requirements completed!'));
4064
- } else {
4065
- console.log(chalk.bold.yellow('\n🎉 No requirements to process.'));
4066
- }
4067
- console.log(chalk.gray(`${t('auto.direct.no.more.todo.items')}\n`));
4068
- }
4069
- break;
4070
- }
4071
-
4072
- // Check if this is a new requirement or the same one we're retrying
4073
- if (requirement.text !== lastRequirementText) {
4074
- // New requirement - reset attempt counter
4075
- providerAttempts = 0;
4076
- lastRequirementText = requirement.text;
4077
- }
4078
-
4079
- // Increment attempt counter
4080
- providerAttempts++;
4081
-
4082
- // Check if we've exceeded maximum attempts for this requirement
4083
- if (providerAttempts > MAX_PROVIDER_ATTEMPTS) {
4084
- console.log(chalk.red(`\n✗ Maximum provider attempts (${MAX_PROVIDER_ATTEMPTS}) reached for this requirement`));
4085
- console.log(chalk.yellow(' All available providers have failed or are rate limited'));
4086
- console.log(chalk.gray(' Skipping this requirement and moving to next...\n'));
4087
-
4088
- // Mark requirement as failed and move on
4089
- failedCount++;
4090
- providerAttempts = 0;
4091
- lastRequirementText = null;
4092
-
4093
- // Move requirement to a "Failed" or "Needs Review" section
4094
- // For now, just continue to next iteration without decrementing i
4095
- continue;
4096
- }
4097
-
4098
- // Calculate current requirement number consistently (before processing)
4099
- // This represents which requirement we're working on (1-based)
4100
- const currentReqNumber = completedCount + failedCount + 1;
4101
-
4102
- console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
4103
- console.log(chalk.bold.magenta(` ${t('auto.direct.requirement.header', { current: currentReqNumber, total: initialEffectiveMax })}`));
4104
- console.log(chalk.bold.magenta(`${'━'.repeat(80)}\n`));
4105
-
4106
- // Update Auto Mode status with current iteration
4107
- await updateAutoModeStatus(repoPath, { chatCount: currentReqNumber });
4108
-
4109
- // Run iteration with full workflow
4110
- const result = await runIteration(requirement, providerConfig, repoPath);
4111
-
4112
- if (result.success) {
4113
- completedCount++;
4114
- providerAttempts = 0; // Reset attempts on success
4115
- lastRequirementText = null;
4116
- console.log(chalk.bold.green(`✅ Requirement ${currentReqNumber}/${initialEffectiveMax} COMPLETE`));
4117
- console.log(chalk.gray('Moving to next requirement...\n'));
4118
-
4119
- // Check if restart CLI is enabled and there are more iterations
4120
- if (config.restartCLI && i < maxChats - 1) {
4121
- console.log(chalk.cyan('🔄 Restarting CLI to pick up latest changes...\n'));
4122
-
4123
- // Calculate remaining iterations
4124
- const remainingIterations = maxChats - (i + 1);
4125
-
4126
- // Spawn new CLI process
4127
- const { spawn } = require('child_process');
4128
- const cliScriptPath = path.join(__dirname, '../../bin/vibecodingmachine.js');
4129
- const args = ['auto:direct', '--max-chats', remainingIterations.toString()];
4130
-
4131
- // Spawn without detached mode - child will inherit terminal
4132
- // We'll exit after a brief delay to let child establish itself
4133
- const child = spawn(process.execPath, [cliScriptPath, ...args], {
4134
- stdio: 'inherit',
4135
- cwd: process.cwd(),
4136
- env: process.env
4137
- });
4138
-
4139
- // Handle child errors (but don't wait for completion)
4140
- child.on('error', (err) => {
4141
- console.error(chalk.red(t('auto.direct.config.restart.error')), err.message);
4142
- });
4143
-
4144
- // Don't wait for child - unref so parent can exit
4145
- child.unref();
4146
-
4147
- // Give child a moment to start before exiting parent
4148
- await new Promise(resolve => setTimeout(resolve, 300));
4149
-
4150
- // Exit this process - child continues with terminal
4151
- await stopAutoMode('restarting');
4152
- process.exit(0);
4153
- } else {
4154
- // Small delay before next iteration (if not restarting)
4155
- if (i < maxChats - 1) {
4156
- await new Promise(resolve => setTimeout(resolve, 2000));
4157
- }
4158
- }
4159
- } else {
4160
- // Check if it's a rate limit error (for logging purposes)
4161
- const isRateLimitError = isRateLimitMessage(result.error);
4162
- const errorType = isRateLimitError ? 'Rate limit' : 'Error';
4163
-
4164
- // Store the provider that failed before switching
4165
- const failedProvider = providerConfig.displayName;
4166
-
4167
- console.log(chalk.yellow(`⚠️ ${errorType} detected, switching to next provider in your list...\n`));
4168
-
4169
- const newProviderConfig = await acquireProviderConfig(providerConfig.provider, providerConfig.model);
4170
- if (newProviderConfig) {
4171
- providerConfig = newProviderConfig;
4172
- console.log(chalk.yellow(`⚠️ ${failedProvider} hit ${errorType.toLowerCase()}`));
4173
- console.log(chalk.green(`✓ Switched to: ${providerConfig.displayName}\n`));
4174
-
4175
- // Retry this iteration with new provider (don't increment i)
4176
- i--;
4177
- continue;
4178
- } else {
4179
- console.log(chalk.red('✗ No alternative providers available\n'));
4180
- }
4181
-
4182
- failedCount++;
4183
- // Use the same currentReqNumber that was calculated at the start of this iteration
4184
- // This ensures consistency: if we showed "Requirement 2 of 3" at the start,
4185
- // we should show "Requirement 2 of 3 FAILED" (not recalculate it)
4186
- console.log(chalk.bold.red(`❌ Requirement ${currentReqNumber}/${initialEffectiveMax} FAILED`));
4187
- console.log(chalk.red(`Error: ${result.error}\n`));
4188
- console.log(chalk.yellow('Continuing to next requirement...\n'));
4189
- }
4190
- }
196
+ // Phase 3: Process requirements
197
+ const requirementsResult = await processRequirementsLoop(state, completedCount, failedCount);
198
+ completedCount = requirementsResult.completedCount;
199
+ failedCount = requirementsResult.failedCount;
4191
200
 
4192
201
  // Final Summary
4193
202
  console.log(chalk.bold.magenta(`\n${'━'.repeat(80)}`));
@@ -4200,7 +209,7 @@ end tell
4200
209
  if (failedCount > 0) {
4201
210
  console.log(chalk.white(t('auto.direct.summary.failed')), chalk.red(`${failedCount} ✗`));
4202
211
  }
4203
- console.log(chalk.white(t('auto.direct.summary.provider')), chalk.cyan(providerConfig.displayName));
212
+ console.log(chalk.white(t('auto.direct.summary.provider')), chalk.cyan(state.providerConfig.displayName));
4204
213
  console.log();
4205
214
 
4206
215
  if (completedCount > 0) {
@@ -4216,8 +225,8 @@ end tell
4216
225
  }
4217
226
 
4218
227
  // Stop requirement monitoring
4219
- if (stopMonitoring) {
4220
- stopMonitoring();
228
+ if (state && state.stopMonitoring) {
229
+ state.stopMonitoring();
4221
230
  }
4222
231
 
4223
232
  } catch (error) {
@@ -4232,8 +241,8 @@ end tell
4232
241
  }
4233
242
 
4234
243
  // Stop requirement monitoring
4235
- if (stopMonitoring) {
4236
- stopMonitoring();
244
+ if (state && state.stopMonitoring) {
245
+ state.stopMonitoring();
4237
246
  }
4238
247
 
4239
248
  // Stop Auto Mode status tracking on fatal error
@@ -4242,5 +251,4 @@ end tell
4242
251
  }
4243
252
  }
4244
253
 
4245
- module.exports = { handleAutoStart, waitForIdeCompletion, acquireProviderConfig, getCurrentRequirement };
4246
-
254
+ module.exports = { handleAutoStart };