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.
- package/bin/auth/auth-compliance.js +7 -7
- package/bin/commands/agent-commands.js +15 -15
- package/bin/commands/auto-commands.js +3 -3
- package/bin/commands/command-aliases.js +13 -4
- package/bin/config/cli-config.js +15 -5
- package/bin/update/update-checker.js +5 -5
- package/bin/vibecodingmachine.js +2 -2
- package/package.json +2 -2
- package/src/commands/agents/add.js +5 -5
- package/src/commands/agents/check.js +19 -19
- package/src/commands/agents/list.js +24 -24
- package/src/commands/agents/remove.js +4 -4
- package/src/commands/agents-check.js +1 -1
- package/src/commands/analyze-file-sizes.js +43 -43
- package/src/commands/auto-direct/auto-provider-manager.js +19 -19
- package/src/commands/auto-direct/auto-start-phases.js +493 -0
- package/src/commands/auto-direct/auto-status-display.js +35 -35
- package/src/commands/auto-direct/auto-utils.js +50 -50
- package/src/commands/auto-direct/cline-installer.js +56 -0
- package/src/commands/auto-direct/code-processor.js +27 -27
- package/src/commands/auto-direct/file-scanner.js +19 -19
- package/src/commands/auto-direct/ide-completion-waiter.js +485 -0
- package/src/commands/auto-direct/ide-fallback-runner.js +226 -0
- package/src/commands/auto-direct/ide-provider-runner.js +103 -0
- package/src/commands/auto-direct/iteration-handlers.js +189 -0
- package/src/commands/auto-direct/iteration-runner.js +485 -0
- package/src/commands/auto-direct/provider-config.js +38 -7
- package/src/commands/auto-direct/provider-manager.js +132 -6
- package/src/commands/auto-direct/requirement-manager.js +169 -104
- package/src/commands/auto-direct/requirement-mover.js +350 -0
- package/src/commands/auto-direct/spec-handlers.js +155 -0
- package/src/commands/auto-direct/spec-ide-runner.js +318 -0
- package/src/commands/auto-direct/spec-processing.js +203 -0
- package/src/commands/auto-direct/status-display.js +9 -9
- package/src/commands/auto-direct/utils.js +83 -1
- package/src/commands/auto-direct-refactored.js +1 -413
- package/src/commands/auto-direct.js +127 -4119
- package/src/commands/auto-execution.js +21 -21
- package/src/commands/auto-status-helpers.js +0 -2
- package/src/commands/auto.js +22 -22
- package/src/commands/check-compliance.js +65 -65
- package/src/commands/computers.js +39 -39
- package/src/commands/continuous-scan.js +19 -19
- package/src/commands/ide.js +4 -4
- package/src/commands/locale.js +7 -7
- package/src/commands/refactor-file.js +59 -59
- package/src/commands/requirements/commands.js +17 -17
- package/src/commands/requirements/default-handlers.js +30 -30
- package/src/commands/requirements/disable.js +3 -3
- package/src/commands/requirements/enable.js +3 -3
- package/src/commands/requirements/utils.js +6 -6
- package/src/commands/requirements-refactored.js +3 -3
- package/src/commands/requirements-remote.js +38 -38
- package/src/commands/requirements.js +3 -3
- package/src/commands/settings.js +111 -0
- package/src/commands/specs/count.js +60 -0
- package/src/commands/specs/disable.js +3 -3
- package/src/commands/specs/enable.js +3 -3
- package/src/commands/status.js +10 -10
- package/src/commands/sync.js +25 -25
- package/src/commands/timeout.js +35 -35
- package/src/trui/TruiInterface.js +2 -2
- package/src/trui/agents/AgentInterface.js +4 -4
- package/src/trui/agents/handlers/CommandHandler.js +4 -4
- package/src/trui/agents/handlers/ContextManager.js +1 -1
- package/src/trui/agents/handlers/DisplayHandler.js +11 -11
- package/src/trui/agents/handlers/HelpHandler.js +1 -1
- package/src/utils/agent-selector.js +6 -6
- package/src/utils/antigravity-installer.js +4 -4
- package/src/utils/asset-cleanup.js +1 -1
- package/src/utils/auth.js +9 -12
- package/src/utils/clarification-actions.js +4 -4
- package/src/utils/cline-js-handler.js +5 -5
- package/src/utils/compliance-check.js +6 -6
- package/src/utils/config.js +12 -12
- package/src/utils/display-formatters-complete.js +2 -2
- package/src/utils/display-formatters-extracted.js +2 -2
- package/src/utils/display-formatters.js +2 -2
- package/src/utils/feedback-handler.js +2 -2
- package/src/utils/first-run.js +7 -7
- package/src/utils/ide-detection.js +1 -1
- package/src/utils/ide-handlers.js +6 -6
- package/src/utils/interactive/clarification-actions.js +3 -3
- package/src/utils/interactive/core-ui.js +7 -7
- package/src/utils/interactive/file-backup.js +6 -6
- package/src/utils/interactive/file-import-export.js +49 -49
- package/src/utils/interactive/file-operations.js +3 -3
- package/src/utils/interactive/file-validation.js +41 -41
- package/src/utils/interactive/interactive-prompts.js +41 -41
- package/src/utils/interactive/requirement-actions.js +5 -5
- package/src/utils/interactive/requirement-crud.js +4 -4
- package/src/utils/interactive/requirements-navigation.js +10 -10
- package/src/utils/interactive-broken.js +6 -6
- package/src/utils/interactive.js +37 -37
- package/src/utils/keyboard-handler.js +4 -4
- package/src/utils/prompt-helper.js +6 -6
- package/src/utils/provider-checker/agent-checker.js +1 -1
- package/src/utils/provider-checker/agent-runner.js +203 -314
- package/src/utils/provider-checker/agents-file-lock.js +134 -0
- package/src/utils/provider-checker/agents-manager.js +224 -36
- package/src/utils/provider-checker/cli-installer.js +28 -28
- package/src/utils/provider-checker/cli-utils.js +2 -2
- package/src/utils/provider-checker/cursor-approval-clicker.js +108 -0
- package/src/utils/provider-checker/format-utils.js +4 -4
- package/src/utils/provider-checker/ide-installer-helper.js +96 -0
- package/src/utils/provider-checker/ide-manager.js +19 -8
- package/src/utils/provider-checker/ide-quota-checker.js +120 -0
- package/src/utils/provider-checker/ide-utils.js +2 -2
- package/src/utils/provider-checker/node-detector.js +4 -4
- package/src/utils/provider-checker/node-utils.js +5 -5
- package/src/utils/provider-checker/opencode-checker.js +107 -73
- package/src/utils/provider-checker/process-utils.js +1 -1
- package/src/utils/provider-checker/provider-validator.js +11 -11
- package/src/utils/provider-checker/quota-checker.js +5 -5
- package/src/utils/provider-checker/quota-detector.js +5 -5
- package/src/utils/provider-checker/requirements-manager.js +6 -6
- package/src/utils/provider-checker/test-requirements.js +1 -1
- package/src/utils/provider-checker/vscode-approval-clicker.js +328 -0
- package/src/utils/provider-checker-new.js +6 -6
- package/src/utils/provider-checker.js +6 -6
- package/src/utils/provider-checkers/ide-manager.js +13 -13
- package/src/utils/provider-checkers/node-executable-finder.js +4 -4
- package/src/utils/provider-checkers/provider-checker-core.js +5 -5
- package/src/utils/provider-checkers/provider-checker-main.js +17 -17
- package/src/utils/provider-registry.js +5 -6
- package/src/utils/provider-utils.js +12 -12
- package/src/utils/quota-detectors.js +32 -32
- package/src/utils/requirement-action-handlers.js +12 -12
- package/src/utils/requirement-actions/requirement-operations.js +3 -3
- package/src/utils/requirement-actions.js +1 -1
- package/src/utils/requirement-file-operations.js +5 -5
- package/src/utils/requirement-helpers.js +1 -1
- package/src/utils/requirement-management.js +5 -5
- package/src/utils/requirement-navigation.js +2 -2
- package/src/utils/requirement-organization.js +3 -3
- package/src/utils/rui-trui-adapter.js +14 -14
- package/src/utils/simple-trui.js +3 -3
- package/src/utils/status-helpers-extracted.js +3 -3
- package/src/utils/trui-clarifications.js +11 -11
- package/src/utils/trui-debug.js +3 -2
- package/src/utils/trui-devin.js +217 -0
- package/src/utils/trui-feedback.js +7 -7
- package/src/utils/trui-kiro-integration.js +34 -34
- package/src/utils/trui-main-handlers.js +20 -21
- package/src/utils/trui-main-menu.js +19 -19
- package/src/utils/trui-nav-agents.js +59 -8
- package/src/utils/trui-nav-requirements.js +3 -3
- package/src/utils/trui-nav-settings.js +10 -10
- package/src/utils/trui-nav-specifications.js +1 -1
- package/src/utils/trui-navigation-backup.js +11 -11
- package/src/utils/trui-navigation.js +9 -9
- package/src/utils/trui-provider-health.js +25 -25
- package/src/utils/trui-provider-manager.js +28 -28
- package/src/utils/trui-quick-menu.js +2 -2
- package/src/utils/trui-req-actions-backup.js +21 -21
- package/src/utils/trui-req-actions.js +20 -20
- package/src/utils/trui-req-editor.js +10 -10
- package/src/utils/trui-req-file-ops.js +3 -3
- package/src/utils/trui-req-tree.js +7 -7
- package/src/utils/trui-windsurf.js +103 -103
- package/src/utils/user-tracking.js +15 -15
- 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
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
3455
|
-
|
|
60
|
+
const {
|
|
61
|
+
updateRequirementsStatus,
|
|
62
|
+
getCurrentRequirement,
|
|
63
|
+
countTodoRequirements,
|
|
64
|
+
moveRequirementToVerify,
|
|
65
|
+
moveRequirementToRecycle
|
|
66
|
+
} = require('./auto-direct/requirement-manager');
|
|
3456
67
|
|
|
3457
|
-
|
|
68
|
+
const {
|
|
69
|
+
parseSearchReplaceBlocks,
|
|
70
|
+
normalizeWhitespace,
|
|
71
|
+
extractIdentifiers,
|
|
72
|
+
extractPattern,
|
|
73
|
+
applyFileChange
|
|
74
|
+
} = require('./auto-direct/code-processor');
|
|
3458
75
|
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
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
|
-
|
|
3477
|
-
|
|
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
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
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
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
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
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
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
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
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
|
-
|
|
3546
|
-
|
|
3547
|
-
iterationCount = defaultStatus.iterationCount + 1;
|
|
125
|
+
// Global keyboard handler reference for cleanup
|
|
126
|
+
let keyboardHandler = null;
|
|
3548
127
|
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
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
|
-
|
|
3555
|
-
|
|
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
|
-
|
|
3562
|
-
|
|
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
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
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
|
-
|
|
3578
|
-
|
|
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
|
-
|
|
3586
|
-
|
|
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
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
console.log(chalk.gray('═'.repeat(80)));
|
|
176
|
+
try {
|
|
177
|
+
// Phase 1: Initialize auto mode
|
|
178
|
+
state = await initializeAutoMode(options, sharedRefs);
|
|
3712
179
|
|
|
3713
|
-
//
|
|
3714
|
-
|
|
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
|
-
//
|
|
3722
|
-
const
|
|
3723
|
-
|
|
3724
|
-
|
|
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
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
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
|
-
//
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
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
|
|
4246
|
-
|
|
254
|
+
module.exports = { handleAutoStart };
|