/** * Voxyz Trigger Rules Service * Monitors conditions and triggers mission proposals * Phase 10: Autonomous Architecture */ import { TriggerRule, TriggerEvaluationContext, TriggerEvaluationResult, MissionProposalInsert, TriggerType, TriggerLog } from '@/lib/supabase/voxyz.types'; import { Task } from '@/lib/data/tasks'; import { getServiceSupabase } from '@/lib/supabase/client'; import { startExecutionLog, completeExecutionLog } from '@/lib/services/daily-mission-agent'; /** * Get all active trigger rules */ export async function getActiveTriggerRules(): Promise { const supabase = getServiceSupabase(); const { data, error } = await supabase .from('ops_trigger_rules') .select('*') .eq('is_active', true) .order('priority', { ascending: false }); if (error) { console.error('Error fetching trigger rules:', error); return []; } return data || []; } /** * Evaluate all trigger rules against current context */ export async function evaluateAllTriggers( context: TriggerEvaluationContext ): Promise { const rules = await getActiveTriggerRules(); const results: TriggerEvaluationResult[] = []; for (const rule of rules) { // Check cooldown if (await isOnCooldown(rule)) { continue; } // Check daily limit if (await hasReachedDailyLimit(rule)) { continue; } const result = await evaluateTrigger(rule, context); if (result.triggered) { results.push(result); // Log the trigger await logTrigger(rule, result, true); // Update last triggered time await updateTriggerTimestamp(rule.id); } } return results; } /** * Evaluate a single trigger rule */ async function evaluateTrigger( rule: TriggerRule, context: TriggerEvaluationContext ): Promise { const conditions = rule.conditions; switch (rule.trigger_type) { case 'deadline_approaching': return evaluateDeadlineTrigger(rule, context, conditions); case 'task_blocked': return evaluateBlockedTrigger(rule, context, conditions); case 'high_priority_created': return evaluateHighPriorityTrigger(rule, context, conditions); case 'stale_task': return evaluateStaleTaskTrigger(rule, context, conditions); case 'sprint_ending': return evaluateSprintEndingTrigger(rule, context, conditions); case 'daily_mission_time': return evaluateDailyMissionTimeTrigger(rule, context, conditions); case 'calendar_conflict': return evaluateCalendarConflictTrigger(rule, context, conditions); default: return { triggered: false, rule, context: {} }; } } /** * Evaluate deadline approaching trigger */ function evaluateDeadlineTrigger( rule: TriggerRule, context: TriggerEvaluationContext, conditions: Record ): TriggerEvaluationResult { const hoursBefore = (conditions.hours_before as number) || 24; const priorityFilter = conditions.priority as string | undefined; const triggeredTasks: Task[] = []; for (const task of context.activeTasks) { if (!task.dueDate) continue; const dueDate = new Date(task.dueDate); const hoursUntil = (dueDate.getTime() - context.currentTime.getTime()) / (1000 * 60 * 60); if (hoursUntil > 0 && hoursUntil <= hoursBefore) { // Check priority filter if specified if (priorityFilter && task.priority !== priorityFilter) { continue; } triggeredTasks.push(task); } } if (triggeredTasks.length === 0) { return { triggered: false, rule, context: {} }; } // Pick the most urgent task const mostUrgent = triggeredTasks.sort((a, b) => { const aDue = new Date(a.dueDate!).getTime(); const bDue = new Date(b.dueDate!).getTime(); return aDue - bDue; })[0]; return { triggered: true, rule, context: { hoursUntilDeadline: (new Date(mostUrgent.dueDate!).getTime() - context.currentTime.getTime()) / (1000 * 60 * 60), task: mostUrgent, }, proposalInput: { title: mostUrgent.title, description: mostUrgent.description || undefined, sourceTaskIds: [mostUrgent.id], sourceProjectIds: [mostUrgent.projectId], }, priorityBoost: (rule.action_config.priority_boost as number) || 0, }; } /** * Evaluate blocked task trigger */ function evaluateBlockedTrigger( rule: TriggerRule, context: TriggerEvaluationContext, conditions: Record ): TriggerEvaluationResult { const blockedHours = (conditions.blocked_hours as number) || 24; const triggeredTasks = context.activeTasks.filter(task => { if (task.status !== 'blocked') return false; // Check how long it's been blocked const updatedAt = new Date(task.updatedAt); const hoursBlocked = (context.currentTime.getTime() - updatedAt.getTime()) / (1000 * 60 * 60); return hoursBlocked >= blockedHours; }); if (triggeredTasks.length === 0) { return { triggered: false, rule, context: {} }; } const task = triggeredTasks[0]; return { triggered: true, rule, context: { hoursBlocked: (context.currentTime.getTime() - new Date(task.updatedAt).getTime()) / (1000 * 60 * 60), task, }, proposalInput: { title: `Unblock: ${task.title}`, description: `This task has been blocked for an extended period. ${task.description || ''}`, sourceTaskIds: [task.id], sourceProjectIds: [task.projectId], }, priorityBoost: (rule.action_config.priority_boost as number) || 30, }; } /** * Evaluate high priority created trigger */ function evaluateHighPriorityTrigger( rule: TriggerRule, context: TriggerEvaluationContext, conditions: Record ): TriggerEvaluationResult { const priorityFilter = (conditions.priority as string) || 'urgent'; // Look for recently created high priority tasks (within last hour) const oneHourAgo = new Date(context.currentTime.getTime() - 60 * 60 * 1000); const triggeredTasks = context.activeTasks.filter(task => { if (task.priority !== priorityFilter) return false; const createdAt = new Date(task.createdAt || task.updatedAt); return createdAt > oneHourAgo; }); if (triggeredTasks.length === 0) { return { triggered: false, rule, context: {} }; } const task = triggeredTasks[0]; return { triggered: true, rule, context: { task, createdRecently: true, }, proposalInput: { title: task.title, description: task.description || undefined, sourceTaskIds: [task.id], sourceProjectIds: [task.projectId], }, priorityBoost: (rule.action_config.priority_boost as number) || 25, }; } /** * Evaluate stale task trigger */ function evaluateStaleTaskTrigger( rule: TriggerRule, context: TriggerEvaluationContext, conditions: Record ): TriggerEvaluationResult { const staleDays = (conditions.stale_days as number) || 7; const statusIn = (conditions.status_in as string[]) || ['todo', 'in-progress']; const triggeredTasks = context.activeTasks.filter(task => { if (!statusIn.includes(task.status)) return false; const updatedAt = new Date(task.updatedAt); const daysSinceUpdate = (context.currentTime.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24); return daysSinceUpdate >= staleDays; }); if (triggeredTasks.length === 0) { return { triggered: false, rule, context: {} }; } // Pick the stalest task const stalestTask = triggeredTasks.sort((a, b) => { return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(); })[0]; const daysStale = Math.floor((context.currentTime.getTime() - new Date(stalestTask.updatedAt).getTime()) / (1000 * 60 * 60 * 24)); return { triggered: true, rule, context: { daysStale, task: stalestTask, }, proposalInput: { title: `Review stale task: ${stalestTask.title}`, description: `This task has been ${stalestTask.status} for ${daysStale} days without updates. ${stalestTask.description || ''}`, sourceTaskIds: [stalestTask.id], sourceProjectIds: [stalestTask.projectId], }, priorityBoost: (rule.action_config.priority_boost as number) || 15, }; } /** * Evaluate sprint ending trigger */ function evaluateSprintEndingTrigger( rule: TriggerRule, context: TriggerEvaluationContext, conditions: Record ): TriggerEvaluationResult { const daysBefore = (conditions.days_before as number) || 3; // This would require sprint data - simplified version // In real implementation, fetch sprint end date from gantt-board // For now, trigger if it's end of week (Friday) and there are incomplete tasks const dayOfWeek = context.currentTime.getDay(); const isEndOfWeek = dayOfWeek === 5; // Friday if (!isEndOfWeek) { return { triggered: false, rule, context: {} }; } const incompleteTasks = context.activeTasks.filter(t => t.status !== 'done' && t.status !== 'canceled' ); if (incompleteTasks.length === 0) { return { triggered: false, rule, context: {} }; } // Find highest priority incomplete task const priorityOrder = { 'urgent': 4, 'high': 3, 'medium': 2, 'low': 1 }; const topTask = incompleteTasks.sort((a, b) => (priorityOrder[b.priority] || 0) - (priorityOrder[a.priority] || 0) )[0]; return { triggered: true, rule, context: { daysUntilSprintEnd: daysBefore, incompleteTasks: incompleteTasks.length, task: topTask, }, proposalInput: { title: `Sprint completion: ${topTask.title}`, description: `Sprint ends soon. Focus on completing this high-priority task.`, sourceTaskIds: [topTask.id], sourceProjectIds: [topTask.projectId], }, priorityBoost: (rule.action_config.priority_boost as number) || 20, }; } /** * Evaluate daily mission time trigger */ function evaluateDailyMissionTimeTrigger( rule: TriggerRule, context: TriggerEvaluationContext, conditions: Record ): TriggerEvaluationResult { const hour = (conditions.hour as number) || 7; const minute = (conditions.minute as number) || 0; const currentHour = context.currentTime.getHours(); const currentMinute = context.currentTime.getMinutes(); // Trigger if it's within 5 minutes of the target time const targetMinutes = hour * 60 + minute; const currentMinutes = currentHour * 60 + currentMinute; if (Math.abs(currentMinutes - targetMinutes) > 5) { return { triggered: false, rule, context: {} }; } return { triggered: true, rule, context: { scheduledTime: `${hour}:${minute.toString().padStart(2, '0')}`, }, // This trigger is handled by the daily mission agent, not proposal creation proposalInput: undefined, }; } /** * Evaluate calendar conflict trigger */ function evaluateCalendarConflictTrigger( rule: TriggerRule, context: TriggerEvaluationContext, conditions: Record ): TriggerEvaluationResult { // Check for conflicts between proposed missions and calendar // This would require fetching calendar data // Simplified: check if any active task has a due date that conflicts with meetings // In real implementation, this would check against Google Calendar API return { triggered: false, rule, context: {} }; } /** * Check if trigger is on cooldown */ async function isOnCooldown(rule: TriggerRule): Promise { if (!rule.last_triggered_at) return false; const lastTriggered = new Date(rule.last_triggered_at); const cooldownMs = (rule.cooldown_minutes || 60) * 60 * 1000; const cooldownEnd = new Date(lastTriggered.getTime() + cooldownMs); return new Date() < cooldownEnd; } /** * Check if trigger has reached daily limit */ async function hasReachedDailyLimit(rule: TriggerRule): Promise { const supabase = getServiceSupabase(); const today = new Date().toISOString().split('T')[0]; const { count, error } = await supabase .from('ops_trigger_log') .select('*', { count: 'exact', head: true }) .eq('rule_id', rule.id) .gte('triggered_at', `${today}T00:00:00Z`); if (error) { console.error('Error checking trigger limit:', error); return false; } return (count || 0) >= (rule.max_triggers_per_day || 10); } /** * Update trigger timestamp */ async function updateTriggerTimestamp(ruleId: string): Promise { const supabase = getServiceSupabase(); await supabase .from('ops_trigger_rules') .update({ last_triggered_at: new Date().toISOString(), trigger_count: supabase.rpc('increment_trigger_count', { rule_id: ruleId }), }) .eq('id', ruleId); } /** * Log a trigger execution */ async function logTrigger( rule: TriggerRule, result: TriggerEvaluationResult, success: boolean ): Promise { const supabase = getServiceSupabase(); await supabase .from('ops_trigger_log') .insert({ rule_id: rule.id, rule_name: rule.name, triggered_at: new Date().toISOString(), trigger_type: rule.trigger_type, context: result.context, task_ids: result.proposalInput?.sourceTaskIds || [], action_taken: rule.action_type, action_result: success ? 'triggered' : 'failed', success, }); } /** * Run the trigger monitor agent */ export async function runTriggerMonitor(): Promise { const logId = await startExecutionLog('trigger_monitor', 'scheduled'); const startTime = Date.now(); try { // Fetch active tasks const { fetchAllTasks } = await import('@/lib/data/tasks'); const tasks = await fetchAllTasks(); const context: TriggerEvaluationContext = { currentTime: new Date(), activeTasks: tasks.filter(t => t.status !== 'done' && t.status !== 'canceled'), recentProposals: [], // Would fetch from DB todayMissions: [], // Would fetch from DB userAvailability: [], // Would fetch from calendar }; const triggered = await evaluateAllTriggers(context); await completeExecutionLog(logId, { status: 'success', duration: Date.now() - startTime, tasksAnalyzed: context.activeTasks.length, outputSummary: { triggersEvaluated: triggered.length, triggersTriggered: triggered.filter(t => t.triggered).length, }, }); } catch (error) { console.error('Error running trigger monitor:', error); await completeExecutionLog(logId, { status: 'failed', duration: Date.now() - startTime, errorMessage: error instanceof Error ? error.message : 'Unknown error', }); } } /** * Create a manual trigger */ export async function triggerManualEvaluation( triggerType: TriggerType, taskIds?: string[] ): Promise { // Fetch active trigger rules of this type const supabase = getServiceSupabase(); const { data: rules } = await supabase .from('ops_trigger_rules') .select('*') .eq('trigger_type', triggerType) .eq('is_active', true); if (!rules || rules.length === 0) { return []; } const { fetchAllTasks } = await import('@/lib/data/tasks'); const tasks = await fetchAllTasks(); const filteredTasks = taskIds ? tasks.filter(t => taskIds.includes(t.id)) : tasks; const context: TriggerEvaluationContext = { currentTime: new Date(), activeTasks: filteredTasks.filter(t => t.status !== 'done'), recentProposals: [], todayMissions: [], userAvailability: [], }; const results: TriggerEvaluationResult[] = []; for (const rule of rules) { const result = await evaluateTrigger(rule as TriggerRule, context); if (result.triggered) { results.push(result); } } return results; } /** * Get recent trigger logs */ export async function getRecentTriggerLogs(limit: number = 50): Promise { const supabase = getServiceSupabase(); const { data, error } = await supabase .from('ops_trigger_log') .select('*') .order('created_at', { ascending: false }) .limit(limit); if (error) { console.error('Error fetching trigger logs:', error); return []; } return data || []; }