UNPKG

@jjdenhertog/ai-driven-development

Version:

AI-driven development workflow with learning capabilities for Claude

420 lines 17.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createSessionReport = createSessionReport; const fs_extra_1 = require("fs-extra"); const node_path_1 = require("node:path"); const logger_1 = require("../logger"); function createSessionReport(options) { return __awaiter(this, void 0, void 0, function* () { var _a; const { taskId, taskName, worktreePath, logsDir, exitCode } = options; const debugLogsPath = (0, node_path_1.join)(worktreePath, 'debug_logs'); const outputPath = (0, node_path_1.join)(logsDir, 'claude.json'); (0, fs_extra_1.ensureDirSync)(logsDir); try { // Check if debug logs directory exists if (!(0, fs_extra_1.existsSync)(debugLogsPath)) { (0, logger_1.log)('No debug logs found, creating error report', 'warn'); return yield saveErrorReport({ taskId, taskName, errorMessage: 'Debug logs directory not found', outputPath: (0, node_path_1.join)(logsDir, 'claude.json') }); } // Find the first log file const logFiles = (0, fs_extra_1.readdirSync)(debugLogsPath).filter(f => f.endsWith('.jsonl')); if (logFiles.length === 0) { (0, logger_1.log)('No log files found in debug_logs', 'warn'); return yield saveErrorReport({ taskId, taskName, errorMessage: 'No log files found in debug_logs directory', outputPath: (0, node_path_1.join)(logsDir, 'claude.json') }); } const logFilePath = (0, node_path_1.join)(debugLogsPath, logFiles[0]); (0, logger_1.log)(`Processing log file: ${logFiles[0]}`, 'info'); // Read and parse the log file const logContent = (0, fs_extra_1.readFileSync)(logFilePath, 'utf8'); const logEntries = logContent .split('\n') .filter(line => line.trim()) .map(line => { try { return JSON.parse(line); } catch (_a) { return null; } }) .filter(entry => entry !== null); // Extract session ID from the first valid entry const sessionId = (_a = logEntries.find(entry => entry.sessionId)) === null || _a === void 0 ? void 0 : _a.sessionId; if (!sessionId) { (0, logger_1.log)('Could not find session ID in logs', 'warn'); return yield saveErrorReport({ taskId, taskName, errorMessage: 'Session ID not found in logs', outputPath: (0, node_path_1.join)(logsDir, 'claude.json') }); } // Find transcript path from logs const transcriptPath = findTranscriptPath(logEntries); let transcriptEntries = []; if (transcriptPath && (0, fs_extra_1.existsSync)(transcriptPath)) { const transcriptContent = (0, fs_extra_1.readFileSync)(transcriptPath, 'utf8'); transcriptEntries = transcriptContent .split('\n') .filter(line => line.trim()) .map(line => { try { return JSON.parse(line); } catch (_a) { return null; } }) .filter(entry => entry !== null); } // Build the session report const report = buildSessionReport(sessionId, taskId, taskName, logEntries, transcriptEntries, exitCode); // Save the report (0, fs_extra_1.writeFileSync)(outputPath, JSON.stringify(report, null, 2)); (0, logger_1.log)(`Session report created: ${outputPath}`, 'success'); // Clean up debug logs try { (0, fs_extra_1.rmSync)(debugLogsPath, { recursive: true, force: true }); (0, logger_1.log)('Debug logs cleaned up', 'info'); } catch (cleanupError) { (0, logger_1.log)(`Failed to clean up debug logs: ${String(cleanupError)}`, 'warn'); } return report; } catch (error) { (0, logger_1.log)(`Error creating session report: ${String(error)}`, 'error'); return yield saveErrorReport({ errorMessage: `Failed to create session report: ${error instanceof Error ? error.message : String(error)}`, outputPath }); } }); } function findTranscriptPath(logEntries) { var _a; for (const entry of logEntries) { if ((_a = entry.data) === null || _a === void 0 ? void 0 : _a.transcript_path) { return entry.data.transcript_path; } } return null; } function buildSessionReport(sessionId, taskId, taskName, logEntries, transcriptEntries, exitCode) { var _a, _b; const timeline = []; // Extract user prompt from transcript const userPrompt = extractUserPrompt(transcriptEntries) || 'No user prompt found'; // Get start and end times const startTime = (_a = logEntries[0]) === null || _a === void 0 ? void 0 : _a.timestamp; const endTime = (_b = logEntries.at(-1)) === null || _b === void 0 ? void 0 : _b.timestamp; const totalDuration = startTime && endTime ? new Date(endTime).getTime() - new Date(startTime).getTime() : 0; // Process notifications from transcript const notifications = extractNotifications(transcriptEntries); // Process tool executions from logs const toolExecutions = processToolExecutions(logEntries); // Merge timeline events timeline.push(...notifications, ...toolExecutions); // Sort timeline by timestamp timeline.sort((a, b) => { if (!a.timestamp || !b.timestamp) return 0; return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); }); // Extract metadata const toolsUsed = [...new Set(logEntries .filter(e => { var _a; return (_a = e.data) === null || _a === void 0 ? void 0 : _a.tool_name; }) .map(e => e.data.tool_name))]; // Determine success based on multiple factors const success = determineSuccess({ exitCode, timeline, transcriptEntries }); return { session_id: sessionId, task_id: taskId, task_name: taskName, user_prompt: userPrompt, start_time: startTime, end_time: endTime, total_duration_ms: totalDuration, success, timeline, metadata: { exit_code: exitCode, tools_used: toolsUsed, total_tokens: calculateTotalTokens(transcriptEntries) } }; } function extractUserPrompt(transcriptEntries) { var _a; // Find the first non-meta user message for (const entry of transcriptEntries) { if (entry.type === 'user' && ((_a = entry.message) === null || _a === void 0 ? void 0 : _a.content)) { if (typeof entry.message.content === 'string') { // Skip meta messages if (!entry.message.content.includes('DO NOT respond to these messages')) { return entry.message.content; } } } } return null; } function extractNotifications(transcriptEntries) { var _a; const notifications = []; for (const entry of transcriptEntries) { if (entry.type !== 'assistant' || !((_a = entry.message) === null || _a === void 0 ? void 0 : _a.content)) { continue; } const { content } = entry.message; // Handle text responses if (!Array.isArray(content)) { continue; } for (const item of content) { if (item.type === 'text' && item.text) { notifications.push({ type: 'status', timestamp: entry.timestamp, message: `⏺ ${item.text}` }); } } } return notifications; } function processToolExecutions(logEntries) { var _a, _b; const executions = []; const toolStarts = new Map(); for (const entry of logEntries) { if (((_a = entry.data) === null || _a === void 0 ? void 0 : _a.hook_event_name) === 'PreToolUse' && entry.data.tool_name) { const key = `${entry.data.tool_name}-${entry.timestamp}`; toolStarts.set(key, entry); } else if (((_b = entry.data) === null || _b === void 0 ? void 0 : _b.hook_event_name) === 'PostToolUse' && entry.data.tool_name) { // Find matching PreToolUse let matchingStart; let matchingKey; for (const [key, start] of toolStarts) { if (key.startsWith(entry.data.tool_name)) { matchingStart = start; matchingKey = key; break; } } if (matchingStart && matchingKey) { toolStarts.delete(matchingKey); const duration = new Date(entry.timestamp).getTime() - new Date(matchingStart.timestamp).getTime(); const toolEntry = createToolEntry(entry.data.tool_name, matchingStart.data.tool_input, entry.data.tool_response, matchingStart.timestamp, duration); executions.push(toolEntry); } } } return executions; } function createToolEntry(toolName, input, response, timestamp, duration) { const entry = { type: 'tool', name: toolName, timestamp, duration_ms: duration, stats: `${(duration / 1000).toFixed(1)}s` }; // Handle specific tools switch (toolName) { case 'Write': case 'Edit': case 'MultiEdit': if (response === null || response === void 0 ? void 0 : response.filePath) { entry.file_path = response.filePath; entry.description = response.filePath; } if (response === null || response === void 0 ? void 0 : response.content) { const lines = response.content.split('\n'); entry.summary = `Wrote ${lines.length} lines to ${response.filePath || 'file'}`; entry.preview = lines.slice(0, 10).join('\n'); entry.full_content = response.content; entry.expandable = true; } break; case 'WebSearch': if (input === null || input === void 0 ? void 0 : input.query) { entry.description = input.query; } if (response === null || response === void 0 ? void 0 : response.results) { entry.summary = `Found ${response.results.length} results`; } break; case 'Task': if (input === null || input === void 0 ? void 0 : input.description) { entry.description = input.description; } entry.details = { tools_used: (response === null || response === void 0 ? void 0 : response.tools_used) || [], tokens: (response === null || response === void 0 ? void 0 : response.tokens) || 0 }; break; case 'TodoWrite': if (response === null || response === void 0 ? void 0 : response.newTodos) { const todos = response.newTodos; entry.summary = `Updated ${todos.length} todos`; entry.details = todos; } break; } return entry; } function calculateTotalTokens(transcriptEntries) { let total = 0; for (const entry of transcriptEntries) { if (entry.type === 'assistant' && entry.message) { const { usage } = entry.message; if (usage) { total += usage.input_tokens || 0; total += usage.output_tokens || 0; } } } return total; } function determineSuccess(options) { const { timeline, transcriptEntries } = options; // Since exitCode is always 0 due to manual killing, we need to analyze the timeline // 1. Check for error entries in timeline const hasErrors = timeline.some(entry => entry.type === 'error'); if (hasErrors) return false; // 2. Check TodoWrite tool completions const todoWriteEntries = timeline.filter(entry => entry.name === 'TodoWrite' && entry.details); if (todoWriteEntries.length > 0) { // Get the last TodoWrite entry const lastTodoEntry = todoWriteEntries[todoWriteEntries.length - 1]; if (lastTodoEntry.details && Array.isArray(lastTodoEntry.details)) { const todos = lastTodoEntry.details; const allCompleted = todos.every(todo => todo.status === 'completed'); const hasInProgress = todos.some(todo => todo.status === 'in_progress'); const hasPending = todos.some(todo => todo.status === 'pending'); // If all todos are completed, it's likely successful if (allCompleted && todos.length > 0) return true; // If there are still pending or in-progress todos, it might not be complete if (hasInProgress || hasPending) { // But we need to check if the last messages indicate completion // Continue to keyword analysis below } } } // 3. Analyze the last few assistant messages for success/failure keywords const lastMessages = extractLastAssistantMessages(transcriptEntries, 5); // Success keywords (case-insensitive) const successKeywords = [ 'completed', 'done', 'finished', 'successfully', 'created', 'implemented', 'fixed', 'resolved', 'all tests pass', 'build succeeded', 'no errors' ]; // Failure keywords (case-insensitive) const failureKeywords = [ 'failed', 'error', 'unable', 'cannot', 'issue', 'problem', 'blocked', 'stuck', 'unresolved', 'tests failing', 'build failed', 'type error' ]; let successScore = 0; let failureScore = 0; for (const message of lastMessages) { const lowerMessage = message.toLowerCase(); // Check for success keywords for (const keyword of successKeywords) { if (lowerMessage.includes(keyword)) { successScore++; } } // Check for failure keywords for (const keyword of failureKeywords) { if (lowerMessage.includes(keyword)) { failureScore++; } } } // 4. Check if the session ended abruptly (very few timeline entries) if (timeline.length < 3) { return false; // Likely an early termination } // Make a decision based on the scores if (failureScore > successScore) { return false; } // Default to true if we have more success indicators or no clear failure return successScore > 0 || (failureScore === 0 && timeline.length > 5); } function extractLastAssistantMessages(transcriptEntries, count) { var _a; const messages = []; // Iterate from the end backwards for (let i = transcriptEntries.length - 1; i >= 0 && messages.length < count; i--) { const entry = transcriptEntries[i]; if (entry.type === 'assistant' && ((_a = entry.message) === null || _a === void 0 ? void 0 : _a.content)) { const content = entry.message.content; if (typeof content === 'string') { messages.push(content); } else if (Array.isArray(content)) { // Extract text from content array for (const item of content) { if (item.type === 'text' && item.text) { messages.push(item.text); } } } } } return messages; } function saveErrorReport(options) { return __awaiter(this, void 0, void 0, function* () { const { taskId, taskName, errorMessage, outputPath } = options; const report = { session_id: 'unknown', task_id: taskId || 'unknown', task_name: taskName || 'unknown', user_prompt: 'Unable to retrieve user prompt', success: false, timeline: [{ type: 'error', timestamp: new Date().toISOString(), message: errorMessage }] }; // Ensure directory exists (0, fs_extra_1.ensureDirSync)((0, node_path_1.dirname)(outputPath)); (0, fs_extra_1.writeFileSync)(outputPath, JSON.stringify(report, null, 2)); return report; }); } //# sourceMappingURL=createSessionReport.js.map