BMAD-METHOD/tools/claude-code-hooks/stop.js

172 lines
5.3 KiB
JavaScript

#!/usr/bin/env node
/**
* BMAD Stop Hook
* Generates session summary and next steps
* Runs when Claude Code session ends
*/
const fs = require('fs').promises;
const path = require('path');
const { readdir, stat, readFile } = require('fs').promises;
const { readStdinJson } = require('./lib/stdin-reader');
async function generateSessionSummary() {
try {
// Read input from stdin (if any)
const input = await readStdinJson();
const messages = [];
const workspacePath = '.workspace';
// Check for session progress
try {
await stat(workspacePath);
const progressFile = path.join(workspacePath, 'progress.json');
try {
const progressData = await readFile(progressFile, 'utf8');
const progress = JSON.parse(progressData);
const currentSession = progress.sessions && progress.sessions[process.pid];
if (currentSession && currentSession.operations.length > 0) {
const fileChanges = currentSession.operations
.filter(op => op.target !== 'N/A')
.map(op => `${op.tool}: ${path.basename(op.target)}`)
.filter((value, index, self) => self.indexOf(value) === index);
if (fileChanges.length > 0) {
messages.push({
role: 'system',
content: `📊 BMAD Session Summary:\n\nFiles Modified:\n${fileChanges.join('\n')}\n\n` +
`Total Operations: ${currentSession.operations.length}\n` +
`Session Duration: ${calculateDuration(currentSession.startTime)}`
});
}
}
} catch (err) {
// Progress file doesn't exist
}
} catch (err) {
// Workspace doesn't exist
}
// Check for active story and suggest next steps
const storyFiles = await findFiles('.', 'STORY-*.md', [
'node_modules', '.git', 'dist', 'web-bundles'
]);
let activeStory = null;
for (const file of storyFiles) {
try {
const content = await readFile(file, 'utf8');
if (content.includes('Status: In Progress') || content.includes('Status\nIn Progress')) {
activeStory = { path: file, content };
break;
}
} catch (err) {
continue;
}
}
if (activeStory) {
// Count completed tasks
const taskMatches = activeStory.content.match(/- \[([ x])\]/g) || [];
const completedTasks = taskMatches.filter(task => task.includes('[x]')).length;
const totalTasks = taskMatches.length;
const completionPercent = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
let nextSteps = '';
if (completionPercent === 100) {
nextSteps = '✅ All tasks complete! Next: Run *reality-audit for final validation';
} else if (completionPercent >= 75) {
nextSteps = '🎯 Almost done! Complete remaining tasks then run tests';
} else if (completionPercent >= 50) {
nextSteps = '📈 Good progress! Continue with remaining implementation tasks';
} else {
nextSteps = '🚀 Getting started! Focus on core functionality first';
}
messages.push({
role: 'system',
content: `📋 Story Progress: ${path.basename(activeStory.path)}\n` +
`Completion: ${completedTasks}/${totalTasks} tasks (${completionPercent}%)\n\n` +
`${nextSteps}`
});
}
// Quality reminder
messages.push({
role: 'system',
content: '💡 BMAD Tip: Remember to run *reality-audit periodically to ensure code quality. ' +
'Use *run-tests to validate your implementation.'
});
console.log(JSON.stringify({ messages }));
} catch (error) {
console.log(JSON.stringify({ messages: [] }));
}
}
function calculateDuration(startTime) {
try {
const start = new Date(startTime);
const end = new Date();
const durationMs = end - start;
const hours = Math.floor(durationMs / 3600000);
const minutes = Math.floor((durationMs % 3600000) / 60000);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
} catch (err) {
return 'N/A';
}
}
// Recursive file finder using only built-in modules
async function findFiles(dir, pattern, ignore = []) {
const results = [];
try {
const files = await readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
// Skip ignored directories
if (ignore.some(ign => filePath.includes(ign))) {
continue;
}
try {
const stats = await stat(filePath);
if (stats.isDirectory()) {
// Recursively search subdirectories
const subResults = await findFiles(filePath, pattern, ignore);
results.push(...subResults);
} else if (stats.isFile()) {
// Check if filename matches pattern
const fileName = path.basename(filePath);
const regex = new RegExp(pattern.replace('*', '.*'));
if (regex.test(fileName)) {
results.push(filePath);
}
}
} catch (err) {
// Skip files we can't access
continue;
}
}
} catch (err) {
// Skip directories we can't read
}
return results;
}
generateSessionSummary();