172 lines
5.0 KiB
JavaScript
172 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* BMAD UserPromptSubmit Hook
|
|
* Automatically loads active story context and quality reminders
|
|
* Runs on UserPromptSubmit to enhance prompts with relevant context
|
|
*/
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const { promisify } = require('util');
|
|
const { readdir, stat } = require('fs').promises;
|
|
const Cache = require('./lib/cache');
|
|
const { filterContext, filterAcceptanceCriteria } = require('./lib/context-filter');
|
|
const PerformanceMonitor = require('./lib/performance');
|
|
const { readStdinJson } = require('./lib/stdin-reader');
|
|
|
|
// Cache for story files (5 minute TTL)
|
|
const storyCache = new Cache(300000);
|
|
|
|
async function loadActiveContext() {
|
|
return PerformanceMonitor.measure('UserPromptSubmit', async () => {
|
|
try {
|
|
// Read input from stdin
|
|
const input = await readStdinJson();
|
|
const prompt = input.prompt || '';
|
|
|
|
// Check cache first
|
|
const cacheKey = 'active-story';
|
|
let activeStory = storyCache.get(cacheKey);
|
|
|
|
if (!activeStory) {
|
|
// Find story files recursively using built-in fs
|
|
const storyFiles = await findFiles('.', 'STORY-*.md', [
|
|
'node_modules', '.git', 'dist', 'web-bundles'
|
|
]);
|
|
|
|
let activeStory = null;
|
|
let latestTime = 0;
|
|
|
|
for (const file of storyFiles) {
|
|
try {
|
|
const stats = await stat(file);
|
|
const content = await fs.readFile(file, 'utf8');
|
|
|
|
if ((content.includes('Status: In Progress') || content.includes('Status\nIn Progress'))
|
|
&& stats.mtimeMs > latestTime) {
|
|
activeStory = file;
|
|
latestTime = stats.mtimeMs;
|
|
}
|
|
} catch (err) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const context = {
|
|
approve: true,
|
|
messages: []
|
|
};
|
|
|
|
if (activeStory) {
|
|
storyCache.set(cacheKey, activeStory);
|
|
}
|
|
}
|
|
|
|
if (activeStory) {
|
|
const storyContent = await fs.readFile(activeStory, 'utf8');
|
|
|
|
const acceptanceCriteria = extractSection(storyContent, 'Acceptance Criteria');
|
|
const implementationNotes = extractSection(storyContent, 'Implementation Notes');
|
|
const devNotes = extractSection(storyContent, 'Dev Notes');
|
|
|
|
let contextMessage = `Active Story Context from ${path.basename(activeStory)}:\n\n`;
|
|
|
|
if (acceptanceCriteria) {
|
|
const filtered = filterAcceptanceCriteria(acceptanceCriteria);
|
|
contextMessage += `Acceptance Criteria:\n${filtered}\n\n`;
|
|
}
|
|
|
|
if (implementationNotes) {
|
|
contextMessage += `Implementation Notes:\n${implementationNotes}\n\n`;
|
|
}
|
|
|
|
if (devNotes) {
|
|
contextMessage += `Dev Notes:\n${devNotes}\n\n`;
|
|
}
|
|
|
|
if (acceptanceCriteria || implementationNotes || devNotes) {
|
|
const filteredContent = filterContext(contextMessage.trim(), prompt);
|
|
context.messages.push({
|
|
role: 'system',
|
|
content: filteredContent
|
|
});
|
|
}
|
|
}
|
|
|
|
const implementKeywords = ['implement', 'develop', 'fix', 'create', 'build', 'write'];
|
|
|
|
if (implementKeywords.some(keyword => prompt.toLowerCase().includes(keyword))) {
|
|
context.messages.push({
|
|
role: 'system',
|
|
content: 'BMAD Quality Reminder: Ensure all implementations are complete and functional. ' +
|
|
'No stubs, mocks, or placeholder code. Use *reality-audit for validation.'
|
|
});
|
|
}
|
|
|
|
console.log(JSON.stringify(context, null, 2));
|
|
} catch (error) {
|
|
console.log(JSON.stringify({ approve: true }));
|
|
}
|
|
});
|
|
}
|
|
|
|
function extractSection(content, sectionName) {
|
|
const patterns = [
|
|
new RegExp(`^##\\s+${sectionName}\\s*([\\s\\S]*?)(?=^##\\s|$)`, 'mi'),
|
|
new RegExp(`^###\\s+${sectionName}\\s*([\\s\\S]*?)(?=^###\\s|^##\\s|$)`, 'mi')
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = content.match(pattern);
|
|
if (match) {
|
|
return match[1].trim();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
loadActiveContext(); |