developer.js
1 /** 2 * Developer Agent 3 * 4 * Handles bug fixes, feature implementation, and code changes. 5 * Works autonomously but hands off to QA for verification. 6 */ 7 8 import { BaseAgent } from './base-agent.js'; 9 import { execSync } from 'child_process'; 10 import fs from 'fs/promises'; 11 import path from 'path'; 12 import * as fileOps from './utils/file-operations.js'; 13 import { runTests, runTestsForFile } from './utils/test-runner.js'; 14 import { simpleLLMCall } from './utils/agent-claude-api.js'; 15 16 // Injectable dependencies — tests can override these to mock ESM modules 17 export const _deps = { 18 readFile: (...args) => fileOps.readFile(...args), 19 getFileContext: (...args) => fileOps.getFileContext(...args), 20 editFile: (...args) => fileOps.editFile(...args), 21 writeFile: (...args) => fileOps.writeFile(...args), 22 restoreBackup: (...args) => fileOps.restoreBackup(...args), 23 cleanupBackups: (...args) => fileOps.cleanupBackups(...args), 24 listBackups: (...args) => fileOps.listBackups(...args), 25 runTestsForFile: (...args) => runTestsForFile(...args), 26 runTests: (...args) => runTests(...args), 27 simpleLLMCall: (...args) => simpleLLMCall(...args), 28 execSync: (...args) => execSync(...args), 29 readFileCoverage: (path, enc) => fs.readFile(path, enc), 30 }; 31 32 export class DeveloperAgent extends BaseAgent { 33 constructor() { 34 super('developer', ['base.md', 'developer.md']); 35 } 36 37 /** 38 * Process a developer task 39 * 40 * @param {Object} task - Task object 41 * @returns {Promise<void>} 42 */ 43 async processTask(task) { 44 try { 45 // Validate context exists 46 if (!task.context_json) { 47 throw new Error('Task context is required'); 48 } 49 50 const context = 51 typeof task.context_json === 'string' ? JSON.parse(task.context_json) : task.context_json; 52 53 // Ensure context is attached to task for handlers 54 task.context_json = context; 55 56 switch (task.task_type) { 57 case 'implementation_plan': 58 await this.createImplementationPlan(task); 59 break; 60 61 case 'fix_bug': 62 await this.fixBug(task); 63 break; 64 65 case 'implement_feature': 66 await this.implementFeature(task); 67 break; 68 69 case 'refactor_code': 70 await this.refactorCode(task); 71 break; 72 73 case 'apply_feedback': 74 await this.applyFeedback(task); 75 break; 76 77 default: 78 // Unknown task types - delegate to correct agent via task routing 79 await this.log('warn', 'Unknown task type received, delegating', { 80 task_id: task.id, 81 task_type: task.task_type, 82 }); 83 await this.delegateToCorrectAgent(task); 84 } 85 } catch (error) { 86 await this.log('error', `Developer task ${task.id} failed: ${error.message}`, { 87 task_id: task.id, 88 task_type: task.task_type, 89 error: error.message, 90 stack: error.stack, 91 }); 92 throw error; // Re-throw so task manager can handle 93 } 94 } 95 96 /** 97 * Fix a bug 98 * 99 * @param {Object} task - Task with bug details in context_json 100 * @returns {Promise<void>} 101 */ 102 async fixBug(task) { 103 const context = task.context_json || {}; 104 const { 105 error_type, 106 error_message, 107 stack_trace, 108 stage, 109 suggested_fix, 110 file_path: contextFilePath, 111 files: contextFiles, 112 } = context; 113 114 if (!error_message) { 115 await this.failTask(task.id, 'Missing required field: error_message in context'); 116 return; 117 } 118 119 await this.log('info', 'Starting bug fix', { 120 task_id: task.id, 121 error_type, 122 error_message: 123 typeof error_message === 'string' ? error_message.substring(0, 200) : String(error_message), 124 }); 125 126 // Resolve file path: explicit context fields take priority over regex extraction 127 const filePath = 128 contextFilePath || 129 (Array.isArray(contextFiles) && contextFiles[0]) || 130 this.extractFilePath(error_message, stack_trace); 131 132 if (!filePath) { 133 await this.log('warn', 'Could not extract file path from error', { 134 task_id: task.id, 135 error_message, 136 }); 137 138 // Create a message to triage asking for clarification 139 const errorPreview = 140 typeof error_message === 'string' ? error_message.substring(0, 200) : String(error_message); 141 await this.askQuestion( 142 task.id, 143 'triage', 144 `Could not identify file from error. Error: ${errorPreview}. Please provide file path.` 145 ); 146 147 await this.blockTask(task.id, 'Waiting for file path clarification'); 148 return; 149 } 150 151 await this.log('info', 'Identified file for fix', { 152 task_id: task.id, 153 file_path: filePath, 154 }); 155 156 // FULL IMPLEMENTATION: Actually fix the bug 157 let analysis = null; // Declare outside try block so it's accessible later 158 159 try { 160 // 1. Read the file 161 const fileData = await _deps.readFile(filePath); 162 163 await this.log('info', 'Read file for bug fix', { 164 task_id: task.id, 165 file_size: fileData.size, 166 }); 167 168 // 2. Get file context (imports, dependencies, tests) 169 const context = await _deps.getFileContext(filePath); 170 171 // 3. Generate fix using Claude API 172 const fixPrompt = `You are an expert developer fixing a bug in a Node.js/ESM codebase. 173 174 Error Type: ${error_type} 175 Error Message: ${error_message} 176 ${stack_trace ? `Stack Trace:\n${stack_trace}` : ''} 177 Stage: ${stage} 178 File: ${filePath} 179 ${suggested_fix ? `Suggested Fix: ${suggested_fix}` : ''} 180 181 Recommended Action: ${this.getActionForErrorType(error_type)} 182 183 File Context: 184 - Imports: ${context.imports.join(', ')} 185 - Test Files: ${context.testFiles.join(', ')} 186 187 Current Code: 188 \`\`\`javascript 189 ${fileData.content} 190 \`\`\` 191 192 Generate a fix in JSON format: 193 { 194 "old_string": "exact string to replace (must match code EXACTLY, including whitespace)", 195 "new_string": "fixed code with proper error handling", 196 "explanation": "why this fix solves the problem", 197 "test_cases": ["test case 1", "test case 2"] 198 } 199 200 CRITICAL: 201 - old_string must match the vulnerable code EXACTLY (including indentation) 202 - Preserve code style and formatting 203 - Keep the fix minimal - only change what's necessary 204 - Add proper error handling (try-catch, null checks, validation) 205 - Follow existing patterns in the codebase`; 206 207 const fixResponse = await _deps.simpleLLMCall('developer', task.id, { 208 prompt: fixPrompt, 209 temperature: 0.2, 210 maxTokens: 2000, 211 }); 212 213 // Parse fix from Claude's response 214 // Try: 1) ```json block 2) ``` block 3) first JSON object anywhere in prose response 215 const jsonBlockMatch = 216 fixResponse.match(/```json\s*([\s\S]*?)\s*```/) || 217 fixResponse.match(/```\s*(\{[\s\S]*?\})\s*```/); 218 const jsonObjMatch = fixResponse.match(/(\{[\s\S]*\})/); 219 const jsonStr = jsonBlockMatch ? jsonBlockMatch[1] : jsonObjMatch ? jsonObjMatch[1] : null; 220 221 if (!jsonStr) { 222 await this.log('error', 'LLM returned prose analysis instead of JSON fix', { 223 task_id: task.id, 224 response_preview: fixResponse.substring(0, 300), 225 }); 226 await this.failTask( 227 task.id, 228 `LLM did not return JSON fix format. Response: ${fixResponse.substring(0, 150)}...` 229 ); 230 return; 231 } 232 233 let fix; 234 try { 235 fix = JSON.parse(jsonStr.trim()); 236 } catch (parseError) { 237 await this.log('error', 'Failed to parse LLM response as JSON', { 238 task_id: task.id, 239 error: parseError.message, 240 response_preview: fixResponse.substring(0, 200), 241 json_str_preview: jsonStr.substring(0, 200), 242 }); 243 throw new Error( 244 `Failed to parse fix JSON: ${parseError.message}. Response: ${fixResponse.substring(0, 100)}...` 245 ); 246 } 247 248 if (!fix.old_string || !fix.new_string) { 249 throw new Error('Invalid fix: missing old_string or new_string'); 250 } 251 252 await this.log('info', 'Generated fix using Claude API', { 253 task_id: task.id, 254 explanation: fix.explanation, 255 }); 256 257 // 4. Apply the fix using file-operations (with backup) 258 // Retry on database locked errors (pipeline may hold the DB briefly) 259 let editResult; 260 for (let attempt = 1; attempt <= 3; attempt++) { 261 try { 262 editResult = await _deps.editFile(filePath, { 263 oldContent: fix.old_string, 264 newContent: fix.new_string, 265 }); 266 break; 267 } catch (editErr) { 268 if (editErr.message?.includes('database is locked') && attempt < 3) { 269 await this.log('warn', `DB locked on edit attempt ${attempt}, retrying in 5s`, { 270 task_id: task.id, 271 }); 272 await new Promise(r => setTimeout(r, 5000)); 273 } else { 274 throw editErr; 275 } 276 } 277 } 278 279 await this.log('info', 'Applied fix to file', { 280 task_id: task.id, 281 backup_path: editResult.backupPath, 282 diff_lines: editResult.diff?.split('\n').length || 0, 283 }); 284 285 // 5. Run tests to verify fix 286 const testResult = await _deps.runTestsForFile(filePath); 287 288 if (!testResult.success) { 289 // Tests failed - restore from backup 290 await this.log('error', 'Tests failed after fix - restoring backup', { 291 task_id: task.id, 292 failures: testResult.failures, 293 }); 294 295 await _deps.restoreBackup(editResult.backupPath); 296 297 // Hand off to human for manual fix 298 await this.askQuestion( 299 task.id, 300 'architect', 301 `Automated fix failed for ${error_type} in ${filePath}. Tests failed:\n${testResult.failures.map(f => `- ${f.name}: ${f.message}`).join('\n')}\n\nOriginal error: ${error_message}\n\nAttempted fix: ${fix.explanation}\n\nPlease review manually.` 302 ); 303 304 await this.failTask(task.id, 'Automated fix failed - tests did not pass'); 305 return; 306 } 307 308 await this.log('info', 'Tests passed after fix', { 309 task_id: task.id, 310 tests_passed: testResult.stats.pass, 311 }); 312 313 // 6. Check coverage and commit (85% gate enforced by createCommit) 314 try { 315 const commitHash = await this.createCommit( 316 `fix(${stage}): ${error_type} in ${path.basename(filePath)}\n\n${fix.explanation}`, 317 [filePath], 318 task.id 319 ); 320 321 await this.log('info', 'Fix committed successfully', { 322 task_id: task.id, 323 commit_hash: commitHash, 324 }); 325 } catch (coverageError) { 326 // Coverage gate failed - task already escalated to Architect 327 await this.log('warn', 'Commit blocked by coverage gate', { 328 task_id: task.id, 329 error: coverageError.message, 330 }); 331 332 // Clean up the backup but keep the fix in place 333 await _deps.cleanupBackups(filePath, 5); 334 335 // Mark task as blocked (escalation already happened in createCommit) 336 await this.blockTask(task.id, coverageError.message); 337 return; 338 } 339 340 analysis = { 341 error_type, 342 file_path: filePath, 343 fix_applied: fix.explanation, 344 tests_passed: testResult.stats.pass, 345 coverage: testResult.coverage, 346 }; 347 348 await this.log('info', 'Bug fix complete', { 349 task_id: task.id, 350 analysis, 351 }); 352 } catch (error) { 353 await this.log('error', 'Bug fix implementation failed', { 354 task_id: task.id, 355 error: error.message, 356 }); 357 358 // Ask triage to re-categorize or provide more context 359 await this.askQuestion( 360 task.id, 361 'triage', 362 `Failed to fix ${error_type} in ${filePath}: ${error.message}. Original error: ${error_message}. Please provide more context or re-categorize.` 363 ); 364 365 await this.failTask(task.id, `Failed to apply automated fix: ${error.message}`); 366 return; 367 } 368 369 // Create QA verification task 370 const qaTaskId = await this.createTask({ 371 task_type: 'verify_fix', 372 assigned_to: 'qa', 373 priority: task.priority || 5, 374 parent_task_id: task.id, 375 context: { 376 original_error: error_message, 377 fix_analysis: analysis, 378 files_changed: [filePath], 379 test_instructions: `Verify ${error_type} fix in ${filePath}`, 380 }, 381 }); 382 383 // Send handoff message 384 await this.handoff( 385 task.id, 386 'qa', 387 `Bug fix complete for ${error_type} in ${filePath}. Ready for verification.`, 388 { qa_task_id: qaTaskId } 389 ); 390 391 await this.completeTask(task.id, { 392 files_analyzed: [filePath], 393 fix_type: error_type, 394 qa_task_id: qaTaskId, 395 note: 'Analysis complete - actual code changes not implemented in this phase', 396 }); 397 } 398 399 /** 400 * Extract file path from error message or stack trace 401 * 402 * @param {string} errorMessage - Error message 403 * @param {string} [stackTrace] - Stack trace 404 * @returns {string|null} - File path or null 405 */ 406 extractFilePath(errorMessage, stackTrace = '') { 407 const combined = `${errorMessage}\n${stackTrace}`; 408 409 // Priority 1: Look for explicit "File:" or "Files:" prefix (most reliable) 410 // Example: "Files: src/utils/stealth-browser.js, package.json" 411 const filesMatch = combined.match(/Files?:\s+([^\s,]+\.js)/i); 412 if (filesMatch) { 413 return filesMatch[1]; 414 } 415 416 // Priority 2: Look for file paths in stack trace 417 // Example: at Object.<anonymous> (/path/to/file.js:123:45) 418 const stackMatch = combined.match(/\(([^)]+\.js):(\d+):(\d+)\)/); 419 if (stackMatch) { 420 return stackMatch[1]; 421 } 422 423 // Priority 3: Look for nested src paths (src/utils/file.js, src/agents/utils/file.js) 424 // Handles underscores, hyphens, and multiple directory levels 425 const nestedSrcMatch = combined.match(/(src\/[a-z0-9/_-]+\.js)/i); 426 if (nestedSrcMatch) { 427 return nestedSrcMatch[1]; 428 } 429 430 // Priority 4: Look for other common directories (tests, scripts, etc.) 431 const commonDirMatch = combined.match( 432 /\b((?:tests|scripts|prompts|docs)\/[a-z0-9/_-]+\.js)\b/i 433 ); 434 if (commonDirMatch) { 435 return commonDirMatch[1]; 436 } 437 438 // Priority 5: Look for file paths with "in", "at", or "file:" prefix 439 // Example: "Error in src/scoring.js" - but only if it includes a directory 440 const pathWithDirMatch = combined.match(/(?:in|at|file:)\s+([a-z0-9/_-]+\/[a-z0-9/_-]+\.js)/i); 441 if (pathWithDirMatch) { 442 return pathWithDirMatch[1]; 443 } 444 445 return null; 446 } 447 448 /** 449 * Get recommended action for error type 450 * 451 * @param {string} errorType - Error type 452 * @returns {string} - Action description 453 */ 454 getActionForErrorType(errorType) { 455 switch (errorType) { 456 case 'null_pointer': 457 return 'Add null checks with optional chaining (?.) and default values'; 458 459 case 'database': 460 return 'Review SQL query, add proper error handling, check for duplicates before INSERT'; 461 462 case 'network': 463 return 'Wrap in retryWithBackoff(), add timeout handling'; 464 465 case 'api_error': 466 return 'Add rate limiting, implement exponential backoff, check API key validity'; 467 468 case 'configuration': 469 return 'Add environment variable validation at startup, update .env.example'; 470 471 case 'performance': 472 return 'Profile code, optimize queries, add caching if appropriate'; 473 474 case 'validation': 475 return 'Add input validation, sanitize user input, improve error messages'; 476 477 case 'integration': 478 return 'Add error handling for external service, implement fallback behavior'; 479 480 default: 481 return 'Investigate root cause, add appropriate error handling'; 482 } 483 } 484 485 /** 486 * Create implementation plan for approved design 487 * 488 * @param {Object} task - Task with design proposal 489 * @returns {Promise<void>} 490 */ 491 async createImplementationPlan(task) { 492 const context = task.context_json || {}; 493 const { design_proposal } = context; 494 495 if (!context || !design_proposal) { 496 await this.failTask(task.id, 'Missing required field: design_proposal in context'); 497 return; 498 } 499 500 await this.log('info', 'Creating implementation plan', { 501 task_id: task.id, 502 design: design_proposal.title, 503 }); 504 505 // 1. Break down design into specific steps 506 const steps = [ 507 { 508 step: 1, 509 action: 'Update database schema (if needed)', 510 files: design_proposal.requires_migration ? ['db/migrations/'] : [], 511 }, 512 { 513 step: 2, 514 action: 'Implement core logic', 515 files: design_proposal.files_affected || [], 516 }, 517 { 518 step: 3, 519 action: 'Write unit tests', 520 files: (design_proposal.files_affected || []).map(f => 521 f.replace('src/', 'tests/').replace('.js', '.test.js') 522 ), 523 }, 524 { 525 step: 4, 526 action: 'Update documentation', 527 files: ['README.md', 'CLAUDE.md'], 528 }, 529 ]; 530 531 // 2. Identify all files to modify 532 const filesToModify = design_proposal.files_affected || []; 533 534 // 3. Create test plan 535 const testPlan = { 536 unit_tests: filesToModify.map(f => `tests/${path.basename(f).replace('.js', '.test.js')}`), 537 integration_tests: [], 538 coverage_target: 85, // Minimum required coverage 539 }; 540 541 // 4. Create risk mitigations 542 const risksMitigations = (design_proposal.risks || []).map(risk => ({ 543 risk, 544 mitigation: 'Add error handling and comprehensive tests', 545 })); 546 547 const plan = { 548 summary: `Implementation plan for ${design_proposal.title}`, 549 steps, 550 files_to_modify: filesToModify, 551 test_plan: testPlan, 552 estimated_hours: design_proposal.estimated_effort || 4, 553 risks_mitigations: risksMitigations, 554 }; 555 556 // 5. Request Architect approval 557 await this.requestArchitectApproval(task.id, plan); 558 559 await this.log('info', 'Implementation plan created, awaiting Architect approval', { 560 task_id: task.id, 561 }); 562 } 563 564 /** 565 * Implement a feature 566 * 567 * @param {Object} task - Task with feature details 568 * @returns {Promise<void>} 569 */ 570 async implementFeature(task) { 571 const context = task.context_json || {}; 572 573 if (!context) { 574 await this.failTask(task.id, 'Missing context_json in task'); 575 return; 576 } 577 578 // Validate workflow: Must have approved implementation_plan as parent 579 const validation = await this.validateWorkflowDependencies(task); 580 581 if (!validation.valid) { 582 // If missing design_proposal, auto-create it instead of failing 583 if ( 584 validation.requiredPrerequisite && 585 validation.requiredPrerequisite.task_type === 'design_proposal' 586 ) { 587 await this.log('info', 'Auto-creating design_proposal prerequisite', { 588 task_id: task.id, 589 feature: context.feature_description, 590 }); 591 592 // Derive feature_description from whichever context field is available 593 const derivedDescription = 594 context.feature_description || 595 context.task_name || 596 context.description || 597 (context.files_to_modify?.length 598 ? `Implement feature affecting: ${context.files_to_modify.join(', ')}` 599 : null); 600 601 if (!derivedDescription) { 602 await this.failTask( 603 task.id, 604 'Cannot auto-create design_proposal: no feature_description, task_name, or description in context' 605 ); 606 return; 607 } 608 609 const designTaskId = await this.createTask({ 610 ...validation.requiredPrerequisite, 611 context: { 612 feature_description: derivedDescription, 613 requirements: context.requirements, 614 files_to_modify: context.files_to_modify, 615 }, 616 }); 617 618 // Block current task until design is approved 619 await this.blockTask( 620 task.id, 621 `Waiting for design_proposal (task #${designTaskId}) approval` 622 ); 623 return; 624 } 625 626 // For other validation failures, fail the task 627 await this.failTask(task.id, validation.reason); 628 return; 629 } 630 631 const { feature_description, requirements, files_to_modify } = context; 632 633 await this.log('info', 'Starting feature implementation', { 634 task_id: task.id, 635 feature: feature_description, 636 }); 637 638 // FULL IMPLEMENTATION: Actually implement the feature 639 try { 640 const filesToModify = files_to_modify || []; 641 const modifiedFiles = []; 642 643 // 1. Read parent task to get approved design proposal 644 const { getTaskById } = await import('./utils/task-manager.js'); 645 const parentTask = task.parent_task_id ? getTaskById(task.parent_task_id) : null; 646 const rawResult = parentTask?.result_json; 647 const parsedResult = typeof rawResult === 'string' ? JSON.parse(rawResult) : rawResult; 648 const designProposal = parsedResult?.design_proposal ?? null; 649 650 // 2. For each file to modify, generate and apply changes 651 for (const file of filesToModify) { 652 await this.log('info', 'Implementing changes', { 653 task_id: task.id, 654 file, 655 }); 656 657 // Read existing file (or prepare for new file creation) 658 let existingContent = ''; 659 let fileExists = true; 660 661 try { 662 const fileData = await _deps.readFile(file); 663 existingContent = fileData.content; 664 } catch (error) { 665 // File doesn't exist - we'll create it 666 fileExists = false; 667 } 668 669 // Get file context 670 const context = fileExists 671 ? await _deps.getFileContext(file) 672 : { imports: [], testFiles: [] }; 673 674 // 3. Generate implementation using Claude API 675 const implementPrompt = `You are an expert developer implementing a feature in a Node.js/ESM codebase. 676 677 Feature: ${feature_description} 678 679 Requirements: 680 ${Array.isArray(requirements) ? requirements.map(r => `- ${r}`).join('\n') : requirements} 681 682 ${designProposal ? `Design Proposal:\n${JSON.stringify(designProposal, null, 2)}\n` : ''} 683 684 File: ${file} 685 ${fileExists ? `\nExisting Code:\n\`\`\`javascript\n${existingContent}\n\`\`\`` : '\nThis is a new file to create.'} 686 687 ${fileExists ? `File Context:\n- Imports: ${context.imports.join(', ')}\n- Test Files: ${context.testFiles.join(', ')}` : ''} 688 689 Generate the implementation in JSON format: 690 ${ 691 fileExists 692 ? `{ 693 "old_string": "section of code to replace (must match EXACTLY)", 694 "new_string": "new implementation with feature", 695 "explanation": "what this implementation does", 696 "test_cases": ["test case 1", "test case 2"] 697 }` 698 : `{ 699 "file_content": "complete new file content", 700 "explanation": "what this file does", 701 "test_cases": ["test case 1", "test case 2"] 702 }` 703 } 704 705 CRITICAL: 706 - Follow existing code patterns and style 707 - Add proper error handling 708 - Include JSDoc comments 709 - Keep it simple - avoid over-engineering 710 - ${fileExists ? 'old_string must match EXACTLY (including indentation)' : 'Include proper imports and exports'}`; 711 712 const implResponse = await _deps.simpleLLMCall('developer', task.id, { 713 prompt: implementPrompt, 714 temperature: 0.3, 715 maxTokens: 3000, 716 }); 717 718 // Parse implementation from Claude's response 719 const jsonMatch = 720 implResponse.match(/```json\s*([\s\S]*?)\s*```/) || 721 implResponse.match(/```\s*([\s\S]*?)\s*```/); 722 const jsonStr = jsonMatch ? jsonMatch[1] : implResponse; 723 const impl = JSON.parse(jsonStr); 724 725 // 4. Apply changes 726 let writeResult; 727 728 if (fileExists) { 729 if (!impl.old_string || !impl.new_string) { 730 throw new Error(`Invalid implementation for ${file}: missing old_string or new_string`); 731 } 732 733 writeResult = await _deps.editFile(file, { 734 oldContent: impl.old_string, 735 newContent: impl.new_string, 736 }); 737 } else { 738 if (!impl.file_content) { 739 throw new Error(`Invalid implementation for ${file}: missing file_content`); 740 } 741 742 writeResult = await _deps.writeFile(file, impl.file_content); 743 } 744 745 modifiedFiles.push(file); 746 747 await this.log('info', 'Applied implementation', { 748 task_id: task.id, 749 file, 750 backup_path: writeResult.backupPath, 751 }); 752 } 753 754 // 5. Run tests for all modified files 755 await this.log('info', 'Running tests for modified files', { 756 task_id: task.id, 757 files: modifiedFiles, 758 }); 759 760 const testResult = await _deps.runTests({ files: modifiedFiles, coverage: true }); 761 762 if (!testResult.success) { 763 // Tests failed - restore all backups 764 await this.log('error', 'Tests failed after implementation - restoring backups', { 765 task_id: task.id, 766 failures: testResult.failures, 767 }); 768 769 for (const file of modifiedFiles) { 770 const backups = await _deps.listBackups(file); 771 if (backups.length > 0) { 772 await _deps.restoreBackup(backups[0]); // Restore most recent backup 773 } 774 } 775 776 await this.failTask( 777 task.id, 778 `Feature implementation failed tests:\n${testResult.failures.map(f => `- ${f.name}: ${f.message}`).join('\n')}` 779 ); 780 return; 781 } 782 783 await this.log('info', 'Tests passed after implementation', { 784 task_id: task.id, 785 tests_passed: testResult.stats.pass, 786 }); 787 788 // 6. Commit with coverage gate 789 try { 790 const commitHash = await this.createCommit( 791 `feat: ${feature_description}\n\n${requirements ? (Array.isArray(requirements) ? requirements.join(', ') : requirements) : ''}`, 792 modifiedFiles, 793 task.id 794 ); 795 796 await this.log('info', 'Feature committed successfully', { 797 task_id: task.id, 798 commit_hash: commitHash, 799 files_modified: modifiedFiles.length, 800 }); 801 } catch (coverageError) { 802 // Coverage gate failed - escalated to Architect 803 await this.blockTask(task.id, coverageError.message); 804 return; 805 } 806 } catch (error) { 807 await this.log('error', 'Feature implementation failed', { 808 task_id: task.id, 809 error: error.message, 810 }); 811 812 await this.failTask(task.id, `Failed to implement feature: ${error.message}`); 813 return; 814 } 815 816 // Create QA task for testing 817 const qaTaskId = await this.createTask({ 818 task_type: 'write_test', 819 assigned_to: 'qa', 820 priority: task.priority || 5, 821 parent_task_id: task.id, 822 context: { 823 feature: feature_description, 824 requirements, 825 files_changed: files_to_modify || [], 826 test_instructions: `Write tests for new feature: ${feature_description}`, 827 }, 828 }); 829 830 await this.handoff( 831 task.id, 832 'qa', 833 `Feature implementation complete: ${feature_description}. Ready for testing.`, 834 { qa_task_id: qaTaskId } 835 ); 836 837 await this.completeTask(task.id, { 838 feature: feature_description, 839 qa_task_id: qaTaskId, 840 note: 'Analysis complete - actual implementation not done in this phase', 841 }); 842 } 843 844 /** 845 * Refactor code 846 * 847 * @param {Object} task - Task with refactoring details 848 * @returns {Promise<void>} 849 */ 850 async refactorCode(task) { 851 const context = task.context_json || {}; 852 const { file_path, reason, complexity_issues } = context; 853 854 if (!context || !file_path) { 855 await this.failTask(task.id, 'Missing required field: file_path in context'); 856 return; 857 } 858 859 await this.log('info', 'Starting refactoring', { 860 task_id: task.id, 861 file: file_path, 862 reason, 863 }); 864 865 // FULL IMPLEMENTATION: Actually refactor the code 866 try { 867 // 1. Read the file 868 const fileData = await _deps.readFile(file_path); 869 const context = await _deps.getFileContext(file_path); 870 871 // 2. Run tests BEFORE refactoring (establish baseline) 872 const beforeTests = await _deps.runTestsForFile(file_path); 873 874 if (!beforeTests.success) { 875 await this.failTask( 876 task.id, 877 `Cannot refactor - tests are already failing: ${beforeTests.failures.map(f => f.name).join(', ')}` 878 ); 879 return; 880 } 881 882 await this.log('info', 'Baseline tests passed', { 883 task_id: task.id, 884 tests_passed: beforeTests.stats.pass, 885 }); 886 887 // 3. Generate refactoring using Claude API 888 const refactorPrompt = `You are an expert developer refactoring code for better maintainability. 889 890 File: ${file_path} 891 Reason for refactoring: ${reason} 892 893 Complexity Issues: 894 ${Array.isArray(complexity_issues) ? complexity_issues.map(i => `- ${i}`).join('\n') : complexity_issues} 895 896 Current Code: 897 \`\`\`javascript 898 ${fileData.content} 899 \`\`\` 900 901 File Context: 902 - Imports: ${context.imports.join(', ')} 903 - Test Files: ${context.testFiles.join(', ')} 904 905 Refactor this code to address the complexity issues while maintaining exact functionality. 906 907 Generate refactoring in JSON format: 908 { 909 "old_string": "entire current file content (must match EXACTLY)", 910 "new_string": "refactored code with improvements", 911 "changes": ["change 1", "change 2", "..."], 912 "explanation": "why these changes improve maintainability" 913 } 914 915 CRITICAL: 916 - MUST maintain exact same functionality (tests must pass) 917 - Extract complex functions into smaller helper functions 918 - Reduce nesting depth (max 4 levels) 919 - Keep functions under 50 lines 920 - Improve variable names for clarity 921 - Add JSDoc comments for exported functions 922 - Preserve all imports/exports 923 - Keep existing code style`; 924 925 const refactorResponse = await _deps.simpleLLMCall('developer', task.id, { 926 prompt: refactorPrompt, 927 temperature: 0.2, 928 maxTokens: 4000, 929 }); 930 931 // Parse refactoring from Claude's response 932 const jsonMatch = 933 refactorResponse.match(/```json\s*([\s\S]*?)\s*```/) || 934 refactorResponse.match(/```\s*([\s\S]*?)\s*```/); 935 const jsonStr = jsonMatch ? jsonMatch[1] : refactorResponse; 936 const refactor = JSON.parse(jsonStr); 937 938 if (!refactor.old_string || !refactor.new_string) { 939 throw new Error('Invalid refactoring: missing old_string or new_string'); 940 } 941 942 await this.log('info', 'Generated refactoring', { 943 task_id: task.id, 944 changes: refactor.changes, 945 }); 946 947 // 4. Apply refactoring 948 const editResult = await _deps.editFile(file_path, { 949 oldContent: refactor.old_string, 950 newContent: refactor.new_string, 951 }); 952 953 await this.log('info', 'Applied refactoring', { 954 task_id: task.id, 955 backup_path: editResult.backupPath, 956 }); 957 958 // 5. Run tests to ensure functionality preserved 959 const afterTests = await _deps.runTestsForFile(file_path); 960 961 if (!afterTests.success) { 962 // Tests failed - restore backup 963 await this.log('error', 'Tests failed after refactoring - restoring backup', { 964 task_id: task.id, 965 failures: afterTests.failures, 966 }); 967 968 await _deps.restoreBackup(editResult.backupPath); 969 970 await this.failTask( 971 task.id, 972 `Refactoring broke tests: ${afterTests.failures.map(f => `${f.name}: ${f.message}`).join(', ')}` 973 ); 974 return; 975 } 976 977 await this.log('info', 'Tests passed after refactoring', { 978 task_id: task.id, 979 tests_passed: afterTests.stats.pass, 980 }); 981 982 // 6. Commit refactoring 983 try { 984 const commitHash = await this.createCommit( 985 `refactor(${path.basename(file_path)}): ${reason}\n\nChanges:\n${refactor.changes.map(c => `- ${c}`).join('\n')}`, 986 [file_path], 987 task.id 988 ); 989 990 await this.log('info', 'Refactoring committed', { 991 task_id: task.id, 992 commit_hash: commitHash, 993 }); 994 } catch (coverageError) { 995 await this.blockTask(task.id, coverageError.message); 996 return; 997 } 998 } catch (error) { 999 await this.log('error', 'Refactoring failed', { 1000 task_id: task.id, 1001 error: error.message, 1002 }); 1003 1004 await this.failTask(task.id, `Failed to refactor: ${error.message}`); 1005 return; 1006 } 1007 1008 // Create QA task to verify refactoring 1009 const qaTaskId = await this.createTask({ 1010 task_type: 'verify_fix', 1011 assigned_to: 'qa', 1012 priority: task.priority || 5, 1013 parent_task_id: task.id, 1014 context: { 1015 type: 'refactoring', 1016 file: file_path, 1017 files_changed: [file_path], 1018 test_instructions: `Verify refactoring maintains behavior for ${file_path}`, 1019 }, 1020 }); 1021 1022 await this.handoff( 1023 task.id, 1024 'qa', 1025 `Refactoring complete for ${file_path}. Ready for verification.`, 1026 { qa_task_id: qaTaskId } 1027 ); 1028 1029 await this.completeTask(task.id, { 1030 file: file_path, 1031 qa_task_id: qaTaskId, 1032 note: 'Analysis complete - actual refactoring not done in this phase', 1033 }); 1034 } 1035 1036 /** 1037 * Apply feedback from other agents 1038 * 1039 * @param {Object} task - Task with feedback details 1040 * @returns {Promise<void>} 1041 */ 1042 async applyFeedback(task) { 1043 const context = task.context_json || {}; 1044 const { feedback_from, feedback_message, files_to_update } = context; 1045 1046 if (!context || !feedback_message) { 1047 await this.failTask(task.id, 'Missing required field: feedback_message in context'); 1048 return; 1049 } 1050 1051 const feedbackPreview = 1052 typeof feedback_message === 'string' 1053 ? feedback_message.substring(0, 200) 1054 : String(feedback_message); 1055 1056 await this.log('info', 'Applying feedback', { 1057 task_id: task.id, 1058 from: feedback_from, 1059 feedback: feedbackPreview, 1060 }); 1061 1062 // FULL IMPLEMENTATION: Actually apply the feedback 1063 try { 1064 const filesToUpdate = files_to_update || []; 1065 const modifiedFiles = []; 1066 1067 // 1. For each file to update, apply feedback 1068 for (const file of filesToUpdate) { 1069 await this.log('info', 'Applying feedback to file', { 1070 task_id: task.id, 1071 file, 1072 }); 1073 1074 // Read existing file 1075 const fileData = await _deps.readFile(file); 1076 const context = await _deps.getFileContext(file); 1077 1078 // 2. Generate changes based on feedback using Claude API 1079 const feedbackPrompt = `You are an expert developer addressing code review feedback. 1080 1081 Feedback From: ${feedback_from} 1082 Feedback Message: 1083 ${feedback_message} 1084 1085 File: ${file} 1086 1087 Current Code: 1088 \`\`\`javascript 1089 ${fileData.content} 1090 \`\`\` 1091 1092 File Context: 1093 - Imports: ${context.imports.join(', ')} 1094 - Test Files: ${context.testFiles.join(', ')} 1095 1096 Address the feedback by making the requested changes. 1097 1098 Generate changes in JSON format: 1099 { 1100 "old_string": "section of code to change (must match EXACTLY)", 1101 "new_string": "updated code addressing feedback", 1102 "explanation": "how this addresses the feedback", 1103 "addresses": ["feedback point 1", "feedback point 2"] 1104 } 1105 1106 CRITICAL: 1107 - old_string must match EXACTLY (including indentation) 1108 - Address ALL points in the feedback 1109 - Maintain code functionality 1110 - Follow existing patterns and style`; 1111 1112 const feedbackResponse = await _deps.simpleLLMCall('developer', task.id, { 1113 prompt: feedbackPrompt, 1114 temperature: 0.2, 1115 maxTokens: 2000, 1116 }); 1117 1118 // Parse changes from Claude's response 1119 const jsonMatch = 1120 feedbackResponse.match(/```json\s*([\s\S]*?)\s*```/) || 1121 feedbackResponse.match(/```\s*([\s\S]*?)\s*```/); 1122 const jsonStr = jsonMatch ? jsonMatch[1] : feedbackResponse; 1123 const changes = JSON.parse(jsonStr); 1124 1125 if (!changes.old_string || !changes.new_string) { 1126 throw new Error( 1127 `Invalid feedback response for ${file}: missing old_string or new_string` 1128 ); 1129 } 1130 1131 // 3. Apply changes 1132 const editResult = await _deps.editFile(file, { 1133 oldContent: changes.old_string, 1134 newContent: changes.new_string, 1135 }); 1136 1137 modifiedFiles.push(file); 1138 1139 await this.log('info', 'Applied feedback changes', { 1140 task_id: task.id, 1141 file, 1142 backup_path: editResult.backupPath, 1143 }); 1144 } 1145 1146 // 4. Run tests 1147 const testResult = 1148 modifiedFiles.length > 0 1149 ? await _deps.runTests({ files: modifiedFiles, coverage: true }) 1150 : { success: true, stats: { pass: 0 } }; 1151 1152 if (!testResult.success) { 1153 // Tests failed - restore backups 1154 await this.log('error', 'Tests failed after applying feedback - restoring backups', { 1155 task_id: task.id, 1156 failures: testResult.failures, 1157 }); 1158 1159 for (const file of modifiedFiles) { 1160 const backups = await _deps.listBackups(file); 1161 if (backups.length > 0) { 1162 await _deps.restoreBackup(backups[0]); 1163 } 1164 } 1165 1166 await this.failTask( 1167 task.id, 1168 `Feedback application failed tests: ${testResult.failures.map(f => `${f.name}: ${f.message}`).join(', ')}` 1169 ); 1170 return; 1171 } 1172 1173 await this.log('info', 'Tests passed after applying feedback', { 1174 task_id: task.id, 1175 tests_passed: testResult.stats.pass, 1176 }); 1177 1178 // 5. Commit changes 1179 if (modifiedFiles.length > 0) { 1180 try { 1181 const commitHash = await this.createCommit( 1182 `fix: address ${feedback_from} feedback\n\n${feedbackPreview}`, 1183 modifiedFiles, 1184 task.id 1185 ); 1186 1187 await this.log('info', 'Feedback changes committed', { 1188 task_id: task.id, 1189 commit_hash: commitHash, 1190 }); 1191 } catch (coverageError) { 1192 await this.blockTask(task.id, coverageError.message); 1193 return; 1194 } 1195 } 1196 } catch (error) { 1197 await this.log('error', 'Failed to apply feedback', { 1198 task_id: task.id, 1199 error: error.message, 1200 }); 1201 1202 await this.failTask(task.id, `Failed to apply feedback: ${error.message}`); 1203 return; 1204 } 1205 1206 // Send response back to feedback provider 1207 await this.sendAnswer( 1208 task.id, 1209 feedback_from, 1210 'Feedback addressed. Changes analyzed and ready for re-verification.' 1211 ); 1212 1213 await this.completeTask(task.id, { 1214 feedback_from, 1215 files_updated: files_to_update, 1216 note: 'Analysis complete - actual changes not done in this phase', 1217 }); 1218 } 1219 1220 /** 1221 * Run tests for specific files 1222 * 1223 * @param {string[]} files - Files to test 1224 * @returns {Promise<{success: boolean, output: string}>} 1225 */ 1226 async runTests(files = []) { 1227 try { 1228 await this.log('info', 'Running tests', { files }); 1229 1230 // If specific files provided, run only those tests 1231 let command = 'npm test'; 1232 if (files.length > 0) { 1233 const testFiles = files.map(f => f.replace(/\.js$/, '.test.js')).join(' '); 1234 command = `npm test ${testFiles}`; 1235 } 1236 1237 const output = _deps.execSync(command, { 1238 encoding: 'utf8', 1239 timeout: 60000, // 1 minute timeout 1240 }); 1241 1242 await this.log('info', 'Tests passed', { files }); 1243 1244 return { success: true, output }; 1245 } catch (error) { 1246 await this.log('error', 'Tests failed', { 1247 files, 1248 error: error.message, 1249 }); 1250 1251 return { success: false, output: error.message }; 1252 } 1253 } 1254 1255 /** 1256 * Check coverage before committing (CRITICAL GATE: 85%+) 1257 * 1258 * @param {string[]} files - Files to check coverage for 1259 * @param {number} taskId - Current task ID 1260 * @returns {Promise<{canCommit: boolean, coverage: Object, reason?: string}>} 1261 */ 1262 async checkCoverageBeforeCommit(files, taskId) { 1263 await this.log('info', 'Checking coverage before commit (85% gate)', { 1264 task_id: taskId, 1265 files, 1266 }); 1267 1268 // Filter to only source files (not tests, not docs) 1269 const sourceFiles = files.filter( 1270 f => f.startsWith('src/') && f.endsWith('.js') && !f.includes('.test.') 1271 ); 1272 1273 if (sourceFiles.length === 0) { 1274 // No source files to check (docs/config/test-only changes) 1275 return { canCommit: true, coverage: {} }; 1276 } 1277 1278 const coverage = await this.getFileCoverage(sourceFiles); 1279 1280 const belowThreshold = []; 1281 for (const [file, cov] of Object.entries(coverage)) { 1282 if (cov < 85) { 1283 belowThreshold.push({ file, coverage: cov, gap: 85 - cov }); 1284 } 1285 } 1286 1287 if (belowThreshold.length > 0) { 1288 await this.log('warn', 'Coverage below 85% threshold', { 1289 task_id: taskId, 1290 below_threshold: belowThreshold, 1291 }); 1292 1293 return { 1294 canCommit: false, 1295 coverage, 1296 belowThreshold, 1297 reason: `Coverage gate: ${belowThreshold.length} file(s) below 85%`, 1298 }; 1299 } 1300 1301 await this.log('info', 'Coverage check passed - 85%+ on all files', { 1302 task_id: taskId, 1303 coverage, 1304 }); 1305 1306 return { canCommit: true, coverage }; 1307 } 1308 1309 /** 1310 * Attempt to write tests to meet coverage threshold 1311 * 1312 * @param {Array<{file: string, coverage: number, gap: number}>} belowThreshold - Files needing tests 1313 * @param {number} taskId - Current task ID 1314 * @returns {Promise<boolean>} - True if tests written successfully 1315 */ 1316 async attemptWriteTestsForCoverage(belowThreshold, taskId) { 1317 await this.log('info', 'Attempting to write tests to meet 85% coverage', { 1318 task_id: taskId, 1319 files_needing_tests: belowThreshold, 1320 }); 1321 1322 try { 1323 // For each file below threshold, analyze and generate tests 1324 for (const { file, coverage, gap } of belowThreshold) { 1325 await this.log('info', `Analyzing coverage gaps for ${file}`, { 1326 task_id: taskId, 1327 current_coverage: coverage, 1328 gap, 1329 }); 1330 1331 // 1. Read the source file 1332 const sourceCode = await _deps.readFileCoverage(file, 'utf8'); 1333 1334 // 2. Run coverage with detailed output to find uncovered lines 1335 const coverageData = await this.getDetailedCoverage(file); 1336 if (!coverageData || !coverageData.uncoveredLines) { 1337 await this.log('warn', `Could not get detailed coverage for ${file}`, { 1338 task_id: taskId, 1339 }); 1340 continue; 1341 } 1342 1343 // 3. Determine test file path 1344 const testFile = this.getTestFilePath(file); 1345 1346 // 4. Read existing tests (if any) 1347 let existingTests = ''; 1348 try { 1349 existingTests = await _deps.readFileCoverage(testFile, 'utf8'); 1350 } catch (err) { 1351 // Test file doesn't exist yet - will create new one 1352 } 1353 1354 // 5. Ask QA agent to generate tests for uncovered lines 1355 // Delegate to QA agent which has test-writing capability 1356 await this.log('info', `Delegating test generation to QA agent`, { 1357 task_id: taskId, 1358 file, 1359 test_file: testFile, 1360 }); 1361 1362 const qaTaskId = await this.createTask({ 1363 task_type: 'run_tests', 1364 assigned_to: 'qa', 1365 priority: 5, 1366 parent_task_id: taskId, 1367 context: { 1368 source_file: file, 1369 test_file: testFile, 1370 uncovered_lines: coverageData.uncoveredLines, 1371 current_coverage: coverage, 1372 target_coverage: 85, 1373 delegated_from: taskId, 1374 }, 1375 }); 1376 1377 await this.log('info', `Created QA task ${qaTaskId} for test generation`, { 1378 task_id: taskId, 1379 qa_task_id: qaTaskId, 1380 }); 1381 } 1382 1383 // Return false to indicate we delegated to QA 1384 // QA will generate tests and re-run coverage 1385 return false; 1386 } catch (error) { 1387 await this.log('error', 'Failed to analyze coverage gaps', { 1388 task_id: taskId, 1389 error: error.message, 1390 }); 1391 return false; 1392 } 1393 } 1394 1395 /** 1396 * Get detailed coverage data for a file 1397 * 1398 * @param {string} filePath - Source file path 1399 * @returns {Promise<Object|null>} - Coverage data with uncovered lines 1400 */ 1401 async getDetailedCoverage(filePath) { 1402 try { 1403 // Run c8 with JSON output 1404 const testFile = this.getTestFilePath(filePath); 1405 1406 const result = _deps.execSync( 1407 `npx c8 --reporter=json --reporter=text node --test ${testFile}`, 1408 { 1409 encoding: 'utf8', 1410 stdio: ['pipe', 'pipe', 'pipe'], 1411 } 1412 ); 1413 1414 // Parse JSON coverage output 1415 const coverageDir = path.join(process.cwd(), 'coverage'); 1416 const coverageFile = path.join(coverageDir, 'coverage-final.json'); 1417 1418 let coverageJson; 1419 try { 1420 const coverageData = await _deps.readFileCoverage(coverageFile, 'utf8'); 1421 coverageJson = JSON.parse(coverageData); 1422 } catch (err) { 1423 return null; 1424 } 1425 1426 // Find coverage for our specific file 1427 const fileKey = Object.keys(coverageJson).find(key => key.endsWith(filePath)); 1428 if (!fileKey) { 1429 return null; 1430 } 1431 1432 const fileCoverage = coverageJson[fileKey]; 1433 1434 // Extract uncovered lines 1435 const uncoveredLines = []; 1436 const statementMap = fileCoverage.statementMap || {}; 1437 const s = fileCoverage.s || {}; 1438 1439 for (const [id, count] of Object.entries(s)) { 1440 if (count === 0 && statementMap[id]) { 1441 const loc = statementMap[id]; 1442 uncoveredLines.push({ 1443 start: loc.start.line, 1444 end: loc.end.line, 1445 }); 1446 } 1447 } 1448 1449 return { 1450 uncoveredLines, 1451 coverage: fileCoverage.lines.pct || 0, 1452 }; 1453 } catch (error) { 1454 return null; 1455 } 1456 } 1457 1458 /** 1459 * Get test file path for a source file 1460 * 1461 * @param {string} sourceFile - Source file path 1462 * @returns {string} - Test file path 1463 */ 1464 getTestFilePath(sourceFile) { 1465 const parsed = path.parse(sourceFile); 1466 return path.join('tests', `${parsed.name}.test.js`); 1467 } 1468 1469 /** 1470 * Escalate coverage issue to human review 1471 * 1472 * @param {Array<{file: string, coverage: number, gap: number}>} belowThreshold - Files below threshold 1473 * @param {number} taskId - Current task ID 1474 * @returns {Promise<void>} 1475 */ 1476 async escalateCoverageToHuman(belowThreshold, taskId) { 1477 const filesStr = belowThreshold.map(f => `${f.file} (${f.coverage}%)`).join(', '); 1478 1479 await this.log('warn', 'Escalating coverage issue to human review', { 1480 task_id: taskId, 1481 below_threshold: belowThreshold, 1482 }); 1483 1484 // Create human review question via Architect agent 1485 // Architect can provide guidance on testability improvements 1486 await this.askQuestion( 1487 taskId, 1488 'architect', 1489 `Cannot achieve 85% coverage for: ${filesStr}. Options:\n` + 1490 `(a) Refactor for better testability\n` + 1491 `(b) Accept lower coverage with technical debt justification\n` + 1492 `(c) Provide manual test guidance for uncovered branches\n\n` + 1493 `Please advise on approach.`, 1494 { below_threshold: belowThreshold, threshold: 85 } 1495 ); 1496 } 1497 1498 /** 1499 * Create a git commit (with 85% coverage gate) 1500 * 1501 * @param {string} message - Commit message 1502 * @param {string[]} files - Files to commit 1503 * @param {number} taskId - Current task ID 1504 * @returns {Promise<string>} - Commit hash 1505 */ 1506 async createCommit(message, files, taskId) { 1507 // CRITICAL: Check coverage before committing (85% gate) 1508 const coverageCheck = await this.checkCoverageBeforeCommit(files, taskId); 1509 1510 if (!coverageCheck.canCommit) { 1511 await this.log('error', 'Cannot commit - coverage below 85%', { 1512 task_id: taskId, 1513 files, 1514 below_threshold: coverageCheck.belowThreshold, 1515 }); 1516 1517 // Try to write tests automatically 1518 const testsWritten = await this.attemptWriteTestsForCoverage( 1519 coverageCheck.belowThreshold, 1520 taskId 1521 ); 1522 1523 if (!testsWritten) { 1524 // Escalate to human for guidance 1525 await this.escalateCoverageToHuman(coverageCheck.belowThreshold, taskId); 1526 1527 throw new Error( 1528 `Coverage gate failed: ${coverageCheck.belowThreshold.length} file(s) below 85%. ` + 1529 `Escalated to Architect for guidance.` 1530 ); 1531 } 1532 1533 // Re-check coverage after writing tests 1534 const recheckResult = await this.checkCoverageBeforeCommit(files, taskId); 1535 if (!recheckResult.canCommit) { 1536 throw new Error( 1537 `Coverage still below 85% after test generation. Manual intervention needed.` 1538 ); 1539 } 1540 } 1541 1542 try { 1543 // Add Co-Authored-By trailer 1544 const commitMessage = `${message}\n\nCo-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`; 1545 1546 // Stage files 1547 for (const file of files) { 1548 _deps.execSync(`git add "${file}"`, { encoding: 'utf8' }); 1549 } 1550 1551 // Commit 1552 const hash = _deps 1553 .execSync(`git commit -m "${commitMessage}"`, { 1554 encoding: 'utf8', 1555 }) 1556 .trim(); 1557 1558 await this.log('info', 'Commit created', { 1559 files, 1560 message: message.substring(0, 100), 1561 hash, 1562 coverage: coverageCheck.coverage, 1563 }); 1564 1565 return hash; 1566 } catch (error) { 1567 await this.log('error', 'Commit failed', { 1568 files, 1569 error: error.message, 1570 }); 1571 1572 throw error; 1573 } 1574 } 1575 1576 /** 1577 * Get coverage for specific files 1578 * 1579 * @param {string[]} files - File paths 1580 * @returns {Promise<Object>} - Coverage by file 1581 */ 1582 async getFileCoverage(files) { 1583 try { 1584 // Run tests with coverage for specific files 1585 await this.log('info', 'Running coverage check', { files }); 1586 1587 _deps.execSync('npm test', { 1588 encoding: 'utf8', 1589 timeout: 120000, 1590 stdio: 'pipe', // Suppress output 1591 }); 1592 1593 // Read coverage summary 1594 const coverageData = JSON.parse( 1595 await _deps.readFileCoverage('coverage/coverage-summary.json', 'utf8') 1596 ); 1597 1598 const results = {}; 1599 for (const file of files) { 1600 // Try multiple path formats 1601 const projectRoot = process.cwd(); 1602 const absolutePath = path.join(projectRoot, file); 1603 1604 let fileData = coverageData[file] || coverageData[`/${file}`] || coverageData[absolutePath]; 1605 1606 if (!fileData) { 1607 // Try normalized path 1608 const normalized = file.replace(/^\/+/, ''); 1609 fileData = coverageData[normalized]; 1610 } 1611 1612 if (fileData) { 1613 // c8 reports line coverage in lines.pct 1614 results[file] = fileData.lines.pct; 1615 } else { 1616 await this.log('warn', 'Coverage data not found for file', { 1617 file, 1618 tried_paths: [file, `/${file}`, absolutePath], 1619 }); 1620 results[file] = 0; // Default to 0 if not found 1621 } 1622 } 1623 1624 return results; 1625 } catch (error) { 1626 await this.log('error', 'Failed to get coverage data', { 1627 error: error.message, 1628 }); 1629 1630 // Return 0 coverage for all files if can't read coverage 1631 const results = {}; 1632 for (const file of files) { 1633 results[file] = 0; 1634 } 1635 return results; 1636 } 1637 } 1638 1639 /** 1640 * Check if file exists 1641 * 1642 * @param {string} filePath - File path 1643 * @returns {Promise<boolean>} 1644 */ 1645 async fileExists(filePath) { 1646 try { 1647 await fs.access(filePath); 1648 return true; 1649 } catch { 1650 return false; 1651 } 1652 } 1653 }