#!/usr/bin/env ts-node import { WorkflowStatusManager } from '../src/backend/services/WorkflowStatusManager'; import { getPendingPhaseUpdates } from '../src/backend/services/PhaseUpdateQueue'; 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; getRecentCommitsFn?: (windowHours: number) => Promise<{hash: string; msg: string; date: string}[]>; }; 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; export 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 (config.getRecentCommitsFn || 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 > 0) { for (const c of commits) { 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 > 0) { out += `\n## Blockers\n`; for (const b of blockers) { out += `- ${b}\n`; } } out += `\n## Pending Phase Updates\n`; if (pendingUpdates.length > 0) { for (const e of pendingUpdates) { 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;