recipe-manager/scripts/morning-report.ts

148 lines
5.0 KiB
TypeScript

#!/usr/bin/env ts-node
import { WorkflowStatusManager } from './src/backend/services/WorkflowStatusManager';
import { getPendingPhaseUpdates, getAllPhaseUpdates } from './src/backend/services/PhaseUpdateQueue';
import * as fs from 'fs/promises';
import * as path from 'path';
// Configurable recency window in hours for recent commits
type ReportConfig = {
commitWindowHours?: number;
statusPath?: string;
phaseUpdatesPath?: string;
stalledThresholdMinutes?: number;
now?: Date;
};
const DEFAULT_STATUS_PATH = path.join(process.cwd(), 'status/workflow-status.json');
const DEFAULT_PHASE_UPDATES_PATH = path.join(process.cwd(), 'status/phase-updates.jsonl');
const DEFAULT_STALLED_THRESHOLD_MINUTES = 60;
async function getRecentCommits(windowHours: number): Promise<{hash: string, msg: string, date: string}[]> {
const sinceArg = `--since='${windowHours} hours ago'`;
const cmd = `git log --oneline --date=iso --pretty=format:'%h|%s|%cd' ${sinceArg}`;
const { exec } = require('child_process');
return new Promise((resolve) => {
exec(cmd, { cwd: process.cwd() }, (err: any, stdout: string) => {
if (err) return resolve([]);
const lines = stdout.trim().split(/\n/);
const result = lines.filter(Boolean).map((line: string) => {
const [hash, msg, date] = line.split('|');
return {hash, msg, date};
});
resolve(result);
});
});
}
function minutesBetween(a: Date, b: Date): number {
return Math.abs((a.getTime() - b.getTime()) / 60000);
}
function detectStalled(status: any, thresholdMinutes: number, now: Date): {stalled: boolean; reason?: string; recommend?: string} {
if (!status) return {stalled: false};
if (["completed", "failed", "idle"].includes(status.overallStatus)) return {stalled: false};
const last = new Date(status.lastUpdated);
const mins = minutesBetween(now, last);
if (mins >= thresholdMinutes) {
return {
stalled: true,
reason: `No progress in ${mins.toFixed(0)} minutes (threshold: ${thresholdMinutes}m). Last update: ${last.toISOString()}`,
recommend: status.overallStatus === "blocked" ? "Manual intervention may be required. See blockers below." : "Restart or debug orchestrator."
};
}
return {stalled: false};
}
function extractBlockers(status: any): string[] {
const out: string[] = [];
if (!status) return out;
if (status.overallStatus === 'blocked') {
if (status.lastFailureReason) out.push(`❗ Blocked: ${status.lastFailureReason}`);
else out.push(`❗ Blocked: Reason not specified.`);
}
if (status.overallStatus === 'failed') {
out.push('❌ Workflow failed.');
if (status.lastFailureReason) out.push(`Failure: ${status.lastFailureReason}`);
}
if (status.nextAction && status.nextAction.includes('manual')) {
out.push(`⚠️ Manual intervention required: ${status.nextAction}`);
}
return out;
}
async function main(config: ReportConfig = {}) {
const commitWindow = config.commitWindowHours || 24;
const statusPath = config.statusPath || DEFAULT_STATUS_PATH;
const phaseUpdatesPath = config.phaseUpdatesPath || DEFAULT_PHASE_UPDATES_PATH;
const threshold = config.stalledThresholdMinutes || DEFAULT_STALLED_THRESHOLD_MINUTES;
const now = config.now || new Date();
// 1. Recent commits
const commits = await getRecentCommits(commitWindow);
// 2. Workflow status
const wsm = new WorkflowStatusManager(statusPath);
const status = await wsm.read();
// 3. Blockers
const blockers = extractBlockers(status);
// 4. Pending phase updates
const pendingUpdates = await getPendingPhaseUpdates();
// 5. Stalled-state detection
const stalled = detectStalled(status, threshold, now);
// Render report
let out = `# 🌅 Morning Workflow Report\n`;
out += `\n## Recent Commits (last ${commitWindow}h)\n`;
if (commits.length) {
commits.forEach(c => {
out += `- \\`${c.hash}\\` ${c.msg} _(at ${c.date})_\n`;
});
} else {
out += '(No commits in window)\n';
}
out += `\n## Workflow Status\n`;
if (status) {
out += `- Current phase: **${status.currentPhase || '—'}**\n`;
out += `- State: **${status.overallStatus}**\n`;
out += `- Last updated: ${status.lastUpdated}\n`;
if (status.completedPhases && status.completedPhases.length)
out += `- Completed phases: ${status.completedPhases.join(', ')}\n`;
} else {
out += '- No workflow status available.\n';
}
if (stalled.stalled) {
out += `\n## ⚠️ Workflow Stalled\n- Reason: ${stalled.reason}\n- Recommendation: ${stalled.recommend}\n`;
}
if (blockers.length) {
out += `\n## Blockers\n`;
blockers.forEach(b => {
out += `- ${b}\n`;
});
}
out += `\n## Pending Phase Updates\n`;
if (pendingUpdates.length) {
pendingUpdates.forEach(e => {
out += `- [${e.eventType}] ${e.summary} (phase: ${e.phase}) at ${e.timestamp} [id: ${e.id}]\n`;
});
} else {
out += '(All phase updates relayed)\n';
}
console.log(out);
}
// CLI wrapper
if (require.main === module) {
main();
}
export default main;