/ src / agents / developer.js
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  }