-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathorchestrator.js
More file actions
479 lines (420 loc) · 22.6 KB
/
orchestrator.js
File metadata and controls
479 lines (420 loc) · 22.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
// Main orchestrator script
// Placeholder for future logic
console.log("Orchestrator started.");
// TODO: Implement main workflow loop
import { runTaskMasterCommand, parseNextOutput, parseShowOutput } from './taskMasterUtils.js';
import fs from 'fs/promises'; // Added for reading complexity report
import { execSync } from 'child_process'; // Import execSync here as well
class Orchestrator {
constructor() {
this.currentTaskId = null; // Basic state management
// Disable test failure simulation flag
// this.simulateTestFailureForTask9 = true;
console.log("Orchestrator initialized.");
}
/**
* Fetches the next available task from Task Master.
* @returns {Promise<{taskId: string, taskTitle: string} | null>}
*/
async getNextTask(silent = false) {
if (!silent) console.log("\n--- Fetching next task ---");
const nextTaskResult = await runTaskMasterCommand('next');
if (!nextTaskResult.success) {
console.error("Failed to get next task.");
return null;
}
const { taskId, taskTitle } = parseNextOutput(nextTaskResult.output);
if (!taskId && !silent) {
console.log("No available tasks found or failed to parse.");
}
if (taskId && !silent) console.log(`Next task identified: ID=${taskId}, Title=${taskTitle}`);
return { taskId, taskTitle };
}
/**
* Fetches details for a specific task.
* @param {string} taskId
* @returns {Promise<{details: string | null, testStrategy: string | null, title: string | null}>}
*/
async getTaskDetails(taskId) {
console.log(`\n--- Fetching task details for ${taskId} ---`);
const showTaskResult = await runTaskMasterCommand('show', [`--id=${taskId}`]);
if (!showTaskResult.success) {
console.error(`Failed to get details for task ${taskId}.`);
return { details: null, testStrategy: null, title: null };
}
const parsedDetails = parseShowOutput(showTaskResult.output);
if (!parsedDetails.details || !parsedDetails.testStrategy) {
console.warn(`Could not parse all details or test strategy for task ${taskId}`);
}
return parsedDetails;
}
/**
* Generates a prompt for Cursor AI.
* @param {object} taskDetails
* @param {string} taskId
* @returns {string}
*/
generateCodePrompt({ details, testStrategy, title }, taskId = 'N/A') {
console.log("\n--- Generating AI Prompt ---");
let testingRequirements = 'Please ensure the changes are well-tested...';
if (testStrategy && testStrategy.trim() !== '') {
testingRequirements = `Please adhere to the following testing strategy:\n${testStrategy}`;
} else {
testingRequirements = 'No specific test strategy provided...';
}
const prompt = `
Task ID: ${taskId}
Task Title: ${title || 'N/A'}
**Goal:**
${details || 'No specific details provided...'}
**Context:**
// TODO: Add context...
**Testing & Verification Requirements:**
${testingRequirements}
**Instructions:**
// ... (Instructions)
`;
console.log("Generated Prompt (snippet):\n", prompt.substring(0, 250) + "...");
return prompt;
}
/**
* Runs the configured linter.
* @returns {Promise<{success: boolean, output: string, exit_code: number | null}>}
*/
async runLinter() {
// Simplified linter check
this.log("Running linter...");
this.log("Simulating linter execution (assuming pass)..." );
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate delay
return { success: true, output: "Simulated linter success." };
}
/**
* Runs the configured tests.
* @returns {Promise<{success: boolean, output: string, exit_code: number | null}>}
*/
async runTests() {
this.log("Running tests...");
// Disable test failure simulation for Task 9
/*
if (this.currentTaskId === '9' && this.simulateTestFailureForTask9) {
this.log("SIMULATING TEST FAILURE for Task 9 (first attempt)");
this.simulateTestFailureForTask9 = false; // Reset flag so it passes next time
return { success: false, output: "Simulated test failure output for Task 9." };
}
*/
// Run actual tests via task master command
this.log("Running actual tests via Task Master...");
const testResult = await runTaskMasterCommand('test', []);
this.log(`Test command finished. Success: ${testResult.success}`);
return { success: testResult.success, output: testResult.output };
}
/**
* Stages and commits changes.
* @param {string} taskId
* @param {string} taskTitle
* @returns {Promise<boolean>}
*/
async commitChanges(taskId, taskTitle) {
console.log(`\n--- Committing changes for Task ${taskId} ---`);
const commitMessage = `feat: Complete task ${taskId} - ${taskTitle}`;
console.log(`Commit message: "${commitMessage}"`);
try {
// Stage all changes
this.log("Staging changes with 'git add .'...");
execSync('git add .', { stdio: 'inherit' }); // Show output/errors directly
this.log("Git add successful.");
// Commit the changes
this.log(`Committing with message: "${commitMessage}"`);
// Need to handle potential quotes in the commit message for the command line
const escapedCommitMessage = commitMessage.replace(/"/g, '\"'); // Basic escaping
execSync(`git commit -m "${escapedCommitMessage}"`, { stdio: 'inherit' }); // Show output/errors directly
this.log("Git commit successful.");
return true;
} catch (error) {
console.error("An error occurred during Git operations:");
// Errors from execSync often include status, stderr, stdout
console.error(` Status Code: ${error.status}`);
const stderrOutput = error.stderr ? error.stderr.toString().trim() : 'No stderr output.';
console.error(` Stderr: ${stderrOutput}`);
const stdoutOutput = error.stdout ? error.stdout.toString().trim() : 'No stdout output on error.';
console.error(` Stdout: ${stdoutOutput}`);
console.error(` Error Message: ${error.message}`);
this.log("Git operations failed.");
return false;
}
}
/**
* Marks a task as completed.
* @param {string} taskId
* @returns {Promise<boolean>}
*/
async setTaskCompleted(taskId) {
console.log(`\n--- Marking task ${taskId} as done ---`);
const result = await runTaskMasterCommand('set-status', [`--id=${taskId}`, '--status=done']);
if (!result.success) console.error(`Failed to set status for task ${taskId}.`);
return result.success;
}
// Add a simple logging helper
log(message) {
const prefix = this.currentTaskId ? `[Task ${this.currentTaskId}]` : '[Orchestrator]';
console.log(`${prefix} ${message}`);
}
/**
* Runs the main orchestrator loop for one iteration.
*/
async run() {
this.log("\n--- Starting new loop iteration ---");
// Do not reset currentTaskId here, set it after fetching
// Do not reset simulation flag here, let runTests handle it.
try {
const nextTaskInfo = await this.getNextTask();
if (!nextTaskInfo || !nextTaskInfo.taskId) { // Check taskId too
this.log("No more tasks or failed to fetch. Stopping.");
return false; // Signal to stop the loop
}
this.currentTaskId = nextTaskInfo.taskId; // Set current task ID *here*
const { taskTitle } = nextTaskInfo;
this.log(`Processing Task: ${taskTitle}`);
const taskDetails = await this.getTaskDetails(this.currentTaskId);
if (!taskDetails || !taskDetails.details) {
console.error(`[Task ${this.currentTaskId}] Failed to get sufficient details... Skipping.`);
this.currentTaskId = null; // Clear task ID on failure
return false; // Signal to stop the loop
}
const fullTaskDetails = { ...taskDetails, title: taskTitle };
this.log(`Fetched details.`);
// --- Task 10.1: Integrate Complexity Analysis Call ---
this.log("Running complexity analysis...");
const analysisResult = await runTaskMasterCommand('analyze-complexity', [`--id=${this.currentTaskId}`, '--research']); // Added --research flag as per task description
if (!analysisResult.success) {
// Log warning but potentially continue? Or stop? Task 10.4 might clarify.
// For now, let's log a warning and continue, assuming analysis failure isn't critical block
this.log(`Warning: Complexity analysis failed for task ${this.currentTaskId}. Proceeding without expansion check.`);
// TODO: Revisit error handling in Task 10.4
} else {
this.log("Complexity analysis completed.");
// Log analysis output snippet for debugging (optional)
// this.log(`Analysis Output (snippet): ${analysisResult.output.substring(0, 200)}...`);
}
// --- End Task 10.1 ---
// TODO: Implement Task 10.2: Parse Output (using analysisResult.output)
// --- Task 10.2: Parse Complexity Analysis Output ---
let needsExpansion = false;
let expansionDetails = null; // To store details if needed for Task 10.3 prompt
const COMPLEXITY_THRESHOLD = 8; // Restore original threshold
if (analysisResult.success) {
try {
// Assuming analyze-complexity writes to the default file
const reportPath = 'scripts/task-complexity-report.json';
// --- START: Added Check and Creation of Dummy Report ---
try {
await fs.access(reportPath); // Check if file exists
this.log("Complexity report file found.");
} catch (accessError) {
if (accessError.code === 'ENOENT') {
// File does not exist, create a dummy one for simulation
this.log("Complexity report file not found. Creating dummy report for simulation.");
const dummyReportContent = JSON.stringify({ complexityAnalysis: [] }, null, 2);
await fs.writeFile(reportPath, dummyReportContent, 'utf-8');
this.log("Dummy complexity report file created.");
} else {
// Other error accessing the file, re-throw
throw accessError;
}
}
// --- END: Added Check and Creation of Dummy Report ---
// We need a way to read files in Node.js. Let's use the 'fs' module.
const reportContent = await fs.readFile(reportPath, 'utf-8');
const reportData = JSON.parse(reportContent);
if (reportData && reportData.complexityAnalysis) {
const taskAnalysis = reportData.complexityAnalysis.find(
(task) => task.taskId.toString() === this.currentTaskId.toString()
);
if (taskAnalysis) {
this.log(`Task ${this.currentTaskId} complexity score: ${taskAnalysis.complexityScore}`);
if (taskAnalysis.complexityScore >= COMPLEXITY_THRESHOLD) {
needsExpansion = true;
expansionDetails = taskAnalysis; // Store for potential use in expand prompt
this.log(`Task ${this.currentTaskId} identified as complex (score >= ${COMPLEXITY_THRESHOLD}). Flagging for expansion.`);
} else {
this.log(`Task ${this.currentTaskId} complexity score is below threshold. No expansion needed.`);
}
} else {
this.log(`Warning: Could not find complexity analysis data for task ${this.currentTaskId} in the report.`);
}
} else {
this.log("Warning: Complexity report format is invalid or missing 'complexityAnalysis' array.");
}
} catch (error) {
this.log(`Error reading or parsing complexity report: ${error.message}. Proceeding without expansion.`);
// Consider more robust error handling in Task 10.4
}
} else {
this.log("Skipping complexity parsing due to failed analysis step.");
}
// --- End Task 10.2 ---
// TODO: Implement Task 10.3: Conditional Expansion Call (using needsExpansion flag)
// --- Task 10.3: Implement Conditional Task Expansion Call ---
let expansionAttempted = false;
let expansionSucceeded = false;
if (needsExpansion) {
this.log(`Task ${this.currentTaskId} requires expansion. Calling 'task-master expand'...`);
expansionAttempted = true;
// Construct arguments for expand command
const expandArgs = [`--id=${this.currentTaskId}`];
if (expansionDetails && expansionDetails.expansionPrompt) {
// Pass the recommended prompt from the complexity report
expandArgs.push(`--prompt="${expansionDetails.expansionPrompt}"`);
this.log(`Using expansion prompt from report: "${expansionDetails.expansionPrompt}"`);
}
// Add --research flag consistent with analysis? Task description implies it.
expandArgs.push('--research'); // Let's assume we want research-backed expansion too
const expandResult = await runTaskMasterCommand('expand', expandArgs);
if (expandResult.success) {
this.log(`Task ${this.currentTaskId} expanded successfully.`);
expansionSucceeded = true;
// Task 10.4 will handle restarting the loop or fetching the next (sub)task
} else {
this.log(`Error: Failed to expand task ${this.currentTaskId}. Output: ${expandResult.output}`);
expansionSucceeded = false;
// Task 10.4 will need to decide how to handle expansion failure (stop or continue with original task?)
}
}
// --- End Task 10.3 ---
// --- Task 10.4: Adjust Orchestrator Workflow and Error Handling ---
// TODO: Implement this next based on expansionAttempted and expansionSucceeded
if (!expansionAttempted) {
this.log("Proceeding with original task (no expansion attempted/needed).");
// --- START: Original Task Processing Block ---
const aiPrompt = this.generateCodePrompt(fullTaskDetails, this.currentTaskId);
this.log(`Generated AI Prompt.`);
console.log("\n--- Simulating AI Code Generation ---");
this.log(`(Would send prompt to AI now)`);
this.log("Running Quality Assurance...");
const linterResult = await this.runLinter();
if (!linterResult.success) {
this.log("Linter failed. Stopping task processing.");
this.currentTaskId = null; // Clear task ID on failure
return false; // Signal to stop the loop
}
const testResult = await this.runTests();
if (!testResult.success) {
// --- Task 9.1: Detect Test Failure and Gather Context ---
this.log("Tests failed. Attempting automated fix...");
const context = {
taskId: this.currentTaskId,
taskTitle: taskTitle,
taskDetails: fullTaskDetails,
failedTestOutput: testResult.output
};
this.log(`Gathered context for task ${this.currentTaskId}: ${JSON.stringify(context, null, 2)}`);
const fixPrompt = this.generateTestFixPrompt(context);
this.log(`Generated prompt for fix: ${fixPrompt.substring(0, 100)}...`);
this.log("Simulating AI interaction to get test fix...");
const suggestedFix = "// Placeholder: AI suggested code fix based on prompt";
this.log(`Received suggested fix (placeholder): ${suggestedFix}`);
this.log("Simulating application of the fix...");
this.log("Re-running tests after applying simulated fix...");
const reRunResult = await this.runTests();
this.log(`Test re-run result: ${reRunResult.success ? 'Passed' : 'Failed'}`);
// --- End Task 9.3 ---
// --- Task 9.4: Handle Test Re-run Outcome ---
if (reRunResult.success) {
this.log("Automated test fix successful! Tests passed after simulated fix.");
} else {
this.log("Automated test fix failed. Tests still failing after simulated fix.");
this.log("Stopping task processing due to persistent test failure.");
this.currentTaskId = null;
return false; // Signal to stop the loop
}
// --- End Task 9.4 ---
}
this.log("QA Passed.");
this.log("Attempting Git Commit...");
const commitSuccess = await this.commitChanges(this.currentTaskId, taskTitle);
if (!commitSuccess) {
this.log("Git commit failed. Stopping task processing.");
this.currentTaskId = null;
return false; // Signal to stop the loop
}
this.log("Git operations completed successfully.");
const statusUpdated = await this.setTaskCompleted(this.currentTaskId);
if (!statusUpdated) {
this.log(`Failed to mark task as done.`);
}
this.log("Task marked as done.");
// --- END: Original Task Processing Block ---
// Original task completed successfully without expansion
this.log("End of one loop iteration (original task completed).");
this.currentTaskId = null; // Clear after successful completion
return true; // Signal to continue the loop
} else if (expansionSucceeded) {
this.log("Task expanded successfully. Restarting loop...");
// Instead of returning, we want to start the next loop iteration immediately.
// Clear the current ID and call run again asynchronously.
this.currentTaskId = null;
setTimeout(() => this.run(), 0); // Schedule the next run asynchronously
return true; // Let current loop know it should technically continue
} else { // Expansion attempted but failed
this.log("Error: Task expansion failed. Stopping processing.");
// Keep the existing behavior: stop and clear ID.
this.currentTaskId = null; // Clear ID as we are stopping
return false; // Signal to stop the loop due to critical failure
}
// --- End Task 10.4 ---
// Note: The logic for clearing currentTaskId after successful completion
// is now handled within the specific branches above.
} catch (error) {
console.error(`[Task ${this.currentTaskId || 'N/A'}] An error occurred:`, error);
this.currentTaskId = null; // Clear task ID on error
// Consider adding restart logic here if needed
return false; // Stop the loop on unhandled errors
}
// Should not be reached if logic is correct, but default to continue
// return true; // Let's remove this as all paths should return explicitly
}
generateTestFixPrompt(context) {
this.log("Generating prompt for automated test fix...");
const { taskId, taskTitle, taskDetails, failedTestOutput } = context;
// Restore the actual prompt generation logic
const prompt = `
Task ID: ${taskId}
Task Title: ${taskTitle}
Task Details:
${taskDetails.details || 'No details provided.'}
Test Strategy:
${taskDetails.testStrategy || 'No test strategy provided.'}
The following tests failed:
\`\`\`
${failedTestOutput}
\`\`\`
Based on the task requirements and the failed test output, please provide the necessary code modifications to fix the tests. Focus only on the code changes required. Assume relevant files are available for editing.
`;
this.log("Generated test fix prompt.");
// console.log("--- Test Fix Prompt ---");
// console.log(prompt);
// console.log("--- End Test Fix Prompt ---");
return prompt; // Ensure return statement is still present
}
}
// Instantiate the orchestrator
const orchestrator = new Orchestrator();
// Function to run the orchestrator loop and schedule the next run
async function runLoop() {
try {
const shouldContinue = await orchestrator.run(); // Run one iteration and get status
if (!shouldContinue) {
console.log("[Main] Orchestrator run signaled stop. Exiting loop.");
return; // Exit the loop
}
// If run() returned true, schedule the next iteration
console.log("[Main] Orchestrator run completed. Scheduling next run...");
setTimeout(runLoop, 1000); // Schedule next iteration after 1 second delay
} catch (mainLoopError) {
console.error("[Main] Uncaught error in orchestrator run:", mainLoopError);
console.log("[Main] Stopping due to unhandled error.");
// Optionally add a longer delay retry here if needed
}
}
// Start the first iteration
runLoop();