@jjdenhertog/ai-driven-development
Version:
AI-driven development workflow with learning capabilities for Claude
420 lines • 17.9 kB
JavaScript
;
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