aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
332 lines (283 loc) • 9.18 kB
JavaScript
/**
* Layer 2: PR Automation
*
* Runs automated PR reviews:
* - CodeRabbit integration
* - Quinn (@qa agent) automated review
*
* @module core/quality-gates/layer2-pr-automation
* @version 1.0.0
* @story 2.10 - Quality Gate Manager
*/
const { spawn } = require('child_process');
const fs = require('fs').promises;
const path = require('path');
const { BaseLayer } = require('./base-layer');
/**
* Layer 2: PR Automation checks
* @extends BaseLayer
*/
class Layer2PRAutomation extends BaseLayer {
/**
* Create Layer 2 instance
* @param {Object} config - Layer 2 configuration
*/
constructor(config = {}) {
super('Layer 2: PR Automation', config);
this.coderabbit = config.coderabbit || {};
this.quinn = config.quinn || {};
}
/**
* Execute all Layer 2 checks
* @param {Object} context - Execution context
* @returns {Promise<Object>} Layer results
*/
async execute(context = {}) {
this.reset();
this.startTimer();
const { verbose = false } = context;
if (!this.enabled) {
this.addResult({
check: 'layer2',
pass: true,
skipped: true,
message: 'Layer 2 disabled',
});
this.stopTimer();
return this.getSummary();
}
if (verbose) {
console.log('\n🤖 Layer 2: PR Automation');
console.log('━'.repeat(50));
}
// Run CodeRabbit
if (this.coderabbit?.enabled !== false) {
const coderabbitResult = await this.runCodeRabbit(context);
this.addResult(coderabbitResult);
}
// Run Quinn review
if (this.quinn?.enabled !== false) {
const quinnResult = await this.runQuinnReview(context);
this.addResult(quinnResult);
}
this.stopTimer();
if (verbose) {
const summary = this.getSummary();
const icon = summary.pass ? '✅' : '⚠️';
console.log(
`\n${icon} Layer 2 ${summary.pass ? 'PASSED' : 'HAS ISSUES'} (${this.formatDuration(summary.duration)})`,
);
}
return this.getSummary();
}
/**
* Run CodeRabbit review
* @param {Object} context - Execution context
* @returns {Promise<Object>} CodeRabbit result
*/
async runCodeRabbit(context = {}) {
const { verbose = false } = context;
const timeout = this.coderabbit.timeout || 900000; // 15 minutes default
if (verbose) {
console.log(' 🐰 Running CodeRabbit review...');
}
try {
// Check if CodeRabbit is available
const command =
this.coderabbit.command ||
"wsl bash -c 'cd ${PROJECT_ROOT} && ~/.local/bin/coderabbit --prompt-only -t uncommitted'";
const result = await this.runCommand(command, timeout);
// Parse CodeRabbit output for issues
const issues = this.parseCodeRabbitOutput(result.stdout + result.stderr);
const criticalCount = issues.filter((i) => i.severity === 'CRITICAL').length;
const highCount = issues.filter((i) => i.severity === 'HIGH').length;
const mediumCount = issues.filter((i) => i.severity === 'MEDIUM').length;
const lowCount = issues.filter((i) => i.severity === 'LOW').length;
// Block on CRITICAL issues
const blockOn = this.coderabbit.blockOn || ['CRITICAL'];
const hasBlockingIssues = issues.some((i) => blockOn.includes(i.severity));
const pass = !hasBlockingIssues;
const coderabbitResult = {
check: 'coderabbit',
pass,
issues: {
critical: criticalCount,
high: highCount,
medium: mediumCount,
low: lowCount,
total: issues.length,
},
details: issues,
duration: result.duration,
message: pass
? `CodeRabbit passed (${criticalCount} CRITICAL, ${highCount} HIGH)`
: `CodeRabbit found blocking issues (${criticalCount} CRITICAL)`,
};
if (verbose) {
const icon = pass ? '✓' : '⚠️';
console.log(
` ${icon} CodeRabbit: ${criticalCount} CRITICAL, ${highCount} HIGH, ${mediumCount} MEDIUM`,
);
}
return coderabbitResult;
} catch (error) {
// CodeRabbit not installed or not accessible - graceful degradation
if (error.message.includes('not found') || error.message.includes('command not found')) {
if (verbose) {
console.log(' ⏭️ CodeRabbit: Skipped (not installed)');
}
return {
check: 'coderabbit',
pass: true,
skipped: true,
message: 'CodeRabbit not installed - skipping (graceful degradation)',
};
}
return {
check: 'coderabbit',
pass: false,
error: error.message,
message: `CodeRabbit error: ${error.message}`,
};
}
}
/**
* Parse CodeRabbit output for issues
* @param {string} output - Command output
* @returns {Array<Object>} Parsed issues
*/
parseCodeRabbitOutput(output) {
const issues = [];
// Match patterns like "CRITICAL:", "HIGH:", etc.
const patterns = [
{ regex: /\bCRITICAL\b[:\s]+([^\n]+)/gi, severity: 'CRITICAL' },
{ regex: /\bHIGH\b[:\s]+([^\n]+)/gi, severity: 'HIGH' },
{ regex: /\bMEDIUM\b[:\s]+([^\n]+)/gi, severity: 'MEDIUM' },
{ regex: /\bLOW\b[:\s]+([^\n]+)/gi, severity: 'LOW' },
];
patterns.forEach(({ regex, severity }) => {
let match;
while ((match = regex.exec(output)) !== null) {
issues.push({
severity,
message: match[1].trim(),
raw: match[0],
});
}
});
return issues;
}
/**
* Run Quinn (@qa) automated review
* @param {Object} context - Execution context
* @returns {Promise<Object>} Quinn review result
*/
async runQuinnReview(context = {}) {
const { verbose = false } = context;
if (verbose) {
console.log(' 🧪 Running Quinn (@qa) review...');
}
try {
// Quinn review is typically triggered by the QA agent
// This integration point generates a review request
const suggestions = await this.generateQuinnSuggestions(context);
const blockingSuggestions = suggestions.filter((s) =>
this.quinn.severity?.block?.includes(s.severity),
);
const pass = blockingSuggestions.length === 0;
const quinnResult = {
check: 'quinn',
pass,
suggestions: suggestions.length,
blocking: blockingSuggestions.length,
details: suggestions,
message: pass
? `Quinn review: ${suggestions.length} suggestions`
: `Quinn review: ${blockingSuggestions.length} blocking issues`,
};
if (verbose) {
const icon = pass ? '✓' : '⚠️';
console.log(
` ${icon} Quinn: ${suggestions.length} suggestions, ${blockingSuggestions.length} blocking`,
);
}
return quinnResult;
} catch (error) {
return {
check: 'quinn',
pass: true, // Don't block on Quinn errors
skipped: true,
error: error.message,
message: `Quinn skipped: ${error.message}`,
};
}
}
/**
* Generate Quinn suggestions (placeholder for full integration)
* @param {Object} context - Execution context
* @returns {Promise<Array>} Suggestions
*/
async generateQuinnSuggestions(_context = {}) {
// This would integrate with the QA agent for automated review
// For now, return empty suggestions - full integration in Story 2.11
return [];
}
/**
* Save review results to reports directory
* @param {string} reportPath - Path to save report
* @returns {Promise<void>}
*/
async saveReport(reportPath) {
const summary = this.getSummary();
const report = {
timestamp: new Date().toISOString(),
layer: this.name,
...summary,
};
await fs.mkdir(path.dirname(reportPath), { recursive: true });
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
}
/**
* Run a command and capture output
* @param {string} command - Command to run
* @param {number} timeout - Timeout in milliseconds
* @returns {Promise<Object>} Command result
*/
runCommand(command, timeout = 60000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const options = {
shell: true,
cwd: process.cwd(),
env: { ...process.env },
};
const child = spawn(command, [], options);
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
const timer = setTimeout(() => {
child.kill();
reject(new Error(`Command timed out after ${timeout}ms`));
}, timeout);
child.on('close', (exitCode) => {
clearTimeout(timer);
resolve({
exitCode,
stdout,
stderr,
duration: Date.now() - startTime,
});
});
child.on('error', (error) => {
clearTimeout(timer);
reject(error);
});
});
}
}
module.exports = { Layer2PRAutomation };