/** * Voxyz Daily Mission Generation Agent * AI-powered agent that generates daily mission proposals at 7 AM * Phase 10: Autonomous Architecture */ import { DailyMission, DailyMissionInsert, MissionProposal, MissionProposalInsert, DailyMissionGenerationInput, DailyMissionGenerationResult, AgentExecutionLog, CalendarBlock } from '@/lib/supabase/voxyz.types'; import { Task, fetchAllTasks } from '@/lib/data/tasks'; import { Project, fetchAllProjects } from '@/lib/data/projects'; import { createProposalFromTask, ProposalContext, validateProposalAgainstCapGates, saveProposal } from '@/lib/services/proposal.service'; import { checkCapGates, getCapacityStatus, initializeCapGates, resetCapGatesForNewDay } from '@/lib/services/cap-gates.service'; import { getServiceSupabase } from '@/lib/supabase/client'; const GENERATION_HOUR = 7; const GENERATION_MINUTE = 0; /** * Generate daily missions for a specific date * This is the main entry point for the daily mission generation agent */ export async function generateDailyMissions( input: DailyMissionGenerationInput ): Promise { const executionLogId = await startExecutionLog('proposal_generator', 'scheduled'); const startTime = Date.now(); try { // Step 1: Initialize/reset cap gates for the day await initializeCapGates(); if (isNewDay()) { await resetCapGatesForNewDay(); } // Step 2: Fetch all active tasks and projects const [tasks, projects] = await Promise.all([ fetchAllTasks(), fetchAllProjects(), ]); // Step 3: Filter to active, non-done tasks const activeTasks = tasks.filter(task => task.status !== 'done' && task.status !== 'canceled' && task.status !== 'archived' ); // Step 4: Score and rank tasks const scoredTasks = scoreAndRankTasks(activeTasks, projects); // Step 5: Get capacity status const capacityStatus = await getCapacityStatus(); // Step 6: Generate proposals up to the limit const proposals: MissionProposal[] = []; const calendarBlocks: CalendarBlock[] = []; const targetCount = Math.min( input.targetMissionCount, capacityStatus.dailyMissionsRemaining ); for (let i = 0; i < scoredTasks.length && proposals.length < targetCount; i++) { const { task, project, score } = scoredTasks[i]; // Check if we can add another proposal const canAdd = await canAddAnotherProposal(task, proposals); if (!canAdd.allowed) { break; } // Create proposal const context: ProposalContext = { currentTime: new Date(), activeTasks, recentProposals: proposals, }; const proposalInsert = createProposalFromTask(task, project, context); // Validate against cap gates const existingProposals = proposals.map(p => ({ ...p, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), })) as MissionProposal[]; const capGateResult = await validateProposalAgainstCapGates( proposalInsert, existingProposals ); if (capGateResult.allPassed || i < 2) { // Allow first 2 even if gates fail // Save proposal to database const savedProposal = await saveProposal(proposalInsert, capGateResult); if (savedProposal) { proposals.push(savedProposal); // Create calendar block const calendarBlock = await createCalendarBlockForProposal(savedProposal); if (calendarBlock) { calendarBlocks.push(calendarBlock); } } } } // Step 7: Create daily mission record const dailyMission = await createDailyMissionRecord( input.date, proposals, calendarBlocks ); // Step 8: Complete execution log const duration = Date.now() - startTime; await completeExecutionLog(executionLogId, { status: 'success', duration, proposalsCreated: proposals.length, tasksAnalyzed: activeTasks.length, outputSummary: { date: input.date.toISOString(), missionsGenerated: proposals.length, primaryMissionId: proposals[0]?.id, secondaryMissionId: proposals[1]?.id, tertiaryMissionId: proposals[2]?.id, }, }); // Step 9: Return result const finalCapGateCheck = await checkCapGates(0, proposals); return { dailyMission, proposals, capGatesChecked: finalCapGateCheck, calendarBlocks, executionLogId, }; } catch (error) { console.error('Error generating daily missions:', error); // Log failure await completeExecutionLog(executionLogId, { status: 'failed', duration: Date.now() - startTime, errorMessage: error instanceof Error ? error.message : 'Unknown error', }); return null; } } /** * Score and rank tasks for mission selection */ function scoreAndRankTasks( tasks: Task[], projects: Project[] ): Array<{ task: Task; project: Project | undefined; score: number }> { const projectMap = new Map(projects.map(p => [p.id, p])); const now = new Date(); const scored = tasks.map(task => { const project = projectMap.get(task.projectId); let score = 0; // Priority score (0-25) const priorityScores: Record = { 'urgent': 25, 'high': 20, 'medium': 12, 'low': 5, }; score += priorityScores[task.priority] || 10; // Deadline score (0-30) if (task.dueDate) { const dueDate = new Date(task.dueDate); const hoursUntil = (dueDate.getTime() - now.getTime()) / (1000 * 60 * 60); if (hoursUntil < 0) score += 30; // Overdue else if (hoursUntil < 24) score += 27; else if (hoursUntil < 72) score += 21; else if (hoursUntil < 168) score += 15; else if (hoursUntil < 336) score += 9; else score += 3; } // Staleness score (0-15) const updatedAt = new Date(task.updatedAt); const daysSinceUpdate = (now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24); if (task.status === 'blocked') { score += 15; } else if (daysSinceUpdate > 14) { score += 12; } else if (daysSinceUpdate > 7) { score += 9; } else if (daysSinceUpdate > 3) { score += 5; } // Project importance (0-20) if (project) { const nameLower = project.name.toLowerCase(); if (nameLower.includes('ios') || nameLower.includes('revenue')) { score += 20; } else if (nameLower.includes('mission') || nameLower.includes('critical')) { score += 15; } else { score += 10; } } // Status bonus (0-10) if (task.status === 'in-progress') { score += 10; // Finish what's started } else if (task.status === 'todo') { score += 5; } return { task, project, score }; }); // Sort by score descending return scored.sort((a, b) => b.score - a.score); } /** * Check if we can add another proposal */ async function canAddAnotherProposal( task: Task, existingProposals: MissionProposal[] ): Promise<{ allowed: boolean; reason?: string }> { const capacity = await getCapacityStatus(); if (capacity.dailyMissionsRemaining <= 0) { return { allowed: false, reason: 'Daily mission limit reached' }; } // Check for duplicates const isDuplicate = existingProposals.some(p => p.source_task_ids.includes(task.id) ); if (isDuplicate) { return { allowed: false, reason: 'Task already has a proposal' }; } return { allowed: true }; } /** * Create a calendar block for a proposal */ async function createCalendarBlockForProposal( proposal: MissionProposal ): Promise { const supabase = getServiceSupabase(); const startTime = proposal.suggested_start_time ? new Date(proposal.suggested_start_time) : new Date(new Date().setHours(9, 0, 0, 0)); const duration = proposal.estimated_duration_minutes || 120; const endTime = new Date(startTime.getTime() + duration * 60000); const block: Omit = { block_date: proposal.proposal_date, start_time: startTime.toISOString(), end_time: endTime.toISOString(), block_type: 'mission_execution', mission_id: null, // Will be set when daily mission is created proposal_id: proposal.id, calendar_event_id: null, calendar_provider: 'google', calendar_synced: false, calendar_synced_at: null, status: 'scheduled', notes: proposal.title, }; const { data, error } = await supabase .from('ops_calendar_blocks') .insert(block) .select() .single(); if (error) { console.error('Error creating calendar block:', error); return null; } return data as CalendarBlock; } /** * Create daily mission record */ async function createDailyMissionRecord( date: Date, proposals: MissionProposal[], calendarBlocks: CalendarBlock[] ): Promise { const supabase = getServiceSupabase(); const totalMinutes = proposals.reduce((sum, p) => sum + (p.estimated_duration_minutes || 120), 0 ); // Calculate focus time (exclude reaction time) const workdayMinutes = 480; const reactionRatio = 0.30; const focusMinutes = Math.round(totalMinutes * (1 - reactionRatio)); // Calculate focus time window const focusStart = proposals[0]?.suggested_start_time ? new Date(proposals[0].suggested_start_time) : new Date(date.setHours(9, 0, 0, 0)); const focusEnd = new Date(focusStart.getTime() + focusMinutes * 60000); const missionInsert: Omit = { mission_date: date.toISOString().split('T')[0], generated_at: new Date().toISOString(), generated_by: 'ai_agent', status: 'draft', primary_mission_id: proposals[0]?.id || null, secondary_mission_id: proposals[1]?.id || null, tertiary_mission_id: proposals[2]?.id || null, total_estimated_minutes: totalMinutes, focus_time_start: focusStart.toISOString(), focus_time_end: focusEnd.toISOString(), focus_time_minutes: focusMinutes, calendar_blocks: calendarBlocks.map(b => ({ id: b.id, start_time: b.start_time, end_time: b.end_time, block_type: b.block_type, })), planned_reactions: Math.round(proposals.length * reactionRatio * 10), actual_reactions: 0, reaction_ratio: reactionRatio, completed_at: null, completion_notes: null, completion_rating: null, user_feedback: {}, }; const { data, error } = await supabase .from('ops_daily_missions') .insert(missionInsert) .select() .single(); if (error) { console.error('Error creating daily mission:', error); throw error; } // Update calendar blocks with mission_id if (data) { for (const block of calendarBlocks) { await supabase .from('ops_calendar_blocks') .update({ mission_id: data.id }) .eq('id', block.id); } } return data as DailyMission; } /** * Start execution log entry */ export async function startExecutionLog( agentName: 'proposal_generator' | 'cap_gate_validator' | 'calendar_scheduler' | 'trigger_monitor' | 'mission_optimizer' | 'stale_task_detector', executionType: 'scheduled' | 'triggered' | 'manual' | 'reaction' ): Promise { const supabase = getServiceSupabase(); const { data, error } = await supabase .from('ops_agent_execution_log') .insert({ agent_name: agentName, execution_type: executionType, started_at: new Date().toISOString(), status: 'running', input_summary: { timestamp: new Date().toISOString(), agent: agentName, type: executionType, }, }) .select('id') .single(); if (error) { console.error('Error starting execution log:', error); return ''; } return data?.id || ''; } /** * Complete execution log entry */ export async function completeExecutionLog( logId: string, result: { status: 'success' | 'partial' | 'failed'; duration: number; proposalsCreated?: number; proposalsApproved?: number; tasksAnalyzed?: number; outputSummary?: Record; errorMessage?: string; } ): Promise { const supabase = getServiceSupabase(); await supabase .from('ops_agent_execution_log') .update({ completed_at: new Date().toISOString(), duration_ms: result.duration, status: result.status, proposals_created: result.proposalsCreated || 0, proposals_approved: result.proposalsApproved || 0, tasks_analyzed: result.tasksAnalyzed || 0, output_summary: result.outputSummary || {}, error_message: result.errorMessage || null, }) .eq('id', logId); } /** * Check if this is a new day (for cap gate reset) */ function isNewDay(): boolean { const now = new Date(); return now.getHours() === GENERATION_HOUR && now.getMinutes() < 30; } /** * Get today's daily mission */ export async function getTodaysDailyMission(): Promise { const supabase = getServiceSupabase(); const today = new Date().toISOString().split('T')[0]; const { data, error } = await supabase .from('ops_daily_missions') .select('*') .eq('mission_date', today) .single(); if (error) { if (error.code === 'PGRST116') { return null; // No mission found } console.error('Error fetching daily mission:', error); return null; } return data as DailyMission; } /** * Check if daily missions need to be generated */ export async function shouldGenerateDailyMissions(): Promise { const now = new Date(); // Check if it's generation time (7 AM) if (now.getHours() !== GENERATION_HOUR) { return false; } // Check if already generated today const existingMission = await getTodaysDailyMission(); if (existingMission) { return false; } return true; } /** * Run the daily mission generation agent * This is the entry point for scheduled execution */ export async function runDailyMissionGeneration(): Promise { const shouldGenerate = await shouldGenerateDailyMissions(); if (!shouldGenerate) { console.log('Daily missions already generated or not generation time'); return null; } const input: DailyMissionGenerationInput = { date: new Date(), targetMissionCount: 3, includeReactions: true, }; return generateDailyMissions(input); } /** * Approve daily mission */ export async function approveDailyMission( missionId: string ): Promise { const supabase = getServiceSupabase(); const { error } = await supabase .from('ops_daily_missions') .update({ status: 'approved', }) .eq('id', missionId); if (error) { console.error('Error approving daily mission:', error); return false; } return true; } /** * Schedule daily mission (create calendar events) */ export async function scheduleDailyMission( missionId: string ): Promise { const supabase = getServiceSupabase(); // Get mission details const { data: mission } = await supabase .from('ops_daily_missions') .select('*') .eq('id', missionId) .single(); if (!mission) { return false; } // Get proposals const proposalIds = [ mission.primary_mission_id, mission.secondary_mission_id, mission.tertiary_mission_id, ].filter(Boolean); const { data: proposals } = await supabase .from('ops_mission_proposals') .select('*') .in('id', proposalIds); if (!proposals) { return false; } // Approve all proposals for (const proposal of proposals) { await supabase .from('ops_mission_proposals') .update({ status: 'approved', approved_at: new Date().toISOString(), }) .eq('id', proposal.id); } // Update mission status await supabase .from('ops_daily_missions') .update({ status: 'scheduled', }) .eq('id', missionId); // TODO: Sync with Google Calendar (Phase 10.3) return true; } /** * Complete daily mission */ export async function completeDailyMission( missionId: string, rating?: number, notes?: string ): Promise { const supabase = getServiceSupabase(); const { error } = await supabase .from('ops_daily_missions') .update({ status: 'completed', completed_at: new Date().toISOString(), completion_rating: rating || null, completion_notes: notes || null, }) .eq('id', missionId); if (error) { console.error('Error completing daily mission:', error); return false; } return true; }