104 lines
3.5 KiB
TypeScript
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;
|