recipe-manager/scripts/morning-report.ts

104 lines
3.5 KiB
TypeScript

import { WorkflowStatusManager } from '../src/backend/services/WorkflowStatusManager.ts';
import { getPendingPhaseUpdates } from '../src/backend/services/PhaseUpdateQueue.ts';
import { exec as execCb } from 'child_process';
import { promisify } from 'util';
const exec = promisify(execCb);
type CommitInfo = { hash: string; msg: string; date: string };
type MorningReportOptions = {
commitWindowHours?: number;
stalledThresholdMinutes?: number;
now?: Date;
getRecentCommitsFn?: (sinceIso: string) => Promise<CommitInfo[]>;
};
async function getRecentCommits(sinceIso: string): Promise<CommitInfo[]> {
const cmd = `git log --since="${sinceIso}" --pretty=format:"%h|%s|%cI"`;
const { stdout } = await exec(cmd);
if (!stdout.trim()) return [];
return stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [hash, msg, date] = line.split('|');
return { hash, msg, date };
});
}
function minutesSince(iso: string, now: Date): number {
return Math.floor((now.getTime() - new Date(iso).getTime()) / 60000);
}
export async function generateMorningReport(options: MorningReportOptions = {}): Promise<string> {
const commitWindowHours = options.commitWindowHours ?? 24;
const stalledThresholdMinutes = options.stalledThresholdMinutes ?? 60;
const now = options.now ?? new Date();
const getRecentCommitsFn = options.getRecentCommitsFn ?? getRecentCommits;
const since = new Date(now.getTime() - commitWindowHours * 60 * 60 * 1000).toISOString();
const commits = await getRecentCommitsFn(since);
const statusManager = new WorkflowStatusManager();
const status = await statusManager.read();
const pending = await getPendingPhaseUpdates();
let out = '# 🌅 Morning Workflow Report\n\n';
out += `## Recent Commits (last ${commitWindowHours}h)\n`;
if (!commits.length) {
out += '- (No commits in window)\n\n';
} else {
for (const c of commits) {
out += `- \`${c.hash}\` ${c.msg} _(at ${c.date})_\n`;
}
out += '\n';
}
out += '## Workflow Status\n';
if (!status) {
out += '- No workflow status available\n\n';
} else {
out += `- Current phase: **${status.currentPhase ?? '-'}**\n`;
out += `- State: **${status.overallStatus ?? '-'}**\n`;
out += `- Last updated: ${status.lastUpdated ?? '-'}\n`;
out += `- Completed phases: ${(status.completedPhases ?? []).join(', ') || '-'}\n\n`;
if (status.overallStatus === 'running' && status.lastUpdated) {
const elapsed = minutesSince(status.lastUpdated, now);
if (elapsed >= stalledThresholdMinutes) {
out += '## ⚠️ Workflow Stalled\n';
out += `- Reason: No progress in ${elapsed} minutes (threshold: ${stalledThresholdMinutes}m). Last update: ${status.lastUpdated}\n`;
out += '- Recommendation: Restart or debug orchestrator.\n\n';
}
}
if (status.lastFailureReason) {
out += '## Blockers\n';
out += `- ❗ ${status.lastFailureReason}\n\n`;
}
}
out += '## Pending Phase Updates\n';
if (!pending.length) {
out += '- All phase updates relayed\n';
} else {
for (const p of pending) {
out += `- [${p.eventType}] ${p.summary} (phase: ${p.phase ?? '-'}) at ${p.timestamp} [id: ${p.id}]\n`;
}
}
console.log(out);
return out;
}
if (import.meta.url === `file://${process.argv[1]}`) {
generateMorningReport().catch((err) => {
console.error('Failed to generate morning report', err);
process.exit(1);
});
}
export default generateMorningReport;