automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
254 lines (220 loc) • 6.98 kB
JavaScript
/**
* Genie Workflow Output Parser
*
* Extracts structured data from Genie agent sessions for use in git hooks.
*
* Usage:
* node genie-workflow-parser.js <sessionId> <outputFormat>
*
* Formats:
* - json: Structured JSON output
* - exit-code: Returns 0/1/2 based on validation results
* - markdown: Markdown summary
* - validation: Validation results only
*
* Example:
* const result = require('./genie-workflow-parser.js')
* const output = result.parseSession('abc123', 'json')
*/
const fs = require('fs');
const path = require('path');
const REPO_ROOT = path.join(__dirname, '..');
const LOGS_DIR = path.join(REPO_ROOT, '.genie', 'state', 'agents', 'logs');
const SESSIONS_FILE = path.join(REPO_ROOT, '.genie', 'state', 'agents', 'sessions.json');
class GenieworkflowParser {
constructor() {
this.output = [];
this.errors = [];
this.warnings = [];
this.validations = {};
this.exitCode = 0;
}
/**
* Find log file for session
*/
findLogFile(sessionId) {
if (!fs.existsSync(LOGS_DIR)) return null;
// Try direct lookup from sessions file first
if (fs.existsSync(SESSIONS_FILE)) {
try {
const data = JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8'));
const sessions = data.sessions || {};
const entry = Object.values(sessions).find(s => s.sessionId === sessionId);
if (entry && entry.logFile) {
const logPath = entry.logFile.startsWith('/')
? entry.logFile
: path.join(REPO_ROOT, entry.logFile);
if (fs.existsSync(logPath)) return logPath;
}
} catch {}
}
// Fallback: find by pattern
const files = fs.readdirSync(LOGS_DIR);
const matching = files.find(f => f.includes(sessionId.substring(0, 8)));
return matching ? path.join(LOGS_DIR, matching) : null;
}
/**
* Parse JSONL log file
*/
parseLogFile(logPath) {
if (!fs.existsSync(logPath)) {
this.errors.push(`Log file not found: ${logPath}`);
return null;
}
const content = fs.readFileSync(logPath, 'utf8');
const lines = content.split('\n').filter(l => l.trim());
const events = [];
const messages = [];
for (const line of lines) {
try {
const event = JSON.parse(line);
events.push(event);
// Extract assistant messages
if (event.type === 'item.completed' && event.item?.item_type === 'assistant_message') {
messages.push(event.item.text);
}
} catch {
// Skip unparseable lines
}
}
return { events, messages, fullText: messages.join('\n') };
}
/**
* Extract validation structure from workflow output
*
* Genie workflows typically produce structured output like:
* ```markdown
* ## Validation Results
*
* ### ✅ Passed
* - Rule 1 passed
* - Rule 2 passed
*
* ### ⚠️ Warnings (2)
* 1. Warning 1
* 2. Warning 2
*
* ### ❌ Errors (1)
* 1. Error 1
* ```
*/
extractValidationStructure(fullText) {
const result = {
passed: [],
warnings: [],
errors: [],
hasBlockingErrors: false,
hasWarnings: false
};
// Extract passed items
const passedMatch = fullText.match(/###\s*✅\s*Passed[\s\S]*?(?=###|$)/);
if (passedMatch) {
const items = passedMatch[0].match(/[-*]\s+(.+)/g) || [];
result.passed = items.map(item => item.replace(/^[-*]\s+/, '').trim());
}
// Extract warnings
const warningsMatch = fullText.match(/###\s*⚠️\s*Warnings?\s*\((\d+)\)[\s\S]*?(?=###|$)/);
if (warningsMatch) {
const count = parseInt(warningsMatch[1]);
if (count > 0) {
result.hasWarnings = true;
const items = warningsMatch[0].match(/^\d+\.\s+(.+?)(?=\n\d+\.|$)/gm) || [];
result.warnings = items.map(item => item.replace(/^\d+\.\s+/, '').trim());
}
}
// Extract errors
const errorsMatch = fullText.match(/###\s*❌\s*(?:Blocking\s+)?Issues?\s*\((\d+)\)[\s\S]*?(?=###|$)/);
if (errorsMatch) {
const count = parseInt(errorsMatch[1]);
if (count > 0) {
result.hasBlockingErrors = true;
const items = errorsMatch[0].match(/^\d+\.\s+(.+?)(?=\n\d+\.|$)/gm) || [];
result.errors = items.map(item => item.replace(/^\d+\.\s+/, '').trim());
}
}
return result;
}
/**
* Parse session and extract validation data
*/
parseSession(sessionId, format = 'json') {
const logPath = this.findLogFile(sessionId);
if (!logPath) {
this.errors.push(`Session not found: ${sessionId}`);
return this.formatOutput(format);
}
const parsed = this.parseLogFile(logPath);
if (!parsed) {
return this.formatOutput(format);
}
// Extract validation structure
this.validations = this.extractValidationStructure(parsed.fullText);
// Determine exit code
if (this.validations.hasBlockingErrors) {
this.exitCode = 2;
} else if (this.validations.hasWarnings) {
this.exitCode = 1;
} else {
this.exitCode = 0;
}
return this.formatOutput(format, parsed.fullText);
}
/**
* Format output based on requested format
*/
formatOutput(format, fullText = '') {
switch (format) {
case 'json':
return JSON.stringify({
exitCode: this.exitCode,
validations: this.validations,
errors: this.errors,
warnings: this.warnings
}, null, 2);
case 'exit-code':
return this.exitCode.toString();
case 'validation':
return JSON.stringify(this.validations, null, 2);
case 'markdown':
return fullText;
case 'summary':
return {
exitCode: this.exitCode,
passed: this.validations.passed?.length || 0,
warnings: this.validations.warnings?.length || 0,
errors: this.validations.errors?.length || 0,
blocking: this.validations.hasBlockingErrors
};
default:
return JSON.stringify({
exitCode: this.exitCode,
validations: this.validations
}, null, 2);
}
}
}
// Module export
if (require.main === module) {
// CLI usage: node script.js <sessionId> <format>
const sessionId = process.argv[2];
const format = process.argv[3] || 'json';
if (!sessionId) {
console.error('Usage: genie-workflow-parser.js <sessionId> [format]');
console.error('Formats: json, exit-code, markdown, validation, summary');
process.exit(1);
}
const parser = new GenieworkflowParser();
const output = parser.parseSession(sessionId, format);
if (format === 'exit-code') {
process.stdout.write(output);
process.exit(parseInt(output));
} else if (format === 'summary' && typeof output === 'object') {
console.log(JSON.stringify(output, null, 2));
} else {
console.log(output);
}
} else {
// Module export
module.exports = GenieworkflowParser;
}